back to duplex-by-default but now with optional synchronization
This commit is contained in:
parent
7ea9da7e09
commit
1d5ca112eb
3 changed files with 105 additions and 95 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
142
test/runtests.jl
142
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\""""
|
||||
|
|
Loading…
Reference in a new issue