Compare commits
122 commits
new_ringbu
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
06c6fd0495 | ||
|
fbcd539a76 | ||
|
3939d47a8d | ||
|
19a49931ad | ||
|
d21e1e0363 | ||
|
7e0ca0122f | ||
|
156eae0db8 | ||
|
497567e329 | ||
|
24acc0247b | ||
|
78a0a9918d | ||
|
9c701415c0 | ||
|
f6cd300ec8 | ||
|
d570288ebe | ||
|
44d4ca38f8 | ||
|
3cd4551d81 | ||
|
7799ea1749 | ||
|
8a3b0d2a8a | ||
|
17faf321e7 | ||
|
01c58dab91 | ||
|
d6c3595f03 | ||
|
6a018cfc32 | ||
|
b3cddf5669 | ||
|
89020cafc7 | ||
|
50eb168f9a | ||
|
dd68835815 | ||
|
0187b4937d | ||
|
5bdd8975a9 | ||
|
e8c1e6a8f4 | ||
|
94a8a7f283 | ||
|
c4e1594518 | ||
|
1d9e441168 | ||
|
ff6dedec1f | ||
|
4652e394d8 | ||
|
57a74e0bca | ||
|
06a1a0f243 | ||
|
435e968b5a | ||
|
d71d971d66 | ||
|
b5eed5a7c7 | ||
|
9f96451356 | ||
|
d7d29880d6 | ||
|
5754f52034 | ||
|
a18ac17eba | ||
|
da0b3de1d8 | ||
|
b905a7f31a | ||
|
819de99d9c | ||
|
52be2700bf | ||
|
30a64d1f45 | ||
|
b1e973dba2 | ||
|
ab620dc64c | ||
|
578f34d0e5 | ||
|
ad9c3142da | ||
|
c4423b04bd | ||
|
7e317452f9 | ||
|
c05dff245e | ||
|
28c89c24e4 | ||
|
25f9c1230f | ||
|
461cdc1557 | ||
|
b9c604533d | ||
|
81720e0155 | ||
|
03a32623e9 | ||
|
c18963ac53 | ||
|
1a1a10cb69 | ||
|
c3570b0d07 | ||
|
1fe68cf857 | ||
|
bea3577abe | ||
|
25993bce0e | ||
|
d9ffc44f8c | ||
|
a1a2230ed8 | ||
|
3ffdbb9bc9 | ||
|
93916a630d | ||
|
b18b9bdcae | ||
|
9d780e4950 | ||
|
d069e75a9f | ||
|
f123478231 | ||
|
16d0bc48be | ||
|
9eb565e487 | ||
|
4c2ad4dc06 | ||
|
a7919c5b64 | ||
|
7944836b49 | ||
|
918e9d6986 | ||
|
1f1f721fec | ||
|
577d7adfef | ||
|
221afe9b88 | ||
|
ea2c524426 | ||
|
01ddd6b835 | ||
|
0425fbfe3b | ||
|
7f51c78596 | ||
|
207ec25f1e | ||
|
8bd884d394 | ||
|
c66ad398bd | ||
|
8d42b94a6a | ||
|
7d1be74eae | ||
|
45bfdc4830 | ||
|
bc32d13f7d | ||
|
308e88b7cf | ||
|
3551896de1 | ||
|
4d73324a7f | ||
|
06d3a4b099 | ||
|
d14a5f4b1f | ||
|
b1e0183538 | ||
|
f6213dc5ef | ||
|
ca7c8b91d8 | ||
|
f1828824a1 | ||
|
23f657dbbe | ||
|
5823404f1a | ||
|
03aefe619d | ||
|
312d4a90ca | ||
|
54ac000878 | ||
|
ce51f8497f | ||
|
1b26ea2f0f | ||
|
f06a53363b | ||
|
5ea61ea30e | ||
|
f57d201ef4 | ||
|
9dfb27a002 | ||
|
47ea6a0c30 | ||
|
de0dd1054f | ||
|
860b54ade0 | ||
|
a7cc0672a5 | ||
|
e45ca2e0b6 | ||
|
e7b67133b3 | ||
|
efd70272ab | ||
|
af8f53eb0e |
41 changed files with 2455 additions and 1226 deletions
9
.JuliaFormatter.toml
Normal file
9
.JuliaFormatter.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
always_for_in = true
|
||||||
|
whitespace_typedefs = true
|
||||||
|
whitespace_ops_in_indices = true
|
||||||
|
remove_extra_newlines = true
|
||||||
|
import_to_using = true
|
||||||
|
short_to_long_function_def = true
|
||||||
|
format_docstrings = true
|
||||||
|
align_pair_arrow = false
|
||||||
|
conditional_to_if = true
|
22
.github/pull_request_template.md
vendored
Normal file
22
.github/pull_request_template.md
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
Thanks for contributing a pull request!
|
||||||
|
|
||||||
|
Please be aware that we are a loose team of volunteers so patience is
|
||||||
|
necessary. Assistance handling other issues is very welcome. We value
|
||||||
|
all user contributions, no matter how minor they are. If we are slow to
|
||||||
|
review, either the pull request needs some benchmarking, tinkering,
|
||||||
|
convincing, etc. or more likely the reviewers are simply busy. In either
|
||||||
|
case, we ask for your understanding during the review process.
|
||||||
|
|
||||||
|
Again, thanks for contributing!
|
||||||
|
|
||||||
|
|
||||||
|
#### What does this implement/fix?
|
||||||
|
Explain your changes. Please be as descriptive as possible.
|
||||||
|
|
||||||
|
|
||||||
|
#### Reference issue
|
||||||
|
Example: Fixes #1234.
|
||||||
|
|
||||||
|
|
||||||
|
#### Additional information
|
||||||
|
Any additional information you think is important.
|
25
.github/workflows/CompatHelper.yml
vendored
Normal file
25
.github/workflows/CompatHelper.yml
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
name: CompatHelper
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: 0 0 * * *
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
CompatHelper:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "Install CompatHelper"
|
||||||
|
run: |
|
||||||
|
import Pkg
|
||||||
|
name = "CompatHelper"
|
||||||
|
uuid = "aa819f21-2bde-4658-8897-bab36330d9b7"
|
||||||
|
version = "2"
|
||||||
|
Pkg.add(; name, uuid, version)
|
||||||
|
shell: julia --color=yes {0}
|
||||||
|
- name: "Run CompatHelper"
|
||||||
|
run: |
|
||||||
|
import CompatHelper
|
||||||
|
CompatHelper.main()
|
||||||
|
shell: julia --color=yes {0}
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }}
|
16
.github/workflows/Documentation.yml
vendored
Normal file
16
.github/workflows/Documentation.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
name: Build documentation
|
||||||
|
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 }}
|
15
.github/workflows/TagBot.yml
vendored
Normal file
15
.github/workflows/TagBot.yml
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
name: TagBot
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types:
|
||||||
|
- created
|
||||||
|
workflow_dispatch:
|
||||||
|
jobs:
|
||||||
|
TagBot:
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: JuliaRegistries/TagBot@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ssh: ${{ secrets.COMPATHELPER_PRIV }}
|
41
.github/workflows/Tests.yml
vendored
Normal file
41
.github/workflows/Tests.yml
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
tags: '*'
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 30
|
||||||
|
name: ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
version:
|
||||||
|
- '1.6'
|
||||||
|
- '1'
|
||||||
|
- 'nightly'
|
||||||
|
os:
|
||||||
|
- ubuntu-latest
|
||||||
|
- macOS-latest
|
||||||
|
- windows-latest
|
||||||
|
arch:
|
||||||
|
- x64
|
||||||
|
- x86
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: julia-actions/setup-julia@v1
|
||||||
|
with:
|
||||||
|
version: ${{ matrix.version }}
|
||||||
|
- uses: julia-actions/julia-buildpkg@v1
|
||||||
|
- uses: julia-actions/julia-runtest@v1
|
||||||
|
- uses: julia-actions/julia-processcoverage@v1
|
||||||
|
- uses: codecov/codecov-action@v1
|
||||||
|
with:
|
||||||
|
file: lcov.info
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,7 +1,12 @@
|
||||||
*.swp
|
*.swp
|
||||||
*.o
|
*.o
|
||||||
deps/deps.jl
|
deps/deps.jl
|
||||||
|
deps/build.log
|
||||||
|
docs/build
|
||||||
*.wav
|
*.wav
|
||||||
*.flac
|
*.flac
|
||||||
*.cov
|
*.cov
|
||||||
coverage
|
coverage
|
||||||
|
deps/usr/lib/pa_shim.so
|
||||||
|
deps/usr/lib/pa_shim.dylib
|
||||||
|
deps/usr/lib/pa_shim.dll
|
||||||
|
|
14
.travis.yml
14
.travis.yml
|
@ -1,14 +0,0 @@
|
||||||
# Documentation: http://docs.travis-ci.com/user/languages/julia/
|
|
||||||
language: julia
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
- osx
|
|
||||||
sudo: required
|
|
||||||
julia:
|
|
||||||
- 0.6
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
script:
|
|
||||||
# we can't actually run on travis, so just make sure it's installable
|
|
||||||
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
|
||||||
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); using PortAudio'
|
|
26
Project.toml
Normal file
26
Project.toml
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name = "PortAudio"
|
||||||
|
uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d"
|
||||||
|
repo = "https://github.com/JuliaAudio/PortAudio.jl.git"
|
||||||
|
version = "1.3.0"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7"
|
||||||
|
libportaudio_jll = "2d7b7beb-0762-5160-978e-1ab83a1e8a31"
|
||||||
|
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||||
|
SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
|
||||||
|
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
|
||||||
|
|
||||||
|
[compat]
|
||||||
|
julia = "1.6"
|
||||||
|
alsa_plugins_jll = "1.2.2"
|
||||||
|
libportaudio_jll = "19.6.0"
|
||||||
|
SampledSignals = "2.1.1"
|
||||||
|
Suppressor = "0.2"
|
||||||
|
|
||||||
|
[extras]
|
||||||
|
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||||
|
LibSndFile = "b13ce0c6-77b0-50c6-a2db-140568b8d1a5"
|
||||||
|
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||||
|
|
||||||
|
[targets]
|
||||||
|
test = ["Documenter", "LibSndFile", "Test"]
|
105
README.md
105
README.md
|
@ -1,18 +1,23 @@
|
||||||
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)
|
||||||
|
[![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 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.
|
||||||
|
|
||||||
Installable on Linux and OSX: [![Build Status](https://travis-ci.org/JuliaAudio/PortAudio.jl.svg?branch=master)](https://travis-ci.org/JuliaAudio/PortAudio.jl)
|
|
||||||
|
|
||||||
Installable on Windows: [![Build status](https://ci.appveyor.com/api/projects/status/6x1ha7uvrnel060g/branch/master?svg=true)](https://ci.appveyor.com/project/ssfrr/portaudio-jl/branch/master)
|
|
||||||
|
|
||||||
## Opening a stream
|
## 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.
|
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.
|
||||||
|
If named keyword arguments `latency` or `samplerate` are unspecified, then PortAudio will use device defaults.
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000Hz, blocksize=4096, synced=false)
|
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000, latency=0.1)
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
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
|
||||||
|
@ -26,51 +31,77 @@ You can get a list of your system's devices with the `PortAudio.devices()` funct
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
julia> PortAudio.devices()
|
julia> PortAudio.devices()
|
||||||
6-element Array{PortAudio.PortAudioDevice,1}:
|
14-element Vector{PortAudio.PortAudioDevice}:
|
||||||
PortAudio.PortAudioDevice("AirPlay","Core Audio",0,2,0)
|
"sof-hda-dsp: - (hw:0,0)" 2→2
|
||||||
PortAudio.PortAudioDevice("Built-in Microph","Core Audio",2,0,1)
|
"sof-hda-dsp: - (hw:0,3)" 0→2
|
||||||
PortAudio.PortAudioDevice("Built-in Output","Core Audio",0,2,2)
|
"sof-hda-dsp: - (hw:0,4)" 0→2
|
||||||
PortAudio.PortAudioDevice("JackRouter","Core Audio",2,2,3)
|
"sof-hda-dsp: - (hw:0,5)" 0→2
|
||||||
PortAudio.PortAudioDevice("After Effects 13.5","Core Audio",0,0,4)
|
⋮
|
||||||
PortAudio.PortAudioDevice("Built-In Aggregate","Core Audio",2,2,5)
|
"upmix" 8→8
|
||||||
|
"vdownmix" 6→6
|
||||||
|
"dmix" 0→2
|
||||||
|
"default" 32→32
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
## 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.
|
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 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
|
||||||
|
|
||||||
|
If you are experiencing issues and wish to view detailed logging and debug information, set
|
||||||
|
|
||||||
|
```
|
||||||
|
ENV["JULIA_DEBUG"] = :PortAudio
|
||||||
|
```
|
||||||
|
|
||||||
|
before using the package.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Set up an audio pass-through from microphone to speaker
|
### Set up an audio pass-through from microphone to speaker
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
stream = PortAudioStream(2, 2)
|
stream = PortAudioStream(2, 2)
|
||||||
write(stream, stream)
|
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
|
### Open your built-in microphone and speaker by name
|
||||||
```julia
|
```julia
|
||||||
stream = PortAudioStream("Built-in Microph", "Built-in Output")
|
PortAudioStream("default", "default") do stream
|
||||||
write(stream, stream)
|
write(stream, stream)
|
||||||
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
### Record 10 seconds of audio and save to an ogg file
|
### Record 10 seconds of audio and save to an ogg file
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
julia> using PortAudio, SampledSignals, LibSndFile
|
julia> import LibSndFile # must be in Manifest for FileIO.save to work
|
||||||
|
|
||||||
julia> stream = PortAudioStream("Built-in Microph", 2, 0)
|
julia> using PortAudio: PortAudioStream
|
||||||
PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
|
|
||||||
Samplerate: 48000 s⁻¹
|
julia> using SampledSignals: s
|
||||||
Buffer Size: 4096 frames
|
|
||||||
2 channel source: "Built-in Microph"
|
julia> using FileIO: save
|
||||||
|
|
||||||
|
julia> stream = PortAudioStream(1, 0) # default input (e.g., built-in microphone)
|
||||||
|
PortAudioStream{Float32}
|
||||||
|
Samplerate: 44100.0Hz
|
||||||
|
2 channel source: "default"
|
||||||
|
|
||||||
julia> buf = read(stream, 10s)
|
julia> buf = read(stream, 10s)
|
||||||
480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
|
480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
|
||||||
|
@ -78,16 +109,18 @@ julia> buf = read(stream, 10s)
|
||||||
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
||||||
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
||||||
|
|
||||||
|
julia> close(stream)
|
||||||
|
|
||||||
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
|
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the shim library
|
### Play an audio signal through the default sound output device
|
||||||
|
|
||||||
Because PortAudio calls its callback from a separate audio thread, we can't handle it in Julia directly. To work around this we've included a small shim library written in C that uses ring buffers to pass audio data between the callback context and the main Julia context. To build the shim you'll need a few prerequisites:
|
```julia
|
||||||
|
using PortAudio, SampledSignals
|
||||||
* libportaudio
|
S = 8192 # sampling rate (samples / second)
|
||||||
* make
|
x = cos.(2pi*(1:2S)*440/S) # A440 tone for 2 seconds
|
||||||
* a C compiler (gcc on linux/macOS, mingw64 on Windows)
|
PortAudioStream(0, 2; samplerate=S) do stream
|
||||||
* The `RingBuffers` julia package, installed in a folder next to this one. The portaudio shim links against the `pa_ringbuffer` library that comes with `RingBuffers`.
|
write(stream, x)
|
||||||
|
end
|
||||||
To build the shim, go into the `deps/src` directory and type `make`.
|
```
|
||||||
|
|
6
REQUIRE
6
REQUIRE
|
@ -1,6 +0,0 @@
|
||||||
julia 0.6-
|
|
||||||
BinDeps
|
|
||||||
SampledSignals 0.3.0
|
|
||||||
RingBuffers 1.0.0
|
|
||||||
@osx Homebrew
|
|
||||||
@windows WinRPM
|
|
27
appveyor.yml
27
appveyor.yml
|
@ -1,27 +0,0 @@
|
||||||
environment:
|
|
||||||
matrix:
|
|
||||||
- JULIAVERSION: "julialang/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe"
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
- provider: Email
|
|
||||||
on_build_success: false
|
|
||||||
on_build_failure: false
|
|
||||||
on_build_status_changed: false
|
|
||||||
|
|
||||||
install:
|
|
||||||
# Download most recent Julia Windows binary
|
|
||||||
- ps: (new-object net.webclient).DownloadFile(
|
|
||||||
$("http://s3.amazonaws.com/"+$env:JULIAVERSION),
|
|
||||||
"C:\projects\julia-binary.exe")
|
|
||||||
# Run installer silently, output to C:\projects\julia
|
|
||||||
- C:\projects\julia-binary.exe /S /D=C:\projects\julia
|
|
||||||
|
|
||||||
build_script:
|
|
||||||
# Need to convert from shallow to complete for Pkg.clone to work
|
|
||||||
- IF EXIST .git\shallow (git fetch --unshallow)
|
|
||||||
- C:\projects\julia\bin\julia -e "versioninfo();
|
|
||||||
Pkg.clone(pwd(), \"PortAudio\"); Pkg.build(\"PortAudio\")"
|
|
||||||
|
|
||||||
test_script:
|
|
||||||
# can't actually run the test, so just make sure it's installable
|
|
||||||
- C:\projects\julia\bin\julia -e "using PortAudio"
|
|
26
deps/build.jl
vendored
26
deps/build.jl
vendored
|
@ -1,26 +0,0 @@
|
||||||
using BinDeps
|
|
||||||
using Compat
|
|
||||||
|
|
||||||
@BinDeps.setup
|
|
||||||
|
|
||||||
ENV["JULIA_ROOT"] = abspath(JULIA_HOME, "../../")
|
|
||||||
|
|
||||||
# include alias for WinRPM library
|
|
||||||
libportaudio = library_dependency("libportaudio", aliases=["libportaudio-2"])
|
|
||||||
|
|
||||||
# TODO: add other providers with correct names
|
|
||||||
provides(AptGet, "libportaudio2", libportaudio)
|
|
||||||
provides(Pacman, "portaudio", libportaudio)
|
|
||||||
|
|
||||||
|
|
||||||
@static if is_apple()
|
|
||||||
using Homebrew
|
|
||||||
provides(Homebrew.HB, "portaudio", libportaudio)
|
|
||||||
end
|
|
||||||
|
|
||||||
@static if is_windows()
|
|
||||||
using WinRPM
|
|
||||||
provides(WinRPM.RPM, "libportaudio2", libportaudio, os = :Windows)
|
|
||||||
end
|
|
||||||
|
|
||||||
@BinDeps.install @compat(Dict(:libportaudio => :libportaudio, ))
|
|
68
deps/src/Makefile
vendored
68
deps/src/Makefile
vendored
|
@ -1,68 +0,0 @@
|
||||||
# Makefile lifted from Clang.jl
|
|
||||||
# Copyright (c) 2012-: Isaiah Norton and [contributors](https://github.com/ihnorton/Clang.jl/graphs/contributors)
|
|
||||||
|
|
||||||
ifeq (exists, $(shell [ -e Make.user ] && echo exists ))
|
|
||||||
include Make.user
|
|
||||||
endif
|
|
||||||
|
|
||||||
TARGETDIR = ../usr/lib
|
|
||||||
OBJS = pa_shim.o
|
|
||||||
|
|
||||||
# Figure out OS and architecture
|
|
||||||
OS=$(shell uname)
|
|
||||||
ifneq (,$(findstring MINGW,$(OS)))
|
|
||||||
OS=WINNT
|
|
||||||
endif
|
|
||||||
|
|
||||||
# file extensions and platform-specific libs
|
|
||||||
ifeq ($(OS), WINNT)
|
|
||||||
LIBS +=
|
|
||||||
LDFLAGS += -shared -L../../../RingBuffers/deps/usr/lib -lpa_ringbuffer
|
|
||||||
INC +=
|
|
||||||
SHACMD = sha256sum
|
|
||||||
SHLIB_EXT = dll
|
|
||||||
else ifeq ($(OS), Darwin)
|
|
||||||
LIBS +=
|
|
||||||
INC +=
|
|
||||||
# we'll rely on Julia to load RingBuffers.jl, which will in turn load the C
|
|
||||||
# library that we depend on for these symbols
|
|
||||||
LDFLAGS += -dynamiclib -Wl,-undefined,dynamic_lookup
|
|
||||||
SHLIB_EXT = dylib
|
|
||||||
SHACMD = shasum -a256
|
|
||||||
else
|
|
||||||
LIBS +=
|
|
||||||
INC +=
|
|
||||||
LDFLAGS += -shared
|
|
||||||
SHLIB_EXT = so
|
|
||||||
SHACMD = sha256sum
|
|
||||||
endif
|
|
||||||
|
|
||||||
SOURCEHASH = $(shell $(SHACMD) pa_shim.c | awk '{print $$1}')
|
|
||||||
|
|
||||||
CFLAGS += -I../../../RingBuffers/deps/src -Wall -Wno-strict-aliasing -fno-omit-frame-pointer -fPIC -DSOURCEHASH=\"$(SOURCEHASH)\"
|
|
||||||
LDFLAGS +=
|
|
||||||
# LINUX_LIBS =-lrt
|
|
||||||
# LINUX_LDFLAGS =-rdynamic
|
|
||||||
# add the Homebrew.jl tree to the include dirs in case we used it for
|
|
||||||
# portaudio and libsndfile
|
|
||||||
# DARWIN_LDFLAGS =-L../../../Homebrew/deps/usr/lib
|
|
||||||
# DARWIN_INC =-I../../../Homebrew/deps/usr/include
|
|
||||||
|
|
||||||
TARGET = $(TARGETDIR)/pa_shim.$(SHLIB_EXT)
|
|
||||||
|
|
||||||
.PHONY: clean default
|
|
||||||
|
|
||||||
default: $(TARGET)
|
|
||||||
|
|
||||||
%.o: %.c Makefile
|
|
||||||
$(CC) $< -fPIC -c -o $@ $(INC) $(CFLAGS)
|
|
||||||
|
|
||||||
$(TARGETDIR):
|
|
||||||
mkdir -p $@
|
|
||||||
|
|
||||||
$(TARGET): $(OBJS) $(TARGETDIR) Makefile
|
|
||||||
$(CC) $(OBJS) -shared -o $@ $(LDFLAGS) $(LIBS)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f $(OBJS)
|
|
||||||
rm -f $(TARGET)
|
|
104
deps/src/pa_shim.c
vendored
104
deps/src/pa_shim.c
vendored
|
@ -1,104 +0,0 @@
|
||||||
#include <portaudio.h>
|
|
||||||
#include <pa_ringbuffer.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define MIN(x, y) ((x) < (y) ? (x) : (y))
|
|
||||||
|
|
||||||
typedef enum {
|
|
||||||
PA_SHIM_ERRMSG_OVERFLOW, // input overflow
|
|
||||||
PA_SHIM_ERRMSG_UNDERFLOW, // output underflow
|
|
||||||
PA_SHIM_ERRMSG_ERR_OVERFLOW, // error buffer overflowed
|
|
||||||
} pa_shim_errmsg_t;
|
|
||||||
|
|
||||||
// this callback type is used to notify the Julia side that the portaudio
|
|
||||||
// callback has run
|
|
||||||
typedef void (*pa_shim_notifycb_t)(void *userdata);
|
|
||||||
|
|
||||||
// This struct is shared between the Julia side and C
|
|
||||||
typedef struct {
|
|
||||||
PaUtilRingBuffer *inputbuf; // ringbuffer for input
|
|
||||||
PaUtilRingBuffer *outputbuf; // ringbuffer for output
|
|
||||||
PaUtilRingBuffer *errorbuf; // ringbuffer to send error notifications
|
|
||||||
int sync; // keep input/output ring buffers synchronized (0/1)
|
|
||||||
pa_shim_notifycb_t notifycb; // Julia callback to notify conditions
|
|
||||||
void *inputhandle; // condition to notify on new input
|
|
||||||
void *outputhandle; // condition to notify when ready for output
|
|
||||||
void *errorhandle; // condition to notify on new error
|
|
||||||
} pa_shim_info_t;
|
|
||||||
|
|
||||||
void senderr(pa_shim_info_t *info, pa_shim_errmsg_t msg) {
|
|
||||||
if(PaUtil_GetRingBufferWriteAvailable(info->errorbuf) < 2) {
|
|
||||||
// we've overflowed our error buffer! notify the host.
|
|
||||||
msg = PA_SHIM_ERRMSG_ERR_OVERFLOW;
|
|
||||||
}
|
|
||||||
PaUtil_WriteRingBuffer(info->errorbuf, &msg, 1);
|
|
||||||
if(info->notifycb) {
|
|
||||||
info->notifycb(info->errorhandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the sha256 hash of the shim source so we can make sure things are in sync
|
|
||||||
const char *pa_shim_getsourcehash(void)
|
|
||||||
{
|
|
||||||
// defined on the command-line at build-time
|
|
||||||
return SOURCEHASH;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This routine will be called by the PortAudio engine when audio is needed.
|
|
||||||
* It may called at interrupt level on some machines so don't do anything that
|
|
||||||
* could mess up the system like calling malloc() or free().
|
|
||||||
*/
|
|
||||||
int pa_shim_processcb(const void *input, void *output,
|
|
||||||
unsigned long frameCount,
|
|
||||||
const PaStreamCallbackTimeInfo* timeInfo,
|
|
||||||
PaStreamCallbackFlags statusFlags,
|
|
||||||
void *userData)
|
|
||||||
{
|
|
||||||
pa_shim_info_t *info = userData;
|
|
||||||
if(info->notifycb == NULL) {
|
|
||||||
fprintf(stderr, "pa_shim ERROR: notifycb is NULL\n");
|
|
||||||
}
|
|
||||||
int nwrite;
|
|
||||||
if(info->inputbuf) {
|
|
||||||
nwrite = PaUtil_GetRingBufferWriteAvailable(info->inputbuf);
|
|
||||||
nwrite = MIN(frameCount, nwrite);
|
|
||||||
}
|
|
||||||
int nread;
|
|
||||||
if(info->outputbuf) {
|
|
||||||
nread = PaUtil_GetRingBufferReadAvailable(info->outputbuf);
|
|
||||||
nread = MIN(frameCount, nread);
|
|
||||||
}
|
|
||||||
if(info->inputbuf && info->outputbuf && info->sync) {
|
|
||||||
// to keep the buffers synchronized, set readable and writable to
|
|
||||||
// their minimum value
|
|
||||||
nread = MIN(nread, nwrite);
|
|
||||||
nwrite = nread;
|
|
||||||
}
|
|
||||||
// read/write from the ringbuffers
|
|
||||||
if(info->inputbuf) {
|
|
||||||
PaUtil_WriteRingBuffer(info->inputbuf, input, nwrite);
|
|
||||||
if(info->notifycb) {
|
|
||||||
info->notifycb(info->inputhandle);
|
|
||||||
}
|
|
||||||
if(nwrite < frameCount) {
|
|
||||||
senderr(info, PA_SHIM_ERRMSG_OVERFLOW);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(info->outputbuf) {
|
|
||||||
PaUtil_ReadRingBuffer(info->outputbuf, output, nread);
|
|
||||||
if(info->notifycb) {
|
|
||||||
info->notifycb(info->outputhandle);
|
|
||||||
}
|
|
||||||
if(nread < frameCount) {
|
|
||||||
senderr(info, PA_SHIM_ERRMSG_UNDERFLOW);
|
|
||||||
// we didn't fill the whole output buffer, so zero it out
|
|
||||||
memset(output+nread*info->outputbuf->elementSizeBytes, 0,
|
|
||||||
(frameCount - nread)*info->outputbuf->elementSizeBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return paContinue;
|
|
||||||
}
|
|
BIN
deps/usr/lib/pa_shim.dylib
vendored
BIN
deps/usr/lib/pa_shim.dylib
vendored
Binary file not shown.
BIN
deps/usr/lib/pa_shim.so
vendored
BIN
deps/usr/lib/pa_shim.so
vendored
Binary file not shown.
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,43 +1,49 @@
|
||||||
using PortAudio
|
using PortAudio
|
||||||
|
|
||||||
"""Continuously read from the default audio input and plot an
|
"""
|
||||||
ASCII level/peak meter"""
|
Continuously read from the default audio input and plot an
|
||||||
|
ASCII level/peak meter
|
||||||
|
"""
|
||||||
function micmeter(metersize)
|
function micmeter(metersize)
|
||||||
mic = PortAudioStream(1, 0; blocksize=512)
|
mic = PortAudioStream(1, 0; latency = 0.1)
|
||||||
|
|
||||||
signalmax = zero(eltype(mic))
|
signalmax = zero(eltype(mic))
|
||||||
println("Press Ctrl-C to quit")
|
println("Press Ctrl-C to quit")
|
||||||
while true
|
while true
|
||||||
block = read(mic, 512)
|
block = read(mic, 512)
|
||||||
blockmax = maximum(abs(block)) # find the maximum value in the block
|
blockmax = maximum(abs.(block)) # find the maximum value in the block
|
||||||
signalmax = max(signalmax, blockmax) # keep the maximum value ever
|
signalmax = max(signalmax, blockmax) # keep the maximum value ever
|
||||||
print("\r") # reset the cursor to the beginning of the line
|
print("\r") # reset the cursor to the beginning of the line
|
||||||
printmeter(metersize, blockmax, signalmax)
|
printmeter(metersize, blockmax, signalmax)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
"""Print an ASCII level meter of the given size. Signal and peak
|
"""
|
||||||
levels are assumed to be scaled from 0.0-1.0, with peak >= signal"""
|
Print an ASCII level meter of the given size. Signal and peak
|
||||||
|
levels are assumed to be scaled from 0.0-1.0, with peak >= signal
|
||||||
|
"""
|
||||||
function printmeter(metersize, signal, peak)
|
function printmeter(metersize, signal, peak)
|
||||||
# calculate the positions in terms of characters
|
# calculate the positions in terms of characters
|
||||||
peakpos = clamp(round(Int, peak * metersize), 0, metersize)
|
peakpos = clamp(round(Int, peak * metersize), 0, metersize)
|
||||||
meterchars = clamp(round(Int, signal * metersize), 0, peakpos-1)
|
meterchars = clamp(round(Int, signal * metersize), 0, peakpos - 1)
|
||||||
blankchars = max(0, peakpos-meterchars-1)
|
blankchars = max(0, peakpos - meterchars - 1)
|
||||||
|
|
||||||
for position in 1:meterchars
|
for position in 1:meterchars
|
||||||
print_with_color(barcolor(metersize, position), ">")
|
printstyled(">", color = barcolor(metersize, position))
|
||||||
end
|
end
|
||||||
|
|
||||||
print(" " ^ blankchars)
|
print(" "^blankchars)
|
||||||
print_with_color(barcolor(metersize, peakpos), "|")
|
printstyled("|", color = barcolor(metersize, peakpos))
|
||||||
print(" " ^ (metersize - peakpos))
|
print(" "^(metersize - peakpos))
|
||||||
end
|
end
|
||||||
|
|
||||||
"""Compute the proper color for a given position in the bar graph. The first
|
"""
|
||||||
|
Compute the proper color for a given position in the bar graph. The first
|
||||||
half of the bar should be green, then the remainder is yellow except the final
|
half of the bar should be green, then the remainder is yellow except the final
|
||||||
character, which is red."""
|
character, which is red.
|
||||||
|
"""
|
||||||
function barcolor(metersize, position)
|
function barcolor(metersize, position)
|
||||||
if position/metersize <= 0.5
|
if position / metersize <= 0.5
|
||||||
:green
|
:green
|
||||||
elseif position == metersize
|
elseif position == metersize
|
||||||
:red
|
:red
|
||||||
|
|
|
@ -1,127 +1,156 @@
|
||||||
# Thanks to Jiahao Chen for this great example!
|
using Distributed, PortAudio
|
||||||
|
|
||||||
##
|
# Modified from Jiahao Chen's example in the obsolete AudioIO module.
|
||||||
## NOTE: THIS NEEDS TO BE PORTED OVER TO THE NEW ARCHITECTURE
|
# Will use first output device found in system's listing or DEFAULTDEVICE if set below
|
||||||
##
|
const DEFAULTDEVICE = -1
|
||||||
|
|
||||||
using AudioIO
|
function paudio()
|
||||||
import AudioIO.play
|
devs = PortAudio.devices()
|
||||||
|
if DEFAULTDEVICE < 0
|
||||||
|
devnum = findfirst(x -> x.maxoutchans > 0, devs)
|
||||||
|
(devnum == nothing) && error("No output device for audio found")
|
||||||
|
else
|
||||||
|
devnum = DEFAULTDEVICE + 1
|
||||||
|
end
|
||||||
|
return ostream = PortAudioStream(devs[devnum].name, 0, 2)
|
||||||
|
end
|
||||||
|
|
||||||
type note{S<:Real, T<:Real}
|
play(ostream, sample::Array{Float64, 1}) = write(ostream, sample)
|
||||||
|
play(ostr, sample::Array{Int64, 1}) = play(ostr, Float64.(sample))
|
||||||
|
|
||||||
|
struct Note{S <: Real, T <: Real}
|
||||||
pitch::S
|
pitch::S
|
||||||
duration::T
|
duration::T
|
||||||
sustained::Bool
|
sustained::Bool
|
||||||
end
|
end
|
||||||
|
|
||||||
function play(A::note, samplingfreq::Real=44100, shape::Function=t->0.6sin(t)+0.2sin(2t)+.05*sin(8t))
|
function play(
|
||||||
timesamples=0:1/samplingfreq:(A.duration*(A.sustained ? 0.98 : 0.9))
|
ostream,
|
||||||
v = Float64[shape(2π*A.pitch*t) for t in timesamples]
|
A::Note,
|
||||||
|
samplingfreq::Real = 44100,
|
||||||
|
shape::Function = t -> 0.6sin(t) + 0.2sin(2t) + 0.05 * sin(8t),
|
||||||
|
)
|
||||||
|
timesamples = 0:(1 / samplingfreq):(A.duration * (A.sustained ? 0.98 : 0.9))
|
||||||
|
v = Float64[shape(2π * A.pitch * t) for t in timesamples]
|
||||||
if !A.sustained
|
if !A.sustained
|
||||||
decay_length = int(length(timesamples) * 0.2)
|
decay_length = div(length(timesamples), 5)
|
||||||
v[end-decay_length:end-1] = v[end-decay_length:end-1] .* linspace(1, 0, decay_length)
|
v[(end - decay_length):(end - 1)] =
|
||||||
|
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
|
||||||
end
|
end
|
||||||
play(v)
|
play(ostream, v)
|
||||||
sleep(A.duration)
|
sleep(A.duration)
|
||||||
end
|
end
|
||||||
|
|
||||||
function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
|
function parsevoice(melody::String; tempo = 132, beatunit = 4, lyrics = nothing)
|
||||||
play([0]) #Force AudioIO to initialize
|
ostream = paudio() # initialize audio for output
|
||||||
lyrics_syllables = lyrics==nothing? nothing : split(lyrics)
|
lyrics_syllables = lyrics == nothing ? nothing : split(lyrics)
|
||||||
|
lyrics_syllables != nothing && (lyrics_syllables[end] *= "\n")
|
||||||
note_idx = 1
|
note_idx = 1
|
||||||
oldduration = 4
|
oldduration = 4
|
||||||
for line in split(melody, '\n')
|
for line in split(melody, '\n')
|
||||||
percent_idx = findfirst(line, '%') #Trim comment
|
percent_idx = findfirst('%', line) # Trim comment
|
||||||
percent_idx == 0 || (line = line[1:percent_idx-1])
|
percent_idx == nothing || (line = line[1:(percent_idx - 1)])
|
||||||
for token in split(line)
|
for token in split(line)
|
||||||
pitch, duration, dotted, sustained =parsetoken(token)
|
pitch, duration, dotted, sustained = parsetoken(token)
|
||||||
duration==nothing && (duration = oldduration)
|
duration == nothing && (duration = oldduration)
|
||||||
oldduration = duration
|
oldduration = duration
|
||||||
dotted && (duration *= 1.5)
|
dotted && (duration *= 1.5)
|
||||||
if lyrics_syllables!=nothing && 1<=note_idx<=length(lyrics_syllables) #Print the lyrics, omitting hyphens
|
if lyrics_syllables != nothing && 1 <= note_idx <= length(lyrics_syllables)
|
||||||
if lyrics_syllables[note_idx][end]=='-'
|
# Print the lyrics, omitting hyphens
|
||||||
print(lyrics_syllables[note_idx][1:end-1])
|
if lyrics_syllables[note_idx][end] == '-'
|
||||||
|
print(join(split(lyrics_syllables[note_idx][:], "")[1:(end - 1)]), "")
|
||||||
else
|
else
|
||||||
print(lyrics_syllables[note_idx], ' ')
|
print(lyrics_syllables[note_idx], ' ')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
play(note(pitch, (beatunit/duration)*(60/tempo), sustained))
|
play(ostream, Note(pitch, (beatunit / duration) * (60 / tempo), sustained))
|
||||||
note_idx += 1
|
note_idx += 1
|
||||||
end
|
end
|
||||||
println()
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function parsetoken(token::String, Atuning::Real=220)
|
function parsetoken(token, Atuning::Real = 220)
|
||||||
state = :findpitch
|
state = :findpitch
|
||||||
pitch = 0.0
|
pitch = 0.0
|
||||||
sustain = dotted = false
|
sustain = dotted = false
|
||||||
lengthbuf = Char[]
|
lengthbuf = Char[]
|
||||||
for char in token
|
for char in token
|
||||||
if state == :findpitch
|
if state == :findpitch
|
||||||
scale_idx = findfirst('a':'g', char) + findfirst('A':'G', char)
|
scale_idx =
|
||||||
if scale_idx!=0
|
something(findfirst(char, String(collect('a':'g'))), 0) +
|
||||||
const halfsteps = [12, 14, 3, 5, 7, 8, 10]
|
something(findfirst(char, String(collect('A':'G'))), 0)
|
||||||
pitch = Atuning*2^(halfsteps[scale_idx]/12)
|
if scale_idx != 0
|
||||||
|
halfsteps = [12, 14, 3, 5, 7, 8, 10]
|
||||||
|
pitch = Atuning * 2^(halfsteps[scale_idx] / 12)
|
||||||
state = :findlength
|
state = :findlength
|
||||||
elseif char=='r'
|
elseif char == 'r'
|
||||||
pitch, state = 0, :findlength
|
pitch, state = 0, :findlength
|
||||||
else
|
else
|
||||||
error("unknown pitch: $char")
|
error("unknown pitch: $char")
|
||||||
end
|
end
|
||||||
elseif state == :findlength
|
elseif state == :findlength
|
||||||
if char == '#' ; pitch *= 2^(1/12) #sharp
|
if char == '#'
|
||||||
elseif char == 'b' ; pitch /= 2^(1/12) #flat
|
pitch *= 2^(1 / 12) # sharp
|
||||||
elseif char == '\''; pitch *= 2 #higher octave
|
elseif char == 'b'
|
||||||
elseif char == ',' ; pitch /= 2 #lower octave
|
pitch /= 2^(1 / 12) # flat
|
||||||
elseif char == '.' ; dotted = true #dotted note
|
elseif char == '\''
|
||||||
elseif char == '~' ; sustain = true #tied note
|
pitch *= 2 # higher octave
|
||||||
|
elseif char == ','
|
||||||
|
pitch /= 2 # lower octave
|
||||||
|
elseif char == '.'
|
||||||
|
dotted = true # dotted note
|
||||||
|
elseif char == '~'
|
||||||
|
sustain = true # tied note
|
||||||
else
|
else
|
||||||
push!(lengthbuf, char)
|
push!(lengthbuf, char)
|
||||||
#Check for "is" and "es" suffixes for sharps and flats
|
# Check for "is" and "es" suffixes for sharps and flats
|
||||||
if length(lengthbuf) >= 2
|
if length(lengthbuf) >= 2
|
||||||
if lengthbuf[end-1:end] == "is"
|
if lengthbuf[(end - 1):end] == "is"
|
||||||
pitch *= 2^(1/12)
|
pitch *= 2^(1 / 12)
|
||||||
lengthbuf = lengthbuf[1:end-2]
|
lengthbuf = lengthbuf[1:(end - 2)]
|
||||||
elseif lengthbuf[end-1:end] == "es"
|
elseif lengthbuf[(end - 1):end] == "es"
|
||||||
pitch /= 2^(1/12)
|
pitch /= 2^(1 / 12)
|
||||||
lengthbuf = lengthbuf[1:end-2]
|
lengthbuf = lengthbuf[1:(end - 2)]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
#finalize length
|
#finalize length
|
||||||
lengthstr = convert(String, lengthbuf)
|
lengthstr = String(lengthbuf)
|
||||||
duration = isempty(lengthstr) ? nothing : parseint(lengthstr)
|
duration = isempty(lengthstr) ? nothing : tryparse(Int, lengthstr)
|
||||||
return (pitch, duration, sustain, dotted)
|
return (pitch, duration, sustain, dotted)
|
||||||
end
|
end
|
||||||
|
|
||||||
parsevoice("""
|
parsevoice(
|
||||||
|
"""
|
||||||
f# f# g a a g f# e d d e f# f#~ f#8 e e2
|
f# f# g a a g f# e d d e f# f#~ f#8 e e2
|
||||||
f#4 f# g a a g f# e d d e f# e~ e8 d d2
|
f#4 f# g a a g f# e d d e f# e~ e8 d d2
|
||||||
e4 e f# d e f#8~ g8 f#4 d e f#8~ g f#4 e d e a,
|
e4 e f# d e f#8~ g8 f#4 d e f#8~ g f#4 e d e a,
|
||||||
f#2 f#4 g a a g f# e d d e f# e~ e8 d8 d2""",
|
f#2 f#4 g a a g f# e d d e f# e~ e8 d8 d2""",
|
||||||
lyrics="""
|
lyrics = """
|
||||||
Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!
|
Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!
|
||||||
Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!
|
Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!
|
||||||
Dei- ne Zau- ber bin den - wie- der, was die - Mo- de streng ge- theilt,
|
Dei- ne Zau- ber bin den - wie- der, was die - Mo- de streng ge- theilt,
|
||||||
al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt.
|
al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt.
|
||||||
""")
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
# And now with harmony!
|
# And now with harmony!
|
||||||
|
|
||||||
soprano = @async parsevoice("""
|
soprano = @spawn parsevoice(
|
||||||
|
"""
|
||||||
f'#. f'#. g'. a'. a'. g'. f'#. e'~ e'8 d.'4 d.' e.' f#'. f#'.~ f#' e'8 e'4~ e'2
|
f'#. f'#. g'. a'. a'. g'. f'#. e'~ e'8 d.'4 d.' e.' f#'. f#'.~ f#' e'8 e'4~ e'2
|
||||||
""", lyrics="Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!"
|
""",
|
||||||
|
lyrics = "Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!",
|
||||||
)
|
)
|
||||||
alto = @async parsevoice("""
|
alto = @spawn parsevoice("""
|
||||||
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2
|
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2
|
||||||
""")
|
""")
|
||||||
tenor = @async parsevoice("""
|
tenor = @spawn parsevoice("""
|
||||||
d. d. e. f#. f#. e. d. d~ e8 f#.4 f#. a,. d. d.~ d c#8 c#4 c#2
|
d. d. e. f#. f#. e. d. d~ e8 f#.4 f#. a,. d. d.~ d c#8 c#4 c#2
|
||||||
""")
|
""")
|
||||||
bass = @async parsevoice("""
|
bass = @spawn parsevoice("""
|
||||||
d. d. d. d. a,. a,. a,. b,~ c8 d. a., a., a., a., a, a8, a,4 a,2
|
d. d. d. d. a,. a,. a,. b,~ c8 d. a., a., a., a., a, a8, a,4 a,2
|
||||||
""")
|
""")
|
||||||
wait(soprano)
|
wait(soprano)
|
||||||
|
@ -129,19 +158,21 @@ wait(alto)
|
||||||
wait(tenor)
|
wait(tenor)
|
||||||
wait(bass)
|
wait(bass)
|
||||||
|
|
||||||
soprano = @async parsevoice("""
|
soprano = @spawn parsevoice(
|
||||||
|
"""
|
||||||
f'#.4 f'#. g'. a'. a'. g'. f'#. e'. d'. d'. e'. f'#. e'.~ e' d'8 d'4~ d'2
|
f'#.4 f'#. g'. a'. a'. g'. f'#. e'. d'. d'. e'. f'#. e'.~ e' d'8 d'4~ d'2
|
||||||
""", lyrics="Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!")
|
""",
|
||||||
alto = @async parsevoice("""
|
lyrics = "Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!",
|
||||||
|
)
|
||||||
|
alto = @spawn parsevoice("""
|
||||||
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2
|
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2
|
||||||
""")
|
""")
|
||||||
tenor = @async parsevoice("""
|
tenor = @spawn parsevoice("""
|
||||||
d.4 d. d. d. d. d. d. d. d. d. c#. d. c#.~ c# d8 d d2
|
d.4 d. d. d. d. d. d. d. d. d. c#. d. c#.~ c# d8 d d2
|
||||||
""")
|
""")
|
||||||
bass = @async parsevoice("""
|
bass = @spawn parsevoice("""
|
||||||
d.4 d. d. d. a,. a,. a,. a., a., a., a., a., a.,~ a, a,8 d, d,2
|
d.4 d. d. d. a,. a,. a,. a., a., a., a., a., a.,~ a, a,8 d, d,2
|
||||||
""")
|
""")
|
||||||
|
|
||||||
wait(soprano)
|
wait(soprano)
|
||||||
wait(alto)
|
wait(alto)
|
||||||
wait(tenor)
|
wait(tenor)
|
||||||
|
|
65
examples/measure_latency.jl
Normal file
65
examples/measure_latency.jl
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
using PortAudio
|
||||||
|
using DSP
|
||||||
|
|
||||||
|
function create_measure_signal()
|
||||||
|
signal = zeros(Float32, 20000)
|
||||||
|
for i in 1:3
|
||||||
|
signal = vcat(signal, rand(Float32, 100), zeros(Float32, i * 10000))
|
||||||
|
end
|
||||||
|
return signal
|
||||||
|
end
|
||||||
|
|
||||||
|
function measure_latency(in_latency = 0.1, out_latency = 0.1; is_warmup = false)
|
||||||
|
in_stream = PortAudioStream(1, 0; latency = in_latency)
|
||||||
|
out_stream = PortAudioStream(0, 1; latency = out_latency)
|
||||||
|
|
||||||
|
cond = Base.Event()
|
||||||
|
|
||||||
|
writer_start_time = Int64(0)
|
||||||
|
reader_start_time = Int64(0)
|
||||||
|
|
||||||
|
reader = Threads.@spawn begin
|
||||||
|
wait(cond)
|
||||||
|
writer_start_time = time_ns() |> Int64
|
||||||
|
return read(in_stream, 100000)
|
||||||
|
end
|
||||||
|
|
||||||
|
signal = create_measure_signal()
|
||||||
|
writer = Threads.@spawn begin
|
||||||
|
wait(cond)
|
||||||
|
reader_start_time = time_ns() |> Int64
|
||||||
|
write(out_stream, signal)
|
||||||
|
end
|
||||||
|
|
||||||
|
notify(cond)
|
||||||
|
|
||||||
|
wait(reader)
|
||||||
|
wait(writer)
|
||||||
|
|
||||||
|
recorded = collect(reader.result)[:, 1]
|
||||||
|
|
||||||
|
close(in_stream)
|
||||||
|
close(out_stream)
|
||||||
|
|
||||||
|
diff = reader_start_time - writer_start_time |> abs
|
||||||
|
|
||||||
|
diff_in_ms = diff / 10^6 # 1 ms = 10^6 ns
|
||||||
|
|
||||||
|
if !is_warmup && diff_in_ms > 1
|
||||||
|
@warn "Threads start time difference $diff_in_ms ms is bigger than 1 ms"
|
||||||
|
end
|
||||||
|
|
||||||
|
delay = finddelay(recorded, signal) / 48000
|
||||||
|
|
||||||
|
return trunc(Int, delay * 1000)# result in ms
|
||||||
|
end
|
||||||
|
|
||||||
|
measure_latency(0.1, 0.1; is_warmup = true) # warmup
|
||||||
|
|
||||||
|
latencies = [0.1, 0.01, 0.005]
|
||||||
|
for in_latency in latencies
|
||||||
|
for out_latency in latencies
|
||||||
|
measure = measure_latency(in_latency, out_latency)
|
||||||
|
println("$measure ms latency for in_latency=$in_latency, out_latency=$out_latency")
|
||||||
|
end
|
||||||
|
end
|
89
examples/octave-shift.jl
Normal file
89
examples/octave-shift.jl
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
#=
|
||||||
|
This code illustrates real-time octave down shift
|
||||||
|
using a crude FFT-based method.
|
||||||
|
It also plots the input and output signals and their spectra.
|
||||||
|
|
||||||
|
This code uses the system defaults for the audio input and output devices.
|
||||||
|
If you use the built-in speakers and built-in microphone,
|
||||||
|
you will likely get undesirable audio feedback.
|
||||||
|
|
||||||
|
It works "best" if you play the audio output through headphones
|
||||||
|
so that the output does not feed back into the input.
|
||||||
|
|
||||||
|
The spectrum plotting came from the example in
|
||||||
|
https://github.com/JuliaAudio/PortAudio.jl/blob/master/examples
|
||||||
|
=#
|
||||||
|
|
||||||
|
using PortAudio: PortAudioStream
|
||||||
|
using SampledSignals: Hz, domain
|
||||||
|
using SampledSignals: (..) # see EllipsisNotation.jl and IntervalSets.jl
|
||||||
|
using FFTW: fft, ifft
|
||||||
|
using Plots: plot, gui, default; default(label="")
|
||||||
|
|
||||||
|
|
||||||
|
function pitch_halver(x) # decrease pitch by one octave via FFT
|
||||||
|
N = length(x)
|
||||||
|
mod(N,2) == 0 || throw("N must be multiple of 2")
|
||||||
|
F = fft(x) # original spectrum
|
||||||
|
Fnew = [F[1:N÷2]; zeros(N+1); F[(N÷2+2):N]]
|
||||||
|
out = 2 * real(ifft(Fnew))[1:N]
|
||||||
|
out.samplerate /= 2 # trick!
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Plot input and output signals and their spectra.
|
||||||
|
# Quantize the vertical axis limits to reduce plot jitter.
|
||||||
|
function plotter(buf, out, N, fmin, fmax, fs; quant::Number = 0.1)
|
||||||
|
bmax = quant * ceil(maximum(abs, buf) / quant)
|
||||||
|
xticks = [1, N]; ylims = (-1,1) .* bmax; yticks = (-1:1)*bmax
|
||||||
|
p1 = plot(buf; xticks, ylims, yticks, title="input")
|
||||||
|
p3 = plot(out; xticks, ylims, yticks, title="output")
|
||||||
|
|
||||||
|
X = (2/N) * abs.(fft(buf)[fmin..fmax]) # spectrum
|
||||||
|
Xmax = quant * ceil(maximum(X) / quant)
|
||||||
|
xlims = (fs[1], fs[end]); ylims = (0, Xmax); yticks = [0,Xmax]
|
||||||
|
p2 = plot(fs, X; xlims, ylims, yticks)
|
||||||
|
|
||||||
|
Y = (2/N) * abs.(fft(out)[fmin..fmax])
|
||||||
|
p4 = plot(fs, Y; xlims, ylims, yticks)
|
||||||
|
|
||||||
|
plot(p1, p2, p3, p4)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
octave_shift(seconds; N, ...)
|
||||||
|
|
||||||
|
Shift audio down by one octave.
|
||||||
|
|
||||||
|
# Input
|
||||||
|
* `seconds` : how long to run in seconds; defaults to 300 (5 minutes)
|
||||||
|
|
||||||
|
# Options
|
||||||
|
* `N` : buffer size; default 1024 samples
|
||||||
|
* `fmin`,`fmax` : range of frequencies to display; default 0Hz to 4000Hz
|
||||||
|
"""
|
||||||
|
function octave_shift(
|
||||||
|
seconds::Number = 300;
|
||||||
|
N::Int = 1024,
|
||||||
|
fmin::Number = 0Hz,
|
||||||
|
fmax::Number = 4000Hz,
|
||||||
|
# undocumented options below here that are unlikely to be modified
|
||||||
|
in_stream = PortAudioStream(1, 0), # default input device
|
||||||
|
out_stream = PortAudioStream(0, 1), # default output device
|
||||||
|
buf::AbstractArray = read(in_stream, N), # warm-up
|
||||||
|
fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])],
|
||||||
|
Niters::Int = ceil(Int, seconds * in_stream.sample_rate / N),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _ in 1:Niters
|
||||||
|
read!(in_stream, buf)
|
||||||
|
out = pitch_halver(buf) # decrease pitch by one octave
|
||||||
|
write(out_stream, out)
|
||||||
|
plotter(buf, out, N, fmin, fmax, fs); gui()
|
||||||
|
end
|
||||||
|
nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
octave_shift(5)
|
|
@ -3,10 +3,10 @@
|
||||||
|
|
||||||
module SpectrumExample
|
module SpectrumExample
|
||||||
|
|
||||||
using GR, PortAudio, SampledSignals
|
using GR, PortAudio, SampledSignals, FFTW
|
||||||
|
|
||||||
const N = 1024
|
const N = 1024
|
||||||
const stream = PortAudioStream(1, 0, blocksize=N)
|
const stream = PortAudioStream(1, 0)
|
||||||
const buf = read(stream, N)
|
const buf = read(stream, N)
|
||||||
const fmin = 0Hz
|
const fmin = 0Hz
|
||||||
const fmax = 10000Hz
|
const fmax = 10000Hz
|
||||||
|
@ -14,7 +14,7 @@ const fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
|
||||||
|
|
||||||
while true
|
while true
|
||||||
read!(stream, buf)
|
read!(stream, buf)
|
||||||
plot(fs, abs(fft(buf)[fmin..fmax]), xlim=(fs[1],fs[end]), ylim=(0,100))
|
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100))
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
21
examples/tone-buffer.jl
Normal file
21
examples/tone-buffer.jl
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
#=
|
||||||
|
This example illustrates synthesizing a long tone in small pieces
|
||||||
|
and routing it to the default audio output device using `write()`.
|
||||||
|
=#
|
||||||
|
|
||||||
|
using PortAudio: PortAudioStream, write
|
||||||
|
|
||||||
|
stream = PortAudioStream(0, 1; warn_xruns=false)
|
||||||
|
|
||||||
|
function play_tone(stream, freq::Real, duration::Real; buf_size::Int = 1024)
|
||||||
|
S = stream.sample_rate
|
||||||
|
current = 1
|
||||||
|
while current < duration*S
|
||||||
|
x = 0.7 * sin.(2π * (current .+ (1:buf_size)) * freq / S)
|
||||||
|
write(stream, x)
|
||||||
|
current += buf_size
|
||||||
|
end
|
||||||
|
nothing
|
||||||
|
end
|
||||||
|
|
||||||
|
play_tone(stream, 440, 2)
|
67
examples/waterfall_heatmap.jl
Normal file
67
examples/waterfall_heatmap.jl
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
using Makie
|
||||||
|
using PortAudio
|
||||||
|
using DSP
|
||||||
|
|
||||||
|
"""
|
||||||
|
Slide the values in the given matrix to the right by 1.
|
||||||
|
The rightmosts column is discarded and the leftmost column is
|
||||||
|
left alone.
|
||||||
|
"""
|
||||||
|
function shift1!(buf::AbstractMatrix)
|
||||||
|
for col in size(buf, 2):-1:2
|
||||||
|
@. buf[:, col] = buf[:, col - 1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
takes a block of audio, FFT it, and write it to the beginning of the buffer
|
||||||
|
"""
|
||||||
|
function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan)
|
||||||
|
readbuf .*= win
|
||||||
|
A_mul_B!(fftbuf, fftplan, readbuf)
|
||||||
|
shift1!(dispbuf)
|
||||||
|
@. dispbuf[end:-1:1, 1] = log(clamp(abs(fftbuf[1:D]), 0.0001, Inf))
|
||||||
|
end
|
||||||
|
|
||||||
|
function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
|
||||||
|
read!(src, buf)
|
||||||
|
for dispbuf in dispbufs
|
||||||
|
processbuf!(buf, win, dispbuf, fftbuf, fftplan)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
N = 1024 # size of audio read
|
||||||
|
N2 = N ÷ 2 + 1 # size of rfft output
|
||||||
|
D = 200 # number of bins to display
|
||||||
|
M = 200 # amount of history to keep
|
||||||
|
src = PortAudioStream(1, 2)
|
||||||
|
buf = Array{Float32}(N) # buffer for reading
|
||||||
|
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
|
||||||
|
fftbuf = Array{Complex{Float32}}(N2) # destination buf for FFT
|
||||||
|
dispbufs = [zeros(Float32, D, M) for i in 1:5, j in 1:5] # STFT bufs
|
||||||
|
win = gaussian(N, 0.125)
|
||||||
|
|
||||||
|
scene = Scene(resolution = (1000, 1000))
|
||||||
|
|
||||||
|
#pre-fill the display buffer so we can do a reasonable colormap
|
||||||
|
for _ in 1:M
|
||||||
|
processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
|
||||||
|
end
|
||||||
|
|
||||||
|
heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf
|
||||||
|
i = ibuf[1]
|
||||||
|
buf = ibuf[2]
|
||||||
|
|
||||||
|
# some function of the 2D index and the value
|
||||||
|
heatmap(buf, offset = (i[2] * size(buf, 2), i[1] * size(buf, 1)))
|
||||||
|
end
|
||||||
|
|
||||||
|
center!(scene)
|
||||||
|
|
||||||
|
while isopen(scene[:screen])
|
||||||
|
processblock!(src, buf, dispbufs, fftbuf, fftplan)
|
||||||
|
for (hm, db) in zip(heatmaps, dispbufs)
|
||||||
|
hm[:heatmap] = db
|
||||||
|
end
|
||||||
|
render_frame(scene)
|
||||||
|
end
|
43
examples/waterfall_lines.jl
Normal file
43
examples/waterfall_lines.jl
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
using Makie, GeometryTypes
|
||||||
|
using PortAudio
|
||||||
|
|
||||||
|
N = 1024 # size of audio read
|
||||||
|
N2 = N ÷ 2 + 1 # size of rfft output
|
||||||
|
D = 200 # number of bins to display
|
||||||
|
M = 100 # number of lines to draw
|
||||||
|
S = 0.5 # motion speed of lines
|
||||||
|
src = PortAudioStream(1, 2)
|
||||||
|
buf = Array{Float32}(N)
|
||||||
|
fftbuf = Array{Complex{Float32}}(N2)
|
||||||
|
magbuf = Array{Float32}(N2)
|
||||||
|
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
|
||||||
|
|
||||||
|
scene = Scene(resolution = (500, 500))
|
||||||
|
ax = axis(0:0.1:1, 0:0.1:1, 0:0.1:0.5)
|
||||||
|
center!(scene)
|
||||||
|
|
||||||
|
ls = map(1:M) do _
|
||||||
|
yoffset = to_node(to_value(scene[:time]))
|
||||||
|
offset = lift_node(scene[:time], yoffset) do t, yoff
|
||||||
|
Point3f0(0.0f0, (t - yoff) * S, 0.0f0)
|
||||||
|
end
|
||||||
|
l = lines(
|
||||||
|
linspace(0, 1, D),
|
||||||
|
0.0f0,
|
||||||
|
zeros(Float32, D),
|
||||||
|
offset = offset,
|
||||||
|
color = (:black, 0.1),
|
||||||
|
)
|
||||||
|
(yoffset, l)
|
||||||
|
end
|
||||||
|
|
||||||
|
while isopen(scene[:screen])
|
||||||
|
for (yoffset, line) in ls
|
||||||
|
isopen(scene[:screen]) || break
|
||||||
|
read!(src, buf)
|
||||||
|
A_mul_B!(fftbuf, fftplan, buf)
|
||||||
|
@. magbuf = log(clamp(abs(fftbuf), 0.0001, Inf)) / 10 + 0.5
|
||||||
|
line[:z] = magbuf[1:D]
|
||||||
|
push!(yoffset, to_value(scene[:time]))
|
||||||
|
end
|
||||||
|
end
|
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
|
11
runtests.sh
11
runtests.sh
|
@ -1,11 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Runs the tests including generating an lcov.info file
|
|
||||||
|
|
||||||
# abort on failure
|
|
||||||
set -e
|
|
||||||
|
|
||||||
julia -e 'using Coverage; clean_folder(".");'
|
|
||||||
julia --color=yes --inline=no --code-coverage=user test/runtests.jl
|
|
||||||
mkdir -p coverage
|
|
||||||
julia -e 'using Coverage; res=process_folder(); LCOV.writefile("coverage/lcov.info", res)'
|
|
1304
src/PortAudio.jl
1304
src/PortAudio.jl
File diff suppressed because it is too large
Load diff
|
@ -1,250 +1,439 @@
|
||||||
# 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 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{Void}
|
|
||||||
const PaStreamCallback = Void
|
|
||||||
const PaStreamFlags = Culong
|
|
||||||
|
|
||||||
const paNoFlag = PaStreamFlags(0x00)
|
@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
|
||||||
|
|
||||||
const PA_NO_ERROR = 0
|
function Pa_GetErrorText(errorCode)
|
||||||
const PA_INPUT_OVERFLOWED = -10000 + 19
|
ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), errorCode)
|
||||||
const PA_OUTPUT_UNDERFLOWED = -10000 + 20
|
end
|
||||||
|
|
||||||
# sample format types
|
|
||||||
const paFloat32 = PaSampleFormat(0x01)
|
|
||||||
const paInt32 = PaSampleFormat(0x02)
|
|
||||||
const paInt24 = PaSampleFormat(0x04)
|
|
||||||
const paInt16 = PaSampleFormat(0x08)
|
|
||||||
const paInt8 = PaSampleFormat(0x10)
|
|
||||||
const paUInt8 = PaSampleFormat(0x20)
|
|
||||||
const paNonInterleaved = PaSampleFormat(0x80000000)
|
|
||||||
|
|
||||||
const type_to_fmt = Dict{Type, PaSampleFormat}(
|
|
||||||
Float32 => 1,
|
|
||||||
Int32 => 2,
|
|
||||||
# Int24 => 4,
|
|
||||||
Int16 => 8,
|
|
||||||
Int8 => 16,
|
|
||||||
UInt8 => 3
|
|
||||||
)
|
|
||||||
|
|
||||||
const PaStreamCallbackResult = Cint
|
|
||||||
# Callback return values
|
|
||||||
const paContinue = PaStreamCallbackResult(0)
|
|
||||||
const paComplete = PaStreamCallbackResult(1)
|
|
||||||
const paAbort = PaStreamCallbackResult(2)
|
|
||||||
|
|
||||||
function Pa_Initialize()
|
function Pa_Initialize()
|
||||||
err = ccall((:Pa_Initialize, libportaudio), PaError, ())
|
ccall((:Pa_Initialize, libportaudio), PaError, ())
|
||||||
handle_status(err)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_Terminate()
|
function Pa_Terminate()
|
||||||
err = ccall((:Pa_Terminate, libportaudio), PaError, ())
|
ccall((:Pa_Terminate, libportaudio), PaError, ())
|
||||||
handle_status(err)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ())
|
const PaDeviceIndex = Cint
|
||||||
|
|
||||||
function Pa_GetVersionText()
|
const PaHostApiIndex = Cint
|
||||||
versionPtr = 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
|
||||||
|
paCoreAudio = 5
|
||||||
|
paOSS = 7
|
||||||
|
paALSA = 8
|
||||||
|
paAL = 9
|
||||||
|
paBeOS = 10
|
||||||
|
paWDMKS = 11
|
||||||
|
paJACK = 12
|
||||||
|
paWASAPI = 13
|
||||||
|
paAudioScienceHPI = 14
|
||||||
|
end
|
||||||
|
|
||||||
# PaHostApiTypeId values
|
mutable struct PaHostApiInfo
|
||||||
const pa_host_api_names = Dict{PaHostApiTypeId, String}(
|
structVersion::Cint
|
||||||
0 => "In Development", # use while developing support for a new host API
|
type::PaHostApiTypeId
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PaHostApiInfo
|
|
||||||
struct_version::Cint
|
|
||||||
api_type::PaHostApiTypeId
|
|
||||||
name::Ptr{Cchar}
|
name::Ptr{Cchar}
|
||||||
deviceCount::Cint
|
deviceCount::Cint
|
||||||
defaultInputDevice::PaDeviceIndex
|
defaultInputDevice::PaDeviceIndex
|
||||||
defaultOutputDevice::PaDeviceIndex
|
defaultOutputDevice::PaDeviceIndex
|
||||||
end
|
end
|
||||||
|
|
||||||
Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio),
|
function Pa_GetHostApiInfo(hostApi)
|
||||||
Ptr{PaHostApiInfo}, (PaHostApiIndex,), i))
|
ccall(
|
||||||
|
(:Pa_GetHostApiInfo, libportaudio),
|
||||||
# Device Functions
|
Ptr{PaHostApiInfo},
|
||||||
|
(PaHostApiIndex,),
|
||||||
type PaDeviceInfo
|
hostApi,
|
||||||
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
|
end
|
||||||
|
|
||||||
Pa_GetDeviceCount() = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
|
function Pa_HostApiTypeIdToHostApiIndex(type)
|
||||||
|
ccall(
|
||||||
|
(:Pa_HostApiTypeIdToHostApiIndex, libportaudio),
|
||||||
|
PaHostApiIndex,
|
||||||
|
(PaHostApiTypeId,),
|
||||||
|
type,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio),
|
function Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex)
|
||||||
Ptr{PaDeviceInfo}, (PaDeviceIndex,), i))
|
ccall(
|
||||||
|
(:Pa_HostApiDeviceIndexToDeviceIndex, libportaudio),
|
||||||
|
PaDeviceIndex,
|
||||||
|
(PaHostApiIndex, Cint),
|
||||||
|
hostApi,
|
||||||
|
hostApiDeviceIndex,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
Pa_GetDefaultInputDevice() = ccall((:Pa_GetDefaultInputDevice, libportaudio),
|
mutable struct PaHostErrorInfo
|
||||||
PaDeviceIndex, ())
|
hostApiType::PaHostApiTypeId
|
||||||
|
errorCode::Clong
|
||||||
|
errorText::Ptr{Cchar}
|
||||||
|
end
|
||||||
|
|
||||||
Pa_GetDefaultOutputDevice() = ccall((:Pa_GetDefaultOutputDevice, libportaudio),
|
function Pa_GetLastHostErrorInfo()
|
||||||
PaDeviceIndex, ())
|
ccall((:Pa_GetLastHostErrorInfo, libportaudio), Ptr{PaHostErrorInfo}, ())
|
||||||
|
end
|
||||||
|
|
||||||
# Stream Functions
|
function Pa_GetDeviceCount()
|
||||||
|
ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
|
||||||
|
end
|
||||||
|
|
||||||
type Pa_StreamParameters
|
function Pa_GetDefaultInputDevice()
|
||||||
|
ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetDefaultOutputDevice()
|
||||||
|
ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
const PaTime = Cdouble
|
||||||
|
|
||||||
|
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{Void}
|
hostApiSpecificStreamInfo::Ptr{Cvoid}
|
||||||
end
|
end
|
||||||
|
|
||||||
type PaStreamInfo
|
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
|
||||||
structVersion::Cint
|
structVersion::Cint
|
||||||
inputLatency::PaTime
|
inputLatency::PaTime
|
||||||
outputLatency::PaTime
|
outputLatency::PaTime
|
||||||
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)
|
|
||||||
# streamPtr = Ref{PaStream}(0)
|
|
||||||
# err = ccall((:Pa_OpenDefaultStream, libportaudio),
|
|
||||||
# PaError, (Ref{PaStream}, Cint, Cint,
|
|
||||||
# PaSampleFormat, Cdouble, Culong,
|
|
||||||
# Ref{Void}, Ref{Void}),
|
|
||||||
# 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 = ccall((:Pa_OpenStream, libportaudio), PaError,
|
|
||||||
(Ref{PaStream},
|
|
||||||
Ptr{Pa_StreamParameters},
|
|
||||||
Ptr{Pa_StreamParameters},
|
|
||||||
Cdouble, Culong, PaStreamFlags,
|
|
||||||
Ptr{Void}, Ptr{Void}),
|
|
||||||
streamPtr,
|
|
||||||
inParams, outParams,
|
|
||||||
sampleRate, framesPerBuffer, flags,
|
|
||||||
callback, userdata)
|
|
||||||
handle_status(err)
|
|
||||||
streamPtr[]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_StartStream(stream::PaStream)
|
function Pa_GetStreamTime(stream)
|
||||||
err = ccall((:Pa_StartStream, libportaudio), PaError,
|
ccall((:Pa_GetStreamTime, libportaudio), PaTime, (Ptr{PaStream},), stream)
|
||||||
(PaStream,), stream)
|
|
||||||
handle_status(err)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_StopStream(stream::PaStream)
|
function Pa_GetStreamCpuLoad(stream)
|
||||||
err = ccall((:Pa_StopStream, libportaudio), PaError,
|
ccall((:Pa_GetStreamCpuLoad, libportaudio), Cdouble, (Ptr{PaStream},), stream)
|
||||||
(PaStream,), stream)
|
|
||||||
handle_status(err)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_CloseStream(stream::PaStream)
|
function Pa_ReadStream(stream, buffer, frames)
|
||||||
err = ccall((:Pa_CloseStream, libportaudio), PaError,
|
ccall(
|
||||||
(PaStream,), stream)
|
(:Pa_ReadStream, libportaudio),
|
||||||
handle_status(err)
|
PaError,
|
||||||
|
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||||
|
stream,
|
||||||
|
buffer,
|
||||||
|
frames,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_GetStreamReadAvailable(stream::PaStream)
|
function Pa_WriteStream(stream, buffer, frames)
|
||||||
avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
|
ccall(
|
||||||
(PaStream,), stream)
|
(:Pa_WriteStream, libportaudio),
|
||||||
avail >= 0 || handle_status(avail)
|
PaError,
|
||||||
avail
|
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||||
|
stream,
|
||||||
|
buffer,
|
||||||
|
frames,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_GetStreamWriteAvailable(stream::PaStream)
|
function Pa_GetStreamReadAvailable(stream)
|
||||||
avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
|
ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||||
(PaStream,), stream)
|
|
||||||
avail >= 0 || handle_status(avail)
|
|
||||||
avail
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
|
function Pa_GetStreamWriteAvailable(stream)
|
||||||
show_warnings::Bool=true)
|
ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||||
frames <= length(buf) || error("Need a buffer at least $frames long")
|
|
||||||
err = ccall((:Pa_ReadStream, libportaudio), PaError,
|
|
||||||
(PaStream, Ptr{Void}, Culong),
|
|
||||||
stream, buf, frames)
|
|
||||||
handle_status(err, show_warnings)
|
|
||||||
buf
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
|
function Pa_GetSampleSize(format)
|
||||||
show_warnings::Bool=true)
|
ccall((:Pa_GetSampleSize, libportaudio), PaError, (PaSampleFormat,), format)
|
||||||
frames <= length(buf) || error("Need a buffer at least $frames long")
|
|
||||||
err = ccall((:Pa_WriteStream, libportaudio), PaError,
|
|
||||||
(PaStream, Ptr{Void}, Culong),
|
|
||||||
stream, buf, frames)
|
|
||||||
handle_status(err, show_warnings)
|
|
||||||
nothing
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# function Pa_GetStreamInfo(stream::PaStream)
|
function Pa_Sleep(msec)
|
||||||
# infoptr = ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo},
|
ccall((:Pa_Sleep, libportaudio), Cvoid, (Clong,), msec)
|
||||||
# (PaStream, ), stream)
|
end
|
||||||
#
|
|
||||||
# unsafe_load(infoptr)
|
const paNoDevice = PaDeviceIndex(-1)
|
||||||
# end
|
|
||||||
#
|
const paUseHostApiSpecificDeviceSpecification = PaDeviceIndex(-2)
|
||||||
# General utility function to handle the status from the Pa_* functions
|
|
||||||
function handle_status(err::PaError, show_warnings::Bool=true)
|
const paFloat32 = PaSampleFormat(0x00000001)
|
||||||
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
|
|
||||||
if show_warnings
|
const paInt32 = PaSampleFormat(0x00000002)
|
||||||
msg = ccall((:Pa_GetErrorText, libportaudio),
|
|
||||||
Ptr{Cchar}, (PaError,), err)
|
const paInt24 = PaSampleFormat(0x00000004)
|
||||||
warn("libportaudio: " * unsafe_string(msg))
|
|
||||||
end
|
const paInt16 = PaSampleFormat(0x00000008)
|
||||||
elseif err != PA_NO_ERROR
|
|
||||||
msg = ccall((:Pa_GetErrorText, libportaudio),
|
const paInt8 = PaSampleFormat(0x00000010)
|
||||||
Ptr{Cchar}, (PaError,), err)
|
|
||||||
error("libportaudio: " * unsafe_string(msg))
|
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
|
||||||
|
|
|
@ -1,40 +0,0 @@
|
||||||
function init_pa_shim()
|
|
||||||
global const libpa_shim = Libdl.find_library(
|
|
||||||
["pa_shim"],
|
|
||||||
[joinpath(dirname(@__FILE__), "..", "deps", "usr", "lib")])
|
|
||||||
shim_dlib = Libdl.dlopen(libpa_shim)
|
|
||||||
# pointer to the shim's process callback
|
|
||||||
global const shim_processcb_c = Libdl.dlsym(shim_dlib, :pa_shim_processcb)
|
|
||||||
if shim_processcb_c == C_NULL
|
|
||||||
error("Got NULL pointer loading `pa_shim_processcb`")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
const pa_shim_errmsg_t = Cint
|
|
||||||
const PA_SHIM_ERRMSG_OVERFLOW = Cint(0) # input overflow
|
|
||||||
const PA_SHIM_ERRMSG_UNDERFLOW = Cint(1) # output underflow
|
|
||||||
const PA_SHIM_ERRMSG_ERR_OVERFLOW = Cint(2) # error buffer overflowed
|
|
||||||
|
|
||||||
|
|
||||||
# This struct is shared with pa_shim.c
|
|
||||||
mutable struct pa_shim_info_t
|
|
||||||
inputbuf::Ptr{PaUtilRingBuffer} # ringbuffer for input
|
|
||||||
outputbuf::Ptr{PaUtilRingBuffer} # ringbuffer for output
|
|
||||||
errorbuf::Ptr{PaUtilRingBuffer} # ringbuffer to send error notifications
|
|
||||||
sync::Cint # keep input/output ring buffers synchronized (0/1)
|
|
||||||
notifycb::Ptr{Void} # Julia callback to notify on updates (called from audio thread)
|
|
||||||
inputhandle::Ptr{Void} # condition to notify on new input data
|
|
||||||
outputhandle::Ptr{Void} # condition to notify when ready for output
|
|
||||||
errorhandle::Ptr{Void} # condition to notify on new errors
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
PortAudio.shimhash()
|
|
||||||
|
|
||||||
Return the sha256 hash(as a string) of the source file used to build the shim.
|
|
||||||
We may use this sometime to verify that the distributed binary stays in sync
|
|
||||||
with the rest of the package.
|
|
||||||
"""
|
|
||||||
shimhash() = unsafe_string(
|
|
||||||
ccall((:pa_shim_getsourcehash, libpa_shim), Cstring, ()))
|
|
||||||
Base.unsafe_convert(::Type{Ptr{Void}}, info::pa_shim_info_t) = pointer_from_objref(info)
|
|
29
src/precompile.jl
Normal file
29
src/precompile.jl
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# precompile some important functions
|
||||||
|
const DEFAULT_SINK_MESSENGER_TYPE = Messenger{Float32, SampledSignalsWriter, Tuple{Matrix{Float32}, Int64, Int64}, Int64}
|
||||||
|
const DEFAULT_SOURCE_MESSENGER_TYPE = Messenger{Float32, SampledSignalsReader, Tuple{Matrix{Float32}, Int64, Int64}, Int64}
|
||||||
|
const DEFAULT_STREAM_TYPE = PortAudioStream{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
|
||||||
|
const DEFAULT_SINK_TYPE = PortAudioSink{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
|
||||||
|
const DEFAULT_SOURCE_TYPE = PortAudioSource{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
|
||||||
|
|
||||||
|
precompile(close, (DEFAULT_STREAM_TYPE,))
|
||||||
|
precompile(devices, ())
|
||||||
|
precompile(__init__, ())
|
||||||
|
precompile(isopen, (DEFAULT_STREAM_TYPE,))
|
||||||
|
precompile(nchannels, (DEFAULT_SINK_TYPE,))
|
||||||
|
precompile(nchannels, (DEFAULT_SOURCE_TYPE,))
|
||||||
|
precompile(PortAudioStream, (Int, Int))
|
||||||
|
precompile(PortAudioStream, (String, Int, Int))
|
||||||
|
precompile(PortAudioStream, (String, String, Int, Int))
|
||||||
|
precompile(samplerate, (DEFAULT_STREAM_TYPE,))
|
||||||
|
precompile(send, (DEFAULT_SINK_MESSENGER_TYPE,))
|
||||||
|
precompile(send, (DEFAULT_SOURCE_MESSENGER_TYPE,))
|
||||||
|
precompile(unsafe_read!, (DEFAULT_SOURCE_TYPE, Vector{Float32}, Int, Int))
|
||||||
|
precompile(unsafe_read!, (DEFAULT_SOURCE_TYPE, Matrix{Float32}, Int, Int))
|
||||||
|
precompile(unsafe_write, (DEFAULT_SINK_TYPE, Vector{Float32}, Int, Int))
|
||||||
|
precompile(unsafe_write, (DEFAULT_SINK_TYPE, Matrix{Float32}, Int, Int))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# while waiting for this PR to get merged: https://github.com/Ismael-VC/Suppressor.jl/pull/12
|
|
||||||
# we'll just include the relevant code here
|
|
||||||
|
|
||||||
macro suppress_err(block)
|
|
||||||
quote
|
|
||||||
if ccall(:jl_generating_output, Cint, ()) == 0
|
|
||||||
ORIGINAL_STDERR = STDERR
|
|
||||||
err_rd, err_wr = redirect_stderr()
|
|
||||||
err_reader = @async readstring(err_rd)
|
|
||||||
end
|
|
||||||
|
|
||||||
value = $(esc(block))
|
|
||||||
|
|
||||||
if ccall(:jl_generating_output, Cint, ()) == 0
|
|
||||||
redirect_stderr(ORIGINAL_STDERR)
|
|
||||||
close(err_wr)
|
|
||||||
end
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1 +0,0 @@
|
||||||
BaseTestNext
|
|
553
test/runtests.jl
553
test/runtests.jl
|
@ -1,333 +1,256 @@
|
||||||
#!/usr/bin/env julia
|
#!/usr/bin/env julia
|
||||||
|
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,
|
||||||
|
initialize,
|
||||||
|
name,
|
||||||
|
PortAudioException,
|
||||||
|
PortAudio,
|
||||||
|
PortAudioDevice,
|
||||||
|
PortAudioStream,
|
||||||
|
safe_load,
|
||||||
|
seek_alsa_conf,
|
||||||
|
terminate,
|
||||||
|
write_buffer
|
||||||
|
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
|
||||||
|
|
||||||
using Base.Test
|
@testset "Tests without sound" begin
|
||||||
using TestSetExtensions
|
|
||||||
using PortAudio
|
|
||||||
using SampledSignals
|
|
||||||
using RingBuffers
|
|
||||||
|
|
||||||
# pull in some extra stuff we need to test the callback directly
|
|
||||||
using PortAudio: notifyhandle, notifycb_c, shim_processcb_c
|
|
||||||
using PortAudio: pa_shim_errmsg_t, pa_shim_info_t
|
|
||||||
using PortAudio: PA_SHIM_ERRMSG_ERR_OVERFLOW, PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW
|
|
||||||
|
|
||||||
"Setup buffers to test callback behavior"
|
|
||||||
function setup_callback(inchans, outchans, nframes, synced)
|
|
||||||
sourcebuf = RingBuffer{Float32}(inchans, nframes*2) # the microphone input should end up here
|
|
||||||
sinkbuf = RingBuffer{Float32}(outchans, nframes*2) # the callback should copy this to cb_output
|
|
||||||
errbuf = RingBuffer{pa_shim_errmsg_t}(1, 8)
|
|
||||||
|
|
||||||
# pass NULL for i/o we're not using
|
|
||||||
info = pa_shim_info_t(
|
|
||||||
inchans > 0 ? pointer(sourcebuf) : C_NULL,
|
|
||||||
outchans > 0 ? pointer(sinkbuf) : C_NULL,
|
|
||||||
pointer(errbuf),
|
|
||||||
synced, notifycb_c,
|
|
||||||
inchans > 0 ? notifyhandle(sourcebuf) : C_NULL,
|
|
||||||
outchans > 0 ? notifyhandle(sinkbuf) : C_NULL,
|
|
||||||
notifyhandle(errbuf)
|
|
||||||
)
|
|
||||||
flags = Culong(0)
|
|
||||||
|
|
||||||
cb_input = rand(Float32, inchans, nframes) # simulate microphone input
|
|
||||||
cb_output = rand(Float32, outchans, nframes) # this is where the output should go
|
|
||||||
|
|
||||||
function processfunc()
|
|
||||||
ccall(shim_processcb_c, Cint,
|
|
||||||
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{Void}),
|
|
||||||
cb_input, cb_output, nframes, C_NULL, flags, pointer_from_objref(info))
|
|
||||||
end
|
|
||||||
|
|
||||||
(sourcebuf, sinkbuf, errbuf, cb_input, cb_output, processfunc)
|
|
||||||
end
|
|
||||||
|
|
||||||
function test_callback(inchans, outchans, synced)
|
|
||||||
nframes = 8
|
|
||||||
(sourcebuf, sinkbuf, errbuf,
|
|
||||||
cb_input, cb_output, process) = setup_callback(inchans, outchans,
|
|
||||||
nframes, synced)
|
|
||||||
if outchans > 0
|
|
||||||
testout = rand(Float32, outchans, nframes) # generate some test data to play
|
|
||||||
write(sinkbuf, testout) # fill the output ringbuffer
|
|
||||||
end
|
|
||||||
@test process() == PortAudio.paContinue
|
|
||||||
if outchans > 0
|
|
||||||
# testout -> sinkbuf -> cb_output
|
|
||||||
@test cb_output == testout
|
|
||||||
end
|
|
||||||
if inchans > 0
|
|
||||||
# cb_input -> sourcebuf
|
|
||||||
@test read(sourcebuf, nframes) == cb_input
|
|
||||||
end
|
|
||||||
@test framesreadable(errbuf) == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
"""
|
|
||||||
test_callback_underflow(inchans, outchans; nframes=8, underfill=3, synced=false)
|
|
||||||
|
|
||||||
Test that the callback works on underflow conditions. underfill is the numer of
|
|
||||||
frames we feed in, which should be less than nframes.
|
|
||||||
"""
|
|
||||||
function test_callback_underflow(inchans, outchans, synced)
|
|
||||||
nframes = 8
|
|
||||||
underfill = 3 # must be less than nframes
|
|
||||||
(sourcebuf, sinkbuf, errbuf,
|
|
||||||
cb_input, cb_output, process) = setup_callback(inchans, outchans,
|
|
||||||
nframes, synced)
|
|
||||||
outchans > 0 || error("Can't test underflow with no output")
|
|
||||||
testout = rand(Float32, outchans, underfill)
|
|
||||||
write(sinkbuf, testout) # underfill the output ringbuffer
|
|
||||||
# call callback (partial underflow)
|
|
||||||
@test process() == PortAudio.paContinue
|
|
||||||
@test cb_output[:, 1:underfill] == testout
|
|
||||||
@test cb_output[:, (underfill+1):nframes] == zeros(Float32, outchans, (nframes-underfill))
|
|
||||||
errs = readavailable(errbuf)
|
|
||||||
if inchans > 0
|
|
||||||
received = readavailable(sourcebuf)
|
|
||||||
if synced
|
|
||||||
@test size(received, 2) == underfill
|
|
||||||
@test received == cb_input[:, 1:underfill]
|
|
||||||
@test length(errs) == 2
|
|
||||||
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
|
|
||||||
else
|
|
||||||
@test size(received, 2) == nframes
|
|
||||||
@test received == cb_input
|
|
||||||
@test length(errs) == 1
|
|
||||||
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@test length(errs) == 1
|
|
||||||
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
|
|
||||||
end
|
|
||||||
|
|
||||||
# call again (total underflow)
|
|
||||||
@test process() == PortAudio.paContinue
|
|
||||||
@test cb_output == zeros(Float32, outchans, nframes)
|
|
||||||
errs = readavailable(errbuf)
|
|
||||||
if inchans > 0
|
|
||||||
received = readavailable(sourcebuf)
|
|
||||||
if synced
|
|
||||||
@test size(received, 2) == 0
|
|
||||||
@test length(errs) == 2
|
|
||||||
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
|
|
||||||
else
|
|
||||||
@test size(received, 2) == nframes
|
|
||||||
@test received == cb_input
|
|
||||||
@test length(errs) == 1
|
|
||||||
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@test length(errs) == 1
|
|
||||||
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function test_callback_overflow(inchans, outchans, synced)
|
|
||||||
nframes = 8
|
|
||||||
(sourcebuf, sinkbuf, errbuf,
|
|
||||||
cb_input, cb_output, process) = setup_callback(inchans, outchans,
|
|
||||||
nframes, synced)
|
|
||||||
inchans > 0 || error("Can't test overflow with no input")
|
|
||||||
@test frameswritable(sinkbuf) == nframes*2
|
|
||||||
|
|
||||||
# the first time it should half-fill the input ring buffer
|
|
||||||
if outchans > 0
|
|
||||||
testout = rand(Float32, outchans, nframes)
|
|
||||||
write(sinkbuf, testout)
|
|
||||||
end
|
|
||||||
@test framesreadable(sourcebuf) == 0
|
|
||||||
outchans > 0 && @test frameswritable(sinkbuf) == nframes
|
|
||||||
@test process() == PortAudio.paContinue
|
|
||||||
@test framesreadable(errbuf) == 0
|
|
||||||
@test framesreadable(sourcebuf) == nframes
|
|
||||||
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
|
|
||||||
|
|
||||||
# now run the process func again to completely fill the input ring buffer
|
|
||||||
outchans > 0 && write(sinkbuf, testout)
|
|
||||||
@test framesreadable(sourcebuf) == nframes
|
|
||||||
outchans > 0 && @test frameswritable(sinkbuf) == nframes
|
|
||||||
@test process() == PortAudio.paContinue
|
|
||||||
@test framesreadable(errbuf) == 0
|
|
||||||
@test framesreadable(sourcebuf) == nframes*2
|
|
||||||
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
|
|
||||||
|
|
||||||
# now this time the process func should overflow the input buffer
|
|
||||||
outchans > 0 && write(sinkbuf, testout)
|
|
||||||
@test framesreadable(sourcebuf) == nframes*2
|
|
||||||
outchans > 0 && @test frameswritable(sinkbuf) == nframes
|
|
||||||
@test process() == PortAudio.paContinue
|
|
||||||
@test framesreadable(sourcebuf) == nframes*2
|
|
||||||
errs = readavailable(errbuf)
|
|
||||||
if outchans > 0
|
|
||||||
if synced
|
|
||||||
# if input and output are synced, thec callback didn't pull from
|
|
||||||
# the output ringbuf
|
|
||||||
@test frameswritable(sinkbuf) == nframes
|
|
||||||
@test cb_output == zeros(Float32, outchans, nframes)
|
|
||||||
@test length(errs) == 2
|
|
||||||
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
|
|
||||||
else
|
|
||||||
@test frameswritable(sinkbuf) == nframes*2
|
|
||||||
@test length(errs) == 1
|
|
||||||
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
|
|
||||||
end
|
|
||||||
else
|
|
||||||
@test length(errs) == 1
|
|
||||||
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# these test are currently set up to run on OSX
|
|
||||||
|
|
||||||
@testset DottedTestSet "PortAudio Tests" begin
|
|
||||||
if is_windows()
|
|
||||||
default_indev = "Microphone Array (Realtek High "
|
|
||||||
default_outdev = "Speaker/Headphone (Realtek High"
|
|
||||||
elseif is_apple()
|
|
||||||
default_indev = "Built-in Microph"
|
|
||||||
default_outdev = "Built-in Output"
|
|
||||||
elseif is_linux()
|
|
||||||
default_indev = "default"
|
|
||||||
default_outdev = "default"
|
|
||||||
end
|
|
||||||
|
|
||||||
devs = PortAudio.devices()
|
|
||||||
i = findfirst(d -> d.maxinchans > 0, devs)
|
|
||||||
indev = i > 0 ? devs[i] : nothing
|
|
||||||
i = findfirst(d -> d.maxoutchans > 0, devs)
|
|
||||||
outdev = i > 0 ? devs[i] : nothing
|
|
||||||
i = findfirst(d -> d.maxoutchans > 0 && d.maxinchans > 0, devs)
|
|
||||||
duplexdev = i > 0 ? devs[i] : nothing
|
|
||||||
|
|
||||||
@testset "Reports version" begin
|
@testset "Reports version" begin
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
PortAudio.versioninfo(io)
|
PortAudio.versioninfo(io)
|
||||||
result = split(String(take!((io))), "\n")
|
result = split(String(take!((io))), "\n")
|
||||||
# make sure this is the same version I tested with
|
# make sure this is the same version I tested with
|
||||||
@test startswith(result[1], "PortAudio V19")
|
@test startswith(result[1], "PortAudio V19")
|
||||||
@test result[3] == "Shim Source Hash: 4ea2a8526b"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "Basic callback functionality" begin
|
@testset "Can list devices without crashing" begin
|
||||||
@testset "basic duplex (no sync)" begin
|
display(devices())
|
||||||
test_callback(2, 3, false)
|
println()
|
||||||
end
|
|
||||||
@testset "basic input-only (no sync)" begin
|
|
||||||
test_callback(2, 0, false)
|
|
||||||
end
|
|
||||||
@testset "basic output-only (no sync)" begin
|
|
||||||
test_callback(0, 2, false)
|
|
||||||
end
|
|
||||||
@testset "basic no input or output (no sync)" begin
|
|
||||||
test_callback(0, 0, false)
|
|
||||||
end
|
|
||||||
@testset "basic duplex (sync)" begin
|
|
||||||
test_callback(2, 3, true)
|
|
||||||
end
|
|
||||||
@testset "basic input-only (sync)" begin
|
|
||||||
test_callback(2, 0, true)
|
|
||||||
end
|
|
||||||
@testset "basic output-only (sync)" begin
|
|
||||||
test_callback(0, 2, true)
|
|
||||||
end
|
|
||||||
@testset "basic no input or output (sync)" begin
|
|
||||||
test_callback(0, 0, true)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "Ouput underflow" begin
|
@testset "libortaudio without sound" begin
|
||||||
@testset "underflow duplex (nosync)" begin
|
@test handle_status(Pa_GetHostApiCount()) >= 0
|
||||||
test_callback_underflow(2, 3, false)
|
@test handle_status(Pa_GetDefaultHostApi()) >= 0
|
||||||
end
|
# version info not available on windows?
|
||||||
@testset "underflow output-only (nosync)" begin
|
if !Sys.iswindows()
|
||||||
test_callback_underflow(0, 3, false)
|
@test safe_load(Pa_GetVersionInfo(), ErrorException("no info")) isa
|
||||||
end
|
PaVersionInfo
|
||||||
@testset "underflow duplex (sync)" begin
|
|
||||||
test_callback_underflow(2, 3, true)
|
|
||||||
end
|
|
||||||
@testset "underflow output-only (sync)" begin
|
|
||||||
test_callback_underflow(0, 3, true)
|
|
||||||
end
|
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 "Input overflow" begin
|
@testset "Errors without sound" begin
|
||||||
@testset "overflow duplex (nosync)" begin
|
@test sprint(showerror, PortAudioException(paNotInitialized)) ==
|
||||||
test_callback_overflow(2, 3, false)
|
"PortAudioException: PortAudio not initialized"
|
||||||
end
|
@test_throws KeyError("foobarbaz") get_device("foobarbaz")
|
||||||
@testset "overflow input-only (nosync)" begin
|
@test_throws KeyError(-1) get_device(-1)
|
||||||
test_callback_overflow(2, 0, false)
|
@test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(())
|
||||||
end
|
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
|
||||||
@testset "overflow duplex (sync)" begin
|
PaError(paOutputUnderflowed),
|
||||||
test_callback_overflow(2, 3, true)
|
)
|
||||||
end
|
@test_throws PortAudioException(paNotInitialized) handle_status(
|
||||||
@testset "overflow input-only (sync)" begin
|
PaError(paNotInitialized),
|
||||||
test_callback_overflow(2, 0, true)
|
)
|
||||||
end
|
Pa_Sleep(1)
|
||||||
|
@test Pa_GetSampleSize(paFloat32) == 4
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "Open Default Device" begin
|
# make sure we can terminate, then reinitialize
|
||||||
println("Recording...")
|
terminate()
|
||||||
stream = PortAudioStream(2, 0)
|
initialize()
|
||||||
buf = read(stream, 5s)
|
end
|
||||||
close(stream)
|
|
||||||
@test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
if isempty(devices())
|
||||||
println("Playing back recording...")
|
@test_throws ArgumentError("No input device available") get_default_input_index()
|
||||||
stream = PortAudioStream(0, 2)
|
else
|
||||||
write(stream, buf)
|
@testset "Tests with sound" begin
|
||||||
println("flushing...")
|
# these default values are specific to local machines
|
||||||
flush(stream)
|
input_name = get_device(get_default_input_index()).name
|
||||||
close(stream)
|
output_name = get_device(get_default_output_index()).name
|
||||||
println("Testing pass-through")
|
|
||||||
stream = PortAudioStream(2, 2)
|
@testset "Interactive tests" begin
|
||||||
write(stream, stream, 5s)
|
println("Recording...")
|
||||||
flush(stream)
|
stream = PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true)
|
||||||
close(stream)
|
buffer = read(stream, 5s)
|
||||||
println("done")
|
@test size(buffer) ==
|
||||||
end
|
(round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
||||||
@testset "Samplerate-converting writing" begin
|
close(stream)
|
||||||
stream = PortAudioStream()
|
sleep(1)
|
||||||
write(stream, SinSource(eltype(stream), samplerate(stream)*0.8, [220, 330]), 3s)
|
println("Playing back recording...")
|
||||||
write(stream, SinSource(eltype(stream), samplerate(stream)*1.2, [220, 330]), 3s)
|
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||||
flush(stream)
|
write(stream, buffer)
|
||||||
close(stream)
|
end
|
||||||
end
|
sleep(1)
|
||||||
@testset "Open Device by name" begin
|
println("Testing pass-through")
|
||||||
stream = PortAudioStream(default_indev, default_outdev)
|
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
|
||||||
buf = read(stream, 0.001s)
|
write_buffer(stream.sink_messenger.buffer, acquire_lock = false)
|
||||||
@test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
|
sink = stream.sink
|
||||||
write(stream, buf)
|
source = stream.source
|
||||||
io = IOBuffer()
|
@test sprint(show, stream) == """
|
||||||
show(io, stream)
|
PortAudioStream{Float32}
|
||||||
@test String(take!(io)) == """
|
Samplerate: 44100Hz
|
||||||
PortAudio.PortAudioStream{Float32}
|
2 channel sink: $(repr(output_name))
|
||||||
Samplerate: 44100.0Hz
|
2 channel source: $(repr(input_name))"""
|
||||||
Buffer Size: 4096 frames
|
@test sprint(show, source) == "2 channel source: $(repr(input_name))"
|
||||||
2 channel sink: "$default_outdev"
|
@test sprint(show, sink) == "2 channel sink: $(repr(output_name))"
|
||||||
2 channel source: "$default_indev\""""
|
write(stream, stream, 5s)
|
||||||
close(stream)
|
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
|
||||||
end
|
@test isopen(stream)
|
||||||
@testset "Error on wrong name" begin
|
close(stream)
|
||||||
@test_throws ErrorException PortAudioStream("foobarbaz")
|
sleep(1)
|
||||||
end
|
@test !isopen(stream)
|
||||||
# no way to check that the right data is actually getting read or written here,
|
@test !isopen(sink)
|
||||||
# but at least it's not crashing.
|
@test !isopen(source)
|
||||||
@testset "Queued Writing" begin
|
println("done")
|
||||||
stream = PortAudioStream(0, 2)
|
end
|
||||||
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.sink))*0.1, samplerate(stream))
|
@testset "Samplerate-converting writing" begin
|
||||||
t1 = @async write(stream, buf)
|
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||||
t2 = @async write(stream, buf)
|
write(
|
||||||
@test wait(t1) == 48000
|
stream,
|
||||||
@test wait(t2) == 48000
|
SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]),
|
||||||
flush(stream)
|
3s,
|
||||||
close(stream)
|
)
|
||||||
end
|
println("expected blip")
|
||||||
@testset "Queued Reading" begin
|
write(
|
||||||
stream = PortAudioStream(2, 0)
|
stream,
|
||||||
buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source))*0.1, samplerate(stream))
|
SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]),
|
||||||
t1 = @async read!(stream, buf)
|
3s,
|
||||||
t2 = @async read!(stream, buf)
|
)
|
||||||
@test wait(t1) == 48000
|
end
|
||||||
@test wait(t2) == 48000
|
end
|
||||||
close(stream)
|
sleep(1)
|
||||||
end
|
# 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
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
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
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
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 output channels for $output_name",
|
||||||
|
) PortAudioStream(input_name, output_name, 0, big)
|
||||||
|
@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 \"$input_name\" disagrees with
|
||||||
|
default sample rate 1 for output \"$output_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
|
end
|
||||||
|
|
95
test/runtests_local.jl
Normal file
95
test/runtests_local.jl
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
# This file has runs the normal tests and also adds tests that can only be run
|
||||||
|
# locally on a machine with a sound card. It's mostly to put the library through
|
||||||
|
# its paces assuming a human is listening.
|
||||||
|
|
||||||
|
include("runtests.jl")
|
||||||
|
|
||||||
|
# these default values are specific to my machines
|
||||||
|
if Sys.iswindows()
|
||||||
|
default_indev = "Microphone Array (Realtek High "
|
||||||
|
default_outdev = "Speaker/Headphone (Realtek High"
|
||||||
|
elseif Sys.isapple()
|
||||||
|
default_indev = "Built-in Microphone"
|
||||||
|
default_outdev = "Built-in Output"
|
||||||
|
elseif Sys.islinux()
|
||||||
|
default_indev = "default"
|
||||||
|
default_outdev = "default"
|
||||||
|
end
|
||||||
|
|
||||||
|
@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...")
|
||||||
|
stream = PortAudioStream(0, 2)
|
||||||
|
write(stream, buf)
|
||||||
|
println("flushing...")
|
||||||
|
flush(stream)
|
||||||
|
close(stream)
|
||||||
|
println("Testing pass-through")
|
||||||
|
stream = PortAudioStream(2, 2)
|
||||||
|
write(stream, stream, 5s)
|
||||||
|
flush(stream)
|
||||||
|
close(stream)
|
||||||
|
println("done")
|
||||||
|
end
|
||||||
|
@testset "Samplerate-converting writing" begin
|
||||||
|
stream = PortAudioStream(0, 2)
|
||||||
|
write(stream, SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]), 3s)
|
||||||
|
write(stream, SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]), 3s)
|
||||||
|
flush(stream)
|
||||||
|
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
|
||||||
|
Buffer Size: 4096 frames
|
||||||
|
2 channel sink: "$default_outdev"
|
||||||
|
2 channel source: "$default_indev\"""",
|
||||||
|
String(take!(io)),
|
||||||
|
)
|
||||||
|
close(stream)
|
||||||
|
end
|
||||||
|
@testset "Error on wrong name" begin
|
||||||
|
@test_throws ErrorException PortAudioStream("foobarbaz")
|
||||||
|
end
|
||||||
|
# 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(
|
||||||
|
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
|
||||||
|
flush(stream)
|
||||||
|
close(stream)
|
||||||
|
end
|
||||||
|
@testset "Queued Reading" begin
|
||||||
|
stream = PortAudioStream(2, 0)
|
||||||
|
buf = 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)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue