refactored AudioNode to contain a AudioRenderer, tests passing

This commit is contained in:
Spencer Russell 2014-06-23 02:10:35 -04:00
parent 0ddd57c0a9
commit a1ed357629
7 changed files with 211 additions and 219 deletions

View file

@ -12,11 +12,12 @@ typealias AudioSample Float32
# A frame of audio, possibly multi-channel # A frame of audio, possibly multi-channel
typealias AudioBuf Array{AudioSample} typealias AudioBuf Array{AudioSample}
# A node in the render tree # used as a type parameter for AudioNodes. Subtypes handle the actual DSP for
abstract AudioNode # each node
abstract AudioRenderer
# A stream of audio (for instance that writes to hardware) # A stream of audio (for instance that writes to hardware). All AudioStream
# All AudioStream subtypes should have a mixer and info field # subtypes should have a mixer and info field
abstract AudioStream abstract AudioStream
# An audio interface is usually a physical sound card, but could # An audio interface is usually a physical sound card, but could
@ -29,6 +30,31 @@ type DeviceInfo
buf_size::Integer buf_size::Integer
end end
type AudioNode{T<:AudioRenderer}
active::Bool
end_cond::Condition
renderer::T
function AudioNode(renderer::AudioRenderer)
new(true, Condition(), renderer)
end
end
function render(node::AudioNode, input::AudioBuf, info::DeviceInfo)
# TODO: not sure if the compiler will infer that render() always returns an
# AudioBuf. Might need to help it
if node.active
result = render(node.renderer, input, info)
if length(result) < info.buf_size
node.active = false
notify(node.end_cond)
end
return result
else
return AudioSample[]
end
end
include("nodes.jl") include("nodes.jl")
include("portaudio.jl") include("portaudio.jl")
include("sndfile.jl") include("sndfile.jl")
@ -38,8 +64,7 @@ include("operators.jl")
# Play an AudioNode by adding it as an input to the root mixer node # Play an AudioNode by adding it as an input to the root mixer node
function play(node::AudioNode, stream::AudioStream) function play(node::AudioNode, stream::AudioStream)
activate(node) push!(stream.root, node)
add_input(stream.mixer, node)
return node return node
end end
@ -53,26 +78,13 @@ function play(node::AudioNode)
end end
function stop(node::AudioNode) function stop(node::AudioNode)
deactivate(node)
node
end
function activate(node::AudioNode)
node.active = true
end
function deactivate(node::AudioNode)
node.active = false node.active = false
notify(node.deactivate_cond) notify(node.end_cond)
end
function is_active(node::AudioNode)
node.active
end end
function Base.wait(node::AudioNode) function Base.wait(node::AudioNode)
if is_active(node) if node.active
wait(node.deactivate_cond) wait(node.end_cond)
end end
end end

View file

