From 0c36e1eec5e2a86e2510e400b4854000b79f2d2b Mon Sep 17 00:00:00 2001 From: Spencer Russell Date: Thu, 11 May 2017 00:58:49 -0400 Subject: [PATCH] mostly adding tests and fixing bugs. passing tests now --- deps/src/pa_shim.c | 51 +++++--- src/PortAudio.jl | 57 ++++---- src/libportaudio.jl | 4 +- test/runtests.jl | 313 ++++++++++++++++++++++++++++++++------------ 4 files changed, 293 insertions(+), 132 deletions(-) diff --git a/deps/src/pa_shim.c b/deps/src/pa_shim.c index 4317568..4cdecdc 100644 --- a/deps/src/pa_shim.c +++ b/deps/src/pa_shim.c @@ -60,34 +60,43 @@ int pa_shim_processcb(const void *input, void *output, if(info->notifycb == NULL) { fprintf(stderr, "pa_shim ERROR: notifycb is NULL\n"); } - - int nwrite = PaUtil_GetRingBufferWriteAvailable(info->inputbuf); - int nread = PaUtil_GetRingBufferReadAvailable(info->outputbuf); - nwrite = MIN(frameCount, nwrite); - nread = MIN(frameCount, nread); - if(info->sync) { + int nwrite; + if(info->inputbuf) { + nwrite = PaUtil_GetRingBufferWriteAvailable(info->inputbuf); + nwrite = MIN(frameCount, nwrite); + } + int nread; + if(info->outputbuf) { + nread = PaUtil_GetRingBufferReadAvailable(info->outputbuf); + nread = MIN(frameCount, nread); + } + if(info->inputbuf && info->outputbuf && info->sync) { // to keep the buffers synchronized, set readable and writable to // their minimum value nread = MIN(nread, nwrite); nwrite = nread; } // read/write from the ringbuffers - PaUtil_WriteRingBuffer(info->inputbuf, input, nwrite); - if(info->notifycb) { - info->notifycb(info->inputhandle); + if(info->inputbuf) { + PaUtil_WriteRingBuffer(info->inputbuf, input, nwrite); + if(info->notifycb) { + info->notifycb(info->inputhandle); + } + if(nwrite < frameCount) { + senderr(info, PA_SHIM_ERRMSG_OVERFLOW); + } } - PaUtil_ReadRingBuffer(info->outputbuf, output, nread); - if(info->notifycb) { - info->notifycb(info->outputhandle); - } - if(nwrite < frameCount) { - senderr(info, PA_SHIM_ERRMSG_OVERFLOW); - } - if(nread < frameCount) { - senderr(info, PA_SHIM_ERRMSG_UNDERFLOW); - // we didn't fill the whole output buffer, so zero it out - memset(output+nread*info->outputbuf->elementSizeBytes, 0, - (frameCount - nread)*info->outputbuf->elementSizeBytes); + if(info->outputbuf) { + PaUtil_ReadRingBuffer(info->outputbuf, output, nread); + if(info->notifycb) { + info->notifycb(info->outputhandle); + } + if(nread < frameCount) { + senderr(info, PA_SHIM_ERRMSG_UNDERFLOW); + // we didn't fill the whole output buffer, so zero it out + memset(output+nread*info->outputbuf->elementSizeBytes, 0, + (frameCount - nread)*info->outputbuf->elementSizeBytes); + } } return paContinue; diff --git a/src/PortAudio.jl b/src/PortAudio.jl index d81d462..46d3daf 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -8,6 +8,10 @@ using Suppressor using Base: AsyncCondition +import Base: eltype, show +import Base: close, isopen +import Base: read, read!, write, flush + # Get binary dependencies loaded from BinDeps include("../deps/deps.jl") include("pa_shim.jl") @@ -20,7 +24,6 @@ function __init__() @suppress_err Pa_Initialize() end - export PortAudioStream # These sizes are all in frames @@ -41,7 +44,7 @@ const ERR_BUFSIZE=512 function versioninfo(io::IO=STDOUT) println(io, Pa_GetVersionText()) println(io, "Version: ", Pa_GetVersion()) - println(io, "Shim Version: ", shimversion()) + println(io, "Shim Source Hash: ", shimhash()[1:10]) end type PortAudioDevice @@ -104,13 +107,15 @@ type PortAudioStream{T} # we've got a synchronized duplex stream. initialize with the output buffer full write(this.sink, SampleBuf(zeros(T, blocksize*2, outchans), sr)) end - this.bufinfo = pa_shim_info_t(bufpointer(this.source), - bufpointer(this.sink), - pointer(this.errbuf), - synced, notifycb_c, - getnotifyhandle(this.sink), - getnotifyhandle(this.source), - getnotifyhandle(this.errbuf)) + # pass NULL for input/output we're not using + this.bufinfo = pa_shim_info_t( + inchans > 0 ? bufpointer(this.source) : C_NULL, + outchans > 0 ? bufpointer(this.sink) : C_NULL, + pointer(this.errbuf), + synced, notifycb_c, + inchans > 0 ? notifyhandle(this.source) : C_NULL, + outchans > 0 ? notifyhandle(this.sink) : C_NULL, + notifyhandle(this.errbuf)) this.stream = @suppress_err Pa_OpenStream(inparams, outparams, float(sr), blocksize, paNoFlag, shim_processcb_c, @@ -184,7 +189,7 @@ function PortAudioStream(inchans=2, outchans=2; kwargs...) PortAudioStream(indevice, outdevice, inchans, outchans; kwargs...) end -function Base.close(stream::PortAudioStream) +function close(stream::PortAudioStream) if stream.stream != C_NULL Pa_StopStream(stream.stream) Pa_CloseStream(stream.stream) @@ -196,18 +201,18 @@ function Base.close(stream::PortAudioStream) nothing end -Base.isopen(stream::PortAudioStream) = stream.stream != C_NULL +isopen(stream::PortAudioStream) = stream.stream != C_NULL SampledSignals.samplerate(stream::PortAudioStream) = stream.samplerate -Base.eltype{T}(stream::PortAudioStream{T}) = T +eltype{T}(stream::PortAudioStream{T}) = T -Base.read(stream::PortAudioStream, args...) = read(stream.source, args...) -Base.read!(stream::PortAudioStream, args...) = read!(stream.source, args...) -Base.write(stream::PortAudioStream, args...) = write(stream.sink, args...) -Base.write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...) -Base.flush(stream::PortAudioStream) = flush(stream.sink) +read(stream::PortAudioStream, args...) = read(stream.source, args...) +read!(stream::PortAudioStream, args...) = read!(stream.source, args...) +write(stream::PortAudioStream, args...) = write(stream.sink, args...) +write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...) +flush(stream::PortAudioStream) = flush(stream.sink) -function Base.show(io::IO, stream::PortAudioStream) +function show(io::IO, stream::PortAudioStream) println(io, typeof(stream)) println(io, " Samplerate: ", samplerate(stream), "Hz") print(io, " Buffer Size: ", stream.blocksize, " frames") @@ -272,23 +277,19 @@ end SampledSignals.nchannels(s::Union{PortAudioSink, PortAudioSource}) = s.nchannels SampledSignals.samplerate(s::Union{PortAudioSink, PortAudioSource}) = samplerate(s.stream) SampledSignals.blocksize(s::Union{PortAudioSink, PortAudioSource}) = s.stream.blocksize -Base.eltype(::Union{PortAudioSink{T}, PortAudioSource{T}}) where {T} = T -Base.close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf) -Base.isopen(s::Union{PortAudioSink, PortAudioSource}) = isopen(s.ringbuf) -RingBuffers.getnotifyhandle(s::Union{PortAudioSink, PortAudioSource}) = getnotifyhandle(s.ringbuf) +eltype(::Union{PortAudioSink{T}, PortAudioSource{T}}) where {T} = T +close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf) +isopen(s::Union{PortAudioSink, PortAudioSource}) = isopen(s.ringbuf) +RingBuffers.notifyhandle(s::Union{PortAudioSink, PortAudioSource}) = notifyhandle(s.ringbuf) bufpointer(s::Union{PortAudioSink, PortAudioSource}) = pointer(s.ringbuf) name(s::Union{PortAudioSink, PortAudioSource}) = s.name -function Base.show(io::IO, stream::T) where {T <: Union{PortAudioSink, PortAudioSource}} +function show(io::IO, stream::T) where {T <: Union{PortAudioSink, PortAudioSource}} println(io, T, "(\"", stream.name, "\")") print(io, nchannels(stream), " channels") end -# function Base.flush(sink::PortAudioSink) -# while nwritable(sink.ringbuf) < length(sink.ringbuf) -# wait(sink.ringbuf) -# end -# end +flush(sink::PortAudioSink) = flush(sink.ringbuf) function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount) nwritten = 0 diff --git a/src/libportaudio.jl b/src/libportaudio.jl index 2ce996c..ffae4ea 100644 --- a/src/libportaudio.jl +++ b/src/libportaudio.jl @@ -240,11 +240,11 @@ function handle_status(err::PaError, show_warnings::Bool=true) if show_warnings msg = ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err) - warn("libportaudio: " * bytestring(msg)) + warn("libportaudio: " * unsafe_string(msg)) end elseif err != PA_NO_ERROR msg = ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err) - error("libportaudio: " * bytestring(msg)) + error("libportaudio: " * unsafe_string(msg)) end end diff --git a/test/runtests.jl b/test/runtests.jl index c208e42..e38f869 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,86 +1,193 @@ #!/usr/bin/env julia -if VERSION >= v"0.5.0-dev+7720" - using Base.Test -else - using BaseTestNext -end +using Base.Test +using TestSetExtensions using PortAudio using SampledSignals using RingBuffers -function test_callback(inchans, outchans) - nframes = Culong(8) +# pull in some extra stuff we need to test the callback directly +using PortAudio: notifyhandle, notifycb_c, shim_processcb_c +using PortAudio: pa_shim_errmsg_t, pa_shim_info_t +using PortAudio: PA_SHIM_ERRMSG_ERR_OVERFLOW, PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW - cb = PortAudio.pa_callbacks[Float32] - inbuf = rand(Float32, inchans*nframes) # simulate microphone input - sourcebuf = LockFreeRingBuffer(Float32, inchans*nframes*8) # the microphone input should end up here +"Setup buffers to test callback behavior" +function setup_callback(inchans, outchans, nframes, synced) + sourcebuf = RingBuffer{Float32}(inchans, nframes*2) # the microphone input should end up here + sinkbuf = RingBuffer{Float32}(outchans, nframes*2) # the callback should copy this to cb_output + errbuf = RingBuffer{pa_shim_errmsg_t}(1, 8) - outbuf = zeros(Float32, outchans*nframes) # this is where the output should go - sinkbuf = LockFreeRingBuffer(Float32, outchans*nframes*8) # the callback should copy this to outbuf - - # 2 input channels, 3 output channels - info = PortAudio.CallbackInfo(inchans, sourcebuf, outchans, sinkbuf, true) - - # handle any conversions here so they don't mess with the allocation - # the seemingly-redundant type specifiers avoid some allocation during the ccall. - # might be due to https://github.com/JuliaLang/julia/issues/15276 - inptr::Ptr{Float32} = Ptr{Float32}(pointer(inbuf)) - outptr::Ptr{Float32} = Ptr{Float32}(pointer(outbuf)) + # pass NULL for i/o we're not using + info = pa_shim_info_t( + inchans > 0 ? pointer(sourcebuf) : C_NULL, + outchans > 0 ? pointer(sinkbuf) : C_NULL, + pointer(errbuf), + synced, notifycb_c, + inchans > 0 ? notifyhandle(sourcebuf) : C_NULL, + outchans > 0 ? notifyhandle(sinkbuf) : C_NULL, + notifyhandle(errbuf) + ) flags = Culong(0) - infoptr::Ptr{PortAudio.CallbackInfo{Float32}} = Ptr{PortAudio.CallbackInfo{Float32}}(pointer_from_objref(info)) - testin = zeros(Float32, inchans*nframes) - testout = rand(Float32, outchans*nframes) - write(sinkbuf, testout) # fill the output ringbuffer - ret = ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test ret === PortAudio.paContinue - @test outbuf == testout - read!(sourcebuf, testin) - @test inbuf == testin + cb_input = rand(Float32, inchans, nframes) # simulate microphone input + cb_output = rand(Float32, outchans, nframes) # this is where the output should go + function processfunc() + ccall(shim_processcb_c, Cint, + (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{Void}), + cb_input, cb_output, nframes, C_NULL, flags, pointer_from_objref(info)) + end + + (sourcebuf, sinkbuf, errbuf, cb_input, cb_output, processfunc) +end + +function test_callback(inchans, outchans, synced) + nframes = 8 + (sourcebuf, sinkbuf, errbuf, + cb_input, cb_output, process) = setup_callback(inchans, outchans, + nframes, synced) if outchans > 0 - underfill = 3 # should be less than nframes - testout = rand(Float32, outchans*underfill) - write(sinkbuf, testout) # underfill the output ringbuffer - # call again (partial underrun) - ret = ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test ret === PortAudio.paContinue - @test outbuf[1:outchans*underfill] == testout - @test outbuf[outchans*underfill+1:outchans*nframes] == zeros(Float32, (nframes-underfill)*outchans) - @test nreadable(sourcebuf) == inchans*underfill - @test read!(sourcebuf, testin) == inchans*underfill - @test testin[1:inchans*underfill] == inbuf[1:inchans*underfill] - - # call again (total underrun) - ret = ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test ret === PortAudio.paContinue - @test outbuf == zeros(Float32, outchans*nframes) - @test nreadable(sourcebuf) == 0 - + testout = rand(Float32, outchans, nframes) # generate some test data to play write(sinkbuf, testout) # fill the output ringbuffer - # test allocation - alloc = @allocated ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test alloc == 0 - # now test allocation in underrun state - alloc = @allocated ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test alloc == 0 + end + @test process() == PortAudio.paContinue + if outchans > 0 + # testout -> sinkbuf -> cb_output + @test cb_output == testout + end + if inchans > 0 + # cb_input -> sourcebuf + @test read(sourcebuf, nframes) == cb_input + end + @test framesreadable(errbuf) == 0 +end + +""" + test_callback_underflow(inchans, outchans; nframes=8, underfill=3, synced=false) + +Test that the callback works on underflow conditions. underfill is the numer of +frames we feed in, which should be less than nframes. +""" +function test_callback_underflow(inchans, outchans, synced) + nframes = 8 + underfill = 3 # must be less than nframes + (sourcebuf, sinkbuf, errbuf, + cb_input, cb_output, process) = setup_callback(inchans, outchans, + nframes, synced) + outchans > 0 || error("Can't test underflow with no output") + testout = rand(Float32, outchans, underfill) + write(sinkbuf, testout) # underfill the output ringbuffer + # call callback (partial underflow) + @test process() == PortAudio.paContinue + @test cb_output[:, 1:underfill] == testout + @test cb_output[:, (underfill+1):nframes] == zeros(Float32, outchans, (nframes-underfill)) + errs = readavailable(errbuf) + if inchans > 0 + received = readavailable(sourcebuf) + if synced + @test size(received, 2) == underfill + @test received == cb_input[:, 1:underfill] + @test length(errs) == 2 + @test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW]) + else + @test size(received, 2) == nframes + @test received == cb_input + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW + end + else + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW + end + + # call again (total underflow) + @test process() == PortAudio.paContinue + @test cb_output == zeros(Float32, outchans, nframes) + errs = readavailable(errbuf) + if inchans > 0 + received = readavailable(sourcebuf) + if synced + @test size(received, 2) == 0 + @test length(errs) == 2 + @test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW]) + else + @test size(received, 2) == nframes + @test received == cb_input + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW + end + else + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW + end +end + +function test_callback_overflow(inchans, outchans, synced) + nframes = 8 + (sourcebuf, sinkbuf, errbuf, + cb_input, cb_output, process) = setup_callback(inchans, outchans, + nframes, synced) + inchans > 0 || error("Can't test overflow with no input") + @test frameswritable(sinkbuf) == nframes*2 + + # the first time it should half-fill the input ring buffer + if outchans > 0 + testout = rand(Float32, outchans, nframes) + write(sinkbuf, testout) + end + @test framesreadable(sourcebuf) == 0 + outchans > 0 && @test frameswritable(sinkbuf) == nframes + @test process() == PortAudio.paContinue + @test framesreadable(errbuf) == 0 + @test framesreadable(sourcebuf) == nframes + outchans > 0 && @test frameswritable(sinkbuf) == nframes*2 + + # now run the process func again to completely fill the input ring buffer + outchans > 0 && write(sinkbuf, testout) + @test framesreadable(sourcebuf) == nframes + outchans > 0 && @test frameswritable(sinkbuf) == nframes + @test process() == PortAudio.paContinue + @test framesreadable(errbuf) == 0 + @test framesreadable(sourcebuf) == nframes*2 + outchans > 0 && @test frameswritable(sinkbuf) == nframes*2 + + # now this time the process func should overflow the input buffer + outchans > 0 && write(sinkbuf, testout) + @test framesreadable(sourcebuf) == nframes*2 + outchans > 0 && @test frameswritable(sinkbuf) == nframes + @test process() == PortAudio.paContinue + @test framesreadable(sourcebuf) == nframes*2 + errs = readavailable(errbuf) + if outchans > 0 + if synced + # if input and output are synced, thec callback didn't pull from + # the output ringbuf + @test frameswritable(sinkbuf) == nframes + @test cb_output == zeros(Float32, outchans, nframes) + @test length(errs) == 2 + @test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW]) + else + @test frameswritable(sinkbuf) == nframes*2 + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_OVERFLOW + end + else + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_OVERFLOW end end # these test are currently set up to run on OSX -@testset "PortAudio Tests" begin +@testset DottedTestSet "PortAudio Tests" begin + if is_windows() + default_indev = "Microphone Array (Realtek High " + default_outdev = "Speaker/Headphone (Realtek High" + elseif is_apple() + default_indev = "Built-in Microph" + default_outdev = "Built-in Output" + end + devs = PortAudio.devices() i = findfirst(d -> d.maxinchans > 0, devs) indev = i > 0 ? devs[i] : nothing @@ -92,24 +199,68 @@ end @testset "Reports version" begin io = IOBuffer() PortAudio.versioninfo(io) - result = takebuf_string(io) + result = split(String(take!((io))), "\n") # make sure this is the same version I tested with - @test result == - """PortAudio V19-devel (built Aug 6 2014 17:54:39) - Version Number: 1899 - """ + @test startswith(result[1], "PortAudio V19-devel") + @test result[2] == "Version: 1899" + @test result[3] == "Shim Source Hash: 4bfafb6888" end - @testset "PortAudio Callback works for duplex stream" begin - test_callback(2, 3) + @testset "Basic callback functionality" begin + @testset "basic duplex (no sync)" begin + test_callback(2, 3, false) + end + @testset "basic input-only (no sync)" begin + test_callback(2, 0, false) + end + @testset "basic output-only (no sync)" begin + test_callback(0, 2, false) + end + @testset "basic no input or output (no sync)" begin + test_callback(0, 0, false) + end + @testset "basic duplex (sync)" begin + test_callback(2, 3, true) + end + @testset "basic input-only (sync)" begin + test_callback(2, 0, true) + end + @testset "basic output-only (sync)" begin + test_callback(0, 2, true) + end + @testset "basic no input or output (sync)" begin + test_callback(0, 0, true) + end end - @testset "Callback works with input-only stream" begin - test_callback(2, 0) + @testset "Ouput underflow" begin + @testset "underflow duplex (nosync)" begin + test_callback_underflow(2, 3, false) + end + @testset "underflow output-only (nosync)" begin + test_callback_underflow(0, 3, false) + end + @testset "underflow duplex (sync)" begin + test_callback_underflow(2, 3, true) + end + @testset "underflow output-only (sync)" begin + test_callback_underflow(0, 3, true) + end end - @testset "Callback works with output-only stream" begin - test_callback(0, 2) + @testset "Input overflow" begin + @testset "overflow duplex (nosync)" begin + test_callback_overflow(2, 3, false) + end + @testset "overflow input-only (nosync)" begin + test_callback_overflow(2, 0, false) + end + @testset "overflow duplex (sync)" begin + test_callback_overflow(2, 3, true) + end + @testset "overflow input-only (sync)" begin + test_callback_overflow(2, 0, true) + end end @testset "Open Default Device" begin @@ -139,18 +290,18 @@ end close(stream) end @testset "Open Device by name" begin - stream = PortAudioStream("Built-in Microph", "Built-in Output") + stream = PortAudioStream(default_indev, default_outdev) buf = read(stream, 0.001s) @test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source)) write(stream, buf) io = IOBuffer() show(io, stream) - @test takebuf_string(io) == """ + @test String(take!(io)) == """ PortAudio.PortAudioStream{Float32} - Samplerate: 48000.0Hz + Samplerate: 44100.0Hz Buffer Size: 4096 frames - 2 channel sink: "Built-in Output" - 2 channel source: "Built-in Microph\"""" + 2 channel sink: "$default_outdev" + 2 channel source: "$default_indev\"""" close(stream) end @testset "Error on wrong name" begin