Merge pull request #40 from JuliaAudio/nocallback

Julia 1.0, Artifacts, JLLs, no ringbuffers
This commit is contained in:
Spencer Russell 2020-02-17 12:06:45 -06:00 committed by GitHub
commit 25993bce0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 397 additions and 2051 deletions

31
.appveyor.yml Normal file
View file

@ -0,0 +1,31 @@
# Documentation: https://github.com/JuliaCI/Appveyor.jl
environment:
matrix:
- julia_version: 1
- julia_version: nightly
platform:
- x86
- x64
matrix:
allow_failures:
- julia_version: nightly
branches:
only:
- master
- /release-.*/
notifications:
- provider: Email
on_build_success: false
on_build_failure: false
on_build_status_changed: true
install:
- ps: iex ((new-object net.webclient).DownloadString("https://raw.githubusercontent.com/JuliaCI/Appveyor.jl/version-1/bin/install.ps1"))
build_script:
- echo "%JL_BUILD_SCRIPT%"
- C:\julia\bin\julia -e "%JL_BUILD_SCRIPT%"
test_script:
- echo "%JL_TEST_SCRIPT%"
- C:\julia\bin\julia -e "%JL_TEST_SCRIPT%"
on_success:
- echo "%JL_CODECOV_SCRIPT%"
- C:\julia\bin\julia -e "%JL_CODECOV_SCRIPT%"

1
.gitignore vendored
View file

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

View file

@ -3,11 +3,14 @@ language: julia
os:
- linux
- osx
sudo: required
julia:
- 0.6
- 1
- nightly
matrix:
allow_failures:
- julia: nightly
fast_finish: true
notifications:
email: false
script:
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); Pkg.test("PortAudio")'
email: true
after_success:
- julia -e 'using Pkg; Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())'

20
Project.toml Normal file
View file

