Compare commits

..

1 commit

Author SHA1 Message Date
Spencer Russell
dc2e216130 this config hangs 2018-08-19 19:14:46 -04:00
50 changed files with 2624 additions and 2401 deletions

View file

@ -1,9 +0,0 @@
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

View file

@ -1,22 +0,0 @@
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.

View file

@ -1,25 +0,0 @@
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 }}

View file

@ -1,16 +0,0 @@
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 }}

View file

@ -1,15 +0,0 @@
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 }}

View file

@ -1,41 +0,0 @@
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

1
.gitignore vendored
View file

@ -2,7 +2,6 @@
*.o *.o
deps/deps.jl deps/deps.jl
deps/build.log deps/build.log
docs/build
*.wav *.wav
*.flac *.flac
*.cov *.cov

29
.travis.yml Normal file
View file

@ -0,0 +1,29 @@
# Documentation: http://docs.travis-ci.com/user/languages/julia/
language: julia
os:
- linux
- osx
sudo: required
julia:
- 0.6
- 1.0
- nightly
matrix:
allow_failures:
- julia: nightly
fast_finish: true
branches:
only:
master
notifications:
email: false
script:
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
- julia --color=yes ci_setup.jl
- julia --color=yes --code-coverage test/runtests.jl
after_success:
- julia -e 'VERSION >= v"0.7.0-" && using Pkg;
VERSION < v"0.7.0-" && cd(Pkg.dir("PortAudio"));
Pkg.add("Coverage");
using Coverage;
Codecov.submit(process_folder())'

View file

@ -1,26 +0,0 @@
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"]

102
README.md
View file

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

7
REQUIRE Normal file
View file

@ -0,0 +1,7 @@
julia 0.6
BinDeps 0.8.8
SampledSignals 1.1.2
RingBuffers 1.1.2
Compat 0.66.0
@osx Homebrew
@windows WinRPM

38
appveyor.yml Normal file
View file

