Compare commits
328 commits
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 | ||
![]() |
3e7c6b5c1b | ||
![]() |
acaa305dfa | ||
![]() |
c58143404f | ||
![]() |
af8f53eb0e | ||
![]() |
c53ac388e9 | ||
![]() |
85c34d3906 | ||
![]() |
0c36e1eec5 | ||
![]() |
9e3e66d37a | ||
![]() |
5c40329df6 | ||
![]() |
ba5f60e097 | ||
![]() |
a24cf8e9bc | ||
![]() |
15dcee6245 | ||
![]() |
eaa5aa96bb | ||
![]() |
9b67ba645c | ||
![]() |
4fe967465c | ||
![]() |
7012c445f8 | ||
![]() |
b5f125c1b1 | ||
![]() |
5f53bcebc3 | ||
![]() |
f19e9ca160 | ||
![]() |
b6f59a1e9d | ||
![]() |
1e4bfaff06 | ||
![]() |
93334b0de9 | ||
![]() |
80af028efb | ||
![]() |
fcf87c0c61 | ||
![]() |
1d5ca112eb | ||
![]() |
7ea9da7e09 | ||
![]() |
de5753e7f2 | ||
![]() |
1d16bdecba | ||
![]() |
44339ff755 | ||
![]() |
5460e461bd | ||
![]() |
09b1cd3e47 | ||
![]() |
3f5f107c81 | ||
![]() |
e7cdbad4b3 | ||
![]() |
fb30e51f91 | ||
![]() |
2cc49cc3e0 | ||
![]() |
ede482ce6f | ||
![]() |
0d64e4bd0c | ||
![]() |
fd425b3ace | ||
![]() |
7cccb28d2b | ||
![]() |
829a09a2ae | ||
![]() |
30803bce97 | ||
![]() |
77dcb8965c | ||
![]() |
e40933b97b | ||
![]() |
6433b6fb93 | ||
![]() |
2889669b7f | ||
![]() |
390ae258bd | ||
![]() |
2bda1cf25e | ||
![]() |
43292ccaf8 | ||
![]() |
547cce821a | ||
![]() |
2366f76a06 | ||
![]() |
8e0bd3c255 | ||
![]() |
6970d2b81e | ||
![]() |
db2f697d6c | ||
![]() |
8a7a7d5baa | ||
![]() |
096cfd49da | ||
![]() |
8a3bebff2c | ||
![]() |
ae923ba3a6 | ||
![]() |
56cf15e9df | ||
![]() |
f868087e99 | ||
![]() |
333bbbf8d8 | ||
![]() |
5854270183 | ||
![]() |
f02e733fe7 | ||
![]() |
bea06357b8 | ||
![]() |
c3d8723b5b | ||
![]() |
cf7f170c72 | ||
![]() |
641de1e92b | ||
![]() |
ee4d05fcb2 | ||
![]() |
f7b9b48658 | ||
![]() |
64a08bc90f | ||
![]() |
e51c980f24 | ||
![]() |
738c4cff4b | ||
![]() |
b8e5c8786e | ||
![]() |
d419f51f68 | ||
![]() |
f834773b34 | ||
![]() |
d193a9c83d | ||
![]() |
8ee46f5125 | ||
![]() |
559dca24c2 | ||
![]() |
9b57992d83 | ||
![]() |
9bafaeaa45 | ||
![]() |
6830452b9c | ||
![]() |
4cb94e168d | ||
![]() |
958cccf16f | ||
![]() |
fae9b38224 | ||
![]() |
80d329eb39 | ||
![]() |
c4dfef9178 | ||
![]() |
826fdafe5f | ||
![]() |
348e2576f9 | ||
![]() |
3c663c675a | ||
![]() |
1ad2d5b27d | ||
![]() |
2c4b21940c | ||
![]() |
e87ef290a2 | ||
![]() |
6786c2b4bb | ||
![]() |
57fcabb6de | ||
![]() |
3f90e3c907 | ||
![]() |
9c23d02066 | ||
![]() |
26fd1fdf23 | ||
![]() |
87a160ba78 | ||
![]() |
d871de743f | ||
![]() |
a55b59d481 | ||
![]() |
569cdf0d73 | ||
![]() |
4ebd7b946d | ||
![]() |
8d291288cc | ||
![]() |
4f1ec99080 | ||
![]() |
2f8464a1eb | ||
![]() |
9e02643482 | ||
![]() |
f75de3299f | ||
![]() |
eaca8109f5 | ||
![]() |
f4adf31d15 | ||
![]() |
cbcfedbad0 | ||
![]() |
ead54a9bdd | ||
![]() |
e9483f1782 | ||
![]() |
93c2215e2f | ||
![]() |
ae76badebb | ||
![]() |
87925d1d8d | ||
![]() |
e64ea0b26c | ||
![]() |
0c396b095b | ||
![]() |
0d06ce2c96 | ||
![]() |
5ce3c0cc97 | ||
![]() |
942fae05ca | ||
![]() |
3601ea2098 | ||
![]() |
36362d45cf | ||
![]() |
8d456062ca | ||
![]() |
86e459cc87 | ||
![]() |
dd28b530ea | ||
![]() |
2620e6ecb2 | ||
![]() |
5f074a4f87 | ||
![]() |
11e6dce20c | ||
![]() |
edf2f8fd04 | ||
![]() |
d4b4b33361 | ||
![]() |
c22b6b2347 | ||
![]() |
20ecbb5b8e | ||
![]() |
10802a61f3 | ||
![]() |
89043b80c9 | ||
![]() |
26cf69ad94 | ||
![]() |
b79b47a461 | ||
![]() |
19c33d71c6 | ||
![]() |
0cd33b8289 | ||
![]() |
289d7a56cd | ||
![]() |
18f0cee2e5 | ||
![]() |
f6544cf7fd | ||
![]() |
6af95e6f57 | ||
![]() |
9d9af6e4cd | ||
![]() |
864f35db75 | ||
![]() |
3d1def96ec | ||
![]() |
450a9e9184 | ||
![]() |
6e6ca7d1fe | ||
![]() |
9c5c01aa7b | ||
![]() |
326f28a2b5 | ||
![]() |
48113ff5f3 | ||
![]() |
68414ef7cf | ||
![]() |
9bed45bbc8 | ||
![]() |
131fe7804d | ||
![]() |
00e98a2b88 | ||
![]() |
389636fda2 | ||
![]() |
8c134edc26 | ||
![]() |
4eb15ae29f | ||
![]() |
201d2852c6 | ||
![]() |
16857615d3 | ||
![]() |
fb26c6f011 | ||
![]() |
7cfdff73ac | ||
![]() |
d5ae9237d6 | ||
![]() |
36c95f1a16 | ||
![]() |
91aa390554 | ||
![]() |
b07230919d | ||
![]() |
eabf9717aa | ||
![]() |
8cc51a435c | ||
![]() |
b8802449c9 | ||
![]() |
fab2f8ea8d | ||
![]() |
66654d3aaf | ||
![]() |
e451509293 | ||
![]() |
6a065f0af2 | ||
![]() |
7aafc697b0 | ||
![]() |
dd49d374fa | ||
![]() |
efd841d74d | ||
![]() |
bf2e5bfb84 | ||
![]() |
a334a44a2e | ||
![]() |
9312fa745d | ||
![]() |
afe4e0d8be | ||
![]() |
f240f3fbf6 | ||
![]() |
45cae6bed4 | ||
![]() |
a1ed357629 | ||
![]() |
0ddd57c0a9 | ||
![]() |
c5587a9b47 | ||
![]() |
e48c6361fd | ||
![]() |
0d84cb409b | ||
![]() |
81a8503c1d | ||
![]() |
7831578955 | ||
![]() |
eb34b85922 | ||
![]() |
f7df8623b9 | ||
![]() |
0fa2977889 | ||
![]() |
1f5d9a2612 | ||
![]() |
f0480dc63c | ||
![]() |
22dba358b8 | ||
![]() |
255050b6c5 | ||
![]() |
d3ae48dd09 | ||
![]() |
83c4199096 | ||
![]() |
d100457717 | ||
![]() |
e060c0a9bc | ||
![]() |
854eee5c5c | ||
![]() |
0ca3627668 | ||
![]() |
207fa26fd9 | ||
![]() |
7d7fd71341 | ||
![]() |
5afba7136b | ||
![]() |
1fe3e123c1 | ||
![]() |
31c912ce77 | ||
![]() |
7ed79f833b | ||
![]() |
c2fdb53925 |
40 changed files with 2763 additions and 836 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
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,5 +1,12 @@
|
||||||
*.swp
|
*.swp
|
||||||
*.so
|
|
||||||
*.o
|
*.o
|
||||||
deps/usr
|
|
||||||
deps/deps.jl
|
deps/deps.jl
|
||||||
|
deps/build.log
|
||||||
|
docs/build
|
||||||
|
*.wav
|
||||||
|
*.flac
|
||||||
|
*.cov
|
||||||
|
coverage
|
||||||
|
deps/usr/lib/pa_shim.so
|
||||||
|
deps/usr/lib/pa_shim.dylib
|
||||||
|
deps/usr/lib/pa_shim.dll
|
||||||
|
|
13
.travis.yml
13
.travis.yml
|
@ -1,13 +0,0 @@
|
||||||
language: cpp
|
|
||||||
compiler:
|
|
||||||
- clang
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
before_install:
|
|
||||||
- sudo add-apt-repository ppa:staticfloat/julia-deps -y
|
|
||||||
- sudo add-apt-repository ppa:staticfloat/julianightlies -y
|
|
||||||
- sudo apt-get update -qq -y
|
|
||||||
- sudo apt-get install libpcre3-dev julia -y
|
|
||||||
script:
|
|
||||||
- julia -e 'Pkg.init(); run(`ln -s $(pwd()) $(Pkg.dir("AudioIO"))`); Pkg.pin("AudioIO"); Pkg.resolve(); Pkg.add("BinDeps"); Pkg.build("AudioIO")'
|
|
||||||
- test/test.jl
|
|
6
LICENSE
6
LICENSE
|
@ -19,3 +19,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
suppressor.jl includes code from the Suppressor.jl package, licensed under the
|
||||||
|
MIT "Expat" License:
|
||||||
|
|
||||||
|
Copyright (c) 2016: Ismael Venegas Castelló.
|
||||||
|
|
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"]
|
166
README.md
166
README.md
|
@ -1,78 +1,126 @@
|
||||||
AudioIO.jl
|
PortAudio.jl
|
||||||
==========
|
============
|
||||||
|
|
||||||
[](https://travis-ci.org/ssfrr/AudioIO.jl)
|
[](https://JuliaAudio.github.io/PortAudio.jl/dev)
|
||||||
|
[](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml)
|
||||||
|
[](https://codecov.io/gh/JuliaAudio/PortAudio.jl)
|
||||||
|
|
||||||
AudioIO is a Julia library for interfacing to audio streams, which include
|
|
||||||
playing to and recording from sound cards, reading and writing audio files,
|
|
||||||
sending to network audio streams, etc. Currently only playing to the sound card
|
|
||||||
through PortAudio is supported. It is under heavy development, so the API could
|
|
||||||
change, there will be bugs, there are important missing features.
|
|
||||||
|
|
||||||
If you want to try it anyways, from your julia console:
|
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.
|
||||||
|
|
||||||
julia> Pkg.clone("https://github.com/ssfrr/AudioIO.jl.git")
|
## Opening a stream
|
||||||
julia> Pkg.build("AudioIO")
|
|
||||||
|
|
||||||
Basic Array Playback
|
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.
|
||||||
|
|
||||||
Arrays in various formats can be played through your soundcard. Currently the
|
```julia
|
||||||
native format that is delivered to the PortAudio backend is Float32 in the
|
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000, latency=0.1)
|
||||||
range of [-1, 1]. Arrays in other sizes of float are converted. Arrays
|
```
|
||||||
in Signed or Unsigned Integer types are scaled so that the full range is
|
|
||||||
mapped to [-1, 1] floating point values.
|
|
||||||
|
|
||||||
To play a 1-second burst of noise:
|
You can open a specific device by adding it as the first argument, either as a `PortAudioDevice` instance or by name. You can also give separate names or devices if you want different input and output devices
|
||||||
|
|
||||||
julia> v = rand(44100) * 0.1
|
```julia
|
||||||
julia> play(v)
|
PortAudioStream(device::PortAudioDevice, args...; kwargs...)
|
||||||
|
PortAudioStream(devname::AbstractString, args...; kwargs...)
|
||||||
|
```
|
||||||
|
|
||||||
AudioNodes
|
You can get a list of your system's devices with the `PortAudio.devices()` function:
|
||||||
----------
|
|
||||||
|
|
||||||
In addition to the basic `play` function you can create more complex networks
|
```julia
|
||||||
of AudioNodes in a render chain. In fact, when using the basic `play` to play
|
julia> PortAudio.devices()
|
||||||
an Array, behind the scenes an instance of the ArrayPlayer type is created
|
14-element Vector{PortAudio.PortAudioDevice}:
|
||||||
and added to the master AudioMixer inputs. Audionodes also implement a `stop`
|
"sof-hda-dsp: - (hw:0,0)" 2→2
|
||||||
function, which will remove them from the render graph. When an implicit
|
"sof-hda-dsp: - (hw:0,3)" 0→2
|
||||||
AudioNode is created automatically, such as when using `play` on an Array, the
|
"sof-hda-dsp: - (hw:0,4)" 0→2
|
||||||
`play` function should return the audio node that is playing the Array, so it
|
"sof-hda-dsp: - (hw:0,5)" 0→2
|
||||||
can be stopped if desired.
|
⋮
|
||||||
|
"upmix" 8→8
|
||||||
|
"vdownmix" 6→6
|
||||||
|
"dmix" 0→2
|
||||||
|
"default" 32→32
|
||||||
|
```
|
||||||
|
|
||||||
To explictly do the same as above:
|
## Reading and Writing
|
||||||
|
|
||||||
julia> v = rand(44100) * 0.1
|
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.
|
||||||
julia> player = ArrayPlayer(v)
|
|
||||||
julia> play(player)
|
|
||||||
|
|
||||||
To generate 2 sin tones:
|
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.
|
||||||
|
|
||||||
julia> osc1 = SinOsc(440)
|
## Debugging
|
||||||
julia> osc2 = SinOsc(660)
|
|
||||||
julia> play(osc1)
|
|
||||||
julia> play(osc2)
|
|
||||||
julia> stop(osc1)
|
|
||||||
julia> stop(osc2)
|
|
||||||
|
|
||||||
All AudioNodes must implement a `render` function that can be called to
|
If you are experiencing issues and wish to view detailed logging and debug information, set
|
||||||
retreive the next block of audio.
|
|
||||||
|
|
||||||
AudioStreams
|
```
|
||||||
------------
|
ENV["JULIA_DEBUG"] = :PortAudio
|
||||||
|
```
|
||||||
|
|
||||||
AudioStreams represent an external source or destination for audio, such as the
|
before using the package.
|
||||||
sound card. The `play` function attaches AudioNodes to the default stream
|
|
||||||
unless a stream is given as the 2nd argument.
|
|
||||||
|
|
||||||
AudioStream is an abstract type, which currently has a PortAudioStream subtype
|
## Examples
|
||||||
that writes to the sound card, and a TestAudioStream that is used in the unit
|
|
||||||
tests.
|
|
||||||
|
|
||||||
Currently only 1 stream at a time is supported so there's no reason to provide
|
### Set up an audio pass-through from microphone to speaker
|
||||||
an explicit stream to the `play` function. The stream has a root mixer field
|
|
||||||
which is an instance of the AudioMixer type, so that multiple AudioNodes
|
```julia
|
||||||
can be heard at the same time. Whenever a new frame of audio is needed by the
|
stream = PortAudioStream(2, 2)
|
||||||
sound card, the stream calls the `render` method on the root audio mixer, which
|
try
|
||||||
will in turn call the `render` methods on any input AudioNodes that are set
|
# cancel with Ctrl-C
|
||||||
up as inputs.
|
write(stream, stream)
|
||||||
|
finally
|
||||||
|
close(stream)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use `do` syntax to auto-close the stream
|
||||||
|
```julia
|
||||||
|
PortAudioStream(2, 2) do stream
|
||||||
|
write(stream, stream)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Open your built-in microphone and speaker by name
|
||||||
|
```julia
|
||||||
|
PortAudioStream("default", "default") do stream
|
||||||
|
write(stream, stream)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Record 10 seconds of audio and save to an ogg file
|
||||||
|
|
||||||
|
```julia
|
||||||
|
julia> import LibSndFile # must be in Manifest for FileIO.save to work
|
||||||
|
|
||||||
|
julia> using PortAudio: PortAudioStream
|
||||||
|
|
||||||
|
julia> using SampledSignals: s
|
||||||
|
|
||||||
|
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)
|
||||||
|
480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
|
||||||
|
10.0 s at 48000 s⁻¹
|
||||||
|
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
||||||
|
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
||||||
|
|
||||||
|
julia> close(stream)
|
||||||
|
|
||||||
|
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Play an audio signal through the default sound output device
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using PortAudio, SampledSignals
|
||||||
|
S = 8192 # sampling rate (samples / second)
|
||||||
|
x = cos.(2pi*(1:2S)*440/S) # A440 tone for 2 seconds
|
||||||
|
PortAudioStream(0, 2; samplerate=S) do stream
|
||||||
|
write(stream, x)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
24
deps/build.jl
vendored
24
deps/build.jl
vendored
|
@ -1,24 +0,0 @@
|
||||||
using BinDeps
|
|
||||||
|
|
||||||
@BinDeps.setup
|
|
||||||
|
|
||||||
ENV["JULIA_ROOT"] = abspath(JULIA_HOME, "../../")
|
|
||||||
|
|
||||||
libportaudio = library_dependency("libportaudio")
|
|
||||||
|
|
||||||
# TODO: add other providers with correct names
|
|
||||||
provides(AptGet,
|
|
||||||
{"portaudio19-dev" => libportaudio}
|
|
||||||
)
|
|
||||||
|
|
||||||
@BinDeps.install [:libportaudio => :libportaudio]
|
|
||||||
|
|
||||||
cd(joinpath(Pkg.dir(), "AudioIO", "deps", "src") )
|
|
||||||
run(`make`)
|
|
||||||
if (!ispath("../usr"))
|
|
||||||
run(`mkdir ../usr`)
|
|
||||||
end
|
|
||||||
if (!ispath("../usr/lib"))
|
|
||||||
run(`mkdir ../usr/lib`)
|
|
||||||
end
|
|
||||||
run(`mv libportaudio_shim.$(BinDeps.shlib_ext) ../usr/lib`)
|
|
51
deps/src/Makefile
vendored
51
deps/src/Makefile
vendored
|
@ -1,51 +0,0 @@
|
||||||
# Makefile lifted from Clang.jl
|
|
||||||
|
|
||||||
all: default
|
|
||||||
|
|
||||||
ifeq (exists, $(shell [ -e Make.user ] && echo exists ))
|
|
||||||
include Make.user
|
|
||||||
endif
|
|
||||||
|
|
||||||
.PHONY: all clean check-env default
|
|
||||||
|
|
||||||
#check-env:
|
|
||||||
#ifndef JULIA_INC
|
|
||||||
# $(error Environment variable JULIA_INC is not set.)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#INC =-I"$(JULIA_INC)"
|
|
||||||
FLAGS =-Wall -Wno-strict-aliasing -fno-omit-frame-pointer -fPIC
|
|
||||||
CFLAGS =-g
|
|
||||||
LIBS =-lportaudio
|
|
||||||
LINUX_LIBS =-lrt
|
|
||||||
LINUX_LDFLAGS =-rdynamic
|
|
||||||
|
|
||||||
OBJS = shim.o
|
|
||||||
|
|
||||||
# Figure out OS and architecture
|
|
||||||
OS = $(shell uname)
|
|
||||||
ifeq ($(OS), MINGW32_NT-6.1)
|
|
||||||
OS=WINNT
|
|
||||||
endif
|
|
||||||
|
|
||||||
# file extensions and platform-specific libs
|
|
||||||
ifeq ($(OS), WINNT)
|
|
||||||
SHLIB_EXT = dll
|
|
||||||
else ifeq ($(OS), Darwin)
|
|
||||||
SHLIB_EXT = dylib
|
|
||||||
else
|
|
||||||
LIBS += $(LINUX_LIBS)
|
|
||||||
LDFLAGS += $(LINUX_LDFLAGS)
|
|
||||||
SHLIB_EXT = so
|
|
||||||
endif
|
|
||||||
|
|
||||||
default: libportaudio_shim.$(SHLIB_EXT)
|
|
||||||
|
|
||||||
%.o: %.c Makefile
|
|
||||||
$(CC) $< -fPIC -c -o $@ $(INC) $(CFLAGS) $(FLAGS)
|
|
||||||
|
|
||||||
libportaudio_shim.$(SHLIB_EXT): $(OBJS)
|
|
||||||
$(CC) $(OBJS) -shared -o $@ $(LDFLAGS) $(LIBS)
|
|
||||||
|
|
||||||
clean:
|
|
||||||
rm -f *.o *.$(SHLIB_EXT)
|
|
110
deps/src/shim.c
vendored
110
deps/src/shim.c
vendored
|
@ -1,110 +0,0 @@
|
||||||
#include <portaudio.h>
|
|
||||||
#include <semaphore.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
static int paCallback(const void *inputBuffer, void *outputBuffer,
|
|
||||||
unsigned long framesPerBuffer,
|
|
||||||
const PaStreamCallbackTimeInfo* timeInfo,
|
|
||||||
PaStreamCallbackFlags statusFlags,
|
|
||||||
void *userData);
|
|
||||||
|
|
||||||
static PaStream *AudioStream;
|
|
||||||
static int JuliaPipeReadFD = 0;
|
|
||||||
static int JuliaPipeWriteFD = 0;
|
|
||||||
static sem_t CSemaphore;
|
|
||||||
static void *OutData = NULL;
|
|
||||||
static unsigned long OutFrames = 0;
|
|
||||||
|
|
||||||
|
|
||||||
int make_pipe(void)
|
|
||||||
{
|
|
||||||
int pipefd[2];
|
|
||||||
pipe(pipefd);
|
|
||||||
JuliaPipeReadFD = pipefd[0];
|
|
||||||
JuliaPipeWriteFD = pipefd[1];
|
|
||||||
sem_init(&CSemaphore, 0, 0);
|
|
||||||
return JuliaPipeReadFD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void wake_callback_thread(void *outData, unsigned int outFrames)
|
|
||||||
{
|
|
||||||
OutData = outData;
|
|
||||||
OutFrames = outFrames;
|
|
||||||
sem_post(&CSemaphore);
|
|
||||||
}
|
|
||||||
|
|
||||||
PaError open_stream(unsigned int sampleRate, unsigned int bufSize)
|
|
||||||
{
|
|
||||||
PaError err;
|
|
||||||
|
|
||||||
|
|
||||||
err = Pa_OpenDefaultStream(&AudioStream,
|
|
||||||
0, /* no input channels */
|
|
||||||
1, /* mono output */
|
|
||||||
paFloat32, /* 32 bit floating point output */
|
|
||||||
sampleRate,
|
|
||||||
bufSize, /* frames per buffer, i.e. the number of sample frames
|
|
||||||
that PortAudio will request from the callback. Many
|
|
||||||
apps may want to use paFramesPerBufferUnspecified,
|
|
||||||
which tells PortAudio to pick the best, possibly
|
|
||||||
changing, buffer size.*/
|
|
||||||
paCallback, /* this is your callback function */
|
|
||||||
NULL); /*This is a pointer that will be passed to your callback*/
|
|
||||||
if(err != paNoError)
|
|
||||||
{
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = Pa_StartStream(AudioStream);
|
|
||||||
if(err != paNoError)
|
|
||||||
{
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return paNoError;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//PaError stop_sin(void)
|
|
||||||
//{
|
|
||||||
// PaError err;
|
|
||||||
// err = Pa_StopStream(sin_stream);
|
|
||||||
// if(err != paNoError)
|
|
||||||
// {
|
|
||||||
// return err;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// err = Pa_CloseStream(sin_stream);
|
|
||||||
// if( err != paNoError )
|
|
||||||
// {
|
|
||||||
// return err;
|
|
||||||
// }
|
|
||||||
// return paNoError;
|
|
||||||
//}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 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().
|
|
||||||
*/
|
|
||||||
static int paCallback(const void *inputBuffer, void *outputBuffer,
|
|
||||||
unsigned long framesPerBuffer,
|
|
||||||
const PaStreamCallbackTimeInfo* timeInfo,
|
|
||||||
PaStreamCallbackFlags statusFlags,
|
|
||||||
void *userData)
|
|
||||||
{
|
|
||||||
unsigned int i;
|
|
||||||
unsigned char fd_data = 0;
|
|
||||||
|
|
||||||
sem_wait(&CSemaphore);
|
|
||||||
for(i=0; i<framesPerBuffer; i++)
|
|
||||||
{
|
|
||||||
((float *)outputBuffer)[i] = ((float *)OutData)[i];
|
|
||||||
}
|
|
||||||
// TODO: copy the input data somewhere
|
|
||||||
write(JuliaPipeWriteFD, &fd_data, 1);
|
|
||||||
return 0;
|
|
||||||
}
|
|
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
|
||||||
|
```
|
55
examples/audiometer.jl
Normal file
55
examples/audiometer.jl
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
using PortAudio
|
||||||
|
|
||||||
|
"""
|
||||||
|
Continuously read from the default audio input and plot an
|
||||||
|
ASCII level/peak meter
|
||||||
|
"""
|
||||||
|
function micmeter(metersize)
|
||||||
|
mic = PortAudioStream(1, 0; latency = 0.1)
|
||||||
|
|
||||||
|
signalmax = zero(eltype(mic))
|
||||||
|
println("Press Ctrl-C to quit")
|
||||||
|
while true
|
||||||
|
block = read(mic, 512)
|
||||||
|
blockmax = maximum(abs.(block)) # find the maximum value in the block
|
||||||
|
signalmax = max(signalmax, blockmax) # keep the maximum value ever
|
||||||
|
print("\r") # reset the cursor to the beginning of the line
|
||||||
|
printmeter(metersize, blockmax, signalmax)
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
function printmeter(metersize, signal, peak)
|
||||||
|
# calculate the positions in terms of characters
|
||||||
|
peakpos = clamp(round(Int, peak * metersize), 0, metersize)
|
||||||
|
meterchars = clamp(round(Int, signal * metersize), 0, peakpos - 1)
|
||||||
|
blankchars = max(0, peakpos - meterchars - 1)
|
||||||
|
|
||||||
|
for position in 1:meterchars
|
||||||
|
printstyled(">", color = barcolor(metersize, position))
|
||||||
|
end
|
||||||
|
|
||||||
|
print(" "^blankchars)
|
||||||
|
printstyled("|", color = barcolor(metersize, peakpos))
|
||||||
|
print(" "^(metersize - peakpos))
|
||||||
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
character, which is red.
|
||||||
|
"""
|
||||||
|
function barcolor(metersize, position)
|
||||||
|
if position / metersize <= 0.5
|
||||||
|
:green
|
||||||
|
elseif position == metersize
|
||||||
|
:red
|
||||||
|
else
|
||||||
|
:yellow
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
micmeter(80)
|
179
examples/lilyplay.jl
Normal file
179
examples/lilyplay.jl
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
using Distributed, PortAudio
|
||||||
|
|
||||||
|
# Modified from Jiahao Chen's example in the obsolete AudioIO module.
|
||||||
|
# Will use first output device found in system's listing or DEFAULTDEVICE if set below
|
||||||
|
const DEFAULTDEVICE = -1
|
||||||
|
|
||||||
|
function paudio()
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
duration::T
|
||||||
|
sustained::Bool
|
||||||
|
end
|
||||||
|
|
||||||
|
function play(
|
||||||
|
ostream,
|
||||||
|
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
|
||||||
|
decay_length = div(length(timesamples), 5)
|
||||||
|
v[(end - decay_length):(end - 1)] =
|
||||||
|
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
|
||||||
|
end
|
||||||
|
play(ostream, v)
|
||||||
|
sleep(A.duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
function parsevoice(melody::String; tempo = 132, beatunit = 4, lyrics = nothing)
|
||||||
|
ostream = paudio() # initialize audio for output
|
||||||
|
lyrics_syllables = lyrics == nothing ? nothing : split(lyrics)
|
||||||
|
lyrics_syllables != nothing && (lyrics_syllables[end] *= "\n")
|
||||||
|
note_idx = 1
|
||||||
|
oldduration = 4
|
||||||
|
for line in split(melody, '\n')
|
||||||
|
percent_idx = findfirst('%', line) # Trim comment
|
||||||
|
percent_idx == nothing || (line = line[1:(percent_idx - 1)])
|
||||||
|
for token in split(line)
|
||||||
|
pitch, duration, dotted, sustained = parsetoken(token)
|
||||||
|
duration == nothing && (duration = oldduration)
|
||||||
|
oldduration = duration
|
||||||
|
dotted && (duration *= 1.5)
|
||||||
|
if lyrics_syllables != nothing && 1 <= note_idx <= length(lyrics_syllables)
|
||||||
|
# Print the lyrics, omitting hyphens
|
||||||
|
if lyrics_syllables[note_idx][end] == '-'
|
||||||
|
print(join(split(lyrics_syllables[note_idx][:], "")[1:(end - 1)]), "")
|
||||||
|
else
|
||||||
|
print(lyrics_syllables[note_idx], ' ')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
play(ostream, Note(pitch, (beatunit / duration) * (60 / tempo), sustained))
|
||||||
|
note_idx += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function parsetoken(token, Atuning::Real = 220)
|
||||||
|
state = :findpitch
|
||||||
|
pitch = 0.0
|
||||||
|
sustain = dotted = false
|
||||||
|
lengthbuf = Char[]
|
||||||
|
for char in token
|
||||||
|
if state == :findpitch
|
||||||
|
scale_idx =
|
||||||
|
something(findfirst(char, String(collect('a':'g'))), 0) +
|
||||||
|
something(findfirst(char, String(collect('A':'G'))), 0)
|
||||||
|
if scale_idx != 0
|
||||||
|
halfsteps = [12, 14, 3, 5, 7, 8, 10]
|
||||||
|
pitch = Atuning * 2^(halfsteps[scale_idx] / 12)
|
||||||
|
state = :findlength
|
||||||
|
elseif char == 'r'
|
||||||
|
pitch, state = 0, :findlength
|
||||||
|
else
|
||||||
|
error("unknown pitch: $char")
|
||||||
|
end
|
||||||
|
elseif state == :findlength
|
||||||
|
if char == '#'
|
||||||
|
pitch *= 2^(1 / 12) # sharp
|
||||||
|
elseif char == 'b'
|
||||||
|
pitch /= 2^(1 / 12) # flat
|
||||||
|
elseif char == '\''
|
||||||
|
pitch *= 2 # higher octave
|
||||||
|
elseif char == ','
|
||||||
|
pitch /= 2 # lower octave
|
||||||
|
elseif char == '.'
|
||||||
|
dotted = true # dotted note
|
||||||
|
elseif char == '~'
|
||||||
|
sustain = true # tied note
|
||||||
|
else
|
||||||
|
push!(lengthbuf, char)
|
||||||
|
# Check for "is" and "es" suffixes for sharps and flats
|
||||||
|
if length(lengthbuf) >= 2
|
||||||
|
if lengthbuf[(end - 1):end] == "is"
|
||||||
|
pitch *= 2^(1 / 12)
|
||||||
|
lengthbuf = lengthbuf[1:(end - 2)]
|
||||||
|
elseif lengthbuf[(end - 1):end] == "es"
|
||||||
|
pitch /= 2^(1 / 12)
|
||||||
|
lengthbuf = lengthbuf[1:(end - 2)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
#finalize length
|
||||||
|
lengthstr = String(lengthbuf)
|
||||||
|
duration = isempty(lengthstr) ? nothing : tryparse(Int, lengthstr)
|
||||||
|
return (pitch, duration, sustain, dotted)
|
||||||
|
end
|
||||||
|
|
||||||
|
parsevoice(
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
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""",
|
||||||
|
lyrics = """
|
||||||
|
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!
|
||||||
|
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.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# And now with harmony!
|
||||||
|
|
||||||
|
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
|
||||||
|
""",
|
||||||
|
lyrics = "Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!",
|
||||||
|
)
|
||||||
|
alto = @spawn parsevoice("""
|
||||||
|
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2
|
||||||
|
""")
|
||||||
|
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
|
||||||
|
""")
|
||||||
|
bass = @spawn parsevoice("""
|
||||||
|
d. d. d. d. a,. a,. a,. b,~ c8 d. a., a., a., a., a, a8, a,4 a,2
|
||||||
|
""")
|
||||||
|
wait(soprano)
|
||||||
|
wait(alto)
|
||||||
|
wait(tenor)
|
||||||
|
wait(bass)
|
||||||
|
|
||||||
|
soprano = @spawn parsevoice(
|
||||||
|
"""
|
||||||
|
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 = @spawn parsevoice("""
|
||||||
|
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2
|
||||||
|
""")
|
||||||
|
tenor = @spawn parsevoice("""
|
||||||
|
d.4 d. d. d. d. d. d. d. d. d. c#. d. c#.~ c# d8 d d2
|
||||||
|
""")
|
||||||
|
bass = @spawn parsevoice("""
|
||||||
|
d.4 d. d. d. a,. a,. a,. a., a., a., a., a., a.,~ a, a,8 d, d,2
|
||||||
|
""")
|
||||||
|
wait(soprano)
|
||||||
|
wait(alto)
|
||||||
|
wait(tenor)
|
||||||
|
wait(bass)
|
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)
|
20
examples/spectrum.jl
Normal file
20
examples/spectrum.jl
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# plot a real-time spectrogram. This example is adapted from the GR example
|
||||||
|
# at http://gr-framework.org/examples/audio_ex.html
|
||||||
|
|
||||||
|
module SpectrumExample
|
||||||
|
|
||||||
|
using GR, PortAudio, SampledSignals, FFTW
|
||||||
|
|
||||||
|
const N = 1024
|
||||||
|
const stream = PortAudioStream(1, 0)
|
||||||
|
const buf = read(stream, N)
|
||||||
|
const fmin = 0Hz
|
||||||
|
const fmax = 10000Hz
|
||||||
|
const fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
|
||||||
|
|
||||||
|
while true
|
||||||
|
read!(stream, buf)
|
||||||
|
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100))
|
||||||
|
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
|
|
@ -1,82 +0,0 @@
|
||||||
module AudioIO
|
|
||||||
|
|
||||||
# export the basic API
|
|
||||||
export play, stop
|
|
||||||
|
|
||||||
# default stream used when none is given
|
|
||||||
_stream = nothing
|
|
||||||
|
|
||||||
################## Types ####################
|
|
||||||
|
|
||||||
typealias AudioSample Float32
|
|
||||||
# A frame of audio, possibly multi-channel
|
|
||||||
typealias AudioBuf Array{AudioSample}
|
|
||||||
|
|
||||||
# A node in the render tree
|
|
||||||
abstract AudioNode
|
|
||||||
|
|
||||||
# A stream of audio (for instance that writes to hardware)
|
|
||||||
# All AudioStream subtypes should have a mixer and info field
|
|
||||||
abstract AudioStream
|
|
||||||
|
|
||||||
# Info about the hardware device
|
|
||||||
type DeviceInfo
|
|
||||||
sample_rate::Integer
|
|
||||||
buf_size::Integer
|
|
||||||
end
|
|
||||||
|
|
||||||
include("nodes.jl")
|
|
||||||
include("portaudio.jl")
|
|
||||||
|
|
||||||
############ Exported Functions #############
|
|
||||||
|
|
||||||
# Play an AudioNode by adding it as an input to the root mixer node
|
|
||||||
function play(node::AudioNode, stream::AudioStream)
|
|
||||||
node.active = true
|
|
||||||
add_input(stream.mixer, node)
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
# If the stream is not given, use the default global PortAudio stream
|
|
||||||
function play(node::AudioNode)
|
|
||||||
global _stream
|
|
||||||
if _stream == nothing
|
|
||||||
_stream = PortAudioStream()
|
|
||||||
end
|
|
||||||
play(node, _stream)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Allow users to play a raw array by wrapping it in an ArrayPlayer
|
|
||||||
function play(arr::AudioBuf, args...)
|
|
||||||
player = ArrayPlayer(arr)
|
|
||||||
play(player, args...)
|
|
||||||
end
|
|
||||||
|
|
||||||
# If the array is the wrong floating type, convert it
|
|
||||||
function play{T <: FloatingPoint}(arr::Array{T}, args...)
|
|
||||||
arr = convert(AudioBuf, arr)
|
|
||||||
play(arr, args...)
|
|
||||||
end
|
|
||||||
|
|
||||||
# If the array is an integer type, scale to [-1, 1] floating point
|
|
||||||
|
|
||||||
# integer audio can be slightly (by 1) more negative than positive,
|
|
||||||
# so we just scale so that +/- typemax(T) becomes +/- 1
|
|
||||||
function play{T <: Signed}(arr::Array{T}, args...)
|
|
||||||
arr = arr / typemax(T)
|
|
||||||
play(arr, args...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function play{T <: Unsigned}(arr::Array{T}, args...)
|
|
||||||
zero = (typemax(T) + 1) / 2
|
|
||||||
range = floor(typemax(T) / 2)
|
|
||||||
arr = (arr - zero) / range
|
|
||||||
play(arr, args...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function stop(node::AudioNode)
|
|
||||||
node.active = false
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
end # module AudioIO
|
|
1069
src/PortAudio.jl
Normal file
1069
src/PortAudio.jl
Normal file
File diff suppressed because it is too large
Load diff
439
src/libportaudio.jl
Normal file
439
src/libportaudio.jl
Normal file
|
@ -0,0 +1,439 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
const PaError = Cint
|
||||||
|
|
||||||
|
@enum PaErrorCode::Int32 begin
|
||||||
|
paNoError = 0
|
||||||
|
paNotInitialized = -10000
|
||||||
|
paUnanticipatedHostError = -9999
|
||||||
|
paInvalidChannelCount = -9998
|
||||||
|
paInvalidSampleRate = -9997
|
||||||
|
paInvalidDevice = -9996
|
||||||
|
paInvalidFlag = -9995
|
||||||
|
paSampleFormatNotSupported = -9994
|
||||||
|
paBadIODeviceCombination = -9993
|
||||||
|
paInsufficientMemory = -9992
|
||||||
|
paBufferTooBig = -9991
|
||||||
|
paBufferTooSmall = -9990
|
||||||
|
paNullCallback = -9989
|
||||||
|
paBadStreamPtr = -9988
|
||||||
|
paTimedOut = -9987
|
||||||
|
paInternalError = -9986
|
||||||
|
paDeviceUnavailable = -9985
|
||||||
|
paIncompatibleHostApiSpecificStreamInfo = -9984
|
||||||
|
paStreamIsStopped = -9983
|
||||||
|
paStreamIsNotStopped = -9982
|
||||||
|
paInputOverflowed = -9981
|
||||||
|
paOutputUnderflowed = -9980
|
||||||
|
paHostApiNotFound = -9979
|
||||||
|
paInvalidHostApi = -9978
|
||||||
|
paCanNotReadFromACallbackStream = -9977
|
||||||
|
paCanNotWriteToACallbackStream = -9976
|
||||||
|
paCanNotReadFromAnOutputOnlyStream = -9975
|
||||||
|
paCanNotWriteToAnInputOnlyStream = -9974
|
||||||
|
paIncompatibleStreamHostApi = -9973
|
||||||
|
paBadBufferPtr = -9972
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetErrorText(errorCode)
|
||||||
|
ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), errorCode)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_Initialize()
|
||||||
|
ccall((:Pa_Initialize, libportaudio), PaError, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_Terminate()
|
||||||
|
ccall((:Pa_Terminate, libportaudio), PaError, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
const PaDeviceIndex = Cint
|
||||||
|
|
||||||
|
const PaHostApiIndex = Cint
|
||||||
|
|
||||||
|
function Pa_GetHostApiCount()
|
||||||
|
ccall((:Pa_GetHostApiCount, libportaudio), PaHostApiIndex, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetDefaultHostApi()
|
||||||
|
ccall((:Pa_GetDefaultHostApi, libportaudio), PaHostApiIndex, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
@enum PaHostApiTypeId::UInt32 begin
|
||||||
|
paInDevelopment = 0
|
||||||
|
paDirectSound = 1
|
||||||
|
paMME = 2
|
||||||
|
paASIO = 3
|
||||||
|
paSoundManager = 4
|
||||||
|
paCoreAudio = 5
|
||||||
|
paOSS = 7
|
||||||
|
paALSA = 8
|
||||||
|
paAL = 9
|
||||||
|
paBeOS = 10
|
||||||
|
paWDMKS = 11
|
||||||
|
paJACK = 12
|
||||||
|
paWASAPI = 13
|
||||||
|
paAudioScienceHPI = 14
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct PaHostApiInfo
|
||||||
|
structVersion::Cint
|
||||||
|
type::PaHostApiTypeId
|
||||||
|
name::Ptr{Cchar}
|
||||||
|
deviceCount::Cint
|
||||||
|
defaultInputDevice::PaDeviceIndex
|
||||||
|
defaultOutputDevice::PaDeviceIndex
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetHostApiInfo(hostApi)
|
||||||
|
ccall(
|
||||||
|
(:Pa_GetHostApiInfo, libportaudio),
|
||||||
|
Ptr{PaHostApiInfo},
|
||||||
|
(PaHostApiIndex,),
|
||||||
|
hostApi,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_HostApiTypeIdToHostApiIndex(type)
|
||||||
|
ccall(
|
||||||
|
(:Pa_HostApiTypeIdToHostApiIndex, libportaudio),
|
||||||
|
PaHostApiIndex,
|
||||||
|
(PaHostApiTypeId,),
|
||||||
|
type,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex)
|
||||||
|
ccall(
|
||||||
|
(:Pa_HostApiDeviceIndexToDeviceIndex, libportaudio),
|
||||||
|
PaDeviceIndex,
|
||||||
|
(PaHostApiIndex, Cint),
|
||||||
|
hostApi,
|
||||||
|
hostApiDeviceIndex,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct PaHostErrorInfo
|
||||||
|
hostApiType::PaHostApiTypeId
|
||||||
|
errorCode::Clong
|
||||||
|
errorText::Ptr{Cchar}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetLastHostErrorInfo()
|
||||||
|
ccall((:Pa_GetLastHostErrorInfo, libportaudio), Ptr{PaHostErrorInfo}, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetDeviceCount()
|
||||||
|
ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetDefaultInputDevice()
|
||||||
|
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
|
||||||
|
channelCount::Cint
|
||||||
|
sampleFormat::PaSampleFormat
|
||||||
|
suggestedLatency::PaTime
|
||||||
|
hostApiSpecificStreamInfo::Ptr{Cvoid}
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_IsFormatSupported(inputParameters, outputParameters, sampleRate)
|
||||||
|
ccall(
|
||||||
|
(:Pa_IsFormatSupported, libportaudio),
|
||||||
|
PaError,
|
||||||
|
(Ptr{PaStreamParameters}, Ptr{PaStreamParameters}, Cdouble),
|
||||||
|
inputParameters,
|
||||||
|
outputParameters,
|
||||||
|
sampleRate,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
const PaStream = Cvoid
|
||||||
|
|
||||||
|
const PaStreamFlags = Culong
|
||||||
|
|
||||||
|
mutable struct PaStreamCallbackTimeInfo
|
||||||
|
inputBufferAdcTime::PaTime
|
||||||
|
currentTime::PaTime
|
||||||
|
outputBufferDacTime::PaTime
|
||||||
|
end
|
||||||
|
|
||||||
|
const PaStreamCallbackFlags = Culong
|
||||||
|
|
||||||
|
@enum PaStreamCallbackResult::UInt32 begin
|
||||||
|
paContinue = 0
|
||||||
|
paComplete = 1
|
||||||
|
paAbort = 2
|
||||||
|
end
|
||||||
|
|
||||||
|
# typedef int PaStreamCallback ( const void * input , void * output , unsigned long frameCount , const PaStreamCallbackTimeInfo * timeInfo , PaStreamCallbackFlags statusFlags , void * userData )
|
||||||
|
const PaStreamCallback = Cvoid
|
||||||
|
|
||||||
|
function Pa_OpenStream(
|
||||||
|
stream,
|
||||||
|
inputParameters,
|
||||||
|
outputParameters,
|
||||||
|
sampleRate,
|
||||||
|
framesPerBuffer,
|
||||||
|
streamFlags,
|
||||||
|
streamCallback,
|
||||||
|
userData,
|
||||||
|
)
|
||||||
|
ccall(
|
||||||
|
(:Pa_OpenStream, libportaudio),
|
||||||
|
PaError,
|
||||||
|
(
|
||||||
|
Ptr{Ptr{PaStream}},
|
||||||
|
Ptr{PaStreamParameters},
|
||||||
|
Ptr{PaStreamParameters},
|
||||||
|
Cdouble,
|
||||||
|
Culong,
|
||||||
|
PaStreamFlags,
|
||||||
|
Ptr{Cvoid},
|
||||||
|
Ptr{Cvoid},
|
||||||
|
),
|
||||||
|
stream,
|
||||||
|
inputParameters,
|
||||||
|
outputParameters,
|
||||||
|
sampleRate,
|
||||||
|
framesPerBuffer,
|
||||||
|
streamFlags,
|
||||||
|
streamCallback,
|
||||||
|
userData,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_OpenDefaultStream(
|
||||||
|
stream,
|
||||||
|
numInputChannels,
|
||||||
|
numOutputChannels,
|
||||||
|
sampleFormat,
|
||||||
|
sampleRate,
|
||||||
|
framesPerBuffer,
|
||||||
|
streamCallback,
|
||||||
|
userData,
|
||||||
|
)
|
||||||
|
ccall(
|
||||||
|
(:Pa_OpenDefaultStream, libportaudio),
|
||||||
|
PaError,
|
||||||
|
(
|
||||||
|
Ptr{Ptr{PaStream}},
|
||||||
|
Cint,
|
||||||
|
Cint,
|
||||||
|
PaSampleFormat,
|
||||||
|
Cdouble,
|
||||||
|
Culong,
|
||||||
|
Ptr{Cvoid},
|
||||||
|
Ptr{Cvoid},
|
||||||
|
),
|
||||||
|
stream,
|
||||||
|
numInputChannels,
|
||||||
|
numOutputChannels,
|
||||||
|
sampleFormat,
|
||||||
|
sampleRate,
|
||||||
|
framesPerBuffer,
|
||||||
|
streamCallback,
|
||||||
|
userData,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_CloseStream(stream)
|
||||||
|
ccall((:Pa_CloseStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
# typedef void PaStreamFinishedCallback ( void * userData )
|
||||||
|
const PaStreamFinishedCallback = Cvoid
|
||||||
|
|
||||||
|
function Pa_SetStreamFinishedCallback(stream, streamFinishedCallback)
|
||||||
|
ccall(
|
||||||
|
(:Pa_SetStreamFinishedCallback, libportaudio),
|
||||||
|
PaError,
|
||||||
|
(Ptr{PaStream}, Ptr{Cvoid}),
|
||||||
|
stream,
|
||||||
|
streamFinishedCallback,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_StartStream(stream)
|
||||||
|
ccall((:Pa_StartStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_StopStream(stream)
|
||||||
|
ccall((:Pa_StopStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_AbortStream(stream)
|
||||||
|
ccall((:Pa_AbortStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_IsStreamStopped(stream)
|
||||||
|
ccall((:Pa_IsStreamStopped, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_IsStreamActive(stream)
|
||||||
|
ccall((:Pa_IsStreamActive, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
mutable struct PaStreamInfo
|
||||||
|
structVersion::Cint
|
||||||
|
inputLatency::PaTime
|
||||||
|
outputLatency::PaTime
|
||||||
|
sampleRate::Cdouble
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetStreamInfo(stream)
|
||||||
|
ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo}, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetStreamTime(stream)
|
||||||
|
ccall((:Pa_GetStreamTime, libportaudio), PaTime, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetStreamCpuLoad(stream)
|
||||||
|
ccall((:Pa_GetStreamCpuLoad, libportaudio), Cdouble, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_ReadStream(stream, buffer, frames)
|
||||||
|
ccall(
|
||||||
|
(:Pa_ReadStream, libportaudio),
|
||||||
|
PaError,
|
||||||
|
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||||
|
stream,
|
||||||
|
buffer,
|
||||||
|
frames,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_WriteStream(stream, buffer, frames)
|
||||||
|
ccall(
|
||||||
|
(:Pa_WriteStream, libportaudio),
|
||||||
|
PaError,
|
||||||
|
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||||
|
stream,
|
||||||
|
buffer,
|
||||||
|
frames,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetStreamReadAvailable(stream)
|
||||||
|
ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetStreamWriteAvailable(stream)
|
||||||
|
ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_GetSampleSize(format)
|
||||||
|
ccall((:Pa_GetSampleSize, libportaudio), PaError, (PaSampleFormat,), format)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Pa_Sleep(msec)
|
||||||
|
ccall((:Pa_Sleep, libportaudio), Cvoid, (Clong,), msec)
|
||||||
|
end
|
||||||
|
|
||||||
|
const paNoDevice = PaDeviceIndex(-1)
|
||||||
|
|
||||||
|
const paUseHostApiSpecificDeviceSpecification = PaDeviceIndex(-2)
|
||||||
|
|
||||||
|
const paFloat32 = PaSampleFormat(0x00000001)
|
||||||
|
|
||||||
|
const paInt32 = PaSampleFormat(0x00000002)
|
||||||
|
|
||||||
|
const paInt24 = PaSampleFormat(0x00000004)
|
||||||
|
|
||||||
|
const paInt16 = PaSampleFormat(0x00000008)
|
||||||
|
|
||||||
|
const paInt8 = PaSampleFormat(0x00000010)
|
||||||
|
|
||||||
|
const paUInt8 = PaSampleFormat(0x00000020)
|
||||||
|
|
||||||
|
const paCustomFormat = PaSampleFormat(0x00010000)
|
||||||
|
|
||||||
|
const paNonInterleaved = PaSampleFormat(0x80000000)
|
||||||
|
|
||||||
|
const paFormatIsSupported = 0
|
||||||
|
|
||||||
|
const paFramesPerBufferUnspecified = 0
|
||||||
|
|
||||||
|
const paNoFlag = PaStreamFlags(0)
|
||||||
|
|
||||||
|
const paClipOff = PaStreamFlags(0x00000001)
|
||||||
|
|
||||||
|
const paDitherOff = PaStreamFlags(0x00000002)
|
||||||
|
|
||||||
|
const paNeverDropInput = PaStreamFlags(0x00000004)
|
||||||
|
|
||||||
|
const paPrimeOutputBuffersUsingStreamCallback = PaStreamFlags(0x00000008)
|
||||||
|
|
||||||
|
const paPlatformSpecificFlags = PaStreamFlags(0xffff0000)
|
||||||
|
|
||||||
|
const paInputUnderflow = PaStreamCallbackFlags(0x00000001)
|
||||||
|
|
||||||
|
const paInputOverflow = PaStreamCallbackFlags(0x00000002)
|
||||||
|
|
||||||
|
const paOutputUnderflow = PaStreamCallbackFlags(0x00000004)
|
||||||
|
|
||||||
|
const paOutputOverflow = PaStreamCallbackFlags(0x00000008)
|
||||||
|
|
||||||
|
const paPrimingOutput = PaStreamCallbackFlags(0x00000010)
|
||||||
|
|
||||||
|
# exports
|
||||||
|
const PREFIXES = ["Pa", "pa"]
|
||||||
|
for name in names(@__MODULE__; all = true), prefix in PREFIXES
|
||||||
|
if startswith(string(name), prefix)
|
||||||
|
@eval export $name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end # module
|
139
src/nodes.jl
139
src/nodes.jl
|
@ -1,139 +0,0 @@
|
||||||
export SinOsc, AudioMixer, ArrayPlayer, AudioInput
|
|
||||||
|
|
||||||
#### SinOsc ####
|
|
||||||
|
|
||||||
# Generates a sin tone at the given frequency
|
|
||||||
|
|
||||||
type SinOsc <: AudioNode
|
|
||||||
active::Bool
|
|
||||||
freq::Real
|
|
||||||
phase::FloatingPoint
|
|
||||||
|
|
||||||
function SinOsc(freq::Real)
|
|
||||||
new(false, freq, 0.0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function render(node::SinOsc, device_input::AudioBuf, info::DeviceInfo)
|
|
||||||
phase = AudioSample[1:info.buf_size] * 2pi * node.freq / info.sample_rate
|
|
||||||
phase += node.phase
|
|
||||||
node.phase = phase[end]
|
|
||||||
return sin(phase), node.active
|
|
||||||
end
|
|
||||||
|
|
||||||
#### AudioMixer ####
|
|
||||||
|
|
||||||
# Mixes a set of inputs equally
|
|
||||||
|
|
||||||
# a convenience alias used in the array of mix inputs
|
|
||||||
typealias MaybeAudioNode Union(AudioNode, Nothing)
|
|
||||||
const MAX_MIXER_INPUTS = 32
|
|
||||||
|
|
||||||
type AudioMixer <: AudioNode
|
|
||||||
active::Bool
|
|
||||||
mix_inputs::Array{MaybeAudioNode}
|
|
||||||
|
|
||||||
function AudioMixer{T <: AudioNode}(mix_inputs::Array{T})
|
|
||||||
input_array = Array(MaybeAudioNode, MAX_MIXER_INPUTS)
|
|
||||||
fill!(input_array, nothing)
|
|
||||||
for (i, node) in enumerate(mix_inputs)
|
|
||||||
input_array[i] = node
|
|
||||||
end
|
|
||||||
new(false, input_array)
|
|
||||||
end
|
|
||||||
|
|
||||||
function AudioMixer()
|
|
||||||
AudioMixer(AudioNode[])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: at some point we need to figure out what the general API is for wiring
|
|
||||||
# up AudioNodes to each other
|
|
||||||
function add_input(mixer::AudioMixer, in_node::AudioNode)
|
|
||||||
for (i, node) in enumerate(mixer.mix_inputs)
|
|
||||||
if node === nothing
|
|
||||||
mixer.mix_inputs[i] = in_node
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
error("Mixer input array is full")
|
|
||||||
end
|
|
||||||
|
|
||||||
# removes the given node from the mix inputs. If the node isn't an input the
|
|
||||||
# function returns without error
|
|
||||||
function remove_input(mixer::AudioMixer, in_node::AudioNode)
|
|
||||||
for (i, node) in enumerate(mixer.mix_inputs)
|
|
||||||
if node === in_node
|
|
||||||
mixer.mix_inputs[i] = nothing
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
# not an error if we didn't find it
|
|
||||||
end
|
|
||||||
|
|
||||||
function render(node::AudioMixer, device_input::AudioBuf, info::DeviceInfo)
|
|
||||||
# TODO: we probably want to pre-allocate this buffer and share between
|
|
||||||
# render calls. Unfortunately we don't know the right size when the object
|
|
||||||
# is created, so maybe we check the size on every render call and only
|
|
||||||
# re-allocate when the size changes? I suppose that's got to be cheaper
|
|
||||||
# than the GC and allocation every frame
|
|
||||||
mix_buffer = zeros(AudioSample, info.buf_size)
|
|
||||||
for in_node in node.mix_inputs
|
|
||||||
if in_node !== nothing
|
|
||||||
in_buffer, active = render(in_node, device_input, info)
|
|
||||||
mix_buffer += in_buffer
|
|
||||||
if !active
|
|
||||||
remove_input(node, in_node)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return mix_buffer, node.active
|
|
||||||
end
|
|
||||||
|
|
||||||
#### Array Player ####
|
|
||||||
|
|
||||||
# Plays a AudioBuf by rendering it out piece-by-piece
|
|
||||||
|
|
||||||
type ArrayPlayer <: AudioNode
|
|
||||||
active::Bool
|
|
||||||
arr::AudioBuf
|
|
||||||
arr_index::Int
|
|
||||||
|
|
||||||
function ArrayPlayer(arr::AudioBuf)
|
|
||||||
new(false, arr, 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function render(node::ArrayPlayer, device_input::AudioBuf, info::DeviceInfo)
|
|
||||||
# TODO: this should remove itself from the render tree when playback is
|
|
||||||
# complete
|
|
||||||
i = node.arr_index
|
|
||||||
range_end = min(i + info.buf_size-1, length(node.arr))
|
|
||||||
output = node.arr[i:range_end]
|
|
||||||
if length(output) < info.buf_size
|
|
||||||
# we're finished with the array, pad with zeros and clear our active
|
|
||||||
# flag
|
|
||||||
output = vcat(output, zeros(AudioSample, info.buf_size - length(output)))
|
|
||||||
node.active = false
|
|
||||||
end
|
|
||||||
node.arr_index = range_end + 1
|
|
||||||
return output, node.active
|
|
||||||
end
|
|
||||||
|
|
||||||
#### AudioInput ####
|
|
||||||
|
|
||||||
# Renders incoming audio input from the hardware
|
|
||||||
|
|
||||||
type AudioInput <: AudioNode
|
|
||||||
active::Bool
|
|
||||||
channel::Int
|
|
||||||
|
|
||||||
function AudioInput(channel::Int)
|
|
||||||
new(false, channel)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function render(node::AudioInput, device_input::AudioBuf, info::DeviceInfo)
|
|
||||||
@assert size(device_input, 1) == info.buf_size
|
|
||||||
return device_input[:, node.channel], node.active
|
|
||||||
end
|
|
176
src/portaudio.jl
176
src/portaudio.jl
|
@ -1,176 +0,0 @@
|
||||||
typealias PaTime Cdouble
|
|
||||||
typealias PaError Cint
|
|
||||||
typealias PaSampleFormat Culong
|
|
||||||
typealias PaStream Void
|
|
||||||
|
|
||||||
const PA_NO_ERROR = 0
|
|
||||||
const libportaudio_shim = find_library(["libportaudio_shim",],
|
|
||||||
[Pkg.dir("AudioIO", "deps", "usr", "lib"),])
|
|
||||||
|
|
||||||
# track whether we've already inited PortAudio
|
|
||||||
portaudio_inited = false
|
|
||||||
|
|
||||||
################## Types ####################
|
|
||||||
|
|
||||||
type PortAudioStream <: AudioStream
|
|
||||||
mixer::AudioMixer
|
|
||||||
info::DeviceInfo
|
|
||||||
|
|
||||||
function PortAudioStream(sample_rate::Int=44100, buf_size::Int=1024)
|
|
||||||
global portaudio_inited
|
|
||||||
if !portaudio_inited
|
|
||||||
@assert(libportaudio_shim != "", "Failed to find required library libportaudio_shim. Try re-running the package script using Pkg.build(\"AudioIO\"), then reloading with reload(\"AudioIO\")")
|
|
||||||
|
|
||||||
init_portaudio()
|
|
||||||
portaudio_inited = true
|
|
||||||
else
|
|
||||||
error("Currently only 1 stream is supported at a time")
|
|
||||||
end
|
|
||||||
mixer = AudioMixer()
|
|
||||||
stream = new(mixer, DeviceInfo(sample_rate, buf_size))
|
|
||||||
# we need to start up the stream with the portaudio library
|
|
||||||
open_portaudio_stream(stream)
|
|
||||||
return stream
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
############ Internal Functions ############
|
|
||||||
|
|
||||||
function wake_callback_thread(out_array)
|
|
||||||
ccall((:wake_callback_thread, libportaudio_shim), Void,
|
|
||||||
(Ptr{Void}, Cuint),
|
|
||||||
out_array, size(out_array, 1))
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
function init_portaudio()
|
|
||||||
info("Initializing PortAudio. Expect errors as we scan devices")
|
|
||||||
err = ccall((:Pa_Initialize, "libportaudio"), PaError, ())
|
|
||||||
handle_status(err)
|
|
||||||
end
|
|
||||||
|
|
||||||
function open_portaudio_stream(stream::PortAudioStream)
|
|
||||||
# starts up a stream with the portaudio library and associates it with the
|
|
||||||
# given AudioIO PortAudioStream
|
|
||||||
|
|
||||||
# TODO: handle more streams
|
|
||||||
|
|
||||||
fd = ccall((:make_pipe, libportaudio_shim), Cint, ())
|
|
||||||
|
|
||||||
info("Launching PortAudio Task...")
|
|
||||||
function task_wrapper()
|
|
||||||
portaudio_task(fd, stream)
|
|
||||||
end
|
|
||||||
schedule(Task(task_wrapper))
|
|
||||||
# TODO: test not yielding here
|
|
||||||
yield()
|
|
||||||
info("Audio Task Yielded, starting the stream...")
|
|
||||||
|
|
||||||
err = ccall((:open_stream, libportaudio_shim), PaError,
|
|
||||||
(Cuint, Cuint),
|
|
||||||
stream.info.sample_rate, stream.info.buf_size)
|
|
||||||
handle_status(err)
|
|
||||||
info("Portaudio stream started.")
|
|
||||||
end
|
|
||||||
|
|
||||||
function handle_status(err::PaError)
|
|
||||||
if err != PA_NO_ERROR
|
|
||||||
msg = ccall((:Pa_GetErrorText, "libportaudio"),
|
|
||||||
Ptr{Cchar}, (PaError,), err)
|
|
||||||
error("libportaudio: " * bytestring(msg))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function portaudio_task(jl_filedesc::Integer, stream::PortAudioStream)
|
|
||||||
info("Audio Task Launched")
|
|
||||||
in_array = zeros(AudioSample, stream.info.buf_size)
|
|
||||||
desc_bytes = Cchar[0]
|
|
||||||
jl_stream = fdio(jl_filedesc)
|
|
||||||
jl_rawfd = RawFD(jl_filedesc)
|
|
||||||
try
|
|
||||||
while true
|
|
||||||
# assume the root mixer is always active
|
|
||||||
out_array::AudioBuf, _::Bool = render(stream.mixer, in_array,
|
|
||||||
stream.info)
|
|
||||||
# wake the C code so it knows we've given it some more data
|
|
||||||
wake_callback_thread(out_array)
|
|
||||||
# wait for new data to be available from the sound card (and for it
|
|
||||||
# to have processed our last frame of data). At some point we
|
|
||||||
# should do something with the data we get from the callback
|
|
||||||
wait(jl_rawfd, readable=true)
|
|
||||||
# read from the file descriptor so that it's empty. We're using
|
|
||||||
# ccall here because readbytes() was blocking the whole julia
|
|
||||||
# thread. This shouldn't block at all because we just waited on it
|
|
||||||
ccall(:read, Clong, (Cint, Ptr{Void}, Culong),
|
|
||||||
jl_filedesc, desc_bytes, 1)
|
|
||||||
end
|
|
||||||
finally
|
|
||||||
# TODO: we need to close the stream here. Otherwise the audio callback
|
|
||||||
# will segfault accessing the output array if there were exceptions
|
|
||||||
# thrown in the render loop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Old code for reference during initial development. We can get rid of this
|
|
||||||
# once the library is a little more mature
|
|
||||||
|
|
||||||
|
|
||||||
#type PaStreamCallbackTimeInfo
|
|
||||||
# inputBufferAdcTime::PaTime
|
|
||||||
# currentTime::PaTime
|
|
||||||
# outputBufferDacTime::PaTime
|
|
||||||
#end
|
|
||||||
#
|
|
||||||
#typealias PaStreamCallbackFlags Culong
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#function stream_callback{T}( input_::Ptr{T},
|
|
||||||
# output_::Ptr{T},
|
|
||||||
# frame_count::Culong,
|
|
||||||
# time_info::Ptr{PaStreamCallbackTimeInfo},
|
|
||||||
# status_flags::PaStreamCallbackFlags,
|
|
||||||
# user_data::Ptr{Void})
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# println("stfl:$status_flags \tframe_count:$frame_count")
|
|
||||||
#
|
|
||||||
# ret = 0
|
|
||||||
# return convert(Cint,ret)::Cint #continue stream
|
|
||||||
#
|
|
||||||
#end
|
|
||||||
#
|
|
||||||
#T=Float32
|
|
||||||
#stream_callback_c = cfunction(stream_callback,Cint,
|
|
||||||
#(Ptr{T},Ptr{T},Culong,Ptr{PaStreamCallbackTimeInfo},PaStreamCallbackFlags,Ptr{Void})
|
|
||||||
#)
|
|
||||||
#stream_obj = Array(Ptr{PaStream},1)
|
|
||||||
#
|
|
||||||
#pa_err = ccall(
|
|
||||||
#(:Pa_Initialize,"libportaudio"),
|
|
||||||
#PaError,
|
|
||||||
#(),
|
|
||||||
#)
|
|
||||||
#
|
|
||||||
#println(get_error_text(pa_err))
|
|
||||||
#
|
|
||||||
#pa_err = ccall(
|
|
||||||
#(:Pa_OpenDefaultStream,"libportaudio"),
|
|
||||||
#PaError,
|
|
||||||
#(Ptr{Ptr{PaStream}},Cint,Cint,PaSampleFormat,Cdouble,Culong,Ptr{Void},Any),
|
|
||||||
#stream_obj,0,1,0x1,8000.0,4096,stream_callback_c,None
|
|
||||||
#)
|
|
||||||
#
|
|
||||||
#println(get_error_text(pa_err))
|
|
||||||
#
|
|
||||||
#function start_stream(stream)
|
|
||||||
# pa_err = ccall(
|
|
||||||
# (:Pa_StartStream,"libportaudio"),
|
|
||||||
# PaError,
|
|
||||||
# (Ptr{PaStream},),
|
|
||||||
# stream
|
|
||||||
# )
|
|
||||||
# println(get_error_text(pa_err))
|
|
||||||
#end
|
|
||||||
#
|
|
||||||
#end #module
|
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
256
test/runtests.jl
Executable file
256
test/runtests.jl
Executable file
|
@ -0,0 +1,256 @@
|
||||||
|
#!/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
|
||||||
|
|
||||||
|
@testset "Tests without sound" begin
|
||||||
|
@testset "Reports version" begin
|
||||||
|
io = IOBuffer()
|
||||||
|
PortAudio.versioninfo(io)
|
||||||
|
result = split(String(take!((io))), "\n")
|
||||||
|
# make sure this is the same version I tested with
|
||||||
|
@test startswith(result[1], "PortAudio V19")
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "Can list devices without crashing" begin
|
||||||
|
display(devices())
|
||||||
|
println()
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "libortaudio without sound" begin
|
||||||
|
@test handle_status(Pa_GetHostApiCount()) >= 0
|
||||||
|
@test handle_status(Pa_GetDefaultHostApi()) >= 0
|
||||||
|
# version info not available on windows?
|
||||||
|
if !Sys.iswindows()
|
||||||
|
@test safe_load(Pa_GetVersionInfo(), ErrorException("no info")) isa
|
||||||
|
PaVersionInfo
|
||||||
|
end
|
||||||
|
@test safe_load(Pa_GetLastHostErrorInfo(), ErrorException("no info")) isa
|
||||||
|
PaHostErrorInfo
|
||||||
|
@test PaErrorCode(Pa_IsFormatSupported(C_NULL, C_NULL, 0.0)) == paInvalidDevice
|
||||||
|
@test PaErrorCode(
|
||||||
|
Pa_OpenDefaultStream(Ref(C_NULL), 0, 0, paFloat32, 0.0, 0, C_NULL, C_NULL),
|
||||||
|
) == paInvalidDevice
|
||||||
|
end
|
||||||
|
|
||||||
|
@testset "Errors without sound" begin
|
||||||
|
@test sprint(showerror, PortAudioException(paNotInitialized)) ==
|
||||||
|
"PortAudioException: PortAudio not initialized"
|
||||||
|
@test_throws KeyError("foobarbaz") get_device("foobarbaz")
|
||||||
|
@test_throws KeyError(-1) get_device(-1)
|
||||||
|
@test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(())
|
||||||
|
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
|
||||||
|
PaError(paOutputUnderflowed),
|
||||||
|
)
|
||||||
|
@test_throws PortAudioException(paNotInitialized) handle_status(
|
||||||
|
PaError(paNotInitialized),
|
||||||
|
)
|
||||||
|
Pa_Sleep(1)
|
||||||
|
@test Pa_GetSampleSize(paFloat32) == 4
|
||||||
|
end
|
||||||
|
|
||||||
|
# make sure we can terminate, then reinitialize
|
||||||
|
terminate()
|
||||||
|
initialize()
|
||||||
|
end
|
||||||
|
|
||||||
|
if isempty(devices())
|
||||||
|
@test_throws ArgumentError("No input device available") get_default_input_index()
|
||||||
|
else
|
||||||
|
@testset "Tests with sound" begin
|
||||||
|
# these default values are specific to local machines
|
||||||
|
input_name = get_device(get_default_input_index()).name
|
||||||
|
output_name = get_device(get_default_output_index()).name
|
||||||
|
|
||||||
|
@testset "Interactive tests" begin
|
||||||
|
println("Recording...")
|
||||||
|
stream = PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true)
|
||||||
|
buffer = read(stream, 5s)
|
||||||
|
@test size(buffer) ==
|
||||||
|
(round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
||||||
|
close(stream)
|
||||||
|
sleep(1)
|
||||||
|
println("Playing back recording...")
|
||||||
|
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||||
|
write(stream, buffer)
|
||||||
|
end
|
||||||
|
sleep(1)
|
||||||
|
println("Testing pass-through")
|
||||||
|
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
|
||||||
|
write_buffer(stream.sink_messenger.buffer, acquire_lock = false)
|
||||||
|
sink = stream.sink
|
||||||
|
source = stream.source
|
||||||
|
@test sprint(show, stream) == """
|
||||||
|
PortAudioStream{Float32}
|
||||||
|
Samplerate: 44100Hz
|
||||||
|
2 channel sink: $(repr(output_name))
|
||||||
|
2 channel source: $(repr(input_name))"""
|
||||||
|
@test sprint(show, source) == "2 channel source: $(repr(input_name))"
|
||||||
|
@test sprint(show, sink) == "2 channel sink: $(repr(output_name))"
|
||||||
|
write(stream, stream, 5s)
|
||||||
|
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
|
||||||
|
@test isopen(stream)
|
||||||
|
close(stream)
|
||||||
|
sleep(1)
|
||||||
|
@test !isopen(stream)
|
||||||
|
@test !isopen(sink)
|
||||||
|
@test !isopen(source)
|
||||||
|
println("done")
|
||||||
|
end
|
||||||
|
@testset "Samplerate-converting writing" begin
|
||||||
|
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||||
|
write(
|
||||||
|
stream,
|
||||||
|
SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]),
|
||||||
|
3s,
|
||||||
|
)
|
||||||
|
println("expected blip")
|
||||||
|
write(
|
||||||
|
stream,
|
||||||
|
SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]),
|
||||||
|
3s,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sleep(1)
|
||||||
|
# no way to check that the right data is actually getting read or written here,
|
||||||
|
# but at least it's not crashing.
|
||||||
|
@testset "Queued Writing" begin
|
||||||
|
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
|
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
|
17
test/test.jl
17
test/test.jl
|
@ -1,17 +0,0 @@
|
||||||
#!/usr/bin/env julia
|
|
||||||
|
|
||||||
test_regex = r"^test_.*\.jl$"
|
|
||||||
test_dir = "test"
|
|
||||||
|
|
||||||
test_files = filter(n -> ismatch(test_regex, n), readdir(test_dir))
|
|
||||||
if length(test_files) == 0
|
|
||||||
error("No test files found. Make sure you're running from the root directory")
|
|
||||||
end
|
|
||||||
|
|
||||||
for test_file in test_files
|
|
||||||
info("")
|
|
||||||
info("Running tests from \"$(test_file)\"...")
|
|
||||||
info("===================================================================")
|
|
||||||
include(test_file)
|
|
||||||
info("===================================================================")
|
|
||||||
end
|
|
|
@ -1,74 +0,0 @@
|
||||||
using Base.Test
|
|
||||||
using AudioIO
|
|
||||||
|
|
||||||
const TEST_SAMPLERATE = 44100
|
|
||||||
const TEST_BUF_SIZE = 1024
|
|
||||||
|
|
||||||
type TestAudioStream <: AudioIO.AudioStream
|
|
||||||
mixer::AudioMixer
|
|
||||||
info::AudioIO.DeviceInfo
|
|
||||||
|
|
||||||
function TestAudioStream()
|
|
||||||
mixer = AudioMixer()
|
|
||||||
new(mixer, AudioIO.DeviceInfo(TEST_SAMPLERATE, TEST_BUF_SIZE))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# render the stream and return the next block of audio. This is used in testing
|
|
||||||
# to simulate the audio callback that's normally called by the device.
|
|
||||||
function process(stream::TestAudioStream)
|
|
||||||
in_array = zeros(AudioIO.AudioSample, stream.info.buf_size)
|
|
||||||
out_array, _ = AudioIO.render(stream.mixer, in_array, stream.info)
|
|
||||||
return out_array
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
#### Test playing back various vector types ####
|
|
||||||
|
|
||||||
# data shared between tests, for convenience
|
|
||||||
t = linspace(0, 2, 2 * 44100)
|
|
||||||
phase = 2pi * 100 * t
|
|
||||||
|
|
||||||
## Test Float32 arrays, this is currently the native audio playback format
|
|
||||||
info("Testing Playing Float32 arrays...")
|
|
||||||
f32 = convert(Array{Float32}, sin(phase))
|
|
||||||
test_stream = TestAudioStream()
|
|
||||||
player = play(f32, test_stream)
|
|
||||||
@test process(test_stream) == f32[1:TEST_BUF_SIZE]
|
|
||||||
|
|
||||||
|
|
||||||
info("Testing Playing Float64 arrays...")
|
|
||||||
f64 = convert(Array{Float64}, sin(phase))
|
|
||||||
test_stream = TestAudioStream()
|
|
||||||
player = play(f64, test_stream)
|
|
||||||
@test process(test_stream) == convert(AudioIO.AudioBuf, f64[1:TEST_BUF_SIZE])
|
|
||||||
|
|
||||||
info("Testing Playing Int8(Signed) arrays...")
|
|
||||||
i8 = Int8[-127:127]
|
|
||||||
test_stream = TestAudioStream()
|
|
||||||
player = play(i8, test_stream)
|
|
||||||
@test_approx_eq(process(test_stream)[1:255],
|
|
||||||
convert(AudioIO.AudioBuf, linspace(-1.0, 1.0, 255)))
|
|
||||||
|
|
||||||
info("Testing Playing Uint8(Unsigned) arrays...")
|
|
||||||
# for unsigned 8-bit audio silence is represented as 128, so the symmetric range
|
|
||||||
# is 1-255
|
|
||||||
ui8 = Uint8[1:255]
|
|
||||||
test_stream = TestAudioStream()
|
|
||||||
player = play(ui8, test_stream)
|
|
||||||
@test_approx_eq(process(test_stream)[1:255],
|
|
||||||
convert(AudioIO.AudioBuf, linspace(-1.0, 1.0, 255)))
|
|
||||||
|
|
||||||
|
|
||||||
info("Testing AudioNode Stopping...")
|
|
||||||
test_stream = TestAudioStream()
|
|
||||||
node = SinOsc(440)
|
|
||||||
@test !node.active
|
|
||||||
play(node, test_stream)
|
|
||||||
@test node.active
|
|
||||||
process(test_stream)
|
|
||||||
stop(node)
|
|
||||||
@test !node.active
|
|
||||||
# give the render task a chance to clean up
|
|
||||||
process(test_stream)
|
|
||||||
@test process(test_stream) == zeros(AudioIO.AudioSample, TEST_BUF_SIZE)
|
|
|
@ -1,89 +0,0 @@
|
||||||
using Base.Test
|
|
||||||
using AudioIO
|
|
||||||
|
|
||||||
test_info = AudioIO.DeviceInfo(44100, 512)
|
|
||||||
dev_input = zeros(AudioIO.AudioSample, test_info.buf_size)
|
|
||||||
|
|
||||||
# A TestNode just renders out 1:buf_size each frame
|
|
||||||
type TestNode <: AudioIO.AudioNode
|
|
||||||
active::Bool
|
|
||||||
|
|
||||||
function TestNode()
|
|
||||||
return new(false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function AudioIO.render(node::TestNode,
|
|
||||||
device_input::AudioIO.AudioBuf,
|
|
||||||
info::AudioIO.DeviceInfo)
|
|
||||||
return AudioIO.AudioSample[1:info.buf_size], node.active
|
|
||||||
end
|
|
||||||
|
|
||||||
#### AudioMixer Tests ####
|
|
||||||
|
|
||||||
# TODO: there should be a setup/teardown mechanism and some way to isolate
|
|
||||||
# tests
|
|
||||||
|
|
||||||
info("Testing AudioMixer...")
|
|
||||||
mix = AudioMixer()
|
|
||||||
render_output, active = AudioIO.render(mix, dev_input, test_info)
|
|
||||||
@test render_output == zeros(AudioIO.AudioSample, test_info.buf_size)
|
|
||||||
|
|
||||||
testnode = TestNode()
|
|
||||||
mix = AudioMixer([testnode])
|
|
||||||
render_output, active = AudioIO.render(mix, dev_input, test_info)
|
|
||||||
@test render_output == AudioIO.AudioSample[1:test_info.buf_size]
|
|
||||||
|
|
||||||
test1 = TestNode()
|
|
||||||
test2 = TestNode()
|
|
||||||
mix = AudioMixer([test1, test2])
|
|
||||||
render_output, active = AudioIO.render(mix, dev_input, test_info)
|
|
||||||
# make sure the two inputs are being added together
|
|
||||||
@test render_output == 2 * AudioIO.AudioSample[1:test_info.buf_size]
|
|
||||||
|
|
||||||
# now we'll stop one of the inputs and make sure it gets removed
|
|
||||||
# TODO: this test should depend on the render output, not on the internals of
|
|
||||||
# the mixer
|
|
||||||
stop(test1)
|
|
||||||
AudioIO.render(mix, dev_input, test_info)
|
|
||||||
@test !in(test1, mix.mix_inputs)
|
|
||||||
|
|
||||||
stop(mix)
|
|
||||||
render_output, active = AudioIO.render(mix, dev_input, test_info)
|
|
||||||
@test !active
|
|
||||||
|
|
||||||
info("Testing SinOSC...")
|
|
||||||
freq = 440
|
|
||||||
t = linspace(1 / test_info.sample_rate,
|
|
||||||
test_info.buf_size / test_info.sample_rate,
|
|
||||||
test_info.buf_size)
|
|
||||||
test_vect = convert(AudioIO.AudioBuf, sin(2pi * t * freq))
|
|
||||||
osc = SinOsc(freq)
|
|
||||||
render_output, active = AudioIO.render(osc, dev_input, test_info)
|
|
||||||
@test_approx_eq(render_output, test_vect)
|
|
||||||
stop(osc)
|
|
||||||
render_output, active = AudioIO.render(osc, dev_input, test_info)
|
|
||||||
@test !active
|
|
||||||
|
|
||||||
info("Testing ArrayPlayer...")
|
|
||||||
v = rand(AudioIO.AudioSample, 44100)
|
|
||||||
player = ArrayPlayer(v)
|
|
||||||
player.active = true
|
|
||||||
render_output, active = AudioIO.render(player, dev_input, test_info)
|
|
||||||
@test render_output == v[1:test_info.buf_size]
|
|
||||||
@test active
|
|
||||||
render_output, active = AudioIO.render(player, dev_input, test_info)
|
|
||||||
@test render_output == v[(test_info.buf_size + 1) : (2*test_info.buf_size)]
|
|
||||||
@test active
|
|
||||||
stop(player)
|
|
||||||
render_output, active = AudioIO.render(player, dev_input, test_info)
|
|
||||||
@test !active
|
|
||||||
|
|
||||||
# give a vector just a bit larger than 1 buffer size
|
|
||||||
v = rand(AudioIO.AudioSample, test_info.buf_size + 1)
|
|
||||||
player = ArrayPlayer(v)
|
|
||||||
player.active = true
|
|
||||||
_, active = AudioIO.render(player, dev_input, test_info)
|
|
||||||
@test active
|
|
||||||
_, active = AudioIO.render(player, dev_input, test_info)
|
|
||||||
@test !active
|
|
Loading…
Add table
Reference in a new issue