@ -0,0 +1,20 @@
name = "PortAudio"
uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d"
repo = "https://github.com/JuliaAudio/PortAudio.jl.git"
version = "1.1.0"
[deps]
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
libportaudio_jll = "2d7b7beb-0762-5160-978e-1ab83a1e8a31"
[compat]
julia = "1.3"
[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
TestSetExtensions = "98d24dd4-01ad-11ea-1b02-c9a08f80db04"
[targets]
test = ["Test", "TestSetExtensions"]

View file

@ -4,7 +4,6 @@ PortAudio.jl
[![Build Status](https://travis-ci.org/JuliaAudio/PortAudio.jl.svg?branch=master)](https://travis-ci.org/JuliaAudio/PortAudio.jl)
[![Build status](https://ci.appveyor.com/api/projects/status/6x1ha7uvrnel060g/branch/master?svg=true)](https://ci.appveyor.com/project/ssfrr/portaudio-jl/branch/master)
PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which gives cross-platform access to audio devices. It is compatible with the types defined in [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl). It provides a `PortAudioStream` type, which can be read from and written to.
## Opening a stream
@ -52,13 +51,26 @@ PortAudio.jl also provides convenience wrappers around the `PortAudioStream` typ
```julia
stream = PortAudioStream(2, 2)
write(stream, stream)
try
# cancel with Ctrl-C
write(stream, stream)
finally
close(stream)
end
```
### Use `do` syntax to auto-close the stream
```julia
PortAudioStream(2, 2) do stream
write(stream, stream)
end
```
### Open your built-in microphone and speaker by name
```julia
stream = PortAudioStream("Built-in Microph", "Built-in Output")
write(stream, stream)
PortAudioStream("Built-in Microph", "Built-in Output") do stream
write(stream, stream)
end
```
### Record 10 seconds of audio and save to an ogg file
@ -78,6 +90,8 @@ julia> buf = read(stream, 10s)
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
julia> close(stream)
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
```

View file

@ -1,6 +0,0 @@
julia 0.6.0-dev.2746
BinDeps
SampledSignals 0.3.0
RingBuffers 1.0.0
@osx Homebrew
@windows WinRPM

View file

@ -1,28 +0,0 @@
environment:
matrix:
- 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
on_build_success: false
on_build_failure: false
on_build_status_changed: false
install:
- ps: "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12"
# Download most recent Julia Windows binary
- ps: (new-object net.webclient).DownloadFile(
$env:JULIA_URL,
"C:\projects\julia-binary.exe")
# Run installer silently, output to C:\projects\julia
- C:\projects\julia-binary.exe /S /D=C:\projects\julia
build_script:
# Need to convert from shallow to complete for Pkg.clone to work
- IF EXIST .git\shallow (git fetch --unshallow)
- C:\projects\julia\bin\julia -e "versioninfo();
Pkg.clone(pwd(), \"PortAudio\"); Pkg.build(\"PortAudio\")"
test_script:
- C:\projects\julia\bin\julia --check-bounds=yes -e "Pkg.test(\"PortAudio\")"

25
deps/build.jl vendored
View file

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

80
deps/src/Makefile vendored
View file

@ -1,80 +0,0 @@
# 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
View file

@ -1,54 +0,0 @@
#!/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 "================================"

View file

@ -1,8 +0,0 @@
#!/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

View file

@ -1,13 +0,0 @@
#!/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
View file

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

1225
deps/src/portaudio.h vendored

File diff suppressed because it is too large Load diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -25,11 +25,11 @@ function printmeter(metersize, signal, peak)
blankchars = max(0, peakpos-meterchars-1)
for position in 1:meterchars
print_with_color(barcolor(metersize, position), ">")
printstyled(">", color=barcolor(metersize, position))
end
print(" " ^ blankchars)
print_with_color(barcolor(metersize, peakpos), "|")
printstyled("|", color=barcolor(metersize, peakpos))
print(" " ^ (metersize - peakpos))
end

View file

@ -3,7 +3,7 @@
module SpectrumExample
using GR, PortAudio, SampledSignals
using GR, PortAudio, SampledSignals, FFTW
const N = 1024
const stream = PortAudioStream(1, 0, blocksize=N)

View file

@ -0,0 +1,67 @@
using Makie
using PortAudio
using DSP
"""
Slide the values in the given matrix to the right by 1.
The rightmosts column is discarded and the leftmost column is
left alone.
"""
function shift1!(buf::AbstractMatrix)
for col in size(buf,2):-1:2
@. buf[:, col] = buf[:, col-1]
end
end
"""
takes a block of audio, FFT it, and write it to the beginning of the buffer
"""
function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan)
readbuf .*= win
A_mul_B!(fftbuf, fftplan, readbuf)
shift1!(dispbuf)
@. dispbuf[end:-1:1,1] = log(clamp(abs(fftbuf[1:D]), 0.0001, Inf))
end
function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
read!(src, buf)
for dispbuf in dispbufs
processbuf!(buf, win, dispbuf, fftbuf, fftplan)
end
end
N = 1024 # size of audio read
N2 = N÷2+1 # size of rfft output
D = 200 # number of bins to display
M = 200 # amount of history to keep
src = PortAudioStream(1, 2, blocksize=N)
buf = Array{Float32}(N) # buffer for reading
fftplan = plan_rfft(buf; flags=FFTW.EXHAUSTIVE)
fftbuf = Array{Complex{Float32}}(N2) # destination buf for FFT
dispbufs = [zeros(Float32, D, M) for i in 1:5, j in 1:5] # STFT bufs
win = gaussian(N, 0.125)
scene = Scene(resolution=(1000,1000))
#pre-fill the display buffer so we can do a reasonable colormap
for _ in 1:M
processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
end
heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf
i = ibuf[1]
buf = ibuf[2]
# some function of the 2D index and the value
heatmap(buf, offset=(i[2]*size(buf, 2), i[1]*size(buf, 1)))
end
center!(scene)
while isopen(scene[:screen])
processblock!(src, buf, dispbufs, fftbuf, fftplan)
for (hm, db) in zip(heatmaps, dispbufs)
hm[:heatmap] = db
end
render_frame(scene)
end

View file

@ -0,0 +1,38 @@
using Makie, GeometryTypes
using PortAudio
N = 1024 # size of audio read
N2 = N÷2+1 # size of rfft output
D = 200 # number of bins to display
M = 100 # number of lines to draw
S = 0.5 # motion speed of lines
src = PortAudioStream(1, 2, blocksize=N)
buf = Array{Float32}(N)
fftbuf = Array{Complex{Float32}}(N2)
magbuf = Array{Float32}(N2)
fftplan = plan_rfft(buf; flags=FFTW.EXHAUSTIVE)
scene = Scene(resolution=(500,500))
ax = axis(0:0.1:1, 0:0.1:1, 0:0.1:0.5)
center!(scene)
ls = map(1:M) do _
yoffset = to_node(to_value(scene[:time]))
offset = lift_node(scene[:time], yoffset) do t, yoff
Point3f0(0.0f0, (t-yoff)*S, 0.0f0)
end
l = lines(linspace(0,1,D), 0.0f0, zeros(Float32, D),
offset=offset, color=(:black, 0.1))
(yoffset, l)
end
while isopen(scene[:screen])
for (yoffset, line) in ls
isopen(scene[:screen]) || break
read!(src, buf)
A_mul_B!(fftbuf, fftplan, buf)
@. magbuf = log(clamp(abs(fftbuf), 0.0001, Inf))/10+0.5
line[:z] = magbuf[1:D]
push!(yoffset, to_value(scene[:time]))
end
end

View file

@ -1,52 +1,33 @@
__precompile__()
module PortAudio
using SampledSignals
using RingBuffers
#using Suppressor
using libportaudio_jll, SampledSignals
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("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
import LinearAlgebra
import LinearAlgebra: transpose!
export PortAudioStream
include("libportaudio.jl")
# 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.
# data is passed to and from portaudio in chunks with this many frames, because
# we need to interleave the samples
const CHUNKSIZE=128
# ringbuffer to receive errors from the audio processing thread
const ERR_BUFSIZE=512
function versioninfo(io::IO=STDOUT)
function versioninfo(io::IO=stdout)
println(io, Pa_GetVersionText())
println(io, "Version: ", Pa_GetVersion())
println(io, "Shim Source Hash: ", shimhash()[1:10])
end
type PortAudioDevice
mutable struct PortAudioDevice
name::String
hostapi::String
maxinchans::Int
@ -76,57 +57,52 @@ devnames() = join(["\"$(dev.name)\"" for dev in devices()], "\n")
# PortAudioStream
##################
type PortAudioStream{T}
mutable struct PortAudioStream{T}
samplerate::Float64
blocksize::Int
stream::PaStream
warn_xruns::Bool
sink # untyped because of circular type definition
source # untyped because of circular type definition
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
# TODO: handle blocksize=0, that should be the default and generally works
# much better than trying to specify
# TODO: expose latency parameter
# TODO: pre-fill outbut buffer on init
# TODO: recover from xruns - currently with low latencies (e.g. 0.01) it
# will run fine for a while and then fail with the first xrun.
# TODO: figure out whether we can get deterministic latency...
# TODO: write a latency tester app
function PortAudioStream{T}(indev::PortAudioDevice, outdev::PortAudioDevice,
inchans, outchans, sr, blocksize, synced) where {T}
inchans, outchans, sr, blocksize,
warn_xruns) where {T}
inchans = inchans == -1 ? indev.maxinchans : inchans
outchans = outchans == -1 ? outdev.maxoutchans : outchans
inparams = (inchans == 0) ?
Ptr{Pa_StreamParameters}(0) :
Ref(Pa_StreamParameters(indev.idx, inchans, type_to_fmt[T], 0.0, C_NULL))
Ref(Pa_StreamParameters(indev.idx, inchans, type_to_fmt[T], 0.1, C_NULL))
outparams = (outchans == 0) ?
Ptr{Pa_StreamParameters}(0) :
Ref(Pa_StreamParameters(outdev.idx, outchans, type_to_fmt[T], 0.0, C_NULL))
this = new(sr, blocksize, C_NULL)
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))
Ref(Pa_StreamParameters(outdev.idx, outchans, type_to_fmt[T], 0.1, C_NULL))
this = new(sr, blocksize, C_NULL, warn_xruns)
# finalizer(close, this)
this.sink = PortAudioSink{T}(outdev.name, this, outchans)
this.source = PortAudioSource{T}(indev.name, this, inchans)
this.stream = suppress_err() do
Pa_OpenStream(inparams, outparams, sr, blocksize, paNoFlag,
nothing, nothing)
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
# this is the top-level outer constructor that all the other outer constructors end up calling
"""
PortAudioStream(inchannels=2, outchannels=2; options...)
PortAudioStream(duplexdevice, inchannels=2, outchannels=2; options...)
@ -144,16 +120,14 @@ Options:
* `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.
* `warn_xruns`: Display a warning if there is a stream overrun or underrun,
which often happens when Julia is compiling, or with a
particularly large GC run. This can be quite verbose so is
false by default.
"""
# this is the top-level outer constructor that all the other outer constructors
# end up calling
function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
inchans=2, outchans=2; eltype=Float32, samplerate=-1, blocksize=DEFAULT_BLOCKSIZE, synced=false)
inchans=2, outchans=2; eltype=Float32, samplerate=-1, blocksize=DEFAULT_BLOCKSIZE,
warn_xruns=false)
if samplerate == -1
sampleratein = indev.defaultsamplerate
samplerateout = outdev.defaultsamplerate
@ -168,7 +142,7 @@ function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice,
samplerate = samplerateout
end
end
PortAudioStream{eltype}(indev, outdev, inchans, outchans, samplerate, blocksize, synced)
PortAudioStream{eltype}(indev, outdev, inchans, outchans, samplerate, blocksize, warn_xruns)
end
# handle device names given as streams
@ -211,12 +185,20 @@ function PortAudioStream(inchans=2, outchans=2; kwargs...)
PortAudioStream(indevice, outdevice, inchans, outchans; kwargs...)
end
# handle do-syntax
function PortAudioStream(fn::Function, args...; kwargs...)
str = PortAudioStream(args...; kwargs...)
try
fn(str)
finally
close(str)
end
end
function close(stream::PortAudioStream)
if stream.stream != C_NULL
Pa_StopStream(stream.stream)
Pa_CloseStream(stream.stream)
close(stream.source)
close(stream.sink)
stream.stream = C_NULL
end
@ -226,7 +208,8 @@ end
isopen(stream::PortAudioStream) = stream.stream != C_NULL
SampledSignals.samplerate(stream::PortAudioStream) = stream.samplerate
eltype{T}(stream::PortAudioStream{T}) = T
SampledSignals.blocksize(stream::PortAudioStream) = stream.blocksize
eltype(stream::PortAudioStream{T}) where T = T
read(stream::PortAudioStream, args...) = read(stream.source, args...)
read!(stream::PortAudioStream, args...) = read!(stream.source, args...)
@ -246,32 +229,6 @@ function show(io::IO, stream::PortAudioStream)
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
##################################
@ -279,19 +236,17 @@ end
# Define our source and sink types
for (TypeName, Super) in ((:PortAudioSink, :SampleSink),
(:PortAudioSource, :SampleSource))
@eval type $TypeName{T} <: $Super
@eval mutable struct $TypeName{T} <: $Super
name::String
stream::PortAudioStream{T}
chunkbuf::Array{T, 2}
ringbuf::RingBuffer{T}
nchannels::Int
function $TypeName{T}(name, stream, channels, ringbufsize) where {T}
function $TypeName{T}(name, stream, channels) 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 = RingBuffer{T}(channels, ringbufsize)
new(name, stream, chunkbuf, ringbuf, channels)
new(name, stream, chunkbuf, channels)
end
end
end
@ -300,30 +255,36 @@ 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
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)
function close(s::Union{PortAudioSink, PortAudioSource})
throw(ErrorException("Attempted to close PortAudioSink or PortAudioSource.
Close the containing PortAudioStream instead"))
end
isopen(s::Union{PortAudioSink, PortAudioSource}) = isopen(s.stream)
name(s::Union{PortAudioSink, PortAudioSource}) = s.name
function show(io::IO, stream::T) where {T <: Union{PortAudioSink, PortAudioSource}}
println(io, T, "(\"", stream.name, "\")")
print(io, nchannels(stream), " channels")
function show(io::IO, ::Type{PortAudioSink{T}}) where T
print(io, "PortAudioSink{$T}")
end
flush(sink::PortAudioSink) = flush(sink.ringbuf)
function show(io::IO, ::Type{PortAudioSource{T}}) where T
print(io, "PortAudioSource{$T}")
end
function show(io::IO, stream::T) where {T <: Union{PortAudioSink, PortAudioSource}}
print(io, nchannels(stream), "-channel ", T, "(\"", stream.name, "\")")
end
function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount)
nwritten = 0
while nwritten < framecount
towrite = min(framecount-nwritten, CHUNKSIZE)
n = min(framecount-nwritten, CHUNKSIZE)
# make a buffer of interleaved samples
transpose!(view(sink.chunkbuf, :, 1:towrite),
view(buf, (1:towrite)+nwritten+frameoffset, :))
n = write(sink.ringbuf, sink.chunkbuf, towrite)
transpose!(view(sink.chunkbuf, :, 1:n),
view(buf, (1:n) .+ nwritten .+ frameoffset, :))
# TODO: if the stream is closed we just want to return a
# shorter-than-requested frame count instead of throwing an error
Pa_WriteStream(sink.stream.stream, sink.chunkbuf, n, sink.stream.warn_xruns)
nwritten += n
# break early if the stream is closed
n < towrite && break
end
nwritten
@ -332,23 +293,67 @@ end
function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameoffset, framecount)
nread = 0
while nread < framecount
toread = min(framecount-nread, CHUNKSIZE)
n = read!(source.ringbuf, source.chunkbuf, toread)
n = min(framecount-nread, CHUNKSIZE)
# TODO: if the stream is closed we just want to return a
# shorter-than-requested frame count instead of throwing an error
Pa_ReadStream(source.stream.stream, source.chunkbuf, n, source.stream.warn_xruns)
# de-interleave the samples
transpose!(view(buf, (1:toread)+nread+frameoffset, :),
view(source.chunkbuf, :, 1:toread))
transpose!(view(buf, (1:n) .+ nread .+ frameoffset, :),
view(source.chunkbuf, :, 1:n))
nread += toread
# break early if the stream is closed
n < toread && break
nread += n
end
nread
end
# 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)
function suppress_err(dofunc::Function)
nullfile = @static Sys.iswindows() ? "nul" : "/dev/null"
open(nullfile, "w") do io
redirect_stderr(dofunc, io)
end
end
function __init__()
if Sys.islinux()
envkey = "ALSA_CONFIG_DIR"
if envkey keys(ENV)
searchdirs = ["/usr/share/alsa",
"/usr/local/share/alsa",
"/etc/alsa"]
confdir_idx = findfirst(searchdirs) do d
isfile(joinpath(d, "alsa.conf"))
end
if confdir_idx === nothing
throw(ErrorException(
"""
Could not find ALSA config directory. Searched:
$(join(searchdirs, "\n"))
if ALSA is installed, set the "ALSA_CONFIG_DIR" environment
variable. The given directory should have a file "alsa.conf".
If it would be useful to others, please file an issue at
https://github.com/JuliaAudio/PortAudio.jl/issues
with your alsa config directory so we can add it to the search
paths.
"""))
end
confdir = searchdirs[confdir_idx]
ENV[envkey] = confdir
end
end
# initialize PortAudio on module load. libportaudio prints a bunch of
# junk to STDOUT on initialization, so we swallow it.
# TODO: actually check the junk to make sure there's nothing in there we
# don't expect
suppress_err() do
Pa_Initialize()
end
atexit() do
Pa_Terminate()
end
end
end # module PortAudio

View file

@ -9,8 +9,8 @@ const PaHostApiIndex = Cint
const PaHostApiTypeId = Cint
# PaStream is always used as an opaque type, so we're always dealing
# with the pointer
const PaStream = Ptr{Void}
const PaStreamCallback = Void
const PaStream = Ptr{Cvoid}
const PaStreamCallback = Cvoid
const PaStreamFlags = Culong
const paNoFlag = PaStreamFlags(0x00)
@ -43,20 +43,41 @@ const paContinue = PaStreamCallbackResult(0)
const paComplete = PaStreamCallbackResult(1)
const paAbort = PaStreamCallbackResult(2)
"""
Call the given expression in a separate thread, waiting on the result. This is
useful when running code that would otherwise block the Julia process (like a
`ccall` into a function that does IO).
"""
macro tcall(ex)
:(fetch(Base.Threads.@spawn $(esc(ex))))
end
# because we're calling Pa_ReadStream and PA_WriteStream from separate threads,
# we put a mutex around libportaudio calls
const pamutex = ReentrantLock()
macro locked(ex)
quote
lock(pamutex) do
$(esc(ex))
end
end
end
function Pa_Initialize()
err = ccall((:Pa_Initialize, libportaudio), PaError, ())
err = @locked ccall((:Pa_Initialize, libportaudio), PaError, ())
handle_status(err)
end
function Pa_Terminate()
err = ccall((:Pa_Terminate, libportaudio), PaError, ())
err = @locked ccall((:Pa_Terminate, libportaudio), PaError, ())
handle_status(err)
end
Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ())
Pa_GetVersion() = @locked ccall((:Pa_GetVersion, libportaudio), Cint, ())
function Pa_GetVersionText()
versionPtr = ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
versionPtr = @locked ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
unsafe_string(versionPtr)
end
@ -86,7 +107,7 @@ const pa_host_api_names = Dict{PaHostApiTypeId, String}(
14 => "AudioScience HPI"
)
type PaHostApiInfo
mutable struct PaHostApiInfo
struct_version::Cint
api_type::PaHostApiTypeId
name::Ptr{Cchar}
@ -95,12 +116,12 @@ type PaHostApiInfo
defaultOutputDevice::PaDeviceIndex
end
Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio),
Pa_GetHostApiInfo(i) = unsafe_load(@locked ccall((:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo}, (PaHostApiIndex,), i))
# Device Functions
type PaDeviceInfo
mutable struct PaDeviceInfo
struct_version::Cint
name::Ptr{Cchar}
host_api::PaHostApiIndex
@ -113,28 +134,28 @@ type PaDeviceInfo
default_sample_rate::Cdouble
end
Pa_GetDeviceCount() = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
Pa_GetDeviceCount() = @locked ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio),
Pa_GetDeviceInfo(i) = unsafe_load(@locked ccall((:Pa_GetDeviceInfo, libportaudio),
Ptr{PaDeviceInfo}, (PaDeviceIndex,), i))
Pa_GetDefaultInputDevice() = ccall((:Pa_GetDefaultInputDevice, libportaudio),
Pa_GetDefaultInputDevice() = @locked ccall((:Pa_GetDefaultInputDevice, libportaudio),
PaDeviceIndex, ())
Pa_GetDefaultOutputDevice() = ccall((:Pa_GetDefaultOutputDevice, libportaudio),
Pa_GetDefaultOutputDevice() = @locked ccall((:Pa_GetDefaultOutputDevice, libportaudio),
PaDeviceIndex, ())
# Stream Functions
type Pa_StreamParameters
mutable struct Pa_StreamParameters
device::PaDeviceIndex
channelCount::Cint
sampleFormat::PaSampleFormat
suggestedLatency::PaTime
hostAPISpecificStreamInfo::Ptr{Void}
hostAPISpecificStreamInfo::Ptr{Cvoid}
end
type PaStreamInfo
mutable struct PaStreamInfo
structVersion::Cint
inputLatency::PaTime
outputLatency::PaTime
@ -148,7 +169,7 @@ end
# err = ccall((:Pa_OpenDefaultStream, libportaudio),
# PaError, (Ref{PaStream}, Cint, Cint,
# PaSampleFormat, Cdouble, Culong,
# Ref{Void}, Ref{Void}),
# Ref{Cvoid}, Ref{Cvoid}),
# streamPtr, inChannels, outChannels, sampleFormat, sampleRate,
# framesPerBuffer, C_NULL, C_NULL)
# handle_status(err)
@ -161,47 +182,49 @@ function Pa_OpenStream(inParams, outParams,
flags::PaStreamFlags,
callback, userdata)
streamPtr = Ref{PaStream}(0)
err = ccall((:Pa_OpenStream, libportaudio), PaError,
(Ref{PaStream},
Ptr{Pa_StreamParameters},
Ptr{Pa_StreamParameters},
Cdouble, Culong, PaStreamFlags,
Ptr{Void}, Ptr{Void}),
streamPtr,
inParams, outParams,
sampleRate, framesPerBuffer, flags,
callback, userdata)
err = @locked ccall((:Pa_OpenStream, libportaudio), PaError,
(Ref{PaStream}, Ref{Pa_StreamParameters}, Ref{Pa_StreamParameters},
Cdouble, Culong, PaStreamFlags, Ref{Cvoid},
# it seems like we should be able to use Ref{T} here, with
# userdata::T above, and avoid the `pointer_from_objref` below.
# that's not working on 0.6 though, and it shouldn't really
# matter because userdata should be GC-rooted anyways
Ptr{Cvoid}),
streamPtr, inParams, outParams,
float(sampleRate), framesPerBuffer, flags,
callback === nothing ? C_NULL : callback,
userdata === nothing ? C_NULL : pointer_from_objref(userdata))
handle_status(err)
streamPtr[]
end
function Pa_StartStream(stream::PaStream)
err = ccall((:Pa_StartStream, libportaudio), PaError,
err = @locked ccall((:Pa_StartStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_StopStream(stream::PaStream)
err = ccall((:Pa_StopStream, libportaudio), PaError,
err = @locked ccall((:Pa_StopStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_CloseStream(stream::PaStream)
err = ccall((:Pa_CloseStream, libportaudio), PaError,
err = @locked ccall((:Pa_CloseStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_GetStreamReadAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
avail = @locked ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
end
function Pa_GetStreamWriteAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
avail = @locked ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
@ -210,9 +233,9 @@ end
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
err = @tcall @locked ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Cvoid}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
buf
end
@ -220,9 +243,9 @@ end
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_WriteStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
err = @tcall @locked ccall((:Pa_WriteStream, libportaudio), PaError,
(PaStream, Ptr{Cvoid}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
nothing
end
@ -238,13 +261,13 @@ end
function handle_status(err::PaError, show_warnings::Bool=true)
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
if show_warnings
msg = ccall((:Pa_GetErrorText, libportaudio),
msg = @locked ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
warn("libportaudio: " * unsafe_string(msg))
@warn("libportaudio: " * unsafe_string(msg))
end
elseif err != PA_NO_ERROR
msg = ccall((:Pa_GetErrorText, libportaudio),
msg = @locked ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
error("libportaudio: " * unsafe_string(msg))
throw(ErrorException("libportaudio: " * unsafe_string(msg)))
end
end

View file

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

View file

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

View file

@ -1 +0,0 @@
TestSetExtensions

View file

@ -1,183 +1,9 @@
#!/usr/bin/env julia
using Base.Test
using TestSetExtensions
using PortAudio
using SampledSignals
using RingBuffers
using Test
# pull in some extra stuff we need to test the callback directly
using PortAudio: notifyhandle, notifycb_c, shim_processcb_c
using PortAudio: pa_shim_errmsg_t, pa_shim_info_t
using PortAudio: PA_SHIM_ERRMSG_ERR_OVERFLOW, PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW
"Setup buffers to test callback behavior"
function setup_callback(inchans, outchans, nframes, synced)
sourcebuf = RingBuffer{Float32}(inchans, nframes*2) # the microphone input should end up here
sinkbuf = RingBuffer{Float32}(outchans, nframes*2) # the callback should copy this to cb_output
errbuf = RingBuffer{pa_shim_errmsg_t}(1, 8)
# pass NULL for i/o we're not using
info = pa_shim_info_t(
inchans > 0 ? pointer(sourcebuf) : C_NULL,
outchans > 0 ? pointer(sinkbuf) : C_NULL,
pointer(errbuf),
synced, notifycb_c,
inchans > 0 ? notifyhandle(sourcebuf) : C_NULL,
outchans > 0 ? notifyhandle(sinkbuf) : C_NULL,
notifyhandle(errbuf)
)
flags = Culong(0)
cb_input = rand(Float32, inchans, nframes) # simulate microphone input
cb_output = rand(Float32, outchans, nframes) # this is where the output should go
function processfunc()
ccall(shim_processcb_c, Cint,
(Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{Void}),
cb_input, cb_output, nframes, C_NULL, flags, pointer_from_objref(info))
end
(sourcebuf, sinkbuf, errbuf, cb_input, cb_output, processfunc)
end
function test_callback(inchans, outchans, synced)
nframes = 8
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
if outchans > 0
testout = rand(Float32, outchans, nframes) # generate some test data to play
write(sinkbuf, testout) # fill the output ringbuffer
end
@test process() == PortAudio.paContinue
if outchans > 0
# testout -> sinkbuf -> cb_output
@test cb_output == testout
end
if inchans > 0
# cb_input -> sourcebuf
@test read(sourcebuf, nframes) == cb_input
end
@test framesreadable(errbuf) == 0
end
"""
test_callback_underflow(inchans, outchans; nframes=8, underfill=3, synced=false)
Test that the callback works on underflow conditions. underfill is the numer of
frames we feed in, which should be less than nframes.
"""
function test_callback_underflow(inchans, outchans, synced)
nframes = 8
underfill = 3 # must be less than nframes
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
outchans > 0 || error("Can't test underflow with no output")
testout = rand(Float32, outchans, underfill)
write(sinkbuf, testout) # underfill the output ringbuffer
# call callback (partial underflow)
@test process() == PortAudio.paContinue
@test cb_output[:, 1:underfill] == testout
@test cb_output[:, (underfill+1):nframes] == zeros(Float32, outchans, (nframes-underfill))
errs = readavailable(errbuf)
if inchans > 0
received = readavailable(sourcebuf)
if synced
@test size(received, 2) == underfill
@test received == cb_input[:, 1:underfill]
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test size(received, 2) == nframes
@test received == cb_input
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
# call again (total underflow)
@test process() == PortAudio.paContinue
@test cb_output == zeros(Float32, outchans, nframes)
errs = readavailable(errbuf)
if inchans > 0
received = readavailable(sourcebuf)
if synced
@test size(received, 2) == 0
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test size(received, 2) == nframes
@test received == cb_input
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW
end
end
function test_callback_overflow(inchans, outchans, synced)
nframes = 8
(sourcebuf, sinkbuf, errbuf,
cb_input, cb_output, process) = setup_callback(inchans, outchans,
nframes, synced)
inchans > 0 || error("Can't test overflow with no input")
@test frameswritable(sinkbuf) == nframes*2
# the first time it should half-fill the input ring buffer
if outchans > 0
testout = rand(Float32, outchans, nframes)
write(sinkbuf, testout)
end
@test framesreadable(sourcebuf) == 0
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(errbuf) == 0
@test framesreadable(sourcebuf) == nframes
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
# now run the process func again to completely fill the input ring buffer
outchans > 0 && write(sinkbuf, testout)
@test framesreadable(sourcebuf) == nframes
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(errbuf) == 0
@test framesreadable(sourcebuf) == nframes*2
outchans > 0 && @test frameswritable(sinkbuf) == nframes*2
# now this time the process func should overflow the input buffer
outchans > 0 && write(sinkbuf, testout)
@test framesreadable(sourcebuf) == nframes*2
outchans > 0 && @test frameswritable(sinkbuf) == nframes
@test process() == PortAudio.paContinue
@test framesreadable(sourcebuf) == nframes*2
errs = readavailable(errbuf)
if outchans > 0
if synced
# if input and output are synced, thec callback didn't pull from
# the output ringbuf
@test frameswritable(sinkbuf) == nframes
@test cb_output == zeros(Float32, outchans, nframes)
@test length(errs) == 2
@test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW])
else
@test frameswritable(sinkbuf) == nframes*2
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
end
else
@test length(errs) == 1
@test errs[1] == PA_SHIM_ERRMSG_OVERFLOW
end
end
@testset ExtendedTestSet "PortAudio Tests" begin
@testset "PortAudio Tests" begin
@testset "Reports version" begin
io = IOBuffer()
PortAudio.versioninfo(io)
@ -186,64 +12,7 @@ end
@test startswith(result[1], "PortAudio V19")
end
@testset "using correct shim version" begin
@test PortAudio.shimhash() == "87021557a9f999545828eb11e4ebad2cd278b734dd91a8bd3faf05c89912cf80"
end
@testset "Basic callback functionality" begin
@testset "basic duplex (no sync)" begin
test_callback(2, 3, false)
end
@testset "basic input-only (no sync)" begin
test_callback(2, 0, false)
end
@testset "basic output-only (no sync)" begin
test_callback(0, 2, false)
end
@testset "basic no input or output (no sync)" begin
test_callback(0, 0, false)
end
@testset "basic duplex (sync)" begin
test_callback(2, 3, true)
end
@testset "basic input-only (sync)" begin
test_callback(2, 0, true)
end
@testset "basic output-only (sync)" begin
test_callback(0, 2, true)
end
@testset "basic no input or output (sync)" begin
test_callback(0, 0, true)
end
end
@testset "Ouput underflow" begin
@testset "underflow duplex (nosync)" begin
test_callback_underflow(2, 3, false)
end
@testset "underflow output-only (nosync)" begin
test_callback_underflow(0, 3, false)
end
@testset "underflow duplex (sync)" begin
test_callback_underflow(2, 3, true)
end
@testset "underflow output-only (sync)" begin
test_callback_underflow(0, 3, true)
end
end
@testset "Input overflow" begin
@testset "overflow duplex (nosync)" begin
test_callback_overflow(2, 3, false)
end
@testset "overflow input-only (nosync)" begin
test_callback_overflow(2, 0, false)
end
@testset "overflow duplex (sync)" begin
test_callback_overflow(2, 3, true)
end
@testset "overflow input-only (sync)" begin
test_callback_overflow(2, 0, true)
end
@testset "Can list devices without crashing" begin
PortAudio.devices()
end
end

View file

@ -5,18 +5,18 @@
include("runtests.jl")
# these default values are specific to my machines
if is_windows()
if Sys.iswindows()
default_indev = "Microphone Array (Realtek High "
default_outdev = "Speaker/Headphone (Realtek High"
elseif is_apple()
default_indev = "Built-in Microph"
elseif Sys.isapple()
default_indev = "Built-in Microphone"
default_outdev = "Built-in Output"
elseif is_linux()
elseif Sys.islinux()
default_indev = "default"
default_outdev = "default"
end
@testset ExtendedTestSet "Local Tests" begin
@testset "Local Tests" begin
@testset "Open Default Device" begin
println("Recording...")
stream = PortAudioStream(2, 0)
@ -50,12 +50,12 @@ end
write(stream, buf)
io = IOBuffer()
show(io, stream)
@test String(take!(io)) == """
PortAudio.PortAudioStream{Float32}
@test occursin("""
PortAudioStream{Float32}
Samplerate: 44100.0Hz
Buffer Size: 4096 frames
2 channel sink: "$default_outdev"
2 channel source: "$default_indev\""""
2 channel source: "$default_indev\"""", String(take!(io)))
close(stream)
end
@testset "Error on wrong name" begin
@ -68,8 +68,8 @@ end
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
@test fetch(t1) == 48000
@test fetch(t2) == 48000
flush(stream)
close(stream)
end
@ -78,8 +78,8 @@ end
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
@test fetch(t1) == 48000
@test fetch(t2) == 48000
close(stream)
end
end