@ -0,0 +1,38 @@
environment:
matrix:
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/1.0/julia-1.0-latest-win32.exe"
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/1.0/julia-1.0-latest-win64.exe"
matrix:
allow_failures:
# currently failing on 1.0 until https://github.com/JuliaLang/METADATA.jl/pull/16370 is merged
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/1.0/julia-1.0-latest-win64.exe"
notifications:
- provider: Email
on_build_success: false
on_build_failure: false
on_build_status_changed: false
# only build master and PRs
branches:
only:
- master
install:
- ps: "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12"
# Download most recent Julia Windows binary
- ps: (new-object net.webclient).DownloadFile(
$env:JULIA_URL,
"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 --color=yes ci_setup.jl
test_script:
# - C:\projects\julia\bin\julia --check-bounds=yes -e "Pkg.test(\"SampledSignals\")"
- C:\projects\julia\bin\julia --color=yes --code-coverage --check-bounds=yes test/runtests.jl

21
ci_setup.jl Normal file
View file

@ -0,0 +1,21 @@
VERSION >= v"0.7.0-" && using InteractiveUtils
versioninfo()
if VERSION < v"0.7.0-"
Pkg.clone(pwd(), "PortAudio")
Pkg.build("PortAudio")
# for now we need SampledSignals and RingBuffers master
Pkg.checkout("SampledSignals")
Pkg.checkout("RingBuffers")
else
using Pkg
# for now we need to `clone` because there's no way to specify the
# package name for `add`
Pkg.clone(pwd(), "PortAudio")
Pkg.build("PortAudio")
Pkg.add(PackageSpec(name="SampledSignals", rev="master"))
Pkg.add(PackageSpec(name="RingBuffers", rev="master"))
end
# add test deps manually because we'll be running test/runtests.jl manually
Pkg.add("Compat")

27
deps/build.jl vendored Normal file
View file

@ -0,0 +1,27 @@
using BinDeps
using Compat
using Compat.Sys: isapple, iswindows
@BinDeps.setup
ENV["JULIA_ROOT"] = abspath(Compat.Sys.BINDIR, "../../")
# 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 isapple()
using Homebrew
provides(Homebrew.HB, "portaudio", libportaudio)
end
@static if iswindows()
using WinRPM
provides(WinRPM.RPM, "libportaudio2", libportaudio, os = :Windows)
end
@BinDeps.install Dict(:libportaudio => :libportaudio, )

80
deps/src/Makefile vendored Normal file
View file

@ -0,0 +1,80 @@
# Makefile originally 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
TARGETBASENAME=pa_shim
OBJS = pa_shim.o
# check to see if the user passed in a HOST variable for cross-compiling
ifeq ($(HOST),)
# Figure out OS and architecture
OS=$(shell uname)
ifneq ($(findstring MINGW,$(OS)),)
OS=WINNT
endif
else
HOSTSUFFIX=_$(HOST)
ifneq ($(findstring linux,$(HOST)),)
OS=Linux
else ifneq ($(findstring darwin,$(HOST)),)
OS=Darwin
else ifneq ($(findstring mingw,$(HOST)),)
OS=WINNT
endif
endif
CFLAGS = -Wall -Wno-strict-aliasing -fno-omit-frame-pointer -I../../../RingBuffers/deps/src
LDFLAGS +=
ifeq ($(OS), WINNT)
LIBS +=
LDFLAGS += -shared -L../../../RingBuffers/deps/usr/lib -lpa_ringbuffer$(HOSTSUFFIX)
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
CFLAGS += -fPIC
LIBS +=
INC +=
LDFLAGS += -shared
SHLIB_EXT = so
SHACMD = sha256sum
endif
SOURCEHASH = $(shell $(SHACMD) pa_shim.c | awk '{print $$1}')
CFLAGS += -DSOURCEHASH=\"$(SOURCEHASH)\"
TARGET=$(TARGETDIR)/$(TARGETBASENAME)$(HOSTSUFFIX).$(SHLIB_EXT)
.PHONY: clean cleantemp default
default: $(TARGET)
%.o: %.c Makefile
$(CC) $< -c -o $@ $(INC) $(CFLAGS)
$(TARGETDIR):
mkdir -p $@
$(TARGET): $(OBJS) $(TARGETDIR) Makefile
$(CC) $(OBJS) $(LDFLAGS) -o $@ $(LIBS)
cleantemp:
rm -f $(OBJS)
clean: cleantemp
rm -f $(TARGETDIR)/$(TARGETBASENAME)*.so
rm -f $(TARGETDIR)/$(TARGETBASENAME)*.dylib
rm -f $(TARGETDIR)/$(TARGETBASENAME)*.dll

54
deps/src/build.sh vendored Executable file
View file

@ -0,0 +1,54 @@
#!/bin/bash
# User docker to build pa_shim library for all supported platforms.
set -e
make clean
echo ""
# NOTE: the darwin build ends up actually being x86_64-apple-darwin14. It gets
# mapped within the docker machine
for platform in \
arm-linux-gnueabihf \
powerpc64le-linux-gnu \
x86_64-apple-darwin \
x86_64-w64-mingw32 \
i686-w64-mingw32; do
echo "================================"
echo "building for $platform..."
docker run --rm \
-v $(pwd)/../..:/workdir \
-v $(pwd)/../../../RingBuffers:/RingBuffers \
-w /workdir/deps/src \
-e CROSS_TRIPLE=$platform \
multiarch/crossbuild \
./dockerbuild_cb.sh
echo "================================"
echo ""
done
# we use holy-build-box for the x86 linux builds because it uses an older
# glibc so it should be compatible with more user environments
echo "================================"
echo "building for x86_64-linux-gnu..."
docker run --rm \
-v $(pwd)/../..:/workdir \
-v $(pwd)/../../../RingBuffers:/RingBuffers \
-w /workdir/deps/src \
-e HOST=x86_64-linux-gnu \
phusion/holy-build-box-64 \
./dockerbuild_hbb.sh
echo "================================"
echo ""
echo "================================"
echo "building for i686-linux-gnu..."
docker run --rm \
-v $(pwd)/../..:/workdir \
-v $(pwd)/../../../RingBuffers:/RingBuffers \
-w /workdir/deps/src \
-e HOST=i686-linux-gnu \
phusion/holy-build-box-32 \
./dockerbuild_hbb.sh
echo "================================"

8
deps/src/dockerbuild_cb.sh vendored Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
# this script is run by build.sh within each docker instance to do the build.
set -e
make HOST=$CROSS_TRIPLE
make cleantemp

13
deps/src/dockerbuild_hbb.sh vendored Executable file
View file

@ -0,0 +1,13 @@
#!/bin/bash
# this script is run by build.sh within each docker instance to do the build.
# it's meant to be run from within a "Holy Build Box" docker instance.
set -e
# Activate Holy Build Box environment.
source /hbb_exe/activate
set -x
make HOST=$HOST
make cleantemp

104
deps/src/pa_shim.c vendored Normal file
View file

@ -0,0 +1,104 @@
#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;
}

1225
deps/src/portaudio.h vendored Normal file

File diff suppressed because it is too large Load diff

BIN
deps/usr/lib/pa_shim_arm-linux-gnueabihf.so vendored Executable file

Binary file not shown.

BIN
deps/usr/lib/pa_shim_i686-linux-gnu.so vendored Executable file

Binary file not shown.

BIN
deps/usr/lib/pa_shim_i686-w64-mingw32.dll vendored Executable file

Binary file not shown.

BIN
deps/usr/lib/pa_shim_powerpc64le-linux-gnu.so vendored Executable file

Binary file not shown.

Binary file not shown.

BIN
deps/usr/lib/pa_shim_x86_64-linux-gnu.so vendored Executable file

Binary file not shown.

BIN
deps/usr/lib/pa_shim_x86_64-w64-mingw32.dll vendored Executable file

Binary file not shown.

View file

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

View file

@ -1,12 +0,0 @@
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")

View file

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

View file

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

View file

@ -1,49 +1,43 @@
using PortAudio using PortAudio
""" """Continuously read from the default audio input and plot an
Continuously read from the default audio input and plot an ASCII level/peak meter"""
ASCII level/peak meter
"""
function micmeter(metersize) function micmeter(metersize)
mic = PortAudioStream(1, 0; latency = 0.1) mic = PortAudioStream(1, 0; blocksize=512)
signalmax = zero(eltype(mic)) signalmax = zero(eltype(mic))
println("Press Ctrl-C to quit") println("Press Ctrl-C to quit")
while true while true
block = read(mic, 512) block = read(mic, 512)
blockmax = maximum(abs.(block)) # find the maximum value in the block blockmax = maximum(abs(block)) # find the maximum value in the block
signalmax = max(signalmax, blockmax) # keep the maximum value ever signalmax = max(signalmax, blockmax) # keep the maximum value ever
print("\r") # reset the cursor to the beginning of the line print("\r") # reset the cursor to the beginning of the line
printmeter(metersize, blockmax, signalmax) printmeter(metersize, blockmax, signalmax)
end end
end end
""" """Print an ASCII level meter of the given size. Signal and peak
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"""
levels are assumed to be scaled from 0.0-1.0, with peak >= signal
"""
function printmeter(metersize, signal, peak) function printmeter(metersize, signal, peak)
# calculate the positions in terms of characters # calculate the positions in terms of characters
peakpos = clamp(round(Int, peak * metersize), 0, metersize) peakpos = clamp(round(Int, peak * metersize), 0, metersize)
meterchars = clamp(round(Int, signal * metersize), 0, peakpos - 1) meterchars = clamp(round(Int, signal * metersize), 0, peakpos-1)
blankchars = max(0, peakpos - meterchars - 1) blankchars = max(0, peakpos-meterchars-1)
for position in 1:meterchars for position in 1:meterchars
printstyled(">", color = barcolor(metersize, position)) print_with_color(barcolor(metersize, position), ">")
end end
print(" "^blankchars) print(" " ^ blankchars)
printstyled("|", color = barcolor(metersize, peakpos)) print_with_color(barcolor(metersize, peakpos), "|")
print(" "^(metersize - peakpos)) print(" " ^ (metersize - peakpos))
end end
""" """Compute the proper color for a given position in the bar graph. The first
Compute the proper color for a given position in the bar graph. The first
half of the bar should be green, then the remainder is yellow except the final half of the bar should be green, then the remainder is yellow except the final
character, which is red. character, which is red."""
"""
function barcolor(metersize, position) function barcolor(metersize, position)
if position / metersize <= 0.5 if position/metersize <= 0.5
:green :green
elseif position == metersize elseif position == metersize
:red :red

View file