@ -1,146 +1,120 @@
#### NullNode ####
type NullRenderer <: AudioRenderer end
typealias NullNode AudioNode{NullRenderer}
NullNode() = NullNode(NullRenderer())
export NullNode
function render(node::NullRenderer, device_input::AudioBuf, info::DeviceInfo)
# TODO: preallocate buffer
return zeros(info.buf_size)
end
#### SinOsc #### #### SinOsc ####
# Generates a sin tone at the given frequency # Generates a sin tone at the given frequency
export SinOsc type SinOscRenderer <: AudioRenderer
type SinOsc <: AudioNode
active::Bool
deactivate_cond::Condition
freq::Real freq::Real
phase::FloatingPoint phase::FloatingPoint
function SinOsc(freq::Real) function SinOscRenderer(freq::Real)
new(false, Condition(), freq, 0.0) new(freq, 0.0)
end end
end end
function render(node::SinOsc, device_input::AudioBuf, info::DeviceInfo) typealias SinOsc AudioNode{SinOscRenderer}
phase = AudioSample[1:info.buf_size] * 2pi * node.freq / info.sample_rate SinOsc(freq::Real) = SinOsc(SinOscRenderer(freq))
SinOsc() = SinOsc(440)
export SinOsc
function render(node::SinOscRenderer, device_input::AudioBuf, info::DeviceInfo)
phase = AudioSample[0:(info.buf_size-1)] * 2pi * node.freq / info.sample_rate
phase .+= node.phase phase .+= node.phase
node.phase = phase[end] node.phase = phase[end] + 2pi * node.freq / info.sample_rate
return sin(phase), is_active(node) return sin(phase)
end end
#### Gain ####
export Gain
type Gain <: AudioNode
active::Bool
deactivate_cond::Condition
in_node::AudioNode
gain::Float32
function Gain(in_node::AudioNode, gain::Real)
new(false, Condition(), in_node, gain)
end
end
function render(node::Gain, device_input::AudioBuf, info::DeviceInfo)
input, child_active = render(node.in_node, device_input, info)
# TODO: should we check the active flag of the input?
return input .* node.gain, is_active(node)
end
#### AudioMixer #### #### AudioMixer ####
# Mixes a set of inputs equally # Mixes a set of inputs equally
# a convenience alias used in the array of mix inputs type MixRenderer <: AudioRenderer
typealias MaybeAudioNode Union(AudioNode, Nothing) inputs::Vector{AudioNode}
const MAX_MIXER_INPUTS = 32 end
typealias AudioMixer AudioNode{MixRenderer}
AudioMixer{T<:AudioNode}(inputs::Vector{T}) = AudioMixer(MixRenderer(inputs))
AudioMixer() = AudioMixer(AudioNode[])
export AudioMixer export AudioMixer
type AudioMixer <: AudioNode
active::Bool
deactivate_cond::Condition
mix_inputs::Array{MaybeAudioNode}
function AudioMixer{T <: AudioNode}(mix_inputs::Array{T}) function render(node::MixRenderer, device_input::AudioBuf, info::DeviceInfo)
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, Condition(), input_array)
end
function AudioMixer()
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 probably want to pre-allocate this buffer and share between # 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 # 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 # 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 # re-allocate when the size changes? I suppose that's got to be cheaper
# than the GC and allocation every frame # than the GC and allocation every frame
mix_buffer = zeros(AudioSample, info.buf_size) mix_buffer = zeros(AudioSample, info.buf_size)
for in_node in node.mix_inputs n_inputs = length(node.inputs)
if in_node !== nothing i = 1
in_buffer, active = render(in_node, device_input, info) max_samples = 0
mix_buffer += in_buffer while i <= n_inputs
if !active rendered = render(node.inputs[i], device_input, info)::AudioBuf
remove_input(node, in_node) nsamples = length(rendered)
end max_samples = max(max_samples, nsamples)
mix_buffer[1:nsamples] .+= rendered
if nsamples < info.buf_size
deleteat!(node.inputs, i)
n_inputs -= 1
else
i += 1
end end
end end
return mix_buffer, is_active(node) return mix_buffer[1:max_samples]
end end
Base.push!(mixer::AudioMixer, node::AudioNode) = push!(mixer.renderer.inputs, node)
#### Gain ####
type GainRenderer <: AudioRenderer
in_node::AudioNode
gain::Float32
end
function render(node::GainRenderer, device_input::AudioBuf, info::DeviceInfo)
input = render(node.in_node, device_input, info)
return input .* node.gain
end
typealias Gain AudioNode{GainRenderer}
Gain(in_node::AudioNode, gain::Real) = Gain(GainRenderer(in_node, gain))
export Gain
#### Array Player #### #### Array Player ####
# Plays a AudioBuf by rendering it out piece-by-piece # Plays a AudioBuf by rendering it out piece-by-piece
export ArrayPlayer type ArrayRenderer <: AudioRenderer
type ArrayPlayer <: AudioNode
active::Bool
deactivate_cond::Condition
arr::AudioBuf arr::AudioBuf
arr_index::Int arr_index::Int
function ArrayPlayer(arr::AudioBuf) ArrayRenderer(arr::AudioBuf) = new(arr, 1)
new(false, Condition(), arr, 1)
end
end end
function render(node::ArrayPlayer, device_input::AudioBuf, info::DeviceInfo) typealias ArrayPlayer AudioNode{ArrayRenderer}
ArrayPlayer(arr::AudioBuf) = ArrayPlayer(ArrayRenderer(arr))
export ArrayPlayer
function render(node::ArrayRenderer, device_input::AudioBuf, info::DeviceInfo)
# TODO: this should remove itself from the render tree when playback is # TODO: this should remove itself from the render tree when playback is
# complete # complete
i = node.arr_index i = node.arr_index
range_end = min(i + info.buf_size-1, length(node.arr)) range_end = min(i + info.buf_size-1, length(node.arr))
output = node.arr[i:range_end] output = node.arr[i:range_end]
if length(output) < info.buf_size
# we're finished with the array, pad with zeros and deactivate
output = vcat(output, zeros(AudioSample, info.buf_size - length(output)))
deactivate(node)
end
node.arr_index = range_end + 1 node.arr_index = range_end + 1
return output, is_active(node) return output
end end
# Allow users to play a raw array by wrapping it in an ArrayPlayer # Allow users to play a raw array by wrapping it in an ArrayPlayer
@ -171,22 +145,22 @@ function play{T <: Unsigned}(arr::Array{T}, args...)
play(arr, args...) play(arr, args...)
end end
#### AudioInput #### ##### AudioInput ####
#
# Renders incoming audio input from the hardware ## Renders incoming audio input from the hardware
#
export AudioInput #export AudioInput
type AudioInput <: AudioNode #type AudioInput <: AudioNode
active::Bool # active::Bool
deactivate_cond::Condition # deactivate_cond::Condition
channel::Int # channel::Int
#
function AudioInput(channel::Int) # function AudioInput(channel::Int)
new(false, Condition(), channel) # new(false, Condition(), channel)
end # end
end #end
#
function render(node::AudioInput, device_input::AudioBuf, info::DeviceInfo) #function render(node::AudioInput, device_input::AudioBuf, info::DeviceInfo)
@assert size(device_input, 1) == info.buf_size # @assert size(device_input, 1) == info.buf_size
return device_input[:, node.channel], is_active(node) # return device_input[:, node.channel], is_active(node)
end #end

View file

@ -1,2 +1,13 @@
*(node::AudioNode, coef::Real) = Gain(node, coef) *(node::AudioNode, coef::Real) = Gain(node, coef)
*(coef::Real, node::AudioNode) = Gain(node, coef) *(coef::Real, node::AudioNode) = Gain(node, coef)
# multiplying by silence gives silence
*(in1::NullNode, in2::NullNode) = in1
*(in1::AudioNode, in2::NullNode) = in2
*(in1::NullNode, in2::AudioNode) = in1
+(in1::AudioNode, in2::AudioNode) = AudioMixer([in1, in2])
# adding silence has no effect
+(in1::NullNode, in2::NullNode) = in1
+(in1::AudioNode, in2::NullNode) = in1
+(in1::NullNode, in2::AudioNode) = in2

View file

@ -35,13 +35,13 @@ portaudio_inited = false
################## Types #################### ################## Types ####################
type PortAudioStream <: AudioStream type PortAudioStream <: AudioStream
mixer::AudioMixer root::AudioMixer
info::DeviceInfo info::DeviceInfo
function PortAudioStream(sample_rate::Int=44100, buf_size::Int=1024) function PortAudioStream(sample_rate::Int=44100, buf_size::Int=1024)
init_portaudio() init_portaudio()
mixer = AudioMixer() root = AudioMixer()
stream = new(mixer, DeviceInfo(sample_rate, buf_size)) stream = new(root, DeviceInfo(sample_rate, buf_size))
# we need to start up the stream with the portaudio library # we need to start up the stream with the portaudio library
open_portaudio_stream(stream) open_portaudio_stream(stream)
return stream return stream
@ -91,8 +91,14 @@ function portaudio_task(jl_filedesc::Integer, stream::PortAudioStream)
jl_rawfd = RawFD(jl_filedesc) jl_rawfd = RawFD(jl_filedesc)
try try
while true while true
# assume the root mixer is always active # assume the root is always active
buffer::AudioBuf, _::Bool = render(stream.mixer, buffer, stream.info) rendered = render(stream.root, buffer, stream.info)
for i in 1:length(rendered)
buffer[i] = rendered[i]
end
for i in (length(rendered)+1):length(buffer)
buffer[i] = 0.0
end
# wake the C code so it knows we've given it some more data # wake the C code so it knows we've given it some more data
synchronize_buffer(buffer) synchronize_buffer(buffer)

