├── .gitignore ├── Gemfile ├── README.md ├── Rakefile ├── audio.gemspec └── lib ├── audio.rb └── macos ├── audio_toolbox.rb ├── audio_toolbox └── audio_converter.rb ├── core_audio.rb ├── core_audio ├── audio_device.rb ├── audio_object.rb ├── audio_stream.rb └── audio_types.rb └── core_foundation.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Audio 2 | 3 | 'Audio' is a cross-platform audio device interface that allows you to read and 4 | write audio streams from the comfort and safety of Ruby. 5 | 6 | ## Cross-Platform Status 7 | 8 | Currently, only support for OS X has been implemented. Support for Windows and 9 | Linux will be added in the future. If you feel like working on either of those, 10 | I'm more than happy to take Pull Requests! 11 | 12 | ## Usage 13 | 14 | ### Listing Available Audio Devices 15 | 16 | The Audio module provides a method that retrieves a list of all of the audio 17 | devices available on your system. 18 | 19 | ```ruby 20 | require 'audio' 21 | 22 | devices = Audio.devices # => An Array of Device objects 23 | ``` 24 | 25 | Each Device object has attributes that wrap the various properties provided by 26 | the host operating system. For example, to get a list of device names... 27 | 28 | ```ruby 29 | Audio.devices.each do |device| 30 | puts "Device Name: #{device.device_name}" 31 | end 32 | ``` 33 | 34 | Running that on a MacBook Pro (late 2013), with a single external microphone 35 | connected, produces... 36 | 37 | ``` 38 | Device Name: Built-in Microphone 39 | Device Name: Built-in Output 40 | Device Name: Blue Snowball 41 | ``` 42 | 43 | ### Using the default Audio Devices 44 | 45 | If you just want to use a default device, Audio has you covered. 46 | 47 | ```ruby 48 | Audio.default_input # => Device Name: Built-in Microphone 49 | Audio.default_output # => Device Name: Built-in Output 50 | ``` 51 | 52 | ### Recording Audio 53 | 54 | You can record audio from a particular device by calling its `start` method. 55 | Once started, the device will continue recording until its `stop` method is 56 | called. If you provide a block parameter to `start`, it will be called whenever 57 | the host OS provides a new buffer of audio samples. 58 | 59 | ```ruby 60 | Audio.default_input.start do |*args| 61 | # Do something fancy 62 | end 63 | 64 | sleep 5 # Record 5 seconds of audio 65 | 66 | device.stop 67 | ``` 68 | 69 | ## Installation 70 | 71 | Add this line to your application's Gemfile: 72 | 73 | ```ruby 74 | gem 'audio' 75 | ``` 76 | 77 | And then execute: 78 | 79 | $ bundle 80 | 81 | Or install it yourself as: 82 | 83 | $ gem install audio 84 | 85 | License 86 | ------- 87 | 88 | Copyright 2015 Brandon Fosdick and released under the BSD license. 89 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /audio.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "audio" 7 | spec.version = '0.2' 8 | spec.authors = ["Brandon Fosdick"] 9 | spec.email = ["bfoz@bfoz.net"] 10 | spec.summary = %q{Cross-platform Audio Device Input and Output} 11 | spec.description = %q{Access all of your audio devices from the comfort of Ruby} 12 | spec.homepage = "http://github.com/bfoz/audio-ruby" 13 | spec.license = "BSD" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_development_dependency "bundler", "~> 1.7" 21 | spec.add_development_dependency "rake", "~> 10.0" 22 | end 23 | -------------------------------------------------------------------------------- /lib/audio.rb: -------------------------------------------------------------------------------- 1 | require_relative 'macos/core_audio' 2 | require_relative 'macos/audio_toolbox' 3 | 4 | module Audio 5 | # @return [Array] the list of available audio devices 6 | def self.devices 7 | CoreAudio.devices 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/macos/audio_toolbox.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | 3 | require_relative 'audio_toolbox/audio_converter' 4 | require_relative 'core_foundation' 5 | require_relative 'core_audio/audio_stream' 6 | 7 | module AudioToolbox 8 | extend FFI::Library 9 | ffi_lib '/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox' 10 | 11 | OSStatus = CoreFoundation::OSStatus 12 | AudioStreamBasicDescription = CoreAudio::AudioStreamBasicDescription 13 | 14 | typedef :pointer, :AudioConverterRef 15 | 16 | # OSStatus (*AudioConverterComplexInputDataProc)(AudioConverterRef inAudioConverter, 17 | # UInt32* ioNumberDataPackets, 18 | # AudioBufferList* ioData, 19 | # AudioStreamPacketDescription** outDataPacketDescription, 20 | # void* inUserData) 21 | callback :AudioConverterComplexInputDataProc, [:AudioConverterRef, :pointer, :pointer, :pointer, :pointer], OSStatus 22 | 23 | # OSStatus AudioConverterNew(const AudioStreamBasicDescription* inSourceFormat, 24 | # const AudioStreamBasicDescription* inDestinationFormat, 25 | # AudioConverterRef* outAudioConverter) 26 | attach_function :AudioConverterNew, [AudioStreamBasicDescription.by_ref, AudioStreamBasicDescription.by_ref, :pointer], OSStatus 27 | 28 | # OSStatus AudioConverterFillComplexBuffer(AudioConverterRef inAudioConverter, 29 | # AudioConverterComplexInputDataProc inInputDataProc, 30 | # void* inInputDataProcUserData, 31 | # UInt32* ioOutputDataPacketSize, 32 | # AudioBufferList* outOutputData, 33 | # AudioStreamPacketDescription* outPacketDescription) 34 | attach_function :AudioConverterFillComplexBuffer, [:AudioConverterRef, :AudioConverterComplexInputDataProc, :pointer, :pointer, :pointer, :pointer], OSStatus 35 | 36 | # OSStatus AudioConverterSetProperty(AudioConverterRef inAudioConverter, 37 | # AudioConverterPropertyID inPropertyID, 38 | # UInt32 inPropertyDataSize, 39 | # const void* inPropertyData) 40 | attach_function :AudioConverterSetProperty, [:AudioConverterRef, :uint32, :uint32, :pointer], OSStatus 41 | 42 | # Create a new {AudioConverter} that convertes between the given stream formats 43 | # @param from [AudioStreamBasicDescription] the stream format to convert from 44 | # @param to [AudioStreamBasicDescription] the stream format to convert to 45 | # @return [AudioConverterRef] 46 | def self.converter(from, to) 47 | reference = FFI::MemoryPointer.new(AudioConverterRef) 48 | status = AudioToolbox.AudioConverterNew(from, to, reference) 49 | raise "No converter '#{[status].pack('L').reverse}'" unless status.zero? 50 | AudioConverterRef.new(reference.get_pointer(0)).tap do |converter| 51 | converter.from = from 52 | converter.to = to 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/macos/audio_toolbox/audio_converter.rb: -------------------------------------------------------------------------------- 1 | module AudioToolbox 2 | class AudioConverterRef < FFI::Pointer 3 | attr_accessor :from 4 | attr_accessor :to 5 | 6 | # @group AudioConverter.h 7 | PrimeMethod = 'prmm' 8 | # @endgroup 9 | 10 | # @param buffer [AudioBufferList] the list of buffers to be converted 11 | # @return [AudioBufferList] the converted data 12 | def convert(buffer) 13 | num_from_packets = (buffer.bytesize / from[:mBytesPerPacket]).floor 14 | remaining_packets = num_from_packets 15 | 16 | block = Proc.new do |_, num_packets, buffer_list, _| 17 | unless remaining_packets.zero? 18 | buffer_list = CoreAudio::AudioBufferList.new(buffer_list) 19 | buffer_list.buffers.first[:mData] = buffer.buffers.first[:mData] 20 | buffer_list.buffers.first[:mDataByteSize] = buffer.buffers.first[:mDataByteSize] 21 | end 22 | 23 | # Report the number of packets actually sent 24 | num_packets.put_uint32(0, remaining_packets) 25 | 26 | remaining_packets = 0 # No more packets remaining 27 | 28 | 0 # All is well 29 | end 30 | 31 | bytes_per_packet = to[:mBytesPerPacket] 32 | num_output_packets = (num_from_packets * to.sample_rate / from.sample_rate).floor 33 | 34 | output_list = CoreAudio::AudioBufferList.buffer_list(size:bytes_per_packet * num_output_packets) 35 | raise("No buffer list") unless output_list 36 | 37 | num_packets = FFI::MemoryPointer.new(:uint32).put_uint32(0, num_output_packets) 38 | status = AudioToolbox.AudioConverterFillComplexBuffer(self, block, nil, num_packets, output_list, nil) 39 | raise("Convert failed: '#{[status].pack('L').reverse}'") unless status.zero? 40 | 41 | output_list 42 | end 43 | 44 | # @param method [Symbol] :pre, :normal, or :none 45 | def prime_method=(method=:normal) 46 | method = case method 47 | when :pre then 0 48 | when :normal then 1 49 | when :none then 2 50 | end 51 | data = FFI::MemoryPointer.new(:uint32).put_uint32(0, method) 52 | AudioToolbox.AudioConverterSetProperty(self, PrimeMethod.reverse.unpack('L').first, 4, data) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/macos/core_audio.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | 3 | require_relative 'core_audio/audio_types' 4 | 5 | module CoreAudio 6 | extend FFI::Library 7 | ffi_lib '/System/Library/Frameworks/CoreAudio.framework/CoreAudio' 8 | 9 | typedef :uint32, :OSStatus 10 | end 11 | 12 | require_relative 'core_audio/audio_device' 13 | 14 | module CoreAudio 15 | # @group AudioHardware.h 16 | # OSStatus AudioObjectGetPropertyDataSize(AudioObjectID inObjectID, 17 | # const AudioObjectPropertyAddress* inAddress, 18 | # UInt32 inQualifierDataSize, 19 | # const void* inQualifierData, 20 | # UInt32* outDataSize) 21 | attach_function :AudioObjectGetPropertyDataSize, [AudioObject::ObjectID, AudioObject::PropertyAddress.by_ref, :uint32, :pointer, :pointer], :OSStatus 22 | 23 | # OSStatus AudioObjectGetPropertyData(AudioObjectID inObjectID, 24 | # const AudioObjectPropertyAddress* inAddress, 25 | # UInt32 inQualifierDataSize, 26 | # const void* inQualifierData, 27 | # UInt32* ioDataSize, 28 | # void* outData) 29 | attach_function :AudioObjectGetPropertyData, [AudioObject::ObjectID, AudioObject::PropertyAddress.by_ref, :uint32, :pointer, :pointer, :pointer], :OSStatus 30 | 31 | # OSStatus AudioObjectSetPropertyData(AudioObjectID inObjectID, 32 | # const AudioObjectPropertyAddress* inAddress, 33 | # UInt32 inQualifierDataSize, 34 | # const void* inQualifierData, 35 | # UInt32 inDataSize, 36 | # const void* inData) 37 | attach_function :AudioObjectSetPropertyData, [AudioObject::ObjectID, AudioObject::PropertyAddress.by_ref, :uint32, :pointer, :uint32, :pointer], :OSStatus 38 | # @endgroup 39 | 40 | # @return [Array] the list of available audio devices 41 | def self.devices 42 | address = AudioObject::PropertyAddress.global_master(AudioHardware::PropertyDevices) 43 | buffer = AudioObject.system.get_property(address) 44 | device_IDs = buffer.get_array_of_int32(0, buffer.size/4) 45 | device_IDs.map {|id| AudioDevice.new(id)} 46 | end 47 | 48 | # @return [AudioDevice] the default input device 49 | def self.default_input 50 | address = AudioObject::PropertyAddress.global_master(AudioHardware::PropertyDefaultInputDevice) 51 | buffer = AudioObject.system.get_property(address) 52 | AudioDevice.new(buffer.get_uint32(0)) 53 | end 54 | 55 | # @return [AudioDevice] the default output device 56 | def self.default_output 57 | address = AudioObject::PropertyAddress.global_master(AudioHardware::PropertyDefaultOutputDevice) 58 | buffer = AudioObject.system.get_property(address) 59 | AudioDevice.new(buffer.get_uint32(0)) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/macos/core_audio/audio_device.rb: -------------------------------------------------------------------------------- 1 | require_relative 'audio_object' 2 | 3 | module CoreAudio 4 | AudioDeviceIOProcID = FFI::Pointer 5 | typedef :pointer, :AudioDeviceIOProcID 6 | 7 | # @group AudioHardware.h 8 | 9 | # typedef OSStatus (*AudioDeviceIOProc)(AudioObjectID inDevice, 10 | # const AudioTimeStamp* inNow, 11 | # const AudioBufferList* inInputData, 12 | # const AudioTimeStamp* inInputTime, 13 | # AudioBufferList* outOutputData, 14 | # const AudioTimeStamp* inOutputTime, 15 | # void* inClientData); 16 | callback :AudioDeviceIOProc, [AudioObject::ObjectID, :pointer, AudioBufferList.by_ref, :pointer, AudioBufferList.by_ref, :pointer, :pointer], :OSStatus 17 | 18 | # OSStatus AudioDeviceCreateIOProcID(AudioObjectID inDevice, 19 | # AudioDeviceIOProc inProc, 20 | # void* inClientData, 21 | # AudioDeviceIOProcID* outIOProcID) 22 | attach_function :AudioDeviceCreateIOProcID, [AudioObject::ObjectID, :AudioDeviceIOProc, :pointer, :pointer], :OSStatus 23 | 24 | # OSStatus AudioDeviceDestroyIOProcID(AudioObjectID inDevice, 25 | # AudioDeviceIOProcID inIOProcID 26 | attach_function :AudioDeviceDestroyIOProcID, [AudioObject::ObjectID, :AudioDeviceIOProc], :OSStatus 27 | 28 | # OSStatus AudioDeviceStart(AudioObjectID inDevice, 29 | # AudioDeviceIOProcID inProcID) 30 | attach_function :AudioDeviceStart, [AudioObject::ObjectID, :AudioDeviceIOProcID], :OSStatus 31 | 32 | # OSStatus AudioDeviceStop(AudioObjectID inDevice, 33 | # AudioDeviceIOProcID inProcID) 34 | attach_function :AudioDeviceStop, [AudioObject::ObjectID, :AudioDeviceIOProcID], :OSStatus 35 | 36 | # @endgroup 37 | 38 | # Split the constants so that AudioStream can see 'PropertyLatency' 39 | class AudioDevice < AudioObject 40 | # @group AudioHardwareBase.h: AudioDevice Properties 41 | PropertyConfigurationApplication = 'capp' 42 | PropertyDeviceUID = 'uid ' 43 | PropertyModelUID = 'muid' 44 | PropertyTransportType = 'tran' 45 | PropertyRelatedDevices = 'akin' 46 | PropertyClockDomain = 'clkd' 47 | PropertyDeviceIsAlive = 'livn' 48 | PropertyDeviceIsRunning = 'goin' 49 | PropertyDeviceCanBeDefaultDevice = 'dflt' 50 | PropertyDeviceCanBeDefaultSystemDevice = 'sflt' 51 | PropertyLatency = 'ltnc' 52 | PropertyStreams = 'stm#' 53 | PropertyControlList = 'ctrl' 54 | PropertySafetyOffset = 'saft' 55 | PropertyNominalSampleRate = 'nsrt' 56 | PropertyAvailableNominalSampleRates = 'nsr#' 57 | PropertyIcon = 'icon' 58 | PropertyIsHidden = 'hidn' 59 | PropertyPreferredChannelsForStereo = 'dch2' 60 | PropertyPreferredChannelLayout = 'srnd' 61 | # @endgroup 62 | end 63 | 64 | require_relative 'audio_stream' 65 | 66 | class AudioDevice 67 | # @group AudioHardware.h: AudioDevice Properties 68 | PropertyPlugIn = 'plug' 69 | PropertyDeviceHasChanged = 'diff' 70 | PropertyDeviceIsRunningSomewhere = 'gone' 71 | ProcessorOverload = 'over' 72 | PropertyIOStoppedAbnormally = 'stpd' 73 | PropertyHogMode = 'oink' 74 | PropertyBufferFrameSize = 'fsiz' 75 | PropertyBufferFrameSizeRange = 'fsz#' 76 | PropertyUsesVariableBufferFrameSizes = 'vfsz' 77 | PropertyIOCycleUsage = 'ncyc' 78 | PropertyStreamConfiguration = 'slay' 79 | PropertyIOProcStreamUsage = 'suse' 80 | PropertyActualSampleRate = 'asrt' 81 | # @endgroup 82 | 83 | # @group Properties 84 | 85 | def buffer_frame_size 86 | address = PropertyAddress.global_master(PropertyBufferFrameSize) 87 | get_property(address).get_uint32(0) 88 | end 89 | 90 | # @return [Bool] true if the device is running 91 | def running? 92 | address = PropertyAddress.global_master(PropertyDeviceIsRunning) 93 | 0 != get_property(address).get_uint32(0) 94 | end 95 | 96 | def running_somewhere? 97 | address = PropertyAddress.global_master(PropertyDeviceIsRunningSomewhere) 98 | 0 != get_property(address).get_uint32(0) 99 | end 100 | 101 | # @return [Array] an array of {AudioStream}s, one for each stream provided by the device 102 | def streams 103 | address = PropertyAddress.global_master(PropertyStreams) 104 | buffer = get_property(address) 105 | buffer.get_array_of_uint32(0, buffer.size/FFI::Type::UINT32.size).map {|stream_id| AudioStream.new(stream_id)} 106 | end 107 | 108 | # @group Sample Rate 109 | 110 | # @return [Float] the measured sample rate in Hertz 111 | def actual_sample_rate 112 | address = PropertyAddress.global_master(PropertyActualSampleRate) 113 | get_property(address).get_float64(0) 114 | end 115 | 116 | # @return [Array] the available sampling rates, or sample-rate-ranges 117 | def available_sample_rates 118 | address = PropertyAddress.global_master(PropertyAvailableNominalSampleRates) 119 | buffer = get_property(address) 120 | buffer = buffer.get_array_of_float64(0, buffer.size / FFI::Type::DOUBLE.size) 121 | 122 | # Convert the range pairs into actual Ranges, unless the Range is empty 123 | buffer.each_slice(2).map {|a,b| (a==b) ? a : (a..b)} 124 | end 125 | 126 | # @return [Float] the device's nominal sample rate 127 | def sample_rate 128 | address = PropertyAddress.global_master(PropertyNominalSampleRate) 129 | get_property(address).get_float64(0) 130 | end 131 | 132 | # @param rate [Float] the new sample rate in Hertz 133 | def sample_rate=(rate) 134 | address = PropertyAddress.global_master(PropertyNominalSampleRate) 135 | ffi_rate = FFI::MemoryPointer.new(:double) 136 | ffi_rate.put_float64(0, rate) 137 | status = set_property(address, ffi_rate) 138 | raise "status #{status} => '#{[status].pack('L').reverse}'" unless 0 == status 139 | end 140 | # @endgroup 141 | # @endgroup 142 | 143 | # Start the AudioDevice 144 | # If a block is provided, register it as a callback before starting the device 145 | # @note The device will continue to run until `stop` is called 146 | def start(&block) 147 | if block_given? 148 | io_proc_id = FFI::MemoryPointer.new(:pointer) 149 | status = CoreAudio.AudioDeviceCreateIOProcID(id, block, nil, io_proc_id) 150 | 151 | raise "Couldn't create an IO Proc #{status} => '#{[status].pack('L').reverse}'" unless status.zero? # && !@proc_id.nil? 152 | 153 | @proc_id = io_proc_id.get_pointer(0) 154 | end 155 | 156 | Thread.start do 157 | CoreAudio.AudioDeviceStart(id, @proc_id) 158 | end 159 | end 160 | 161 | # Stop the AudioDevice and delete any registered callbacks 162 | def stop 163 | CoreAudio.AudioDeviceStop(id, @proc_id) 164 | CoreAudio.AudioDeviceDestroyIOProcID(id, @proc_id) 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/macos/core_audio/audio_object.rb: -------------------------------------------------------------------------------- 1 | require_relative '../core_foundation' 2 | 3 | module CoreAudio 4 | extend FFI::Library 5 | 6 | module AudioHardwareBase 7 | # AudioHardwareBase.h: AudioDevice Properties 8 | AudioDevicePropertyConfigurationApplication = 'capp' 9 | AudioDevicePropertyDeviceUID = 'uid ' 10 | AudioDevicePropertyModelUID = 'muid' 11 | AudioDevicePropertyTransportType = 'tran' 12 | AudioDevicePropertyRelatedDevices = 'akin' 13 | AudioDevicePropertyClockDomain = 'clkd' 14 | AudioDevicePropertyDeviceIsAlive = 'livn' 15 | AudioDevicePropertyDeviceIsRunning = 'goin' 16 | AudioDevicePropertyDeviceCanBeDefaultDevice = 'dflt' 17 | AudioDevicePropertyDeviceCanBeDefaultSystemDevice = 'sflt' 18 | AudioDevicePropertyLatency = 'ltnc' 19 | AudioDevicePropertyStreams = 'stm#' 20 | AudioObjectPropertyControlList = 'ctrl' 21 | AudioDevicePropertySafetyOffset = 'saft' 22 | AudioDevicePropertyNominalSampleRate = 'nsrt' 23 | AudioDevicePropertyAvailableNominalSampleRates = 'nsr#' 24 | AudioDevicePropertyIcon = 'icon' 25 | AudioDevicePropertyIsHidden = 'hidn' 26 | AudioDevicePropertyPreferredChannelsForStereo = 'dch2' 27 | AudioDevicePropertyPreferredChannelLayout = 'srnd' 28 | end 29 | 30 | module AudioHardware 31 | # AudioHardware.h: AudioSystemObject Properties 32 | PropertyDevices = 'dev#' 33 | PropertyDefaultInputDevice = 'dIn ' 34 | PropertyDefaultOutputDevice = 'dOut' 35 | PropertyDefaultSystemOutputDevice = 'sOut' 36 | PropertyTranslateUIDToDevice = 'uidd' 37 | PropertyMixStereoToMono = 'stmo' 38 | PropertyPlugInList = 'plg#' 39 | PropertyTranslateBundleIDToPlugIn = 'bidp' 40 | PropertyTransportManagerList = 'tmg#' 41 | PropertyTranslateBundleIDToTransportManager = 'tmbi' 42 | PropertyBoxList = 'box#' 43 | PropertyTranslateUIDToBox = 'uidb' 44 | PropertyProcessIsMaster = 'mast' 45 | PropertyIsInitingOrExiting = 'inot' 46 | PropertyUserIDChanged = 'euid' 47 | PropertyProcessIsAudible = 'pmut' 48 | PropertySleepingIsAllowed = 'slep' 49 | PropertyUnloadingIsAllowed = 'unld' 50 | PropertyHogModeIsAllowed = 'hogr' 51 | PropertyUserSessionIsActiveOrHeadless = 'user' 52 | PropertyServiceRestarted = 'srst' 53 | PropertyPowerHint = 'powh' 54 | end 55 | 56 | class AudioObject 57 | attr_reader :id 58 | 59 | ObjectID = FFI::Type::UINT32 60 | PropertyElement = FFI::Type::UINT32 61 | PropertyScope = FFI::Type::UINT32 62 | PropertySelector = FFI::Type::UINT32 63 | 64 | # AudioHardwareBase.h: Basic Constants 65 | PropertyScopeGlobal = 'glob' 66 | PropertyScopeInput = 'inpt' 67 | PropertyScopeOutput = 'outp' 68 | PropertyScopePlayThrough = 'ptru' 69 | PropertyElementMaster = 0 70 | 71 | # AudioHardwareBase.h: AudioObject Properties 72 | PropertyBaseClass = 'bcls' 73 | PropertyClass = 'clas' 74 | PropertyOwner = 'stdv' 75 | PropertyName = 'lnam' 76 | PropertyModelName = 'lmod' 77 | PropertyManufacturer = 'lmak' 78 | PropertyElementName = 'lchn' 79 | PropertyElementCategoryName = 'lccn' 80 | PropertyElementNumberName = 'lcnn' 81 | PropertyOwnedObjects = 'ownd' 82 | PropertyIdentify = 'iden' 83 | PropertySerialNumber = 'snum' 84 | PropertyFirmwareVersion = 'fwvn' 85 | 86 | # AudioHardware.h: Basic Constants 87 | SystemObject = 1 88 | 89 | def self.system 90 | new(SystemObject) 91 | end 92 | 93 | def initialize(id) 94 | @id = id 95 | end 96 | 97 | # @return [FFI::MemoryPointer] 98 | def get_property(address) 99 | buffer_size = FFI::MemoryPointer.new(:uint32) 100 | status = CoreAudio.AudioObjectGetPropertyDataSize(id, address, 0, nil, buffer_size) 101 | raise('Could not get audio property size') unless 0 == status 102 | 103 | # buffer_size is now the size of the buffer to be passed to AudioObjectGetPropertyData() 104 | buffer = FFI::MemoryPointer.new(1, buffer_size.get_int32(0)) 105 | status = CoreAudio.AudioObjectGetPropertyData(id, address, 0, nil, buffer_size, buffer) 106 | raise('Could not get the audio property data') unless 0 == status 107 | 108 | buffer 109 | end 110 | 111 | def set_property(address, buffer, qualifier=nil) 112 | qualifier_size = qualifier.size rescue 0 113 | CoreAudio.AudioObjectSetPropertyData(id, address, 0, nil, buffer.size, buffer) 114 | end 115 | 116 | # @group Convenience Attributes 117 | def external? 118 | not internal? 119 | end 120 | 121 | def internal? 122 | transport_type == 'bltn' 123 | end 124 | # @endgroup 125 | 126 | # @return [String] the name of the device 127 | def device_name 128 | address = PropertyAddress.global_master(PropertyName) 129 | get_string(address) 130 | end 131 | 132 | def device_uid 133 | address = PropertyAddress.global_master(AudioHardwareBase::AudioDevicePropertyDeviceUID) 134 | get_string(address) 135 | end 136 | 137 | # @return [String] a persistent identifier for the model of an AudioDevice 138 | def model_uid 139 | address = PropertyAddress.global_master(AudioHardwareBase::AudioDevicePropertyModelUID) 140 | get_string(address) 141 | end 142 | 143 | # @return [String] the 4-character transport type identifier 144 | def transport_type 145 | address = PropertyAddress.global_master(AudioHardwareBase::AudioDevicePropertyTransportType) 146 | buffer = get_property(address) 147 | buffer.get_bytes(0, buffer.size).reverse 148 | end 149 | 150 | class PropertyAddress < FFI::Struct 151 | layout :mSelector, AudioObject::PropertySelector, 152 | :mScope, AudioObject::PropertyScope, 153 | :mElement, AudioObject::PropertyElement 154 | 155 | def self.make(selector, scope, element) 156 | element, scope, selector = [element, scope, selector].map {|a| a.is_a?(String) ? a.reverse.unpack('L').first : a } 157 | new.tap do |address| 158 | address[:mSelector] = selector 159 | address[:mScope] = scope 160 | address[:mElement] = element 161 | end 162 | end 163 | 164 | def self.global_master(selector) 165 | make(selector, PropertyScopeGlobal, PropertyElementMaster) 166 | end 167 | end 168 | 169 | private 170 | 171 | # @return [String] the String for the addressed property 172 | def get_string(address) 173 | buffer = get_property(address) 174 | cf_string_ref = buffer.get_pointer(0) 175 | CoreFoundation::CFStringRef.new(cf_string_ref).to_s 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/macos/core_audio/audio_stream.rb: -------------------------------------------------------------------------------- 1 | module CoreAudio 2 | class AudioStreamBasicDescription < FFI::Struct 3 | # @group CoreAudioTypes.h 4 | FormatFlagIsFloat = (1 << 0) 5 | FormatFlagIsSignedInteger = (1 << 2) 6 | # @endgroup 7 | 8 | layout :mSampleRate, :double, 9 | :mFormatID, :uint32, 10 | :mFormatFlags, :uint32, 11 | :mBytesPerPacket, :uint32, 12 | :mFramesPerPacket, :uint32, 13 | :mBytesPerFrame, :uint32, 14 | :mChannelsPerFrame, :uint32, 15 | :mBitsPerChannel, :uint32, 16 | :mReserved, :uint32 17 | 18 | # @!attribute channels 19 | # @return [Integer] the number of channels per frame 20 | def channels 21 | self[:mChannelsPerFrame] 22 | end 23 | 24 | # @param number [Integer] the number of channels per frame 25 | def channels=(number) 26 | self[:mChannelsPerFrame] = number.to_i 27 | end 28 | 29 | # @!attribute channel_width 30 | # @return [Integer] the number of bits per channel 31 | def channel_width=(bits) 32 | self[:mBitsPerChannel] = bits 33 | self[:mBytesPerFrame] = self[:mChannelsPerFrame] * bits / 8 34 | self[:mBytesPerPacket] = self[:mBytesPerFrame] * self[:mFramesPerPacket] 35 | end 36 | 37 | # @!attribute float? 38 | # @return [Bool] true if the stream samples are {Float} 39 | def float? 40 | (self[:mFormatFlags] & FormatFlagIsFloat) != 0 41 | end 42 | 43 | def integer 44 | self[:mFormatFlags] &= ~FormatFlagIsFloat 45 | self[:mFormatFlags] |= FormatFlagIsSignedInteger 46 | end 47 | 48 | # @!attribute sample_rate 49 | # @return [Float] the number of sample frames per second 50 | def sample_rate; self[:mSampleRate]; end 51 | def sample_rate=(rate) 52 | self[:mSampleRate] = rate 53 | end 54 | end 55 | 56 | class AudioValueRange < FFI::Struct 57 | layout :minimum, :double, 58 | :maximum, :double 59 | end 60 | 61 | class AudioStreamRangedDescription < FFI::Struct 62 | layout :mFormat, AudioStreamBasicDescription, 63 | :mSampleRateRange, AudioValueRange 64 | end 65 | 66 | class AudioStream < AudioObject 67 | PropertyIsActive = 'sact', 68 | PropertyDirection = 'sdir', 69 | PropertyTerminalType = 'term', 70 | PropertyStartingChannel = 'schn', 71 | PropertyLatency = CoreAudio::AudioDevice::PropertyLatency, 72 | PropertyVirtualFormat = 'sfmt', 73 | PropertyAvailableVirtualFormats = 'sfma', 74 | PropertyPhysicalFormat = 'pft ', 75 | PropertyAvailablePhysicalFormats = 'pfta' 76 | 77 | def virtual_format 78 | address = PropertyAddress.global_master(PropertyVirtualFormat) 79 | buffer = get_property(address) 80 | AudioStreamBasicDescription.new(buffer) 81 | end 82 | 83 | def virtual_formats 84 | address = PropertyAddress.global_master(PropertyAvailableVirtualFormats) 85 | buffer = get_property(address) 86 | count = buffer.size/AudioStreamRangedDescription.size 87 | output = [] 88 | count.times do |i| 89 | output << AudioStreamRangedDescription.new(buffer) 90 | buffer += AudioStreamRangedDescription.size 91 | end 92 | output 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/macos/core_audio/audio_types.rb: -------------------------------------------------------------------------------- 1 | module CoreAudio 2 | class AudioBuffer < FFI::Struct 3 | layout :mNumberChannels, :uint32, 4 | :mDataByteSize, :uint32, 5 | :mData, :pointer 6 | 7 | # @return [String] the raw bytes 8 | def bytes 9 | self[:mData].get_bytes(0, self[:mDataByteSize]) 10 | end 11 | 12 | def bytesize 13 | self[:mDataByteSize] 14 | end 15 | 16 | # @return [Array] an array of samples, converted to signed integers 17 | def samples_int(bytes_per_channel) 18 | self[:mData].get_array_of_int16(0, self[:mDataByteSize]/bytes_per_channel) 19 | end 20 | 21 | # @return [Array] an array of samples, converted to {Float} 22 | def samples_float(bytes_per_channel) 23 | self[:mData].get_array_of_float32(0, self[:mDataByteSize]/bytes_per_channel) 24 | end 25 | end 26 | 27 | class AudioBufferList < FFI::Struct 28 | layout :mNumberBuffers, :uint32, 29 | :mBuffers, AudioBuffer 30 | 31 | def self.buffer_list(channels:1, size:nil) 32 | self.new.tap do |list| 33 | list[:mNumberBuffers] = 1 34 | list[:mBuffers][:mNumberChannels] = channels 35 | 36 | if( size ) 37 | list[:mBuffers][:mData] = FFI::MemoryPointer.new(size) 38 | list[:mBuffers][:mDataByteSize] = size 39 | else 40 | list[:mBuffers][:mDataByteSize] = 0 41 | end 42 | end 43 | end 44 | 45 | # @return [Array] the buffers 46 | def buffers 47 | raise("Can't handle multiple buffers yet") if self[:mNumberBuffers] > 1 48 | [self[:mBuffers]] 49 | end 50 | 51 | # @return [String] the raw bytes 52 | def bytes 53 | buffers.map(&:bytes).join 54 | end 55 | 56 | # @return [Number] the total number of bytes in the buffer list 57 | def bytesize 58 | buffers.map(&:bytesize).reduce(&:+) 59 | end 60 | 61 | # Retrieve the samples as an {Array}, after converting to the requested type 62 | # @param type [Symbol] :float or :int. Convert the samples to the given type. 63 | # @param bytes_per_channel [Number] the number of bytes for each sample 64 | # @return [Array] an array of samples, converted to {Float} or {Integer} 65 | def samples(type, bytes_per_channel) 66 | if type == :float 67 | buffers.map {|buffer| buffer.samples_float(bytes_per_channel)}.flatten 68 | else 69 | buffers.map {|buffer| buffer.samples_int(bytes_per_channel)}.flatten 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/macos/core_foundation.rb: -------------------------------------------------------------------------------- 1 | require 'ffi' 2 | 3 | module CoreFoundation 4 | extend FFI::Library 5 | ffi_lib '/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation' 6 | 7 | typedef :pointer, :CFStringRef 8 | 9 | OSStatus = typedef :uint32, :OSStatus 10 | 11 | if FFI::Platform::ARCH == 'x86_64' 12 | CFIndex = FFI::Type::LONG_LONG 13 | else 14 | CFIndex = FFI::Type::LONG 15 | end 16 | 17 | # CFString.h 18 | CFStringEncoding = enum :uint32, 19 | :MacRoman, 0, 20 | :WindowsLatin1, 0x0500, # ANSI codepage 1252 21 | :ISOLatin1, 0x0201, # ISO 8859-1 22 | :NextStepLatin, 0x0B01, # NextStep encoding 23 | :ASCII, 0x0600, # 0..127 (in creating CFString, values greater than 0x7F are treated as corresponding Unicode value) 24 | :Unicode, 0x0100, # kTextEncodingUnicodeDefault + kTextEncodingDefaultFormat (aka kUnicode16BitFormat) 25 | :UTF8, 0x08000100, # kTextEncodingUnicodeDefault + kUnicodeUTF8Format 26 | :NonLossyASCII, 0x0BFF, # 7bit Unicode variants used by Cocoa & Java 27 | :UTF16, 0x0100, # kTextEncodingUnicodeDefault + kUnicodeUTF16Format (alias of kCFStringEncodingUnicode) 28 | :UTF16BE, 0x10000100, # kTextEncodingUnicodeDefault + kUnicodeUTF16BEFormat 29 | :UTF16LE, 0x14000100, # kTextEncodingUnicodeDefault + kUnicodeUTF16LEFormat 30 | :UTF32, 0x0c000100, # kTextEncodingUnicodeDefault + kUnicodeUTF32Format 31 | :UTF32BE, 0x18000100, # kTextEncodingUnicodeDefault + kUnicodeUTF32BEFormat 32 | :UTF32LE, 0x1c000100, # kTextEncodingUnicodeDefault + kUnicodeUTF32LEFormat 33 | :Invalid, 0xffffffff # Invalid Encoding 34 | 35 | class CFRange < FFI::Struct 36 | layout :location, CFIndex, 37 | :length, CFIndex 38 | 39 | def self.make(location:0, length:0) 40 | new.tap do |range| 41 | range[:location] = location 42 | range[:length] = length 43 | end 44 | end 45 | end 46 | 47 | class CFStringRef < FFI::Pointer 48 | # @return [CFIndex] the length of the referenced CFString 49 | def length 50 | CoreFoundation.CFStringGetLength(self) 51 | end 52 | 53 | # @return [CFIndex] the maximum size of the buffer that will hold the string 54 | def max_size 55 | CoreFoundation.CFStringGetMaximumSizeForEncoding(length, CFStringEncoding[:UTF8]) 56 | end 57 | 58 | # @return [String] the CFString, converted to a UTF-8 string 59 | def to_s 60 | buffer = FFI::MemoryPointer.new(:char, max_size) 61 | used_bytes = FFI::MemoryPointer.new(CFIndex) 62 | CoreFoundation.CFStringGetBytes(self, 63 | CFRange.make(location:0, length:length), 64 | CFStringEncoding[:UTF8], 65 | 0, 66 | false, 67 | buffer, 68 | buffer.size, 69 | used_bytes) 70 | 71 | used_bytes = if CFIndex == CoreFoundation.find_type(:long_long) 72 | used_bytes.read_long_long 73 | else 74 | used_bytes.read_long 75 | end 76 | 77 | buffer.read_string(used_bytes).force_encoding(Encoding::UTF_8) 78 | end 79 | end 80 | 81 | # CFIndex CFStringGetBytes(CFStringRef theString, CFRange range, CFStringEncoding encoding, UInt8 lossByte, Boolean isExternalRepresentation, UInt8 *buffer, CFIndex maxBufLen, CFIndex *usedBufLen) 82 | attach_function :CFStringGetBytes, [:CFStringRef, CFRange.by_value, CFStringEncoding, :uint8, :bool, :buffer_out, CFIndex, :buffer_out], CFIndex 83 | 84 | # CFIndex CFStringGetLength(CFStringRef theString) 85 | attach_function :CFStringGetLength, [:CFStringRef], CFIndex 86 | 87 | # CFIndex CFStringGetMaximumSizeForEncoding(CFIndex length, CFStringEncoding encoding) 88 | attach_function :CFStringGetMaximumSizeForEncoding, [CFIndex, CFStringEncoding], CFIndex 89 | end --------------------------------------------------------------------------------