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:
parent
6a018cfc32
commit
d6c3595f03
14 changed files with 1579 additions and 924 deletions
16
.github/workflows/permanent.yml
vendored
Normal file
16
.github/workflows/permanent.yml
vendored
Normal 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
1
.gitignore
vendored
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
93
README.md
93
README.md
|
@ -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
2
docs/Project.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[deps]
|
||||||
|
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
12
docs/make.jl
Normal file
12
docs/make.jl
Normal 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
10
docs/src/index.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Public interface
|
||||||
|
|
||||||
|
```@index
|
||||||
|
Pages = ["index.md"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```@autodocs
|
||||||
|
Modules = [PortAudio]
|
||||||
|
Private = false
|
||||||
|
```
|
10
docs/src/internals.md
Normal file
10
docs/src/internals.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# Internals
|
||||||
|
|
||||||
|
```@index
|
||||||
|
Pages = ["internals.md"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```@autodocs
|
||||||
|
Modules = [PortAudio]
|
||||||
|
Public = false
|
||||||
|
```
|
1
gen/README.md
Normal file
1
gen/README.md
Normal 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
16
gen/generator.jl
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using Clang.Generators
|
||||||
|
using libportaudio_jll
|
||||||
|
|
||||||
|
cd(@__DIR__)
|
||||||
|
|
||||||
|
include_dir = joinpath(libportaudio_jll.artifact_dir, "include") |> normpath
|
||||||
|
portaudio_h = joinpath(include_dir, "portaudio.h")
|
||||||
|
|
||||||
|
options = load_options(joinpath(@__DIR__, "generator.toml"))
|
||||||
|
|
||||||
|
args = get_default_args()
|
||||||
|
push!(args, "-I$include_dir")
|
||||||
|
|
||||||
|
ctx = create_context(portaudio_h, args, options)
|
||||||
|
|
||||||
|
build!(ctx)
|
9
gen/generator.toml
Normal file
9
gen/generator.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[general]
|
||||||
|
library_name = "libportaudio"
|
||||||
|
output_file_path = "../src/LibPortAudio.jl"
|
||||||
|
module_name = "LibPortAudio"
|
||||||
|
jll_pkg_name = "libportaudio_jll"
|
||||||
|
export_symbol_prefixes = ["Pa", "pa"]
|
||||||
|
|
||||||
|
use_julia_native_enum_type = true
|
||||||
|
auto_mutability = true
|
1351
src/PortAudio.jl
1351
src/PortAudio.jl
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||||
|
|
344
test/runtests.jl
344
test/runtests.jl
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue