stripped out non-portaudio stuff and starting to reorganize portaudio stuff
This commit is contained in:
parent
c4dfef9178
commit
80d329eb39
20 changed files with 514 additions and 1823 deletions
27
.travis.yml
27
.travis.yml
|
@ -1,21 +1,18 @@
|
||||||
|
# Documentation: http://docs.travis-ci.com/user/languages/julia/
|
||||||
language: julia
|
language: julia
|
||||||
compiler:
|
os:
|
||||||
- clang
|
- linux
|
||||||
notifications:
|
- osx
|
||||||
email: spencer.f.russell@gmail.com
|
|
||||||
julia:
|
julia:
|
||||||
- 0.3
|
# - release
|
||||||
- 0.4
|
- 0.4
|
||||||
# - nightly
|
- nightly
|
||||||
# Fails with:
|
notifications:
|
||||||
# LoadError: ccall: could not find function jl_read_sonames
|
email: false
|
||||||
# while loading /home/travis/.julia/v0.5/AudioIO/deps/build.jl, in expression starting on line 29
|
|
||||||
script:
|
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
|
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
||||||
- julia -e 'Pkg.init()'
|
- julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); Pkg.test("PortAudio"; coverage=true)'
|
||||||
- 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
|
|
||||||
after_success:
|
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
133
README.md
|
@ -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)
|
[![Build Status](https://travis-ci.org/JuliaAudio/PortAudio.jl.svg?branch=master)](https://travis-ci.org/JuliaAudio/PortAudio.jl)
|
||||||
[![Pkgs Status](http://pkg.julialang.org/badges/AudioIO_release.svg)](http://pkg.julialang.org/?pkg=AudioIO&ver=release)
|
[![codecov.io] (http://codecov.io/github/JuliaAudio/PortAudio.jl/coverage.svg?branch=master)] (http://codecov.io/github/JuliaAudio/PortAudio.jl?branch=master)
|
||||||
[![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")
|
|
||||||
```
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
2
REQUIRE
2
REQUIRE
|
@ -1,4 +1,4 @@
|
||||||
julia 0.3-
|
julia 0.4
|
||||||
BinDeps
|
BinDeps
|
||||||
Compat
|
Compat
|
||||||
@osx Homebrew
|
@osx Homebrew
|
||||||
|
|
8
deps/build.jl
vendored
8
deps/build.jl
vendored
|
@ -6,26 +6,20 @@ using Compat
|
||||||
ENV["JULIA_ROOT"] = abspath(JULIA_HOME, "../../")
|
ENV["JULIA_ROOT"] = abspath(JULIA_HOME, "../../")
|
||||||
|
|
||||||
libportaudio = library_dependency("libportaudio")
|
libportaudio = library_dependency("libportaudio")
|
||||||
libsndfile = library_dependency("libsndfile")
|
|
||||||
|
|
||||||
# TODO: add other providers with correct names
|
# TODO: add other providers with correct names
|
||||||
provides(AptGet, "portaudio19-dev", libportaudio)
|
provides(AptGet, "portaudio19-dev", libportaudio)
|
||||||
provides(AptGet, "libsndfile1-dev", libsndfile)
|
|
||||||
provides(Pacman, "portaudio", libportaudio)
|
provides(Pacman, "portaudio", libportaudio)
|
||||||
provides(Pacman, "libsndfile", libsndfile)
|
|
||||||
|
|
||||||
|
|
||||||
@osx_only begin
|
@osx_only begin
|
||||||
using Homebrew
|
using Homebrew
|
||||||
provides(Homebrew.HB, "portaudio", libportaudio)
|
provides(Homebrew.HB, "portaudio", libportaudio)
|
||||||
provides(Homebrew.HB, "libsndfile", libsndfile)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@windows_only begin
|
@windows_only begin
|
||||||
using WinRPM
|
using WinRPM
|
||||||
provides(WinRPM.RPM, "libportaudio2", libportaudio, os = :Windows)
|
provides(WinRPM.RPM, "libportaudio2", libportaudio, os = :Windows)
|
||||||
provides(WinRPM.RPM, "libsndfile1", libsndfile, os = :Windows)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@BinDeps.install @compat(Dict(:libportaudio => :libportaudio,
|
@BinDeps.install @compat(Dict(:libportaudio => :libportaudio, ))
|
||||||
:libsndfile => :libsndfile))
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
100
src/AudioIO.jl
100
src/AudioIO.jl
|
@ -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
261
src/PortAudio.jl
Normal 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
230
src/libportaudio.jl
Normal 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
|
342
src/nodes.jl
342
src/nodes.jl
|
@ -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
|
|
|
@ -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)
|
|
473
src/portaudio.jl
473
src/portaudio.jl
|
@ -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
|
|
209
src/sndfile.jl
209
src/sndfile.jl
|
@ -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
|
|
|
@ -1 +1 @@
|
||||||
FactCheck
|
BaseTestNext
|
||||||
|
|
|
@ -1,18 +1,7 @@
|
||||||
#!/usr/bin/env julia
|
#!/usr/bin/env julia
|
||||||
|
|
||||||
using FactCheck
|
@testset "No Tests" begin
|
||||||
|
@test false
|
||||||
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")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
for test_file in test_files
|
exit(0)
|
||||||
include(test_file)
|
|
||||||
end
|
|
||||||
|
|
||||||
# return the overall exit status
|
|
||||||
exitstatus()
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
Loading…
Reference in a new issue