stripped out non-portaudio stuff and starting to reorganize portaudio stuff

This commit is contained in:
Spencer Russell 2016-03-18 18:09:37 -04:00
parent c4dfef9178
commit 80d329eb39
20 changed files with 514 additions and 1823 deletions

View file

@ -1,21 +1,18 @@
# Documentation: http://docs.travis-ci.com/user/languages/julia/
language: julia
compiler:
- clang
notifications:
email: spencer.f.russell@gmail.com
os:
- linux
- osx
julia:
- 0.3
# - release
- 0.4
# - nightly
# Fails with:
# LoadError: ccall: could not find function jl_read_sonames
# while loading /home/travis/.julia/v0.5/AudioIO/deps/build.jl, in expression starting on line 29
- nightly
notifications:
email: false
script:
# SampleTypes is unregistered so clone it for testing
- julia -e 'Pkg.clone("https://github.com/JuliaAudio/SampleTypes.jl.git")'
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
- julia -e 'Pkg.init()'
- julia -e 'Pkg.add("BinDeps"); Pkg.checkout("BinDeps")' # latest master needed for Pacman support
- julia -e 'Pkg.clone(pwd()); Pkg.build("AudioIO")'
- julia -e 'Pkg.add("FactCheck")' # add FactCheck manually because we're not using Pkg.test()
- julia --code-coverage=user test/runtests.jl # Pkg.test disables inlining when enabling coverage, which kills our allocation tests
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); Pkg.test("PortAudio"; coverage=true)'
after_success:
- if [ $JULIAVERSION = "juliareleases" ]; then julia -e 'cd(Pkg.dir("AudioIO")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder())'; fi
- julia -e 'cd(Pkg.dir("PortAudio")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())'

133
README.md
View file

@ -1,132 +1,7 @@
AudioIO.jl
PortAudio.jl
==========
[![Build Status](https://travis-ci.org/ssfrr/AudioIO.jl.svg?branch=master)](https://travis-ci.org/ssfrr/AudioIO.jl)
[![Pkgs Status](http://pkg.julialang.org/badges/AudioIO_release.svg)](http://pkg.julialang.org/?pkg=AudioIO&ver=release)
[![Coverage Status](https://img.shields.io/coveralls/ssfrr/AudioIO.jl.svg)](https://coveralls.io/r/ssfrr/AudioIO.jl?branch=master)
AudioIO interfaces to audio streams, including real-time recording, audio
processing, and playback through your sound card using PortAudio. It also
supports reading and writing audio files in a variety of formats. It is under
active development and the low-level API could change, but the basic
functionality (reading and writing files, the `play` function, etc.) should be
stable and usable by the general Julia community.
File I/O
--------
File I/O is handled by [libsndfile](http://www.mega-nerd.com/libsndfile/), so
we can support a wide variety of file and sample formats. Use the
`AudioIO.open` function to open a file. It has the same API as the built-in
Base.open, but returns an `AudioFile` type. Opening an audio file and reading
its contents into an array is as simple as:
```julia
f = AudioIO.open("data/never_gonna_give_you_up.wav")
data = read(f)
close(f)
```
Or to hand closing the file automatically (including in the case of unexpected
exceptions), we support the `do` block syntax:
```julia
data = AudioIO.open("data/never_gonna_let_you_down.wav") do f
read(f)
end
```
By default the returned array will be in whatever format the original audio file is
(Float32, UInt16, etc.). We also support automatic conversion by supplying a type:
```julia
data = AudioIO.open("data/never_gonna_run_around.wav") do f
read(f, Float32)
end
```
Basic Array Playback
--------------------
Arrays in various formats can be played through your soundcard. Currently the
native format that is delivered to the PortAudio backend is Float32 in the
range of [-1, 1]. Arrays in other sizes of float are converted. Arrays
in Signed or Unsigned Integer types are scaled so that the full range is
mapped to [-1, 1] floating point values.
To play a 1-second burst of noise:
```julia
julia> v = rand(44100) * 0.1
julia> play(v)
```
AudioNodes
----------
In addition to the basic `play` function you can create more complex networks
of AudioNodes in a render chain. In fact, when using the basic `play` to play
an Array, behind the scenes an instance of the ArrayPlayer type is created
and added to the master AudioMixer inputs. Audionodes also implement a `stop`
function, which will remove them from the render graph. When an implicit
AudioNode is created automatically, such as when using `play` on an Array, the
`play` function should return the audio node that is playing the Array, so it
can be stopped if desired.
To explictly do the same as above:
```julia
julia> v = rand(44100) * 0.1
julia> player = ArrayPlayer(v)
julia> play(player)
```
To generate 2 sin tones:
```julia
julia> osc1 = SinOsc(440)
julia> osc2 = SinOsc(660)
julia> play(osc1)
julia> play(osc2)
julia> stop(osc1)
julia> stop(osc2)
```
All AudioNodes must implement a `render` function that can be called to
retreive the next block of audio.
AudioStreams
------------
AudioStreams represent an external source or destination for audio, such as the
sound card. The `play` function attaches AudioNodes to the default stream
unless a stream is given as the 2nd argument.
AudioStream is an abstract type, which currently has a PortAudioStream subtype
that writes to the sound card, and a TestAudioStream that is used in the unit
tests.
Currently only 1 stream at a time is supported so there's no reason to provide
an explicit stream to the `play` function. The stream has a root mixer field
which is an instance of the AudioMixer type, so that multiple AudioNodes
can be heard at the same time. Whenever a new frame of audio is needed by the
sound card, the stream calls the `render` method on the root audio mixer, which
will in turn call the `render` methods on any input AudioNodes that are set
up as inputs.
Installation
------------
To install the latest release version, simply run
```julia
julia> Pkg.add("AudioIO")
```
If you want to install the lastest master, it's almost as easy:
```julia
julia> Pkg.clone("AudioIO")
julia> Pkg.build("AudioIO")
```
[![Build Status](https://travis-ci.org/JuliaAudio/PortAudio.jl.svg?branch=master)](https://travis-ci.org/JuliaAudio/PortAudio.jl)
[![codecov.io] (http://codecov.io/github/JuliaAudio/PortAudio.jl/coverage.svg?branch=master)] (http://codecov.io/github/JuliaAudio/PortAudio.jl?branch=master)
PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which gives cross-platform access to audio devices. It is compatible with the types defined in [SampleTypes.jl](https://github.com/JuliaAudio/SampleTypes.jl), so it provides `PASampleSink` and `PASampleSource` types, which can be read from and written to.

View file

@ -1,4 +1,4 @@
julia 0.3-
julia 0.4
BinDeps
Compat
@osx Homebrew

8
deps/build.jl vendored
View file

@ -6,26 +6,20 @@ using Compat
ENV["JULIA_ROOT"] = abspath(JULIA_HOME, "../../")
libportaudio = library_dependency("libportaudio")
libsndfile = library_dependency("libsndfile")
# TODO: add other providers with correct names
provides(AptGet, "portaudio19-dev", libportaudio)
provides(AptGet, "libsndfile1-dev", libsndfile)
provides(Pacman, "portaudio", libportaudio)
provides(Pacman, "libsndfile", libsndfile)
@osx_only begin
using Homebrew
provides(Homebrew.HB, "portaudio", libportaudio)
provides(Homebrew.HB, "libsndfile", libsndfile)
end
@windows_only begin
using WinRPM
provides(WinRPM.RPM, "libportaudio2", libportaudio, os = :Windows)
provides(WinRPM.RPM, "libsndfile1", libsndfile, os = :Windows)
end
@BinDeps.install @compat(Dict(:libportaudio => :libportaudio,
:libsndfile => :libsndfile))
@BinDeps.install @compat(Dict(:libportaudio => :libportaudio, ))

View file

@ -1,101 +0,0 @@
Some possible API concepts for dealing with files
=================================================
Notes
-----
* requires libflac for flac decoding
Use Cases
---------
* Play a file through the speakers
* Use a file as input to an AudioNode for processing
* Read a file into an array
* Write an array into a file
* Write the output of an AudioNode to a file
IOStream API
------------
* users use standard julia "open" function to create an IOStream object
* FilePlayer <: AudioNode takes an IOStream and uses `sf_open_fd` to open and
play
* play(io::IOStream) creates a FilePlayer and plays it (just like ArrayPlayer)
* FileStream
### Play a file through the speakers
sndfile = open("myfile.wav")
play(sndfile)
close(sndfile)
### Use a file as input to an AudioNode for processing
sndfile = open("myfile.wav")
# maybe FilePlayer also takes a string input for convenience
node = FilePlayer(sndfile)
mixer = AudioMixer([node])
# etc.
### Read a file into an array
# TODO
### Write an array into a file
# TODO
### Write the output of an AudioNode to a file
node = SinOsc(440)
# ???
Separate Open Function API
--------------------------
* users use an explicit `AudioIO.open` function to open sound files
* `AudioIO.open` takes mode arguments just like the regular julia `open` function
* `AudioIO.open` returns a AudioFile instance.
### Play a file through the speakers
sndfile = AudioIO.open("myfile.wav")
play(sndfile)
close(sndfile)
or
play("myfile.wav")
### Use a file as input to an AudioNode for processing
sndfile = AudioIO.open("myfile.wav")
# FilePlayer also can take a string filename for convenience
node = FilePlayer(sndfile)
mixer = AudioMixer([node])
# etc.
### Read a file into an array
sndfile = AudioIO.open("myfile.wav")
vec = read(sndfile) # takes an optional arg for number of frames to read
close(sndfile)
### Write an array into a file
sndfile = AudioIO.open("myfile.wav", "w") #TODO: need to specify format
vec = rand(Float32, 441000) # 10 seconds of noise
write(sndfile, vec)
close(sndfile)
### Write the output of an AudioNode to a file
sndfile = AudioIO.open("myfile.wav", "w") #TODO: need to specify format
node = SinOsc(440)
write(sndfile, node, 44100) # record 1 second, optional block_size
# note that write() can handle sample depth conversions, and render() is
# called with the sampling rate of the file
close(sndfile)

View file

@ -1,22 +0,0 @@
One design challenge is how to handle nodes with a finite length, e.g.
ArrayPlayers. Also this comes up with how do we stop a node.
Considerations:
1. typically the end of the signal will happen in the middle of a block.
2. we want to avoid the AudioNodes allocating a new block every render cycle
3. force-stopping nodes will typicaly happen on a block boundary
4. A node should be able to send its signal to multiple receivers, but it doesn't
know what they are (it doesn't store a reference to them), so if a node is finished
it needs to communicate that in the value returned from render()
Options:
1. We could take the block size as a maximum, and if there aren't that many
frames of audio left then a short (or empty) block is returned.
2. We could return a (Array, Bool) tuple with the full block-size, padded with
zeros (or extending the last value out), and the bool indicating whether
there is more data
3. We could raturn a (Array, Int) tuple that indicates how many frames were
written
4. We could ignore it and just have them keep playing. This makes the simple
play(node) usage dangerous because they never get cleaned up

View file

@ -1,3 +0,0 @@
There are a few issues regarding the types in AudioIO:
1. There are some fields that need to be shared between all nodes

View file

@ -1,100 +0,0 @@
module AudioIO
using Compat
importall Base.Operators
# export the basic API
export play, stop, get_audio_devices
# default stream used when none is given
_stream = nothing
################## Types ####################
typealias AudioSample Float32
# A frame of audio, possibly multi-channel
typealias AudioBuf Array{AudioSample}
# used as a type parameter for AudioNodes. Subtypes handle the actual DSP for
# each node
abstract AudioRenderer
# A stream of audio (for instance that writes to hardware). All AudioStream
# subtypes should have a root and info field
abstract AudioStream
samplerate(str::AudioStream) = str.info.sample_rate
bufsize(str::AudioStream) = str.info.buf_size
# An audio interface is usually a physical sound card, but could
# be anything you'd want to connect a stream to
abstract AudioInterface
# Info about the hardware device
type DeviceInfo
sample_rate::Float32
buf_size::Integer
end
type AudioNode{T<:AudioRenderer}
active::Bool
end_cond::Condition
renderer::T
AudioNode(renderer::AudioRenderer) = new(true, Condition(), renderer)
AudioNode(args...) = AudioNode{T}(T(args...))
end
function render(node::AudioNode, input::AudioBuf, info::DeviceInfo)
# TODO: not sure if the compiler will infer that render() always returns an
# AudioBuf. Might need to help it
if node.active
result = render(node.renderer, input, info)
if length(result) < info.buf_size
node.active = false
notify(node.end_cond)
end
return result
else
return AudioSample[]
end
end
# Get binary dependencies loaded from BinDeps
include( "../deps/deps.jl")
include("nodes.jl")
include("portaudio.jl")
include("sndfile.jl")
include("operators.jl")
############ Exported Functions #############
# Play an AudioNode by adding it as an input to the root mixer node
function play(node::AudioNode, stream::AudioStream)
push!(stream.root, node)
return node
end
# If the stream is not given, use the default global PortAudio stream
function play(node::AudioNode)
global _stream
if _stream == nothing
_stream = PortAudioStream()
end
play(node, _stream)
end
function stop(node::AudioNode)
node.active = false
notify(node.end_cond)
end
function Base.wait(node::AudioNode)
if node.active
wait(node.end_cond)
end
end
function get_audio_devices()
return get_portaudio_devices()
end
end # module AudioIO

261
src/PortAudio.jl Normal file
View file

@ -0,0 +1,261 @@
module PortAudio
using SampleTypes
using Compat
# Get binary dependencies loaded from BinDeps
include( "../deps/deps.jl")
include("libportaudio.jl")
# Info about the hardware device
type DeviceInfo
sample_rate::Float32
buf_size::Integer
end
function devices()
return get_portaudio_devices()
end
type PortAudioStream
info::DeviceInfo
show_warnings::Bool
stream::PaStream
function PortAudioStream(sample_rate=44100Hz,
buf_size::Integer=1024,
show_warnings::Bool=false)
# Pa_Initialize can be called multiple times, as long as each is
# paired with Pa_Terminate()
Pa_Initialize()
stream = Pa_OpenDefaultStream(2, 2, paFloat32, Int(sample_rate), buf_size)
Pa_StartStream(stream)
this = new(root, DeviceInfo(sample_rate, buf_size), show_warnings, stream)
@schedule(portaudio_task(this))
finalizer(this, close)
this
end
end
type PortAudioSink <: SampleSink
stream::PaStream
end
type PortAudioSource <: SampleSource
stream::PaStream
end
function close(stream::PortAudioStream)
Pa_StopStream(stream.stream)
Pa_CloseStream(stream.stream)
Pa_Terminate()
end
type Pa_StreamParameters
device::PaDeviceIndex
channelCount::Cint
sampleFormat::PaSampleFormat
suggestedLatency::PaTime
hostAPISpecificStreamInfo::Ptr{Void}
end
type PortAudioInterface <: AudioInterface
name::AbstractString
host_api::AbstractString
max_input_channels::Int
max_output_channels::Int
device_index::PaDeviceIndex
end
type Pa_AudioStream <: AudioStream
root::AudioMixer
info::DeviceInfo
show_warnings::Bool
stream::PaStream
sformat::PaSampleFormat
sbuffer::Array{Real}
sbuffer_output_waiting::Integer
parent_may_use_buffer::Bool
"""
Get device parameters needed for opening with portaudio
default is input as 44100/16bit int, same as CD audio type input
"""
function Pa_AudioStream(device_index, channels=2, input=false,
sample_rate::Integer=44100,
framesPerBuffer::Integer=2048,
show_warnings::Bool=false,
sample_format::PaSampleFormat=paInt16)
require_portaudio_init()
stream = Pa_OpenStream(device_index, channels, input, sample_format,
Cdouble(sample_rate), Culong(framesPerBuffer))
Pa_StartStream(stream)
root = AudioMixer()
datatype = PaSampleFormat_to_T(sample_format)
sbuf = ones(datatype, framesPerBuffer)
this = new(root, DeviceInfo(sample_rate, framesPerBuffer),
show_warnings, stream, sample_format, sbuf, 0, false)
info("Scheduling PortAudio Render Task...")
if input
@schedule(pa_input_task(this))
else
@schedule(pa_output_task(this))
end
this
end
end
"""
Blocking read from a Pa_AudioStream that is open as input
"""
function read_Pa_AudioStream(stream::Pa_AudioStream)
while true
while stream.parent_may_use_buffer == false
sleep(0.001)
end
buffer = deepcopy(stream.sbuffer)
stream.parent_may_use_buffer = false
return buffer
end
end
"""
Blocking write to a Pa_AudioStream that is open for output
"""
function write_Pa_AudioStream(stream::Pa_AudioStream, buffer)
retval = 1
sbufsize = length(stream.sbuffer)
inputlen = length(buffer)
if(inputlen > sbufsize)
info("Overflow at write_Pa_AudioStream")
retval = 0
elseif(inputlen < sbufsize)
info("Underflow at write_Pa_AudioStream")
retval = -1
end
while true
while stream.parent_may_use_buffer == false
sleep(0.001)
end
for idx in 1:min(sbufsize, inputlen)
stream.sbuffer[idx] = buffer[idx]
end
stream.parent_may_use_buffer = false
end
retval
end
############ Internal Functions ############
function portaudio_task(stream::PortAudioStream)
info("PortAudio Render Task Running...")
n = bufsize(stream)
buffer = zeros(AudioSample, n)
try
while true
while Pa_GetStreamReadAvailable(stream.stream) < n
sleep(0.005)
end
Pa_ReadStream(stream.stream, buffer, n, stream.show_warnings)
# assume the root is always active
rendered = render(stream.root.renderer, buffer, stream.info)::AudioBuf
for i in 1:length(rendered)
buffer[i] = rendered[i]
end
for i in (length(rendered)+1):n
buffer[i] = 0.0
end
while Pa_GetStreamWriteAvailable(stream.stream) < n
sleep(0.005)
end
Pa_WriteStream(stream.stream, buffer, n, stream.show_warnings)
end
catch ex
warn("Audio Task died with exception: $ex")
Base.show_backtrace(STDOUT, catch_backtrace())
end
end
"""
Get input device data, pass as a producer, no rendering
"""
function pa_input_task(stream::Pa_AudioStream)
info("PortAudio Input Task Running...")
n = bufsize(stream)
datatype = PaSampleFormat_to_T(stream.sformat)
# bigger ccall buffer to avoid overflow related errors
buffer = zeros(datatype, n * 8)
try
while true
while Pa_GetStreamReadAvailable(stream.stream) < n
sleep(0.005)
end
while stream.parent_may_use_buffer
sleep(0.005)
end
err = ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream.stream, buffer, n)
handle_status(err, stream.show_warnings)
stream.sbuffer[1: n] = buffer[1: n]
stream.parent_may_use_buffer = true
sleep(0.005)
end
catch ex
warn("Audio Input Task died with exception: $ex")
Base.show_backtrace(STDOUT, catch_backtrace())
end
end
"""
Send output device data, no rendering
"""
function pa_output_task(stream::Pa_AudioStream)
info("PortAudio Output Task Running...")
n = bufsize(stream)
try
while true
navail = stream.sbuffer_output_waiting
if navail > n
info("Possible output buffer overflow in stream")
navail = n
end
if (navail > 1) & (stream.parent_may_use_buffer == false) &
(Pa_GetStreamWriteAvailable(stream.stream) < navail)
Pa_WriteStream(stream.stream, stream.sbuffer,
navail, stream.show_warnings)
stream.parent_may_use_buffer = true
else
sleep(0.005)
end
end
catch ex
warn("Audio Output Task died with exception: $ex")
Base.show_backtrace(STDOUT, catch_backtrace())
end
end
function get_portaudio_devices()
require_portaudio_init()
device_count = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
pa_devices = [ [Pa_GetDeviceInfo(i), i] for i in 0:(device_count - 1)]
[PortAudioInterface(bytestring(d[1].name),
bytestring(Pa_GetHostApiInfo(d[1].host_api).name),
d[1].max_input_channels,
d[1].max_output_channels,
d[2])
for d in pa_devices]
end
function require_portaudio_init()
# can be called multiple times with no effect
global portaudio_inited
if !portaudio_inited
info("Initializing PortAudio. Expect errors as we scan devices")
Pa_Initialize()
portaudio_inited = true
end
end
end # module PortAudio

230
src/libportaudio.jl Normal file
View file

@ -0,0 +1,230 @@
# Low-level wrappers for Portaudio calls
# General type aliases
typealias PaTime Cdouble
typealias PaError Cint
typealias PaSampleFormat Culong
const PA_NO_ERROR = 0
const PA_INPUT_OVERFLOWED = -10000 + 19
const PA_OUTPUT_UNDERFLOWED = -10000 + 20
const paFloat32 = PaSampleFormat(0x01)
const paInt32 = PaSampleFormat(0x02)
const paInt24 = PaSampleFormat(0x04)
const paInt16 = PaSampleFormat(0x08)
const paInt8 = PaSampleFormat(0x10)
const paUInt8 = PaSampleFormat(0x20)
@compat const pa_sample_formats = Dict{PaSampleFormat, Type}(
1 => Float32
2 => Int32
4 => Int24
8 => Int16
16 => Int8
32 => UInt8
)
function Pa_Initialize()
err = ccall((:Pa_Initialize, libportaudio), PaError, ())
handle_status(err)
end
function Pa_Terminate()
err = ccall((:Pa_Terminate, libportaudio), PaError, ())
handle_status(err)
end
Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ())
function Pa_GetVersionText()
versionPtr = ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
bytestring(versionPtr)
end
# Host API Functions
# A Host API is the top-level of the PortAudio hierarchy. Each host API has a
# unique type ID that tells you which native backend it is (JACK, ALSA, ASIO,
# etc.). On a given system you can identify each backend by its index, which
# will range between 0 and Pa_GetHostApiCount() - 1. You can enumerate through
# all the host APIs on the system by iterating through those values.
typealias PaHostApiIndex Cint
typealias PaHostApiTypeId Cint
# PaHostApiTypeId values
@compat const pa_host_api_names = Dict{PaHostApiTypeId, ASCIIString}(
0 => "In Development", # use while developing support for a new host API
1 => "Direct Sound",
2 => "MME",
3 => "ASIO",
4 => "Sound Manager",
5 => "Core Audio",
7 => "OSS",
8 => "ALSA",
9 => "AL",
10 => "BeOS",
11 => "WDMKS",
12 => "Jack",
13 => "WASAPI",
14 => "AudioScience HPI"
)
type PaHostApiInfo
struct_version::Cint
api_type::PaHostApiTypeId
name::Ptr{Cchar}
deviceCount::Cint
defaultInputDevice::PaDeviceIndex
defaultOutputDevice::PaDeviceIndex
end
Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo}, (PaHostApiIndex,), i))
# Device Functions
typealias PaDeviceIndex Cint
type PaDeviceInfo
struct_version::Cint
name::Ptr{Cchar}
host_api::PaHostApiIndex
max_input_channels::Cint
max_output_channels::Cint
default_low_input_latency::PaTime
default_low_output_latency::PaTime
default_high_input_latency::PaTime
default_high_output_latency::PaTime
default_sample_rate::Cdouble
end
Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio),
Ptr{PaDeviceInfo}, (PaDeviceIndex,), i))
# Stream Functions
# 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
function Pa_OpenDefaultStream(inChannels::Integer, outChannels::Integer,
sampleFormat::PaSampleFormat,
sampleRate::Real, framesPerBuffer::Integer)
streamPtr::Array{PaStream} = PaStream[0]
err = ccall((:Pa_OpenDefaultStream, libportaudio),
PaError, (Ptr{PaStream}, Cint, Cint,
PaSampleFormat, Cdouble, Culong,
Ptr{PaStreamCallback}, Ptr{Void}),
streamPtr, inChannels, outChannels, sampleFormat, sampleRate,
framesPerBuffer, 0, 0)
handle_status(err)
streamPtr[1]
end
"""
Open a single stream, not necessarily the default one
The stream is unidirectional, either inout or default output
see http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html
"""
function Pa_OpenStream(device::PaDeviceIndex,
channels::Cint, input::Bool,
sampleFormat::PaSampleFormat,
sampleRate::Cdouble, framesPerBuffer::Culong)
streamPtr::Array{PaStream} = PaStream[0]
ioParameters = Pa_StreamParameters(device, channels,
sampleFormat, PaTime(0.001),
Ptr{Void}(0))
# CURRENTLY WORKING THIS OUT
if input
err = ccall((:Pa_OpenStream, libportaudio), PaError,
(PaStream,
Ptr{Pa_StreamParameters}, Ptr{Pa_StreamParameters},
Cdouble, Culong, PaStreamFlags,
Ptr{PaStreamCallback}, Ptr{Void}),
streamPtr, ioParameters, Ptr{Void}(0),
sampleRate, framesPerBuffer, 0,
Ptr{PaStreamCallback}(0), Ptr{Void}(0))
else
err = ccall((:Pa_OpenStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Ref{Pa_StreamParameters},
Cdouble, Culong, Culong,
Ptr{PaStreamCallback}, Ptr{Void}),
streamPtr, Ptr{Void}(0), ioParameters,
sampleRate, framesPerBuffer, 0,
Ptr{PaStreamCallback}(0), Ptr{Void}(0))
end
handle_status(err)
streamPtr[1]
end
function Pa_StartStream(stream::PaStream)
err = ccall((:Pa_StartStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_StopStream(stream::PaStream)
err = ccall((:Pa_StopStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_CloseStream(stream::PaStream)
err = ccall((:Pa_CloseStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_GetStreamReadAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
end
function Pa_GetStreamWriteAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
end
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
buf
end
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_WriteStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
nothing
end
# General utility function to handle the status from the Pa_* functions
function handle_status(err::PaError, show_warnings::Bool=true)
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
if show_warnings
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
warn("libportaudio: " * bytestring(msg))
end
elseif err != PA_NO_ERROR
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
error("libportaudio: " * bytestring(msg))
end
end

View file

@ -1,342 +0,0 @@
#### NullNode ####
type NullRenderer <: AudioRenderer end
typealias NullNode AudioNode{NullRenderer}
export NullNode
function render(node::NullRenderer, device_input::AudioBuf, info::DeviceInfo)
# TODO: preallocate buffer
return zeros(info.buf_size)
end
#### SinOsc ####
# Generates a sin tone at the given frequency
@compat type SinOscRenderer{T<:Union{Float32, AudioNode}} <: AudioRenderer
freq::T
phase::Float32
buf::AudioBuf
function SinOscRenderer(freq)
new(freq, 0.0, AudioSample[])
end
end
typealias SinOsc AudioNode{SinOscRenderer}
SinOsc(freq::Real) = SinOsc(SinOscRenderer{Float32}(freq))
SinOsc(freq::AudioNode) = SinOsc(SinOscRenderer{AudioNode}(freq))
SinOsc() = SinOsc(440)
export SinOsc
function render(node::SinOscRenderer{Float32}, device_input::AudioBuf,
info::DeviceInfo)
if length(node.buf) != info.buf_size
resize!(node.buf, info.buf_size)
end
outbuf = node.buf
phase = node.phase
freq = node.freq
# make sure these are Float32s so that we don't allocate doing conversions
# in the tight loop
pi2::Float32 = 2pi
phase_inc::Float32 = 2pi * freq / info.sample_rate
i::Int = 1
while i <= info.buf_size
outbuf[i] = sin(phase)
phase = (phase + phase_inc) % pi2
i += 1
end
node.phase = phase
return outbuf
end
function render(node::SinOscRenderer{AudioNode}, device_input::AudioBuf,
info::DeviceInfo)
freq = render(node.freq, device_input, info)::AudioBuf
block_size = min(length(freq), info.buf_size)
if(length(node.buf) != block_size)
resize!(node.buf, block_size)
end
outbuf = node.buf
phase::Float32 = node.phase
pi2::Float32 = 2pi
phase_step::Float32 = 2pi/(info.sample_rate)
i::Int = 1
while i <= block_size
outbuf[i] = sin(phase)
phase = (phase + phase_step*freq[i]) % pi2
i += 1
end
node.phase = phase
return outbuf
end
#### AudioMixer ####
# Mixes a set of inputs equally
type MixRenderer <: AudioRenderer
inputs::Vector{AudioNode}
buf::AudioBuf
MixRenderer(inputs) = new(inputs, AudioSample[])
MixRenderer() = MixRenderer(AudioNode[])
end
typealias AudioMixer AudioNode{MixRenderer}
export AudioMixer
function render(node::MixRenderer, device_input::AudioBuf, info::DeviceInfo)
if length(node.buf) != info.buf_size
resize!(node.buf, info.buf_size)
end
mix_buffer = node.buf
n_inputs = length(node.inputs)
i = 1
max_samples = 0
fill!(mix_buffer, 0)
while i <= n_inputs
rendered = render(node.inputs[i], device_input, info)::AudioBuf
nsamples = length(rendered)
max_samples = max(max_samples, nsamples)
j::Int = 1
while j <= nsamples
mix_buffer[j] += rendered[j]
j += 1
end
if nsamples < info.buf_size
deleteat!(node.inputs, i)
n_inputs -= 1
else
i += 1
end
end
if max_samples < length(mix_buffer)
return mix_buffer[1:max_samples]
else
# save the allocate and copy if we don't need to
return mix_buffer
end
end
Base.push!(mixer::AudioMixer, node::AudioNode) = push!(mixer.renderer.inputs, node)
#### Gain ####
@compat type GainRenderer{T<:Union{Float32, AudioNode}} <: AudioRenderer
in1::AudioNode
in2::T
buf::AudioBuf
GainRenderer(in1, in2) = new(in1, in2, AudioSample[])
end
function render(node::GainRenderer{Float32},
device_input::AudioBuf,
info::DeviceInfo)
input = render(node.in1, device_input, info)::AudioBuf
if length(node.buf) != length(input)
resize!(node.buf, length(input))
end
i = 1
while i <= length(input)
node.buf[i] = input[i] * node.in2
i += 1
end
return node.buf
end
function render(node::GainRenderer{AudioNode},
device_input::AudioBuf,
info::DeviceInfo)
in1_data = render(node.in1, device_input, info)::AudioBuf
in2_data = render(node.in2, device_input, info)::AudioBuf
block_size = min(length(in1_data), length(in2_data))
if length(node.buf) != block_size
resize!(node.buf, block_size)
end
i = 1
while i <= block_size
node.buf[i] = in1_data[i] * in2_data[i]
i += 1
end
return node.buf
end
typealias Gain AudioNode{GainRenderer}
Gain(in1::AudioNode, in2::Real) = Gain(GainRenderer{Float32}(in1, in2))
Gain(in1::AudioNode, in2::AudioNode) = Gain(GainRenderer{AudioNode}(in1, in2))
export Gain
#### Offset ####
type OffsetRenderer <: AudioRenderer
in_node::AudioNode
offset::Float32
buf::AudioBuf
OffsetRenderer(in_node, offset) = new(in_node, offset, AudioSample[])
end
function render(node::OffsetRenderer, device_input::AudioBuf, info::DeviceInfo)
input = render(node.in_node, device_input, info)::AudioBuf
if length(node.buf) != length(input)
resize!(node.buf, length(input))
end
i = 1
while i <= length(input)
node.buf[i] = input[i] + node.offset
i += 1
end
return node.buf
end
typealias Offset AudioNode{OffsetRenderer}
export Offset
#### Array Player ####
# Plays a AudioBuf by rendering it out piece-by-piece
type ArrayRenderer <: AudioRenderer
arr::AudioBuf
arr_index::Int
buf::AudioBuf
ArrayRenderer(arr::AudioBuf) = new(arr, 1, AudioSample[])
end
typealias ArrayPlayer AudioNode{ArrayRenderer}
export ArrayPlayer
function render(node::ArrayRenderer, device_input::AudioBuf, info::DeviceInfo)
range_end = min(node.arr_index + info.buf_size-1, length(node.arr))
block_size = range_end - node.arr_index + 1
if length(node.buf) != block_size
resize!(node.buf, block_size)
end
copy!(node.buf, 1, node.arr, node.arr_index, block_size)
node.arr_index = range_end + 1
return node.buf
end
# Allow users to play a raw array by wrapping it in an ArrayPlayer
function play(arr::AudioBuf, args...)
player = ArrayPlayer(arr)
play(player, args...)
end
# If the array is the wrong floating type, convert it
function play{T <: AbstractFloat}(arr::Array{T}, args...)
arr = convert(AudioBuf, arr)
play(arr, args...)
end
# If the array is an integer type, scale to [-1, 1] floating point
# integer audio can be slightly (by 1) more negative than positive,
# so we just scale so that +/- typemax(T) becomes +/- 1
function play{T <: Signed}(arr::Array{T}, args...)
arr = arr / typemax(T)
play(arr, args...)
end
function play{T <: Unsigned}(arr::Array{T}, args...)
zero = (typemax(T) + 1) / 2
range = floor(typemax(T) / 2)
arr = (arr .- zero) / range
play(arr, args...)
end
#### Noise ####
type WhiteNoiseRenderer <: AudioRenderer end
typealias WhiteNoise AudioNode{WhiteNoiseRenderer}
export WhiteNoise
function render(node::WhiteNoiseRenderer, device_input::AudioBuf, info::DeviceInfo)
return rand(AudioSample, info.buf_size) .* 2 .- 1
end
#### AudioInput ####
# Renders incoming audio input from the hardware
type InputRenderer <: AudioRenderer
channel::Int
InputRenderer(channel::Integer) = new(channel)
InputRenderer() = new(1)
end
function render(node::InputRenderer, device_input::AudioBuf, info::DeviceInfo)
@assert size(device_input, 1) == info.buf_size
return device_input[:, node.channel]
end
typealias AudioInput AudioNode{InputRenderer}
export AudioInput
#### LinRamp ####
type LinRampRenderer <: AudioRenderer
key_samples::Array{AudioSample}
key_durations::Array{Float32}
duration::Float32
buf::AudioBuf
LinRampRenderer(start, finish, dur) = LinRampRenderer([start,finish], [dur])
LinRampRenderer(key_samples, key_durations) =
LinRampRenderer(
[convert(AudioSample,s) for s in key_samples],
[convert(Float32,d) for d in key_durations]
)
function LinRampRenderer(key_samples::Array{AudioSample}, key_durations::Array{Float32})
@assert length(key_samples) == length(key_durations) + 1
new(key_samples, key_durations, sum(key_durations), AudioSample[])
end
end
typealias LinRamp AudioNode{LinRampRenderer}
export LinRamp
function render(node::LinRampRenderer, device_input::AudioBuf, info::DeviceInfo)
# Resize buffer if (1) it's too small or (2) we've hit the end of the ramp
ramp_samples::Int = round(Int, node.duration * info.sample_rate)
block_samples = min(ramp_samples, info.buf_size)
if length(node.buf) != block_samples
resize!(node.buf, block_samples)
end
# Fill the buffer as long as there are more segments
dt::Float32 = 1/info.sample_rate
i::Int = 1
while i <= length(node.buf) && length(node.key_samples) > 1
# Fill as much of the buffer as we can with the current segment
ds::Float32 = (node.key_samples[2] - node.key_samples[1]) / node.key_durations[1] / info.sample_rate
while i <= length(node.buf)
node.buf[i] = node.key_samples[1]
node.key_samples[1] += ds
node.key_durations[1] -= dt
node.duration -= dt
i += 1
# Discard segment if we're finished
if node.key_durations[1] <= 0
if length(node.key_durations) > 1
node.key_durations[2] -= node.key_durations[1]
end
shift!(node.key_samples)
shift!(node.key_durations)
break
end
end
end
return node.buf
end

View file

@ -1,16 +0,0 @@
*(node::AudioNode, coef::Real) = Gain(node, coef)
*(coef::Real, node::AudioNode) = Gain(node, coef)
*(node1::AudioNode, node2::AudioNode) = Gain(node1, node2)
# multiplying by silence gives silence
*(in1::NullNode, in2::NullNode) = in1
*(in1::AudioNode, in2::NullNode) = in2
*(in1::NullNode, in2::AudioNode) = in1
+(in1::AudioNode, in2::AudioNode) = AudioMixer([in1, in2])
# adding silence has no effect
+(in1::NullNode, in2::NullNode) = in1
+(in1::AudioNode, in2::NullNode) = in1
+(in1::NullNode, in2::AudioNode) = in2
+(in1::AudioNode, in2::Real) = Offset(in1, in2)
+(in1::Real, in2::AudioNode) = Offset(in2, in1)

View file

@ -1,473 +0,0 @@
typealias PaTime Cdouble
typealias PaError Cint
typealias PaSampleFormat Culong
# PaStream is always used as an opaque type, so we're always dealing
# with the pointer
typealias PaStream Ptr{Void}
typealias PaDeviceIndex Cint
typealias PaHostApiIndex Cint
typealias PaTime Cdouble
typealias PaHostApiTypeId Cint
typealias PaStreamCallback Void
typealias PaStreamFlags Culong
const PA_NO_ERROR = 0
const PA_INPUT_OVERFLOWED = -10000 + 19
const PA_OUTPUT_UNDERFLOWED = -10000 + 20
const paFloat32 = convert(PaSampleFormat, 0x01)
const paInt32 = convert(PaSampleFormat, 0x02)
const paInt24 = convert(PaSampleFormat, 0x04)
const paInt16 = convert(PaSampleFormat, 0x08)
const paInt8 = convert(PaSampleFormat, 0x10)
const paUInt8 = convert(PaSampleFormat, 0x20)
# PaHostApiTypeId values
@compat const pa_host_api_names = Dict(
0 => "In Development", # use while developing support for a new host API
1 => "Direct Sound",
2 => "MME",
3 => "ASIO",
4 => "Sound Manager",
5 => "Core Audio",
7 => "OSS",
8 => "ALSA",
9 => "AL",
10 => "BeOS",
11 => "WDMKS",
12 => "Jack",
13 => "WASAPI",
14 => "AudioScience HPI"
)
# track whether we've already inited PortAudio
portaudio_inited = false
################## Types ####################
type PortAudioStream <: AudioStream
root::AudioMixer
info::DeviceInfo
show_warnings::Bool
stream::PaStream
function PortAudioStream(sample_rate::Integer=44100,
buf_size::Integer=1024,
show_warnings::Bool=false)
require_portaudio_init()
stream = Pa_OpenDefaultStream(1, 1, paFloat32, sample_rate, buf_size)
Pa_StartStream(stream)
root = AudioMixer()
this = new(root, DeviceInfo(sample_rate, buf_size),
show_warnings, stream)
info("Scheduling PortAudio Render Task...")
# the task will actually start running the next time the current task yields
@schedule(portaudio_task(this))
finalizer(this, destroy)
this
end
end
function destroy(stream::PortAudioStream)
# in 0.3 we can't print from a finalizer, as STDOUT may have been GC'ed
# already and we get a segfault. See
# https://github.com/JuliaLang/julia/issues/6075
#info("Cleaning up stream")
Pa_StopStream(stream.stream)
Pa_CloseStream(stream.stream)
# we only have 1 stream at a time, so if we're closing out we can just
# terminate PortAudio.
Pa_Terminate()
portaudio_inited = false
end
type Pa_StreamParameters
device::PaDeviceIndex
channelCount::Cint
sampleFormat::PaSampleFormat
suggestedLatency::PaTime
hostAPISpecificStreamInfo::Ptr{Void}
end
"""
Open a single stream, not necessarily the default one
The stream is unidirectional, either inout or default output
see http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html
"""
function Pa_OpenStream(device::PaDeviceIndex,
channels::Cint, input::Bool,
sampleFormat::PaSampleFormat,
sampleRate::Cdouble, framesPerBuffer::Culong)
streamPtr::Array{PaStream} = PaStream[0]
ioParameters = Pa_StreamParameters(device, channels,
sampleFormat, PaTime(0.001),
Ptr{Void}(0))
if input
err = ccall((:Pa_OpenStream, libportaudio), PaError,
(PaStream, Ref{Pa_StreamParameters}, Ptr{Void},
Cdouble, Culong, Culong,
Ptr{PaStreamCallback}, Ptr{Void}),
streamPtr, ioParameters, Ptr{Void}(0),
sampleRate, framesPerBuffer, 0,
Ptr{PaStreamCallback}(0), Ptr{Void}(0))
else
err = ccall((:Pa_OpenStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Ref{Pa_StreamParameters},
Cdouble, Culong, Culong,
Ptr{PaStreamCallback}, Ptr{Void}),
streamPtr, Ptr{Void}(0), ioParameters,
sampleRate, framesPerBuffer, 0,
Ptr{PaStreamCallback}(0), Ptr{Void}(0))
end
handle_status(err)
streamPtr[1]
end
type Pa_AudioStream <: AudioStream
root::AudioMixer
info::DeviceInfo
show_warnings::Bool
stream::PaStream
sformat::PaSampleFormat
sbuffer::Array{Real}
sbuffer_output_waiting::Integer
parent_may_use_buffer::Bool
"""
Get device parameters needed for opening with portaudio
default is input as 44100/16bit int, same as CD audio type input
"""
function Pa_AudioStream(device_index, channels=2, input=false,
sample_rate::Integer=44100,
framesPerBuffer::Integer=2048,
show_warnings::Bool=false,
sample_format::PaSampleFormat=paInt16)
require_portaudio_init()
stream = Pa_OpenStream(device_index, channels, input, sample_format,
Cdouble(sample_rate), Culong(framesPerBuffer))
Pa_StartStream(stream)
root = AudioMixer()
datatype = PaSampleFormat_to_T(sample_format)
sbuf = ones(datatype, framesPerBuffer)
this = new(root, DeviceInfo(sample_rate, framesPerBuffer),
show_warnings, stream, sample_format, sbuf, 0, false)
info("Scheduling PortAudio Render Task...")
if input
@schedule(pa_input_task(this))
else
@schedule(pa_output_task(this))
end
this
end
end
"""
Blocking read from a Pa_AudioStream that is open as input
"""
function read_Pa_AudioStream(stream::Pa_AudioStream)
while true
while stream.parent_may_use_buffer == false
sleep(0.001)
end
buffer = deepcopy(stream.sbuffer)
stream.parent_may_use_buffer = false
return buffer
end
end
"""
Blocking write to a Pa_AudioStream that is open for output
"""
function write_Pa_AudioStream(stream::Pa_AudioStream, buffer)
retval = 1
sbufsize = length(stream.sbuffer)
inputlen = length(buffer)
if(inputlen > sbufsize)
info("Overflow at write_Pa_AudioStream")
retval = 0
elseif(inputlen < sbufsize)
info("Underflow at write_Pa_AudioStream")
retval = -1
end
while true
while stream.parent_may_use_buffer == false
sleep(0.001)
end
for idx in 1:min(sbufsize, inputlen)
stream.sbuffer[idx] = buffer[idx]
end
stream.parent_may_use_buffer = false
end
retval
end
############ Internal Functions ############
function portaudio_task(stream::PortAudioStream)
info("PortAudio Render Task Running...")
n = bufsize(stream)
buffer = zeros(AudioSample, n)
try
while true
while Pa_GetStreamReadAvailable(stream.stream) < n
sleep(0.005)
end
Pa_ReadStream(stream.stream, buffer, n, stream.show_warnings)
# assume the root is always active
rendered = render(stream.root.renderer, buffer, stream.info)::AudioBuf
for i in 1:length(rendered)
buffer[i] = rendered[i]
end
for i in (length(rendered)+1):n
buffer[i] = 0.0
end
while Pa_GetStreamWriteAvailable(stream.stream) < n
sleep(0.005)
end
Pa_WriteStream(stream.stream, buffer, n, stream.show_warnings)
end
catch ex
warn("Audio Task died with exception: $ex")
Base.show_backtrace(STDOUT, catch_backtrace())
end
end
"""
Helper function to make the right type of buffer for various
sample formats. Converts PaSampleFormat to a typeof
"""
function PaSampleFormat_to_T(fmt::PaSampleFormat)
retval = UInt8(0x0)
if fmt == 1
retval = Float32(1.0)
elseif fmt == 2
retval = Int32(0x02)
elseif fmt == 4
retval = Int24(0x04)
elseif fmt == 8
retval = Int16(0x08)
elseif fmt == 16
retval = Int8(0x10)
elseif fmt == 32
retval = UInt8(0x20)
else
info("Flawed input to PaSampleFormat_to_T, primitive unknown")
end
typeof(retval)
end
"""
Get input device data, pass as a producer, no rendering
"""
function pa_input_task(stream::Pa_AudioStream)
info("PortAudio Input Task Running...")
n = bufsize(stream)
datatype = PaSampleFormat_to_T(stream.sformat)
# bigger ccall buffer to avoid overflow related errors
buffer = zeros(datatype, n * 8)
try
while true
while Pa_GetStreamReadAvailable(stream.stream) < n
sleep(0.005)
end
while stream.parent_may_use_buffer
sleep(0.005)
end
err = ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream.stream, buffer, n)
handle_status(err, stream.show_warnings)
stream.sbuffer[1: n] = buffer[1: n]
stream.parent_may_use_buffer = true
sleep(0.005)
end
catch ex
warn("Audio Input Task died with exception: $ex")
Base.show_backtrace(STDOUT, catch_backtrace())
end
end
"""
Send output device data, no rendering
"""
function pa_output_task(stream::Pa_AudioStream)
info("PortAudio Output Task Running...")
n = bufsize(stream)
try
while true
navail = stream.sbuffer_output_waiting
if navail > n
info("Possible output buffer overflow in stream")
navail = n
end
if (navail > 1) & (stream.parent_may_use_buffer == false) &
(Pa_GetStreamWriteAvailable(stream.stream) < navail)
Pa_WriteStream(stream.stream, stream.sbuffer,
navail, stream.show_warnings)
stream.parent_may_use_buffer = true
else
sleep(0.005)
end
end
catch ex
warn("Audio Output Task died with exception: $ex")
Base.show_backtrace(STDOUT, catch_backtrace())
end
end
type PaDeviceInfo
struct_version::Cint
name::Ptr{Cchar}
host_api::PaHostApiIndex
max_input_channels::Cint
max_output_channels::Cint
default_low_input_latency::PaTime
default_low_output_latency::PaTime
default_high_input_latency::PaTime
default_high_output_latency::PaTime
default_sample_rate::Cdouble
end
type PaHostApiInfo
struct_version::Cint
api_type::PaHostApiTypeId
name::Ptr{Cchar}
deviceCount::Cint
defaultInputDevice::PaDeviceIndex
defaultOutputDevice::PaDeviceIndex
end
type PortAudioInterface <: AudioInterface
name::AbstractString
host_api::AbstractString
max_input_channels::Int
max_output_channels::Int
device_index::PaDeviceIndex
end
function get_portaudio_devices()
require_portaudio_init()
device_count = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
pa_devices = [ [Pa_GetDeviceInfo(i), i] for i in 0:(device_count - 1)]
[PortAudioInterface(bytestring(d[1].name),
bytestring(Pa_GetHostApiInfo(d[1].host_api).name),
d[1].max_input_channels,
d[1].max_output_channels,
d[2])
for d in pa_devices]
end
function require_portaudio_init()
# can be called multiple times with no effect
global portaudio_inited
if !portaudio_inited
info("Initializing PortAudio. Expect errors as we scan devices")
Pa_Initialize()
portaudio_inited = true
end
end
# Low-level wrappers for Portaudio calls
Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio),
Ptr{PaDeviceInfo}, (PaDeviceIndex,), i))
Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio),
Ptr{PaHostApiInfo}, (PaHostApiIndex,), i))
function Pa_Initialize()
err = ccall((:Pa_Initialize, libportaudio), PaError, ())
handle_status(err)
end
function Pa_Terminate()
err = ccall((:Pa_Terminate, libportaudio), PaError, ())
handle_status(err)
end
function Pa_StartStream(stream::PaStream)
err = ccall((:Pa_StartStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_StopStream(stream::PaStream)
err = ccall((:Pa_StopStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_CloseStream(stream::PaStream)
err = ccall((:Pa_CloseStream, libportaudio), PaError,
(PaStream,), stream)
handle_status(err)
end
function Pa_GetStreamReadAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
end
function Pa_GetStreamWriteAvailable(stream::PaStream)
avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
(PaStream,), stream)
avail >= 0 || handle_status(avail)
avail
end
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_ReadStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
buf
end
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf),
show_warnings::Bool=true)
frames <= length(buf) || error("Need a buffer at least $frames long")
err = ccall((:Pa_WriteStream, libportaudio), PaError,
(PaStream, Ptr{Void}, Culong),
stream, buf, frames)
handle_status(err, show_warnings)
nothing
end
Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ())
function Pa_GetVersionText()
versionPtr = ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
bytestring(versionPtr)
end
function Pa_OpenDefaultStream(inChannels::Integer, outChannels::Integer,
sampleFormat::PaSampleFormat,
sampleRate::Real, framesPerBuffer::Integer)
streamPtr::Array{PaStream} = PaStream[0]
err = ccall((:Pa_OpenDefaultStream, libportaudio),
PaError, (Ptr{PaStream}, Cint, Cint,
PaSampleFormat, Cdouble, Culong,
Ptr{PaStreamCallback}, Ptr{Void}),
streamPtr, inChannels, outChannels, sampleFormat, sampleRate,
framesPerBuffer, 0, 0)
handle_status(err)
streamPtr[1]
end
function handle_status(err::PaError, show_warnings::Bool=true)
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
if show_warnings
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
warn("libportaudio: " * bytestring(msg))
end
elseif err != PA_NO_ERROR
msg = ccall((:Pa_GetErrorText, libportaudio),
Ptr{Cchar}, (PaError,), err)
error("libportaudio: " * bytestring(msg))
end
end

View file

@ -1,209 +0,0 @@
export af_open, FilePlayer, rewind, samplerate
@compat const SFM_READ = Int32(0x10)
@compat const SFM_WRITE = Int32(0x20)
const SF_FORMAT_WAV = 0x010000
const SF_FORMAT_FLAC = 0x170000
const SF_FORMAT_OGG = 0x200060
const SF_FORMAT_PCM_S8 = 0x0001 # Signed 8 bit data
const SF_FORMAT_PCM_16 = 0x0002 # Signed 16 bit data
const SF_FORMAT_PCM_24 = 0x0003 # Signed 24 bit data
const SF_FORMAT_PCM_32 = 0x0004 # Signed 32 bit data
const SF_SEEK_SET = 0
const SF_SEEK_CUR = 1
const SF_SEEK_END = 2
@compat const EXT_TO_FORMAT = Dict(
".wav" => SF_FORMAT_WAV,
".flac" => SF_FORMAT_FLAC
)
type SF_INFO
frames::Int64
samplerate::Int32
channels::Int32
format::Int32
sections::Int32
seekable::Int32
end
type AudioFile
filePtr::Ptr{Void}
sfinfo::SF_INFO
end
samplerate(f::AudioFile) = f.sfinfo.samplerate
# AudioIO.open is part of the public API, but is not exported so that it
# doesn't conflict with Base.open
function open(path::AbstractString, mode::AbstractString = "r",
sampleRate::Integer = 44100, channels::Integer = 1,
format::Integer = 0)
@assert channels <= 2
sfinfo = SF_INFO(0, 0, 0, 0, 0, 0)
file_mode = SFM_READ
if mode == "w"
file_mode = SFM_WRITE
sfinfo.samplerate = sampleRate
sfinfo.channels = channels
if format == 0
_, ext = splitext(path)
sfinfo.format = EXT_TO_FORMAT[ext] | SF_FORMAT_PCM_16
else
sfinfo.format = format
end
end
filePtr = ccall((:sf_open, libsndfile), Ptr{Void},
(Ptr{UInt8}, Int32, Ptr{SF_INFO}),
path, file_mode, &sfinfo)
if filePtr == C_NULL
errmsg = ccall((:sf_strerror, libsndfile), Ptr{UInt8}, (Ptr{Void},), filePtr)
error(bytestring(errmsg))
end
return AudioFile(filePtr, sfinfo)
end
function Base.close(file::AudioFile)
err = ccall((:sf_close, libsndfile), Int32, (Ptr{Void},), file.filePtr)
if err != 0
error("Failed to close file")
end
end
function open(f::Function, args...)
file = AudioIO.open(args...)
try
f(file)
finally
close(file)
end
end
function af_open(args...)
warn("af_open is deprecated, please use AudioIO.open instead")
AudioIO.open(args...)
end
# TODO: we should implement a general read(node::AudioNode) that pulls data
# through an arbitrary render chain and returns the result as a vector
function Base.read(file::AudioFile, nframes::Integer, dtype::Type)
@assert file.sfinfo.channels <= 2
# the data comes in interleaved
arr = zeros(dtype, file.sfinfo.channels, nframes)
if dtype == Int16
nread = ccall((:sf_readf_short, libsndfile), Int64,
(Ptr{Void}, Ptr{Int16}, Int64),
file.filePtr, arr, nframes)
elseif dtype == Int32
nread = ccall((:sf_readf_int, libsndfile), Int64,
(Ptr{Void}, Ptr{Int32}, Int64),
file.filePtr, arr, nframes)
elseif dtype == Float32
nread = ccall((:sf_readf_float, libsndfile), Int64,
(Ptr{Void}, Ptr{Float32}, Int64),
file.filePtr, arr, nframes)
elseif dtype == Float64
nread = ccall((:sf_readf_double, libsndfile), Int64,
(Ptr{Void}, Ptr{Float64}, Int64),
file.filePtr, arr, nframes)
end
return arr[:, 1:nread]'
end
Base.read(file::AudioFile, dtype::Type) = Base.read(file, file.sfinfo.frames, dtype)
Base.read(file::AudioFile, nframes::Integer) = Base.read(file, nframes, Int16)
Base.read(file::AudioFile) = Base.read(file, Int16)
function Base.write{T}(file::AudioFile, frames::Array{T})
@assert file.sfinfo.channels <= 2
nframes = round(Int, length(frames) / file.sfinfo.channels)
if T == Int16
return ccall((:sf_writef_short, libsndfile), Int64,
(Ptr{Void}, Ptr{Int16}, Int64),
file.filePtr, frames, nframes)
elseif T == Int32
return ccall((:sf_writef_int, libsndfile), Int64,
(Ptr{Void}, Ptr{Int32}, Int64),
file.filePtr, frames, nframes)
elseif T == Float32
return ccall((:sf_writef_float, libsndfile), Int64,
(Ptr{Void}, Ptr{Float32}, Int64),
file.filePtr, frames, nframes)
elseif T == Float64
return ccall((:sf_writef_double, libsndfile), Int64,
(Ptr{Void}, Ptr{Float64}, Int64),
file.filePtr, frames, nframes)
end
end
function Base.seek(file::AudioFile, offset::Integer, whence::Integer)
new_offset = ccall((:sf_seek, libsndfile), Int64,
(Ptr{Void}, Int64, Int32), file.filePtr, offset, whence)
if new_offset < 0
error("Could not seek to $(offset) in file")
end
new_offset
end
# Some convenience methods for easily navigating through a sound file
Base.seek(file::AudioFile, offset::Integer) = seek(file, offset, SF_SEEK_SET)
rewind(file::AudioFile) = seek(file, 0, SF_SEEK_SET)
type FileRenderer <: AudioRenderer
file::AudioFile
function FileRenderer(file::AudioFile)
node = new(file)
finalizer(node, n -> close(n.file))
return node
end
end
typealias FilePlayer AudioNode{FileRenderer}
FilePlayer(file::AudioFile) = FilePlayer(FileRenderer(file))
FilePlayer(path::AbstractString) = FilePlayer(AudioIO.open(path))
function render(node::FileRenderer, device_input::AudioBuf, info::DeviceInfo)
@assert node.file.sfinfo.samplerate == info.sample_rate
# Keep reading data from the file until the output buffer is full, but stop
# as soon as no more data can be read from the file
audio = Array(AudioSample, 0, node.file.sfinfo.channels)
while true
read_audio = read(node.file, info.buf_size-size(audio, 1), AudioSample)
audio = vcat(audio, read_audio)
if size(audio, 1) >= info.buf_size || size(read_audio, 1) <= 0
break
end
end
# if the file is stereo, mix the two channels together
if node.file.sfinfo.channels == 2
return (audio[:, 1] / 2) + (audio[:, 2] / 2)
else
return audio
end
end
function play(filename::AbstractString, args...)
player = FilePlayer(filename)
play(player, args...)
end
function play(file::AudioFile, args...)
player = FilePlayer(file)
play(player, args...)
end

View file

@ -1 +1 @@
FactCheck
BaseTestNext

View file

@ -1,18 +1,7 @@
#!/usr/bin/env julia
using FactCheck
test_regex = r"^test_.*\.jl$"
test_dir = Pkg.dir("AudioIO", "test")
test_files = filter(n -> ismatch(test_regex, n), readdir(test_dir))
if length(test_files) == 0
error("No test files found. Make sure you're running from the root directory")
@testset "No Tests" begin
@test false
end
for test_file in test_files
include(test_file)
end
# return the overall exit status
exitstatus()
exit(0)

View file

@ -1,90 +0,0 @@
module TestAudioIO
using FactCheck
using Compat
using AudioIO
import AudioIO.AudioBuf
const TEST_SAMPLERATE = 44100
const TEST_BUF_SIZE = 1024
include("testhelpers.jl")
type TestAudioStream <: AudioIO.AudioStream
root::AudioIO.AudioMixer
info::AudioIO.DeviceInfo
function TestAudioStream()
root = AudioMixer()
new(root, AudioIO.DeviceInfo(TEST_SAMPLERATE, TEST_BUF_SIZE))
end
end
# render the stream and return the next block of audio. This is used in testing
# to simulate the audio callback that's normally called by the device.
function process(stream::TestAudioStream)
out_array = zeros(AudioIO.AudioSample, stream.info.buf_size)
in_array = zeros(AudioIO.AudioSample, stream.info.buf_size)
rendered = AudioIO.render(stream.root, in_array, stream.info)
out_array[1:length(rendered)] = rendered
return out_array
end
#### Test playing back various vector types ####
facts("Array playback") do
# data shared between tests, for convenience
t = linspace(0, 2, 2 * 44100)
phase = 2pi * 100 * t
## Test Float32 arrays, this is currently the native audio playback format
context("Playing Float32 arrays") do
f32 = convert(Array{Float32}, sin(phase))
test_stream = TestAudioStream()
player = play(f32, test_stream)
@fact process(test_stream) --> f32[1:TEST_BUF_SIZE]
end
context("Playing Float64 arrays") do
f64 = convert(Array{Float64}, sin(phase))
test_stream = TestAudioStream()
player = play(f64, test_stream)
@fact process(test_stream) --> convert(AudioBuf, f64[1:TEST_BUF_SIZE])
end
context("Playing Int8(Signed) arrays") do
i8 = Int8[-127:127;]
test_stream = TestAudioStream()
player = play(i8, test_stream)
@fact process(test_stream)[1:255] -->
mse(convert(AudioBuf, collect(linspace(-1.0, 1.0, 255))))
end
context("Playing UInt8(Unsigned) arrays") do
# for unsigned 8-bit audio silence is represented as 128, so the symmetric range
# is 1-255
ui8 = UInt8[1:255;]
test_stream = TestAudioStream()
player = play(ui8, test_stream)
@fact process(test_stream)[1:255] -->
mse(convert(AudioBuf, collect(linspace(-1.0, 1.0, 255))))
end
end
facts("AudioNode Stopping") do
test_stream = TestAudioStream()
node = SinOsc(440)
play(node, test_stream)
process(test_stream)
stop(node)
@fact process(test_stream) --> zeros(AudioIO.AudioSample, TEST_BUF_SIZE)
end
facts("Audio Device Listing") do
# there aren't any devices on the Travis machine so just test that this doesn't crash
@fact get_audio_devices() --> issubtype(Array)
end
end # module TestAudioIO

View file

@ -1,194 +0,0 @@
module TestAudioIONodes
using Compat
using FactCheck
using AudioIO
import AudioIO: AudioSample, AudioBuf, AudioRenderer, AudioNode
import AudioIO: DeviceInfo, render
include("testhelpers.jl")
# A TestNode just renders out 1:buf_size each frame
type TestRenderer <: AudioRenderer
buf::AudioBuf
TestRenderer(buf_size::Integer) = new(AudioSample[1:buf_size;])
end
typealias TestNode AudioNode{TestRenderer}
TestNode(buf_size) = TestNode(TestRenderer(buf_size))
function render(node::TestRenderer,
device_input::AudioBuf,
info::DeviceInfo)
return node.buf
end
test_info = DeviceInfo(44100, 512)
dev_input = zeros(AudioSample, test_info.buf_size)
facts("Validating TestNode allocation") do
# first validate that the TestNode doesn't allocate so it doesn't mess up our
# other tests
test = TestNode(test_info.buf_size)
# JIT
render(test, dev_input, test_info)
@fact (@allocated render(test, dev_input, test_info)) --> 16
end
#### AudioMixer Tests ####
# TODO: there should be a setup/teardown mechanism and some way to isolate
# tests
facts("AudioMixer") do
context("0 Input Mixer") do
mix = AudioMixer()
render_output = render(mix, dev_input, test_info)
@fact render_output --> AudioSample[]
# 0.4 uses 64 bytes, 0.3 uses 48
@fact (@allocated render(mix, dev_input, test_info)) --> less_than(65)
end
context("1 Input Mixer") do
testnode = TestNode(test_info.buf_size)
mix = AudioMixer([testnode])
render_output = render(mix, dev_input, test_info)
@fact render_output --> AudioSample[1:test_info.buf_size;]
@fact (@allocated render(mix, dev_input, test_info)) --> 64
end
context("2 Input Mixer") do
test1 = TestNode(test_info.buf_size)
test2 = TestNode(test_info.buf_size)
mix = AudioMixer([test1, test2])
render_output = render(mix, dev_input, test_info)
# make sure the two inputs are being added together
@fact render_output --> 2 * AudioSample[1:test_info.buf_size;]
@fact (@allocated render(mix, dev_input, test_info)) --> 96
# now we'll stop one of the inputs and make sure it gets removed
stop(test1)
render_output = render(mix, dev_input, test_info)
# make sure the two inputs are being added together
@fact render_output --> AudioSample[1:test_info.buf_size;]
stop(mix)
render_output = render(mix, dev_input, test_info)
@fact render_output --> AudioSample[]
end
end
MSE_THRESH = 1e-7
facts("SinOSC") do
freq = 440
# note that this range includes the end, which is why there are
# sample_rate+1 samples
t = linspace(0, 1, round(Int, test_info.sample_rate+1))
test_vect = convert(AudioBuf, sin(2pi * t * freq))
context("Fixed Frequency") do
osc = SinOsc(freq)
render_output = render(osc, dev_input, test_info)
@fact mse(render_output, test_vect[1:test_info.buf_size]) -->
lessthan(MSE_THRESH)
render_output = render(osc, dev_input, test_info)
@fact mse(render_output,
test_vect[test_info.buf_size+1:2*test_info.buf_size]) -->
lessthan(MSE_THRESH)
@fact (@allocated render(osc, dev_input, test_info)) --> 64
stop(osc)
render_output = render(osc, dev_input, test_info)
@fact render_output --> AudioSample[]
end
context("Testing SinOsc with signal input") do
t = linspace(0, 1, round(Int, test_info.sample_rate+1))
f = 440 .- t .* (440-110)
dt = 1 / test_info.sample_rate
# NOTE - this treats the phase as constant at each sample, which isn't strictly
# true. Unfortunately doing this correctly requires knowing more about the
# modulating signal and doing the real integral
phase = cumsum(2pi * dt .* f)
unshift!(phase, 0)
expected = convert(AudioBuf, sin(phase))
freq = LinRamp(440, 110, 1)
osc = SinOsc(freq)
render_output = render(osc, dev_input, test_info)
@fact mse(render_output, expected[1:test_info.buf_size]) -->
lessthan(MSE_THRESH)
render_output = render(osc, dev_input, test_info)
@fact mse(render_output,
expected[test_info.buf_size+1:2*test_info.buf_size]) -->
lessthan(MSE_THRESH)
# give a bigger budget here because we're rendering 2 nodes
@fact (@allocated render(osc, dev_input, test_info)) --> 160
end
end
facts("AudioInput") do
node = AudioInput()
test_data = rand(AudioSample, test_info.buf_size)
render_output = render(node, test_data, test_info)
@fact render_output --> test_data
end
facts("ArrayPlayer") do
context("playing long sample") do
v = rand(AudioSample, 44100)
player = ArrayPlayer(v)
render_output = render(player, dev_input, test_info)
@fact render_output --> v[1:test_info.buf_size]
render_output = render(player, dev_input, test_info)
@fact render_output --> v[(test_info.buf_size + 1) : (2*test_info.buf_size)]
@fact (@allocated render(player, dev_input, test_info)) --> 192
stop(player)
render_output = render(player, dev_input, test_info)
@fact render_output --> AudioSample[]
end
context("testing end of vector") do
# give a vector just a bit larger than 1 buffer size
v = rand(AudioSample, test_info.buf_size + 1)
player = ArrayPlayer(v)
render(player, dev_input, test_info)
render_output = render(player, dev_input, test_info)
@fact render_output --> v[test_info.buf_size+1:end]
end
end
facts("Gain") do
context("Constant Gain") do
gained = TestNode(test_info.buf_size) * 0.75
render_output = render(gained, dev_input, test_info)
@fact render_output --> 0.75 * AudioSample[1:test_info.buf_size;]
@fact (@allocated render(gained, dev_input, test_info)) --> 32
end
context("Gain by a Signal") do
gained = TestNode(test_info.buf_size) * TestNode(test_info.buf_size)
render_output = render(gained, dev_input, test_info)
@fact render_output --> AudioSample[1:test_info.buf_size;] .* AudioSample[1:test_info.buf_size;]
@fact (@allocated render(gained, dev_input, test_info)) --> 48
end
end
facts("LinRamp") do
ramp = LinRamp(0.25, 0.80, 1)
expected = convert(AudioBuf, collect(linspace(0.25, 0.80, round(Int, test_info.sample_rate+1))))
render_output = render(ramp, dev_input, test_info)
@fact mse(render_output, expected[1:test_info.buf_size]) -->
lessthan(MSE_THRESH)
render_output = render(ramp, dev_input, test_info)
@fact mse(render_output,
expected[(test_info.buf_size+1):(2*test_info.buf_size)]) -->
lessthan(MSE_THRESH)
@fact (@allocated render(ramp, dev_input, test_info)) --> 64
end
facts("Offset") do
offs = TestNode(test_info.buf_size) + 0.5
render_output = render(offs, dev_input, test_info)
@fact render_output --> 0.5 + AudioSample[1:test_info.buf_size;]
@fact (@allocated render(offs, dev_input, test_info)) --> 32
end
end # module TestAudioIONodes

View file

@ -1,83 +0,0 @@
module TestSndfile
using Compat
using FactCheck
using AudioIO
import AudioIO: DeviceInfo, render, AudioSample, AudioBuf
include("testhelpers.jl")
facts("WAV file write/read") do
fname = Pkg.dir("AudioIO", "test", "sinwave.wav")
srate = 44100
freq = 440
t = collect(0 : 2 * srate - 1) / srate
phase = 2 * pi * freq * t
reference = round(Int16, (2 ^ 15 - 1) * sin(phase))
AudioIO.open(fname, "w") do f
write(f, reference)
end
# test basic reading
AudioIO.open(fname) do f
@fact f.sfinfo.channels --> 1
@fact f.sfinfo.frames --> 2 * srate
actual = read(f)
@fact length(reference) --> length(actual)
@fact reference --> actual[:, 1]
@fact samplerate(f) --> srate
end
# test seeking
# test rendering as an AudioNode
AudioIO.open(fname) do f
# pretend we have a stream at the same rate as the file
bufsize = 1024
input = zeros(AudioSample, bufsize)
test_info = DeviceInfo(srate, bufsize)
node = FilePlayer(f)
# convert to floating point because that's what AudioIO uses natively
expected = convert(AudioBuf, reference ./ (2^15))
buf = render(node, input, test_info)
@fact expected[1:bufsize] --> buf[1:bufsize]
buf = render(node, input, test_info)
@fact expected[bufsize+1:2*bufsize] --> buf[1:bufsize]
end
end
facts("Stereo file reading") do
fname = Pkg.dir("AudioIO", "test", "440left_880right.wav")
srate = 44100
t = collect(0:(2 * srate - 1)) / srate
expected = round(Int16, (2^15-1) * hcat(sin(2pi*t*440), sin(2pi*t*880)))
AudioIO.open(fname) do f
buf = read(f)
@fact buf --> mse(expected, 5)
end
end
# note - currently AudioIO just mixes down to Mono. soon we'll support this
# new-fangled stereo sound stuff
facts("Stereo file rendering") do
fname = Pkg.dir("AudioIO", "test", "440left_880right.wav")
srate = 44100
bufsize = 1024
input = zeros(AudioSample, bufsize)
test_info = DeviceInfo(srate, bufsize)
t = collect(0 : 2 * srate - 1) / srate
expected = convert(AudioBuf, 0.5 * (sin(2pi*t*440) + sin(2pi*t*880)))
AudioIO.open(fname) do f
node = FilePlayer(f)
buf = render(node, input, test_info)
@fact buf[1:bufsize] --> mse(expected[1:bufsize])
buf = render(node, input, test_info)
@fact buf[1:bufsize] --> mse(expected[bufsize+1:2*bufsize])
end
end
end # module TestSndfile

View file

@ -1,22 +0,0 @@
# convenience function to calculate the mean-squared error
function mse(arr1::AbstractArray, arr2::AbstractArray)
@assert length(arr1) == length(arr2)
N = length(arr1)
err = 0.0
for i in 1:N
err += (arr2[i] - arr1[i])^2
end
err /= N
end
mse(X::AbstractArray, thresh=1e-8) = Y::AbstractArray -> begin
if size(X) != size(Y)
return false
end
return mse(X, Y) < thresh
end
issubtype(T::Type) = x -> typeof(x) <: T
lessthan(rhs) = lhs -> lhs < rhs
greaterthan(rhs) = lhs -> lhs > rhs