├── .gitignore ├── LICENSE ├── README.md ├── binding.gyp ├── examples ├── prelude.coffee └── testtone.js ├── macaudio.cc └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .lock-wscript 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 nao yonamine 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-macaudio 2 | The interface is an AudioUnit(Mac) which can generate audio directly using JavaScript. 3 | 4 | ## Requirements 5 | - Mac OS X (AudioUnit) 6 | - [node.js](https://github.com/joyent/node) v0.6.x 7 | 8 | ## Install 9 | `node-macaudio` is available through npm: 10 | ```sh 11 | $ npm install macaudio 12 | ``` 13 | 14 | ## Usage 15 | ```javascript 16 | var macaudio = require("macaudio"); 17 | 18 | var bufferSize = 1024; // 512, 1024, 2048 or 4096 19 | var node = new macaudio.JavaScriptOutputNode(bufferSize); 20 | 21 | console.log("sampleRate:", node.sampleRate); 22 | console.log("channels :", node.channels); 23 | console.log("bufferSize:", node.bufferSize); 24 | 25 | var phase = 0; 26 | var phaseStep = 880 / node.sampleRate; 27 | 28 | node.onaudioprocess = function(e) { 29 | var L = e.getChannelData(0); 30 | var R = e.getChannelData(1); 31 | for (var i = 0; i < e.bufferSize; i++) { 32 | L[i] = R[i] = Math.sin(2 * Math.PI * phase); 33 | phase += phaseStep; 34 | } 35 | }; 36 | 37 | node.start(); 38 | setTimeout(function() { node.stop(); }, 1000); 39 | ``` 40 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "macaudio", 5 | "sources": [ 6 | "macaudio.cc" 7 | ], 8 | "link_settings": { 9 | "libraries": [ 10 | "-framework", 11 | "AudioUnit" 12 | ] 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /examples/prelude.coffee: -------------------------------------------------------------------------------- 1 | macaudio = require "macaudio" 2 | 3 | 4 | class SineWave 5 | constructor: (frequency, sampleRate=44100, bufferSize=1024)-> 6 | @frequency = frequency 7 | @sampleRate = sampleRate 8 | @phase = 0 9 | @phaseStep = frequency / sampleRate 10 | @buffer = new Float32Array(bufferSize) 11 | 12 | setFrequency: (frequency)-> 13 | @frequency = frequency 14 | @phaseStep = frequency / @sampleRate 15 | 16 | process: -> 17 | for i in [0...@buffer.length] 18 | @buffer[i] = Math.sin(2 * Math.PI * @phase) 19 | @phase += @phaseStep 20 | @buffer 21 | 22 | 23 | bufferSize = 1024 24 | node = new macaudio.JavaScriptOutputNode(bufferSize) 25 | sine = new SineWave(0, node.sampleRate, node.bufferSize) 26 | 27 | console.log "node.sampleRate: #{node.sampleRate}" 28 | console.log "node.channels : #{node.channels}" 29 | console.log "node.bufferSize: #{node.bufferSize}" 30 | 31 | node.onaudioprocess = (e)-> 32 | buffer = sine.process() 33 | amp = 0.25 34 | 35 | outL = e.getChannelData(0) 36 | outR = e.getChannelData(1) 37 | 38 | for i in [0...e.bufferSize] 39 | outL[i] = buffer[i] * amp 40 | outR[i] = buffer[i] * amp 41 | 42 | 43 | Prelude = """ 44 | C5 E5 G5 C6 E6 G5 C6 E6 C5 E5 G5 C6 E6 G5 C6 E6 C5 D5 A5 D6 F6 A5 D6 F6 C5 D5 A5 D6 F6 A5 D6 F6 45 | B4 D5 G5 D6 F6 G5 D6 F6 B4 D5 G5 D6 F6 G5 D6 F6 C5 E5 G5 C6 E6 G5 C6 E6 C5 E5 G5 C6 E6 G5 C6 E6 46 | C5 E5 A5 E6 A6 A5 E6 A6 C5 E5 A5 E6 A6 A5 E6 A6 C5 D5 F+5 A5 D6 F+5 A5 D6 C5 D5 F+5 A5 D6 F+5 A5 D6 47 | B4 D5 G5 D6 G6 G5 D6 G6 B4 D5 G5 D6 G6 G5 D6 G6 B4 C5 E5 G5 C6 E5 G5 C6 B4 C5 E5 G5 C6 E5 G5 C6 48 | A4 C5 E5 G5 C6 E5 G5 C6 A4 C5 E5 G5 C6 E5 G5 C6 D4 A4 D5 F+5 C6 D5 F+5 C6 D4 A4 D5 F+5 C6 D5 F+5 C6 49 | G4 B4 D5 G5 B5 D5 G5 B5 G4 B4 D5 G5 B5 D5 G5 B5 G4 B-4 E5 G5 C+6 E5 G5 C+6 G4 B-4 E5 G5 C+6 E5 G5 C+6 50 | F4 A4 D5 A5 D6 D5 A5 D6 F4 A4 D5 A5 D6 D5 A5 D6 F4 A-4 D5 F5 B5 D5 F5 B5 F4 A-4 D5 F5 B5 D5 F5 B5 51 | E4 G4 C5 G5 C6 C5 G5 C6 E4 G4 C5 G5 C6 C5 G5 C6 E4 F4 A4 C5 F5 A4 C5 F5 E4 F4 A4 C5 F5 A4 C5 F5 52 | D4 F4 A4 C5 F5 A4 C5 F5 D4 F4 A4 C5 F5 A4 C5 F5 G3 D4 G4 B4 F5 G4 B4 F5 G3 D4 G4 B4 F5 G4 B4 F5 53 | C4 E4 G4 C5 E5 G4 C5 E5 C4 E4 G4 C5 E5 G4 C5 E5 C4 G4 B-4 C5 E5 B-4 C5 E5 C4 G4 B-4 C5 E5 B-4 C5 E5 54 | F3 F4 A4 C5 E5 A4 C5 E5 F3 F4 A4 C5 E5 A4 C5 E5 F+3 C4 A4 C5 E-5 A4 C5 E-5 F+3 C4 A4 C5 E-5 A4 C5 E-5 55 | A-3 F4 B4 C5 D5 B4 C5 D5 A-3 F4 B4 C5 D5 B4 C5 D5 G3 F4 G4 B4 D5 G4 B4 D5 G3 F4 G4 B4 D5 G4 B4 D5 56 | G3 E4 G4 C5 E5 G4 C5 E5 G3 E4 G4 C5 E5 G4 C5 E5 G3 D4 G4 C5 F5 G4 C5 F5 G3 D4 G4 C5 F5 G4 C5 F5 57 | G3 D4 G4 B4 F5 G4 B4 F5 G3 D4 G4 B4 F5 G4 B4 F5 G3 E-4 A4 C5 F+5 A4 C5 F+5 G3 E-4 A4 C5 F+5 A4 C5 F+5 58 | G3 E4 G4 C5 G5 G4 C5 G5 G3 E4 G4 C5 G5 G4 C5 G5 G3 D4 G4 C5 F5 G4 C5 F5 G3 D4 G4 C5 F5 G4 C5 F5 59 | G3 D4 G4 B4 F5 G4 B4 F5 G3 D4 G4 B4 F5 G4 B4 F5 C3 C4 G4 B-4 E5 G4 B-4 E5 C3 C4 G4 B-4 E5 G4 B-4 E5 60 | C3 C4 F4 A4 C5 F5 C5 A4 C5 A4 F4 A4 F4 D4 F4 D4 C3 B3 G5 B5 D6 F6 D6 B5 D6 B5 G5 B5 D5 F5 E5 D5 61 | C5 C5 C5 C5 62 | """ 63 | 64 | mtof = (m)-> 65 | 440 * Math.pow(Math.pow(2, (1 / 12)), m - 69) 66 | 67 | atom = (a)-> 68 | if (m = /^([CDEFGAB])([-+]?)([0-9]?)$/.exec(a)) != null 69 | x = { C:0, D:2, E:4, F:5, G:7, A:9, B:11 }[m[1]] 70 | x += 12 * (m[3]|0) + { "-":-1, "":0, "+":+1 }[m[2]] 71 | else 0 72 | 73 | sequence = ( mtof(atom i) for i in Prelude.trim().split /\s+/ ) 74 | 75 | timerId = setInterval -> 76 | if sequence.length 77 | sine.setFrequency sequence.shift() * 2 78 | else 79 | node.stop() 80 | clearInterval timerId 81 | , 180 82 | 83 | node.start() 84 | -------------------------------------------------------------------------------- /examples/testtone.js: -------------------------------------------------------------------------------- 1 | var macaudio = require("../build/Release/macaudio"); 2 | 3 | var bufferSize = 1024; // 512 or 1024 or 2048 or 4096 4 | var node = new macaudio.JavaScriptOutputNode(bufferSize); 5 | 6 | console.log("sampleRate:", node.sampleRate); 7 | console.log("channels :", node.channels); 8 | console.log("bufferSize:", node.bufferSize); 9 | 10 | var phase = 0; 11 | var phaseStep = 880 / node.sampleRate; 12 | 13 | node.onaudioprocess = function(e) { 14 | var L = e.getChannelData(0); 15 | var R = e.getChannelData(1); 16 | for (var i = 0; i < e.bufferSize; i++) { 17 | L[i] = R[i] = Math.sin(2 * Math.PI * phase); 18 | phase += phaseStep; 19 | } 20 | }; 21 | 22 | node.start(); 23 | setTimeout(function() { node.stop(); }, 1000); 24 | -------------------------------------------------------------------------------- /macaudio.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #import 7 | 8 | using namespace v8; 9 | using namespace node; 10 | 11 | 12 | class JSOutputNode : ObjectWrap { 13 | public: 14 | static v8::Handle New(const Arguments& args) { 15 | HandleScope scope; 16 | 17 | size_t bufferSize; 18 | if (args.Length() == 1 && args[0]->IsNumber()) { 19 | bufferSize = args[0]->NumberValue(); 20 | if (bufferSize != 4096 && bufferSize != 2048 && 21 | bufferSize != 1024 && bufferSize != 512) { 22 | bufferSize = 1024; 23 | } 24 | } else { 25 | bufferSize = 1024; 26 | } 27 | 28 | JSOutputNode *node = new JSOutputNode(); 29 | 30 | // AudioUnit 31 | AudioComponentDescription cd; 32 | cd.componentType = kAudioUnitType_Output; 33 | cd.componentSubType = kAudioUnitSubType_DefaultOutput; 34 | cd.componentManufacturer = kAudioUnitManufacturer_Apple; 35 | cd.componentFlags = 0; 36 | cd.componentFlagsMask = 0; 37 | 38 | AudioComponent component = AudioComponentFindNext(NULL, &cd); 39 | AudioComponentInstanceNew(component, &node->_audioUnit); 40 | AudioUnitInitialize(node->_audioUnit); 41 | 42 | uint sampleRate; 43 | { 44 | Float64 _sampleRate; 45 | UInt32 size = sizeof(Float64); 46 | AudioUnitGetProperty(node->_audioUnit, 47 | kAudioUnitProperty_SampleRate, 48 | kAudioUnitScope_Output, 49 | 0, 50 | &_sampleRate, 51 | &size); 52 | sampleRate = (uint)_sampleRate; 53 | } 54 | uint channels = 2; 55 | node->init(sampleRate, channels, bufferSize); 56 | 57 | AURenderCallbackStruct callback; 58 | callback.inputProc = JSOutputNode::AudioUnitCallback; 59 | callback.inputProcRefCon = node; 60 | 61 | AudioUnitSetProperty(node->_audioUnit, 62 | kAudioUnitProperty_SetRenderCallback, 63 | kAudioUnitScope_Input, 64 | 0, 65 | &callback, 66 | sizeof(AURenderCallbackStruct)); 67 | 68 | AudioStreamBasicDescription audioFormat; 69 | audioFormat.mSampleRate = sampleRate; 70 | audioFormat.mFormatID = kAudioFormatLinearPCM; 71 | audioFormat.mFormatFlags = kAudioFormatFlagsAudioUnitCanonical; 72 | audioFormat.mChannelsPerFrame = channels; 73 | audioFormat.mBytesPerPacket = sizeof(AudioUnitSampleType); 74 | audioFormat.mBytesPerFrame = sizeof(AudioUnitSampleType); 75 | audioFormat.mFramesPerPacket = 1; 76 | audioFormat.mBitsPerChannel = 8 * sizeof(AudioUnitSampleType); 77 | audioFormat.mReserved = 0; 78 | 79 | AudioUnitSetProperty(node->_audioUnit, 80 | kAudioUnitProperty_StreamFormat, 81 | kAudioUnitScope_Input, 82 | 0, 83 | &audioFormat, 84 | sizeof(audioFormat)); 85 | 86 | // audioProcessEvent 87 | Local t = ObjectTemplate::New(); 88 | t->SetInternalFieldCount(1); 89 | 90 | Persistent e = Persistent::New(t->NewInstance()); 91 | e->Set(String::New("sampleRate"), Integer::New(sampleRate)); 92 | e->Set(String::New("channels") , Integer::New(channels)); 93 | e->Set(String::New("bufferSize"), Integer::New(bufferSize)); 94 | e->Set(String::New("getChannelData"), 95 | FunctionTemplate::New(JSOutputNode::AudioProcessEventGetChannelData) 96 | ->GetFunction()); 97 | e->SetPointerInInternalField(0, node); 98 | node->_audioProcessEvent = e; 99 | 100 | node->Wrap(args.This()); 101 | 102 | return args.This(); 103 | } 104 | 105 | static v8::Handle Start(const Arguments& args) { 106 | HandleScope scope; 107 | 108 | JSOutputNode *node = ObjectWrap::Unwrap(args.This()); 109 | if (! node->_isPlaying) { 110 | node->_isPlaying = true; 111 | node->_notifier.data = node; 112 | uv_async_init(uv_default_loop(), &node->_notifier, 113 | JSOutputNode::CallOnAudioProcess); 114 | uv_async_send(&node->_notifier); 115 | AudioOutputUnitStart(node->_audioUnit); 116 | } 117 | return scope.Close(Undefined()); 118 | } 119 | 120 | static OSStatus AudioUnitCallback 121 | (void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, 122 | const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, 123 | UInt32 inNumberFrames, AudioBufferList *ioData) { 124 | JSOutputNode *node = static_cast(inRefCon); 125 | for (uint ch = 0; ch < node->_channels; ch++) { 126 | AudioUnitSampleType *out = (AudioUnitSampleType*)ioData->mBuffers[ch].mData; 127 | float *inp = node->_data[ch] + node->_readIndex; 128 | for (uint i = 0; i < inNumberFrames; i++) { 129 | *out++ = *inp++; 130 | } 131 | } 132 | node->_readIndex += inNumberFrames; 133 | if (node->_readIndex == node->_bufferSize) { 134 | uv_async_send(&node->_notifier); 135 | node->_readIndex = 0; 136 | } 137 | return noErr; 138 | } 139 | 140 | static void CallOnAudioProcess(uv_async_t *handle, int status) { 141 | HandleScope scope; 142 | JSOutputNode *node = static_cast(handle->data); 143 | if (!node->_onAudioProcess.IsEmpty()) { 144 | v8::Handle argv[] = { node->_audioProcessEvent }; 145 | node->_onAudioProcess->Call(Context::GetCurrent()->Global(), 1, argv); 146 | } 147 | } 148 | 149 | static v8::Handle Stop(const Arguments& args) { 150 | HandleScope scope; 151 | 152 | JSOutputNode *node = ObjectWrap::Unwrap(args.This()); 153 | if (node->_isPlaying) { 154 | node->_isPlaying = false; 155 | AudioOutputUnitStop(node->_audioUnit); 156 | #if NODE_VERSION_AT_LEAST(0, 7, 9) 157 | uv_unref((uv_handle_t *)&scope); 158 | #else 159 | uv_unref(uv_default_loop()); 160 | #endif 161 | } 162 | return scope.Close(Undefined()); 163 | } 164 | 165 | static v8::Handle GetSampleRate 166 | (Local property, const AccessorInfo& info) { 167 | JSOutputNode *node = ObjectWrap::Unwrap(info.Holder()); 168 | return Integer::New(node->_sampleRate); 169 | } 170 | 171 | static v8::Handle GetChannels 172 | (Local property, const AccessorInfo& info) { 173 | JSOutputNode *node = ObjectWrap::Unwrap(info.Holder()); 174 | return Integer::New(node->_channels); 175 | } 176 | 177 | static v8::Handle GetBufferSize 178 | (Local property, const AccessorInfo& info) { 179 | JSOutputNode *node = ObjectWrap::Unwrap(info.Holder()); 180 | return Integer::New(node->_bufferSize); 181 | } 182 | 183 | static v8::Handle GetIsPlaying 184 | (Local property, const AccessorInfo& info) { 185 | JSOutputNode *node = ObjectWrap::Unwrap(info.Holder()); 186 | return v8::Boolean::New(node->_isPlaying); 187 | } 188 | 189 | static v8::Handle GetOnAudioProcess 190 | (Local property, const AccessorInfo& info) { 191 | JSOutputNode *node = ObjectWrap::Unwrap(info.Holder()); 192 | return node->_onAudioProcess; 193 | } 194 | 195 | static void SetOnAudioProcess 196 | (Local property, Local value, const AccessorInfo& info) { 197 | if (!value->IsFunction()) return; 198 | JSOutputNode *node = ObjectWrap::Unwrap(info.Holder()); 199 | node->_onAudioProcess.Dispose(); 200 | Local func = Local::Cast(value); 201 | node->_onAudioProcess = Persistent::New(func); 202 | } 203 | 204 | static v8::Handle AudioProcessEventGetChannelData(const Arguments& args) { 205 | HandleScope scope; 206 | if (args.Length() == 1 && args[0]->IsNumber()) { 207 | size_t index = args[0]->ToUint32()->Value(); 208 | 209 | Local e = args.Holder(); 210 | void *p = e->GetPointerFromInternalField(0); 211 | JSOutputNode *node = static_cast(p); 212 | if (index < node->_channels) { 213 | Local q = Local::New(Object::New()); 214 | q->SetIndexedPropertiesToExternalArrayData(node->_data[index], 215 | kExternalFloatArray, 216 | node->_bufferSize); 217 | return scope.Close(q); 218 | } 219 | } 220 | return Undefined(); 221 | } 222 | 223 | 224 | private: 225 | uint _sampleRate; 226 | uint _channels; 227 | size_t _bufferSize; 228 | Persistent _onAudioProcess; 229 | Persistent _audioProcessEvent; 230 | 231 | float **_data; 232 | uint _readIndex; 233 | bool _isPlaying; 234 | uv_async_t _notifier; 235 | AudioUnit _audioUnit; 236 | 237 | void init(uint sampleRate, uint channels, size_t bufferSize) { 238 | _sampleRate = sampleRate; 239 | _channels = channels; 240 | _bufferSize = bufferSize; 241 | _data = new float*[channels]; 242 | for (uint i = 0; i < channels; i++) { 243 | _data[i] = new float[bufferSize](); 244 | } 245 | _readIndex = 0; 246 | _isPlaying = false; 247 | } 248 | 249 | ~JSOutputNode() { 250 | if (_isPlaying) { 251 | _isPlaying = false; 252 | #if NODE_VERSION_AT_LEAST(0, 7, 9) 253 | 254 | #else 255 | uv_unref(uv_default_loop()); 256 | #endif 257 | } 258 | for (uint i = 0; i < _channels; i++) { 259 | delete _data[i]; 260 | } 261 | delete _data; 262 | _audioProcessEvent.Dispose(); 263 | _onAudioProcess.Dispose(); 264 | } 265 | }; 266 | 267 | 268 | void Initialize(v8::Handle exports) { 269 | HandleScope scope; 270 | 271 | Local t = FunctionTemplate::New(JSOutputNode::New); 272 | t->SetClassName(String::NewSymbol("JavaScriptOutputNode")); 273 | 274 | NODE_SET_PROTOTYPE_METHOD(t, "start", JSOutputNode::Start); 275 | NODE_SET_PROTOTYPE_METHOD(t, "stop" , JSOutputNode::Stop ); 276 | 277 | Local i = t->InstanceTemplate(); 278 | i->SetInternalFieldCount(1); 279 | i->SetAccessor(String::New("sampleRate"), 280 | JSOutputNode::GetSampleRate, NULL, 281 | v8::Handle(), 282 | v8::PROHIBITS_OVERWRITING, 283 | PropertyAttribute(v8::ReadOnly|v8::DontDelete)); 284 | i->SetAccessor(String::New("channels"), 285 | JSOutputNode::GetChannels, NULL, 286 | v8::Handle(), 287 | v8::PROHIBITS_OVERWRITING, 288 | PropertyAttribute(v8::ReadOnly|v8::DontDelete)); 289 | i->SetAccessor(String::New("bufferSize"), 290 | JSOutputNode::GetBufferSize, NULL, 291 | v8::Handle(), 292 | v8::PROHIBITS_OVERWRITING, 293 | PropertyAttribute(v8::ReadOnly|v8::DontDelete)); 294 | i->SetAccessor(String::New("isPlaying"), 295 | JSOutputNode::GetIsPlaying, NULL, 296 | v8::Handle(), 297 | v8::PROHIBITS_OVERWRITING, 298 | PropertyAttribute(v8::ReadOnly|v8::DontDelete)); 299 | i->SetAccessor(String::New("onaudioprocess"), 300 | JSOutputNode::GetOnAudioProcess, 301 | JSOutputNode::SetOnAudioProcess); 302 | exports->Set(String::New("JavaScriptOutputNode"), t->GetFunction()); 303 | } 304 | 305 | 306 | NODE_MODULE(macaudio, Initialize) 307 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "macaudio", 3 | "version": "0.0.3", 4 | "description": "The interface is an AudioUnit(Mac) which can generate audio directly using JavaScript.", 5 | "keywords": ["audio", "AudioUnit", "mac", "sound", "music"], 6 | "author": "nao yonamine (http://app.mohayonao.com)", 7 | "repository": { 8 | "type" : "git", 9 | "url": "https://github.com/mohayonao/node-macaudio.git" 10 | }, 11 | "main": "./build/Release/macaudio", 12 | "licenses": [ 13 | { "type": "MIT", 14 | "url" : "https://raw.github.com/mohayonao/node-macaudio/master/LICENSE" } 15 | ], 16 | "engines": { 17 | "node": ">=0.8.0" 18 | } 19 | } 20 | --------------------------------------------------------------------------------