diff --git a/README.md b/README.md index 36e6526..70f784a 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which g ## Opening a stream -The easiest way to open a source or sink is with the default `PortAudioStream()` constructor, which will open a 0-in, 2-out stream to your system's default device(s). The constructor can also take the input and output channel counts as positional arguments, or a variety of other keyword arguments. +The easiest way to open a source or sink is with the default `PortAudioStream()` constructor, which will open a 2-in, 2-out stream to your system's default device(s). The constructor can also take the input and output channel counts as positional arguments, or a variety of other keyword arguments. ```julia -PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000Hz, blocksize=4096) +PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000Hz, blocksize=4096, synced=false) ``` You can open a specific device by adding it as the first argument, either as a `PortAudioDevice` instance or by name. You can also give separate names or devices if you want different input and output devices diff --git a/src/PortAudio.jl b/src/PortAudio.jl index 3590d9d..880cc37 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -87,6 +87,7 @@ immutable CallbackInfo{T} inbuf::LockFreeRingBuffer{T} outchannels::Int outbuf::LockFreeRingBuffer{T} + synced::Bool end # paramaterized on the sample type and sampling rate type @@ -101,7 +102,7 @@ type PortAudioStream{T, U} # this inner constructor is generally called via the top-level outer # constructor below function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice, - inchans, outchans, sr, blocksize) + inchans, outchans, sr, blocksize, synced) inchans = inchans == -1 ? indev.maxinchans : inchans outchans = outchans == -1 ? outdev.maxoutchans : outchans inparams = (inchans == 0) ? @@ -114,12 +115,12 @@ type PortAudioStream{T, U} finalizer(this, close) this.sink = PortAudioSink{T, U}(outdev.name, this, outchans, DEFAULT_RINGBUFSIZE) this.source = PortAudioSource{T, U}(indev.name, this, inchans, DEFAULT_RINGBUFSIZE) - if inchans > 0 && outchans > 0 - # we've got a duplex stream. initialize with the output buffer full + if synced && inchans > 0 && outchans > 0 + # we've got a synchronized duplex stream. initialize with the output buffer full write(this.sink, SampleBuf(zeros(T, DEFAULT_PREFILL, outchans), sr)) end this.bufinfo = CallbackInfo(inchans, this.source.ringbuf, - outchans, this.sink.ringbuf) + outchans, this.sink.ringbuf, synced) this.stream = Pa_OpenStream(inparams, outparams, float(sr), blocksize, paNoFlag, pa_callbacks[T], fieldptr(this, :bufinfo)) @@ -132,19 +133,22 @@ end # this is the top-level outer constructor that all the other outer constructors # end up calling function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice, - inchans=-1, outchans=-1; eltype=Float32, samplerate=-1, blocksize=DEFAULT_BLOCKSIZE) + inchans=2, outchans=2; eltype=Float32, samplerate=-1, blocksize=DEFAULT_BLOCKSIZE, synced=false) if samplerate == -1 sampleratein = rationalize(indev.defaultsamplerate) * Hz; samplerateout = rationalize(outdev.defaultsamplerate) * Hz; if inchans > 0 && outchans > 0 && sampleratein != samplerateout - error("Can't open duplex stream with mismatched samplerates") + error(""" + Can't open duplex stream with mismatched samplerates (in: $sampleratein, out: $samplerateout). + Try changing your sample rate in your driver settings or open separate input and output + streams""") elseif inchans > 0 samplerate = sampleratein else samplerate = samplerateout end end - PortAudioStream{eltype, typeof(samplerate)}(indev, outdev, inchans, outchans, samplerate, blocksize) + PortAudioStream{eltype, typeof(samplerate)}(indev, outdev, inchans, outchans, samplerate, blocksize, synced) end # handle device names given as streams @@ -171,17 +175,15 @@ end # if one device is given, use it for input and output, but set inchans=0 so we # end up with an output-only stream -function PortAudioStream(device::PortAudioDevice, inchans=-1, outchans=-1; kwargs...) - inchans = inchans == -1 ? 0 : inchans +function PortAudioStream(device::PortAudioDevice, inchans=2, outchans=2; kwargs...) PortAudioStream(device, device, inchans, outchans; kwargs...) end -function PortAudioStream(device::AbstractString, inchans=-1, outchans=-1; kwargs...) - inchans = inchans == -1 ? 0 : inchans +function PortAudioStream(device::AbstractString, inchans=2, outchans=2; kwargs...) PortAudioStream(device, device, inchans, outchans; kwargs...) end # use the default input and output devices -function PortAudioStream(inchans=0, outchans=-1; kwargs...) +function PortAudioStream(inchans=2, outchans=2; kwargs...) inidx = Pa_GetDefaultInputDevice() indevice = PortAudioDevice(Pa_GetDeviceInfo(inidx), inidx) outidx = Pa_GetDefaultOutputDevice() @@ -310,32 +312,24 @@ end function portaudio_callback{T}(inptr::Ptr{T}, outptr::Ptr{T}, nframes, timeinfo, flags, userdata::Ptr{CallbackInfo{T}}) info = unsafe_load(userdata) - insamples = nframes * info.inchannels - outsamples = nframes * info.outchannels - bufsamples = if insamples == UInt(0) && outsamples > UInt(0) - # playback-only - nreadable(info.outbuf) - elseif insamples > UInt(0) && outsamples == UInt(0) - # record-only - nwritable(info.inbuf) - elseif insamples > UInt(0) && outsamples > UInt(0) - # duplex - min(nreadable(info.outbuf), nwritable(info.inbuf)) - else - UInt(0) + # if there are no channels, treat it as if we can write as many 0-frame channels as we want + framesreadable = info.outchannels > 0 ? div(nreadable(info.outbuf), info.outchannels) : nframes + frameswritable = info.inchannels > 0 ? div(nwritable(info.inbuf), info.inchannels) : nframes + if info.synced + framesreadable = min(framesreadable, frameswritable) + frameswritable = framesreadable end - - toread = min(bufsamples, outsamples) - towrite = min(bufsamples, insamples) + towrite = min(frameswritable, nframes) * info.inchannels + toread = min(framesreadable, nframes) * info.outchannels read!(info.outbuf, outptr, toread) write(info.inbuf, inptr, towrite) - if toread < outsamples + if framesreadable < nframes + outsamples = nframes * info.outchannels # xrun, copy zeros to outbuffer # TODO: send a notification to an error msg ringbuf memset(outptr+sizeof(T)*toread, 0, sizeof(T)*(outsamples-toread)) - return paContinue end paContinue diff --git a/test/runtests.jl b/test/runtests.jl index 4b90f22..a61ff6b 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,6 +9,75 @@ using PortAudio using SampledSignals using RingBuffers +function test_callback(inchans, outchans) + nframes = Culong(8) + + 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 + + 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)) + 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 + + 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 + + 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 +end + # these test are currently set up to run on OSX @testset "PortAudio Tests" begin @@ -31,69 +100,16 @@ using RingBuffers """ end - @testset "PortAudio Callback works and doesn't allocate" begin - cb = PortAudio.pa_callbacks[Float32] - inbuf = rand(Float32, 16) # simulate microphone input - sourcebuf = LockFreeRingBuffer(Float32, 64) # the microphone input should end up here + @testset "PortAudio Callback works for duplex stream" begin + test_callback(2, 3) + end - outbuf = zeros(Float32, 24) # this is where the output should go - sinkbuf = LockFreeRingBuffer(Float32, 64) # the callback should copy this to outbuf + @testset "Callback works with input-only stream" begin + test_callback(2, 0) + end - # 2 input channels, 3 output channels - info = PortAudio.CallbackInfo(2, sourcebuf, 3, sinkbuf) - - # 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)) - nframes = Culong(8) - flags = Culong(0) - infoptr::Ptr{PortAudio.CallbackInfo{Float32}} = Ptr{PortAudio.CallbackInfo{Float32}}(pointer_from_objref(info)) - - testin = zeros(Float32, 16) - testout = rand(Float32, 24) - 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 - - testout = rand(Float32, 10) - 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:10] == testout - @test outbuf[11:24] == zeros(Float32, 14) - @test nreadable(sourcebuf) == 10 - @test read!(sourcebuf, testin) == 10 - @test testin[1:10] == inbuf[1:10] - - # 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, 24) - @test nreadable(sourcebuf) == 0 - - 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 + @testset "Callback works with output-only stream" begin + test_callback(0, 2) end @testset "Open Default Device" begin @@ -130,8 +146,8 @@ using RingBuffers io = IOBuffer() show(io, stream) @test takebuf_string(io) == """ - PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}} - Samplerate: 48000 s⁻¹ + PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Rational{Int64},0,0,-1,0,0,0,0,0,0}} + Samplerate: 48000//1 s⁻¹ Buffer Size: 4096 frames 2 channel sink: "Built-in Output" 2 channel source: "Built-in Microph\""""