Compare commits

..

7 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
6 changed files with 141 additions and 54 deletions

View file

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

View file

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

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, Pa_Initialize,
paInputOverflowed, paInputOverflowed,
Pa_IsStreamStopped, Pa_IsStreamStopped,
paNoDevice,
paNoFlag, paNoFlag,
Pa_OpenStream, Pa_OpenStream,
paOutputUnderflowed, paOutputUnderflowed,
@ -195,12 +196,22 @@ function show(io::IO, device::PortAudioDevice)
print(io, device.output_bounds.max_channels) print(io, device.output_bounds.max_channels)
end 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() function get_default_input_index()
handle_status(Pa_GetDefaultInputDevice()) device_index = Pa_GetDefaultInputDevice()
check_device_exists(device_index, "input")
device_index
end end
function get_default_output_index() function get_default_output_index()
handle_status(Pa_GetDefaultOutputDevice()) device_index = Pa_GetDefaultOutputDevice()
check_device_exists(device_index, "output")
device_index
end end
# we can look up devices by index or name # we can look up devices by index or name
@ -230,19 +241,25 @@ function devices()
end end
# we can handle reading and writing from buffers in a similar way # 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( handle_status(
# because we're calling Pa_ReadStream and Pa_WriteStream from separate threads, if acquire_lock
# we put a lock around these calls # because we're calling Pa_ReadStream and Pa_WriteStream from separate threads,
lock( # we put a lock around these calls
let a_function = a_function, lock(
pointer_to = buffer.pointer_to, let a_function = a_function,
data = buffer.data, pointer_to = pointer_to,
use_frames = use_frames data = data,
() -> a_function(pointer_to, data, use_frames) use_frames = use_frames
end, () -> a_function(pointer_to, data, use_frames)
buffer.stream_lock, end,
), buffer.stream_lock,
)
else
a_function(pointer_to, data, use_frames)
end;
warn_xruns = buffer.warn_xruns, warn_xruns = buffer.warn_xruns,
) )
end end
@ -407,25 +424,39 @@ eltype(::Type{Buffer{Sample}}) where {Sample} = Sample
nchannels(buffer::Buffer) = buffer.number_of_channels 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. 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) function write_buffer(buffer::Buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true)
read_or_write(Pa_WriteStream, buffer, use_frames) read_or_write(Pa_WriteStream, buffer, use_frames; acquire_lock = acquire_lock)
end 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). 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) function read_buffer!(buffer, use_frames = buffer.frames_per_buffer; acquire_lock = true)
read_or_write(Pa_ReadStream, buffer, use_frames) read_or_write(Pa_ReadStream, buffer, use_frames; acquire_lock = acquire_lock)
end 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} struct Messenger{Sample, Scribe, Input, Output}
device_name::String device_name::String
buffer::Buffer{Sample} buffer::Buffer{Sample}
@ -497,7 +528,7 @@ function messenger_task(
messenger, task messenger, task
end end
function fetch_messanger(messenger, task) function fetch_messenger(messenger, task)
if has_channels(messenger) if has_channels(messenger)
# this will shut down the channels, which will shut down the thread # this will shut down the channels, which will shut down the thread
close(messenger.input_channel) close(messenger.input_channel)
@ -517,9 +548,9 @@ struct PortAudioStream{SinkMessenger, SourceMessenger}
sample_rate::Float64 sample_rate::Float64
# pointer to the c object # pointer to the c object
pointer_to::Ptr{PaStream} pointer_to::Ptr{PaStream}
sink_messanger::SinkMessenger sink_messenger::SinkMessenger
sink_task::Task sink_task::Task
source_messanger::SourceMessenger source_messenger::SourceMessenger
source_task::Task source_task::Task
end end
@ -584,10 +615,9 @@ function combine_default_sample_rates(
) )
if input_sample_rate != output_sample_rate if input_sample_rate != output_sample_rate
throw( throw(
ArgumentError( ArgumentError("""
""" Default sample rate $input_sample_rate for input \"$(name(input_device))\" disagrees with
Default sample rate $input_sample_rate for input $(name(input_device)) disagrees with default sample rate $output_sample_rate for output \"$(name(output_device))\".
default sample rate $output_sample_rate for output $(name(output_device)).
Please specify a sample rate. Please specify a sample rate.
""", """,
), ),
@ -874,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 # 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 # but we have don't know exactly what the tasks are doing
# for now, just close one and then the other # for now, just close one and then the other
fetch_messanger(stream.source_messanger, stream.source_task) fetch_messenger(stream.source_messenger, stream.source_task)
fetch_messanger(stream.sink_messanger, stream.sink_task) fetch_messenger(stream.sink_messenger, stream.sink_task)
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
if !Bool(handle_status(Pa_IsStreamStopped(pointer_to))) if !Bool(handle_status(Pa_IsStreamStopped(pointer_to)))
@ -897,13 +927,14 @@ end
isopen(stream::PortAudioStream) = isopen(stream.pointer_to) isopen(stream::PortAudioStream) = isopen(stream.pointer_to)
samplerate(stream::PortAudioStream) = stream.sample_rate samplerate(stream::PortAudioStream) = stream.sample_rate
function eltype( function eltype(
::Type{<:PortAudioStream{<:Messenger{Sample}, <:Messenger{Sample}}}, ::Type{<:PortAudioStream{<:Messenger{Sample}, <:Messenger{Sample}}},
) where {Sample} ) where {Sample}
Sample Sample
end 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 # 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...)
read!(stream::PortAudioStream, arguments...) = read!(stream.source, arguments...) read!(stream::PortAudioStream, arguments...) = read!(stream.source, arguments...)
@ -917,7 +948,7 @@ function show(io::IO, stream::PortAudioStream)
print(io, "PortAudioStream{") print(io, "PortAudioStream{")
print(io, eltype(stream)) print(io, eltype(stream))
println(io, "}") 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 # show source or sink if there's any channels
sink = stream.sink sink = stream.sink
if has_channels(sink) if has_channels(sink)
@ -963,10 +994,10 @@ function getproperty(
end end
function nchannels(source_or_sink::PortAudioSource) function nchannels(source_or_sink::PortAudioSource)
nchannels(source_or_sink.stream.source_messanger) nchannels(source_or_sink.stream.source_messenger)
end end
function nchannels(source_or_sink::PortAudioSink) function nchannels(source_or_sink::PortAudioSink)
nchannels(source_or_sink.stream.sink_messanger) nchannels(source_or_sink.stream.sink_messenger)
end end
function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource}) function samplerate(source_or_sink::Union{PortAudioSink, PortAudioSource})
samplerate(source_or_sink.stream) samplerate(source_or_sink.stream)
@ -984,8 +1015,8 @@ end
function isopen(source_or_sink::Union{PortAudioSink, PortAudioSource}) function isopen(source_or_sink::Union{PortAudioSink, PortAudioSource})
isopen(source_or_sink.stream) isopen(source_or_sink.stream)
end end
name(source_or_sink::PortAudioSink) = name(source_or_sink.stream.sink_messanger) name(source_or_sink::PortAudioSink) = name(source_or_sink.stream.sink_messenger)
name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messanger) name(source_or_sink::PortAudioSource) = name(source_or_sink.stream.source_messenger)
# could show full type name, but the PortAudio part is probably redundant # 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 # because these will usually only get printed as part of show for PortAudioStream
@ -1014,14 +1045,14 @@ end
as_matrix(matrix::Matrix) = matrix as_matrix(matrix::Matrix) = matrix
as_matrix(vector::Vector) = reshape(vector, length(vector), 1) 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( function unsafe_write(
sink::PortAudioSink{<:Messenger{<:Any, <:SampledSignalsWriter}}, sink::PortAudioSink{<:Messenger{<:Any, <:SampledSignalsWriter}},
julia_buffer::Array, julia_buffer::Array,
already, already,
frame_count, 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 end
function unsafe_read!( function unsafe_read!(
@ -1030,7 +1061,9 @@ function unsafe_read!(
already, already,
frame_count, 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 end
include("precompile.jl")
end # module PortAudio 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, get_output_type,
handle_status, handle_status,
initialize, initialize,
name,
PortAudioException, PortAudioException,
PortAudio, PortAudio,
PortAudioDevice, PortAudioDevice,
@ -18,7 +19,7 @@ using PortAudio:
safe_load, safe_load,
seek_alsa_conf, seek_alsa_conf,
terminate, terminate,
name write_buffer
using PortAudio.LibPortAudio: using PortAudio.LibPortAudio:
Pa_AbortStream, Pa_AbortStream,
PaError, PaError,
@ -109,7 +110,9 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
initialize() initialize()
end end
if !isempty(devices()) if isempty(devices())
@test_throws ArgumentError("No input device available") get_default_input_index()
else
@testset "Tests with sound" begin @testset "Tests with sound" begin
# these default values are specific to local machines # these default values are specific to local machines
input_name = get_device(get_default_input_index()).name input_name = get_device(get_default_input_index()).name
@ -130,15 +133,16 @@ if !isempty(devices())
sleep(1) sleep(1)
println("Testing pass-through") println("Testing pass-through")
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true) stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
write_buffer(stream.sink_messenger.buffer, acquire_lock = false)
sink = stream.sink sink = stream.sink
source = stream.source source = stream.source
@test sprint(show, stream) == """ @test sprint(show, stream) == """
PortAudioStream{Float32} PortAudioStream{Float32}
Samplerate: 44100.0Hz Samplerate: 44100Hz
2 channel sink: $(repr(input_name)) 2 channel sink: $(repr(output_name))
2 channel source: $(repr(output_name))""" 2 channel source: $(repr(input_name))"""
@test sprint(show, source) == "2 channel source: $(repr(output_name))" @test sprint(show, source) == "2 channel source: $(repr(input_name))"
@test sprint(show, sink) == "2 channel sink: $(repr(input_name))" @test sprint(show, sink) == "2 channel sink: $(repr(output_name))"
write(stream, stream, 5s) write(stream, stream, 5s)
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError @test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
@test isopen(stream) @test isopen(stream)
@ -209,8 +213,8 @@ if !isempty(devices())
big = typemax(Int) big = typemax(Int)
@test_throws DomainError( @test_throws DomainError(
typemax(Int), typemax(Int),
"$big exceeds maximum input channels for $output_name", "$big exceeds maximum output channels for $output_name",
) PortAudioStream(input_name, output_name, big, 0) ) PortAudioStream(input_name, output_name, 0, big)
@test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream( @test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream(
input_name, input_name,
output_name, output_name,
@ -219,8 +223,8 @@ if !isempty(devices())
adjust_channels = true, adjust_channels = true,
) )
@test_throws ArgumentError(""" @test_throws ArgumentError("""
Default sample rate 0 for input $output_name disagrees with Default sample rate 0 for input \"$input_name\" disagrees with
default sample rate 1 for output $input_name. default sample rate 1 for output \"$output_name\".
Please specify a sample rate. Please specify a sample rate.
""") combine_default_sample_rates( """) combine_default_sample_rates(
get_device(input_name), get_device(input_name),