Compare commits

..

4 commits

Author SHA1 Message Date
Brandon Taylor
d90a483c83 version 2022-03-09 14:42:46 -05:00
Brandon Taylor
5056541968 makie fail 2022-03-08 20:36:47 -05:00
Brandon Taylor
f708835e5a Get examples running 2022-02-13 22:40:39 -05:00
Brandon Taylor
aa8e9fc7cd no runtime error capturing 2022-01-06 13:58:10 -05:00
12 changed files with 225 additions and 396 deletions

View file

@ -1,7 +1,7 @@
name = "PortAudio" name = "PortAudio"
uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d" uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d"
repo = "https://github.com/JuliaAudio/PortAudio.jl.git" repo = "https://github.com/JuliaAudio/PortAudio.jl.git"
version = "1.3.0" version = "1.2.0"
[deps] [deps]
alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7" alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7"
@ -11,7 +11,7 @@ SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
[compat] [compat]
julia = "1.6" julia = "1.3"
alsa_plugins_jll = "1.2.2" alsa_plugins_jll = "1.2.2"
libportaudio_jll = "19.6.0" libportaudio_jll = "19.6.0"
SampledSignals = "2.1.1" SampledSignals = "2.1.1"
@ -19,8 +19,11 @@ Suppressor = "0.2"
[extras] [extras]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
DSP = "717857b8-e6f2-59f4-9121-6e50c889abd2"
FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341"
GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71"
LibSndFile = "b13ce0c6-77b0-50c6-a2db-140568b8d1a5" LibSndFile = "b13ce0c6-77b0-50c6-a2db-140568b8d1a5"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets] [targets]
test = ["Documenter", "LibSndFile", "Test"] test = ["DSP", "Documenter", "FFTW", "GR", "LibSndFile", "Test"]

View file

