10

Is there a way to emulate key presses of the media keys (volume up/down, play, pause, prev, next) on common Apple notebooks?

How?

Albert
  • 65,406
  • 61
  • 242
  • 386

3 Answers3

16

That took some time and many hacks (trying around with ctypes, the IOKit native interface, Quartz and/or Cocoa). This seems like an easy solution now:

#!/usr/bin/python

import Quartz

# NSEvent.h
NSSystemDefined = 14

# hidsystem/ev_keymap.h
NX_KEYTYPE_SOUND_UP = 0
NX_KEYTYPE_SOUND_DOWN = 1
NX_KEYTYPE_PLAY = 16
NX_KEYTYPE_NEXT = 17
NX_KEYTYPE_PREVIOUS = 18
NX_KEYTYPE_FAST = 19
NX_KEYTYPE_REWIND = 20

def HIDPostAuxKey(key):
  def doKey(down):
    ev = Quartz.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
      NSSystemDefined, # type
      (0,0), # location
      0xa00 if down else 0xb00, # flags
      0, # timestamp
      0, # window
      0, # ctx
      8, # subtype
      (key << 16) | ((0xa if down else 0xb) << 8), # data1
      -1 # data2
      )
    cev = ev.CGEvent()
    Quartz.CGEventPost(0, cev)
  doKey(True)
  doKey(False)

for _ in range(10):
  HIDPostAuxKey(NX_KEYTYPE_SOUND_UP)
HIDPostAuxKey(NX_KEYTYPE_PLAY)

(While I needed this in Python for now, my question was not really Python related and of course you can easily translate that to any other language, esp. ObjC.)

Brian Kung
  • 3,957
  • 4
  • 21
  • 30
Albert
  • 65,406
  • 61
  • 242
  • 386
  • Thank you! Very useful. Noticing a small difference on my M1 Mac Mini running Big Sur when doing this, compared to the standard media keys: these commands cause the dock to briefly get some extra padding on the right hand side when triggered. Like a new app is launched but then quits before its icon actually appears in the dock. Happens even if Music.app is running. – Henrik N Jan 01 '21 at 23:48
8

Swift 5 / MacOS 10.14.4 / Xcode 10.2

    @IBAction func mediaPressed(_ sender: AnyObject) {
        let NX_KEYTYPE_SOUND_UP: UInt32 = 0
        let NX_KEYTYPE_SOUND_DOWN: UInt32 = 1
        let NX_KEYTYPE_PLAY: UInt32 = 16
        let NX_KEYTYPE_NEXT: UInt32 = 17
        let NX_KEYTYPE_PREVIOUS: UInt32 = 18
        let NX_KEYTYPE_FAST: UInt32 = 19
        let NX_KEYTYPE_REWIND: UInt32 = 20

        func HIDPostAuxKey(key: UInt32) {
            func doKey(down: Bool) {
                let flags = NSEvent.ModifierFlags(rawValue: (down ? 0xa00 : 0xb00))
                let data1 = Int((key<<16) | (down ? 0xa00 : 0xb00))

                let ev = NSEvent.otherEvent(with: NSEvent.EventType.systemDefined,
                                            location: NSPoint(x:0,y:0),
                                            modifierFlags: flags,
                                            timestamp: 0,
                                            windowNumber: 0,
                                            context: nil,
                                            subtype: 8,
                                            data1: data1,
                                            data2: -1
                                            )
                let cev = ev?.cgEvent
                cev?.post(tap: CGEventTapLocation.cghidEventTap)
            }
            doKey(down: true)
            doKey(down: false)
        }

        for _ in 1...10 {
            HIDPostAuxKey(key:NX_KEYTYPE_SOUND_UP)
        }
        HIDPostAuxKey(key:NX_KEYTYPE_PLAY)
    }
the quantity
  • 126
  • 1
  • 6
  • This works great when I'm running it from Xcode but if I try to use it in a notorized app nothing happens. – Mark Bridges Apr 18 '22 at 15:09
  • 2
    Ignore that. It's some sort of bug in macOS. If anyone runs into this, I had to remove the app from Accessibility in Privacy & Settings. Then try to trigger the event again. Then I got the prompt again and it worked. – Mark Bridges Apr 18 '22 at 18:42
  • @MarkBridges not a bug. You created a new application binary by signing it. That in turn makes your new binary fail on the original binaries permissions. – Till Jan 27 '23 at 23:42
6

Thank you Albert for that! I expanded on your script a bit to make it an executable that could in turn be called by Quicksilver or another launcher/trigger handler.

#!/usr/bin/python

# CLI program to control the mediakeys on OS X. Used to emulate the mediakey on a keyboard with no such keys.
# Easiest used in combination with a launcher/trigger software such as Quicksilver.
# Main part taken from http://stackoverflow.com/questions/11045814/emulate-media-key-press-on-mac
# Glue to make it into cli program by Fredrik Wallner http://www.wallner.nu/fredrik/

import Quartz
import sys

# NSEvent.h
NSSystemDefined = 14

# hidsystem/ev_keymap.h
NX_KEYTYPE_SOUND_UP = 0
NX_KEYTYPE_SOUND_DOWN = 1
NX_KEYTYPE_PLAY = 16
NX_KEYTYPE_NEXT = 17
NX_KEYTYPE_PREVIOUS = 18
NX_KEYTYPE_FAST = 19
NX_KEYTYPE_REWIND = 20

supportedcmds = {'playpause': NX_KEYTYPE_PLAY, 'next': NX_KEYTYPE_NEXT, 'prev': NX_KEYTYPE_PREVIOUS, 'volup': NX_KEYTYPE_SOUND_UP, 'voldown': NX_KEYTYPE_SOUND_DOWN}

def HIDPostAuxKey(key):
  def doKey(down):
    ev = Quartz.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_(
      NSSystemDefined, # type
      (0,0), # location
      0xa00 if down else 0xb00, # flags
      0, # timestamp
      0, # window
      0, # ctx
      8, # subtype
      (key << 16) | ((0xa if down else 0xb) << 8), # data1
      -1 # data2
      )
    cev = ev.CGEvent()
    Quartz.CGEventPost(0, cev)
  doKey(True)
  doKey(False)

if __name__ == "__main__":
  try:
    command = sys.argv[1]
    assert(command in supportedcmds)
    HIDPostAuxKey(supportedcmds[command])
  except (IndexError, AssertionError):
    print "Usage: %s command" % (sys.argv[0],)
    print "\tSupported commands are %s" % supportedcmds.keys()

The script can be found at https://gist.github.com/4078034

Brian Kung
  • 3,957
  • 4
  • 21
  • 30
Fredrik
  • 83
  • 1
  • 6
  • 1
    Used this to much success with [ControllerMate](http://www.orderedbytes.com/controllermate/) in order to emulate the media keys on a keyboard without them but with extra F keys. This is great because it doesn't require calls to each application individually. – sordid Jan 31 '14 at 14:33