View file

@ -88,7 +88,6 @@ end
# through an arbitrary render chain and returns the result as a vector # through an arbitrary render chain and returns the result as a vector
function Base.read(file::AudioFile, nframes::Integer = file.sfinfo.frames, function Base.read(file::AudioFile, nframes::Integer = file.sfinfo.frames,
dtype::Type = Int16) dtype::Type = Int16)
arr = []
@assert file.sfinfo.channels <= 2 @assert file.sfinfo.channels <= 2
if file.sfinfo.channels == 2 if file.sfinfo.channels == 2
arr = zeros(dtype, 2, nframes) arr = zeros(dtype, 2, nframes)
@ -114,11 +113,7 @@ function Base.read(file::AudioFile, nframes::Integer = file.sfinfo.frames,
file.filePtr, arr, nframes) file.filePtr, arr, nframes)
end end
if nread == 0 return arr[1:nread]
return Nothing
end
return arr
end end
function Base.write{T}(file::AudioFile, frames::Array{T}) function Base.write{T}(file::AudioFile, frames::Array{T})
@ -144,38 +139,34 @@ function Base.write{T}(file::AudioFile, frames::Array{T})
end end
end end
type FilePlayer <: AudioNode type FileRenderer <: AudioRenderer
active::Bool
deactivate_cond::Condition
file::AudioFile file::AudioFile
function FilePlayer(file::AudioFile) function FileRenderer(file::AudioFile)
node = new(false, Condition(), file)
finalizer(node, node -> close(node.file)) finalizer(node, node -> close(node.file))
return node return node
end end
function FilePlayer(path::String)
return FilePlayer(af_open(path))
end
end end
function render(node::FilePlayer, device_input::AudioBuf, info::DeviceInfo) typealias FilePlayer AudioNode{FileRenderer}
FilePlayer(file::AudioFile) = FilePlayer(FileRenderer(file))
FilePlayer(path::String) = FilePlayer(af_open(path))
function render(node::FileRenderer, device_input::AudioBuf, info::DeviceInfo)
@assert node.file.sfinfo.samplerate == info.sample_rate @assert node.file.sfinfo.samplerate == info.sample_rate
audio = read(node.file, info.buf_size, AudioSample) audio = read(node.file, info.buf_size, AudioSample)
if audio == Nothing if audio == Nothing
deactivate(node) return AudioSample[]
return zeros(AudioSample, info.buf_size), is_active(node)
end end
# if the file is stereo, mix the two channels together # if the file is stereo, mix the two channels together
if node.file.sfinfo.channels == 2 if node.file.sfinfo.channels == 2
return (audio[1, :] / 2) + (audio[2, :] / 2), is_active(node) return (audio[1, :] / 2) + (audio[2, :] / 2)
end end
return audio, is_active(node) return audio
end end
function play(filename::String, args...) function play(filename::String, args...)

View file

