almost there

This commit is contained in:
Brandon Taylor 2021-06-30 12:11:42 -04:00
parent 6de81d1227
commit 98172f7438
2 changed files with 339 additions and 306 deletions

View file

@ -1,8 +1,22 @@
module PortAudio
using Base: alloc_buf_hook, Bool
using Base: rest
using alsa_plugins_jll: alsa_plugins_jll
import Base: close, eltype, getproperty, isopen, read, read!, show, showerror, write
import Base:
close,
eltype,
getindex,
getproperty,
IteratorSize,
isopen,
iterate,
length,
read,
read!,
show,
showerror,
write
using Base.Iterators: flatten, repeated
using Base.Threads: @spawn
using libportaudio_jll: libportaudio
using LinearAlgebra: transpose!
@ -111,7 +125,7 @@ function seek_alsa_conf(folders)
return folder
end
end
throw(ArgumentError("Could not find ALSA config"))
throw(ArgumentError("Could not find alsa.conf in $folders"))
end
function __init__()
@ -119,7 +133,7 @@ function __init__()
config_folder = "ALSA_CONFIG_DIR"
if config_folder keys(ENV)
ENV[config_folder] =
seek_alsa_conf(["/usr/share/alsa", "/usr/local/share/alsa", "/etc/alsa"])
seek_alsa_conf(("/usr/share/alsa", "/usr/local/share/alsa", "/etc/alsa"))
end
# the plugin folder will contain plugins for, critically, PulseAudio
plugin_folder = "ALSA_PLUGIN_DIR"
@ -133,15 +147,12 @@ function __init__()
end
end
# data is passed to and from portaudio in chunks with this many frames
const CHUNK_FRAMES = 128
function versioninfo(io::IO = stdout)
println(io, unsafe_string(Pa_GetVersionText()))
println(io, "Version: ", Pa_GetVersion())
end
# bounds for when a device is used as an input, or as an output
# bounds for when a device is used as an input or output
struct Bounds
max_channels::Int
low_latency::Float64
@ -178,7 +189,7 @@ function PortAudioDevice(info::PaDeviceInfo, index)
end
name(device::PortAudioDevice) = device.name
# show just key info about the device
# show name and input and output bounds
function show(io::IO, device::PortAudioDevice)
print(io, repr(name(device)))
print(io, ' ')
@ -197,7 +208,7 @@ end
# we can look up devices by index or name
function get_device_info(index::Integer)
safe_key(Pa_GetDeviceInfo, index)
PortAudioDevice(safe_key(Pa_GetDeviceInfo, index), index)
end
function get_device_info(device_name::AbstractString)
@ -211,197 +222,221 @@ function get_device_info(device_name::AbstractString)
end
function devices()
[
PortAudioDevice(get_device_info(index), index) for
# 0 indexing for C
index in (1:handle_status(Pa_GetDeviceCount())) .- 1
]
# need to use 0 indexing for C
map(get_device_info, (1:handle_status(Pa_GetDeviceCount())) .- 1)
end
# because we're calling Pa_ReadStream and Pa_WriteStream from separate threads,
# we put a lock around these calls
function write_stream(
stream_lock,
pointer_to,
port_audio_buffer,
chunk_frames;
warn_xruns = true,
)
# we can handle reading and writing from buffers in a similar way
function read_or_write(a_function, buffer, args...)
handle_status(
lock(stream_lock) do
Pa_WriteStream(pointer_to, port_audio_buffer, chunk_frames)
# because we're calling Pa_ReadStream and Pa_WriteStream from separate threads,
# we put a lock around these calls
lock(buffer.stream_lock) do
a_function(buffer.pointer_to, buffer.data, args...)
end,
warn_xruns = warn_xruns,
warn_xruns = buffer.scribe.warn_xruns,
)
end
function read_stream(
stream_lock,
pointer_to,
port_audio_buffer,
chunk_frames;
warn_xruns = true,
)
handle_status(
lock(stream_lock) do
Pa_ReadStream(pointer_to, port_audio_buffer, chunk_frames)
end;
warn_xruns = warn_xruns,
)
function write_buffer(buffer, use_frames)
read_or_write(Pa_WriteStream, buffer, use_frames)
end
function read_buffer(buffer, use_frames)
read_or_write(Pa_ReadStream, buffer, use_frames)
end
# these will do the actual reading and writing
# you can switch out the SampledSignalsReader/Writer defaults if you want more direct access
# a ReaderOrWriter must implement the following interface:
# a Scribe must implement the following interface:
# must have a warn_xruns::Bool field
# must have get_input_type and get_output_type methods
# must overload call for four arguments:
# 1) stream_lock: a lock around the stream
# 2) pointer_to: the pointer to the stream
# 3) port_audio_buffer: the source/sink buffer
# 4) custom inputs
# must overload call for 2 arguments:
# 1) the buffer
# 2) a tuple of custom inputs
# and return the output type
# this call method should make use of read_stream/write_stream methods above
abstract type ReaderOrWriter end
# this call method should make use of read_buffer/write_buffer methods above
abstract type Scribe end
struct SampledSignalsReader{Sample} <: ReaderOrWriter
struct SampledSignalsReader{Sample} <: Scribe
warn_xruns::Bool
end
struct SampledSignalsWriter{Sample} <: ReaderOrWriter
struct SampledSignalsWriter{Sample} <: Scribe
warn_xruns::Bool
end
# inputs will be a triple of the last 3 arguments to unsafe_read/write
# define on types
function get_input_type(a_type::Type)
throw(MethodError(get_input_type, (a_type,)))
end
function get_output_type(a_type::Type)
throw(MethodError(get_output_type, (a_type,)))
end
# convenience functions so you can pass objects too
function get_input_type(::Thing) where {Thing}
get_input_type(Thing)
end
function get_output_type(::Thing) where {Thing}
get_output_type(Thing)
end
# SampledSignals inputs will be a triple of the last 3 arguments to unsafe_read/write
# we will already have access to the stream itself
function get_input_type(
::Union{<:SampledSignalsReader{Sample}, <:SampledSignalsWriter{Sample}},
::Type{<:Union{<:SampledSignalsReader{Sample}, <:SampledSignalsWriter{Sample}}},
) where {Sample}
Tuple{Array{Sample, 2}, Int, Int}
end
# output is the number of frames read/written
function get_output_type(::Union{SampledSignalsReader, SampledSignalsWriter})
function get_output_type(::Type{<:Union{SampledSignalsReader, SampledSignalsWriter}})
Int
end
# we need to transpose column-major buffer from Julia back and forth between the row-major buffer from PortAudio
function translate!(
julia_buffer,
port_audio_buffer,
chunk_frames,
offset,
already,
port_audio_to_julia,
)
port_audio_range = 1:chunk_frames
# the julia buffer is longer, so we might need to start from the middle
julia_view = view(julia_buffer, port_audio_range .+ offset .+ already, :)
port_audio_view = view(port_audio_buffer, :, port_audio_range)
if port_audio_to_julia
transpose!(julia_view, port_audio_view)
else
transpose!(port_audio_view, julia_view)
end
function cut_to_size(buffer, julia_buffer, use_frames, offset, already)
port_audio_range = 1:use_frames
(
view(buffer.data, :, port_audio_range),
# the julia buffer is longer, so we might need to start from the middle
view(julia_buffer, port_audio_range .+ offset .+ already, :),
)
end
function (writer::SampledSignalsWriter)(
stream_lock,
pointer_to,
port_audio_buffer,
buffer,
(julia_buffer, offset, frame_count),
# data is passed to and from portaudio in chunks with this many frames
)
warn_xruns = writer.warn_xruns
already = 0
chunk_frames = buffer.chunk_frames
# if we still have frames to write
while already < frame_count
# take either a whole chunk, or whatever is left if it's smaller
chunk_frames = min(frame_count - already, CHUNK_FRAMES)
use_frames = min(frame_count - already, chunk_frames)
port_audio_view, julia_view =
cut_to_size(buffer, julia_buffer, use_frames, offset, already)
# we need to transpose column-major buffer from Julia back and forth between the row-major buffer from PortAudio
# transpose, then send the data
translate!(julia_buffer, port_audio_buffer, chunk_frames, offset, already, false)
# TODO: if the stream is closed we just want to return a
# shorter-than-requested frame count instead of throwing an error
write_stream(
stream_lock,
pointer_to,
port_audio_buffer,
chunk_frames;
warn_xruns = warn_xruns,
)
already += chunk_frames
transpose!(port_audio_view, julia_view)
write_buffer(buffer, use_frames)
already += use_frames
end
already
end
function (reader::SampledSignalsReader)(
stream_lock,
pointer_to,
port_audio_buffer,
(julia_buffer, offset, frame_count),
)
warn_xruns = reader.warn_xruns
function (reader::SampledSignalsReader)(buffer, (julia_buffer, offset, frame_count))
already = 0
chunk_frames = buffer.chunk_frames
# if we still have frames to write
while already < frame_count
# take either a whole chunk, or whatever is left if it's smaller
chunk_frames = min(frame_count - already, CHUNK_FRAMES)
use_frames = min(frame_count - already, chunk_frames)
# receive the data, then transpose
# TODO: if the stream is closed we just want to return a
# shorter-than-requested frame count instead of throwing an error
read_stream(
stream_lock,
pointer_to,
port_audio_buffer,
chunk_frames;
warn_xruns = warn_xruns,
)
translate!(julia_buffer, port_audio_buffer, chunk_frames, offset, already, true)
already += chunk_frames
read_buffer(buffer, use_frames)
port_audio_view, julia_view =
cut_to_size(buffer, julia_buffer, use_frames, offset, already)
transpose!(julia_view, port_audio_view)
already += use_frames
end
already
end
# a Messanger contains
# the PortAudio device
# the number of channels
# an input channel, for passing inputs to the messanger
# an output channel for sending outputs from the messanger
struct Messanger{InputType, OutputType}
# a Buffer contains not just a buffer
# but everything you might need for a source or sink
struct Buffer{Sample, Scribe, InputType, OutputType}
stream_lock::ReentrantLock
pointer_to::Ptr{PaStream}
device::PortAudioDevice
data::Array{Sample}
number_of_channels::Int
chunk_frames::Int
scribe::Scribe
inputs::Channel{InputType}
outputs::Channel{OutputType}
end
nchannels(messanger::Messanger) = messanger.number_of_channels
name(messanger::Messanger) = name(messanger.device)
has_channels(something) = nchannels(something) > 0
function close(messanger::Messanger)
close(messanger.inputs)
close(messanger.outputs)
function buffer_task(
stream_lock,
pointer_to,
device,
channels,
scribe::Scribe;
Sample = Float32,
chunk_frames = 128,
) where {Scribe}
InputType = get_input_type(scribe)
OutputType = get_output_type(scribe)
input_channel = Channel{InputType}(0)
output_channel = Channel{OutputType}(0)
# unbuffered channels so putting and taking will block till everyone's ready
buffer = Buffer{Sample, Scribe, InputType, OutputType}(
stream_lock,
pointer_to,
device,
zeros(Sample, channels, chunk_frames),
channels,
chunk_frames,
scribe,
input_channel,
output_channel,
)
# we will spawn new threads to read from and write to port audio
# while the reading thread is talking to PortAudio, the writing thread can be setting up, and vice versa
# start the scribe thread when its created
# 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(() -> run_scribe(buffer))
task.sticky = false
if has_channels(buffer)
schedule(task)
else
close(input_channel)
close(output_channel)
end
buffer, task
end
nchannels(buffer::Buffer) = buffer.number_of_channels
name(buffer::Buffer) = name(buffer.device)
function close(buffer::Buffer)
close(buffer.inputs)
close(buffer.outputs)
end
#
# PortAudioStream
#
struct PortAudioStream{Sample, SinkMessanger, SourceMessanger}
struct PortAudioStream{Sample, SinkBuffer, SourceBuffer}
sample_rate::Float64
# pointer to the c object
pointer_to::Ptr{PaStream}
sink_messanger::SinkMessanger
source_messanger::SourceMessanger
sink_buffer::SinkBuffer
sink_task::Task
source_buffer::SourceBuffer
source_task::Task
end
function PortAudioStream{Sample}(
sample_rate::Float64,
pointer_to::Ptr{PaStream},
sink_messanger::SinkMessanger,
source_messanger::SourceMessanger,
) where {Sample, SinkMessanger, SourceMessanger}
PortAudioStream{Sample, SinkMessanger, SourceMessanger}(
sink_buffer::SinkBuffer,
sink_task::Task,
source_buffer::SourceBuffer,
source_task::Task,
) where {Sample, SinkBuffer, SourceBuffer}
PortAudioStream{Sample, SinkBuffer, SourceBuffer}(
sample_rate,
pointer_to,
sink_messanger,
source_messanger,
sink_buffer,
sink_task,
source_buffer,
source_task,
)
end
@ -436,148 +471,68 @@ function make_parameters(device, channels, Sample, latency, host_api_specific_st
end
end
# if users pass max as the number of channels, we fill it in for them
# if users passes max as the number of channels, we fill it in for them
# this is currently undocumented
function fill_max_channels(channels, bounds)
function fill_max_channels(kind, device, bounds, channels)
max_channels = bounds.max_channels
if channels === max
max_channels
elseif channels > max_channels
throw(DomainError(channels, "max channels exceeded"))
throw(
DomainError(
channels,
"$channels exceeds max $kind channels for $(name(device))",
),
)
else
channels
end
end
const AT_LEAST_ONE = ArgumentError("Input or output must have at least 1 channel")
# fill max for both channels based on device bounds
function fill_both_channels(input_channels, input_device, output_channels, output_device)
input_channels_filled = fill_max_channels(input_channels, input_device.input_bounds)
output_channels_filled = fill_max_channels(output_channels, output_device.output_bounds)
if input_channels_filled == 0 && output_channels_filled == 0
throw(AT_LEAST_ONE)
else
input_channels_filled, output_channels_filled
end
end
# if input or output only, use the corresponding value
# otherwise, run a custom combine function
function combine_defaults(
combine_function,
input_channels_filled,
output_channels_filled,
input,
output,
)
if input_channels_filled > 0
if output_channels_filled > 0
combine_function(input, output)
else
input
end
else
if output_channels_filled > 0
output
else
# this check is probably redundant, but included for completeness
throw(AT_LEAST_ONE)
end
end
end
function get_default_latency(input_channels, input_device, output_channels, output_device)
input_channels_filled, output_channels_filled =
fill_both_channels(input_channels, input_device, output_channels, output_device)
combine_defaults(
# worst case scenario: max of high latency for input and output
max,
input_channels_filled,
output_channels_filled,
input_device.input_bounds.high_latency,
output_device.output_bounds.high_latency,
)
end
# we can only have one sample rate
# so if the default sample rates differ, throw an error
function combine_default_sample_rates(input_sample_rate, output_sample_rate)
function combine_default_sample_rates(
input_device,
input_sample_rate,
output_device,
output_sample_rate,
)
if input_sample_rate != output_sample_rate
throw(ArgumentError("Default input and output sample rates disagree"))
# TODO: just name
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)).
Please specify a sample rate.
""",
),
)
end
input_sample_rate
end
function get_default_sample_rates(
input_channels,
input_device,
output_channels,
output_device,
)
input_channels_filled, output_channels_filled =
fill_both_channels(input_channels, input_device, output_channels, output_device)
combine_defaults(
combine_default_sample_rates,
input_channels_filled,
output_channels_filled,
input_device.default_sample_rate,
output_device.default_sample_rate,
)
end
# the messanger will be running on a separate thread in the background
# the scribe will be running on a separate thread in the background
# alternating transposing and
# waiting to pass inputs and outputs back and forth to PortAudio
function run_reader_or_writer(
messanger,
reader_or_writer,
stream_lock,
pointer_to,
port_audio_buffer,
)
inputs = messanger.inputs
outputs = messanger.outputs
function run_scribe(buffer)
scribe = buffer.scribe
inputs = buffer.inputs
outputs = buffer.outputs
while true
output = if isopen(inputs)
reader_or_writer(stream_lock, pointer_to, port_audio_buffer, take!(inputs))
else
# no frames can be read/written if the input channel is closed
0
end
if isopen(outputs)
put!(outputs, output)
else
# if the output channel has closed too, we're done
break
input = try
take!(inputs)
catch an_error
if an_error isa InvalidStateException
break
else
rethrow(an_error)
end
end
put!(outputs, scribe(buffer, input))
end
end
# we will spawn new threads to read from and write to port audio
# while the reading thread is talking to PortAudio, the writing thread can be setting up, and vice versa
function Messanger(reader_or_writer, stream_lock, Sample, pointer_to, device, channels)
InputType = get_input_type(reader_or_writer)
OutputType = get_output_type(reader_or_writer)
# unbuffered channels so putting and taking will block till everyone's ready
port_audio_buffer = zeros(Sample, channels, CHUNK_FRAMES)
messanger = Messanger{InputType, OutputType}(
device,
channels,
Channel{InputType}(0),
Channel{OutputType}(0),
)
# start the messanger thread when its created
@spawn run_reader_or_writer(
messanger,
reader_or_writer,
stream_lock,
pointer_to,
port_audio_buffer,
)
messanger
end
# this is the top-level outer constructor that all the other outer constructors end up calling
"""
PortAudioStream(input_channels = 2, output_channels = 2; options...)
@ -607,20 +562,12 @@ function PortAudioStream(
input_channels = 2,
output_channels = 2;
Sample = Float32,
sample_rate = get_default_sample_rates(
input_channels,
input_device,
output_channels,
output_device,
),
latency = get_default_latency(
input_channels,
input_device,
output_channels,
output_device,
),
# for several keywords, nothing means we will fill them with defaults
sample_rate = nothing,
latency = nothing,
warn_xruns = true,
# these defaults are currently undocumented
chunk_frames = 128,
frames_per_buffer = 0,
flags = paNoFlag,
call_back = nothing,
@ -629,11 +576,61 @@ function PortAudioStream(
output_info = nothing,
stream_lock = ReentrantLock(),
# this is where you can insert custom readers or writers instead
writer = SampledSignalsWriter{Sample}(warn_xruns),
reader = SampledSignalsReader{Sample}(warn_xruns),
writer = nothing,
reader = nothing,
)
input_channels_filled, output_channels_filled =
fill_both_channels(input_channels, input_device, output_channels, output_device)
input_channels_filled =
fill_max_channels("input", input_device, input_device.input_bounds, input_channels)
output_channels_filled = fill_max_channels(
"output",
output_device,
output_device.output_bounds,
output_channels,
)
# which defaults we use will depend on whether input or output have any channels
if input_channels_filled > 0
if output_channels_filled > 0
if latency === nothing
# use the max of high latency for input and output
latency = max(
input_device.input_bounds.high_latency,
output_device.output_bounds.high_latency,
)
end
if sample_rate === nothing
sample_rate = combine_default_sample_rates(
input_device,
input_device.default_sample_rate,
output_device,
output_device.default_sample_rate,
)
end
else
if latency === nothing
latency = input_device.input_bounds.high_latency
end
if sample_rate === nothing
sample_rate = input_device.default_sample_rate
end
end
else
if output_channels_filled > 0
if latency === nothing
latency = output_device.output_bounds.high_latency
end
if sample_rate === nothing
sample_rate = output_device.default_sample_rate
end
else
throw(ArgumentError("Input or output must have at least 1 channel"))
end
end
if writer === nothing
writer = SampledSignalsWriter{Sample}(warn_xruns)
end
if reader === nothing
reader = SampledSignalsReader{Sample}(warn_xruns)
end
# we need a mutable pointer so portaudio can set it for us
mutable_pointer = Ref{Ptr{PaStream}}(0)
handle_status(
@ -666,22 +663,24 @@ function PortAudioStream(
PortAudioStream{Sample}(
sample_rate,
pointer_to,
Messanger(
writer,
buffer_task(
stream_lock,
Sample,
pointer_to,
output_device,
output_channels_filled,
),
Messanger(
reader,
writer;
Sample = Sample,
chunk_frames = chunk_frames,
)...,
buffer_task(
stream_lock,
Sample,
pointer_to,
input_device,
input_channels_filled,
),
reader;
Sample = Sample,
chunk_frames = chunk_frames,
)...,
)
end
@ -715,8 +714,8 @@ function PortAudioStream(input_channels = 2, output_channels = 2; keywords...)
in_index = get_default_input_index()
out_index = get_default_output_index()
PortAudioStream(
PortAudioDevice(get_device_info(in_index), in_index),
PortAudioDevice(get_device_info(out_index), out_index),
get_device_info(in_index),
get_device_info(out_index),
input_channels,
output_channels;
keywords...,
@ -735,8 +734,17 @@ end
function close(stream::PortAudioStream)
# this will shut down the channels, which will shut down the threads
close(stream.sink_messanger)
close(stream.source_messanger)
sink_buffer = stream.sink_buffer
if has_channels(sink_buffer)
close(sink_buffer)
# wait for tasks to finish to make sure any errors get caught
wait(stream.sink_task)
end
source_buffer = stream.source_buffer
if has_channels(source_buffer)
close(source_buffer)
wait(stream.source_task)
end
pointer_to = stream.pointer_to
# only stop if it's not already stopped
if !Bool(handle_status(Pa_IsStreamStopped(pointer_to)))
@ -773,13 +781,14 @@ function show(io::IO, stream::PortAudioStream)
print(io, eltype(stream))
println(io, "}")
print(io, " Samplerate: ", samplerate(stream), "Hz")
# show source or sink if there's any channels
sink = stream.sink
if nchannels(sink) > 0
if has_channels(sink)
print(io, "\n ")
show(io, sink)
end
source = stream.source
if nchannels(source) > 0
if has_channels(source)
print(io, "\n ")
show(io, source)
end
@ -793,8 +802,8 @@ end
# If we had multiple inheritance, then PortAudioStreams could be both a sink and source
# Since we don't, we have to make wrappers instead
for (TypeName, Super) in ((:PortAudioSink, :SampleSink), (:PortAudioSource, :SampleSource))
@eval struct $TypeName{Sample} <: $Super
stream::PortAudioStream{Sample}
@eval struct $TypeName{Sample, InputMessager, OutputBuffer} <: $Super
stream::PortAudioStream{Sample, InputMessager, OutputBuffer}
end
end
@ -812,9 +821,11 @@ function getproperty(stream::PortAudioStream, property::Symbol)
end
function nchannels(source_or_sink::PortAudioSource)
nchannels(source_or_sink.stream.source_messanger)
nchannels(source_or_sink.stream.source_buffer)
end
function nchannels(source_or_sink::PortAudioSink)
nchannels(source_or_sink.stream.sink_buffer)
end
nchannels(source_or_sink::PortAudioSink) = nchannels(source_or_sink.stream.sink_messanger)
function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource})
samplerate(source_or_sink.stream)
end
@ -826,8 +837,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_messanger)
name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messanger)
name(source_or_sink::PortAudioSink) = name(source_or_sink.stream.sink_buffer)
name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_buffer)
# could show full type name, but the PortAudio part is probably redundant
kind(::PortAudioSink) = "sink"
@ -847,17 +858,27 @@ end
# both reading and writing will outsource to the readers or writers
# so we just need to pass inputs in and take outputs out
# SampledSignals can take care of this feeding for us
function exchange(messanger, arguments...)
put!(messanger.inputs, arguments)
take!(messanger.outputs)
function exchange(buffer, arguments...)
put!(buffer.inputs, arguments)
take!(buffer.outputs)
end
function unsafe_write(sink::PortAudioSink, julia_buffer::Array, offset, frame_count)
exchange(sink.stream.sink_messanger, julia_buffer, offset, frame_count)
function unsafe_write(
sink::PortAudioSink{<:Any, <:Buffer{<:Any, <:SampledSignalsWriter}},
julia_buffer::Array,
offset,
frame_count,
)
exchange(sink.stream.sink_buffer, julia_buffer, offset, frame_count)
end
function unsafe_read!(source::PortAudioSource, julia_buffer::Array, offset, frame_count)
exchange(source.stream.source_messanger, julia_buffer, offset, frame_count)
function unsafe_read!(
source::PortAudioSource{<:Any, <:Any, <:Buffer{<:Any, <:SampledSignalsReader}},
julia_buffer::Array,
offset,
frame_count,
)
exchange(source.stream.source_buffer, julia_buffer, offset, frame_count)
end
end # module PortAudio

View file

@ -6,6 +6,8 @@ using PortAudio:
get_default_input_index,
get_default_output_index,
get_device_info,
get_input_type,
get_output_type,
handle_status,
initialize,
PortAudioException,
@ -85,16 +87,11 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
end
@testset "Errors without sound" begin
@test_throws ArgumentError("Default input and output sample rates disagree") combine_default_sample_rates(
0,
1,
)
wrong = "foobarbaz"
@test sprint(showerror, PortAudioException(paNotInitialized)) ==
"PortAudioException: PortAudio not initialized"
@test_throws KeyError(wrong) get_device_info(wrong)
@test_throws KeyError("foobarbaz") get_device_info("foobarbaz")
@test_throws KeyError(-1) get_device_info(-1)
@test_throws ArgumentError("Could not find ALSA config") seek_alsa_conf([])
@test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(())
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
PaError(paOutputUnderflowed),
)
@ -103,6 +100,8 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
)
Pa_Sleep(1)
@test Pa_GetSampleSize(paFloat32) == 4
@test_throws MethodError(get_input_type, (Any,)) get_input_type(Any)
@test_throws MethodError(get_output_type, (Any,)) get_output_type(Any)
end
end
@ -113,10 +112,11 @@ if !isempty(devices())
# these default values are specific to local machines
input_index = get_default_input_index()
default_input_device = PortAudioDevice(get_device_info(input_index), input_index).name
default_input_device = get_device_info(input_index)
default_input_device_name = default_input_device.name
output_index = get_default_output_index()
default_output_device =
PortAudioDevice(get_device_info(output_index), output_index).name
default_output_device = get_device_info(output_index)
default_output_device_name = default_output_device.name
@testset "Tests with sound" begin
@testset "Interactive tests" begin
@ -137,10 +137,11 @@ if !isempty(devices())
@test sprint(show, stream) == """
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel sink: $(repr(default_output_device))
2 channel source: $(repr(default_input_device))"""
@test sprint(show, sink) == "2 channel sink: $(repr(default_input_device))"
@test sprint(show, source) == "2 channel source: $(repr(default_output_device))"
2 channel sink: $(repr(default_output_device_name))
2 channel source: $(repr(default_input_device_name))"""
@test sprint(show, sink) == "2 channel sink: $(repr(default_input_device_name))"
@test sprint(show, source) ==
"2 channel source: $(repr(default_output_device_name))"
write(stream, stream, 5s)
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
@test isopen(stream)
@ -165,7 +166,7 @@ if !isempty(devices())
end
end
@testset "Open Device by name" begin
PortAudioStream(default_input_device, default_output_device) do stream
PortAudioStream(default_input_device_name, default_output_device_name) do stream
end
end
# no way to check that the right data is actually getting read or written here,
@ -198,19 +199,30 @@ if !isempty(devices())
PortAudioStream(2, max; call_back = C_NULL) do stream
@test isopen(stream)
end
PortAudioStream(default_input_device) do stream
PortAudioStream(default_input_device_name) do stream
@test isopen(stream)
end
end
@testset "Errors with sound" begin
@test_throws DomainError(typemax(Int), "max channels exceeded") PortAudioStream(
big = typemax(Int)
@test_throws DomainError(
typemax(Int),
0,
)
"$big exceeds max input channels for $default_input_device_name",
) PortAudioStream(big, 0)
@test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream(
0,
0,
)
@test_throws ArgumentError("""
Default sample rate 0 for input $default_input_device_name disagrees with
default sample rate 1 for output $default_output_device_name.
Please specify a sample rate.
""") combine_default_sample_rates(
default_input_device,
0,
default_output_device,
1,
)
end
@testset "libportaudio with sound" begin
@test PaErrorCode(Pa_HostApiTypeIdToHostApiIndex(paInDevelopment)) ==