use CLANG wrappers
This commit is contained in:
parent
619c79c489
commit
4f62de7fda
5 changed files with 584 additions and 332 deletions
16
gen/generator.jl
Normal file
16
gen/generator.jl
Normal file
|
@ -0,0 +1,16 @@
|
|||
using Clang.Generators
|
||||
using libportaudio_jll
|
||||
|
||||
cd(@__DIR__)
|
||||
|
||||
include_dir = joinpath(libportaudio_jll.artifact_dir, "include") |> normpath
|
||||
portaudio_h = joinpath(include_dir, "portaudio.h")
|
||||
|
||||
options = load_options(joinpath(@__DIR__, "generator.toml"))
|
||||
|
||||
args = get_default_args()
|
||||
push!(args, "-I$include_dir")
|
||||
|
||||
ctx = create_context(portaudio_h, args, options)
|
||||
|
||||
build!(ctx)
|
9
gen/generator.toml
Normal file
9
gen/generator.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[general]
|
||||
library_name = "libportaudio"
|
||||
output_file_path = "../src/LibPortAudio.jl"
|
||||
module_name = "LibPortAudio"
|
||||
jll_pkg_name = "libportaudio_jll"
|
||||
export_symbol_prefixes = ["Pa", "pa"]
|
||||
|
||||
use_julia_native_enum_type = true
|
||||
auto_mutability = true
|
217
src/PortAudio.jl
217
src/PortAudio.jl
|
@ -14,6 +14,92 @@ using LinearAlgebra: transpose!
|
|||
export PortAudioStream
|
||||
|
||||
include("libportaudio.jl")
|
||||
using .LibPortAudio:
|
||||
PaSampleFormat,
|
||||
Pa_Initialize,
|
||||
Pa_Terminate,
|
||||
Pa_GetVersion,
|
||||
PaDeviceIndex,
|
||||
PaDeviceInfo,
|
||||
Pa_GetVersionText,
|
||||
PaHostApiTypeId,
|
||||
Pa_GetHostApiInfo,
|
||||
PaStream,
|
||||
Pa_GetDeviceCount,
|
||||
Pa_GetDeviceInfo,
|
||||
Pa_GetDefaultInputDevice,
|
||||
Pa_GetDefaultOutputDevice,
|
||||
Pa_OpenStream,
|
||||
Pa_StartStream,
|
||||
Pa_StopStream,
|
||||
Pa_CloseStream,
|
||||
Pa_GetStreamReadAvailable,
|
||||
Pa_GetStreamWriteAvailable,
|
||||
Pa_ReadStream,
|
||||
Pa_WriteStream,
|
||||
Pa_GetErrorText,
|
||||
paOutputUnderflowed,
|
||||
paInputOverflowed,
|
||||
paNoFlag,
|
||||
PaStreamParameters,
|
||||
PaErrorCode
|
||||
|
||||
function safe_load(result, an_error)
|
||||
if result == C_NULL
|
||||
throw(an_error)
|
||||
end
|
||||
unsafe_load(result)
|
||||
end
|
||||
|
||||
"""
|
||||
Call the given expression in a separate thread, waiting on the result. This is
|
||||
useful when running code that would otherwise block the Julia process (like a
|
||||
`ccall` into a function that does IO).
|
||||
"""
|
||||
macro tcall(ex)
|
||||
:(fetch(Base.Threads.@spawn $(esc(ex))))
|
||||
end
|
||||
|
||||
# because we're calling Pa_ReadStream and PA_WriteStream from separate threads,
|
||||
# we put a mutex around libportaudio calls
|
||||
const pamutex = ReentrantLock()
|
||||
|
||||
macro locked(ex)
|
||||
quote
|
||||
lock(pamutex) do
|
||||
$(esc(ex))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
convert_nothing(::Nothing) = C_NULL
|
||||
convert_nothing(something) = something
|
||||
|
||||
function is_xrun(error_code)
|
||||
error_code == paOutputUnderflowed || error_code == paInputOverflowed
|
||||
end
|
||||
|
||||
function is_xrun(number::Integer)
|
||||
is_xrun(PaErrorCode(number))
|
||||
end
|
||||
|
||||
function get_error_text(error_code)
|
||||
unsafe_string(@locked Pa_GetErrorText(error_code))
|
||||
end
|
||||
|
||||
# General utility function to handle the status from the Pa_* functions
|
||||
function handle_status(err; warn_xruns::Bool = true)
|
||||
if Int(err) < 0
|
||||
if is_xrun(err)
|
||||
if warn_xruns
|
||||
@warn("libportaudio: " * get_error_text(err))
|
||||
end
|
||||
else
|
||||
throw(ErrorException("libportaudio: " * get_error_text(err)))
|
||||
end
|
||||
end
|
||||
err
|
||||
end
|
||||
|
||||
macro stderr_as_debug(expression)
|
||||
quote
|
||||
|
@ -31,8 +117,8 @@ end
|
|||
const CHUNKFRAMES = 128
|
||||
|
||||
function versioninfo(io::IO = stdout)
|
||||
println(io, Pa_GetVersionText())
|
||||
println(io, "Version: ", Pa_GetVersion())
|
||||
println(io, unsafe_string(@locked Pa_GetVersionText()))
|
||||
println(io, "Version: ", @locked Pa_GetVersion())
|
||||
end
|
||||
|
||||
struct Bounds
|
||||
|
@ -53,26 +139,36 @@ end
|
|||
function PortAudioDevice(info::PaDeviceInfo, idx)
|
||||
PortAudioDevice(
|
||||
unsafe_string(info.name),
|
||||
unsafe_string(Pa_GetHostApiInfo(info.host_api).name),
|
||||
info.default_sample_rate,
|
||||
unsafe_string(safe_load(
|
||||
(@locked Pa_GetHostApiInfo(info.hostApi)),
|
||||
BoundsError(Pa_GetHostApiInfo, idx),
|
||||
).name),
|
||||
info.defaultSampleRate,
|
||||
idx,
|
||||
Bounds(
|
||||
info.max_input_channels,
|
||||
info.default_low_input_latency,
|
||||
info.default_high_input_latency,
|
||||
info.maxInputChannels,
|
||||
info.defaultLowInputLatency,
|
||||
info.defaultHighInputLatency,
|
||||
),
|
||||
Bounds(
|
||||
info.max_output_channels,
|
||||
info.default_low_output_latency,
|
||||
info.default_high_output_latency,
|
||||
info.maxOutputChannels,
|
||||
info.defaultLowOutputLatency,
|
||||
info.defaultHighInputLatency,
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
name(device::PortAudioDevice) = device.name
|
||||
|
||||
function get_device_info(i)
|
||||
safe_load(
|
||||
(@locked Pa_GetDeviceInfo(i)),
|
||||
BoundsError(Pa_GetDeviceInfo, i),
|
||||
)
|
||||
end
|
||||
|
||||
function devices()
|
||||
[PortAudioDevice(Pa_GetDeviceInfo(i), i) for i in 0:(Pa_GetDeviceCount() - 1)]
|
||||
[PortAudioDevice(get_device_info(i), i) for i in 0:(handle_status(@locked Pa_GetDeviceCount()) - 1)]
|
||||
end
|
||||
|
||||
struct Buffer{T}
|
||||
|
@ -88,19 +184,28 @@ end
|
|||
struct PortAudioStream{T}
|
||||
samplerate::Float64
|
||||
latency::Float64
|
||||
pointer_ref::Ref{PaStream}
|
||||
pointer_ref::Ref{Ptr{PaStream}}
|
||||
warn_xruns::Bool
|
||||
recover_xruns::Bool
|
||||
sink_buffer::Buffer{T}
|
||||
source_buffer::Buffer{T}
|
||||
end
|
||||
|
||||
const type_to_fmt = Dict{Type, PaSampleFormat}(
|
||||
Float32 => 1,
|
||||
Int32 => 2,
|
||||
# Int24 => 4,
|
||||
Int16 => 8,
|
||||
Int8 => 16,
|
||||
UInt8 => 3,
|
||||
)
|
||||
|
||||
function make_parameters(device, channels, T, latency, host_api_specific_stream_info)
|
||||
if channels == 0
|
||||
Ptr{Pa_StreamParameters}(0)
|
||||
Ptr{PaStreamParameters}(0)
|
||||
else
|
||||
Ref(
|
||||
Pa_StreamParameters(
|
||||
PaStreamParameters(
|
||||
device.idx,
|
||||
channels,
|
||||
type_to_fmt[T],
|
||||
|
@ -205,16 +310,20 @@ function PortAudioStream(
|
|||
)
|
||||
inchans = fill_max_channels(inchans, indev.input_bounds)
|
||||
outchans = fill_max_channels(outchans, outdev.output_bounds)
|
||||
pointer_ref = @stderr_as_debug Pa_OpenStream(
|
||||
make_parameters(indev, inchans, eltype, latency, input_info),
|
||||
make_parameters(outdev, outchans, eltype, latency, output_info),
|
||||
samplerate,
|
||||
frames_per_buffer,
|
||||
flags,
|
||||
callback,
|
||||
user_data,
|
||||
pointer_ref = streamPtr = Ref{Ptr{PaStream}}(0)
|
||||
handle_status(
|
||||
@locked @stderr_as_debug Pa_OpenStream(
|
||||
streamPtr,
|
||||
make_parameters(indev, inchans, eltype, latency, input_info),
|
||||
make_parameters(outdev, outchans, eltype, latency, output_info),
|
||||
float(samplerate),
|
||||
frames_per_buffer,
|
||||
flags,
|
||||
convert_nothing(callback),
|
||||
convert_nothing(user_data),
|
||||
)
|
||||
)
|
||||
Pa_StartStream(pointer_ref[])
|
||||
handle_status(@locked Pa_StartStream(pointer_ref[]))
|
||||
this = PortAudioStream{eltype}(
|
||||
samplerate,
|
||||
latency,
|
||||
|
@ -273,11 +382,11 @@ end
|
|||
|
||||
# use the default input and output devices
|
||||
function PortAudioStream(inchans = 2, outchans = 2; kwargs...)
|
||||
inidx = Pa_GetDefaultInputDevice()
|
||||
outidx = Pa_GetDefaultOutputDevice()
|
||||
inidx = handle_status(@locked Pa_GetDefaultInputDevice())
|
||||
outidx = handle_status(@locked Pa_GetDefaultOutputDevice())
|
||||
PortAudioStream(
|
||||
PortAudioDevice(Pa_GetDeviceInfo(inidx), inidx),
|
||||
PortAudioDevice(Pa_GetDeviceInfo(outidx), outidx),
|
||||
PortAudioDevice(get_device_info(inidx), inidx),
|
||||
PortAudioDevice(get_device_info(outidx), outidx),
|
||||
inchans,
|
||||
outchans;
|
||||
kwargs...,
|
||||
|
@ -298,8 +407,8 @@ function close(stream::PortAudioStream)
|
|||
pointer_ref = stream.pointer_ref
|
||||
pointer = pointer_ref[]
|
||||
if pointer != C_NULL
|
||||
Pa_StopStream(pointer)
|
||||
Pa_CloseStream(pointer)
|
||||
handle_status(@locked Pa_StopStream(pointer))
|
||||
handle_status(@locked Pa_CloseStream(pointer))
|
||||
pointer_ref[] = C_NULL
|
||||
end
|
||||
nothing
|
||||
|
@ -402,12 +511,24 @@ function interleave!(long, wide, n, already, offset, wide_to_long)
|
|||
end
|
||||
|
||||
function handle_xrun(stream, error_code, recover_xruns)
|
||||
if recover_xruns &&
|
||||
(error_code == PA_OUTPUT_UNDERFLOWED || error_code == PA_INPUT_OVERFLOWED)
|
||||
if recover_xruns && is_xrun(error_code)
|
||||
recover_xrun(stream)
|
||||
end
|
||||
end
|
||||
|
||||
function write_stream(stream::Ptr{PaStream}, buf::Array, frames::Integer; warn_xruns = true)
|
||||
handle_status(
|
||||
disable_sigint() do
|
||||
@tcall @locked Pa_WriteStream(
|
||||
stream,
|
||||
buf,
|
||||
frames,
|
||||
)
|
||||
end,
|
||||
warn_xruns = warn_xruns,
|
||||
)
|
||||
end
|
||||
|
||||
function SampledSignals.unsafe_write(
|
||||
sink::PortAudioSink,
|
||||
buf::Array,
|
||||
|
@ -428,7 +549,7 @@ function SampledSignals.unsafe_write(
|
|||
# shorter-than-requested frame count instead of throwing an error
|
||||
handle_xrun(
|
||||
stream,
|
||||
Pa_WriteStream(pointer, chunkbuf, n, warn_xruns = warn_xruns),
|
||||
write_stream(pointer, chunkbuf, n, warn_xruns = warn_xruns),
|
||||
recover_xruns,
|
||||
)
|
||||
nwritten += n
|
||||
|
@ -437,6 +558,24 @@ function SampledSignals.unsafe_write(
|
|||
nwritten
|
||||
end
|
||||
|
||||
function read_stream(stream::Ptr{PaStream}, buf::Array, frames::Integer; warn_xruns = true)
|
||||
# without disable_sigint I get a segfault with the error:
|
||||
# "error thrown and no exception handler available."
|
||||
# if the user tries to ctrl-C. Note I've still had some crash problems with
|
||||
# ctrl-C within `pasuspend`, so for now I think either don't use `pasuspend` or
|
||||
# don't use ctrl-C.
|
||||
handle_status(
|
||||
disable_sigint() do
|
||||
@tcall @locked Pa_ReadStream(
|
||||
stream,
|
||||
buf,
|
||||
frames
|
||||
)
|
||||
end,
|
||||
warn_xruns = warn_xruns,
|
||||
)
|
||||
end
|
||||
|
||||
function SampledSignals.unsafe_read!(
|
||||
source::PortAudioSource,
|
||||
buf::Array,
|
||||
|
@ -455,7 +594,7 @@ function SampledSignals.unsafe_read!(
|
|||
# shorter-than-requested frame count instead of throwing an error
|
||||
handle_xrun(
|
||||
stream,
|
||||
Pa_ReadStream(pointer, chunkbuf, n, warn_xruns = warn_xruns),
|
||||
read_stream(pointer, chunkbuf, n, warn_xruns = warn_xruns),
|
||||
recover_xruns,
|
||||
)
|
||||
# de-interleave the samples
|
||||
|
@ -476,11 +615,11 @@ function prefill_output(sink::PortAudioSink)
|
|||
stream = sink.stream
|
||||
pointer = stream.pointer_ref[]
|
||||
chunkbuf = stream.sink_buffer.chunkbuf
|
||||
towrite = Pa_GetStreamWriteAvailable(pointer)
|
||||
towrite = handle_status(@locked Pa_GetStreamWriteAvailable(pointer))
|
||||
while towrite > 0
|
||||
n = min(towrite, CHUNKFRAMES)
|
||||
fill!(chunkbuf, zero(eltype(chunkbuf)))
|
||||
Pa_WriteStream(pointer, chunkbuf, n, warn_xruns = false)
|
||||
write_stream(pointer, chunkbuf, n, warn_xruns = false)
|
||||
towrite -= n
|
||||
end
|
||||
end
|
||||
|
@ -495,10 +634,10 @@ function discard_input(source::PortAudioSource)
|
|||
stream = source.stream
|
||||
pointer = stream.pointer_ref[]
|
||||
chunkbuf = stream.source_buffer.chunkbuf
|
||||
toread = Pa_GetStreamReadAvailable(pointer)
|
||||
toread = handle_status(@locked Pa_GetStreamReadAvailable(pointer))
|
||||
while toread > 0
|
||||
n = min(toread, CHUNKFRAMES)
|
||||
Pa_ReadStream(pointer, chunkbuf, n, warn_xruns = false)
|
||||
read_stream(pointer, chunkbuf, n, warn_xruns = false)
|
||||
toread -= n
|
||||
end
|
||||
end
|
||||
|
@ -543,10 +682,10 @@ function __init__()
|
|||
# junk to STDOUT on initialization, so we swallow it.
|
||||
# TODO: actually check the junk to make sure there's nothing in there we
|
||||
# don't expect
|
||||
@stderr_as_debug Pa_Initialize()
|
||||
@stderr_as_debug handle_status(@locked Pa_Initialize())
|
||||
|
||||
atexit() do
|
||||
Pa_Terminate()
|
||||
handle_status(@locked Pa_Terminate())
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,190 +1,328 @@
|
|||
# Low-level wrappers for Portaudio calls
|
||||
module LibPortAudio
|
||||
|
||||
using libportaudio_jll
|
||||
export libportaudio_jll
|
||||
|
||||
function Pa_GetVersion()
|
||||
ccall((:Pa_GetVersion, libportaudio), Cint, ())
|
||||
end
|
||||
|
||||
function Pa_GetVersionText()
|
||||
ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
|
||||
end
|
||||
|
||||
mutable struct PaVersionInfo
|
||||
versionMajor::Cint
|
||||
versionMinor::Cint
|
||||
versionSubMinor::Cint
|
||||
versionControlRevision::Ptr{Cchar}
|
||||
versionText::Ptr{Cchar}
|
||||
end
|
||||
|
||||
# no prototype is found for this function at portaudio.h:114:22, please use with caution
|
||||
function Pa_GetVersionInfo()
|
||||
ccall((:Pa_GetVersionInfo, libportaudio), Ptr{PaVersionInfo}, ())
|
||||
end
|
||||
|
||||
# General type aliases
|
||||
const PaTime = Cdouble
|
||||
const PaError = Cint
|
||||
const PaSampleFormat = Culong
|
||||
const PaDeviceIndex = Cint
|
||||
const PaHostApiIndex = Cint
|
||||
const PaHostApiTypeId = Cint
|
||||
# PaStream is always used as an opaque type, so we're always dealing
|
||||
# with the pointer
|
||||
const PaStream = Ptr{Cvoid}
|
||||
const PaStreamCallback = Cvoid
|
||||
const PaStreamFlags = Culong
|
||||
|
||||
const paNoFlag = PaStreamFlags(0x00)
|
||||
|
||||
const PA_NO_ERROR = 0
|
||||
const PA_INPUT_OVERFLOWED = -10000 + 19
|
||||
const PA_OUTPUT_UNDERFLOWED = -10000 + 20
|
||||
|
||||
# sample format types
|
||||
const paFloat32 = PaSampleFormat(0x01)
|
||||
const paInt32 = PaSampleFormat(0x02)
|
||||
const paInt24 = PaSampleFormat(0x04)
|
||||
const paInt16 = PaSampleFormat(0x08)
|
||||
const paInt8 = PaSampleFormat(0x10)
|
||||
const paUInt8 = PaSampleFormat(0x20)
|
||||
const paNonInterleaved = PaSampleFormat(0x80000000)
|
||||
|
||||
const type_to_fmt = Dict{Type, PaSampleFormat}(
|
||||
Float32 => 1,
|
||||
Int32 => 2,
|
||||
# Int24 => 4,
|
||||
Int16 => 8,
|
||||
Int8 => 16,
|
||||
UInt8 => 3,
|
||||
)
|
||||
|
||||
const PaStreamCallbackResult = Cint
|
||||
# Callback return values
|
||||
const paContinue = PaStreamCallbackResult(0)
|
||||
const paComplete = PaStreamCallbackResult(1)
|
||||
const paAbort = PaStreamCallbackResult(2)
|
||||
|
||||
function safe_load(result, an_error)
|
||||
if result == C_NULL
|
||||
throw(an_error)
|
||||
end
|
||||
unsafe_load(result)
|
||||
@enum PaErrorCode::Int32 begin
|
||||
paNoError = 0
|
||||
paNotInitialized = -10000
|
||||
paUnanticipatedHostError = -9999
|
||||
paInvalidChannelCount = -9998
|
||||
paInvalidSampleRate = -9997
|
||||
paInvalidDevice = -9996
|
||||
paInvalidFlag = -9995
|
||||
paSampleFormatNotSupported = -9994
|
||||
paBadIODeviceCombination = -9993
|
||||
paInsufficientMemory = -9992
|
||||
paBufferTooBig = -9991
|
||||
paBufferTooSmall = -9990
|
||||
paNullCallback = -9989
|
||||
paBadStreamPtr = -9988
|
||||
paTimedOut = -9987
|
||||
paInternalError = -9986
|
||||
paDeviceUnavailable = -9985
|
||||
paIncompatibleHostApiSpecificStreamInfo = -9984
|
||||
paStreamIsStopped = -9983
|
||||
paStreamIsNotStopped = -9982
|
||||
paInputOverflowed = -9981
|
||||
paOutputUnderflowed = -9980
|
||||
paHostApiNotFound = -9979
|
||||
paInvalidHostApi = -9978
|
||||
paCanNotReadFromACallbackStream = -9977
|
||||
paCanNotWriteToACallbackStream = -9976
|
||||
paCanNotReadFromAnOutputOnlyStream = -9975
|
||||
paCanNotWriteToAnInputOnlyStream = -9974
|
||||
paIncompatibleStreamHostApi = -9973
|
||||
paBadBufferPtr = -9972
|
||||
end
|
||||
|
||||
"""
|
||||
Call the given expression in a separate thread, waiting on the result. This is
|
||||
useful when running code that would otherwise block the Julia process (like a
|
||||
`ccall` into a function that does IO).
|
||||
"""
|
||||
macro tcall(ex)
|
||||
:(fetch(Base.Threads.@spawn $(esc(ex))))
|
||||
end
|
||||
|
||||
# because we're calling Pa_ReadStream and PA_WriteStream from separate threads,
|
||||
# we put a mutex around libportaudio calls
|
||||
const pamutex = ReentrantLock()
|
||||
|
||||
macro locked(ex)
|
||||
quote
|
||||
lock(pamutex) do
|
||||
$(esc(ex))
|
||||
end
|
||||
end
|
||||
function Pa_GetErrorText(errorCode)
|
||||
ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), errorCode)
|
||||
end
|
||||
|
||||
function Pa_Initialize()
|
||||
handle_status(@locked ccall((:Pa_Initialize, libportaudio), PaError, ()))
|
||||
nothing
|
||||
ccall((:Pa_Initialize, libportaudio), PaError, ())
|
||||
end
|
||||
|
||||
function Pa_Terminate()
|
||||
handle_status(@locked ccall((:Pa_Terminate, libportaudio), PaError, ()))
|
||||
nothing
|
||||
ccall((:Pa_Terminate, libportaudio), PaError, ())
|
||||
end
|
||||
|
||||
Pa_GetVersion() = @locked ccall((:Pa_GetVersion, libportaudio), Cint, ())
|
||||
const PaDeviceIndex = Cint
|
||||
|
||||
function Pa_GetVersionText()
|
||||
unsafe_string(@locked ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ()))
|
||||
const PaHostApiIndex = Cint
|
||||
|
||||
function Pa_GetHostApiCount()
|
||||
ccall((:Pa_GetHostApiCount, libportaudio), PaHostApiIndex, ())
|
||||
end
|
||||
|
||||
# Host API Functions
|
||||
function Pa_GetDefaultHostApi()
|
||||
ccall((:Pa_GetDefaultHostApi, libportaudio), PaHostApiIndex, ())
|
||||
end
|
||||
|
||||
# A Host API is the top-level of the PortAudio hierarchy. Each host API has a
|
||||
# unique type ID that tells you which native backend it is (JACK, ALSA, ASIO,
|
||||
# etc.). On a given system you can identify each backend by its index, which
|
||||
# will range between 0 and Pa_GetHostApiCount() - 1. You can enumerate through
|
||||
# all the host APIs on the system by iterating through those values.
|
||||
|
||||
# PaHostApiTypeId values
|
||||
const pa_host_api_names = Dict{PaHostApiTypeId, String}(
|
||||
0 => "In Development", # use while developing support for a new host API
|
||||
1 => "Direct Sound",
|
||||
2 => "MME",
|
||||
3 => "ASIO",
|
||||
4 => "Sound Manager",
|
||||
5 => "Core Audio",
|
||||
7 => "OSS",
|
||||
8 => "ALSA",
|
||||
9 => "AL",
|
||||
10 => "BeOS",
|
||||
11 => "WDMKS",
|
||||
12 => "Jack",
|
||||
13 => "WASAPI",
|
||||
14 => "AudioScience HPI",
|
||||
)
|
||||
@enum PaHostApiTypeId::UInt32 begin
|
||||
paInDevelopment = 0
|
||||
paDirectSound = 1
|
||||
paMME = 2
|
||||
paASIO = 3
|
||||
paSoundManager = 4
|
||||
paCoreAudio = 5
|
||||
paOSS = 7
|
||||
paALSA = 8
|
||||
paAL = 9
|
||||
paBeOS = 10
|
||||
paWDMKS = 11
|
||||
paJACK = 12
|
||||
paWASAPI = 13
|
||||
paAudioScienceHPI = 14
|
||||
end
|
||||
|
||||
mutable struct PaHostApiInfo
|
||||
struct_version::Cint
|
||||
api_type::PaHostApiTypeId
|
||||
structVersion::Cint
|
||||
type::PaHostApiTypeId
|
||||
name::Ptr{Cchar}
|
||||
deviceCount::Cint
|
||||
defaultInputDevice::PaDeviceIndex
|
||||
defaultOutputDevice::PaDeviceIndex
|
||||
end
|
||||
|
||||
function Pa_GetHostApiInfo(i)
|
||||
safe_load(
|
||||
(@locked ccall(
|
||||
(:Pa_GetHostApiInfo, libportaudio),
|
||||
Ptr{PaHostApiInfo},
|
||||
(PaHostApiIndex,),
|
||||
i,
|
||||
)),
|
||||
BoundsError(Pa_GetHostApiInfo, i),
|
||||
function Pa_GetHostApiInfo(hostApi)
|
||||
ccall(
|
||||
(:Pa_GetHostApiInfo, libportaudio),
|
||||
Ptr{PaHostApiInfo},
|
||||
(PaHostApiIndex,),
|
||||
hostApi,
|
||||
)
|
||||
end
|
||||
|
||||
# Device Functions
|
||||
function Pa_HostApiTypeIdToHostApiIndex(type)
|
||||
ccall(
|
||||
(:Pa_HostApiTypeIdToHostApiIndex, libportaudio),
|
||||
PaHostApiIndex,
|
||||
(PaHostApiTypeId,),
|
||||
type,
|
||||
)
|
||||
end
|
||||
|
||||
mutable struct PaDeviceInfo
|
||||
struct_version::Cint
|
||||
name::Ptr{Cchar}
|
||||
host_api::PaHostApiIndex
|
||||
max_input_channels::Cint
|
||||
max_output_channels::Cint
|
||||
default_low_input_latency::PaTime
|
||||
default_low_output_latency::PaTime
|
||||
default_high_input_latency::PaTime
|
||||
default_high_output_latency::PaTime
|
||||
default_sample_rate::Cdouble
|
||||
function Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex)
|
||||
ccall(
|
||||
(:Pa_HostApiDeviceIndexToDeviceIndex, libportaudio),
|
||||
PaDeviceIndex,
|
||||
(PaHostApiIndex, Cint),
|
||||
hostApi,
|
||||
hostApiDeviceIndex,
|
||||
)
|
||||
end
|
||||
|
||||
mutable struct PaHostErrorInfo
|
||||
hostApiType::PaHostApiTypeId
|
||||
errorCode::Clong
|
||||
errorText::Ptr{Cchar}
|
||||
end
|
||||
|
||||
function Pa_GetLastHostErrorInfo()
|
||||
ccall((:Pa_GetLastHostErrorInfo, libportaudio), Ptr{PaHostErrorInfo}, ())
|
||||
end
|
||||
|
||||
function Pa_GetDeviceCount()
|
||||
handle_status(@locked ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ()))
|
||||
end
|
||||
|
||||
function Pa_GetDeviceInfo(i)
|
||||
safe_load(
|
||||
(@locked ccall(
|
||||
(:Pa_GetDeviceInfo, libportaudio),
|
||||
Ptr{PaDeviceInfo},
|
||||
(PaDeviceIndex,),
|
||||
i,
|
||||
)),
|
||||
BoundsError(Pa_GetDeviceInfo, i),
|
||||
)
|
||||
ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
|
||||
end
|
||||
|
||||
function Pa_GetDefaultInputDevice()
|
||||
handle_status(
|
||||
@locked ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
|
||||
)
|
||||
ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
|
||||
end
|
||||
|
||||
function Pa_GetDefaultOutputDevice()
|
||||
handle_status(
|
||||
@locked ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ())
|
||||
)
|
||||
ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ())
|
||||
end
|
||||
|
||||
# Stream Functions
|
||||
const PaTime = Cdouble
|
||||
|
||||
mutable struct Pa_StreamParameters
|
||||
const PaSampleFormat = Culong
|
||||
|
||||
mutable struct PaDeviceInfo
|
||||
structVersion::Cint
|
||||
name::Ptr{Cchar}
|
||||
hostApi::PaHostApiIndex
|
||||
maxInputChannels::Cint
|
||||
maxOutputChannels::Cint
|
||||
defaultLowInputLatency::PaTime
|
||||
defaultLowOutputLatency::PaTime
|
||||
defaultHighInputLatency::PaTime
|
||||
defaultHighOutputLatency::PaTime
|
||||
defaultSampleRate::Cdouble
|
||||
end
|
||||
|
||||
function Pa_GetDeviceInfo(device)
|
||||
ccall((:Pa_GetDeviceInfo, libportaudio), Ptr{PaDeviceInfo}, (PaDeviceIndex,), device)
|
||||
end
|
||||
|
||||
struct PaStreamParameters
|
||||
device::PaDeviceIndex
|
||||
channelCount::Cint
|
||||
sampleFormat::PaSampleFormat
|
||||
suggestedLatency::PaTime
|
||||
hostAPISpecificStreamInfo::Ptr{Cvoid}
|
||||
hostApiSpecificStreamInfo::Ptr{Cvoid}
|
||||
end
|
||||
|
||||
function Pa_IsFormatSupported(inputParameters, outputParameters, sampleRate)
|
||||
ccall(
|
||||
(:Pa_IsFormatSupported, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStreamParameters}, Ptr{PaStreamParameters}, Cdouble),
|
||||
inputParameters,
|
||||
outputParameters,
|
||||
sampleRate,
|
||||
)
|
||||
end
|
||||
|
||||
const PaStream = Cvoid
|
||||
|
||||
const PaStreamFlags = Culong
|
||||
|
||||
mutable struct PaStreamCallbackTimeInfo
|
||||
inputBufferAdcTime::PaTime
|
||||
currentTime::PaTime
|
||||
outputBufferDacTime::PaTime
|
||||
end
|
||||
|
||||
const PaStreamCallbackFlags = Culong
|
||||
|
||||
@enum PaStreamCallbackResult::UInt32 begin
|
||||
paContinue = 0
|
||||
paComplete = 1
|
||||
paAbort = 2
|
||||
end
|
||||
|
||||
# typedef int PaStreamCallback ( const void * input , void * output , unsigned long frameCount , const PaStreamCallbackTimeInfo * timeInfo , PaStreamCallbackFlags statusFlags , void * userData )
|
||||
const PaStreamCallback = Cvoid
|
||||
|
||||
function Pa_OpenStream(
|
||||
stream,
|
||||
inputParameters,
|
||||
outputParameters,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamFlags,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
ccall(
|
||||
(:Pa_OpenStream, libportaudio),
|
||||
PaError,
|
||||
(
|
||||
Ptr{Ptr{PaStream}},
|
||||
Ptr{PaStreamParameters},
|
||||
Ptr{PaStreamParameters},
|
||||
Cdouble,
|
||||
Culong,
|
||||
PaStreamFlags,
|
||||
Ptr{Cvoid},
|
||||
Ptr{Cvoid},
|
||||
),
|
||||
stream,
|
||||
inputParameters,
|
||||
outputParameters,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamFlags,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_OpenDefaultStream(
|
||||
stream,
|
||||
numInputChannels,
|
||||
numOutputChannels,
|
||||
sampleFormat,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
ccall(
|
||||
(:Pa_OpenDefaultStream, libportaudio),
|
||||
PaError,
|
||||
(
|
||||
Ptr{Ptr{PaStream}},
|
||||
Cint,
|
||||
Cint,
|
||||
PaSampleFormat,
|
||||
Cdouble,
|
||||
Culong,
|
||||
Ptr{Cvoid},
|
||||
Ptr{Cvoid},
|
||||
),
|
||||
stream,
|
||||
numInputChannels,
|
||||
numOutputChannels,
|
||||
sampleFormat,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_CloseStream(stream)
|
||||
ccall((:Pa_CloseStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
# typedef void PaStreamFinishedCallback ( void * userData )
|
||||
const PaStreamFinishedCallback = Cvoid
|
||||
|
||||
function Pa_SetStreamFinishedCallback(stream, streamFinishedCallback)
|
||||
ccall(
|
||||
(:Pa_SetStreamFinishedCallback, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStream}, Ptr{Cvoid}),
|
||||
stream,
|
||||
streamFinishedCallback,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_StartStream(stream)
|
||||
ccall((:Pa_StartStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_StopStream(stream)
|
||||
ccall((:Pa_StopStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_AbortStream(stream)
|
||||
ccall((:Pa_AbortStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_IsStreamStopped(stream)
|
||||
ccall((:Pa_IsStreamStopped, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_IsStreamActive(stream)
|
||||
ccall((:Pa_IsStreamActive, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
mutable struct PaStreamInfo
|
||||
|
@ -194,158 +332,108 @@ mutable struct PaStreamInfo
|
|||
sampleRate::Cdouble
|
||||
end
|
||||
|
||||
convert_nothing(::Nothing) = C_NULL
|
||||
convert_nothing(something) = something
|
||||
|
||||
# function Pa_OpenDefaultStream(inChannels, outChannels,
|
||||
# sampleFormat::PaSampleFormat,
|
||||
# sampleRate, framesPerBuffer)
|
||||
# streamPtr = Ref{PaStream}(0)
|
||||
# handle_status(ccall((:Pa_OpenDefaultStream, libportaudio),
|
||||
# PaError, (Ref{PaStream}, Cint, Cint,
|
||||
# PaSampleFormat, Cdouble, Culong,
|
||||
# Ref{Cvoid}, Ref{Cvoid}),
|
||||
# streamPtr, inChannels, outChannels, sampleFormat, sampleRate,
|
||||
# framesPerBuffer, C_NULL, C_NULL))
|
||||
# streamPtr[]
|
||||
# end
|
||||
#
|
||||
function Pa_OpenStream(
|
||||
inParams,
|
||||
outParams,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
flags::PaStreamFlags,
|
||||
callback,
|
||||
userdata::UserData,
|
||||
) where {UserData}
|
||||
streamPtr = Ref{PaStream}(0)
|
||||
handle_status(
|
||||
@locked ccall(
|
||||
(:Pa_OpenStream, libportaudio),
|
||||
PaError,
|
||||
(
|
||||
Ref{PaStream},
|
||||
Ref{Pa_StreamParameters},
|
||||
Ref{Pa_StreamParameters},
|
||||
Cdouble,
|
||||
Culong,
|
||||
PaStreamFlags,
|
||||
Ref{Cvoid},
|
||||
Ref{UserData},
|
||||
),
|
||||
streamPtr,
|
||||
inParams,
|
||||
outParams,
|
||||
float(sampleRate),
|
||||
framesPerBuffer,
|
||||
flags,
|
||||
convert_nothing(callback),
|
||||
convert_nothing(userdata),
|
||||
)
|
||||
)
|
||||
streamPtr
|
||||
function Pa_GetStreamInfo(stream)
|
||||
ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo}, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_StartStream(stream::PaStream)
|
||||
handle_status(
|
||||
@locked ccall((:Pa_StartStream, libportaudio), PaError, (PaStream,), stream)
|
||||
)
|
||||
nothing
|
||||
function Pa_GetStreamTime(stream)
|
||||
ccall((:Pa_GetStreamTime, libportaudio), PaTime, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_StopStream(stream::PaStream)
|
||||
handle_status(
|
||||
@locked ccall((:Pa_StopStream, libportaudio), PaError, (PaStream,), stream)
|
||||
)
|
||||
nothing
|
||||
function Pa_GetStreamCpuLoad(stream)
|
||||
ccall((:Pa_GetStreamCpuLoad, libportaudio), Cdouble, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_CloseStream(stream::PaStream)
|
||||
handle_status(
|
||||
@locked ccall((:Pa_CloseStream, libportaudio), PaError, (PaStream,), stream)
|
||||
)
|
||||
nothing
|
||||
end
|
||||
|
||||
function Pa_GetStreamReadAvailable(stream::PaStream)
|
||||
handle_status(
|
||||
@locked ccall(
|
||||
(:Pa_GetStreamReadAvailable, libportaudio),
|
||||
Clong,
|
||||
(PaStream,),
|
||||
stream,
|
||||
)
|
||||
function Pa_ReadStream(stream, buffer, frames)
|
||||
ccall(
|
||||
(:Pa_ReadStream, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||
stream,
|
||||
buffer,
|
||||
frames,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_GetStreamWriteAvailable(stream::PaStream)
|
||||
handle_status(
|
||||
@locked ccall(
|
||||
(:Pa_GetStreamWriteAvailable, libportaudio),
|
||||
Clong,
|
||||
(PaStream,),
|
||||
stream,
|
||||
)
|
||||
function Pa_WriteStream(stream, buffer, frames)
|
||||
ccall(
|
||||
(:Pa_WriteStream, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||
stream,
|
||||
buffer,
|
||||
frames,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer; warn_xruns = true)
|
||||
# without disable_sigint I get a segfault with the error:
|
||||
# "error thrown and no exception handler available."
|
||||
# if the user tries to ctrl-C. Note I've still had some crash problems with
|
||||
# ctrl-C within `pasuspend`, so for now I think either don't use `pasuspend` or
|
||||
# don't use ctrl-C.
|
||||
handle_status(
|
||||
disable_sigint() do
|
||||
@tcall @locked ccall(
|
||||
(:Pa_ReadStream, libportaudio),
|
||||
PaError,
|
||||
(PaStream, Ptr{Cvoid}, Culong),
|
||||
stream,
|
||||
buf,
|
||||
frames,
|
||||
)
|
||||
end,
|
||||
warn_xruns = warn_xruns,
|
||||
)
|
||||
function Pa_GetStreamReadAvailable(stream)
|
||||
ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer; warn_xruns = true)
|
||||
handle_status(
|
||||
disable_sigint() do
|
||||
@tcall @locked ccall(
|
||||
(:Pa_WriteStream, libportaudio),
|
||||
PaError,
|
||||
(PaStream, Ptr{Cvoid}, Culong),
|
||||
stream,
|
||||
buf,
|
||||
frames,
|
||||
)
|
||||
end,
|
||||
warn_xruns = warn_xruns,
|
||||
)
|
||||
function Pa_GetStreamWriteAvailable(stream)
|
||||
ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
# function Pa_GetStreamInfo(stream::PaStream)
|
||||
# safe_load(
|
||||
# ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo},
|
||||
# (PaStream, ), stream),
|
||||
# ArgumentError("Error getting stream info. Is the stream already closed?")
|
||||
# )
|
||||
# end
|
||||
#
|
||||
# General utility function to handle the status from the Pa_* functions
|
||||
function handle_status(err::Integer; warn_xruns::Bool = true)
|
||||
if err < 0
|
||||
msg = @locked ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err)
|
||||
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
|
||||
if warn_xruns
|
||||
@warn("libportaudio: " * unsafe_string(msg))
|
||||
end
|
||||
else
|
||||
throw(ErrorException("libportaudio: " * unsafe_string(msg)))
|
||||
end
|
||||
function Pa_GetSampleSize(format)
|
||||
ccall((:Pa_GetSampleSize, libportaudio), PaError, (PaSampleFormat,), format)
|
||||
end
|
||||
|
||||
function Pa_Sleep(msec)
|
||||
ccall((:Pa_Sleep, libportaudio), Cvoid, (Clong,), msec)
|
||||
end
|
||||
|
||||
const paNoDevice = PaDeviceIndex(-1)
|
||||
|
||||
const paUseHostApiSpecificDeviceSpecification = PaDeviceIndex(-2)
|
||||
|
||||
const paFloat32 = PaSampleFormat(0x00000001)
|
||||
|
||||
const paInt32 = PaSampleFormat(0x00000002)
|
||||
|
||||
const paInt24 = PaSampleFormat(0x00000004)
|
||||
|
||||
const paInt16 = PaSampleFormat(0x00000008)
|
||||
|
||||
const paInt8 = PaSampleFormat(0x00000010)
|
||||
|
||||
const paUInt8 = PaSampleFormat(0x00000020)
|
||||
|
||||
const paCustomFormat = PaSampleFormat(0x00010000)
|
||||
|
||||
const paNonInterleaved = PaSampleFormat(0x80000000)
|
||||
|
||||
const paFormatIsSupported = 0
|
||||
|
||||
const paFramesPerBufferUnspecified = 0
|
||||
|
||||
const paNoFlag = PaStreamFlags(0)
|
||||
|
||||
const paClipOff = PaStreamFlags(0x00000001)
|
||||
|
||||
const paDitherOff = PaStreamFlags(0x00000002)
|
||||
|
||||
const paNeverDropInput = PaStreamFlags(0x00000004)
|
||||
|
||||
const paPrimeOutputBuffersUsingStreamCallback = PaStreamFlags(0x00000008)
|
||||
|
||||
const paPlatformSpecificFlags = PaStreamFlags(0xffff0000)
|
||||
|
||||
const paInputUnderflow = PaStreamCallbackFlags(0x00000001)
|
||||
|
||||
const paInputOverflow = PaStreamCallbackFlags(0x00000002)
|
||||
|
||||
const paOutputUnderflow = PaStreamCallbackFlags(0x00000004)
|
||||
|
||||
const paOutputOverflow = PaStreamCallbackFlags(0x00000008)
|
||||
|
||||
const paPrimingOutput = PaStreamCallbackFlags(0x00000010)
|
||||
|
||||
# exports
|
||||
const PREFIXES = ["Pa", "pa"]
|
||||
for name in names(@__MODULE__; all = true), prefix in PREFIXES
|
||||
if startswith(string(name), prefix)
|
||||
@eval export $name
|
||||
end
|
||||
err
|
||||
end
|
||||
|
||||
end # module
|
||||
|
|
|
@ -4,18 +4,19 @@ using Logging: Debug
|
|||
using PortAudio
|
||||
using PortAudio:
|
||||
combine_default_sample_rates,
|
||||
get_device_info,
|
||||
handle_status,
|
||||
Pa_GetDefaultInputDevice,
|
||||
Pa_GetDefaultOutputDevice,
|
||||
Pa_GetDeviceInfo,
|
||||
Pa_GetHostApiInfo,
|
||||
Pa_Initialize,
|
||||
PA_OUTPUT_UNDERFLOWED,
|
||||
paOutputUnderflowed,
|
||||
Pa_Terminate,
|
||||
PortAudioDevice,
|
||||
recover_xrun,
|
||||
seek_alsa_conf,
|
||||
@stderr_as_debug
|
||||
@stderr_as_debug,
|
||||
@locked
|
||||
using PortAudio.LibPortAudio: paNotInitialized
|
||||
using SampledSignals
|
||||
using Test
|
||||
|
||||
|
@ -40,21 +41,20 @@ end
|
|||
end
|
||||
|
||||
@testset "Null errors" begin
|
||||
@test_throws BoundsError Pa_GetDeviceInfo(-1)
|
||||
@test_throws BoundsError Pa_GetHostApiInfo(-1)
|
||||
@test_throws BoundsError get_device_info(-1)
|
||||
end
|
||||
end
|
||||
|
||||
if !isempty(PortAudio.devices())
|
||||
# make sure we can terminate, then reinitialize
|
||||
Pa_Terminate()
|
||||
@stderr_as_debug Pa_Initialize()
|
||||
handle_status(@locked Pa_Terminate())
|
||||
@stderr_as_debug handle_status(@locked Pa_Initialize())
|
||||
|
||||
# these default values are specific to my machines
|
||||
inidx = Pa_GetDefaultInputDevice()
|
||||
default_indev = PortAudioDevice(Pa_GetDeviceInfo(inidx), inidx).name
|
||||
outidx = Pa_GetDefaultOutputDevice()
|
||||
default_outdev = PortAudioDevice(Pa_GetDeviceInfo(outidx), outidx).name
|
||||
inidx = handle_status(@locked Pa_GetDefaultInputDevice())
|
||||
default_indev = PortAudioDevice(get_device_info(inidx), inidx).name
|
||||
outidx = handle_status(@locked Pa_GetDefaultOutputDevice())
|
||||
default_outdev = PortAudioDevice(get_device_info(outidx), outidx).name
|
||||
|
||||
@testset "Local Tests" begin
|
||||
@testset "Open Default Device" begin
|
||||
|
@ -126,10 +126,10 @@ if !isempty(PortAudio.devices())
|
|||
@test_throws ErrorException PortAudioStream("foobarbaz")
|
||||
@test_throws ErrorException PortAudioStream(default_indev, "foobarbaz")
|
||||
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
|
||||
PA_OUTPUT_UNDERFLOWED,
|
||||
paOutputUnderflowed,
|
||||
)
|
||||
@test_throws ErrorException("libportaudio: PortAudio not initialized") handle_status(
|
||||
-10000,
|
||||
paNotInitialized,
|
||||
)
|
||||
@test_throws ErrorException("""
|
||||
Could not find ALSA config directory. Searched:
|
||||
|
|
Loading…
Add table
Reference in a new issue