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
29
.travis.yml
29
.travis.yml
|
@ -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:
|
||||
- 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
|
||||
# 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.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
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)
|
||||
[![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.
|
||||
|
|
2
REQUIRE
2
REQUIRE
|
@ -1,4 +1,4 @@
|
|||
julia 0.3-
|
||||
julia 0.4
|
||||
BinDeps
|
||||
Compat
|
||||
@osx Homebrew
|
||||
|
|
8
deps/build.jl
vendored
8
deps/build.jl
vendored
|
@ -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, ))
|
||||
|
|
|
@ -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
|
||||
|
||||
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)
|
||||
|
|
|
@ -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