Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
![]() |
99ac59c936 |
49 changed files with 1741 additions and 2668 deletions
|
@ -1,9 +0,0 @@
|
|||
always_for_in = true
|
||||
whitespace_typedefs = true
|
||||
whitespace_ops_in_indices = true
|
||||
remove_extra_newlines = true
|
||||
import_to_using = true
|
||||
short_to_long_function_def = true
|
||||
format_docstrings = true
|
||||
align_pair_arrow = false
|
||||
conditional_to_if = true
|
22
.github/pull_request_template.md
vendored
22
.github/pull_request_template.md
vendored
|
@ -1,22 +0,0 @@
|
|||
Thanks for contributing a pull request!
|
||||
|
||||
Please be aware that we are a loose team of volunteers so patience is
|
||||
necessary. Assistance handling other issues is very welcome. We value
|
||||
all user contributions, no matter how minor they are. If we are slow to
|
||||
review, either the pull request needs some benchmarking, tinkering,
|
||||
convincing, etc. or more likely the reviewers are simply busy. In either
|
||||
case, we ask for your understanding during the review process.
|
||||
|
||||
Again, thanks for contributing!
|
||||
|
||||
|
||||
#### What does this implement/fix?
|
||||
Explain your changes. Please be as descriptive as possible.
|
||||
|
||||
|
||||
#### Reference issue
|
||||
Example: Fixes #1234.
|
||||
|
||||
|
||||
#### Additional information
|
||||
Any additional information you think is important.
|
25
.github/workflows/CompatHelper.yml
vendored
25
.github/workflows/CompatHelper.yml
vendored
|
@ -1,25 +0,0 @@
|
|||
name: CompatHelper
|
||||
on:
|
||||
schedule:
|
||||
- cron: 0 0 * * *
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
CompatHelper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Install CompatHelper"
|
||||
run: |
|
||||
import Pkg
|
||||
name = "CompatHelper"
|
||||
uuid = "aa819f21-2bde-4658-8897-bab36330d9b7"
|
||||
version = "2"
|
||||
Pkg.add(; name, uuid, version)
|
||||
shell: julia --color=yes {0}
|
||||
- name: "Run CompatHelper"
|
||||
run: |
|
||||
import CompatHelper
|
||||
CompatHelper.main()
|
||||
shell: julia --color=yes {0}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }}
|
16
.github/workflows/Documentation.yml
vendored
16
.github/workflows/Documentation.yml
vendored
|
@ -1,16 +0,0 @@
|
|||
name: Build documentation
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
jobs:
|
||||
document:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: julia-actions/setup-julia@latest
|
||||
with:
|
||||
version: '1.6'
|
||||
- uses: julia-actions/julia-docdeploy@releases/v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
15
.github/workflows/TagBot.yml
vendored
15
.github/workflows/TagBot.yml
vendored
|
@ -1,15 +0,0 @@
|
|||
name: TagBot
|
||||
on:
|
||||
issue_comment:
|
||||
types:
|
||||
- created
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
TagBot:
|
||||
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: JuliaRegistries/TagBot@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ssh: ${{ secrets.COMPATHELPER_PRIV }}
|
41
.github/workflows/Tests.yml
vendored
41
.github/workflows/Tests.yml
vendored
|
@ -1,41 +0,0 @@
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags: '*'
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
timeout-minutes: 30
|
||||
name: ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
version:
|
||||
- '1.6'
|
||||
- '1'
|
||||
- 'nightly'
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
- windows-latest
|
||||
arch:
|
||||
- x64
|
||||
- x86
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: julia-actions/setup-julia@v1
|
||||
with:
|
||||
version: ${{ matrix.version }}
|
||||
- uses: julia-actions/julia-buildpkg@v1
|
||||
- uses: julia-actions/julia-runtest@v1
|
||||
- uses: julia-actions/julia-processcoverage@v1
|
||||
- uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: lcov.info
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,12 +1,6 @@
|
|||
*.swp
|
||||
*.so
|
||||
*.o
|
||||
deps/deps.jl
|
||||
deps/build.log
|
||||
docs/build
|
||||
*.wav
|
||||
*.flac
|
||||
*.cov
|
||||
coverage
|
||||
deps/usr/lib/pa_shim.so
|
||||
deps/usr/lib/pa_shim.dylib
|
||||
deps/usr/lib/pa_shim.dll
|
||||
|
|
20
.travis.yml
Normal file
20
.travis.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
language: cpp
|
||||
compiler:
|
||||
- clang
|
||||
notifications:
|
||||
email: spencer.f.russell@gmail.com
|
||||
env:
|
||||
- JULIAVERSION="julianightlies"
|
||||
- JULIAVERSION="juliareleases"
|
||||
before_install:
|
||||
- sudo add-apt-repository ppa:staticfloat/julia-deps -y
|
||||
- sudo add-apt-repository ppa:staticfloat/$JULIAVERSION -y
|
||||
- sudo apt-get update -qq -y
|
||||
- sudo apt-get install libpcre3-dev julia -y
|
||||
- if [[ -a .git/shallow ]]; then git fetch --unshallow; fi
|
||||
script:
|
||||
- julia -e 'Pkg.init()'
|
||||
- julia -e 'Pkg.clone(pwd()); Pkg.build("AudioIO")'
|
||||
- julia -e 'Pkg.test("AudioIO", 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
|
6
LICENSE
6
LICENSE
|
@ -19,9 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
suppressor.jl includes code from the Suppressor.jl package, licensed under the
|
||||
MIT "Expat" License:
|
||||
|
||||
Copyright (c) 2016: Ismael Venegas Castelló.
|
||||
|
|
26
Project.toml
26
Project.toml
|
@ -1,26 +0,0 @@
|
|||
name = "PortAudio"
|
||||
uuid = "80ea8bcb-4634-5cb3-8ee8-a132660d1d2d"
|
||||
repo = "https://github.com/JuliaAudio/PortAudio.jl.git"
|
||||
version = "1.3.0"
|
||||
|
||||
[deps]
|
||||
alsa_plugins_jll = "5ac2f6bb-493e-5871-9171-112d4c21a6e7"
|
||||
libportaudio_jll = "2d7b7beb-0762-5160-978e-1ab83a1e8a31"
|
||||
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
|
||||
SampledSignals = "bd7594eb-a658-542f-9e75-4c4d8908c167"
|
||||
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
|
||||
|
||||
[compat]
|
||||
julia = "1.6"
|
||||
alsa_plugins_jll = "1.2.2"
|
||||
libportaudio_jll = "19.6.0"
|
||||
SampledSignals = "2.1.1"
|
||||
Suppressor = "0.2"
|
||||
|
||||
[extras]
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
||||
LibSndFile = "b13ce0c6-77b0-50c6-a2db-140568b8d1a5"
|
||||
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
|
||||
|
||||
[targets]
|
||||
test = ["Documenter", "LibSndFile", "Test"]
|
207
README.md
207
README.md
|
@ -1,126 +1,131 @@
|
|||
PortAudio.jl
|
||||
============
|
||||
AudioIO.jl
|
||||
==========
|
||||
|
||||
[](https://JuliaAudio.github.io/PortAudio.jl/dev)
|
||||
[](https://github.com/JuliaAudio/PortAudio.jl/actions/workflows/Tests.yml)
|
||||
[](https://codecov.io/gh/JuliaAudio/PortAudio.jl)
|
||||
[](https://travis-ci.org/ssfrr/AudioIO.jl)
|
||||
[](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.
|
||||
|
||||
PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which gives cross-platform access to audio devices. It is compatible with the types defined in [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl). It provides a `PortAudioStream` type, which can be read from and written to.
|
||||
File I/O
|
||||
--------
|
||||
|
||||
## Opening a stream
|
||||
|
||||
The easiest way to open a source or sink is with the default `PortAudioStream()` constructor,
|
||||
which will open a 2-in, 2-out stream to your system's default device(s).
|
||||
The constructor can also take the input and output channel counts as positional arguments,
|
||||
or a variety of other keyword arguments.
|
||||
If named keyword arguments `latency` or `samplerate` are unspecified, then PortAudio will use device defaults.
|
||||
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
|
||||
PortAudioStream(inchans=2, outchans=2; eltype=Float32, samplerate=48000, latency=0.1)
|
||||
f = AudioIO.open("data/never_gonna_give_you_up.wav")
|
||||
data = read(f)
|
||||
close(f)
|
||||
```
|
||||
|
||||
You can open a specific device by adding it as the first argument, either as a `PortAudioDevice` instance or by name. You can also give separate names or devices if you want different input and output devices
|
||||
Or to hand closing the file automatically (including in the case of unexpected
|
||||
exceptions), we support the `do` block syntax:
|
||||
|
||||
```julia
|
||||
PortAudioStream(device::PortAudioDevice, args...; kwargs...)
|
||||
PortAudioStream(devname::AbstractString, args...; kwargs...)
|
||||
```
|
||||
|
||||
You can get a list of your system's devices with the `PortAudio.devices()` function:
|
||||
|
||||
```julia
|
||||
julia> PortAudio.devices()
|
||||
14-element Vector{PortAudio.PortAudioDevice}:
|
||||
"sof-hda-dsp: - (hw:0,0)" 2→2
|
||||
"sof-hda-dsp: - (hw:0,3)" 0→2
|
||||
"sof-hda-dsp: - (hw:0,4)" 0→2
|
||||
"sof-hda-dsp: - (hw:0,5)" 0→2
|
||||
⋮
|
||||
"upmix" 8→8
|
||||
"vdownmix" 6→6
|
||||
"dmix" 0→2
|
||||
"default" 32→32
|
||||
```
|
||||
|
||||
## Reading and Writing
|
||||
|
||||
The `PortAudioStream` type has `source` and `sink` fields which are of type `PortAudioSource <: SampleSource` and `PortAudioSink <: SampleSink`, respectively. are subtypes of `SampleSource` and `SampleSink`, respectively (from [SampledSignals.jl](https://github.com/JuliaAudio/SampledSignals.jl)). This means they support all the stream and buffer features defined there. For example, if you load SampledSignals with `using SampledSignals` you can read 5 seconds to a buffer with `buf = read(stream.source, 5s)`, regardless of the sample rate of the device.
|
||||
|
||||
PortAudio.jl also provides convenience wrappers around the `PortAudioStream` type so you can read and write to it directly, e.g. `write(stream, stream)` will set up a loopback that will read from the input and play it back on the output.
|
||||
|
||||
## Debugging
|
||||
|
||||
If you are experiencing issues and wish to view detailed logging and debug information, set
|
||||
|
||||
```
|
||||
ENV["JULIA_DEBUG"] = :PortAudio
|
||||
```
|
||||
|
||||
before using the package.
|
||||
|
||||
## Examples
|
||||
|
||||
### Set up an audio pass-through from microphone to speaker
|
||||
|
||||
```julia
|
||||
stream = PortAudioStream(2, 2)
|
||||
try
|
||||
# cancel with Ctrl-C
|
||||
write(stream, stream)
|
||||
finally
|
||||
close(stream)
|
||||
data = AudioIO.open("data/never_gonna_let_you_down.wav") do f
|
||||
read(f)
|
||||
end
|
||||
```
|
||||
|
||||
### Use `do` syntax to auto-close the stream
|
||||
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
|
||||
PortAudioStream(2, 2) do stream
|
||||
write(stream, stream)
|
||||
data = AudioIO.open("data/never_gonna_run_around.wav") do f
|
||||
read(f, Float32)
|
||||
end
|
||||
```
|
||||
|
||||
### Open your built-in microphone and speaker by name
|
||||
```julia
|
||||
PortAudioStream("default", "default") do stream
|
||||
write(stream, stream)
|
||||
end
|
||||
```
|
||||
Basic Array Playback
|
||||
--------------------
|
||||
|
||||
### Record 10 seconds of audio and save to an ogg file
|
||||
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> import LibSndFile # must be in Manifest for FileIO.save to work
|
||||
|
||||
julia> using PortAudio: PortAudioStream
|
||||
|
||||
julia> using SampledSignals: s
|
||||
|
||||
julia> using FileIO: save
|
||||
|
||||
julia> stream = PortAudioStream(1, 0) # default input (e.g., built-in microphone)
|
||||
PortAudioStream{Float32}
|
||||
Samplerate: 44100.0Hz
|
||||
2 channel source: "default"
|
||||
|
||||
julia> buf = read(stream, 10s)
|
||||
480000-frame, 2-channel SampleBuf{Float32, 2, SIUnits.SIQuantity{Int64,0,0,-1,0,0,0,0,0,0}}
|
||||
10.0 s at 48000 s⁻¹
|
||||
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
||||
▁▄▂▃▅▃▂▄▃▂▂▁▁▂▂▁▁▄▃▁▁▄▂▁▁▁▄▃▁▁▃▃▁▁▁▁▁▁▁▁▄▄▄▄▄▂▂▂▁▃▃▁▃▄▂▁▁▁▁▃▃▂▁▁▁▁▁▁▃▃▂▂▁▃▃▃▁▁▁▁
|
||||
|
||||
julia> close(stream)
|
||||
|
||||
julia> save(joinpath(homedir(), "Desktop", "myvoice.ogg"), buf)
|
||||
julia> v = rand(44100) * 0.1
|
||||
julia> play(v)
|
||||
```
|
||||
|
||||
### Play an audio signal through the default sound output device
|
||||
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
|
||||
using PortAudio, SampledSignals
|
||||
S = 8192 # sampling rate (samples / second)
|
||||
x = cos.(2pi*(1:2S)*440/S) # A440 tone for 2 seconds
|
||||
PortAudioStream(0, 2; samplerate=S) do stream
|
||||
write(stream, x)
|
||||
end
|
||||
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")
|
||||
```
|
||||
|
||||
|
|
3
REQUIRE
Normal file
3
REQUIRE
Normal file
|
@ -0,0 +1,3 @@
|
|||
julia 0.3-
|
||||
BinDeps
|
||||
@osx Homebrew
|
27
deps/build.jl
vendored
Normal file
27
deps/build.jl
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
using BinDeps
|
||||
|
||||
@BinDeps.setup
|
||||
|
||||
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
|
||||
if Pkg.installed("Homebrew") === nothing
|
||||
error("Homebrew package not installed, please run Pkg.add(\"Homebrew\")")
|
||||
end
|
||||
using Homebrew
|
||||
provides(Homebrew.HB, {"portaudio" => libportaudio})
|
||||
provides(Homebrew.HB, {"libsndfile" => libsndfile})
|
||||
end
|
||||
|
||||
@BinDeps.install [:libportaudio => :libportaudio,
|
||||
:libsndfile => :libsndfile]
|
|
@ -1,2 +0,0 @@
|
|||
[deps]
|
||||
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
|
12
docs/make.jl
12
docs/make.jl
|
@ -1,12 +0,0 @@
|
|||
using PortAudio
|
||||
using Documenter: deploydocs, makedocs
|
||||
|
||||
makedocs(
|
||||
sitename = "PortAudio.jl",
|
||||
modules = [PortAudio],
|
||||
pages = [
|
||||
"Public interface" => "index.md",
|
||||
"Internals" => "internals.md"
|
||||
]
|
||||
)
|
||||
deploydocs(repo = "github.com/JuliaAudio/PortAudio.jl.git")
|
|
@ -1,10 +0,0 @@
|
|||
# Public interface
|
||||
|
||||
```@index
|
||||
Pages = ["index.md"]
|
||||
```
|
||||
|
||||
```@autodocs
|
||||
Modules = [PortAudio]
|
||||
Private = false
|
||||
```
|
|
@ -1,10 +0,0 @@
|
|||
# Internals
|
||||
|
||||
```@index
|
||||
Pages = ["internals.md"]
|
||||
```
|
||||
|
||||
```@autodocs
|
||||
Modules = [PortAudio]
|
||||
Public = false
|
||||
```
|
|
@ -1,55 +0,0 @@
|
|||
using PortAudio
|
||||
|
||||
"""
|
||||
Continuously read from the default audio input and plot an
|
||||
ASCII level/peak meter
|
||||
"""
|
||||
function micmeter(metersize)
|
||||
mic = PortAudioStream(1, 0; latency = 0.1)
|
||||
|
||||
signalmax = zero(eltype(mic))
|
||||
println("Press Ctrl-C to quit")
|
||||
while true
|
||||
block = read(mic, 512)
|
||||
blockmax = maximum(abs.(block)) # find the maximum value in the block
|
||||
signalmax = max(signalmax, blockmax) # keep the maximum value ever
|
||||
print("\r") # reset the cursor to the beginning of the line
|
||||
printmeter(metersize, blockmax, signalmax)
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
Print an ASCII level meter of the given size. Signal and peak
|
||||
levels are assumed to be scaled from 0.0-1.0, with peak >= signal
|
||||
"""
|
||||
function printmeter(metersize, signal, peak)
|
||||
# calculate the positions in terms of characters
|
||||
peakpos = clamp(round(Int, peak * metersize), 0, metersize)
|
||||
meterchars = clamp(round(Int, signal * metersize), 0, peakpos - 1)
|
||||
blankchars = max(0, peakpos - meterchars - 1)
|
||||
|
||||
for position in 1:meterchars
|
||||
printstyled(">", color = barcolor(metersize, position))
|
||||
end
|
||||
|
||||
print(" "^blankchars)
|
||||
printstyled("|", color = barcolor(metersize, peakpos))
|
||||
print(" "^(metersize - peakpos))
|
||||
end
|
||||
|
||||
"""
|
||||
Compute the proper color for a given position in the bar graph. The first
|
||||
half of the bar should be green, then the remainder is yellow except the final
|
||||
character, which is red.
|
||||
"""
|
||||
function barcolor(metersize, position)
|
||||
if position / metersize <= 0.5
|
||||
:green
|
||||
elseif position == metersize
|
||||
:red
|
||||
else
|
||||
:yellow
|
||||
end
|
||||
end
|
||||
|
||||
micmeter(80)
|
|
@ -1,86 +1,63 @@
|
|||
using Distributed, PortAudio
|
||||
# Thanks to Jiahao Chen for this great example!
|
||||
|
||||
# Modified from Jiahao Chen's example in the obsolete AudioIO module.
|
||||
# Will use first output device found in system's listing or DEFAULTDEVICE if set below
|
||||
const DEFAULTDEVICE = -1
|
||||
using AudioIO
|
||||
import AudioIO.play
|
||||
|
||||
function paudio()
|
||||
devs = PortAudio.devices()
|
||||
if DEFAULTDEVICE < 0
|
||||
devnum = findfirst(x -> x.maxoutchans > 0, devs)
|
||||
(devnum == nothing) && error("No output device for audio found")
|
||||
else
|
||||
devnum = DEFAULTDEVICE + 1
|
||||
end
|
||||
return ostream = PortAudioStream(devs[devnum].name, 0, 2)
|
||||
end
|
||||
|
||||
play(ostream, sample::Array{Float64, 1}) = write(ostream, sample)
|
||||
play(ostr, sample::Array{Int64, 1}) = play(ostr, Float64.(sample))
|
||||
|
||||
struct Note{S <: Real, T <: Real}
|
||||
type note{S<:Real, T<:Real}
|
||||
pitch::S
|
||||
duration::T
|
||||
sustained::Bool
|
||||
end
|
||||
|
||||
function play(
|
||||
ostream,
|
||||
A::Note,
|
||||
samplingfreq::Real = 44100,
|
||||
shape::Function = t -> 0.6sin(t) + 0.2sin(2t) + 0.05 * sin(8t),
|
||||
)
|
||||
timesamples = 0:(1 / samplingfreq):(A.duration * (A.sustained ? 0.98 : 0.9))
|
||||
function play(A::note, samplingfreq::Real=44100, shape::Function=t->0.6sin(t)+0.2sin(2t)+.05*sin(8t))
|
||||
timesamples=0:1/samplingfreq:(A.duration*(A.sustained ? 0.98 : 0.9))
|
||||
v = Float64[shape(2π*A.pitch*t) for t in timesamples]
|
||||
if !A.sustained
|
||||
decay_length = div(length(timesamples), 5)
|
||||
v[(end - decay_length):(end - 1)] =
|
||||
v[(end - decay_length):(end - 1)] .* LinRange(1, 0, decay_length)
|
||||
decay_length = int(length(timesamples) * 0.2)
|
||||
v[end-decay_length:end-1] = v[end-decay_length:end-1] .* linspace(1, 0, decay_length)
|
||||
end
|
||||
play(ostream, v)
|
||||
play(v)
|
||||
sleep(A.duration)
|
||||
end
|
||||
|
||||
function parsevoice(melody::String; tempo=132, beatunit=4, lyrics=nothing)
|
||||
ostream = paudio() # initialize audio for output
|
||||
play([0]) #Force AudioIO to initialize
|
||||
lyrics_syllables = lyrics==nothing? nothing : split(lyrics)
|
||||
lyrics_syllables != nothing && (lyrics_syllables[end] *= "\n")
|
||||
|
||||
note_idx = 1
|
||||
oldduration = 4
|
||||
for line in split(melody, '\n')
|
||||
percent_idx = findfirst('%', line) # Trim comment
|
||||
percent_idx == nothing || (line = line[1:(percent_idx - 1)])
|
||||
percent_idx = findfirst(line, '%') #Trim comment
|
||||
percent_idx == 0 || (line = line[1:percent_idx-1])
|
||||
for token in split(line)
|
||||
pitch, duration, dotted, sustained =parsetoken(token)
|
||||
duration==nothing && (duration = oldduration)
|
||||
oldduration = duration
|
||||
dotted && (duration *= 1.5)
|
||||
if lyrics_syllables != nothing && 1 <= note_idx <= length(lyrics_syllables)
|
||||
# Print the lyrics, omitting hyphens
|
||||
if lyrics_syllables!=nothing && 1<=note_idx<=length(lyrics_syllables) #Print the lyrics, omitting hyphens
|
||||
if lyrics_syllables[note_idx][end]=='-'
|
||||
print(join(split(lyrics_syllables[note_idx][:], "")[1:(end - 1)]), "")
|
||||
print(lyrics_syllables[note_idx][1:end-1])
|
||||
else
|
||||
print(lyrics_syllables[note_idx], ' ')
|
||||
end
|
||||
end
|
||||
play(ostream, Note(pitch, (beatunit / duration) * (60 / tempo), sustained))
|
||||
play(note(pitch, (beatunit/duration)*(60/tempo), sustained))
|
||||
note_idx += 1
|
||||
end
|
||||
println()
|
||||
end
|
||||
end
|
||||
|
||||
function parsetoken(token, Atuning::Real = 220)
|
||||
function parsetoken(token::String, Atuning::Real=220)
|
||||
state = :findpitch
|
||||
pitch = 0.0
|
||||
sustain = dotted = false
|
||||
lengthbuf = Char[]
|
||||
for char in token
|
||||
if state == :findpitch
|
||||
scale_idx =
|
||||
something(findfirst(char, String(collect('a':'g'))), 0) +
|
||||
something(findfirst(char, String(collect('A':'G'))), 0)
|
||||
scale_idx = findfirst('a':'g', char) + findfirst('A':'G', char)
|
||||
if scale_idx!=0
|
||||
halfsteps = [12, 14, 3, 5, 7, 8, 10]
|
||||
const halfsteps = [12, 14, 3, 5, 7, 8, 10]
|
||||
pitch = Atuning*2^(halfsteps[scale_idx]/12)
|
||||
state = :findlength
|
||||
elseif char=='r'
|
||||
|
@ -89,41 +66,34 @@ function parsetoken(token, Atuning::Real = 220)
|
|||
error("unknown pitch: $char")
|
||||
end
|
||||
elseif state == :findlength
|
||||
if char == '#'
|
||||
pitch *= 2^(1 / 12) # sharp
|
||||
elseif char == 'b'
|
||||
pitch /= 2^(1 / 12) # flat
|
||||
elseif char == '\''
|
||||
pitch *= 2 # higher octave
|
||||
elseif char == ','
|
||||
pitch /= 2 # lower octave
|
||||
elseif char == '.'
|
||||
dotted = true # dotted note
|
||||
elseif char == '~'
|
||||
sustain = true # tied note
|
||||
if char == '#' ; pitch *= 2^(1/12) #sharp
|
||||
elseif char == 'b' ; pitch /= 2^(1/12) #flat
|
||||
elseif char == '\''; pitch *= 2 #higher octave
|
||||
elseif char == ',' ; pitch /= 2 #lower octave
|
||||
elseif char == '.' ; dotted = true #dotted note
|
||||
elseif char == '~' ; sustain = true #tied note
|
||||
else
|
||||
push!(lengthbuf, char)
|
||||
#Check for "is" and "es" suffixes for sharps and flats
|
||||
if length(lengthbuf) >= 2
|
||||
if lengthbuf[(end - 1):end] == "is"
|
||||
if lengthbuf[end-1:end] == "is"
|
||||
pitch *= 2^(1/12)
|
||||
lengthbuf = lengthbuf[1:(end - 2)]
|
||||
elseif lengthbuf[(end - 1):end] == "es"
|
||||
lengthbuf = lengthbuf[1:end-2]
|
||||
elseif lengthbuf[end-1:end] == "es"
|
||||
pitch /= 2^(1/12)
|
||||
lengthbuf = lengthbuf[1:(end - 2)]
|
||||
lengthbuf = lengthbuf[1:end-2]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
#finalize length
|
||||
lengthstr = String(lengthbuf)
|
||||
duration = isempty(lengthstr) ? nothing : tryparse(Int, lengthstr)
|
||||
lengthstr = convert(String, lengthbuf)
|
||||
duration = isempty(lengthstr) ? nothing : parseint(lengthstr)
|
||||
return (pitch, duration, sustain, dotted)
|
||||
end
|
||||
|
||||
parsevoice(
|
||||
"""
|
||||
parsevoice("""
|
||||
f# f# g a a g f# e d d e f# f#~ f#8 e e2
|
||||
f#4 f# g a a g f# e d d e f# e~ e8 d d2
|
||||
e4 e f# d e f#8~ g8 f#4 d e f#8~ g f#4 e d e a,
|
||||
|
@ -133,24 +103,21 @@ f#2 f#4 g a a g f# e d d e f# e~ e8 d8 d2""",
|
|||
Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!
|
||||
Dei- ne Zau- ber bin den - wie- der, was die - Mo- de streng ge- theilt,
|
||||
al- le mensch- en wer- den Brü- der wo dein sanf- ter Flü- - gel weilt.
|
||||
""",
|
||||
)
|
||||
""")
|
||||
|
||||
# And now with harmony!
|
||||
|
||||
soprano = @spawn parsevoice(
|
||||
"""
|
||||
soprano = @async parsevoice("""
|
||||
f'#. f'#. g'. a'. a'. g'. f'#. e'~ e'8 d.'4 d.' e.' f#'. f#'.~ f#' e'8 e'4~ e'2
|
||||
""",
|
||||
lyrics = "Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!",
|
||||
""", lyrics="Freu- de, schö- ner Göt- ter- fun- ken, Toch- ter aus E- li- - si- um!"
|
||||
)
|
||||
alto = @spawn parsevoice("""
|
||||
alto = @async parsevoice("""
|
||||
a. a. a. a. a. a. a. a~ g8 f#.4 a. a. a. a.~ a a8 a4~ a2
|
||||
""")
|
||||
tenor = @spawn parsevoice("""
|
||||
tenor = @async parsevoice("""
|
||||
d. d. e. f#. f#. e. d. d~ e8 f#.4 f#. a,. d. d.~ d c#8 c#4 c#2
|
||||
""")
|
||||
bass = @spawn parsevoice("""
|
||||
bass = @async parsevoice("""
|
||||
d. d. d. d. a,. a,. a,. b,~ c8 d. a., a., a., a., a, a8, a,4 a,2
|
||||
""")
|
||||
wait(soprano)
|
||||
|
@ -158,21 +125,19 @@ wait(alto)
|
|||
wait(tenor)
|
||||
wait(bass)
|
||||
|
||||
soprano = @spawn parsevoice(
|
||||
"""
|
||||
soprano = @async parsevoice("""
|
||||
f'#.4 f'#. g'. a'. a'. g'. f'#. e'. d'. d'. e'. f'#. e'.~ e' d'8 d'4~ d'2
|
||||
""",
|
||||
lyrics = "Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!",
|
||||
)
|
||||
alto = @spawn parsevoice("""
|
||||
""", lyrics="Wir be- tre- ten feu- er- trun- ken, Himm- li- sche, dein Hei- - lig- thum!")
|
||||
alto = @async parsevoice("""
|
||||
a.4 a. b. c'. c'. b. a. g. f#. f#. g. f#. g.~ g4 f#8 f#~ f#2
|
||||
""")
|
||||
tenor = @spawn parsevoice("""
|
||||
tenor = @async parsevoice("""
|
||||
d.4 d. d. d. d. d. d. d. d. d. c#. d. c#.~ c# d8 d d2
|
||||
""")
|
||||
bass = @spawn parsevoice("""
|
||||
bass = @async parsevoice("""
|
||||
d.4 d. d. d. a,. a,. a,. a., a., a., a., a., a.,~ a, a,8 d, d,2
|
||||
""")
|
||||
|
||||
wait(soprano)
|
||||
wait(alto)
|
||||
wait(tenor)
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
using PortAudio
|
||||
using DSP
|
||||
|
||||
function create_measure_signal()
|
||||
signal = zeros(Float32, 20000)
|
||||
for i in 1:3
|
||||
signal = vcat(signal, rand(Float32, 100), zeros(Float32, i * 10000))
|
||||
end
|
||||
return signal
|
||||
end
|
||||
|
||||
function measure_latency(in_latency = 0.1, out_latency = 0.1; is_warmup = false)
|
||||
in_stream = PortAudioStream(1, 0; latency = in_latency)
|
||||
out_stream = PortAudioStream(0, 1; latency = out_latency)
|
||||
|
||||
cond = Base.Event()
|
||||
|
||||
writer_start_time = Int64(0)
|
||||
reader_start_time = Int64(0)
|
||||
|
||||
reader = Threads.@spawn begin
|
||||
wait(cond)
|
||||
writer_start_time = time_ns() |> Int64
|
||||
return read(in_stream, 100000)
|
||||
end
|
||||
|
||||
signal = create_measure_signal()
|
||||
writer = Threads.@spawn begin
|
||||
wait(cond)
|
||||
reader_start_time = time_ns() |> Int64
|
||||
write(out_stream, signal)
|
||||
end
|
||||
|
||||
notify(cond)
|
||||
|
||||
wait(reader)
|
||||
wait(writer)
|
||||
|
||||
recorded = collect(reader.result)[:, 1]
|
||||
|
||||
close(in_stream)
|
||||
close(out_stream)
|
||||
|
||||
diff = reader_start_time - writer_start_time |> abs
|
||||
|
||||
diff_in_ms = diff / 10^6 # 1 ms = 10^6 ns
|
||||
|
||||
if !is_warmup && diff_in_ms > 1
|
||||
@warn "Threads start time difference $diff_in_ms ms is bigger than 1 ms"
|
||||
end
|
||||
|
||||
delay = finddelay(recorded, signal) / 48000
|
||||
|
||||
return trunc(Int, delay * 1000)# result in ms
|
||||
end
|
||||
|
||||
measure_latency(0.1, 0.1; is_warmup = true) # warmup
|
||||
|
||||
latencies = [0.1, 0.01, 0.005]
|
||||
for in_latency in latencies
|
||||
for out_latency in latencies
|
||||
measure = measure_latency(in_latency, out_latency)
|
||||
println("$measure ms latency for in_latency=$in_latency, out_latency=$out_latency")
|
||||
end
|
||||
end
|
40
examples/mutable_streaming_node.jl
Normal file
40
examples/mutable_streaming_node.jl
Normal file
|
@ -0,0 +1,40 @@
|
|||
# This demos how real-time audio manipulation can be done using AudioNodes. To
|
||||
# run it, hook up some input audio to your default recording device and run the
|
||||
# script. The demo will run for 10 seconds alternating the node between a muted
|
||||
# and unmuted state
|
||||
using AudioIO
|
||||
|
||||
type MutableNode <: AudioIO.AudioNode
|
||||
active::Bool
|
||||
deactivate_cond::Condition
|
||||
muted::Bool
|
||||
|
||||
function MutableNode(muted::Bool)
|
||||
new(false, Condition(), muted)
|
||||
end
|
||||
end
|
||||
|
||||
function MutableNode()
|
||||
MutableNode(false)
|
||||
end
|
||||
|
||||
import AudioIO.render
|
||||
function render(node::MutableNode, device_input::AudioIO.AudioBuf, info::AudioIO.DeviceInfo)
|
||||
return device_input .* !node.muted, AudioIO.is_active(node)
|
||||
end
|
||||
|
||||
function mute(node::MutableNode)
|
||||
node.muted = true
|
||||
end
|
||||
|
||||
function unmute(node::MutableNode)
|
||||
node.muted = false
|
||||
end
|
||||
|
||||
mutableNode = MutableNode()
|
||||
AudioIO.play(mutableNode)
|
||||
muteTransitions = { true => unmute, false => mute }
|
||||
for i in 1:10
|
||||
sleep(1)
|
||||
muteTransitions[mutableNode.muted](mutableNode)
|
||||
end
|
|
@ -1,89 +0,0 @@
|
|||
#=
|
||||
This code illustrates real-time octave down shift
|
||||
using a crude FFT-based method.
|
||||
It also plots the input and output signals and their spectra.
|
||||
|
||||
This code uses the system defaults for the audio input and output devices.
|
||||
If you use the built-in speakers and built-in microphone,
|
||||
you will likely get undesirable audio feedback.
|
||||
|
||||
It works "best" if you play the audio output through headphones
|
||||
so that the output does not feed back into the input.
|
||||
|
||||
The spectrum plotting came from the example in
|
||||
https://github.com/JuliaAudio/PortAudio.jl/blob/master/examples
|
||||
=#
|
||||
|
||||
using PortAudio: PortAudioStream
|
||||
using SampledSignals: Hz, domain
|
||||
using SampledSignals: (..) # see EllipsisNotation.jl and IntervalSets.jl
|
||||
using FFTW: fft, ifft
|
||||
using Plots: plot, gui, default; default(label="")
|
||||
|
||||
|
||||
function pitch_halver(x) # decrease pitch by one octave via FFT
|
||||
N = length(x)
|
||||
mod(N,2) == 0 || throw("N must be multiple of 2")
|
||||
F = fft(x) # original spectrum
|
||||
Fnew = [F[1:N÷2]; zeros(N+1); F[(N÷2+2):N]]
|
||||
out = 2 * real(ifft(Fnew))[1:N]
|
||||
out.samplerate /= 2 # trick!
|
||||
return out
|
||||
end
|
||||
|
||||
|
||||
# Plot input and output signals and their spectra.
|
||||
# Quantize the vertical axis limits to reduce plot jitter.
|
||||
function plotter(buf, out, N, fmin, fmax, fs; quant::Number = 0.1)
|
||||
bmax = quant * ceil(maximum(abs, buf) / quant)
|
||||
xticks = [1, N]; ylims = (-1,1) .* bmax; yticks = (-1:1)*bmax
|
||||
p1 = plot(buf; xticks, ylims, yticks, title="input")
|
||||
p3 = plot(out; xticks, ylims, yticks, title="output")
|
||||
|
||||
X = (2/N) * abs.(fft(buf)[fmin..fmax]) # spectrum
|
||||
Xmax = quant * ceil(maximum(X) / quant)
|
||||
xlims = (fs[1], fs[end]); ylims = (0, Xmax); yticks = [0,Xmax]
|
||||
p2 = plot(fs, X; xlims, ylims, yticks)
|
||||
|
||||
Y = (2/N) * abs.(fft(out)[fmin..fmax])
|
||||
p4 = plot(fs, Y; xlims, ylims, yticks)
|
||||
|
||||
plot(p1, p2, p3, p4)
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
octave_shift(seconds; N, ...)
|
||||
|
||||
Shift audio down by one octave.
|
||||
|
||||
# Input
|
||||
* `seconds` : how long to run in seconds; defaults to 300 (5 minutes)
|
||||
|
||||
# Options
|
||||
* `N` : buffer size; default 1024 samples
|
||||
* `fmin`,`fmax` : range of frequencies to display; default 0Hz to 4000Hz
|
||||
"""
|
||||
function octave_shift(
|
||||
seconds::Number = 300;
|
||||
N::Int = 1024,
|
||||
fmin::Number = 0Hz,
|
||||
fmax::Number = 4000Hz,
|
||||
# undocumented options below here that are unlikely to be modified
|
||||
in_stream = PortAudioStream(1, 0), # default input device
|
||||
out_stream = PortAudioStream(0, 1), # default output device
|
||||
buf::AbstractArray = read(in_stream, N), # warm-up
|
||||
fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])],
|
||||
Niters::Int = ceil(Int, seconds * in_stream.sample_rate / N),
|
||||
)
|
||||
|
||||
for _ in 1:Niters
|
||||
read!(in_stream, buf)
|
||||
out = pitch_halver(buf) # decrease pitch by one octave
|
||||
write(out_stream, out)
|
||||
plotter(buf, out, N, fmin, fmax, fs); gui()
|
||||
end
|
||||
nothing
|
||||
end
|
||||
|
||||
octave_shift(5)
|
32
examples/ramp.jl
Normal file
32
examples/ramp.jl
Normal file
|
@ -0,0 +1,32 @@
|
|||
using AudioIO
|
||||
|
||||
# Give PortAudio time to load
|
||||
play([0])
|
||||
sleep(2)
|
||||
|
||||
println("""
|
||||
*
|
||||
* *
|
||||
* * *
|
||||
* * * *
|
||||
* * * * *
|
||||
* * * * * *
|
||||
""")
|
||||
wave = SinOsc(440) * LinRamp(0.0, 1.0, 2.0)
|
||||
play(wave)
|
||||
sleep(2)
|
||||
stop(wave)
|
||||
|
||||
|
||||
println("""
|
||||
*
|
||||
* * *
|
||||
* * * * *
|
||||
* * * * * * *
|
||||
* * * * * * * * *
|
||||
* * * * * * * * * * *
|
||||
""")
|
||||
wave = SinOsc(440) * LinRamp([0.0, 1.0, 0.0], [2.0, 2.0])
|
||||
play(wave)
|
||||
sleep(4)
|
||||
stop(wave)
|
|
@ -1,20 +0,0 @@
|
|||
# plot a real-time spectrogram. This example is adapted from the GR example
|
||||
# at http://gr-framework.org/examples/audio_ex.html
|
||||
|
||||
module SpectrumExample
|
||||
|
||||
using GR, PortAudio, SampledSignals, FFTW
|
||||
|
||||
const N = 1024
|
||||
const stream = PortAudioStream(1, 0)
|
||||
const buf = read(stream, N)
|
||||
const fmin = 0Hz
|
||||
const fmax = 10000Hz
|
||||
const fs = Float32[float(f) for f in domain(fft(buf)[fmin..fmax])]
|
||||
|
||||
while true
|
||||
read!(stream, buf)
|
||||
plot(fs, abs.(fft(buf)[fmin..fmax]), xlim = (fs[1], fs[end]), ylim = (0, 100))
|
||||
end
|
||||
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
#=
|
||||
This example illustrates synthesizing a long tone in small pieces
|
||||
and routing it to the default audio output device using `write()`.
|
||||
=#
|
||||
|
||||
using PortAudio: PortAudioStream, write
|
||||
|
||||
stream = PortAudioStream(0, 1; warn_xruns=false)
|
||||
|
||||
function play_tone(stream, freq::Real, duration::Real; buf_size::Int = 1024)
|
||||
S = stream.sample_rate
|
||||
current = 1
|
||||
while current < duration*S
|
||||
x = 0.7 * sin.(2π * (current .+ (1:buf_size)) * freq / S)
|
||||
write(stream, x)
|
||||
current += buf_size
|
||||
end
|
||||
nothing
|
||||
end
|
||||
|
||||
play_tone(stream, 440, 2)
|
|
@ -1,67 +0,0 @@
|
|||
using Makie
|
||||
using PortAudio
|
||||
using DSP
|
||||
|
||||
"""
|
||||
Slide the values in the given matrix to the right by 1.
|
||||
The rightmosts column is discarded and the leftmost column is
|
||||
left alone.
|
||||
"""
|
||||
function shift1!(buf::AbstractMatrix)
|
||||
for col in size(buf, 2):-1:2
|
||||
@. buf[:, col] = buf[:, col - 1]
|
||||
end
|
||||
end
|
||||
|
||||
"""
|
||||
takes a block of audio, FFT it, and write it to the beginning of the buffer
|
||||
"""
|
||||
function processbuf!(readbuf, win, dispbuf, fftbuf, fftplan)
|
||||
readbuf .*= win
|
||||
A_mul_B!(fftbuf, fftplan, readbuf)
|
||||
shift1!(dispbuf)
|
||||
@. dispbuf[end:-1:1, 1] = log(clamp(abs(fftbuf[1:D]), 0.0001, Inf))
|
||||
end
|
||||
|
||||
function processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
|
||||
read!(src, buf)
|
||||
for dispbuf in dispbufs
|
||||
processbuf!(buf, win, dispbuf, fftbuf, fftplan)
|
||||
end
|
||||
end
|
||||
|
||||
N = 1024 # size of audio read
|
||||
N2 = N ÷ 2 + 1 # size of rfft output
|
||||
D = 200 # number of bins to display
|
||||
M = 200 # amount of history to keep
|
||||
src = PortAudioStream(1, 2)
|
||||
buf = Array{Float32}(N) # buffer for reading
|
||||
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
|
||||
fftbuf = Array{Complex{Float32}}(N2) # destination buf for FFT
|
||||
dispbufs = [zeros(Float32, D, M) for i in 1:5, j in 1:5] # STFT bufs
|
||||
win = gaussian(N, 0.125)
|
||||
|
||||
scene = Scene(resolution = (1000, 1000))
|
||||
|
||||
#pre-fill the display buffer so we can do a reasonable colormap
|
||||
for _ in 1:M
|
||||
processblock!(src, buf, win, dispbufs, fftbuf, fftplan)
|
||||
end
|
||||
|
||||
heatmaps = map(enumerate(IndexCartesian(), dispbufs)) do ibuf
|
||||
i = ibuf[1]
|
||||
buf = ibuf[2]
|
||||
|
||||
# some function of the 2D index and the value
|
||||
heatmap(buf, offset = (i[2] * size(buf, 2), i[1] * size(buf, 1)))
|
||||
end
|
||||
|
||||
center!(scene)
|
||||
|
||||
while isopen(scene[:screen])
|
||||
processblock!(src, buf, dispbufs, fftbuf, fftplan)
|
||||
for (hm, db) in zip(heatmaps, dispbufs)
|
||||
hm[:heatmap] = db
|
||||
end
|
||||
render_frame(scene)
|
||||
end
|
|
@ -1,43 +0,0 @@
|
|||
using Makie, GeometryTypes
|
||||
using PortAudio
|
||||
|
||||
N = 1024 # size of audio read
|
||||
N2 = N ÷ 2 + 1 # size of rfft output
|
||||
D = 200 # number of bins to display
|
||||
M = 100 # number of lines to draw
|
||||
S = 0.5 # motion speed of lines
|
||||
src = PortAudioStream(1, 2)
|
||||
buf = Array{Float32}(N)
|
||||
fftbuf = Array{Complex{Float32}}(N2)
|
||||
magbuf = Array{Float32}(N2)
|
||||
fftplan = plan_rfft(buf; flags = FFTW.EXHAUSTIVE)
|
||||
|
||||
scene = Scene(resolution = (500, 500))
|
||||
ax = axis(0:0.1:1, 0:0.1:1, 0:0.1:0.5)
|
||||
center!(scene)
|
||||
|
||||
ls = map(1:M) do _
|
||||
yoffset = to_node(to_value(scene[:time]))
|
||||
offset = lift_node(scene[:time], yoffset) do t, yoff
|
||||
Point3f0(0.0f0, (t - yoff) * S, 0.0f0)
|
||||
end
|
||||
l = lines(
|
||||
linspace(0, 1, D),
|
||||
0.0f0,
|
||||
zeros(Float32, D),
|
||||
offset = offset,
|
||||
color = (:black, 0.1),
|
||||
)
|
||||
(yoffset, l)
|
||||
end
|
||||
|
||||
while isopen(scene[:screen])
|
||||
for (yoffset, line) in ls
|
||||
isopen(scene[:screen]) || break
|
||||
read!(src, buf)
|
||||
A_mul_B!(fftbuf, fftplan, buf)
|
||||
@. magbuf = log(clamp(abs(fftbuf), 0.0001, Inf)) / 10 + 0.5
|
||||
line[:z] = magbuf[1:D]
|
||||
push!(yoffset, to_value(scene[:time]))
|
||||
end
|
||||
end
|
|
@ -1 +0,0 @@
|
|||
The clang generators will automatically generate wrappers for a C library based on its headers. So everything you see in libportaudio.jl is automatically generated from the C library. If a newer version of portaudio adds more features, we won't have to add new wrappers: clang will handle it for us. It is easy to use currently unused features: the wrappers have already been written for us. Even though it does an admirable job, clang doesn't handle errors and set locks. Fortunately, it's very easy to add secondary wrappers, or just do it at point of use.
|
|
@ -1,16 +0,0 @@
|
|||
using Clang.Generators
|
||||
using libportaudio_jll
|
||||
|
||||
cd(@__DIR__)
|
||||
|
||||
include_dir = joinpath(libportaudio_jll.artifact_dir, "include") |> normpath
|
||||
portaudio_h = joinpath(include_dir, "portaudio.h")
|
||||
|
||||
options = load_options(joinpath(@__DIR__, "generator.toml"))
|
||||
|
||||
args = get_default_args()
|
||||
push!(args, "-I$include_dir")
|
||||
|
||||
ctx = create_context(portaudio_h, args, options)
|
||||
|
||||
build!(ctx)
|
|
@ -1,9 +0,0 @@
|
|||
[general]
|
||||
library_name = "libportaudio"
|
||||
output_file_path = "../src/LibPortAudio.jl"
|
||||
module_name = "LibPortAudio"
|
||||
jll_pkg_name = "libportaudio_jll"
|
||||
export_symbol_prefixes = ["Pa", "pa"]
|
||||
|
||||
use_julia_native_enum_type = true
|
||||
auto_mutability = true
|
101
notes/file_api.md
Normal file
101
notes/file_api.md
Normal file
|
@ -0,0 +1,101 @@
|
|||
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)
|
||||
|
22
notes/node_finishing.md
Normal file
22
notes/node_finishing.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
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
|
3
notes/subtyping.md
Normal file
3
notes/subtyping.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
There are a few issues regarding the types in AudioIO:
|
||||
|
||||
1. There are some fields that need to be shared between all nodes
|
98
src/AudioIO.jl
Normal file
98
src/AudioIO.jl
Normal file
|
@ -0,0 +1,98 @@
|
|||
module AudioIO
|
||||
|
||||
# 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
|
1069
src/PortAudio.jl
1069
src/PortAudio.jl
File diff suppressed because it is too large
Load diff
|
@ -1,439 +0,0 @@
|
|||
module LibPortAudio
|
||||
|
||||
using libportaudio_jll
|
||||
export libportaudio_jll
|
||||
|
||||
function Pa_GetVersion()
|
||||
ccall((:Pa_GetVersion, libportaudio), Cint, ())
|
||||
end
|
||||
|
||||
function Pa_GetVersionText()
|
||||
ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ())
|
||||
end
|
||||
|
||||
mutable struct PaVersionInfo
|
||||
versionMajor::Cint
|
||||
versionMinor::Cint
|
||||
versionSubMinor::Cint
|
||||
versionControlRevision::Ptr{Cchar}
|
||||
versionText::Ptr{Cchar}
|
||||
end
|
||||
|
||||
# no prototype is found for this function at portaudio.h:114:22, please use with caution
|
||||
function Pa_GetVersionInfo()
|
||||
ccall((:Pa_GetVersionInfo, libportaudio), Ptr{PaVersionInfo}, ())
|
||||
end
|
||||
|
||||
const PaError = Cint
|
||||
|
||||
@enum PaErrorCode::Int32 begin
|
||||
paNoError = 0
|
||||
paNotInitialized = -10000
|
||||
paUnanticipatedHostError = -9999
|
||||
paInvalidChannelCount = -9998
|
||||
paInvalidSampleRate = -9997
|
||||
paInvalidDevice = -9996
|
||||
paInvalidFlag = -9995
|
||||
paSampleFormatNotSupported = -9994
|
||||
paBadIODeviceCombination = -9993
|
||||
paInsufficientMemory = -9992
|
||||
paBufferTooBig = -9991
|
||||
paBufferTooSmall = -9990
|
||||
paNullCallback = -9989
|
||||
paBadStreamPtr = -9988
|
||||
paTimedOut = -9987
|
||||
paInternalError = -9986
|
||||
paDeviceUnavailable = -9985
|
||||
paIncompatibleHostApiSpecificStreamInfo = -9984
|
||||
paStreamIsStopped = -9983
|
||||
paStreamIsNotStopped = -9982
|
||||
paInputOverflowed = -9981
|
||||
paOutputUnderflowed = -9980
|
||||
paHostApiNotFound = -9979
|
||||
paInvalidHostApi = -9978
|
||||
paCanNotReadFromACallbackStream = -9977
|
||||
paCanNotWriteToACallbackStream = -9976
|
||||
paCanNotReadFromAnOutputOnlyStream = -9975
|
||||
paCanNotWriteToAnInputOnlyStream = -9974
|
||||
paIncompatibleStreamHostApi = -9973
|
||||
paBadBufferPtr = -9972
|
||||
end
|
||||
|
||||
function Pa_GetErrorText(errorCode)
|
||||
ccall((:Pa_GetErrorText, libportaudio), Ptr{Cchar}, (PaError,), errorCode)
|
||||
end
|
||||
|
||||
function Pa_Initialize()
|
||||
ccall((:Pa_Initialize, libportaudio), PaError, ())
|
||||
end
|
||||
|
||||
function Pa_Terminate()
|
||||
ccall((:Pa_Terminate, libportaudio), PaError, ())
|
||||
end
|
||||
|
||||
const PaDeviceIndex = Cint
|
||||
|
||||
const PaHostApiIndex = Cint
|
||||
|
||||
function Pa_GetHostApiCount()
|
||||
ccall((:Pa_GetHostApiCount, libportaudio), PaHostApiIndex, ())
|
||||
end
|
||||
|
||||
function Pa_GetDefaultHostApi()
|
||||
ccall((:Pa_GetDefaultHostApi, libportaudio), PaHostApiIndex, ())
|
||||
end
|
||||
|
||||
@enum PaHostApiTypeId::UInt32 begin
|
||||
paInDevelopment = 0
|
||||
paDirectSound = 1
|
||||
paMME = 2
|
||||
paASIO = 3
|
||||
paSoundManager = 4
|
||||
paCoreAudio = 5
|
||||
paOSS = 7
|
||||
paALSA = 8
|
||||
paAL = 9
|
||||
paBeOS = 10
|
||||
paWDMKS = 11
|
||||
paJACK = 12
|
||||
paWASAPI = 13
|
||||
paAudioScienceHPI = 14
|
||||
end
|
||||
|
||||
mutable struct PaHostApiInfo
|
||||
structVersion::Cint
|
||||
type::PaHostApiTypeId
|
||||
name::Ptr{Cchar}
|
||||
deviceCount::Cint
|
||||
defaultInputDevice::PaDeviceIndex
|
||||
defaultOutputDevice::PaDeviceIndex
|
||||
end
|
||||
|
||||
function Pa_GetHostApiInfo(hostApi)
|
||||
ccall(
|
||||
(:Pa_GetHostApiInfo, libportaudio),
|
||||
Ptr{PaHostApiInfo},
|
||||
(PaHostApiIndex,),
|
||||
hostApi,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_HostApiTypeIdToHostApiIndex(type)
|
||||
ccall(
|
||||
(:Pa_HostApiTypeIdToHostApiIndex, libportaudio),
|
||||
PaHostApiIndex,
|
||||
(PaHostApiTypeId,),
|
||||
type,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_HostApiDeviceIndexToDeviceIndex(hostApi, hostApiDeviceIndex)
|
||||
ccall(
|
||||
(:Pa_HostApiDeviceIndexToDeviceIndex, libportaudio),
|
||||
PaDeviceIndex,
|
||||
(PaHostApiIndex, Cint),
|
||||
hostApi,
|
||||
hostApiDeviceIndex,
|
||||
)
|
||||
end
|
||||
|
||||
mutable struct PaHostErrorInfo
|
||||
hostApiType::PaHostApiTypeId
|
||||
errorCode::Clong
|
||||
errorText::Ptr{Cchar}
|
||||
end
|
||||
|
||||
function Pa_GetLastHostErrorInfo()
|
||||
ccall((:Pa_GetLastHostErrorInfo, libportaudio), Ptr{PaHostErrorInfo}, ())
|
||||
end
|
||||
|
||||
function Pa_GetDeviceCount()
|
||||
ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
|
||||
end
|
||||
|
||||
function Pa_GetDefaultInputDevice()
|
||||
ccall((:Pa_GetDefaultInputDevice, libportaudio), PaDeviceIndex, ())
|
||||
end
|
||||
|
||||
function Pa_GetDefaultOutputDevice()
|
||||
ccall((:Pa_GetDefaultOutputDevice, libportaudio), PaDeviceIndex, ())
|
||||
end
|
||||
|
||||
const PaTime = Cdouble
|
||||
|
||||
const PaSampleFormat = Culong
|
||||
|
||||
mutable struct PaDeviceInfo
|
||||
structVersion::Cint
|
||||
name::Ptr{Cchar}
|
||||
hostApi::PaHostApiIndex
|
||||
maxInputChannels::Cint
|
||||
maxOutputChannels::Cint
|
||||
defaultLowInputLatency::PaTime
|
||||
defaultLowOutputLatency::PaTime
|
||||
defaultHighInputLatency::PaTime
|
||||
defaultHighOutputLatency::PaTime
|
||||
defaultSampleRate::Cdouble
|
||||
end
|
||||
|
||||
function Pa_GetDeviceInfo(device)
|
||||
ccall((:Pa_GetDeviceInfo, libportaudio), Ptr{PaDeviceInfo}, (PaDeviceIndex,), device)
|
||||
end
|
||||
|
||||
struct PaStreamParameters
|
||||
device::PaDeviceIndex
|
||||
channelCount::Cint
|
||||
sampleFormat::PaSampleFormat
|
||||
suggestedLatency::PaTime
|
||||
hostApiSpecificStreamInfo::Ptr{Cvoid}
|
||||
end
|
||||
|
||||
function Pa_IsFormatSupported(inputParameters, outputParameters, sampleRate)
|
||||
ccall(
|
||||
(:Pa_IsFormatSupported, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStreamParameters}, Ptr{PaStreamParameters}, Cdouble),
|
||||
inputParameters,
|
||||
outputParameters,
|
||||
sampleRate,
|
||||
)
|
||||
end
|
||||
|
||||
const PaStream = Cvoid
|
||||
|
||||
const PaStreamFlags = Culong
|
||||
|
||||
mutable struct PaStreamCallbackTimeInfo
|
||||
inputBufferAdcTime::PaTime
|
||||
currentTime::PaTime
|
||||
outputBufferDacTime::PaTime
|
||||
end
|
||||
|
||||
const PaStreamCallbackFlags = Culong
|
||||
|
||||
@enum PaStreamCallbackResult::UInt32 begin
|
||||
paContinue = 0
|
||||
paComplete = 1
|
||||
paAbort = 2
|
||||
end
|
||||
|
||||
# typedef int PaStreamCallback ( const void * input , void * output , unsigned long frameCount , const PaStreamCallbackTimeInfo * timeInfo , PaStreamCallbackFlags statusFlags , void * userData )
|
||||
const PaStreamCallback = Cvoid
|
||||
|
||||
function Pa_OpenStream(
|
||||
stream,
|
||||
inputParameters,
|
||||
outputParameters,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamFlags,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
ccall(
|
||||
(:Pa_OpenStream, libportaudio),
|
||||
PaError,
|
||||
(
|
||||
Ptr{Ptr{PaStream}},
|
||||
Ptr{PaStreamParameters},
|
||||
Ptr{PaStreamParameters},
|
||||
Cdouble,
|
||||
Culong,
|
||||
PaStreamFlags,
|
||||
Ptr{Cvoid},
|
||||
Ptr{Cvoid},
|
||||
),
|
||||
stream,
|
||||
inputParameters,
|
||||
outputParameters,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamFlags,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_OpenDefaultStream(
|
||||
stream,
|
||||
numInputChannels,
|
||||
numOutputChannels,
|
||||
sampleFormat,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
ccall(
|
||||
(:Pa_OpenDefaultStream, libportaudio),
|
||||
PaError,
|
||||
(
|
||||
Ptr{Ptr{PaStream}},
|
||||
Cint,
|
||||
Cint,
|
||||
PaSampleFormat,
|
||||
Cdouble,
|
||||
Culong,
|
||||
Ptr{Cvoid},
|
||||
Ptr{Cvoid},
|
||||
),
|
||||
stream,
|
||||
numInputChannels,
|
||||
numOutputChannels,
|
||||
sampleFormat,
|
||||
sampleRate,
|
||||
framesPerBuffer,
|
||||
streamCallback,
|
||||
userData,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_CloseStream(stream)
|
||||
ccall((:Pa_CloseStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
# typedef void PaStreamFinishedCallback ( void * userData )
|
||||
const PaStreamFinishedCallback = Cvoid
|
||||
|
||||
function Pa_SetStreamFinishedCallback(stream, streamFinishedCallback)
|
||||
ccall(
|
||||
(:Pa_SetStreamFinishedCallback, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStream}, Ptr{Cvoid}),
|
||||
stream,
|
||||
streamFinishedCallback,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_StartStream(stream)
|
||||
ccall((:Pa_StartStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_StopStream(stream)
|
||||
ccall((:Pa_StopStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_AbortStream(stream)
|
||||
ccall((:Pa_AbortStream, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_IsStreamStopped(stream)
|
||||
ccall((:Pa_IsStreamStopped, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_IsStreamActive(stream)
|
||||
ccall((:Pa_IsStreamActive, libportaudio), PaError, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
mutable struct PaStreamInfo
|
||||
structVersion::Cint
|
||||
inputLatency::PaTime
|
||||
outputLatency::PaTime
|
||||
sampleRate::Cdouble
|
||||
end
|
||||
|
||||
function Pa_GetStreamInfo(stream)
|
||||
ccall((:Pa_GetStreamInfo, libportaudio), Ptr{PaStreamInfo}, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_GetStreamTime(stream)
|
||||
ccall((:Pa_GetStreamTime, libportaudio), PaTime, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_GetStreamCpuLoad(stream)
|
||||
ccall((:Pa_GetStreamCpuLoad, libportaudio), Cdouble, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_ReadStream(stream, buffer, frames)
|
||||
ccall(
|
||||
(:Pa_ReadStream, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||
stream,
|
||||
buffer,
|
||||
frames,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_WriteStream(stream, buffer, frames)
|
||||
ccall(
|
||||
(:Pa_WriteStream, libportaudio),
|
||||
PaError,
|
||||
(Ptr{PaStream}, Ptr{Cvoid}, Culong),
|
||||
stream,
|
||||
buffer,
|
||||
frames,
|
||||
)
|
||||
end
|
||||
|
||||
function Pa_GetStreamReadAvailable(stream)
|
||||
ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_GetStreamWriteAvailable(stream)
|
||||
ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, (Ptr{PaStream},), stream)
|
||||
end
|
||||
|
||||
function Pa_GetSampleSize(format)
|
||||
ccall((:Pa_GetSampleSize, libportaudio), PaError, (PaSampleFormat,), format)
|
||||
end
|
||||
|
||||
function Pa_Sleep(msec)
|
||||
ccall((:Pa_Sleep, libportaudio), Cvoid, (Clong,), msec)
|
||||
end
|
||||
|
||||
const paNoDevice = PaDeviceIndex(-1)
|
||||
|
||||
const paUseHostApiSpecificDeviceSpecification = PaDeviceIndex(-2)
|
||||
|
||||
const paFloat32 = PaSampleFormat(0x00000001)
|
||||
|
||||
const paInt32 = PaSampleFormat(0x00000002)
|
||||
|
||||
const paInt24 = PaSampleFormat(0x00000004)
|
||||
|
||||
const paInt16 = PaSampleFormat(0x00000008)
|
||||
|
||||
const paInt8 = PaSampleFormat(0x00000010)
|
||||
|
||||
const paUInt8 = PaSampleFormat(0x00000020)
|
||||
|
||||
const paCustomFormat = PaSampleFormat(0x00010000)
|
||||
|
||||
const paNonInterleaved = PaSampleFormat(0x80000000)
|
||||
|
||||
const paFormatIsSupported = 0
|
||||
|
||||
const paFramesPerBufferUnspecified = 0
|
||||
|
||||
const paNoFlag = PaStreamFlags(0)
|
||||
|
||||
const paClipOff = PaStreamFlags(0x00000001)
|
||||
|
||||
const paDitherOff = PaStreamFlags(0x00000002)
|
||||
|
||||
const paNeverDropInput = PaStreamFlags(0x00000004)
|
||||
|
||||
const paPrimeOutputBuffersUsingStreamCallback = PaStreamFlags(0x00000008)
|
||||
|
||||
const paPlatformSpecificFlags = PaStreamFlags(0xffff0000)
|
||||
|
||||
const paInputUnderflow = PaStreamCallbackFlags(0x00000001)
|
||||
|
||||
const paInputOverflow = PaStreamCallbackFlags(0x00000002)
|
||||
|
||||
const paOutputUnderflow = PaStreamCallbackFlags(0x00000004)
|
||||
|
||||
const paOutputOverflow = PaStreamCallbackFlags(0x00000008)
|
||||
|
||||
const paPrimingOutput = PaStreamCallbackFlags(0x00000010)
|
||||
|
||||
# exports
|
||||
const PREFIXES = ["Pa", "pa"]
|
||||
for name in names(@__MODULE__; all = true), prefix in PREFIXES
|
||||
if startswith(string(name), prefix)
|
||||
@eval export $name
|
||||
end
|
||||
end
|
||||
|
||||
end # module
|
342
src/nodes.jl
Normal file
342
src/nodes.jl
Normal file
|
@ -0,0 +1,342 @@
|
|||
#### 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
|
||||
|
||||
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 ####
|
||||
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 <: FloatingPoint}(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 = 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
|
16
src/operators.jl
Normal file
16
src/operators.jl
Normal file
|
@ -0,0 +1,16 @@
|
|||
*(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(in1, in2)
|
254
src/portaudio.jl
Normal file
254
src/portaudio.jl
Normal file
|
@ -0,0 +1,254 @@
|
|||
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
|
||||
|
||||
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
|
||||
const pa_host_api_names = {
|
||||
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
|
||||
stream::PaStream
|
||||
|
||||
function PortAudioStream(sample_rate::Integer=44100, buf_size::Integer=1024)
|
||||
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), 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
|
||||
|
||||
############ 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)
|
||||
# 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)
|
||||
end
|
||||
catch ex
|
||||
warn("Audio 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::String
|
||||
host_api::String
|
||||
max_input_channels::Int
|
||||
max_output_channels::Int
|
||||
end
|
||||
|
||||
function get_portaudio_devices()
|
||||
require_portaudio_init()
|
||||
device_count = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
|
||||
pa_devices = [Pa_GetDeviceInfo(i) for i in 0:(device_count - 1)]
|
||||
[PortAudioInterface(bytestring(d.name),
|
||||
bytestring(Pa_GetHostApiInfo(d.host_api).name),
|
||||
d.max_input_channels,
|
||||
d.max_output_channels)
|
||||
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))
|
||||
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)
|
||||
buf
|
||||
end
|
||||
|
||||
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf))
|
||||
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)
|
||||
nothing
|
||||
end
|
||||
|
||||
Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ())
|
||||
|
||||
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)
|
||||
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
|
||||
msg = ccall((:Pa_GetErrorText, libportaudio),
|
||||
Ptr{Cchar}, (PaError,), err)
|
||||
warn("libportaudio: " * bytestring(msg))
|
||||
elseif err != PA_NO_ERROR
|
||||
msg = ccall((:Pa_GetErrorText, libportaudio),
|
||||
Ptr{Cchar}, (PaError,), err)
|
||||
error("libportaudio: " * bytestring(msg))
|
||||
end
|
||||
end
|
|
@ -1,29 +0,0 @@
|
|||
# precompile some important functions
|
||||
const DEFAULT_SINK_MESSENGER_TYPE = Messenger{Float32, SampledSignalsWriter, Tuple{Matrix{Float32}, Int64, Int64}, Int64}
|
||||
const DEFAULT_SOURCE_MESSENGER_TYPE = Messenger{Float32, SampledSignalsReader, Tuple{Matrix{Float32}, Int64, Int64}, Int64}
|
||||
const DEFAULT_STREAM_TYPE = PortAudioStream{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
|
||||
const DEFAULT_SINK_TYPE = PortAudioSink{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
|
||||
const DEFAULT_SOURCE_TYPE = PortAudioSource{DEFAULT_SINK_MESSENGER_TYPE, DEFAULT_SOURCE_MESSENGER_TYPE}
|
||||
|
||||
precompile(close, (DEFAULT_STREAM_TYPE,))
|
||||
precompile(devices, ())
|
||||
precompile(__init__, ())
|
||||
precompile(isopen, (DEFAULT_STREAM_TYPE,))
|
||||
precompile(nchannels, (DEFAULT_SINK_TYPE,))
|
||||
precompile(nchannels, (DEFAULT_SOURCE_TYPE,))
|
||||
precompile(PortAudioStream, (Int, Int))
|
||||
precompile(PortAudioStream, (String, Int, Int))
|
||||
precompile(PortAudioStream, (String, String, Int, Int))
|
||||
precompile(samplerate, (DEFAULT_STREAM_TYPE,))
|
||||
precompile(send, (DEFAULT_SINK_MESSENGER_TYPE,))
|
||||
precompile(send, (DEFAULT_SOURCE_MESSENGER_TYPE,))
|
||||
precompile(unsafe_read!, (DEFAULT_SOURCE_TYPE, Vector{Float32}, Int, Int))
|
||||
precompile(unsafe_read!, (DEFAULT_SOURCE_TYPE, Matrix{Float32}, Int, Int))
|
||||
precompile(unsafe_write, (DEFAULT_SINK_TYPE, Vector{Float32}, Int, Int))
|
||||
precompile(unsafe_write, (DEFAULT_SINK_TYPE, Matrix{Float32}, Int, Int))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
215
src/sndfile.jl
Normal file
215
src/sndfile.jl
Normal file
|
@ -0,0 +1,215 @@
|
|||
export af_open, FilePlayer, rewind, samplerate
|
||||
|
||||
const SFM_READ = int32(0x10)
|
||||
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
|
||||
|
||||
const EXT_TO_FORMAT = [
|
||||
".wav" => SF_FORMAT_WAV,
|
||||
".flac" => SF_FORMAT_FLAC
|
||||
]
|
||||
|
||||
type SF_INFO
|
||||
frames::Int64
|
||||
samplerate::Int32
|
||||
channels::Int32
|
||||
format::Int32
|
||||
sections::Int32
|
||||
seekable::Int32
|
||||
|
||||
function SF_INFO(frames::Integer, samplerate::Integer, channels::Integer,
|
||||
format::Integer, sections::Integer, seekable::Integer)
|
||||
new(int64(frames), int32(samplerate), int32(channels), int32(format),
|
||||
int32(sections), int32(seekable))
|
||||
end
|
||||
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::String, mode::String = "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 = 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::String) = 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::String, args...)
|
||||
player = FilePlayer(filename)
|
||||
play(player, args...)
|
||||
end
|
||||
|
||||
function play(file::AudioFile, args...)
|
||||
player = FilePlayer(file)
|
||||
play(player, args...)
|
||||
end
|
BIN
test/440left_880right.wav
Normal file
BIN
test/440left_880right.wav
Normal file
Binary file not shown.
1
test/REQUIRE
Normal file
1
test/REQUIRE
Normal file
|
@ -0,0 +1 @@
|
|||
FactCheck
|
262
test/runtests.jl
262
test/runtests.jl
|
@ -1,256 +1,18 @@
|
|||
#!/usr/bin/env julia
|
||||
using Base.Sys: iswindows
|
||||
using Documenter: doctest
|
||||
using PortAudio:
|
||||
combine_default_sample_rates,
|
||||
devices,
|
||||
get_default_input_index,
|
||||
get_default_output_index,
|
||||
get_device,
|
||||
get_input_type,
|
||||
get_output_type,
|
||||
handle_status,
|
||||
initialize,
|
||||
name,
|
||||
PortAudioException,
|
||||
PortAudio,
|
||||
PortAudioDevice,
|
||||
PortAudioStream,
|
||||
safe_load,
|
||||
seek_alsa_conf,
|
||||
terminate,
|
||||
write_buffer
|
||||
using PortAudio.LibPortAudio:
|
||||
Pa_AbortStream,
|
||||
PaError,
|
||||
PaErrorCode,
|
||||
paFloat32,
|
||||
Pa_GetDefaultHostApi,
|
||||
Pa_GetDeviceInfo,
|
||||
Pa_GetHostApiCount,
|
||||
Pa_GetLastHostErrorInfo,
|
||||
Pa_GetSampleSize,
|
||||
Pa_GetStreamCpuLoad,
|
||||
Pa_GetStreamInfo,
|
||||
Pa_GetStreamReadAvailable,
|
||||
Pa_GetStreamTime,
|
||||
Pa_GetStreamWriteAvailable,
|
||||
Pa_GetVersionInfo,
|
||||
Pa_HostApiDeviceIndexToDeviceIndex,
|
||||
paHostApiNotFound,
|
||||
Pa_HostApiTypeIdToHostApiIndex,
|
||||
PaHostErrorInfo,
|
||||
paInDevelopment,
|
||||
paInvalidDevice,
|
||||
Pa_IsFormatSupported,
|
||||
Pa_IsStreamActive,
|
||||
paNoError,
|
||||
paNoFlag,
|
||||
paNotInitialized,
|
||||
Pa_OpenDefaultStream,
|
||||
paOutputUnderflowed,
|
||||
Pa_SetStreamFinishedCallback,
|
||||
Pa_Sleep,
|
||||
Pa_StopStream,
|
||||
PaStream,
|
||||
PaStreamInfo,
|
||||
PaStreamParameters,
|
||||
PaVersionInfo
|
||||
using SampledSignals: nchannels, s, SampleBuf, samplerate, SinSource
|
||||
using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws
|
||||
|
||||
@testset "Tests without sound" begin
|
||||
@testset "Reports version" begin
|
||||
io = IOBuffer()
|
||||
PortAudio.versioninfo(io)
|
||||
result = split(String(take!((io))), "\n")
|
||||
# make sure this is the same version I tested with
|
||||
@test startswith(result[1], "PortAudio V19")
|
||||
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")
|
||||
end
|
||||
|
||||
@testset "Can list devices without crashing" begin
|
||||
display(devices())
|
||||
println()
|
||||
for test_file in test_files
|
||||
include(test_file)
|
||||
end
|
||||
|
||||
@testset "libortaudio without sound" begin
|
||||
@test handle_status(Pa_GetHostApiCount()) >= 0
|
||||
@test handle_status(Pa_GetDefaultHostApi()) >= 0
|
||||
# version info not available on windows?
|
||||
if !Sys.iswindows()
|
||||
@test safe_load(Pa_GetVersionInfo(), ErrorException("no info")) isa
|
||||
PaVersionInfo
|
||||
end
|
||||
@test safe_load(Pa_GetLastHostErrorInfo(), ErrorException("no info")) isa
|
||||
PaHostErrorInfo
|
||||
@test PaErrorCode(Pa_IsFormatSupported(C_NULL, C_NULL, 0.0)) == paInvalidDevice
|
||||
@test PaErrorCode(
|
||||
Pa_OpenDefaultStream(Ref(C_NULL), 0, 0, paFloat32, 0.0, 0, C_NULL, C_NULL),
|
||||
) == paInvalidDevice
|
||||
end
|
||||
|
||||
@testset "Errors without sound" begin
|
||||
@test sprint(showerror, PortAudioException(paNotInitialized)) ==
|
||||
"PortAudioException: PortAudio not initialized"
|
||||
@test_throws KeyError("foobarbaz") get_device("foobarbaz")
|
||||
@test_throws KeyError(-1) get_device(-1)
|
||||
@test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(())
|
||||
@test_logs (:warn, "libportaudio: Output underflowed") handle_status(
|
||||
PaError(paOutputUnderflowed),
|
||||
)
|
||||
@test_throws PortAudioException(paNotInitialized) handle_status(
|
||||
PaError(paNotInitialized),
|
||||
)
|
||||
Pa_Sleep(1)
|
||||
@test Pa_GetSampleSize(paFloat32) == 4
|
||||
end
|
||||
|
||||
# make sure we can terminate, then reinitialize
|
||||
terminate()
|
||||
initialize()
|
||||
end
|
||||
|
||||
if isempty(devices())
|
||||
@test_throws ArgumentError("No input device available") get_default_input_index()
|
||||
else
|
||||
@testset "Tests with sound" begin
|
||||
# these default values are specific to local machines
|
||||
input_name = get_device(get_default_input_index()).name
|
||||
output_name = get_device(get_default_output_index()).name
|
||||
|
||||
@testset "Interactive tests" begin
|
||||
println("Recording...")
|
||||
stream = PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true)
|
||||
buffer = read(stream, 5s)
|
||||
@test size(buffer) ==
|
||||
(round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
||||
close(stream)
|
||||
sleep(1)
|
||||
println("Playing back recording...")
|
||||
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||
write(stream, buffer)
|
||||
end
|
||||
sleep(1)
|
||||
println("Testing pass-through")
|
||||
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
|
||||
write_buffer(stream.sink_messenger.buffer, acquire_lock = false)
|
||||
sink = stream.sink
|
||||
source = stream.source
|
||||
@test sprint(show, stream) == """
|
||||
PortAudioStream{Float32}
|
||||
Samplerate: 44100Hz
|
||||
2 channel sink: $(repr(output_name))
|
||||
2 channel source: $(repr(input_name))"""
|
||||
@test sprint(show, source) == "2 channel source: $(repr(input_name))"
|
||||
@test sprint(show, sink) == "2 channel sink: $(repr(output_name))"
|
||||
write(stream, stream, 5s)
|
||||
@test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError
|
||||
@test isopen(stream)
|
||||
close(stream)
|
||||
sleep(1)
|
||||
@test !isopen(stream)
|
||||
@test !isopen(sink)
|
||||
@test !isopen(source)
|
||||
println("done")
|
||||
end
|
||||
@testset "Samplerate-converting writing" begin
|
||||
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||
write(
|
||||
stream,
|
||||
SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]),
|
||||
3s,
|
||||
)
|
||||
println("expected blip")
|
||||
write(
|
||||
stream,
|
||||
SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]),
|
||||
3s,
|
||||
)
|
||||
end
|
||||
end
|
||||
sleep(1)
|
||||
# no way to check that the right data is actually getting read or written here,
|
||||
# but at least it's not crashing.
|
||||
@testset "Queued Writing" begin
|
||||
PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream
|
||||
buffer = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
frame_count_1 = @async write(stream, buffer)
|
||||
frame_count_2 = @async write(stream, buffer)
|
||||
@test fetch(frame_count_1) == 48000
|
||||
println("expected blip")
|
||||
@test fetch(frame_count_2) == 48000
|
||||
end
|
||||
sleep(1)
|
||||
end
|
||||
@testset "Queued Reading" begin
|
||||
PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true) do stream
|
||||
buffer = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
frame_count_1 = @async read!(stream, buffer)
|
||||
frame_count_2 = @async read!(stream, buffer)
|
||||
@test fetch(frame_count_1) == 48000
|
||||
@test fetch(frame_count_2) == 48000
|
||||
end
|
||||
sleep(1)
|
||||
end
|
||||
@testset "Constructors" begin
|
||||
PortAudioStream(2, maximum; adjust_channels = true) do stream
|
||||
@test isopen(stream)
|
||||
end
|
||||
PortAudioStream(output_name; adjust_channels = true) do stream
|
||||
@test isopen(stream)
|
||||
end
|
||||
PortAudioStream(input_name, output_name; adjust_channels = true) do stream
|
||||
@test isopen(stream)
|
||||
end
|
||||
end
|
||||
@testset "Errors with sound" begin
|
||||
big = typemax(Int)
|
||||
@test_throws DomainError(
|
||||
typemax(Int),
|
||||
"$big exceeds maximum output channels for $output_name",
|
||||
) PortAudioStream(input_name, output_name, 0, big)
|
||||
@test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream(
|
||||
input_name,
|
||||
output_name,
|
||||
0,
|
||||
0;
|
||||
adjust_channels = true,
|
||||
)
|
||||
@test_throws ArgumentError("""
|
||||
Default sample rate 0 for input \"$input_name\" disagrees with
|
||||
default sample rate 1 for output \"$output_name\".
|
||||
Please specify a sample rate.
|
||||
""") combine_default_sample_rates(
|
||||
get_device(input_name),
|
||||
0,
|
||||
get_device(output_name),
|
||||
1,
|
||||
)
|
||||
end
|
||||
@testset "libportaudio with sound" begin
|
||||
@test PaErrorCode(Pa_HostApiTypeIdToHostApiIndex(paInDevelopment)) ==
|
||||
paHostApiNotFound
|
||||
@test Pa_HostApiDeviceIndexToDeviceIndex(paInDevelopment, 0) == 0
|
||||
stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true)
|
||||
pointer_to = stream.pointer_to
|
||||
@test handle_status(Pa_GetStreamReadAvailable(pointer_to)) >= 0
|
||||
@test handle_status(Pa_GetStreamWriteAvailable(pointer_to)) >= 0
|
||||
@test Bool(handle_status(Pa_IsStreamActive(pointer_to)))
|
||||
@test safe_load(Pa_GetStreamInfo(pointer_to), ErrorException("no info")) isa
|
||||
PaStreamInfo
|
||||
@test Pa_GetStreamTime(pointer_to) >= 0
|
||||
@test Pa_GetStreamCpuLoad(pointer_to) >= 0
|
||||
@test PaErrorCode(handle_status(Pa_AbortStream(pointer_to))) == paNoError
|
||||
@test PaErrorCode(
|
||||
handle_status(Pa_SetStreamFinishedCallback(pointer_to, C_NULL)),
|
||||
) == paNoError
|
||||
end
|
||||
end
|
||||
doctest(PortAudio)
|
||||
end
|
||||
# return the overall exit status
|
||||
exitstatus()
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
# This file has runs the normal tests and also adds tests that can only be run
|
||||
# locally on a machine with a sound card. It's mostly to put the library through
|
||||
# its paces assuming a human is listening.
|
||||
|
||||
include("runtests.jl")
|
||||
|
||||
# these default values are specific to my machines
|
||||
if Sys.iswindows()
|
||||
default_indev = "Microphone Array (Realtek High "
|
||||
default_outdev = "Speaker/Headphone (Realtek High"
|
||||
elseif Sys.isapple()
|
||||
default_indev = "Built-in Microphone"
|
||||
default_outdev = "Built-in Output"
|
||||
elseif Sys.islinux()
|
||||
default_indev = "default"
|
||||
default_outdev = "default"
|
||||
end
|
||||
|
||||
@testset "Local Tests" begin
|
||||
@testset "Open Default Device" begin
|
||||
println("Recording...")
|
||||
stream = PortAudioStream(2, 0)
|
||||
buf = read(stream, 5s)
|
||||
close(stream)
|
||||
@test size(buf) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source))
|
||||
println("Playing back recording...")
|
||||
stream = PortAudioStream(0, 2)
|
||||
write(stream, buf)
|
||||
println("flushing...")
|
||||
flush(stream)
|
||||
close(stream)
|
||||
println("Testing pass-through")
|
||||
stream = PortAudioStream(2, 2)
|
||||
write(stream, stream, 5s)
|
||||
flush(stream)
|
||||
close(stream)
|
||||
println("done")
|
||||
end
|
||||
@testset "Samplerate-converting writing" begin
|
||||
stream = PortAudioStream(0, 2)
|
||||
write(stream, SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]), 3s)
|
||||
write(stream, SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]), 3s)
|
||||
flush(stream)
|
||||
close(stream)
|
||||
end
|
||||
@testset "Open Device by name" begin
|
||||
stream = PortAudioStream(default_indev, default_outdev)
|
||||
buf = read(stream, 0.001s)
|
||||
@test size(buf) ==
|
||||
(round(Int, 0.001 * samplerate(stream)), nchannels(stream.source))
|
||||
write(stream, buf)
|
||||
io = IOBuffer()
|
||||
show(io, stream)
|
||||
@test occursin(
|
||||
"""
|
||||
PortAudioStream{Float32}
|
||||
Samplerate: 44100.0Hz
|
||||
Buffer Size: 4096 frames
|
||||
2 channel sink: "$default_outdev"
|
||||
2 channel source: "$default_indev\"""",
|
||||
String(take!(io)),
|
||||
)
|
||||
close(stream)
|
||||
end
|
||||
@testset "Error on wrong name" begin
|
||||
@test_throws ErrorException PortAudioStream("foobarbaz")
|
||||
end
|
||||
# no way to check that the right data is actually getting read or written here,
|
||||
# but at least it's not crashing.
|
||||
@testset "Queued Writing" begin
|
||||
stream = PortAudioStream(0, 2)
|
||||
buf = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
t1 = @async write(stream, buf)
|
||||
t2 = @async write(stream, buf)
|
||||
@test fetch(t1) == 48000
|
||||
@test fetch(t2) == 48000
|
||||
flush(stream)
|
||||
close(stream)
|
||||
end
|
||||
@testset "Queued Reading" begin
|
||||
stream = PortAudioStream(2, 0)
|
||||
buf = SampleBuf(
|
||||
rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1,
|
||||
samplerate(stream),
|
||||
)
|
||||
t1 = @async read!(stream, buf)
|
||||
t2 = @async read!(stream, buf)
|
||||
@test fetch(t1) == 48000
|
||||
@test fetch(t2) == 48000
|
||||
close(stream)
|
||||
end
|
||||
end
|
89
test/test_AudioIO.jl
Normal file
89
test/test_AudioIO.jl
Normal file
|
@ -0,0 +1,89 @@
|
|||
module TestAudioIO
|
||||
|
||||
using FactCheck
|
||||
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, 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, 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
|
192
test/test_nodes.jl
Normal file
192
test/test_nodes.jl
Normal file
|
@ -0,0 +1,192 @@
|
|||
module TestAudioIONodes
|
||||
|
||||
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[]
|
||||
@fact (@allocated render(mix, dev_input, test_info)) => 48
|
||||
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, 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, 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, linspace(0.25, 0.80, 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
|
82
test/test_sndfile.jl
Normal file
82
test/test_sndfile.jl
Normal file
|
@ -0,0 +1,82 @@
|
|||
module TestSndfile
|
||||
|
||||
include("testhelpers.jl")
|
||||
|
||||
using AudioIO
|
||||
using FactCheck
|
||||
import AudioIO: DeviceInfo, render, AudioSample, AudioBuf
|
||||
|
||||
facts("WAV file write/read") do
|
||||
fname = Pkg.dir("AudioIO", "test", "sinwave.wav")
|
||||
|
||||
srate = 44100
|
||||
freq = 440
|
||||
t = [0 : 2 * srate - 1] / srate
|
||||
phase = 2 * pi * freq * t
|
||||
reference = 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 = [0 : 2 * srate - 1] / srate
|
||||
expected = 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 = [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
|
22
test/testhelpers.jl
Normal file
22
test/testhelpers.jl
Normal file
|
@ -0,0 +1,22 @@
|
|||
# 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…
Add table
Reference in a new issue