back to duplex-by-default but now with optional synchronization

This commit is contained in:
Spencer Russell 2016-08-16 18:10:03 -04:00
parent 7ea9da7e09
commit 1d5ca112eb
3 changed files with 105 additions and 95 deletions

View file

@ -7,10 +7,10 @@ PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which g
## Opening a stream ## 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 ```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 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

View file

@ -87,6 +87,7 @@ immutable CallbackInfo{T}
inbuf::LockFreeRingBuffer{T} inbuf::LockFreeRingBuffer{T}
outchannels::Int outchannels::Int
outbuf::LockFreeRingBuffer{T} outbuf::LockFreeRingBuffer{T}
synced::Bool
end end
# paramaterized on the sample type and sampling rate type # 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 # this inner constructor is generally called via the top-level outer
# constructor below # constructor below
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice, function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
inchans, outchans, sr, blocksize) inchans, outchans, sr, blocksize, synced)
inchans = inchans == -1 ? indev.maxinchans : inchans inchans = inchans == -1 ? indev.maxinchans : inchans
outchans = outchans == -1 ? outdev.maxoutchans : outchans outchans = outchans == -1 ? outdev.maxoutchans : outchans
inparams = (inchans == 0) ? inparams = (inchans == 0) ?
@ -114,12 +115,12 @@ type PortAudioStream{T, U}
finalizer(this, close) finalizer(this, close)
this.sink = PortAudioSink{T, U}(outdev.name, this, outchans, DEFAULT_RINGBUFSIZE) this.sink = PortAudioSink{T, U}(outdev.name, this, outchans, DEFAULT_RINGBUFSIZE)
this.source = PortAudioSource{T, U}(indev.name, this, inchans, DEFAULT_RINGBUFSIZE) this.source = PortAudioSource{T, U}(indev.name, this, inchans, DEFAULT_RINGBUFSIZE)
if inchans > 0 && outchans > 0 if synced && inchans > 0 && outchans > 0
# we've got a duplex stream. initialize with the output buffer full # we've got a synchronized duplex stream. initialize with the output buffer full
write(this.sink, SampleBuf(zeros(T, DEFAULT_PREFILL, outchans), sr)) write(this.sink, SampleBuf(zeros(T, DEFAULT_PREFILL, outchans), sr))
end end
this.bufinfo = CallbackInfo(inchans, this.source.ringbuf, 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, this.stream = Pa_OpenStream(inparams, outparams, float(sr), blocksize,
paNoFlag, pa_callbacks[T], fieldptr(this, :bufinfo)) 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 # this is the top-level outer constructor that all the other outer constructors
# end up calling # end up calling
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice, 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 if samplerate == -1
sampleratein = rationalize(indev.defaultsamplerate) * Hz; sampleratein = rationalize(indev.defaultsamplerate) * Hz;
samplerateout = rationalize(outdev.defaultsamplerate) * Hz; samplerateout = rationalize(outdev.defaultsamplerate) * Hz;
if inchans > 0 && outchans > 0 && sampleratein != samplerateout 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 elseif inchans > 0
samplerate = sampleratein samplerate = sampleratein
else else
samplerate = samplerateout samplerate = samplerateout
end end
end end
PortAudioStream{eltype, typeof(samplerate)}(indev, outdev, inchans, outchans, samplerate, blocksize) PortAudioStream{eltype, typeof(samplerate)}(indev, outdev, inchans, outchans, samplerate, blocksize, synced)
end end
# handle device names given as streams # 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 # if one device is given, use it for input and output, but set inchans=0 so we
# end up with an output-only stream # end up with an output-only stream
function PortAudioStream(device::PortAudioDevice, inchans=-1, outchans=-1; kwargs...) function PortAudioStream(device::PortAudioDevice, inchans=2, outchans=2; kwargs...)
inchans = inchans == -1 ? 0 : inchans
PortAudioStream(device, device, inchans, outchans; kwargs...) PortAudioStream(device, device, inchans, outchans; kwargs...)
end end
function PortAudioStream(device::AbstractString, inchans=-1, outchans=-1; kwargs...) function PortAudioStream(device::AbstractString, inchans=2, outchans=2; kwargs...)
inchans = inchans == -1 ? 0 : inchans
PortAudioStream(device, device, inchans, outchans; kwargs...) PortAudioStream(device, device, inchans, outchans; kwargs...)
end end
# use the default input and output devices # use the default input and output devices
function PortAudioStream(inchans=0, outchans=-1; kwargs...) function PortAudioStream(inchans=2, outchans=2; kwargs...)
inidx = Pa_GetDefaultInputDevice() inidx = Pa_GetDefaultInputDevice()
indevice = PortAudioDevice(Pa_GetDeviceInfo(inidx), inidx) indevice = PortAudioDevice(Pa_GetDeviceInfo(inidx), inidx)
outidx = Pa_GetDefaultOutputDevice() outidx = Pa_GetDefaultOutputDevice()
@ -310,32 +312,24 @@ end
function portaudio_callback{T}(inptr::Ptr{T}, outptr::Ptr{T}, function portaudio_callback{T}(inptr::Ptr{T}, outptr::Ptr{T},
nframes, timeinfo, flags, userdata::Ptr{CallbackInfo{T}}) nframes, timeinfo, flags, userdata::Ptr{CallbackInfo{T}})
info = unsafe_load(userdata) info = unsafe_load(userdata)
insamples = nframes * info.inchannels # if there are no channels, treat it as if we can write as many 0-frame channels as we want
outsamples = nframes * info.outchannels framesreadable = info.outchannels > 0 ? div(nreadable(info.outbuf), info.outchannels) : nframes
bufsamples = if insamples == UInt(0) && outsamples > UInt(0) frameswritable = info.inchannels > 0 ? div(nwritable(info.inbuf), info.inchannels) : nframes
# playback-only if info.synced
nreadable(info.outbuf) framesreadable = min(framesreadable, frameswritable)
elseif insamples > UInt(0) && outsamples == UInt(0) frameswritable = framesreadable
# record-only
nwritable(info.inbuf)
elseif insamples > UInt(0) && outsamples > UInt(0)
# duplex
min(nreadable(info.outbuf), nwritable(info.inbuf))
else
UInt(0)
end end
towrite = min(frameswritable, nframes) * info.inchannels
toread = min(bufsamples, outsamples) toread = min(framesreadable, nframes) * info.outchannels
towrite = min(bufsamples, insamples)
read!(info.outbuf, outptr, toread) read!(info.outbuf, outptr, toread)
write(info.inbuf, inptr, towrite) write(info.inbuf, inptr, towrite)
if toread < outsamples if framesreadable < nframes
outsamples = nframes * info.outchannels
# xrun, copy zeros to outbuffer # xrun, copy zeros to outbuffer
# TODO: send a notification to an error msg ringbuf # TODO: send a notification to an error msg ringbuf
memset(outptr+sizeof(T)*toread, 0, sizeof(T)*(outsamples-toread)) memset(outptr+sizeof(T)*toread, 0, sizeof(T)*(outsamples-toread))
return paContinue
end end
paContinue paContinue

View file

@ -9,6 +9,75 @@ using PortAudio
using SampledSignals using SampledSignals
using RingBuffers 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 # these test are currently set up to run on OSX
@testset "PortAudio Tests" begin @testset "PortAudio Tests" begin
@ -31,69 +100,16 @@ using RingBuffers
""" """
end end
@testset "PortAudio Callback works and doesn't allocate" begin @testset "PortAudio Callback works for duplex stream" begin
cb = PortAudio.pa_callbacks[Float32] test_callback(2, 3)
inbuf = rand(Float32, 16) # simulate microphone input end
sourcebuf = LockFreeRingBuffer(Float32, 64) # the microphone input should end up here
outbuf = zeros(Float32, 24) # this is where the output should go @testset "Callback works with input-only stream" begin
sinkbuf = LockFreeRingBuffer(Float32, 64) # the callback should copy this to outbuf test_callback(2, 0)
end
# 2 input channels, 3 output channels @testset "Callback works with output-only stream" begin
info = PortAudio.CallbackInfo(2, sourcebuf, 3, sinkbuf) test_callback(0, 2)
# 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
end end
@testset "Open Default Device" begin @testset "Open Default Device" begin
@ -130,8 +146,8 @@ using RingBuffers
io = IOBuffer() io = IOBuffer()
show(io, stream) show(io, stream)
@test takebuf_string(io) == """ @test takebuf_string(io) == """
PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}} PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Rational{Int64},0,0,-1,0,0,0,0,0,0}}
Samplerate: 48000 s⁻¹ Samplerate: 48000//1 s⁻¹
Buffer Size: 4096 frames Buffer Size: 4096 frames
2 channel sink: "Built-in Output" 2 channel sink: "Built-in Output"
2 channel source: "Built-in Microph\"""" 2 channel source: "Built-in Microph\""""