Compare commits

..

10 commits

Author SHA1 Message Date
Brandon Taylor
06c6fd0495 fix bug, test 2022-07-24 11:54:55 -04:00
bramtayl
fbcd539a76
Allow skipping locks, precompile (#120)
* Allow skipping locks, precompile

* fix tests

* version
2022-07-23 15:42:04 -04:00
Jeff Fessler
3939d47a8d
Add tone with buffer example (#117) 2022-04-05 14:32:13 -04:00
Jeff Fessler
19a49931ad
Merge pull request #116 from JuliaAudio/jf-v1.2
Back to v1.2
2022-04-02 18:34:33 -04:00
Jeff Fessler
d21e1e0363 Back to v1.2 2022-04-02 18:12:53 -04:00
Jeff Fessler
7e0ca0122f
Fix remaining messanger typos, add docstring (#115)
* Fix typo, add docstring

* v1.3.0
2022-04-02 18:04:30 -04:00
bramtayl
156eae0db8
Update readme (#111)
* update readme

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update src/PortAudio.jl

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>
2022-03-29 13:00:39 -04:00
Abhaya Parthy
497567e329
Update save file example in README.md (#102)
* Update save file example in README.md

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Remove extra stream

Co-authored-by: bramtayl <brandon.taylor221@gmail.com>
Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>
2022-03-23 11:36:06 -04:00
Jeff Fessler
24acc0247b
Add octave shift example (#110)
* Add octave shift example

* specify duration

* use for loop
2022-03-22 11:06:41 -04:00
bramtayl
78a0a9918d
work with vector buffers (#109)
* work with vector buffers

* no redundant tests
2022-03-09 17:24:25 -05:00
8 changed files with 272 additions and 78 deletions

View file

@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
version:
- '1.3'
- '1.6'
- '1'
- 'nightly'
os:

View file

@ -1,7 +1,7 @@
name = "PortAudio"
uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d"
repo = "https://github.com/JuliaAudio/PortAudio.jl.git"
version = "1.2.0"
version = "1.3.0"
[deps]
alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7"
@ -11,7 +11,7 @@ SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
[compat]
julia = "1.3"
julia = "1.6"
alsa_plugins_jll = "1.2.2"
libportaudio_jll = "19.6.0"
SampledSignals = "2.1.1"

View file

@ -10,10 +10,14 @@ PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which g
## Opening a stream
The easiest way to open a source or sink is with the default `PortAudioStream()` constructor, which will open a 2-in, 2-out stream to your system's default device(s). The constructor can also take the input and output channel counts as positional arguments, or a variety of other keyword arguments.
The easiest way to open a source or sink is with the default `PortAudioStream()` constructor,
which will open a 2-in, 2-out stream to your system's default device(s).
The constructor can also take the input and output channel counts as positional arguments,
or a variety of other keyword arguments.
If named keyword arguments `latency` or `samplerate` are unspecified, then PortAudio will use device defaults.
```julia
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000Hz, latency=0.1, synced=false)
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000, latency=0.1)
```
You can open a specific device by adding it as the first argument, either as a `PortAudioDevice` instance or by name. You can also give separate names or devices if you want different input and output devices
@ -27,13 +31,16 @@ You can get a list of your system's devices with the `PortAudio.devices()` funct
```julia
julia> PortAudio.devices()
6-element Array{PortAudio.PortAudioDevice,1}:
PortAudio.PortAudioDevice("AirPlay","Core Audio",0,2,0)
PortAudio.PortAudioDevice("Built-in Microph","Core Audio",2,0,1)
PortAudio.PortAudioDevice("Built-in Output","Core Audio",0,2,2)
PortAudio.PortAudioDevice("JackRouter","Core Audio",2,2,3)
PortAudio.PortAudioDevice("After Effects 13.5","Core Audio",0,0,4)
PortAudio.PortAudioDevice("Built-In Aggregate","Core Audio",2,2,5)
14-element Vector{PortAudio.PortAudioDevice}:
"sof-hda-dsp: - (hw:0,0)" 2→2
"sof-hda-dsp: - (hw:0,3)" 0→2
"sof-hda-dsp: - (hw:0,4)" 0→2
"sof-hda-dsp: - (hw:0,5)" 0→2
"upmix" 8→8
"vdownmix" 6→6
"dmix" 0→2
"default" 32→32
```
## Reading and Writing
@ -75,7 +82,7 @@ end
### Open your built-in microphone and speaker by name
```julia
PortAudioStream("Built-in Microph", "Built-in Output") do stream
PortAudioStream("default", "default") do stream
write(stream, stream)
end
```
@ -83,13 +90,18 @@ end
### Record 10 seconds of audio and save to an ogg file
```julia
julia> using PortAudio, SampledSignals, LibSndFile
julia> import LibSndFile # must be in Manifest for FileIO.save to work
julia> stream = PortAudioStream("Built-in Microph", 2, 0)
PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
Samplerate: 48000 s⁻¹
Buffer Size: 4096 frames
2 channel source: "Built-in Microph"
julia> using PortAudio: PortAudioStream
julia> using SampledSignals: s
julia> using FileIO: save
julia> stream = PortAudioStream(1, 0) # default input (e.g., built-in microphone)
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel source: "default"
julia> buf = read(stream, 10s)
480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
@ -108,7 +120,7 @@ julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
using PortAudio, SampledSignals
S = 8192 # sampling rate (samples / second)
x = cos.(2pi*(1:2S)*440/S) # A440 tone for 2 seconds
PortAudioStream(0, 2; samplerate=Float64(S)) do stream
PortAudioStream(0, 2; samplerate=S) do stream
write(stream, x)
end
```

89
examples/octave-shift.jl Normal file
View file

@ -0,0 +1,89 @@
#=
This code illustrates real-time octave down shift
using a crude FFT-based method.
It also plots the input and output signals and their spectra.
This code uses the system defaults for the audio input and output devices.
If you use the built-in speakers and built-in microphone,
you will likely get undesirable audio feedback.
It works "best" if you play the audio output through headphones
so that the output does not feed back into the input.
The spectrum plotting came from the example in
https://github.com/JuliaAudio/PortAudio.jl/blob/master/examples
=#
using PortAudio: PortAudioStream
using SampledSignals: Hz, domain
using SampledSignals: (..) # see EllipsisNotation.jl and IntervalSets.jl
using FFTW: fft, ifft
using Plots: plot, gui, default; default(label="")
function pitch_halver(x) # decrease pitch by one octave via FFT
N = length(x)
mod(N,2) == 0 || throw("N must be multiple of 2")
F = fft(x) # original spectrum
Fnew = [F[1:N÷2]; zeros(N+1); F[(N÷2+2):N]]
out = 2 * real(ifft(Fnew))[1:N]
out.samplerate /= 2 # trick!
return out
end
# Plot input and output signals and their spectra.
# Quantize the vertical axis limits to reduce plot jitter.
function plotter(buf, out, N, fmin, fmax, fs; quant::Number = 0.1)
bmax = quant * ceil(maximum(abs, buf) / quant)
xticks = [1, N]; ylims = (-1,1) .* bmax; yticks = (-1:1)*bmax
p1 = plot(buf; xticks, ylims, yticks, title="input")
p3 = plot(out; xticks, ylims, yticks, title="output")
X = (2/N) * abs.(fft(buf)[fmin..fmax]) # spectrum
Xmax = quant * ceil(maximum(X) / quant)
xlims = (fs[1], fs[end]); ylims = (0, Xmax); yticks = [0,Xmax]
p2 = plot(fs, X; xlims, ylims, yticks)
Y = (2/N) * abs.(fft(out)[fmin..fmax])
p4 = plot(fs, Y; xlims, ylims, yticks)
plot(p1, p2, p3, p4)
end
"""
octave_shift(seconds; N, ...)
Shift audio down by one octave.
# Input
* `seconds` : how long to run in seconds; defaults to 300 (5 minutes)
# Options
* `N` : buffer size; default 1024 samples
* `fmin`,`fmax` : range of frequencies to display; default 0Hz to 4000Hz
"""
function octave_shift(
seconds::Number = 300;
N::Int = 1024,
fmin::Number = 0Hz,
fmax::Number = 4000Hz,
# undocumented options below here that are unlikely to be modified
in_stream = PortAudioStream(1, 0), # default input device
out_stream = PortAudioStream(0, 1), # default output device
buf::AbstractArray = read(in_stream, N), # warm-up
fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])],
Niters::Int = ceil(Int, seconds * in_stream.sample_rate / N),
)
for _ in 1:Niters
read!(in_stream, buf)
out = pitch_halver(buf) # decrease pitch by one octave
write(out_stream, out)
plotter(buf, out, N, fmin, fmax, fs); gui()
end
nothing
end
octave_shift(5)

21
examples/tone-buffer.jl Normal file
View file

@ -0,0 +1,21 @@
#=
This example illustrates synthesizing a long tone in small pieces
and routing it to the default audio output device using `write()`.
=#
using PortAudio: PortAudioStream, write
stream = PortAudioStream(0, 1; warn_xruns=false)
function play_tone(stream, freq::Real, duration::Real; buf_size::Int = 1024)
S = stream.sample_rate
current = 1
while current < duration*S
x = 0.7 * sin.(2π * (current .+ (1:buf_size)) * freq / S)
write(stream, x)
current += buf_size
end
nothing
end
play_tone(stream, 440, 2)

View file

@ -48,6 +48,7 @@ using .LibPortAudio:
Pa_Initialize,
paInputOverflowed,
Pa_IsStreamStopped,
paNoDevice,
paNoFlag,
Pa_OpenStream,
paOutputUnderflowed,
@ -195,12 +196,22 @@ function show(io::IO, device::PortAudioDevice)
print(io, device.output_bounds.max_channels)
end
function check_device_exists(device_index, device_type)
if device_index == paNoDevice
throw(ArgumentError("No $device_type device available"))
end
end
function get_default_input_index()
handle_status(Pa_GetDefaultInputDevice())
device_index = Pa_GetDefaultInputDevice()
check_device_exists(device_index, "input")
device_index
end
function get_default_output_index()
handle_status(Pa_GetDefaultOutputDevice())
device_index = Pa_GetDefaultOutputDevice()
check_device_exists(device_index, "output")
device_index
end
# we can look up devices by index or name
@ -230,19 +241,25 @@ function devices()
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)
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(
# 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 = buffer.pointer_to,
data = buffer.data,
use_frames = use_frames
() -> a_function(pointer_to, data, use_frames)
end,
buffer.stream_lock,
),
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
@ -407,25 +424,39 @@ eltype(::Type{Buffer{Sample}}) where {Sample} = Sample
nchannels(buffer::Buffer) = buffer.number_of_channels
"""
PortAudio.write_buffer(buffer, use_frames = buffer.frames_per_buffer)
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)
read_or_write(Pa_WriteStream, buffer, use_frames)
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)
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)
read_or_write(Pa_ReadStream, buffer, use_frames)
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
# the messenger will send tasks to the scribe
# the scribe will read/write from the buffer
"""
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}
@ -497,7 +528,7 @@ function messenger_task(
messenger, task
end
function fetch_messanger(messenger, task)
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)
@ -517,9 +548,9 @@ struct PortAudioStream{SinkMessenger, SourceMessenger}
sample_rate::Float64
# pointer to the c object
pointer_to::Ptr{PaStream}
sink_messanger::SinkMessenger
sink_messenger::SinkMessenger
sink_task::Task
source_messanger::SourceMessenger
source_messenger::SourceMessenger
source_task::Task
end
@ -584,10 +615,9 @@ function combine_default_sample_rates(
)
if input_sample_rate != output_sample_rate
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)).
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.
""",
),
@ -730,20 +760,24 @@ function PortAudioStream(
output_device.output_bounds.high_latency,
)
end
if samplerate === nothing
samplerate = combine_default_sample_rates(
samplerate = if samplerate === nothing
combine_default_sample_rates(
input_device,
input_device.default_sample_rate,
output_device,
output_device.default_sample_rate,
)
else
float(samplerate)
end
else
if latency === nothing
latency = input_device.input_bounds.high_latency
end
if samplerate === nothing
samplerate = input_device.default_sample_rate
samplerate = if samplerate === nothing
input_device.default_sample_rate
else
float(samplerate)
end
end
else
@ -751,9 +785,11 @@ function PortAudioStream(
if latency === nothing
latency = output_device.output_bounds.high_latency
end
if samplerate === nothing
samplerate = output_device.default_sample_rate
end
samplerate = if samplerate === nothing
output_device.default_sample_rate
else
float(samplerate)
end
else
throw(ArgumentError("Input or output must have at least 1 channel"))
end
@ -868,8 +904,8 @@ 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_messanger(stream.source_messanger, stream.source_task)
fetch_messanger(stream.sink_messanger, stream.sink_task)
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)))
@ -891,13 +927,14 @@ 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-sampledsignal scribes
# 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...)
@ -911,7 +948,7 @@ function show(io::IO, stream::PortAudioStream)
print(io, "PortAudioStream{")
print(io, eltype(stream))
println(io, "}")
print(io, " Samplerate: ", samplerate(stream), "Hz")
print(io, " Samplerate: ", round(Int, samplerate(stream)), "Hz")
# show source or sink if there's any channels
sink = stream.sink
if has_channels(sink)
@ -957,10 +994,10 @@ function getproperty(
end
function nchannels(source_or_sink::PortAudioSource)
nchannels(source_or_sink.stream.source_messanger)
nchannels(source_or_sink.stream.source_messenger)
end
function nchannels(source_or_sink::PortAudioSink)
nchannels(source_or_sink.stream.sink_messanger)
nchannels(source_or_sink.stream.sink_messenger)
end
function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource})
samplerate(source_or_sink.stream)
@ -978,8 +1015,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_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
@ -1008,14 +1045,14 @@ end
as_matrix(matrix::Matrix) = matrix
as_matrix(vector::Vector) = reshape(vector, length(vector), 1)
# these will only work with sampledsignals scribes
# 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_messanger, as_matrix(julia_buffer), already, frame_count)
exchange(sink.stream.sink_messenger, as_matrix(julia_buffer), already, frame_count)
end
function unsafe_read!(
@ -1024,7 +1061,9 @@ function unsafe_read!(
already,
frame_count,
)
exchange(source.stream.source_messanger, as_matrix(julia_buffer), already, frame_count)
exchange(source.stream.source_messenger, as_matrix(julia_buffer), already, frame_count)
end
include("precompile.jl")
end # module PortAudio

29
src/precompile.jl Normal file
View file

@ -0,0 +1,29 @@
# 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))

View file

@ -11,6 +11,7 @@ using PortAudio:
get_output_type,
handle_status,
initialize,
name,
PortAudioException,
PortAudio,
PortAudioDevice,
@ -18,7 +19,7 @@ using PortAudio:
safe_load,
seek_alsa_conf,
terminate,
name
write_buffer
using PortAudio.LibPortAudio:
Pa_AbortStream,
PaError,
@ -109,7 +110,9 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
initialize()
end
if !isempty(devices())
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
@ -130,15 +133,16 @@ if !isempty(devices())
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: 44100.0Hz
2 channel sink: $(repr(input_name))
2 channel source: $(repr(output_name))"""
@test sprint(show, source) == "2 channel source: $(repr(output_name))"
@test sprint(show, sink) == "2 channel sink: $(repr(input_name))"
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)
@ -209,8 +213,8 @@ if !isempty(devices())
big = typemax(Int)
@test_throws DomainError(
typemax(Int),
"$big exceeds maximum input channels for $output_name",
) PortAudioStream(input_name, output_name, big, 0)
"$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,
@ -219,8 +223,8 @@ if !isempty(devices())
adjust_channels = true,
)
@test_throws ArgumentError("""
Default sample rate 0 for input $output_name disagrees with
default sample rate 1 for output $input_name.
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),