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"
uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d"
repo = "https://github.com/JuliaAudio/PortAudio.jl.git"
version = "1.3.0"
version = "1.2.0"
[deps]
alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7"
@ -11,7 +11,7 @@ SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
[compat]
julia = "1.6"
julia = "1.3"
alsa_plugins_jll = "1.2.2"
libportaudio_jll = "19.6.0"
SampledSignals = "2.1.1"
@ -19,8 +19,11 @@ Suppressor = "0.2"
[extras]
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"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[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
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.
If named keyword arguments `latency` or `samplerate` are unspecified, then PortAudio will use device defaults.
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.
```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
@ -31,16 +27,13 @@ You can get a list of your system's devices with the `PortAudio.devices()` funct
```julia
julia> PortAudio.devices()
14-element Vector{PortAudio.PortAudioDevice}:
"sof-hda-dsp: - (hw:0,0)" 2→2
"sof-hda-dsp: - (hw:0,3)" 0→2
"sof-hda-dsp: - (hw:0,4)" 0→2
"sof-hda-dsp: - (hw:0,5)" 0→2
"upmix" 8→8
"vdownmix" 6→6
"dmix" 0→2
"default" 32→32
6-element Array{PortAudio.PortAudioDevice,1}:
PortAudio.PortAudioDevice("AirPlay","Core Audio",0,2,0)
PortAudio.PortAudioDevice("Built-in Microph","Core Audio",2,0,1)
PortAudio.PortAudioDevice("Built-in Output","Core Audio",0,2,2)
PortAudio.PortAudioDevice("JackRouter","Core Audio",2,2,3)
PortAudio.PortAudioDevice("After Effects 13.5","Core Audio",0,0,4)
PortAudio.PortAudioDevice("Built-In Aggregate","Core Audio",2,2,5)
```
## Reading and Writing
@ -82,7 +75,7 @@ end
### Open your built-in microphone and speaker by name
```julia
PortAudioStream("default", "default") do stream
PortAudioStream("Built-in Microph", "Built-in Output") do stream
write(stream, stream)
end
```
@ -90,18 +83,13 @@ end
### Record 10 seconds of audio and save to an ogg file
```julia
julia> import LibSndFile # must be in Manifest for FileIO.save to work
julia> using PortAudio, SampledSignals, LibSndFile
julia> using PortAudio: PortAudioStream
julia> using SampledSignals: s
julia> using FileIO: save
julia> stream = PortAudioStream(1, 0) # default input (e.g., built-in microphone)
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel source: "default"
julia> stream = PortAudioStream("Built-in Microph", 2, 0)
PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
Samplerate: 48000 s⁻¹
Buffer Size: 4096 frames
2 channel source: "Built-in Microph"
julia> buf = read(stream, 10s)
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> 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
ASCII level/peak meter
"""
function micmeter(metersize)
mic = PortAudioStream(1, 0; latency = 0.1)
function micmeter(seconds; metersize = 80)
PortAudioStream(1, 0; latency = 0.1) do mic
done = false
signalmax = zero(eltype(mic))
println("Press Ctrl-C to quit")
while true
@sync begin
@async while !done
block = read(mic, 512)
blockmax = maximum(abs.(block)) # find the maximum value in the block
signalmax = max(signalmax, blockmax) # keep the maximum value ever
print("\r") # reset the cursor to the beginning of the line
printmeter(metersize, blockmax, signalmax)
end
sleep(seconds)
done = true
end
end
nothing
end
"""
@ -52,4 +57,4 @@ function barcolor(metersize, position)
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.
# 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}
pitch::S
@ -37,12 +23,11 @@ function play(
v[(end - decay_length):(end - 1)] =
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
end
play(ostream, v)
sleep(A.duration)
write(ostream, v)
end
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 != nothing && (lyrics_syllables[end] *= "\n")
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
# at http://gr-framework.org/examples/audio_ex.html
module SpectrumExample
using GR, PortAudio, SampledSignals, FFTW
const N = 1024
const stream = PortAudioStream(1, 0)
const buf = read(stream, N)
const fmin = 0Hz
const fmax = 10000Hz
const fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
while true
function plot_spectrogram(seconds;
N = 1024,
fmin = 0Hz,
fmax = 10000Hz
)
PortAudioStream(1, 0) do stream
done = false
buf = read(stream, N)
fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
@sync begin
@async while !done
read!(stream, buf)
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100))
end
sleep(seconds)
done = true
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 PortAudio
using DSP
using LinearAlgebra
using FFTW
"""
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
"""
function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan)
function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan; D = 200)
readbuf .*= win
A_mul_B!(fftbuf, fftplan, readbuf)
mul!(fftbuf, fftplan, readbuf)
shift1!(dispbuf)
@. dispbuf[end:-1:1, 1] = log(clamp(abs(fftbuf[1:D]), 0.0001, Inf))
end
function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
function processblock!(src, buf, win, dispbufs, fftbuf, fftplan; D = 200)
read!(src, buf)
for dispbuf in dispbufs
processbuf!(buf, win, dispbuf, fftbuf, fftplan)
processbuf!(buf, win, dispbuf, fftbuf, fftplan; D = D)
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
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
PortAudioStream(1, 2) do src
buf = Array{Float32}(undef, N) # buffer for reading
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
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
for _ in 1:M
processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
processblock!(src, buf, win, dispbufs, fftbuf, fftplan; D = D)
end
heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf
heatmaps = map(zip(CartesianIndices(dispbufs), dispbufs)) do ibuf
i = ibuf[1]
buf = ibuf[2]
@ -58,10 +62,20 @@ end
center!(scene)
while isopen(scene[:screen])
processblock!(src, buf, dispbufs, fftbuf, fftplan)
done = false
@sync begin
@async while !done
processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
for (hm, db) in zip(heatmaps, dispbufs)
hm[:heatmap] = db
end
render_frame(scene)
end
sleep(seconds)
done = true
end
end
end
waterfall_heatmap(5)

View file

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

View file

@ -48,7 +48,6 @@ using .LibPortAudio:
Pa_Initialize,
paInputOverflowed,
Pa_IsStreamStopped,
paNoDevice,
paNoFlag,
Pa_OpenStream,
paOutputUnderflowed,
@ -196,22 +195,12 @@ function show(io::IO, device::PortAudioDevice)
print(io, device.output_bounds.max_channels)
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()
device_index = Pa_GetDefaultInputDevice()
check_device_exists(device_index, "input")
device_index
handle_status(Pa_GetDefaultInputDevice())
end
function get_default_output_index()
device_index = Pa_GetDefaultOutputDevice()
check_device_exists(device_index, "output")
device_index
handle_status(Pa_GetDefaultOutputDevice())
end
# we can look up devices by index or name
@ -241,25 +230,19 @@ function devices()
end
# 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)
pointer_to = buffer.pointer_to
data = buffer.data
function read_or_write(a_function, buffer, use_frames = buffer.frames_per_buffer)
handle_status(
if acquire_lock
# because we're calling Pa_ReadStream and Pa_WriteStream from separate threads,
# we put a lock around these calls
lock(
let a_function = a_function,
pointer_to = pointer_to,
data = data,
pointer_to = buffer.pointer_to,
data = buffer.data,
use_frames = use_frames
() -> a_function(pointer_to, data, use_frames)
end,
buffer.stream_lock,
)
else
a_function(pointer_to, data, use_frames)
end;
),
warn_xruns = buffer.warn_xruns,
)
end
@ -396,7 +379,7 @@ Use [`PortAudio.write_buffer`](@ref) to write data to PortAudio, and [`PortAudio
struct Buffer{Sample}
stream_lock::ReentrantLock
pointer_to::Ptr{PaStream}
data::Array{Sample, 2}
data::Array{Sample}
number_of_channels::Int
frames_per_buffer::Int
warn_xruns::Bool
@ -424,39 +407,25 @@ eltype(::Type{Buffer{Sample}}) where {Sample} = Sample
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.
Set `acquire_lock = false` to skip acquiring the lock.
"""
function write_buffer(buffer::Buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true)
read_or_write(Pa_WriteStream, buffer, use_frames; acquire_lock = acquire_lock)
function write_buffer(buffer::Buffer, use_frames = buffer.frames_per_buffer)
read_or_write(Pa_WriteStream, buffer, use_frames)
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).
Set `acquire_lock = false` to skip acquiring the acquire_lock.
"""
function read_buffer!(buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true)
read_or_write(Pa_ReadStream, buffer, use_frames; acquire_lock = acquire_lock)
function read_buffer!(buffer, use_frames = buffer.frames_per_buffer)
read_or_write(Pa_ReadStream, buffer, use_frames)
end
"""
Messenger{Sample, Scribe, Input, Output}
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.
"""
# the messenger will send tasks to the scribe
# the scribe will read/write from the buffer
struct Messenger{Sample, Scribe, Input, Output}
device_name::String
buffer::Buffer{Sample}
@ -528,7 +497,7 @@ function messenger_task(
messenger, task
end
function fetch_messenger(messenger, task)
function fetch_messanger(messenger, task)
if has_channels(messenger)
# this will shut down the channels, which will shut down the thread
close(messenger.input_channel)
@ -548,9 +517,9 @@ struct PortAudioStream{SinkMessenger, SourceMessenger}
sample_rate::Float64
# pointer to the c object
pointer_to::Ptr{PaStream}
sink_messenger::SinkMessenger
sink_messanger::SinkMessenger
sink_task::Task
source_messenger::SourceMessenger
source_messanger::SourceMessenger
source_task::Task
end
@ -615,9 +584,10 @@ function combine_default_sample_rates(
)
if input_sample_rate != output_sample_rate
throw(
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))\".
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)).
Please specify a sample rate.
""",
),
@ -760,24 +730,20 @@ function PortAudioStream(
output_device.output_bounds.high_latency,
)
end
samplerate = if samplerate === nothing
combine_default_sample_rates(
if samplerate === nothing
samplerate = combine_default_sample_rates(
input_device,
input_device.default_sample_rate,
output_device,
output_device.default_sample_rate,
)
else
float(samplerate)
end
else
if latency === nothing
latency = input_device.input_bounds.high_latency
end
samplerate = if samplerate === nothing
input_device.default_sample_rate
else
float(samplerate)
if samplerate === nothing
samplerate = input_device.default_sample_rate
end
end
else
@ -785,10 +751,8 @@ function PortAudioStream(
if latency === nothing
latency = output_device.output_bounds.high_latency
end
samplerate = if samplerate === nothing
output_device.default_sample_rate
else
float(samplerate)
if samplerate === nothing
samplerate = output_device.default_sample_rate
end
else
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
# but we have don't know exactly what the tasks are doing
# for now, just close one and then the other
fetch_messenger(stream.source_messenger, stream.source_task)
fetch_messenger(stream.sink_messenger, stream.sink_task)
fetch_messanger(stream.source_messanger, stream.source_task)
fetch_messanger(stream.sink_messanger, stream.sink_task)
pointer_to = stream.pointer_to
# only stop if it's not already stopped
if !Bool(handle_status(Pa_IsStreamStopped(pointer_to)))
@ -927,14 +891,13 @@ end
isopen(stream::PortAudioStream) = isopen(stream.pointer_to)
samplerate(stream::PortAudioStream) = stream.sample_rate
function eltype(
::Type{<:PortAudioStream{<:Messenger{Sample}, <:Messenger{Sample}}},
) where {Sample}
Sample
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
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, eltype(stream))
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
sink = stream.sink
if has_channels(sink)
@ -994,10 +957,10 @@ function getproperty(
end
function nchannels(source_or_sink::PortAudioSource)
nchannels(source_or_sink.stream.source_messenger)
nchannels(source_or_sink.stream.source_messanger)
end
function nchannels(source_or_sink::PortAudioSink)
nchannels(source_or_sink.stream.sink_messenger)
nchannels(source_or_sink.stream.sink_messanger)
end
function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource})
samplerate(source_or_sink.stream)
@ -1015,8 +978,8 @@ end
function isopen(source_or_sink::Union{PortAudioSink, PortAudioSource})
isopen(source_or_sink.stream)
end
name(source_or_sink::PortAudioSink) = name(source_or_sink.stream.sink_messenger)
name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messenger)
name(source_or_sink::PortAudioSink) = name(source_or_sink.stream.sink_messanger)
name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messanger)
# 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
@ -1045,14 +1008,14 @@ end
as_matrix(matrix::Matrix) = matrix
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(
sink::PortAudioSink{<:Messenger{<:Any, <:SampledSignalsWriter}},
julia_buffer::Array,
already,
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
function unsafe_read!(
@ -1061,9 +1024,7 @@ function unsafe_read!(
already,
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
include("precompile.jl")
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,
handle_status,
initialize,
name,
PortAudioException,
PortAudio,
PortAudioDevice,
@ -19,7 +18,7 @@ using PortAudio:
safe_load,
seek_alsa_conf,
terminate,
write_buffer
name
using PortAudio.LibPortAudio:
Pa_AbortStream,
PaError,
@ -110,9 +109,7 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
initialize()
end
if isempty(devices())
@test_throws ArgumentError("No input device available") get_default_input_index()
else
if !isempty(devices())
@testset "Tests with sound" begin
# these default values are specific to local machines
input_name = get_device(get_default_input_index()).name
@ -133,16 +130,15 @@ else
sleep(1)
println("Testing pass-through")
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
write_buffer(stream.sink_messenger.buffer, acquire_lock = false)
sink = stream.sink
source = stream.source
@test sprint(show, stream) == """
PortAudioStream{Float32}
Samplerate: 44100Hz
2 channel sink: $(repr(output_name))
2 channel source: $(repr(input_name))"""
@test sprint(show, source) == "2 channel source: $(repr(input_name))"
@test sprint(show, sink) == "2 channel sink: $(repr(output_name))"
Samplerate: 44100.0Hz
2 channel sink: $(repr(input_name))
2 channel source: $(repr(output_name))"""
@test sprint(show, source) == "2 channel source: $(repr(output_name))"
@test sprint(show, sink) == "2 channel sink: $(repr(input_name))"
write(stream, stream, 5s)
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
@test isopen(stream)
@ -213,8 +209,8 @@ else
big = typemax(Int)
@test_throws DomainError(
typemax(Int),
"$big exceeds maximum output channels for $output_name",
) PortAudioStream(input_name, output_name, 0, big)
"$big exceeds maximum input channels for $output_name",
) PortAudioStream(input_name, output_name, big, 0)
@test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream(
input_name,
output_name,
@ -223,8 +219,8 @@ else
adjust_channels = true,
)
@test_throws ArgumentError("""
Default sample rate 0 for input \"$input_name\" disagrees with
default sample rate 1 for output \"$output_name\".
Default sample rate 0 for input $output_name disagrees with
default sample rate 1 for output $input_name.
Please specify a sample rate.
""") combine_default_sample_rates(
get_device(input_name),
@ -251,6 +247,14 @@ else
handle_status(Pa_SetStreamFinishedCallback(pointer_to, C_NULL)),
) == paNoError
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
doctest(PortAudio)
end