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
*.so
*.o
deps/deps.jl
*.wav
*.flac
*.cov
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
sudo: required
julia:
- 0.4
- 0.5
- 0.6
notifications:
email: false
script:
# we can't actually run on travis, so just make sure it's installable
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); using PortAudio'
- 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,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
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)
```
## 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
Compat 0.8.8
julia 0.6-
BinDeps
Devectorize
SampledSignals 0.3.0
RingBuffers 0.1.0
RingBuffers 1.0.0
@osx Homebrew
@windows WinRPM

View file

@ -1,9 +1,7 @@
environment:
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/x64/0.4/julia-0.4-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"
- 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.6/julia-0.6-latest-win64.exe"
notifications:
- provider: Email
@ -27,5 +25,4 @@ build_script:
Pkg.clone(pwd(), \"PortAudio\"); Pkg.build(\"PortAudio\")"
test_script:
# can't actually run the test, so just make sure it's installable
- C:\projects\julia\bin\julia -e "using PortAudio"
- C:\projects\julia\bin\julia --check-bounds=yes -e "Pkg.test(\"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
using SampledSignals
using Devectorize
using RingBuffers
using Compat
import Compat: UTF8String, view
#using Suppressor
import Base: eltype, show
import Base: close, isopen
import Base: read, read!, write, flush
# Get binary dependencies loaded from BinDeps
include( "../deps/deps.jl")
include("../deps/deps.jl")
include("suppressor.jl")
include("pa_shim.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
# These sizes are all in frames
# the block size is what we request from portaudio if no blocksize is given.
# The ringbuffer and pre-fill will be twice the blocksize
const DEFAULT_BLOCKSIZE=4096
# 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
# 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.
const CHUNKSIZE=128
function __init__()
# initialize PortAudio on module load
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
# ringbuffer to receive errors from the audio processing thread
const ERR_BUFSIZE=512
function versioninfo(io::IO=STDOUT)
println(io, Pa_GetVersionText())
println(io, "Version Number: ", Pa_GetVersion())
println(io, "Version: ", Pa_GetVersion())
println(io, "Shim Source Hash: ", shimhash()[1:10])
end
type PortAudioDevice
name::UTF8String
hostapi::UTF8String
name::String
hostapi::String
maxinchans::Int
maxoutchans::Int
defaultsamplerate::Float64
@ -71,23 +72,9 @@ end
# not for external use, used in error message printing
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)
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
##################
# PortAudioStream
##################
type PortAudioStream{T}
samplerate::Float64
@ -95,12 +82,13 @@ type PortAudioStream{T}
stream::PaStream
sink # 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
# constructor below
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
inchans, outchans, sr, blocksize, synced)
function PortAudioStream{T}(indev::PortAudioDevice, outdev::PortAudioDevice,
inchans, outchans, sr, blocksize, synced) where {T}
inchans = inchans == -1 ? indev.maxinchans : inchans
outchans = outchans == -1 ? outdev.maxoutchans : outchans
inparams = (inchans == 0) ?
@ -113,23 +101,55 @@ type PortAudioStream{T}
finalizer(this, close)
this.sink = PortAudioSink{T}(outdev.name, this, outchans, 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
# we've got a synchronized duplex stream. initialize with the output buffer full
write(this.sink, SampleBuf(zeros(T, blocksize*2, outchans), sr))
end
this.bufinfo = CallbackInfo(inchans, this.source.ringbuf,
outchans, this.sink.ringbuf, synced)
this.stream = swallow_stderr() do
Pa_OpenStream(inparams, outparams, float(sr), blocksize,
paNoFlag, pa_callbacks[T], fieldptr(this, :bufinfo))
end
# pass NULL for input/output we're not using
this.bufinfo = pa_shim_info_t(
inchans > 0 ? bufpointer(this.source) : C_NULL,
outchans > 0 ? bufpointer(this.sink) : C_NULL,
pointer(this.errbuf),
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)
@async handle_errors(this)
this
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
# end up calling
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
@ -191,7 +211,7 @@ function PortAudioStream(inchans=2, outchans=2; kwargs...)
PortAudioStream(indevice, outdevice, inchans, outchans; kwargs...)
end
function Base.close(stream::PortAudioStream)
function close(stream::PortAudioStream)
if stream.stream != C_NULL
Pa_StopStream(stream.stream)
Pa_CloseStream(stream.stream)
@ -203,44 +223,74 @@ function Base.close(stream::PortAudioStream)
nothing
end
Base.isopen(stream::PortAudioStream) = stream.stream != C_NULL
isopen(stream::PortAudioStream) = stream.stream != C_NULL
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...)
Base.read!(stream::PortAudioStream, args...) = read!(stream.source, args...)
Base.write(stream::PortAudioStream, args...) = write(stream.sink, args...)
Base.write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...)
Base.flush(stream::PortAudioStream) = flush(stream.sink)
read(stream::PortAudioStream, args...) = read(stream.source, args...)
read!(stream::PortAudioStream, args...) = read!(stream.source, args...)
write(stream::PortAudioStream, args...) = write(stream.sink, args...)
write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...)
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, " Samplerate: ", samplerate(stream), "Hz")
print(io, " Buffer Size: ", stream.blocksize, " frames")
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
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
"""
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
for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
(:PortAudioSource, :SampleSource))
@eval type $TypeName{T} <: $Super
name::UTF8String
name::String
stream::PortAudioStream{T}
chunkbuf::Array{T, 2}
ringbuf::LockFreeRingBuffer{T}
ringbuf::RingBuffer{T}
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
# it back and forth to julia column-major
chunkbuf = zeros(T, channels, CHUNKSIZE)
ringbuf = LockFreeRingBuffer(T, ringbufsize * channels)
ringbuf = RingBuffer{T}(channels, ringbufsize)
new(name, stream, chunkbuf, ringbuf, channels)
end
end
@ -249,35 +299,31 @@ end
SampledSignals.nchannels(s::Union{PortAudioSink, PortAudioSource}) = s.nchannels
SampledSignals.samplerate(s::Union{PortAudioSink, PortAudioSource}) = samplerate(s.stream)
SampledSignals.blocksize(s::Union{PortAudioSink, PortAudioSource}) = s.stream.blocksize
Base.eltype{T}(::Union{PortAudioSink{T}, PortAudioSource{T}}) = T
Base.close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf)
eltype(::Union{PortAudioSink{T}, PortAudioSource{T}}) where {T} = T
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, "\")")
print(io, nchannels(stream), " channels")
end
function Base.flush(sink::PortAudioSink)
while nwritable(sink.ringbuf) < length(sink.ringbuf)
wait(sink.ringbuf)
end
end
flush(sink::PortAudioSink) = flush(sink.ringbuf)
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount)
nwritten = 0
while nwritten < framecount
while nwritable(sink.ringbuf) == 0
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))
towrite = min(framecount-nwritten, CHUNKSIZE)
# make a buffer of interleaved samples
transpose!(view(sink.chunkbuf, :, 1:towrite),
view(buf, (1:towrite)+nwritten+frameoffset, :))
write(sink.ringbuf, sink.chunkbuf, towrite*nchannels(sink))
nwritten += towrite
n = write(sink.ringbuf, sink.chunkbuf, towrite)
nwritten += n
# break early if the stream is closed
n < towrite && break
end
nwritten
@ -286,66 +332,23 @@ end
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameoffset, framecount)
nread = 0
while nread < framecount
while nreadable(source.ringbuf) == 0
wait(source.ringbuf)
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))
toread = min(framecount-nread, CHUNKSIZE)
n = read!(source.ringbuf, source.chunkbuf, toread)
# de-interleave the samples
transpose!(view(buf, (1:toread)+nread+frameoffset, :),
view(source.chunkbuf, :, 1:toread))
nread += toread
# break early if the stream is closed
n < toread && break
end
nread
end
# This is the callback function that gets called directly in the PortAudio
# audio thread, so it's critical that it not interact with the Julia GC
function portaudio_callback{T}(inptr::Ptr{T}, outptr::Ptr{T},
nframes, timeinfo, flags, userdata::Ptr{CallbackInfo{T}})
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)
# this is called by the shim process callback to notify that there is new data.
# it's run in the audio context so don't do anything besides wake up the
# AsyncCondition handle associated with that ring buffer
notifycb(handle) = ccall(:uv_async_send, Cint, (Ptr{Void}, ), handle)
end # module PortAudio

View file

@ -1,17 +1,17 @@
# Low-level wrappers for Portaudio calls
# General type aliases
typealias PaTime Cdouble
typealias PaError Cint
typealias PaSampleFormat Culong
typealias PaDeviceIndex Cint
typealias PaHostApiIndex Cint
typealias PaHostApiTypeId Cint
const PaTime = Cdouble
const PaError = Cint
const PaSampleFormat = Culong
const PaDeviceIndex = Cint
const PaHostApiIndex = Cint
const PaHostApiTypeId = Cint
# PaStream is always used as an opaque type, so we're always dealing
# with the pointer
typealias PaStream Ptr{Void}
typealias PaStreamCallback Void
typealias PaStreamFlags Culong
const PaStream = Ptr{Void}
const PaStreamCallback = Void
const PaStreamFlags = Culong
const paNoFlag = PaStreamFlags(0x00)
@ -37,7 +37,7 @@ const type_to_fmt = Dict{Type, PaSampleFormat}(
UInt8 => 3
)
typealias PaStreamCallbackResult Cint
const PaStreamCallbackResult = Cint
# Callback return values
const paContinue = PaStreamCallbackResult(0)
const paComplete = PaStreamCallbackResult(1)
@ -69,7 +69,7 @@ end
# all the host APIs on the system by iterating through those 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
1 => "Direct Sound",
2 => "MME",
@ -240,11 +240,11 @@ function handle_status(err::PaError, show_warnings::Bool=true)
if show_warnings
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
warn("libportaudio: " * bytestring(msg))
warn("libportaudio: " * unsafe_string(msg))
end
elseif err != PA_NO_ERROR
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
error("libportaudio: " * bytestring(msg))
error("libportaudio: " * unsafe_string(msg))
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
if VERSION >= v"0.5.0-dev+7720"
using Base.Test
else
using BaseTestNext
end
using Base.Test
using TestSetExtensions
using PortAudio
using SampledSignals
using RingBuffers
function test_callback(inchans, outchans)
nframes = Culong(8)
# pull in some extra stuff we need to test the callback directly
using PortAudio: notifyhandle, notifycb_c, shim_processcb_c
using PortAudio: pa_shim_errmsg_t, pa_shim_info_t
using PortAudio: PA_SHIM_ERRMSG_ERR_OVERFLOW, PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW
cb = PortAudio.pa_callbacks[Float32]
inbuf = rand(Float32, inchans*nframes) # simulate microphone input
sourcebuf = LockFreeRingBuffer(Float32, inchans*nframes*8) # the microphone input should end up here
"Setup buffers to test callback behavior"
function setup_callback(inchans, outchans, nframes, synced)
sourcebuf = RingBuffer{Float32}(inchans, nframes*2) # the microphone input should end up here
sinkbuf = RingBuffer{Float32}(outchans, nframes*2) # the callback should copy this to cb_output
errbuf = RingBuffer{pa_shim_errmsg_t}(1, 8)
outbuf = zeros(Float32, outchans*nframes) # this is where the output should go
sinkbuf = LockFreeRingBuffer(Float32, outchans*nframes*8) # the callback should copy this to outbuf
# 2 input channels, 3 output channels
info = PortAudio.CallbackInfo(inchans, sourcebuf, outchans, sinkbuf, true)
# handle any conversions here so they don't mess with the allocation
# the seemingly-redundant type specifiers avoid some allocation during the ccall.
# might be due to https://github.com/JuliaLang/julia/issues/15276
inptr::Ptr{Float32} = Ptr{Float32}(pointer(inbuf))
outptr::Ptr{Float32} = Ptr{Float32}(pointer(outbuf))
# pass NULL for i/o we're not using
info = pa_shim_info_t(
inchans > 0 ? pointer(sourcebuf) : C_NULL,
outchans > 0 ? pointer(sinkbuf) : C_NULL,
pointer(errbuf),
synced, notifycb_c,
inchans > 0 ? notifyhandle(sourcebuf) : C_NULL,
outchans > 0 ? notifyhandle(sinkbuf) : C_NULL,
notifyhandle(errbuf)
)
flags = Culong(0)
infoptr::Ptr{PortAudio.CallbackInfo{Float32}} = Ptr{PortAudio.CallbackInfo{Float32}}(pointer_from_objref(info))
testin = zeros(Float32, inchans*nframes)
testout = rand(Float32, outchans*nframes)
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
cb_input = rand(Float32, inchans, nframes) # simulate microphone input
cb_output = rand(Float32, outchans, nframes) # this is where the output should go
function processfunc()
ccall(shim_processcb_c, Cint,
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{Void}),
cb_input, cb_output, nframes, C_NULL, flags, pointer_from_objref(info))
end
(sourcebuf, sinkbuf, errbuf, cb_input, cb_output, processfunc)
end
function test_callback(inchans, outchans, synced)
nframes = 8
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
if outchans > 0
underfill = 3 # should be less than nframes
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
testout = rand(Float32, outchans, nframes) # generate some test data to play
write(sinkbuf, testout) # fill the output ringbuffer
# test allocation
alloc = @allocated ccall(cb, Cint,
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}),
inptr, outptr, nframes, C_NULL, flags, infoptr)
@test alloc == 0
# now test allocation in underrun state
alloc = @allocated ccall(cb, Cint,
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}),
inptr, outptr, nframes, C_NULL, flags, infoptr)
@test alloc == 0
end
@test process() == PortAudio.paContinue
if outchans > 0
# testout -> sinkbuf -> cb_output
@test cb_output == testout
end
if inchans > 0
# cb_input -> sourcebuf
@test read(sourcebuf, nframes) == cb_input
end
@test framesreadable(errbuf) == 0
end
"""
test_callback_underflow(inchans, outchans; nframes=8, underfill=3, synced=false)
Test that the callback works on underflow conditions. underfill is the numer of
frames we feed in, which should be less than nframes.
"""
function test_callback_underflow(inchans, outchans, synced)
nframes = 8
underfill = 3 # must be less than nframes
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
outchans > 0 || error("Can't test underflow with no output")
testout = rand(Float32, outchans, underfill)
write(sinkbuf, testout) # underfill the output ringbuffer
# call callback (partial underflow)
@test process() == PortAudio.paContinue
@test cb_output[:, 1:underfill] == testout
@test cb_output[:, (underfill+1):nframes] == zeros(Float32, outchans, (nframes-underfill))
errs = readavailable(errbuf)
if inchans > 0
received = readavailable(sourcebuf)
if synced
@test size(received, 2) == underfill
@test received == cb_input[:, 1:underfill]
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test size(received, 2) == nframes
@test received == cb_input
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
# call again (total underflow)
@test process() == PortAudio.paContinue
@test cb_output == zeros(Float32, outchans, nframes)
errs = readavailable(errbuf)
if inchans > 0
received = readavailable(sourcebuf)
if synced
@test size(received, 2) == 0
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test size(received, 2) == nframes
@test received == cb_input
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
end
# 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
devs = PortAudio.devices()
i = findfirst(d -> d.maxinchans > 0, devs)
indev = i > 0 ? devs[i] : nothing
i = findfirst(d -> d.maxoutchans > 0, devs)
outdev = i > 0 ? devs[i] : nothing
i = findfirst(d -> d.maxoutchans > 0 && d.maxinchans > 0, devs)
duplexdev = i > 0 ? devs[i] : nothing
# the first time it should half-fill the input ring buffer
if outchans > 0
testout = rand(Float32, outchans, nframes)
write(sinkbuf, testout)
end
@test framesreadable(sourcebuf) == 0
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(errbuf) == 0
@test framesreadable(sourcebuf) == nframes
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
# now run the process func again to completely fill the input ring buffer
outchans > 0 && write(sinkbuf, testout)
@test framesreadable(sourcebuf) == nframes
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(errbuf) == 0
@test framesreadable(sourcebuf) == nframes*2
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
# now this time the process func should overflow the input buffer
outchans > 0 && write(sinkbuf, testout)
@test framesreadable(sourcebuf) == nframes*2
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(sourcebuf) == nframes*2
errs = readavailable(errbuf)
if outchans > 0
if synced
# if input and output are synced, thec callback didn't pull from
# the output ringbuf
@test frameswritable(sinkbuf) == nframes
@test cb_output == zeros(Float32, outchans, nframes)
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test frameswritable(sinkbuf) == nframes*2
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
end
end
@testset ExtendedTestSet "PortAudio Tests" begin
@testset "Reports version" begin
io = IOBuffer()
PortAudio.versioninfo(io)
result = takebuf_string(io)
result = split(String(take!((io))), "\n")
# make sure this is the same version I tested with
@test result ==
"""PortAudio V19-devel (built Aug 6 2014 17:54:39)
Version Number: 1899
"""
@test startswith(result[1], "PortAudio V19")
end
@testset "PortAudio Callback works for duplex stream" begin
test_callback(2, 3)
@testset "using correct shim version" begin
@test PortAudio.shimhash() == "87021557a9f999545828eb11e4ebad2cd278b734dd91a8bd3faf05c89912cf80"
end
@testset "Callback works with input-only stream" begin
test_callback(2, 0)
@testset "Basic callback functionality" begin
@testset "basic duplex (no sync)" begin
test_callback(2, 3, false)
end
@testset "basic input-only (no sync)" begin
test_callback(2, 0, false)
end
@testset "basic output-only (no sync)" begin
test_callback(0, 2, false)
end
@testset "basic no input or output (no sync)" begin
test_callback(0, 0, false)
end
@testset "basic duplex (sync)" begin
test_callback(2, 3, true)
end
@testset "basic input-only (sync)" begin
test_callback(2, 0, true)
end
@testset "basic output-only (sync)" begin
test_callback(0, 2, true)
end
@testset "basic no input or output (sync)" begin
test_callback(0, 0, true)
end
end
@testset "Callback works with output-only stream" begin
test_callback(0, 2)
@testset "Ouput underflow" begin
@testset "underflow duplex (nosync)" begin
test_callback_underflow(2, 3, false)
end
@testset "underflow output-only (nosync)" begin
test_callback_underflow(0, 3, false)
end
@testset "underflow duplex (sync)" begin
test_callback_underflow(2, 3, true)
end
@testset "underflow output-only (sync)" begin
test_callback_underflow(0, 3, true)
end
end
@testset "Open Default Device" begin
println("Recording...")
stream = PortAudioStream(2, 0)
buf = read(stream, 5s)
close(stream)
@test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
println("Playing back recording...")
stream = PortAudioStream(0, 2)
write(stream, buf)
println("flushing...")
flush(stream)
close(stream)
println("Testing pass-through")
stream = PortAudioStream(2, 2)
write(stream, stream, 5s)
flush(stream)
close(stream)
println("done")
@testset "Input overflow" begin
@testset "overflow duplex (nosync)" begin
test_callback_overflow(2, 3, false)
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)
@testset "overflow input-only (nosync)" begin
test_callback_overflow(2, 0, false)
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)
@testset "overflow duplex (sync)" begin
test_callback_overflow(2, 3, true)
end
@testset "Error on wrong name" begin
@test_throws ErrorException PortAudioStream("foobarbaz")
@testset "overflow input-only (sync)" begin
test_callback_overflow(2, 0, true)
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

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