a little restructuring and starting to add some tests
This commit is contained in:
parent
3a29b40c97
commit
5a2222ccea
3 changed files with 127 additions and 89 deletions
|
@ -1,9 +1,6 @@
|
||||||
module PortAudio
|
module PortAudio
|
||||||
|
|
||||||
# simple interface
|
|
||||||
export play
|
export play
|
||||||
|
|
||||||
# AudioNodes
|
|
||||||
export SinOsc, AudioMixer, ArrayPlayer, AudioInput
|
export SinOsc, AudioMixer, ArrayPlayer, AudioInput
|
||||||
|
|
||||||
typealias PaTime Cdouble
|
typealias PaTime Cdouble
|
||||||
|
@ -18,13 +15,12 @@ _stream = nothing
|
||||||
|
|
||||||
################## Types ####################
|
################## Types ####################
|
||||||
|
|
||||||
# A node in the render tree
|
|
||||||
abstract AudioNode
|
|
||||||
|
|
||||||
typealias AudioSample Float32
|
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
|
||||||
|
abstract AudioNode
|
||||||
|
|
||||||
# Info about the hardware device
|
# Info about the hardware device
|
||||||
type DeviceInfo
|
type DeviceInfo
|
||||||
|
@ -32,99 +28,24 @@ type DeviceInfo
|
||||||
buf_size::Integer
|
buf_size::Integer
|
||||||
end
|
end
|
||||||
|
|
||||||
|
include("nodes.jl")
|
||||||
|
|
||||||
type AudioStream
|
type AudioStream
|
||||||
# TODO: this union may have performance penalties
|
mixer::AudioMixer
|
||||||
root_node::Union(AudioNode, Nothing)
|
|
||||||
info::DeviceInfo
|
info::DeviceInfo
|
||||||
|
|
||||||
function AudioStream(sample_rate, buf_size)
|
function AudioStream(sample_rate, buf_size)
|
||||||
new(nothing, DeviceInfo(sample_rate, buf_size))
|
mixer = AudioMixer()
|
||||||
|
new(mixer, DeviceInfo(sample_rate, buf_size))
|
||||||
end
|
end
|
||||||
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 #############
|
############ Exported Functions #############
|
||||||
|
|
||||||
|
|
||||||
function play(node::AudioNode, stream::AudioStream)
|
function play(node::AudioNode, stream::AudioStream)
|
||||||
stream.root_node = node
|
# TODO: don't break demeter
|
||||||
|
stream.mixer = vcat(stream.mixer.mix_inputs, [node])
|
||||||
end
|
end
|
||||||
|
|
||||||
function play(node::AudioNode)
|
function play(node::AudioNode)
|
||||||
|
@ -196,7 +117,7 @@ function audio_task(jl_filedesc::Integer, stream::AudioStream)
|
||||||
jl_stream = fdio(jl_filedesc)
|
jl_stream = fdio(jl_filedesc)
|
||||||
jl_rawfd = RawFD(jl_filedesc)
|
jl_rawfd = RawFD(jl_filedesc)
|
||||||
while true
|
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 the C code so it knows we've given it some more data
|
||||||
wake_callback_thread(out_array)
|
wake_callback_thread(out_array)
|
||||||
# wait for new data to be available from the sound card (and for it to
|
# wait for new data to be available from the sound card (and for it to
|
||||||
|
|
82
src/nodes.jl
Normal file
82
src/nodes.jl
Normal file
|
@ -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
|
35
test/nodes.jl
Normal file
35
test/nodes.jl
Normal file
|
@ -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]
|
||||||
|
|
Loading…
Reference in a new issue