massive restructuring. mono output to default sink works

This commit is contained in:
Spencer Russell 2016-03-19 21:54:46 -04:00
parent 80d329eb39
commit fae9b38224
4 changed files with 363 additions and 286 deletions

View file

@ -1,5 +1,6 @@
julia 0.4
BinDeps
Compat
Devectorize
@osx Homebrew
@windows WinRPM

View file

@ -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
# 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

View file

@ -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
@ -85,8 +114,6 @@ Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio),
# Device Functions
typealias PaDeviceIndex Cint
type PaDeviceInfo
struct_version::Cint
name::Ptr{Cchar}
@ -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,
function Pa_OpenDefaultStream(inChannels, outChannels,
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
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)

View file

@ -1,6 +1,6 @@
#!/usr/bin/env julia
@testset "No Tests" begin
@testset "PortAudio Tests" begin
@test false
end