@ -5,20 +5,22 @@ const TEST_SAMPLERATE = 44100
const TEST_BUF_SIZE = 1024 const TEST_BUF_SIZE = 1024
type TestAudioStream <: AudioIO.AudioStream type TestAudioStream <: AudioIO.AudioStream
mixer::AudioMixer root::AudioIO.AudioMixer
info::AudioIO.DeviceInfo info::AudioIO.DeviceInfo
function TestAudioStream() function TestAudioStream()
mixer = AudioMixer() root = AudioMixer()
new(mixer, AudioIO.DeviceInfo(TEST_SAMPLERATE, TEST_BUF_SIZE)) new(root, AudioIO.DeviceInfo(TEST_SAMPLERATE, TEST_BUF_SIZE))
end end
end end
# render the stream and return the next block of audio. This is used in testing # render the stream and return the next block of audio. This is used in testing
# to simulate the audio callback that's normally called by the device. # to simulate the audio callback that's normally called by the device.
function process(stream::TestAudioStream) function process(stream::TestAudioStream)
out_array = zeros(AudioIO.AudioSample, stream.info.buf_size)
in_array = zeros(AudioIO.AudioSample, stream.info.buf_size) in_array = zeros(AudioIO.AudioSample, stream.info.buf_size)
out_array, _ = AudioIO.render(stream.mixer, in_array, stream.info) rendered = AudioIO.render(stream.root, in_array, stream.info)
out_array[1:length(rendered)] = rendered
return out_array return out_array
end end
@ -59,18 +61,12 @@ player = play(ui8, test_stream)
@test_approx_eq(process(test_stream)[1:255], @test_approx_eq(process(test_stream)[1:255],
convert(AudioIO.AudioBuf, linspace(-1.0, 1.0, 255))) convert(AudioIO.AudioBuf, linspace(-1.0, 1.0, 255)))
info("Testing AudioNode Stopping...") info("Testing AudioNode Stopping...")
test_stream = TestAudioStream() test_stream = TestAudioStream()
node = SinOsc(440) node = SinOsc(440)
@test !node.active
play(node, test_stream) play(node, test_stream)
@test node.active
process(test_stream) process(test_stream)
stop(node) 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) @test process(test_stream) == zeros(AudioIO.AudioSample, TEST_BUF_SIZE)
info("Testing wav file write/read") info("Testing wav file write/read")

View file

@ -1,23 +1,25 @@
using Base.Test using Base.Test
using AudioIO using AudioIO
import AudioIO.AudioSample
import AudioIO.AudioBuf
import AudioIO.AudioRenderer
import AudioIO.AudioNode
import AudioIO.DeviceInfo
import AudioIO.render
test_info = AudioIO.DeviceInfo(44100, 512) test_info = DeviceInfo(44100, 512)
dev_input = zeros(AudioIO.AudioSample, test_info.buf_size) dev_input = zeros(AudioSample, test_info.buf_size)
# A TestNode just renders out 1:buf_size each frame # A TestNode just renders out 1:buf_size each frame
type TestNode <: AudioIO.AudioNode type TestRenderer <: AudioRenderer end
active::Bool
deactivate_cond::Condition
function TestNode() typealias TestNode AudioNode{TestRenderer}
return new(false, Condition()) TestNode() = TestNode(TestRenderer())
end
end
function AudioIO.render(node::TestNode, function render(node::TestRenderer,
device_input::AudioIO.AudioBuf, device_input::AudioBuf,
info::AudioIO.DeviceInfo) info::DeviceInfo)
return AudioIO.AudioSample[1:info.buf_size], node.active return AudioSample[1:info.buf_size]
end end
#### AudioMixer Tests #### #### AudioMixer Tests ####
@ -27,64 +29,64 @@ end
info("Testing AudioMixer...") info("Testing AudioMixer...")
mix = AudioMixer() mix = AudioMixer()
render_output, active = AudioIO.render(mix, dev_input, test_info) render_output = render(mix, dev_input, test_info)
@test render_output == zeros(AudioIO.AudioSample, test_info.buf_size) @test render_output == AudioSample[]
testnode = TestNode() testnode = TestNode()
mix = AudioMixer([testnode]) mix = AudioMixer([testnode])
render_output, active = AudioIO.render(mix, dev_input, test_info) render_output = render(mix, dev_input, test_info)
@test render_output == AudioIO.AudioSample[1:test_info.buf_size] @test render_output == AudioSample[1:test_info.buf_size]
test1 = TestNode() test1 = TestNode()
test2 = TestNode() test2 = TestNode()
mix = AudioMixer([test1, test2]) mix = AudioMixer([test1, test2])
render_output, active = AudioIO.render(mix, dev_input, test_info) render_output = render(mix, dev_input, test_info)
# make sure the two inputs are being added together # make sure the two inputs are being added together
@test render_output == 2 * AudioIO.AudioSample[1:test_info.buf_size] @test render_output == 2 * AudioSample[1:test_info.buf_size]
# now we'll stop one of the inputs and make sure it gets removed # 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) stop(test1)
AudioIO.render(mix, dev_input, test_info) render_output = render(mix, dev_input, test_info)
@test !in(test1, mix.mix_inputs) # make sure the two inputs are being added together
@test render_output == AudioSample[1:test_info.buf_size]
stop(mix) stop(mix)
render_output, active = AudioIO.render(mix, dev_input, test_info) render_output = render(mix, dev_input, test_info)
@test !active @test render_output == AudioSample[]
info("Testing SinOSC...") info("Testing SinOSC...")
freq = 440 freq = 440
t = linspace(1 / test_info.sample_rate, t = linspace(0,
test_info.buf_size / test_info.sample_rate, (test_info.buf_size-1) / test_info.sample_rate,
test_info.buf_size) test_info.buf_size)
test_vect = convert(AudioIO.AudioBuf, sin(2pi * t * freq)) test_vect = convert(AudioBuf, sin(2pi * t * freq))
osc = SinOsc(freq) osc = SinOsc(freq)
render_output, active = AudioIO.render(osc, dev_input, test_info) render_output = render(osc, dev_input, test_info)
@test_approx_eq(render_output, test_vect) @test_approx_eq(render_output, test_vect)
stop(osc) stop(osc)
render_output, active = AudioIO.render(osc, dev_input, test_info) render_output = render(osc, dev_input, test_info)
@test !active @test render_output == AudioSample[]
info("Testing ArrayPlayer...") info("Testing ArrayPlayer...")
v = rand(AudioIO.AudioSample, 44100) v = rand(AudioSample, 44100)
player = ArrayPlayer(v) player = ArrayPlayer(v)
player.active = true render_output = render(player, dev_input, test_info)
render_output, active = AudioIO.render(player, dev_input, test_info)
@test render_output == v[1:test_info.buf_size] @test render_output == v[1:test_info.buf_size]
@test active render_output = render(player, dev_input, test_info)
render_output, active = AudioIO.render(player, dev_input, test_info)
@test render_output == v[(test_info.buf_size + 1) : (2*test_info.buf_size)] @test render_output == v[(test_info.buf_size + 1) : (2*test_info.buf_size)]
@test active
stop(player) stop(player)
render_output, active = AudioIO.render(player, dev_input, test_info) render_output = render(player, dev_input, test_info)
@test !active @test render_output == AudioSample[]
# give a vector just a bit larger than 1 buffer size # give a vector just a bit larger than 1 buffer size
v = rand(AudioIO.AudioSample, test_info.buf_size + 1) v = rand(AudioSample, test_info.buf_size + 1)
player = ArrayPlayer(v) player = ArrayPlayer(v)
player.active = true render(player, dev_input, test_info)
_, active = AudioIO.render(player, dev_input, test_info) render_output = render(player, dev_input, test_info)
@test active @test render_output == v[test_info.buf_size+1:end]
_, active = AudioIO.render(player, dev_input, test_info)
@test !active info("Testing Gain...")
gained = TestNode() * 0.75
render_output = render(gained, dev_input, test_info)
@test render_output == 0.75 * AudioSample[1:test_info.buf_size]