Merge pull request #40 from JuliaAudio/nocallback
Julia 1.0, Artifacts, JLLs, no ringbuffers
This commit is contained in:
commit
25993bce0e
32 changed files with 397 additions and 2051 deletions
31
.appveyor.yml
Normal file
31
.appveyor.yml
Normal 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
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
*.swp
|
||||
*.o
|
||||
deps/deps.jl
|
||||
deps/build.log
|
||||
*.wav
|
||||
*.flac
|
||||
*.cov
|
||||
|
|
15
.travis.yml
15
.travis.yml
|
@ -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
20
Project.toml
Normal 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"]
|
22
README.md
22
README.md
|
@ -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)
|
||||
```
|
||||
|
||||
|
|
6
REQUIRE
6
REQUIRE
|
@ -1,6 +0,0 @@
|
|||
julia 0.6.0-dev.2746
|
||||
BinDeps
|
||||
SampledSignals 0.3.0
|
||||
RingBuffers 1.0.0
|
||||
@osx Homebrew
|
||||
@windows WinRPM
|
28
appveyor.yml
28
appveyor.yml
|
@ -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
25
deps/build.jl
vendored
|
@ -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
80
deps/src/Makefile
vendored
|
@ -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
54
deps/src/build.sh
vendored
|
@ -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 "================================"
|
8
deps/src/dockerbuild_cb.sh
vendored
8
deps/src/dockerbuild_cb.sh
vendored
|
@ -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
|
13
deps/src/dockerbuild_hbb.sh
vendored
13
deps/src/dockerbuild_hbb.sh
vendored
|
@ -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
104
deps/src/pa_shim.c
vendored
|
@ -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
1225
deps/src/portaudio.h
vendored
File diff suppressed because it is too large
Load diff
BIN
deps/usr/lib/pa_shim_arm-linux-gnueabihf.so
vendored
BIN
deps/usr/lib/pa_shim_arm-linux-gnueabihf.so
vendored
Binary file not shown.
BIN
deps/usr/lib/pa_shim_i686-linux-gnu.so
vendored
BIN
deps/usr/lib/pa_shim_i686-linux-gnu.so
vendored
Binary file not shown.
BIN
deps/usr/lib/pa_shim_i686-w64-mingw32.dll
vendored
BIN
deps/usr/lib/pa_shim_i686-w64-mingw32.dll
vendored
Binary file not shown.
BIN
deps/usr/lib/pa_shim_powerpc64le-linux-gnu.so
vendored
BIN
deps/usr/lib/pa_shim_powerpc64le-linux-gnu.so
vendored
Binary file not shown.
BIN
deps/usr/lib/pa_shim_x86_64-apple-darwin14.dylib
vendored
BIN
deps/usr/lib/pa_shim_x86_64-apple-darwin14.dylib
vendored
Binary file not shown.
BIN
deps/usr/lib/pa_shim_x86_64-linux-gnu.so
vendored
BIN
deps/usr/lib/pa_shim_x86_64-linux-gnu.so
vendored
Binary file not shown.
BIN
deps/usr/lib/pa_shim_x86_64-w64-mingw32.dll
vendored
BIN
deps/usr/lib/pa_shim_x86_64-w64-mingw32.dll
vendored
Binary file not shown.
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
67
examples/waterfall_heatmap.jl
Normal file
67
examples/waterfall_heatmap.jl
Normal 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
|
38
examples/waterfall_lines.jl
Normal file
38
examples/waterfall_lines.jl
Normal 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
|
253
src/PortAudio.jl
253
src/PortAudio.jl
|
@ -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
|
||||
|
|
|
@ -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,8 +233,8 @@ 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),
|
||||
err = @tcall @locked ccall((:Pa_ReadStream, libportaudio), PaError,
|
||||
(PaStream, Ptr{Cvoid}, Culong),
|
||||
stream, buf, frames)
|
||||
handle_status(err, show_warnings)
|
||||
buf
|
||||
|
@ -220,8 +243,8 @@ 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),
|
||||
err = @tcall @locked ccall((:Pa_WriteStream, libportaudio), PaError,
|
||||
(PaStream, Ptr{Cvoid}, Culong),
|
||||
stream, buf, frames)
|
||||
handle_status(err, show_warnings)
|
||||
nothing
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
TestSetExtensions
|
239
test/runtests.jl
239
test/runtests.jl
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue