diff --git a/src/AudioIO.jl b/src/AudioIO.jl index 73427ab..77e3314 100644 --- a/src/AudioIO.jl +++ b/src/AudioIO.jl @@ -30,13 +30,11 @@ include("portaudio.jl") ############ Exported Functions ############# -# TODO: we should have "stop" functions that remove nodes from the render tree - # Play an AudioNode by adding it as an input to the root mixer node function play(node::AudioNode, stream::AudioStream) - # TODO: don't break demeter - append!(stream.mixer.mix_inputs, [node]) - return nothing + node.active = true + add_input(stream.mixer, node) + return node end # If the stream is not given, use the default global PortAudio stream @@ -78,6 +76,7 @@ end function stop(node::AudioNode) node.active = false + return node end end # module AudioIO diff --git a/src/nodes.jl b/src/nodes.jl index 9b4bd33..2f98f97 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -10,7 +10,7 @@ type SinOsc <: AudioNode phase::FloatingPoint function SinOsc(freq::Real) - new(true, freq, 0.0) + new(false, freq, 0.0) end end @@ -25,26 +25,67 @@ end # Mixes a set of inputs equally +# a convenience alias used in the array of mix inputs +typealias MaybeAudioNode Union(AudioNode, Nothing) +const MAX_MIXER_INPUTS = 32 + type AudioMixer <: AudioNode active::Bool - mix_inputs::Array{AudioNode} + mix_inputs::Array{MaybeAudioNode} function AudioMixer{T <: AudioNode}(mix_inputs::Array{T}) - new(true, mix_inputs) + input_array = Array(MaybeAudioNode, MAX_MIXER_INPUTS) + fill!(input_array, nothing) + for (i, node) in enumerate(mix_inputs) + input_array[i] = node + end + new(false, input_array) end function AudioMixer() - new(true, AudioNode[]) + AudioMixer(AudioNode[]) end end +# TODO: at some point we need to figure out what the general API is for wiring +# up AudioNodes to each other +function add_input(mixer::AudioMixer, in_node::AudioNode) + for (i, node) in enumerate(mixer.mix_inputs) + if node === nothing + mixer.mix_inputs[i] = in_node + return + end + end + error("Mixer input array is full") +end + +# removes the given node from the mix inputs. If the node isn't an input the +# function returns without error +function remove_input(mixer::AudioMixer, in_node::AudioNode) + for (i, node) in enumerate(mixer.mix_inputs) + if node === in_node + mixer.mix_inputs[i] = nothing + return + end + end + # not an error if we didn't find it +end + function render(node::AudioMixer, device_input::AudioBuf, info::DeviceInfo) - # TODO: we may want to pre-allocate this buffer and share between render - # calls + # TODO: we probably want to pre-allocate this buffer and share between + # render calls. Unfortunately we don't know the right size when the object + # is created, so maybe we check the size on every render call and only + # re-allocate when the size changes? I suppose that's got to be cheaper + # than the GC and allocation every frame mix_buffer = zeros(AudioSample, info.buf_size) for in_node in node.mix_inputs - in_buffer, active = render(in_node, device_input, info) - mix_buffer += in_buffer + if in_node !== nothing + in_buffer, active = render(in_node, device_input, info) + mix_buffer += in_buffer + if !active + remove_input(node, in_node) + end + end end return mix_buffer, node.active end @@ -59,7 +100,7 @@ type ArrayPlayer <: AudioNode arr_index::Int function ArrayPlayer(arr::AudioBuf) - new(true, arr, 1) + new(false, arr, 1) end end @@ -84,10 +125,15 @@ end # Renders incoming audio input from the hardware type AudioInput <: AudioNode + active::Bool channel::Int + + function AudioInput(channel::Int) + new(false, channel) + end end function render(node::AudioInput, device_input::AudioBuf, info::DeviceInfo) @assert size(device_input, 1) == info.buf_size - return device_input[:, node.channel], true + return device_input[:, node.channel], node.active end diff --git a/src/portaudio.jl b/src/portaudio.jl index 22f0c8d..b850ebf 100644 --- a/src/portaudio.jl +++ b/src/portaudio.jl @@ -87,19 +87,27 @@ function portaudio_task(jl_filedesc::Integer, stream::PortAudioStream) desc_bytes = Cchar[0] jl_stream = fdio(jl_filedesc) jl_rawfd = RawFD(jl_filedesc) - while true - # assume the root mixer is always active - out_array, _ = render(stream.mixer, in_array, stream.info)::AudioBuf - # wake the C code so it knows we've given it some more data - wake_callback_thread(out_array) - # wait for new data to be available from the sound card (and for it to - # have processed our last frame of data). At some point we should do - # something with the data we get from the callback - wait(jl_rawfd, readable=true) - # read from the file descriptor so that it's empty. We're using ccall - # here because readbytes() was blocking the whole julia thread. This - # shouldn't block at all because we just waited on it - ccall(:read, Clong, (Cint, Ptr{Void}, Culong), jl_filedesc, desc_bytes, 1) + try + while true + # assume the root mixer is always active + out_array::AudioBuf, _::Bool = render(stream.mixer, in_array, + stream.info) + # wake the C code so it knows we've given it some more data + wake_callback_thread(out_array) + # wait for new data to be available from the sound card (and for it + # to have processed our last frame of data). At some point we + # should do something with the data we get from the callback + wait(jl_rawfd, readable=true) + # read from the file descriptor so that it's empty. We're using + # ccall here because readbytes() was blocking the whole julia + # thread. This shouldn't block at all because we just waited on it + ccall(:read, Clong, (Cint, Ptr{Void}, Culong), + jl_filedesc, desc_bytes, 1) + end + finally + # TODO: we need to close the stream here. Otherwise the audio callback + # will segfault accessing the output array if there were exceptions + # thrown in the render loop end end diff --git a/test/test_AudioIO.jl b/test/test_AudioIO.jl index 894f232..35e300b 100644 --- a/test/test_AudioIO.jl +++ b/test/test_AudioIO.jl @@ -35,8 +35,6 @@ f32 = convert(Array{Float32}, sin(phase)) test_stream = TestAudioStream() player = play(f32, test_stream) @test process(test_stream) == f32[1:TEST_BUF_SIZE] -#stop(player) -#@test process(test_stream) == zeros(AudioIO.AudioSample, TEST_BUF_SIZE) info("Testing Playing Float64 arrays...") @@ -62,10 +60,15 @@ player = play(ui8, test_stream) convert(AudioIO.AudioBuf, linspace(-1.0, 1.0, 255))) -#info("Testing AudioNode Stopping...") -#test_stream = TestAudioStream() -#node = SinOsc(440) -#play(node, test_stream) -#process(test_stream) -#stop(node) -#@test process(test_stream) == zeros(AudioIO.AudioSample, TEST_BUF_SIZE) +info("Testing AudioNode Stopping...") +test_stream = TestAudioStream() +node = SinOsc(440) +@test !node.active +play(node, test_stream) +@test node.active +process(test_stream) +stop(node) +@test !node.active +# give the render task a chance to clean up +process(test_stream) +@test process(test_stream) == zeros(AudioIO.AudioSample, TEST_BUF_SIZE) diff --git a/test/test_nodes.jl b/test/test_nodes.jl index fdb281e..57283a8 100644 --- a/test/test_nodes.jl +++ b/test/test_nodes.jl @@ -6,12 +6,17 @@ dev_input = zeros(AudioIO.AudioSample, test_info.buf_size) # A TestNode just renders out 1:buf_size each frame type TestNode <: AudioIO.AudioNode + active::Bool + + function TestNode() + return new(false) + end end function AudioIO.render(node::TestNode, device_input::AudioIO.AudioBuf, info::AudioIO.DeviceInfo) - return AudioIO.AudioSample[1:info.buf_size], true + return AudioIO.AudioSample[1:info.buf_size], node.active end #### AudioMixer Tests #### @@ -22,21 +27,26 @@ end info("Testing AudioMixer...") mix = AudioMixer() render_output, active = AudioIO.render(mix, dev_input, test_info) -@test mix.mix_inputs == AudioIO.AudioNode[] @test render_output == zeros(AudioIO.AudioSample, test_info.buf_size) -@test active testnode = TestNode() mix = AudioMixer([testnode]) render_output, active = AudioIO.render(mix, dev_input, test_info) -@test mix.mix_inputs == AudioIO.AudioNode[testnode] @test render_output == AudioIO.AudioSample[1:test_info.buf_size] -@test active -mix = AudioMixer([TestNode(), TestNode()]) +test1 = TestNode() +test2 = TestNode() +mix = AudioMixer([test1, test2]) render_output, active = AudioIO.render(mix, dev_input, test_info) +# make sure the two inputs are being added together @test render_output == 2 * AudioIO.AudioSample[1:test_info.buf_size] -@test active + +# now we'll stop one of the inputs and make sure it gets removed +# TODO: this test should depend on the render output, not on the internals of +# the mixer +stop(test1) +AudioIO.render(mix, dev_input, test_info) +@test !in(test1, mix.mix_inputs) stop(mix) render_output, active = AudioIO.render(mix, dev_input, test_info) @@ -51,7 +61,6 @@ test_vect = convert(AudioIO.AudioBuf, sin(2pi * t * freq)) osc = SinOsc(freq) render_output, active = AudioIO.render(osc, dev_input, test_info) @test_approx_eq(render_output, test_vect) -@test active stop(osc) render_output, active = AudioIO.render(osc, dev_input, test_info) @test !active @@ -59,6 +68,7 @@ render_output, active = AudioIO.render(osc, dev_input, test_info) info("Testing ArrayPlayer...") v = rand(AudioIO.AudioSample, 44100) player = ArrayPlayer(v) +player.active = true render_output, active = AudioIO.render(player, dev_input, test_info) @test render_output == v[1:test_info.buf_size] @test active @@ -72,6 +82,7 @@ render_output, active = AudioIO.render(player, dev_input, test_info) # give a vector just a bit larger than 1 buffer size v = rand(AudioIO.AudioSample, test_info.buf_size + 1) player = ArrayPlayer(v) +player.active = true _, active = AudioIO.render(player, dev_input, test_info) @test active _, active = AudioIO.render(player, dev_input, test_info)