@ -1,156 +1,127 @@
using Distributed, PortAudio # Thanks to Jiahao Chen for this great example!
# 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 ## NOTE: THIS NEEDS TO BE PORTED OVER TO THE NEW ARCHITECTURE
const DEFAULTDEVICE = -1 ##
function paudio() using AudioIO
devs = PortAudio.devices() import AudioIO.play
if DEFAULTDEVICE < 0
devnum = findfirst(x -> x.maxoutchans > 0, devs)
(devnum == nothing) && error("No output device for audio found")
else
devnum = DEFAULTDEVICE + 1
end
return ostream = PortAudioStream(devs[devnum].name, 0, 2)
end
play(ostream, sample::Array{Float64, 1}) = write(ostream, sample) type note{S<:Real, T<:Real}
play(ostr, sample::Array{Int64, 1}) = play(ostr, Float64.(sample))
struct Note{S <: Real, T <: Real}
pitch::S pitch::S
duration::T duration::T
sustained::Bool sustained::Bool
end end
function play( function play(A::note, samplingfreq::Real=44100, shape::Function=t->0.6sin(t)+0.2sin(2t)+.05*sin(8t))
ostream, timesamples=0:1/samplingfreq:(A.duration*(A.sustained ? 0.98 : 0.9))
A::Note, v = Float64[shape(2π*A.pitch*t) for t in timesamples]
samplingfreq::Real = 44100,
shape::Function = t -> 0.6sin(t) + 0.2sin(2t) + 0.05 * sin(8t),
)
timesamples = 0:(1 / samplingfreq):(A.duration * (A.sustained ? 0.98 : 0.9))
v = Float64[shape(2π * A.pitch * t) for t in timesamples]
if !A.sustained if !A.sustained
decay_length = div(length(timesamples), 5) decay_length = int(length(timesamples) * 0.2)
v[(end - decay_length):(end - 1)] = v[end-decay_length:end-1] = v[end-decay_length:end-1] .* linspace(1, 0, decay_length)
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
end end
play(ostream, v) play(v)
sleep(A.duration) sleep(A.duration)
end end
function parsevoice(melody::String; tempo = 132, beatunit = 4, lyrics = nothing) function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
ostream = paudio() # initialize audio for output play([0]) #Force AudioIO to initialize
lyrics_syllables = lyrics == nothing ? nothing : split(lyrics) lyrics_syllables = lyrics==nothing? nothing : split(lyrics)
lyrics_syllables != nothing && (lyrics_syllables[end] *= "\n")
note_idx = 1 note_idx = 1
oldduration = 4 oldduration = 4
for line in split(melody, '\n') for line in split(melody, '\n')
percent_idx = findfirst('%', line) # Trim comment percent_idx = findfirst(line, '%') #Trim comment
percent_idx == nothing || (line = line[1:(percent_idx - 1)]) percent_idx == 0 || (line = line[1:percent_idx-1])
for token in split(line) for token in split(line)
pitch, duration, dotted, sustained = parsetoken(token) pitch, duration, dotted, sustained =parsetoken(token)
duration == nothing && (duration = oldduration) duration==nothing && (duration = oldduration)
oldduration = duration oldduration = duration
dotted && (duration *= 1.5) dotted && (duration *= 1.5)
if lyrics_syllables != nothing && 1 <= note_idx <= length(lyrics_syllables) if lyrics_syllables!=nothing && 1<=note_idx<=length(lyrics_syllables) #Print the lyrics, omitting hyphens
# Print the lyrics, omitting hyphens if lyrics_syllables[note_idx][end]=='-'
if lyrics_syllables[note_idx][end] == '-' print(lyrics_syllables[note_idx][1:end-1])
print(join(split(lyrics_syllables[note_idx][:], "")[1:(end - 1)]), "")
else else
print(lyrics_syllables[note_idx], ' ') print(lyrics_syllables[note_idx], ' ')
end end
end end
play(ostream, Note(pitch, (beatunit / duration) * (60 / tempo), sustained)) play(note(pitch, (beatunit/duration)*(60/tempo), sustained))
note_idx += 1 note_idx += 1
end end
println()
end end
end end
function parsetoken(token, Atuning::Real = 220) function parsetoken(token::String, Atuning::Real=220)
state = :findpitch state = :findpitch
pitch = 0.0 pitch = 0.0
sustain = dotted = false sustain = dotted = false
lengthbuf = Char[] lengthbuf = Char[]
for char in token for char in token
if state == :findpitch if state == :findpitch
scale_idx = scale_idx = findfirst('a':'g', char) + findfirst('A':'G', char)
something(findfirst(char, String(collect('a':'g'))), 0) + if scale_idx!=0
something(findfirst(char, String(collect('A':'G'))), 0) const halfsteps = [12, 14, 3, 5, 7, 8, 10]
if scale_idx != 0 pitch = Atuning*2^(halfsteps[scale_idx]/12)
halfsteps = [12, 14, 3, 5, 7, 8, 10]
pitch = Atuning * 2^(halfsteps[scale_idx] / 12)
state = :findlength state = :findlength
elseif char == 'r' elseif char=='r'
pitch, state = 0, :findlength pitch, state = 0, :findlength
else else
error("unknown pitch: $char") error("unknown pitch: $char")
end end
elseif state == :findlength elseif state == :findlength
if char == '#' if char == '#' ; pitch *= 2^(1/12) #sharp
pitch *= 2^(1 / 12) # sharp elseif char == 'b' ; pitch /= 2^(1/12) #flat
elseif char == 'b' elseif char == '\''; pitch *= 2 #higher octave
pitch /= 2^(1 / 12) # flat elseif char == ',' ; pitch /= 2 #lower octave
elseif char == '\'' elseif char == '.' ; dotted = true #dotted note
pitch *= 2 # higher octave elseif char == '~' ; sustain = true #tied note
elseif char == ','
pitch /= 2 # lower octave
elseif char == '.'
dotted = true # dotted note
elseif char == '~'
sustain = true # tied note
else else
push!(lengthbuf, char) push!(lengthbuf, char)
# Check for "is" and "es" suffixes for sharps and flats #Check for "is" and "es" suffixes for sharps and flats
if length(lengthbuf) >= 2 if length(lengthbuf) >= 2
if lengthbuf[(end - 1):end] == "is" if lengthbuf[end-1:end] == "is"
pitch *= 2^(1 / 12) pitch *= 2^(1/12)
lengthbuf = lengthbuf[1:(end - 2)] lengthbuf = lengthbuf[1:end-2]
elseif lengthbuf[(end - 1):end] == "es" elseif lengthbuf[end-1:end] == "es"
pitch /= 2^(1 / 12) pitch /= 2^(1/12)
lengthbuf = lengthbuf[1:(end - 2)] lengthbuf = lengthbuf[1:end-2]
end end
end end
end end
end end
end end
#finalize length #finalize length
lengthstr = String(lengthbuf) lengthstr = convert(String, lengthbuf)
duration = isempty(lengthstr) ? nothing : tryparse(Int, lengthstr) duration = isempty(lengthstr) ? nothing : parseint(lengthstr)
return (pitch, duration, sustain, dotted) return (pitch, duration, sustain, dotted)
end end
parsevoice( parsevoice("""
"""
f# f# g a a g f# e d d e f# f#~ f#8 e e2 f# f# g a a g f# e d d e f# f#~ f#8 e e2
f#4 f# g a a g f# e d d e f# e~ e8 d d2 f#4 f# g a a g f# e d d e f# e~ e8 d d2
e4 e f# d e f#8~ g8 f#4 d e f#8~ g f#4 e d e a, e4 e f# d e f#8~ g8 f#4 d e f#8~ g f#4 e d e a,
f#2 f#4 g a a g f# e d d e f# e~ e8 d8 d2""", f#2 f#4 g a a g f# e d d e f# e~ e8 d8 d2""",
lyrics = """ lyrics="""
Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um! Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!
Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum! Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!
Dei- ne Zau- ber bin den - wie- der, was die - Mo- de streng ge- theilt, Dei- ne Zau- ber bin den - wie- der, was die - Mo- de streng ge- theilt,
al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt. al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt.
""", """)
)
# And now with harmony! # And now with harmony!
soprano = @spawn parsevoice( soprano = @async parsevoice("""
"""
f'#. f'#. g'. a'. a'. g'. f'#. e'~ e'8 d.'4 d.' e.' f#'. f#'.~ f#' e'8 e'4~ e'2 f'#. f'#. g'. a'. a'. g'. f'#. e'~ e'8 d.'4 d.' e.' f#'. f#'.~ f#' e'8 e'4~ e'2
""", """, lyrics="Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!"
lyrics = "Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!",
) )
alto = @spawn parsevoice(""" alto = @async parsevoice("""
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2 a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2
""") """)
tenor = @spawn parsevoice(""" tenor = @async parsevoice("""
d. d. e. f#. f#. e. d. d~ e8 f#.4 f#. a,. d. d.~ d c#8 c#4 c#2 d. d. e. f#. f#. e. d. d~ e8 f#.4 f#. a,. d. d.~ d c#8 c#4 c#2
""") """)
bass = @spawn parsevoice(""" bass = @async parsevoice("""
d. d. d. d. a,. a,. a,. b,~ c8 d. a., a., a., a., a, a8, a,4 a,2 d. d. d. d. a,. a,. a,. b,~ c8 d. a., a., a., a., a, a8, a,4 a,2
""") """)
wait(soprano) wait(soprano)
@ -158,21 +129,19 @@ wait(alto)
wait(tenor) wait(tenor)
wait(bass) wait(bass)
soprano = @spawn parsevoice( soprano = @async parsevoice("""
"""
f'#.4 f'#. g'. a'. a'. g'. f'#. e'. d'. d'. e'. f'#. e'.~ e' d'8 d'4~ d'2 f'#.4 f'#. g'. a'. a'. g'. f'#. e'. d'. d'. e'. f'#. e'.~ e' d'8 d'4~ d'2
""", """, lyrics="Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!")
lyrics = "Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!", alto = @async parsevoice("""
)
alto = @spawn parsevoice("""
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2 a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2
""") """)
tenor = @spawn parsevoice(""" tenor = @async parsevoice("""
d.4 d. d. d. d. d. d. d. d. d. c#. d. c#.~ c# d8 d d2 d.4 d. d. d. d. d. d. d. d. d. c#. d. c#.~ c# d8 d d2
""") """)
bass = @spawn parsevoice(""" bass = @async parsevoice("""
d.4 d. d. d. a,. a,. a,. a., a., a., a., a., a.,~ a, a,8 d, d,2 d.4 d. d. d. a,. a,. a,. a., a., a., a., a., a.,~ a, a,8 d, d,2
""") """)
wait(soprano) wait(soprano)
wait(alto) wait(alto)
wait(tenor) wait(tenor)

View file

@ -1,65 +0,0 @@
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

View file

@ -1,89 +0,0 @@
#=
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 module SpectrumExample
using GR, PortAudio, SampledSignals, FFTW using GR, PortAudio, SampledSignals
const N = 1024 const N = 1024
const stream = PortAudioStream(1, 0) const stream = PortAudioStream(1, 0, blocksize=N)
const buf = read(stream, N) const buf = read(stream, N)
const fmin = 0Hz const fmin = 0Hz
const fmax = 10000Hz const fmax = 10000Hz
@ -14,7 +14,7 @@ const fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
while true while true
read!(stream, buf) read!(stream, buf)
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100)) plot(fs, abs(fft(buf)[fmin..fmax]), xlim=(fs[1],fs[end]), ylim=(0,100))
end end
end end

