callback
This commit is contained in:
parent
987d2840da
commit
494d7c8ffa
6 changed files with 237 additions and 1069 deletions
|
@ -6,21 +6,17 @@ version = "1.3.0"
|
|||
[deps]
|
||||
alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7"
|
||||
libportaudio_jll = "2d7b7beb-0762-5160-978e-1ab83a1e8a31"
|
||||
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
|
||||
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
|
||||
|
||||
[compat]
|
||||
julia = "1.6"
|
||||
alsa_plugins_jll = "1.2.2"
|
||||
libportaudio_jll = "19.6.0"
|
||||
SampledSignals = "2.1.1"
|
||||
Suppressor = "0.2"
|
||||
|
||||
[extras]
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
LibSndFile = "b13ce0c6-77b0-50c6-a2db-140568b8d1a5"
|
||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
|
||||
[targets]
|
||||
test = ["Documenter", "LibSndFile", "Test"]
|
||||
test = ["Documenter", "Test"]
|
||||
|
|
|
@ -2,11 +2,8 @@ using PortAudio
|
|||
using Documenter: deploydocs, makedocs
|
||||
|
||||
makedocs(
|
||||
sitename = "PortAudio.jl",
|
||||
sitename = "PortAudio.jl",
|
||||
modules = [PortAudio],
|
||||
pages = [
|
||||
"Public interface" => "index.md",
|
||||
"Internals" => "internals.md"
|
||||
]
|
||||
pages = ["Public interface" => "index.md", "Internals" => "internals.md"],
|
||||
)
|
||||
deploydocs(repo = "github.com/JuliaAudio/PortAudio.jl.git")
|
||||
deploydocs(repo = "github.com/JuliaAudio/PortAudio.jl.git")
|
||||
|
|
899
src/PortAudio.jl
899
src/PortAudio.jl
File diff suppressed because it is too large
Load diff
|
@ -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))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
268
test/runtests.jl
268
test/runtests.jl
|
@ -1,256 +1,22 @@
|
|||
#!/usr/bin/env julia
|
||||
using Base.Sys: iswindows
|
||||
using Documenter: doctest
|
||||
using PortAudio:
|
||||
combine_default_sample_rates,
|
||||
devices,
|
||||
get_default_input_index,
|
||||
get_default_output_index,
|
||||
get_device,
|
||||
get_input_type,
|
||||
get_output_type,
|
||||
handle_status,
|
||||
initialize,
|
||||
name,
|
||||
PortAudioException,
|
||||
PortAudio,
|
||||
PortAudioDevice,
|
||||
PortAudioStream,
|
||||
safe_load,
|
||||
seek_alsa_conf,
|
||||
terminate,
|
||||
write_buffer
|
||||
using PortAudio.LibPortAudio:
|
||||
Pa_AbortStream,
|
||||
PaError,
|
||||
PaErrorCode,
|
||||
paFloat32,
|
||||
Pa_GetDefaultHostApi,
|
||||
Pa_GetDeviceInfo,
|
||||
Pa_GetHostApiCount,
|
||||
Pa_GetLastHostErrorInfo,
|
||||
Pa_GetSampleSize,
|
||||
Pa_GetStreamCpuLoad,
|
||||
Pa_GetStreamInfo,
|
||||
Pa_GetStreamReadAvailable,
|
||||
Pa_GetStreamTime,
|
||||
Pa_GetStreamWriteAvailable,
|
||||
Pa_GetVersionInfo,
|
||||
Pa_HostApiDeviceIndexToDeviceIndex,
|
||||
paHostApiNotFound,
|
||||
Pa_HostApiTypeIdToHostApiIndex,
|
||||
PaHostErrorInfo,
|
||||
paInDevelopment,
|
||||
paInvalidDevice,
|
||||
Pa_IsFormatSupported,
|
||||
Pa_IsStreamActive,
|
||||
paNoError,
|
||||
paNoFlag,
|
||||
paNotInitialized,
|
||||
Pa_OpenDefaultStream,
|
||||
paOutputUnderflowed,
|
||||
Pa_SetStreamFinishedCallback,
|
||||
Pa_Sleep,
|
||||
Pa_StopStream,
|
||||
PaStream,
|
||||
PaStreamInfo,
|
||||
PaStreamParameters,
|
||||
PaVersionInfo
|
||||
using SampledSignals: nchannels, s, SampleBuf, samplerate, SinSource
|
||||
using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
|
||||
using PortAudio
|
||||
|
||||
@testset "Tests without sound" begin
|
||||
@testset "Reports version" begin
|
||||
io = IOBuffer()
|
||||
PortAudio.versioninfo(io)
|
||||
result = split(String(take!((io))), "\n")
|
||||
# make sure this is the same version I tested with
|
||||
@test startswith(result[1], "PortAudio V19")
|
||||
end
|
||||
const RADIANS_PER_FRAME = 1 / 44100 * 440 * 2 * pi
|
||||
|
||||
@testset "Can list devices without crashing" begin
|
||||
display(devices())
|
||||
println()
|
||||
end
|
||||
|
||||
@testset "libortaudio without sound" begin
|
||||
@test handle_status(Pa_GetHostApiCount()) >= 0
|
||||
@test handle_status(Pa_GetDefaultHostApi()) >= 0
|
||||
# version info not available on windows?
|
||||
if !Sys.iswindows()
|
||||
@test safe_load(Pa_GetVersionInfo(), ErrorException("no info")) isa
|
||||
PaVersionInfo
|
||||
function test()
|
||||
stream = PortAudioStream() do _, output_array, frames_per_buffer, frames_already
|
||||
# 44100 frames / second
|
||||
# 440 cycles / second
|
||||
# 2pi radians / cycle
|
||||
if frames_already > 44100
|
||||
0
|
||||
else
|
||||
for frame in 1:frames_per_buffer
|
||||
output_array[1, frame] = sin((frames_already + frame) * RADIANS_PER_FRAME)
|
||||
end
|
||||
frames_per_buffer
|
||||
end
|
||||
@test safe_load(Pa_GetLastHostErrorInfo(), ErrorException("no info")) isa
|
||||
PaHostErrorInfo
|
||||
@test PaErrorCode(Pa_IsFormatSupported(C_NULL, C_NULL, 0.0)) == paInvalidDevice
|
||||
@test PaErrorCode(
|
||||
Pa_OpenDefaultStream(Ref(C_NULL), 0, 0, paFloat32, 0.0, 0, C_NULL, C_NULL),
|
||||
) == paInvalidDevice
|
||||
end
|
||||
|
||||
@testset "Errors without sound" begin
|
||||
@test sprint(showerror, PortAudioException(paNotInitialized)) ==
|
||||
"PortAudioException: PortAudio not initialized"
|
||||
@test_throws KeyError("foobarbaz") get_device("foobarbaz")
|
||||
@test_throws KeyError(-1) get_device(-1)
|
||||
@test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(())
|
||||
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
|
||||
PaError(paOutputUnderflowed),
|
||||
)
|
||||
@test_throws PortAudioException(paNotInitialized) handle_status(
|
||||
PaError(paNotInitialized),
|
||||
)
|
||||
Pa_Sleep(1)
|
||||
@test Pa_GetSampleSize(paFloat32) == 4
|
||||
end
|
||||
|
||||
# make sure we can terminate, then reinitialize
|
||||
terminate()
|
||||
initialize()
|
||||
end
|
||||
|
||||
if isempty(devices())
|
||||
@test_throws ArgumentError("No input device available") get_default_input_index()
|
||||
else
|
||||
@testset "Tests with sound" begin
|
||||
# these default values are specific to local machines
|
||||
input_name = get_device(get_default_input_index()).name
|
||||
output_name = get_device(get_default_output_index()).name
|
||||
|
||||
@testset "Interactive tests" begin
|
||||
println("Recording...")
|
||||
stream = PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true)
|
||||
buffer = read(stream, 5s)
|
||||
@test size(buffer) ==
|
||||
(round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
||||
close(stream)
|
||||
sleep(1)
|
||||
println("Playing back recording...")
|
||||
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||
write(stream, buffer)
|
||||
end
|
||||
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))"
|
||||
write(stream, stream, 5s)
|
||||
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
|
||||
@test isopen(stream)
|
||||
close(stream)
|
||||
sleep(1)
|
||||
@test !isopen(stream)
|
||||
@test !isopen(sink)
|
||||
@test !isopen(source)
|
||||
println("done")
|
||||
end
|
||||
@testset "Samplerate-converting writing" begin
|
||||
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||
write(
|
||||
stream,
|
||||
SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]),
|
||||
3s,
|
||||
)
|
||||
println("expected blip")
|
||||
write(
|
||||
stream,
|
||||
SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]),
|
||||
3s,
|
||||
)
|
||||
end
|
||||
end
|
||||
sleep(1)
|
||||
# no way to check that the right data is actually getting read or written here,
|
||||
# but at least it's not crashing.
|
||||
@testset "Queued Writing" begin
|
||||
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||
buffer = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
frame_count_1 = @async write(stream, buffer)
|
||||
frame_count_2 = @async write(stream, buffer)
|
||||
@test fetch(frame_count_1) == 48000
|
||||
println("expected blip")
|
||||
@test fetch(frame_count_2) == 48000
|
||||
end
|
||||
sleep(1)
|
||||
end
|
||||
@testset "Queued Reading" begin
|
||||
PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true) do stream
|
||||
buffer = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
frame_count_1 = @async read!(stream, buffer)
|
||||
frame_count_2 = @async read!(stream, buffer)
|
||||
@test fetch(frame_count_1) == 48000
|
||||
@test fetch(frame_count_2) == 48000
|
||||
end
|
||||
sleep(1)
|
||||
end
|
||||
@testset "Constructors" begin
|
||||
PortAudioStream(2, maximum; adjust_channels = true) do stream
|
||||
@test isopen(stream)
|
||||
end
|
||||
PortAudioStream(output_name; adjust_channels = true) do stream
|
||||
@test isopen(stream)
|
||||
end
|
||||
PortAudioStream(input_name, output_name; adjust_channels = true) do stream
|
||||
@test isopen(stream)
|
||||
end
|
||||
end
|
||||
@testset "Errors with sound" begin
|
||||
big = typemax(Int)
|
||||
@test_throws DomainError(
|
||||
typemax(Int),
|
||||
"$big exceeds maximum output channels for $output_name",
|
||||
) PortAudioStream(input_name, output_name, 0, big)
|
||||
@test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream(
|
||||
input_name,
|
||||
output_name,
|
||||
0,
|
||||
0;
|
||||
adjust_channels = true,
|
||||
)
|
||||
@test_throws ArgumentError("""
|
||||
Default sample rate 0 for input \"$input_name\" disagrees with
|
||||
default sample rate 1 for output \"$output_name\".
|
||||
Please specify a sample rate.
|
||||
""") combine_default_sample_rates(
|
||||
get_device(input_name),
|
||||
0,
|
||||
get_device(output_name),
|
||||
1,
|
||||
)
|
||||
end
|
||||
@testset "libportaudio with sound" begin
|
||||
@test PaErrorCode(Pa_HostApiTypeIdToHostApiIndex(paInDevelopment)) ==
|
||||
paHostApiNotFound
|
||||
@test Pa_HostApiDeviceIndexToDeviceIndex(paInDevelopment, 0) == 0
|
||||
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
|
||||
pointer_to = stream.pointer_to
|
||||
@test handle_status(Pa_GetStreamReadAvailable(pointer_to)) >= 0
|
||||
@test handle_status(Pa_GetStreamWriteAvailable(pointer_to)) >= 0
|
||||
@test Bool(handle_status(Pa_IsStreamActive(pointer_to)))
|
||||
@test safe_load(Pa_GetStreamInfo(pointer_to), ErrorException("no info")) isa
|
||||
PaStreamInfo
|
||||
@test Pa_GetStreamTime(pointer_to) >= 0
|
||||
@test Pa_GetStreamCpuLoad(pointer_to) >= 0
|
||||
@test PaErrorCode(handle_status(Pa_AbortStream(pointer_to))) == paNoError
|
||||
@test PaErrorCode(
|
||||
handle_status(Pa_SetStreamFinishedCallback(pointer_to, C_NULL)),
|
||||
) == paNoError
|
||||
end
|
||||
end
|
||||
doctest(PortAudio)
|
||||
start(stream)
|
||||
sleep(2)
|
||||
close(stream)
|
||||
end
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
# This file has runs the normal tests and also adds tests that can only be run
|
||||
# locally on a machine with a sound card. It's mostly to put the library through
|
||||
# its paces assuming a human is listening.
|
||||
|
||||
include("runtests.jl")
|
||||
|
||||
# these default values are specific to my machines
|
||||
if Sys.iswindows()
|
||||
default_indev = "Microphone Array (Realtek High "
|
||||
default_outdev = "Speaker/Headphone (Realtek High"
|
||||
elseif Sys.isapple()
|
||||
default_indev = "Built-in Microphone"
|
||||
default_outdev = "Built-in Output"
|
||||
elseif Sys.islinux()
|
||||
default_indev = "default"
|
||||
default_outdev = "default"
|
||||
end
|
||||
|
||||
@testset "Local Tests" begin
|
||||
@testset "Open Default Device" begin
|
||||
println("Recording...")
|
||||
stream = PortAudioStream(2, 0)
|
||||
buf = read(stream, 5s)
|
||||
close(stream)
|
||||
@test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
||||
println("Playing back recording...")
|
||||
stream = PortAudioStream(0, 2)
|
||||
write(stream, buf)
|
||||
println("flushing...")
|
||||
flush(stream)
|
||||
close(stream)
|
||||
println("Testing pass-through")
|
||||
stream = PortAudioStream(2, 2)
|
||||
write(stream, stream, 5s)
|
||||
flush(stream)
|
||||
close(stream)
|
||||
println("done")
|
||||
end
|
||||
@testset "Samplerate-converting writing" begin
|
||||
stream = PortAudioStream(0, 2)
|
||||
write(stream, SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]), 3s)
|
||||
write(stream, SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]), 3s)
|
||||
flush(stream)
|
||||
close(stream)
|
||||
end
|
||||
@testset "Open Device by name" begin
|
||||
stream = PortAudioStream(default_indev, default_outdev)
|
||||
buf = read(stream, 0.001s)
|
||||
@test size(buf) ==
|
||||
(round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
|
||||
write(stream, buf)
|
||||
io = IOBuffer()
|
||||
show(io, stream)
|
||||
@test occursin(
|
||||
"""
|
||||
PortAudioStream{Float32}
|
||||
Samplerate: 44100.0Hz
|
||||
Buffer Size: 4096 frames
|
||||
2 channel sink: "$default_outdev"
|
||||
2 channel source: "$default_indev\"""",
|
||||
String(take!(io)),
|
||||
)
|
||||
close(stream)
|
||||
end
|
||||
@testset "Error on wrong name" begin
|
||||
@test_throws ErrorException PortAudioStream("foobarbaz")
|
||||
end
|
||||
# no way to check that the right data is actually getting read or written here,
|
||||
# but at least it's not crashing.
|
||||
@testset "Queued Writing" begin
|
||||
stream = PortAudioStream(0, 2)
|
||||
buf = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
t1 = @async write(stream, buf)
|
||||
t2 = @async write(stream, buf)
|
||||
@test fetch(t1) == 48000
|
||||
@test fetch(t2) == 48000
|
||||
flush(stream)
|
||||
close(stream)
|
||||
end
|
||||
@testset "Queued Reading" begin
|
||||
stream = PortAudioStream(2, 0)
|
||||
buf = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
t1 = @async read!(stream, buf)
|
||||
t2 = @async read!(stream, buf)
|
||||
@test fetch(t1) == 48000
|
||||
@test fetch(t2) == 48000
|
||||
close(stream)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue