run JuliaFormatter (#77)

This commit is contained in:
bramtayl 2021-06-01 13:44:23 -04:00 committed by GitHub
parent 89020cafc7
commit b3cddf5669
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 453 additions and 262 deletions

9
.JuliaFormatter.toml Normal file
View file

@ -0,0 +1,9 @@
always_for_in = true
whitespace_typedefs = true
whitespace_ops_in_indices = true
remove_extra_newlines = true
import_to_using = true
short_to_long_function_def = true
format_docstrings = true
align_pair_arrow = false
conditional_to_if = true

View file

@ -1,9 +1,11 @@
using PortAudio
"""Continuously read from the default audio input and plot an
ASCII level/peak meter"""
"""
Continuously read from the default audio input and plot an
ASCII level/peak meter
"""
function micmeter(metersize)
mic = PortAudioStream(1, 0; latency=0.1)
mic = PortAudioStream(1, 0; latency = 0.1)
signalmax = zero(eltype(mic))
println("Press Ctrl-C to quit")
@ -16,28 +18,32 @@ function micmeter(metersize)
end
end
"""Print an ASCII level meter of the given size. Signal and peak
levels are assumed to be scaled from 0.0-1.0, with peak >= signal"""
"""
Print an ASCII level meter of the given size. Signal and peak
levels are assumed to be scaled from 0.0-1.0, with peak >= signal
"""
function printmeter(metersize, signal, peak)
# calculate the positions in terms of characters
peakpos = clamp(round(Int, peak * metersize), 0, metersize)
meterchars = clamp(round(Int, signal * metersize), 0, peakpos-1)
blankchars = max(0, peakpos-meterchars-1)
meterchars = clamp(round(Int, signal * metersize), 0, peakpos - 1)
blankchars = max(0, peakpos - meterchars - 1)
for position in 1:meterchars
printstyled(">", color=barcolor(metersize, position))
printstyled(">", color = barcolor(metersize, position))
end
print(" " ^ blankchars)
printstyled("|", color=barcolor(metersize, peakpos))
print(" " ^ (metersize - peakpos))
print(" "^blankchars)
printstyled("|", color = barcolor(metersize, peakpos))
print(" "^(metersize - peakpos))
end
"""Compute the proper color for a given position in the bar graph. The first
"""
Compute the proper color for a given position in the bar graph. The first
half of the bar should be green, then the remainder is yellow except the final
character, which is red."""
character, which is red.
"""
function barcolor(metersize, position)
if position/metersize <= 0.5
if position / metersize <= 0.5
:green
elseif position == metersize
:red

View file

@ -15,30 +15,33 @@ function paudio()
return ostream = PortAudioStream(devs[devnum].name, 0, 2)
end
play(ostream, sample::Array{Float64,1}) = write(ostream, sample)
play(ostr, sample::Array{Int64,1}) = play(ostr, Float64.(sample))
play(ostream, sample::Array{Float64, 1}) = write(ostream, sample)
play(ostr, sample::Array{Int64, 1}) = play(ostr, Float64.(sample))
struct Note{S<:Real, T<:Real}
struct Note{S <: Real, T <: Real}
pitch::S
duration::T
sustained::Bool
end
function play(ostream,
A::Note,
samplingfreq::Real=44100,
shape::Function=t->0.6sin(t)+0.2sin(2t)+.05*sin(8t))
function play(
ostream,
A::Note,
samplingfreq::Real = 44100,
shape::Function = t -> 0.6sin(t) + 0.2sin(2t) + 0.05 * sin(8t),
)
timesamples = 0:(1 / samplingfreq):(A.duration * (A.sustained ? 0.98 : 0.9))
v = Float64[shape(2π * A.pitch * t) for t in timesamples]
if !A.sustained
decay_length = div(length(timesamples), 5)
v[end-decay_length:end-1] = v[end-decay_length:end-1] .* LinRange(1, 0, decay_length)
v[(end - decay_length):(end - 1)] =
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
end
play(ostream, v)
sleep(A.duration)
end
function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
function parsevoice(melody::String; tempo = 132, beatunit = 4, lyrics = nothing)
ostream = paudio() # initialize audio for output
lyrics_syllables = lyrics == nothing ? nothing : split(lyrics)
lyrics_syllables != nothing && (lyrics_syllables[end] *= "\n")
@ -46,7 +49,7 @@ function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
oldduration = 4
for line in split(melody, '\n')
percent_idx = findfirst('%', line) # Trim comment
percent_idx == nothing || (line = line[1:percent_idx-1])
percent_idx == nothing || (line = line[1:(percent_idx - 1)])
for token in split(line)
pitch, duration, dotted, sustained = parsetoken(token)
duration == nothing && (duration = oldduration)
@ -55,7 +58,7 @@ function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
if lyrics_syllables != nothing && 1 <= note_idx <= length(lyrics_syllables)
# Print the lyrics, omitting hyphens
if lyrics_syllables[note_idx][end] == '-'
print(join(split(lyrics_syllables[note_idx][:], "")[1:end-1]), "")
print(join(split(lyrics_syllables[note_idx][:], "")[1:(end - 1)]), "")
else
print(lyrics_syllables[note_idx], ' ')
end
@ -66,18 +69,19 @@ function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
end
end
function parsetoken(token, Atuning::Real=220)
function parsetoken(token, Atuning::Real = 220)
state = :findpitch
pitch = 0.0
sustain = dotted = false
lengthbuf = Char[]
for char in token
if state == :findpitch
scale_idx = something(findfirst(char, String(collect('a':'g'))), 0) +
scale_idx =
something(findfirst(char, String(collect('a':'g'))), 0) +
something(findfirst(char, String(collect('A':'G'))), 0)
if scale_idx != 0
halfsteps = [12, 14, 3, 5, 7, 8, 10]
pitch = Atuning * 2 ^ (halfsteps[scale_idx] / 12)
pitch = Atuning * 2^(halfsteps[scale_idx] / 12)
state = :findlength
elseif char == 'r'
pitch, state = 0, :findlength
@ -85,22 +89,28 @@ function parsetoken(token, Atuning::Real=220)
error("unknown pitch: $char")
end
elseif state == :findlength
if char == '#' ; pitch *= 2^(1 / 12) # sharp
elseif char == 'b' ; pitch /= 2^(1 / 12) # flat
elseif char == '\''; pitch *= 2 # higher octave
elseif char == ',' ; pitch /= 2 # lower octave
elseif char == '.' ; dotted = true # dotted note
elseif char == '~' ; sustain = true # tied note
if char == '#'
pitch *= 2^(1 / 12) # sharp
elseif char == 'b'
pitch /= 2^(1 / 12) # flat
elseif char == '\''
pitch *= 2 # higher octave
elseif char == ','
pitch /= 2 # lower octave
elseif char == '.'
dotted = true # dotted note
elseif char == '~'
sustain = true # tied note
else
push!(lengthbuf, char)
# Check for "is" and "es" suffixes for sharps and flats
if length(lengthbuf) >= 2
if lengthbuf[end-1:end] == "is"
if lengthbuf[(end - 1):end] == "is"
pitch *= 2^(1 / 12)
lengthbuf = lengthbuf[1:end-2]
elseif lengthbuf[end-1:end] == "es"
lengthbuf = lengthbuf[1:(end - 2)]
elseif lengthbuf[(end - 1):end] == "es"
pitch /= 2^(1 / 12)
lengthbuf = lengthbuf[1:end-2]
lengthbuf = lengthbuf[1:(end - 2)]
end
end
end
@ -112,23 +122,27 @@ function parsetoken(token, Atuning::Real=220)
return (pitch, duration, sustain, dotted)
end
parsevoice("""
parsevoice(
"""
f# f# g a a g f# e d d e f# f#~ f#8 e e2
f#4 f# g a a g f# e d d e f# e~ e8 d d2
e4 e f# d e f#8~ g8 f#4 d e f#8~ g f#4 e d e a,
f#2 f#4 g a a g f# e d d e f# e~ e8 d8 d2""",
lyrics="""
Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!
Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!
Dei- ne Zau- ber bin den - wie- der, was die - Mo- de streng ge- theilt,
al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt.
""")
lyrics = """
Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!
Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!
Dei- ne Zau- ber bin den - wie- der, was die - Mo- de streng ge- theilt,
al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt.
""",
)
# And now with harmony!
soprano = @spawn parsevoice("""
soprano = @spawn parsevoice(
"""
f'#. f'#. g'. a'. a'. g'. f'#. e'~ e'8 d.'4 d.' e.' f#'. f#'.~ f#' e'8 e'4~ e'2
""", lyrics="Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!"
""",
lyrics = "Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!",
)
alto = @spawn parsevoice("""
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2
@ -144,9 +158,12 @@ wait(alto)
wait(tenor)
wait(bass)
soprano = @spawn parsevoice("""
soprano = @spawn parsevoice(
"""
f'#.4 f'#. g'. a'. a'. g'. f'#. e'. d'. d'. e'. f'#. e'.~ e' d'8 d'4~ d'2
""", lyrics="Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!")
""",
lyrics = "Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!",
)
alto = @spawn parsevoice("""
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2
""")

View file

@ -4,15 +4,14 @@ using DSP
function create_measure_signal()
signal = zeros(Float32, 20000)
for i in 1:3
signal = vcat(signal, rand(Float32, 100), zeros(Float32, i*10000))
signal = vcat(signal, rand(Float32, 100), zeros(Float32, i * 10000))
end
return signal
end
function measure_latency(in_latency = 0.1, out_latency=0.1; is_warmup = false)
in_stream = PortAudioStream(1,0; latency=in_latency)
out_stream = PortAudioStream(0,1; latency=out_latency)
function measure_latency(in_latency = 0.1, out_latency = 0.1; is_warmup = false)
in_stream = PortAudioStream(1, 0; latency = in_latency)
out_stream = PortAudioStream(0, 1; latency = out_latency)
cond = Base.Event()
@ -27,9 +26,9 @@ function measure_latency(in_latency = 0.1, out_latency=0.1; is_warmup = false)
signal = create_measure_signal()
writer = Threads.@spawn begin
wait(cond)
reader_start_time = time_ns() |> Int64
write(out_stream, signal)
wait(cond)
reader_start_time = time_ns() |> Int64
write(out_stream, signal)
end
notify(cond)
@ -37,8 +36,8 @@ function measure_latency(in_latency = 0.1, out_latency=0.1; is_warmup = false)
wait(reader)
wait(writer)
recorded = collect(reader.result)[:,1]
recorded = collect(reader.result)[:, 1]
close(in_stream)
close(out_stream)

View file

@ -14,7 +14,7 @@ const fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
while true
read!(stream, buf)
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim=(fs[1],fs[end]), ylim=(0,100))
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100))
end
end

View file

@ -8,8 +8,8 @@ The rightmosts column is discarded and the leftmost column is
left alone.
"""
function shift1!(buf::AbstractMatrix)
for col in size(buf,2):-1:2
@. buf[:, col] = buf[:, col-1]
for col in size(buf, 2):-1:2
@. buf[:, col] = buf[:, col - 1]
end
end
@ -20,7 +20,7 @@ function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan)
readbuf .*= win
A_mul_B!(fftbuf, fftplan, readbuf)
shift1!(dispbuf)
@. dispbuf[end:-1:1,1] = log(clamp(abs(fftbuf[1:D]), 0.0001, Inf))
@. dispbuf[end:-1:1, 1] = log(clamp(abs(fftbuf[1:D]), 0.0001, Inf))
end
function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
@ -31,17 +31,17 @@ function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
end
N = 1024 # size of audio read
N2 = N÷2+1 # size of rfft output
N2 = N ÷ 2 + 1 # size of rfft output
D = 200 # number of bins to display
M = 200 # amount of history to keep
src = PortAudioStream(1, 2)
buf = Array{Float32}(N) # buffer for reading
fftplan = plan_rfft(buf; flags=FFTW.EXHAUSTIVE)
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
fftbuf = Array{Complex{Float32}}(N2) # destination buf for FFT
dispbufs = [zeros(Float32, D, M) for i in 1:5, j in 1:5] # STFT bufs
win = gaussian(N, 0.125)
scene = Scene(resolution=(1000,1000))
scene = Scene(resolution = (1000, 1000))
#pre-fill the display buffer so we can do a reasonable colormap
for _ in 1:M
@ -53,7 +53,7 @@ heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf
buf = ibuf[2]
# some function of the 2D index and the value
heatmap(buf, offset=(i[2]*size(buf, 2), i[1]*size(buf, 1)))
heatmap(buf, offset = (i[2] * size(buf, 2), i[1] * size(buf, 1)))
end
center!(scene)

View file

@ -2,7 +2,7 @@ using Makie, GeometryTypes
using PortAudio
N = 1024 # size of audio read
N2 = N÷2+1 # size of rfft output
N2 = N ÷ 2 + 1 # size of rfft output
D = 200 # number of bins to display
M = 100 # number of lines to draw
S = 0.5 # motion speed of lines
@ -10,19 +10,24 @@ src = PortAudioStream(1, 2)
buf = Array{Float32}(N)
fftbuf = Array{Complex{Float32}}(N2)
magbuf = Array{Float32}(N2)
fftplan = plan_rfft(buf; flags=FFTW.EXHAUSTIVE)
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
scene = Scene(resolution=(500,500))
scene = Scene(resolution = (500, 500))
ax = axis(0:0.1:1, 0:0.1:1, 0:0.1:0.5)
center!(scene)
ls = map(1:M) do _
yoffset = to_node(to_value(scene[:time]))
offset = lift_node(scene[:time], yoffset) do t, yoff
Point3f0(0.0f0, (t-yoff)*S, 0.0f0)
Point3f0(0.0f0, (t - yoff) * S, 0.0f0)
end
l = lines(linspace(0,1,D), 0.0f0, zeros(Float32, D),
offset=offset, color=(:black, 0.1))
l = lines(
linspace(0, 1, D),
0.0f0,
zeros(Float32, D),
offset = offset,
color = (:black, 0.1),
)
(yoffset, l)
end
@ -31,7 +36,7 @@ while isopen(scene[:screen])
isopen(scene[:screen]) || break
read!(src, buf)
A_mul_B!(fftbuf, fftplan, buf)
@. magbuf = log(clamp(abs(fftbuf), 0.0001, Inf))/10+0.5
@. magbuf = log(clamp(abs(fftbuf), 0.0001, Inf)) / 10 + 0.5
line[:z] = magbuf[1:D]
push!(yoffset, to_value(scene[:time]))
end

View file

@ -9,7 +9,7 @@ import Base: eltype, show
import Base: close, isopen
import Base: read, read!, write
import LinearAlgebra
using LinearAlgebra: LinearAlgebra
import LinearAlgebra: transpose!
export PortAudioStream
@ -29,9 +29,9 @@ end
# data is passed to and from portaudio in chunks with this many frames, because
# we need to interleave the samples
const CHUNKFRAMES=128
const CHUNKFRAMES = 128
function versioninfo(io::IO=stdout)
function versioninfo(io::IO = stdout)
println(io, Pa_GetVersionText())
println(io, "Version: ", Pa_GetVersion())
end
@ -49,7 +49,8 @@ mutable struct PortAudioDevice
highoutputlatency::Float64
end
PortAudioDevice(info::PaDeviceInfo, idx) = PortAudioDevice(
function PortAudioDevice(info::PaDeviceInfo, idx)
PortAudioDevice(
unsafe_string(info.name),
unsafe_string(Pa_GetHostApiInfo(info.host_api).name),
info.max_input_channels,
@ -59,12 +60,14 @@ PortAudioDevice(info::PaDeviceInfo, idx) = PortAudioDevice(
info.default_low_input_latency,
info.default_low_output_latency,
info.default_high_input_latency,
info.default_high_output_latency)
info.default_high_output_latency,
)
end
function devices()
ndevices = Pa_GetDeviceCount()
infos = PaDeviceInfo[Pa_GetDeviceInfo(i) for i in 0:(ndevices - 1)]
PortAudioDevice[PortAudioDevice(info, idx-1) for (idx, info) in enumerate(infos)]
PortAudioDevice[PortAudioDevice(info, idx - 1) for (idx, info) in enumerate(infos)]
end
# not for external use, used in error message printing
@ -80,8 +83,8 @@ mutable struct PortAudioStream{T}
stream::PaStream
warn_xruns::Bool
recover_xruns::Bool
sink # untyped because of circular type definition
source # untyped because of circular type definition
sink::Any # untyped because of circular type definition
source::Any # untyped because of circular type definition
# this inner constructor is generally called via the top-level outer
# constructor below
@ -90,23 +93,41 @@ mutable struct PortAudioStream{T}
# TODO: recover from xruns - currently with low latencies (e.g. 0.01) it
# will run fine for a while and then fail with the first xrun.
# TODO: figure out whether we can get deterministic latency...
function PortAudioStream{T}(indev::PortAudioDevice, outdev::PortAudioDevice,
inchans, outchans, sr,
latency, warn_xruns, recover_xruns) where {T}
function PortAudioStream{T}(
indev::PortAudioDevice,
outdev::PortAudioDevice,
inchans,
outchans,
sr,
latency,
warn_xruns,
recover_xruns,
) where {T}
inchans = inchans == -1 ? indev.maxinchans : inchans
outchans = outchans == -1 ? outdev.maxoutchans : outchans
inparams = (inchans == 0) ?
Ptr{Pa_StreamParameters}(0) :
inparams = if (inchans == 0)
Ptr{Pa_StreamParameters}(0)
else
Ref(Pa_StreamParameters(indev.idx, inchans, type_to_fmt[T], latency, C_NULL))
outparams = (outchans == 0) ?
Ptr{Pa_StreamParameters}(0) :
end
outparams = if (outchans == 0)
Ptr{Pa_StreamParameters}(0)
else
Ref(Pa_StreamParameters(outdev.idx, outchans, type_to_fmt[T], latency, C_NULL))
end
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)
this.stream = @stderr_as_debug Pa_OpenStream(inparams, outparams, sr, 0, paNoFlag,
nothing, nothing)
this.stream = @stderr_as_debug Pa_OpenStream(
inparams,
outparams,
sr,
0,
paNoFlag,
nothing,
nothing,
)
Pa_StartStream(this.stream)
# pre-fill the output stream so we're less likely to underrun
@ -116,7 +137,6 @@ mutable struct PortAudioStream{T}
end
end
function recover_xrun(stream::PortAudioStream)
playback = nchannels(stream.sink) > 0
capture = nchannels(stream.source) > 0
@ -133,15 +153,18 @@ function recover_xrun(stream::PortAudioStream)
end
end
defaultlatency(devices...) = maximum(d -> max(d.highoutputlatency, d.highinputlatency), devices)
function defaultlatency(devices...)
maximum(d -> max(d.highoutputlatency, d.highinputlatency), devices)
end
function combine_default_sample_rates(inchans, sampleratein, outchans, samplerateout)
if inchans > 0 && outchans > 0 && sampleratein != samplerateout
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.
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
sampleratein
@ -164,33 +187,56 @@ 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.
* `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.
- `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, recover_xruns=true)
function PortAudioStream(
indev::PortAudioDevice,
outdev::PortAudioDevice,
inchans = 2,
outchans = 2;
eltype = Float32,
samplerate = -1,
latency = defaultlatency(indev, outdev),
warn_xruns = false,
recover_xruns = true,
)
if samplerate == -1
samplerate = combine_default_sample_rates(
inchans, indev.defaultsamplerate,
outchans, outdev.defaultsamplerate
)
inchans,
indev.defaultsamplerate,
outchans,
outdev.defaultsamplerate,
)
end
PortAudioStream{eltype}(indev, outdev, inchans, outchans, samplerate,
latency, warn_xruns, recover_xruns)
PortAudioStream{eltype}(
indev,
outdev,
inchans,
outchans,
samplerate,
latency,
warn_xruns,
recover_xruns,
)
end
# handle device names given as streams
function PortAudioStream(indevname::AbstractString, outdevname::AbstractString, args...; kwargs...)
function PortAudioStream(
indevname::AbstractString,
outdevname::AbstractString,
args...;
kwargs...,
)
indev = nothing
outdev = nothing
for d in devices()
@ -205,7 +251,9 @@ function PortAudioStream(indevname::AbstractString, outdevname::AbstractString,
error("No device matching \"$indevname\" found.\nAvailable Devices:\n$(devnames())")
end
if outdev == nothing
error("No device matching \"$outdevname\" found.\nAvailable Devices:\n$(devnames())")
error(
"No device matching \"$outdevname\" found.\nAvailable Devices:\n$(devnames())",
)
end
PortAudioStream(indev, outdev, args...; kwargs...)
@ -213,12 +261,17 @@ 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::Union{PortAudioDevice, AbstractString}, inchans=2, outchans=2; kwargs...)
function PortAudioStream(
device::Union{PortAudioDevice, AbstractString},
inchans = 2,
outchans = 2;
kwargs...,
)
PortAudioStream(device, device, inchans, outchans; kwargs...)
end
# use the default input and output devices
function PortAudioStream(inchans=2, outchans=2; kwargs...)
function PortAudioStream(inchans = 2, outchans = 2; kwargs...)
inidx = Pa_GetDefaultInputDevice()
indevice = PortAudioDevice(Pa_GetDeviceInfo(inidx), inidx)
outidx = Pa_GetDefaultOutputDevice()
@ -249,21 +302,37 @@ end
isopen(stream::PortAudioStream) = stream.stream != C_NULL
SampledSignals.samplerate(stream::PortAudioStream) = stream.samplerate
eltype(stream::PortAudioStream{T}) where T = T
eltype(stream::PortAudioStream{T}) where {T} = T
read(stream::PortAudioStream, args...) = read(stream.source, args...)
read!(stream::PortAudioStream, args...) = read!(stream.source, args...)
write(stream::PortAudioStream, args...) = write(stream.sink, args...)
write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...)
function write(sink::PortAudioStream, source::PortAudioStream, args...)
write(sink.sink, source.source, args...)
end
function show(io::IO, stream::PortAudioStream)
println(io, typeof(stream))
println(io, " Samplerate: ", samplerate(stream), "Hz")
if nchannels(stream.sink) > 0
print(io, "\n ", nchannels(stream.sink), " channel sink: \"", name(stream.sink), "\"")
print(
io,
"\n ",
nchannels(stream.sink),
" channel sink: \"",
name(stream.sink),
"\"",
)
end
if nchannels(stream.source) > 0
print(io, "\n ", nchannels(stream.source), " channel source: \"", name(stream.source), "\"")
print(
io,
"\n ",
nchannels(stream.source),
" channel source: \"",
name(stream.source),
"\"",
)
end
end
@ -272,8 +341,7 @@ end
#
# Define our source and sink types
for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
(:PortAudioSource, :SampleSource))
for (TypeName, Super) in ((:PortAudioSink, :SampleSink), (:PortAudioSource, :SampleSource))
@eval mutable struct $TypeName{T} <: $Super
name::String
stream::PortAudioStream{T}
@ -296,17 +364,16 @@ function close(s::Union{PortAudioSink, PortAudioSource})
throw(ErrorException("""
Attempted to close PortAudioSink or PortAudioSource.
Close the containing PortAudioStream instead
"""
))
"""))
end
isopen(s::Union{PortAudioSink, PortAudioSource}) = isopen(s.stream)
name(s::Union{PortAudioSink, PortAudioSource}) = s.name
function show(io::IO, ::Type{PortAudioSink{T}}) where T
function show(io::IO, ::Type{PortAudioSink{T}}) where {T}
print(io, "PortAudioSink{$T}")
end
function show(io::IO, ::Type{PortAudioSource{T}}) where T
function show(io::IO, ::Type{PortAudioSource{T}}) where {T}
print(io, "PortAudioSource{$T}")
end
@ -314,17 +381,23 @@ function show(io::IO, stream::T) where {T <: Union{PortAudioSink, PortAudioSourc
print(io, nchannels(stream), "-channel ", T, "(\"", stream.name, "\")")
end
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount)
function SampledSignals.unsafe_write(
sink::PortAudioSink,
buf::Array,
frameoffset,
framecount,
)
nwritten = 0
while nwritten < framecount
n = min(framecount-nwritten, CHUNKFRAMES)
n = min(framecount - nwritten, CHUNKFRAMES)
# make a buffer of interleaved samples
transpose!(view(sink.chunkbuf, :, 1:n),
view(buf, (1:n) .+ nwritten .+ frameoffset, :))
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
err = 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
@ -334,20 +407,31 @@ function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffse
nwritten
end
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameoffset, framecount)
function SampledSignals.unsafe_read!(
source::PortAudioSource,
buf::Array,
frameoffset,
framecount,
)
nread = 0
while nread < framecount
n = min(framecount-nread, CHUNKFRAMES)
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
err = 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))
transpose!(
view(buf, (1:n) .+ nread .+ frameoffset, :),
view(source.chunkbuf, :, 1:n),
)
nread += n
end
@ -391,20 +475,20 @@ function seek_alsa_conf(searchdirs)
isfile(joinpath(d, "alsa.conf"))
end
if confdir_idx === nothing
throw(ErrorException(
"""
Could not find ALSA config directory. Searched:
$(join(searchdirs, "\n"))
throw(
ErrorException("""
Could not find ALSA config directory. Searched:
$(join(searchdirs, "\n"))
If ALSA is installed, set the "ALSA_CONFIG_DIR" environment
variable. The given directory should have a file "alsa.conf".
If ALSA is installed, set the "ALSA_CONFIG_DIR" environment
variable. The given directory should have a file "alsa.conf".
If it would be useful to others, please file an issue at
https://github.com/JuliaAudio/PortAudio.jl/issues
with your alsa config directory so we can add it to the search
paths.
"""
))
If it would be useful to others, please file an issue at
https://github.com/JuliaAudio/PortAudio.jl/issues
with your alsa config directory so we can add it to the search
paths.
"""),
)
end
searchdirs[confdir_idx]
end
@ -413,11 +497,8 @@ function __init__()
if Sys.islinux()
envkey = "ALSA_CONFIG_DIR"
if envkey keys(ENV)
ENV[envkey] = seek_alsa_conf([
"/usr/share/alsa",
"/usr/local/share/alsa",
"/etc/alsa"
])
ENV[envkey] =
seek_alsa_conf(["/usr/share/alsa", "/usr/local/share/alsa", "/etc/alsa"])
end
plugin_key = "ALSA_PLUGIN_DIR"

View file

@ -21,20 +21,20 @@ const PA_OUTPUT_UNDERFLOWED = -10000 + 20
# sample format types
const paFloat32 = PaSampleFormat(0x01)
const paInt32 = PaSampleFormat(0x02)
const paInt24 = PaSampleFormat(0x04)
const paInt16 = PaSampleFormat(0x08)
const paInt8 = PaSampleFormat(0x10)
const paUInt8 = PaSampleFormat(0x20)
const paInt32 = PaSampleFormat(0x02)
const paInt24 = PaSampleFormat(0x04)
const paInt16 = PaSampleFormat(0x08)
const paInt8 = PaSampleFormat(0x10)
const paUInt8 = PaSampleFormat(0x20)
const paNonInterleaved = PaSampleFormat(0x80000000)
const type_to_fmt = Dict{Type, PaSampleFormat}(
Float32 => 1,
Int32 => 2,
Int32 => 2,
# Int24 => 4,
Int16 => 8,
Int8 => 16,
UInt8 => 3
Int16 => 8,
Int8 => 16,
UInt8 => 3,
)
const PaStreamCallbackResult = Cint
@ -104,7 +104,7 @@ const pa_host_api_names = Dict{PaHostApiTypeId, String}(
11 => "WDMKS",
12 => "Jack",
13 => "WASAPI",
14 => "AudioScience HPI"
14 => "AudioScience HPI",
)
mutable struct PaHostApiInfo
@ -117,8 +117,12 @@ mutable struct PaHostApiInfo
end
function Pa_GetHostApiInfo(i)
result = @locked ccall((:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo}, (PaHostApiIndex,), i)
result = @locked ccall(
(:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo},
(PaHostApiIndex,),
i,
)
if result == C_NULL
throw(BoundsError(Pa_GetHostApiInfo, i))
end
@ -143,19 +147,25 @@ end
Pa_GetDeviceCount() = @locked ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
function Pa_GetDeviceInfo(i)
result = @locked ccall((:Pa_GetDeviceInfo, libportaudio),
Ptr{PaDeviceInfo}, (PaDeviceIndex,), i)
result = @locked ccall(
(:Pa_GetDeviceInfo, libportaudio),
Ptr{PaDeviceInfo},
(PaDeviceIndex,),
i,
)
if result == C_NULL
throw(BoundsError(Pa_GetDeviceInfo, i))
end
unsafe_load(result)
end
Pa_GetDefaultInputDevice() = @locked ccall((:Pa_GetDefaultInputDevice, libportaudio),
PaDeviceIndex, ())
function Pa_GetDefaultInputDevice()
@locked ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
end
Pa_GetDefaultOutputDevice() = @locked ccall((:Pa_GetDefaultOutputDevice, libportaudio),
PaDeviceIndex, ())
function Pa_GetDefaultOutputDevice()
@locked ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ())
end
# Stream Functions
@ -189,81 +199,113 @@ end
# streamPtr[]
# end
#
function Pa_OpenStream(inParams, outParams,
sampleRate, framesPerBuffer,
flags::PaStreamFlags,
callback, userdata)
function Pa_OpenStream(
inParams,
outParams,
sampleRate,
framesPerBuffer,
flags::PaStreamFlags,
callback,
userdata,
)
streamPtr = Ref{PaStream}(0)
err = @locked ccall((:Pa_OpenStream, libportaudio), PaError,
(Ref{PaStream}, Ref{Pa_StreamParameters}, Ref{Pa_StreamParameters},
Cdouble, Culong, PaStreamFlags, Ref{Cvoid},
# it seems like we should be able to use Ref{T} here, with
# userdata::T above, and avoid the `pointer_from_objref` below.
# that's not working on 0.6 though, and it shouldn't really
# matter because userdata should be GC-rooted anyways
Ptr{Cvoid}),
streamPtr, inParams, outParams,
float(sampleRate), framesPerBuffer, flags,
callback === nothing ? C_NULL : callback,
userdata === nothing ? C_NULL : pointer_from_objref(userdata))
err = @locked ccall(
(:Pa_OpenStream, libportaudio),
PaError,
(
Ref{PaStream},
Ref{Pa_StreamParameters},
Ref{Pa_StreamParameters},
Cdouble,
Culong,
PaStreamFlags,
Ref{Cvoid},
# it seems like we should be able to use Ref{T} here, with
# userdata::T above, and avoid the `pointer_from_objref` below.
# that's not working on 0.6 though, and it shouldn't really
# matter because userdata should be GC-rooted anyways
Ptr{Cvoid},
),
streamPtr,
inParams,
outParams,
float(sampleRate),
framesPerBuffer,
flags,
callback === nothing ? C_NULL : callback,
userdata === nothing ? C_NULL : pointer_from_objref(userdata),
)
handle_status(err)
streamPtr[]
end
function Pa_StartStream(stream::PaStream)
err = @locked ccall((:Pa_StartStream, libportaudio), PaError,
(PaStream,), stream)
err = @locked ccall((:Pa_StartStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
end
function Pa_StopStream(stream::PaStream)
err = @locked ccall((:Pa_StopStream, libportaudio), PaError,
(PaStream,), stream)
err = @locked ccall((:Pa_StopStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
end
function Pa_CloseStream(stream::PaStream)
err = @locked ccall((:Pa_CloseStream, libportaudio), PaError,
(PaStream,), stream)
err = @locked ccall((:Pa_CloseStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
end
function Pa_GetStreamReadAvailable(stream::PaStream)
avail = @locked ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
(PaStream,), stream)
avail = @locked ccall(
(:Pa_GetStreamReadAvailable, libportaudio),
Clong,
(PaStream,),
stream,
)
avail >= 0 || handle_status(avail)
avail
end
function Pa_GetStreamWriteAvailable(stream::PaStream)
avail = @locked ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
(PaStream,), stream)
avail = @locked ccall(
(:Pa_GetStreamWriteAvailable, libportaudio),
Clong,
(PaStream,),
stream,
)
avail >= 0 || handle_status(avail)
avail
end
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer,
show_warnings=true)
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
# don't use ctrl-C.
err = disable_sigint() do
@tcall @locked ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Cvoid}, Culong),
stream, buf, frames)
@tcall @locked ccall(
(:Pa_ReadStream, libportaudio),
PaError,
(PaStream, Ptr{Cvoid}, Culong),
stream,
buf,
frames,
)
end
handle_status(err, show_warnings)
err
end
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer,
show_warnings=true)
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)
@tcall @locked ccall(
(:Pa_WriteStream, libportaudio),
PaError,
(PaStream, Ptr{Cvoid}, Culong),
stream,
buf,
frames,
)
end
handle_status(err, show_warnings)
err
@ -279,16 +321,15 @@ end
# end
#
# General utility function to handle the status from the Pa_* functions
function handle_status(err::Integer, show_warnings::Bool=true)
function handle_status(err::Integer, show_warnings::Bool = true)
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
if show_warnings
msg = @locked ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
msg =
@locked ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err)
@warn("libportaudio: " * unsafe_string(msg))
end
elseif err != PA_NO_ERROR
msg = @locked ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
msg = @locked ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err)
throw(ErrorException("libportaudio: " * unsafe_string(msg)))
end
end

View file

@ -2,13 +2,13 @@
using Logging: Debug
using PortAudio
using PortAudio:
using PortAudio:
combine_default_sample_rates,
handle_status,
Pa_GetDefaultInputDevice,
Pa_GetDefaultOutputDevice,
Pa_GetDeviceInfo,
Pa_GetHostApiInfo,
Pa_GetDefaultInputDevice,
Pa_GetDefaultOutputDevice,
Pa_GetDeviceInfo,
Pa_GetHostApiInfo,
Pa_Initialize,
PA_OUTPUT_UNDERFLOWED,
Pa_Terminate,
@ -62,7 +62,8 @@ if !isempty(PortAudio.devices())
stream = PortAudioStream(2, 0)
buf = read(stream, 5s)
close(stream)
@test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
@test size(buf) ==
(round(Int, 5 * samplerate(stream)), nchannels(stream.source))
println("Playing back recording...")
PortAudioStream(0, 2) do stream
write(stream, buf)
@ -73,20 +74,20 @@ if !isempty(PortAudio.devices())
source = stream.source
@test sprint(show, typeof(sink)) == "PortAudioSink{Float32}"
@test sprint(show, typeof(source)) == "PortAudioSource{Float32}"
@test sprint(show, sink) == "2-channel PortAudioSink{Float32}($(repr(default_indev)))"
@test sprint(show, source) == "2-channel PortAudioSource{Float32}($(repr(default_outdev)))"
@test sprint(show, sink) ==
"2-channel PortAudioSink{Float32}($(repr(default_indev)))"
@test sprint(show, source) ==
"2-channel PortAudioSource{Float32}($(repr(default_outdev)))"
write(stream, stream, 5s)
recover_xrun(stream)
@test_throws ErrorException("""
Attempted to close PortAudioSink or PortAudioSource.
Close the containing PortAudioStream instead
"""
) close(sink)
""") close(sink)
@test_throws ErrorException("""
Attempted to close PortAudioSink or PortAudioSource.
Close the containing PortAudioStream instead
"""
) close(source)
""") close(source)
close(stream)
@test !isopen(stream)
@test !isopen(sink)
@ -95,55 +96,74 @@ if !isempty(PortAudio.devices())
end
@testset "Samplerate-converting writing" begin
stream = PortAudioStream(0, 2)
write(stream, SinSource(eltype(stream), samplerate(stream)*0.8, [220, 330]), 3s)
write(stream, SinSource(eltype(stream), samplerate(stream)*1.2, [220, 330]), 3s)
write(
stream,
SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]),
3s,
)
write(
stream,
SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]),
3s,
)
close(stream)
end
@testset "Open Device by name" begin
stream = PortAudioStream(default_indev, default_outdev)
buf = read(stream, 0.001s)
@test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
@test size(buf) ==
(round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
write(stream, buf)
io = IOBuffer()
show(io, stream)
@test occursin("""
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel sink: "$default_outdev"
2 channel source: "$default_indev\"""", String(take!(io)))
@test occursin(
"""
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel sink: "$default_outdev"
2 channel source: "$default_indev\"""",
String(take!(io)),
)
close(stream)
end
@testset "Error handling" begin
@test_throws ErrorException PortAudioStream("foobarbaz")
@test_throws ErrorException PortAudioStream(default_indev, "foobarbaz")
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(PA_OUTPUT_UNDERFLOWED)
@test_throws ErrorException("libportaudio: PortAudio not initialized") handle_status(-10000)
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
PA_OUTPUT_UNDERFLOWED,
)
@test_throws ErrorException("libportaudio: PortAudio not initialized") handle_status(
-10000,
)
@test_throws ErrorException("""
Could not find ALSA config directory. Searched:
If ALSA is installed, set the "ALSA_CONFIG_DIR" environment
variable. The given directory should have a file "alsa.conf".
If it would be useful to others, please file an issue at
https://github.com/JuliaAudio/PortAudio.jl/issues
with your alsa config directory so we can add it to the search
paths.
""") seek_alsa_conf([])
@test_throws ErrorException(
"""
) seek_alsa_conf([])
@test_throws ErrorException("""
Can't open duplex stream with mismatched samplerates (in: 0, out: 1).
Try changing your sample rate in your driver settings or open separate input and output
streams.
"""
Can't open duplex stream with mismatched samplerates (in: 0, out: 1).
Try changing your sample rate in your driver settings or open separate input and output
streams.
""",
) combine_default_sample_rates(1, 0, 1, 1)
end
# no way to check that the right data is actually getting read or written here,
# but at least it's not crashing.
@testset "Queued Writing" begin
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)) * 0.1,
samplerate(stream),
)
t1 = @async write(stream, buf)
t2 = @async write(stream, buf)
@test fetch(t1) == 48000
@ -152,7 +172,10 @@ if !isempty(PortAudio.devices())
end
@testset "Queued Reading" begin
stream = PortAudioStream(2, 0)
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source))*0.1, samplerate(stream))
buf = SampleBuf(
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
samplerate(stream),
)
t1 = @async read!(stream, buf)
t2 = @async read!(stream, buf)
@test fetch(t1) == 48000

