From 4f62de7fdacc1fd299cfbb7127c453d48b7b2116 Mon Sep 17 00:00:00 2001 From: Brandon Taylor Date: Thu, 17 Jun 2021 13:49:33 -0400 Subject: [PATCH] use CLANG wrappers --- gen/generator.jl | 16 ++ gen/generator.toml | 9 + src/PortAudio.jl | 217 ++++++++++++--- src/libportaudio.jl | 646 +++++++++++++++++++++++++------------------- test/runtests.jl | 28 +- 5 files changed, 584 insertions(+), 332 deletions(-) create mode 100644 gen/generator.jl create mode 100644 gen/generator.toml diff --git a/gen/generator.jl b/gen/generator.jl new file mode 100644 index 0000000..75f0b75 --- /dev/null +++ b/gen/generator.jl @@ -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) diff --git a/gen/generator.toml b/gen/generator.toml new file mode 100644 index 0000000..88855bd --- /dev/null +++ b/gen/generator.toml @@ -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 \ No newline at end of file diff --git a/src/PortAudio.jl b/src/PortAudio.jl index d7b0e42..a428b8a 100644 --- a/src/PortAudio.jl +++ b/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 diff --git a/src/libportaudio.jl b/src/libportaudio.jl index a340771..38574d1 100644 --- a/src/libportaudio.jl +++ b/src/libportaudio.jl @@ -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 diff --git a/test/runtests.jl b/test/runtests.jl index 65e505d..cc4eda5 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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: