Playing/recording signals with PortAudio.jl

Hi all,

I’m trying to use PortAudio.jl to stream and record audio signals.
What I try to achieve is playing a signal through a loudspeaker and record this same signal through a mic. Basically, I want to record what I’m playing through the loudspeaker.

Playback is ok, recording is ok, but I can’t figure out how to make it work synchronously (i.e. record what is played). I tried what @ssfr suggested here, but when I use an @async:

  • the sound played through the loudspeaker is “crackling”
  • the script exits before the sound is finished being played trough the loudspeaker

Here is an example:

using PortAudio: PortAudioStream
sampleRate = 44100 
# a 2 seconds sine at 440 Hz
signal     = sin.(2pi * 440 * (1:sampleRate*2)/sampleRate)
stream     = PortAudioStream(1,1)
@async write(stream, signal)
buffer = read(stream, length(signal))   

Using Julia 1.7.3 and PortAudio 1.3.0.

Any help is appreciated !
Thomas

2 Likes

Hi again,

Found some information here :

Seems that synced play/record is not fully implemented in PortAudio.jl (as far as I can understand…).
Couldn’t get the PortAudio.jl solution working, but a PyCall workaround works like a charm !
If anyone is interested :

using PyCall

sampleRate = 44100 
signal     = sin.(2pi*440*(1:sampleRate*2)/sampleRate)

py"""
import sounddevice as sd
def record(testsignal, fs):
    sd.default.samplerate = fs
    sd.default.dtype      = 'float64'
    # Start the recording
    recorded = sd.playrec(testsignal, samplerate=fs, channels=1)
    sd.wait()

    return recorded
"""

recorded = py"record"(signal, 44100) ;

Cheers!

3 Likes

Just for fun I’m going to paste some code here that I played with. It’s very rough but it uses the callback interface of PortAudio to do what you want. I give it an output array and an empty input array, and as long as there’s data in the output array it writes that to the output buffer and stores input buffer data in the recording array. I just think it’s cool that one can write c functions with Julia in this way, of course what I’ve written here is quite brittle in that when you introduce a bug in the c function it will usually just segfault, so it’s not the smoothest thing to work on. Also I’m just unsafe_converting my arrays without any attention to GC safety, which is just overall bad style, but I didn’t have more time. Anyway, here it goes (and it seems to work fine):

using PortAudio.LibPortAudio

mutable struct AudioData
    n::Int
    out::Ptr{Float32}
    in::Ptr{Float32}
    offset::Ptr{Cint}
end

function portaudio_callback(inputbuffer, outputbuffer, framecount, timeinfo, statusflags, userdata)::Cint

    audiodata = unsafe_load(Ptr{AudioData}(userdata))
    offset = unsafe_load(audiodata.offset)
    n = audiodata.n

    for i in 1:framecount
        ind = offset + i
        unsafe_store!(outputbuffer, ind > n ? 0f0 : unsafe_load(audiodata.out, ind), i)
        ind <= n && unsafe_store!(audiodata.in, unsafe_load(inputbuffer, i), ind)
    end

    new_offset = offset + framecount
    unsafe_store!(audiodata.offset, new_offset)

    return new_offset > n ? 1 : 0
end

cfunc = @cfunction portaudio_callback Cint (
    Ptr{Float32},
    Ptr{Float32},
    Culong,
    Ptr{LibPortAudio.PaStreamCallbackTimeInfo},
    LibPortAudio.PaStreamCallbackFlags,
    Ptr{Cvoid}
)

macro checkerr(exp)
    quote
        errcode = $(esc(exp))
        e = LibPortAudio.PaErrorCode(errcode)
        if e != LibPortAudio.paNoError
            error("PortAudio errored with status code $errcode ($(string(e)))")
        end
    end
end

@info "Initializing PortAudio"
@checkerr LibPortAudio.Pa_Initialize()
mutable_pointer = Ref{Ptr{LibPortAudio.PaStream}}(0)
n_in = 1
n_out = 1
samplerate = 44100
framecount = 256

signal = Float32.(sin.(2pi*440*(1:samplerate*2)/samplerate))
recording = similar(signal)
offset = Cint[0]
audiodata = AudioData(
    length(signal),
    Base.unsafe_convert(Ptr{Float32}, signal),
    Base.unsafe_convert(Ptr{Float32}, recording),
    Base.unsafe_convert(Ptr{Cint}, offset)
)

@checkerr LibPortAudio.Pa_OpenDefaultStream(
    mutable_pointer,
    n_in,
    n_out,
    LibPortAudio.paFloat32,
    samplerate,
    framecount,
    cfunc,
    Ref(audiodata),
)

pointer_to = mutable_pointer[]

try
    @info "Starting stream"
    @checkerr LibPortAudio.Pa_StartStream(pointer_to)
    # wait for the cfunction to return 1
    while LibPortAudio.Pa_IsStreamActive(pointer_to) == 1
        sleep(1/10)
    end
finally
    @info "Stopping stream"
    @checkerr LibPortAudio.Pa_StopStream(pointer_to)
    @info "Terminating PortAudio"
    @checkerr LibPortAudio.Pa_Terminate()
end

After you’ve run this you can do

PortAudioStream(0, 1; samplerate) do stream
    write(stream, recording)
end

to listen to the recording.

3 Likes

I tried the c function option before heading to Python. No doubt it works but to be honest it’s beyond my programming skills.
But thanks anyways, I’ll have a look at it.