Last active
April 26, 2017 13:47
-
-
Save adamjmurray/5667817 to your computer and use it in GitHub Desktop.
How to play MIDI using the OS X built-in DLS synth using Ruby & FFI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'ffi' | |
# A MIDI driver to play MIDI using OSX's built in DLS synthesizer. | |
# | |
# == Authors | |
# | |
# * Adam Murray <[email protected]> | |
# | |
# == Copyright | |
# | |
# Copyright (c) 2013 Adam Murray | |
# | |
# This code released under the terms of the MIT license. | |
# | |
class DLSSynthDriver | |
attr_accessor :synth | |
module AudioToolbox | |
extend FFI::Library | |
ffi_lib '/System/Library/Frameworks/AudioToolbox.framework/Versions/Current/AudioToolbox' | |
ffi_lib '/System/Library/Frameworks/AudioUnit.framework/Versions/Current/AudioUnit' | |
class ComponentDescription < FFI::Struct | |
layout :componentType, :int, | |
:componentSubType, :int, | |
:componentManufacturer, :int, | |
:componentFlags, :int, | |
:componentFlagsMask, :int | |
end | |
def self.to_bytes(s) | |
bytes = 0 | |
s.each_byte do |byte| | |
bytes <<= 8 | |
bytes += byte | |
end | |
return bytes | |
end | |
AUDIO_UNIT_MANUFACTURER_APPLE = to_bytes('appl') # to_bytes may not be strictly necessary but these are supposed to be 4 byte numbers | |
AUDIO_UNIT_TYPE_MUSIC_DEVICE = to_bytes('aumu') | |
AUDIO_UNIT_SUBTYPE_DLS_SYNTH = to_bytes('dls ') | |
AUDIO_UNIT_TYPE_OUTPUT = to_bytes('auou') | |
AUDIO_UNIT_SUBTYPE_DEFAULT_OUTPUT = to_bytes('def ') | |
# int NewAUGraph(void *) | |
attach_function :NewAUGraph, [:pointer], :int | |
# int AUGraphAddNode(void *, ComponentDescription *, void *) | |
attach_function :AUGraphAddNode, [:pointer, :pointer, :pointer], :int | |
# int AUGraphOpen(void *) | |
attach_function :AUGraphOpen, [:pointer], :int | |
# int AUGraphConnectNodeInput(void *, void *, int, void *, int) | |
attach_function :AUGraphConnectNodeInput, [:pointer, :pointer, :int, :pointer, :int], :int | |
# int AUGraphNodeInfo(void *, void *, ComponentDescription *, void *) | |
attach_function :AUGraphNodeInfo, [:pointer, :pointer, :pointer, :pointer], :int | |
# int AUGraphInitialize(void *) | |
attach_function :AUGraphInitialize, [:pointer], :int | |
# int AUGraphStart(void *) | |
attach_function :AUGraphStart, [:pointer], :int | |
# int AUGraphStop(void *) | |
attach_function :AUGraphStop, [:pointer], :int | |
# int DisposeAUGraph(void *) | |
attach_function :DisposeAUGraph, [:pointer], :int | |
# void * CAShow(void *) | |
attach_function :CAShow, [:pointer], :void | |
# void * MusicDeviceMIDIEvent(void *, int, int, int, int) | |
attach_function :MusicDeviceMIDIEvent, [:pointer, :int, :int, :int, :int], :void | |
end | |
def require_noerr(action_description, &block) | |
if block.call != 0 | |
fail "Failed to #{action_description}" | |
end | |
end | |
def open | |
synth_pointer = FFI::MemoryPointer.new(:pointer) | |
graph_pointer = FFI::MemoryPointer.new(:pointer) | |
synth_node_pointer = FFI::MemoryPointer.new(:pointer) | |
out_node_pointer = FFI::MemoryPointer.new(:pointer) | |
cd = AudioToolbox::ComponentDescription.new | |
cd[:componentManufacturer] = AudioToolbox::AUDIO_UNIT_MANUFACTURER_APPLE | |
cd[:componentFlags] = 0 | |
cd[:componentFlagsMask] = 0 | |
require_noerr('create AUGraph') { AudioToolbox.NewAUGraph(graph_pointer) } | |
@graph = graph_pointer.get_pointer(0) | |
cd[:componentType] = AudioToolbox::AUDIO_UNIT_TYPE_MUSIC_DEVICE | |
cd[:componentSubType] = AudioToolbox::AUDIO_UNIT_SUBTYPE_DLS_SYNTH | |
require_noerr('add synthNode') { AudioToolbox.AUGraphAddNode(@graph, cd, synth_node_pointer) } | |
synth_node = synth_node_pointer.get_pointer(0) | |
cd[:componentType] = AudioToolbox::AUDIO_UNIT_TYPE_OUTPUT | |
cd[:componentSubType] = AudioToolbox::AUDIO_UNIT_SUBTYPE_DEFAULT_OUTPUT | |
require_noerr('add outNode') { AudioToolbox.AUGraphAddNode(@graph, cd, out_node_pointer) } | |
out_node = out_node_pointer.get_pointer(0) | |
require_noerr('open graph') { AudioToolbox.AUGraphOpen(@graph) } | |
require_noerr('connect synth to out') { AudioToolbox.AUGraphConnectNodeInput(@graph, synth_node, 0, out_node, 0) } | |
require_noerr('graph info') { AudioToolbox.AUGraphNodeInfo(@graph, synth_node, nil, synth_pointer) } | |
@synth = synth_pointer.get_pointer(0) | |
require_noerr('init graph') { AudioToolbox.AUGraphInitialize(@graph) } | |
require_noerr('start graph') { AudioToolbox.AUGraphStart(@graph) } | |
AudioToolbox.CAShow(@graph) # for debugging | |
end | |
def message(*args) | |
arg0 = args[0] || 0 | |
arg1 = args[1] || 0 | |
arg2 = args[2] || 0 | |
AudioToolbox.MusicDeviceMIDIEvent(@synth, arg0, arg1, arg2, 0) | |
end | |
def close | |
if @graph | |
AudioToolbox.AUGraphStop(@graph) | |
AudioToolbox.DisposeAUGraph(@graph) | |
end | |
end | |
end | |
######################## | |
# A quick test | |
ON = 0x90 | |
OFF = 0x80 | |
PC = 0xc0 | |
channel = 0 | |
driver = DLSSynthDriver.new | |
driver.open | |
driver.message(ON | channel, 60, 127) | |
sleep(1) | |
driver.message(OFF | channel, 60, 127) | |
sleep(1) | |
driver.message(PC | channel, 5) | |
driver.message(ON | channel, 62, 127) | |
sleep(1) | |
driver.message(OFF | channel, 62, 127) | |
sleep(1) | |
driver.message(PC | channel, 20) | |
driver.message(ON | channel, 64, 127) | |
sleep(1) | |
driver.message(OFF | channel, 64, 127) | |
sleep(1) | |
driver.close |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment