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 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) function micmeter(metersize)
mic = PortAudioStream(1, 0; latency=0.1) mic = PortAudioStream(1, 0; latency = 0.1)
signalmax = zero(eltype(mic)) signalmax = zero(eltype(mic))
println("Press Ctrl-C to quit") println("Press Ctrl-C to quit")
@ -16,28 +18,32 @@ function micmeter(metersize)
end end
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) function printmeter(metersize, signal, peak)
# calculate the positions in terms of characters # calculate the positions in terms of characters
peakpos = clamp(round(Int, peak * metersize), 0, metersize) peakpos = clamp(round(Int, peak * metersize), 0, metersize)
meterchars = clamp(round(Int, signal * metersize), 0, peakpos-1) meterchars = clamp(round(Int, signal * metersize), 0, peakpos - 1)
blankchars = max(0, peakpos-meterchars-1) blankchars = max(0, peakpos - meterchars - 1)
for position in 1:meterchars for position in 1:meterchars
printstyled(">", color=barcolor(metersize, position)) printstyled(">", color = barcolor(metersize, position))
end end
print(" " ^ blankchars) print(" "^blankchars)
printstyled("|", color=barcolor(metersize, peakpos)) printstyled("|", color = barcolor(metersize, peakpos))
print(" " ^ (metersize - peakpos)) print(" "^(metersize - peakpos))
end 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 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) function barcolor(metersize, position)
if position/metersize <= 0.5 if position / metersize <= 0.5
:green :green
elseif position == metersize elseif position == metersize
:red :red

View file

