From 39bae583edb3f7d9b36ecfeeb5248bcc257ea65b Mon Sep 17 00:00:00 2001 From: Spencer Russell Date: Mon, 30 Dec 2013 01:00:04 -0800 Subject: [PATCH] adds tests and support for playing more array types --- src/PortAudio.jl | 54 ++++++++++++++++++++++++++----------- src/nodes.jl | 2 ++ test/test_PortAudio.jl | 60 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 test/test_PortAudio.jl diff --git a/src/PortAudio.jl b/src/PortAudio.jl index 673842f..e670212 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -22,6 +22,10 @@ typealias AudioBuf Array{AudioSample} # A node in the render tree abstract AudioNode +# A stream of audio (for instance that writes to hardware) +# All AudioStream subtypes should have a mixer and info field +abstract AudioStream + # Info about the hardware device type DeviceInfo sample_rate::Integer @@ -30,11 +34,11 @@ end include("nodes.jl") -type AudioStream +type PortAudioStream <: AudioStream mixer::AudioMixer info::DeviceInfo - function AudioStream(sample_rate, buf_size) + function PortAudioStream(sample_rate, buf_size) mixer = AudioMixer() new(mixer, DeviceInfo(sample_rate, buf_size)) end @@ -43,37 +47,55 @@ end ############ 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 - stream.mixer.mix_inputs = vcat(stream.mixer.mix_inputs, [node]) + append!(stream.mixer.mix_inputs, [node]) return nothing end +# If the stream is not given, use the default global stream function play(node::AudioNode) global _stream if _stream == nothing - _stream = open_stream() + _stream = open_portaudio_stream() end play(node, _stream) end -function play(arr::AudioBuf, stream::AudioStream) - # TODO: use a mixer as the root node so multiple playbacks get mixed +# Allow users to play a raw array by wrapping it in an ArrayPlayer +function play(arr::AudioBuf, args...) player = ArrayPlayer(arr) - play(player, stream) + play(player, args...) end -function play(arr::AudioBuf) - global _stream - if _stream == nothing - _stream = open_stream() - end - play(arr, _stream) +# If the array is the wrong floating type, convert it +function play{T <: FloatingPoint}(arr::Array{T}, args...) + arr = convert(AudioBuf, arr) + play(arr, args...) +end + +# If the array is an integer type, scale to [-1, 1] floating point + +# integer audio can be slightly (by 1) more negative than positive, +# so we just scale so that +/- typemax(T) becomes +/- 1 +function play{T <: Signed}(arr::Array{T}, args...) + arr = arr / typemax(T) + play(arr, args...) +end + +function play{T <: Unsigned}(arr::Array{T}, args...) + zero = (typemax(T) + 1) / 2 + range = floor(typemax(T) / 2) + arr = (arr - zero) / range + play(arr, args...) end ############ Internal Functions ############ -function open_stream(sample_rate::Int=44100, buf_size::Int=1024) +function open_portaudio_stream(sample_rate::Int=44100, buf_size::Int=1024) # TODO: handle more streams global _stream if _stream != nothing @@ -82,7 +104,7 @@ function open_stream(sample_rate::Int=44100, buf_size::Int=1024) # TODO: when we support multiple streams we won't set _stream here. # this is just to ensure that only one stream is ever opened - _stream = AudioStream(sample_rate, buf_size) + _stream = PortAudioStream(sample_rate, buf_size) fd = ccall((:make_pipe, libportaudio_shim), Cint, ()) @@ -111,7 +133,7 @@ function wake_callback_thread(out_array) out_array, size(out_array, 1)) end -function audio_task(jl_filedesc::Integer, stream::AudioStream) +function audio_task(jl_filedesc::Integer, stream::PortAudioStream) info("Audio Task Launched") in_array = zeros(AudioSample, stream.info.buf_size) desc_bytes = Cchar[0] diff --git a/src/nodes.jl b/src/nodes.jl index b5e3c18..3bf41cd 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -58,6 +58,8 @@ type ArrayPlayer <: AudioNode end function render(node::ArrayPlayer, device_input::AudioBuf, info::DeviceInfo) + # TODO: this should remove itself from the render tree when playback is + # complete i = node.arr_index range_end = min(i + info.buf_size-1, length(node.arr)) output = node.arr[i:range_end] diff --git a/test/test_PortAudio.jl b/test/test_PortAudio.jl new file mode 100644 index 0000000..a5adc8c --- /dev/null +++ b/test/test_PortAudio.jl @@ -0,0 +1,60 @@ +using Base.Test +using PortAudio + +const TEST_SAMPLERATE = 44100 +const TEST_BUF_SIZE = 1024 + +type TestAudioStream <: PortAudio.AudioStream + mixer::AudioMixer + info::PortAudio.DeviceInfo + + function TestAudioStream() + mixer = AudioMixer() + new(mixer, PortAudio.DeviceInfo(TEST_SAMPLERATE, TEST_BUF_SIZE)) + end +end + +# 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. +function process(stream::TestAudioStream) + in_array = zeros(PortAudio.AudioSample, stream.info.buf_size) + return PortAudio.render(stream.mixer, in_array, stream.info) +end + + +#### Test playing back various vector types #### + +# data shared between tests, for convenience +t = linspace(0, 2, 2 * 44100) +phase = 2pi * 100 * t + +## Test Float32 arrays, this is currently the native audio playback format +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(PortAudio.AudioSample, TEST_BUF_SIZE) + + +## Test Float64 arrays +f64 = convert(Array{Float64}, sin(phase)) +test_stream = TestAudioStream() +player = play(f64, test_stream) +@test process(test_stream) == convert(PortAudio.AudioBuf, f64[1:TEST_BUF_SIZE]) + +## Test Int8(Signed) arrays +i8 = Int8[-127:127] +test_stream = TestAudioStream() +player = play(i8, test_stream) +@test_approx_eq(process(test_stream)[1:255], + convert(PortAudio.AudioBuf, linspace(-1.0, 1.0, 255))) + +## Test Uint8(Unsigned) arrays +# for unsigned 8-bit audio silence is represented as 128, so the symmetric range +# is 1-255 +ui8 = Uint8[1:255] +test_stream = TestAudioStream() +player = play(ui8, test_stream) +@test_approx_eq(process(test_stream)[1:255], + convert(PortAudio.AudioBuf, linspace(-1.0, 1.0, 255)))