diff --git a/.travis.yml b/.travis.yml index 1d6d0b0..bb46f83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,21 +1,18 @@ +# Documentation: http://docs.travis-ci.com/user/languages/julia/ language: julia -compiler: - - clang -notifications: - email: spencer.f.russell@gmail.com +os: + - linux + - osx julia: - - 0.3 + # - release - 0.4 -# - nightly -# Fails with: -# LoadError: ccall: could not find function jl_read_sonames -# while loading /home/travis/.julia/v0.5/AudioIO/deps/build.jl, in expression starting on line 29 + - nightly +notifications: + email: false script: - - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi - - julia -e 'Pkg.init()' - - julia -e 'Pkg.add("BinDeps"); Pkg.checkout("BinDeps")' # latest master needed for Pacman support - - julia -e 'Pkg.clone(pwd()); Pkg.build("AudioIO")' - - julia -e 'Pkg.add("FactCheck")' # add FactCheck manually because we're not using Pkg.test() - - julia --code-coverage=user test/runtests.jl # Pkg.test disables inlining when enabling coverage, which kills our allocation tests + # SampleTypes is unregistered so clone it for testing + - julia -e 'Pkg.clone("https://github.com/JuliaAudio/SampleTypes.jl.git")' + - if [[ -a .git/shallow ]]; then git fetch --unshallow; fi + - julia -e 'Pkg.clone(pwd()); Pkg.build("PortAudio"); Pkg.test("PortAudio"; coverage=true)' after_success: -- if [ $JULIAVERSION = "juliareleases" ]; then julia -e 'cd(Pkg.dir("AudioIO")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder())'; fi + - julia -e 'cd(Pkg.dir("PortAudio")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' \ No newline at end of file diff --git a/README.md b/README.md index e19230e..428400c 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,7 @@ -AudioIO.jl +PortAudio.jl ========== -[![Build Status](https://travis-ci.org/ssfrr/AudioIO.jl.svg?branch=master)](https://travis-ci.org/ssfrr/AudioIO.jl) -[![Pkgs Status](http://pkg.julialang.org/badges/AudioIO_release.svg)](http://pkg.julialang.org/?pkg=AudioIO&ver=release) -[![Coverage Status](https://img.shields.io/coveralls/ssfrr/AudioIO.jl.svg)](https://coveralls.io/r/ssfrr/AudioIO.jl?branch=master) - -AudioIO interfaces to audio streams, including real-time recording, audio -processing, and playback through your sound card using PortAudio. It also -supports reading and writing audio files in a variety of formats. It is under -active development and the low-level API could change, but the basic -functionality (reading and writing files, the `play` function, etc.) should be -stable and usable by the general Julia community. - -File I/O --------- - -File I/O is handled by [libsndfile](http://www.mega-nerd.com/libsndfile/), so -we can support a wide variety of file and sample formats. Use the -`AudioIO.open` function to open a file. It has the same API as the built-in -Base.open, but returns an `AudioFile` type. Opening an audio file and reading -its contents into an array is as simple as: - -```julia -f = AudioIO.open("data/never_gonna_give_you_up.wav") -data = read(f) -close(f) -``` - -Or to hand closing the file automatically (including in the case of unexpected -exceptions), we support the `do` block syntax: - -```julia -data = AudioIO.open("data/never_gonna_let_you_down.wav") do f - read(f) -end -``` - -By default the returned array will be in whatever format the original audio file is -(Float32, UInt16, etc.). We also support automatic conversion by supplying a type: - -```julia -data = AudioIO.open("data/never_gonna_run_around.wav") do f - read(f, Float32) -end -``` - -Basic Array Playback --------------------- - -Arrays in various formats can be played through your soundcard. Currently the -native format that is delivered to the PortAudio backend is Float32 in the -range of [-1, 1]. Arrays in other sizes of float are converted. Arrays -in Signed or Unsigned Integer types are scaled so that the full range is -mapped to [-1, 1] floating point values. - -To play a 1-second burst of noise: - -```julia -julia> v = rand(44100) * 0.1 -julia> play(v) -``` - -AudioNodes ----------- - -In addition to the basic `play` function you can create more complex networks -of AudioNodes in a render chain. In fact, when using the basic `play` to play -an Array, behind the scenes an instance of the ArrayPlayer type is created -and added to the master AudioMixer inputs. Audionodes also implement a `stop` -function, which will remove them from the render graph. When an implicit -AudioNode is created automatically, such as when using `play` on an Array, the -`play` function should return the audio node that is playing the Array, so it -can be stopped if desired. - -To explictly do the same as above: - -```julia -julia> v = rand(44100) * 0.1 -julia> player = ArrayPlayer(v) -julia> play(player) -``` - -To generate 2 sin tones: - -```julia -julia> osc1 = SinOsc(440) -julia> osc2 = SinOsc(660) -julia> play(osc1) -julia> play(osc2) -julia> stop(osc1) -julia> stop(osc2) -``` - -All AudioNodes must implement a `render` function that can be called to -retreive the next block of audio. - -AudioStreams ------------- - -AudioStreams represent an external source or destination for audio, such as the -sound card. The `play` function attaches AudioNodes to the default stream -unless a stream is given as the 2nd argument. - -AudioStream is an abstract type, which currently has a PortAudioStream subtype -that writes to the sound card, and a TestAudioStream that is used in the unit -tests. - -Currently only 1 stream at a time is supported so there's no reason to provide -an explicit stream to the `play` function. The stream has a root mixer field -which is an instance of the AudioMixer type, so that multiple AudioNodes -can be heard at the same time. Whenever a new frame of audio is needed by the -sound card, the stream calls the `render` method on the root audio mixer, which -will in turn call the `render` methods on any input AudioNodes that are set -up as inputs. - -Installation ------------- - -To install the latest release version, simply run - -```julia -julia> Pkg.add("AudioIO") -``` - -If you want to install the lastest master, it's almost as easy: - -```julia -julia> Pkg.clone("AudioIO") -julia> Pkg.build("AudioIO") -``` +[![Build Status](https://travis-ci.org/JuliaAudio/PortAudio.jl.svg?branch=master)](https://travis-ci.org/JuliaAudio/PortAudio.jl) +[![codecov.io] (http://codecov.io/github/JuliaAudio/PortAudio.jl/coverage.svg?branch=master)] (http://codecov.io/github/JuliaAudio/PortAudio.jl?branch=master) +PortAudio.jl is a wrapper for [libportaudio](http://www.portaudio.com/), which gives cross-platform access to audio devices. It is compatible with the types defined in [SampleTypes.jl](https://github.com/JuliaAudio/SampleTypes.jl), so it provides `PASampleSink` and `PASampleSource` types, which can be read from and written to. diff --git a/REQUIRE b/REQUIRE index 754bd86..4929239 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,4 +1,4 @@ -julia 0.3- +julia 0.4 BinDeps Compat @osx Homebrew diff --git a/deps/build.jl b/deps/build.jl index e1c4af3..95a548e 100644 --- a/deps/build.jl +++ b/deps/build.jl @@ -6,26 +6,20 @@ using Compat ENV["JULIA_ROOT"] = abspath(JULIA_HOME, "../../") libportaudio = library_dependency("libportaudio") -libsndfile = library_dependency("libsndfile") # TODO: add other providers with correct names provides(AptGet, "portaudio19-dev", libportaudio) -provides(AptGet, "libsndfile1-dev", libsndfile) provides(Pacman, "portaudio", libportaudio) -provides(Pacman, "libsndfile", libsndfile) @osx_only begin using Homebrew provides(Homebrew.HB, "portaudio", libportaudio) - provides(Homebrew.HB, "libsndfile", libsndfile) end @windows_only begin using WinRPM provides(WinRPM.RPM, "libportaudio2", libportaudio, os = :Windows) - provides(WinRPM.RPM, "libsndfile1", libsndfile, os = :Windows) end -@BinDeps.install @compat(Dict(:libportaudio => :libportaudio, - :libsndfile => :libsndfile)) +@BinDeps.install @compat(Dict(:libportaudio => :libportaudio, )) diff --git a/notes/file_api.md b/notes/file_api.md deleted file mode 100644 index 009fb4f..0000000 --- a/notes/file_api.md +++ /dev/null @@ -1,101 +0,0 @@ -Some possible API concepts for dealing with files -================================================= - -Notes ------ - -* requires libflac for flac decoding - -Use Cases ---------- - -* Play a file through the speakers -* Use a file as input to an AudioNode for processing -* Read a file into an array -* Write an array into a file -* Write the output of an AudioNode to a file - - -IOStream API ------------- - -* users use standard julia "open" function to create an IOStream object -* FilePlayer <: AudioNode takes an IOStream and uses `sf_open_fd` to open and - play -* play(io::IOStream) creates a FilePlayer and plays it (just like ArrayPlayer) -* FileStream - -### Play a file through the speakers - - sndfile = open("myfile.wav") - play(sndfile) - close(sndfile) - -### Use a file as input to an AudioNode for processing - - sndfile = open("myfile.wav") - # maybe FilePlayer also takes a string input for convenience - node = FilePlayer(sndfile) - mixer = AudioMixer([node]) - # etc. - -### Read a file into an array - - # TODO - -### Write an array into a file - - # TODO - -### Write the output of an AudioNode to a file - - node = SinOsc(440) - # ??? - -Separate Open Function API --------------------------- - -* users use an explicit `AudioIO.open` function to open sound files -* `AudioIO.open` takes mode arguments just like the regular julia `open` function -* `AudioIO.open` returns a AudioFile instance. - -### Play a file through the speakers - - sndfile = AudioIO.open("myfile.wav") - play(sndfile) - close(sndfile) - -or - - play("myfile.wav") - -### Use a file as input to an AudioNode for processing - - sndfile = AudioIO.open("myfile.wav") - # FilePlayer also can take a string filename for convenience - node = FilePlayer(sndfile) - mixer = AudioMixer([node]) - # etc. - -### Read a file into an array - - sndfile = AudioIO.open("myfile.wav") - vec = read(sndfile) # takes an optional arg for number of frames to read - close(sndfile) - -### Write an array into a file - - sndfile = AudioIO.open("myfile.wav", "w") #TODO: need to specify format - vec = rand(Float32, 441000) # 10 seconds of noise - write(sndfile, vec) - close(sndfile) - -### Write the output of an AudioNode to a file - - sndfile = AudioIO.open("myfile.wav", "w") #TODO: need to specify format - node = SinOsc(440) - write(sndfile, node, 44100) # record 1 second, optional block_size - # note that write() can handle sample depth conversions, and render() is - # called with the sampling rate of the file - close(sndfile) - diff --git a/notes/node_finishing.md b/notes/node_finishing.md deleted file mode 100644 index bf9cb72..0000000 --- a/notes/node_finishing.md +++ /dev/null @@ -1,22 +0,0 @@ -One design challenge is how to handle nodes with a finite length, e.g. -ArrayPlayers. Also this comes up with how do we stop a node. - -Considerations: -1. typically the end of the signal will happen in the middle of a block. -2. we want to avoid the AudioNodes allocating a new block every render cycle -3. force-stopping nodes will typicaly happen on a block boundary -4. A node should be able to send its signal to multiple receivers, but it doesn't - know what they are (it doesn't store a reference to them), so if a node is finished - it needs to communicate that in the value returned from render() - -Options: - -1. We could take the block size as a maximum, and if there aren't that many - frames of audio left then a short (or empty) block is returned. -2. We could return a (Array, Bool) tuple with the full block-size, padded with - zeros (or extending the last value out), and the bool indicating whether - there is more data -3. We could raturn a (Array, Int) tuple that indicates how many frames were - written -4. We could ignore it and just have them keep playing. This makes the simple - play(node) usage dangerous because they never get cleaned up diff --git a/notes/subtyping.md b/notes/subtyping.md deleted file mode 100644 index 3aa600d..0000000 --- a/notes/subtyping.md +++ /dev/null @@ -1,3 +0,0 @@ -There are a few issues regarding the types in AudioIO: - -1. There are some fields that need to be shared between all nodes diff --git a/src/AudioIO.jl b/src/AudioIO.jl deleted file mode 100644 index 95d7d5d..0000000 --- a/src/AudioIO.jl +++ /dev/null @@ -1,100 +0,0 @@ -module AudioIO -using Compat -importall Base.Operators - -# export the basic API -export play, stop, get_audio_devices - -# default stream used when none is given -_stream = nothing - -################## Types #################### - -typealias AudioSample Float32 -# A frame of audio, possibly multi-channel -typealias AudioBuf Array{AudioSample} - -# used as a type parameter for AudioNodes. Subtypes handle the actual DSP for -# each node -abstract AudioRenderer - -# A stream of audio (for instance that writes to hardware). All AudioStream -# subtypes should have a root and info field -abstract AudioStream -samplerate(str::AudioStream) = str.info.sample_rate -bufsize(str::AudioStream) = str.info.buf_size - -# An audio interface is usually a physical sound card, but could -# be anything you'd want to connect a stream to -abstract AudioInterface - -# Info about the hardware device -type DeviceInfo - sample_rate::Float32 - buf_size::Integer -end - -type AudioNode{T<:AudioRenderer} - active::Bool - end_cond::Condition - renderer::T - - AudioNode(renderer::AudioRenderer) = new(true, Condition(), renderer) - AudioNode(args...) = AudioNode{T}(T(args...)) -end - -function render(node::AudioNode, input::AudioBuf, info::DeviceInfo) - # TODO: not sure if the compiler will infer that render() always returns an - # AudioBuf. Might need to help it - if node.active - result = render(node.renderer, input, info) - if length(result) < info.buf_size - node.active = false - notify(node.end_cond) - end - return result - else - return AudioSample[] - end -end - -# Get binary dependencies loaded from BinDeps -include( "../deps/deps.jl") -include("nodes.jl") -include("portaudio.jl") -include("sndfile.jl") -include("operators.jl") - -############ Exported Functions ############# - -# Play an AudioNode by adding it as an input to the root mixer node -function play(node::AudioNode, stream::AudioStream) - push!(stream.root, node) - return node -end - -# If the stream is not given, use the default global PortAudio stream -function play(node::AudioNode) - global _stream - if _stream == nothing - _stream = PortAudioStream() - end - play(node, _stream) -end - -function stop(node::AudioNode) - node.active = false - notify(node.end_cond) -end - -function Base.wait(node::AudioNode) - if node.active - wait(node.end_cond) - end -end - -function get_audio_devices() - return get_portaudio_devices() -end - -end # module AudioIO diff --git a/src/PortAudio.jl b/src/PortAudio.jl new file mode 100644 index 0000000..5d4c858 --- /dev/null +++ b/src/PortAudio.jl @@ -0,0 +1,261 @@ +module PortAudio +using SampleTypes +using Compat + +# Get binary dependencies loaded from BinDeps +include( "../deps/deps.jl") +include("libportaudio.jl") + +# Info about the hardware device +type DeviceInfo + sample_rate::Float32 + buf_size::Integer +end + +function devices() + return get_portaudio_devices() +end + +type PortAudioStream + info::DeviceInfo + show_warnings::Bool + stream::PaStream + + function PortAudioStream(sample_rate=44100Hz, + buf_size::Integer=1024, + show_warnings::Bool=false) + # Pa_Initialize can be called multiple times, as long as each is + # paired with Pa_Terminate() + Pa_Initialize() + stream = Pa_OpenDefaultStream(2, 2, paFloat32, Int(sample_rate), buf_size) + Pa_StartStream(stream) + this = new(root, DeviceInfo(sample_rate, buf_size), show_warnings, stream) + @schedule(portaudio_task(this)) + finalizer(this, close) + + this + end +end + +type PortAudioSink <: SampleSink + stream::PaStream +end + +type PortAudioSource <: SampleSource + stream::PaStream +end + +function close(stream::PortAudioStream) + Pa_StopStream(stream.stream) + Pa_CloseStream(stream.stream) + Pa_Terminate() +end + +type Pa_StreamParameters + device::PaDeviceIndex + channelCount::Cint + sampleFormat::PaSampleFormat + suggestedLatency::PaTime + hostAPISpecificStreamInfo::Ptr{Void} +end + +type PortAudioInterface <: AudioInterface + name::AbstractString + host_api::AbstractString + max_input_channels::Int + max_output_channels::Int + device_index::PaDeviceIndex +end + + +type Pa_AudioStream <: AudioStream + root::AudioMixer + info::DeviceInfo + show_warnings::Bool + stream::PaStream + sformat::PaSampleFormat + sbuffer::Array{Real} + sbuffer_output_waiting::Integer + parent_may_use_buffer::Bool + + """ + Get device parameters needed for opening with portaudio + default is input as 44100/16bit int, same as CD audio type input + """ + function Pa_AudioStream(device_index, channels=2, input=false, + sample_rate::Integer=44100, + framesPerBuffer::Integer=2048, + show_warnings::Bool=false, + sample_format::PaSampleFormat=paInt16) + require_portaudio_init() + stream = Pa_OpenStream(device_index, channels, input, sample_format, + Cdouble(sample_rate), Culong(framesPerBuffer)) + Pa_StartStream(stream) + root = AudioMixer() + datatype = PaSampleFormat_to_T(sample_format) + sbuf = ones(datatype, framesPerBuffer) + this = new(root, DeviceInfo(sample_rate, framesPerBuffer), + show_warnings, stream, sample_format, sbuf, 0, false) + info("Scheduling PortAudio Render Task...") + if input + @schedule(pa_input_task(this)) + else + @schedule(pa_output_task(this)) + end + this + end +end + +""" +Blocking read from a Pa_AudioStream that is open as input +""" +function read_Pa_AudioStream(stream::Pa_AudioStream) + while true + while stream.parent_may_use_buffer == false + sleep(0.001) + end + buffer = deepcopy(stream.sbuffer) + stream.parent_may_use_buffer = false + return buffer + end +end + +""" +Blocking write to a Pa_AudioStream that is open for output +""" +function write_Pa_AudioStream(stream::Pa_AudioStream, buffer) + retval = 1 + sbufsize = length(stream.sbuffer) + inputlen = length(buffer) + if(inputlen > sbufsize) + info("Overflow at write_Pa_AudioStream") + retval = 0 + elseif(inputlen < sbufsize) + info("Underflow at write_Pa_AudioStream") + retval = -1 + end + while true + while stream.parent_may_use_buffer == false + sleep(0.001) + end + for idx in 1:min(sbufsize, inputlen) + stream.sbuffer[idx] = buffer[idx] + end + stream.parent_may_use_buffer = false + end + retval +end + +############ Internal Functions ############ + +function portaudio_task(stream::PortAudioStream) + info("PortAudio Render Task Running...") + n = bufsize(stream) + buffer = zeros(AudioSample, n) + try + while true + while Pa_GetStreamReadAvailable(stream.stream) < n + sleep(0.005) + end + Pa_ReadStream(stream.stream, buffer, n, stream.show_warnings) + # assume the root is always active + rendered = render(stream.root.renderer, buffer, stream.info)::AudioBuf + for i in 1:length(rendered) + buffer[i] = rendered[i] + end + for i in (length(rendered)+1):n + buffer[i] = 0.0 + end + while Pa_GetStreamWriteAvailable(stream.stream) < n + sleep(0.005) + end + Pa_WriteStream(stream.stream, buffer, n, stream.show_warnings) + end + catch ex + warn("Audio Task died with exception: $ex") + Base.show_backtrace(STDOUT, catch_backtrace()) + end +end + +""" + Get input device data, pass as a producer, no rendering +""" +function pa_input_task(stream::Pa_AudioStream) + info("PortAudio Input Task Running...") + n = bufsize(stream) + datatype = PaSampleFormat_to_T(stream.sformat) + # bigger ccall buffer to avoid overflow related errors + buffer = zeros(datatype, n * 8) + try + while true + while Pa_GetStreamReadAvailable(stream.stream) < n + sleep(0.005) + end + while stream.parent_may_use_buffer + sleep(0.005) + end + err = ccall((:Pa_ReadStream, libportaudio), PaError, + (PaStream, Ptr{Void}, Culong), + stream.stream, buffer, n) + handle_status(err, stream.show_warnings) + stream.sbuffer[1: n] = buffer[1: n] + stream.parent_may_use_buffer = true + sleep(0.005) + end + catch ex + warn("Audio Input Task died with exception: $ex") + Base.show_backtrace(STDOUT, catch_backtrace()) + end +end + +""" + Send output device data, no rendering +""" +function pa_output_task(stream::Pa_AudioStream) + info("PortAudio Output Task Running...") + n = bufsize(stream) + try + while true + navail = stream.sbuffer_output_waiting + if navail > n + info("Possible output buffer overflow in stream") + navail = n + end + if (navail > 1) & (stream.parent_may_use_buffer == false) & + (Pa_GetStreamWriteAvailable(stream.stream) < navail) + Pa_WriteStream(stream.stream, stream.sbuffer, + navail, stream.show_warnings) + stream.parent_may_use_buffer = true + else + sleep(0.005) + end + end + catch ex + warn("Audio Output Task died with exception: $ex") + Base.show_backtrace(STDOUT, catch_backtrace()) + end +end + +function get_portaudio_devices() + require_portaudio_init() + device_count = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ()) + pa_devices = [ [Pa_GetDeviceInfo(i), i] for i in 0:(device_count - 1)] + [PortAudioInterface(bytestring(d[1].name), + bytestring(Pa_GetHostApiInfo(d[1].host_api).name), + d[1].max_input_channels, + d[1].max_output_channels, + d[2]) + for d in pa_devices] +end + +function require_portaudio_init() + # can be called multiple times with no effect + global portaudio_inited + if !portaudio_inited + info("Initializing PortAudio. Expect errors as we scan devices") + Pa_Initialize() + portaudio_inited = true + end +end + +end # module PortAudio \ No newline at end of file diff --git a/src/libportaudio.jl b/src/libportaudio.jl new file mode 100644 index 0000000..83a16f8 --- /dev/null +++ b/src/libportaudio.jl @@ -0,0 +1,230 @@ +# Low-level wrappers for Portaudio calls + +# General type aliases +typealias PaTime Cdouble +typealias PaError Cint +typealias PaSampleFormat Culong + +const PA_NO_ERROR = 0 +const PA_INPUT_OVERFLOWED = -10000 + 19 +const PA_OUTPUT_UNDERFLOWED = -10000 + 20 + +const paFloat32 = PaSampleFormat(0x01) +const paInt32 = PaSampleFormat(0x02) +const paInt24 = PaSampleFormat(0x04) +const paInt16 = PaSampleFormat(0x08) +const paInt8 = PaSampleFormat(0x10) +const paUInt8 = PaSampleFormat(0x20) + +@compat const pa_sample_formats = Dict{PaSampleFormat, Type}( + 1 => Float32 + 2 => Int32 + 4 => Int24 + 8 => Int16 + 16 => Int8 + 32 => UInt8 +) + +function Pa_Initialize() + err = ccall((:Pa_Initialize, libportaudio), PaError, ()) + handle_status(err) +end + +function Pa_Terminate() + err = ccall((:Pa_Terminate, libportaudio), PaError, ()) + handle_status(err) +end + +Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ()) + +function Pa_GetVersionText() + versionPtr = ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ()) + bytestring(versionPtr) +end + +# Host API Functions + +# A Host API is the top-level of the PortAudio hierarchy. Each host API has a +# unique type ID that tells you which native backend it is (JACK, ALSA, ASIO, +# etc.). On a given system you can identify each backend by its index, which +# will range between 0 and Pa_GetHostApiCount() - 1. You can enumerate through +# all the host APIs on the system by iterating through those values. + +typealias PaHostApiIndex Cint +typealias PaHostApiTypeId Cint + +# PaHostApiTypeId values +@compat const pa_host_api_names = Dict{PaHostApiTypeId, ASCIIString}( + 0 => "In Development", # use while developing support for a new host API + 1 => "Direct Sound", + 2 => "MME", + 3 => "ASIO", + 4 => "Sound Manager", + 5 => "Core Audio", + 7 => "OSS", + 8 => "ALSA", + 9 => "AL", + 10 => "BeOS", + 11 => "WDMKS", + 12 => "Jack", + 13 => "WASAPI", + 14 => "AudioScience HPI" +) + +type PaHostApiInfo + struct_version::Cint + api_type::PaHostApiTypeId + name::Ptr{Cchar} + deviceCount::Cint + defaultInputDevice::PaDeviceIndex + defaultOutputDevice::PaDeviceIndex +end + +Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio), + Ptr{PaHostApiInfo}, (PaHostApiIndex,), i)) + +# Device Functions + +typealias PaDeviceIndex Cint + +type PaDeviceInfo + struct_version::Cint + name::Ptr{Cchar} + host_api::PaHostApiIndex + max_input_channels::Cint + max_output_channels::Cint + default_low_input_latency::PaTime + default_low_output_latency::PaTime + default_high_input_latency::PaTime + default_high_output_latency::PaTime + default_sample_rate::Cdouble +end + +Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio), + Ptr{PaDeviceInfo}, (PaDeviceIndex,), i)) + +# Stream Functions + +# PaStream is always used as an opaque type, so we're always dealing +# with the pointer +typealias PaStream Ptr{Void} +typealias PaStreamCallback Void +typealias PaStreamFlags Culong + +function Pa_OpenDefaultStream(inChannels::Integer, outChannels::Integer, + sampleFormat::PaSampleFormat, + sampleRate::Real, framesPerBuffer::Integer) + streamPtr::Array{PaStream} = PaStream[0] + err = ccall((:Pa_OpenDefaultStream, libportaudio), + PaError, (Ptr{PaStream}, Cint, Cint, + PaSampleFormat, Cdouble, Culong, + Ptr{PaStreamCallback}, Ptr{Void}), + streamPtr, inChannels, outChannels, sampleFormat, sampleRate, + framesPerBuffer, 0, 0) + handle_status(err) + + streamPtr[1] +end + +""" +Open a single stream, not necessarily the default one +The stream is unidirectional, either inout or default output +see http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html +""" +function Pa_OpenStream(device::PaDeviceIndex, + channels::Cint, input::Bool, + sampleFormat::PaSampleFormat, + sampleRate::Cdouble, framesPerBuffer::Culong) + streamPtr::Array{PaStream} = PaStream[0] + ioParameters = Pa_StreamParameters(device, channels, + sampleFormat, PaTime(0.001), + Ptr{Void}(0)) + # CURRENTLY WORKING THIS OUT + if input + err = ccall((:Pa_OpenStream, libportaudio), PaError, + (PaStream, + Ptr{Pa_StreamParameters}, Ptr{Pa_StreamParameters}, + Cdouble, Culong, PaStreamFlags, + Ptr{PaStreamCallback}, Ptr{Void}), + streamPtr, ioParameters, Ptr{Void}(0), + sampleRate, framesPerBuffer, 0, + Ptr{PaStreamCallback}(0), Ptr{Void}(0)) + else + err = ccall((:Pa_OpenStream, libportaudio), PaError, + (PaStream, Ptr{Void}, Ref{Pa_StreamParameters}, + Cdouble, Culong, Culong, + Ptr{PaStreamCallback}, Ptr{Void}), + streamPtr, Ptr{Void}(0), ioParameters, + sampleRate, framesPerBuffer, 0, + Ptr{PaStreamCallback}(0), Ptr{Void}(0)) + end + handle_status(err) + streamPtr[1] +end + +function Pa_StartStream(stream::PaStream) + err = ccall((:Pa_StartStream, libportaudio), PaError, + (PaStream,), stream) + handle_status(err) +end + +function Pa_StopStream(stream::PaStream) + err = ccall((:Pa_StopStream, libportaudio), PaError, + (PaStream,), stream) + handle_status(err) +end + +function Pa_CloseStream(stream::PaStream) + err = ccall((:Pa_CloseStream, libportaudio), PaError, + (PaStream,), stream) + handle_status(err) +end + +function Pa_GetStreamReadAvailable(stream::PaStream) + avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, + (PaStream,), stream) + avail >= 0 || handle_status(avail) + avail +end + +function Pa_GetStreamWriteAvailable(stream::PaStream) + avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, + (PaStream,), stream) + avail >= 0 || handle_status(avail) + avail +end + +function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf), + show_warnings::Bool=true) + frames <= length(buf) || error("Need a buffer at least $frames long") + err = ccall((:Pa_ReadStream, libportaudio), PaError, + (PaStream, Ptr{Void}, Culong), + stream, buf, frames) + handle_status(err, show_warnings) + buf +end + +function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf), + show_warnings::Bool=true) + frames <= length(buf) || error("Need a buffer at least $frames long") + err = ccall((:Pa_WriteStream, libportaudio), PaError, + (PaStream, Ptr{Void}, Culong), + stream, buf, frames) + handle_status(err, show_warnings) + nothing +end + +# General utility function to handle the status from the Pa_* functions +function handle_status(err::PaError, show_warnings::Bool=true) + if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED + if show_warnings + msg = ccall((:Pa_GetErrorText, libportaudio), + Ptr{Cchar}, (PaError,), err) + warn("libportaudio: " * bytestring(msg)) + end + elseif err != PA_NO_ERROR + msg = ccall((:Pa_GetErrorText, libportaudio), + Ptr{Cchar}, (PaError,), err) + error("libportaudio: " * bytestring(msg)) + end +end diff --git a/src/nodes.jl b/src/nodes.jl deleted file mode 100644 index 19b579b..0000000 --- a/src/nodes.jl +++ /dev/null @@ -1,342 +0,0 @@ -#### NullNode #### - -type NullRenderer <: AudioRenderer end -typealias NullNode AudioNode{NullRenderer} -export NullNode - -function render(node::NullRenderer, device_input::AudioBuf, info::DeviceInfo) - # TODO: preallocate buffer - return zeros(info.buf_size) -end - -#### SinOsc #### - -# Generates a sin tone at the given frequency - -@compat type SinOscRenderer{T<:Union{Float32, AudioNode}} <: AudioRenderer - freq::T - phase::Float32 - buf::AudioBuf - - function SinOscRenderer(freq) - new(freq, 0.0, AudioSample[]) - end -end - -typealias SinOsc AudioNode{SinOscRenderer} -SinOsc(freq::Real) = SinOsc(SinOscRenderer{Float32}(freq)) -SinOsc(freq::AudioNode) = SinOsc(SinOscRenderer{AudioNode}(freq)) -SinOsc() = SinOsc(440) -export SinOsc - -function render(node::SinOscRenderer{Float32}, device_input::AudioBuf, - info::DeviceInfo) - if length(node.buf) != info.buf_size - resize!(node.buf, info.buf_size) - end - outbuf = node.buf - phase = node.phase - freq = node.freq - # make sure these are Float32s so that we don't allocate doing conversions - # in the tight loop - pi2::Float32 = 2pi - phase_inc::Float32 = 2pi * freq / info.sample_rate - i::Int = 1 - while i <= info.buf_size - outbuf[i] = sin(phase) - phase = (phase + phase_inc) % pi2 - i += 1 - end - node.phase = phase - return outbuf -end - -function render(node::SinOscRenderer{AudioNode}, device_input::AudioBuf, - info::DeviceInfo) - freq = render(node.freq, device_input, info)::AudioBuf - block_size = min(length(freq), info.buf_size) - if(length(node.buf) != block_size) - resize!(node.buf, block_size) - end - outbuf = node.buf - - phase::Float32 = node.phase - pi2::Float32 = 2pi - phase_step::Float32 = 2pi/(info.sample_rate) - i::Int = 1 - while i <= block_size - outbuf[i] = sin(phase) - phase = (phase + phase_step*freq[i]) % pi2 - i += 1 - end - node.phase = phase - return outbuf -end - -#### AudioMixer #### - -# Mixes a set of inputs equally - -type MixRenderer <: AudioRenderer - inputs::Vector{AudioNode} - buf::AudioBuf - - MixRenderer(inputs) = new(inputs, AudioSample[]) - MixRenderer() = MixRenderer(AudioNode[]) -end - -typealias AudioMixer AudioNode{MixRenderer} -export AudioMixer - -function render(node::MixRenderer, device_input::AudioBuf, info::DeviceInfo) - if length(node.buf) != info.buf_size - resize!(node.buf, info.buf_size) - end - mix_buffer = node.buf - n_inputs = length(node.inputs) - i = 1 - max_samples = 0 - fill!(mix_buffer, 0) - while i <= n_inputs - rendered = render(node.inputs[i], device_input, info)::AudioBuf - nsamples = length(rendered) - max_samples = max(max_samples, nsamples) - j::Int = 1 - while j <= nsamples - mix_buffer[j] += rendered[j] - j += 1 - end - if nsamples < info.buf_size - deleteat!(node.inputs, i) - n_inputs -= 1 - else - i += 1 - end - end - if max_samples < length(mix_buffer) - return mix_buffer[1:max_samples] - else - # save the allocate and copy if we don't need to - return mix_buffer - end -end - -Base.push!(mixer::AudioMixer, node::AudioNode) = push!(mixer.renderer.inputs, node) - -#### Gain #### -@compat type GainRenderer{T<:Union{Float32, AudioNode}} <: AudioRenderer - in1::AudioNode - in2::T - buf::AudioBuf - - GainRenderer(in1, in2) = new(in1, in2, AudioSample[]) -end - -function render(node::GainRenderer{Float32}, - device_input::AudioBuf, - info::DeviceInfo) - input = render(node.in1, device_input, info)::AudioBuf - if length(node.buf) != length(input) - resize!(node.buf, length(input)) - end - i = 1 - while i <= length(input) - node.buf[i] = input[i] * node.in2 - i += 1 - end - return node.buf -end - -function render(node::GainRenderer{AudioNode}, - device_input::AudioBuf, - info::DeviceInfo) - in1_data = render(node.in1, device_input, info)::AudioBuf - in2_data = render(node.in2, device_input, info)::AudioBuf - block_size = min(length(in1_data), length(in2_data)) - if length(node.buf) != block_size - resize!(node.buf, block_size) - end - i = 1 - while i <= block_size - node.buf[i] = in1_data[i] * in2_data[i] - i += 1 - end - return node.buf -end - -typealias Gain AudioNode{GainRenderer} -Gain(in1::AudioNode, in2::Real) = Gain(GainRenderer{Float32}(in1, in2)) -Gain(in1::AudioNode, in2::AudioNode) = Gain(GainRenderer{AudioNode}(in1, in2)) -export Gain - -#### Offset #### -type OffsetRenderer <: AudioRenderer - in_node::AudioNode - offset::Float32 - buf::AudioBuf - - OffsetRenderer(in_node, offset) = new(in_node, offset, AudioSample[]) -end - -function render(node::OffsetRenderer, device_input::AudioBuf, info::DeviceInfo) - input = render(node.in_node, device_input, info)::AudioBuf - if length(node.buf) != length(input) - resize!(node.buf, length(input)) - end - i = 1 - while i <= length(input) - node.buf[i] = input[i] + node.offset - i += 1 - end - return node.buf -end - -typealias Offset AudioNode{OffsetRenderer} -export Offset - - -#### Array Player #### - -# Plays a AudioBuf by rendering it out piece-by-piece - -type ArrayRenderer <: AudioRenderer - arr::AudioBuf - arr_index::Int - buf::AudioBuf - - ArrayRenderer(arr::AudioBuf) = new(arr, 1, AudioSample[]) -end - -typealias ArrayPlayer AudioNode{ArrayRenderer} -export ArrayPlayer - -function render(node::ArrayRenderer, device_input::AudioBuf, info::DeviceInfo) - range_end = min(node.arr_index + info.buf_size-1, length(node.arr)) - block_size = range_end - node.arr_index + 1 - if length(node.buf) != block_size - resize!(node.buf, block_size) - end - copy!(node.buf, 1, node.arr, node.arr_index, block_size) - node.arr_index = range_end + 1 - return node.buf -end - -# Allow users to play a raw array by wrapping it in an ArrayPlayer -function play(arr::AudioBuf, args...) - player = ArrayPlayer(arr) - play(player, args...) -end - -# If the array is the wrong floating type, convert it -function play{T <: AbstractFloat}(arr::Array{T}, args...) - arr = convert(AudioBuf, arr) - play(arr, args...) -end - -# If the array is an integer type, scale to [-1, 1] floating point - -# integer audio can be slightly (by 1) more negative than positive, -# so we just scale so that +/- typemax(T) becomes +/- 1 -function play{T <: Signed}(arr::Array{T}, args...) - arr = arr / typemax(T) - play(arr, args...) -end - -function play{T <: Unsigned}(arr::Array{T}, args...) - zero = (typemax(T) + 1) / 2 - range = floor(typemax(T) / 2) - arr = (arr .- zero) / range - play(arr, args...) -end - -#### Noise #### - -type WhiteNoiseRenderer <: AudioRenderer end -typealias WhiteNoise AudioNode{WhiteNoiseRenderer} -export WhiteNoise - -function render(node::WhiteNoiseRenderer, device_input::AudioBuf, info::DeviceInfo) - return rand(AudioSample, info.buf_size) .* 2 .- 1 -end - - -#### AudioInput #### - -# Renders incoming audio input from the hardware - -type InputRenderer <: AudioRenderer - channel::Int - InputRenderer(channel::Integer) = new(channel) - InputRenderer() = new(1) -end - -function render(node::InputRenderer, device_input::AudioBuf, info::DeviceInfo) - @assert size(device_input, 1) == info.buf_size - return device_input[:, node.channel] -end - -typealias AudioInput AudioNode{InputRenderer} -export AudioInput - -#### LinRamp #### - -type LinRampRenderer <: AudioRenderer - key_samples::Array{AudioSample} - key_durations::Array{Float32} - - duration::Float32 - buf::AudioBuf - - LinRampRenderer(start, finish, dur) = LinRampRenderer([start,finish], [dur]) - - LinRampRenderer(key_samples, key_durations) = - LinRampRenderer( - [convert(AudioSample,s) for s in key_samples], - [convert(Float32,d) for d in key_durations] - ) - - function LinRampRenderer(key_samples::Array{AudioSample}, key_durations::Array{Float32}) - @assert length(key_samples) == length(key_durations) + 1 - new(key_samples, key_durations, sum(key_durations), AudioSample[]) - end -end - -typealias LinRamp AudioNode{LinRampRenderer} -export LinRamp - -function render(node::LinRampRenderer, device_input::AudioBuf, info::DeviceInfo) - # Resize buffer if (1) it's too small or (2) we've hit the end of the ramp - ramp_samples::Int = round(Int, node.duration * info.sample_rate) - block_samples = min(ramp_samples, info.buf_size) - if length(node.buf) != block_samples - resize!(node.buf, block_samples) - end - - # Fill the buffer as long as there are more segments - dt::Float32 = 1/info.sample_rate - i::Int = 1 - while i <= length(node.buf) && length(node.key_samples) > 1 - - # Fill as much of the buffer as we can with the current segment - ds::Float32 = (node.key_samples[2] - node.key_samples[1]) / node.key_durations[1] / info.sample_rate - while i <= length(node.buf) - node.buf[i] = node.key_samples[1] - node.key_samples[1] += ds - node.key_durations[1] -= dt - node.duration -= dt - i += 1 - - # Discard segment if we're finished - if node.key_durations[1] <= 0 - if length(node.key_durations) > 1 - node.key_durations[2] -= node.key_durations[1] - end - shift!(node.key_samples) - shift!(node.key_durations) - break - end - end - end - - return node.buf -end diff --git a/src/operators.jl b/src/operators.jl deleted file mode 100644 index 1dbb6e7..0000000 --- a/src/operators.jl +++ /dev/null @@ -1,16 +0,0 @@ -*(node::AudioNode, coef::Real) = Gain(node, coef) -*(coef::Real, node::AudioNode) = Gain(node, coef) -*(node1::AudioNode, node2::AudioNode) = Gain(node1, node2) -# multiplying by silence gives silence -*(in1::NullNode, in2::NullNode) = in1 -*(in1::AudioNode, in2::NullNode) = in2 -*(in1::NullNode, in2::AudioNode) = in1 - - -+(in1::AudioNode, in2::AudioNode) = AudioMixer([in1, in2]) -# adding silence has no effect -+(in1::NullNode, in2::NullNode) = in1 -+(in1::AudioNode, in2::NullNode) = in1 -+(in1::NullNode, in2::AudioNode) = in2 -+(in1::AudioNode, in2::Real) = Offset(in1, in2) -+(in1::Real, in2::AudioNode) = Offset(in2, in1) diff --git a/src/portaudio.jl b/src/portaudio.jl deleted file mode 100644 index 941ad4f..0000000 --- a/src/portaudio.jl +++ /dev/null @@ -1,473 +0,0 @@ -typealias PaTime Cdouble -typealias PaError Cint -typealias PaSampleFormat Culong -# PaStream is always used as an opaque type, so we're always dealing -# with the pointer -typealias PaStream Ptr{Void} -typealias PaDeviceIndex Cint -typealias PaHostApiIndex Cint -typealias PaTime Cdouble -typealias PaHostApiTypeId Cint -typealias PaStreamCallback Void -typealias PaStreamFlags Culong - -const PA_NO_ERROR = 0 -const PA_INPUT_OVERFLOWED = -10000 + 19 -const PA_OUTPUT_UNDERFLOWED = -10000 + 20 - -const paFloat32 = convert(PaSampleFormat, 0x01) -const paInt32 = convert(PaSampleFormat, 0x02) -const paInt24 = convert(PaSampleFormat, 0x04) -const paInt16 = convert(PaSampleFormat, 0x08) -const paInt8 = convert(PaSampleFormat, 0x10) -const paUInt8 = convert(PaSampleFormat, 0x20) - -# PaHostApiTypeId values -@compat const pa_host_api_names = Dict( - 0 => "In Development", # use while developing support for a new host API - 1 => "Direct Sound", - 2 => "MME", - 3 => "ASIO", - 4 => "Sound Manager", - 5 => "Core Audio", - 7 => "OSS", - 8 => "ALSA", - 9 => "AL", - 10 => "BeOS", - 11 => "WDMKS", - 12 => "Jack", - 13 => "WASAPI", - 14 => "AudioScience HPI" -) - -# track whether we've already inited PortAudio -portaudio_inited = false - -################## Types #################### - -type PortAudioStream <: AudioStream - root::AudioMixer - info::DeviceInfo - show_warnings::Bool - stream::PaStream - - function PortAudioStream(sample_rate::Integer=44100, - buf_size::Integer=1024, - show_warnings::Bool=false) - require_portaudio_init() - stream = Pa_OpenDefaultStream(1, 1, paFloat32, sample_rate, buf_size) - Pa_StartStream(stream) - root = AudioMixer() - this = new(root, DeviceInfo(sample_rate, buf_size), - show_warnings, stream) - info("Scheduling PortAudio Render Task...") - # the task will actually start running the next time the current task yields - @schedule(portaudio_task(this)) - finalizer(this, destroy) - - this - end -end - -function destroy(stream::PortAudioStream) - # in 0.3 we can't print from a finalizer, as STDOUT may have been GC'ed - # already and we get a segfault. See - # https://github.com/JuliaLang/julia/issues/6075 - #info("Cleaning up stream") - Pa_StopStream(stream.stream) - Pa_CloseStream(stream.stream) - # we only have 1 stream at a time, so if we're closing out we can just - # terminate PortAudio. - Pa_Terminate() - portaudio_inited = false -end - -type Pa_StreamParameters - device::PaDeviceIndex - channelCount::Cint - sampleFormat::PaSampleFormat - suggestedLatency::PaTime - hostAPISpecificStreamInfo::Ptr{Void} -end - -""" - Open a single stream, not necessarily the default one - The stream is unidirectional, either inout or default output - see http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html -""" -function Pa_OpenStream(device::PaDeviceIndex, - channels::Cint, input::Bool, - sampleFormat::PaSampleFormat, - sampleRate::Cdouble, framesPerBuffer::Culong) - streamPtr::Array{PaStream} = PaStream[0] - ioParameters = Pa_StreamParameters(device, channels, - sampleFormat, PaTime(0.001), - Ptr{Void}(0)) - if input - err = ccall((:Pa_OpenStream, libportaudio), PaError, - (PaStream, Ref{Pa_StreamParameters}, Ptr{Void}, - Cdouble, Culong, Culong, - Ptr{PaStreamCallback}, Ptr{Void}), - streamPtr, ioParameters, Ptr{Void}(0), - sampleRate, framesPerBuffer, 0, - Ptr{PaStreamCallback}(0), Ptr{Void}(0)) - else - err = ccall((:Pa_OpenStream, libportaudio), PaError, - (PaStream, Ptr{Void}, Ref{Pa_StreamParameters}, - Cdouble, Culong, Culong, - Ptr{PaStreamCallback}, Ptr{Void}), - streamPtr, Ptr{Void}(0), ioParameters, - sampleRate, framesPerBuffer, 0, - Ptr{PaStreamCallback}(0), Ptr{Void}(0)) - end - handle_status(err) - streamPtr[1] -end - -type Pa_AudioStream <: AudioStream - root::AudioMixer - info::DeviceInfo - show_warnings::Bool - stream::PaStream - sformat::PaSampleFormat - sbuffer::Array{Real} - sbuffer_output_waiting::Integer - parent_may_use_buffer::Bool - - """ - Get device parameters needed for opening with portaudio - default is input as 44100/16bit int, same as CD audio type input - """ - function Pa_AudioStream(device_index, channels=2, input=false, - sample_rate::Integer=44100, - framesPerBuffer::Integer=2048, - show_warnings::Bool=false, - sample_format::PaSampleFormat=paInt16) - require_portaudio_init() - stream = Pa_OpenStream(device_index, channels, input, sample_format, - Cdouble(sample_rate), Culong(framesPerBuffer)) - Pa_StartStream(stream) - root = AudioMixer() - datatype = PaSampleFormat_to_T(sample_format) - sbuf = ones(datatype, framesPerBuffer) - this = new(root, DeviceInfo(sample_rate, framesPerBuffer), - show_warnings, stream, sample_format, sbuf, 0, false) - info("Scheduling PortAudio Render Task...") - if input - @schedule(pa_input_task(this)) - else - @schedule(pa_output_task(this)) - end - this - end -end - -""" -Blocking read from a Pa_AudioStream that is open as input -""" -function read_Pa_AudioStream(stream::Pa_AudioStream) - while true - while stream.parent_may_use_buffer == false - sleep(0.001) - end - buffer = deepcopy(stream.sbuffer) - stream.parent_may_use_buffer = false - return buffer - end -end - -""" -Blocking write to a Pa_AudioStream that is open for output -""" -function write_Pa_AudioStream(stream::Pa_AudioStream, buffer) - retval = 1 - sbufsize = length(stream.sbuffer) - inputlen = length(buffer) - if(inputlen > sbufsize) - info("Overflow at write_Pa_AudioStream") - retval = 0 - elseif(inputlen < sbufsize) - info("Underflow at write_Pa_AudioStream") - retval = -1 - end - while true - while stream.parent_may_use_buffer == false - sleep(0.001) - end - for idx in 1:min(sbufsize, inputlen) - stream.sbuffer[idx] = buffer[idx] - end - stream.parent_may_use_buffer = false - end - retval -end - -############ Internal Functions ############ - -function portaudio_task(stream::PortAudioStream) - info("PortAudio Render Task Running...") - n = bufsize(stream) - buffer = zeros(AudioSample, n) - try - while true - while Pa_GetStreamReadAvailable(stream.stream) < n - sleep(0.005) - end - Pa_ReadStream(stream.stream, buffer, n, stream.show_warnings) - # assume the root is always active - rendered = render(stream.root.renderer, buffer, stream.info)::AudioBuf - for i in 1:length(rendered) - buffer[i] = rendered[i] - end - for i in (length(rendered)+1):n - buffer[i] = 0.0 - end - while Pa_GetStreamWriteAvailable(stream.stream) < n - sleep(0.005) - end - Pa_WriteStream(stream.stream, buffer, n, stream.show_warnings) - end - catch ex - warn("Audio Task died with exception: $ex") - Base.show_backtrace(STDOUT, catch_backtrace()) - end -end - -""" - Helper function to make the right type of buffer for various - sample formats. Converts PaSampleFormat to a typeof -""" -function PaSampleFormat_to_T(fmt::PaSampleFormat) - retval = UInt8(0x0) - if fmt == 1 - retval = Float32(1.0) - elseif fmt == 2 - retval = Int32(0x02) - elseif fmt == 4 - retval = Int24(0x04) - elseif fmt == 8 - retval = Int16(0x08) - elseif fmt == 16 - retval = Int8(0x10) - elseif fmt == 32 - retval = UInt8(0x20) - else - info("Flawed input to PaSampleFormat_to_T, primitive unknown") - end - typeof(retval) -end - -""" - Get input device data, pass as a producer, no rendering -""" -function pa_input_task(stream::Pa_AudioStream) - info("PortAudio Input Task Running...") - n = bufsize(stream) - datatype = PaSampleFormat_to_T(stream.sformat) - # bigger ccall buffer to avoid overflow related errors - buffer = zeros(datatype, n * 8) - try - while true - while Pa_GetStreamReadAvailable(stream.stream) < n - sleep(0.005) - end - while stream.parent_may_use_buffer - sleep(0.005) - end - err = ccall((:Pa_ReadStream, libportaudio), PaError, - (PaStream, Ptr{Void}, Culong), - stream.stream, buffer, n) - handle_status(err, stream.show_warnings) - stream.sbuffer[1: n] = buffer[1: n] - stream.parent_may_use_buffer = true - sleep(0.005) - end - catch ex - warn("Audio Input Task died with exception: $ex") - Base.show_backtrace(STDOUT, catch_backtrace()) - end -end - -""" - Send output device data, no rendering -""" -function pa_output_task(stream::Pa_AudioStream) - info("PortAudio Output Task Running...") - n = bufsize(stream) - try - while true - navail = stream.sbuffer_output_waiting - if navail > n - info("Possible output buffer overflow in stream") - navail = n - end - if (navail > 1) & (stream.parent_may_use_buffer == false) & - (Pa_GetStreamWriteAvailable(stream.stream) < navail) - Pa_WriteStream(stream.stream, stream.sbuffer, - navail, stream.show_warnings) - stream.parent_may_use_buffer = true - else - sleep(0.005) - end - end - catch ex - warn("Audio Output Task died with exception: $ex") - Base.show_backtrace(STDOUT, catch_backtrace()) - end -end - -type PaDeviceInfo - struct_version::Cint - name::Ptr{Cchar} - host_api::PaHostApiIndex - max_input_channels::Cint - max_output_channels::Cint - default_low_input_latency::PaTime - default_low_output_latency::PaTime - default_high_input_latency::PaTime - default_high_output_latency::PaTime - default_sample_rate::Cdouble -end - -type PaHostApiInfo - struct_version::Cint - api_type::PaHostApiTypeId - name::Ptr{Cchar} - deviceCount::Cint - defaultInputDevice::PaDeviceIndex - defaultOutputDevice::PaDeviceIndex -end - -type PortAudioInterface <: AudioInterface - name::AbstractString - host_api::AbstractString - max_input_channels::Int - max_output_channels::Int - device_index::PaDeviceIndex -end - -function get_portaudio_devices() - require_portaudio_init() - device_count = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ()) - pa_devices = [ [Pa_GetDeviceInfo(i), i] for i in 0:(device_count - 1)] - [PortAudioInterface(bytestring(d[1].name), - bytestring(Pa_GetHostApiInfo(d[1].host_api).name), - d[1].max_input_channels, - d[1].max_output_channels, - d[2]) - for d in pa_devices] -end - -function require_portaudio_init() - # can be called multiple times with no effect - global portaudio_inited - if !portaudio_inited - info("Initializing PortAudio. Expect errors as we scan devices") - Pa_Initialize() - portaudio_inited = true - end -end - -# Low-level wrappers for Portaudio calls -Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio), - Ptr{PaDeviceInfo}, (PaDeviceIndex,), i)) -Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio), - Ptr{PaHostApiInfo}, (PaHostApiIndex,), i)) - -function Pa_Initialize() - err = ccall((:Pa_Initialize, libportaudio), PaError, ()) - handle_status(err) -end - -function Pa_Terminate() - err = ccall((:Pa_Terminate, libportaudio), PaError, ()) - handle_status(err) -end - -function Pa_StartStream(stream::PaStream) - err = ccall((:Pa_StartStream, libportaudio), PaError, - (PaStream,), stream) - handle_status(err) -end - -function Pa_StopStream(stream::PaStream) - err = ccall((:Pa_StopStream, libportaudio), PaError, - (PaStream,), stream) - handle_status(err) -end - -function Pa_CloseStream(stream::PaStream) - err = ccall((:Pa_CloseStream, libportaudio), PaError, - (PaStream,), stream) - handle_status(err) -end - -function Pa_GetStreamReadAvailable(stream::PaStream) - avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong, - (PaStream,), stream) - avail >= 0 || handle_status(avail) - avail -end - -function Pa_GetStreamWriteAvailable(stream::PaStream) - avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong, - (PaStream,), stream) - avail >= 0 || handle_status(avail) - avail -end - -function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf), - show_warnings::Bool=true) - frames <= length(buf) || error("Need a buffer at least $frames long") - err = ccall((:Pa_ReadStream, libportaudio), PaError, - (PaStream, Ptr{Void}, Culong), - stream, buf, frames) - handle_status(err, show_warnings) - buf -end - -function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf), - show_warnings::Bool=true) - frames <= length(buf) || error("Need a buffer at least $frames long") - err = ccall((:Pa_WriteStream, libportaudio), PaError, - (PaStream, Ptr{Void}, Culong), - stream, buf, frames) - handle_status(err, show_warnings) - nothing -end - -Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ()) - -function Pa_GetVersionText() - versionPtr = ccall((:Pa_GetVersionText, libportaudio), Ptr{Cchar}, ()) - bytestring(versionPtr) -end - -function Pa_OpenDefaultStream(inChannels::Integer, outChannels::Integer, - sampleFormat::PaSampleFormat, - sampleRate::Real, framesPerBuffer::Integer) - streamPtr::Array{PaStream} = PaStream[0] - err = ccall((:Pa_OpenDefaultStream, libportaudio), - PaError, (Ptr{PaStream}, Cint, Cint, - PaSampleFormat, Cdouble, Culong, - Ptr{PaStreamCallback}, Ptr{Void}), - streamPtr, inChannels, outChannels, sampleFormat, sampleRate, - framesPerBuffer, 0, 0) - handle_status(err) - - streamPtr[1] -end - -function handle_status(err::PaError, show_warnings::Bool=true) - if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED - if show_warnings - msg = ccall((:Pa_GetErrorText, libportaudio), - Ptr{Cchar}, (PaError,), err) - warn("libportaudio: " * bytestring(msg)) - end - elseif err != PA_NO_ERROR - msg = ccall((:Pa_GetErrorText, libportaudio), - Ptr{Cchar}, (PaError,), err) - error("libportaudio: " * bytestring(msg)) - end -end diff --git a/src/sndfile.jl b/src/sndfile.jl deleted file mode 100644 index 12f66a8..0000000 --- a/src/sndfile.jl +++ /dev/null @@ -1,209 +0,0 @@ -export af_open, FilePlayer, rewind, samplerate - -@compat const SFM_READ = Int32(0x10) -@compat const SFM_WRITE = Int32(0x20) - -const SF_FORMAT_WAV = 0x010000 -const SF_FORMAT_FLAC = 0x170000 -const SF_FORMAT_OGG = 0x200060 - -const SF_FORMAT_PCM_S8 = 0x0001 # Signed 8 bit data -const SF_FORMAT_PCM_16 = 0x0002 # Signed 16 bit data -const SF_FORMAT_PCM_24 = 0x0003 # Signed 24 bit data -const SF_FORMAT_PCM_32 = 0x0004 # Signed 32 bit data - -const SF_SEEK_SET = 0 -const SF_SEEK_CUR = 1 -const SF_SEEK_END = 2 - -@compat const EXT_TO_FORMAT = Dict( - ".wav" => SF_FORMAT_WAV, - ".flac" => SF_FORMAT_FLAC -) - -type SF_INFO - frames::Int64 - samplerate::Int32 - channels::Int32 - format::Int32 - sections::Int32 - seekable::Int32 -end - -type AudioFile - filePtr::Ptr{Void} - sfinfo::SF_INFO -end - -samplerate(f::AudioFile) = f.sfinfo.samplerate - -# AudioIO.open is part of the public API, but is not exported so that it -# doesn't conflict with Base.open -function open(path::AbstractString, mode::AbstractString = "r", - sampleRate::Integer = 44100, channels::Integer = 1, - format::Integer = 0) - @assert channels <= 2 - - sfinfo = SF_INFO(0, 0, 0, 0, 0, 0) - file_mode = SFM_READ - - if mode == "w" - file_mode = SFM_WRITE - sfinfo.samplerate = sampleRate - sfinfo.channels = channels - if format == 0 - _, ext = splitext(path) - sfinfo.format = EXT_TO_FORMAT[ext] | SF_FORMAT_PCM_16 - else - sfinfo.format = format - end - end - - filePtr = ccall((:sf_open, libsndfile), Ptr{Void}, - (Ptr{UInt8}, Int32, Ptr{SF_INFO}), - path, file_mode, &sfinfo) - - if filePtr == C_NULL - errmsg = ccall((:sf_strerror, libsndfile), Ptr{UInt8}, (Ptr{Void},), filePtr) - error(bytestring(errmsg)) - end - - return AudioFile(filePtr, sfinfo) -end - -function Base.close(file::AudioFile) - err = ccall((:sf_close, libsndfile), Int32, (Ptr{Void},), file.filePtr) - if err != 0 - error("Failed to close file") - end -end - -function open(f::Function, args...) - file = AudioIO.open(args...) - try - f(file) - finally - close(file) - end -end - -function af_open(args...) - warn("af_open is deprecated, please use AudioIO.open instead") - AudioIO.open(args...) -end - -# TODO: we should implement a general read(node::AudioNode) that pulls data -# through an arbitrary render chain and returns the result as a vector -function Base.read(file::AudioFile, nframes::Integer, dtype::Type) - @assert file.sfinfo.channels <= 2 - # the data comes in interleaved - arr = zeros(dtype, file.sfinfo.channels, nframes) - - if dtype == Int16 - nread = ccall((:sf_readf_short, libsndfile), Int64, - (Ptr{Void}, Ptr{Int16}, Int64), - file.filePtr, arr, nframes) - elseif dtype == Int32 - nread = ccall((:sf_readf_int, libsndfile), Int64, - (Ptr{Void}, Ptr{Int32}, Int64), - file.filePtr, arr, nframes) - elseif dtype == Float32 - nread = ccall((:sf_readf_float, libsndfile), Int64, - (Ptr{Void}, Ptr{Float32}, Int64), - file.filePtr, arr, nframes) - elseif dtype == Float64 - nread = ccall((:sf_readf_double, libsndfile), Int64, - (Ptr{Void}, Ptr{Float64}, Int64), - file.filePtr, arr, nframes) - end - - return arr[:, 1:nread]' -end - -Base.read(file::AudioFile, dtype::Type) = Base.read(file, file.sfinfo.frames, dtype) -Base.read(file::AudioFile, nframes::Integer) = Base.read(file, nframes, Int16) -Base.read(file::AudioFile) = Base.read(file, Int16) - -function Base.write{T}(file::AudioFile, frames::Array{T}) - @assert file.sfinfo.channels <= 2 - nframes = round(Int, length(frames) / file.sfinfo.channels) - - if T == Int16 - return ccall((:sf_writef_short, libsndfile), Int64, - (Ptr{Void}, Ptr{Int16}, Int64), - file.filePtr, frames, nframes) - elseif T == Int32 - return ccall((:sf_writef_int, libsndfile), Int64, - (Ptr{Void}, Ptr{Int32}, Int64), - file.filePtr, frames, nframes) - elseif T == Float32 - return ccall((:sf_writef_float, libsndfile), Int64, - (Ptr{Void}, Ptr{Float32}, Int64), - file.filePtr, frames, nframes) - elseif T == Float64 - return ccall((:sf_writef_double, libsndfile), Int64, - (Ptr{Void}, Ptr{Float64}, Int64), - file.filePtr, frames, nframes) - end -end - -function Base.seek(file::AudioFile, offset::Integer, whence::Integer) - new_offset = ccall((:sf_seek, libsndfile), Int64, - (Ptr{Void}, Int64, Int32), file.filePtr, offset, whence) - - if new_offset < 0 - error("Could not seek to $(offset) in file") - end - - new_offset -end - -# Some convenience methods for easily navigating through a sound file -Base.seek(file::AudioFile, offset::Integer) = seek(file, offset, SF_SEEK_SET) -rewind(file::AudioFile) = seek(file, 0, SF_SEEK_SET) - -type FileRenderer <: AudioRenderer - file::AudioFile - - function FileRenderer(file::AudioFile) - node = new(file) - finalizer(node, n -> close(n.file)) - return node - end -end - -typealias FilePlayer AudioNode{FileRenderer} -FilePlayer(file::AudioFile) = FilePlayer(FileRenderer(file)) -FilePlayer(path::AbstractString) = FilePlayer(AudioIO.open(path)) - -function render(node::FileRenderer, device_input::AudioBuf, info::DeviceInfo) - @assert node.file.sfinfo.samplerate == info.sample_rate - - # Keep reading data from the file until the output buffer is full, but stop - # as soon as no more data can be read from the file - audio = Array(AudioSample, 0, node.file.sfinfo.channels) - while true - read_audio = read(node.file, info.buf_size-size(audio, 1), AudioSample) - audio = vcat(audio, read_audio) - if size(audio, 1) >= info.buf_size || size(read_audio, 1) <= 0 - break - end - end - - # if the file is stereo, mix the two channels together - if node.file.sfinfo.channels == 2 - return (audio[:, 1] / 2) + (audio[:, 2] / 2) - else - return audio - end -end - -function play(filename::AbstractString, args...) - player = FilePlayer(filename) - play(player, args...) -end - -function play(file::AudioFile, args...) - player = FilePlayer(file) - play(player, args...) -end diff --git a/test/REQUIRE b/test/REQUIRE index bc3e234..94e516f 100644 --- a/test/REQUIRE +++ b/test/REQUIRE @@ -1 +1 @@ -FactCheck +BaseTestNext diff --git a/test/runtests.jl b/test/runtests.jl index 63338f6..ba00736 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,18 +1,7 @@ #!/usr/bin/env julia -using FactCheck - -test_regex = r"^test_.*\.jl$" -test_dir = Pkg.dir("AudioIO", "test") - -test_files = filter(n -> ismatch(test_regex, n), readdir(test_dir)) -if length(test_files) == 0 - error("No test files found. Make sure you're running from the root directory") +@testset "No Tests" begin + @test false end -for test_file in test_files - include(test_file) -end - -# return the overall exit status -exitstatus() +exit(0) diff --git a/test/test_AudioIO.jl b/test/test_AudioIO.jl deleted file mode 100644 index a2672ed..0000000 --- a/test/test_AudioIO.jl +++ /dev/null @@ -1,90 +0,0 @@ -module TestAudioIO - -using FactCheck -using Compat -using AudioIO -import AudioIO.AudioBuf - -const TEST_SAMPLERATE = 44100 -const TEST_BUF_SIZE = 1024 - -include("testhelpers.jl") - - -type TestAudioStream <: AudioIO.AudioStream - root::AudioIO.AudioMixer - info::AudioIO.DeviceInfo - - function TestAudioStream() - root = AudioMixer() - new(root, AudioIO.DeviceInfo(TEST_SAMPLERATE, TEST_BUF_SIZE)) - end -end - -# render the stream and return the next block of audio. This is used in testing -# to simulate the audio callback that's normally called by the device. -function process(stream::TestAudioStream) - out_array = zeros(AudioIO.AudioSample, stream.info.buf_size) - in_array = zeros(AudioIO.AudioSample, stream.info.buf_size) - rendered = AudioIO.render(stream.root, in_array, stream.info) - out_array[1:length(rendered)] = rendered - return out_array -end - - -#### Test playing back various vector types #### - -facts("Array playback") do - # data shared between tests, for convenience - t = linspace(0, 2, 2 * 44100) - phase = 2pi * 100 * t - - ## Test Float32 arrays, this is currently the native audio playback format - context("Playing Float32 arrays") do - f32 = convert(Array{Float32}, sin(phase)) - test_stream = TestAudioStream() - player = play(f32, test_stream) - @fact process(test_stream) --> f32[1:TEST_BUF_SIZE] - end - - context("Playing Float64 arrays") do - f64 = convert(Array{Float64}, sin(phase)) - test_stream = TestAudioStream() - player = play(f64, test_stream) - @fact process(test_stream) --> convert(AudioBuf, f64[1:TEST_BUF_SIZE]) - end - - context("Playing Int8(Signed) arrays") do - i8 = Int8[-127:127;] - test_stream = TestAudioStream() - player = play(i8, test_stream) - @fact process(test_stream)[1:255] --> - mse(convert(AudioBuf, collect(linspace(-1.0, 1.0, 255)))) - end - - context("Playing UInt8(Unsigned) arrays") do - # for unsigned 8-bit audio silence is represented as 128, so the symmetric range - # is 1-255 - ui8 = UInt8[1:255;] - test_stream = TestAudioStream() - player = play(ui8, test_stream) - @fact process(test_stream)[1:255] --> - mse(convert(AudioBuf, collect(linspace(-1.0, 1.0, 255)))) - end -end - -facts("AudioNode Stopping") do - test_stream = TestAudioStream() - node = SinOsc(440) - play(node, test_stream) - process(test_stream) - stop(node) - @fact process(test_stream) --> zeros(AudioIO.AudioSample, TEST_BUF_SIZE) -end - -facts("Audio Device Listing") do - # there aren't any devices on the Travis machine so just test that this doesn't crash - @fact get_audio_devices() --> issubtype(Array) -end - -end # module TestAudioIO diff --git a/test/test_nodes.jl b/test/test_nodes.jl deleted file mode 100644 index 8ead8ea..0000000 --- a/test/test_nodes.jl +++ /dev/null @@ -1,194 +0,0 @@ -module TestAudioIONodes - -using Compat -using FactCheck -using AudioIO -import AudioIO: AudioSample, AudioBuf, AudioRenderer, AudioNode -import AudioIO: DeviceInfo, render - -include("testhelpers.jl") - -# A TestNode just renders out 1:buf_size each frame -type TestRenderer <: AudioRenderer - buf::AudioBuf - TestRenderer(buf_size::Integer) = new(AudioSample[1:buf_size;]) -end - -typealias TestNode AudioNode{TestRenderer} -TestNode(buf_size) = TestNode(TestRenderer(buf_size)) - -function render(node::TestRenderer, - device_input::AudioBuf, - info::DeviceInfo) - return node.buf -end - -test_info = DeviceInfo(44100, 512) -dev_input = zeros(AudioSample, test_info.buf_size) - -facts("Validating TestNode allocation") do - # first validate that the TestNode doesn't allocate so it doesn't mess up our - # other tests - test = TestNode(test_info.buf_size) - # JIT - render(test, dev_input, test_info) - @fact (@allocated render(test, dev_input, test_info)) --> 16 -end - -#### AudioMixer Tests #### - -# TODO: there should be a setup/teardown mechanism and some way to isolate -# tests - -facts("AudioMixer") do - context("0 Input Mixer") do - mix = AudioMixer() - render_output = render(mix, dev_input, test_info) - @fact render_output --> AudioSample[] - # 0.4 uses 64 bytes, 0.3 uses 48 - @fact (@allocated render(mix, dev_input, test_info)) --> less_than(65) - end - - context("1 Input Mixer") do - testnode = TestNode(test_info.buf_size) - mix = AudioMixer([testnode]) - render_output = render(mix, dev_input, test_info) - @fact render_output --> AudioSample[1:test_info.buf_size;] - @fact (@allocated render(mix, dev_input, test_info)) --> 64 - end - - context("2 Input Mixer") do - test1 = TestNode(test_info.buf_size) - test2 = TestNode(test_info.buf_size) - mix = AudioMixer([test1, test2]) - render_output = render(mix, dev_input, test_info) - # make sure the two inputs are being added together - @fact render_output --> 2 * AudioSample[1:test_info.buf_size;] - @fact (@allocated render(mix, dev_input, test_info)) --> 96 - # now we'll stop one of the inputs and make sure it gets removed - stop(test1) - render_output = render(mix, dev_input, test_info) - # make sure the two inputs are being added together - @fact render_output --> AudioSample[1:test_info.buf_size;] - - stop(mix) - render_output = render(mix, dev_input, test_info) - @fact render_output --> AudioSample[] - end -end - -MSE_THRESH = 1e-7 - -facts("SinOSC") do - freq = 440 - # note that this range includes the end, which is why there are - # sample_rate+1 samples - t = linspace(0, 1, round(Int, test_info.sample_rate+1)) - test_vect = convert(AudioBuf, sin(2pi * t * freq)) - context("Fixed Frequency") do - osc = SinOsc(freq) - render_output = render(osc, dev_input, test_info) - @fact mse(render_output, test_vect[1:test_info.buf_size]) --> - lessthan(MSE_THRESH) - render_output = render(osc, dev_input, test_info) - @fact mse(render_output, - test_vect[test_info.buf_size+1:2*test_info.buf_size]) --> - lessthan(MSE_THRESH) - @fact (@allocated render(osc, dev_input, test_info)) --> 64 - stop(osc) - render_output = render(osc, dev_input, test_info) - @fact render_output --> AudioSample[] - end - - context("Testing SinOsc with signal input") do - t = linspace(0, 1, round(Int, test_info.sample_rate+1)) - f = 440 .- t .* (440-110) - dt = 1 / test_info.sample_rate - # NOTE - this treats the phase as constant at each sample, which isn't strictly - # true. Unfortunately doing this correctly requires knowing more about the - # modulating signal and doing the real integral - phase = cumsum(2pi * dt .* f) - unshift!(phase, 0) - expected = convert(AudioBuf, sin(phase)) - - freq = LinRamp(440, 110, 1) - osc = SinOsc(freq) - render_output = render(osc, dev_input, test_info) - @fact mse(render_output, expected[1:test_info.buf_size]) --> - lessthan(MSE_THRESH) - render_output = render(osc, dev_input, test_info) - @fact mse(render_output, - expected[test_info.buf_size+1:2*test_info.buf_size]) --> - lessthan(MSE_THRESH) - # give a bigger budget here because we're rendering 2 nodes - @fact (@allocated render(osc, dev_input, test_info)) --> 160 - end -end - -facts("AudioInput") do - node = AudioInput() - test_data = rand(AudioSample, test_info.buf_size) - render_output = render(node, test_data, test_info) - @fact render_output --> test_data -end - -facts("ArrayPlayer") do - context("playing long sample") do - v = rand(AudioSample, 44100) - player = ArrayPlayer(v) - render_output = render(player, dev_input, test_info) - @fact render_output --> v[1:test_info.buf_size] - render_output = render(player, dev_input, test_info) - @fact render_output --> v[(test_info.buf_size + 1) : (2*test_info.buf_size)] - @fact (@allocated render(player, dev_input, test_info)) --> 192 - stop(player) - render_output = render(player, dev_input, test_info) - @fact render_output --> AudioSample[] - end - - context("testing end of vector") do - # give a vector just a bit larger than 1 buffer size - v = rand(AudioSample, test_info.buf_size + 1) - player = ArrayPlayer(v) - render(player, dev_input, test_info) - render_output = render(player, dev_input, test_info) - @fact render_output --> v[test_info.buf_size+1:end] - end -end - -facts("Gain") do - context("Constant Gain") do - gained = TestNode(test_info.buf_size) * 0.75 - render_output = render(gained, dev_input, test_info) - @fact render_output --> 0.75 * AudioSample[1:test_info.buf_size;] - @fact (@allocated render(gained, dev_input, test_info)) --> 32 - end - context("Gain by a Signal") do - gained = TestNode(test_info.buf_size) * TestNode(test_info.buf_size) - render_output = render(gained, dev_input, test_info) - @fact render_output --> AudioSample[1:test_info.buf_size;] .* AudioSample[1:test_info.buf_size;] - @fact (@allocated render(gained, dev_input, test_info)) --> 48 - end -end - -facts("LinRamp") do - ramp = LinRamp(0.25, 0.80, 1) - expected = convert(AudioBuf, collect(linspace(0.25, 0.80, round(Int, test_info.sample_rate+1)))) - render_output = render(ramp, dev_input, test_info) - @fact mse(render_output, expected[1:test_info.buf_size]) --> - lessthan(MSE_THRESH) - render_output = render(ramp, dev_input, test_info) - @fact mse(render_output, - expected[(test_info.buf_size+1):(2*test_info.buf_size)]) --> - lessthan(MSE_THRESH) - @fact (@allocated render(ramp, dev_input, test_info)) --> 64 -end - -facts("Offset") do - offs = TestNode(test_info.buf_size) + 0.5 - render_output = render(offs, dev_input, test_info) - @fact render_output --> 0.5 + AudioSample[1:test_info.buf_size;] - @fact (@allocated render(offs, dev_input, test_info)) --> 32 -end - -end # module TestAudioIONodes diff --git a/test/test_sndfile.jl b/test/test_sndfile.jl deleted file mode 100644 index dcc0747..0000000 --- a/test/test_sndfile.jl +++ /dev/null @@ -1,83 +0,0 @@ -module TestSndfile - -using Compat -using FactCheck -using AudioIO -import AudioIO: DeviceInfo, render, AudioSample, AudioBuf - -include("testhelpers.jl") - -facts("WAV file write/read") do - fname = Pkg.dir("AudioIO", "test", "sinwave.wav") - - srate = 44100 - freq = 440 - t = collect(0 : 2 * srate - 1) / srate - phase = 2 * pi * freq * t - reference = round(Int16, (2 ^ 15 - 1) * sin(phase)) - - AudioIO.open(fname, "w") do f - write(f, reference) - end - - # test basic reading - AudioIO.open(fname) do f - @fact f.sfinfo.channels --> 1 - @fact f.sfinfo.frames --> 2 * srate - actual = read(f) - @fact length(reference) --> length(actual) - @fact reference --> actual[:, 1] - @fact samplerate(f) --> srate - end - - # test seeking - - # test rendering as an AudioNode - AudioIO.open(fname) do f - # pretend we have a stream at the same rate as the file - bufsize = 1024 - input = zeros(AudioSample, bufsize) - test_info = DeviceInfo(srate, bufsize) - node = FilePlayer(f) - # convert to floating point because that's what AudioIO uses natively - expected = convert(AudioBuf, reference ./ (2^15)) - buf = render(node, input, test_info) - @fact expected[1:bufsize] --> buf[1:bufsize] - buf = render(node, input, test_info) - @fact expected[bufsize+1:2*bufsize] --> buf[1:bufsize] - end -end - -facts("Stereo file reading") do - fname = Pkg.dir("AudioIO", "test", "440left_880right.wav") - srate = 44100 - t = collect(0:(2 * srate - 1)) / srate - expected = round(Int16, (2^15-1) * hcat(sin(2pi*t*440), sin(2pi*t*880))) - - AudioIO.open(fname) do f - buf = read(f) - @fact buf --> mse(expected, 5) - end -end - -# note - currently AudioIO just mixes down to Mono. soon we'll support this -# new-fangled stereo sound stuff -facts("Stereo file rendering") do - fname = Pkg.dir("AudioIO", "test", "440left_880right.wav") - srate = 44100 - bufsize = 1024 - input = zeros(AudioSample, bufsize) - test_info = DeviceInfo(srate, bufsize) - t = collect(0 : 2 * srate - 1) / srate - expected = convert(AudioBuf, 0.5 * (sin(2pi*t*440) + sin(2pi*t*880))) - - AudioIO.open(fname) do f - node = FilePlayer(f) - buf = render(node, input, test_info) - @fact buf[1:bufsize] --> mse(expected[1:bufsize]) - buf = render(node, input, test_info) - @fact buf[1:bufsize] --> mse(expected[bufsize+1:2*bufsize]) - end -end - -end # module TestSndfile diff --git a/test/testhelpers.jl b/test/testhelpers.jl deleted file mode 100644 index b0b4aa9..0000000 --- a/test/testhelpers.jl +++ /dev/null @@ -1,22 +0,0 @@ -# convenience function to calculate the mean-squared error -function mse(arr1::AbstractArray, arr2::AbstractArray) - @assert length(arr1) == length(arr2) - N = length(arr1) - err = 0.0 - for i in 1:N - err += (arr2[i] - arr1[i])^2 - end - err /= N -end - -mse(X::AbstractArray, thresh=1e-8) = Y::AbstractArray -> begin - if size(X) != size(Y) - return false - end - - return mse(X, Y) < thresh -end - -issubtype(T::Type) = x -> typeof(x) <: T -lessthan(rhs) = lhs -> lhs < rhs -greaterthan(rhs) = lhs -> lhs > rhs