diff --git a/README.md b/README.md index e19230e..1175182 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,8 @@ 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 +With the method PortAudioStream, which uses the default input and output device(s), +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 @@ -114,6 +115,22 @@ 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. +When a Pa_AudioStream is created via a call to +Pa_AudioStream(device_index, channels=2, input=false, + sample_rate::Integer=44100, + framesPerBuffer::Integer=2048, + show_warnings::Bool=false, + sample_format::PaSampleFormat=paInt16, + callback=0) +a single stream is opened for reading or writing. This returns a Pa_AudioStream on success, +which is used either via the functions AudioIO.read(::Pa_AudioStream) or +AudioIO.write(::Pa_AudioStream, buffer) for read and write respectively. + +If nonblocking I/O is needed, a callback may be passed to the Pa_AudioStream constructor, +which is provided by the user and should return a buffer of frames if used for output and should +handle a buffer of frames as an argument if used as input. This is similar +to the methods used by PyAudio's python interface to PortAudio. See the examples also. + Installation ------------ diff --git a/REQUIRE b/REQUIRE index a613e52..754bd86 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,4 +1,5 @@ julia 0.3- BinDeps +Compat @osx Homebrew @windows WinRPM diff --git a/examples/callback_bands.jl b/examples/callback_bands.jl new file mode 100644 index 0000000..e770720 --- /dev/null +++ b/examples/callback_bands.jl @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Examples for Julia AudioIO module + +# display relative (not absolute) dB of bass, mid, treble bands + +using AudioIO, DSP + + +""" +Custom callback for read, processes passed data array +""" +function process_read(buf) + fchunk = map(Float64, buf) + pgram = welch_pgram(fchunk, 4096, 0, fs=SRATE*CHANNELS) + pxx = power(pgram) + frqs = freq(pgram) + bass = log(sum(abs(pxx[(frqs .<= 640.0) & (frqs .> 100.0)]))) + midrange = log(sum(abs(pxx[(frqs .<= 5120.0) & (frqs .> 640.0)]))) + treble = log(sum(abs(pxx[(frqs .<= 20480.0) & (frqs .> 5120.0)]))) + println("bass $bass, midrange $midrange, treble $treble") +end + +CHUNKSIZE = 40960 +FORMAT = AudioIO.paInt16 +CHANNELS = Cint(2) +SRATE = 44100 +istream = AudioIO.open_read(Cint(0), CHANNELS, SRATE, CHUNKSIZE, + false, FORMAT, process_read) +println("Starting callback type reading, control-C to stop") + +while true + sleep(20) +end diff --git a/examples/callback_read_write.jl b/examples/callback_read_write.jl new file mode 100644 index 0000000..ca7fa42 --- /dev/null +++ b/examples/callback_read_write.jl @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- + +# Examples for Julia AudioIO module + +using AudioIO + +CHUNKSIZE = 40960 +FORMAT = AudioIO.paInt16 +CALLBACK_DATATYPE = AudioIO.PaSampleFormat_to_T(FORMAT) +CHANNELS = 2 +SRATE = 44100 +RECORD_SECONDS = 3 + +BUFFER = zeros(Int16, SRATE * RECORD_SECONDS * 4) +BUFSIZE = SRATE * RECORD_SECONDS + +""" +choose the devices for 2 channel IO +""" +function choose_input_output() + devices = get_audio_devices() + indev = -1 + outdev = -1 + for aud in devices + println("$(aud.device_index) $(aud.name)") + if (aud.max_input_channels == CHANNELS) & (indev == -1 ) + indev = aud.device_index + end + if(aud.max_output_channels == CHANNELS) & (outdev == -1) + outdev = aud.device_index + end + end + if indev == -1 + info("Appropriate input device not found.") + elseif outdev == -1 + info("Appropriate output device not found.") + else + info("Using input device ", bytestring(devices[indev + 1].name), + ", number ", devices[indev + 1].device_index, + " and output device ", bytestring(devices[outdev + 1].name), + ", number ", devices[outdev + 1].device_index) + end + return indev, outdev +end + +read_position = 0 +""" +Custom callback for read, processes passed data array +""" +function store_read(buf) + read_size = length(buf) + global read_position + if read_position + read_size > length(BUFFER) + read_size = length(BUFFER) - read_position + end + if read_size > 0 + BUFFER[read_position + 1 : read_position + read_size] = + buf[1: read_size] + read_position += read_size + end +end + +write_position = 0 +""" +Custom callback for write, returns data array +""" +function get_writeable() + write_size = CHUNKSIZE * CHANNELS + global write_position + if write_position + write_size > length(BUFFER) + write_position = length(BUFFER) - write_size + retval = 1 + end + start_position = write_position + 1 + write_position += write_size + buf = BUFFER[start_position: write_position] + buf +end + +INS, OUTS = choose_input_output() + +istream = AudioIO.open_read(INS, CHANNELS, SRATE, CHUNKSIZE, + false, FORMAT, store_read) +println("Started callback type reading device number $INS") +sleep(5) +AudioIO.Pa_CloseStream(istream.stream) + +ostream = AudioIO.open_write(OUTS, CHANNELS, SRATE, CHUNKSIZE, + false, FORMAT, get_writeable) +println("Started callback type writing device number $OUTS") +sleep(3) +AudioIO.Pa_CloseStream(ostream.stream) diff --git a/examples/read_and_write.jl b/examples/read_and_write.jl new file mode 100644 index 0000000..5f27098 --- /dev/null +++ b/examples/read_and_write.jl @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- + +# Examples for Julia AudioIO module + +using AudioIO + +CHUNKSIZE = 40960 +FORMAT = AudioIO.paInt16 +CHANNELS = 2 +SRATE = 44100 +RECORD_SECONDS = 3 + +BUFFER = zeros(Int16, SRATE * RECORD_SECONDS * 4) +BUFSIZE = SRATE * RECORD_SECONDS + +""" +choose the devices for 2 channel IO +""" +function choose_input_output() + devices = get_audio_devices() + indev = -1 + outdev = -1 + for aud in devices + println("$(aud.device_index) $(aud.name)") + if (aud.max_input_channels == CHANNELS) & (indev == -1 ) + indev = aud.device_index + end + if(aud.max_output_channels == CHANNELS) & (outdev == -1) + outdev = aud.device_index + end + end + if indev == -1 + info("Appropriate input device not found.") + elseif outdev == -1 + info("Appropriate output device not found.") + else + info("Using input device ", bytestring(devices[indev + 1].name), + ", number ", devices[indev + 1].device_index, + " and output device ", bytestring(devices[outdev + 1].name), + ", number ", devices[outdev + 1].device_index) + end + return indev, outdev +end + +""" +read from input +""" +function read_blocking(devnum, buffer) + instream = AudioIO.open_read(devnum, CHANNELS, SRATE, CHUNKSIZE) + buf = AudioIO.read(instream, BUFSIZE) + buflen = length(buf) + buffer[1: buflen] = buf[1: buflen] + AudioIO.Pa_CloseStream(instream.stream) +end + +""" +write to output +""" +function write_blocking(devnum, buffer) + outstream = AudioIO.open_write(devnum, CHANNELS, SRATE, CHUNKSIZE) + AudioIO.write(outstream, buffer) + AudioIO.Pa_CloseStream(outstream.stream) +end + +""" +Create a string of numbers representing a sinewave audio tone +""" +function make_note_buffer(frequency, amplitude, duration, srate) + N = round(Int, srate / frequency) + T = round(Int, frequency * duration) # repeat for T cycles + dt = 1.0 / srate + tone = zeros(Int16, (N + N) * T) + idx = 1 + while idx < (N + N) * T + tone[idx] = round(Int, amplitude * sin(2 * pi * frequency * + idx * dt) * 32767.0) + tone[idx + 1] = tone[idx] + idx += 2 + end + tone +end + +""" +Write a note to output device +""" +function play_note(frequency, amplitude, duration, srate, ostream) + note = make_note_buffer(frequency, amplitude, duration, srate) + AudioIO.write(ostream, note) +end + +INS, OUTS = choose_input_output() + +read_blocking(INS, BUFFER) +println("Finished blocking type reading device number $INS") +println("Recording volume is $(mean(abs(BUFFER))*(100/16783))% of max") +sleep(2) + +write_blocking(OUTS, BUFFER) +println("Finished blocking type writing device number $OUTS") + +outstream = AudioIO.open_write(OUTS, CHANNELS, SRATE, CHUNKSIZE) +# play the C major scale +scale = [130.8, 146.8, 164.8, 174.6, 195.0, 220.0, 246.9, 261.6] +for note in scale + play_note(note, 0.1, 0.75, SRATE, outstream) +end +# up an octave +for note in scale[2:8] + play_note(2*note, 0.1, 0.75, SRATE, outstream) +end diff --git a/examples/record_to_wav.jl b/examples/record_to_wav.jl new file mode 100644 index 0000000..3d9bde8 --- /dev/null +++ b/examples/record_to_wav.jl @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Examples for Julia AudioIO module + +using AudioIO +using WAV + +CHUNKSIZE = 40960 +FORMAT = AudioIO.paInt16 +CHANNELS = 2 +SRATE = 44100 +RECORD_SECONDS = 20 + +""" +choose the devices for 2 channel IO +""" +function choose_input_output() + devices = get_audio_devices() + indev = -1 + outdev = -1 + for aud in devices + println("$(aud.device_index) $(aud.name)") + if (aud.max_input_channels == CHANNELS) & (indev == -1 ) + indev = aud.device_index + end + if(aud.max_output_channels == CHANNELS) & (outdev == -1) + outdev = aud.device_index + end + end + if indev == -1 + info("Appropriate input device not found.") + elseif outdev == -1 + info("Appropriate output device not found.") + else + info("Using input device ", bytestring(devices[indev + 1].name), + ", number ", devices[indev + 1].device_index, + " and output device ", bytestring(devices[outdev + 1].name), + ", number ", devices[outdev + 1].device_index) + end + return indev, outdev +end + +""" +read from input +""" +function record_audio(devnum, seconds) + instream = AudioIO.open_read(devnum, CHANNELS, SRATE, CHUNKSIZE) + bufsize = seconds * SRATE + buf = AudioIO.read(instream, bufsize) + AudioIO.Pa_CloseStream(instream.stream) + buf +end + +""" +write to WAV file +""" +function write_as_WAV(buffer, filename="temp.WAV") + fio = open(filename, "w") + WAV.wavwrite(deinterlace_stereo(buffer), fio, Fs=SRATE) +end + +""" +interlaced to noninterlaced stereo data +""" +function deinterlace_stereo(buffer) + reshape([buffer[1:2:end]; buffer[2:2:end]], + (floor(Int, length(buffer)/2), 2)) +end + +INS, OUTS = choose_input_output() + +println("Starting recording...") +BUF = record_audio(INS, RECORD_SECONDS) +println("Finished reading from device number $INS") +println("Recording volume was $(mean(abs(BUF))*(100/16783))% of max") + +write_as_WAV(BUF) +println("Finished writing to WAV file.") diff --git a/src/AudioIO.jl b/src/AudioIO.jl index 644ad8f..9256f60 100644 --- a/src/AudioIO.jl +++ b/src/AudioIO.jl @@ -1,7 +1,9 @@ module AudioIO +using Compat +importall Base.Operators # export the basic API -export play, stop, get_audio_devices +export play, stop, get_audio_devices, open_read, open_write, read, write # default stream used when none is given _stream = nothing diff --git a/src/nodes.jl b/src/nodes.jl index daca2cd..0f142b1 100644 --- a/src/nodes.jl +++ b/src/nodes.jl @@ -13,7 +13,7 @@ end # Generates a sin tone at the given frequency -type SinOscRenderer{T<:Union(Float32, AudioNode)} <: AudioRenderer +@compat type SinOscRenderer{T<:Union{Float32, AudioNode}} <: AudioRenderer freq::T phase::Float32 buf::AudioBuf @@ -124,7 +124,7 @@ end Base.push!(mixer::AudioMixer, node::AudioNode) = push!(mixer.renderer.inputs, node) #### Gain #### -type GainRenderer{T<:Union(Float32, AudioNode)} <: AudioRenderer +@compat type GainRenderer{T<:Union{Float32, AudioNode}} <: AudioRenderer in1::AudioNode in2::T buf::AudioBuf @@ -228,7 +228,7 @@ function play(arr::AudioBuf, args...) end # If the array is the wrong floating type, convert it -function play{T <: FloatingPoint}(arr::Array{T}, args...) +@compat function play{T <: AbstractFloat}(arr::Array{T}, args...) arr = convert(AudioBuf, arr) play(arr, args...) end diff --git a/src/portaudio.jl b/src/portaudio.jl index 6b081bb..72e2ccd 100644 --- a/src/portaudio.jl +++ b/src/portaudio.jl @@ -1,14 +1,15 @@ 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 +# 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 @@ -22,7 +23,7 @@ const paInt8 = convert(PaSampleFormat, 0x10) const paUInt8 = convert(PaSampleFormat, 0x20) # PaHostApiTypeId values -const pa_host_api_names = { +@compat const pa_host_api_names = ( 0 => "In Development", # use while developing support for a new host API 1 => "Direct Sound", 2 => "MME", @@ -37,7 +38,7 @@ const pa_host_api_names = { 12 => "Jack", 13 => "WASAPI", 14 => "AudioScience HPI" -} +) # track whether we've already inited PortAudio portaudio_inited = false @@ -81,6 +82,203 @@ function destroy(stream::PortAudioStream) portaudio_inited = false end +type CCallbackTimeInfo + inAdc::Cdouble + current::Cdouble + outAdc::Cdouble +end + +typealias PaStreamCallbackFlags Culong + +type CCallbackUdata + func::Function + sample_format::PaSampleFormat + channels::Cint + is_read::Bool +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 input or default output + see http://portaudio.com/docs/v19-doxydocs/portaudio_8h.html +""" +function Pa_OpenStream(device::PaDeviceIndex, channels::Integer, + input::Bool, sampleFormat::PaSampleFormat, + sampleRate::Cdouble, framesPerBuffer::Culong, + callback, udata) + streamPtr::Array{PaStream} = PaStream[1] + ioParameters = Pa_StreamParameters(device, Cint(channels), + sampleFormat, PaTime(0.001), + Ptr{Void}(0)) + streamcallback = Ptr{PaStreamCallback}(callback) + 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, + streamcallback, Ptr{Void}(udata)) + 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, + streamcallback, Ptr{Void}(udata)) + end + handle_status(err) + streamPtr[1] +end + +type Pa_AudioStream <: AudioStream + root::AudioMixer + info::DeviceInfo + show_warnings::Bool + stream::PaStream + sformat::PaSampleFormat + channels::Integer + framesPerBuffer::Integer + + """ + Get device parameters needed for opening with portaudio + default is output as 44100/16bit int, same as CD audio type input + callback is optional, otherwise use blocing read and write functions + the callback must be a function that, if for writing, can return as an argument + a buffer the size and type same as stream.sbuffer, or if for output should + """ + function Pa_AudioStream(device_index, channels=2, input=false, + sample_rate::Integer=44100, + framesPerBuffer::Integer=2048, + show_warnings::Bool=false, + sample_format::PaSampleFormat=paInt16, + callback=0, udata=0) + require_portaudio_init() + if (callback != 0) & (udata == 0) + # need to make a callback udata package + udata = CCallbackUdata(callback, sample_format, channels, input) + callback = pa_callback + end + stream = Pa_OpenStream(device_index, channels, input, sample_format, + Cdouble(sample_rate), Culong(framesPerBuffer), + callback, pointer_from_objref(udata)) + Pa_StartStream(stream) + root = AudioMixer() + this = new(root, DeviceInfo(sample_rate, framesPerBuffer), + show_warnings, stream, sample_format, + channels, framesPerBuffer) + end +end + +function open_read(device_index, channels=2, + sample_rate::Integer=44100, + framesPerBuffer::Integer=2048, + show_warnings::Bool=false, + sample_format::PaSampleFormat=paInt16, + callback=0) + Pa_AudioStream(device_index, channels, true, sample_rate, + framesPerBuffer, show_warnings, + sample_format, callback) +end + +function open_write(device_index, channels=2, + sample_rate::Integer=44100, + framesPerBuffer::Integer=2048, + show_warnings::Bool=false, + sample_format::PaSampleFormat=paInt16, + callback=0) + Pa_AudioStream(device_index, channels, false, sample_rate, + framesPerBuffer, show_warnings, + sample_format, callback) +end + +""" +Blocking read from a Pa_AudioStream that is open as input +""" +function read(stream::Pa_AudioStream, nframes::Int) + datatype = PaSampleFormat_to_T(stream.sformat) + buf = zeros(datatype, stream.framesPerBuffer * stream.channels) + return_buffer = zeros(datatype, nframes * stream.channels) + cur = 0 + read_needed = nframes + while read_needed > 0 + read_size = min(read_needed, stream.framesPerBuffer) + used = read_size * stream.channels + while Pa_GetStreamReadAvailable(stream.stream) < read_size + sleep(0.001) + yield() + end + err = ccall((:Pa_ReadStream, libportaudio), PaError, + (PaStream, Ptr{Void}, Culong), + stream.stream, buf, read_size) + handle_status(err, stream.show_warnings) + return_buffer[cur + 1: cur + used] = buf[1:used] + cur = cur + used + read_needed -= read_size + end + return_buffer +end + +""" +Blocking write to a Pa_AudioStream that is open for output +""" +function write(ostream::Pa_AudioStream, buffer) + cur = 0 + write_needed = length(buffer) + while write_needed > 0 + write_size = min(write_needed, ostream.framesPerBuffer) + while Pa_GetStreamWriteAvailable(ostream.stream) < write_size + sleep(0.001) + yield() + end + end_cur = cur + write_size + err = ccall((:Pa_WriteStream, libportaudio), PaError, + (PaStream, Ptr{Void}, Culong), ostream.stream, + buffer[cur + 1: end_cur], + Culong(write_size/ostream.channels)) + handle_status(err, ostream.show_warnings) + write_needed -= write_size + cur = end_cur + end + 0 +end + +""" +Take the c callback, restore types, pass to the callback functions +""" +function callback_wrapper(input::Ptr{Void}, output::Ptr{Void}, + frameCount::Culong, + timeInfo::Ptr{AudioIO.CCallbackTimeInfo}, + sflags::Culong, udata::Ptr{Void}) + ("$udata") # workaround, get julia to prep for the dereference + cudata = unsafe_pointer_to_objref(Ptr{CCallbackUdata}(udata)) + datatype = PaSampleFormat_to_T(cudata.sample_format) + if cudata.is_read + indata = pointer_to_array(Ptr{datatype}(input), + (frameCount * cudata.channels, )) + cudata.func(indata) + else + outdata = (cudata.func)() + ccall(:memcpy, Ptr{Void}, (Ptr{Void}, Ptr{Void}, Cint), + output, outdata, length(outdata) * sizeof(outdata[1])) + end + Cint(0) +end + +const pa_callback = cfunction(callback_wrapper, Cint, (Ptr{Void}, + Ptr{Void}, + Culong, Ptr{AudioIO.CCallbackTimeInfo}, + Culong, Ptr{Void})) + ############ Internal Functions ############ function portaudio_task(stream::PortAudioStream) @@ -112,6 +310,30 @@ function portaudio_task(stream::PortAudioStream) 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 + type PaDeviceInfo struct_version::Cint name::Ptr{Cchar} @@ -134,21 +356,23 @@ type PaHostApiInfo defaultOutputDevice::PaDeviceIndex end -type PortAudioInterface <: AudioInterface - name::String - host_api::String +@compat 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) for i in 0:(device_count - 1)] - [PortAudioInterface(bytestring(d.name), - bytestring(Pa_GetHostApiInfo(d.host_api).name), - d.max_input_channels, - d.max_output_channels) + 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 diff --git a/src/sndfile.jl b/src/sndfile.jl index b8ac331..66fd589 100644 --- a/src/sndfile.jl +++ b/src/sndfile.jl @@ -1,7 +1,7 @@ export af_open, FilePlayer, rewind, samplerate -const SFM_READ = int32(0x10) -const SFM_WRITE = int32(0x20) +@compat const SFM_READ = Int32(0x10) +@compat const SFM_WRITE = Int32(0x20) const SF_FORMAT_WAV = 0x010000 const SF_FORMAT_FLAC = 0x170000 @@ -16,10 +16,10 @@ const SF_SEEK_SET = 0 const SF_SEEK_CUR = 1 const SF_SEEK_END = 2 -const EXT_TO_FORMAT = [ +@compat const EXT_TO_FORMAT = ( ".wav" => SF_FORMAT_WAV, ".flac" => SF_FORMAT_FLAC -] +) type SF_INFO frames::Int64 @@ -45,7 +45,7 @@ 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::String, mode::String = "r", +@compat function open(path::AbstractString, mode::AbstractString = "r", sampleRate::Integer = 44100, channels::Integer = 1, format::Integer = 0) @assert channels <= 2 @@ -180,7 +180,7 @@ end typealias FilePlayer AudioNode{FileRenderer} FilePlayer(file::AudioFile) = FilePlayer(FileRenderer(file)) -FilePlayer(path::String) = FilePlayer(AudioIO.open(path)) +@compat FilePlayer(path::AbstractString) = FilePlayer(AudioIO.open(path)) function render(node::FileRenderer, device_input::AudioBuf, info::DeviceInfo) @assert node.file.sfinfo.samplerate == info.sample_rate @@ -204,7 +204,7 @@ function render(node::FileRenderer, device_input::AudioBuf, info::DeviceInfo) end end -function play(filename::String, args...) +@compat function play(filename::AbstractString, args...) player = FilePlayer(filename) play(player, args...) end