View file

@ -1,21 +0,0 @@
#=
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

@ -1,67 +0,0 @@
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

@ -1,43 +0,0 @@
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

View file

@ -1 +0,0 @@
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.

View file

@ -1,16 +0,0 @@
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)

View file

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

11
runtests.sh Executable file
View file

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

45
src/pa_shim.jl Normal file
View file

@ -0,0 +1,45 @@
function find_pa_shim()
libdir = joinpath(@__DIR__, "..", "deps", "usr", "lib")
libsuffix = ""
basename = "pa_shim"
@static if Compat.Sys.islinux() && Sys.ARCH == :x86_64
libsuffix = "x86_64-linux-gnu"
elseif Compat.Sys.islinux() && Sys.ARCH == :i686
libsuffix = "i686-linux-gnu"
elseif Compat.Sys.isapple() && Sys.ARCH == :x86_64
libsuffix = "x86_64-apple-darwin14"
elseif Compat.Sys.iswindows() && Sys.ARCH == :x86_64
libsuffix = "x86_64-w64-mingw32"
elseif Compat.Sys.iswindows() && Sys.ARCH == :i686
libsuffix = "i686-w64-mingw32"
elseif !any(
(sfx) -> isfile(joinpath(libdir, "$basename.$sfx")),
("so", "dll", "dylib"))
error("Unsupported platform $(Sys.MACHINE). You can build your own library by running `make` from $(joinpath(@__FILE__, "..", "deps", "src"))")
end
# if there's a suffix-less library, it was built natively on this machine,
# so load that one first, otherwise load the pre-built one
libpa_shim = Libdl.find_library(
[basename, "$(basename)_$libsuffix"],
[libdir])
libpa_shim == "" && error("Could not load $basename library, please file an issue at https://github.com/JuliaAudio/RingBuffers.jl/issues with your `versioninfo()` output")
return libpa_shim
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{Cvoid} # Julia callback to notify on updates (called from audio thread)
inputhandle::Ptr{Cvoid} # condition to notify on new input data
outputhandle::Ptr{Cvoid} # condition to notify when ready for output
errorhandle::Ptr{Cvoid} # condition to notify on new errors
end

