2014-01-03 07:41:00 -08:00
module AudioIO
2013-12-11 20:18:36 -05:00
2013-12-30 03:29:43 -08:00
# export the basic API
2013-12-21 20:39:47 -05:00
export play
2013-12-22 17:27:29 -05:00
2013-12-11 20:18:36 -05:00
typealias PaTime Cdouble
typealias PaError Cint
typealias PaSampleFormat Culong
typealias PaStream Void
2013-12-13 00:19:26 -05:00
const PA_NO_ERROR = 0
2013-12-11 20:18:36 -05:00
2013-12-21 20:39:47 -05:00
# default stream used when none is given
2013-12-22 16:45:16 -05:00
_stream = nothing
2013-12-21 20:39:47 -05:00
2013-12-22 16:45:16 -05:00
################## Types ####################
2013-12-22 17:16:53 -05:00
typealias AudioSample Float32
2013-12-22 16:45:16 -05:00
# A frame of audio, possibly multi-channel
2013-12-22 17:16:53 -05:00
typealias AudioBuf Array { AudioSample }
2013-12-22 16:45:16 -05:00
2013-12-22 18:06:02 -05:00
# A node in the render tree
abstract AudioNode
2013-12-22 16:45:16 -05:00
2013-12-30 01:00:04 -08:00
# A stream of audio (for instance that writes to hardware)
# All AudioStream subtypes should have a mixer and info field
abstract AudioStream
2013-12-22 16:45:16 -05:00
# Info about the hardware device
type DeviceInfo
sample_rate :: Integer
buf_size :: Integer
end
2013-12-22 18:06:02 -05:00
include ( " nodes.jl " )
2013-12-30 01:00:04 -08:00
type PortAudioStream <: AudioStream
2013-12-22 18:06:02 -05:00
mixer :: AudioMixer
2013-12-22 16:45:16 -05:00
info :: DeviceInfo
2013-12-30 01:00:04 -08:00
function PortAudioStream ( sample_rate , buf_size )
2013-12-22 18:06:02 -05:00
mixer = AudioMixer ( )
new ( mixer , DeviceInfo ( sample_rate , buf_size ) )
2013-12-22 16:45:16 -05:00
end
end
2013-12-21 03:20:39 -05:00
2013-12-13 02:17:21 -05:00
############ Exported Functions #############
2013-12-30 01:00:04 -08:00
# TODO: we should have "stop" functions that remove nodes from the render tree
# Play an AudioNode by adding it as an input to the root mixer node
2013-12-22 17:27:29 -05:00
function play ( node :: AudioNode , stream :: AudioStream )
2013-12-22 18:06:02 -05:00
# TODO: don't break demeter
2013-12-30 01:00:04 -08:00
append! ( stream . mixer . mix_inputs , [ node ] )
2013-12-22 18:13:57 -05:00
return nothing
2013-12-22 16:45:16 -05:00
end
2013-12-30 01:00:04 -08:00
# If the stream is not given, use the default global stream
2013-12-22 17:27:29 -05:00
function play ( node :: AudioNode )
global _stream
if _stream == nothing
2013-12-30 01:00:04 -08:00
_stream = open_portaudio_stream ( )
2013-12-22 17:27:29 -05:00
end
play ( node , _stream )
end
2013-12-30 01:00:04 -08:00
# Allow users to play a raw array by wrapping it in an ArrayPlayer
function play ( arr :: AudioBuf , args ... )
2013-12-22 16:45:16 -05:00
player = ArrayPlayer ( arr )
2013-12-30 01:00:04 -08:00
play ( player , args ... )
2013-12-21 20:39:47 -05:00
end
2013-12-30 01:00:04 -08:00
# If the array is the wrong floating type, convert it
function play { T <: FloatingPoint } ( arr :: Array { T } , args ... )
arr = convert ( AudioBuf , arr )
play ( arr , args ... )
end
# If the array is an integer type, scale to [-1, 1] floating point
# integer audio can be slightly (by 1) more negative than positive,
# so we just scale so that +/- typemax(T) becomes +/- 1
function play { T <: Signed } ( arr :: Array { T } , args ... )
arr = arr / typemax ( T )
play ( arr , args ... )
end
function play { T <: Unsigned } ( arr :: Array { T } , args ... )
zero = ( typemax ( T ) + 1 ) / 2
range = floor ( typemax ( T ) / 2 )
arr = ( arr - zero ) / range
play ( arr , args ... )
2013-12-21 20:39:47 -05:00
end
############ Internal Functions ############
2013-12-30 01:00:04 -08:00
function open_portaudio_stream ( sample_rate :: Int = 44100 , buf_size :: Int = 1024 )
2013-12-22 16:45:16 -05:00
# TODO: handle more streams
2013-12-21 20:39:47 -05:00
global _stream
2013-12-22 16:45:16 -05:00
if _stream != nothing
2013-12-21 20:39:47 -05:00
error ( " Currently only 1 stream is supported at a time " )
end
2013-12-13 02:17:21 -05:00
2013-12-22 16:45:16 -05:00
# TODO: when we support multiple streams we won't set _stream here.
# this is just to ensure that only one stream is ever opened
2013-12-30 01:00:04 -08:00
_stream = PortAudioStream ( sample_rate , buf_size )
2013-12-22 16:45:16 -05:00
2013-12-21 18:48:36 -05:00
fd = ccall ( ( :make_pipe , libportaudio_shim ) , Cint , ( ) )
2013-12-21 03:20:39 -05:00
info ( " Launching Audio Task... " )
2013-12-21 18:48:36 -05:00
function task_wrapper ( )
2013-12-22 16:45:16 -05:00
audio_task ( fd , _stream )
2013-12-21 18:48:36 -05:00
end
schedule ( Task ( task_wrapper ) )
2013-12-21 20:39:47 -05:00
# TODO: test not yielding here
2013-12-21 18:48:36 -05:00
yield ( )
info ( " Audio Task Yielded, starting the stream... " )
2013-12-21 20:39:47 -05:00
err = ccall ( ( :open_stream , libportaudio_shim ) , PaError ,
( Cuint , Cuint ) ,
sample_rate , buf_size )
handle_status ( err )
2013-12-21 18:48:36 -05:00
info ( " Portaudio stream started. " )
2013-12-13 02:17:21 -05:00
2013-12-22 16:45:16 -05:00
return _stream
2013-12-21 03:20:39 -05:00
end
function wake_callback_thread ( out_array )
ccall ( ( :wake_callback_thread , libportaudio_shim ) , Void ,
( Ptr { Void } , Cuint ) ,
out_array , size ( out_array , 1 ) )
end
2013-12-30 01:00:04 -08:00
function audio_task ( jl_filedesc :: Integer , stream :: PortAudioStream )
2013-12-21 03:20:39 -05:00
info ( " Audio Task Launched " )
2013-12-22 17:16:53 -05:00
in_array = zeros ( AudioSample , stream . info . buf_size )
2013-12-21 18:48:36 -05:00
desc_bytes = Cchar [ 0 ]
jl_stream = fdio ( jl_filedesc )
jl_rawfd = RawFD ( jl_filedesc )
2013-12-21 03:20:39 -05:00
while true
2013-12-22 18:06:02 -05:00
out_array = render ( stream . mixer , in_array , stream . info ) :: AudioBuf
2013-12-21 03:20:39 -05:00
# wake the C code so it knows we've given it some more data
wake_callback_thread ( out_array )
# wait for new data to be available from the sound card (and for it to
# have processed our last frame of data). At some point we should do
# something with the data we get from the callback
2013-12-21 18:48:36 -05:00
wait ( jl_rawfd , readable = true )
2013-12-22 18:13:57 -05:00
# read from the file descriptor so that it's empty. We're using ccall
# here because readbytes() was blocking the whole julia thread. This
# shouldn't block at all because we just waited on it
2013-12-22 11:49:05 -05:00
ccall ( :read , Clong , ( Cint , Ptr { Void } , Culong ) , jl_filedesc , desc_bytes , 1 )
2013-12-21 03:20:39 -05:00
end
end
2013-12-11 20:18:36 -05:00
function handle_status ( err :: PaError )
if err != PA_NO_ERROR
msg = ccall ( ( :Pa_GetErrorText , " libportaudio " ) ,
Ptr { Cchar } , ( PaError , ) , err )
2013-12-13 01:47:58 -05:00
error ( " libportaudio: " * bytestring ( msg ) )
2013-12-11 20:18:36 -05:00
end
end
2013-12-13 02:21:10 -05:00
function init_portaudio ( )
info ( " Initializing PortAudio. Expect errors as we scan devices " )
2013-12-11 20:18:36 -05:00
err = ccall ( ( :Pa_Initialize , " libportaudio " ) , PaError , ( ) )
handle_status ( err )
end
2013-12-13 02:17:21 -05:00
########### Module Initialization ##############
2013-12-11 20:18:36 -05:00
2013-12-13 02:17:21 -05:00
const libportaudio_shim = find_library ( [ " libportaudio_shim " , ] ,
2014-01-03 07:41:00 -08:00
[ Pkg . dir ( " AudioIO " , " deps " , " usr " , " lib " ) , ] )
2013-12-13 02:17:21 -05:00
2014-01-03 07:41:00 -08:00
@assert ( libportaudio_shim != " " , " Failed to find required library libportaudio_shim. Try re-running the package script using Pkg.build( \" AudioIO \" ), then reloading with reload( \" AudioIO \" ) " )
2013-12-13 02:17:21 -05:00
2013-12-13 02:21:10 -05:00
init_portaudio ( )
2013-12-11 20:18:36 -05:00
2014-01-03 07:41:00 -08:00
end # module AudioIO
2013-12-11 20:18:36 -05:00
2013-12-13 00:17:44 -05:00
#type PaStreamCallbackTimeInfo
# inputBufferAdcTime::PaTime
# currentTime::PaTime
# outputBufferDacTime::PaTime
#end
#
#typealias PaStreamCallbackFlags Culong
#
#
#function stream_callback{T}( input_::Ptr{T},
# output_::Ptr{T},
# frame_count::Culong,
# time_info::Ptr{PaStreamCallbackTimeInfo},
# status_flags::PaStreamCallbackFlags,
# user_data::Ptr{Void})
#
#
# println("stfl:$status_flags \tframe_count:$frame_count")
#
# ret = 0
# return convert(Cint,ret)::Cint #continue stream
#
#end
#
#T=Float32
#stream_callback_c = cfunction(stream_callback,Cint,
#(Ptr{T},Ptr{T},Culong,Ptr{PaStreamCallbackTimeInfo},PaStreamCallbackFlags,Ptr{Void})
#)
#stream_obj = Array(Ptr{PaStream},1)
#
#pa_err = ccall(
#(:Pa_Initialize,"libportaudio"),
#PaError,
#(),
#)
#
#println(get_error_text(pa_err))
#
#pa_err = ccall(
#(:Pa_OpenDefaultStream,"libportaudio"),
#PaError,
#(Ptr{Ptr{PaStream}},Cint,Cint,PaSampleFormat,Cdouble,Culong,Ptr{Void},Any),
#stream_obj,0,1,0x1,8000.0,4096,stream_callback_c,None
#)
#
#println(get_error_text(pa_err))
#
#function start_stream(stream)
# pa_err = ccall(
# (:Pa_StartStream,"libportaudio"),
# PaError,
# (Ptr{PaStream},),
# stream
# )
# println(get_error_text(pa_err))
#end
#
#end #module