performance improvements

This commit is contained in:
Brandon Taylor 2021-07-03 17:22:49 -04:00
parent 3c6bb1a27f
commit 8d6abcb785

View file

@ -110,7 +110,9 @@ function initialize()
# ALSA will throw extraneous warnings on start-up # ALSA will throw extraneous warnings on start-up
# send them to debug instead # send them to debug instead
debug_message = @capture_err handle_status(Pa_Initialize()) debug_message = @capture_err handle_status(Pa_Initialize())
@debug debug_message if !isempty(debug_message)
@debug debug_message
end
end end
function terminate() function terminate()
@ -260,9 +262,15 @@ abstract type Scribe end
struct SampledSignalsReader{Sample} <: Scribe struct SampledSignalsReader{Sample} <: Scribe
warn_xruns::Bool warn_xruns::Bool
end end
function SampledSignalsReader(; Sample = Float32, warn_xruns = true)
SampledSignalsReader{Sample}(warn_xruns)
end
struct SampledSignalsWriter{Sample} <: Scribe struct SampledSignalsWriter{Sample} <: Scribe
warn_xruns::Bool warn_xruns::Bool
end end
function SampledSignalsWriter(; Sample = Float32, warn_xruns = true)
SampledSignalsWriter{Sample}(warn_xruns)
end
# define on types # define on types
# throw an error if not defined # throw an error if not defined
@ -295,48 +303,59 @@ function get_output_type(::Type{<:Union{SampledSignalsReader, SampledSignalsWrit
Int Int
end end
function cut_to_size(buffer, julia_buffer, use_frames, offset, already) function full_write!(buffer, julia_buffer, already)
port_audio_range = 1:use_frames chunk_frames = buffer.chunk_frames
( @inbounds transpose!(
view(buffer.data, :, port_audio_range), buffer.data,
# the julia buffer is longer, so we might need to start from the middle view(julia_buffer, (1:chunk_frames) .+ already, :),
view(julia_buffer, port_audio_range .+ offset .+ already, :),
) )
write_buffer(buffer, chunk_frames)
end end
function (writer::SampledSignalsWriter)(buffer, (julia_buffer, offset, frame_count)) function (writer::SampledSignalsWriter)(buffer, (julia_buffer, offset, frame_count))
already = 0
chunk_frames = buffer.chunk_frames chunk_frames = buffer.chunk_frames
# if we still have frames to write foreach(
while already < frame_count let buffer = buffer, julia_buffer = julia_buffer
# take either a whole chunk, or whatever is left if it's smaller already -> full_write!(buffer, julia_buffer, already)
use_frames = min(frame_count - already, chunk_frames) end,
port_audio_view, julia_view = # keep going until there is less than a chunk left
cut_to_size(buffer, julia_buffer, use_frames, offset, already) offset:chunk_frames:(offset + frame_count - chunk_frames),
# we need to transpose column-major buffer from Julia back and forth between the row-major buffer from PortAudio )
# transpose, then send the data left = frame_count % chunk_frames
transpose!(port_audio_view, julia_view) port_audio_range = 1:left
write_buffer(buffer, use_frames) @inbounds transpose!(
already += use_frames view(buffer.data, :, port_audio_range),
end view(julia_buffer, port_audio_range .+ (offset + frame_count - left), :),
already )
write_buffer(buffer, left)
frame_count
end
function full_read!(buffer, julia_buffer, already)
chunk_frames = buffer.chunk_frames
read_buffer(buffer, chunk_frames)
@inbounds transpose!(
view(julia_buffer, (1:chunk_frames) .+ already, :),
buffer.data,
)
end end
function (reader::SampledSignalsReader)(buffer, (julia_buffer, offset, frame_count)) function (reader::SampledSignalsReader)(buffer, (julia_buffer, offset, frame_count))
already = 0
chunk_frames = buffer.chunk_frames chunk_frames = buffer.chunk_frames
# if we still have frames to write foreach(
while already < frame_count let buffer = buffer, julia_buffer = julia_buffer
# take either a whole chunk, or whatever is left if it's smaller already -> full_read!(buffer, julia_buffer, already)
use_frames = min(frame_count - already, chunk_frames) end,
# receive the data, then transpose offset:chunk_frames:(offset + frame_count - chunk_frames),
read_buffer(buffer, use_frames) )
port_audio_view, julia_view = left = frame_count % chunk_frames
cut_to_size(buffer, julia_buffer, use_frames, offset, already) port_audio_range = 1:left
transpose!(julia_view, port_audio_view) read_buffer(buffer, left)
already += use_frames @inbounds transpose!(
end view(julia_buffer, port_audio_range .+ (offset + frame_count - left), :),
already view(buffer.data, :, port_audio_range),
)
frame_count
end end
# a Buffer contains not just a buffer # a Buffer contains not just a buffer
@ -351,6 +370,7 @@ struct Buffer{Sample, Scribe, InputType, OutputType}
scribe::Scribe scribe::Scribe
inputs::Channel{InputType} inputs::Channel{InputType}
outputs::Channel{OutputType} outputs::Channel{OutputType}
debug::IOStream
end end
has_channels(something) = nchannels(something) > 0 has_channels(something) = nchannels(something) > 0
@ -360,9 +380,10 @@ function buffer_task(
pointer_to, pointer_to,
device, device,
channels, channels,
scribe::Scribe; scribe::Scribe,
debug;
Sample = Float32, Sample = Float32,
chunk_frames = 128, chunk_frames = 128
) where {Scribe} ) where {Scribe}
InputType = get_input_type(scribe) InputType = get_input_type(scribe)
OutputType = get_output_type(scribe) OutputType = get_output_type(scribe)
@ -379,16 +400,25 @@ function buffer_task(
scribe, scribe,
input_channel, input_channel,
output_channel, output_channel,
debug
) )
# we will spawn new threads to read from and write to port audio # 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 # 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 # start the scribe thread when its created
# if there's channels at all # if there's channels at all
# we can't make the task a field of the buffer, because the task uses the buffer # we can't make the task a field of the buffer, because the task uses the buffer
task = Task(() -> run_scribe(buffer)) task = Task(let buffer = buffer
# 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
() -> redirect_stderr(buffer.debug) do
run_scribe(buffer)
end
end)
task.sticky = false task.sticky = false
if has_channels(buffer) if has_channels(buffer)
schedule(task) schedule(task)
bind(output_channel, task)
else else
close(input_channel) close(input_channel)
close(output_channel) close(output_channel)
@ -416,6 +446,7 @@ struct PortAudioStream{SinkBuffer, SourceBuffer}
sink_task::Task sink_task::Task
source_buffer::SourceBuffer source_buffer::SourceBuffer
source_task::Task source_task::Task
debug::IOStream
end end
# portaudio uses codes instead of types for the sample format # portaudio uses codes instead of types for the sample format
@ -428,16 +459,12 @@ const TYPE_TO_FORMAT = Dict{Type, PaSampleFormat}(
UInt8 => 3, UInt8 => 3,
) )
# we need to convert nothing so it will be handled by C correctly
nothing_to_c_null(::Nothing) = C_NULL
nothing_to_c_null(something) = something
function make_parameters( function make_parameters(
device, device,
channels, channels,
latency; latency;
Sample = Float32, Sample = Float32,
host_api_specific_stream_info = nothing, host_api_specific_stream_info = C_NULL,
) )
if channels == 0 if channels == 0
# if we don't need any channels, we don't need the source/sink at all # if we don't need any channels, we don't need the source/sink at all
@ -449,7 +476,7 @@ function make_parameters(
channels, channels,
TYPE_TO_FORMAT[Sample], TYPE_TO_FORMAT[Sample],
latency, latency,
nothing_to_c_null(host_api_specific_stream_info), host_api_specific_stream_info,
), ),
) )
end end
@ -556,14 +583,15 @@ function PortAudioStream(
chunk_frames = 128, chunk_frames = 128,
frames_per_buffer = 0, frames_per_buffer = 0,
flags = paNoFlag, flags = paNoFlag,
call_back = nothing, call_back = C_NULL,
user_data = nothing, user_data = C_NULL,
input_info = nothing, input_info = C_NULL,
output_info = nothing, output_info = C_NULL,
stream_lock = ReentrantLock(), stream_lock = ReentrantLock(),
# this is where you can insert custom readers or writers instead # this is where you can insert custom readers or writers instead
writer = nothing, writer = SampledSignalsWriter(; Sample = Sample, warn_xruns = warn_xruns),
reader = nothing, reader = SampledSignalsReader(; Sample = Sample, warn_xruns = warn_xruns),
debug = mktemp()[2]
) )
input_channels_filled = input_channels_filled =
fill_max_channels("input", input_device, input_device.input_bounds, input_channels) fill_max_channels("input", input_device, input_device.input_bounds, input_channels)
@ -611,12 +639,6 @@ function PortAudioStream(
throw(ArgumentError("Input or output must have at least 1 channel")) throw(ArgumentError("Input or output must have at least 1 channel"))
end end
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 # we need a mutable pointer so portaudio can set it for us
mutable_pointer = Ref{Ptr{PaStream}}(0) mutable_pointer = Ref{Ptr{PaStream}}(0)
handle_status( handle_status(
@ -638,8 +660,8 @@ function PortAudioStream(
sample_rate, sample_rate,
frames_per_buffer, frames_per_buffer,
flags, flags,
nothing_to_c_null(call_back), call_back,
nothing_to_c_null(user_data), user_data,
), ),
) )
pointer_to = mutable_pointer[] pointer_to = mutable_pointer[]
@ -652,7 +674,8 @@ function PortAudioStream(
pointer_to, pointer_to,
output_device, output_device,
output_channels_filled, output_channels_filled,
writer; writer,
debug;
Sample = Sample, Sample = Sample,
chunk_frames = chunk_frames, chunk_frames = chunk_frames,
)..., )...,
@ -661,10 +684,12 @@ function PortAudioStream(
pointer_to, pointer_to,
input_device, input_device,
input_channels_filled, input_channels_filled,
reader; reader,
debug;
Sample = Sample, Sample = Sample,
chunk_frames = chunk_frames, chunk_frames = chunk_frames,
)..., )...,
debug
) )
end end
@ -717,17 +742,18 @@ function PortAudioStream(do_function::Function, arguments...; keywords...)
end end
function close(stream::PortAudioStream) function close(stream::PortAudioStream)
# this will shut down the channels, which will shut down the threads
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 source_buffer = stream.source_buffer
sink_buffer = stream.sink_buffer
if has_channels(source_buffer) if has_channels(source_buffer)
close(source_buffer) # this will shut down the channels, which will shut down the threads
close(source_buffer.inputs)
# wait for tasks to finish to make sure any errors get caught
wait(stream.source_task) wait(stream.source_task)
# output channels will close because they are bound to the task
end
if has_channels(sink_buffer)
close(sink_buffer.inputs)
wait(stream.sink_task)
end end
pointer_to = stream.pointer_to pointer_to = stream.pointer_to
# only stop if it's not already stopped # only stop if it's not already stopped
@ -735,6 +761,11 @@ function close(stream::PortAudioStream)
handle_status(Pa_StopStream(pointer_to)) handle_status(Pa_StopStream(pointer_to))
end end
handle_status(Pa_CloseStream(pointer_to)) handle_status(Pa_CloseStream(pointer_to))
debug_log = read(stream.debug, String)
# this will contain duplicate xrun warnings mentioned above
if !isempty(debug_log)
@debug debug_log
end
end end
function isopen(pointer_to::Ptr{PaStream}) function isopen(pointer_to::Ptr{PaStream})