adds tests and support for playing more array types

This commit is contained in:
Spencer Russell 2013-12-30 01:00:04 -08:00
parent 6c8b99407c
commit 39bae583ed
3 changed files with 100 additions and 16 deletions

View file

@ -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()
# 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
play(arr, _stream)
# 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]

View file

@ -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]

60
test/test_PortAudio.jl Normal file
View file

@ -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)))