Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d90a483c83 | ||
|
5056541968 | ||
|
f708835e5a | ||
|
aa8e9fc7cd |
12 changed files with 225 additions and 396 deletions
|
@ -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"]
|
||||
|
|
56
README.md
56
README.md
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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)
|
||||
|
||||
signalmax = zero(eltype(mic))
|
||||
println("Press Ctrl-C to quit")
|
||||
while true
|
||||
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)
|
||||
function micmeter(seconds; metersize = 80)
|
||||
PortAudioStream(1, 0; latency = 0.1) do mic
|
||||
done = false
|
||||
signalmax = zero(eltype(mic))
|
||||
@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)
|
||||
|
|
|
@ -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
|
||||
|
@ -35,14 +21,13 @@ function play(
|
|||
if !A.sustained
|
||||
decay_length = div(length(timesamples), 5)
|
||||
v[(end - decay_length):(end - 1)] =
|
||||
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
|
||||
v[(end - decay_length):(end - 1)] .* 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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
read!(stream, buf)
|
||||
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100))
|
||||
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
|
||||
|
||||
end
|
||||
plot_spectrogram(5)
|
||||
|
|
|
@ -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)
|
|
@ -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,52 +18,64 @@ 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
|
||||
N2 = N ÷ 2 + 1 # size of rfft output
|
||||
D = 200 # number of bins to display
|
||||
M = 200 # amount of history to keep
|
||||
src = PortAudioStream(1, 2)
|
||||
buf = Array{Float32}(N) # buffer for reading
|
||||
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
|
||||
fftbuf = Array{Complex{Float32}}(N2) # destination buf for FFT
|
||||
dispbufs = [zeros(Float32, D, M) for i in 1:5, j in 1:5] # STFT bufs
|
||||
win = gaussian(N, 0.125)
|
||||
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
|
||||
PortAudioStream(1, 2) do src
|
||||
buf = Array{Float32}(undef, N) # buffer for reading
|
||||
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
|
||||
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)
|
||||
|
||||
scene = Scene(resolution = (1000, 1000))
|
||||
scene = Scene(resolution = (1000, 1000))
|
||||
|
||||
#pre-fill the display buffer so we can do a reasonable colormap
|
||||
for _ in 1:M
|
||||
processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
|
||||
end
|
||||
#pre-fill the display buffer so we can do a reasonable colormap
|
||||
for _ in 1:M
|
||||
processblock!(src, buf, win, dispbufs, fftbuf, fftplan; D = D)
|
||||
end
|
||||
|
||||
heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf
|
||||
i = ibuf[1]
|
||||
buf = ibuf[2]
|
||||
heatmaps = map(zip(CartesianIndices(dispbufs), dispbufs)) do ibuf
|
||||
i = ibuf[1]
|
||||
buf = ibuf[2]
|
||||
|
||||
# some function of the 2D index and the value
|
||||
heatmap(buf, offset = (i[2] * size(buf, 2), i[1] * size(buf, 1)))
|
||||
end
|
||||
# some function of the 2D index and the value
|
||||
heatmap(buf, offset = (i[2] * size(buf, 2), i[1] * size(buf, 1)))
|
||||
end
|
||||
|
||||
center!(scene)
|
||||
center!(scene)
|
||||
|
||||
while isopen(scene[:screen])
|
||||
processblock!(src, buf, dispbufs, fftbuf, fftplan)
|
||||
for (hm, db) in zip(heatmaps, dispbufs)
|
||||
hm[:heatmap] = db
|
||||
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
|
||||
render_frame(scene)
|
||||
end
|
||||
|
||||
waterfall_heatmap(5)
|
|
@ -1,43 +1,57 @@
|
|||
using Makie, GeometryTypes
|
||||
using PortAudio
|
||||
using FFTW
|
||||
using CairoMakie
|
||||
|
||||
N = 1024 # size of audio read
|
||||
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)
|
||||
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
|
||||
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
|
||||
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))
|
||||
ax = axis(0:0.1:1, 0:0.1:1, 0:0.1:0.5)
|
||||
center!(scene)
|
||||
scene = Scene(resolution = (500, 500))
|
||||
ax = axis(0:0.1:1, 0:0.1:1, 0:0.1:0.5)
|
||||
center!(scene)
|
||||
|
||||
ls = map(1:M) do _
|
||||
yoffset = to_node(to_value(scene[:time]))
|
||||
offset = lift_node(scene[:time], yoffset) do t, yoff
|
||||
Point3f0(0.0f0, (t - yoff) * S, 0.0f0)
|
||||
end
|
||||
l = lines(
|
||||
linspace(0, 1, D),
|
||||
0.0f0,
|
||||
zeros(Float32, D),
|
||||
offset = offset,
|
||||
color = (:black, 0.1),
|
||||
)
|
||||
(yoffset, l)
|
||||
end
|
||||
|
||||
while isopen(scene[:screen])
|
||||
for (yoffset, line) in ls
|
||||
isopen(scene[:screen]) || break
|
||||
read!(src, buf)
|
||||
A_mul_B!(fftbuf, fftplan, buf)
|
||||
@. magbuf = log(clamp(abs(fftbuf), 0.0001, Inf)) / 10 + 0.5
|
||||
line[:z] = magbuf[1:D]
|
||||
push!(yoffset, to_value(scene[:time]))
|
||||
ls = map(1:M) do _
|
||||
yoffset = to_node(to_value(scene[:time]))
|
||||
offset = lift_node(scene[:time], yoffset) do t, yoff
|
||||
Point3f0(0.0f0, (t - yoff) * S, 0.0f0)
|
||||
end
|
||||
l = lines(
|
||||
linspace(0, 1, D),
|
||||
0.0f0,
|
||||
zeros(Float32, D),
|
||||
offset = offset,
|
||||
color = (:black, 0.1),
|
||||
)
|
||||
(yoffset, l)
|
||||
end
|
||||
done = false
|
||||
@sync begin
|
||||
@async begin
|
||||
while !done
|
||||
for (yoffset, line) in ls
|
||||
isopen(scene[:screen]) || break
|
||||
read!(src, buf)
|
||||
A_mul_B!(fftbuf, fftplan, buf)
|
||||
@. magbuf = log(clamp(abs(fftbuf), 0.0001, Inf)) / 10 + 0.5
|
||||
line[:z] = magbuf[1:D]
|
||||
push!(yoffset, to_value(scene[:time]))
|
||||
end
|
||||
end
|
||||
end
|
||||
sleep(seconds)
|
||||
done = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
waterfall_lines(5)
|
135
src/PortAudio.jl
135
src/PortAudio.jl
|
@ -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,
|
||||
use_frames = use_frames
|
||||
() -> a_function(pointer_to, data, use_frames)
|
||||
end,
|
||||
buffer.stream_lock,
|
||||
)
|
||||
else
|
||||
a_function(pointer_to, data, use_frames)
|
||||
end;
|
||||
# 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 = buffer.pointer_to,
|
||||
data = buffer.data,
|
||||
use_frames = use_frames
|
||||
() -> a_function(pointer_to, data, use_frames)
|
||||
end,
|
||||
buffer.stream_lock,
|
||||
),
|
||||
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,11 +751,9 @@ 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)
|
||||
end
|
||||
if samplerate === nothing
|
||||
samplerate = output_device.default_sample_rate
|
||||
end
|
||||
else
|
||||
throw(ArgumentError("Input or output must have at least 1 channel"))
|
||||
end
|
||||
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue