Merge pull request #9 from JuliaAudio/crosscompile
Switches to using native extension for ringbuffer exchange. fixes #6.
This commit is contained in:
commit
5ea61ea30e
26 changed files with 2042 additions and 308 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,8 +1,10 @@
|
||||||
*.swp
|
*.swp
|
||||||
*.so
|
|
||||||
*.o
|
*.o
|
||||||
deps/deps.jl
|
deps/deps.jl
|
||||||
*.wav
|
*.wav
|
||||||
*.flac
|
*.flac
|
||||||
*.cov
|
*.cov
|
||||||
coverage
|
coverage
|
||||||
|
deps/usr/lib/pa_shim.so
|
||||||
|
deps/usr/lib/pa_shim.dylib
|
||||||
|
deps/usr/lib/pa_shim.dll
|
||||||
|
|
|
@ -5,11 +5,9 @@ os:
|
||||||
- osx
|
- osx
|
||||||
sudo: required
|
sudo: required
|
||||||
julia:
|
julia:
|
||||||
- 0.4
|
- 0.6
|
||||||
- 0.5
|
|
||||||
notifications:
|
notifications:
|
||||||
email: false
|
email: false
|
||||||
script:
|
script:
|
||||||
# we can't actually run on travis, so just make sure it's installable
|
|
||||||
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
||||||
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); using PortAudio'
|
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); Pkg.test("PortAudio")'
|
||||||
|
|
6
LICENSE
6
LICENSE
|
@ -19,3 +19,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
|
suppressor.jl includes code from the Suppressor.jl package, licensed under the
|
||||||
|
MIT "Expat" License:
|
||||||
|
|
||||||
|
Copyright (c) 2016: Ismael Venegas Castelló.
|
||||||
|
|
11
README.md
11
README.md
|
@ -80,3 +80,14 @@ julia> buf = read(stream, 10s)
|
||||||
|
|
||||||
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
|
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Building the shim library
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
6
REQUIRE
6
REQUIRE
|
@ -1,8 +1,6 @@
|
||||||
julia 0.4
|
julia 0.6-
|
||||||
Compat 0.8.8
|
|
||||||
BinDeps
|
BinDeps
|
||||||
Devectorize
|
|
||||||
SampledSignals 0.3.0
|
SampledSignals 0.3.0
|
||||||
RingBuffers 0.1.0
|
RingBuffers 1.0.0
|
||||||
@osx Homebrew
|
@osx Homebrew
|
||||||
@windows WinRPM
|
@windows WinRPM
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.4/julia-0.4-latest-win32.exe"
|
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe"
|
||||||
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.4/julia-0.4-latest-win64.exe"
|
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe"
|
||||||
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.5/julia-0.5-latest-win32.exe"
|
|
||||||
- JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.5/julia-0.5-latest-win64.exe"
|
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
- provider: Email
|
- provider: Email
|
||||||
|
@ -27,5 +25,4 @@ build_script:
|
||||||
Pkg.clone(pwd(), \"PortAudio\"); Pkg.build(\"PortAudio\")"
|
Pkg.clone(pwd(), \"PortAudio\"); Pkg.build(\"PortAudio\")"
|
||||||
|
|
||||||
test_script:
|
test_script:
|
||||||
# can't actually run the test, so just make sure it's installable
|
- C:\projects\julia\bin\julia --check-bounds=yes -e "Pkg.test(\"PortAudio\")"
|
||||||
- C:\projects\julia\bin\julia -e "using PortAudio"
|
|
||||||
|
|
80
deps/src/Makefile
vendored
Normal file
80
deps/src/Makefile
vendored
Normal 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
54
deps/src/build.sh
vendored
Executable 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
8
deps/src/dockerbuild_cb.sh
vendored
Executable 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
13
deps/src/dockerbuild_hbb.sh
vendored
Executable 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
104
deps/src/pa_shim.c
vendored
Normal 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
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
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
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
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
BIN
deps/usr/lib/pa_shim_powerpc64le-linux-gnu.so
vendored
Executable file
Binary file not shown.
BIN
deps/usr/lib/pa_shim_x86_64-apple-darwin14.dylib
vendored
Executable file
BIN
deps/usr/lib/pa_shim_x86_64-apple-darwin14.dylib
vendored
Executable file
Binary file not shown.
BIN
deps/usr/lib/pa_shim_x86_64-linux-gnu.so
vendored
Executable file
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
BIN
deps/usr/lib/pa_shim_x86_64-w64-mingw32.dll
vendored
Executable file
Binary file not shown.
265
src/PortAudio.jl
265
src/PortAudio.jl
|
@ -3,51 +3,52 @@ __precompile__()
|
||||||
module PortAudio
|
module PortAudio
|
||||||
|
|
||||||
using SampledSignals
|
using SampledSignals
|
||||||
using Devectorize
|
|
||||||
using RingBuffers
|
using RingBuffers
|
||||||
using Compat
|
#using Suppressor
|
||||||
import Compat: UTF8String, view
|
|
||||||
|
import Base: eltype, show
|
||||||
|
import Base: close, isopen
|
||||||
|
import Base: read, read!, write, flush
|
||||||
|
|
||||||
# Get binary dependencies loaded from BinDeps
|
# Get binary dependencies loaded from BinDeps
|
||||||
include( "../deps/deps.jl")
|
include("../deps/deps.jl")
|
||||||
|
include("suppressor.jl")
|
||||||
|
include("pa_shim.jl")
|
||||||
include("libportaudio.jl")
|
include("libportaudio.jl")
|
||||||
|
|
||||||
|
function __init__()
|
||||||
|
init_pa_shim()
|
||||||
|
global const notifycb_c = cfunction(notifycb, Cint, (Ptr{Void}, ))
|
||||||
|
# initialize PortAudio on module load
|
||||||
|
@suppress_err Pa_Initialize()
|
||||||
|
end
|
||||||
|
|
||||||
export PortAudioStream
|
export PortAudioStream
|
||||||
|
|
||||||
# These sizes are all in frames
|
# These sizes are all in frames
|
||||||
|
|
||||||
# the block size is what we request from portaudio if no blocksize is given.
|
# the block size is what we request from portaudio if no blocksize is given.
|
||||||
# The ringbuffer and pre-fill will be twice the blocksize
|
# The ringbuffer and pre-fill will be twice the blocksize
|
||||||
const DEFAULT_BLOCKSIZE=4096
|
const DEFAULT_BLOCKSIZE=4096
|
||||||
|
|
||||||
# data is passed to and from the ringbuffer in chunks with this many frames
|
# data is passed to and from the ringbuffer in chunks with this many frames
|
||||||
# it should be at most the ringbuffer size, and must evenly divide into the
|
# it should be at most the ringbuffer size, and must evenly divide into the
|
||||||
# the underlying portaudio buffer size. E.g. if PortAudio is running with a
|
# the underlying portaudio buffer size. E.g. if PortAudio is running with a
|
||||||
# 2048-frame buffer period, the chunk size can be 2048, 1024, 512, 256, etc.
|
# 2048-frame buffer period, the chunk size can be 2048, 1024, 512, 256, etc.
|
||||||
const CHUNKSIZE=128
|
const CHUNKSIZE=128
|
||||||
|
|
||||||
function __init__()
|
# ringbuffer to receive errors from the audio processing thread
|
||||||
# initialize PortAudio on module load
|
const ERR_BUFSIZE=512
|
||||||
swallow_stderr() do
|
|
||||||
Pa_Initialize()
|
|
||||||
end
|
|
||||||
|
|
||||||
# the portaudio callbacks are parametric on the sample type
|
|
||||||
global const pa_callbacks = Dict{Type, Ptr{Void}}()
|
|
||||||
|
|
||||||
for T in (Float32, Int32, Int16, Int8, UInt8)
|
|
||||||
pa_callbacks[T] = cfunction(portaudio_callback, Cint,
|
|
||||||
(Ptr{T}, Ptr{T}, Culong, Ptr{Void}, Culong,
|
|
||||||
Ptr{CallbackInfo{T}}))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function versioninfo(io::IO=STDOUT)
|
function versioninfo(io::IO=STDOUT)
|
||||||
println(io, Pa_GetVersionText())
|
println(io, Pa_GetVersionText())
|
||||||
println(io, "Version Number: ", Pa_GetVersion())
|
println(io, "Version: ", Pa_GetVersion())
|
||||||
|
println(io, "Shim Source Hash: ", shimhash()[1:10])
|
||||||
end
|
end
|
||||||
|
|
||||||
type PortAudioDevice
|
type PortAudioDevice
|
||||||
name::UTF8String
|
name::String
|
||||||
hostapi::UTF8String
|
hostapi::String
|
||||||
maxinchans::Int
|
maxinchans::Int
|
||||||
maxoutchans::Int
|
maxoutchans::Int
|
||||||
defaultsamplerate::Float64
|
defaultsamplerate::Float64
|
||||||
|
@ -71,23 +72,9 @@ end
|
||||||
# not for external use, used in error message printing
|
# not for external use, used in error message printing
|
||||||
devnames() = join(["\"$(dev.name)\"" for dev in devices()], "\n")
|
devnames() = join(["\"$(dev.name)\"" for dev in devices()], "\n")
|
||||||
|
|
||||||
"""Give a pointer to the given field within a Julia object"""
|
##################
|
||||||
function fieldptr{T}(obj::T, field::Symbol)
|
# PortAudioStream
|
||||||
fieldnum = findfirst(fieldnames(T), field)
|
##################
|
||||||
offset = fieldoffset(T, fieldnum)
|
|
||||||
FT = fieldtype(T, field)
|
|
||||||
|
|
||||||
Ptr{FT}(pointer_from_objref(obj) + offset)
|
|
||||||
end
|
|
||||||
|
|
||||||
# we want this to be immutable so we can stack allocate it
|
|
||||||
immutable CallbackInfo{T}
|
|
||||||
inchannels::Int
|
|
||||||
inbuf::LockFreeRingBuffer{T}
|
|
||||||
outchannels::Int
|
|
||||||
outbuf::LockFreeRingBuffer{T}
|
|
||||||
synced::Bool
|
|
||||||
end
|
|
||||||
|
|
||||||
type PortAudioStream{T}
|
type PortAudioStream{T}
|
||||||
samplerate::Float64
|
samplerate::Float64
|
||||||
|
@ -95,12 +82,13 @@ type PortAudioStream{T}
|
||||||
stream::PaStream
|
stream::PaStream
|
||||||
sink # untyped because of circular type definition
|
sink # untyped because of circular type definition
|
||||||
source # untyped because of circular type definition
|
source # untyped because of circular type definition
|
||||||
bufinfo::CallbackInfo{T} # immutable data used in the portaudio callback
|
errbuf::RingBuffer{pa_shim_errmsg_t} # used to send errors from the portaudio callback
|
||||||
|
bufinfo::pa_shim_info_t # data used in the portaudio callback
|
||||||
|
|
||||||
# this inner constructor is generally called via the top-level outer
|
# this inner constructor is generally called via the top-level outer
|
||||||
# constructor below
|
# constructor below
|
||||||
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
|
function PortAudioStream{T}(indev::PortAudioDevice, outdev::PortAudioDevice,
|
||||||
inchans, outchans, sr, blocksize, synced)
|
inchans, outchans, sr, blocksize, synced) where {T}
|
||||||
inchans = inchans == -1 ? indev.maxinchans : inchans
|
inchans = inchans == -1 ? indev.maxinchans : inchans
|
||||||
outchans = outchans == -1 ? outdev.maxoutchans : outchans
|
outchans = outchans == -1 ? outdev.maxoutchans : outchans
|
||||||
inparams = (inchans == 0) ?
|
inparams = (inchans == 0) ?
|
||||||
|
@ -113,23 +101,55 @@ type PortAudioStream{T}
|
||||||
finalizer(this, close)
|
finalizer(this, close)
|
||||||
this.sink = PortAudioSink{T}(outdev.name, this, outchans, blocksize*2)
|
this.sink = PortAudioSink{T}(outdev.name, this, outchans, blocksize*2)
|
||||||
this.source = PortAudioSource{T}(indev.name, this, inchans, blocksize*2)
|
this.source = PortAudioSource{T}(indev.name, this, inchans, blocksize*2)
|
||||||
|
this.errbuf = RingBuffer{pa_shim_errmsg_t}(1, ERR_BUFSIZE)
|
||||||
if synced && inchans > 0 && outchans > 0
|
if synced && inchans > 0 && outchans > 0
|
||||||
# we've got a synchronized duplex stream. initialize with the output buffer full
|
# we've got a synchronized duplex stream. initialize with the output buffer full
|
||||||
write(this.sink, SampleBuf(zeros(T, blocksize*2, outchans), sr))
|
write(this.sink, SampleBuf(zeros(T, blocksize*2, outchans), sr))
|
||||||
end
|
end
|
||||||
this.bufinfo = CallbackInfo(inchans, this.source.ringbuf,
|
# pass NULL for input/output we're not using
|
||||||
outchans, this.sink.ringbuf, synced)
|
this.bufinfo = pa_shim_info_t(
|
||||||
this.stream = swallow_stderr() do
|
inchans > 0 ? bufpointer(this.source) : C_NULL,
|
||||||
Pa_OpenStream(inparams, outparams, float(sr), blocksize,
|
outchans > 0 ? bufpointer(this.sink) : C_NULL,
|
||||||
paNoFlag, pa_callbacks[T], fieldptr(this, :bufinfo))
|
pointer(this.errbuf),
|
||||||
end
|
synced, notifycb_c,
|
||||||
|
inchans > 0 ? notifyhandle(this.source) : C_NULL,
|
||||||
|
outchans > 0 ? notifyhandle(this.sink) : C_NULL,
|
||||||
|
notifyhandle(this.errbuf))
|
||||||
|
this.stream = @suppress_err Pa_OpenStream(inparams, outparams,
|
||||||
|
float(sr), blocksize,
|
||||||
|
paNoFlag, shim_processcb_c,
|
||||||
|
this.bufinfo)
|
||||||
|
|
||||||
Pa_StartStream(this.stream)
|
Pa_StartStream(this.stream)
|
||||||
|
@async handle_errors(this)
|
||||||
|
|
||||||
this
|
this
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
PortAudioStream(inchannels=2, outchannels=2; options...)
|
||||||
|
PortAudioStream(duplexdevice, inchannels=2, outchannels=2; options...)
|
||||||
|
PortAudioStream(indevice, outdevice, inchannels=2, outchannels=2; options...)
|
||||||
|
|
||||||
|
Audio devices can either be `PortAudioDevice` instances as returned
|
||||||
|
by `PortAudio.devices()`, or strings with the device name as reported by the
|
||||||
|
operating system. If a single `duplexdevice` is given it will be used for both
|
||||||
|
input and output. If no devices are given the system default devices will be
|
||||||
|
used.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
* `eltype`: Sample type of the audio stream (defaults to Float32)
|
||||||
|
* `samplerate`: Sample rate (defaults to device sample rate)
|
||||||
|
* `blocksize`: Size of the blocks that are written to and read from the audio
|
||||||
|
device. (Defaults to $DEFAULT_BLOCKSIZE)
|
||||||
|
* `synced`: Determines whether the input and output streams are kept in
|
||||||
|
sync. If `true`, you must read and write an equal number of
|
||||||
|
frames, and the round-trip latency is guaranteed constant. If
|
||||||
|
`false`, you are free to read and write separately, but
|
||||||
|
overflow or underflow can affect the round-trip latency.
|
||||||
|
"""
|
||||||
# this is the top-level outer constructor that all the other outer constructors
|
# this is the top-level outer constructor that all the other outer constructors
|
||||||
# end up calling
|
# end up calling
|
||||||
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
|
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
|
||||||
|
@ -191,7 +211,7 @@ function PortAudioStream(inchans=2, outchans=2; kwargs...)
|
||||||
PortAudioStream(indevice, outdevice, inchans, outchans; kwargs...)
|
PortAudioStream(indevice, outdevice, inchans, outchans; kwargs...)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Base.close(stream::PortAudioStream)
|
function close(stream::PortAudioStream)
|
||||||
if stream.stream != C_NULL
|
if stream.stream != C_NULL
|
||||||
Pa_StopStream(stream.stream)
|
Pa_StopStream(stream.stream)
|
||||||
Pa_CloseStream(stream.stream)
|
Pa_CloseStream(stream.stream)
|
||||||
|
@ -203,44 +223,74 @@ function Base.close(stream::PortAudioStream)
|
||||||
nothing
|
nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
Base.isopen(stream::PortAudioStream) = stream.stream != C_NULL
|
isopen(stream::PortAudioStream) = stream.stream != C_NULL
|
||||||
|
|
||||||
SampledSignals.samplerate(stream::PortAudioStream) = stream.samplerate
|
SampledSignals.samplerate(stream::PortAudioStream) = stream.samplerate
|
||||||
Base.eltype{T}(stream::PortAudioStream{T}) = T
|
eltype{T}(stream::PortAudioStream{T}) = T
|
||||||
|
|
||||||
Base.read(stream::PortAudioStream, args...) = read(stream.source, args...)
|
read(stream::PortAudioStream, args...) = read(stream.source, args...)
|
||||||
Base.read!(stream::PortAudioStream, args...) = read!(stream.source, args...)
|
read!(stream::PortAudioStream, args...) = read!(stream.source, args...)
|
||||||
Base.write(stream::PortAudioStream, args...) = write(stream.sink, args...)
|
write(stream::PortAudioStream, args...) = write(stream.sink, args...)
|
||||||
Base.write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...)
|
write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...)
|
||||||
Base.flush(stream::PortAudioStream) = flush(stream.sink)
|
flush(stream::PortAudioStream) = flush(stream.sink)
|
||||||
|
|
||||||
function Base.show(io::IO, stream::PortAudioStream)
|
function show(io::IO, stream::PortAudioStream)
|
||||||
println(io, typeof(stream))
|
println(io, typeof(stream))
|
||||||
println(io, " Samplerate: ", samplerate(stream), "Hz")
|
println(io, " Samplerate: ", samplerate(stream), "Hz")
|
||||||
print(io, " Buffer Size: ", stream.blocksize, " frames")
|
print(io, " Buffer Size: ", stream.blocksize, " frames")
|
||||||
if nchannels(stream.sink) > 0
|
if nchannels(stream.sink) > 0
|
||||||
print(io, "\n ", nchannels(stream.sink), " channel sink: \"", stream.sink.name, "\"")
|
print(io, "\n ", nchannels(stream.sink), " channel sink: \"", name(stream.sink), "\"")
|
||||||
end
|
end
|
||||||
if nchannels(stream.source) > 0
|
if nchannels(stream.source) > 0
|
||||||
print(io, "\n ", nchannels(stream.source), " channel source: \"", stream.source.name, "\"")
|
print(io, "\n ", nchannels(stream.source), " channel source: \"", name(stream.source), "\"")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"""
|
||||||
|
handle_errors(stream::PortAudioStream)
|
||||||
|
|
||||||
|
Handle errors coming over the error stream from PortAudio. This is run as an
|
||||||
|
independent task while the stream is active.
|
||||||
|
"""
|
||||||
|
function handle_errors(stream::PortAudioStream)
|
||||||
|
err = Vector{pa_shim_errmsg_t}(1)
|
||||||
|
while true
|
||||||
|
nread = read!(stream.errbuf, err)
|
||||||
|
nread == 1 || break
|
||||||
|
if err[1] == PA_SHIM_ERRMSG_ERR_OVERFLOW
|
||||||
|
warn("Error buffer overflowed on stream $(stream.name)")
|
||||||
|
elseif err[1] == PA_SHIM_ERRMSG_OVERFLOW
|
||||||
|
# warn("Input overflowed from $(name(stream.source))")
|
||||||
|
elseif err[1] == PA_SHIM_ERRMSG_UNDERFLOW
|
||||||
|
# warn("Output underflowed to $(name(stream.sink))")
|
||||||
|
else
|
||||||
|
error("""
|
||||||
|
Got unrecognized error code $(err[1]) from audio thread for
|
||||||
|
stream "$(stream.name)". Please file an issue at
|
||||||
|
https://github.com/juliaaudio/portaudio.jl/issues""")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
##################################
|
||||||
|
# PortAudioSink & PortAudioSource
|
||||||
|
##################################
|
||||||
|
|
||||||
# Define our source and sink types
|
# Define our source and sink types
|
||||||
for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
|
for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
|
||||||
(:PortAudioSource, :SampleSource))
|
(:PortAudioSource, :SampleSource))
|
||||||
@eval type $TypeName{T} <: $Super
|
@eval type $TypeName{T} <: $Super
|
||||||
name::UTF8String
|
name::String
|
||||||
stream::PortAudioStream{T}
|
stream::PortAudioStream{T}
|
||||||
chunkbuf::Array{T, 2}
|
chunkbuf::Array{T, 2}
|
||||||
ringbuf::LockFreeRingBuffer{T}
|
ringbuf::RingBuffer{T}
|
||||||
nchannels::Int
|
nchannels::Int
|
||||||
|
|
||||||
function $TypeName(name, stream, channels, ringbufsize)
|
function $TypeName{T}(name, stream, channels, ringbufsize) where {T}
|
||||||
# portaudio data comes in interleaved, so we'll end up transposing
|
# portaudio data comes in interleaved, so we'll end up transposing
|
||||||
# it back and forth to julia column-major
|
# it back and forth to julia column-major
|
||||||
chunkbuf = zeros(T, channels, CHUNKSIZE)
|
chunkbuf = zeros(T, channels, CHUNKSIZE)
|
||||||
ringbuf = LockFreeRingBuffer(T, ringbufsize * channels)
|
ringbuf = RingBuffer{T}(channels, ringbufsize)
|
||||||
new(name, stream, chunkbuf, ringbuf, channels)
|
new(name, stream, chunkbuf, ringbuf, channels)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -249,35 +299,31 @@ end
|
||||||
SampledSignals.nchannels(s::Union{PortAudioSink, PortAudioSource}) = s.nchannels
|
SampledSignals.nchannels(s::Union{PortAudioSink, PortAudioSource}) = s.nchannels
|
||||||
SampledSignals.samplerate(s::Union{PortAudioSink, PortAudioSource}) = samplerate(s.stream)
|
SampledSignals.samplerate(s::Union{PortAudioSink, PortAudioSource}) = samplerate(s.stream)
|
||||||
SampledSignals.blocksize(s::Union{PortAudioSink, PortAudioSource}) = s.stream.blocksize
|
SampledSignals.blocksize(s::Union{PortAudioSink, PortAudioSource}) = s.stream.blocksize
|
||||||
Base.eltype{T}(::Union{PortAudioSink{T}, PortAudioSource{T}}) = T
|
eltype(::Union{PortAudioSink{T}, PortAudioSource{T}}) where {T} = T
|
||||||
Base.close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf)
|
close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf)
|
||||||
|
isopen(s::Union{PortAudioSink, PortAudioSource}) = isopen(s.ringbuf)
|
||||||
|
RingBuffers.notifyhandle(s::Union{PortAudioSink, PortAudioSource}) = notifyhandle(s.ringbuf)
|
||||||
|
bufpointer(s::Union{PortAudioSink, PortAudioSource}) = pointer(s.ringbuf)
|
||||||
|
name(s::Union{PortAudioSink, PortAudioSource}) = s.name
|
||||||
|
|
||||||
function Base.show{T <: Union{PortAudioSink, PortAudioSource}}(io::IO, stream::T)
|
function show(io::IO, stream::T) where {T <: Union{PortAudioSink, PortAudioSource}}
|
||||||
println(io, T, "(\"", stream.name, "\")")
|
println(io, T, "(\"", stream.name, "\")")
|
||||||
print(io, nchannels(stream), " channels")
|
print(io, nchannels(stream), " channels")
|
||||||
end
|
end
|
||||||
|
|
||||||
function Base.flush(sink::PortAudioSink)
|
flush(sink::PortAudioSink) = flush(sink.ringbuf)
|
||||||
while nwritable(sink.ringbuf) < length(sink.ringbuf)
|
|
||||||
wait(sink.ringbuf)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount)
|
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount)
|
||||||
nwritten = 0
|
nwritten = 0
|
||||||
while nwritten < framecount
|
while nwritten < framecount
|
||||||
while nwritable(sink.ringbuf) == 0
|
towrite = min(framecount-nwritten, CHUNKSIZE)
|
||||||
wait(sink.ringbuf)
|
|
||||||
end
|
|
||||||
# in 0.4 transpose! throws an error if the range is a UInt
|
|
||||||
writable = div(nwritable(sink.ringbuf), nchannels(sink))
|
|
||||||
towrite = Int(min(writable, CHUNKSIZE, framecount-nwritten))
|
|
||||||
# make a buffer of interleaved samples
|
# make a buffer of interleaved samples
|
||||||
transpose!(view(sink.chunkbuf, :, 1:towrite),
|
transpose!(view(sink.chunkbuf, :, 1:towrite),
|
||||||
view(buf, (1:towrite)+nwritten+frameoffset, :))
|
view(buf, (1:towrite)+nwritten+frameoffset, :))
|
||||||
write(sink.ringbuf, sink.chunkbuf, towrite*nchannels(sink))
|
n = write(sink.ringbuf, sink.chunkbuf, towrite)
|
||||||
|
nwritten += n
|
||||||
nwritten += towrite
|
# break early if the stream is closed
|
||||||
|
n < towrite && break
|
||||||
end
|
end
|
||||||
|
|
||||||
nwritten
|
nwritten
|
||||||
|
@ -286,66 +332,23 @@ end
|
||||||
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameoffset, framecount)
|
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameoffset, framecount)
|
||||||
nread = 0
|
nread = 0
|
||||||
while nread < framecount
|
while nread < framecount
|
||||||
while nreadable(source.ringbuf) == 0
|
toread = min(framecount-nread, CHUNKSIZE)
|
||||||
wait(source.ringbuf)
|
n = read!(source.ringbuf, source.chunkbuf, toread)
|
||||||
end
|
|
||||||
# in 0.4 transpose! throws an error if the range is a UInt
|
|
||||||
readable = div(nreadable(source.ringbuf), nchannels(source))
|
|
||||||
toread = Int(min(readable, CHUNKSIZE, framecount-nread))
|
|
||||||
read!(source.ringbuf, source.chunkbuf, toread*nchannels(source))
|
|
||||||
# de-interleave the samples
|
# de-interleave the samples
|
||||||
transpose!(view(buf, (1:toread)+nread+frameoffset, :),
|
transpose!(view(buf, (1:toread)+nread+frameoffset, :),
|
||||||
view(source.chunkbuf, :, 1:toread))
|
view(source.chunkbuf, :, 1:toread))
|
||||||
|
|
||||||
nread += toread
|
nread += toread
|
||||||
|
# break early if the stream is closed
|
||||||
|
n < toread && break
|
||||||
end
|
end
|
||||||
|
|
||||||
nread
|
nread
|
||||||
end
|
end
|
||||||
|
|
||||||
# This is the callback function that gets called directly in the PortAudio
|
# this is called by the shim process callback to notify that there is new data.
|
||||||
# audio thread, so it's critical that it not interact with the Julia GC
|
# it's run in the audio context so don't do anything besides wake up the
|
||||||
function portaudio_callback{T}(inptr::Ptr{T}, outptr::Ptr{T},
|
# AsyncCondition handle associated with that ring buffer
|
||||||
nframes, timeinfo, flags, userdata::Ptr{CallbackInfo{T}})
|
notifycb(handle) = ccall(:uv_async_send, Cint, (Ptr{Void}, ), handle)
|
||||||
info = unsafe_load(userdata)
|
|
||||||
# if there are no channels, treat it as if we can write as many 0-frame channels as we want
|
|
||||||
framesreadable = info.outchannels > 0 ? div(nreadable(info.outbuf), info.outchannels) : nframes
|
|
||||||
frameswritable = info.inchannels > 0 ? div(nwritable(info.inbuf), info.inchannels) : nframes
|
|
||||||
if info.synced
|
|
||||||
framesreadable = min(framesreadable, frameswritable)
|
|
||||||
frameswritable = framesreadable
|
|
||||||
end
|
|
||||||
towrite = min(frameswritable, nframes) * info.inchannels
|
|
||||||
toread = min(framesreadable, nframes) * info.outchannels
|
|
||||||
|
|
||||||
read!(info.outbuf, outptr, toread)
|
|
||||||
write(info.inbuf, inptr, towrite)
|
|
||||||
|
|
||||||
if framesreadable < nframes
|
|
||||||
outsamples = nframes * info.outchannels
|
|
||||||
# xrun, copy zeros to outbuffer
|
|
||||||
# TODO: send a notification to an error msg ringbuf
|
|
||||||
memset(outptr+sizeof(T)*toread, 0, sizeof(T)*(outsamples-toread))
|
|
||||||
end
|
|
||||||
|
|
||||||
paContinue
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
"""Call the given function and discard stdout and stderr"""
|
|
||||||
function swallow_stderr(f)
|
|
||||||
origerr = STDERR
|
|
||||||
(errread, errwrite) = redirect_stderr()
|
|
||||||
result = f()
|
|
||||||
redirect_stderr(origerr)
|
|
||||||
close(errwrite)
|
|
||||||
close(errread)
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
memset(buf, val, count) = ccall(:memset, Ptr{Void},
|
|
||||||
(Ptr{Void}, Cint, Csize_t),
|
|
||||||
buf, val, count)
|
|
||||||
|
|
||||||
end # module PortAudio
|
end # module PortAudio
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
# Low-level wrappers for Portaudio calls
|
# Low-level wrappers for Portaudio calls
|
||||||
|
|
||||||
# General type aliases
|
# General type aliases
|
||||||
typealias PaTime Cdouble
|
const PaTime = Cdouble
|
||||||
typealias PaError Cint
|
const PaError = Cint
|
||||||
typealias PaSampleFormat Culong
|
const PaSampleFormat = Culong
|
||||||
typealias PaDeviceIndex Cint
|
const PaDeviceIndex = Cint
|
||||||
typealias PaHostApiIndex Cint
|
const PaHostApiIndex = Cint
|
||||||
typealias PaHostApiTypeId Cint
|
const PaHostApiTypeId = Cint
|
||||||
# PaStream is always used as an opaque type, so we're always dealing
|
# PaStream is always used as an opaque type, so we're always dealing
|
||||||
# with the pointer
|
# with the pointer
|
||||||
typealias PaStream Ptr{Void}
|
const PaStream = Ptr{Void}
|
||||||
typealias PaStreamCallback Void
|
const PaStreamCallback = Void
|
||||||
typealias PaStreamFlags Culong
|
const PaStreamFlags = Culong
|
||||||
|
|
||||||
const paNoFlag = PaStreamFlags(0x00)
|
const paNoFlag = PaStreamFlags(0x00)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ const type_to_fmt = Dict{Type, PaSampleFormat}(
|
||||||
UInt8 => 3
|
UInt8 => 3
|
||||||
)
|
)
|
||||||
|
|
||||||
typealias PaStreamCallbackResult Cint
|
const PaStreamCallbackResult = Cint
|
||||||
# Callback return values
|
# Callback return values
|
||||||
const paContinue = PaStreamCallbackResult(0)
|
const paContinue = PaStreamCallbackResult(0)
|
||||||
const paComplete = PaStreamCallbackResult(1)
|
const paComplete = PaStreamCallbackResult(1)
|
||||||
|
@ -69,7 +69,7 @@ end
|
||||||
# all the host APIs on the system by iterating through those values.
|
# all the host APIs on the system by iterating through those values.
|
||||||
|
|
||||||
# PaHostApiTypeId values
|
# PaHostApiTypeId values
|
||||||
const pa_host_api_names = Dict{PaHostApiTypeId, UTF8String}(
|
const pa_host_api_names = Dict{PaHostApiTypeId, String}(
|
||||||
0 => "In Development", # use while developing support for a new host API
|
0 => "In Development", # use while developing support for a new host API
|
||||||
1 => "Direct Sound",
|
1 => "Direct Sound",
|
||||||
2 => "MME",
|
2 => "MME",
|
||||||
|
@ -240,11 +240,11 @@ function handle_status(err::PaError, show_warnings::Bool=true)
|
||||||
if show_warnings
|
if show_warnings
|
||||||
msg = ccall((:Pa_GetErrorText, libportaudio),
|
msg = ccall((:Pa_GetErrorText, libportaudio),
|
||||||
Ptr{Cchar}, (PaError,), err)
|
Ptr{Cchar}, (PaError,), err)
|
||||||
warn("libportaudio: " * bytestring(msg))
|
warn("libportaudio: " * unsafe_string(msg))
|
||||||
end
|
end
|
||||||
elseif err != PA_NO_ERROR
|
elseif err != PA_NO_ERROR
|
||||||
msg = ccall((:Pa_GetErrorText, libportaudio),
|
msg = ccall((:Pa_GetErrorText, libportaudio),
|
||||||
Ptr{Cchar}, (PaError,), err)
|
Ptr{Cchar}, (PaError,), err)
|
||||||
error("libportaudio: " * bytestring(msg))
|
error("libportaudio: " * unsafe_string(msg))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
61
src/pa_shim.jl
Normal file
61
src/pa_shim.jl
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
function init_pa_shim()
|
||||||
|
libdir = joinpath(dirname(@__FILE__), "..", "deps", "usr", "lib")
|
||||||
|
libsuffix = ""
|
||||||
|
basename = "pa_shim"
|
||||||
|
@static if is_linux() && Sys.ARCH == :x86_64
|
||||||
|
libsuffix = "x86_64-linux-gnu"
|
||||||
|
elseif is_linux() && Sys.ARCH == :i686
|
||||||
|
libsuffix = "i686-linux-gnu"
|
||||||
|
elseif is_apple() && Sys.ARCH == :x86_64
|
||||||
|
libsuffix = "x86_64-apple-darwin14"
|
||||||
|
elseif is_windows() && Sys.ARCH == :x86_64
|
||||||
|
libsuffix = "x86_64-w64-mingw32"
|
||||||
|
elseif is_windows() && 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
|
||||||
|
global const libpa_shim = Base.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")
|
||||||
|
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)
|
20
src/suppressor.jl
Normal file
20
src/suppressor.jl
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# while waiting for this PR to get merged: https://github.com/Ismael-VC/Suppressor.jl/pull/12
|
||||||
|
# we'll just include the relevant code here
|
||||||
|
|
||||||
|
macro suppress_err(block)
|
||||||
|
quote
|
||||||
|
if ccall(:jl_generating_output, Cint, ()) == 0
|
||||||
|
ORIGINAL_STDERR = STDERR
|
||||||
|
err_rd, err_wr = redirect_stderr()
|
||||||
|
err_reader = @async readstring(err_rd)
|
||||||
|
end
|
||||||
|
|
||||||
|
value = $(esc(block))
|
||||||
|
|
||||||
|
if ccall(:jl_generating_output, Cint, ()) == 0
|
||||||
|
redirect_stderr(ORIGINAL_STDERR)
|
||||||
|
close(err_wr)
|
||||||
|
end
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
|
@ -1 +1 @@
|
||||||
BaseTestNext
|
TestSetExtensions
|
||||||
|
|
365
test/runtests.jl
365
test/runtests.jl
|
@ -1,180 +1,249 @@
|
||||||
#!/usr/bin/env julia
|
#!/usr/bin/env julia
|
||||||
|
|
||||||
if VERSION >= v"0.5.0-dev+7720"
|
using Base.Test
|
||||||
using Base.Test
|
using TestSetExtensions
|
||||||
else
|
|
||||||
using BaseTestNext
|
|
||||||
end
|
|
||||||
using PortAudio
|
using PortAudio
|
||||||
using SampledSignals
|
using SampledSignals
|
||||||
using RingBuffers
|
using RingBuffers
|
||||||
|
|
||||||
function test_callback(inchans, outchans)
|
# pull in some extra stuff we need to test the callback directly
|
||||||
nframes = Culong(8)
|
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
|
||||||
|
|
||||||
cb = PortAudio.pa_callbacks[Float32]
|
"Setup buffers to test callback behavior"
|
||||||
inbuf = rand(Float32, inchans*nframes) # simulate microphone input
|
function setup_callback(inchans, outchans, nframes, synced)
|
||||||
sourcebuf = LockFreeRingBuffer(Float32, inchans*nframes*8) # the microphone input should end up here
|
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)
|
||||||
|
|
||||||
outbuf = zeros(Float32, outchans*nframes) # this is where the output should go
|
# pass NULL for i/o we're not using
|
||||||
sinkbuf = LockFreeRingBuffer(Float32, outchans*nframes*8) # the callback should copy this to outbuf
|
info = pa_shim_info_t(
|
||||||
|
inchans > 0 ? pointer(sourcebuf) : C_NULL,
|
||||||
# 2 input channels, 3 output channels
|
outchans > 0 ? pointer(sinkbuf) : C_NULL,
|
||||||
info = PortAudio.CallbackInfo(inchans, sourcebuf, outchans, sinkbuf, true)
|
pointer(errbuf),
|
||||||
|
synced, notifycb_c,
|
||||||
# handle any conversions here so they don't mess with the allocation
|
inchans > 0 ? notifyhandle(sourcebuf) : C_NULL,
|
||||||
# the seemingly-redundant type specifiers avoid some allocation during the ccall.
|
outchans > 0 ? notifyhandle(sinkbuf) : C_NULL,
|
||||||
# might be due to https://github.com/JuliaLang/julia/issues/15276
|
notifyhandle(errbuf)
|
||||||
inptr::Ptr{Float32} = Ptr{Float32}(pointer(inbuf))
|
)
|
||||||
outptr::Ptr{Float32} = Ptr{Float32}(pointer(outbuf))
|
|
||||||
flags = Culong(0)
|
flags = Culong(0)
|
||||||
infoptr::Ptr{PortAudio.CallbackInfo{Float32}} = Ptr{PortAudio.CallbackInfo{Float32}}(pointer_from_objref(info))
|
|
||||||
|
|
||||||
testin = zeros(Float32, inchans*nframes)
|
cb_input = rand(Float32, inchans, nframes) # simulate microphone input
|
||||||
testout = rand(Float32, outchans*nframes)
|
cb_output = rand(Float32, outchans, nframes) # this is where the output should go
|
||||||
write(sinkbuf, testout) # fill the output ringbuffer
|
|
||||||
ret = ccall(cb, Cint,
|
|
||||||
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}),
|
|
||||||
inptr, outptr, nframes, C_NULL, flags, infoptr)
|
|
||||||
@test ret === PortAudio.paContinue
|
|
||||||
@test outbuf == testout
|
|
||||||
read!(sourcebuf, testin)
|
|
||||||
@test inbuf == testin
|
|
||||||
|
|
||||||
|
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
|
if outchans > 0
|
||||||
underfill = 3 # should be less than nframes
|
testout = rand(Float32, outchans, nframes) # generate some test data to play
|
||||||
testout = rand(Float32, outchans*underfill)
|
|
||||||
write(sinkbuf, testout) # underfill the output ringbuffer
|
|
||||||
# call again (partial underrun)
|
|
||||||
ret = ccall(cb, Cint,
|
|
||||||
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}),
|
|
||||||
inptr, outptr, nframes, C_NULL, flags, infoptr)
|
|
||||||
@test ret === PortAudio.paContinue
|
|
||||||
@test outbuf[1:outchans*underfill] == testout
|
|
||||||
@test outbuf[outchans*underfill+1:outchans*nframes] == zeros(Float32, (nframes-underfill)*outchans)
|
|
||||||
@test nreadable(sourcebuf) == inchans*underfill
|
|
||||||
@test read!(sourcebuf, testin) == inchans*underfill
|
|
||||||
@test testin[1:inchans*underfill] == inbuf[1:inchans*underfill]
|
|
||||||
|
|
||||||
# call again (total underrun)
|
|
||||||
ret = ccall(cb, Cint,
|
|
||||||
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}),
|
|
||||||
inptr, outptr, nframes, C_NULL, flags, infoptr)
|
|
||||||
@test ret === PortAudio.paContinue
|
|
||||||
@test outbuf == zeros(Float32, outchans*nframes)
|
|
||||||
@test nreadable(sourcebuf) == 0
|
|
||||||
|
|
||||||
write(sinkbuf, testout) # fill the output ringbuffer
|
write(sinkbuf, testout) # fill the output ringbuffer
|
||||||
# test allocation
|
end
|
||||||
alloc = @allocated ccall(cb, Cint,
|
@test process() == PortAudio.paContinue
|
||||||
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}),
|
if outchans > 0
|
||||||
inptr, outptr, nframes, C_NULL, flags, infoptr)
|
# testout -> sinkbuf -> cb_output
|
||||||
@test alloc == 0
|
@test cb_output == testout
|
||||||
# now test allocation in underrun state
|
end
|
||||||
alloc = @allocated ccall(cb, Cint,
|
if inchans > 0
|
||||||
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}),
|
# cb_input -> sourcebuf
|
||||||
inptr, outptr, nframes, C_NULL, flags, infoptr)
|
@test read(sourcebuf, nframes) == cb_input
|
||||||
@test alloc == 0
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# these test are currently set up to run on OSX
|
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
|
||||||
|
|
||||||
@testset "PortAudio Tests" begin
|
# the first time it should half-fill the input ring buffer
|
||||||
devs = PortAudio.devices()
|
if outchans > 0
|
||||||
i = findfirst(d -> d.maxinchans > 0, devs)
|
testout = rand(Float32, outchans, nframes)
|
||||||
indev = i > 0 ? devs[i] : nothing
|
write(sinkbuf, testout)
|
||||||
i = findfirst(d -> d.maxoutchans > 0, devs)
|
end
|
||||||
outdev = i > 0 ? devs[i] : nothing
|
@test framesreadable(sourcebuf) == 0
|
||||||
i = findfirst(d -> d.maxoutchans > 0 && d.maxinchans > 0, devs)
|
outchans > 0 && @test frameswritable(sinkbuf) == nframes
|
||||||
duplexdev = i > 0 ? devs[i] : nothing
|
@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 ExtendedTestSet "PortAudio Tests" begin
|
||||||
@testset "Reports version" begin
|
@testset "Reports version" begin
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
PortAudio.versioninfo(io)
|
PortAudio.versioninfo(io)
|
||||||
result = takebuf_string(io)
|
result = split(String(take!((io))), "\n")
|
||||||
# make sure this is the same version I tested with
|
# make sure this is the same version I tested with
|
||||||
@test result ==
|
@test startswith(result[1], "PortAudio V19")
|
||||||
"""PortAudio V19-devel (built Aug 6 2014 17:54:39)
|
|
||||||
Version Number: 1899
|
|
||||||
"""
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "PortAudio Callback works for duplex stream" begin
|
@testset "using correct shim version" begin
|
||||||
test_callback(2, 3)
|
@test PortAudio.shimhash() == "87021557a9f999545828eb11e4ebad2cd278b734dd91a8bd3faf05c89912cf80"
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "Callback works with input-only stream" begin
|
@testset "Basic callback functionality" begin
|
||||||
test_callback(2, 0)
|
@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
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "Callback works with output-only stream" begin
|
@testset "Ouput underflow" begin
|
||||||
test_callback(0, 2)
|
@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)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@testset "Open Default Device" begin
|
@testset "Input overflow" begin
|
||||||
println("Recording...")
|
@testset "overflow duplex (nosync)" begin
|
||||||
stream = PortAudioStream(2, 0)
|
test_callback_overflow(2, 3, false)
|
||||||
buf = read(stream, 5s)
|
end
|
||||||
close(stream)
|
@testset "overflow input-only (nosync)" begin
|
||||||
@test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
test_callback_overflow(2, 0, false)
|
||||||
println("Playing back recording...")
|
end
|
||||||
stream = PortAudioStream(0, 2)
|
@testset "overflow duplex (sync)" begin
|
||||||
write(stream, buf)
|
test_callback_overflow(2, 3, true)
|
||||||
println("flushing...")
|
end
|
||||||
flush(stream)
|
@testset "overflow input-only (sync)" begin
|
||||||
close(stream)
|
test_callback_overflow(2, 0, true)
|
||||||
println("Testing pass-through")
|
end
|
||||||
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("Built-in Microph", "Built-in Output")
|
|
||||||
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 takebuf_string(io) == """
|
|
||||||
PortAudio.PortAudioStream{Float32}
|
|
||||||
Samplerate: 48000.0Hz
|
|
||||||
Buffer Size: 4096 frames
|
|
||||||
2 channel sink: "Built-in Output"
|
|
||||||
2 channel source: "Built-in Microph\""""
|
|
||||||
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
|
end
|
||||||
end
|
end
|
||||||
|
|
85
test/runtests_local.jl
Normal file
85
test/runtests_local.jl
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
# 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 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
|
||||||
|
|
||||||
|
@testset ExtendedTestSet "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 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
|
||||||
|
end
|
Loading…
Reference in a new issue