View file

@ -1,29 +0,0 @@
# 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))

3
test/REQUIRE Normal file
View file

@ -0,0 +1,3 @@
Compat
SampledSignals
RingBuffers

View file

@ -1,65 +1,191 @@
#!/usr/bin/env julia #!/usr/bin/env julia
using Base.Sys: iswindows
using Documenter: doctest
using PortAudio:
combine_default_sample_rates,
devices,
get_default_input_index,
get_default_output_index,
get_device,
get_input_type,
get_output_type,
handle_status,
initialize,
name,
PortAudioException,
PortAudio,
PortAudioDevice,
PortAudioStream,
safe_load,
seek_alsa_conf,
terminate,
write_buffer
using PortAudio.LibPortAudio:
Pa_AbortStream,
PaError,
PaErrorCode,
paFloat32,
Pa_GetDefaultHostApi,
Pa_GetDeviceInfo,
Pa_GetHostApiCount,
Pa_GetLastHostErrorInfo,
Pa_GetSampleSize,
Pa_GetStreamCpuLoad,
Pa_GetStreamInfo,
Pa_GetStreamReadAvailable,
Pa_GetStreamTime,
Pa_GetStreamWriteAvailable,
Pa_GetVersionInfo,
Pa_HostApiDeviceIndexToDeviceIndex,
paHostApiNotFound,
Pa_HostApiTypeIdToHostApiIndex,
PaHostErrorInfo,
paInDevelopment,
paInvalidDevice,
Pa_IsFormatSupported,
Pa_IsStreamActive,
paNoError,
paNoFlag,
paNotInitialized,
Pa_OpenDefaultStream,
paOutputUnderflowed,
Pa_SetStreamFinishedCallback,
Pa_Sleep,
Pa_StopStream,
PaStream,
PaStreamInfo,
PaStreamParameters,
PaVersionInfo
using SampledSignals: nchannels, s, SampleBuf, samplerate, SinSource
using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
@testset "Tests without sound" begin using Compat
using Compat.Test
import Compat: Cvoid
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,
(Ref{Float32}, Ref{Float32}, Culong, Ref{Cvoid}, Culong, Ref{pa_shim_info_t}),
cb_input, cb_output, nframes, C_NULL, flags, 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
# the process closure only has a pointer (not a ref) to sinkbuf
@static if VERSION >= v"0.7.0-"
GC.@preserve sinkbuf begin
@test process() == PortAudio.paContinue
end
else
@test process() == PortAudio.paContinue
end
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
@testset "PortAudio Tests" begin
@testset "Reports version" begin @testset "Reports version" begin
io = IOBuffer() io = IOBuffer()
PortAudio.versioninfo(io) PortAudio.versioninfo(io)
@ -68,189 +194,64 @@ using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
@test startswith(result[1], "PortAudio V19") @test startswith(result[1], "PortAudio V19")
end end
@testset "Can list devices without crashing" begin @testset "using correct shim version" begin
display(devices()) @test PortAudio.shimhash() == "87021557a9f999545828eb11e4ebad2cd278b734dd91a8bd3faf05c89912cf80"
println()
end end
@testset "libortaudio without sound" begin @testset "Basic callback functionality" begin
@test handle_status(Pa_GetHostApiCount()) >= 0 @testset "basic duplex (no sync)" begin
@test handle_status(Pa_GetDefaultHostApi()) >= 0 test_callback(2, 3, false)
# version info not available on windows? end
if !Sys.iswindows() @testset "basic input-only (no sync)" begin
@test safe_load(Pa_GetVersionInfo(), ErrorException("no info")) isa test_callback(2, 0, false)
PaVersionInfo end
@testset "basic output-only (no sync)" begin
test_callback(0, 2, false)
end
@testset "basic no input or output (no sync)" begin
test_callback(0, 0, false)
end
@testset "basic duplex (sync)" begin
test_callback(2, 3, true)
end
@testset "basic input-only (sync)" begin
test_callback(2, 0, true)
end
@testset "basic output-only (sync)" begin
test_callback(0, 2, true)
end
@testset "basic no input or output (sync)" begin
test_callback(0, 0, true)
end end
@test safe_load(Pa_GetLastHostErrorInfo(), ErrorException("no info")) isa
PaHostErrorInfo
@test PaErrorCode(Pa_IsFormatSupported(C_NULL, C_NULL, 0.0)) == paInvalidDevice
@test PaErrorCode(
Pa_OpenDefaultStream(Ref(C_NULL), 0, 0, paFloat32, 0.0, 0, C_NULL, C_NULL),
) == paInvalidDevice
end end
@testset "Errors without sound" begin @testset "Ouput underflow" begin
@test sprint(showerror, PortAudioException(paNotInitialized)) == @testset "underflow duplex (nosync)" begin
"PortAudioException: PortAudio not initialized" test_callback_underflow(2, 3, false)
@test_throws KeyError("foobarbaz") get_device("foobarbaz") end
@test_throws KeyError(-1) get_device(-1) @testset "underflow output-only (nosync)" begin
@test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(()) test_callback_underflow(0, 3, false)
@test_logs (:warn, "libportaudio: Output underflowed") handle_status( end
PaError(paOutputUnderflowed), @testset "underflow duplex (sync)" begin
) test_callback_underflow(2, 3, true)
@test_throws PortAudioException(paNotInitialized) handle_status( end
PaError(paNotInitialized), @testset "underflow output-only (sync)" begin
) test_callback_underflow(0, 3, true)
Pa_Sleep(1) end
@test Pa_GetSampleSize(paFloat32) == 4
end end
# make sure we can terminate, then reinitialize @testset "Input overflow" begin
terminate() @testset "overflow duplex (nosync)" begin
initialize() test_callback_overflow(2, 3, false)
end end
@testset "overflow input-only (nosync)" begin
if isempty(devices()) test_callback_overflow(2, 0, false)
@test_throws ArgumentError("No input device available") get_default_input_index() end
else @testset "overflow duplex (sync)" begin
@testset "Tests with sound" begin test_callback_overflow(2, 3, true)
# these default values are specific to local machines end
input_name = get_device(get_default_input_index()).name @testset "overflow input-only (sync)" begin
output_name = get_device(get_default_output_index()).name test_callback_overflow(2, 0, true)
end
@testset "Interactive tests" begin end
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 end

