Use Clang wrappers; reduce thread spawning; separate out SampledSignals

fix

fix

use CLANG wrappers

cleanup (again)

more coverage

fix tests

fix?

distinguish error numbers from codes

reduce thread spawning

cleanup

fix?

fix?

coverage

coverage

fix

fix

more cleanup and comments

separate out SampledSignals part

almost there

fix

comments

fix

Add gen README

Update test/runtests.jl

Co-authored-by: Robert Luke <748691+rob-luke@users.noreply.github.com>
performance improvements

fix

more comments

separate messanger from buffer

fix source/sink mix-up

adjust_channels, test device names

slight cleanup

update docs

add links to docs to readme
This commit is contained in:
Brandon Taylor 2021-07-25 13:09:59 -04:00
parent 6a018cfc32
commit d6c3595f03
14 changed files with 1579 additions and 924 deletions

16
.github/workflows/permanent.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: permanent
on:
push:
branches:
- 'master'
jobs:
document:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@latest
with:
version: '1.6'
- uses: julia-actions/julia-docdeploy@releases/v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
*.o *.o
deps/deps.jl deps/deps.jl
deps/build.log deps/build.log
docs/build
*.wav *.wav
*.flac *.flac
*.cov *.cov

View file

@ -15,11 +15,11 @@ julia = "1.3"
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"
Suppressor = "0.2.0"
[extras] [extras]
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
LibSndFile = "b13ce0c6-77b0-50c6-a2db-140568b8d1a5"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets] [targets]
test = ["Logging", "Test"] test = ["Documenter", "LibSndFile", "Test"]

View file

