Compare commits
4 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d90a483c83 | ||
|
5056541968 | ||
|
f708835e5a | ||
|
aa8e9fc7cd |
9 changed files with 160 additions and 138 deletions
4
.github/workflows/Tests.yml
vendored
4
.github/workflows/Tests.yml
vendored
|
@ -17,9 +17,7 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- '1.3'
|
||||
- '1.4'
|
||||
- '1.5'
|
||||
- '1.6'
|
||||
- '1'
|
||||
- 'nightly'
|
||||
os:
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,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,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)
|
|
@ -446,17 +446,7 @@ function send(messenger)
|
|||
scribe = messenger.scribe
|
||||
input_channel = messenger.input_channel
|
||||
output_channel = messenger.output_channel
|
||||
while true
|
||||
input = try
|
||||
take!(input_channel)
|
||||
catch an_error
|
||||
# if the input channel is closed, the scribe knows its done
|
||||
if an_error isa InvalidStateException && an_error.state === :closed
|
||||
break
|
||||
else
|
||||
rethrow(an_error)
|
||||
end
|
||||
end
|
||||
for input in input_channel
|
||||
put!(output_channel, scribe(buffer, input))
|
||||
end
|
||||
end
|
||||
|
@ -488,15 +478,11 @@ function messenger_task(
|
|||
# if there's channels at all
|
||||
# we can't make the task a field of the buffer, because the task uses the buffer
|
||||
task = Task(let messenger = messenger
|
||||
() -> begin
|
||||
# xruns will return an error code and send a duplicate warning to stderr
|
||||
# since we handle the error codes, we don't need the duplicate warnings
|
||||
# so we send them to a debug log
|
||||
log = @capture_err send(messenger)
|
||||
if !isempty(log)
|
||||
@debug log
|
||||
end
|
||||
end
|
||||
# xruns will return an error code and send a duplicate warning to stderr
|
||||
# since we handle the error codes, we don't need the duplicate warnings
|
||||
# so we could send them to a debug log
|
||||
# but that causes problems when done from multiple threads
|
||||
() -> send(messenger)
|
||||
end)
|
||||
# makes it able to run on a separate thread
|
||||
task.sticky = false
|
||||
|
@ -1019,6 +1005,9 @@ function exchange(messenger, arguments...)
|
|||
take!(messenger.output_channel)
|
||||
end
|
||||
|
||||
as_matrix(matrix::Matrix) = matrix
|
||||
as_matrix(vector::Vector) = reshape(vector, length(vector), 1)
|
||||
|
||||
# these will only work with sampledsignals scribes
|
||||
function unsafe_write(
|
||||
sink::PortAudioSink{<:Messenger{<:Any, <:SampledSignalsWriter}},
|
||||
|
@ -1026,7 +1015,7 @@ function unsafe_write(
|
|||
already,
|
||||
frame_count,
|
||||
)
|
||||
exchange(sink.stream.sink_messanger, julia_buffer, already, frame_count)
|
||||
exchange(sink.stream.sink_messanger, as_matrix(julia_buffer), already, frame_count)
|
||||
end
|
||||
|
||||
function unsafe_read!(
|
||||
|
@ -1035,7 +1024,7 @@ function unsafe_read!(
|
|||
already,
|
||||
frame_count,
|
||||
)
|
||||
exchange(source.stream.source_messanger, julia_buffer, already, frame_count)
|
||||
exchange(source.stream.source_messanger, as_matrix(julia_buffer), already, frame_count)
|
||||
end
|
||||
|
||||
end # module PortAudio
|
||||
|
|
|
@ -247,6 +247,14 @@ if !isempty(devices())
|
|||
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