almost there
This commit is contained in:
parent
6de81d1227
commit
98172f7438
2 changed files with 339 additions and 306 deletions
595
src/PortAudio.jl
595
src/PortAudio.jl
|
@ -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
|
||||
|
|
|
@ -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)) ==
|
||||
|
|
Loading…
Add table
Reference in a new issue