diff --git a/REQUIRE b/REQUIRE index 4929239..6934a60 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,5 +1,6 @@ julia 0.4 BinDeps Compat +Devectorize @osx Homebrew @windows WinRPM diff --git a/src/PortAudio.jl b/src/PortAudio.jl index 5d4c858..dafb245 100644 --- a/src/PortAudio.jl +++ b/src/PortAudio.jl @@ -1,261 +1,324 @@ module PortAudio using SampleTypes using Compat +using FixedPointNumbers +using Devectorize +using RingBuffers # 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 +export PortAudioSink, PortAudioSource -function devices() - return get_portaudio_devices() -end +# initialize PortAudio on module load +Pa_Initialize() -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 +type PortAudioDevice + name::UTF8String + host_api::UTF8String max_input_channels::Int max_output_channels::Int device_index::PaDeviceIndex end +function devices() + ndevices = Pa_GetDeviceCount() + infos = PaDeviceInfo[Pa_GetDeviceInfo(i) for i in 0:(ndevices - 1)] -type Pa_AudioStream <: AudioStream - root::AudioMixer - info::DeviceInfo - show_warnings::Bool + [PortAudioDevice(bytestring(d.name), + bytestring(Pa_GetHostApiInfo(d.host_api).name), + d.max_input_channels, + d.max_output_channels, + i-1) + for (i, d) in enumerate(infos)] +end + +# paramaterized on the sample type and sampling rate type +type PortAudioSink{T, U} <: SampleSink stream::PaStream - sformat::PaSampleFormat - sbuffer::Array{Real} - sbuffer_output_waiting::Integer - parent_may_use_buffer::Bool + nchannels::Int + samplerate::U + bufsize::Int + ringbuf::RingBuffer + open::Bool + task::Task - """ - 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)) + function PortAudioSink(eltype, rate, channels, bufsize) + stream = Pa_OpenDefaultStream(0, channels, type_to_fmt[eltype], float(rate), bufsize) + writers = Condition[] + # we want the ringbuf to output zeros to portaudio if it runs out of samples + ringbuf = RingBuffer(eltype, RINGBUF_FRAMES, channels; underflow=PAD) 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 = new(stream, channels, rate, bufsize, ringbuf, true) + this.task = @schedule sinktask(this) + finalizer(this, close) + yield() + 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 +PortAudioSink(eltype=Float32, rate=48000Hz, channels=2, bufsize=DEFAULT_BUFSIZE) = + PortAudioSink{eltype, typeof(rate)}(eltype, rate, channels, bufsize) + + +const DEFAULT_BUFSIZE=4096 +const RINGBUF_FRAMES = 131072 + +function Base.close(sink::PortAudioSink) + sink.open = false + # no task switches inside finalizer, not sure if we'll need this or not + # wait(sink.task) + Pa_StopStream(sink.stream) + Pa_CloseStream(sink.stream) 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 + +SampleTypes.nchannels(sink::PortAudioSink) = sink.nchannels +SampleTypes.samplerate(sink::PortAudioSink) = sink.samplerate +Base.eltype{T, U}(sink::PortAudioSink{T, U}) = T + +# type PortAudioSource <: SampleSource +# stream::PaStream +# end + +function SampleTypes.unsafe_write(sink::PortAudioSink, buf::SampleBuf) + write(sink.ringbuf, buf) end -############ Internal Functions ############ +# function SampleTypes.unsafe_read!(sink::PortAudioSource, buf::SampleBuf) +# end -function portaudio_task(stream::PortAudioStream) - info("PortAudio Render Task Running...") - n = bufsize(stream) - buffer = zeros(AudioSample, n) +function sinktask(sink::PortAudioSink) + println("starting sink task") + buffer = Array(eltype(sink), sink.bufsize, nchannels(sink)) try - while true - while Pa_GetStreamReadAvailable(stream.stream) < n - sleep(0.005) + while sink.open + total = Pa_GetStreamWriteAvailable(sink.stream) + written = 0 + while(written < total) + tocopy = min(size(buffer, 1), total - written) + read!(sink.ringbuf, sub(buffer, 1:tocopy, :)) + # TODO: this will only work for mono streams + Pa_WriteStream(sink.stream, buffer, tocopy, true) + written += tocopy 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") + warn("PortAudio Sink Task died with exception: $ex") Base.show_backtrace(STDOUT, catch_backtrace()) end + println("sink task finished") 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 sourcetask(sink::PortAudioSource) +# while sink.open +# end +# +# 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 -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 + + + + + + + + + + + + + + + +# 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 + +end # module PortAudio diff --git a/src/libportaudio.jl b/src/libportaudio.jl index 83a16f8..2a58ebc 100644 --- a/src/libportaudio.jl +++ b/src/libportaudio.jl @@ -4,6 +4,16 @@ typealias PaTime Cdouble typealias PaError Cint typealias PaSampleFormat Culong +typealias PaDeviceIndex Cint +typealias PaHostApiIndex Cint +typealias PaHostApiTypeId Cint +# 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 + + const PA_NO_ERROR = 0 const PA_INPUT_OVERFLOWED = -10000 + 19 @@ -15,16 +25,38 @@ const paInt24 = PaSampleFormat(0x04) const paInt16 = PaSampleFormat(0x08) const paInt8 = PaSampleFormat(0x10) const paUInt8 = PaSampleFormat(0x20) +const paNonInterleaved = PaSampleFormat(0x80000000) -@compat const pa_sample_formats = Dict{PaSampleFormat, Type}( - 1 => Float32 - 2 => Int32 - 4 => Int24 - 8 => Int16 - 16 => Int8 +const fmt_to_type = Dict{PaSampleFormat, Type}( + 1 => Float32, + 2 => Int32, + # 4 => Int24, + 8 => Int16, + 16 => Int8, 32 => UInt8 ) +# const type_to_fmt = Dict{Type, PaSampleFormat}( +# Float32 => 1 | paNonInterleaved, +# Int32 => 2 | paNonInterleaved, +# # Int24 => 4 | paNonInterleaved, +# Int16 => 8 | paNonInterleaved, +# Int8 => 16 | paNonInterleaved, +# UInt8 => 3 | paNonInterleaved +# ) +# + +# TODO: temporary for testing mono +const type_to_fmt = Dict{Type, PaSampleFormat}( + Float32 => 1, + Int32 => 2, + # Int24 =>, + Int16 => 8, + Int8 => 16, + UInt8 => 3 +) + + function Pa_Initialize() err = ccall((:Pa_Initialize, libportaudio), PaError, ()) handle_status(err) @@ -50,9 +82,6 @@ end # 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 @@ -82,10 +111,8 @@ end Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio), Ptr{PaHostApiInfo}, (PaHostApiIndex,), i)) - -# Device Functions -typealias PaDeviceIndex Cint +# Device Functions type PaDeviceInfo struct_version::Cint @@ -100,66 +127,52 @@ type PaDeviceInfo default_sample_rate::Cdouble end +Pa_GetDeviceCount() = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ()) + 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] +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)) - # 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 +function Pa_OpenDefaultStream(inChannels, outChannels, + sampleFormat::PaSampleFormat, + sampleRate, framesPerBuffer) + streamPtr = Ref{PaStream}(0) + err = ccall((:Pa_OpenDefaultStream, libportaudio), + PaError, (Ref{PaStream}, Cint, Cint, + PaSampleFormat, Cdouble, Culong, + Ref{Void}, Ref{Void}), + streamPtr, inChannels, outChannels, sampleFormat, sampleRate, + framesPerBuffer, C_NULL, C_NULL) handle_status(err) - streamPtr[1] + + streamPtr[] +end + +function Pa_OpenStream(inParams::Pa_StreamParameters, + outParams::Pa_StreamParameters, + sampleRate, framesPerBuffer, + flags::PaStreamFlags) + streamPtr = Ref{PaStream}(0) + err = ccall((:Pa_OpenStream, libportaudio), PaError, + (Ref{PaStream}, + Ref{Pa_StreamParameters}, + Ref{Pa_StreamParameters}, + Cdouble, Culong, PaStreamFlags, + Ref{Void}, Ref{Void}), + streamPtr, inParams, outParams, + sampleRate, framesPerBuffer, flags, + C_NULL, C_NULL) + handle_status(err) + streamPtr[] end function Pa_StartStream(stream::PaStream) diff --git a/test/runtests.jl b/test/runtests.jl index ba00736..14b16b8 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ #!/usr/bin/env julia -@testset "No Tests" begin +@testset "PortAudio Tests" begin @test false end