seems to be mostly working with lockfree ringbuffer
This commit is contained in:
parent
e40933b97b
commit
77dcb8965c
2 changed files with 93 additions and 84 deletions
133
src/PortAudio.jl
133
src/PortAudio.jl
|
@ -6,7 +6,7 @@ using SampledSignals
|
||||||
using Devectorize
|
using Devectorize
|
||||||
using RingBuffers
|
using RingBuffers
|
||||||
using Compat
|
using Compat
|
||||||
import Compat: UTF8String
|
import Compat: UTF8String, view
|
||||||
|
|
||||||
# Get binary dependencies loaded from BinDeps
|
# Get binary dependencies loaded from BinDeps
|
||||||
include( "../deps/deps.jl")
|
include( "../deps/deps.jl")
|
||||||
|
@ -14,7 +14,13 @@ include("libportaudio.jl")
|
||||||
|
|
||||||
export PortAudioStream
|
export PortAudioStream
|
||||||
|
|
||||||
|
# Size of the ringbuffer in frames. 85ms latency at 48kHz
|
||||||
const DEFAULT_BUFSIZE=4096
|
const DEFAULT_BUFSIZE=4096
|
||||||
|
# data is passed to and from the ringbuffer in chunks with this many frames
|
||||||
|
# it should be at most the ringbuffer size, and must evenly divide into the
|
||||||
|
# the underlying portaudio buffer size. E.g. if PortAudio is running with a
|
||||||
|
# 2048-frame buffer period, the chunk size can be 2048, 1024, 512, 256, etc.
|
||||||
|
const CHUNKSIZE=128
|
||||||
|
|
||||||
function __init__()
|
function __init__()
|
||||||
# initialize PortAudio on module load
|
# initialize PortAudio on module load
|
||||||
|
@ -68,17 +74,12 @@ function fieldptr{T}(obj::T, field::Symbol)
|
||||||
Ptr{FT}(pointer_from_objref(obj) + offset)
|
Ptr{FT}(pointer_from_objref(obj) + offset)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Used to synchronize the portaudio callback and Julia task
|
|
||||||
@enum BufferState JuliaPending PortAudioPending
|
|
||||||
|
|
||||||
# we want this to be immutable so we can stack allocate it
|
# we want this to be immutable so we can stack allocate it
|
||||||
immutable CallbackInfo{T}
|
immutable CallbackInfo{T}
|
||||||
inchannels::Int
|
inchannels::Int
|
||||||
inbuf::Ptr{T}
|
inbuf::LockFreeRingBuffer{T}
|
||||||
outchannels::Int
|
outchannels::Int
|
||||||
outbuf::Ptr{T}
|
outbuf::LockFreeRingBuffer{T}
|
||||||
taskhandle::Ptr{Void}
|
|
||||||
bufstate::Ptr{BufferState}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# paramaterized on the sample type and sampling rate type
|
# paramaterized on the sample type and sampling rate type
|
||||||
|
@ -88,8 +89,6 @@ type PortAudioStream{T, U}
|
||||||
stream::PaStream
|
stream::PaStream
|
||||||
sink # untyped because of circular type definition
|
sink # untyped because of circular type definition
|
||||||
source # untyped because of circular type definition
|
source # untyped because of circular type definition
|
||||||
taskwork::Base.SingleAsyncWork
|
|
||||||
bufstate::BufferState # used to synchronize the portaudio and julia sides
|
|
||||||
bufinfo::CallbackInfo{T} # immutable data used in the portaudio callback
|
bufinfo::CallbackInfo{T} # immutable data used in the portaudio callback
|
||||||
|
|
||||||
# this inner constructor is generally called via the top-level outer
|
# this inner constructor is generally called via the top-level outer
|
||||||
|
@ -104,16 +103,14 @@ type PortAudioStream{T, U}
|
||||||
Ref(Pa_StreamParameters(outdev.idx, outchans, type_to_fmt[T], 0.0, C_NULL))
|
Ref(Pa_StreamParameters(outdev.idx, outchans, type_to_fmt[T], 0.0, C_NULL))
|
||||||
this = new(sr, bufsize, C_NULL)
|
this = new(sr, bufsize, C_NULL)
|
||||||
finalizer(this, close)
|
finalizer(this, close)
|
||||||
this.sink = PortAudioSink{T, U}(outdev.name, this, outchans, bufsize;
|
this.sink = PortAudioSink{T, U}(outdev.name, this, outchans, bufsize)
|
||||||
prefill=false, underflow=PAD)
|
this.source = PortAudioSource{T, U}(indev.name, this, inchans, bufsize)
|
||||||
this.source = PortAudioSource{T, U}(indev.name, this, inchans, bufsize;
|
if inchans > 0 && outchans > 0
|
||||||
prefill=true, overflow=OVERWRITE)
|
# we've got a duplex stream. initialize with the output buffer full
|
||||||
this.taskwork = Base.SingleAsyncWork(_ -> audiotask(this))
|
write(this.sink, SampleBuf(zeros(T, bufsize, outchans), sr))
|
||||||
this.bufstate = PortAudioPending
|
end
|
||||||
this.bufinfo = CallbackInfo(inchans, pointer(this.source.pabuf),
|
this.bufinfo = CallbackInfo(inchans, this.source.ringbuf,
|
||||||
outchans, pointer(this.sink.pabuf),
|
outchans, this.sink.ringbuf)
|
||||||
this.taskwork.handle,
|
|
||||||
fieldptr(this, :bufstate))
|
|
||||||
this.stream = Pa_OpenStream(inparams, outparams, float(sr), bufsize,
|
this.stream = Pa_OpenStream(inparams, outparams, float(sr), bufsize,
|
||||||
paNoFlag, pa_callbacks[T], fieldptr(this, :bufinfo))
|
paNoFlag, pa_callbacks[T], fieldptr(this, :bufinfo))
|
||||||
|
|
||||||
|
@ -121,7 +118,6 @@ type PortAudioStream{T, U}
|
||||||
|
|
||||||
this
|
this
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
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
|
||||||
|
@ -169,6 +165,8 @@ function Base.close(stream::PortAudioStream)
|
||||||
if stream.stream != C_NULL
|
if stream.stream != C_NULL
|
||||||
Pa_StopStream(stream.stream)
|
Pa_StopStream(stream.stream)
|
||||||
Pa_CloseStream(stream.stream)
|
Pa_CloseStream(stream.stream)
|
||||||
|
close(stream.source)
|
||||||
|
close(stream.sink)
|
||||||
stream.stream = C_NULL
|
stream.stream = C_NULL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -203,27 +201,24 @@ for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
|
||||||
@eval type $TypeName{T, U} <: $Super
|
@eval type $TypeName{T, U} <: $Super
|
||||||
name::UTF8String
|
name::UTF8String
|
||||||
stream::PortAudioStream{T, U}
|
stream::PortAudioStream{T, U}
|
||||||
jlbuf::Array{T, 2}
|
chunkbuf::Array{T, 2}
|
||||||
pabuf::Array{T, 2}
|
ringbuf::LockFreeRingBuffer{T}
|
||||||
ringbuf::RingBuffer{T}
|
nchannels::Int
|
||||||
|
|
||||||
function $TypeName(name, stream, channels, bufsize; prefill=false, ringbuf_args...)
|
function $TypeName(name, stream, channels, bufsize)
|
||||||
# portaudio data comes in interleaved, so we'll end up transposing
|
# portaudio data comes in interleaved, so we'll end up transposing
|
||||||
# it back and forth to julia column-major
|
# it back and forth to julia column-major
|
||||||
jlbuf = zeros(T, bufsize, channels)
|
chunkbuf = zeros(T, channels, CHUNKSIZE)
|
||||||
pabuf = zeros(T, channels, bufsize)
|
ringbuf = LockFreeRingBuffer(T, bufsize * channels)
|
||||||
ringbuf = RingBuffer(T, bufsize, channels; ringbuf_args...)
|
new(name, stream, chunkbuf, ringbuf, channels)
|
||||||
if prefill
|
|
||||||
write(ringbuf, zeros(T, bufsize, channels))
|
|
||||||
end
|
|
||||||
new(name, stream, jlbuf, pabuf, ringbuf)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
SampledSignals.nchannels(s::Union{PortAudioSink, PortAudioSource}) = size(s.jlbuf, 2)
|
SampledSignals.nchannels(s::Union{PortAudioSink, PortAudioSource}) = s.nchannels
|
||||||
SampledSignals.samplerate(s::Union{PortAudioSink, PortAudioSource}) = samplerate(s.stream)
|
SampledSignals.samplerate(s::Union{PortAudioSink, PortAudioSource}) = samplerate(s.stream)
|
||||||
Base.eltype{T, U}(::Union{PortAudioSink{T, U}, PortAudioSource{T, U}}) = T
|
Base.eltype{T, U}(::Union{PortAudioSink{T, U}, PortAudioSource{T, U}}) = T
|
||||||
|
Base.close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf)
|
||||||
|
|
||||||
function Base.show{T <: Union{PortAudioSink, PortAudioSource}}(io::IO, stream::T)
|
function Base.show{T <: Union{PortAudioSink, PortAudioSource}}(io::IO, stream::T)
|
||||||
println(io, T, "(\"", stream.name, "\")")
|
println(io, T, "(\"", stream.name, "\")")
|
||||||
|
@ -232,11 +227,43 @@ end
|
||||||
|
|
||||||
|
|
||||||
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::SampleBuf)
|
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::SampleBuf)
|
||||||
write(sink.ringbuf, buf)
|
total = nframes(buf)
|
||||||
|
nwritten = 0
|
||||||
|
while nwritten < total
|
||||||
|
towrite = min(CHUNKSIZE, total-nwritten)
|
||||||
|
# make a buffer of interleaved samples
|
||||||
|
# TODO: don't directly access buf.data
|
||||||
|
transpose!(view(sink.chunkbuf, :, 1:towrite),
|
||||||
|
view(buf.data, (1:towrite)+nwritten, :))
|
||||||
|
while nwritable(sink.ringbuf) < towrite
|
||||||
|
wait(sink.ringbuf)
|
||||||
|
end
|
||||||
|
write(sink.ringbuf, sink.chunkbuf, towrite*nchannels(sink))
|
||||||
|
|
||||||
|
nwritten += towrite
|
||||||
|
end
|
||||||
|
|
||||||
|
nwritten
|
||||||
end
|
end
|
||||||
|
|
||||||
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::SampleBuf)
|
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::SampleBuf)
|
||||||
read!(source.ringbuf, buf)
|
total = nframes(buf)
|
||||||
|
nread = 0
|
||||||
|
while nread < total
|
||||||
|
toread = min(CHUNKSIZE, total-nread)
|
||||||
|
while nreadable(source.ringbuf) < toread
|
||||||
|
wait(source.ringbuf)
|
||||||
|
end
|
||||||
|
read!(source.ringbuf, source.chunkbuf, toread*nchannels(source))
|
||||||
|
# de-interleave the samples
|
||||||
|
# TODO: don't directly access buf.data
|
||||||
|
transpose!(view(buf.data, (1:toread)+nread, :),
|
||||||
|
view(source.chunkbuf, :, 1:toread))
|
||||||
|
|
||||||
|
nread += toread
|
||||||
|
end
|
||||||
|
|
||||||
|
nread
|
||||||
end
|
end
|
||||||
|
|
||||||
# This is the callback function that gets called directly in the PortAudio
|
# This is the callback function that gets called directly in the PortAudio
|
||||||
|
@ -244,43 +271,25 @@ 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
|
||||||
|
outsamples = nframes * info.outchannels
|
||||||
|
|
||||||
if(unsafe_load(info.bufstate) != PortAudioPending)
|
if nreadable(info.outbuf) < outsamples ||
|
||||||
|
nwritable(info.inbuf) < insamples
|
||||||
# xrun, copy zeros to outbuffer
|
# xrun, copy zeros to outbuffer
|
||||||
|
# TODO: send a notification to an error msg ringbuf
|
||||||
|
# TODO (maybe): we could do a partial write if there's anything in the
|
||||||
|
# ringbuf, and minimize the dropout
|
||||||
memset(outptr, 0, sizeof(T)*nframes*info.outchannels)
|
memset(outptr, 0, sizeof(T)*nframes*info.outchannels)
|
||||||
return paContinue
|
return paContinue
|
||||||
end
|
end
|
||||||
|
|
||||||
unsafe_copy!(info.inbuf, inptr, nframes * info.inchannels)
|
read!(info.outbuf, outptr, outsamples)
|
||||||
unsafe_copy!(outptr, info.outbuf, nframes * info.outchannels)
|
write(info.inbuf, inptr, insamples)
|
||||||
|
|
||||||
unsafe_store!(info.bufstate, JuliaPending)
|
|
||||||
|
|
||||||
# notify the julia audio task
|
|
||||||
ccall(:uv_async_send, Void, (Ptr{Void},), info.taskhandle)
|
|
||||||
|
|
||||||
paContinue
|
paContinue
|
||||||
end
|
end
|
||||||
|
|
||||||
# this gets called from uv_async_send, so it MUST NOT BLOCK
|
|
||||||
function audiotask{T, U}(stream::PortAudioStream{T, U})
|
|
||||||
try
|
|
||||||
if stream.bufstate != JuliaPending
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
transpose!(stream.source.jlbuf, stream.source.pabuf)
|
|
||||||
write(stream.source.ringbuf, stream.source.jlbuf)
|
|
||||||
|
|
||||||
read!(stream.sink.ringbuf, stream.sink.jlbuf)
|
|
||||||
transpose!(stream.sink.pabuf, stream.sink.jlbuf)
|
|
||||||
|
|
||||||
stream.bufstate = PortAudioPending
|
|
||||||
catch ex
|
|
||||||
warn("Audio Task died with exception: $ex")
|
|
||||||
Base.show_backtrace(STDOUT, catch_backtrace())
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
memset(buf, val, count) = ccall(:memset, Ptr{Void},
|
memset(buf, val, count) = ccall(:memset, Ptr{Void},
|
||||||
(Ptr{Void}, Cint, Csize_t),
|
(Ptr{Void}, Cint, Csize_t),
|
||||||
|
|
|
@ -7,6 +7,7 @@ else
|
||||||
end
|
end
|
||||||
using PortAudio
|
using PortAudio
|
||||||
using SampledSignals
|
using SampledSignals
|
||||||
|
using RingBuffers
|
||||||
|
|
||||||
# these test are currently set up to run on OSX
|
# these test are currently set up to run on OSX
|
||||||
|
|
||||||
|
@ -31,17 +32,16 @@ using SampledSignals
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "PortAudio Callback works and doesn't allocate" begin
|
@testset "PortAudio Callback works and doesn't allocate" begin
|
||||||
inbuf = rand(Float32, 2, 8)
|
inbuf = rand(Float32, 16) # simulate microphone input
|
||||||
outbuf = Array(Float32, 2, 8)
|
sourcebuf = LockFreeRingBuffer(Float32, 64) # the microphone input should end up here
|
||||||
sinkbuf = rand(Float32, 2, 8)
|
testin = zeros(Float32, 16)
|
||||||
sourcebuf = Array(Float32, 2, 8)
|
|
||||||
state = Ref(PortAudio.PortAudioPending)
|
|
||||||
work = Base.SingleAsyncWork(data -> nothing)
|
|
||||||
|
|
||||||
info = PortAudio.CallbackInfo(2, pointer(sourcebuf),
|
outbuf = zeros(Float32, 24) # this is where the output should go
|
||||||
2, pointer(sinkbuf),
|
sinkbuf = LockFreeRingBuffer(Float32, 64) # the callback should copy this to outbuf
|
||||||
work.handle,
|
testout = rand(Float32, 24)
|
||||||
Ptr{PortAudio.BufferState}(pointer_from_objref(state)))
|
write(sinkbuf, testout) # fill the output ringbuffer
|
||||||
|
|
||||||
|
info = PortAudio.CallbackInfo(2, sourcebuf, 3, sinkbuf)
|
||||||
|
|
||||||
# handle any conversions here so they don't mess with the allocation
|
# handle any conversions here so they don't mess with the allocation
|
||||||
inptr = pointer(inbuf)
|
inptr = pointer(inbuf)
|
||||||
|
@ -53,18 +53,18 @@ using SampledSignals
|
||||||
ret = PortAudio.portaudio_callback(inptr, outptr, nframes, C_NULL, flags, infoptr)
|
ret = PortAudio.portaudio_callback(inptr, outptr, nframes, C_NULL, flags, infoptr)
|
||||||
@test isa(ret, Cint)
|
@test isa(ret, Cint)
|
||||||
@test ret == PortAudio.paContinue
|
@test ret == PortAudio.paContinue
|
||||||
@test outbuf == sinkbuf
|
@test outbuf == testout
|
||||||
@test inbuf == sourcebuf
|
read!(sourcebuf, testin)
|
||||||
@test state[] == PortAudio.JuliaPending
|
@test inbuf == testin
|
||||||
|
|
||||||
# call again (underrun)
|
# call again (underrun)
|
||||||
ret = PortAudio.portaudio_callback(inptr, outptr, nframes, C_NULL, flags, infoptr)
|
ret = PortAudio.portaudio_callback(inptr, outptr, nframes, C_NULL, flags, infoptr)
|
||||||
@test isa(ret, Cint)
|
@test isa(ret, Cint)
|
||||||
@test ret == PortAudio.paContinue
|
@test ret == PortAudio.paContinue
|
||||||
@test outbuf == zeros(Float32, 2, 8)
|
@test outbuf == zeros(Float32, 24)
|
||||||
|
|
||||||
|
write(sinkbuf, testout) # fill the output ringbuffer
|
||||||
# test allocation
|
# test allocation
|
||||||
state[] = PortAudio.PortAudioPending
|
|
||||||
alloc = @allocated PortAudio.portaudio_callback(inptr, outptr, nframes, C_NULL, flags, infoptr)
|
alloc = @allocated PortAudio.portaudio_callback(inptr, outptr, nframes, C_NULL, flags, infoptr)
|
||||||
@test alloc == 0
|
@test alloc == 0
|
||||||
# now test allocation in underrun state
|
# now test allocation in underrun state
|
||||||
|
@ -74,15 +74,15 @@ using SampledSignals
|
||||||
|
|
||||||
@testset "Open Default Device" begin
|
@testset "Open Default Device" begin
|
||||||
stream = PortAudioStream()
|
stream = PortAudioStream()
|
||||||
buf = read(stream, 0.1s)
|
buf = read(stream, 0.001s)
|
||||||
@test size(buf) == (round(Int, 0.1s * samplerate(stream)), nchannels(stream.source))
|
@test size(buf) == (round(Int, 0.001s * samplerate(stream)), nchannels(stream.source))
|
||||||
write(stream, buf)
|
write(stream, buf)
|
||||||
close(stream)
|
close(stream)
|
||||||
end
|
end
|
||||||
@testset "Open Device by name" begin
|
@testset "Open Device by name" begin
|
||||||
stream = PortAudioStream("Built-in Microph", "Built-in Output")
|
stream = PortAudioStream("Built-in Microph", "Built-in Output")
|
||||||
buf = read(stream, 0.1s)
|
buf = read(stream, 0.001s)
|
||||||
@test size(buf) == (round(Int, 0.1s * samplerate(stream)), nchannels(stream.source))
|
@test size(buf) == (round(Int, 0.001s * samplerate(stream)), nchannels(stream.source))
|
||||||
write(stream, buf)
|
write(stream, buf)
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
show(io, stream)
|
show(io, stream)
|
||||||
|
@ -100,8 +100,8 @@ using SampledSignals
|
||||||
# no way to check that the right data is actually getting read or written here,
|
# no way to check that the right data is actually getting read or written here,
|
||||||
# but at least it's not crashing.
|
# but at least it's not crashing.
|
||||||
@testset "Queued Writing" begin
|
@testset "Queued Writing" begin
|
||||||
stream = PortAudioStream()
|
stream = PortAudioStream(0, 2)
|
||||||
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.sink))*0.1, samplerate(stream))
|
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.sink)), samplerate(stream))
|
||||||
t1 = @async write(stream, buf)
|
t1 = @async write(stream, buf)
|
||||||
t2 = @async write(stream, buf)
|
t2 = @async write(stream, buf)
|
||||||
@test wait(t1) == 48000
|
@test wait(t1) == 48000
|
||||||
|
@ -109,7 +109,7 @@ using SampledSignals
|
||||||
close(stream)
|
close(stream)
|
||||||
end
|
end
|
||||||
@testset "Queued Reading" begin
|
@testset "Queued Reading" begin
|
||||||
stream = PortAudioStream()
|
stream = PortAudioStream(2, 0)
|
||||||
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source)), samplerate(stream))
|
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source)), samplerate(stream))
|
||||||
t1 = @async read!(stream, buf)
|
t1 = @async read!(stream, buf)
|
||||||
t2 = @async read!(stream, buf)
|
t2 = @async read!(stream, buf)
|
||||||
|
|
Loading…
Reference in a new issue