From a18c7d36d02e230ff0623d44c0c1fbbc11fb5d67 Mon Sep 17 00:00:00 2001 From: Brandon Taylor Date: Tue, 18 Oct 2022 14:11:25 -0400 Subject: [PATCH 1/3] little refactor --- src/PortAudio.jl | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/PortAudio.jl b/src/PortAudio.jl index e48f00e..c9f8001 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -626,6 +626,21 @@ Please specify a sample rate. input_sample_rate end +function fill_one_device(device, bounds, latency, samplerate) + return ( + if latency === nothing + bounds.high_latency + else + latency + end, + if samplerate === nothing + device.default_sample_rate + else + float(samplerate) + end + ) +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...) @@ -771,25 +786,13 @@ function PortAudioStream( float(samplerate) end else - if latency === nothing - latency = input_device.input_bounds.high_latency - end - samplerate = if samplerate === nothing - input_device.default_sample_rate - else - float(samplerate) - end + latency, samplerate = + fill_one_device(input_device, input_device.input_bounds, latency, samplerate) end else if output_channels_filled > 0 - if latency === nothing - latency = output_device.output_bounds.high_latency - end - samplerate = if samplerate === nothing - output_device.default_sample_rate - else - float(samplerate) - end + latency, samplerate = + fill_one_device(output_device, output_device.output_bounds, latency, samplerate) else throw(ArgumentError("Input or output must have at least 1 channel")) end From 987d2840da57e9cc21215c65f3279ae51ebe04b8 Mon Sep 17 00:00:00 2001 From: Brandon Taylor Date: Tue, 18 Oct 2022 16:39:51 -0400 Subject: [PATCH 2/3] fix messenger eltype --- src/PortAudio.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PortAudio.jl b/src/PortAudio.jl index c9f8001..4964ee7 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -465,7 +465,7 @@ struct Messenger{Sample, Scribe, Input, Output} output_channel::Channel{Output} end -eltype(::Type{Messenger{Sample}}) where {Sample} = Sample +eltype(::Type{<: Messenger{Sample}}) where {Sample} = Sample name(messenger::Messenger) = messenger.device_name nchannels(messenger::Messenger) = nchannels(messenger.buffer) @@ -535,9 +535,8 @@ function fetch_messenger(messenger, task) # wait for tasks to finish to make sure any errors get caught wait(task) # output channel will close because it is bound to the task - else - "" end + nothing end # From 494d7c8ffa8331587bf4fa850221c2cdcae390ca Mon Sep 17 00:00:00 2001 From: Brandon Taylor Date: Wed, 19 Oct 2022 15:48:33 -0400 Subject: [PATCH 3/3] callback --- Project.toml | 6 +- docs/make.jl | 9 +- src/PortAudio.jl | 889 ++++++++++------------------------------- src/precompile.jl | 29 -- test/runtests.jl | 266 +----------- test/runtests_local.jl | 95 ----- 6 files changed, 231 insertions(+), 1063 deletions(-) delete mode 100644 src/precompile.jl delete mode 100644 test/runtests_local.jl diff --git a/Project.toml b/Project.toml index c82362b..49ca5c3 100644 --- a/Project.toml +++ b/Project.toml @@ -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"] diff --git a/docs/make.jl b/docs/make.jl index edca21e..03e8d8d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -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") \ No newline at end of file +deploydocs(repo = "github.com/JuliaAudio/PortAudio.jl.git") diff --git a/src/PortAudio.jl b/src/PortAudio.jl index 4964ee7..c47e040 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -1,35 +1,18 @@ module PortAudio using alsa_plugins_jll: alsa_plugins_jll -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! -import SampledSignals: nchannels, samplerate, unsafe_read!, unsafe_write -using SampledSignals: SampleSink, SampleSource using Suppressor: @capture_err -export devices, PortAudioStream +export devices, is_stopped, PortAudioStream, start, stop include("libportaudio.jl") using .LibPortAudio: - paBadStreamPtr, + paAbort, Pa_CloseStream, + paComplete, + paContinue, PaDeviceIndex, PaDeviceInfo, PaError, @@ -40,11 +23,8 @@ using .LibPortAudio: Pa_GetDeviceInfo, Pa_GetErrorText, Pa_GetHostApiInfo, - Pa_GetStreamReadAvailable, - Pa_GetStreamWriteAvailable, Pa_GetVersion, Pa_GetVersionText, - PaHostApiTypeId, Pa_Initialize, paInputOverflowed, Pa_IsStreamStopped, @@ -52,14 +32,14 @@ using .LibPortAudio: paNoFlag, Pa_OpenStream, paOutputUnderflowed, - Pa_ReadStream, PaSampleFormat, Pa_StartStream, - Pa_StopStream, PaStream, + PaStreamCallbackFlags, + PaStreamCallbackResult, + PaStreamCallbackTimeInfo, PaStreamParameters, - Pa_Terminate, - Pa_WriteStream + Pa_Terminate # for structs and strings, PortAudio will return C_NULL instead of erroring # so we need to handle these errors @@ -83,7 +63,8 @@ end struct PortAudioException <: Exception code::PaErrorCode end -function showerror(io::IO, exception::PortAudioException) + +function Base.showerror(io::IO, exception::PortAudioException) print(io, "PortAudioException: ") print(io, get_error_text(PaError(exception.code))) end @@ -188,7 +169,7 @@ function PortAudioDevice(info::PaDeviceInfo, index) end name(device::PortAudioDevice) = device.name -function show(io::IO, device::PortAudioDevice) +function Base.show(io::IO, device::PortAudioDevice) print(io, repr(name(device))) print(io, ' ') print(io, device.input_bounds.max_channels) @@ -229,6 +210,10 @@ function get_device(device_name::AbstractString) throw(KeyError(device_name)) end +function get_device(device::PortAudioDevice) + device +end + """ devices() @@ -240,317 +225,12 @@ function devices() map(get_device, 0:(handle_status(Pa_GetDeviceCount()) - 1)) end -# we can handle reading and writing from buffers in a similar way -function read_or_write(a_function, buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) - pointer_to = buffer.pointer_to - data = buffer.data - handle_status( - if acquire_lock - # because we're calling Pa_ReadStream and Pa_WriteStream from separate threads, - # we put a lock around these calls - lock( - let a_function = a_function, - pointer_to = pointer_to, - data = data, - use_frames = use_frames - () -> a_function(pointer_to, data, use_frames) - end, - buffer.stream_lock, - ) - else - a_function(pointer_to, data, use_frames) - end; - warn_xruns = buffer.warn_xruns, - ) -end - -""" - abstract type PortAudio.Scribe end - -A scribe must implement the following: - - - A method for [`PortAudio.get_input_type`](@ref) - - A method for [`PortAudio.get_output_type`](@ref) - - A method to call itself on two arguments: a [`PortAudio.Buffer`](@ref) and an input of the input type. - This method must return an output of the output type. - This method should make use of [`PortAudio.read_buffer!`](@ref) and [`PortAudio.write_buffer`](@ref). -""" -abstract type Scribe end - -abstract type SampledSignalsScribe <: Scribe end - -""" - struct PortAudio.SampledSignalsReader - -A [`PortAudio.Scribe`](@ref) that will use the `SampledSignals` package to manage reading data from PortAudio. -""" -struct SampledSignalsReader <: SampledSignalsScribe end - -""" - struct PortAudio.SampledSignalsReader - -A [`PortAudio.Scribe`](@ref) that will use the `SampledSignals` package to manage writing data to PortAudio. -""" -struct SampledSignalsWriter <: SampledSignalsScribe end - -""" - PortAudio.get_input_type(scribe::PortAudio.Scribe, Sample) - -Get the input type of a [`PortAudio.Scribe`](@ref) for samples of type `Sample`. -""" -function get_input_type(::SampledSignalsScribe, Sample) - # SampledSignals input_channel will be a triple of the last 3 arguments to unsafe_read/write - # we will already have access to the stream itself - Tuple{Array{Sample, 2}, Int, Int} -end - -""" - PortAudio.get_input_type(scribe::PortAudio.Scribe, Sample) - -Get the output type of a [`PortAudio.Scribe`](@ref) for samples of type `Sample`. -""" -function get_output_type(::SampledSignalsScribe, Sample) - # output is the number of frames read/written - Int -end - -# the julia buffer is bigger than the port audio buffer -# so we need to split it up into chunks -# we do this the same way for both reading and writing -function split_up( - buffer, - julia_buffer, - already, - frame_count, - whole_function, - partial_function, -) - frames_per_buffer = buffer.frames_per_buffer - # when we're done, we'll have written this many frames - goal = already + frame_count - # this is what we'll have left after doing all complete chunks - left = frame_count % frames_per_buffer - # this is how many we'll have written after doing all complete chunks - even = goal - left - foreach( - let whole_function = whole_function, - buffer = buffer, - julia_buffer = julia_buffer, - frames_per_buffer = frames_per_buffer - already -> whole_function( - buffer, - frames_per_buffer, - julia_buffer, - (already + 1):(already + frames_per_buffer) - ) - end, - # start at the already, keep going until there is less than a chunk left - already:frames_per_buffer:(even - frames_per_buffer), - # each time we loop, add chunk frames to already - # after the last loop, we'll reach "even" - ) - # now we just have to read/write what's left - if left > 0 - partial_function(buffer, left, julia_buffer, (even + 1):goal) - end - frame_count -end - -# the full version doesn't have to make a view, but the partial version does -function full_write!(buffer, count, julia_buffer, julia_range) - @inbounds transpose!(buffer.data, view(julia_buffer, julia_range, :)) - write_buffer(buffer, count) -end - -function partial_write!(buffer, count, julia_buffer, julia_range) - @inbounds transpose!(view(buffer.data, :, 1:count), view(julia_buffer, julia_range, :)) - write_buffer(buffer, count) -end - -function (writer::SampledSignalsWriter)(buffer, arguments) - split_up(buffer, arguments..., full_write!, partial_write!) -end - -# similar to above -function full_read!(buffer, count, julia_buffer, julia_range) - read_buffer!(buffer, count) - @inbounds transpose!(view(julia_buffer, julia_range, :), buffer.data) -end - -function partial_read!(buffer, count, julia_buffer, julia_range) - read_buffer!(buffer, count) - @inbounds transpose!(view(julia_buffer, julia_range, :), view(buffer.data, :, 1:count)) -end - -function (reader::SampledSignalsReader)(buffer, arguments) - split_up(buffer, arguments..., full_read!, partial_read!) -end - -""" - struct PortAudio.Buffer{Sample} - -A `PortAudio.Buffer` contains everything you might need to read or write data from or to PortAudio. -The `data` field contains the raw data in the buffer. -Use [`PortAudio.write_buffer`](@ref) to write data to PortAudio, and [`PortAudio.read_buffer!`](@ref) to read data from PortAudio. -""" -struct Buffer{Sample} - stream_lock::ReentrantLock - pointer_to::Ptr{PaStream} - data::Array{Sample, 2} - number_of_channels::Int - frames_per_buffer::Int - warn_xruns::Bool -end - -function Buffer( - stream_lock, - pointer_to, - number_of_channels; - Sample = Float32, - frames_per_buffer = 128, - warn_xruns = true, -) - Buffer{Sample}( - stream_lock, - pointer_to, - zeros(Sample, number_of_channels, frames_per_buffer), - number_of_channels, - frames_per_buffer, - warn_xruns, - ) -end - -eltype(::Type{Buffer{Sample}}) where {Sample} = Sample -nchannels(buffer::Buffer) = buffer.number_of_channels - -""" - PortAudio.write_buffer(buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) - -Write a number of frames (`use_frames`) from a [`PortAudio.Buffer`](@ref) to PortAudio. - -Set `acquire_lock = false` to skip acquiring the lock. -""" -function write_buffer(buffer::Buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) - read_or_write(Pa_WriteStream, buffer, use_frames; acquire_lock = acquire_lock) -end - -""" - PortAudio.read_buffer!(buffer::Buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) - -Read a number of frames (`use_frames`) from PortAudio to a [`PortAudio.Buffer`](@ref). - -Set `acquire_lock = false` to skip acquiring the acquire_lock. -""" -function read_buffer!(buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true) - read_or_write(Pa_ReadStream, buffer, use_frames; acquire_lock = acquire_lock) -end - -""" - Messenger{Sample, Scribe, Input, Output} - -A `struct` with entries -* `device_name::String` -* `buffer::Buffer{Sample}` -* `scribe::Scribe` -* `input_channel::Channel{Input}` -* `output_channel::Channel{Output}` -The messenger will send tasks to the scribe; -the scribe will read/write from the buffer. -""" -struct Messenger{Sample, Scribe, Input, Output} - device_name::String - buffer::Buffer{Sample} - scribe::Scribe - input_channel::Channel{Input} - output_channel::Channel{Output} -end - -eltype(::Type{<: Messenger{Sample}}) where {Sample} = Sample -name(messenger::Messenger) = messenger.device_name -nchannels(messenger::Messenger) = nchannels(messenger.buffer) - -# 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 send(messenger) - buffer = messenger.buffer - scribe = messenger.scribe - input_channel = messenger.input_channel - output_channel = messenger.output_channel - for input in input_channel - put!(output_channel, scribe(buffer, input)) - end -end - -# convenience method -has_channels(something) = nchannels(something) > 0 - -# create the messenger, and start the scribe on a separate task -function messenger_task( - device_name, - buffer::Buffer{Sample}, - scribe::Scribe -) where {Sample, Scribe} - Input = get_input_type(scribe, Sample) - Output = get_output_type(scribe, Sample) - input_channel = Channel{Input}(0) - output_channel = Channel{Output}(0) - # unbuffered channels so putting and taking will block till everyone's ready - messenger = Messenger{Sample, Scribe, Input, Output}( - device_name, - buffer, - 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(let messenger = messenger - # 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 could send them to a debug log - # but that causes problems when done from multiple threads - () -> send(messenger) - end) - # makes it able to run on a separate thread - task.sticky = false - if has_channels(buffer) - schedule(task) - # output channel will close when the task ends - bind(output_channel, task) - else - close(input_channel) - close(output_channel) - end - messenger, task -end - -function fetch_messenger(messenger, task) - if has_channels(messenger) - # this will shut down the channels, which will shut down the thread - close(messenger.input_channel) - # wait for tasks to finish to make sure any errors get caught - wait(task) - # output channel will close because it is bound to the task - end - nothing -end - -# -# PortAudioStream -# - -struct PortAudioStream{SinkMessenger, SourceMessenger} +struct PortAudioStream{Sample} sample_rate::Float64 - # pointer to the c object pointer_to::Ptr{PaStream} - sink_messenger::SinkMessenger - sink_task::Task - source_messenger::SourceMessenger - source_task::Task + input_channels_filled::Int + output_channels_filled::Int + frames_per_buffer::Int end # portaudio uses codes instead of types for the sample format @@ -618,140 +298,184 @@ function combine_default_sample_rates( 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 fill_one_device(device, bounds, latency, samplerate) - return ( - if latency === nothing - bounds.high_latency - else - latency - end, - if samplerate === nothing - device.default_sample_rate - else - float(samplerate) - end - ) + return (if latency === nothing + bounds.high_latency + else + latency + end, if samplerate === nothing + device.default_sample_rate + else + float(samplerate) + end) 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...) - PortAudioStream(duplex_device, input_channels = 2, output_channels = 2; options...) - PortAudioStream(input_device, output_device, input_channels = 2, output_channels = 2; options...) - -Audio devices can either be `PortAudioDevice` instances as returned by [`devices`](@ref), or strings with the device name as reported by the operating system. -Set `input_channels` to `0` for an output only stream; set `output_channels` to `0` for an input only steam. -If you pass the function `maximum` instead of a number of channels, use the maximum channels allowed by the corresponding device. -If a single `duplex_device` is given, it will be used for both input and output. -If no devices are given, the system default devices will be used. - -The `PortAudioStream` type supports all the stream and buffer features defined [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl) by default. -For example, if you load SampledSignals with `using SampledSignals` you can read 5 seconds to a buffer with `buf = read(stream, 5s)`, regardless of the sample rate of the device. -`write(stream, stream)` will set up a loopback that will read from the input and play it back on the output. - -Options: - - - `adjust_channels = false`: If set to `true`, if either `input_channels` or `output_channels` exceeds the corresponding device maximum, adjust down to the maximum. - - `call_back = C_NULL`: The PortAudio call-back function. - Currently, passing anything except `C_NULL` is unsupported. - - `eltype = Float32`: Sample type of the audio stream - - `flags = PortAudio.paNoFlag`: PortAudio flags - - `frames_per_buffer = 128`: the number of frames per buffer - - `input_info = C_NULL`: host API specific stream info for the input device. - Currently, passing anything except `C_NULL` is unsupported. - - `latency = nothing`: Requested latency. Stream could underrun when too low, consider using the defaults. If left as `nothing`, use the defaults below: - - For input/output only streams, use the corresponding device's default high latency. - - For duplex streams, use the max of the default high latency of the input and output devices. - - `output_info = C_NULL`: host API specific stream info for the output device. - Currently, passing anything except `C_NULL` is unsupported. - - `reader = PortAudio.SampledSignalsReader()`: the scribe that will read input. - Defaults to a [`PortAudio.SampledSignalsReader`](@ref). - Users can pass custom scribes; see [`PortAudio.Scribe`](@ref). - - `samplerate = nothing`: Sample rate. If left as `nothing`, use the defaults below: - - For input/output only streams, use the corresponding device's default sample rate. - - For duplex streams, use the default sample rate if the default sample rates for the input and output devices match, otherwise throw an error. - - `warn_xruns = true`: Display a warning if there is a stream overrun or underrun, which often happens when Julia is compiling, or with a particularly large GC run. - Only affects duplex streams. - - `writer = PortAudio.SampledSignalsWriter()`: the scribe that will write output. - Defaults to a [`PortAudio.SampledSignalsReader`](@ref). - Users can pass custom scribes; see [`PortAudio.Scribe`](@ref). +mutable struct CallbackWrapper{Sample, AFunction} + const a_function::AFunction + const input_channels::Int + const output_channels::Int + frames_already::Int +end -## Examples: +function CallbackWrapper{Sample}( + a_function::AFunction, + input_channels, + output_channels, + frames_already = 0, +) where {Sample, AFunction} + CallbackWrapper{Sample, AFunction}( + a_function, + input_channels, + output_channels, + frames_already, + ) +end -Set up an audio pass-through from microphone to speaker +Base.eltype(::Type{<:CallbackWrapper{Sample}}) where {Sample} = Sample -```julia -julia> using PortAudio, SampledSignals +function unwrap_buffer_pointer(pointer, channels, frames_per_buffer) + unsafe_wrap(Array, pointer, (channels, frames_per_buffer)) +end -julia> stream = PortAudioStream(2, 2; warn_xruns = false); +function inner_run_callback( + callback_wrapper, + input_buffer_pointer, + output_buffer_pointer, + frames_per_buffer, + frames_already, +) + callback_wrapper.a_function( + unwrap_buffer_pointer( + input_buffer_pointer, + callback_wrapper.input_channels, + frames_per_buffer, + ), + unwrap_buffer_pointer( + output_buffer_pointer, + callback_wrapper.output_channels, + frames_per_buffer, + ), + frames_per_buffer, + frames_already, + ) +end -julia> try - # cancel with Ctrl-C - write(stream, stream, 2s) - finally - close(stream) - end -``` +function inner_run_callback( + callback_wrapper, + ::Ptr{Nothing}, + output_buffer_pointer, + frames_per_buffer, + frames_already, +) + callback_wrapper.a_function( + nothing, + unwrap_buffer_pointer( + output_buffer_pointer, + callback_wrapper.output_channels, + frames_per_buffer, + ), + frames_per_buffer, + frames_already, + ) +end -Use `do` syntax to auto-close the stream +function inner_run_callback( + callback_wrapper, + input_buffer_pointer, + ::Ptr{Nothing}, + frames_per_buffer, + frames_already, +) + callback_wrapper.a_function( + unwrap_buffer_pointer( + input_buffer_pointer, + callback_wrapper.input_channels, + frames_per_buffer, + ), + nothing, + frames_per_buffer, + frames_already, + ) +end -```julia -julia> using PortAudio, SampledSignals +function inner_run_callback(_, ::Ptr{Nothing}, ::Ptr{Nothing}, __, ___) + throw(ArgumentError("Neither input nor output buffer")) +end -julia> PortAudioStream(2, 2; warn_xruns = false) do stream - write(stream, stream, 2s) +function run_callback( + input_buffer_pointer, + output_buffer_pointer, + unsigned_frames_per_buffer, + _, # time_info_pointer TODO: give to users? + __, # status_flags TODO: give to users? + callback_wrapper_pointer, +) + try + callback_wrapper = unsafe_pointer_to_objref(callback_wrapper_pointer) + frames_already = callback_wrapper.frames_already + frames_per_buffer = Int(unsigned_frames_per_buffer) + written = inner_run_callback( + callback_wrapper, + input_buffer_pointer, + output_buffer_pointer, + frames_per_buffer, + frames_already, + ) + callback_wrapper.frames_already = frames_already + written + if written < frames_per_buffer + paComplete + else + paContinue end -``` - -Open devices by name - -```julia -using PortAudio, SampledSignals -PortAudioStream("Built-in Microph", "Built-in Output"; warn_xruns = false) do stream - write(stream, stream, 2s) + catch an_error + @info Base.showerror(stdout, an_error, Base.catch_backtrace()) + paAbort + end end -2 s -``` -Record 10 seconds of audio and save to an ogg file - -```julia -julia> using PortAudio, SampledSignals, LibSndFile +function callback_cfunction( + ::Callback, + ::Type{Sample}, + ::Type{Input}, + ::Type{Output}, +) where {Callback, Sample, Input, Output} + @cfunction( + run_callback, + PaStreamCallbackResult, # returns + ( + Ptr{Input}, # input buffer pointer + Ptr{Output}, # output buffer pointer + Culong, # unsigned_frames_per_buffer + Ptr{PaStreamCallbackTimeInfo}, # time info pointer + PaStreamCallbackFlags, # status flags + Ptr{CallbackWrapper{Callback, Sample}}, # user data, that is, the "true callback" + ) + ) +end -julia> PortAudioStream(2, 0; warn_xruns = false) do stream - buf = read(stream, 10s) - save(joinpath(tempname(), ".ogg"), buf) - end -2 s -``` -""" function PortAudioStream( - input_device::PortAudioDevice, - output_device::PortAudioDevice, + callback; + input_device = get_default_input_index(), + output_device = get_default_output_index(), input_channels = 2, - output_channels = 2; + output_channels = 2, eltype = Float32, adjust_channels = false, - call_back = C_NULL, flags = paNoFlag, frames_per_buffer = 128, input_info = C_NULL, latency = nothing, output_info = C_NULL, - reader = SampledSignalsReader(), samplerate = nothing, - stream_lock = ReentrantLock(), - user_data = C_NULL, - warn_xruns = true, - writer = SampledSignalsWriter(), ) + input_device = get_device(input_device) + output_device = get_device(output_device) input_channels_filled = fill_max_channels( "input", input_device, @@ -785,19 +509,29 @@ function PortAudioStream( float(samplerate) end else - latency, samplerate = - fill_one_device(input_device, input_device.input_bounds, latency, samplerate) + latency, samplerate = fill_one_device( + input_device, + input_device.input_bounds, + latency, + samplerate, + ) end else if output_channels_filled > 0 - latency, samplerate = - fill_one_device(output_device, output_device.output_bounds, latency, samplerate) + latency, samplerate = fill_one_device( + output_device, + output_device.output_bounds, + latency, + samplerate, + ) else throw(ArgumentError("Input or output must have at least 1 channel")) end end # we need a mutable pointer so portaudio can set it for us mutable_pointer = Ref{Ptr{PaStream}}(0) + callback_wrapper = CallbackWrapper{eltype}(callback, input_channels, output_channels) + handle_status( Pa_OpenStream( mutable_pointer, @@ -817,255 +551,54 @@ function PortAudioStream( samplerate, frames_per_buffer, flags, - call_back, - user_data, + callback_cfunction( + callback, + eltype, + if input_channels_filled > 0 + eltype + else + Nothing + end, + if output_channels_filled > 0 + eltype + else + Nothing + end, + ), + Ref(callback_wrapper), ), ) pointer_to = mutable_pointer[] - handle_status(Pa_StartStream(pointer_to)) - PortAudioStream( + PortAudioStream{eltype}( samplerate, pointer_to, - # we need to keep track of the tasks so we can wait for them to finish and catch errors - messenger_task( - output_device.name, - Buffer( - stream_lock, - pointer_to, - output_channels_filled; - Sample = eltype, - frames_per_buffer = frames_per_buffer, - warn_xruns = warn_xruns, - ), - writer, - )..., - messenger_task( - input_device.name, - Buffer( - stream_lock, - pointer_to, - input_channels_filled; - Sample = eltype, - frames_per_buffer = frames_per_buffer, - warn_xruns = warn_xruns, - ), - reader, - )..., - ) -end - -# handle device names given as streams -function PortAudioStream( - in_device_name::AbstractString, - out_device_name::AbstractString, - arguments...; - keywords..., -) - PortAudioStream( - get_device(in_device_name), - get_device(out_device_name), - arguments...; - keywords..., - ) -end - -# if one device is given, use it for input and output -function PortAudioStream( - device::Union{PortAudioDevice, AbstractString}, - input_channels = 2, - output_channels = 2; - keywords..., -) - PortAudioStream(device, device, input_channels, output_channels; keywords...) -end - -# use the default input and output devices -function PortAudioStream(input_channels = 2, output_channels = 2; keywords...) - in_index = get_default_input_index() - out_index = get_default_output_index() - PortAudioStream( - get_device(in_index), - get_device(out_index), - input_channels, - output_channels; - keywords..., + input_channels_filled, + output_channels_filled, + frames_per_buffer, ) end -# handle do-syntax -function PortAudioStream(do_function::Function, arguments...; keywords...) - stream = PortAudioStream(arguments...; keywords...) - try - do_function(stream) - finally - close(stream) - end +function start(stream::PortAudioStream) + handle_status(Pa_StartStream(stream.pointer_to)) end -function close(stream::PortAudioStream) - # closing is tricky, because we want to make sure we've read exactly as much as we've written - # but we have don't know exactly what the tasks are doing - # for now, just close one and then the other - fetch_messenger(stream.source_messenger, stream.source_task) - fetch_messenger(stream.sink_messenger, stream.sink_task) - pointer_to = stream.pointer_to - # only stop if it's not already stopped - if !Bool(handle_status(Pa_IsStreamStopped(pointer_to))) - handle_status(Pa_StopStream(pointer_to)) - end - handle_status(Pa_CloseStream(pointer_to)) +function stop(stream::PortAudioStream) + handle_status(Pa_StopStream(stream.pointer_to)) end -function isopen(pointer_to::Ptr{PaStream}) - # we aren't actually interested if the stream is stopped or not - # instead, we are looking for the error which comes from checking on a closed stream - error_number = Pa_IsStreamStopped(pointer_to) - if error_number >= 0 +function is_stopped(stream::PortAudioStream) + result = handle_status(Pa_IsStreamStopped(stream.pointer_to)) + if result == 1 true + elseif result == 0 + false else - PaErrorCode(error_number) != paBadStreamPtr + error("Unexpected result $result") end end -isopen(stream::PortAudioStream) = isopen(stream.pointer_to) - -samplerate(stream::PortAudioStream) = stream.sample_rate - -function eltype( - ::Type{<:PortAudioStream{<:Messenger{Sample}, <:Messenger{Sample}}}, -) where {Sample} - Sample -end -# these defaults will error for non-SampledSignals scribes -# which is probably ok; we want these users to define new methods -read(stream::PortAudioStream, arguments...) = read(stream.source, arguments...) -read!(stream::PortAudioStream, arguments...) = read!(stream.source, arguments...) -write(stream::PortAudioStream, arguments...) = write(stream.sink, arguments...) -function write(sink::PortAudioStream, source::PortAudioStream, arguments...) - write(sink.sink, source.source, arguments...) +function Base.close(stream::PortAudioStream) + handle_status(Pa_CloseStream(stream.pointer_to)) end -function show(io::IO, stream::PortAudioStream) - # just show the first type parameter (eltype) - print(io, "PortAudioStream{") - print(io, eltype(stream)) - println(io, "}") - print(io, " Samplerate: ", round(Int, samplerate(stream)), "Hz") - # show source or sink if there's any channels - sink = stream.sink - if has_channels(sink) - print(io, "\n ") - show(io, sink) - end - source = stream.source - if has_channels(source) - print(io, "\n ") - show(io, source) - end -end - -# -# PortAudioSink & PortAudioSource -# - -# Define our source and sink types -# 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{InputMessanger, OutputMessanger} <: $Super - stream::PortAudioStream{InputMessanger, OutputMessanger} - end -end - -# provided for backwards compatibility -# only defined for SampledSignals scribes -function getproperty( - stream::PortAudioStream{ - <:Messenger{<:Any, <:SampledSignalsWriter}, - <:Messenger{<:Any, <:SampledSignalsReader}, - }, - property::Symbol, -) - if property === :sink - PortAudioSink(stream) - elseif property === :source - PortAudioSource(stream) - else - getfield(stream, property) - end -end - -function nchannels(source_or_sink::PortAudioSource) - nchannels(source_or_sink.stream.source_messenger) -end -function nchannels(source_or_sink::PortAudioSink) - nchannels(source_or_sink.stream.sink_messenger) -end -function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource}) - samplerate(source_or_sink.stream) -end -function eltype( - ::Type{ - <:Union{ - <:PortAudioSink{<:Messenger{Sample}, <:Messenger{Sample}}, - <:PortAudioSource{<:Messenger{Sample}, <:Messenger{Sample}}, - }, - }, -) where {Sample} - Sample -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_messenger) -name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messenger) - -# could show full type name, but the PortAudio part is probably redundant -# because these will usually only get printed as part of show for PortAudioStream -kind(::PortAudioSink) = "sink" -kind(::PortAudioSource) = "source" -function show(io::IO, sink_or_source::Union{PortAudioSink, PortAudioSource}) - print( - io, - nchannels(sink_or_source), - " channel ", - kind(sink_or_source), - ": ", - # put in quotes - repr(name(sink_or_source)), - ) -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(messenger, arguments...) - put!(messenger.input_channel, arguments) - take!(messenger.output_channel) -end - -as_matrix(matrix::Matrix) = matrix -as_matrix(vector::Vector) = reshape(vector, length(vector), 1) - -# these will only work with SampledSignals scribes -function unsafe_write( - sink::PortAudioSink{<:Messenger{<:Any, <:SampledSignalsWriter}}, - julia_buffer::Array, - already, - frame_count, -) - exchange(sink.stream.sink_messenger, as_matrix(julia_buffer), already, frame_count) -end - -function unsafe_read!( - source::PortAudioSource{<:Any, <:Messenger{<:Any, <:SampledSignalsReader}}, - julia_buffer::Array, - already, - frame_count, -) - exchange(source.stream.source_messenger, as_matrix(julia_buffer), already, frame_count) -end - -include("precompile.jl") - end # module PortAudio diff --git a/src/precompile.jl b/src/precompile.jl deleted file mode 100644 index 7dd9ab3..0000000 --- a/src/precompile.jl +++ /dev/null @@ -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)) - - - - - - diff --git a/test/runtests.jl b/test/runtests.jl index 9933c68..0588192 100755 --- a/test/runtests.jl +++ b/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 - - @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 - 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 +const RADIANS_PER_FRAME = 1 / 44100 * 440 * 2 * pi - @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) +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 - 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 + frames_per_buffer end end - doctest(PortAudio) + start(stream) + sleep(2) + close(stream) end diff --git a/test/runtests_local.jl b/test/runtests_local.jl deleted file mode 100644 index f98afb1..0000000 --- a/test/runtests_local.jl +++ /dev/null @@ -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