View file

@ -2,94 +2,104 @@
# locally on a machine with a sound card. It's mostly to put the library through # locally on a machine with a sound card. It's mostly to put the library through
# its paces assuming a human is listening. # its paces assuming a human is listening.
include("runtests.jl") # include("runtests.jl")
using Compat
using PortAudio
using SampledSignals
using Test
using RingBuffers
# these default values are specific to my machines # these default values are specific to my machines
if Sys.iswindows() if Compat.Sys.iswindows()
default_indev = "Microphone Array (Realtek High " default_indev = "Microphone Array (Realtek High "
default_outdev = "Speaker/Headphone (Realtek High" default_outdev = "Speaker/Headphone (Realtek High"
elseif Sys.isapple() elseif Compat.Sys.isapple()
default_indev = "Built-in Microphone" default_indev = "Built-in Microphone"
default_outdev = "Built-in Output" default_outdev = "Built-in Output"
elseif Sys.islinux() elseif Compat.Sys.islinux()
default_indev = "default" default_indev = "default"
default_outdev = "default" default_outdev = "default"
end end
@testset "Local Tests" begin @testset "Local Tests" begin
@testset "Open Default Device" begin # @testset "Open Default Device" begin
println("Recording...") # println("Recording...")
stream = PortAudioStream(2, 0) # stream = PortAudioStream(2, 0)
buf = read(stream, 5s) # buf = read(stream, 5s)
close(stream) # close(stream)
@test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source)) # @test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
println("Playing back recording...") # println("Playing back recording...")
stream = PortAudioStream(0, 2) # stream = PortAudioStream(0, 2)
write(stream, buf) # write(stream, buf)
println("flushing...") # println("flushing...")
flush(stream) # flush(stream)
close(stream) # close(stream)
println("Testing pass-through") # println("Testing pass-through")
stream = PortAudioStream(2, 2) # stream = PortAudioStream(2, 2)
write(stream, stream, 5s) # write(stream, stream, 5s)
flush(stream) # flush(stream)
close(stream) # close(stream)
println("done") # println("done")
end # end
@testset "Samplerate-converting writing" begin # @testset "Samplerate-converting writing" begin
stream = PortAudioStream(0, 2) # stream = PortAudioStream(0, 2)
write(stream, SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]), 3s) # write(stream, SinSource(eltype(stream), samplerate(stream)*0.8, [220, 330]), 3s)
write(stream, SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]), 3s) # write(stream, SinSource(eltype(stream), samplerate(stream)*1.2, [220, 330]), 3s)
flush(stream) # flush(stream)
close(stream) # close(stream)
end # end
@testset "Open Device by name" begin # @testset "Open Device by name" begin
stream = PortAudioStream(default_indev, default_outdev) # stream = PortAudioStream(default_indev, default_outdev)
buf = read(stream, 0.001s) # buf = read(stream, 0.001s)
@test size(buf) == # @test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
(round(Int, 0.001 * samplerate(stream)), nchannels(stream.source)) # write(stream, buf)
write(stream, buf) # io = IOBuffer()
io = IOBuffer() # show(io, stream)
show(io, stream) # @test Compat.occursin("""
@test occursin( # PortAudioStream{Float32}
""" # Samplerate: 44100.0Hz
PortAudioStream{Float32} # Buffer Size: 4096 frames
Samplerate: 44100.0Hz # 2 channel sink: "$default_outdev"
Buffer Size: 4096 frames # 2 channel source: "$default_indev\"""", String(take!(io)))
2 channel sink: "$default_outdev" # close(stream)
2 channel source: "$default_indev\"""", # end
String(take!(io)), # @testset "Error on wrong name" begin
) # @test_throws ErrorException PortAudioStream("foobarbaz")
close(stream) # end
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, # no way to check that the right data is actually getting read or written here,
# but at least it's not crashing. # but at least it's not crashing.
@testset "Queued Writing" begin @testset "Queued Writing" begin
stream = PortAudioStream(0, 2) stream = PortAudioStream(0, 2)
buf = SampleBuf( buf = SampleBuf(rand(eltype(stream), 4800, nchannels(stream.sink))*0.1, samplerate(stream))
rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1, io = IOBuffer()
samplerate(stream), t1 = @async begin
) println(io, "t1 - before")
t1 = @async write(stream, buf) w = write(stream, buf)
t2 = @async write(stream, buf) println(io, "t1 - after")
@test fetch(t1) == 48000 w
@test fetch(t2) == 48000 end
t2 = @async begin
println(io, "t2 - before")
w = write(stream, buf)
println(io, "t2 - after")
w
end
@test fetch(t1) == 4800
@test fetch(t2) == 4800
seekstart(io)
print(read(io, String))
flush(stream) flush(stream)
close(stream) close(stream)
@show t1.state
@show t2.state
end end
@testset "Queued Reading" begin # @testset "Queued Reading" begin
stream = PortAudioStream(2, 0) # stream = PortAudioStream(2, 0)
buf = SampleBuf( # buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source))*0.1, samplerate(stream))
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1, # t1 = @async read!(stream, buf)
samplerate(stream), # t2 = @async read!(stream, buf)
) # @test fetch(t1) == 48000
t1 = @async read!(stream, buf) # @test fetch(t2) == 48000
t2 = @async read!(stream, buf) # close(stream)
@test fetch(t1) == 48000 # end
@test fetch(t2) == 48000
close(stream)
end
end end