@ -1,49 +1,12 @@
PortAudio.jl PortAudio.jl
============ ============
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaAudio.github.io/PortAudio.jl/dev)
[![Tests](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml/badge.svg)](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml) [![Tests](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml/badge.svg)](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml)
[![codecov](https://codecov.io/gh/JuliaAudio/PortAudio.jl/branch/master/graph/badge.svg?token=mgDAi8ulPY)](https://codecov.io/gh/JuliaAudio/PortAudio.jl) [![codecov](https://codecov.io/gh/JuliaAudio/PortAudio.jl/branch/master/graph/badge.svg?token=mgDAi8ulPY)](https://codecov.io/gh/JuliaAudio/PortAudio.jl)
PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which gives cross-platform access to audio devices. It is compatible with the types defined in [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl). It provides a `PortAudioStream` type, which can be read from and written to. PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which gives cross-platform access to audio devices.
It provides a `PortAudioStream` type, which can be read from and written to.
## 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.
```julia
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000Hz, latency=0.1, synced=false)
```
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
```julia
PortAudioStream(device::PortAudioDevice, args...; kwargs...)
PortAudioStream(devname::AbstractString, args...; kwargs...)
```
You can get a list of your system's devices with the `PortAudio.devices()` function:
```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)
```
### Input/Output Synchronization
The `synced` keyword argument to `PortAudioStream` controls whether the input and output ringbuffers are kept synchronized or not, which only effects duplex streams. It should be set to `true` if you need consistent input-to-output latency. In a synchronized stream, the underlying PortAudio callback will only read and write to the buffers an equal number of frames. In a synchronized stream, the user must also read and write an equal number of frames to the stream. If it is only written to or read from, it will eventually block. This is why it is `false` by default.
## Reading and Writing
The `PortAudioStream` type has `source` and `sink` fields which are of type `PortAudioSource <: SampleSource` and `PortAudioSink <: SampleSink`, respectively. are subtypes of `SampleSource` and `SampleSink`, respectively (from [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl)). This means they support all the stream and buffer features defined there. For example, if you load SampledSignals with `using SampledSignals` you can read 5 seconds to a buffer with `buf = read(stream.source, 5s)`, regardless of the sample rate of the device.
PortAudio.jl also provides convenience wrappers around the `PortAudioStream` type so you can read and write to it directly, e.g. `write(stream, stream)` will set up a loopback that will read from the input and play it back on the output.
## Debugging ## Debugging
@ -54,53 +17,3 @@ ENV["JULIA_DEBUG"] = :PortAudio
``` ```
before using the package. before using the package.
## Examples
### Set up an audio pass-through from microphone to speaker
```julia
stream = PortAudioStream(2, 2)
try
# cancel with Ctrl-C
write(stream, stream)
finally
close(stream)
end
```
### Use `do` syntax to auto-close the stream
```julia
PortAudioStream(2, 2) do stream
write(stream, stream)
end
```
### Open your built-in microphone and speaker by name
```julia
PortAudioStream("Built-in Microph", "Built-in Output") do stream
write(stream, stream)
end
```
### Record 10 seconds of audio and save to an ogg file
```julia
julia> using PortAudio, SampledSignals, LibSndFile
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> buf = read(stream, 10s)
480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
10.0 s at 48000 s⁻¹
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
julia> close(stream)
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
```

2
docs/Project.toml Normal file
View file

@ -0,0 +1,2 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"

12
docs/make.jl Normal file
View file

@ -0,0 +1,12 @@
using PortAudio
using Documenter: deploydocs, makedocs
makedocs(
sitename = "PortAudio.jl",
modules = [PortAudio],
pages = [
"Public interface" => "index.md",
"Internals" => "internals.md"
]
)
deploydocs(repo = "github.com/JuliaAudio/PortAudio.jl.git")

10
docs/src/index.md Normal file
View file

@ -0,0 +1,10 @@
# Public interface
```@index
Pages = ["index.md"]
```
```@autodocs
Modules = [PortAudio]
Private = false
```

10
docs/src/internals.md Normal file
View file

@ -0,0 +1,10 @@
# Internals
```@index
Pages = ["internals.md"]
```
```@autodocs
Modules = [PortAudio]
Public = false
```

1
gen/README.md Normal file
View file

@ -0,0 +1 @@
The clang generators will automatically generate wrappers for a C library based on its headers. So everything you see in libportaudio.jl is automatically generated from the C library. If a newer version of portaudio adds more features, we won't have to add new wrappers: clang will handle it for us. It is easy to use currently unused features: the wrappers have already been written for us. Even though it does an admirable job, clang doesn't handle errors and set locks. Fortunately, it's very easy to add secondary wrappers, or just do it at point of use.

16
gen/generator.jl Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

View file

@ -1,180 +1,328 @@
# Low-level wrappers for Portaudio calls module LibPortAudio
# General type aliases using libportaudio_jll
const PaTime = Cdouble export libportaudio_jll
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) function Pa_GetVersion()
ccall((:Pa_GetVersion, libportaudio), Cint, ())
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)
"""
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 end
# because we're calling Pa_ReadStream and PA_WriteStream from separate threads, function Pa_GetVersionText()
# we put a mutex around libportaudio calls ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
const pamutex = ReentrantLock() end
macro locked(ex) mutable struct PaVersionInfo
quote versionMajor::Cint
lock(pamutex) do versionMinor::Cint
$(esc(ex)) versionSubMinor::Cint
end versionControlRevision::Ptr{Cchar}
end 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
const PaError = Cint
@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
function Pa_GetErrorText(errorCode)
ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), errorCode)
end end
function Pa_Initialize() function Pa_Initialize()
err = @locked ccall((:Pa_Initialize, libportaudio), PaError, ()) ccall((:Pa_Initialize, libportaudio), PaError, ())
handle_status(err)
end end
function Pa_Terminate() function Pa_Terminate()
err = @locked ccall((:Pa_Terminate, libportaudio), PaError, ()) ccall((:Pa_Terminate, libportaudio), PaError, ())
handle_status(err)
end end
Pa_GetVersion() = @locked ccall((:Pa_GetVersion, libportaudio), Cint, ()) const PaDeviceIndex = Cint
function Pa_GetVersionText() const PaHostApiIndex = Cint
versionPtr = @locked ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
unsafe_string(versionPtr) function Pa_GetHostApiCount()
ccall((:Pa_GetHostApiCount, libportaudio), PaHostApiIndex, ())
end 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 @enum PaHostApiTypeId::UInt32 begin
# unique type ID that tells you which native backend it is (JACK, ALSA, ASIO, paInDevelopment = 0
# etc.). On a given system you can identify each backend by its index, which paDirectSound = 1
# will range between 0 and Pa_GetHostApiCount() - 1. You can enumerate through paMME = 2
# all the host APIs on the system by iterating through those values. paASIO = 3
paSoundManager = 4
# PaHostApiTypeId values paCoreAudio = 5
const pa_host_api_names = Dict{PaHostApiTypeId, String}( paOSS = 7
0 => "In Development", # use while developing support for a new host API paALSA = 8
1 => "Direct Sound", paAL = 9
2 => "MME", paBeOS = 10
3 => "ASIO", paWDMKS = 11
4 => "Sound Manager", paJACK = 12
5 => "Core Audio", paWASAPI = 13
7 => "OSS", paAudioScienceHPI = 14
8 => "ALSA", end
9 => "AL",
10 => "BeOS",
11 => "WDMKS",
12 => "Jack",
13 => "WASAPI",
14 => "AudioScience HPI",
)
mutable struct PaHostApiInfo mutable struct PaHostApiInfo
struct_version::Cint structVersion::Cint
api_type::PaHostApiTypeId type::PaHostApiTypeId
name::Ptr{Cchar} name::Ptr{Cchar}
deviceCount::Cint deviceCount::Cint
defaultInputDevice::PaDeviceIndex defaultInputDevice::PaDeviceIndex
defaultOutputDevice::PaDeviceIndex defaultOutputDevice::PaDeviceIndex
end end
function Pa_GetHostApiInfo(i) function Pa_GetHostApiInfo(hostApi)
result = @locked ccall( ccall(
(:Pa_GetHostApiInfo, libportaudio), (:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo}, Ptr{PaHostApiInfo},
(PaHostApiIndex,), (PaHostApiIndex,),
i, hostApi,
) )
if result == C_NULL
throw(BoundsError(Pa_GetHostApiInfo, i))
end
unsafe_load(result)
end end
# Device Functions function Pa_HostApiTypeIdToHostApiIndex(type)
ccall(
mutable struct PaDeviceInfo (:Pa_HostApiTypeIdToHostApiIndex, libportaudio),
struct_version::Cint PaHostApiIndex,
name::Ptr{Cchar} (PaHostApiTypeId,),
host_api::PaHostApiIndex type,
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
end end
Pa_GetDeviceCount() = @locked ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ()) function Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex)
ccall(
function Pa_GetDeviceInfo(i) (:Pa_HostApiDeviceIndexToDeviceIndex, libportaudio),
result = @locked ccall( PaDeviceIndex,
(:Pa_GetDeviceInfo, libportaudio), (PaHostApiIndex, Cint),
Ptr{PaDeviceInfo}, hostApi,
(PaDeviceIndex,), hostApiDeviceIndex,
i,
) )
if result == C_NULL end
throw(BoundsError(Pa_GetDeviceInfo, i))
end mutable struct PaHostErrorInfo
unsafe_load(result) hostApiType::PaHostApiTypeId
errorCode::Clong
errorText::Ptr{Cchar}
end
function Pa_GetLastHostErrorInfo()
ccall((:Pa_GetLastHostErrorInfo, libportaudio), Ptr{PaHostErrorInfo}, ())
end
function Pa_GetDeviceCount()
ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
end end
function Pa_GetDefaultInputDevice() function Pa_GetDefaultInputDevice()
@locked ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ()) ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
end end
function Pa_GetDefaultOutputDevice() function Pa_GetDefaultOutputDevice()
@locked ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ()) ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ())
end 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 device::PaDeviceIndex
channelCount::Cint channelCount::Cint
sampleFormat::PaSampleFormat sampleFormat::PaSampleFormat
suggestedLatency::PaTime 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 end
mutable struct PaStreamInfo mutable struct PaStreamInfo
@ -184,152 +332,108 @@ mutable struct PaStreamInfo
sampleRate::Cdouble sampleRate::Cdouble
end end
# function Pa_OpenDefaultStream(inChannels, outChannels, function Pa_GetStreamInfo(stream)
# sampleFormat::PaSampleFormat, ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo}, (Ptr{PaStream},), stream)
# sampleRate, framesPerBuffer) end
# streamPtr = Ref{PaStream}(0)
# err = ccall((:Pa_OpenDefaultStream, libportaudio), function Pa_GetStreamTime(stream)
# PaError, (Ref{PaStream}, Cint, Cint, ccall((:Pa_GetStreamTime, libportaudio), PaTime, (Ptr{PaStream},), stream)
# PaSampleFormat, Cdouble, Culong, end
# Ref{Cvoid}, Ref{Cvoid}),
# streamPtr, inChannels, outChannels, sampleFormat, sampleRate, function Pa_GetStreamCpuLoad(stream)
# framesPerBuffer, C_NULL, C_NULL) ccall((:Pa_GetStreamCpuLoad, libportaudio), Cdouble, (Ptr{PaStream},), stream)
# handle_status(err) end
#
# streamPtr[] function Pa_ReadStream(stream, buffer, frames)
# end ccall(
# (:Pa_ReadStream, libportaudio),
function Pa_OpenStream(
inParams,
outParams,
sampleRate,
framesPerBuffer,
flags::PaStreamFlags,
callback,
userdata,
)
streamPtr = Ref{PaStream}(0)
err = @locked ccall(
(:Pa_OpenStream, libportaudio),
PaError, PaError,
( (Ptr{PaStream}, Ptr{Cvoid}, Culong),
Ref{PaStream},
Ref{Pa_StreamParameters},
Ref{Pa_StreamParameters},
Cdouble,
Culong,
PaStreamFlags,
Ref{Cvoid},
# it seems like we should be able to use Ref{T} here, with
# userdata::T above, and avoid the `pointer_from_objref` below.
# that's not working on 0.6 though, and it shouldn't really
# matter because userdata should be GC-rooted anyways
Ptr{Cvoid},
),
streamPtr,
inParams,
outParams,
float(sampleRate),
framesPerBuffer,
flags,
callback === nothing ? C_NULL : callback,
userdata === nothing ? C_NULL : pointer_from_objref(userdata),
)
handle_status(err)
streamPtr
end
function Pa_StartStream(stream::PaStream)
err = @locked ccall((:Pa_StartStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
end
function Pa_StopStream(stream::PaStream)
err = @locked ccall((:Pa_StopStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
end
function Pa_CloseStream(stream::PaStream)
err = @locked ccall((:Pa_CloseStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
end
function Pa_GetStreamReadAvailable(stream::PaStream)
avail = @locked ccall(
(:Pa_GetStreamReadAvailable, libportaudio),
Clong,
(PaStream,),
stream, stream,
buffer,
frames,
) )
avail >= 0 || handle_status(avail)
avail
end end
function Pa_GetStreamWriteAvailable(stream::PaStream) function Pa_WriteStream(stream, buffer, frames)
avail = @locked ccall( ccall(
(:Pa_GetStreamWriteAvailable, libportaudio), (:Pa_WriteStream, libportaudio),
Clong, PaError,
(PaStream,), (Ptr{PaStream}, Ptr{Cvoid}, Culong),
stream, stream,
buffer,
frames,
) )
avail >= 0 || handle_status(avail)
avail
end end
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer, show_warnings = true) function Pa_GetStreamReadAvailable(stream)
# without disable_sigint I get a segfault with the error: ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
# "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.
err = disable_sigint() do
@tcall @locked ccall(
(:Pa_ReadStream, libportaudio),
PaError,
(PaStream, Ptr{Cvoid}, Culong),
stream,
buf,
frames,
)
end
handle_status(err, show_warnings)
err
end end
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer, show_warnings = true) function Pa_GetStreamWriteAvailable(stream)
err = disable_sigint() do ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
@tcall @locked ccall(
(:Pa_WriteStream, libportaudio),
PaError,
(PaStream, Ptr{Cvoid}, Culong),
stream,
buf,
frames,
)
end
handle_status(err, show_warnings)
err
end end
# function Pa_GetStreamInfo(stream::PaStream) function Pa_GetSampleSize(format)
# infoptr = ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo}, ccall((:Pa_GetSampleSize, libportaudio), PaError, (PaSampleFormat,), format)
# (PaStream, ), stream) end
# if infoptr == C_NULL
# error("Error getting stream info. Is the stream already closed?") function Pa_Sleep(msec)
# end ccall((:Pa_Sleep, libportaudio), Cvoid, (Clong,), msec)
# unsafe_load(infoptr) end
# end
# const paNoDevice = PaDeviceIndex(-1)
# General utility function to handle the status from the Pa_* functions
function handle_status(err::Integer, show_warnings::Bool = true) const paUseHostApiSpecificDeviceSpecification = PaDeviceIndex(-2)
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
if show_warnings const paFloat32 = PaSampleFormat(0x00000001)
msg =
@locked ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err) const paInt32 = PaSampleFormat(0x00000002)
@warn("libportaudio: " * unsafe_string(msg))
end const paInt24 = PaSampleFormat(0x00000004)
elseif err != PA_NO_ERROR
msg = @locked ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err) const paInt16 = PaSampleFormat(0x00000008)
throw(ErrorException("libportaudio: " * unsafe_string(msg)))
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 end
end end
end # module

View file

@ -1,32 +1,64 @@
#!/usr/bin/env julia #!/usr/bin/env julia
using Base.Sys: iswindows
using Logging: Debug using Documenter: doctest
using PortAudio
using PortAudio: using PortAudio:
combine_default_sample_rates, combine_default_sample_rates,
devices,
get_default_input_index,
get_default_output_index,
get_device,
get_input_type,
get_output_type,
handle_status, handle_status,
Pa_GetDefaultInputDevice, initialize,
Pa_GetDefaultOutputDevice, PortAudioException,
Pa_GetDeviceInfo, PortAudio,
Pa_GetHostApiInfo,
Pa_Initialize,
PA_OUTPUT_UNDERFLOWED,
Pa_Terminate,
PortAudioDevice, PortAudioDevice,
recover_xrun, PortAudioStream,
safe_load,
seek_alsa_conf, seek_alsa_conf,
@stderr_as_debug terminate,
using SampledSignals name
using Test using PortAudio.LibPortAudio:
Pa_AbortStream,
PaError,
PaErrorCode,
paFloat32,
Pa_GetDefaultHostApi,
Pa_GetDeviceInfo,
Pa_GetHostApiCount,
Pa_GetLastHostErrorInfo,
Pa_GetSampleSize,
Pa_GetStreamCpuLoad,
Pa_GetStreamInfo,
Pa_GetStreamReadAvailable,
Pa_GetStreamTime,
Pa_GetStreamWriteAvailable,
Pa_GetVersionInfo,
Pa_HostApiDeviceIndexToDeviceIndex,
paHostApiNotFound,
Pa_HostApiTypeIdToHostApiIndex,
PaHostErrorInfo,
paInDevelopment,
paInvalidDevice,
Pa_IsFormatSupported,
Pa_IsStreamActive,
paNoError,
paNoFlag,
paNotInitialized,
Pa_OpenDefaultStream,
paOutputUnderflowed,
Pa_SetStreamFinishedCallback,
Pa_Sleep,
Pa_StopStream,
PaStream,
PaStreamInfo,
PaStreamParameters,
PaVersionInfo
using SampledSignals: nchannels, s, SampleBuf, samplerate, SinSource
using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
@testset "Debug messages" begin @testset "Tests without sound" begin
@test_logs (:debug, "hi") min_level = Debug @test_nowarn @stderr_as_debug begin
print(stderr, "hi")
true
end
end
@testset "PortAudio Tests" begin
@testset "Reports version" begin @testset "Reports version" begin
io = IOBuffer() io = IOBuffer()
PortAudio.versioninfo(io) PortAudio.versioninfo(io)
@ -36,151 +68,185 @@ end
end end
@testset "Can list devices without crashing" begin @testset "Can list devices without crashing" begin
PortAudio.devices() display(devices())
println()
end end
@testset "Null errors" begin @testset "libortaudio without sound" begin
@test_throws BoundsError Pa_GetDeviceInfo(-1) @test handle_status(Pa_GetHostApiCount()) >= 0
@test_throws BoundsError Pa_GetHostApiInfo(-1) @test handle_status(Pa_GetDefaultHostApi()) >= 0
# version info not available on windows?
if !Sys.iswindows()
@test safe_load(Pa_GetVersionInfo(), ErrorException("no info")) isa
PaVersionInfo
end
@test safe_load(Pa_GetLastHostErrorInfo(), ErrorException("no info")) isa
PaHostErrorInfo
@test PaErrorCode(Pa_IsFormatSupported(C_NULL, C_NULL, 0.0)) == paInvalidDevice
@test PaErrorCode(
Pa_OpenDefaultStream(Ref(C_NULL), 0, 0, paFloat32, 0.0, 0, C_NULL, C_NULL),
) == paInvalidDevice
end end
@testset "Errors without sound" begin
@test sprint(showerror, PortAudioException(paNotInitialized)) ==
"PortAudioException: PortAudio not initialized"
@test_throws KeyError("foobarbaz") get_device("foobarbaz")
@test_throws KeyError(-1) get_device(-1)
@test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(())
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
PaError(paOutputUnderflowed),
)
@test_throws PortAudioException(paNotInitialized) handle_status(
PaError(paNotInitialized),
)
Pa_Sleep(1)
@test Pa_GetSampleSize(paFloat32) == 4
end
# make sure we can terminate, then reinitialize
terminate()
initialize()
end end
if !isempty(PortAudio.devices()) if !isempty(devices())
# make sure we can terminate, then reinitialize @testset "Tests with sound" begin
Pa_Terminate() # these default values are specific to local machines
@stderr_as_debug Pa_Initialize() input_name = get_device(get_default_input_index()).name
output_name = get_device(get_default_output_index()).name
# these default values are specific to my machines @testset "Interactive tests" begin
inidx = Pa_GetDefaultInputDevice()
default_indev = PortAudioDevice(Pa_GetDeviceInfo(inidx), inidx).name
outidx = Pa_GetDefaultOutputDevice()
default_outdev = PortAudioDevice(Pa_GetDeviceInfo(outidx), outidx).name
@testset "Local Tests" begin
@testset "Open Default Device" begin
println("Recording...") println("Recording...")
stream = PortAudioStream(2, 0) stream = PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true)
buf = read(stream, 5s) buffer = read(stream, 5s)
close(stream) @test size(buffer) ==
@test size(buf) ==
(round(Int, 5 * samplerate(stream)), nchannels(stream.source)) (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
close(stream)
sleep(1)
println("Playing back recording...") println("Playing back recording...")
PortAudioStream(0, 2) do stream PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
write(stream, buf) write(stream, buffer)
end end
sleep(1)
println("Testing pass-through") println("Testing pass-through")
stream = PortAudioStream(2, 2) stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
sink = stream.sink sink = stream.sink
source = stream.source source = stream.source
@test sprint(show, typeof(sink)) == "PortAudioSink{Float32}" @test sprint(show, stream) == """
@test sprint(show, typeof(source)) == "PortAudioSource{Float32}" PortAudioStream{Float32}
@test sprint(show, sink) == Samplerate: 44100.0Hz
"2-channel PortAudioSink{Float32}($(repr(default_indev)))" 2 channel sink: $(repr(input_name))
@test sprint(show, source) == 2 channel source: $(repr(output_name))"""
"2-channel PortAudioSource{Float32}($(repr(default_outdev)))" @test sprint(show, source) == "2 channel source: $(repr(output_name))"
@test sprint(show, sink) == "2 channel sink: $(repr(input_name))"
write(stream, stream, 5s) write(stream, stream, 5s)
recover_xrun(stream) @test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
@test_throws ErrorException(""" @test isopen(stream)
Attempted to close PortAudioSink or PortAudioSource.
Close the containing PortAudioStream instead
""") close(sink)
@test_throws ErrorException("""
Attempted to close PortAudioSink or PortAudioSource.
Close the containing PortAudioStream instead
""") close(source)
close(stream) close(stream)
sleep(1)
@test !isopen(stream) @test !isopen(stream)
@test !isopen(sink) @test !isopen(sink)
@test !isopen(source) @test !isopen(source)
println("done") println("done")
end end
@testset "Samplerate-converting writing" begin @testset "Samplerate-converting writing" begin
stream = PortAudioStream(0, 2) PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
write( write(
stream, stream,
SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]), SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]),
3s, 3s,
) )
write( println("expected blip")
stream, write(
SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]), stream,
3s, SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]),
) 3s,
close(stream) )
end end
@testset "Open Device by name" begin
stream = PortAudioStream(default_indev, default_outdev)
buf = read(stream, 0.001s)
@test size(buf) ==
(round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
write(stream, buf)
io = IOBuffer()
show(io, stream)
@test occursin(
"""
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel sink: "$default_outdev"
2 channel source: "$default_indev\"""",
String(take!(io)),
)
close(stream)
end
@testset "Error handling" begin
@test_throws ErrorException PortAudioStream("foobarbaz")
@test_throws ErrorException PortAudioStream(default_indev, "foobarbaz")
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
PA_OUTPUT_UNDERFLOWED,
)
@test_throws ErrorException("libportaudio: PortAudio not initialized") handle_status(
-10000,
)
@test_throws ErrorException("""
Could not find ALSA config directory. Searched:
If ALSA is installed, set the "ALSA_CONFIG_DIR" environment
variable. The given directory should have a file "alsa.conf".
If it would be useful to others, please file an issue at
https://github.com/JuliaAudio/PortAudio.jl/issues
with your alsa config directory so we can add it to the search
paths.
""") seek_alsa_conf([])
@test_throws ErrorException(
"""
Can't open duplex stream with mismatched samplerates (in: 0, out: 1).
Try changing your sample rate in your driver settings or open separate input and output
streams.
""",
) combine_default_sample_rates(1, 0, 1, 1)
end end
sleep(1)
# no way to check that the right data is actually getting read or written here, # no way to check that the right data is actually getting read or written here,
# but at least it's not crashing. # but at least it's not crashing.
@testset "Queued Writing" begin @testset "Queued Writing" begin
stream = PortAudioStream(0, 2) PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
buf = SampleBuf( buffer = SampleBuf(
rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1, rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1,
samplerate(stream), samplerate(stream),
) )
t1 = @async write(stream, buf) frame_count_1 = @async write(stream, buffer)
t2 = @async write(stream, buf) frame_count_2 = @async write(stream, buffer)
@test fetch(t1) == 48000 @test fetch(frame_count_1) == 48000
@test fetch(t2) == 48000 println("expected blip")
close(stream) @test fetch(frame_count_2) == 48000
end
sleep(1)
end end
@testset "Queued Reading" begin @testset "Queued Reading" begin
stream = PortAudioStream(2, 0) PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true) do stream
buf = SampleBuf( buffer = SampleBuf(
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1, rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
samplerate(stream), samplerate(stream),
)
frame_count_1 = @async read!(stream, buffer)
frame_count_2 = @async read!(stream, buffer)
@test fetch(frame_count_1) == 48000
@test fetch(frame_count_2) == 48000
end
sleep(1)
end
@testset "Constructors" begin
PortAudioStream(2, maximum; adjust_channels = true) do stream
@test isopen(stream)
end
PortAudioStream(output_name; adjust_channels = true) do stream
@test isopen(stream)
end
PortAudioStream(input_name, output_name; adjust_channels = true) do stream
@test isopen(stream)
end
end
@testset "Errors with sound" begin
big = typemax(Int)
@test_throws DomainError(
typemax(Int),
"$big exceeds maximum input channels for $output_name",
) PortAudioStream(input_name, output_name, big, 0)
@test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream(
input_name,
output_name,
0,
0;
adjust_channels = true,
) )
t1 = @async read!(stream, buf) @test_throws ArgumentError("""
t2 = @async read!(stream, buf) Default sample rate 0 for input $output_name disagrees with
@test fetch(t1) == 48000 default sample rate 1 for output $input_name.
@test fetch(t2) == 48000 Please specify a sample rate.
close(stream) """) combine_default_sample_rates(
get_device(input_name),
0,
get_device(output_name),
1,
)
end
@testset "libportaudio with sound" begin
@test PaErrorCode(Pa_HostApiTypeIdToHostApiIndex(paInDevelopment)) ==
paHostApiNotFound
@test Pa_HostApiDeviceIndexToDeviceIndex(paInDevelopment, 0) == 0
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
pointer_to = stream.pointer_to
@test handle_status(Pa_GetStreamReadAvailable(pointer_to)) >= 0
@test handle_status(Pa_GetStreamWriteAvailable(pointer_to)) >= 0
@test Bool(handle_status(Pa_IsStreamActive(pointer_to)))
@test safe_load(Pa_GetStreamInfo(pointer_to), ErrorException("no info")) isa
PaStreamInfo
@test Pa_GetStreamTime(pointer_to) >= 0
@test Pa_GetStreamCpuLoad(pointer_to) >= 0
@test PaErrorCode(handle_status(Pa_AbortStream(pointer_to))) == paNoError
@test PaErrorCode(
handle_status(Pa_SetStreamFinishedCallback(pointer_to, C_NULL)),
) == paNoError
end end
end end
doctest(PortAudio)
end end