parent
461cdc1557
commit
25f9c1230f
2 changed files with 90 additions and 31 deletions
105
src/PortAudio.jl
105
src/PortAudio.jl
|
@ -17,7 +17,7 @@ include("libportaudio.jl")
|
|||
|
||||
# data is passed to and from portaudio in chunks with this many frames, because
|
||||
# we need to interleave the samples
|
||||
const CHUNKSIZE=128
|
||||
const CHUNKFRAMES=128
|
||||
|
||||
function versioninfo(io::IO=stdout)
|
||||
println(io, Pa_GetVersionText())
|
||||
|
@ -58,15 +58,16 @@ end
|
|||
# not for external use, used in error message printing
|
||||
devnames() = join(["\"$(dev.name)\"" for dev in devices()], "\n")
|
||||
|
||||
##################
|
||||
#
|
||||
# PortAudioStream
|
||||
##################
|
||||
#
|
||||
|
||||
mutable struct PortAudioStream{T}
|
||||
samplerate::Float64
|
||||
latency::Float64
|
||||
stream::PaStream
|
||||
warn_xruns::Bool
|
||||
recover_xruns::Bool
|
||||
sink # untyped because of circular type definition
|
||||
source # untyped because of circular type definition
|
||||
|
||||
|
@ -79,7 +80,7 @@ mutable struct PortAudioStream{T}
|
|||
# TODO: figure out whether we can get deterministic latency...
|
||||
function PortAudioStream{T}(indev::PortAudioDevice, outdev::PortAudioDevice,
|
||||
inchans, outchans, sr,
|
||||
latency, warn_xruns) where {T}
|
||||
latency, warn_xruns, recover_xruns) where {T}
|
||||
inchans = inchans == -1 ? indev.maxinchans : inchans
|
||||
outchans = outchans == -1 ? outdev.maxoutchans : outchans
|
||||
inparams = (inchans == 0) ?
|
||||
|
@ -88,7 +89,7 @@ mutable struct PortAudioStream{T}
|
|||
outparams = (outchans == 0) ?
|
||||
Ptr{Pa_StreamParameters}(0) :
|
||||
Ref(Pa_StreamParameters(outdev.idx, outchans, type_to_fmt[T], latency, C_NULL))
|
||||
this = new(sr, latency, C_NULL, warn_xruns)
|
||||
this = new(sr, latency, C_NULL, warn_xruns, recover_xruns)
|
||||
# finalizer(close, this)
|
||||
this.sink = PortAudioSink{T}(outdev.name, this, outchans)
|
||||
this.source = PortAudioSource{T}(indev.name, this, inchans)
|
||||
|
@ -98,11 +99,30 @@ mutable struct PortAudioStream{T}
|
|||
end
|
||||
|
||||
Pa_StartStream(this.stream)
|
||||
# pre-fill the output stream so we're less likely to underrun
|
||||
prefill_output(this.sink)
|
||||
|
||||
this
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function recover_xrun(stream::PortAudioStream)
|
||||
playback = nchannels(stream.sink) > 0
|
||||
capture = nchannels(stream.source) > 0
|
||||
if playback && capture
|
||||
# the best we can do to avoid further xruns is to fill the playback buffer and
|
||||
# discard the capture buffer. Really there's a fundamental problem with our
|
||||
# read/write-based API where you don't know whether we're currently in a state
|
||||
# when the reads and writes should be balanced. In the future we should probably
|
||||
# move to some kind of transaction API that forces them to be balanced, and also
|
||||
# gives a way for the application to signal that the same number of samples
|
||||
# should have been read as written.
|
||||
discard_input(stream.source)
|
||||
prefill_output(stream.sink)
|
||||
end
|
||||
end
|
||||
|
||||
defaultlatency(devices...) = maximum(d -> max(d.highoutputlatency, d.highinputlatency), devices)
|
||||
|
||||
# this is the top-level outer constructor that all the other outer constructors end up calling
|
||||
|
@ -119,18 +139,21 @@ used.
|
|||
|
||||
Options:
|
||||
|
||||
* `eltype`: Sample type of the audio stream (defaults to Float32)
|
||||
* `samplerate`: Sample rate (defaults to device sample rate)
|
||||
* `latency`: Requested latency. Stream could underrun when too low,
|
||||
consider using provided device defaults
|
||||
* `warn_xruns`: Display a warning if there is a stream overrun or underrun,
|
||||
which often happens when Julia is compiling, or with a
|
||||
particularly large GC run. This can be quite verbose so is
|
||||
false by default.
|
||||
* `eltype`: Sample type of the audio stream (defaults to Float32)
|
||||
* `samplerate`: Sample rate (defaults to device sample rate)
|
||||
* `latency`: Requested latency. Stream could underrun when too low, consider
|
||||
using provided device defaults
|
||||
* `warn_xruns`: Display a warning if there is a stream overrun or underrun, which
|
||||
often happens when Julia is compiling, or with a particularly large
|
||||
GC run. This can be quite verbose so is false by default.
|
||||
* `recover_xruns`: Attempt to recover from overruns and underruns by emptying and
|
||||
filling the input and output buffers, respectively. Should result in
|
||||
fewer xruns but could make each xrun more audible. True by default.
|
||||
Only effects duplex streams.
|
||||
"""
|
||||
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
|
||||
inchans=2, outchans=2; eltype=Float32, samplerate=-1,
|
||||
latency=defaultlatency(indev, outdev), warn_xruns=false)
|
||||
latency=defaultlatency(indev, outdev), warn_xruns=false, recover_xruns=true)
|
||||
if samplerate == -1
|
||||
sampleratein = indev.defaultsamplerate
|
||||
samplerateout = outdev.defaultsamplerate
|
||||
|
@ -145,7 +168,8 @@ function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
|
|||
samplerate = samplerateout
|
||||
end
|
||||
end
|
||||
PortAudioStream{eltype}(indev, outdev, inchans, outchans, samplerate, latency, warn_xruns)
|
||||
PortAudioStream{eltype}(indev, outdev, inchans, outchans, samplerate,
|
||||
latency, warn_xruns, recover_xruns)
|
||||
end
|
||||
|
||||
# handle device names given as streams
|
||||
|
@ -230,9 +254,9 @@ function show(io::IO, stream::PortAudioStream)
|
|||
end
|
||||
end
|
||||
|
||||
##################################
|
||||
#
|
||||
# PortAudioSink & PortAudioSource
|
||||
##################################
|
||||
#
|
||||
|
||||
# Define our source and sink types
|
||||
for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
|
||||
|
@ -246,7 +270,7 @@ for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
|
|||
function $TypeName{T}(name, stream, channels) where {T}
|
||||
# portaudio data comes in interleaved, so we'll end up transposing
|
||||
# it back and forth to julia column-major
|
||||
chunkbuf = zeros(T, channels, CHUNKSIZE)
|
||||
chunkbuf = zeros(T, channels, CHUNKFRAMES)
|
||||
new(name, stream, chunkbuf, channels)
|
||||
end
|
||||
end
|
||||
|
@ -277,13 +301,17 @@ end
|
|||
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount)
|
||||
nwritten = 0
|
||||
while nwritten < framecount
|
||||
n = min(framecount-nwritten, CHUNKSIZE)
|
||||
n = min(framecount-nwritten, CHUNKFRAMES)
|
||||
# make a buffer of interleaved samples
|
||||
transpose!(view(sink.chunkbuf, :, 1:n),
|
||||
view(buf, (1:n) .+ nwritten .+ frameoffset, :))
|
||||
# TODO: if the stream is closed we just want to return a
|
||||
# shorter-than-requested frame count instead of throwing an error
|
||||
Pa_WriteStream(sink.stream.stream, sink.chunkbuf, n, sink.stream.warn_xruns)
|
||||
err = Pa_WriteStream(sink.stream.stream, sink.chunkbuf, n,
|
||||
sink.stream.warn_xruns)
|
||||
if err ∈ (PA_OUTPUT_UNDERFLOWED, PA_INPUT_OVERFLOWED) && sink.stream.recover_xruns
|
||||
recover_xrun(sink.stream)
|
||||
end
|
||||
nwritten += n
|
||||
end
|
||||
|
||||
|
@ -293,10 +321,14 @@ end
|
|||
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameoffset, framecount)
|
||||
nread = 0
|
||||
while nread < framecount
|
||||
n = min(framecount-nread, CHUNKSIZE)
|
||||
n = min(framecount-nread, CHUNKFRAMES)
|
||||
# TODO: if the stream is closed we just want to return a
|
||||
# shorter-than-requested frame count instead of throwing an error
|
||||
Pa_ReadStream(source.stream.stream, source.chunkbuf, n, source.stream.warn_xruns)
|
||||
err = Pa_ReadStream(source.stream.stream, source.chunkbuf, n,
|
||||
source.stream.warn_xruns)
|
||||
if err ∈ (PA_OUTPUT_UNDERFLOWED, PA_INPUT_OVERFLOWED) && source.stream.recover_xruns
|
||||
recover_xrun(source.stream)
|
||||
end
|
||||
# de-interleave the samples
|
||||
transpose!(view(buf, (1:n) .+ nread .+ frameoffset, :),
|
||||
view(source.chunkbuf, :, 1:n))
|
||||
|
@ -307,6 +339,35 @@ function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameo
|
|||
nread
|
||||
end
|
||||
|
||||
"""
|
||||
prefill_output(sink::PortAudioSink)
|
||||
|
||||
Fill the playback buffer of the given sink.
|
||||
"""
|
||||
function prefill_output(sink::PortAudioSink)
|
||||
towrite = Pa_GetStreamWriteAvailable(sink.stream.stream)
|
||||
while towrite > 0
|
||||
n = min(towrite, CHUNKFRAMES)
|
||||
fill!(sink.chunkbuf, zero(eltype(sink.chunkbuf)))
|
||||
Pa_WriteStream(sink.stream.stream, sink.chunkbuf, n, false)
|
||||
towrite -= n
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
discard_input(source::PortAudioSource)
|
||||
|
||||
Read and discard data from the capture buffer.
|
||||
"""
|
||||
function discard_input(source::PortAudioSource)
|
||||
toread = Pa_GetStreamReadAvailable(source.stream.stream)
|
||||
while toread > 0
|
||||
n = min(toread, CHUNKFRAMES)
|
||||
Pa_ReadStream(source.stream.stream, source.chunkbuf, n, false)
|
||||
toread -= n
|
||||
end
|
||||
end
|
||||
|
||||
function suppress_err(dofunc::Function)
|
||||
nullfile = @static Sys.iswindows() ? "nul" : "/dev/null"
|
||||
open(nullfile, "w") do io
|
||||
|
|
|
@ -230,10 +230,9 @@ function Pa_GetStreamWriteAvailable(stream::PaStream)
|
|||
avail
|
||||
end
|
||||
|
||||
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
|
||||
show_warnings::Bool=true)
|
||||
frames <= length(buf) || error("Need a buffer at least $frames long")
|
||||
# without this I get a segfault with the error:
|
||||
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer,
|
||||
show_warnings=true)
|
||||
# without disable_sigint I get a segfault with the error:
|
||||
# "error thrown and no exception handler available."
|
||||
# if the user tries to ctrl-C. Note I've still had some crash problems with
|
||||
# ctrl-C within `pasuspend`, so for now I think either don't use `pasuspend` or
|
||||
|
@ -244,19 +243,18 @@ function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf)
|
|||
stream, buf, frames)
|
||||
end
|
||||
handle_status(err, show_warnings)
|
||||
buf
|
||||
err
|
||||
end
|
||||
|
||||
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
|
||||
show_warnings::Bool=true)
|
||||
frames <= length(buf) || error("Need a buffer at least $frames long")
|
||||
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer,
|
||||
show_warnings=true)
|
||||
err = disable_sigint() do
|
||||
@tcall @locked ccall((:Pa_WriteStream, libportaudio), PaError,
|
||||
(PaStream, Ptr{Cvoid}, Culong),
|
||||
stream, buf, frames)
|
||||
end
|
||||
handle_status(err, show_warnings)
|
||||
nothing
|
||||
err
|
||||
end
|
||||
|
||||
# function Pa_GetStreamInfo(stream::PaStream)
|
||||
|
|
Loading…
Reference in a new issue