├── .document ├── .gitignore ├── ChangeLog ├── Gemfile ├── LICENSE.txt ├── README.rdoc ├── Rakefile ├── coreaudio.gemspec ├── examples ├── .gitignore ├── Gemfile ├── convert_wav_to_m4a.rb ├── fft_shift_pitch.rb ├── loopback_delay.rb ├── outbuffer_sine.rb ├── outloop_sine.rb ├── record_to_wave.rb ├── ring_modulator.rb ├── set_def_out_dev.rb └── set_def_out_sample_rate.rb ├── ext └── coreaudio │ ├── audiofile.m │ ├── coreaudio.h │ ├── coreaudio.m │ ├── coreaudio_missing.c │ ├── depend │ └── extconf.rb └── lib ├── coreaudio.rb └── coreaudio ├── audiofile.rb └── version.rb /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ext/Makefile 2 | ext/extconf.h 3 | 4 | *.gem 5 | /Gemfile.lock 6 | 7 | tmp/ 8 | pkg/ 9 | 10 | # rcov generated 11 | coverage 12 | 13 | # rdoc generated 14 | rdoc 15 | 16 | # yard generated 17 | doc/ 18 | .yardoc 19 | 20 | # bundler 21 | .bundle 22 | lib/bundler/man 23 | vendor/bundle 24 | 25 | # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: 26 | # 27 | # * Create a file at ~/.gitignore 28 | # * Include files you want ignored 29 | # * Run: git config --global core.excludesfile ~/.gitignore 30 | # 31 | # After doing this, these files will be ignored in all your git projects, 32 | # saving you from having to 'pollute' every project you touch with them 33 | # 34 | # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line) 35 | # 36 | # For MacOS: 37 | # 38 | .DS_Store 39 | 40 | # For TextMate 41 | #*.tmproj 42 | #tmtags 43 | 44 | # For emacs: 45 | #*~ 46 | #\#* 47 | #.\#* 48 | 49 | # For vim: 50 | #*.swp 51 | 52 | # For redcar: 53 | #.redcar 54 | 55 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | Fri Sep 9 12:11:46 2016 CHIKANAGA Tomoyuki 2 | 3 | * accept option argument of CoreAudio::AudioDevice.new. 4 | 5 | Wed Jan 14 13:55:46 2015 CHIKANAGA Tomoyuki 6 | 7 | * examples/set_def_out_sample_rate.rb: add example for set output 8 | device sampling rate. patched by @xavriley 9 | 10 | Thu May 15 20:05:15 2014 CHIKANAGA Tomoyuki 11 | 12 | * release 0.0.10 13 | * remove stale dependencies. 14 | * add a new feature. 15 | CoreAudio.set_default_output_device - set default output device, 16 | thank to kinushu. 17 | 18 | Sat Mar 10 19:16:03 2012 CHIKANAGA Tomoyuki 19 | 20 | * change gem generate tool from jeweler to Bundler. 21 | 22 | Tue Mar 6 12:39:59 2012 CHIKANAGA Tomoyuki 23 | 24 | * coreaudio.gemspec: fix dependency. narray should runtime dependency. 25 | 26 | * ext/coreaudio.m (ca_buffer_space): add AudioBuffer#space. 27 | it tells space of ring buffer in frame number. 28 | 29 | Tue Mar 6 12:13:01 2012 CHIKANAGA Tomoyuki 30 | 31 | * coreaudio.gemspec: Version bump to 0.0.9. 32 | 33 | Fri Feb 24 01:06:20 2012 CHIKANAGA Tomoyuki 34 | 35 | * release 0.0.8 36 | 0.0.7 has invalid dependency. 37 | 38 | Fri Feb 24 00:47:07 2012 CHIKANAGA Tomoyuki 39 | 40 | * release 0.0.7 41 | 42 | * ext/coreaudio.m (ca_out_buffer_data_append): fix last 1 frame 43 | is overwritten by next buffer's first frame. fixes issue #7. 44 | reported by dturnbull. Thank you! 45 | 46 | Fri Feb 23 23:24:00 2012 CHIKANAGA Tomoyuki 47 | 48 | * Gemfile: use 'gemspec' 49 | * Gemfile.lock: delete from reository 50 | * .gitignore: add Gemfile.lock for ignore list 51 | * coreaudio.gemspec: udpate dependent version of jeweler 52 | 53 | Fri Nov 25 01:03:49 2011 CHIKANAGA Tomoyuki 54 | 55 | * examples/fft_shift_pitch.rb: ignore first element of fft result. 56 | 57 | Tue Nov 22 00:09:30 2011 CHIKANAGA Tomoyuki 58 | 59 | * ext/audiofile.m(ca_audio_file_initialize): convert first argument 60 | to String if necessary. 61 | 62 | Sat Nov 12 18:23:10 2011 CHIKANAGA Tomoyuki 63 | 64 | * release 0.0.6 65 | * change usage of rb_scan_args() in AudioFile.new. support ruby 1.9.2. 66 | 67 | Tue Nov 1 22:01:39 2011 CHIKANAGA Tomoyuki 68 | 69 | * release 0.0.5 70 | * fix an error in get name of audio device on some environment. 71 | (use CFStringGetCString() instead of CFStringGetCStringPtr()) 72 | 73 | Sat Oct 29 00:55:11 2011 CHIKANAGA Tomoyuki 74 | 75 | * release 0.0.4 76 | * use NArray for audio data. 77 | * rename AudioFile.new option keyword(:channel -> :channels) 78 | * add some examples. 79 | 80 | Mon Oct 17 01:19:36 2011 CHIKANAGA Tomoyuki 81 | 82 | * release 0.0.2 83 | * add CoreAudio::AudioFile, ability to read/write audio file. 84 | 85 | Mon Oct 10 23:01:23 2011 CHIKANAGA Tomoyuki 86 | 87 | * release 0.0.2 88 | * fix build error with 1.9.2 89 | 90 | Sat Oct 8 02:52:38 2011 CHIKANAGA Tomoyuki 91 | 92 | * release 0.0.1 93 | * initial release 94 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 Tomoyuki Chikanaga. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = coreaudio 2 | 3 | CoreAudio (Audio Framework of Mac OS X) wrapper library for ruby 2.0.0 and later. 4 | 5 | == Features 6 | * HAL (Hardware Abstraction Layer) wrapper 7 | * Detect audio devices 8 | * Get/Set device properties (numbers of channels, sample rate, frame size...) 9 | * Output audio data (Loop buffer, Stream buffer) 10 | * Input audio data (Stream buffer) 11 | * Save audio data to WAV/M4A file (wrapper for ExtAudioFile) 12 | * Ream audio file (WAV/M4A) 13 | 14 | == ToDo 15 | * Wrapper for AudioUnit/AudioGraph 16 | 17 | == Contributing to coreaudio 18 | 19 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 20 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 21 | * Fork the project 22 | * Start a feature/bugfix branch 23 | * Commit and push until you are happy with your contribution 24 | * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 25 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 26 | 27 | == Copyright 28 | 29 | Copyright (c) 2011 CHIKANAGA Tomoyuki. See LICENSE.txt for 30 | further details. 31 | 32 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | -------------------------------------------------------------------------------- /coreaudio.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/coreaudio/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version= 6 | 7 | gem.authors = ["CHIKANAGA Tomoyuki"] 8 | gem.email = ["nagachika00@gmail.com"] 9 | gem.description = "Mac OS X CoreAudio wrapper library" 10 | gem.summary = "Mac OS X CoreAudio wrapper library" 11 | gem.homepage = "https://github.com/nagachika/ruby-coreaudio" 12 | 13 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 14 | gem.files = `git ls-files`.split("\n") 15 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 16 | gem.name = "coreaudio" 17 | gem.require_paths = ["lib"] 18 | gem.version = CoreAudio::VERSION 19 | 20 | gem.extensions = ["ext/coreaudio/extconf.rb"] 21 | gem.extra_rdoc_files = [ 22 | "ChangeLog", 23 | "LICENSE.txt", 24 | "README.rdoc" 25 | ] 26 | gem.licenses = ["BSDL"] 27 | 28 | if gem.respond_to? :specification_version then 29 | gem.specification_version = 3 30 | 31 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 32 | gem.add_runtime_dependency(%q, ["~> 0.6.0.0"]) 33 | gem.add_development_dependency(%q, [">= 0"]) 34 | gem.add_development_dependency(%q, [">= 0"]) 35 | gem.add_development_dependency(%q, [">= 0"]) 36 | else 37 | gem.add_dependency(%q, ["~> 0.6.0.0"]) 38 | gem.add_dependency(%q, [">= 0"]) 39 | gem.add_dependency(%q, [">= 0"]) 40 | gem.add_dependency(%q, [">= 0"]) 41 | end 42 | else 43 | gem.add_dependency(%q, ["~> 0.6.0.0"]) 44 | gem.add_dependency(%q, [">= 0"]) 45 | gem.add_dependency(%q, [">= 0"]) 46 | gem.add_dependency(%q, [">= 0"]) 47 | end 48 | end 49 | 50 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | -------------------------------------------------------------------------------- /examples/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "coreaudio" 4 | gem "fftw3" 5 | -------------------------------------------------------------------------------- /examples/convert_wav_to_m4a.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "coreaudio" 4 | 5 | wav = CoreAudio::AudioFile.new("sample.wav", :read) 6 | 7 | m4a = CoreAudio::AudioFile.new("sample.m4a", :write, :format => :m4a, 8 | :rate => wav.rate, 9 | :channels => wav.channels) 10 | 11 | loop do 12 | buf = wav.read(1024) 13 | break if buf.nil? 14 | m4a.write(buf) 15 | end 16 | 17 | wav.close 18 | m4a.close 19 | 20 | -------------------------------------------------------------------------------- /examples/fft_shift_pitch.rb: -------------------------------------------------------------------------------- 1 | 2 | require "thread" 3 | require "fftw3" 4 | require "coreaudio" 5 | 6 | Thread.abort_on_exception = true 7 | 8 | inbuf = CoreAudio.default_input_device.input_buffer(1024) 9 | outbuf = CoreAudio.default_output_device.output_buffer(1024) 10 | 11 | queue = Queue.new 12 | pitch_shift_th = Thread.start do 13 | while w = queue.pop 14 | half = w.shape[1] / 2 15 | f = FFTW3.fft(w, 1) 16 | shift = 12 17 | f.shape[0].times do |ch| 18 | f[ch, (shift+1)...half] = f[ch, 1...(half-shift)] 19 | f[ch, 1..shift] = 0 20 | f[ch, (half+1)...(w.shape[1]-shift)] = f[ch, (half+shift+1)..-1] 21 | f[ch, -shift..-1] = 0 22 | end 23 | outbuf << FFTW3.ifft(f, 1) / w.shape[1] 24 | end 25 | end 26 | 27 | th = Thread.start do 28 | loop do 29 | wav = inbuf.read(1024) 30 | queue.push(wav) 31 | end 32 | end 33 | 34 | inbuf.start 35 | outbuf.start 36 | $stdout.print "loopback..." 37 | $stdout.flush 38 | sleep 10; 39 | queue.push(nil) 40 | inbuf.stop 41 | outbuf.stop 42 | $stdout.puts "done." 43 | th.kill.join 44 | pitch_shift_th.kill.join 45 | 46 | puts "#{inbuf.dropped_frame} frame dropped at input buffer." 47 | puts "#{outbuf.dropped_frame} frame dropped at output buffer." 48 | -------------------------------------------------------------------------------- /examples/loopback_delay.rb: -------------------------------------------------------------------------------- 1 | 2 | require "coreaudio" 3 | 4 | inbuf = CoreAudio.default_input_device.input_buffer(1024) 5 | outbuf = CoreAudio.default_output_device.output_buffer(1024) 6 | 7 | th = Thread.start do 8 | ary = [] 9 | loop do 10 | wav = inbuf.read(1024) 11 | ary.push(wav) 12 | # 1024*43 frames delayed. about 1 sec when sample rate = 44100Hz 13 | if ary.size > 43 14 | outbuf << ary.shift 15 | end 16 | end 17 | end 18 | 19 | inbuf.start 20 | outbuf.start 21 | $stdout.print "loopback..." 22 | $stdout.flush 23 | sleep 10; 24 | inbuf.stop 25 | outbuf.stop 26 | $stdout.puts "done." 27 | th.kill.join 28 | 29 | puts "#{inbuf.dropped_frame} frame dropped at input buffer." 30 | puts "#{outbuf.dropped_frame} frame dropped at output buffer." 31 | -------------------------------------------------------------------------------- /examples/outbuffer_sine.rb: -------------------------------------------------------------------------------- 1 | require "coreaudio" 2 | 3 | dev = CoreAudio.default_output_device 4 | buf = dev.output_buffer(1024) 5 | 6 | phase = Math::PI * 2.0 * 440.0 / dev.nominal_rate 7 | th = Thread.start do 8 | i = 0 9 | wav = NArray.sint(1024) 10 | loop do 11 | 1024.times {|j| wav[j] = (0.4 * Math.sin(phase*(i+j)) * 0x7FFF).round } 12 | i += 1024 13 | buf << wav 14 | end 15 | end 16 | 17 | buf.start 18 | sleep 2 19 | buf.stop 20 | 21 | puts "#{buf.dropped_frame} frame dropped." 22 | 23 | th.kill.join 24 | -------------------------------------------------------------------------------- /examples/outloop_sine.rb: -------------------------------------------------------------------------------- 1 | require "coreaudio" 2 | 3 | dev = CoreAudio.default_output_device 4 | buf = dev.output_loop(44000) 5 | 6 | phase = Math::PI * 2.0 * 440.0 / 44000.0 7 | 44000.times do |i| 8 | buf[i] = ((0.4 * Math.sin(phase*i)) * 0x7FFF).round 9 | end 10 | 11 | buf.start 12 | sleep 2 13 | buf.stop 14 | 15 | -------------------------------------------------------------------------------- /examples/record_to_wave.rb: -------------------------------------------------------------------------------- 1 | 2 | require "coreaudio" 3 | 4 | dev = CoreAudio.default_input_device 5 | buf = dev.input_buffer(1024) 6 | 7 | wav = CoreAudio::AudioFile.new("sample.wav", :write, :format => :wav, 8 | :rate => dev.nominal_rate, 9 | :channels => dev.input_stream.channels) 10 | 11 | samples = 0 12 | th = Thread.start do 13 | loop do 14 | w = buf.read(4096) 15 | samples += w.size / dev.input_stream.channels 16 | wav.write(w) 17 | end 18 | end 19 | 20 | buf.start; 21 | $stdout.print "RECORDING..." 22 | $stdout.flush 23 | sleep 5; 24 | buf.stop 25 | $stdout.puts "done." 26 | th.kill.join 27 | 28 | wav.close 29 | 30 | puts "#{samples} samples read." 31 | puts "#{buf.dropped_frame} frame dropped." 32 | -------------------------------------------------------------------------------- /examples/ring_modulator.rb: -------------------------------------------------------------------------------- 1 | 2 | require "thread" 3 | require "coreaudio" 4 | 5 | Thread.abort_on_exception = true 6 | 7 | inbuf = CoreAudio.default_input_device.input_buffer(1024) 8 | outbuf = CoreAudio.default_output_device.output_buffer(1024) 9 | 10 | queue = Queue.new 11 | output_th = Thread.start do 12 | filter = NArray.float(2, 1024) 13 | filter.shape[1].times do |i| 14 | filter[true, i] = (i % 256 - 128) / 128.0 15 | end 16 | 17 | while w = queue.pop 18 | outbuf << w * filter 19 | end 20 | end 21 | 22 | input_th = Thread.start do 23 | loop do 24 | wav = inbuf.read(1024) 25 | queue.push(wav) 26 | end 27 | end 28 | 29 | inbuf.start 30 | outbuf.start 31 | $stdout.print "loopback..." 32 | $stdout.flush 33 | sleep 10; 34 | queue.push(nil) 35 | inbuf.stop 36 | outbuf.stop 37 | $stdout.puts "done." 38 | input_th.kill.join 39 | output_th.kill.join 40 | 41 | puts "#{inbuf.dropped_frame} frame dropped at input buffer." 42 | puts "#{outbuf.dropped_frame} frame dropped at output buffer." 43 | -------------------------------------------------------------------------------- /examples/set_def_out_dev.rb: -------------------------------------------------------------------------------- 1 | require "coreaudio" 2 | 3 | # Select option name device as default output device 4 | def set_interface(name) 5 | if !name 6 | puts 'please enter audio output interface name.' 7 | return -1 8 | end 9 | 10 | devs = CoreAudio.devices 11 | 12 | tgt = devs.find{|dev| dev.name.index(name)} 13 | if !tgt 14 | p "no match interface #{name}" 15 | return -1 16 | end 17 | 18 | CoreAudio.set_default_output_device(tgt) 19 | p "select default output audio interface #{tgt.name}" 20 | end 21 | 22 | set_interface(ARGV[0]) 23 | -------------------------------------------------------------------------------- /examples/set_def_out_sample_rate.rb: -------------------------------------------------------------------------------- 1 | require "coreaudio" 2 | 3 | # Select option name device as default output device 4 | def set_nominal_rate(rate) 5 | available_rates = CoreAudio.default_output_device.available_sample_rate.flatten.uniq 6 | rate = rate.to_f 7 | if (!rate || !available_rates.member?(rate)) 8 | puts "Please enter a valid sample rate. Choose one of the following: #{available_rates.join(', ')}" 9 | return -1 10 | end 11 | 12 | CoreAudio.default_output_device(nominal_rate: rate) 13 | puts "Output device sample rate set to #{rate}" 14 | end 15 | 16 | set_nominal_rate(ARGV[0]) 17 | -------------------------------------------------------------------------------- /ext/coreaudio/audiofile.m: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include 10 | 11 | #include "coreaudio.h" 12 | 13 | 14 | VALUE rb_cAudioFile; 15 | 16 | static VALUE sym_read, sym_write, sym_format; 17 | static VALUE sym_rate, sym_file_rate, sym_channels, sym_file_channels; 18 | static VALUE sym_wav, sym_m4a; 19 | 20 | static void 21 | setASBD(AudioStreamBasicDescription *asbd, 22 | Float64 rate, 23 | UInt32 format, 24 | UInt32 flags, 25 | UInt32 channels, 26 | UInt32 bitsPerChannel, 27 | UInt32 framePerPacket) 28 | { 29 | asbd->mSampleRate = rate; 30 | asbd->mFormatID = format; 31 | asbd->mFormatFlags = flags; 32 | asbd->mBitsPerChannel = bitsPerChannel; 33 | asbd->mChannelsPerFrame = channels; 34 | asbd->mFramesPerPacket = framePerPacket; 35 | asbd->mBytesPerFrame = bitsPerChannel/8*channels; 36 | asbd->mBytesPerPacket = bitsPerChannel/8*channels*framePerPacket; 37 | } 38 | 39 | typedef struct { 40 | AudioStreamBasicDescription file_desc; 41 | AudioStreamBasicDescription inner_desc; 42 | Boolean for_write; 43 | ExtAudioFileRef file; 44 | } ca_audio_file_t; 45 | 46 | static void 47 | ca_audio_file_free(void *ptr) 48 | { 49 | ca_audio_file_t *data = ptr; 50 | 51 | if ( data->file ) { 52 | ExtAudioFileDispose(data->file); 53 | data->file = NULL; 54 | } 55 | } 56 | 57 | static size_t 58 | ca_audio_file_memsize(const void *ptr) 59 | { 60 | (void)ptr; 61 | return sizeof(ca_audio_file_t); 62 | } 63 | 64 | static const rb_data_type_t ca_audio_file_type = { 65 | "ca_audio_file", 66 | {NULL, ca_audio_file_free, ca_audio_file_memsize} 67 | }; 68 | 69 | static VALUE 70 | ca_audio_file_alloc(VALUE klass) 71 | { 72 | VALUE obj; 73 | ca_audio_file_t *ptr; 74 | 75 | obj = TypedData_Make_Struct(klass, ca_audio_file_t, &ca_audio_file_type, ptr); 76 | 77 | return obj; 78 | } 79 | 80 | static void 81 | parse_audio_file_options(VALUE opt, Boolean for_write, 82 | Float64 *rate, Float64 *file_rate, 83 | UInt32 *channels, UInt32 *file_channels) 84 | { 85 | if (NIL_P(opt) || NIL_P(rb_hash_aref(opt, sym_rate))) { 86 | if (for_write) 87 | *rate = 44100.0; 88 | else 89 | *rate = *file_rate; 90 | } else { 91 | *rate = NUM2DBL(rb_hash_aref(opt, sym_rate)); 92 | } 93 | if (NIL_P(opt) || NIL_P(rb_hash_aref(opt, sym_channels))) { 94 | if (for_write) 95 | *channels = 2; 96 | else 97 | *channels = *file_channels; 98 | } else { 99 | *channels = NUM2UINT(rb_hash_aref(opt, sym_channels)); 100 | } 101 | 102 | if (for_write) { 103 | if (NIL_P(opt) || NIL_P(rb_hash_aref(opt, sym_file_rate))) { 104 | *file_rate = *rate; 105 | } else { 106 | *file_rate = NUM2DBL(rb_hash_aref(opt, sym_file_rate)); 107 | } 108 | if (NIL_P(opt) || NIL_P(rb_hash_aref(opt, sym_file_channels))) { 109 | *file_channels = *channels; 110 | } else { 111 | *file_channels = NUM2UINT(rb_hash_aref(opt, sym_file_channels)); 112 | } 113 | } 114 | } 115 | 116 | /* 117 | * call-seq: 118 | * AudioFile.new(filepath, mode, opt) 119 | * 120 | * open audio file at +filepath+. +mode+ should be :read or :write 121 | * corresponding to file mode argument of Kernel#open. 122 | * +opt+ must contain :format key. 123 | * 124 | * 'client data' means audio data pass to AudioFile#write or from 125 | * AudioFile#read method. 126 | * 127 | * :format :: audio file format. currently audio file format and 128 | * codec type is hardcoded. (:wav, :m4a) 129 | * :rate :: sample rate of data pass from AudioFile#read or to AudioFile#write 130 | * If not specified, :file_rate value is used. (Float) 131 | * :channels :: number of channels 132 | * :file_rate :: file data sample rate. only work when open for output. (Float) 133 | * :file_channels :: file data number of channels. only work when open for 134 | * output. 135 | */ 136 | static VALUE 137 | ca_audio_file_initialize(int argc, VALUE *argv, VALUE self) 138 | { 139 | ca_audio_file_t *data; 140 | VALUE path, mode, opt, format; 141 | Float64 rate, file_rate; 142 | UInt32 channels, file_channels; 143 | CFURLRef url = NULL; 144 | AudioFileTypeID filetype = 0; 145 | OSStatus err = noErr; 146 | 147 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, data); 148 | 149 | rb_scan_args(argc, argv, "12", &path, &mode, &opt); 150 | 151 | StringValue(path); 152 | 153 | /* check mode */ 154 | if (NIL_P(mode) || mode == sym_read) 155 | data->for_write = FALSE; 156 | else if (mode == sym_write) 157 | data->for_write = TRUE; 158 | else 159 | rb_raise(rb_eArgError, "coreaudio: mode should be :read or :write"); 160 | 161 | if (data->for_write) { 162 | /* when open for write, parse options before open ExtAudioFile */ 163 | parse_audio_file_options(opt, data->for_write, &rate, &file_rate, 164 | &channels, &file_channels); 165 | 166 | format = rb_hash_aref(opt, sym_format); 167 | if (NIL_P(format)) 168 | rb_raise(rb_eArgError, "coreaudio: :format option must be specified"); 169 | 170 | if (format == sym_wav) { 171 | filetype = kAudioFileWAVEType; 172 | setASBD(&data->file_desc, file_rate, kAudioFormatLinearPCM, 173 | kLinearPCMFormatFlagIsSignedInteger | 174 | kAudioFormatFlagIsPacked, 175 | file_channels, 16, 1); 176 | } else if (format == sym_m4a) { 177 | filetype = kAudioFileM4AType; 178 | setASBD(&data->file_desc, file_rate, kAudioFormatMPEG4AAC, 179 | 0, file_channels, 0, 0); 180 | } else { 181 | volatile VALUE str = rb_inspect(format); 182 | RB_GC_GUARD(str); 183 | rb_raise(rb_eArgError, "coreaudio: unsupported format (%s)", 184 | RSTRING_PTR(str)); 185 | } 186 | } 187 | 188 | /* create URL represent the target filepath */ 189 | url = CFURLCreateFromFileSystemRepresentation( 190 | NULL, StringValueCStr(path), (CFIndex)RSTRING_LEN(path), FALSE); 191 | 192 | /* open ExtAudioFile */ 193 | if (data->for_write) 194 | err = ExtAudioFileCreateWithURL(url, filetype, &data->file_desc, 195 | NULL, kAudioFileFlags_EraseFile, 196 | &data->file); 197 | else 198 | err = ExtAudioFileOpenURL(url, &data->file); 199 | CFRelease(url); 200 | url = NULL; 201 | if (err != noErr) { 202 | rb_raise(rb_eArgError, 203 | "coreaudio: fail to open ExtAudioFile: %d", (int)err); 204 | } 205 | 206 | /* get Audio Stream Basic Description (ASBD) from input file */ 207 | if (!data->for_write) { 208 | UInt32 size = sizeof(data->file_desc); 209 | err = ExtAudioFileGetProperty(data->file, 210 | kExtAudioFileProperty_FileDataFormat, 211 | &size, &data->file_desc); 212 | if (err != noErr) 213 | rb_raise(rb_eRuntimeError, 214 | "coreaudio: fail to Get ExtAudioFile Property %d", err); 215 | 216 | /* parse options */ 217 | file_rate = data->file_desc.mSampleRate; 218 | file_channels = data->file_desc.mChannelsPerFrame; 219 | parse_audio_file_options(opt, data->for_write, &rate, &file_rate, 220 | &channels, &file_channels); 221 | } 222 | 223 | setASBD(&data->inner_desc, rate, kAudioFormatLinearPCM, 224 | kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, 225 | channels, 16, 1); 226 | 227 | err = ExtAudioFileSetProperty( 228 | data->file, kExtAudioFileProperty_ClientDataFormat, 229 | sizeof(data->inner_desc), &data->inner_desc); 230 | if (err != noErr) { 231 | ExtAudioFileDispose(data->file); 232 | data->file = NULL; 233 | rb_raise(rb_eArgError, "coreaudio: fail to set client data format: %d", 234 | (int)err); 235 | } 236 | 237 | return self; 238 | } 239 | 240 | static VALUE 241 | ca_audio_file_close(VALUE self) 242 | { 243 | ca_audio_file_t *data; 244 | 245 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, data); 246 | if (data->file) { 247 | ExtAudioFileDispose(data->file); 248 | data->file = NULL; 249 | } 250 | 251 | return self; 252 | } 253 | 254 | static VALUE 255 | ca_audio_file_write(VALUE self, VALUE data) 256 | { 257 | ca_audio_file_t *file; 258 | UInt32 n_ch; 259 | int rank; 260 | short *buf = NULL; 261 | AudioBufferList buf_list; 262 | UInt32 frames; 263 | size_t alloc_size; 264 | volatile VALUE tmpstr = Qundef; 265 | OSStatus err = noErr; 266 | 267 | /* cast to NArray of SINT and check rank of NArray */ 268 | data = na_cast_object(data, NA_SINT); 269 | rank = NA_RANK(data); 270 | if (rank > 3) 271 | rb_raise(rb_eArgError, "coreaudio: audio buffer rank must be 1 or 2."); 272 | 273 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, file); 274 | 275 | if (file->file == NULL) 276 | rb_raise(rb_eIOError, "coreaudio: already closed file"); 277 | 278 | if (!file->for_write) 279 | rb_raise(rb_eRuntimeError, "coreaudio: audio file opened for reading"); 280 | 281 | n_ch = file->inner_desc.mChannelsPerFrame; 282 | 283 | if (rank == 2 && NA_SHAPE0(data) != (int)n_ch) 284 | rb_raise(rb_eArgError, 285 | "coreaudio: audio buffer size of first dimension must be " 286 | "equal to number of channels"); 287 | 288 | frames = rank == 1 ? NA_SHAPE0(data) : NA_SHAPE1(data); 289 | alloc_size = (file->inner_desc.mBitsPerChannel/8) * frames * n_ch; 290 | 291 | /* prepare interleaved audio buffer */ 292 | buf_list.mNumberBuffers = 1; 293 | buf_list.mBuffers[0].mNumberChannels = n_ch; 294 | buf_list.mBuffers[0].mDataByteSize = (UInt32)alloc_size; 295 | 296 | if ((rank == 1 && n_ch == 1) || rank == 2) { 297 | /* no need to allocate buffer. NArray buffer can be used as mData */ 298 | buf_list.mBuffers[0].mData = NA_PTR_TYPE(data, void *); 299 | } else { 300 | UInt32 i, j; 301 | short *na_buf = NA_PTR_TYPE(data, short *); 302 | 303 | buf_list.mBuffers[0].mData = rb_alloc_tmp_buffer(&tmpstr, alloc_size); 304 | buf = buf_list.mBuffers[0].mData; 305 | 306 | for (i = 0; i < frames; i++) { 307 | for (j = 0; j < n_ch; j++) { 308 | buf[i*n_ch+j] = na_buf[i]; 309 | } 310 | } 311 | } 312 | 313 | err = ExtAudioFileWrite(file->file, frames, &buf_list); 314 | 315 | if (tmpstr != Qundef) 316 | rb_free_tmp_buffer(&tmpstr); 317 | 318 | if (err != noErr) { 319 | rb_raise(rb_eRuntimeError, 320 | "coreaudio: ExtAudioFileWrite() fails: %d", (int)err); 321 | } 322 | 323 | return self; 324 | } 325 | 326 | static VALUE 327 | ca_audio_file_read_frames(VALUE self, VALUE frame_val) 328 | { 329 | ca_audio_file_t *file; 330 | UInt32 channels, frames, read_frames; 331 | AudioBufferList buf_list; 332 | size_t alloc_size; 333 | VALUE nary; 334 | int shape[2]; 335 | OSStatus err = noErr; 336 | 337 | frames = NUM2UINT(frame_val); 338 | 339 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, file); 340 | 341 | if (file->file == NULL) 342 | rb_raise(rb_eIOError, "coreaudio: already closend file"); 343 | 344 | if (file->for_write) 345 | rb_raise(rb_eRuntimeError, "coreaudio: audio file open for writing"); 346 | 347 | channels = file->inner_desc.mChannelsPerFrame; 348 | 349 | shape[0] = channels; 350 | shape[1] = frames; 351 | nary = na_make_object(NA_SINT, 2, shape, cNArray); 352 | 353 | alloc_size = (file->inner_desc.mBitsPerChannel/8) * channels * frames; 354 | 355 | /* prepare interleaved audio buffer */ 356 | buf_list.mNumberBuffers = 1; 357 | buf_list.mBuffers[0].mNumberChannels = channels; 358 | buf_list.mBuffers[0].mDataByteSize = (UInt32)alloc_size; 359 | buf_list.mBuffers[0].mData = NA_PTR_TYPE(nary, void *); 360 | 361 | read_frames = frames; 362 | err = ExtAudioFileRead(file->file, &read_frames, &buf_list); 363 | 364 | if (err != noErr) { 365 | rb_raise(rb_eRuntimeError, 366 | "coreaudio: ExtAudioFileRead() fails: %d", (int)err); 367 | } 368 | 369 | if (read_frames == 0) 370 | return Qnil; 371 | 372 | NA_SHAPE1(nary) = read_frames; 373 | 374 | return nary; 375 | } 376 | 377 | static VALUE 378 | ca_audio_file_rate(VALUE self) 379 | { 380 | ca_audio_file_t *data; 381 | 382 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, data); 383 | 384 | return DBL2NUM(data->file_desc.mSampleRate); 385 | } 386 | 387 | static VALUE 388 | ca_audio_file_channels(VALUE self) 389 | { 390 | ca_audio_file_t *data; 391 | 392 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, data); 393 | 394 | return UINT2NUM((unsigned int)data->file_desc.mChannelsPerFrame); 395 | } 396 | 397 | static VALUE 398 | ca_audio_file_inner_rate(VALUE self) 399 | { 400 | ca_audio_file_t *data; 401 | 402 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, data); 403 | 404 | return DBL2NUM(data->inner_desc.mSampleRate); 405 | } 406 | 407 | static VALUE 408 | ca_audio_file_inner_channels(VALUE self) 409 | { 410 | ca_audio_file_t *data; 411 | 412 | TypedData_Get_Struct(self, ca_audio_file_t, &ca_audio_file_type, data); 413 | 414 | return UINT2NUM((unsigned int)data->inner_desc.mChannelsPerFrame); 415 | } 416 | 417 | void 418 | Init_coreaudio_audiofile(void) 419 | { 420 | sym_read = ID2SYM(rb_intern("read")); 421 | sym_write = ID2SYM(rb_intern("write")); 422 | sym_format = ID2SYM(rb_intern("format")); 423 | sym_rate = ID2SYM(rb_intern("rate")); 424 | sym_file_rate = ID2SYM(rb_intern("file_rate")); 425 | sym_channels = ID2SYM(rb_intern("channels")); 426 | sym_file_channels = ID2SYM(rb_intern("file_channels")); 427 | sym_wav = ID2SYM(rb_intern("wav")); 428 | sym_m4a = ID2SYM(rb_intern("m4a")); 429 | 430 | rb_cAudioFile = rb_define_class_under(rb_mCoreAudio, "AudioFile", 431 | rb_cObject); 432 | 433 | rb_define_alloc_func(rb_cAudioFile, ca_audio_file_alloc); 434 | rb_define_method(rb_cAudioFile, "initialize", ca_audio_file_initialize, -1); 435 | rb_define_method(rb_cAudioFile, "close", ca_audio_file_close, 0); 436 | rb_define_method(rb_cAudioFile, "write", ca_audio_file_write, 1); 437 | rb_define_method(rb_cAudioFile, "read_frames", ca_audio_file_read_frames, 1); 438 | rb_define_method(rb_cAudioFile, "rate", ca_audio_file_rate, 0); 439 | rb_define_method(rb_cAudioFile, "channels", ca_audio_file_channels, 0); 440 | rb_define_method(rb_cAudioFile, "inner_rate", ca_audio_file_inner_rate, 0); 441 | rb_define_method(rb_cAudioFile, "inner_channels", ca_audio_file_inner_channels, 0); 442 | } 443 | -------------------------------------------------------------------------------- /ext/coreaudio/coreaudio.h: -------------------------------------------------------------------------------- 1 | #ifndef COREAUDIO_H 2 | #define COREAUDIO_H 1 3 | 4 | #include 5 | 6 | #include "narray.h" 7 | #include "extconf.h" 8 | 9 | extern VALUE rb_mCoreAudio; 10 | extern VALUE rb_mAudioFile; 11 | 12 | extern void Init_coreaudio_audiofile(void); 13 | 14 | /*-- Utility Macros --*/ 15 | #define CROPF(F) ((F) > 1.0 ? 1.0 : (((F) < -1.0) ? -1.0 : (F))) 16 | #define FLOAT2SHORT(F) ((short)(CROPF(F)*0x7FFF)) 17 | #define SHORT2FLOAT(S) ((float)(S) / (float)32767.0) 18 | 19 | /*-- prototypes for missing functions --*/ 20 | 21 | #ifndef HAVE_RB_ALLOC_TMP_BUFFER 22 | extern void *rb_alloc_tmp_buffer(volatile VALUE *store, long len); 23 | #endif 24 | 25 | #ifndef HAVE_RB_FREE_TMP_BUFFER 26 | extern void rb_free_tmp_buffer(volatile VALUE *store); 27 | #endif 28 | 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /ext/coreaudio/coreaudio.m: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | #include 10 | 11 | #include "coreaudio.h" 12 | 13 | VALUE rb_mCoreAudio; 14 | 15 | static VALUE rb_cAudioDevice; 16 | static VALUE rb_cAudioStream; 17 | static VALUE rb_cOutLoop; 18 | static VALUE rb_cAudioBuffer; 19 | static VALUE rb_cOutputBuffer; 20 | static VALUE rb_cInputBuffer; 21 | static ID sym_iv_devid; 22 | static ID sym_iv_name; 23 | static ID sym_iv_available_sample_rate; 24 | static ID sym_iv_nominal_rate; 25 | static ID sym_iv_input_stream; 26 | static ID sym_iv_output_stream; 27 | static ID sym_iv_channels; 28 | static ID sym_iv_buffer_frame_size; 29 | 30 | /* utility macro */ 31 | #define PropertyAddress { \ 32 | .mScope = kAudioObjectPropertyScopeGlobal, \ 33 | .mElement = kAudioObjectPropertyElementMaster \ 34 | } 35 | 36 | /*--- CoreAudio::AudioStream ---*/ 37 | static VALUE 38 | ca_get_stream_channel_num(AudioDeviceID devID, 39 | AudioObjectPropertyScope scope) 40 | { 41 | AudioObjectPropertyAddress address = PropertyAddress; 42 | UInt32 size; 43 | AudioChannelLayout *layout; 44 | OSStatus status; 45 | UInt32 ch_num; 46 | 47 | address.mSelector = kAudioDevicePropertyPreferredChannelLayout; 48 | address.mScope = scope; 49 | if (!AudioObjectHasProperty(devID, &address)) 50 | return INT2NUM(0); 51 | 52 | status = AudioObjectGetPropertyDataSize(devID, &address, 0, NULL, &size); 53 | 54 | if (status != noErr) { 55 | rb_raise(rb_eArgError, 56 | "coreaudio: get preferred channel layout size failed: %d", status); 57 | } 58 | 59 | layout = alloca(size); 60 | 61 | status = AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, layout); 62 | if (status != noErr) { 63 | rb_raise(rb_eArgError, 64 | "coreaudio: get preferred channel layout failed: %d", status); 65 | } 66 | 67 | if (layout->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelDescriptions) { 68 | ch_num = layout->mNumberChannelDescriptions; 69 | } else if (layout->mChannelLayoutTag == kAudioChannelLayoutTag_UseChannelBitmap) { 70 | UInt32 i; 71 | ch_num = 0; 72 | for ( i = 0; i < sizeof(layout->mChannelBitmap)*8; i++ ) { 73 | if ( (layout->mChannelBitmap >> i) & 0x01 ) 74 | ch_num++; 75 | } 76 | } else { 77 | ch_num = AudioChannelLayoutTag_GetNumberOfChannels(layout->mChannelLayoutTag); 78 | } 79 | return UINT2NUM(ch_num); 80 | } 81 | 82 | static VALUE 83 | ca_get_stream_buffer_frame(AudioDeviceID devID, AudioObjectPropertyScope scope) 84 | { 85 | AudioObjectPropertyAddress address = PropertyAddress; 86 | UInt32 size, framesize; 87 | OSStatus status; 88 | 89 | address.mSelector = kAudioDevicePropertyBufferFrameSize; 90 | address.mScope = scope; 91 | 92 | if (!AudioObjectHasProperty(devID, &address)) 93 | return INT2NUM(0); 94 | 95 | size = sizeof(framesize); 96 | status = AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &framesize); 97 | if (status != noErr) { 98 | rb_raise(rb_eArgError, 99 | "coreaudio: get buffer frame size failed: %d", status); 100 | } 101 | return UINT2NUM(framesize); 102 | } 103 | 104 | static VALUE 105 | ca_stream_initialize(VALUE self, VALUE devid_val, VALUE is_input) 106 | { 107 | AudioDeviceID devID = (AudioDeviceID)NUM2UINT(devid_val); 108 | AudioObjectPropertyScope scope; 109 | 110 | if (RTEST(is_input)) 111 | scope = kAudioDevicePropertyScopeInput; 112 | else 113 | scope = kAudioDevicePropertyScopeOutput; 114 | rb_ivar_set(self, sym_iv_channels, ca_get_stream_channel_num(devID, scope)); 115 | rb_ivar_set(self, sym_iv_buffer_frame_size, ca_get_stream_buffer_frame(devID, scope)); 116 | 117 | return self; 118 | } 119 | 120 | static VALUE 121 | ca_stream_new(VALUE devid, VALUE is_input) 122 | { 123 | VALUE stream; 124 | stream = rb_obj_alloc(rb_cAudioStream); 125 | ca_stream_initialize(stream, devid, is_input); 126 | return stream; 127 | } 128 | 129 | /*--- CoreAudio::AudioDevice ---*/ 130 | static VALUE 131 | ca_get_device_name(AudioDeviceID devID) 132 | { 133 | AudioObjectPropertyAddress address = PropertyAddress; 134 | UInt32 size; 135 | OSStatus status; 136 | CFStringRef deviceName = NULL; 137 | char cbuf[256]; 138 | VALUE str; 139 | 140 | address.mSelector = kAudioObjectPropertyName; 141 | size = sizeof(deviceName); 142 | 143 | status = AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &deviceName); 144 | if ( status != noErr ) { 145 | rb_raise(rb_eArgError, "coreaudio: get device name failed: %d", status); 146 | } 147 | if (!CFStringGetCString(deviceName, cbuf, (CFIndex)sizeof(cbuf), 148 | kCFStringEncodingUTF8)) { 149 | /* String conversion failed. Ignore an error and return empty String */ 150 | cbuf[0] = '\0'; 151 | } 152 | CFRelease(deviceName); 153 | str = rb_str_new2(cbuf); 154 | 155 | return str; 156 | } 157 | 158 | static VALUE 159 | ca_get_device_available_sample_rate(AudioDeviceID devID) 160 | { 161 | AudioObjectPropertyAddress address = PropertyAddress; 162 | UInt32 size; 163 | UInt32 n_rates; 164 | AudioValueRange *sample_rates; 165 | OSStatus status; 166 | VALUE ary; 167 | UInt32 i; 168 | 169 | address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; 170 | status = AudioObjectGetPropertyDataSize(devID, &address, 0, NULL, &size); 171 | 172 | if (status != noErr) { 173 | rb_raise(rb_eArgError, 174 | "coreaudio: get available sample rates size failed: %d", status); 175 | } 176 | 177 | n_rates = size / (UInt32)sizeof(AudioValueRange); 178 | sample_rates = ALLOCA_N(AudioValueRange, n_rates); 179 | 180 | status = AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, sample_rates); 181 | if (status != noErr) { 182 | rb_raise(rb_eArgError, 183 | "coreaudio: get available sample rates failed: %d", status); 184 | } 185 | 186 | ary = rb_ary_new(); 187 | for ( i = 0; i < n_rates; i++ ) { 188 | rb_ary_push(ary, 189 | rb_ary_new3(2, 190 | DBL2NUM((double)sample_rates[i].mMinimum), 191 | DBL2NUM((double)sample_rates[i].mMaximum))); 192 | } 193 | 194 | return ary; 195 | } 196 | 197 | static VALUE 198 | ca_set_device_nominal_sample_rate(AudioDeviceID devID, VALUE sampleRateVal) 199 | { 200 | AudioObjectPropertyAddress address = PropertyAddress; 201 | UInt32 size; 202 | OSStatus status; 203 | Float64 rate = NUM2DBL(sampleRateVal); 204 | 205 | address.mSelector = kAudioDevicePropertyNominalSampleRate; 206 | status = AudioObjectGetPropertyDataSize(devID, &address, 0, NULL, &size); 207 | 208 | if (status != noErr) { 209 | rb_raise(rb_eArgError, 210 | "coreaudio: get nominal sample rates size failed: %d", status); 211 | } 212 | status = AudioObjectSetPropertyData(devID, &address, 0, NULL, size, &rate); 213 | if (status != noErr) { 214 | rb_raise(rb_eArgError, 215 | "coreaudio: set nominal sample rates failed: %d", status); 216 | } 217 | return sampleRateVal; 218 | } 219 | 220 | static VALUE 221 | ca_get_device_nominal_sample_rate(AudioDeviceID devID) 222 | { 223 | AudioObjectPropertyAddress address = PropertyAddress; 224 | UInt32 size; 225 | Float64 rate; 226 | OSStatus status; 227 | 228 | address.mSelector = kAudioDevicePropertyNominalSampleRate; 229 | status = AudioObjectGetPropertyDataSize(devID, &address, 0, NULL, &size); 230 | 231 | if (status != noErr) { 232 | rb_raise(rb_eArgError, 233 | "coreaudio: get nominal sample rates size failed: %d", status); 234 | } 235 | 236 | status = AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &rate); 237 | if (status != noErr) { 238 | rb_raise(rb_eArgError, 239 | "coreaudio: get nominal sample rates failed: %d", status); 240 | } 241 | return DBL2NUM((double)rate); 242 | } 243 | 244 | static VALUE 245 | ca_get_device_actual_sample_rate(VALUE self) 246 | { 247 | AudioDeviceID devID = NUM2UINT(rb_ivar_get(self, sym_iv_devid)); 248 | AudioObjectPropertyAddress address = PropertyAddress; 249 | UInt32 size; 250 | Float64 rate; 251 | OSStatus status; 252 | 253 | address.mSelector = kAudioDevicePropertyActualSampleRate; 254 | status = AudioObjectGetPropertyDataSize(devID, &address, 0, NULL, &size); 255 | 256 | size = sizeof(rate); 257 | status = AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &rate); 258 | if (status != noErr) { 259 | rb_raise(rb_eArgError, 260 | "coreaudio: get actual sample rates failed: %d", status); 261 | } 262 | return DBL2NUM((double)rate); 263 | } 264 | 265 | static VALUE 266 | ca_device_initialize(VALUE self, VALUE devIdVal, VALUE options) 267 | { 268 | AudioDeviceID devID = (AudioDeviceID)NUM2LONG(devIdVal); 269 | VALUE device_name; 270 | VALUE available_sample_rate; 271 | VALUE nominal_rate; 272 | VALUE input_stream, output_stream; 273 | VALUE option_nominal_rate; 274 | 275 | device_name = ca_get_device_name(devID); 276 | available_sample_rate = ca_get_device_available_sample_rate(devID); 277 | rb_obj_freeze(available_sample_rate); 278 | 279 | if (options != Qnil) { 280 | option_nominal_rate = rb_funcall(options, rb_intern("[]"), 1, ID2SYM(rb_intern("nominal_rate"))); 281 | if (option_nominal_rate != Qnil) { 282 | nominal_rate = ca_set_device_nominal_sample_rate(devID, option_nominal_rate); 283 | } 284 | } else { 285 | nominal_rate = ca_get_device_nominal_sample_rate(devID); 286 | } 287 | 288 | input_stream = ca_stream_new(devIdVal, Qtrue); 289 | output_stream = ca_stream_new(devIdVal, Qfalse); 290 | 291 | rb_ivar_set(self, sym_iv_devid, devIdVal); 292 | rb_ivar_set(self, sym_iv_name, device_name); 293 | rb_ivar_set(self, sym_iv_available_sample_rate, available_sample_rate); 294 | rb_ivar_set(self, sym_iv_nominal_rate, nominal_rate); 295 | rb_ivar_set(self, sym_iv_input_stream, input_stream); 296 | rb_ivar_set(self, sym_iv_output_stream, output_stream); 297 | 298 | return self; 299 | } 300 | 301 | static VALUE 302 | ca_device_new(AudioDeviceID devid, VALUE options) 303 | { 304 | VALUE devIdVal = UINT2NUM(devid); 305 | VALUE device; 306 | 307 | device = rb_obj_alloc(rb_cAudioDevice); 308 | ca_device_initialize(device, devIdVal, options); 309 | 310 | return device; 311 | } 312 | 313 | /* 314 | * Document-method: CoreAudio.devices 315 | * call-seq: 316 | * CoreAudio.devices(options = nil) 317 | * 318 | * Get available all audio devices (CoreAudio::AudioDevice object). 319 | * if options[:nominal_rate] is given, set device nominal rate as given one. 320 | */ 321 | static VALUE 322 | ca_devices(int argc, VALUE *argv, VALUE mod) 323 | { 324 | AudioObjectPropertyAddress address = PropertyAddress; 325 | AudioDeviceID *devIDs = NULL; 326 | UInt32 size = 0, devnum = 0; 327 | OSStatus status = noErr; 328 | VALUE ary; 329 | UInt32 i; 330 | VALUE options; 331 | 332 | rb_scan_args(argc, argv, "01", &options); 333 | 334 | address.mSelector = kAudioHardwarePropertyDevices; 335 | 336 | status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, 337 | &address, 0, NULL, &size); 338 | if (status != noErr) 339 | rb_raise(rb_eRuntimeError, "coreaudio: get devices size failed: %d", status); 340 | 341 | devnum = size / (UInt32)sizeof(AudioDeviceID); 342 | devIDs = ALLOCA_N(AudioDeviceID, devnum); 343 | 344 | status = AudioObjectGetPropertyData(kAudioObjectSystemObject, 345 | &address, 0, NULL, &size, devIDs); 346 | if (status != noErr) 347 | rb_raise(rb_eRuntimeError, "coreaudio: get devices failed: %d", status); 348 | 349 | ary = rb_ary_new(); 350 | for (i = 0; i < devnum; i++) { 351 | rb_ary_push(ary, ca_device_new(devIDs[i], options)); 352 | } 353 | return ary; 354 | } 355 | 356 | /* 357 | * Document-method: CoreAudio.default_input_device 358 | * call-seq: 359 | * CoreAudio.default_input_device(options = nil) 360 | * 361 | * Get system default audio input device as CoreAudio::AudioDevice object. 362 | * if options[:nominal_rate] is given, set device nominal rate as given one. 363 | */ 364 | static VALUE 365 | ca_default_input_device(int argc, VALUE *argv, VALUE mod) 366 | { 367 | AudioDeviceID devID; 368 | AudioObjectPropertyAddress address = PropertyAddress; 369 | UInt32 size; 370 | OSStatus status; 371 | VALUE options; 372 | 373 | rb_scan_args(argc, argv, "01", &options); 374 | 375 | address.mSelector = kAudioHardwarePropertyDefaultInputDevice; 376 | size = sizeof(devID); 377 | 378 | status = AudioObjectGetPropertyData(kAudioObjectSystemObject, 379 | &address, 0, NULL, &size, &devID); 380 | 381 | if (status != noErr) 382 | rb_raise(rb_eArgError, "coreaudio: get default input device failed: %d", status); 383 | 384 | return ca_device_new(devID, options); 385 | } 386 | 387 | 388 | /* 389 | * Document-method: CoreAudio.default_output_device 390 | * call-seq: 391 | * CoreAudio.default_output_device(options = nil) 392 | * 393 | * Get system default audio output device as CoreAudio::AudioDevice object. 394 | * if options[:nominal_rate] is given, set device nominal rate as given one. 395 | */ 396 | static VALUE 397 | ca_default_output_device(int argc, VALUE *argv, VALUE mod) 398 | { 399 | AudioDeviceID devID; 400 | AudioObjectPropertyAddress address = PropertyAddress; 401 | UInt32 size; 402 | OSStatus status; 403 | VALUE options; 404 | 405 | rb_scan_args(argc, argv, "01", &options); 406 | 407 | address.mSelector = kAudioHardwarePropertyDefaultOutputDevice; 408 | size = sizeof(devID); 409 | 410 | status = AudioObjectGetPropertyData(kAudioObjectSystemObject, 411 | &address, 0, NULL, &size, &devID); 412 | 413 | if (status != noErr) 414 | rb_raise(rb_eArgError, "coreaudio: get default output device failed: %d", status); 415 | 416 | return ca_device_new(devID, options); 417 | } 418 | 419 | /* 420 | * Document-method: CoreAudio.set_default_output_device 421 | * call-seq: 422 | * CoreAudio.set_default_output_device(audio_device) 423 | * 424 | * Set system default audio output device as CoreAudio::AudioDevice object. 425 | */ 426 | static VALUE 427 | ca_set_default_output_device(VALUE self, VALUE device) 428 | { 429 | AudioDeviceID devID = NUM2UINT(rb_ivar_get(device, sym_iv_devid)); 430 | AudioObjectPropertyAddress address = PropertyAddress; 431 | UInt32 size; 432 | OSStatus status; 433 | 434 | address.mSelector = kAudioHardwarePropertyDefaultOutputDevice; 435 | address.mScope = kAudioObjectPropertyScopeGlobal; 436 | address.mElement = kAudioObjectPropertyElementMaster; 437 | 438 | status = AudioObjectSetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, sizeof(devID), &devID); 439 | if (status != noErr) { 440 | rb_raise(rb_eArgError, 441 | "coreaudio: set default output deveice failed: %d", status); 442 | } 443 | 444 | return Qtrue; 445 | } 446 | 447 | /* 448 | * Document-class: CoreAudio::OutLoop 449 | * 450 | * CoreAudio::OutLoop is an class for loop waveform to output audio stream. 451 | */ 452 | typedef struct { 453 | AudioDeviceID devID; 454 | AudioDeviceIOProcID procID; 455 | UInt32 frame; 456 | UInt32 channels; 457 | short *buf; 458 | } ca_out_loop_data; 459 | 460 | static void 461 | ca_out_loop_data_free(void *ptr) 462 | { 463 | if (ptr) { 464 | ca_out_loop_data *data = ptr; 465 | if (data->procID) 466 | AudioDeviceDestroyIOProcID(data->devID, data->procID); 467 | if (data->buf) 468 | free(data->buf); 469 | free(ptr); 470 | } 471 | } 472 | 473 | static size_t 474 | ca_out_loop_data_memsize(const void *ptr) 475 | { 476 | const ca_out_loop_data *data = ptr; 477 | return sizeof(ca_out_loop_data) + data->channels * data->frame * sizeof(short); 478 | } 479 | 480 | static const rb_data_type_t ca_out_loop_data_type = { 481 | "ca_out_loop_data", 482 | {NULL, ca_out_loop_data_free, ca_out_loop_data_memsize}, 483 | }; 484 | 485 | static OSStatus 486 | ca_out_loop_proc( 487 | AudioDeviceID inDevice, 488 | const AudioTimeStamp* inNow, 489 | const AudioBufferList* inInputData, 490 | const AudioTimeStamp* inInputTime, 491 | AudioBufferList* outOutputData, 492 | const AudioTimeStamp* inOutputTime, 493 | void* inClientData) 494 | { 495 | NSUInteger i; 496 | UInt32 buffers = outOutputData->mNumberBuffers; 497 | ca_out_loop_data *loop_data = inClientData; 498 | UInt32 channels = loop_data->channels; 499 | 500 | for (i = 0; i < buffers; i++) { 501 | float *ptr = outOutputData->mBuffers[i].mData; 502 | UInt32 size = outOutputData->mBuffers[i].mDataByteSize / (UInt32)sizeof(float) / channels; 503 | UInt32 offset = (UInt32)inOutputTime->mSampleTime % loop_data->frame; 504 | UInt32 copied = 0; 505 | 506 | if (outOutputData->mBuffers[i].mNumberChannels != channels) { 507 | memset(ptr, 0, size * channels * sizeof(float)); 508 | continue; 509 | } 510 | 511 | while ( copied < size ) { 512 | UInt32 len = loop_data->frame - offset; 513 | UInt32 j; 514 | if ( len > size - copied ) 515 | len = size - copied; 516 | for (j = 0; j < len*channels; j++) { 517 | ptr[copied*channels+j] = SHORT2FLOAT(loop_data->buf[offset*channels+j]); 518 | } 519 | offset = (offset + len) % loop_data->frame; 520 | copied += len; 521 | } 522 | } 523 | 524 | return 0; 525 | } 526 | 527 | static VALUE 528 | ca_out_loop_data_alloc(VALUE klass) 529 | { 530 | VALUE obj; 531 | ca_out_loop_data *ptr; 532 | 533 | obj = TypedData_Make_Struct(klass, ca_out_loop_data, &ca_out_loop_data_type, ptr); 534 | return obj; 535 | } 536 | 537 | static VALUE 538 | ca_out_loop_data_initialize(VALUE self, VALUE devID, VALUE frame, VALUE channels) 539 | { 540 | ca_out_loop_data *data; 541 | OSStatus status; 542 | 543 | TypedData_Get_Struct(self, ca_out_loop_data, &ca_out_loop_data_type, data); 544 | data->devID = NUM2UINT(devID); 545 | status = AudioDeviceCreateIOProcID(data->devID, ca_out_loop_proc, data, &data->procID); 546 | if ( status != noErr ) 547 | { 548 | rb_raise(rb_eRuntimeError, "coreaudio: create proc ID fail: %d", status); 549 | } 550 | data->frame = NUM2UINT(frame); 551 | data->channels = NUM2UINT(channels); 552 | data->buf = malloc(sizeof(short)*data->frame*data->channels); 553 | if (data->buf == NULL) 554 | rb_raise(rb_eNoMemError, "coreaudio: fail to alloc out loop data buffer"); 555 | return self; 556 | } 557 | 558 | /* 559 | * Document-method: CoreAudio::AudioDevice#out_loop 560 | * call-seq: 561 | * device.out_loop(frame) 562 | * 563 | * Create output audio loop buffer. 564 | * 565 | * == Parameters 566 | * * +frame+ is an integer value indicate loop buffer size in number of 567 | * sample/frame. The number of channel is considered automatically. 568 | */ 569 | static VALUE 570 | ca_device_create_out_loop_proc(VALUE self, VALUE frame) 571 | { 572 | VALUE proc; 573 | VALUE out_stream = rb_ivar_get(self, sym_iv_output_stream); 574 | 575 | proc = ca_out_loop_data_alloc(rb_cOutLoop); 576 | ca_out_loop_data_initialize(proc, rb_ivar_get(self, sym_iv_devid), frame, 577 | rb_ivar_get(out_stream, sym_iv_channels)); 578 | return proc; 579 | } 580 | 581 | /* 582 | * call-seq: 583 | * outloop.start 584 | * 585 | * Start play of output audio loop. 586 | */ 587 | static VALUE 588 | ca_out_loop_data_start(VALUE self) 589 | { 590 | ca_out_loop_data *data; 591 | OSStatus status; 592 | 593 | TypedData_Get_Struct(self, ca_out_loop_data, &ca_out_loop_data_type, data); 594 | 595 | status = AudioDeviceStart(data->devID, data->procID); 596 | if ( status != noErr ) 597 | { 598 | rb_raise(rb_eRuntimeError, "coreaudio: audio device start fail: %d", status); 599 | } 600 | return self; 601 | } 602 | 603 | /* 604 | * call-seq: 605 | * outloop.stop 606 | * 607 | * Stop play of output audio loop. 608 | */ 609 | static VALUE 610 | ca_out_loop_data_stop(VALUE self) 611 | { 612 | ca_out_loop_data *data; 613 | OSStatus status; 614 | 615 | TypedData_Get_Struct(self, ca_out_loop_data, &ca_out_loop_data_type, data); 616 | 617 | status = AudioDeviceStop(data->devID, data->procID); 618 | if ( status != noErr ) 619 | { 620 | rb_raise(rb_eRuntimeError, "coreaudio: audio device stop fail: %d", status); 621 | } 622 | return self; 623 | } 624 | 625 | /* 626 | * call-seq: 627 | * outloop[frame] = sample 628 | * outloop[frame] = [sample1, sample2] 629 | * 630 | * Assign audio loop buffer frame value. 631 | * If assigned value is an Fixnum (-32767..32767, signed 16bit), 632 | * the value is stored to all channels. 633 | * The +sample+ should be normalize to -32767 <= sample <= 32767 range. 634 | * If assigned value is an Array of Fixnum, each value is stored to each 635 | * correponding channel. If size of array is not equal to the AudioDevice's 636 | * output stream channel number, raise ArgumentError. 637 | */ 638 | static VALUE 639 | ca_out_loop_data_assign(VALUE self, VALUE index, VALUE val) 640 | { 641 | ca_out_loop_data *data; 642 | size_t idx; 643 | UInt32 i; 644 | 645 | TypedData_Get_Struct(self, ca_out_loop_data, &ca_out_loop_data_type, data); 646 | 647 | idx = NUM2UINT(index) % data->frame; 648 | if (TYPE(val) == T_ARRAY) { 649 | if (RARRAY_LEN(val) != data->channels) { 650 | rb_raise(rb_eArgError, "size of array and channel size mismatch"); 651 | } 652 | for (i = 0; i < data->channels; i++) { 653 | data->buf[idx*data->channels+i] = (short)NUM2INT(RARRAY_PTR(val)[i]); 654 | } 655 | } else { 656 | for (i = 0; i < data->channels; i++) { 657 | data->buf[idx*data->channels+i] = (short)NUM2INT(val); 658 | } 659 | } 660 | return val; 661 | } 662 | 663 | typedef struct { 664 | AudioDeviceID devID; 665 | AudioDeviceIOProcID procID; 666 | UInt32 frame; 667 | UInt32 channels; 668 | short *buf; 669 | UInt32 start; 670 | UInt32 end; 671 | long dropped_frame; 672 | pthread_mutex_t mutex; 673 | pthread_cond_t cond; 674 | } ca_buffer_data; 675 | 676 | static void 677 | ca_buffer_data_free(void *ptr) 678 | { 679 | if (ptr) { 680 | ca_buffer_data *data = ptr; 681 | if (data->procID) 682 | AudioDeviceDestroyIOProcID(data->devID, data->procID); 683 | pthread_cond_destroy(&data->cond); 684 | pthread_mutex_destroy(&data->mutex); 685 | if (data->buf) 686 | free(data->buf); 687 | free(ptr); 688 | } 689 | } 690 | 691 | static size_t 692 | ca_buffer_data_memsize(const void *ptr) 693 | { 694 | const ca_buffer_data *data = ptr; 695 | return sizeof(ca_buffer_data) + data->channels * data->frame * sizeof(short); 696 | } 697 | 698 | static const rb_data_type_t ca_buffer_data_type = { 699 | "ca_buffer_data", 700 | {NULL, ca_buffer_data_free, ca_buffer_data_memsize}, 701 | }; 702 | 703 | static VALUE 704 | ca_buffer_data_alloc(VALUE klass) 705 | { 706 | VALUE obj; 707 | ca_buffer_data *ptr; 708 | 709 | obj = TypedData_Make_Struct(klass, ca_buffer_data, &ca_buffer_data_type, ptr); 710 | pthread_mutex_init(&ptr->mutex, NULL); 711 | pthread_cond_init(&ptr->cond, NULL); 712 | return obj; 713 | } 714 | 715 | static VALUE 716 | ca_buffer_data_start(VALUE self) 717 | { 718 | ca_buffer_data *data; 719 | OSStatus status; 720 | 721 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 722 | 723 | data->dropped_frame = 0; 724 | status = AudioDeviceStart(data->devID, data->procID); 725 | if ( status != noErr ) 726 | { 727 | rb_raise(rb_eRuntimeError, "coreaudio: audio device start fail: %d", status); 728 | } 729 | return self; 730 | } 731 | 732 | static VALUE 733 | ca_buffer_data_stop(VALUE self) 734 | { 735 | ca_buffer_data *data; 736 | OSStatus status; 737 | 738 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 739 | 740 | status = AudioDeviceStop(data->devID, data->procID); 741 | if ( status != noErr ) 742 | { 743 | rb_raise(rb_eRuntimeError, "coreaudio: audio device stop fail: %d", status); 744 | } 745 | return self; 746 | } 747 | 748 | static VALUE 749 | ca_buffer_wait(void *ptr) 750 | { 751 | ca_buffer_data *data = ptr; 752 | int ret; 753 | 754 | ret = pthread_cond_wait(&data->cond, &data->mutex); 755 | 756 | return (VALUE)ret; 757 | } 758 | 759 | #if 0 760 | /* use pthread_mutex_lock in unblocking function cause deadlock. 761 | * because calling thread have GVL lock and interrupted thread could 762 | * be waiting mutex lock for GVL. 763 | * So use RUBY_UBF_IO for unblocking function. 764 | * Although pthread_cond_wait() shouldn't return EINTR acoording to POSIX, 765 | * on Mac OS X pthread_cond_wait() actually returns when received signals. */ 766 | static void 767 | ca_buffer_unblocking_func(void *ptr) 768 | { 769 | ca_buffer_data *data = ptr; 770 | 771 | pthread_mutex_lock(&data->mutex); 772 | pthread_cond_broadcast(&data->cond); 773 | pthread_mutex_unlock(&data->mutex); 774 | } 775 | #endif 776 | 777 | static VALUE 778 | ca_buffer_wait_blocking(VALUE value) 779 | { 780 | void *ptr = (void *)value; 781 | #if 0 782 | return rb_thread_call_without_gvl(ca_buffer_wait, ptr, 783 | ca_buffer_unblocking_func, ptr); 784 | #endif 785 | return rb_thread_call_without_gvl(ca_buffer_wait, ptr, 786 | RUBY_UBF_IO, NULL); 787 | } 788 | 789 | static size_t 790 | buffer_space(ca_buffer_data *data) 791 | { 792 | UInt32 nxt; 793 | size_t space; 794 | 795 | pthread_mutex_lock(&data->mutex); 796 | nxt = (data->end+1) % data->frame; 797 | if ( nxt <= data->start ) 798 | space = (size_t)(data->start - nxt); 799 | else 800 | space = (size_t)(data->start + data->frame - nxt); 801 | pthread_mutex_unlock(&data->mutex); 802 | return space; 803 | } 804 | 805 | static VALUE 806 | ca_buffer_space(VALUE self) 807 | { 808 | ca_buffer_data *data; 809 | size_t space; 810 | 811 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 812 | 813 | space = buffer_space(data); 814 | return SIZET2NUM(space); 815 | } 816 | 817 | static VALUE 818 | ca_buffer_dropped_frame(VALUE self) 819 | { 820 | ca_buffer_data *data; 821 | long dropped; 822 | 823 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 824 | 825 | pthread_mutex_lock(&data->mutex); 826 | dropped = data->dropped_frame; 827 | pthread_mutex_unlock(&data->mutex); 828 | return LONG2NUM(dropped); 829 | } 830 | 831 | static VALUE 832 | ca_buffer_reset_dropped_frame(VALUE self) 833 | { 834 | ca_buffer_data *data; 835 | long dropped; 836 | 837 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 838 | 839 | pthread_mutex_lock(&data->mutex); 840 | dropped = data->dropped_frame; 841 | data->dropped_frame = 0; 842 | pthread_mutex_unlock(&data->mutex); 843 | return LONG2NUM(dropped); 844 | } 845 | 846 | /* 847 | * Document-class: CoreAudio::OutputBuffer 848 | * 849 | * CoreAudio::OutputBuffer is a class for stream waveform to output audio stream. 850 | */ 851 | static OSStatus 852 | ca_out_buffer_proc( 853 | AudioDeviceID inDevice, 854 | const AudioTimeStamp* inNow, 855 | const AudioBufferList* inInputData, 856 | const AudioTimeStamp* inInputTime, 857 | AudioBufferList* outOutputData, 858 | const AudioTimeStamp* inOutputTime, 859 | void* inClientData) 860 | { 861 | NSUInteger n_buf; 862 | UInt32 buffers = outOutputData->mNumberBuffers; 863 | ca_buffer_data *buffer_data = inClientData; 864 | UInt32 channels = buffer_data->channels; 865 | 866 | for (n_buf = 0; n_buf < buffers; n_buf++) { 867 | float *ptr = outOutputData->mBuffers[n_buf].mData; 868 | UInt32 size = outOutputData->mBuffers[n_buf].mDataByteSize / (UInt32)sizeof(float) / channels; 869 | UInt32 copied = 0; 870 | UInt32 i; 871 | 872 | if (outOutputData->mBuffers[n_buf].mNumberChannels != channels) { 873 | memset(ptr, 0, size * channels * sizeof(float)); 874 | continue; 875 | } 876 | 877 | pthread_mutex_lock(&buffer_data->mutex); 878 | for ( copied = 0, i = buffer_data->start; copied < size && i != buffer_data->end; copied++, i = (i+1) % buffer_data->frame ) { 879 | UInt32 ch; 880 | for (ch = 0; ch < channels; ch++) { 881 | ptr[copied*channels+ch] = SHORT2FLOAT(buffer_data->buf[i*channels+ch]); 882 | } 883 | } 884 | buffer_data->start = i; 885 | pthread_cond_broadcast(&buffer_data->cond); 886 | pthread_mutex_unlock(&buffer_data->mutex); 887 | if ( copied < size ) { 888 | memset(ptr+(copied*channels), 0, sizeof(float)*channels*(size-copied)); 889 | buffer_data->dropped_frame += size - copied; 890 | } 891 | } 892 | 893 | return 0; 894 | } 895 | 896 | static VALUE 897 | ca_out_buffer_data_initialize(VALUE self, VALUE devID, VALUE frame, VALUE channels) 898 | { 899 | ca_buffer_data *data; 900 | OSStatus status; 901 | 902 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 903 | data->devID = NUM2UINT(devID); 904 | status = AudioDeviceCreateIOProcID(data->devID, ca_out_buffer_proc, data, &data->procID); 905 | if ( status != noErr ) 906 | { 907 | rb_raise(rb_eRuntimeError, "coreaudio: create proc ID fail: %d", status); 908 | } 909 | data->frame = NUM2UINT(frame); 910 | data->channels = NUM2UINT(channels); 911 | data->buf = malloc(sizeof(short)*data->frame*data->channels); 912 | if (data->buf == NULL) 913 | rb_raise(rb_eNoMemError, "coreaudio: fail to alloc out buffer data buffer"); 914 | return self; 915 | } 916 | 917 | static VALUE 918 | ca_device_create_out_buffer_proc(VALUE self, VALUE frame) 919 | { 920 | VALUE proc; 921 | VALUE out_stream = rb_ivar_get(self, sym_iv_output_stream); 922 | 923 | proc = ca_buffer_data_alloc(rb_cOutputBuffer); 924 | ca_out_buffer_data_initialize(proc, rb_ivar_get(self, sym_iv_devid), frame, 925 | rb_ivar_get(out_stream, sym_iv_channels)); 926 | return proc; 927 | } 928 | 929 | static VALUE 930 | ca_out_buffer_data_append(VALUE self, VALUE nary) 931 | { 932 | ca_buffer_data *data; 933 | int rank; 934 | short *buf; 935 | UInt32 frames; 936 | UInt32 idx; 937 | long i; 938 | UInt32 j; 939 | 940 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 941 | 942 | nary = na_cast_object(nary, NA_SINT); 943 | rank = NA_RANK(nary); 944 | if (rank == 1) { 945 | frames = NA_SHAPE0(nary); 946 | } else if (rank == 2) { 947 | frames = NA_SHAPE1(nary); 948 | if (NA_SHAPE0(nary) != (int)data->channels) 949 | rb_raise(rb_eArgError, 950 | "coreaudio: audio buffer NArray size of first dim. must be " 951 | "number of channels"); 952 | } else { 953 | rb_raise(rb_eArgError, 954 | "coreaudio: audio buffer NArray rank must be 1 or 2"); 955 | } 956 | buf = NA_PTR_TYPE(nary, short *); 957 | pthread_mutex_lock(&data->mutex); 958 | idx = (data->end + 1) % data->frame; 959 | for ( i = 0; i < frames; i++, idx = (idx+1)%data->frame) { 960 | while ( idx == data->start ) { 961 | int ret, state; 962 | data->end = (idx == 0) ? data->frame-1 : idx - 1; 963 | ret = (int)rb_protect(ca_buffer_wait_blocking, (VALUE)data, &state); 964 | if (state) { 965 | pthread_mutex_unlock(&data->mutex); 966 | rb_jump_tag(state); 967 | } 968 | switch(ret) { 969 | case 0: 970 | case EINTR: 971 | case EAGAIN: 972 | break; 973 | default: 974 | pthread_mutex_unlock(&data->mutex); 975 | rb_sys_fail("pthread_cond_wait"); 976 | break; 977 | } 978 | } 979 | 980 | if (rank == 2) { 981 | memcpy(data->buf + idx * data->channels, 982 | buf + i * data->channels, 983 | sizeof(short) * data->channels); 984 | } else { 985 | for (j = 0; j < data->channels; j++) { 986 | data->buf[idx*data->channels+j] = buf[i]; 987 | } 988 | } 989 | data->end = idx; 990 | } 991 | pthread_mutex_unlock(&data->mutex); 992 | return self; 993 | } 994 | 995 | /* 996 | * Document-class: CoreAudio::InputBuffer 997 | * 998 | * CoreAudio::InputBuffer is a class for stream waveform to input audio stream. 999 | */ 1000 | static OSStatus 1001 | ca_in_buffer_proc( 1002 | AudioDeviceID inDevice, 1003 | const AudioTimeStamp* inNow, 1004 | const AudioBufferList* inInputData, 1005 | const AudioTimeStamp* inInputTime, 1006 | AudioBufferList* outOutputData, 1007 | const AudioTimeStamp* inOutputTime, 1008 | void* inClientData) 1009 | { 1010 | NSUInteger n_buf; 1011 | UInt32 buffers = inInputData->mNumberBuffers; 1012 | ca_buffer_data *buffer_data = inClientData; 1013 | UInt32 channels = buffer_data->channels; 1014 | 1015 | for (n_buf = 0; n_buf < buffers; n_buf++) { 1016 | float *ptr = inInputData->mBuffers[n_buf].mData; 1017 | UInt32 size = inInputData->mBuffers[n_buf].mDataByteSize / (UInt32)sizeof(float) / channels; 1018 | UInt32 copied, idx; 1019 | 1020 | if (inInputData->mBuffers[n_buf].mNumberChannels != channels) { 1021 | continue; 1022 | } 1023 | 1024 | pthread_mutex_lock(&buffer_data->mutex); 1025 | 1026 | copied = 0; 1027 | for (idx = buffer_data->end; 1028 | copied < size && (idx+1) % buffer_data->frame != buffer_data->start; 1029 | copied++, idx = (idx+1) % buffer_data->frame) { 1030 | UInt32 ch; 1031 | for (ch = 0; ch < channels; ch++) { 1032 | buffer_data->buf[idx*channels+ch] = FLOAT2SHORT(ptr[copied*channels+ch]); 1033 | } 1034 | } 1035 | buffer_data->end = idx; 1036 | buffer_data->dropped_frame += size - copied; 1037 | 1038 | pthread_cond_broadcast(&buffer_data->cond); 1039 | pthread_mutex_unlock(&buffer_data->mutex); 1040 | } 1041 | 1042 | return 0; 1043 | } 1044 | 1045 | static VALUE 1046 | ca_in_buffer_data_initialize(VALUE self, VALUE devID, VALUE frame, VALUE channels) 1047 | { 1048 | ca_buffer_data *data; 1049 | OSStatus status; 1050 | 1051 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 1052 | data->devID = NUM2UINT(devID); 1053 | status = AudioDeviceCreateIOProcID(data->devID, ca_in_buffer_proc, data, &data->procID); 1054 | if ( status != noErr ) 1055 | { 1056 | rb_raise(rb_eRuntimeError, "coreaudio: create proc ID fail: %d", status); 1057 | } 1058 | data->frame = NUM2UINT(frame); 1059 | data->channels = NUM2UINT(channels); 1060 | data->buf = malloc(sizeof(short)*data->frame*data->channels); 1061 | if (data->buf == NULL) 1062 | rb_raise(rb_eNoMemError, "coreaudio: fail to alloc input buffer data buffer"); 1063 | return self; 1064 | } 1065 | 1066 | static VALUE 1067 | ca_device_create_in_buffer_proc(VALUE self, VALUE frame) 1068 | { 1069 | VALUE proc; 1070 | VALUE in_stream = rb_ivar_get(self, sym_iv_input_stream); 1071 | 1072 | proc = ca_buffer_data_alloc(rb_cInputBuffer); 1073 | ca_in_buffer_data_initialize(proc, rb_ivar_get(self, sym_iv_devid), frame, 1074 | rb_ivar_get(in_stream, sym_iv_channels)); 1075 | return proc; 1076 | } 1077 | 1078 | static VALUE 1079 | ca_in_buffer_data_read(VALUE self, VALUE num) 1080 | { 1081 | ca_buffer_data *data; 1082 | UInt32 frame = NUM2UINT(num); 1083 | VALUE nary; 1084 | int shape[2]; 1085 | short *buf; 1086 | UInt32 i; 1087 | 1088 | TypedData_Get_Struct(self, ca_buffer_data, &ca_buffer_data_type, data); 1089 | 1090 | shape[0] = data->channels; 1091 | shape[1] = frame; 1092 | nary = na_make_object(NA_SINT, 2, shape, cNArray); 1093 | buf = NA_PTR_TYPE(nary, short *); 1094 | 1095 | pthread_mutex_lock(&data->mutex); 1096 | for ( i = 0; i < frame; i++, data->start = (data->start+1)%data->frame) { 1097 | while (data->start == data->end) { 1098 | int ret, state; 1099 | ret = (int)rb_protect(ca_buffer_wait_blocking, (VALUE)data, &state); 1100 | if (state) { 1101 | pthread_mutex_unlock(&data->mutex); 1102 | rb_jump_tag(state); 1103 | } 1104 | switch(ret) { 1105 | case 0: 1106 | case EINTR: 1107 | case EAGAIN: 1108 | break; 1109 | default: 1110 | pthread_mutex_unlock(&data->mutex); 1111 | rb_sys_fail("pthread_cond_wait"); 1112 | break; 1113 | } 1114 | } 1115 | memcpy(buf + i * data->channels, data->buf + data->start*data->channels, 1116 | sizeof(short) * data->channels); 1117 | } 1118 | pthread_mutex_unlock(&data->mutex); 1119 | return nary; 1120 | } 1121 | 1122 | void 1123 | Init_coreaudio_ext(void) 1124 | { 1125 | sym_iv_devid = rb_intern("@devid"); 1126 | sym_iv_name = rb_intern("@name"); 1127 | sym_iv_available_sample_rate = rb_intern("@available_sample_rate"); 1128 | sym_iv_nominal_rate = rb_intern("@nominal_rate"); 1129 | sym_iv_input_stream = rb_intern("@input_stream"); 1130 | sym_iv_output_stream = rb_intern("@output_stream"); 1131 | sym_iv_channels = rb_intern("@channels"); 1132 | sym_iv_buffer_frame_size = rb_intern("@buffer_frame_size"); 1133 | 1134 | rb_mCoreAudio = rb_define_module("CoreAudio"); 1135 | rb_cAudioDevice = rb_define_class_under(rb_mCoreAudio, "AudioDevice", rb_cObject); 1136 | rb_cAudioStream = rb_define_class_under(rb_mCoreAudio, "AudioStream", rb_cObject); 1137 | rb_cOutLoop = rb_define_class_under(rb_mCoreAudio, "OutLoop", rb_cObject); 1138 | rb_cAudioBuffer = rb_define_class_under(rb_mCoreAudio, "AudioBuffer", rb_cObject); 1139 | rb_cOutputBuffer = rb_define_class_under(rb_mCoreAudio, "OutputBuffer", rb_cAudioBuffer); 1140 | rb_cInputBuffer = rb_define_class_under(rb_mCoreAudio, "InputBuffer", rb_cAudioBuffer); 1141 | 1142 | rb_define_method(rb_cAudioDevice, "initialize", ca_device_initialize, 2); 1143 | rb_define_method(rb_cAudioDevice, "actual_rate", ca_get_device_actual_sample_rate, 0); 1144 | rb_define_method(rb_cAudioDevice, "output_loop", ca_device_create_out_loop_proc, 1); 1145 | rb_define_method(rb_cAudioDevice, "output_buffer", ca_device_create_out_buffer_proc, 1); 1146 | rb_define_method(rb_cAudioDevice, "input_buffer", ca_device_create_in_buffer_proc, 1); 1147 | rb_define_attr(rb_cAudioDevice, "devid", 1, 0); 1148 | rb_define_attr(rb_cAudioDevice, "name", 1, 0); 1149 | rb_define_attr(rb_cAudioDevice, "available_sample_rate", 1, 0); 1150 | rb_define_attr(rb_cAudioDevice, "nominal_rate", 1, 0); 1151 | rb_define_attr(rb_cAudioDevice, "input_stream", 1, 0); 1152 | rb_define_attr(rb_cAudioDevice, "output_stream", 1, 0); 1153 | 1154 | rb_define_method(rb_cAudioStream, "initialize", ca_stream_initialize, 2); 1155 | rb_define_attr(rb_cAudioStream, "channels", 1, 0); 1156 | rb_define_attr(rb_cAudioStream, "buffer_frame_size", 1, 0); 1157 | 1158 | rb_define_singleton_method(rb_mCoreAudio, "devices", ca_devices, -1); 1159 | rb_define_singleton_method(rb_mCoreAudio, "default_input_device", ca_default_input_device, -1); 1160 | rb_define_singleton_method(rb_mCoreAudio, "default_output_device", ca_default_output_device, -1); 1161 | rb_define_singleton_method(rb_mCoreAudio, "set_default_output_device", ca_set_default_output_device, 1); 1162 | 1163 | rb_define_method(rb_cOutLoop, "[]=", ca_out_loop_data_assign, 2); 1164 | rb_define_method(rb_cOutLoop, "start", ca_out_loop_data_start, 0); 1165 | rb_define_method(rb_cOutLoop, "stop", ca_out_loop_data_stop, 0); 1166 | 1167 | rb_define_method(rb_cAudioBuffer, "start", ca_buffer_data_start, 0); 1168 | rb_define_method(rb_cAudioBuffer, "stop", ca_buffer_data_stop, 0); 1169 | rb_define_method(rb_cAudioBuffer, "dropped_frame", ca_buffer_dropped_frame, 0); 1170 | rb_define_method(rb_cAudioBuffer, "reset_dropped_frame", ca_buffer_reset_dropped_frame, 0); 1171 | rb_define_method(rb_cAudioBuffer, "space", ca_buffer_space, 0); 1172 | 1173 | rb_define_method(rb_cOutputBuffer, "<<", ca_out_buffer_data_append, 1); 1174 | 1175 | rb_define_method(rb_cInputBuffer, "read", ca_in_buffer_data_read, 1); 1176 | 1177 | Init_coreaudio_audiofile(); 1178 | } 1179 | -------------------------------------------------------------------------------- /ext/coreaudio/coreaudio_missing.c: -------------------------------------------------------------------------------- 1 | 2 | #include "ruby.h" 3 | #include "coreaudio.h" 4 | 5 | #ifndef HAVE_RB_ALLOC_TMP_BUFFER 6 | void * 7 | rb_alloc_tmp_buffer(volatile VALUE *store, long len) 8 | { 9 | VALUE s = rb_str_tmp_new(len); 10 | *store = s; 11 | return RSTRING_PTR(s); 12 | } 13 | #endif 14 | 15 | #ifndef HAVE_RB_FREE_TMP_BUFFER 16 | void 17 | rb_free_tmp_buffer(volatile VALUE *store) 18 | { 19 | VALUE s = *store; 20 | *store = 0; 21 | if (s) rb_str_clear(s); 22 | } 23 | #endif 24 | 25 | -------------------------------------------------------------------------------- /ext/coreaudio/depend: -------------------------------------------------------------------------------- 1 | coreaudio.o: coreaudio.m coreaudio.h 2 | audiofile.o: audiofile.m coreaudio.h 3 | -------------------------------------------------------------------------------- /ext/coreaudio/extconf.rb: -------------------------------------------------------------------------------- 1 | require 'mkmf' 2 | 3 | $CFLAGS << " " << "-ObjC" 4 | 5 | unless defined?(have_framework) 6 | def have_framework(fw, &b) 7 | checking_for fw do 8 | src = cpp_include("#{fw}/#{fw}.h") << "\n" "int main(void){return 0;}" 9 | if try_link(src, opt = "-ObjC -framework #{fw}", &b) 10 | $defs.push(format("-DHAVE_FRAMEWORK_%s", fw.tr_cpp)) 11 | $LDFLAGS << " " << opt 12 | true 13 | else 14 | false 15 | end 16 | end 17 | end 18 | end 19 | 20 | begin 21 | files = Gem.find_files("narray.h") 22 | if files.empty? 23 | narray_dir = $sitearchdir 24 | else 25 | narray_dir = File.dirname(files.first) 26 | end 27 | rescue 28 | narray_dir = $sitearchdir 29 | end 30 | dir_config("narray", narray_dir, narray_dir) 31 | 32 | if not(have_header("narray.h") and have_header("narray_config.h")) 33 | print <<-EOS 34 | ** configure error ** 35 | narray.h or narray_config.h is not found. 36 | If you have installed narray to /path/to/narray, try the following: 37 | 38 | % ruby extconf.rb --with-narray-dir=/path/to/narray 39 | 40 | or 41 | % gem install coreaudio -- --with-narray-dir=/path/to/narray 42 | 43 | EOS 44 | exit false 45 | end 46 | 47 | if have_framework("CoreAudio") and 48 | have_framework("AudioToolbox") and 49 | have_framework("CoreFoundation") and 50 | have_framework("Cocoa") 51 | 52 | # check ruby API 53 | have_func("rb_alloc_tmp_buffer", "ruby.h") 54 | have_func("rb_free_tmp_buffer", "ruby.h") 55 | 56 | create_header 57 | 58 | # create Makefile 59 | create_makefile("coreaudio/coreaudio_ext") 60 | 61 | # workaround for mkmf.rb in 1.9.2 62 | if RUBY_VERSION < "1.9.3" 63 | open("Makefile", "a") do |f| 64 | f.puts <<-EOS 65 | .m.o: 66 | $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) $(COUTFLAG)$@ -c $< 67 | EOS 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/coreaudio.rb: -------------------------------------------------------------------------------- 1 | require "narray" 2 | require "coreaudio/version" 3 | require "coreaudio/coreaudio_ext" 4 | require "coreaudio/audiofile" 5 | 6 | -------------------------------------------------------------------------------- /lib/coreaudio/audiofile.rb: -------------------------------------------------------------------------------- 1 | 2 | module CoreAudio 3 | class AudioFile 4 | def read(frames=nil) 5 | if frames 6 | frames = Integer(frames) 7 | if frames and frames > 0 8 | return read_frames(frames) 9 | elsif frames == 0 10 | return NArray.sint(0) 11 | else 12 | raise ArgumentError, 13 | "coreaudio: read frame number must be zero or positive" 14 | end 15 | end 16 | 17 | # read all frames 18 | chunk = self.inner_rate.to_i * 10 19 | total = nil 20 | loop do 21 | tmp = read_frames(chunk) 22 | if tmp.nil? 23 | break 24 | end 25 | if total.nil? 26 | total = tmp 27 | else 28 | new_na = NArray.sint(total.shape[0], tmp.shape[1] + total.shape[1]) 29 | new_na[false, 0...total.shape[1]] = total 30 | new_na[false, total.shape[1]..-1] = tmp 31 | total = new_na 32 | end 33 | end 34 | 35 | total 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/coreaudio/version.rb: -------------------------------------------------------------------------------- 1 | module CoreAudio 2 | VERSION = "0.0.12" 3 | end 4 | --------------------------------------------------------------------------------