@ -15,30 +15,33 @@ function paudio()
return ostream = PortAudioStream(devs[devnum].name, 0, 2) return ostream = PortAudioStream(devs[devnum].name, 0, 2)
end end
play(ostream, sample::Array{Float64,1}) = write(ostream, sample) play(ostream, sample::Array{Float64, 1}) = write(ostream, sample)
play(ostr, sample::Array{Int64,1}) = play(ostr, Float64.(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 pitch::S
duration::T duration::T
sustained::Bool sustained::Bool
end end
function play(ostream, function play(
A::Note, ostream,
samplingfreq::Real=44100, A::Note,
shape::Function=t->0.6sin(t)+0.2sin(2t)+.05*sin(8t)) 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)) timesamples = 0:(1 / samplingfreq):(A.duration * (A.sustained ? 0.98 : 0.9))
v = Float64[shape(2π * A.pitch * t) for t in timesamples] v = Float64[shape(2π * A.pitch * t) for t in timesamples]
if !A.sustained if !A.sustained
decay_length = div(length(timesamples), 5) 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 end
play(ostream, v) play(ostream, v)
sleep(A.duration) sleep(A.duration)
end 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 ostream = paudio() # initialize audio for output
lyrics_syllables = lyrics == nothing ? nothing : split(lyrics) lyrics_syllables = lyrics == nothing ? nothing : split(lyrics)
lyrics_syllables != nothing && (lyrics_syllables[end] *= "\n") lyrics_syllables != nothing && (lyrics_syllables[end] *= "\n")
@ -46,7 +49,7 @@ function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
oldduration = 4 oldduration = 4
for line in split(melody, '\n') for line in split(melody, '\n')
percent_idx = findfirst('%', line) # Trim comment 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) for token in split(line)
pitch, duration, dotted, sustained = parsetoken(token) pitch, duration, dotted, sustained = parsetoken(token)
duration == nothing && (duration = oldduration) 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) if lyrics_syllables != nothing && 1 <= note_idx <= length(lyrics_syllables)
# Print the lyrics, omitting hyphens # Print the lyrics, omitting hyphens
if lyrics_syllables[note_idx][end] == '-' 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 else
print(lyrics_syllables[note_idx], ' ') print(lyrics_syllables[note_idx], ' ')
end end
@ -66,18 +69,19 @@ function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
end end
end end
function parsetoken(token, Atuning::Real=220) function parsetoken(token, Atuning::Real = 220)
state = :findpitch state = :findpitch
pitch = 0.0 pitch = 0.0
sustain = dotted = false sustain = dotted = false
lengthbuf = Char[] lengthbuf = Char[]
for char in token for char in token
if state == :findpitch 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) something(findfirst(char, String(collect('A':'G'))), 0)
if scale_idx != 0 if scale_idx != 0
halfsteps = [12, 14, 3, 5, 7, 8, 10] halfsteps = [12, 14, 3, 5, 7, 8, 10]
pitch = Atuning * 2 ^ (halfsteps[scale_idx] / 12) pitch = Atuning * 2^(halfsteps[scale_idx] / 12)
state = :findlength state = :findlength
elseif char == 'r' elseif char == 'r'
pitch, state = 0, :findlength pitch, state = 0, :findlength
@ -85,22 +89,28 @@ function parsetoken(token, Atuning::Real=220)
error("unknown pitch: $char") error("unknown pitch: $char")
end end
elseif state == :findlength elseif state == :findlength
if char == '#' ; pitch *= 2^(1 / 12) # sharp if char == '#'
elseif char == 'b' ; pitch /= 2^(1 / 12) # flat pitch *= 2^(1 / 12) # sharp
elseif char == '\''; pitch *= 2 # higher octave elseif char == 'b'
elseif char == ',' ; pitch /= 2 # lower octave pitch /= 2^(1 / 12) # flat
elseif char == '.' ; dotted = true # dotted note elseif char == '\''
elseif char == '~' ; sustain = true # tied note pitch *= 2 # higher octave
elseif char == ','
pitch /= 2 # lower octave
elseif char == '.'
dotted = true # dotted note
elseif char == '~'
sustain = true # tied note
else else
push!(lengthbuf, char) push!(lengthbuf, char)
# Check for "is" and "es" suffixes for sharps and flats # Check for "is" and "es" suffixes for sharps and flats
if length(lengthbuf) >= 2 if length(lengthbuf) >= 2
if lengthbuf[end-1:end] == "is" if lengthbuf[(end - 1):end] == "is"
pitch *= 2^(1 / 12) pitch *= 2^(1 / 12)
lengthbuf = lengthbuf[1:end-2] lengthbuf = lengthbuf[1:(end - 2)]
elseif lengthbuf[end-1:end] == "es" elseif lengthbuf[(end - 1):end] == "es"
pitch /= 2^(1 / 12) pitch /= 2^(1 / 12)
lengthbuf = lengthbuf[1:end-2] lengthbuf = lengthbuf[1:(end - 2)]
end end
end end
end end
@ -112,23 +122,27 @@ function parsetoken(token, Atuning::Real=220)
return (pitch, duration, sustain, dotted) return (pitch, duration, sustain, dotted)
end end
parsevoice(""" parsevoice(
"""
f# f# g a a g f# e d d e f# f#~ f#8 e e2 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 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, 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""", f#2 f#4 g a a g f# e d d e f# e~ e8 d8 d2""",
lyrics=""" lyrics = """
Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um! 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! 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, 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. al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt.
""") """,
)
# And now with harmony! # 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 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(""" alto = @spawn parsevoice("""
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2 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(tenor)
wait(bass) 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 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(""" alto = @spawn parsevoice("""
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2 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() function create_measure_signal()
signal = zeros(Float32, 20000) signal = zeros(Float32, 20000)
for i in 1:3 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 end
return signal return signal
end end
function measure_latency(in_latency = 0.1, out_latency=0.1; is_warmup = false) function measure_latency(in_latency = 0.1, out_latency = 0.1; is_warmup = false)
in_stream = PortAudioStream(1, 0; latency = in_latency)
in_stream = PortAudioStream(1,0; latency=in_latency) out_stream = PortAudioStream(0, 1; latency = out_latency)
out_stream = PortAudioStream(0,1; latency=out_latency)
cond = Base.Event() 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() signal = create_measure_signal()
writer = Threads.@spawn begin writer = Threads.@spawn begin
wait(cond) wait(cond)
reader_start_time = time_ns() |> Int64 reader_start_time = time_ns() |> Int64
write(out_stream, signal) write(out_stream, signal)
end end
notify(cond) notify(cond)
@ -37,8 +36,8 @@ function measure_latency(in_latency = 0.1, out_latency=0.1; is_warmup = false)
wait(reader) wait(reader)
wait(writer) wait(writer)
recorded = collect(reader.result)[:,1] recorded = collect(reader.result)[:, 1]
close(in_stream) close(in_stream)
close(out_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 while true
read!(stream, buf) 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
end end

View file

@ -8,8 +8,8 @@ The rightmosts column is discarded and the leftmost column is
left alone. left alone.
""" """
function shift1!(buf::AbstractMatrix) function shift1!(buf::AbstractMatrix)
for col in size(buf,2):-1:2 for col in size(buf, 2):-1:2
@. buf[:, col] = buf[:, col-1] @. buf[:, col] = buf[:, col - 1]
end end
end end
@ -20,7 +20,7 @@ function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan)
readbuf .*= win readbuf .*= win
A_mul_B!(fftbuf, fftplan, readbuf) A_mul_B!(fftbuf, fftplan, readbuf)
shift1!(dispbuf) 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 end
function processblock!(src, buf, win, dispbufs, fftbuf, fftplan) function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
@ -31,17 +31,17 @@ function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
end end
N = 1024 # size of audio read 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 D = 200 # number of bins to display
M = 200 # amount of history to keep M = 200 # amount of history to keep
src = PortAudioStream(1, 2) src = PortAudioStream(1, 2)
buf = Array{Float32}(N) # buffer for reading 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 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 dispbufs = [zeros(Float32, D, M) for i in 1:5, j in 1:5] # STFT bufs
win = gaussian(N, 0.125) 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 #pre-fill the display buffer so we can do a reasonable colormap
for _ in 1:M for _ in 1:M
@ -53,7 +53,7 @@ heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf
buf = ibuf[2] buf = ibuf[2]
# some function of the 2D index and the value # 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 end
center!(scene) center!(scene)

View file

@ -2,7 +2,7 @@ using Makie, GeometryTypes
using PortAudio using PortAudio
N = 1024 # size of audio read 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 D = 200 # number of bins to display
M = 100 # number of lines to draw M = 100 # number of lines to draw
S = 0.5 # motion speed of lines S = 0.5 # motion speed of lines
@ -10,19 +10,24 @@ src = PortAudioStream(1, 2)
buf = Array{Float32}(N) buf = Array{Float32}(N)
fftbuf = Array{Complex{Float32}}(N2) fftbuf = Array{Complex{Float32}}(N2)
magbuf = Array{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) ax = axis(0:0.1:1, 0:0.1:1, 0:0.1:0.5)
center!(scene) center!(scene)
ls = map(1:M) do _ ls = map(1:M) do _
yoffset = to_node(to_value(scene[:time])) yoffset = to_node(to_value(scene[:time]))
offset = lift_node(scene[:time], yoffset) do t, yoff 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 end
l = lines(linspace(0,1,D), 0.0f0, zeros(Float32, D), l = lines(
offset=offset, color=(:black, 0.1)) linspace(0, 1, D),
0.0f0,
zeros(Float32, D),
offset = offset,
color = (:black, 0.1),
)
(yoffset, l) (yoffset, l)
end end
@ -31,7 +36,7 @@ while isopen(scene[:screen])
isopen(scene[:screen]) || break isopen(scene[:screen]) || break
read!(src, buf) read!(src, buf)
A_mul_B!(fftbuf, fftplan, 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] line[:z] = magbuf[1:D]
push!(yoffset, to_value(scene[:time])) push!(yoffset, to_value(scene[:time]))
end end

View file

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

View file

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

View file

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

View file

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