diff --git a/.gitignore b/.gitignore index d86d68c..dd2812d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ *.swp -*.so *.o deps/deps.jl *.wav *.flac *.cov coverage +deps/usr/lib/pa_shim.so +deps/usr/lib/pa_shim.dylib +deps/usr/lib/pa_shim.dll diff --git a/.travis.yml b/.travis.yml index fc66025..5d1b762 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,9 @@ os: - osx sudo: required julia: - - 0.4 - - 0.5 + - 0.6 notifications: email: false script: - # we can't actually run on travis, so just make sure it's installable - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi - - julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); using PortAudio' + - julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); Pkg.test("PortAudio")' diff --git a/LICENSE b/LICENSE index 0f797a9..04c37c2 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +suppressor.jl includes code from the Suppressor.jl package, licensed under the +MIT "Expat" License: + +Copyright (c) 2016: Ismael Venegas Castelló. diff --git a/README.md b/README.md index a9139dc..7b2cc9a 100644 --- a/README.md +++ b/README.md @@ -80,3 +80,14 @@ julia> buf = read(stream, 10s) julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf) ``` + +## Building the shim library + +Because PortAudio calls its callback from a separate audio thread, we can't handle it in Julia directly. To work around this we've included a small shim library written in C that uses ring buffers to pass audio data between the callback context and the main Julia context. To build the shim you'll need a few prerequisites: + +* libportaudio +* make +* a C compiler (gcc on linux/macOS, mingw64 on Windows) +* The `RingBuffers` julia package, installed in a folder next to this one. The portaudio shim links against the `pa_ringbuffer` library that comes with `RingBuffers`. + +To build the shim, go into the `deps/src` directory and type `make`. diff --git a/REQUIRE b/REQUIRE index 38ee8b4..3a091fe 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,8 +1,6 @@ -julia 0.4 -Compat 0.8.8 +julia 0.6- BinDeps -Devectorize SampledSignals 0.3.0 -RingBuffers 0.1.0 +RingBuffers 1.0.0 @osx Homebrew @windows WinRPM diff --git a/appveyor.yml b/appveyor.yml index d5508bb..a18e182 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,7 @@ environment: matrix: - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.4/julia-0.4-latest-win32.exe" - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.4/julia-0.4-latest-win64.exe" - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.5/julia-0.5-latest-win32.exe" - - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.5/julia-0.5-latest-win64.exe" + - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x86/0.6/julia-0.6-latest-win32.exe" + - JULIA_URL: "https://julialang-s3.julialang.org/bin/winnt/x64/0.6/julia-0.6-latest-win64.exe" notifications: - provider: Email @@ -27,5 +25,4 @@ build_script: Pkg.clone(pwd(), \"PortAudio\"); Pkg.build(\"PortAudio\")" test_script: - # can't actually run the test, so just make sure it's installable - - C:\projects\julia\bin\julia -e "using PortAudio" + - C:\projects\julia\bin\julia --check-bounds=yes -e "Pkg.test(\"PortAudio\")" diff --git a/deps/src/Makefile b/deps/src/Makefile new file mode 100644 index 0000000..c711226 --- /dev/null +++ b/deps/src/Makefile @@ -0,0 +1,80 @@ +# Makefile originally lifted from Clang.jl +# Copyright (c) 2012-: Isaiah Norton and [contributors](https://github.com/ihnorton/Clang.jl/graphs/contributors) + +ifeq (exists, $(shell [ -e Make.user ] && echo exists )) +include Make.user +endif + +TARGETDIR=../usr/lib +TARGETBASENAME=pa_shim +OBJS = pa_shim.o + +# check to see if the user passed in a HOST variable for cross-compiling +ifeq ($(HOST),) + # Figure out OS and architecture + OS=$(shell uname) + ifneq ($(findstring MINGW,$(OS)),) + OS=WINNT + endif +else + HOSTSUFFIX=_$(HOST) + ifneq ($(findstring linux,$(HOST)),) + OS=Linux + else ifneq ($(findstring darwin,$(HOST)),) + OS=Darwin + else ifneq ($(findstring mingw,$(HOST)),) + OS=WINNT + endif +endif + +CFLAGS = -Wall -Wno-strict-aliasing -fno-omit-frame-pointer -I../../../RingBuffers/deps/src +LDFLAGS += + +ifeq ($(OS), WINNT) + LIBS += + LDFLAGS += -shared -L../../../RingBuffers/deps/usr/lib -lpa_ringbuffer$(HOSTSUFFIX) + INC += + SHACMD = sha256sum + SHLIB_EXT = dll +else ifeq ($(OS), Darwin) + LIBS += + INC += + # we'll rely on Julia to load RingBuffers.jl, which will in turn load the C + # library that we depend on for these symbols + LDFLAGS += -dynamiclib -Wl,-undefined,dynamic_lookup + SHLIB_EXT = dylib + SHACMD = shasum -a256 +else + CFLAGS += -fPIC + LIBS += + INC += + LDFLAGS += -shared + SHLIB_EXT = so + SHACMD = sha256sum +endif + +SOURCEHASH = $(shell $(SHACMD) pa_shim.c | awk '{print $$1}') +CFLAGS += -DSOURCEHASH=\"$(SOURCEHASH)\" + +TARGET=$(TARGETDIR)/$(TARGETBASENAME)$(HOSTSUFFIX).$(SHLIB_EXT) + +.PHONY: clean cleantemp default + +default: $(TARGET) + +%.o: %.c Makefile + $(CC) $< -c -o $@ $(INC) $(CFLAGS) + +$(TARGETDIR): + mkdir -p $@ + +$(TARGET): $(OBJS) $(TARGETDIR) Makefile + $(CC) $(OBJS) $(LDFLAGS) -o $@ $(LIBS) + +cleantemp: + rm -f $(OBJS) + +clean: cleantemp + rm -f $(TARGETDIR)/$(TARGETBASENAME)*.so + rm -f $(TARGETDIR)/$(TARGETBASENAME)*.dylib + rm -f $(TARGETDIR)/$(TARGETBASENAME)*.dll diff --git a/deps/src/build.sh b/deps/src/build.sh new file mode 100755 index 0000000..4e210e1 --- /dev/null +++ b/deps/src/build.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# User docker to build pa_shim library for all supported platforms. + +set -e + +make clean + +echo "" +# NOTE: the darwin build ends up actually being x86_64-apple-darwin14. It gets +# mapped within the docker machine +for platform in \ + arm-linux-gnueabihf \ + powerpc64le-linux-gnu \ + x86_64-apple-darwin \ + x86_64-w64-mingw32 \ + i686-w64-mingw32; do + echo "================================" + echo "building for $platform..." + docker run --rm \ + -v $(pwd)/../..:/workdir \ + -v $(pwd)/../../../RingBuffers:/RingBuffers \ + -w /workdir/deps/src \ + -e CROSS_TRIPLE=$platform \ + multiarch/crossbuild \ + ./dockerbuild_cb.sh + echo "================================" + echo "" +done + +# we use holy-build-box for the x86 linux builds because it uses an older +# glibc so it should be compatible with more user environments +echo "================================" +echo "building for x86_64-linux-gnu..." +docker run --rm \ + -v $(pwd)/../..:/workdir \ + -v $(pwd)/../../../RingBuffers:/RingBuffers \ + -w /workdir/deps/src \ + -e HOST=x86_64-linux-gnu \ + phusion/holy-build-box-64 \ + ./dockerbuild_hbb.sh +echo "================================" +echo "" + +echo "================================" +echo "building for i686-linux-gnu..." +docker run --rm \ + -v $(pwd)/../..:/workdir \ + -v $(pwd)/../../../RingBuffers:/RingBuffers \ + -w /workdir/deps/src \ + -e HOST=i686-linux-gnu \ + phusion/holy-build-box-32 \ + ./dockerbuild_hbb.sh +echo "================================" diff --git a/deps/src/dockerbuild_cb.sh b/deps/src/dockerbuild_cb.sh new file mode 100755 index 0000000..7b04f2b --- /dev/null +++ b/deps/src/dockerbuild_cb.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# this script is run by build.sh within each docker instance to do the build. + +set -e + +make HOST=$CROSS_TRIPLE +make cleantemp diff --git a/deps/src/dockerbuild_hbb.sh b/deps/src/dockerbuild_hbb.sh new file mode 100755 index 0000000..090611a --- /dev/null +++ b/deps/src/dockerbuild_hbb.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# this script is run by build.sh within each docker instance to do the build. +# it's meant to be run from within a "Holy Build Box" docker instance. + +set -e + +# Activate Holy Build Box environment. +source /hbb_exe/activate + +set -x +make HOST=$HOST +make cleantemp diff --git a/deps/src/pa_shim.c b/deps/src/pa_shim.c new file mode 100644 index 0000000..c6d67cc --- /dev/null +++ b/deps/src/pa_shim.c @@ -0,0 +1,104 @@ +#include "portaudio.h" +#include +#include +#include +#include + +#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; +} diff --git a/deps/src/portaudio.h b/deps/src/portaudio.h new file mode 100644 index 0000000..8a94aaf --- /dev/null +++ b/deps/src/portaudio.h @@ -0,0 +1,1225 @@ +#ifndef PORTAUDIO_H +#define PORTAUDIO_H +/* + * $Id$ + * PortAudio Portable Real-Time Audio Library + * PortAudio API Header File + * Latest version available at: http://www.portaudio.com/ + * + * Copyright (c) 1999-2002 Ross Bencina and Phil Burk + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + * ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * The text above constitutes the entire PortAudio license; however, + * the PortAudio community also makes the following non-binding requests: + * + * Any person wishing to distribute modifications to the Software is + * requested to send the modifications to the original developer so that + * they can be incorporated into the canonical version. It is also + * requested that these non-binding requests be included along with the + * license above. + */ + +/** @file + @ingroup public_header + @brief The portable PortAudio API. +*/ + + +#ifdef __cplusplus +extern "C" +{ +#endif /* __cplusplus */ + +/** Retrieve the release number of the currently running PortAudio build. + For example, for version "19.5.1" this will return 0x00130501. + + @see paMakeVersionNumber +*/ +int Pa_GetVersion( void ); + +/** Retrieve a textual description of the current PortAudio build, + e.g. "PortAudio V19.5.0-devel, revision 1952M". + The format of the text may change in the future. Do not try to parse the + returned string. + + @deprecated As of 19.5.0, use Pa_GetVersionInfo()->versionText instead. +*/ +const char* Pa_GetVersionText( void ); + +/** + Generate a packed integer version number in the same format used + by Pa_GetVersion(). Use this to compare a specified version number with + the currently running version. For example: + + @code + if( Pa_GetVersion() < paMakeVersionNumber(19,5,1) ) {} + @endcode + + @see Pa_GetVersion, Pa_GetVersionInfo + @version Available as of 19.5.0. +*/ +#define paMakeVersionNumber(major, minor, subminor) \ + (((major)&0xFF)<<16 | ((minor)&0xFF)<<8 | ((subminor)&0xFF)) + + +/** + A structure containing PortAudio API version information. + @see Pa_GetVersionInfo, paMakeVersionNumber + @version Available as of 19.5.0. +*/ +typedef struct PaVersionInfo { + int versionMajor; + int versionMinor; + int versionSubMinor; + /** + This is currently the Git revision hash but may change in the future. + The versionControlRevision is updated by running a script before compiling the library. + If the update does not occur, this value may refer to an earlier revision. + */ + const char *versionControlRevision; + /** Version as a string, for example "PortAudio V19.5.0-devel, revision 1952M" */ + const char *versionText; +} PaVersionInfo; + +/** Retrieve version information for the currently running PortAudio build. + @return A pointer to an immutable PaVersionInfo structure. + + @note This function can be called at any time. It does not require PortAudio + to be initialized. The structure pointed to is statically allocated. Do not + attempt to free it or modify it. + + @see PaVersionInfo, paMakeVersionNumber + @version Available as of 19.5.0. +*/ +const PaVersionInfo* Pa_GetVersionInfo(); + + +/** Error codes returned by PortAudio functions. + Note that with the exception of paNoError, all PaErrorCodes are negative. +*/ + +typedef int PaError; +typedef enum PaErrorCode +{ + paNoError = 0, + + paNotInitialized = -10000, + paUnanticipatedHostError, + paInvalidChannelCount, + paInvalidSampleRate, + paInvalidDevice, + paInvalidFlag, + paSampleFormatNotSupported, + paBadIODeviceCombination, + paInsufficientMemory, + paBufferTooBig, + paBufferTooSmall, + paNullCallback, + paBadStreamPtr, + paTimedOut, + paInternalError, + paDeviceUnavailable, + paIncompatibleHostApiSpecificStreamInfo, + paStreamIsStopped, + paStreamIsNotStopped, + paInputOverflowed, + paOutputUnderflowed, + paHostApiNotFound, + paInvalidHostApi, + paCanNotReadFromACallbackStream, + paCanNotWriteToACallbackStream, + paCanNotReadFromAnOutputOnlyStream, + paCanNotWriteToAnInputOnlyStream, + paIncompatibleStreamHostApi, + paBadBufferPtr +} PaErrorCode; + + +/** Translate the supplied PortAudio error code into a human readable + message. +*/ +const char *Pa_GetErrorText( PaError errorCode ); + + +/** Library initialization function - call this before using PortAudio. + This function initializes internal data structures and prepares underlying + host APIs for use. With the exception of Pa_GetVersion(), Pa_GetVersionText(), + and Pa_GetErrorText(), this function MUST be called before using any other + PortAudio API functions. + + If Pa_Initialize() is called multiple times, each successful + call must be matched with a corresponding call to Pa_Terminate(). + Pairs of calls to Pa_Initialize()/Pa_Terminate() may overlap, and are not + required to be fully nested. + + Note that if Pa_Initialize() returns an error code, Pa_Terminate() should + NOT be called. + + @return paNoError if successful, otherwise an error code indicating the cause + of failure. + + @see Pa_Terminate +*/ +PaError Pa_Initialize( void ); + + +/** Library termination function - call this when finished using PortAudio. + This function deallocates all resources allocated by PortAudio since it was + initialized by a call to Pa_Initialize(). In cases where Pa_Initialise() has + been called multiple times, each call must be matched with a corresponding call + to Pa_Terminate(). The final matching call to Pa_Terminate() will automatically + close any PortAudio streams that are still open. + + Pa_Terminate() MUST be called before exiting a program which uses PortAudio. + Failure to do so may result in serious resource leaks, such as audio devices + not being available until the next reboot. + + @return paNoError if successful, otherwise an error code indicating the cause + of failure. + + @see Pa_Initialize +*/ +PaError Pa_Terminate( void ); + + + +/** The type used to refer to audio devices. Values of this type usually + range from 0 to (Pa_GetDeviceCount()-1), and may also take on the PaNoDevice + and paUseHostApiSpecificDeviceSpecification values. + + @see Pa_GetDeviceCount, paNoDevice, paUseHostApiSpecificDeviceSpecification +*/ +typedef int PaDeviceIndex; + + +/** A special PaDeviceIndex value indicating that no device is available, + or should be used. + + @see PaDeviceIndex +*/ +#define paNoDevice ((PaDeviceIndex)-1) + + +/** A special PaDeviceIndex value indicating that the device(s) to be used + are specified in the host api specific stream info structure. + + @see PaDeviceIndex +*/ +#define paUseHostApiSpecificDeviceSpecification ((PaDeviceIndex)-2) + + +/* Host API enumeration mechanism */ + +/** The type used to enumerate to host APIs at runtime. Values of this type + range from 0 to (Pa_GetHostApiCount()-1). + + @see Pa_GetHostApiCount +*/ +typedef int PaHostApiIndex; + + +/** Retrieve the number of available host APIs. Even if a host API is + available it may have no devices available. + + @return A non-negative value indicating the number of available host APIs + or, a PaErrorCode (which are always negative) if PortAudio is not initialized + or an error is encountered. + + @see PaHostApiIndex +*/ +PaHostApiIndex Pa_GetHostApiCount( void ); + + +/** Retrieve the index of the default host API. The default host API will be + the lowest common denominator host API on the current platform and is + unlikely to provide the best performance. + + @return A non-negative value ranging from 0 to (Pa_GetHostApiCount()-1) + indicating the default host API index or, a PaErrorCode (which are always + negative) if PortAudio is not initialized or an error is encountered. +*/ +PaHostApiIndex Pa_GetDefaultHostApi( void ); + + +/** Unchanging unique identifiers for each supported host API. This type + is used in the PaHostApiInfo structure. The values are guaranteed to be + unique and to never change, thus allowing code to be written that + conditionally uses host API specific extensions. + + New type ids will be allocated when support for a host API reaches + "public alpha" status, prior to that developers should use the + paInDevelopment type id. + + @see PaHostApiInfo +*/ +typedef enum PaHostApiTypeId +{ + paInDevelopment=0, /* use while developing support for a new host API */ + paDirectSound=1, + paMME=2, + paASIO=3, + paSoundManager=4, + paCoreAudio=5, + paOSS=7, + paALSA=8, + paAL=9, + paBeOS=10, + paWDMKS=11, + paJACK=12, + paWASAPI=13, + paAudioScienceHPI=14 +} PaHostApiTypeId; + + +/** A structure containing information about a particular host API. */ + +typedef struct PaHostApiInfo +{ + /** this is struct version 1 */ + int structVersion; + /** The well known unique identifier of this host API @see PaHostApiTypeId */ + PaHostApiTypeId type; + /** A textual description of the host API for display on user interfaces. */ + const char *name; + + /** The number of devices belonging to this host API. This field may be + used in conjunction with Pa_HostApiDeviceIndexToDeviceIndex() to enumerate + all devices for this host API. + @see Pa_HostApiDeviceIndexToDeviceIndex + */ + int deviceCount; + + /** The default input device for this host API. The value will be a + device index ranging from 0 to (Pa_GetDeviceCount()-1), or paNoDevice + if no default input device is available. + */ + PaDeviceIndex defaultInputDevice; + + /** The default output device for this host API. The value will be a + device index ranging from 0 to (Pa_GetDeviceCount()-1), or paNoDevice + if no default output device is available. + */ + PaDeviceIndex defaultOutputDevice; + +} PaHostApiInfo; + + +/** Retrieve a pointer to a structure containing information about a specific + host Api. + + @param hostApi A valid host API index ranging from 0 to (Pa_GetHostApiCount()-1) + + @return A pointer to an immutable PaHostApiInfo structure describing + a specific host API. If the hostApi parameter is out of range or an error + is encountered, the function returns NULL. + + The returned structure is owned by the PortAudio implementation and must not + be manipulated or freed. The pointer is only guaranteed to be valid between + calls to Pa_Initialize() and Pa_Terminate(). +*/ +const PaHostApiInfo * Pa_GetHostApiInfo( PaHostApiIndex hostApi ); + + +/** Convert a static host API unique identifier, into a runtime + host API index. + + @param type A unique host API identifier belonging to the PaHostApiTypeId + enumeration. + + @return A valid PaHostApiIndex ranging from 0 to (Pa_GetHostApiCount()-1) or, + a PaErrorCode (which are always negative) if PortAudio is not initialized + or an error is encountered. + + The paHostApiNotFound error code indicates that the host API specified by the + type parameter is not available. + + @see PaHostApiTypeId +*/ +PaHostApiIndex Pa_HostApiTypeIdToHostApiIndex( PaHostApiTypeId type ); + + +/** Convert a host-API-specific device index to standard PortAudio device index. + This function may be used in conjunction with the deviceCount field of + PaHostApiInfo to enumerate all devices for the specified host API. + + @param hostApi A valid host API index ranging from 0 to (Pa_GetHostApiCount()-1) + + @param hostApiDeviceIndex A valid per-host device index in the range + 0 to (Pa_GetHostApiInfo(hostApi)->deviceCount-1) + + @return A non-negative PaDeviceIndex ranging from 0 to (Pa_GetDeviceCount()-1) + or, a PaErrorCode (which are always negative) if PortAudio is not initialized + or an error is encountered. + + A paInvalidHostApi error code indicates that the host API index specified by + the hostApi parameter is out of range. + + A paInvalidDevice error code indicates that the hostApiDeviceIndex parameter + is out of range. + + @see PaHostApiInfo +*/ +PaDeviceIndex Pa_HostApiDeviceIndexToDeviceIndex( PaHostApiIndex hostApi, + int hostApiDeviceIndex ); + + + +/** Structure used to return information about a host error condition. +*/ +typedef struct PaHostErrorInfo{ + PaHostApiTypeId hostApiType; /**< the host API which returned the error code */ + long errorCode; /**< the error code returned */ + const char *errorText; /**< a textual description of the error if available, otherwise a zero-length string */ +}PaHostErrorInfo; + + +/** Return information about the last host error encountered. The error + information returned by Pa_GetLastHostErrorInfo() will never be modified + asynchronously by errors occurring in other PortAudio owned threads + (such as the thread that manages the stream callback.) + + This function is provided as a last resort, primarily to enhance debugging + by providing clients with access to all available error information. + + @return A pointer to an immutable structure constraining information about + the host error. The values in this structure will only be valid if a + PortAudio function has previously returned the paUnanticipatedHostError + error code. +*/ +const PaHostErrorInfo* Pa_GetLastHostErrorInfo( void ); + + + +/* Device enumeration and capabilities */ + +/** Retrieve the number of available devices. The number of available devices + may be zero. + + @return A non-negative value indicating the number of available devices or, + a PaErrorCode (which are always negative) if PortAudio is not initialized + or an error is encountered. +*/ +PaDeviceIndex Pa_GetDeviceCount( void ); + + +/** Retrieve the index of the default input device. The result can be + used in the inputDevice parameter to Pa_OpenStream(). + + @return The default input device index for the default host API, or paNoDevice + if no default input device is available or an error was encountered. +*/ +PaDeviceIndex Pa_GetDefaultInputDevice( void ); + + +/** Retrieve the index of the default output device. The result can be + used in the outputDevice parameter to Pa_OpenStream(). + + @return The default output device index for the default host API, or paNoDevice + if no default output device is available or an error was encountered. + + @note + On the PC, the user can specify a default device by + setting an environment variable. For example, to use device #1. +
+ set PA_RECOMMENDED_OUTPUT_DEVICE=1
+
+ The user should first determine the available device ids by using + the supplied application "pa_devs". +*/ +PaDeviceIndex Pa_GetDefaultOutputDevice( void ); + + +/** The type used to represent monotonic time in seconds. PaTime is + used for the fields of the PaStreamCallbackTimeInfo argument to the + PaStreamCallback and as the result of Pa_GetStreamTime(). + + PaTime values have unspecified origin. + + @see PaStreamCallback, PaStreamCallbackTimeInfo, Pa_GetStreamTime +*/ +typedef double PaTime; + + +/** A type used to specify one or more sample formats. Each value indicates + a possible format for sound data passed to and from the stream callback, + Pa_ReadStream and Pa_WriteStream. + + The standard formats paFloat32, paInt16, paInt32, paInt24, paInt8 + and aUInt8 are usually implemented by all implementations. + + The floating point representation (paFloat32) uses +1.0 and -1.0 as the + maximum and minimum respectively. + + paUInt8 is an unsigned 8 bit format where 128 is considered "ground" + + The paNonInterleaved flag indicates that audio data is passed as an array + of pointers to separate buffers, one buffer for each channel. Usually, + when this flag is not used, audio data is passed as a single buffer with + all channels interleaved. + + @see Pa_OpenStream, Pa_OpenDefaultStream, PaDeviceInfo + @see paFloat32, paInt16, paInt32, paInt24, paInt8 + @see paUInt8, paCustomFormat, paNonInterleaved +*/ +typedef unsigned long PaSampleFormat; + + +#define paFloat32 ((PaSampleFormat) 0x00000001) /**< @see PaSampleFormat */ +#define paInt32 ((PaSampleFormat) 0x00000002) /**< @see PaSampleFormat */ +#define paInt24 ((PaSampleFormat) 0x00000004) /**< Packed 24 bit format. @see PaSampleFormat */ +#define paInt16 ((PaSampleFormat) 0x00000008) /**< @see PaSampleFormat */ +#define paInt8 ((PaSampleFormat) 0x00000010) /**< @see PaSampleFormat */ +#define paUInt8 ((PaSampleFormat) 0x00000020) /**< @see PaSampleFormat */ +#define paCustomFormat ((PaSampleFormat) 0x00010000) /**< @see PaSampleFormat */ + +#define paNonInterleaved ((PaSampleFormat) 0x80000000) /**< @see PaSampleFormat */ + +/** A structure providing information and capabilities of PortAudio devices. + Devices may support input, output or both input and output. +*/ +typedef struct PaDeviceInfo +{ + int structVersion; /* this is struct version 2 */ + const char *name; + PaHostApiIndex hostApi; /**< note this is a host API index, not a type id*/ + + int maxInputChannels; + int maxOutputChannels; + + /** Default latency values for interactive performance. */ + PaTime defaultLowInputLatency; + PaTime defaultLowOutputLatency; + /** Default latency values for robust non-interactive applications (eg. playing sound files). */ + PaTime defaultHighInputLatency; + PaTime defaultHighOutputLatency; + + double defaultSampleRate; +} PaDeviceInfo; + + +/** Retrieve a pointer to a PaDeviceInfo structure containing information + about the specified device. + @return A pointer to an immutable PaDeviceInfo structure. If the device + parameter is out of range the function returns NULL. + + @param device A valid device index in the range 0 to (Pa_GetDeviceCount()-1) + + @note PortAudio manages the memory referenced by the returned pointer, + the client must not manipulate or free the memory. The pointer is only + guaranteed to be valid between calls to Pa_Initialize() and Pa_Terminate(). + + @see PaDeviceInfo, PaDeviceIndex +*/ +const PaDeviceInfo* Pa_GetDeviceInfo( PaDeviceIndex device ); + + +/** Parameters for one direction (input or output) of a stream. +*/ +typedef struct PaStreamParameters +{ + /** A valid device index in the range 0 to (Pa_GetDeviceCount()-1) + specifying the device to be used or the special constant + paUseHostApiSpecificDeviceSpecification which indicates that the actual + device(s) to use are specified in hostApiSpecificStreamInfo. + This field must not be set to paNoDevice. + */ + PaDeviceIndex device; + + /** The number of channels of sound to be delivered to the + stream callback or accessed by Pa_ReadStream() or Pa_WriteStream(). + It can range from 1 to the value of maxInputChannels in the + PaDeviceInfo record for the device specified by the device parameter. + */ + int channelCount; + + /** The sample format of the buffer provided to the stream callback, + a_ReadStream() or Pa_WriteStream(). It may be any of the formats described + by the PaSampleFormat enumeration. + */ + PaSampleFormat sampleFormat; + + /** The desired latency in seconds. Where practical, implementations should + configure their latency based on these parameters, otherwise they may + choose the closest viable latency instead. Unless the suggested latency + is greater than the absolute upper limit for the device implementations + should round the suggestedLatency up to the next practical value - ie to + provide an equal or higher latency than suggestedLatency wherever possible. + Actual latency values for an open stream may be retrieved using the + inputLatency and outputLatency fields of the PaStreamInfo structure + returned by Pa_GetStreamInfo(). + @see default*Latency in PaDeviceInfo, *Latency in PaStreamInfo + */ + PaTime suggestedLatency; + + /** An optional pointer to a host api specific data structure + containing additional information for device setup and/or stream processing. + hostApiSpecificStreamInfo is never required for correct operation, + if not used it should be set to NULL. + */ + void *hostApiSpecificStreamInfo; + +} PaStreamParameters; + + +/** Return code for Pa_IsFormatSupported indicating success. */ +#define paFormatIsSupported (0) + +/** Determine whether it would be possible to open a stream with the specified + parameters. + + @param inputParameters A structure that describes the input parameters used to + open a stream. The suggestedLatency field is ignored. See PaStreamParameters + for a description of these parameters. inputParameters must be NULL for + output-only streams. + + @param outputParameters A structure that describes the output parameters used + to open a stream. The suggestedLatency field is ignored. See PaStreamParameters + for a description of these parameters. outputParameters must be NULL for + input-only streams. + + @param sampleRate The required sampleRate. For full-duplex streams it is the + sample rate for both input and output + + @return Returns 0 if the format is supported, and an error code indicating why + the format is not supported otherwise. The constant paFormatIsSupported is + provided to compare with the return value for success. + + @see paFormatIsSupported, PaStreamParameters +*/ +PaError Pa_IsFormatSupported( const PaStreamParameters *inputParameters, + const PaStreamParameters *outputParameters, + double sampleRate ); + + + +/* Streaming types and functions */ + + +/** + A single PaStream can provide multiple channels of real-time + streaming audio input and output to a client application. A stream + provides access to audio hardware represented by one or more + PaDevices. Depending on the underlying Host API, it may be possible + to open multiple streams using the same device, however this behavior + is implementation defined. Portable applications should assume that + a PaDevice may be simultaneously used by at most one PaStream. + + Pointers to PaStream objects are passed between PortAudio functions that + operate on streams. + + @see Pa_OpenStream, Pa_OpenDefaultStream, Pa_OpenDefaultStream, Pa_CloseStream, + Pa_StartStream, Pa_StopStream, Pa_AbortStream, Pa_IsStreamActive, + Pa_GetStreamTime, Pa_GetStreamCpuLoad + +*/ +typedef void PaStream; + + +/** Can be passed as the framesPerBuffer parameter to Pa_OpenStream() + or Pa_OpenDefaultStream() to indicate that the stream callback will + accept buffers of any size. +*/ +#define paFramesPerBufferUnspecified (0) + + +/** Flags used to control the behavior of a stream. They are passed as + parameters to Pa_OpenStream or Pa_OpenDefaultStream. Multiple flags may be + ORed together. + + @see Pa_OpenStream, Pa_OpenDefaultStream + @see paNoFlag, paClipOff, paDitherOff, paNeverDropInput, + paPrimeOutputBuffersUsingStreamCallback, paPlatformSpecificFlags +*/ +typedef unsigned long PaStreamFlags; + +/** @see PaStreamFlags */ +#define paNoFlag ((PaStreamFlags) 0) + +/** Disable default clipping of out of range samples. + @see PaStreamFlags +*/ +#define paClipOff ((PaStreamFlags) 0x00000001) + +/** Disable default dithering. + @see PaStreamFlags +*/ +#define paDitherOff ((PaStreamFlags) 0x00000002) + +/** Flag requests that where possible a full duplex stream will not discard + overflowed input samples without calling the stream callback. This flag is + only valid for full duplex callback streams and only when used in combination + with the paFramesPerBufferUnspecified (0) framesPerBuffer parameter. Using + this flag incorrectly results in a paInvalidFlag error being returned from + Pa_OpenStream and Pa_OpenDefaultStream. + + @see PaStreamFlags, paFramesPerBufferUnspecified +*/ +#define paNeverDropInput ((PaStreamFlags) 0x00000004) + +/** Call the stream callback to fill initial output buffers, rather than the + default behavior of priming the buffers with zeros (silence). This flag has + no effect for input-only and blocking read/write streams. + + @see PaStreamFlags +*/ +#define paPrimeOutputBuffersUsingStreamCallback ((PaStreamFlags) 0x00000008) + +/** A mask specifying the platform specific bits. + @see PaStreamFlags +*/ +#define paPlatformSpecificFlags ((PaStreamFlags)0xFFFF0000) + +/** + Timing information for the buffers passed to the stream callback. + + Time values are expressed in seconds and are synchronised with the time base used by Pa_GetStreamTime() for the associated stream. + + @see PaStreamCallback, Pa_GetStreamTime +*/ +typedef struct PaStreamCallbackTimeInfo{ + PaTime inputBufferAdcTime; /**< The time when the first sample of the input buffer was captured at the ADC input */ + PaTime currentTime; /**< The time when the stream callback was invoked */ + PaTime outputBufferDacTime; /**< The time when the first sample of the output buffer will output the DAC */ +} PaStreamCallbackTimeInfo; + + +/** + Flag bit constants for the statusFlags to PaStreamCallback. + + @see paInputUnderflow, paInputOverflow, paOutputUnderflow, paOutputOverflow, + paPrimingOutput +*/ +typedef unsigned long PaStreamCallbackFlags; + +/** In a stream opened with paFramesPerBufferUnspecified, indicates that + input data is all silence (zeros) because no real data is available. In a + stream opened without paFramesPerBufferUnspecified, it indicates that one or + more zero samples have been inserted into the input buffer to compensate + for an input underflow. + @see PaStreamCallbackFlags +*/ +#define paInputUnderflow ((PaStreamCallbackFlags) 0x00000001) + +/** In a stream opened with paFramesPerBufferUnspecified, indicates that data + prior to the first sample of the input buffer was discarded due to an + overflow, possibly because the stream callback is using too much CPU time. + Otherwise indicates that data prior to one or more samples in the + input buffer was discarded. + @see PaStreamCallbackFlags +*/ +#define paInputOverflow ((PaStreamCallbackFlags) 0x00000002) + +/** Indicates that output data (or a gap) was inserted, possibly because the + stream callback is using too much CPU time. + @see PaStreamCallbackFlags +*/ +#define paOutputUnderflow ((PaStreamCallbackFlags) 0x00000004) + +/** Indicates that output data will be discarded because no room is available. + @see PaStreamCallbackFlags +*/ +#define paOutputOverflow ((PaStreamCallbackFlags) 0x00000008) + +/** Some of all of the output data will be used to prime the stream, input + data may be zero. + @see PaStreamCallbackFlags +*/ +#define paPrimingOutput ((PaStreamCallbackFlags) 0x00000010) + +/** + Allowable return values for the PaStreamCallback. + @see PaStreamCallback +*/ +typedef enum PaStreamCallbackResult +{ + paContinue=0, /**< Signal that the stream should continue invoking the callback and processing audio. */ + paComplete=1, /**< Signal that the stream should stop invoking the callback and finish once all output samples have played. */ + paAbort=2 /**< Signal that the stream should stop invoking the callback and finish as soon as possible. */ +} PaStreamCallbackResult; + + +/** + Functions of type PaStreamCallback are implemented by PortAudio clients. + They consume, process or generate audio in response to requests from an + active PortAudio stream. + + When a stream is running, PortAudio calls the stream callback periodically. + The callback function is responsible for processing buffers of audio samples + passed via the input and output parameters. + + The PortAudio stream callback runs at very high or real-time priority. + It is required to consistently meet its time deadlines. Do not allocate + memory, access the file system, call library functions or call other functions + from the stream callback that may block or take an unpredictable amount of + time to complete. + + In order for a stream to maintain glitch-free operation the callback + must consume and return audio data faster than it is recorded and/or + played. PortAudio anticipates that each callback invocation may execute for + a duration approaching the duration of frameCount audio frames at the stream + sample rate. It is reasonable to expect to be able to utilise 70% or more of + the available CPU time in the PortAudio callback. However, due to buffer size + adaption and other factors, not all host APIs are able to guarantee audio + stability under heavy CPU load with arbitrary fixed callback buffer sizes. + When high callback CPU utilisation is required the most robust behavior + can be achieved by using paFramesPerBufferUnspecified as the + Pa_OpenStream() framesPerBuffer parameter. + + @param input and @param output are either arrays of interleaved samples or; + if non-interleaved samples were requested using the paNonInterleaved sample + format flag, an array of buffer pointers, one non-interleaved buffer for + each channel. + + The format, packing and number of channels used by the buffers are + determined by parameters to Pa_OpenStream(). + + @param frameCount The number of sample frames to be processed by + the stream callback. + + @param timeInfo Timestamps indicating the ADC capture time of the first sample + in the input buffer, the DAC output time of the first sample in the output buffer + and the time the callback was invoked. + See PaStreamCallbackTimeInfo and Pa_GetStreamTime() + + @param statusFlags Flags indicating whether input and/or output buffers + have been inserted or will be dropped to overcome underflow or overflow + conditions. + + @param userData The value of a user supplied pointer passed to + Pa_OpenStream() intended for storing synthesis data etc. + + @return + The stream callback should return one of the values in the + ::PaStreamCallbackResult enumeration. To ensure that the callback continues + to be called, it should return paContinue (0). Either paComplete or paAbort + can be returned to finish stream processing, after either of these values is + returned the callback will not be called again. If paAbort is returned the + stream will finish as soon as possible. If paComplete is returned, the stream + will continue until all buffers generated by the callback have been played. + This may be useful in applications such as soundfile players where a specific + duration of output is required. However, it is not necessary to utilize this + mechanism as Pa_StopStream(), Pa_AbortStream() or Pa_CloseStream() can also + be used to stop the stream. The callback must always fill the entire output + buffer irrespective of its return value. + + @see Pa_OpenStream, Pa_OpenDefaultStream + + @note With the exception of Pa_GetStreamCpuLoad() it is not permissible to call + PortAudio API functions from within the stream callback. +*/ +typedef int PaStreamCallback( + const void *input, void *output, + unsigned long frameCount, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData ); + + +/** Opens a stream for either input, output or both. + + @param stream The address of a PaStream pointer which will receive + a pointer to the newly opened stream. + + @param inputParameters A structure that describes the input parameters used by + the opened stream. See PaStreamParameters for a description of these parameters. + inputParameters must be NULL for output-only streams. + + @param outputParameters A structure that describes the output parameters used by + the opened stream. See PaStreamParameters for a description of these parameters. + outputParameters must be NULL for input-only streams. + + @param sampleRate The desired sampleRate. For full-duplex streams it is the + sample rate for both input and output + + @param framesPerBuffer The number of frames passed to the stream callback + function, or the preferred block granularity for a blocking read/write stream. + The special value paFramesPerBufferUnspecified (0) may be used to request that + the stream callback will receive an optimal (and possibly varying) number of + frames based on host requirements and the requested latency settings. + Note: With some host APIs, the use of non-zero framesPerBuffer for a callback + stream may introduce an additional layer of buffering which could introduce + additional latency. PortAudio guarantees that the additional latency + will be kept to the theoretical minimum however, it is strongly recommended + that a non-zero framesPerBuffer value only be used when your algorithm + requires a fixed number of frames per stream callback. + + @param streamFlags Flags which modify the behavior of the streaming process. + This parameter may contain a combination of flags ORed together. Some flags may + only be relevant to certain buffer formats. + + @param streamCallback A pointer to a client supplied function that is responsible + for processing and filling input and output buffers. If this parameter is NULL + the stream will be opened in 'blocking read/write' mode. In blocking mode, + the client can receive sample data using Pa_ReadStream and write sample data + using Pa_WriteStream, the number of samples that may be read or written + without blocking is returned by Pa_GetStreamReadAvailable and + Pa_GetStreamWriteAvailable respectively. + + @param userData A client supplied pointer which is passed to the stream callback + function. It could for example, contain a pointer to instance data necessary + for processing the audio buffers. This parameter is ignored if streamCallback + is NULL. + + @return + Upon success Pa_OpenStream() returns paNoError and places a pointer to a + valid PaStream in the stream argument. The stream is inactive (stopped). + If a call to Pa_OpenStream() fails, a non-zero error code is returned (see + PaError for possible error codes) and the value of stream is invalid. + + @see PaStreamParameters, PaStreamCallback, Pa_ReadStream, Pa_WriteStream, + Pa_GetStreamReadAvailable, Pa_GetStreamWriteAvailable +*/ +PaError Pa_OpenStream( PaStream** stream, + const PaStreamParameters *inputParameters, + const PaStreamParameters *outputParameters, + double sampleRate, + unsigned long framesPerBuffer, + PaStreamFlags streamFlags, + PaStreamCallback *streamCallback, + void *userData ); + + +/** A simplified version of Pa_OpenStream() that opens the default input + and/or output devices. + + @param stream The address of a PaStream pointer which will receive + a pointer to the newly opened stream. + + @param numInputChannels The number of channels of sound that will be supplied + to the stream callback or returned by Pa_ReadStream. It can range from 1 to + the value of maxInputChannels in the PaDeviceInfo record for the default input + device. If 0 the stream is opened as an output-only stream. + + @param numOutputChannels The number of channels of sound to be delivered to the + stream callback or passed to Pa_WriteStream. It can range from 1 to the value + of maxOutputChannels in the PaDeviceInfo record for the default output device. + If 0 the stream is opened as an output-only stream. + + @param sampleFormat The sample format of both the input and output buffers + provided to the callback or passed to and from Pa_ReadStream and Pa_WriteStream. + sampleFormat may be any of the formats described by the PaSampleFormat + enumeration. + + @param sampleRate Same as Pa_OpenStream parameter of the same name. + @param framesPerBuffer Same as Pa_OpenStream parameter of the same name. + @param streamCallback Same as Pa_OpenStream parameter of the same name. + @param userData Same as Pa_OpenStream parameter of the same name. + + @return As for Pa_OpenStream + + @see Pa_OpenStream, PaStreamCallback +*/ +PaError Pa_OpenDefaultStream( PaStream** stream, + int numInputChannels, + int numOutputChannels, + PaSampleFormat sampleFormat, + double sampleRate, + unsigned long framesPerBuffer, + PaStreamCallback *streamCallback, + void *userData ); + + +/** Closes an audio stream. If the audio stream is active it + discards any pending buffers as if Pa_AbortStream() had been called. +*/ +PaError Pa_CloseStream( PaStream *stream ); + + +/** Functions of type PaStreamFinishedCallback are implemented by PortAudio + clients. They can be registered with a stream using the Pa_SetStreamFinishedCallback + function. Once registered they are called when the stream becomes inactive + (ie once a call to Pa_StopStream() will not block). + A stream will become inactive after the stream callback returns non-zero, + or when Pa_StopStream or Pa_AbortStream is called. For a stream providing audio + output, if the stream callback returns paComplete, or Pa_StopStream() is called, + the stream finished callback will not be called until all generated sample data + has been played. + + @param userData The userData parameter supplied to Pa_OpenStream() + + @see Pa_SetStreamFinishedCallback +*/ +typedef void PaStreamFinishedCallback( void *userData ); + + +/** Register a stream finished callback function which will be called when the + stream becomes inactive. See the description of PaStreamFinishedCallback for + further details about when the callback will be called. + + @param stream a pointer to a PaStream that is in the stopped state - if the + stream is not stopped, the stream's finished callback will remain unchanged + and an error code will be returned. + + @param streamFinishedCallback a pointer to a function with the same signature + as PaStreamFinishedCallback, that will be called when the stream becomes + inactive. Passing NULL for this parameter will un-register a previously + registered stream finished callback function. + + @return on success returns paNoError, otherwise an error code indicating the cause + of the error. + + @see PaStreamFinishedCallback +*/ +PaError Pa_SetStreamFinishedCallback( PaStream *stream, PaStreamFinishedCallback* streamFinishedCallback ); + + +/** Commences audio processing. +*/ +PaError Pa_StartStream( PaStream *stream ); + + +/** Terminates audio processing. It waits until all pending + audio buffers have been played before it returns. +*/ +PaError Pa_StopStream( PaStream *stream ); + + +/** Terminates audio processing immediately without waiting for pending + buffers to complete. +*/ +PaError Pa_AbortStream( PaStream *stream ); + + +/** Determine whether the stream is stopped. + A stream is considered to be stopped prior to a successful call to + Pa_StartStream and after a successful call to Pa_StopStream or Pa_AbortStream. + If a stream callback returns a value other than paContinue the stream is NOT + considered to be stopped. + + @return Returns one (1) when the stream is stopped, zero (0) when + the stream is running or, a PaErrorCode (which are always negative) if + PortAudio is not initialized or an error is encountered. + + @see Pa_StopStream, Pa_AbortStream, Pa_IsStreamActive +*/ +PaError Pa_IsStreamStopped( PaStream *stream ); + + +/** Determine whether the stream is active. + A stream is active after a successful call to Pa_StartStream(), until it + becomes inactive either as a result of a call to Pa_StopStream() or + Pa_AbortStream(), or as a result of a return value other than paContinue from + the stream callback. In the latter case, the stream is considered inactive + after the last buffer has finished playing. + + @return Returns one (1) when the stream is active (ie playing or recording + audio), zero (0) when not playing or, a PaErrorCode (which are always negative) + if PortAudio is not initialized or an error is encountered. + + @see Pa_StopStream, Pa_AbortStream, Pa_IsStreamStopped +*/ +PaError Pa_IsStreamActive( PaStream *stream ); + + + +/** A structure containing unchanging information about an open stream. + @see Pa_GetStreamInfo +*/ + +typedef struct PaStreamInfo +{ + /** this is struct version 1 */ + int structVersion; + + /** The input latency of the stream in seconds. This value provides the most + accurate estimate of input latency available to the implementation. It may + differ significantly from the suggestedLatency value passed to Pa_OpenStream(). + The value of this field will be zero (0.) for output-only streams. + @see PaTime + */ + PaTime inputLatency; + + /** The output latency of the stream in seconds. This value provides the most + accurate estimate of output latency available to the implementation. It may + differ significantly from the suggestedLatency value passed to Pa_OpenStream(). + The value of this field will be zero (0.) for input-only streams. + @see PaTime + */ + PaTime outputLatency; + + /** The sample rate of the stream in Hertz (samples per second). In cases + where the hardware sample rate is inaccurate and PortAudio is aware of it, + the value of this field may be different from the sampleRate parameter + passed to Pa_OpenStream(). If information about the actual hardware sample + rate is not available, this field will have the same value as the sampleRate + parameter passed to Pa_OpenStream(). + */ + double sampleRate; + +} PaStreamInfo; + + +/** Retrieve a pointer to a PaStreamInfo structure containing information + about the specified stream. + @return A pointer to an immutable PaStreamInfo structure. If the stream + parameter is invalid, or an error is encountered, the function returns NULL. + + @param stream A pointer to an open stream previously created with Pa_OpenStream. + + @note PortAudio manages the memory referenced by the returned pointer, + the client must not manipulate or free the memory. The pointer is only + guaranteed to be valid until the specified stream is closed. + + @see PaStreamInfo +*/ +const PaStreamInfo* Pa_GetStreamInfo( PaStream *stream ); + + +/** Returns the current time in seconds for a stream according to the same clock used + to generate callback PaStreamCallbackTimeInfo timestamps. The time values are + monotonically increasing and have unspecified origin. + + Pa_GetStreamTime returns valid time values for the entire life of the stream, + from when the stream is opened until it is closed. Starting and stopping the stream + does not affect the passage of time returned by Pa_GetStreamTime. + + This time may be used for synchronizing other events to the audio stream, for + example synchronizing audio to MIDI. + + @return The stream's current time in seconds, or 0 if an error occurred. + + @see PaTime, PaStreamCallback, PaStreamCallbackTimeInfo +*/ +PaTime Pa_GetStreamTime( PaStream *stream ); + + +/** Retrieve CPU usage information for the specified stream. + The "CPU Load" is a fraction of total CPU time consumed by a callback stream's + audio processing routines including, but not limited to the client supplied + stream callback. This function does not work with blocking read/write streams. + + This function may be called from the stream callback function or the + application. + + @return + A floating point value, typically between 0.0 and 1.0, where 1.0 indicates + that the stream callback is consuming the maximum number of CPU cycles possible + to maintain real-time operation. A value of 0.5 would imply that PortAudio and + the stream callback was consuming roughly 50% of the available CPU time. The + return value may exceed 1.0. A value of 0.0 will always be returned for a + blocking read/write stream, or if an error occurs. +*/ +double Pa_GetStreamCpuLoad( PaStream* stream ); + + +/** Read samples from an input stream. The function doesn't return until + the entire buffer has been filled - this may involve waiting for the operating + system to supply the data. + + @param stream A pointer to an open stream previously created with Pa_OpenStream. + + @param buffer A pointer to a buffer of sample frames. The buffer contains + samples in the format specified by the inputParameters->sampleFormat field + used to open the stream, and the number of channels specified by + inputParameters->numChannels. If non-interleaved samples were requested using + the paNonInterleaved sample format flag, buffer is a pointer to the first element + of an array of buffer pointers, one non-interleaved buffer for each channel. + + @param frames The number of frames to be read into buffer. This parameter + is not constrained to a specific range, however high performance applications + will want to match this parameter to the framesPerBuffer parameter used + when opening the stream. + + @return On success PaNoError will be returned, or PaInputOverflowed if input + data was discarded by PortAudio after the previous call and before this call. +*/ +PaError Pa_ReadStream( PaStream* stream, + void *buffer, + unsigned long frames ); + + +/** Write samples to an output stream. This function doesn't return until the + entire buffer has been written - this may involve waiting for the operating + system to consume the data. + + @param stream A pointer to an open stream previously created with Pa_OpenStream. + + @param buffer A pointer to a buffer of sample frames. The buffer contains + samples in the format specified by the outputParameters->sampleFormat field + used to open the stream, and the number of channels specified by + outputParameters->numChannels. If non-interleaved samples were requested using + the paNonInterleaved sample format flag, buffer is a pointer to the first element + of an array of buffer pointers, one non-interleaved buffer for each channel. + + @param frames The number of frames to be written from buffer. This parameter + is not constrained to a specific range, however high performance applications + will want to match this parameter to the framesPerBuffer parameter used + when opening the stream. + + @return On success PaNoError will be returned, or paOutputUnderflowed if + additional output data was inserted after the previous call and before this + call. +*/ +PaError Pa_WriteStream( PaStream* stream, + const void *buffer, + unsigned long frames ); + + +/** Retrieve the number of frames that can be read from the stream without + waiting. + + @return Returns a non-negative value representing the maximum number of frames + that can be read from the stream without blocking or busy waiting or, a + PaErrorCode (which are always negative) if PortAudio is not initialized or an + error is encountered. +*/ +signed long Pa_GetStreamReadAvailable( PaStream* stream ); + + +/** Retrieve the number of frames that can be written to the stream without + waiting. + + @return Returns a non-negative value representing the maximum number of frames + that can be written to the stream without blocking or busy waiting or, a + PaErrorCode (which are always negative) if PortAudio is not initialized or an + error is encountered. +*/ +signed long Pa_GetStreamWriteAvailable( PaStream* stream ); + + +/* Miscellaneous utilities */ + + +/** Retrieve the size of a given sample format in bytes. + + @return The size in bytes of a single sample in the specified format, + or paSampleFormatNotSupported if the format is not supported. +*/ +PaError Pa_GetSampleSize( PaSampleFormat format ); + + +/** Put the caller to sleep for at least 'msec' milliseconds. This function is + provided only as a convenience for authors of portable code (such as the tests + and examples in the PortAudio distribution.) + + The function may sleep longer than requested so don't rely on this for accurate + musical timing. +*/ +void Pa_Sleep( long msec ); + + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ +#endif /* PORTAUDIO_H */ diff --git a/deps/usr/lib/pa_shim_arm-linux-gnueabihf.so b/deps/usr/lib/pa_shim_arm-linux-gnueabihf.so new file mode 100755 index 0000000..fc99772 Binary files /dev/null and b/deps/usr/lib/pa_shim_arm-linux-gnueabihf.so differ diff --git a/deps/usr/lib/pa_shim_i686-linux-gnu.so b/deps/usr/lib/pa_shim_i686-linux-gnu.so new file mode 100755 index 0000000..4a3af96 Binary files /dev/null and b/deps/usr/lib/pa_shim_i686-linux-gnu.so differ diff --git a/deps/usr/lib/pa_shim_i686-w64-mingw32.dll b/deps/usr/lib/pa_shim_i686-w64-mingw32.dll new file mode 100755 index 0000000..956032b Binary files /dev/null and b/deps/usr/lib/pa_shim_i686-w64-mingw32.dll differ diff --git a/deps/usr/lib/pa_shim_powerpc64le-linux-gnu.so b/deps/usr/lib/pa_shim_powerpc64le-linux-gnu.so new file mode 100755 index 0000000..90373e4 Binary files /dev/null and b/deps/usr/lib/pa_shim_powerpc64le-linux-gnu.so differ diff --git a/deps/usr/lib/pa_shim_x86_64-apple-darwin14.dylib b/deps/usr/lib/pa_shim_x86_64-apple-darwin14.dylib new file mode 100755 index 0000000..b0a5322 Binary files /dev/null and b/deps/usr/lib/pa_shim_x86_64-apple-darwin14.dylib differ diff --git a/deps/usr/lib/pa_shim_x86_64-linux-gnu.so b/deps/usr/lib/pa_shim_x86_64-linux-gnu.so new file mode 100755 index 0000000..4b42a0c Binary files /dev/null and b/deps/usr/lib/pa_shim_x86_64-linux-gnu.so differ diff --git a/deps/usr/lib/pa_shim_x86_64-w64-mingw32.dll b/deps/usr/lib/pa_shim_x86_64-w64-mingw32.dll new file mode 100755 index 0000000..1a39c44 Binary files /dev/null and b/deps/usr/lib/pa_shim_x86_64-w64-mingw32.dll differ diff --git a/src/PortAudio.jl b/src/PortAudio.jl index 48a21db..c301b77 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -3,51 +3,52 @@ __precompile__() module PortAudio using SampledSignals -using Devectorize using RingBuffers -using Compat -import Compat: UTF8String, view +#using Suppressor + +import Base: eltype, show +import Base: close, isopen +import Base: read, read!, write, flush # Get binary dependencies loaded from BinDeps -include( "../deps/deps.jl") +include("../deps/deps.jl") +include("suppressor.jl") +include("pa_shim.jl") include("libportaudio.jl") +function __init__() + init_pa_shim() + global const notifycb_c = cfunction(notifycb, Cint, (Ptr{Void}, )) + # initialize PortAudio on module load + @suppress_err Pa_Initialize() +end + export PortAudioStream # These sizes are all in frames + # the block size is what we request from portaudio if no blocksize is given. # The ringbuffer and pre-fill will be twice the blocksize const DEFAULT_BLOCKSIZE=4096 + # data is passed to and from the ringbuffer in chunks with this many frames # it should be at most the ringbuffer size, and must evenly divide into the # the underlying portaudio buffer size. E.g. if PortAudio is running with a # 2048-frame buffer period, the chunk size can be 2048, 1024, 512, 256, etc. const CHUNKSIZE=128 -function __init__() - # initialize PortAudio on module load - swallow_stderr() do - Pa_Initialize() - end - - # the portaudio callbacks are parametric on the sample type - global const pa_callbacks = Dict{Type, Ptr{Void}}() - - for T in (Float32, Int32, Int16, Int8, UInt8) - pa_callbacks[T] = cfunction(portaudio_callback, Cint, - (Ptr{T}, Ptr{T}, Culong, Ptr{Void}, Culong, - Ptr{CallbackInfo{T}})) - end -end +# ringbuffer to receive errors from the audio processing thread +const ERR_BUFSIZE=512 function versioninfo(io::IO=STDOUT) println(io, Pa_GetVersionText()) - println(io, "Version Number: ", Pa_GetVersion()) + println(io, "Version: ", Pa_GetVersion()) + println(io, "Shim Source Hash: ", shimhash()[1:10]) end type PortAudioDevice - name::UTF8String - hostapi::UTF8String + name::String + hostapi::String maxinchans::Int maxoutchans::Int defaultsamplerate::Float64 @@ -71,23 +72,9 @@ end # not for external use, used in error message printing devnames() = join(["\"$(dev.name)\"" for dev in devices()], "\n") -"""Give a pointer to the given field within a Julia object""" -function fieldptr{T}(obj::T, field::Symbol) - fieldnum = findfirst(fieldnames(T), field) - offset = fieldoffset(T, fieldnum) - FT = fieldtype(T, field) - - Ptr{FT}(pointer_from_objref(obj) + offset) -end - -# we want this to be immutable so we can stack allocate it -immutable CallbackInfo{T} - inchannels::Int - inbuf::LockFreeRingBuffer{T} - outchannels::Int - outbuf::LockFreeRingBuffer{T} - synced::Bool -end +################## +# PortAudioStream +################## type PortAudioStream{T} samplerate::Float64 @@ -95,12 +82,13 @@ type PortAudioStream{T} stream::PaStream sink # untyped because of circular type definition source # untyped because of circular type definition - bufinfo::CallbackInfo{T} # immutable data used in the portaudio callback + errbuf::RingBuffer{pa_shim_errmsg_t} # used to send errors from the portaudio callback + bufinfo::pa_shim_info_t # data used in the portaudio callback # this inner constructor is generally called via the top-level outer # constructor below - function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice, - inchans, outchans, sr, blocksize, synced) + function PortAudioStream{T}(indev::PortAudioDevice, outdev::PortAudioDevice, + inchans, outchans, sr, blocksize, synced) where {T} inchans = inchans == -1 ? indev.maxinchans : inchans outchans = outchans == -1 ? outdev.maxoutchans : outchans inparams = (inchans == 0) ? @@ -113,23 +101,55 @@ type PortAudioStream{T} finalizer(this, close) this.sink = PortAudioSink{T}(outdev.name, this, outchans, blocksize*2) this.source = PortAudioSource{T}(indev.name, this, inchans, blocksize*2) + this.errbuf = RingBuffer{pa_shim_errmsg_t}(1, ERR_BUFSIZE) if synced && inchans > 0 && outchans > 0 # we've got a synchronized duplex stream. initialize with the output buffer full write(this.sink, SampleBuf(zeros(T, blocksize*2, outchans), sr)) end - this.bufinfo = CallbackInfo(inchans, this.source.ringbuf, - outchans, this.sink.ringbuf, synced) - this.stream = swallow_stderr() do - Pa_OpenStream(inparams, outparams, float(sr), blocksize, - paNoFlag, pa_callbacks[T], fieldptr(this, :bufinfo)) - end + # pass NULL for input/output we're not using + this.bufinfo = pa_shim_info_t( + inchans > 0 ? bufpointer(this.source) : C_NULL, + outchans > 0 ? bufpointer(this.sink) : C_NULL, + pointer(this.errbuf), + synced, notifycb_c, + inchans > 0 ? notifyhandle(this.source) : C_NULL, + outchans > 0 ? notifyhandle(this.sink) : C_NULL, + notifyhandle(this.errbuf)) + this.stream = @suppress_err Pa_OpenStream(inparams, outparams, + float(sr), blocksize, + paNoFlag, shim_processcb_c, + this.bufinfo) Pa_StartStream(this.stream) + @async handle_errors(this) this end end +""" + PortAudioStream(inchannels=2, outchannels=2; options...) + PortAudioStream(duplexdevice, inchannels=2, outchannels=2; options...) + PortAudioStream(indevice, outdevice, inchannels=2, outchannels=2; options...) + +Audio devices can either be `PortAudioDevice` instances as returned +by `PortAudio.devices()`, or strings with the device name as reported by the +operating system. If a single `duplexdevice` is given it will be used for both +input and output. If no devices are given the system default devices will be +used. + +Options: + +* `eltype`: Sample type of the audio stream (defaults to Float32) +* `samplerate`: Sample rate (defaults to device sample rate) +* `blocksize`: Size of the blocks that are written to and read from the audio + device. (Defaults to $DEFAULT_BLOCKSIZE) +* `synced`: Determines whether the input and output streams are kept in + sync. If `true`, you must read and write an equal number of + frames, and the round-trip latency is guaranteed constant. If + `false`, you are free to read and write separately, but + overflow or underflow can affect the round-trip latency. +""" # this is the top-level outer constructor that all the other outer constructors # end up calling function PortAudioStream(indev::PortAudioDevice, outdev::PortAudioDevice, @@ -191,7 +211,7 @@ function PortAudioStream(inchans=2, outchans=2; kwargs...) PortAudioStream(indevice, outdevice, inchans, outchans; kwargs...) end -function Base.close(stream::PortAudioStream) +function close(stream::PortAudioStream) if stream.stream != C_NULL Pa_StopStream(stream.stream) Pa_CloseStream(stream.stream) @@ -203,44 +223,74 @@ function Base.close(stream::PortAudioStream) nothing end -Base.isopen(stream::PortAudioStream) = stream.stream != C_NULL +isopen(stream::PortAudioStream) = stream.stream != C_NULL SampledSignals.samplerate(stream::PortAudioStream) = stream.samplerate -Base.eltype{T}(stream::PortAudioStream{T}) = T +eltype{T}(stream::PortAudioStream{T}) = T -Base.read(stream::PortAudioStream, args...) = read(stream.source, args...) -Base.read!(stream::PortAudioStream, args...) = read!(stream.source, args...) -Base.write(stream::PortAudioStream, args...) = write(stream.sink, args...) -Base.write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...) -Base.flush(stream::PortAudioStream) = flush(stream.sink) +read(stream::PortAudioStream, args...) = read(stream.source, args...) +read!(stream::PortAudioStream, args...) = read!(stream.source, args...) +write(stream::PortAudioStream, args...) = write(stream.sink, args...) +write(sink::PortAudioStream, source::PortAudioStream, args...) = write(sink.sink, source.source, args...) +flush(stream::PortAudioStream) = flush(stream.sink) -function Base.show(io::IO, stream::PortAudioStream) +function show(io::IO, stream::PortAudioStream) println(io, typeof(stream)) println(io, " Samplerate: ", samplerate(stream), "Hz") print(io, " Buffer Size: ", stream.blocksize, " frames") if nchannels(stream.sink) > 0 - print(io, "\n ", nchannels(stream.sink), " channel sink: \"", stream.sink.name, "\"") + print(io, "\n ", nchannels(stream.sink), " channel sink: \"", name(stream.sink), "\"") end if nchannels(stream.source) > 0 - print(io, "\n ", nchannels(stream.source), " channel source: \"", stream.source.name, "\"") + print(io, "\n ", nchannels(stream.source), " channel source: \"", name(stream.source), "\"") end end +""" + handle_errors(stream::PortAudioStream) + +Handle errors coming over the error stream from PortAudio. This is run as an +independent task while the stream is active. +""" +function handle_errors(stream::PortAudioStream) + err = Vector{pa_shim_errmsg_t}(1) + while true + nread = read!(stream.errbuf, err) + nread == 1 || break + if err[1] == PA_SHIM_ERRMSG_ERR_OVERFLOW + warn("Error buffer overflowed on stream $(stream.name)") + elseif err[1] == PA_SHIM_ERRMSG_OVERFLOW + # warn("Input overflowed from $(name(stream.source))") + elseif err[1] == PA_SHIM_ERRMSG_UNDERFLOW + # warn("Output underflowed to $(name(stream.sink))") + else + error(""" + Got unrecognized error code $(err[1]) from audio thread for + stream "$(stream.name)". Please file an issue at + https://github.com/juliaaudio/portaudio.jl/issues""") + end + end +end + +################################## +# PortAudioSink & PortAudioSource +################################## + # Define our source and sink types for (TypeName, Super) in ((:PortAudioSink, :SampleSink), (:PortAudioSource, :SampleSource)) @eval type $TypeName{T} <: $Super - name::UTF8String + name::String stream::PortAudioStream{T} chunkbuf::Array{T, 2} - ringbuf::LockFreeRingBuffer{T} + ringbuf::RingBuffer{T} nchannels::Int - function $TypeName(name, stream, channels, ringbufsize) + function $TypeName{T}(name, stream, channels, ringbufsize) where {T} # portaudio data comes in interleaved, so we'll end up transposing # it back and forth to julia column-major chunkbuf = zeros(T, channels, CHUNKSIZE) - ringbuf = LockFreeRingBuffer(T, ringbufsize * channels) + ringbuf = RingBuffer{T}(channels, ringbufsize) new(name, stream, chunkbuf, ringbuf, channels) end end @@ -249,35 +299,31 @@ end SampledSignals.nchannels(s::Union{PortAudioSink, PortAudioSource}) = s.nchannels SampledSignals.samplerate(s::Union{PortAudioSink, PortAudioSource}) = samplerate(s.stream) SampledSignals.blocksize(s::Union{PortAudioSink, PortAudioSource}) = s.stream.blocksize -Base.eltype{T}(::Union{PortAudioSink{T}, PortAudioSource{T}}) = T -Base.close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf) +eltype(::Union{PortAudioSink{T}, PortAudioSource{T}}) where {T} = T +close(s::Union{PortAudioSink, PortAudioSource}) = close(s.ringbuf) +isopen(s::Union{PortAudioSink, PortAudioSource}) = isopen(s.ringbuf) +RingBuffers.notifyhandle(s::Union{PortAudioSink, PortAudioSource}) = notifyhandle(s.ringbuf) +bufpointer(s::Union{PortAudioSink, PortAudioSource}) = pointer(s.ringbuf) +name(s::Union{PortAudioSink, PortAudioSource}) = s.name -function Base.show{T <: Union{PortAudioSink, PortAudioSource}}(io::IO, stream::T) +function show(io::IO, stream::T) where {T <: Union{PortAudioSink, PortAudioSource}} println(io, T, "(\"", stream.name, "\")") print(io, nchannels(stream), " channels") end -function Base.flush(sink::PortAudioSink) - while nwritable(sink.ringbuf) < length(sink.ringbuf) - wait(sink.ringbuf) - end -end +flush(sink::PortAudioSink) = flush(sink.ringbuf) function SampledSignals.unsafe_write(sink::PortAudioSink, buf::Array, frameoffset, framecount) nwritten = 0 while nwritten < framecount - while nwritable(sink.ringbuf) == 0 - wait(sink.ringbuf) - end - # in 0.4 transpose! throws an error if the range is a UInt - writable = div(nwritable(sink.ringbuf), nchannels(sink)) - towrite = Int(min(writable, CHUNKSIZE, framecount-nwritten)) + towrite = min(framecount-nwritten, CHUNKSIZE) # make a buffer of interleaved samples transpose!(view(sink.chunkbuf, :, 1:towrite), view(buf, (1:towrite)+nwritten+frameoffset, :)) - write(sink.ringbuf, sink.chunkbuf, towrite*nchannels(sink)) - - nwritten += towrite + n = write(sink.ringbuf, sink.chunkbuf, towrite) + nwritten += n + # break early if the stream is closed + n < towrite && break end nwritten @@ -286,66 +332,23 @@ end function SampledSignals.unsafe_read!(source::PortAudioSource, buf::Array, frameoffset, framecount) nread = 0 while nread < framecount - while nreadable(source.ringbuf) == 0 - wait(source.ringbuf) - end - # in 0.4 transpose! throws an error if the range is a UInt - readable = div(nreadable(source.ringbuf), nchannels(source)) - toread = Int(min(readable, CHUNKSIZE, framecount-nread)) - read!(source.ringbuf, source.chunkbuf, toread*nchannels(source)) + toread = min(framecount-nread, CHUNKSIZE) + n = read!(source.ringbuf, source.chunkbuf, toread) # de-interleave the samples transpose!(view(buf, (1:toread)+nread+frameoffset, :), view(source.chunkbuf, :, 1:toread)) nread += toread + # break early if the stream is closed + n < toread && break end nread end -# This is the callback function that gets called directly in the PortAudio -# audio thread, so it's critical that it not interact with the Julia GC -function portaudio_callback{T}(inptr::Ptr{T}, outptr::Ptr{T}, - nframes, timeinfo, flags, userdata::Ptr{CallbackInfo{T}}) - info = unsafe_load(userdata) - # if there are no channels, treat it as if we can write as many 0-frame channels as we want - framesreadable = info.outchannels > 0 ? div(nreadable(info.outbuf), info.outchannels) : nframes - frameswritable = info.inchannels > 0 ? div(nwritable(info.inbuf), info.inchannels) : nframes - if info.synced - framesreadable = min(framesreadable, frameswritable) - frameswritable = framesreadable - end - towrite = min(frameswritable, nframes) * info.inchannels - toread = min(framesreadable, nframes) * info.outchannels - - read!(info.outbuf, outptr, toread) - write(info.inbuf, inptr, towrite) - - if framesreadable < nframes - outsamples = nframes * info.outchannels - # xrun, copy zeros to outbuffer - # TODO: send a notification to an error msg ringbuf - memset(outptr+sizeof(T)*toread, 0, sizeof(T)*(outsamples-toread)) - end - - paContinue -end - - -"""Call the given function and discard stdout and stderr""" -function swallow_stderr(f) - origerr = STDERR - (errread, errwrite) = redirect_stderr() - result = f() - redirect_stderr(origerr) - close(errwrite) - close(errread) - - result -end - -memset(buf, val, count) = ccall(:memset, Ptr{Void}, - (Ptr{Void}, Cint, Csize_t), - buf, val, count) +# this is called by the shim process callback to notify that there is new data. +# it's run in the audio context so don't do anything besides wake up the +# AsyncCondition handle associated with that ring buffer +notifycb(handle) = ccall(:uv_async_send, Cint, (Ptr{Void}, ), handle) end # module PortAudio diff --git a/src/libportaudio.jl b/src/libportaudio.jl index ab4a618..ffae4ea 100644 --- a/src/libportaudio.jl +++ b/src/libportaudio.jl @@ -1,17 +1,17 @@ # Low-level wrappers for Portaudio calls # General type aliases -typealias PaTime Cdouble -typealias PaError Cint -typealias PaSampleFormat Culong -typealias PaDeviceIndex Cint -typealias PaHostApiIndex Cint -typealias PaHostApiTypeId Cint +const PaTime = Cdouble +const PaError = Cint +const PaSampleFormat = Culong +const PaDeviceIndex = Cint +const PaHostApiIndex = Cint +const PaHostApiTypeId = Cint # PaStream is always used as an opaque type, so we're always dealing # with the pointer -typealias PaStream Ptr{Void} -typealias PaStreamCallback Void -typealias PaStreamFlags Culong +const PaStream = Ptr{Void} +const PaStreamCallback = Void +const PaStreamFlags = Culong const paNoFlag = PaStreamFlags(0x00) @@ -37,7 +37,7 @@ const type_to_fmt = Dict{Type, PaSampleFormat}( UInt8 => 3 ) -typealias PaStreamCallbackResult Cint +const PaStreamCallbackResult = Cint # Callback return values const paContinue = PaStreamCallbackResult(0) const paComplete = PaStreamCallbackResult(1) @@ -69,7 +69,7 @@ end # all the host APIs on the system by iterating through those values. # PaHostApiTypeId values -const pa_host_api_names = Dict{PaHostApiTypeId, UTF8String}( +const pa_host_api_names = Dict{PaHostApiTypeId, String}( 0 => "In Development", # use while developing support for a new host API 1 => "Direct Sound", 2 => "MME", @@ -240,11 +240,11 @@ function handle_status(err::PaError, show_warnings::Bool=true) if show_warnings msg = ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err) - warn("libportaudio: " * bytestring(msg)) + warn("libportaudio: " * unsafe_string(msg)) end elseif err != PA_NO_ERROR msg = ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), err) - error("libportaudio: " * bytestring(msg)) + error("libportaudio: " * unsafe_string(msg)) end end diff --git a/src/pa_shim.jl b/src/pa_shim.jl new file mode 100644 index 0000000..c1100be --- /dev/null +++ b/src/pa_shim.jl @@ -0,0 +1,61 @@ +function init_pa_shim() + libdir = joinpath(dirname(@__FILE__), "..", "deps", "usr", "lib") + libsuffix = "" + basename = "pa_shim" + @static if is_linux() && Sys.ARCH == :x86_64 + libsuffix = "x86_64-linux-gnu" + elseif is_linux() && Sys.ARCH == :i686 + libsuffix = "i686-linux-gnu" + elseif is_apple() && Sys.ARCH == :x86_64 + libsuffix = "x86_64-apple-darwin14" + elseif is_windows() && Sys.ARCH == :x86_64 + libsuffix = "x86_64-w64-mingw32" + elseif is_windows() && Sys.ARCH == :i686 + libsuffix = "i686-w64-mingw32" + elseif !any( + (sfx) -> isfile(joinpath(libdir, "$basename.$sfx")), + ("so", "dll", "dylib")) + error("Unsupported platform $(Sys.MACHINE). You can build your own library by running `make` from $(joinpath(@__FILE__, "..", "deps", "src"))") + end + # if there's a suffix-less library, it was built natively on this machine, + # so load that one first, otherwise load the pre-built one + global const libpa_shim = Base.Libdl.find_library( + [basename, "$(basename)_$libsuffix"], + [libdir]) + libpa_shim == "" && error("Could not load $basename library, please file an issue at https://github.com/JuliaAudio/RingBuffers.jl/issues with your `versioninfo()` output") + shim_dlib = Libdl.dlopen(libpa_shim) + # pointer to the shim's process callback + global const shim_processcb_c = Libdl.dlsym(shim_dlib, :pa_shim_processcb) + if shim_processcb_c == C_NULL + error("Got NULL pointer loading `pa_shim_processcb`") + end +end + +const pa_shim_errmsg_t = Cint +const PA_SHIM_ERRMSG_OVERFLOW = Cint(0) # input overflow +const PA_SHIM_ERRMSG_UNDERFLOW = Cint(1) # output underflow +const PA_SHIM_ERRMSG_ERR_OVERFLOW = Cint(2) # error buffer overflowed + + +# This struct is shared with pa_shim.c +mutable struct pa_shim_info_t + inputbuf::Ptr{PaUtilRingBuffer} # ringbuffer for input + outputbuf::Ptr{PaUtilRingBuffer} # ringbuffer for output + errorbuf::Ptr{PaUtilRingBuffer} # ringbuffer to send error notifications + sync::Cint # keep input/output ring buffers synchronized (0/1) + notifycb::Ptr{Void} # Julia callback to notify on updates (called from audio thread) + inputhandle::Ptr{Void} # condition to notify on new input data + outputhandle::Ptr{Void} # condition to notify when ready for output + errorhandle::Ptr{Void} # condition to notify on new errors +end + +""" + PortAudio.shimhash() + +Return the sha256 hash(as a string) of the source file used to build the shim. +We may use this sometime to verify that the distributed binary stays in sync +with the rest of the package. +""" +shimhash() = unsafe_string( + ccall((:pa_shim_getsourcehash, libpa_shim), Cstring, ())) +Base.unsafe_convert(::Type{Ptr{Void}}, info::pa_shim_info_t) = pointer_from_objref(info) diff --git a/src/suppressor.jl b/src/suppressor.jl new file mode 100644 index 0000000..ab297bc --- /dev/null +++ b/src/suppressor.jl @@ -0,0 +1,20 @@ +# while waiting for this PR to get merged: https://github.com/Ismael-VC/Suppressor.jl/pull/12 +# we'll just include the relevant code here + +macro suppress_err(block) + quote + if ccall(:jl_generating_output, Cint, ()) == 0 + ORIGINAL_STDERR = STDERR + err_rd, err_wr = redirect_stderr() + err_reader = @async readstring(err_rd) + end + + value = $(esc(block)) + + if ccall(:jl_generating_output, Cint, ()) == 0 + redirect_stderr(ORIGINAL_STDERR) + close(err_wr) + end + value + end +end diff --git a/test/REQUIRE b/test/REQUIRE index 94e516f..557b023 100644 --- a/test/REQUIRE +++ b/test/REQUIRE @@ -1 +1 @@ -BaseTestNext +TestSetExtensions diff --git a/test/runtests.jl b/test/runtests.jl index c208e42..bd77615 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,180 +1,249 @@ #!/usr/bin/env julia -if VERSION >= v"0.5.0-dev+7720" - using Base.Test -else - using BaseTestNext -end +using Base.Test +using TestSetExtensions using PortAudio using SampledSignals using RingBuffers -function test_callback(inchans, outchans) - nframes = Culong(8) +# pull in some extra stuff we need to test the callback directly +using PortAudio: notifyhandle, notifycb_c, shim_processcb_c +using PortAudio: pa_shim_errmsg_t, pa_shim_info_t +using PortAudio: PA_SHIM_ERRMSG_ERR_OVERFLOW, PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW - cb = PortAudio.pa_callbacks[Float32] - inbuf = rand(Float32, inchans*nframes) # simulate microphone input - sourcebuf = LockFreeRingBuffer(Float32, inchans*nframes*8) # the microphone input should end up here +"Setup buffers to test callback behavior" +function setup_callback(inchans, outchans, nframes, synced) + sourcebuf = RingBuffer{Float32}(inchans, nframes*2) # the microphone input should end up here + sinkbuf = RingBuffer{Float32}(outchans, nframes*2) # the callback should copy this to cb_output + errbuf = RingBuffer{pa_shim_errmsg_t}(1, 8) - outbuf = zeros(Float32, outchans*nframes) # this is where the output should go - sinkbuf = LockFreeRingBuffer(Float32, outchans*nframes*8) # the callback should copy this to outbuf - - # 2 input channels, 3 output channels - info = PortAudio.CallbackInfo(inchans, sourcebuf, outchans, sinkbuf, true) - - # handle any conversions here so they don't mess with the allocation - # the seemingly-redundant type specifiers avoid some allocation during the ccall. - # might be due to https://github.com/JuliaLang/julia/issues/15276 - inptr::Ptr{Float32} = Ptr{Float32}(pointer(inbuf)) - outptr::Ptr{Float32} = Ptr{Float32}(pointer(outbuf)) + # pass NULL for i/o we're not using + info = pa_shim_info_t( + inchans > 0 ? pointer(sourcebuf) : C_NULL, + outchans > 0 ? pointer(sinkbuf) : C_NULL, + pointer(errbuf), + synced, notifycb_c, + inchans > 0 ? notifyhandle(sourcebuf) : C_NULL, + outchans > 0 ? notifyhandle(sinkbuf) : C_NULL, + notifyhandle(errbuf) + ) flags = Culong(0) - infoptr::Ptr{PortAudio.CallbackInfo{Float32}} = Ptr{PortAudio.CallbackInfo{Float32}}(pointer_from_objref(info)) - testin = zeros(Float32, inchans*nframes) - testout = rand(Float32, outchans*nframes) - write(sinkbuf, testout) # fill the output ringbuffer - ret = ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test ret === PortAudio.paContinue - @test outbuf == testout - read!(sourcebuf, testin) - @test inbuf == testin + cb_input = rand(Float32, inchans, nframes) # simulate microphone input + cb_output = rand(Float32, outchans, nframes) # this is where the output should go + function processfunc() + ccall(shim_processcb_c, Cint, + (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{Void}), + cb_input, cb_output, nframes, C_NULL, flags, pointer_from_objref(info)) + end + + (sourcebuf, sinkbuf, errbuf, cb_input, cb_output, processfunc) +end + +function test_callback(inchans, outchans, synced) + nframes = 8 + (sourcebuf, sinkbuf, errbuf, + cb_input, cb_output, process) = setup_callback(inchans, outchans, + nframes, synced) if outchans > 0 - underfill = 3 # should be less than nframes - testout = rand(Float32, outchans*underfill) - write(sinkbuf, testout) # underfill the output ringbuffer - # call again (partial underrun) - ret = ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test ret === PortAudio.paContinue - @test outbuf[1:outchans*underfill] == testout - @test outbuf[outchans*underfill+1:outchans*nframes] == zeros(Float32, (nframes-underfill)*outchans) - @test nreadable(sourcebuf) == inchans*underfill - @test read!(sourcebuf, testin) == inchans*underfill - @test testin[1:inchans*underfill] == inbuf[1:inchans*underfill] - - # call again (total underrun) - ret = ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test ret === PortAudio.paContinue - @test outbuf == zeros(Float32, outchans*nframes) - @test nreadable(sourcebuf) == 0 - + testout = rand(Float32, outchans, nframes) # generate some test data to play write(sinkbuf, testout) # fill the output ringbuffer - # test allocation - alloc = @allocated ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test alloc == 0 - # now test allocation in underrun state - alloc = @allocated ccall(cb, Cint, - (Ptr{Float32}, Ptr{Float32}, Culong, Ptr{Void}, Culong, Ptr{PortAudio.CallbackInfo{Float32}}), - inptr, outptr, nframes, C_NULL, flags, infoptr) - @test alloc == 0 + end + @test process() == PortAudio.paContinue + if outchans > 0 + # testout -> sinkbuf -> cb_output + @test cb_output == testout + end + if inchans > 0 + # cb_input -> sourcebuf + @test read(sourcebuf, nframes) == cb_input + end + @test framesreadable(errbuf) == 0 +end + +""" + test_callback_underflow(inchans, outchans; nframes=8, underfill=3, synced=false) + +Test that the callback works on underflow conditions. underfill is the numer of +frames we feed in, which should be less than nframes. +""" +function test_callback_underflow(inchans, outchans, synced) + nframes = 8 + underfill = 3 # must be less than nframes + (sourcebuf, sinkbuf, errbuf, + cb_input, cb_output, process) = setup_callback(inchans, outchans, + nframes, synced) + outchans > 0 || error("Can't test underflow with no output") + testout = rand(Float32, outchans, underfill) + write(sinkbuf, testout) # underfill the output ringbuffer + # call callback (partial underflow) + @test process() == PortAudio.paContinue + @test cb_output[:, 1:underfill] == testout + @test cb_output[:, (underfill+1):nframes] == zeros(Float32, outchans, (nframes-underfill)) + errs = readavailable(errbuf) + if inchans > 0 + received = readavailable(sourcebuf) + if synced + @test size(received, 2) == underfill + @test received == cb_input[:, 1:underfill] + @test length(errs) == 2 + @test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW]) + else + @test size(received, 2) == nframes + @test received == cb_input + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW + end + else + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW + end + + # call again (total underflow) + @test process() == PortAudio.paContinue + @test cb_output == zeros(Float32, outchans, nframes) + errs = readavailable(errbuf) + if inchans > 0 + received = readavailable(sourcebuf) + if synced + @test size(received, 2) == 0 + @test length(errs) == 2 + @test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW]) + else + @test size(received, 2) == nframes + @test received == cb_input + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW + end + else + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_UNDERFLOW end end -# these test are currently set up to run on OSX +function test_callback_overflow(inchans, outchans, synced) + nframes = 8 + (sourcebuf, sinkbuf, errbuf, + cb_input, cb_output, process) = setup_callback(inchans, outchans, + nframes, synced) + inchans > 0 || error("Can't test overflow with no input") + @test frameswritable(sinkbuf) == nframes*2 -@testset "PortAudio Tests" begin - devs = PortAudio.devices() - i = findfirst(d -> d.maxinchans > 0, devs) - indev = i > 0 ? devs[i] : nothing - i = findfirst(d -> d.maxoutchans > 0, devs) - outdev = i > 0 ? devs[i] : nothing - i = findfirst(d -> d.maxoutchans > 0 && d.maxinchans > 0, devs) - duplexdev = i > 0 ? devs[i] : nothing + # the first time it should half-fill the input ring buffer + if outchans > 0 + testout = rand(Float32, outchans, nframes) + write(sinkbuf, testout) + end + @test framesreadable(sourcebuf) == 0 + outchans > 0 && @test frameswritable(sinkbuf) == nframes + @test process() == PortAudio.paContinue + @test framesreadable(errbuf) == 0 + @test framesreadable(sourcebuf) == nframes + outchans > 0 && @test frameswritable(sinkbuf) == nframes*2 + # now run the process func again to completely fill the input ring buffer + outchans > 0 && write(sinkbuf, testout) + @test framesreadable(sourcebuf) == nframes + outchans > 0 && @test frameswritable(sinkbuf) == nframes + @test process() == PortAudio.paContinue + @test framesreadable(errbuf) == 0 + @test framesreadable(sourcebuf) == nframes*2 + outchans > 0 && @test frameswritable(sinkbuf) == nframes*2 + + # now this time the process func should overflow the input buffer + outchans > 0 && write(sinkbuf, testout) + @test framesreadable(sourcebuf) == nframes*2 + outchans > 0 && @test frameswritable(sinkbuf) == nframes + @test process() == PortAudio.paContinue + @test framesreadable(sourcebuf) == nframes*2 + errs = readavailable(errbuf) + if outchans > 0 + if synced + # if input and output are synced, thec callback didn't pull from + # the output ringbuf + @test frameswritable(sinkbuf) == nframes + @test cb_output == zeros(Float32, outchans, nframes) + @test length(errs) == 2 + @test Set(errs) == Set([PA_SHIM_ERRMSG_UNDERFLOW, PA_SHIM_ERRMSG_OVERFLOW]) + else + @test frameswritable(sinkbuf) == nframes*2 + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_OVERFLOW + end + else + @test length(errs) == 1 + @test errs[1] == PA_SHIM_ERRMSG_OVERFLOW + end +end + +@testset ExtendedTestSet "PortAudio Tests" begin @testset "Reports version" begin io = IOBuffer() PortAudio.versioninfo(io) - result = takebuf_string(io) + result = split(String(take!((io))), "\n") # make sure this is the same version I tested with - @test result == - """PortAudio V19-devel (built Aug 6 2014 17:54:39) - Version Number: 1899 - """ + @test startswith(result[1], "PortAudio V19") end - @testset "PortAudio Callback works for duplex stream" begin - test_callback(2, 3) + @testset "using correct shim version" begin + @test PortAudio.shimhash() == "87021557a9f999545828eb11e4ebad2cd278b734dd91a8bd3faf05c89912cf80" end - @testset "Callback works with input-only stream" begin - test_callback(2, 0) + @testset "Basic callback functionality" begin + @testset "basic duplex (no sync)" begin + test_callback(2, 3, false) + end + @testset "basic input-only (no sync)" begin + test_callback(2, 0, false) + end + @testset "basic output-only (no sync)" begin + test_callback(0, 2, false) + end + @testset "basic no input or output (no sync)" begin + test_callback(0, 0, false) + end + @testset "basic duplex (sync)" begin + test_callback(2, 3, true) + end + @testset "basic input-only (sync)" begin + test_callback(2, 0, true) + end + @testset "basic output-only (sync)" begin + test_callback(0, 2, true) + end + @testset "basic no input or output (sync)" begin + test_callback(0, 0, true) + end end - @testset "Callback works with output-only stream" begin - test_callback(0, 2) + @testset "Ouput underflow" begin + @testset "underflow duplex (nosync)" begin + test_callback_underflow(2, 3, false) + end + @testset "underflow output-only (nosync)" begin + test_callback_underflow(0, 3, false) + end + @testset "underflow duplex (sync)" begin + test_callback_underflow(2, 3, true) + end + @testset "underflow output-only (sync)" begin + test_callback_underflow(0, 3, true) + end end - @testset "Open Default Device" begin - println("Recording...") - stream = PortAudioStream(2, 0) - buf = read(stream, 5s) - close(stream) - @test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source)) - println("Playing back recording...") - stream = PortAudioStream(0, 2) - write(stream, buf) - println("flushing...") - flush(stream) - close(stream) - println("Testing pass-through") - stream = PortAudioStream(2, 2) - write(stream, stream, 5s) - flush(stream) - close(stream) - println("done") - end - @testset "Samplerate-converting writing" begin - stream = PortAudioStream() - write(stream, SinSource(eltype(stream), samplerate(stream)*0.8, [220, 330]), 3s) - write(stream, SinSource(eltype(stream), samplerate(stream)*1.2, [220, 330]), 3s) - flush(stream) - close(stream) - end - @testset "Open Device by name" begin - stream = PortAudioStream("Built-in Microph", "Built-in Output") - buf = read(stream, 0.001s) - @test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source)) - write(stream, buf) - io = IOBuffer() - show(io, stream) - @test takebuf_string(io) == """ - PortAudio.PortAudioStream{Float32} - Samplerate: 48000.0Hz - Buffer Size: 4096 frames - 2 channel sink: "Built-in Output" - 2 channel source: "Built-in Microph\"""" - close(stream) - end - @testset "Error on wrong name" begin - @test_throws ErrorException PortAudioStream("foobarbaz") - end - # no way to check that the right data is actually getting read or written here, - # but at least it's not crashing. - @testset "Queued Writing" begin - stream = PortAudioStream(0, 2) - buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.sink))*0.1, samplerate(stream)) - t1 = @async write(stream, buf) - t2 = @async write(stream, buf) - @test wait(t1) == 48000 - @test wait(t2) == 48000 - flush(stream) - close(stream) - end - @testset "Queued Reading" begin - stream = PortAudioStream(2, 0) - buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source))*0.1, samplerate(stream)) - t1 = @async read!(stream, buf) - t2 = @async read!(stream, buf) - @test wait(t1) == 48000 - @test wait(t2) == 48000 - close(stream) + @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 end end diff --git a/test/runtests_local.jl b/test/runtests_local.jl new file mode 100644 index 0000000..76356d9 --- /dev/null +++ b/test/runtests_local.jl @@ -0,0 +1,85 @@ +# This file has runs the normal tests and also adds tests that can only be run +# locally on a machine with a sound card. It's mostly to put the library through +# its paces assuming a human is listening. + +include("runtests.jl") + +# these default values are specific to my machines +if is_windows() + default_indev = "Microphone Array (Realtek High " + default_outdev = "Speaker/Headphone (Realtek High" +elseif is_apple() + default_indev = "Built-in Microph" + default_outdev = "Built-in Output" +elseif is_linux() + default_indev = "default" + default_outdev = "default" +end + +@testset ExtendedTestSet "Local Tests" begin + @testset "Open Default Device" begin + println("Recording...") + stream = PortAudioStream(2, 0) + buf = read(stream, 5s) + close(stream) + @test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source)) + println("Playing back recording...") + stream = PortAudioStream(0, 2) + write(stream, buf) + println("flushing...") + flush(stream) + close(stream) + println("Testing pass-through") + stream = PortAudioStream(2, 2) + write(stream, stream, 5s) + flush(stream) + close(stream) + println("done") + end + @testset "Samplerate-converting writing" begin + stream = PortAudioStream(0, 2) + write(stream, SinSource(eltype(stream), samplerate(stream)*0.8, [220, 330]), 3s) + write(stream, SinSource(eltype(stream), samplerate(stream)*1.2, [220, 330]), 3s) + flush(stream) + close(stream) + end + @testset "Open Device by name" begin + stream = PortAudioStream(default_indev, default_outdev) + buf = read(stream, 0.001s) + @test size(buf) == (round(Int, 0.001 * samplerate(stream)), nchannels(stream.source)) + write(stream, buf) + io = IOBuffer() + show(io, stream) + @test String(take!(io)) == """ + PortAudio.PortAudioStream{Float32} + Samplerate: 44100.0Hz + Buffer Size: 4096 frames + 2 channel sink: "$default_outdev" + 2 channel source: "$default_indev\"""" + close(stream) + end + @testset "Error on wrong name" begin + @test_throws ErrorException PortAudioStream("foobarbaz") + end + # no way to check that the right data is actually getting read or written here, + # but at least it's not crashing. + @testset "Queued Writing" begin + stream = PortAudioStream(0, 2) + buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.sink))*0.1, samplerate(stream)) + t1 = @async write(stream, buf) + t2 = @async write(stream, buf) + @test wait(t1) == 48000 + @test wait(t2) == 48000 + flush(stream) + close(stream) + end + @testset "Queued Reading" begin + stream = PortAudioStream(2, 0) + buf = SampleBuf(rand(eltype(stream), 48000, nchannels(stream.source))*0.1, samplerate(stream)) + t1 = @async read!(stream, buf) + t2 = @async read!(stream, buf) + @test wait(t1) == 48000 + @test wait(t2) == 48000 + close(stream) + end +end