Compare commits

...

122 commits

Author SHA1 Message Date
Brandon Taylor
06c6fd0495 fix bug, test 2022-07-24 11:54:55 -04:00
bramtayl
fbcd539a76
Allow skipping locks, precompile (#120)
* Allow skipping locks, precompile

* fix tests

* version
2022-07-23 15:42:04 -04:00
Jeff Fessler
3939d47a8d
Add tone with buffer example (#117) 2022-04-05 14:32:13 -04:00
Jeff Fessler
19a49931ad
Merge pull request #116 from JuliaAudio/jf-v1.2
Back to v1.2
2022-04-02 18:34:33 -04:00
Jeff Fessler
d21e1e0363 Back to v1.2 2022-04-02 18:12:53 -04:00
Jeff Fessler
7e0ca0122f
Fix remaining messanger typos, add docstring (#115)
* Fix typo, add docstring

* v1.3.0
2022-04-02 18:04:30 -04:00
bramtayl
156eae0db8
Update readme (#111)
* update readme

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update src/PortAudio.jl

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>
2022-03-29 13:00:39 -04:00
Abhaya Parthy
497567e329
Update save file example in README.md (#102)
* Update save file example in README.md

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Update README.md

Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>

* Remove extra stream

Co-authored-by: bramtayl <brandon.taylor221@gmail.com>
Co-authored-by: Jeff Fessler <JeffFessler@users.noreply.github.com>
2022-03-23 11:36:06 -04:00
Jeff Fessler
24acc0247b
Add octave shift example (#110)
* Add octave shift example

* specify duration

* use for loop
2022-03-22 11:06:41 -04:00
bramtayl
78a0a9918d
work with vector buffers (#109)
* work with vector buffers

* no redundant tests
2022-03-09 17:24:25 -05:00
Jeff Fessler
9c701415c0
Add audio signal output example (#94) 2022-02-13 22:19:12 -05:00
Jeevith Gnanakumaran
f6cd300ec8
avoid fields with abstract types (#100)
https://docs.julialang.org/en/v1/manual/performance-tips/#Avoid-fields-with-abstract-type

The Buffer contains a field which is of type Array{T} where T. This is an abstract type, and to make it concrete, we need to specify the dimension of the array (2).
2022-01-10 12:38:16 -05:00
bramtayl
d570288ebe
no runtime error capturing (#99) 2022-01-07 11:53:34 -05:00
bramtayl
44d4ca38f8
spelling (#98)
* spelling

* version
2022-01-06 09:56:42 -05:00
Robert Luke
3cd4551d81
Remove documentation for depreciated synced keyword (#82) 2021-08-12 13:55:46 -04:00
Robert Luke
7799ea1749
Fix name of documentation scripts (#80) 2021-08-11 19:54:15 +10:00
Robert Luke
8a3b0d2a8a
Revert changes to README 2021-07-31 21:16:47 +10:00
Brandon Taylor
17faf321e7 Add lower bound for suppressor 2021-07-25 17:27:33 -04:00
Brandon Taylor
01c58dab91 Bump version 2021-07-25 15:18:59 -04:00
Brandon Taylor
d6c3595f03 Use Clang wrappers; reduce thread spawning; separate out SampledSignals
fix

fix

use CLANG wrappers

cleanup (again)

more coverage

fix tests

fix?

distinguish error numbers from codes

reduce thread spawning

cleanup

fix?

fix?

coverage

coverage

fix

fix

more cleanup and comments

separate out SampledSignals part

almost there

fix

comments

fix

Add gen README

Update test/runtests.jl

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

fix

more comments

separate messanger from buffer

fix source/sink mix-up

adjust_channels, test device names

slight cleanup

update docs

add links to docs to readme
2021-07-25 13:11:55 -04:00
bramtayl
6a018cfc32
Avoid circular type definition (#78)
* avoid recursion

* reuse ref

* fix
2021-06-14 10:06:02 -04:00
bramtayl
b3cddf5669
run JuliaFormatter (#77) 2021-06-01 13:44:23 -04:00
Bill
89020cafc7
Update for PortAudio.jl architecture and Julia 1+ (#47) 2021-06-01 12:53:08 -04:00
bramtayl
50eb168f9a
More coverage (#76)
* more coverage

* more
2021-06-01 12:39:27 -04:00
bramtayl
dd68835815
Send debug to debug (#74)
* send to debug

* use Suppressor

* actually, this might be nicer as a macro

* return

* fix, add test

* small fix

* Logging target

* send xrun messages to debug

* Add note to README

* Revert "send xrun messages to debug"

This reverts commit d47abb9072.
2021-05-24 17:34:37 -04:00
bramtayl
0187b4937d
don't prefill empty output (#72) 2021-05-21 16:12:47 -04:00
bramtayl
5bdd8975a9
add alsa_plugins (#70)
* add alsa_plugins

* avoid get!
2021-05-21 08:27:33 -04:00
bramtayl
e8c1e6a8f4
Merge pull request #73 from rob-luke/PRtemplate
Add a pull request template
2021-05-14 07:46:55 -04:00
Spencer Russell
94a8a7f283
Merge pull request #71 from bramtayl/handle_null
handle C_NULL errors
2021-05-13 20:41:38 -04:00
Robert Luke
c4e1594518
Add a pull request template
This will encourage people committing code to explain the purpose of their pull request and ease reviewing.
2021-05-14 09:56:01 +10:00
Brandon Taylor
1d9e441168 handle C_NULL errors 2021-05-13 13:59:25 -04:00
bramtayl
ff6dedec1f
Merge pull request #69 from JuliaAudio/revert-61-compathelper/new_version/2021-05-09-00-41-15-913-2231612002
Revert "CompatHelper: add new compat entry for "SampledSignals" at version "2.1""
2021-05-13 12:00:07 -04:00
bramtayl
4652e394d8
Revert "CompatHelper: add new compat entry for "SampledSignals" at version "2.1"" 2021-05-13 11:56:18 -04:00
bramtayl
57a74e0bca
Merge pull request #61 from JuliaAudio/compathelper/new_version/2021-05-09-00-41-15-913-2231612002
CompatHelper: add new compat entry for "SampledSignals" at version "2.1"
2021-05-13 11:54:17 -04:00
bramtayl
06a1a0f243
Combine tests (#65)
* combine tests

* Delete runtests_local.jl

* More robust defaults, add SampledSignals

* get rid of flush

* get rid of flush, update printing

* Create runtests_local.jl

* Rename test/test/runtests_local.jl to test/runtests_local.jl
2021-05-13 11:42:09 -04:00
bramtayl
435e968b5a
Merge pull request #68 from rob-luke/badges
Update badges
2021-05-13 11:41:14 -04:00
Robert Luke
d71d971d66
Update badges
Change badges to github actions and code coverage from travis and appveyor
2021-05-13 09:07:10 +10:00
bramtayl
b5eed5a7c7
Merge pull request #66 from rob-luke/dropci
Remove appveyor and travis
2021-05-10 10:11:16 -04:00
Robert Luke
9f96451356 Remove appveyor and travis 2021-05-10 17:30:24 +10:00
bramtayl
d7d29880d6
Merge pull request #60 from rob-luke/ghtests
MRG: Use github actions for tests and expand platforms and versions
2021-05-09 16:34:32 -04:00
Robert Luke
5754f52034
Test against x86 too 2021-05-10 05:46:24 +10:00
bramtayl
a18ac17eba
Bump version 2021-05-09 12:54:05 -04:00
bramtayl
da0b3de1d8
Merge pull request #59 from rob-luke/secretsfix
Use the correct secrets key
2021-05-09 11:09:32 -04:00
Robert Luke
b905a7f31a Merge remote-tracking branch 'upstream/master' into ghtests 2021-05-09 13:23:01 +10:00
bramtayl
819de99d9c
Add compat entries; remove unused packages (#58) 2021-05-09 13:17:35 +10:00
github-actions[bot]
52be2700bf CompatHelper: add new compat entry for "SampledSignals" at version "2.1" 2021-05-09 00:41:16 +00:00
Robert Luke
30a64d1f45
Update Tests.yml 2021-05-09 10:39:47 +10:00
Robert Luke
b1e973dba2
Revert back to 1.3 as expected 2021-05-09 10:26:41 +10:00
Robert Luke
ab620dc64c
Update Project.toml 2021-05-09 10:12:12 +10:00
Robert Luke
578f34d0e5
Use github actions for tests and expand platforms and versions 2021-05-09 09:53:40 +10:00
Robert Luke
ad9c3142da
Update CompatHelper.yml 2021-05-09 09:47:09 +10:00
Robert Luke
c4423b04bd
Fix secret key name to match settings 2021-05-09 09:46:36 +10:00
bramtayl
7e317452f9
Create TagBot.yml 2021-05-08 10:31:53 -04:00
bramtayl
c05dff245e
Create CompatHelper.yml 2021-05-08 10:26:02 -04:00
Spencer Russell
28c89c24e4
removed outdated info on building the shim from README 2020-04-15 20:27:23 -04:00
Spencer Russell
25f9c1230f improves xrun handling and fixes segfaults on ctrl-c
fixes #20
2020-02-23 23:05:32 -05:00
Spencer Russell
461cdc1557 add disable_sigint to make ctrl-C not crash Julia 2020-02-22 21:42:45 -05:00
jakubwro
b9c604533d script fix 2020-02-21 22:47:10 +01:00
Jakub Wronowski
81720e0155 default latency fix 2020-02-20 23:02:10 +01:00
Jakub Wronowski
03a32623e9 default latency 2020-02-20 22:59:05 +01:00
Jakub Wronowski
c18963ac53 removed SampledSignals.blocksize method 2020-02-20 22:48:04 +01:00
Jakub Wronowski
1a1a10cb69 script fixes 2020-02-18 23:52:15 +01:00
Jakub Wronowski
c3570b0d07 readme and script fix 2020-02-18 23:49:50 +01:00
Jakub Wronowski
1fe68cf857 removed blocksize parameter 2020-02-18 23:04:51 +01:00
Jakub Wronowski
bea3577abe Merge branch 'nocallback' of https://github.com/jakubwro/PortAudio.jl 2020-02-18 21:19:54 +01:00
Spencer Russell
25993bce0e
Merge pull request #40 from JuliaAudio/nocallback
Julia 1.0, Artifacts, JLLs, no ringbuffers
2020-02-17 12:06:45 -06:00
jakubwro
d9ffc44f8c measure signal tuning 2020-02-11 14:07:56 +01:00
jakubwro
a1a2230ed8 script for measuring latency 2020-02-11 13:42:10 +01:00
jakubwro
3ffdbb9bc9 exposed pa stream latency 2020-02-11 00:11:21 +01:00
Spencer Russell
93916a630d Removes crufty files and outdated tests 2020-01-30 15:25:08 -05:00
Spencer Russell
b18b9bdcae only look for ALSA on linux systems 2020-01-30 11:54:16 -05:00
Spencer Russell
9d780e4950 re-enables suppressing portaudio initialization output 2020-01-30 11:33:50 -05:00
Spencer Russell
d069e75a9f bumps latency for more reliable performance 2020-01-30 11:28:44 -05:00
Spencer Russell
f123478231 now using a mutex to protect libportaudio access 2020-01-02 14:23:41 -05:00
Spencer Russell
16d0bc48be adds auto-detection of ALSA config dir 2020-01-02 13:56:46 -05:00
Spencer Russell
9eb565e487 mostly working, but crashes sometimes 2020-01-02 00:02:00 -05:00
Spencer Russell
4c2ad4dc06 some more update-related tweaks 2020-01-01 14:59:46 -05:00
Elliot Saba
a7919c5b64 Upgrade PortAudio to JLL packages 2019-12-27 22:50:45 -08:00
Julian P Samaroo
7944836b49 Fix CI attempt 2 2019-09-11 07:23:13 -05:00
Julian P Samaroo
918e9d6986 Fix CI attempt 1 2019-09-11 07:10:05 -05:00
Julian P Samaroo
1f1f721fec Make things work on Julia v1, use BB for some deps
Removed Compat
Switched to BB repo for libportaudio
Re-enabled a now-passing pa_shim test
2019-09-08 18:59:37 -05:00
Spencer Russell
577d7adfef
Merge pull request #36 from mroavi/julia1
Upgraded audiometer.jl and spectrum.jl
2019-07-30 08:58:34 -04:00
Martin Roa Villescas
221afe9b88 Upgrade spectrum.jl example to Julia 1.* 2019-07-25 14:14:14 +02:00
Martin Roa Villescas
ea2c524426 Upgrade audiometer.jl example to Julia 1.* 2019-07-25 13:46:26 +02:00
Spencer Russell
01ddd6b835 adds warn_xrun option 2019-04-02 14:06:00 -04:00
Spencer Russell
0425fbfe3b Merge branch 'julia1' of https://github.com/JuliaAudio/PortAudio.jl into julia1 2019-04-02 13:39:17 -04:00
Spencer Russell
7f51c78596 enable warnings on xruns 2019-04-02 13:39:12 -04:00
Spencer Russell
207ec25f1e adds some examples of scrolling spctrograms with Makie. Probably with an old version of Makie 2019-02-27 09:03:48 -05:00
Spencer Russell
8bd884d394 fixes field acess error when printing other errors 2019-02-27 09:01:09 -05:00
Spencer Russell
c66ad398bd adds warning TODO for do-syntax 2018-12-05 11:22:28 -05:00
Spencer Russell
8d42b94a6a adds do syntax support 2018-12-05 11:17:36 -05:00
Spencer Russell
7d1be74eae adds workaround for libuv/libuv#1951. PA_SHIM REQUIRES LOCAL BUILD 2018-08-28 13:41:04 -04:00
Spencer Russell
45bfdc4830 now properly closing the error ringbuf 2018-08-16 15:59:00 -04:00
Spencer Russell
bc32d13f7d Ref usage wasn't working on 0.6 2018-08-16 14:41:50 -04:00
Spencer Russell
308e88b7cf rounds up some stray underscores 2018-08-16 14:19:00 -04:00
Spencer Russell
3551896de1 install Compat during testing 2018-08-16 14:11:17 -04:00
Spencer Russell
4d73324a7f now only testing appveyor on 1.0, to save CI time 2018-08-16 13:56:32 -04:00
Spencer Russell
06d3a4b099 now also using master of RingBuffers 2018-08-16 13:03:22 -04:00
Spencer Russell
d14a5f4b1f adds back sudo for travis 2018-08-16 12:47:11 -04:00
Spencer Russell
b1e0183538 now testing on 0.6, 0.7, and 1.0 2018-08-16 12:41:35 -04:00
Spencer Russell
f6213dc5ef some more upgrades, changes a bunch of Ptrs to Refs 2018-08-15 23:18:44 -04:00
Spencer Russell
ca7c8b91d8
Merge pull request #27 from EMCP/master
getting a warning when executing regarding abs()
2018-08-10 13:35:16 -04:00
emcp
f1828824a1 Found another warning regarding abs. 2018-08-09 17:32:05 +02:00
Erik
23f657dbbe
getting a warning when executing regarding abs() 2018-08-08 21:11:55 +02:00
WooKyoung Noh
5823404f1a Compat Julia 0.7 2018-06-21 17:02:34 +09:00
Spencer Russell
03aefe619d Merge pull request #11 from tkelman/patch-1
use 0.6.0-pre as minimum julia version in REQUIRE
2017-05-28 22:15:17 -04:00
Tony Kelman
312d4a90ca be even more specific 2017-05-25 15:07:41 -07:00
Tony Kelman
54ac000878 use 0.6.0-pre as minimum julia version in REQUIRE
`mutable struct` syntax won't work on early 0.6.0-dev versions,
so better to stick to the julia-0.5-compatible versions of the package there
2017-05-25 15:02:15 -07:00
Spencer Russell
ce51f8497f removes one last compat 2017-05-21 22:39:15 -04:00
Spencer Russell
1b26ea2f0f README badge tweaking [ci skip] 2017-05-19 23:31:17 -04:00
Spencer Russell
f06a53363b removes spurious Compat 2017-05-19 23:29:14 -04:00
Spencer Russell
5ea61ea30e Merge pull request #9 from JuliaAudio/crosscompile
Switches to using native extension for ringbuffer exchange. fixes #6.
2017-05-19 22:22:10 -04:00
Spencer Russell
f57d201ef4 Merge branch 'master' into crosscompile 2017-05-19 18:00:06 -04:00
Spencer Russell
9dfb27a002 Merge pull request #10 from staticfloat/updated_ci_url
Update CI URLs to point to new caching infrastructure
2017-05-19 17:58:09 -04:00
Spencer Russell
47ea6a0c30 adds TestSetExtensions to test/REQUIRE 2017-05-19 01:10:19 -04:00
Spencer Russell
de0dd1054f adds docstring for PortAudioStream 2017-05-19 01:03:43 -04:00
Spencer Russell
860b54ade0 now actuall test on CI 2017-05-18 21:57:01 -04:00
Spencer Russell
a7cc0672a5 splits tests so we can run as much as possible during CI 2017-05-18 21:36:01 -04:00
Spencer Russell
e45ca2e0b6 adds 32-bit windows testing for appveyor 2017-05-18 21:23:14 -04:00
Spencer Russell
e7b67133b3 tweaks one of the write tests 2017-05-18 21:19:03 -04:00
Spencer Russell
efd70272ab adds cross-compiled multiplatform builds and infrastructure to load them 2017-05-18 12:38:13 -04:00
Elliot Saba
af8f53eb0e Update CI URLs to point to new caching infrastructure 2017-05-16 17:26:12 -07:00
41 changed files with 2455 additions and 1226 deletions

9
.JuliaFormatter.toml Normal file
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,41 @@
name: Tests
on:
pull_request:
push:
branches:
- master
tags: '*'
jobs:
test:
timeout-minutes: 30
name: ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
version:
- '1.6'
- '1'
- 'nightly'
os:
- ubuntu-latest
- macOS-latest
- windows-latest
arch:
- x64
- x86
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.version }}
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v1
with:
file: lcov.info

5
.gitignore vendored
View file

@ -1,7 +1,12 @@
*.swp
*.o
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

View file

@ -1,14 +0,0 @@
# Documentation: http://docs.travis-ci.com/user/languages/julia/
language: julia
os:
- linux
- osx
sudo: required
julia:
- 0.6
notifications:
email: false
script:
# we can't actually run on travis, so just make sure it's installable
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); using PortAudio'

26
Project.toml Normal file
View file

@ -0,0 +1,26 @@
name = "PortAudio"
uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d"
repo = "https://github.com/JuliaAudio/PortAudio.jl.git"
version = "1.3.0"
[deps]
alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7"
libportaudio_jll = "2d7b7beb-0762-5160-978e-1ab83a1e8a31"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
[compat]
julia = "1.6"
alsa_plugins_jll = "1.2.2"
libportaudio_jll = "19.6.0"
SampledSignals = "2.1.1"
Suppressor = "0.2"
[extras]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
LibSndFile = "b13ce0c6-77b0-50c6-a2db-140568b8d1a5"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[targets]
test = ["Documenter", "LibSndFile", "Test"]

105
README.md
View file

@ -1,18 +1,23 @@
PortAudio.jl
============
[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaAudio.github.io/PortAudio.jl/dev)
[![Tests](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml/badge.svg)](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml)
[![codecov](https://codecov.io/gh/JuliaAudio/PortAudio.jl/branch/master/graph/badge.svg?token=mgDAi8ulPY)](https://codecov.io/gh/JuliaAudio/PortAudio.jl)
PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which gives cross-platform access to audio devices. It is compatible with the types defined in [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl). It provides a `PortAudioStream` type, which can be read from and written to.
Installable on Linux and OSX: [![Build Status](https://travis-ci.org/JuliaAudio/PortAudio.jl.svg?branch=master)](https://travis-ci.org/JuliaAudio/PortAudio.jl)
Installable on Windows: [![Build status](https://ci.appveyor.com/api/projects/status/6x1ha7uvrnel060g/branch/master?svg=true)](https://ci.appveyor.com/project/ssfrr/portaudio-jl/branch/master)
## Opening a stream
The easiest way to open a source or sink is with the default `PortAudioStream()` constructor, which will open a 2-in, 2-out stream to your system's default device(s). The constructor can also take the input and output channel counts as positional arguments, or a variety of other keyword arguments.
The easiest way to open a source or sink is with the default `PortAudioStream()` constructor,
which will open a 2-in, 2-out stream to your system's default device(s).
The constructor can also take the input and output channel counts as positional arguments,
or a variety of other keyword arguments.
If named keyword arguments `latency` or `samplerate` are unspecified, then PortAudio will use device defaults.
```julia
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000Hz, blocksize=4096, synced=false)
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000, latency=0.1)
```
You can open a specific device by adding it as the first argument, either as a `PortAudioDevice` instance or by name. You can also give separate names or devices if you want different input and output devices
@ -26,51 +31,77 @@ You can get a list of your system's devices with the `PortAudio.devices()` funct
```julia
julia> PortAudio.devices()
6-element Array{PortAudio.PortAudioDevice,1}:
PortAudio.PortAudioDevice("AirPlay","Core Audio",0,2,0)
PortAudio.PortAudioDevice("Built-in Microph","Core Audio",2,0,1)
PortAudio.PortAudioDevice("Built-in Output","Core Audio",0,2,2)
PortAudio.PortAudioDevice("JackRouter","Core Audio",2,2,3)
PortAudio.PortAudioDevice("After Effects 13.5","Core Audio",0,0,4)
PortAudio.PortAudioDevice("Built-In Aggregate","Core Audio",2,2,5)
14-element Vector{PortAudio.PortAudioDevice}:
"sof-hda-dsp: - (hw:0,0)" 2→2
"sof-hda-dsp: - (hw:0,3)" 0→2
"sof-hda-dsp: - (hw:0,4)" 0→2
"sof-hda-dsp: - (hw:0,5)" 0→2
"upmix" 8→8
"vdownmix" 6→6
"dmix" 0→2
"default" 32→32
```
### Input/Output Synchronization
The `synced` keyword argument to `PortAudioStream` controls whether the input and output ringbuffers are kept synchronized or not, which only effects duplex streams. It should be set to `true` if you need consistent input-to-output latency. In a synchronized stream, the underlying PortAudio callback will only read and write to the buffers an equal number of frames. In a synchronized stream, the user must also read and write an equal number of frames to the stream. If it is only written to or read from, it will eventually block. This is why it is `false` by default.
## Reading and Writing
The `PortAudioStream` type has `source` and `sink` fields which are of type `PortAudioSource <: SampleSource` and `PortAudioSink <: SampleSink`, respectively. are subtypes of `SampleSource` and `SampleSink`, respectively (from [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl)). This means they support all the stream and buffer features defined there. For example, if you load SampledSignals with `using SampledSignals` you can read 5 seconds to a buffer with `buf = read(stream.source, 5s)`, regardless of the sample rate of the device.
PortAudio.jl also provides convenience wrappers around the `PortAudioStream` type so you can read and write to it directly, e.g. `write(stream, stream)` will set up a loopback that will read from the input and play it back on the output.
## Debugging
If you are experiencing issues and wish to view detailed logging and debug information, set
```
ENV["JULIA_DEBUG"] = :PortAudio
```
before using the package.
## Examples
### Set up an audio pass-through from microphone to speaker
```julia
stream = PortAudioStream(2, 2)
write(stream, stream)
try
# cancel with Ctrl-C
write(stream, stream)
finally
close(stream)
end
```
### Use `do` syntax to auto-close the stream
```julia
PortAudioStream(2, 2) do stream
write(stream, stream)
end
```
### Open your built-in microphone and speaker by name
```julia
stream = PortAudioStream("Built-in Microph", "Built-in Output")
write(stream, stream)
PortAudioStream("default", "default") do stream
write(stream, stream)
end
```
### Record 10 seconds of audio and save to an ogg file
```julia
julia> using PortAudio, SampledSignals, LibSndFile
julia> import LibSndFile # must be in Manifest for FileIO.save to work
julia> stream = PortAudioStream("Built-in Microph", 2, 0)
PortAudio.PortAudioStream{Float32,SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
Samplerate: 48000 s⁻¹
Buffer Size: 4096 frames
2 channel source: "Built-in Microph"
julia> 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}}
@ -78,16 +109,18 @@ julia> buf = read(stream, 10s)
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
julia> close(stream)
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
```
## Building the shim library
### Play an audio signal through the default sound output device
Because PortAudio calls its callback from a separate audio thread, we can't handle it in Julia directly. To work around this we've included a small shim library written in C that uses ring buffers to pass audio data between the callback context and the main Julia context. To build the shim you'll need a few prerequisites:
* libportaudio
* make
* a C compiler (gcc on linux/macOS, mingw64 on Windows)
* The `RingBuffers` julia package, installed in a folder next to this one. The portaudio shim links against the `pa_ringbuffer` library that comes with `RingBuffers`.
To build the shim, go into the `deps/src` directory and type `make`.
```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
```

View file

@ -1,6 +0,0 @@
julia 0.6-
BinDeps
SampledSignals 0.3.0
RingBuffers 1.0.0
@osx Homebrew
@windows WinRPM

View file

@ -1,27 +0,0 @@
environment:
matrix:
- JULIAVERSION: "julialang/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe"
notifications:
- provider: Email
on_build_success: false
on_build_failure: false
on_build_status_changed: false
install:
# Download most recent Julia Windows binary
- ps: (new-object net.webclient).DownloadFile(
$("http://s3.amazonaws.com/"+$env:JULIAVERSION),
"C:\projects\julia-binary.exe")
# Run installer silently, output to C:\projects\julia
- C:\projects\julia-binary.exe /S /D=C:\projects\julia
build_script:
# Need to convert from shallow to complete for Pkg.clone to work
- IF EXIST .git\shallow (git fetch --unshallow)
- C:\projects\julia\bin\julia -e "versioninfo();
Pkg.clone(pwd(), \"PortAudio\"); Pkg.build(\"PortAudio\")"
test_script:
# can't actually run the test, so just make sure it's installable
- C:\projects\julia\bin\julia -e "using PortAudio"

26
deps/build.jl vendored
View file

@ -1,26 +0,0 @@
using BinDeps
using Compat
@BinDeps.setup
ENV["JULIA_ROOT"] = abspath(JULIA_HOME, "../../")
# include alias for WinRPM library
libportaudio = library_dependency("libportaudio", aliases=["libportaudio-2"])
# TODO: add other providers with correct names
provides(AptGet, "libportaudio2", libportaudio)
provides(Pacman, "portaudio", libportaudio)
@static if is_apple()
using Homebrew
provides(Homebrew.HB, "portaudio", libportaudio)
end
@static if is_windows()
using WinRPM
provides(WinRPM.RPM, "libportaudio2", libportaudio, os = :Windows)
end
@BinDeps.install @compat(Dict(:libportaudio => :libportaudio, ))

68
deps/src/Makefile vendored
View file

@ -1,68 +0,0 @@
# Makefile lifted from Clang.jl
# Copyright (c) 2012-: Isaiah Norton and [contributors](https://github.com/ihnorton/Clang.jl/graphs/contributors)
ifeq (exists, $(shell [ -e Make.user ] && echo exists ))
include Make.user
endif
TARGETDIR = ../usr/lib
OBJS = pa_shim.o
# Figure out OS and architecture
OS=$(shell uname)
ifneq (,$(findstring MINGW,$(OS)))
OS=WINNT
endif
# file extensions and platform-specific libs
ifeq ($(OS), WINNT)
LIBS +=
LDFLAGS += -shared -L../../../RingBuffers/deps/usr/lib -lpa_ringbuffer
INC +=
SHACMD = sha256sum
SHLIB_EXT = dll
else ifeq ($(OS), Darwin)
LIBS +=
INC +=
# we'll rely on Julia to load RingBuffers.jl, which will in turn load the C
# library that we depend on for these symbols
LDFLAGS += -dynamiclib -Wl,-undefined,dynamic_lookup
SHLIB_EXT = dylib
SHACMD = shasum -a256
else
LIBS +=
INC +=
LDFLAGS += -shared
SHLIB_EXT = so
SHACMD = sha256sum
endif
SOURCEHASH = $(shell $(SHACMD) pa_shim.c | awk '{print $$1}')
CFLAGS += -I../../../RingBuffers/deps/src -Wall -Wno-strict-aliasing -fno-omit-frame-pointer -fPIC -DSOURCEHASH=\"$(SOURCEHASH)\"
LDFLAGS +=
# LINUX_LIBS =-lrt
# LINUX_LDFLAGS =-rdynamic
# add the Homebrew.jl tree to the include dirs in case we used it for
# portaudio and libsndfile
# DARWIN_LDFLAGS =-L../../../Homebrew/deps/usr/lib
# DARWIN_INC =-I../../../Homebrew/deps/usr/include
TARGET = $(TARGETDIR)/pa_shim.$(SHLIB_EXT)
.PHONY: clean default
default: $(TARGET)
%.o: %.c Makefile
$(CC) $< -fPIC -c -o $@ $(INC) $(CFLAGS)
$(TARGETDIR):
mkdir -p $@
$(TARGET): $(OBJS) $(TARGETDIR) Makefile
$(CC) $(OBJS) -shared -o $@ $(LDFLAGS) $(LIBS)
clean:
rm -f $(OBJS)
rm -f $(TARGET)

104
deps/src/pa_shim.c vendored
View file

@ -1,104 +0,0 @@
#include <portaudio.h>
#include <pa_ringbuffer.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define MIN(x, y) ((x) < (y) ? (x) : (y))
typedef enum {
PA_SHIM_ERRMSG_OVERFLOW, // input overflow
PA_SHIM_ERRMSG_UNDERFLOW, // output underflow
PA_SHIM_ERRMSG_ERR_OVERFLOW, // error buffer overflowed
} pa_shim_errmsg_t;
// this callback type is used to notify the Julia side that the portaudio
// callback has run
typedef void (*pa_shim_notifycb_t)(void *userdata);
// This struct is shared between the Julia side and C
typedef struct {
PaUtilRingBuffer *inputbuf; // ringbuffer for input
PaUtilRingBuffer *outputbuf; // ringbuffer for output
PaUtilRingBuffer *errorbuf; // ringbuffer to send error notifications
int sync; // keep input/output ring buffers synchronized (0/1)
pa_shim_notifycb_t notifycb; // Julia callback to notify conditions
void *inputhandle; // condition to notify on new input
void *outputhandle; // condition to notify when ready for output
void *errorhandle; // condition to notify on new error
} pa_shim_info_t;
void senderr(pa_shim_info_t *info, pa_shim_errmsg_t msg) {
if(PaUtil_GetRingBufferWriteAvailable(info->errorbuf) < 2) {
// we've overflowed our error buffer! notify the host.
msg = PA_SHIM_ERRMSG_ERR_OVERFLOW;
}
PaUtil_WriteRingBuffer(info->errorbuf, &msg, 1);
if(info->notifycb) {
info->notifycb(info->errorhandle);
}
}
// return the sha256 hash of the shim source so we can make sure things are in sync
const char *pa_shim_getsourcehash(void)
{
// defined on the command-line at build-time
return SOURCEHASH;
}
/*
* This routine will be called by the PortAudio engine when audio is needed.
* It may called at interrupt level on some machines so don't do anything that
* could mess up the system like calling malloc() or free().
*/
int pa_shim_processcb(const void *input, void *output,
unsigned long frameCount,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData)
{
pa_shim_info_t *info = userData;
if(info->notifycb == NULL) {
fprintf(stderr, "pa_shim ERROR: notifycb is NULL\n");
}
int nwrite;
if(info->inputbuf) {
nwrite = PaUtil_GetRingBufferWriteAvailable(info->inputbuf);
nwrite = MIN(frameCount, nwrite);
}
int nread;
if(info->outputbuf) {
nread = PaUtil_GetRingBufferReadAvailable(info->outputbuf);
nread = MIN(frameCount, nread);
}
if(info->inputbuf && info->outputbuf && info->sync) {
// to keep the buffers synchronized, set readable and writable to
// their minimum value
nread = MIN(nread, nwrite);
nwrite = nread;
}
// read/write from the ringbuffers
if(info->inputbuf) {
PaUtil_WriteRingBuffer(info->inputbuf, input, nwrite);
if(info->notifycb) {
info->notifycb(info->inputhandle);
}
if(nwrite < frameCount) {
senderr(info, PA_SHIM_ERRMSG_OVERFLOW);
}
}
if(info->outputbuf) {
PaUtil_ReadRingBuffer(info->outputbuf, output, nread);
if(info->notifycb) {
info->notifycb(info->outputhandle);
}
if(nread < frameCount) {
senderr(info, PA_SHIM_ERRMSG_UNDERFLOW);
// we didn't fill the whole output buffer, so zero it out
memset(output+nread*info->outputbuf->elementSizeBytes, 0,
(frameCount - nread)*info->outputbuf->elementSizeBytes);
}
}
return paContinue;
}

Binary file not shown.

Binary file not shown.

2
docs/Project.toml Normal file
View file

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

12
docs/make.jl Normal file
View file

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

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

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

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

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

View file

@ -1,43 +1,49 @@
using PortAudio
"""Continuously read from the default audio input and plot an
ASCII level/peak meter"""
"""
Continuously read from the default audio input and plot an
ASCII level/peak meter
"""
function micmeter(metersize)
mic = PortAudioStream(1, 0; blocksize=512)
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
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"""
"""
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)
meterchars = clamp(round(Int, signal * metersize), 0, peakpos - 1)
blankchars = max(0, peakpos - meterchars - 1)
for position in 1:meterchars
print_with_color(barcolor(metersize, position), ">")
printstyled(">", color = barcolor(metersize, position))
end
print(" " ^ blankchars)
print_with_color(barcolor(metersize, peakpos), "|")
print(" " ^ (metersize - peakpos))
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
"""
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."""
character, which is red.
"""
function barcolor(metersize, position)
if position/metersize <= 0.5
if position / metersize <= 0.5
:green
elseif position == metersize
:red

View file

@ -1,127 +1,156 @@
# Thanks to Jiahao Chen for this great example!
using Distributed, PortAudio
##
## NOTE: THIS NEEDS TO BE PORTED OVER TO THE NEW ARCHITECTURE
##
# 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
using AudioIO
import AudioIO.play
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
type note{S<:Real, T<:Real}
play(ostream, sample::Array{Float64, 1}) = write(ostream, sample)
play(ostr, sample::Array{Int64, 1}) = play(ostr, Float64.(sample))
struct Note{S <: Real, T <: Real}
pitch::S
duration::T
sustained::Bool
end
function play(A::note, samplingfreq::Real=44100, shape::Function=t->0.6sin(t)+0.2sin(2t)+.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]
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 = int(length(timesamples) * 0.2)
v[end-decay_length:end-1] = v[end-decay_length:end-1] .* linspace(1, 0, decay_length)
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(v)
play(ostream, v)
sleep(A.duration)
end
function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
play([0]) #Force AudioIO to initialize
lyrics_syllables = lyrics==nothing? nothing : split(lyrics)
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 == 0 || (line = line[1:percent_idx-1])
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)
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(lyrics_syllables[note_idx][1:end-1])
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(note(pitch, (beatunit/duration)*(60/tempo), sustained))
play(ostream, Note(pitch, (beatunit / duration) * (60 / tempo), sustained))
note_idx += 1
end
println()
end
end
function parsetoken(token::String, Atuning::Real=220)
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 = findfirst('a':'g', char) + findfirst('A':'G', char)
if scale_idx!=0
const halfsteps = [12, 14, 3, 5, 7, 8, 10]
pitch = Atuning*2^(halfsteps[scale_idx]/12)
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'
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
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
# 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]
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 = convert(String, lengthbuf)
duration = isempty(lengthstr) ? nothing : parseint(lengthstr)
lengthstr = String(lengthbuf)
duration = isempty(lengthstr) ? nothing : tryparse(Int, lengthstr)
return (pitch, duration, sustain, dotted)
end
parsevoice("""
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.
""")
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 = @async parsevoice("""
soprano = @spawn parsevoice(
"""
f'#. f'#. g'. a'. a'. g'. f'#. e'~ e'8 d.'4 d.' e.' f#'. f#'.~ f#' e'8 e'4~ e'2
""", lyrics="Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!"
""",
lyrics = "Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!",
)
alto = @async parsevoice("""
alto = @spawn parsevoice("""
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2
""")
tenor = @async parsevoice("""
tenor = @spawn parsevoice("""
d. d. e. f#. f#. e. d. d~ e8 f#.4 f#. a,. d. d.~ d c#8 c#4 c#2
""")
bass = @async parsevoice("""
bass = @spawn parsevoice("""
d. d. d. d. a,. a,. a,. b,~ c8 d. a., a., a., a., a, a8, a,4 a,2
""")
wait(soprano)
@ -129,19 +158,21 @@ wait(alto)
wait(tenor)
wait(bass)
soprano = @async parsevoice("""
soprano = @spawn parsevoice(
"""
f'#.4 f'#. g'. a'. a'. g'. f'#. e'. d'. d'. e'. f'#. e'.~ e' d'8 d'4~ d'2
""", lyrics="Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!")
alto = @async parsevoice("""
""",
lyrics = "Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!",
)
alto = @spawn parsevoice("""
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2
""")
tenor = @async parsevoice("""
tenor = @spawn parsevoice("""
d.4 d. d. d. d. d. d. d. d. d. c#. d. c#.~ c# d8 d d2
""")
bass = @async parsevoice("""
bass = @spawn parsevoice("""
d.4 d. d. d. a,. a,. a,. a., a., a., a., a., a.,~ a, a,8 d, d,2
""")
wait(soprano)
wait(alto)
wait(tenor)

View 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
View 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)

View file

@ -3,10 +3,10 @@
module SpectrumExample
using GR, PortAudio, SampledSignals
using GR, PortAudio, SampledSignals, FFTW
const N = 1024
const stream = PortAudioStream(1, 0, blocksize=N)
const stream = PortAudioStream(1, 0)
const buf = read(stream, N)
const fmin = 0Hz
const fmax = 10000Hz
@ -14,7 +14,7 @@ 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))
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100))
end
end

21
examples/tone-buffer.jl Normal file
View 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)

View 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

View 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
View file

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

16
gen/generator.jl Normal file
View file

@ -0,0 +1,16 @@
using Clang.Generators
using libportaudio_jll
cd(@__DIR__)
include_dir = joinpath(libportaudio_jll.artifact_dir, "include") |> normpath
portaudio_h = joinpath(include_dir, "portaudio.h")
options = load_options(joinpath(@__DIR__, "generator.toml"))
args = get_default_args()
push!(args, "-I$include_dir")
ctx = create_context(portaudio_h, args, options)
build!(ctx)

9
gen/generator.toml Normal file
View file

@ -0,0 +1,9 @@
[general]
library_name = "libportaudio"
output_file_path = "../src/LibPortAudio.jl"
module_name = "LibPortAudio"
jll_pkg_name = "libportaudio_jll"
export_symbol_prefixes = ["Pa", "pa"]
use_julia_native_enum_type = true
auto_mutability = true

View file

@ -1,11 +0,0 @@
#!/bin/bash
# Runs the tests including generating an lcov.info file
# abort on failure
set -e
julia -e 'using Coverage; clean_folder(".");'
julia --color=yes --inline=no --code-coverage=user test/runtests.jl
mkdir -p coverage
julia -e 'using Coverage; res=process_folder(); LCOV.writefile("coverage/lcov.info", res)'

File diff suppressed because it is too large Load diff

View file

@ -1,250 +1,439 @@
# Low-level wrappers for Portaudio calls
module LibPortAudio
using libportaudio_jll
export libportaudio_jll
function Pa_GetVersion()
ccall((:Pa_GetVersion, libportaudio), Cint, ())
end
function Pa_GetVersionText()
ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
end
mutable struct PaVersionInfo
versionMajor::Cint
versionMinor::Cint
versionSubMinor::Cint
versionControlRevision::Ptr{Cchar}
versionText::Ptr{Cchar}
end
# no prototype is found for this function at portaudio.h:114:22, please use with caution
function Pa_GetVersionInfo()
ccall((:Pa_GetVersionInfo, libportaudio), Ptr{PaVersionInfo}, ())
end
# General type aliases
const PaTime = Cdouble
const PaError = Cint
const PaSampleFormat = Culong
const PaDeviceIndex = Cint
const PaHostApiIndex = Cint
const PaHostApiTypeId = Cint
# PaStream is always used as an opaque type, so we're always dealing
# with the pointer
const PaStream = Ptr{Void}
const PaStreamCallback = Void
const PaStreamFlags = Culong
const paNoFlag = PaStreamFlags(0x00)
@enum PaErrorCode::Int32 begin
paNoError = 0
paNotInitialized = -10000
paUnanticipatedHostError = -9999
paInvalidChannelCount = -9998
paInvalidSampleRate = -9997
paInvalidDevice = -9996
paInvalidFlag = -9995
paSampleFormatNotSupported = -9994
paBadIODeviceCombination = -9993
paInsufficientMemory = -9992
paBufferTooBig = -9991
paBufferTooSmall = -9990
paNullCallback = -9989
paBadStreamPtr = -9988
paTimedOut = -9987
paInternalError = -9986
paDeviceUnavailable = -9985
paIncompatibleHostApiSpecificStreamInfo = -9984
paStreamIsStopped = -9983
paStreamIsNotStopped = -9982
paInputOverflowed = -9981
paOutputUnderflowed = -9980
paHostApiNotFound = -9979
paInvalidHostApi = -9978
paCanNotReadFromACallbackStream = -9977
paCanNotWriteToACallbackStream = -9976
paCanNotReadFromAnOutputOnlyStream = -9975
paCanNotWriteToAnInputOnlyStream = -9974
paIncompatibleStreamHostApi = -9973
paBadBufferPtr = -9972
end
const PA_NO_ERROR = 0
const PA_INPUT_OVERFLOWED = -10000 + 19
const PA_OUTPUT_UNDERFLOWED = -10000 + 20
# sample format types
const paFloat32 = PaSampleFormat(0x01)
const paInt32 = PaSampleFormat(0x02)
const paInt24 = PaSampleFormat(0x04)
const paInt16 = PaSampleFormat(0x08)
const paInt8 = PaSampleFormat(0x10)
const paUInt8 = PaSampleFormat(0x20)
const paNonInterleaved = PaSampleFormat(0x80000000)
const type_to_fmt = Dict{Type, PaSampleFormat}(
Float32 => 1,
Int32 => 2,
# Int24 => 4,
Int16 => 8,
Int8 => 16,
UInt8 => 3
)
const PaStreamCallbackResult = Cint
# Callback return values
const paContinue = PaStreamCallbackResult(0)
const paComplete = PaStreamCallbackResult(1)
const paAbort = PaStreamCallbackResult(2)
function Pa_GetErrorText(errorCode)
ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), errorCode)
end
function Pa_Initialize()
err = ccall((:Pa_Initialize, libportaudio), PaError, ())
handle_status(err)
ccall((:Pa_Initialize, libportaudio), PaError, ())
end
function Pa_Terminate()
err = ccall((:Pa_Terminate, libportaudio), PaError, ())
handle_status(err)
ccall((:Pa_Terminate, libportaudio), PaError, ())
end
Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ())
const PaDeviceIndex = Cint
function Pa_GetVersionText()
versionPtr = ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
unsafe_string(versionPtr)
const PaHostApiIndex = Cint
function Pa_GetHostApiCount()
ccall((:Pa_GetHostApiCount, libportaudio), PaHostApiIndex, ())
end
# Host API Functions
function Pa_GetDefaultHostApi()
ccall((:Pa_GetDefaultHostApi, libportaudio), PaHostApiIndex, ())
end
# A Host API is the top-level of the PortAudio hierarchy. Each host API has a
# unique type ID that tells you which native backend it is (JACK, ALSA, ASIO,
# etc.). On a given system you can identify each backend by its index, which
# will range between 0 and Pa_GetHostApiCount() - 1. You can enumerate through
# all the host APIs on the system by iterating through those values.
@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
# PaHostApiTypeId values
const pa_host_api_names = Dict{PaHostApiTypeId, String}(
0 => "In Development", # use while developing support for a new host API
1 => "Direct Sound",
2 => "MME",
3 => "ASIO",
4 => "Sound Manager",
5 => "Core Audio",
7 => "OSS",
8 => "ALSA",
9 => "AL",
10 => "BeOS",
11 => "WDMKS",
12 => "Jack",
13 => "WASAPI",
14 => "AudioScience HPI"
)
type PaHostApiInfo
struct_version::Cint
api_type::PaHostApiTypeId
mutable struct PaHostApiInfo
structVersion::Cint
type::PaHostApiTypeId
name::Ptr{Cchar}
deviceCount::Cint
defaultInputDevice::PaDeviceIndex
defaultOutputDevice::PaDeviceIndex
end
Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo}, (PaHostApiIndex,), i))
# Device Functions
type PaDeviceInfo
struct_version::Cint
name::Ptr{Cchar}
host_api::PaHostApiIndex
max_input_channels::Cint
max_output_channels::Cint
default_low_input_latency::PaTime
default_low_output_latency::PaTime
default_high_input_latency::PaTime
default_high_output_latency::PaTime
default_sample_rate::Cdouble
function Pa_GetHostApiInfo(hostApi)
ccall(
(:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo},
(PaHostApiIndex,),
hostApi,
)
end
Pa_GetDeviceCount() = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
function Pa_HostApiTypeIdToHostApiIndex(type)
ccall(
(:Pa_HostApiTypeIdToHostApiIndex, libportaudio),
PaHostApiIndex,
(PaHostApiTypeId,),
type,
)
end
Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio),
Ptr{PaDeviceInfo}, (PaDeviceIndex,), i))
function Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex)
ccall(
(:Pa_HostApiDeviceIndexToDeviceIndex, libportaudio),
PaDeviceIndex,
(PaHostApiIndex, Cint),
hostApi,
hostApiDeviceIndex,
)
end
Pa_GetDefaultInputDevice() = ccall((:Pa_GetDefaultInputDevice, libportaudio),
PaDeviceIndex, ())
mutable struct PaHostErrorInfo
hostApiType::PaHostApiTypeId
errorCode::Clong
errorText::Ptr{Cchar}
end
Pa_GetDefaultOutputDevice() = ccall((:Pa_GetDefaultOutputDevice, libportaudio),
PaDeviceIndex, ())
function Pa_GetLastHostErrorInfo()
ccall((:Pa_GetLastHostErrorInfo, libportaudio), Ptr{PaHostErrorInfo}, ())
end
# Stream Functions
function Pa_GetDeviceCount()
ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
end
type Pa_StreamParameters
function Pa_GetDefaultInputDevice()
ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
end
function Pa_GetDefaultOutputDevice()
ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ())
end
const PaTime = Cdouble
const PaSampleFormat = Culong
mutable struct PaDeviceInfo
structVersion::Cint
name::Ptr{Cchar}
hostApi::PaHostApiIndex
maxInputChannels::Cint
maxOutputChannels::Cint
defaultLowInputLatency::PaTime
defaultLowOutputLatency::PaTime
defaultHighInputLatency::PaTime
defaultHighOutputLatency::PaTime
defaultSampleRate::Cdouble
end
function Pa_GetDeviceInfo(device)
ccall((:Pa_GetDeviceInfo, libportaudio), Ptr{PaDeviceInfo}, (PaDeviceIndex,), device)
end
struct PaStreamParameters
device::PaDeviceIndex
channelCount::Cint
sampleFormat::PaSampleFormat
suggestedLatency::PaTime
hostAPISpecificStreamInfo::Ptr{Void}
hostApiSpecificStreamInfo::Ptr{Cvoid}
end
type PaStreamInfo
function Pa_IsFormatSupported(inputParameters, outputParameters, sampleRate)
ccall(
(:Pa_IsFormatSupported, libportaudio),
PaError,
(Ptr{PaStreamParameters}, Ptr{PaStreamParameters}, Cdouble),
inputParameters,
outputParameters,
sampleRate,
)
end
const PaStream = Cvoid
const PaStreamFlags = Culong
mutable struct PaStreamCallbackTimeInfo
inputBufferAdcTime::PaTime
currentTime::PaTime
outputBufferDacTime::PaTime
end
const PaStreamCallbackFlags = Culong
@enum PaStreamCallbackResult::UInt32 begin
paContinue = 0
paComplete = 1
paAbort = 2
end
# typedef int PaStreamCallback ( const void * input , void * output , unsigned long frameCount , const PaStreamCallbackTimeInfo * timeInfo , PaStreamCallbackFlags statusFlags , void * userData )
const PaStreamCallback = Cvoid
function Pa_OpenStream(
stream,
inputParameters,
outputParameters,
sampleRate,
framesPerBuffer,
streamFlags,
streamCallback,
userData,
)
ccall(
(:Pa_OpenStream, libportaudio),
PaError,
(
Ptr{Ptr{PaStream}},
Ptr{PaStreamParameters},
Ptr{PaStreamParameters},
Cdouble,
Culong,
PaStreamFlags,
Ptr{Cvoid},
Ptr{Cvoid},
),
stream,
inputParameters,
outputParameters,
sampleRate,
framesPerBuffer,
streamFlags,
streamCallback,
userData,
)
end
function Pa_OpenDefaultStream(
stream,
numInputChannels,
numOutputChannels,
sampleFormat,
sampleRate,
framesPerBuffer,
streamCallback,
userData,
)
ccall(
(:Pa_OpenDefaultStream, libportaudio),
PaError,
(
Ptr{Ptr{PaStream}},
Cint,
Cint,
PaSampleFormat,
Cdouble,
Culong,
Ptr{Cvoid},
Ptr{Cvoid},
),
stream,
numInputChannels,
numOutputChannels,
sampleFormat,
sampleRate,
framesPerBuffer,
streamCallback,
userData,
)
end
function Pa_CloseStream(stream)
ccall((:Pa_CloseStream, libportaudio), PaError, (Ptr{PaStream},), stream)
end
# typedef void PaStreamFinishedCallback ( void * userData )
const PaStreamFinishedCallback = Cvoid
function Pa_SetStreamFinishedCallback(stream, streamFinishedCallback)
ccall(
(:Pa_SetStreamFinishedCallback, libportaudio),
PaError,
(Ptr{PaStream}, Ptr{Cvoid}),
stream,
streamFinishedCallback,
)
end
function Pa_StartStream(stream)
ccall((:Pa_StartStream, libportaudio), PaError, (Ptr{PaStream},), stream)
end
function Pa_StopStream(stream)
ccall((:Pa_StopStream, libportaudio), PaError, (Ptr{PaStream},), stream)
end
function Pa_AbortStream(stream)
ccall((:Pa_AbortStream, libportaudio), PaError, (Ptr{PaStream},), stream)
end
function Pa_IsStreamStopped(stream)
ccall((:Pa_IsStreamStopped, libportaudio), PaError, (Ptr{PaStream},), stream)
end
function Pa_IsStreamActive(stream)
ccall((:Pa_IsStreamActive, libportaudio), PaError, (Ptr{PaStream},), stream)
end
mutable struct PaStreamInfo
structVersion::Cint
inputLatency::PaTime
outputLatency::PaTime
sampleRate::Cdouble
end
# function Pa_OpenDefaultStream(inChannels, outChannels,
# sampleFormat::PaSampleFormat,
# sampleRate, framesPerBuffer)
# streamPtr = Ref{PaStream}(0)
# err = ccall((:Pa_OpenDefaultStream, libportaudio),
# PaError, (Ref{PaStream}, Cint, Cint,
# PaSampleFormat, Cdouble, Culong,
# Ref{Void}, Ref{Void}),
# streamPtr, inChannels, outChannels, sampleFormat, sampleRate,
# framesPerBuffer, C_NULL, C_NULL)
# handle_status(err)
#
# streamPtr[]
# end
#
function Pa_OpenStream(inParams, outParams,
sampleRate, framesPerBuffer,
flags::PaStreamFlags,
callback, userdata)
streamPtr = Ref{PaStream}(0)
err = ccall((:Pa_OpenStream, libportaudio), PaError,
(Ref{PaStream},
Ptr{Pa_StreamParameters},
Ptr{Pa_StreamParameters},
Cdouble, Culong, PaStreamFlags,
Ptr{Void}, Ptr{Void}),
streamPtr,
inParams, outParams,
sampleRate, framesPerBuffer, flags,
callback, userdata)
handle_status(err)
streamPtr[]
function Pa_GetStreamInfo(stream)
ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo}, (Ptr{PaStream},), stream)
end
function Pa_StartStream(stream::PaStream)
err = ccall((:Pa_StartStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
function Pa_GetStreamTime(stream)
ccall((:Pa_GetStreamTime, libportaudio), PaTime, (Ptr{PaStream},), stream)
end
function Pa_StopStream(stream::PaStream)
err = ccall((:Pa_StopStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
function Pa_GetStreamCpuLoad(stream)
ccall((:Pa_GetStreamCpuLoad, libportaudio), Cdouble, (Ptr{PaStream},), stream)
end
function Pa_CloseStream(stream::PaStream)
err = ccall((:Pa_CloseStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
function Pa_ReadStream(stream, buffer, frames)
ccall(
(:Pa_ReadStream, libportaudio),
PaError,
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
stream,
buffer,
frames,
)
end
function Pa_GetStreamReadAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
function Pa_WriteStream(stream, buffer, frames)
ccall(
(:Pa_WriteStream, libportaudio),
PaError,
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
stream,
buffer,
frames,
)
end
function Pa_GetStreamWriteAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
function Pa_GetStreamReadAvailable(stream)
ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
end
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
buf
function Pa_GetStreamWriteAvailable(stream)
ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
end
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_WriteStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
nothing
function Pa_GetSampleSize(format)
ccall((:Pa_GetSampleSize, libportaudio), PaError, (PaSampleFormat,), format)
end
# function Pa_GetStreamInfo(stream::PaStream)
# infoptr = ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo},
# (PaStream, ), stream)
#
# unsafe_load(infoptr)
# end
#
# General utility function to handle the status from the Pa_* functions
function handle_status(err::PaError, show_warnings::Bool=true)
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
if show_warnings
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
warn("libportaudio: " * unsafe_string(msg))
end
elseif err != PA_NO_ERROR
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
error("libportaudio: " * unsafe_string(msg))
function Pa_Sleep(msec)
ccall((:Pa_Sleep, libportaudio), Cvoid, (Clong,), msec)
end
const paNoDevice = PaDeviceIndex(-1)
const paUseHostApiSpecificDeviceSpecification = PaDeviceIndex(-2)
const paFloat32 = PaSampleFormat(0x00000001)
const paInt32 = PaSampleFormat(0x00000002)
const paInt24 = PaSampleFormat(0x00000004)
const paInt16 = PaSampleFormat(0x00000008)
const paInt8 = PaSampleFormat(0x00000010)
const paUInt8 = PaSampleFormat(0x00000020)
const paCustomFormat = PaSampleFormat(0x00010000)
const paNonInterleaved = PaSampleFormat(0x80000000)
const paFormatIsSupported = 0
const paFramesPerBufferUnspecified = 0
const paNoFlag = PaStreamFlags(0)
const paClipOff = PaStreamFlags(0x00000001)
const paDitherOff = PaStreamFlags(0x00000002)
const paNeverDropInput = PaStreamFlags(0x00000004)
const paPrimeOutputBuffersUsingStreamCallback = PaStreamFlags(0x00000008)
const paPlatformSpecificFlags = PaStreamFlags(0xffff0000)
const paInputUnderflow = PaStreamCallbackFlags(0x00000001)
const paInputOverflow = PaStreamCallbackFlags(0x00000002)
const paOutputUnderflow = PaStreamCallbackFlags(0x00000004)
const paOutputOverflow = PaStreamCallbackFlags(0x00000008)
const paPrimingOutput = PaStreamCallbackFlags(0x00000010)
# exports
const PREFIXES = ["Pa", "pa"]
for name in names(@__MODULE__; all = true), prefix in PREFIXES
if startswith(string(name), prefix)
@eval export $name
end
end
end # module

View file

@ -1,40 +0,0 @@
function init_pa_shim()
global const libpa_shim = Libdl.find_library(
["pa_shim"],
[joinpath(dirname(@__FILE__), "..", "deps", "usr", "lib")])
shim_dlib = Libdl.dlopen(libpa_shim)
# pointer to the shim's process callback
global const shim_processcb_c = Libdl.dlsym(shim_dlib, :pa_shim_processcb)
if shim_processcb_c == C_NULL
error("Got NULL pointer loading `pa_shim_processcb`")
end
end
const pa_shim_errmsg_t = Cint
const PA_SHIM_ERRMSG_OVERFLOW = Cint(0) # input overflow
const PA_SHIM_ERRMSG_UNDERFLOW = Cint(1) # output underflow
const PA_SHIM_ERRMSG_ERR_OVERFLOW = Cint(2) # error buffer overflowed
# This struct is shared with pa_shim.c
mutable struct pa_shim_info_t
inputbuf::Ptr{PaUtilRingBuffer} # ringbuffer for input
outputbuf::Ptr{PaUtilRingBuffer} # ringbuffer for output
errorbuf::Ptr{PaUtilRingBuffer} # ringbuffer to send error notifications
sync::Cint # keep input/output ring buffers synchronized (0/1)
notifycb::Ptr{Void} # Julia callback to notify on updates (called from audio thread)
inputhandle::Ptr{Void} # condition to notify on new input data
outputhandle::Ptr{Void} # condition to notify when ready for output
errorhandle::Ptr{Void} # condition to notify on new errors
end
"""
PortAudio.shimhash()
Return the sha256 hash(as a string) of the source file used to build the shim.
We may use this sometime to verify that the distributed binary stays in sync
with the rest of the package.
"""
shimhash() = unsafe_string(
ccall((:pa_shim_getsourcehash, libpa_shim), Cstring, ()))
Base.unsafe_convert(::Type{Ptr{Void}}, info::pa_shim_info_t) = pointer_from_objref(info)

29
src/precompile.jl Normal file
View 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))

View file

@ -1,20 +0,0 @@
# while waiting for this PR to get merged: https://github.com/Ismael-VC/Suppressor.jl/pull/12
# we'll just include the relevant code here
macro suppress_err(block)
quote
if ccall(:jl_generating_output, Cint, ()) == 0
ORIGINAL_STDERR = STDERR
err_rd, err_wr = redirect_stderr()
err_reader = @async readstring(err_rd)
end
value = $(esc(block))
if ccall(:jl_generating_output, Cint, ()) == 0
redirect_stderr(ORIGINAL_STDERR)
close(err_wr)
end
value
end
end

View file

@ -1 +0,0 @@
BaseTestNext

View file

@ -1,333 +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
using Base.Test
using TestSetExtensions
using PortAudio
using SampledSignals
using RingBuffers
# pull in some extra stuff we need to test the callback directly
using PortAudio: notifyhandle, notifycb_c, shim_processcb_c
using PortAudio: pa_shim_errmsg_t, pa_shim_info_t
using PortAudio: PA_SHIM_ERRMSG_ERR_OVERFLOW, PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW
"Setup buffers to test callback behavior"
function setup_callback(inchans, outchans, nframes, synced)
sourcebuf = RingBuffer{Float32}(inchans, nframes*2) # the microphone input should end up here
sinkbuf = RingBuffer{Float32}(outchans, nframes*2) # the callback should copy this to cb_output
errbuf = RingBuffer{pa_shim_errmsg_t}(1, 8)
# pass NULL for i/o we're not using
info = pa_shim_info_t(
inchans > 0 ? pointer(sourcebuf) : C_NULL,
outchans > 0 ? pointer(sinkbuf) : C_NULL,
pointer(errbuf),
synced, notifycb_c,
inchans > 0 ? notifyhandle(sourcebuf) : C_NULL,
outchans > 0 ? notifyhandle(sinkbuf) : C_NULL,
notifyhandle(errbuf)
)
flags = Culong(0)
cb_input = rand(Float32, inchans, nframes) # simulate microphone input
cb_output = rand(Float32, outchans, nframes) # this is where the output should go
function processfunc()
ccall(shim_processcb_c, Cint,
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{Void}),
cb_input, cb_output, nframes, C_NULL, flags, pointer_from_objref(info))
end
(sourcebuf, sinkbuf, errbuf, cb_input, cb_output, processfunc)
end
function test_callback(inchans, outchans, synced)
nframes = 8
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
if outchans > 0
testout = rand(Float32, outchans, nframes) # generate some test data to play
write(sinkbuf, testout) # fill the output ringbuffer
end
@test process() == PortAudio.paContinue
if outchans > 0
# testout -> sinkbuf -> cb_output
@test cb_output == testout
end
if inchans > 0
# cb_input -> sourcebuf
@test read(sourcebuf, nframes) == cb_input
end
@test framesreadable(errbuf) == 0
end
"""
test_callback_underflow(inchans, outchans; nframes=8, underfill=3, synced=false)
Test that the callback works on underflow conditions. underfill is the numer of
frames we feed in, which should be less than nframes.
"""
function test_callback_underflow(inchans, outchans, synced)
nframes = 8
underfill = 3 # must be less than nframes
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
outchans > 0 || error("Can't test underflow with no output")
testout = rand(Float32, outchans, underfill)
write(sinkbuf, testout) # underfill the output ringbuffer
# call callback (partial underflow)
@test process() == PortAudio.paContinue
@test cb_output[:, 1:underfill] == testout
@test cb_output[:, (underfill+1):nframes] == zeros(Float32, outchans, (nframes-underfill))
errs = readavailable(errbuf)
if inchans > 0
received = readavailable(sourcebuf)
if synced
@test size(received, 2) == underfill
@test received == cb_input[:, 1:underfill]
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test size(received, 2) == nframes
@test received == cb_input
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
# call again (total underflow)
@test process() == PortAudio.paContinue
@test cb_output == zeros(Float32, outchans, nframes)
errs = readavailable(errbuf)
if inchans > 0
received = readavailable(sourcebuf)
if synced
@test size(received, 2) == 0
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test size(received, 2) == nframes
@test received == cb_input
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
end
function test_callback_overflow(inchans, outchans, synced)
nframes = 8
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
inchans > 0 || error("Can't test overflow with no input")
@test frameswritable(sinkbuf) == nframes*2
# the first time it should half-fill the input ring buffer
if outchans > 0
testout = rand(Float32, outchans, nframes)
write(sinkbuf, testout)
end
@test framesreadable(sourcebuf) == 0
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(errbuf) == 0
@test framesreadable(sourcebuf) == nframes
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
# now run the process func again to completely fill the input ring buffer
outchans > 0 && write(sinkbuf, testout)
@test framesreadable(sourcebuf) == nframes
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(errbuf) == 0
@test framesreadable(sourcebuf) == nframes*2
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
# now this time the process func should overflow the input buffer
outchans > 0 && write(sinkbuf, testout)
@test framesreadable(sourcebuf) == nframes*2
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(sourcebuf) == nframes*2
errs = readavailable(errbuf)
if outchans > 0
if synced
# if input and output are synced, thec callback didn't pull from
# the output ringbuf
@test frameswritable(sinkbuf) == nframes
@test cb_output == zeros(Float32, outchans, nframes)
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test frameswritable(sinkbuf) == nframes*2
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
end
end
# these test are currently set up to run on OSX
@testset DottedTestSet "PortAudio Tests" begin
if is_windows()
default_indev = "Microphone Array (Realtek High "
default_outdev = "Speaker/Headphone (Realtek High"
elseif is_apple()
default_indev = "Built-in Microph"
default_outdev = "Built-in Output"
elseif is_linux()
default_indev = "default"
default_outdev = "default"
end
devs = PortAudio.devices()
i = findfirst(d -> d.maxinchans > 0, devs)
indev = i > 0 ? devs[i] : nothing
i = findfirst(d -> d.maxoutchans > 0, devs)
outdev = i > 0 ? devs[i] : nothing
i = findfirst(d -> d.maxoutchans > 0 && d.maxinchans > 0, devs)
duplexdev = i > 0 ? devs[i] : nothing
@testset "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")
@test result[3] == "Shim Source Hash: 4ea2a8526b"
end
@testset "Basic callback functionality" begin
@testset "basic duplex (no sync)" begin
test_callback(2, 3, false)
end
@testset "basic input-only (no sync)" begin
test_callback(2, 0, false)
end
@testset "basic output-only (no sync)" begin
test_callback(0, 2, false)
end
@testset "basic no input or output (no sync)" begin
test_callback(0, 0, false)
end
@testset "basic duplex (sync)" begin
test_callback(2, 3, true)
end
@testset "basic input-only (sync)" begin
test_callback(2, 0, true)
end
@testset "basic output-only (sync)" begin
test_callback(0, 2, true)
end
@testset "basic no input or output (sync)" begin
test_callback(0, 0, true)
end
@testset "Can list devices without crashing" begin
display(devices())
println()
end
@testset "Ouput underflow" begin
@testset "underflow duplex (nosync)" begin
test_callback_underflow(2, 3, false)
end
@testset "underflow output-only (nosync)" begin
test_callback_underflow(0, 3, false)
end
@testset "underflow duplex (sync)" begin
test_callback_underflow(2, 3, true)
end
@testset "underflow output-only (sync)" begin
test_callback_underflow(0, 3, true)
@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 "Input overflow" begin
@testset "overflow duplex (nosync)" begin
test_callback_overflow(2, 3, false)
end
@testset "overflow input-only (nosync)" begin
test_callback_overflow(2, 0, false)
end
@testset "overflow duplex (sync)" begin
test_callback_overflow(2, 3, true)
end
@testset "overflow input-only (sync)" begin
test_callback_overflow(2, 0, true)
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
@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()
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 String(take!(io)) == """
PortAudio.PortAudioStream{Float32}
Samplerate: 44100.0Hz
Buffer Size: 4096 frames
2 channel sink: "$default_outdev"
2 channel source: "$default_indev\""""
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 wait(t1) == 48000
@test wait(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 wait(t1) == 48000
@test wait(t2) == 48000
close(stream)
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
View 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