diff --git a/src/PortAudio.jl b/src/PortAudio.jl index 776ead6..ca22b7e 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -1,9 +1,6 @@ module PortAudio -# simple interface export play - -# AudioNodes export SinOsc, AudioMixer, ArrayPlayer, AudioInput typealias PaTime Cdouble @@ -18,13 +15,12 @@ _stream = nothing ################## Types #################### -# A node in the render tree -abstract AudioNode - typealias AudioSample Float32 # A frame of audio, possibly multi-channel typealias AudioBuf Array{AudioSample} +# A node in the render tree +abstract AudioNode # Info about the hardware device type DeviceInfo @@ -32,99 +28,24 @@ type DeviceInfo buf_size::Integer end +include("nodes.jl") + type AudioStream - # TODO: this union may have performance penalties - root_node::Union(AudioNode, Nothing) + mixer::AudioMixer info::DeviceInfo function AudioStream(sample_rate, buf_size) - new(nothing, DeviceInfo(sample_rate, buf_size)) + mixer = AudioMixer() + new(mixer, DeviceInfo(sample_rate, buf_size)) end end -function render(node::Nothing, device_input::AudioBuf, info::DeviceInfo) - return zeros(AudioSample, info.buf_size) -end - -#### SinOsc #### - -# Generates a sin tone at the given frequency - -type SinOsc <: AudioNode - freq::Real - phase::FloatingPoint - - function SinOsc(freq::Real) - new(freq, 0.0) - end -end - -function render(node::SinOsc, device_input::AudioBuf, info::DeviceInfo) - phase = AudioSample[1:info.buf_size] * 2pi * node.freq / info.sample_rate - phase += node.phase - node.phase = phase[end] - return sin(phase) -end - -#### AudioMixer #### - -# Mixes a set of inputs equally - -type AudioMixer <: AudioNode - mix_inputs::Array{AudioNode} -end - -function render(node::AudioMixer, device_input::AudioBuf, info::DeviceInfo) - # TODO: we may want to pre-allocate this buffer and share between render - # calls - mix_buffer = zeros(AudioSample, info.buf_size) - for in_node in node.mix_inputs - mix_buffer += render(in_node, device_input, info) - end -end - -#### Array Player #### - -# Plays a AudioBuf by rendering it out piece-by-piece - -type ArrayPlayer <: AudioNode - arr::AudioBuf - arr_index::Int - - function ArrayPlayer(arr::AudioBuf) - new(arr, 1) - end -end - -function render(node::ArrayPlayer, device_input::AudioBuf, info::DeviceInfo) - i = node.arr_index - range_end = min(i + info.buf_size-1, length(node.arr)) - output = node.arr[i:range_end] - if length(output) < info.buf_size - output = vcat(output, zeros(AudioSample, info.buf_size - length(output))) - end - node.arr_index = range_end + 1 - return output -end - -#### AudioInput #### - -# Renders incoming audio input from the hardware - -type AudioInput <: AudioNode - channel::Int -end - -function render(node::AudioInput, device_input::AudioBuf, info::DeviceInfo) - @assert size(device_input, 1) == info.buf_size - return device_input[:, node.channel] -end ############ Exported Functions ############# - function play(node::AudioNode, stream::AudioStream) - stream.root_node = node + # TODO: don't break demeter + stream.mixer = vcat(stream.mixer.mix_inputs, [node]) end function play(node::AudioNode) @@ -196,7 +117,7 @@ function audio_task(jl_filedesc::Integer, stream::AudioStream) jl_stream = fdio(jl_filedesc) jl_rawfd = RawFD(jl_filedesc) while true - out_array = render(stream.root_node, in_array, stream.info)::AudioBuf + 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 diff --git a/src/nodes.jl b/src/nodes.jl new file mode 100644 index 0000000..b5e3c18 --- /dev/null +++ b/src/nodes.jl @@ -0,0 +1,82 @@ +#### SinOsc #### + +# Generates a sin tone at the given frequency + +type SinOsc <: AudioNode + freq::Real + phase::FloatingPoint + + function SinOsc(freq::Real) + new(freq, 0.0) + end +end + +function render(node::SinOsc, device_input::AudioBuf, info::DeviceInfo) + phase = AudioSample[1:info.buf_size] * 2pi * node.freq / info.sample_rate + phase += node.phase + node.phase = phase[end] + return sin(phase) +end + +#### AudioMixer #### + +# Mixes a set of inputs equally + +type AudioMixer <: AudioNode + mix_inputs::Array{AudioNode} + + function AudioMixer{T <: AudioNode}(mix_inputs::Array{T}) + new(mix_inputs) + end + + function AudioMixer() + new(AudioNode[]) + end +end + +function render(node::AudioMixer, device_input::AudioBuf, info::DeviceInfo) + # TODO: we may want to pre-allocate this buffer and share between render + # calls + mix_buffer = zeros(AudioSample, info.buf_size) + for in_node in node.mix_inputs + mix_buffer += render(in_node, device_input, info) + end + return mix_buffer +end + +#### Array Player #### + +# Plays a AudioBuf by rendering it out piece-by-piece + +type ArrayPlayer <: AudioNode + arr::AudioBuf + arr_index::Int + + function ArrayPlayer(arr::AudioBuf) + new(arr, 1) + end +end + +function render(node::ArrayPlayer, device_input::AudioBuf, info::DeviceInfo) + i = node.arr_index + range_end = min(i + info.buf_size-1, length(node.arr)) + output = node.arr[i:range_end] + if length(output) < info.buf_size + output = vcat(output, zeros(AudioSample, info.buf_size - length(output))) + end + node.arr_index = range_end + 1 + return output +end + +#### AudioInput #### + +# Renders incoming audio input from the hardware + +type AudioInput <: AudioNode + channel::Int +end + +function render(node::AudioInput, device_input::AudioBuf, info::DeviceInfo) + @assert size(device_input, 1) == info.buf_size + return device_input[:, node.channel] +end diff --git a/test/nodes.jl b/test/nodes.jl new file mode 100644 index 0000000..a85acbe --- /dev/null +++ b/test/nodes.jl @@ -0,0 +1,35 @@ +using Base.Test +using PortAudio + +info = PortAudio.DeviceInfo(44100, 512) +dev_input = zeros(PortAudio.AudioSample, info.buf_size) + +# A TestNode just renders out 1:buf_size each frame +type TestNode <: PortAudio.AudioNode +end + +function PortAudio.render(node::TestNode, + device_input::PortAudio.AudioBuf, + info::PortAudio.DeviceInfo) + return PortAudio.AudioSample[1:info.buf_size] +end + +#### AudioMixer Tests #### + +# TODO: there should be a setup/teardown mechanism and some way to isolate +# tests + +mix = AudioMixer() +@test mix.mix_inputs == PortAudio.AudioNode[] +@test PortAudio.render(mix, dev_input, info) == zeros(PortAudio.AudioSample, info.buf_size) + +testnode = TestNode() +mix = AudioMixer([testnode]) +@test mix.mix_inputs == PortAudio.AudioNode[testnode] +@test PortAudio.render(mix, dev_input, info) == PortAudio.AudioSample[1:info.buf_size] + +test1 = TestNode() +test2 = TestNode() +mix = AudioMixer([test1, test2]) +@test PortAudio.render(mix, dev_input, info) == 2 * PortAudio.AudioSample[1:info.buf_size] +