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
deps/deps.jl
deps/build.log
docs/build
*.wav
*.flac
*.cov

View file

@ -15,11 +15,11 @@ julia = "1.3"
alsa_plugins_jll = "1.2.2"
libportaudio_jll = "19.6.0"
SampledSignals = "2.1.1"
Suppressor = "0.2.0"
[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"
[targets]
test = ["Logging", "Test"]
test = ["Documenter", "LibSndFile", "Test"]

View file

@ -1,49 +1,12 @@
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)
[![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.
## 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.
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.
## Debugging
@ -54,53 +17,3 @@ ENV["JULIA_DEBUG"] = :PortAudio
```
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
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)
"""
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))))
@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
# 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()
err = @locked ccall((:Pa_Initialize, libportaudio), PaError, ())
handle_status(err)
ccall((:Pa_Initialize, libportaudio), PaError, ())
end
function Pa_Terminate()
err = @locked ccall((:Pa_Terminate, libportaudio), PaError, ())
handle_status(err)
ccall((:Pa_Terminate, libportaudio), PaError, ())
end
Pa_GetVersion() = @locked ccall((:Pa_GetVersion, libportaudio), Cint, ())
const PaDeviceIndex = Cint
function Pa_GetVersionText()
versionPtr = @locked ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
unsafe_string(versionPtr)
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)
result = @locked ccall(
function Pa_GetHostApiInfo(hostApi)
ccall(
(:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo},
(PaHostApiIndex,),
i,
hostApi,
)
if result == C_NULL
throw(BoundsError(Pa_GetHostApiInfo, i))
end
unsafe_load(result)
end
# Device Functions
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
end
Pa_GetDeviceCount() = @locked ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
function Pa_GetDeviceInfo(i)
result = @locked ccall(
(:Pa_GetDeviceInfo, libportaudio),
Ptr{PaDeviceInfo},
(PaDeviceIndex,),
i,
function Pa_HostApiTypeIdToHostApiIndex(type)
ccall(
(:Pa_HostApiTypeIdToHostApiIndex, libportaudio),
PaHostApiIndex,
(PaHostApiTypeId,),
type,
)
if result == C_NULL
throw(BoundsError(Pa_GetDeviceInfo, i))
end
unsafe_load(result)
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()
ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
end
function Pa_GetDefaultInputDevice()
@locked ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
end
function Pa_GetDefaultOutputDevice()
@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
@ -184,152 +332,108 @@ mutable struct PaStreamInfo
sampleRate::Cdouble
end
# function Pa_OpenDefaultStream(inChannels, outChannels,
# sampleFormat::PaSampleFormat,
# sampleRate, framesPerBuffer)
# streamPtr = Ref{PaStream}(0)
# err = 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)
# handle_status(err)
#
# streamPtr[]
# end
#
function Pa_OpenStream(
inParams,
outParams,
sampleRate,
framesPerBuffer,
flags::PaStreamFlags,
callback,
userdata,
)
streamPtr = Ref{PaStream}(0)
err = @locked ccall(
(:Pa_OpenStream, libportaudio),
PaError,
(
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
function Pa_GetStreamInfo(stream)
ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo}, (Ptr{PaStream},), stream)
end
function Pa_StartStream(stream::PaStream)
err = @locked ccall((:Pa_StartStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
function Pa_GetStreamTime(stream)
ccall((:Pa_GetStreamTime, libportaudio), PaTime, (Ptr{PaStream},), stream)
end
function Pa_StopStream(stream::PaStream)
err = @locked ccall((:Pa_StopStream, libportaudio), PaError, (PaStream,), stream)
handle_status(err)
function Pa_GetStreamCpuLoad(stream)
ccall((:Pa_GetStreamCpuLoad, libportaudio), Cdouble, (Ptr{PaStream},), stream)
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,
)
avail >= 0 || handle_status(avail)
avail
end
function Pa_GetStreamWriteAvailable(stream::PaStream)
avail = @locked ccall(
(:Pa_GetStreamWriteAvailable, libportaudio),
Clong,
(PaStream,),
stream,
)
avail >= 0 || handle_status(avail)
avail
end
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer, show_warnings = 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.
err = disable_sigint() do
@tcall @locked ccall(
function Pa_ReadStream(stream, buffer, frames)
ccall(
(:Pa_ReadStream, libportaudio),
PaError,
(PaStream, Ptr{Cvoid}, Culong),
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
stream,
buf,
buffer,
frames,
)
end
handle_status(err, show_warnings)
err
end
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer, show_warnings = true)
err = disable_sigint() do
@tcall @locked ccall(
function Pa_WriteStream(stream, buffer, frames)
ccall(
(:Pa_WriteStream, libportaudio),
PaError,
(PaStream, Ptr{Cvoid}, Culong),
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
stream,
buf,
buffer,
frames,
)
end
handle_status(err, show_warnings)
err
function Pa_GetStreamReadAvailable(stream)
ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
end
# function Pa_GetStreamInfo(stream::PaStream)
# infoptr = ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo},
# (PaStream, ), stream)
# if infoptr == C_NULL
# error("Error getting stream info. Is the stream already closed?")
# end
# unsafe_load(infoptr)
# end
#
# General utility function to handle the status from the Pa_* functions
function handle_status(err::Integer, show_warnings::Bool = true)
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
if show_warnings
msg =
@locked ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err)
@warn("libportaudio: " * unsafe_string(msg))
function Pa_GetStreamWriteAvailable(stream)
ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
end
elseif err != PA_NO_ERROR
msg = @locked ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err)
throw(ErrorException("libportaudio: " * unsafe_string(msg)))
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
end
end # module

View file

@ -1,32 +1,64 @@
#!/usr/bin/env julia
using Logging: Debug
using PortAudio
using Base.Sys: iswindows
using Documenter: doctest
using PortAudio:
combine_default_sample_rates,
devices,
get_default_input_index,
get_default_output_index,
get_device,
get_input_type,
get_output_type,
handle_status,
Pa_GetDefaultInputDevice,
Pa_GetDefaultOutputDevice,
Pa_GetDeviceInfo,
Pa_GetHostApiInfo,
Pa_Initialize,
PA_OUTPUT_UNDERFLOWED,
Pa_Terminate,
initialize,
PortAudioException,
PortAudio,
PortAudioDevice,
recover_xrun,
PortAudioStream,
safe_load,
seek_alsa_conf,
@stderr_as_debug
using SampledSignals
using Test
terminate,
name
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
@test_logs (:debug, "hi") min_level = Debug @test_nowarn @stderr_as_debug begin
print(stderr, "hi")
true
end
end
@testset "PortAudio Tests" begin
@testset "Tests without sound" begin
@testset "Reports version" begin
io = IOBuffer()
PortAudio.versioninfo(io)
@ -36,151 +68,185 @@ end
end
@testset "Can list devices without crashing" begin
PortAudio.devices()
display(devices())
println()
end
@testset "Null errors" begin
@test_throws BoundsError Pa_GetDeviceInfo(-1)
@test_throws BoundsError Pa_GetHostApiInfo(-1)
@testset "libortaudio without sound" begin
@test handle_status(Pa_GetHostApiCount()) >= 0
@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
@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
if !isempty(PortAudio.devices())
# make sure we can terminate, then reinitialize
Pa_Terminate()
@stderr_as_debug 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
@testset "Local Tests" begin
@testset "Open Default Device" begin
println("Recording...")
stream = PortAudioStream(2, 0)
buf = read(stream, 5s)
close(stream)
@test size(buf) ==
(round(Int, 5 * samplerate(stream)), nchannels(stream.source))
println("Playing back recording...")
PortAudioStream(0, 2) do stream
write(stream, buf)
terminate()
initialize()
end
if !isempty(devices())
@testset "Tests with sound" begin
# these default values are specific to local machines
input_name = get_device(get_default_input_index()).name
output_name = get_device(get_default_output_index()).name
@testset "Interactive tests" begin
println("Recording...")
stream = PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true)
buffer = read(stream, 5s)
@test size(buffer) ==
(round(Int, 5 * samplerate(stream)), nchannels(stream.source))
close(stream)
sleep(1)
println("Playing back recording...")
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
write(stream, buffer)
end
sleep(1)
println("Testing pass-through")
stream = PortAudioStream(2, 2)
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
sink = stream.sink
source = stream.source
@test sprint(show, typeof(sink)) == "PortAudioSink{Float32}"
@test sprint(show, typeof(source)) == "PortAudioSource{Float32}"
@test sprint(show, sink) ==
"2-channel PortAudioSink{Float32}($(repr(default_indev)))"
@test sprint(show, source) ==
"2-channel PortAudioSource{Float32}($(repr(default_outdev)))"
@test sprint(show, stream) == """
PortAudioStream{Float32}
Samplerate: 44100.0Hz
2 channel sink: $(repr(input_name))
2 channel source: $(repr(output_name))"""
@test sprint(show, source) == "2 channel source: $(repr(output_name))"
@test sprint(show, sink) == "2 channel sink: $(repr(input_name))"
write(stream, stream, 5s)
recover_xrun(stream)
@test_throws ErrorException("""
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)
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
@test isopen(stream)
close(stream)
sleep(1)
@test !isopen(stream)
@test !isopen(sink)
@test !isopen(source)
println("done")
end
@testset "Samplerate-converting writing" begin
stream = PortAudioStream(0, 2)
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
write(
stream,
SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]),
3s,
)
println("expected blip")
write(
stream,
SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]),
3s,
)
close(stream)
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
sleep(1)
# no way to check that the right data is actually getting read or written here,
# but at least it's not crashing.
@testset "Queued Writing" begin
stream = PortAudioStream(0, 2)
buf = SampleBuf(
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
buffer = SampleBuf(
rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1,
samplerate(stream),
)
t1 = @async write(stream, buf)
t2 = @async write(stream, buf)
@test fetch(t1) == 48000
@test fetch(t2) == 48000
close(stream)
frame_count_1 = @async write(stream, buffer)
frame_count_2 = @async write(stream, buffer)
@test fetch(frame_count_1) == 48000
println("expected blip")
@test fetch(frame_count_2) == 48000
end
sleep(1)
end
@testset "Queued Reading" begin
stream = PortAudioStream(2, 0)
buf = SampleBuf(
PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true) do stream
buffer = SampleBuf(
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
samplerate(stream),
)
t1 = @async read!(stream, buf)
t2 = @async read!(stream, buf)
@test fetch(t1) == 48000
@test fetch(t2) == 48000
close(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,
)
@test_throws ArgumentError("""
Default sample rate 0 for input $output_name disagrees with
default sample rate 1 for output $input_name.
Please specify a sample rate.
""") 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
doctest(PortAudio)
end