adds supporting for stopping AudioNodes

This commit is contained in:
Spencer Russell 2014-01-05 22:50:56 -05:00
parent 0f43ff836a
commit 38c1499e6f
5 changed files with 112 additions and 45 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)