Merge pull request #9 from JuliaAudio/crosscompile

Switches to using native extension for ringbuffer exchange. fixes #6.
This commit is contained in:
Spencer Russell 2017-05-19 22:22:10 -04:00 committed by GitHub
commit 5ea61ea30e
26 changed files with 2042 additions and 308 deletions

4
.gitignore vendored
View file

@ -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

View file

@ -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")'

View file

@ -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ó.

View file

@ -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`.

View file

@ -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

View file

@ -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
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

@ -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

View file

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

View file

@ -1 +1 @@
BaseTestNext TestSetExtensions

View file

@ -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
else using TestSetExtensions
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)
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 end
@testset "Samplerate-converting writing" begin @testset "overflow input-only (nosync)" begin
stream = PortAudioStream() test_callback_overflow(2, 0, false)
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 end
@testset "Open Device by name" begin @testset "overflow duplex (sync)" begin
stream = PortAudioStream("Built-in Microph", "Built-in Output") test_callback_overflow(2, 3, true)
buf = read(stream, 0.001s) end
@test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source)) @testset "overflow input-only (sync)" begin
write(stream, buf) test_callback_overflow(2, 0, true)
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 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
View 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