View file

@ -38,24 +38,28 @@ end
end
@testset "Samplerate-converting writing" begin
stream = PortAudioStream(0, 2)
write(stream, SinSource(eltype(stream), samplerate(stream)*0.8, [220, 330]), 3s)
write(stream, SinSource(eltype(stream), samplerate(stream)*1.2, [220, 330]), 3s)
write(stream, SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]), 3s)
write(stream, SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]), 3s)
flush(stream)
close(stream)
end
@testset "Open Device by name" begin
stream = PortAudioStream(default_indev, default_outdev)
buf = read(stream, 0.001s)
@test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
@test size(buf) ==
(round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
write(stream, buf)
io = IOBuffer()
show(io, stream)
@test occursin("""
PortAudioStream{Float32}
Samplerate: 44100.0Hz
Buffer Size: 4096 frames
2 channel sink: "$default_outdev"
2 channel source: "$default_indev\"""", String(take!(io)))
@test occursin(
"""
PortAudioStream{Float32}
Samplerate: 44100.0Hz
Buffer Size: 4096 frames
2 channel sink: "$default_outdev"
2 channel source: "$default_indev\"""",
String(take!(io)),
)
close(stream)
end
@testset "Error on wrong name" begin
@ -65,7 +69,10 @@ end
# but at least it's not crashing.
@testset "Queued Writing" begin
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)) * 0.1,
samplerate(stream),
)
t1 = @async write(stream, buf)
t2 = @async write(stream, buf)
@test fetch(t1) == 48000
@ -75,7 +82,10 @@ end
end
@testset "Queued Reading" begin
stream = PortAudioStream(2, 0)
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source))*0.1, samplerate(stream))
buf = SampleBuf(
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
samplerate(stream),
)
t1 = @async read!(stream, buf)
t2 = @async read!(stream, buf)
@test fetch(t1) == 48000