@ -10,14 +10,10 @@ PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which g
## Opening a stream ## Opening a stream
The easiest way to open a source or sink is with the default `PortAudioStream()` constructor, The easiest way to open a source or sink is with the default `PortAudioStream()` constructor, which will open a 2-in, 2-out stream to your system's default device(s). The constructor can also take the input and output channel counts as positional arguments, or a variety of other keyword arguments.
which will open a 2-in, 2-out stream to your system's default device(s).
The constructor can also take the input and output channel counts as positional arguments,
or a variety of other keyword arguments.
If named keyword arguments `latency` or `samplerate` are unspecified, then PortAudio will use device defaults.
```julia ```julia
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000, latency=0.1) PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000Hz, latency=0.1, synced=false)
``` ```
You can open a specific device by adding it as the first argument, either as a `PortAudioDevice` instance or by name. You can also give separate names or devices if you want different input and output devices You can open a specific device by adding it as the first argument, either as a `PortAudioDevice` instance or by name. You can also give separate names or devices if you want different input and output devices
@ -31,16 +27,13 @@ You can get a list of your system's devices with the `PortAudio.devices()` funct
```julia ```julia
julia> PortAudio.devices() julia> PortAudio.devices()
14-element Vector{PortAudio.PortAudioDevice}: 6-element Array{PortAudio.PortAudioDevice,1}:
"sof-hda-dsp: - (hw:0,0)" 2→2 PortAudio.PortAudioDevice("AirPlay","Core Audio",0,2,0)
"sof-hda-dsp: - (hw:0,3)" 0→2 PortAudio.PortAudioDevice("Built-in Microph","Core Audio",2,0,1)
"sof-hda-dsp: - (hw:0,4)" 0→2 PortAudio.PortAudioDevice("Built-in Output","Core Audio",0,2,2)
"sof-hda-dsp: - (hw:0,5)" 0→2 PortAudio.PortAudioDevice("JackRouter","Core Audio",2,2,3)
PortAudio.PortAudioDevice("After Effects 13.5","Core Audio",0,0,4)
"upmix" 8→8 PortAudio.PortAudioDevice("Built-In Aggregate","Core Audio",2,2,5)
"vdownmix" 6→6
"dmix" 0→2
"default" 32→32
``` ```
## Reading and Writing ## Reading and Writing
@ -82,7 +75,7 @@ end
### Open your built-in microphone and speaker by name ### Open your built-in microphone and speaker by name
```julia ```julia
PortAudioStream("default", "default") do stream PortAudioStream("Built-in Microph", "Built-in Output") do stream
write(stream, stream) write(stream, stream)
end end
``` ```
@ -90,18 +83,13 @@ end
### Record 10 seconds of audio and save to an ogg file ### Record 10 seconds of audio and save to an ogg file
```julia ```julia
julia> import LibSndFile # must be in Manifest for FileIO.save to work julia> using PortAudio, SampledSignals, LibSndFile
julia> using PortAudio: PortAudioStream julia> stream = PortAudioStream("Built-in Microph", 2, 0)
PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
julia> using SampledSignals: s Samplerate: 48000 s⁻¹
Buffer Size: 4096 frames
julia> using FileIO: save 2 channel source: "Built-in Microph"
julia> stream = PortAudioStream(1, 0) # default input (e.g., built-in microphone)
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel source: "default"
julia> buf = read(stream, 10s) julia> buf = read(stream, 10s)
480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}} 480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
@ -112,15 +100,3 @@ julia> buf = read(stream, 10s)
julia> close(stream) julia> close(stream)
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf) julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
```
### Play an audio signal through the default sound output device
```julia
using PortAudio, SampledSignals
S = 8192 # sampling rate (samples / second)
x = cos.(2pi*(1:2S)*440/S) # A440 tone for 2 seconds
PortAudioStream(0, 2; samplerate=S) do stream
write(stream, x)
end
```

View file

@ -4,18 +4,23 @@ using PortAudio
Continuously read from the default audio input and plot an Continuously read from the default audio input and plot an
ASCII level/peak meter ASCII level/peak meter
""" """
function micmeter(metersize) function micmeter(seconds; metersize = 80)
mic = PortAudioStream(1, 0; latency = 0.1) PortAudioStream(1, 0; latency = 0.1) do mic
done = false
signalmax = zero(eltype(mic)) signalmax = zero(eltype(mic))
println("Press Ctrl-C to quit") @sync begin
while true @async while !done
block = read(mic, 512) block = read(mic, 512)
blockmax = maximum(abs.(block)) # find the maximum value in the block blockmax = maximum(abs.(block)) # find the maximum value in the block
signalmax = max(signalmax, blockmax) # keep the maximum value ever signalmax = max(signalmax, blockmax) # keep the maximum value ever
print("\r") # reset the cursor to the beginning of the line print("\r") # reset the cursor to the beginning of the line
printmeter(metersize, blockmax, signalmax) printmeter(metersize, blockmax, signalmax)
end end
sleep(seconds)
done = true
end
end
nothing
end end
""" """
@ -52,4 +57,4 @@ function barcolor(metersize, position)
end end
end end
micmeter(80) micmeter(5)

View file

@ -1,22 +1,8 @@
using Distributed, PortAudio using PortAudio
using Base.Threads: @spawn
# Modified from Jiahao Chen's example in the obsolete AudioIO module. # Modified from Jiahao Chen's example in the obsolete AudioIO module.
# Will use first output device found in system's listing or DEFAULTDEVICE if set below # Will use first output device found in system's listing or DEFAULTDEVICE if set below
const DEFAULTDEVICE = -1
function paudio()
devs = PortAudio.devices()
if DEFAULTDEVICE < 0
devnum = findfirst(x -> x.maxoutchans > 0, devs)
(devnum == nothing) && error("No output device for audio found")
else
devnum = DEFAULTDEVICE + 1
end
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))
struct Note{S <: Real, T <: Real} struct Note{S <: Real, T <: Real}
pitch::S pitch::S
@ -37,12 +23,11 @@ function play(
v[(end - decay_length):(end - 1)] = v[(end - decay_length):(end - 1)] =
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length) v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
end end
play(ostream, v) write(ostream, v)
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 = PortAudioStream(0, 2; warn_xruns = false) # 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")
note_idx = 1 note_idx = 1

View file

@ -1,89 +0,0 @@
#=
This code illustrates real-time octave down shift
using a crude FFT-based method.
It also plots the input and output signals and their spectra.
This code uses the system defaults for the audio input and output devices.
If you use the built-in speakers and built-in microphone,
you will likely get undesirable audio feedback.
It works "best" if you play the audio output through headphones
so that the output does not feed back into the input.
The spectrum plotting came from the example in
https://github.com/JuliaAudio/PortAudio.jl/blob/master/examples
=#
using PortAudio: PortAudioStream
using SampledSignals: Hz, domain
using SampledSignals: (..) # see EllipsisNotation.jl and IntervalSets.jl
using FFTW: fft, ifft
using Plots: plot, gui, default; default(label="")
function pitch_halver(x) # decrease pitch by one octave via FFT
N = length(x)
mod(N,2) == 0 || throw("N must be multiple of 2")
F = fft(x) # original spectrum
Fnew = [F[1:N÷2]; zeros(N+1); F[(N÷2+2):N]]
out = 2 * real(ifft(Fnew))[1:N]
out.samplerate /= 2 # trick!
return out
end
# Plot input and output signals and their spectra.
# Quantize the vertical axis limits to reduce plot jitter.
function plotter(buf, out, N, fmin, fmax, fs; quant::Number = 0.1)
bmax = quant * ceil(maximum(abs, buf) / quant)
xticks = [1, N]; ylims = (-1,1) .* bmax; yticks = (-1:1)*bmax
p1 = plot(buf; xticks, ylims, yticks, title="input")
p3 = plot(out; xticks, ylims, yticks, title="output")
X = (2/N) * abs.(fft(buf)[fmin..fmax]) # spectrum
Xmax = quant * ceil(maximum(X) / quant)
xlims = (fs[1], fs[end]); ylims = (0, Xmax); yticks = [0,Xmax]
p2 = plot(fs, X; xlims, ylims, yticks)
Y = (2/N) * abs.(fft(out)[fmin..fmax])
p4 = plot(fs, Y; xlims, ylims, yticks)
plot(p1, p2, p3, p4)
end
"""
octave_shift(seconds; N, ...)
Shift audio down by one octave.
# Input
* `seconds` : how long to run in seconds; defaults to 300 (5 minutes)
# Options
* `N` : buffer size; default 1024 samples
* `fmin`,`fmax` : range of frequencies to display; default 0Hz to 4000Hz
"""
function octave_shift(
seconds::Number = 300;
N::Int = 1024,
fmin::Number = 0Hz,
fmax::Number = 4000Hz,
# undocumented options below here that are unlikely to be modified
in_stream = PortAudioStream(1, 0), # default input device
out_stream = PortAudioStream(0, 1), # default output device
buf::AbstractArray = read(in_stream, N), # warm-up
fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])],
Niters::Int = ceil(Int, seconds * in_stream.sample_rate / N),
)
for _ in 1:Niters
read!(in_stream, buf)
out = pitch_halver(buf) # decrease pitch by one octave
write(out_stream, out)
plotter(buf, out, N, fmin, fmax, fs); gui()
end
nothing
end
octave_shift(5)

View file

@ -1,20 +1,26 @@
# plot a real-time spectrogram. This example is adapted from the GR example # plot a real-time spectrogram. This example is adapted from the GR example
# at http://gr-framework.org/examples/audio_ex.html # at http://gr-framework.org/examples/audio_ex.html
module SpectrumExample
using GR, PortAudio, SampledSignals, FFTW using GR, PortAudio, SampledSignals, FFTW
const N = 1024 function plot_spectrogram(seconds;
const stream = PortAudioStream(1, 0) N = 1024,
const buf = read(stream, N) fmin = 0Hz,
const fmin = 0Hz fmax = 10000Hz
const fmax = 10000Hz )
const fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])] PortAudioStream(1, 0) do stream
done = false
while true buf = read(stream, N)
fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
@sync begin
@async while !done
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
sleep(seconds)
done = true
end end
end
end
plot_spectrogram(5)

View file

@ -1,21 +0,0 @@
#=
This example illustrates synthesizing a long tone in small pieces
and routing it to the default audio output device using `write()`.
=#
using PortAudio: PortAudioStream, write
stream = PortAudioStream(0, 1; warn_xruns=false)
function play_tone(stream, freq::Real, duration::Real; buf_size::Int = 1024)
S = stream.sample_rate
current = 1
while current < duration*S
x = 0.7 * sin.(2π * (current .+ (1:buf_size)) * freq / S)
write(stream, x)
current += buf_size
end
nothing
end
play_tone(stream, 440, 2)

View file

@ -1,6 +1,8 @@
using Makie using Makie
using PortAudio using PortAudio
using DSP using DSP
using LinearAlgebra
using FFTW
""" """
Slide the values in the given matrix to the right by 1. Slide the values in the given matrix to the right by 1.
@ -16,28 +18,30 @@ end
""" """
takes a block of audio, FFT it, and write it to the beginning of the buffer takes a block of audio, FFT it, and write it to the beginning of the buffer
""" """
function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan) function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan; D = 200)
readbuf .*= win readbuf .*= win
A_mul_B!(fftbuf, fftplan, readbuf) mul!(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; D = 200)
read!(src, buf) read!(src, buf)
for dispbuf in dispbufs for dispbuf in dispbufs
processbuf!(buf, win, dispbuf, fftbuf, fftplan) processbuf!(buf, win, dispbuf, fftbuf, fftplan; D = D)
end end
end end
N = 1024 # size of audio read function waterfall_heatmap(seconds;
N = 1024, # size of audio read
D = 200, # number of bins to display
M = 200, # amount of history to keep
)
N2 = N ÷ 2 + 1 # size of rfft output N2 = N ÷ 2 + 1 # size of rfft output
D = 200 # number of bins to display PortAudioStream(1, 2) do src
M = 200 # amount of history to keep buf = Array{Float32}(undef, N) # buffer for reading
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 fftbuf = Array{Complex{Float32}}(undef, 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)
@ -45,10 +49,10 @@ 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
processblock!(src, buf, win, dispbufs, fftbuf, fftplan) processblock!(src, buf, win, dispbufs, fftbuf, fftplan; D = D)
end end
heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf heatmaps = map(zip(CartesianIndices(dispbufs), dispbufs)) do ibuf
i = ibuf[1] i = ibuf[1]
buf = ibuf[2] buf = ibuf[2]
@ -58,10 +62,20 @@ end
center!(scene) center!(scene)
while isopen(scene[:screen]) done = false
processblock!(src, buf, dispbufs, fftbuf, fftplan)
@sync begin
@async while !done
processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
for (hm, db) in zip(heatmaps, dispbufs) for (hm, db) in zip(heatmaps, dispbufs)
hm[:heatmap] = db hm[:heatmap] = db
end end
render_frame(scene) render_frame(scene)
end end
sleep(seconds)
done = true
end
end
end
waterfall_heatmap(5)

View file

@ -1,15 +1,19 @@
using Makie, GeometryTypes using Makie, GeometryTypes
using PortAudio using PortAudio
using FFTW
using CairoMakie
N = 1024 # size of audio read function waterfall_lines(seconds;
N = 1024, # size of audio read
D = 200, # number of bins to display
M = 100, # number of lines to draw
S = 0.5, # motion speed of lines
)
PortAudioStream(1, 2) do src
N2 = N ÷ 2 + 1 # size of rfft output N2 = N ÷ 2 + 1 # size of rfft output
D = 200 # number of bins to display buf = Array{Float32}(undef, N)
M = 100 # number of lines to draw fftbuf = Array{Complex{Float32}}(undef, N2)
S = 0.5 # motion speed of lines magbuf = Array{Float32}(undef, N2)
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))
@ -30,8 +34,10 @@ ls = map(1:M) do _
) )
(yoffset, l) (yoffset, l)
end end
done = false
while isopen(scene[:screen]) @sync begin
@async begin
while !done
for (yoffset, line) in ls for (yoffset, line) in ls
isopen(scene[:screen]) || break isopen(scene[:screen]) || break
read!(src, buf) read!(src, buf)
@ -41,3 +47,11 @@ while isopen(scene[:screen])
push!(yoffset, to_value(scene[:time])) push!(yoffset, to_value(scene[:time]))
end end
end end
end
sleep(seconds)
done = true
end
end
end
waterfall_lines(5)

View file

@ -48,7 +48,6 @@ using .LibPortAudio:
Pa_Initialize, Pa_Initialize,
paInputOverflowed, paInputOverflowed,
Pa_IsStreamStopped, Pa_IsStreamStopped,
paNoDevice,
paNoFlag, paNoFlag,
Pa_OpenStream, Pa_OpenStream,
paOutputUnderflowed, paOutputUnderflowed,
@ -196,22 +195,12 @@ function show(io::IO, device::PortAudioDevice)
print(io, device.output_bounds.max_channels) print(io, device.output_bounds.max_channels)
end end
function check_device_exists(device_index, device_type)
if device_index == paNoDevice
throw(ArgumentError("No $device_type device available"))
end
end
function get_default_input_index() function get_default_input_index()
device_index = Pa_GetDefaultInputDevice() handle_status(Pa_GetDefaultInputDevice())
check_device_exists(device_index, "input")
device_index
end end
function get_default_output_index() function get_default_output_index()
device_index = Pa_GetDefaultOutputDevice() handle_status(Pa_GetDefaultOutputDevice())
check_device_exists(device_index, "output")
device_index
end end
# we can look up devices by index or name # we can look up devices by index or name
@ -241,25 +230,19 @@ function devices()
end end
# we can handle reading and writing from buffers in a similar way # we can handle reading and writing from buffers in a similar way
function read_or_write(a_function, buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) function read_or_write(a_function, buffer, use_frames = buffer.frames_per_buffer)
pointer_to = buffer.pointer_to
data = buffer.data
handle_status( handle_status(
if acquire_lock
# because we're calling Pa_ReadStream and Pa_WriteStream from separate threads, # because we're calling Pa_ReadStream and Pa_WriteStream from separate threads,
# we put a lock around these calls # we put a lock around these calls
lock( lock(
let a_function = a_function, let a_function = a_function,
pointer_to = pointer_to, pointer_to = buffer.pointer_to,
data = data, data = buffer.data,
use_frames = use_frames use_frames = use_frames
() -> a_function(pointer_to, data, use_frames) () -> a_function(pointer_to, data, use_frames)
end, end,
buffer.stream_lock, buffer.stream_lock,
) ),
else
a_function(pointer_to, data, use_frames)
end;
warn_xruns = buffer.warn_xruns, warn_xruns = buffer.warn_xruns,
) )
end end
@ -396,7 +379,7 @@ Use [`PortAudio.write_buffer`](@ref) to write data to PortAudio, and [`PortAudio
struct Buffer{Sample} struct Buffer{Sample}
stream_lock::ReentrantLock stream_lock::ReentrantLock
pointer_to::Ptr{PaStream} pointer_to::Ptr{PaStream}
data::Array{Sample, 2} data::Array{Sample}
number_of_channels::Int number_of_channels::Int
frames_per_buffer::Int frames_per_buffer::Int
warn_xruns::Bool warn_xruns::Bool
@ -424,39 +407,25 @@ eltype(::Type{Buffer{Sample}}) where {Sample} = Sample
nchannels(buffer::Buffer) = buffer.number_of_channels nchannels(buffer::Buffer) = buffer.number_of_channels
""" """
PortAudio.write_buffer(buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) PortAudio.write_buffer(buffer, use_frames = buffer.frames_per_buffer)
Write a number of frames (`use_frames`) from a [`PortAudio.Buffer`](@ref) to PortAudio. Write a number of frames (`use_frames`) from a [`PortAudio.Buffer`](@ref) to PortAudio.
Set `acquire_lock = false` to skip acquiring the lock.
""" """
function write_buffer(buffer::Buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) function write_buffer(buffer::Buffer, use_frames = buffer.frames_per_buffer)
read_or_write(Pa_WriteStream, buffer, use_frames; acquire_lock = acquire_lock) read_or_write(Pa_WriteStream, buffer, use_frames)
end end
""" """
PortAudio.read_buffer!(buffer::Buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) PortAudio.read_buffer!(buffer::Buffer, use_frames = buffer.frames_per_buffer)
Read a number of frames (`use_frames`) from PortAudio to a [`PortAudio.Buffer`](@ref). Read a number of frames (`use_frames`) from PortAudio to a [`PortAudio.Buffer`](@ref).
Set `acquire_lock = false` to skip acquiring the acquire_lock.
""" """
function read_buffer!(buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) function read_buffer!(buffer, use_frames = buffer.frames_per_buffer)
read_or_write(Pa_ReadStream, buffer, use_frames; acquire_lock = acquire_lock) read_or_write(Pa_ReadStream, buffer, use_frames)
end end
""" # the messenger will send tasks to the scribe
Messenger{Sample, Scribe, Input, Output} # the scribe will read/write from the buffer
A `struct` with entries
* `device_name::String`
* `buffer::Buffer{Sample}`
* `scribe::Scribe`
* `input_channel::Channel{Input}`
* `output_channel::Channel{Output}`
The messenger will send tasks to the scribe;
the scribe will read/write from the buffer.
"""
struct Messenger{Sample, Scribe, Input, Output} struct Messenger{Sample, Scribe, Input, Output}
device_name::String device_name::String
buffer::Buffer{Sample} buffer::Buffer{Sample}
@ -528,7 +497,7 @@ function messenger_task(
messenger, task messenger, task
end end
function fetch_messenger(messenger, task) function fetch_messanger(messenger, task)
if has_channels(messenger) if has_channels(messenger)
# this will shut down the channels, which will shut down the thread # this will shut down the channels, which will shut down the thread
close(messenger.input_channel) close(messenger.input_channel)
@ -548,9 +517,9 @@ struct PortAudioStream{SinkMessenger, SourceMessenger}
sample_rate::Float64 sample_rate::Float64
# pointer to the c object # pointer to the c object
pointer_to::Ptr{PaStream} pointer_to::Ptr{PaStream}
sink_messenger::SinkMessenger sink_messanger::SinkMessenger
sink_task::Task sink_task::Task
source_messenger::SourceMessenger source_messanger::SourceMessenger
source_task::Task source_task::Task
end end
@ -615,9 +584,10 @@ function combine_default_sample_rates(
) )
if input_sample_rate != output_sample_rate if input_sample_rate != output_sample_rate
throw( throw(
ArgumentError(""" ArgumentError(
Default sample rate $input_sample_rate for input \"$(name(input_device))\" disagrees with """
default sample rate $output_sample_rate for output \"$(name(output_device))\". Default sample rate $input_sample_rate for input $(name(input_device)) disagrees with
default sample rate $output_sample_rate for output $(name(output_device)).
Please specify a sample rate. Please specify a sample rate.
""", """,
), ),
@ -760,24 +730,20 @@ function PortAudioStream(
output_device.output_bounds.high_latency, output_device.output_bounds.high_latency,
) )
end end
samplerate = if samplerate === nothing if samplerate === nothing
combine_default_sample_rates( samplerate = combine_default_sample_rates(
input_device, input_device,
input_device.default_sample_rate, input_device.default_sample_rate,
output_device, output_device,
output_device.default_sample_rate, output_device.default_sample_rate,
) )
else
float(samplerate)
end end
else else
if latency === nothing if latency === nothing
latency = input_device.input_bounds.high_latency latency = input_device.input_bounds.high_latency
end end
samplerate = if samplerate === nothing if samplerate === nothing
input_device.default_sample_rate samplerate = input_device.default_sample_rate
else
float(samplerate)
end end
end end
else else
@ -785,10 +751,8 @@ function PortAudioStream(
if latency === nothing if latency === nothing
latency = output_device.output_bounds.high_latency latency = output_device.output_bounds.high_latency
end end
samplerate = if samplerate === nothing if samplerate === nothing
output_device.default_sample_rate samplerate = output_device.default_sample_rate
else
float(samplerate)
end end
else else
throw(ArgumentError("Input or output must have at least 1 channel")) throw(ArgumentError("Input or output must have at least 1 channel"))
@ -904,8 +868,8 @@ function close(stream::PortAudioStream)
# closing is tricky, because we want to make sure we've read exactly as much as we've written # closing is tricky, because we want to make sure we've read exactly as much as we've written
# but we have don't know exactly what the tasks are doing # but we have don't know exactly what the tasks are doing
# for now, just close one and then the other # for now, just close one and then the other
fetch_messenger(stream.source_messenger, stream.source_task) fetch_messanger(stream.source_messanger, stream.source_task)
fetch_messenger(stream.sink_messenger, stream.sink_task) fetch_messanger(stream.sink_messanger, stream.sink_task)
pointer_to = stream.pointer_to pointer_to = stream.pointer_to
# only stop if it's not already stopped # only stop if it's not already stopped
if !Bool(handle_status(Pa_IsStreamStopped(pointer_to))) if !Bool(handle_status(Pa_IsStreamStopped(pointer_to)))
@ -927,14 +891,13 @@ end
isopen(stream::PortAudioStream) = isopen(stream.pointer_to) isopen(stream::PortAudioStream) = isopen(stream.pointer_to)
samplerate(stream::PortAudioStream) = stream.sample_rate samplerate(stream::PortAudioStream) = stream.sample_rate
function eltype( function eltype(
::Type{<:PortAudioStream{<:Messenger{Sample}, <:Messenger{Sample}}}, ::Type{<:PortAudioStream{<:Messenger{Sample}, <:Messenger{Sample}}},
) where {Sample} ) where {Sample}
Sample Sample
end end
# these defaults will error for non-SampledSignals scribes # these defaults will error for non-sampledsignal scribes
# which is probably ok; we want these users to define new methods # which is probably ok; we want these users to define new methods
read(stream::PortAudioStream, arguments...) = read(stream.source, arguments...) read(stream::PortAudioStream, arguments...) = read(stream.source, arguments...)
read!(stream::PortAudioStream, arguments...) = read!(stream.source, arguments...) read!(stream::PortAudioStream, arguments...) = read!(stream.source, arguments...)
@ -948,7 +911,7 @@ function show(io::IO, stream::PortAudioStream)
print(io, "PortAudioStream{") print(io, "PortAudioStream{")
print(io, eltype(stream)) print(io, eltype(stream))
println(io, "}") println(io, "}")
print(io, " Samplerate: ", round(Int, samplerate(stream)), "Hz") print(io, " Samplerate: ", samplerate(stream), "Hz")
# show source or sink if there's any channels # show source or sink if there's any channels
sink = stream.sink sink = stream.sink
if has_channels(sink) if has_channels(sink)
@ -994,10 +957,10 @@ function getproperty(
end end
function nchannels(source_or_sink::PortAudioSource) function nchannels(source_or_sink::PortAudioSource)
nchannels(source_or_sink.stream.source_messenger) nchannels(source_or_sink.stream.source_messanger)
end end
function nchannels(source_or_sink::PortAudioSink) function nchannels(source_or_sink::PortAudioSink)
nchannels(source_or_sink.stream.sink_messenger) nchannels(source_or_sink.stream.sink_messanger)
end end
function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource}) function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource})
samplerate(source_or_sink.stream) samplerate(source_or_sink.stream)
@ -1015,8 +978,8 @@ end
function isopen(source_or_sink::Union{PortAudioSink, PortAudioSource}) function isopen(source_or_sink::Union{PortAudioSink, PortAudioSource})
isopen(source_or_sink.stream) isopen(source_or_sink.stream)
end end
name(source_or_sink::PortAudioSink) = name(source_or_sink.stream.sink_messenger) name(source_or_sink::PortAudioSink) = name(source_or_sink.stream.sink_messanger)
name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messenger) name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messanger)
# could show full type name, but the PortAudio part is probably redundant # could show full type name, but the PortAudio part is probably redundant
# because these will usually only get printed as part of show for PortAudioStream # because these will usually only get printed as part of show for PortAudioStream
@ -1045,14 +1008,14 @@ end
as_matrix(matrix::Matrix) = matrix as_matrix(matrix::Matrix) = matrix
as_matrix(vector::Vector) = reshape(vector, length(vector), 1) as_matrix(vector::Vector) = reshape(vector, length(vector), 1)
# these will only work with SampledSignals scribes # these will only work with sampledsignals scribes
function unsafe_write( function unsafe_write(
sink::PortAudioSink{<:Messenger{<:Any, <:SampledSignalsWriter}}, sink::PortAudioSink{<:Messenger{<:Any, <:SampledSignalsWriter}},
julia_buffer::Array, julia_buffer::Array,
already, already,
frame_count, frame_count,
) )
exchange(sink.stream.sink_messenger, as_matrix(julia_buffer), already, frame_count) exchange(sink.stream.sink_messanger, as_matrix(julia_buffer), already, frame_count)
end end
function unsafe_read!( function unsafe_read!(
@ -1061,9 +1024,7 @@ function unsafe_read!(
already, already,
frame_count, frame_count,
) )
exchange(source.stream.source_messenger, as_matrix(julia_buffer), already, frame_count) exchange(source.stream.source_messanger, as_matrix(julia_buffer), already, frame_count)
end end
include("precompile.jl")
end # module PortAudio end # module PortAudio

View file

@ -1,29 +0,0 @@
# precompile some important functions
const DEFAULT_SINK_MESSENGER_TYPE = Messenger{Float32, SampledSignalsWriter, Tuple{Matrix{Float32}, Int64, Int64}, Int64}
const DEFAULT_SOURCE_MESSENGER_TYPE = Messenger{Float32, SampledSignalsReader, Tuple{Matrix{Float32}, Int64, Int64}, Int64}
const DEFAULT_STREAM_TYPE = PortAudioStream{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
const DEFAULT_SINK_TYPE = PortAudioSink{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
const DEFAULT_SOURCE_TYPE = PortAudioSource{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
precompile(close, (DEFAULT_STREAM_TYPE,))
precompile(devices, ())
precompile(__init__, ())
precompile(isopen, (DEFAULT_STREAM_TYPE,))
precompile(nchannels, (DEFAULT_SINK_TYPE,))
precompile(nchannels, (DEFAULT_SOURCE_TYPE,))
precompile(PortAudioStream, (Int, Int))
precompile(PortAudioStream, (String, Int, Int))
precompile(PortAudioStream, (String, String, Int, Int))
precompile(samplerate, (DEFAULT_STREAM_TYPE,))
precompile(send, (DEFAULT_SINK_MESSENGER_TYPE,))
precompile(send, (DEFAULT_SOURCE_MESSENGER_TYPE,))
precompile(unsafe_read!, (DEFAULT_SOURCE_TYPE, Vector{Float32}, Int, Int))
precompile(unsafe_read!, (DEFAULT_SOURCE_TYPE, Matrix{Float32}, Int, Int))
precompile(unsafe_write, (DEFAULT_SINK_TYPE, Vector{Float32}, Int, Int))
precompile(unsafe_write, (DEFAULT_SINK_TYPE, Matrix{Float32}, Int, Int))

View file

@ -11,7 +11,6 @@ using PortAudio:
get_output_type, get_output_type,
handle_status, handle_status,
initialize, initialize,
name,
PortAudioException, PortAudioException,
PortAudio, PortAudio,
PortAudioDevice, PortAudioDevice,
@ -19,7 +18,7 @@ using PortAudio:
safe_load, safe_load,
seek_alsa_conf, seek_alsa_conf,
terminate, terminate,
write_buffer name
using PortAudio.LibPortAudio: using PortAudio.LibPortAudio:
Pa_AbortStream, Pa_AbortStream,
PaError, PaError,
@ -110,9 +109,7 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
initialize() initialize()
end end
if isempty(devices()) if !isempty(devices())
@test_throws ArgumentError("No input device available") get_default_input_index()
else
@testset "Tests with sound" begin @testset "Tests with sound" begin
# these default values are specific to local machines # these default values are specific to local machines
input_name = get_device(get_default_input_index()).name input_name = get_device(get_default_input_index()).name
@ -133,16 +130,15 @@ else
sleep(1) sleep(1)
println("Testing pass-through") println("Testing pass-through")
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true) stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
write_buffer(stream.sink_messenger.buffer, acquire_lock = false)
sink = stream.sink sink = stream.sink
source = stream.source source = stream.source
@test sprint(show, stream) == """ @test sprint(show, stream) == """
PortAudioStream{Float32} PortAudioStream{Float32}
Samplerate: 44100Hz Samplerate: 44100.0Hz
2 channel sink: $(repr(output_name)) 2 channel sink: $(repr(input_name))
2 channel source: $(repr(input_name))""" 2 channel source: $(repr(output_name))"""
@test sprint(show, source) == "2 channel source: $(repr(input_name))" @test sprint(show, source) == "2 channel source: $(repr(output_name))"
@test sprint(show, sink) == "2 channel sink: $(repr(output_name))" @test sprint(show, sink) == "2 channel sink: $(repr(input_name))"
write(stream, stream, 5s) write(stream, stream, 5s)
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError @test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
@test isopen(stream) @test isopen(stream)
@ -213,8 +209,8 @@ else
big = typemax(Int) big = typemax(Int)
@test_throws DomainError( @test_throws DomainError(
typemax(Int), typemax(Int),
"$big exceeds maximum output channels for $output_name", "$big exceeds maximum input channels for $output_name",
) PortAudioStream(input_name, output_name, 0, big) ) PortAudioStream(input_name, output_name, big, 0)
@test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream( @test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream(
input_name, input_name,
output_name, output_name,
@ -223,8 +219,8 @@ else
adjust_channels = true, adjust_channels = true,
) )
@test_throws ArgumentError(""" @test_throws ArgumentError("""
Default sample rate 0 for input \"$input_name\" disagrees with Default sample rate 0 for input $output_name disagrees with
default sample rate 1 for output \"$output_name\". default sample rate 1 for output $input_name.
Please specify a sample rate. Please specify a sample rate.
""") combine_default_sample_rates( """) combine_default_sample_rates(
get_device(input_name), get_device(input_name),
@ -251,6 +247,14 @@ else
handle_status(Pa_SetStreamFinishedCallback(pointer_to, C_NULL)), handle_status(Pa_SetStreamFinishedCallback(pointer_to, C_NULL)),
) == paNoError ) == paNoError
end end
@testset "Make sure examples run" begin
include(joinpath(pkgdir(PortAudio), "examples", "audiometer.jl"))
include(joinpath(pkgdir(PortAudio), "examples", "lilyplay.jl"))
include(joinpath(pkgdir(PortAudio), "examples", "measure_latency.jl"))
include(joinpath(pkgdir(PortAudio), "examples", "spectrum.jl"))
# include("waterfall_heatmap.jl")
# include("waterfall_lines.jl")
end
end end
doctest(PortAudio) doctest(PortAudio)
end end