adds supporting for stopping AudioNodes
This commit is contained in:
parent
0f43ff836a
commit
38c1499e6f
5 changed files with 112 additions and 45 deletions
|
@ -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
|
||||
|
|
66
src/nodes.jl
66
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue