2014-01-03 11:47:38 -08:00
|
|
|
typealias PaTime Cdouble
|
|
|
|
typealias PaError Cint
|
|
|
|
typealias PaSampleFormat Culong
|
2014-08-28 16:05:51 -04:00
|
|
|
# PaStream is always used as an opaque type, so we're always dealing with the
|
|
|
|
# pointer
|
|
|
|
typealias PaStream Ptr{Void}
|
2014-05-02 16:10:54 -04:00
|
|
|
typealias PaDeviceIndex Cint
|
|
|
|
typealias PaHostApiIndex Cint
|
|
|
|
typealias PaTime Cdouble
|
|
|
|
typealias PaHostApiTypeId Cint
|
2014-08-28 16:05:51 -04:00
|
|
|
typealias PaStreamCallback Void
|
2014-01-03 11:47:38 -08:00
|
|
|
|
|
|
|
const PA_NO_ERROR = 0
|
2014-08-28 16:05:51 -04:00
|
|
|
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)
|
2014-01-03 11:47:38 -08:00
|
|
|
|
2014-05-02 16:10:54 -04:00
|
|
|
# PaHostApiTypeId values
|
|
|
|
const pa_host_api_names = {
|
|
|
|
0 => "In Development", # use while developing support for a new host API
|
|
|
|
1 => "Direct Sound",
|
|
|
|
2 => "MME",
|
|
|
|
3 => "ASIO",
|
|
|
|
4 => "Sound Manager",
|
|
|
|
5 => "Core Audio",
|
|
|
|
7 => "OSS",
|
|
|
|
8 => "ALSA",
|
|
|
|
9 => "AL",
|
|
|
|
10 => "BeOS",
|
|
|
|
11 => "WDMKS",
|
|
|
|
12 => "Jack",
|
|
|
|
13 => "WASAPI",
|
|
|
|
14 => "AudioScience HPI"
|
|
|
|
}
|
|
|
|
|
2014-01-03 11:47:38 -08:00
|
|
|
# track whether we've already inited PortAudio
|
|
|
|
portaudio_inited = false
|
|
|
|
|
|
|
|
################## Types ####################
|
|
|
|
|
|
|
|
type PortAudioStream <: AudioStream
|
2014-06-23 02:10:35 -04:00
|
|
|
root::AudioMixer
|
2014-01-03 11:47:38 -08:00
|
|
|
info::DeviceInfo
|
2014-08-28 16:05:51 -04:00
|
|
|
stream::PaStream
|
2014-01-03 11:47:38 -08:00
|
|
|
|
2014-08-28 16:05:51 -04:00
|
|
|
function PortAudioStream(sample_rate::Integer=44100, buf_size::Integer=1024)
|
|
|
|
require_portaudio_init()
|
|
|
|
stream = Pa_OpenDefaultStream(1, 1, paFloat32, sample_rate, buf_size)
|
|
|
|
Pa_StartStream(stream)
|
2014-06-23 02:10:35 -04:00
|
|
|
root = AudioMixer()
|
2014-08-28 16:05:51 -04:00
|
|
|
this = new(root, DeviceInfo(sample_rate, buf_size), stream)
|
|
|
|
info("Scheduling PortAudio Render Task...")
|
|
|
|
# the task will actually start running the next time the current task yields
|
|
|
|
@schedule(portaudio_task(this))
|
|
|
|
finalizer(this, destroy)
|
2014-01-03 11:47:38 -08:00
|
|
|
|
2014-08-28 16:05:51 -04:00
|
|
|
this
|
2014-08-28 13:36:38 -04:00
|
|
|
end
|
2014-01-03 11:47:38 -08:00
|
|
|
end
|
|
|
|
|
2014-08-28 16:05:51 -04:00
|
|
|
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
|
2014-01-03 11:47:38 -08:00
|
|
|
end
|
|
|
|
|
2014-08-28 16:05:51 -04:00
|
|
|
############ Internal Functions ############
|
|
|
|
|
|
|
|
function portaudio_task(stream::PortAudioStream)
|
|
|
|
info("PortAudio Render Task Running...")
|
|
|
|
n = bufsize(stream)
|
|
|
|
buffer = zeros(AudioSample, n)
|
2014-01-05 22:50:56 -05:00
|
|
|
try
|
|
|
|
while true
|
2014-08-28 16:05:51 -04:00
|
|
|
while Pa_GetStreamReadAvailable(stream.stream) < n
|
|
|
|
sleep(0.005)
|
|
|
|
end
|
|
|
|
Pa_ReadStream(stream.stream, buffer, n)
|
2014-06-23 02:10:35 -04:00
|
|
|
# assume the root is always active
|
2014-06-26 15:36:52 -05:00
|
|
|
rendered = render(stream.root.renderer, buffer, stream.info)::AudioBuf
|
2014-06-23 02:10:35 -04:00
|
|
|
for i in 1:length(rendered)
|
|
|
|
buffer[i] = rendered[i]
|
|
|
|
end
|
2014-08-28 16:05:51 -04:00
|
|
|
for i in (length(rendered)+1):n
|
2014-06-23 02:10:35 -04:00
|
|
|
buffer[i] = 0.0
|
|
|
|
end
|
2014-08-28 16:05:51 -04:00
|
|
|
while Pa_GetStreamWriteAvailable(stream.stream) < n
|
|
|
|
sleep(0.005)
|
|
|
|
end
|
|
|
|
Pa_WriteStream(stream.stream, buffer, n)
|
2014-01-05 22:50:56 -05:00
|
|
|
end
|
2014-06-24 03:30:38 -04:00
|
|
|
catch ex
|
|
|
|
warn("Audio Task died with exception: $ex")
|
|
|
|
Base.show_backtrace(STDOUT, catch_backtrace())
|
2014-01-03 11:47:38 -08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-05-02 16:10:54 -04:00
|
|
|
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
|
|
|
|
|
2014-05-23 20:59:22 -04:00
|
|
|
type PortAudioInterface <: AudioInterface
|
2014-05-02 16:10:54 -04:00
|
|
|
name::String
|
|
|
|
host_api::String
|
|
|
|
max_input_channels::Int
|
|
|
|
max_output_channels::Int
|
|
|
|
end
|
|
|
|
|
|
|
|
function get_portaudio_devices()
|
2014-08-28 16:05:51 -04:00
|
|
|
require_portaudio_init()
|
2014-07-27 13:51:02 -04:00
|
|
|
device_count = ccall((:Pa_GetDeviceCount, libportaudio), PaDeviceIndex, ())
|
2014-08-28 16:05:51 -04:00
|
|
|
pa_devices = [Pa_GetDeviceInfo(i) for i in 0:(device_count - 1)]
|
2014-05-23 20:59:22 -04:00
|
|
|
[PortAudioInterface(bytestring(d.name),
|
2014-08-28 16:05:51 -04:00
|
|
|
bytestring(Pa_GetHostApiInfo(d.host_api).name),
|
2014-05-23 20:59:22 -04:00
|
|
|
d.max_input_channels,
|
|
|
|
d.max_output_channels)
|
2014-05-02 16:10:54 -04:00
|
|
|
for d in pa_devices]
|
|
|
|
end
|
|
|
|
|
2014-08-28 16:05:51 -04:00
|
|
|
function require_portaudio_init()
|
2014-05-02 16:10:54 -04:00
|
|
|
# can be called multiple times with no effect
|
|
|
|
global portaudio_inited
|
|
|
|
if !portaudio_inited
|
|
|
|
info("Initializing PortAudio. Expect errors as we scan devices")
|
2014-08-28 16:05:51 -04:00
|
|
|
Pa_Initialize()
|
2014-05-02 16:10:54 -04:00
|
|
|
portaudio_inited = true
|
|
|
|
end
|
|
|
|
end
|
2014-08-28 16:05:51 -04:00
|
|
|
|
|
|
|
# Low-level wrappers for Portaudio calls
|
|
|
|
Pa_GetDeviceInfo(i) = unsafe_load(ccall((:Pa_GetDeviceInfo, libportaudio),
|
|
|
|
Ptr{PaDeviceInfo}, (PaDeviceIndex,), i))
|
|
|
|
Pa_GetHostApiInfo(i) = unsafe_load(ccall((:Pa_GetHostApiInfo, libportaudio),
|
|
|
|
Ptr{PaHostApiInfo}, (PaHostApiIndex,), i))
|
|
|
|
|
|
|
|
function Pa_Initialize()
|
|
|
|
err = ccall((:Pa_Initialize, libportaudio), PaError, ())
|
|
|
|
handle_status(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_Terminate()
|
|
|
|
err = ccall((:Pa_Terminate, libportaudio), PaError, ())
|
|
|
|
handle_status(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_StartStream(stream::PaStream)
|
|
|
|
err = ccall((:Pa_StartStream, libportaudio), PaError,
|
|
|
|
(PaStream,), stream)
|
|
|
|
handle_status(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_StopStream(stream::PaStream)
|
|
|
|
err = ccall((:Pa_StopStream, libportaudio), PaError,
|
|
|
|
(PaStream,), stream)
|
|
|
|
handle_status(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_CloseStream(stream::PaStream)
|
|
|
|
err = ccall((:Pa_CloseStream, libportaudio), PaError,
|
|
|
|
(PaStream,), stream)
|
|
|
|
handle_status(err)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_GetStreamReadAvailable(stream::PaStream)
|
|
|
|
avail = ccall((:Pa_GetStreamReadAvailable, libportaudio), Clong,
|
|
|
|
(PaStream,), stream)
|
|
|
|
avail >= 0 || handle_status(avail)
|
|
|
|
avail
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_GetStreamWriteAvailable(stream::PaStream)
|
|
|
|
avail = ccall((:Pa_GetStreamWriteAvailable, libportaudio), Clong,
|
|
|
|
(PaStream,), stream)
|
|
|
|
avail >= 0 || handle_status(avail)
|
|
|
|
avail
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_ReadStream(stream::PaStream, buf::Array, frames::Integer=length(buf))
|
|
|
|
frames <= length(buf) || error("Need a buffer at least $frames long")
|
|
|
|
err = ccall((:Pa_ReadStream, libportaudio), PaError,
|
|
|
|
(PaStream, Ptr{Void}, Culong),
|
|
|
|
stream, buf, frames)
|
|
|
|
handle_status(err)
|
|
|
|
buf
|
|
|
|
end
|
|
|
|
|
|
|
|
function Pa_WriteStream(stream::PaStream, buf::Array, frames::Integer=length(buf))
|
|
|
|
frames <= length(buf) || error("Need a buffer at least $frames long")
|
|
|
|
err = ccall((:Pa_WriteStream, libportaudio), PaError,
|
|
|
|
(PaStream, Ptr{Void}, Culong),
|
|
|
|
stream, buf, frames)
|
|
|
|
handle_status(err)
|
|
|
|
nothing
|
|
|
|
end
|
|
|
|
|
|
|
|
Pa_GetVersion() = ccall((:Pa_GetVersion, libportaudio), Cint, ())
|
|
|
|
|
|
|
|
function Pa_OpenDefaultStream(inChannels::Integer, outChannels::Integer,
|
|
|
|
sampleFormat::PaSampleFormat,
|
|
|
|
sampleRate::Real, framesPerBuffer::Integer)
|
|
|
|
streamPtr::Array{PaStream} = PaStream[0]
|
|
|
|
err = ccall((:Pa_OpenDefaultStream, libportaudio),
|
|
|
|
PaError, (Ptr{PaStream}, Cint, Cint,
|
|
|
|
PaSampleFormat, Cdouble, Culong,
|
|
|
|
Ptr{PaStreamCallback}, Ptr{Void}),
|
|
|
|
streamPtr, inChannels, outChannels, sampleFormat, sampleRate,
|
|
|
|
framesPerBuffer, 0, 0)
|
|
|
|
handle_status(err)
|
|
|
|
|
|
|
|
streamPtr[1]
|
|
|
|
end
|
|
|
|
|
|
|
|
function handle_status(err::PaError)
|
|
|
|
if err == PA_OUTPUT_UNDERFLOWED || err == PA_INPUT_OVERFLOWED
|
|
|
|
msg = ccall((:Pa_GetErrorText, libportaudio),
|
|
|
|
Ptr{Cchar}, (PaError,), err)
|
|
|
|
warn("libportaudio: " * bytestring(msg))
|
|
|
|
elseif err != PA_NO_ERROR
|
|
|
|
msg = ccall((:Pa_GetErrorText, libportaudio),
|
|
|
|
Ptr{Cchar}, (PaError,), err)
|
|
|
|
error("libportaudio: " * bytestring(msg))
|
|
|
|
end
|
|
|
|
end
|