Use portaudio library in Unity

I am looking for a solution that allows to record specific microphone channels in Unity. Sadly, the Unity API is limited in this respect.

Thus, I wonder if it would be possible to create a Unity plugin for portaudio, which is “a portable audio I/O library designed for cross-platform support of audio”, where cross-platform refers to Windows, macOS, Linux.

There is also some C# binding for portaudio on GitHub.

Besides this, I found a plugin to use JACK audio in Unity. Sadly, this does not have a fitting license for my project.

Questions:

  • Do you think a Unity plugin for portaudio could work or are there any insurmountable obstacles?
  • Will it be possible to map from Unity recording device to portaudio recording device?
  • Do you maybe have a better solution to record specific mic channels in Unity?

Do you think a Unity plugin for portaudio could work

Yes, it seems to work in general.
For example, I am able to print the devices using portaudio-sharp.
Nonetheless, it is a bummer that Unity does not support recording specific mic channels by default.

Sadly, the first PortAudio C# binding (atsushieno/portaudio-sharp) did not record any samples for me. Maybe I did something wrong.

Anyway, now I am using this other PortAudio C# binding successfully to record mic data: GitHub - tlove123/portaudiosharp: Automatically exported from code.google.com/p/portaudiosharp

Note that you only need the files PortAudio.cs and Audio.cs from this lib.
The other files are WindowsForms app for demonstration that is not needed and will not work with Unity.

Example:

using System;
using System.Runtime.InteropServices;
using PortAudioSharp;
using UnityEngine;

// See the original portaudio example: http://portaudio.com/docs/v19-doxydocs/paex__record_8c_source.html
// and the used C# binding of portaudio: https://github.com/tlove123/portaudiosharp
public class PortAudioTest : MonoBehaviour
{
    private const int NumChannels = 1;
    private const int SampleRate = 44100;
    private const int NumSeconds = 4;
    private const int SamplesPerBuffer = 512;
    private const int SampleCount = SampleRate * NumSeconds;

    private static float minSample;
    private static float maxSample;

    private static long startTimeMillis;
    private static long stopTimeMillis;

    private static float[] recordedSamples;
    private static uint recordedSamplesIndex;

    public int targetFrameRate = 30;
    public AudioSource audioSource;

    private AudioClip audioClip;
    private Audio portAudioAudio;
    private bool isDone;

    private static PortAudio.PaStreamCallbackResult RecordCallback(
        IntPtr input,
        IntPtr output,
        uint samplesPerBuffer,
        ref PortAudio.PaStreamCallbackTimeInfo timeInfo,
        PortAudio.PaStreamCallbackFlags statusFlags,
        IntPtr localUserData)
    {
        Debug.Log($"RecordCallback - samplesPerBuffer: {samplesPerBuffer}");

        // Read samples from pointer to array
        for (int i = 0; i < samplesPerBuffer && (recordedSamplesIndex + i) < recordedSamples.Length; i++)
        {
            int offsetInArray = i * sizeof(float);
            float sample = Marshal.PtrToStructure<float>(input + offsetInArray);

            if (sample < minSample)
            {
                minSample = sample;
            }
            if (sample > maxSample)
            {
                maxSample = sample;
            }

            recordedSamples[recordedSamplesIndex + i] = sample;

            // Write samples to output array.
            // The audio is played only when the number of output channels has been specified in the Audio constructor.
            // Marshal.StructureToPtr<float>(sample, output + offsetInArray, false);
        }

        recordedSamplesIndex += samplesPerBuffer;
        if (recordedSamplesIndex > recordedSamples.Length)
        {
            recordedSamplesIndex = 0;
            stopTimeMillis = GetUnixTimeMilliseconds();
            return PortAudio.PaStreamCallbackResult.paComplete;
        }

        return PortAudio.PaStreamCallbackResult.paContinue;
    }

    private void Awake()
    {
        Application.targetFrameRate = targetFrameRate;
        recordedSamples = new float[SampleCount];
        minSample = 0;
        maxSample = 0;
        startTimeMillis = 0;
        stopTimeMillis = 0;
    }

    private void Start()
    {
        Debug.Log("Start");
        startTimeMillis = GetUnixTimeMilliseconds();

        Audio.LoggingEnabled = true;
        portAudioAudio = new Audio(
            NumChannels,
            -1, // Specify number of output channels if you want to output audio as well.
            SampleRate,
            SamplesPerBuffer,
            RecordCallback);

        portAudioAudio.Start();

        Debug.Log("Start done");
    }

    private void Update()
    {
        if (stopTimeMillis != 0
            && !isDone)
        {
            isDone = true;
            Debug.Log("Stopped");
            Debug.Log($"Recording duration: {(stopTimeMillis - startTimeMillis) / 1000} seconds");
            Debug.Log($"minSample: {minSample}, maxSample: {maxSample}");

            portAudioAudio.Stop();

            PlayRecordedAudio();
        }
    }

    private void PlayRecordedAudio()
    {
        audioClip = AudioClip.Create("PortAudioRecordedAudio", recordedSamples.Length, NumChannels, SampleRate, false);
        audioClip.SetData(recordedSamples, 0);

        audioSource.clip = audioClip;
        audioSource.Play();
    }

    private void OnDestroy()
    {
        if (portAudioAudio != null)
        {
            portAudioAudio.Stop();
            portAudioAudio.Dispose();
        }

        if (audioClip != null)
        {
            Destroy(audioClip);
        }
    }

    public static long GetUnixTimeMilliseconds()
    {
        // See https://stackoverflow.com/questions/4016483/get-time-in-milliseconds-using-c-sharp
        return DateTimeOffset.Now.ToUnixTimeMilliseconds();
    }
}