more cleanup and comments

This commit is contained in:
Brandon Taylor 2021-06-26 12:27:31 -04:00
parent 175addaf45
commit 05bcc26124
2 changed files with 59 additions and 33 deletions

View file

@ -46,7 +46,8 @@ using .LibPortAudio:
Pa_Terminate,
Pa_WriteStream
# for structs and strings results, PortAudio will return NULL instead of erroring
# for structs from indexes
# PortAudio will return C_NULL instead of erroring
# so we need to handle these errors
function safe_load(result, an_error)
if result == C_NULL
@ -55,6 +56,11 @@ function safe_load(result, an_error)
unsafe_load(result)
end
# for functions that retrieve an index, throw a key error if it doesn't exist
function safe_key(a_function, an_index)
safe_load(a_function(an_index), KeyError(an_index))
end
# error numbers are integers, while error codes are an @enum
function get_error_text(error_number)
unsafe_string(Pa_GetErrorText(error_number))
@ -114,7 +120,7 @@ function __init__()
ENV[config_folder] =
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"
if plugin_folder keys(ENV) && alsa_plugins_jll.is_available()
ENV[plugin_folder] = joinpath(alsa_plugins_jll.artifact_dir, "lib", "alsa-lib")
@ -134,6 +140,7 @@ function versioninfo(io::IO = stdout)
println(io, "Version: ", Pa_GetVersion())
end
# bounds for when a device is used as an input, or as an output
struct Bounds
max_channels::Int
low_latency::Float64
@ -152,12 +159,8 @@ end
function PortAudioDevice(info::PaDeviceInfo, index)
PortAudioDevice(
unsafe_string(info.name),
unsafe_string(
safe_load(
(Pa_GetHostApiInfo(info.hostApi)),
BoundsError(Pa_GetHostApiInfo, index),
).name,
),
# replace host api code with its name
unsafe_string(safe_key(Pa_GetHostApiInfo, info.hostApi,).name),
info.defaultSampleRate,
index,
Bounds(
@ -174,17 +177,26 @@ function PortAudioDevice(info::PaDeviceInfo, index)
end
name(device::PortAudioDevice) = device.name
# show just key info about the device
function show(io::IO, device::PortAudioDevice)
print(io, repr(name(device)))
print(io, ' ')
print(io, device.input_bounds.max_channels)
print(io, '→')
print(io, device.output_bounds.max_channels)
end
function get_default_input_device()
function get_default_input_index()
handle_status(Pa_GetDefaultInputDevice())
end
function get_default_output_device()
function get_default_output_index()
handle_status(Pa_GetDefaultOutputDevice())
end
# we can look up devices by index or name
function get_device_info(index::Integer)
safe_load((Pa_GetDeviceInfo(index)), BoundsError(Pa_GetDeviceInfo, index))
safe_key(Pa_GetDeviceInfo, index)
end
function get_device_info(device_name::AbstractString)
@ -231,6 +243,7 @@ function Messanger{Sample}(device, channels) where {Sample}
device,
zeros(Sample, channels, CHUNK_FRAMES),
channels,
# unbuffered channels so putting and taking will block till everyone's ready
INPUT_CHANNEL_TYPE{Sample}(0),
OUTPUT_CHANNEL_TYPE(0),
)
@ -250,6 +263,7 @@ end
struct PortAudioStream{Sample}
sample_rate::Float64
# pointer to the c object
pointer_to::Ptr{PaStream}
sink_messanger::Messanger{Sample}
source_messanger::Messanger{Sample}
@ -266,8 +280,8 @@ const TYPE_TO_FORMAT = Dict{Type, PaSampleFormat}(
)
# we need to convert nothing so it will be handled by C correctly
convert_nothing(::Nothing) = C_NULL
convert_nothing(something) = something
nothing_to_c_null(::Nothing) = C_NULL
nothing_to_c_null(something) = something
function make_parameters(device, channels, Sample, latency, host_api_specific_stream_info)
if channels == 0
@ -280,7 +294,7 @@ function make_parameters(device, channels, Sample, latency, host_api_specific_st
channels,
TYPE_TO_FORMAT[Sample],
latency,
convert_nothing(host_api_specific_stream_info),
nothing_to_c_null(host_api_specific_stream_info),
),
)
end
@ -301,6 +315,7 @@ 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)
@ -311,7 +326,9 @@ function fill_both_channels(input_channels, input_device, output_channels, outpu
end
end
function input_output_or_both(
# 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,
@ -328,16 +345,17 @@ function input_output_or_both(
if output_channels_filled > 0
output
else
# this check is probably redundant, but included for completeness
throw(AT_LEAST_ONE)
end
end
end
# worst case scenario
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)
input_output_or_both(
combine_defaults(
# worst case scenario: max of high latency for input and output
max,
input_channels_filled,
output_channels_filled,
@ -346,6 +364,8 @@ function get_default_latency(input_channels, input_device, output_channels, outp
)
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)
if input_sample_rate != output_sample_rate
throw(ArgumentError("Default input and output sample rates disagree"))
@ -353,8 +373,6 @@ function combine_default_sample_rates(input_sample_rate, output_sample_rate)
input_sample_rate
end
# we can only have one sample rate
# so if the default sample rates differ, throw an error
function get_default_sample_rates(
input_channels,
input_device,
@ -363,7 +381,7 @@ function get_default_sample_rates(
)
input_channels_filled, output_channels_filled =
fill_both_channels(input_channels, input_device, output_channels, output_device)
input_output_or_both(
combine_defaults(
combine_default_sample_rates,
input_channels_filled,
output_channels_filled,
@ -372,6 +390,9 @@ function get_default_sample_rates(
)
end
# the messanger 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_messanger(
a_function,
pointer_to,
@ -387,10 +408,10 @@ function run_messanger(
# no frames can be read/written if the input channel is closed
0
end
# check to see if the output channel has closed too
if isopen(outputs)
put!(outputs, output)
else
# if the output channel has closed too, we're done
break
end
end
@ -410,6 +431,7 @@ function start_messanger(
port_audio_buffer = messanger.port_audio_buffer
inputs = messanger.inputs
outputs = messanger.outputs
# start the messanger thread when its created
@spawn run_messanger(
a_function,
pointer_to,
@ -489,7 +511,6 @@ function real_read!(
# 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
# get the data, then transpose
handle_status(
lock(PORT_AUDIO_LOCK) do
Pa_ReadStream(pointer_to, port_audio_buffer, chunk_frames)
@ -573,11 +594,12 @@ function PortAudioStream(
latency,
output_info,
),
# can't be an integer
float(sample_rate),
frames_per_buffer,
flags,
convert_nothing(call_back),
convert_nothing(user_data),
nothing_to_c_null(call_back),
nothing_to_c_null(user_data),
),
)
pointer_to = mutable_pointer[]
@ -631,8 +653,8 @@ end
# use the default input and output devices
function PortAudioStream(input_channels = 2, output_channels = 2; keywords...)
in_index = get_default_input_device()
out_index = get_default_output_device()
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),
@ -653,10 +675,11 @@ function PortAudioStream(do_function::Function, arguments...; keywords...)
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)
pointer_to = stream.pointer_to
# only stop if it's not arlready stopped
# only stop if it's not already stopped
if !Bool(handle_status(Pa_IsStreamStopped(pointer_to)))
handle_status(Pa_StopStream(pointer_to))
end
@ -742,6 +765,7 @@ 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)
# could show full type name, but the PortAudio part is probably redundant
kind(::PortAudioSink) = "sink"
kind(::PortAudioSource) = "source"
function show(io::IO, sink_or_source::Union{PortAudioSink, PortAudioSource})
@ -751,6 +775,7 @@ function show(io::IO, sink_or_source::Union{PortAudioSink, PortAudioSource})
" channel ",
kind(sink_or_source),
": ",
# put in quotes
repr(name(sink_or_source)),
)
end

View file

@ -3,8 +3,8 @@ using Base.Sys: iswindows
using PortAudio:
combine_default_sample_rates,
devices,
get_default_input_device,
get_default_output_device,
get_default_input_index,
get_default_output_index,
get_device_info,
handle_status,
initialize,
@ -64,7 +64,8 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
end
@testset "Can list devices without crashing" begin
devices()
display(devices())
println()
end
@testset "libortaudio without sound" begin
@ -92,7 +93,7 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
@test sprint(showerror, PortAudioException(paNotInitialized)) ==
"PortAudioException: PortAudio not initialized"
@test_throws KeyError(wrong) get_device_info(wrong)
@test_throws BoundsError(Pa_GetDeviceInfo, -1) get_device_info(-1)
@test_throws KeyError(-1) get_device_info(-1)
@test_throws ArgumentError("Could not find ALSA config") seek_alsa_conf([])
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
PaError(paOutputUnderflowed),
@ -111,9 +112,9 @@ if !isempty(devices())
initialize()
# these default values are specific to local machines
input_index = get_default_input_device()
input_index = get_default_input_index()
default_input_device = PortAudioDevice(get_device_info(input_index), input_index).name
output_index = get_default_output_device()
output_index = get_default_output_index()
default_output_device =
PortAudioDevice(get_device_info(output_index), output_index).name