// SPDX-License-Identifier: Unlicense

#include "sound.h"

#include "data.h"
#include "global.h"
#include "milk.h"
#include "pcm.h"
#include "recorder.h"
#include "settings.h"
#include "sfx.h"
#include "tunerThread.h"

#ifdef SHR3D_AUDIO_ASIO
#include "asio.h"
#endif // SHR3D_AUDIO_ASIO

#ifdef SHR3D_AUDIO_AAUDIO
#include <aaudio/AAudio.h>
#include <jni.h>
#endif // SHR3D_AUDIO_AAUDIO

#if defined(PLATFORM_WINDOWS) && defined(SHR3D_AUDIO_JACK) && not defined(SHR3D_AUDIO_JACK_NO_DLOPEN)
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
typedef HMODULE dlhandle;
#define dlopen(x, y) LoadLibrary(x)
#define dlsym(x, y) (void*)GetProcAddress(x, y)
#endif // PLATFORM_WINDOWS && SHR3D_AUDIO_JACK && !SHR3D_AUDIO_JACK_NO_DLOPEN

#if defined(PLATFORM_LINUX) && ((defined(SHR3D_AUDIO_JACK) && not defined(SHR3D_AUDIO_JACK_NO_DLOPEN)) || (defined(SHR3D_AUDIO_PIPEWIRE) && not defined(SHR3D_AUDIO_PIPEWIRE_NO_DLOPEN)))
#include <dlfcn.h>
#endif // PLATFORM_LINUX && ((SHR3D_AUDIO_JACK && !SHR3D_AUDIO_JACK_NO_DLOPEN) || (SHR3D_AUDIO_PIPEWIRE && !SHR3D_AUDIO_PIPEWIRE_NO_DLOPEN))

#if defined(SHR3D_AUDIO_JACK) && defined(SHR3D_AUDIO_JACK_NO_DLOPEN)
#include <jack/jack.h>
#endif // SHR3D_AUDIO_JACK && SHR3D_AUDIO_JACK_NO_DLOPEN

#ifdef SHR3D_AUDIO_PIPEWIRE
#include <pipewire/pipewire.h>
#include <spa/param/latency-utils.h>
#endif // SHR3D_AUDIO_PIPEWIRE

#ifdef SHR3D_AUDIO_SDL
#include <SDL.h>
#include <condition_variable>
#include <mutex>
#endif // SHR3D_AUDIO_SDL

#ifdef SHR3D_AUDIO_SUPERPOWERED
#include "../deps/Superpowered/Android/include/Superpowered.h"
#include "../deps/Superpowered/Android/include/SuperpoweredAndroidUSB.h"
#include "../deps/Superpowered/Android/include/SuperpoweredCPU.h"
#include <jni.h>
#endif // SHR3D_AUDIO_SUPERPOWERED

#ifdef SHR3D_AUDIO_WASAPI
#include <Audioclient.h>
#include <Mmdeviceapi.h>
#include <functiondiscoverykeys.h> // PKEY_Device_FriendlyName
#endif // SHR3D_AUDIO_WASAPI

#ifdef SHR3D_WINDOW_SDL
#include <SDL.h>
#include <SDL_syswm.h>
#endif // SHR3D_WINDOW_SDL

#ifdef SHR3D_AUDIO_WEBAUDIO
#include <emscripten/webaudio.h>
#endif // SHR3D_AUDIO_WEBAUDIO

//static void deinterleave(float* outData, const float* inData, const int blockSize)
//{
//  for (i32 x = 0; x != blockSize; ++x)
//  {
//    outData[x] = inData[x * 2];
//    outData[blockSize + x] = inData[x * 2 + 1];
//  }
//}

// channel: 0:left, 1:right
//static void deinterleaveFromMono(f32* vstStream, const f32* sdlStream, const i32 blockSize, const i32 channel, const f32 volume)
//{
//  for (i32 i = 0; i < blockSize; ++i)
//  {
//    vstStream[i] = sdlStream[i * 2 + channel] * volume;
//    vstStream[blockSize + i] = vstStream[i];
//  }
//}

static void deinterleaveFromMono(f32* vstStream, const f32* sdlStream, const i32 blockSize, const i32 channel, const f32 volume, const i32 channelStride = 2)
{
  for (i32 i = 0; i < blockSize; ++i)
  {
    vstStream[i] = sdlStream[i * channelStride + channel] * volume;
    vstStream[blockSize + i] = vstStream[i];
  }
}

static void interleave(f32* sdlStream, const f32* vstStream, const i32 blockSize)
{
  for (i32 i = 0; i < blockSize; ++i)
  {
    sdlStream[i * 2] = vstStream[i];
    sdlStream[i * 2 + 1] = vstStream[blockSize + i];
  }
}


static inline void mixAudio(f32* dst, const f32* src, const i32 len, const f32 volume)
{
  if (volume != 0.0f)
    for (i32 i = 0; i < len; ++i)
      dst[i] += src[i] * volume;
}

static i32 metronome(f32* bufferMono, const i32 blockSize, i32 sampleRate)
{
  static TimeNS lastMusicTimeElapsed = 0;

  if (Global::songTrackLevelAdjusted[Global::selectedArrangementIndex].anchors.size() > 0
    && Global::musicTimeElapsedNS >= Global::songTrackLevelAdjusted[Global::selectedArrangementIndex].anchors[0].timeNS)
  {
    const i32 clickLength = (Settings::metronomeClickLength * sampleRate) / 1000;

    static Song::Beat* foundBeat = nullptr;
    static i32 playedClickLength = 0;

    for (Song::Beat& beat : Global::songTracks[Global::selectedArrangementIndex].beats)
    {
      if (lastMusicTimeElapsed <= beat.timeNS && beat.timeNS < Global::musicTimeElapsedNS)
      {
        foundBeat = &beat;
        playedClickLength = 0;
        break;
      }
    }

    if (foundBeat != nullptr)
    {
      // make sinus beep that is gets damped to 0
      // formular is sin(feq * x) * (1-x) * (1-x)
      // played from x between 0.0 and 1.0

      const f32 frequency = (foundBeat->isPrimary ? Settings::metronomeFrequency0 : Settings::metronomeFrequency1) / sampleRate;

      const i32 remainingLenMono = clamp(clickLength - playedClickLength, 0, blockSize); // clamp negative length to 0. This can happen when user decreases click length while a click is playing.

      for (i32 i = 0; i < remainingLenMono; ++i)
      {
        const i32 progress = i + playedClickLength;

        f32 x = sin(f32(progress) * frequency);
        if (Settings::metronomeDecay)
        {
          const f32 damp0To1 = 1.0f - f32(progress) / f32(clickLength);
          x *= damp0To1 * damp0To1;
        }

        bufferMono[i] = x;
      }
      playedClickLength += remainingLenMono;

      if (playedClickLength >= clickLength)
      {
        playedClickLength = 0;
        foundBeat = nullptr;
      }

      lastMusicTimeElapsed = Global::musicTimeElapsedNS;
      return remainingLenMono;
    }
  }

  lastMusicTimeElapsed = Global::musicTimeElapsedNS;
  return 0;
}

#ifdef SHR3D_SFX
static bool sfxProcessBlock(f32* inBlock[2], f32* outBlock[2], const i32 blockSize, SfxChainEffect effectChain[16])
{
  bool isProcessedDataInInBlock = true;

#ifdef SHR3D_SFX_CORE_HEXFIN
  if (Global::tunerPlugin.system != SfxSystem::empty && Global::tunerPlugin.sfxIndex != to_underlying_(SfxCore::Hexfin) && Global::inputTuner.toggled) // TODO: move the tuner out. it should be moved out into a separate thread since it will not modify the audio.
  {
    const ProcessBlockResult processedIntoOutBlock = Sfx::processBlock(Global::tunerPlugin, 0, inBlock, outBlock, blockSize);

    if (processedIntoOutBlock == ProcessBlockResult::ProcessedInOutBlock)
      isProcessedDataInInBlock = !isProcessedDataInInBlock;
  }
#endif // SHR3D_SFX_CORE_HEXFIN

  if (Global::tunerMidiPlugin.system != SfxSystem::empty)
  {
    Sfx::processBlock(Global::tunerMidiPlugin, 0, inBlock, outBlock, blockSize);
    //const bool processedIntoOutBlock = Sfx::processBlock(Global::tunerMidiPlugin, 0, inBlock, outBlock, blockSize);
    //if (processedIntoOutBlock)
    //  isProcessedDataInInBlock = !isProcessedDataInInBlock;
  }

  for (i32 i = 0; i < ARRAY_SIZE(Global::effectChain); ++i)
  {
    if (effectChain[i].id.system == SfxSystem::empty || effectChain[i].state == SfxEffectState::muted)
      continue;

    i32 instance = 0;
    for (i32 j = 0; j < i; ++j)
      if (effectChain[i].id.system == effectChain[j].id.system && effectChain[i].id.sfxIndex == effectChain[j].id.sfxIndex)
        ++instance;

    const ProcessBlockResult processedIntoOutBlock = Sfx::processBlock(effectChain[i].id, instance,
      isProcessedDataInInBlock ? inBlock : outBlock,
      isProcessedDataInInBlock ? outBlock : inBlock,
      blockSize);

    if (processedIntoOutBlock == ProcessBlockResult::ProcessedInOutBlock)
      isProcessedDataInInBlock = !isProcessedDataInInBlock;
  }

  return isProcessedDataInInBlock;
}
#endif // SHR3D_SFX

static TimeNS exchangeMusicPlaybackSeekerTimeNS()
{
#ifndef PLATFORM_WINDOWS
  return __atomic_exchange_n(&Global::musicPlaybackSeekerTimeNS, 0, __ATOMIC_ACQUIRE);
#else // PLATFORM_WINDOWS
  return _InterlockedExchange64(&Global::musicPlaybackSeekerTimeNS, 0);
#endif // PLATFORM_WINDOWS
}

static void process_main(f32* inBlock[2], f32* outBlock[2], const i32 blockSize
#if defined(SHR3D_SFX) && defined(SHR3D_COOP)
  , f32* inCoopBlock[2]
#endif // SHR3D_SFX && SHR3D_COOP
)
{
  if (Settings::highwayVUMeter)
  {
    f32 inputVolumeMono = 0.0f;
    for (i32 i = 0; i < blockSize; ++i)
      inputVolumeMono = max_(inputVolumeMono, abs(inBlock[0][i]));
    Global::inputVolumeMono = inputVolumeMono;
  }

#ifndef PLATFORM_EMSCRIPTEN // TODO: the tuner crashes in emscripten. fix it
#ifdef SHR3D_SFX_CORE_HEXFIN
  memcpy(TunerThread::inBlock, inBlock[0], blockSize * sizeof(f32));
  TunerThread::tick();
#endif // SHR3D_SFX_CORE_HEXFIN
#endif // PLATFORM_EMSCRIPTEN

#ifdef SHR3D_SFX
  {
    // process the fx chain
    const bool isProcessedDataInInBlock = sfxProcessBlock(inBlock, outBlock, blockSize, Global::effectChain);
    if (!isProcessedDataInInBlock)
    {
      memcpy(inBlock[0], outBlock[0], blockSize * sizeof(f32));
      memcpy(inBlock[1], outBlock[1], blockSize * sizeof(f32));
    }
    for (i32 i = 0; i < blockSize; ++i)
      outBlock[0][i] = inBlock[0][i] * Settings::audioEffectVolume;
    for (i32 i = 0; i < blockSize; ++i)
      outBlock[1][i] = inBlock[1][i] * Settings::audioEffectVolume;
  }

  if (Settings::highwayVUMeter)
  {
    f32 effectVolumeLeft = 0.0f;
    f32 effectVolumeRight = 0.0f;
    for (i32 i = 0; i < blockSize; ++i)
    {
      effectVolumeLeft = max_(effectVolumeLeft, abs(outBlock[0][i]));
      effectVolumeRight = max_(effectVolumeRight, abs(outBlock[1][i]));
    }
    Global::effectVolumeLeft = effectVolumeLeft;
    Global::effectVolumeRight = effectVolumeRight;
  }

#ifdef SHR3D_COOP
  if (Global::inputCoop.toggled)
  {
    const bool isProcessedDataInInBlock = sfxProcessBlock(inCoopBlock, inBlock, blockSize, Global::effectChainCoop); // reuse inBlock. It was already mixed into outBlock.
    if (!isProcessedDataInInBlock)
    {
      memcpy(inCoopBlock[0], inBlock[0], blockSize * sizeof(f32));
      memcpy(inCoopBlock[1], inBlock[1], blockSize * sizeof(f32));
    }
    mixAudio(outBlock[0], inBlock[0], blockSize, Settings::audioEffectVolumeCoop);
    mixAudio(outBlock[1], inBlock[1], blockSize, Settings::audioEffectVolumeCoop);
  }
#endif // SHR3D_COOP
#else // SHR3D_SFX
  memset(outBlock[0], 0, blockSize * sizeof(f32));
  memset(outBlock[1], 0, blockSize * sizeof(f32));
#endif // SHR3D_SFX

  // process metronome and mix it.
  if (Settings::metronomeEnabled)
  {
    const i32 remainingLenMono = metronome(inBlock[0], blockSize, sampleRate()); // reuse inBlock. It was already mixed into outBlock.
    if (remainingLenMono > 0)
    {
      switch (Settings::metronomeSide)
      {
      case MetronomeSide::left:
        mixAudio(outBlock[0], inBlock[0], remainingLenMono, Settings::metronomeVolume);
        break;
      case MetronomeSide::right:
        mixAudio(outBlock[1], inBlock[0], remainingLenMono, Settings::metronomeVolume);
        break;
      case MetronomeSide::stereo:
        mixAudio(outBlock[0], inBlock[0], remainingLenMono, Settings::metronomeVolume);
        mixAudio(outBlock[1], inBlock[0], remainingLenMono, Settings::metronomeVolume);
        break;
      }
    }
  }

  { // song playback

    switch (Global::musicDoubleBufferStatus)
    {
    case MusicDoubleBufferStatus::DataInNonStreched1Delete0:
      free(Global::musicDoubleBuffer[0]);
      Global::musicDoubleBufferStatus = MusicDoubleBufferStatus::DataInNonStreched1;
#ifndef NDEBUG
      ASSERT(Global::musicDoubleBuffer[0] != nullptr);
      Global::musicDoubleBuffer[0] = nullptr;
#endif // NDEBUG
      break;
    case MusicDoubleBufferStatus::DataInNonStreched0Delete1:
      free(Global::musicDoubleBuffer[1]);
      Global::musicDoubleBufferStatus = MusicDoubleBufferStatus::DataInNonStreched0;
#ifndef NDEBUG
      ASSERT(Global::musicDoubleBuffer[1] != nullptr);
      Global::musicDoubleBuffer[1] = nullptr;
#endif // NDEBUG
      break;
    case MusicDoubleBufferStatus::inital:
      break;
    }

    if (Global::selectedSongIndex >= 0)
    {
      const TimeNS musicSeekNS = exchangeMusicPlaybackSeekerTimeNS();
      if (musicSeekNS != 0)
      {
        const TimeNS nextMusicTimeElapsedNS = max_(-Global::songInfos[Global::selectedSongIndex].shredDelayBegin, Global::musicTimeElapsedNS + musicSeekNS);
#ifdef SHR3D_MUSIC_STRETCHER
        Global::musicPlaybackPosition = i64(musicTimeElapsedToPlaybackPositionNonStretched(nextMusicTimeElapsedNS, f32(sampleRate())) * Global::musicStretchRatio);
#else // SHR3D_MUSIC_STRETCHER
        Global::musicPlaybackPosition = i64(TimeNS_To_Seconds(nextMusicTimeElapsedNS) * f32(sampleRate()));
#endif // SHR3D_MUSIC_STRETCHER
        Global::musicTimeElapsedNS = nextMusicTimeElapsedNS;
      }
    }

    if (Global::musicPlaybackPosition < Global::musicPlaybackLength)
    {
      const i64 remainingLength = Global::musicPlaybackLength - Global::musicPlaybackPosition;
      ASSERT(remainingLength > 0);
      const i32 filledBlockLen = i32(min_(i64(blockSize), remainingLength));
      if (Global::musicPlaybackPosition >= 0)
      {
        if (!Global::inputMute.toggled) // m key mutes the output
        {
          //#ifdef SHR3D_MUSICTHREAD
          //          const bool musicReady = Global::musicRingBuffer.read(inBlock, blockSize);
          //          ASSERT(musicReady);
          //          if (musicReady)
          //          {
          //            for (i32 i = 0; i < blockSize; ++i)
          //            {
          //              outBlock[0][i] += inBlock[0][i] * Settings::audioMusicVolume;
          //              outBlock[1][i] += inBlock[1][i] * Settings::audioMusicVolume;
          //            }
          //          }
          //#else // SHR3D_MUSICTHREAD
          for (i32 i = 0; i < filledBlockLen; ++i)
          {
            outBlock[0][i] += Global::musicPlaybackBufferLR[0][Global::musicPlaybackPosition + i] * Settings::audioMusicVolume;
            outBlock[1][i] += Global::musicPlaybackBufferLR[1][Global::musicPlaybackPosition + i] * Settings::audioMusicVolume;
          }
          //#endif // SHR3D_MUSICTHREAD
        }

#ifdef SHR3D_RECORDER
        if (Global::inputRecorder.toggled)
          Recorder::processBlock(outBlock, blockSize);
#endif // SHR3D_RECORDER

#ifdef SHR3D_ENVIRONMENT_MILK
        Milk::processAudioFrame(outBlock, blockSize);
#endif // SHR3D_ENVIRONMENT_MILK
      }
      Global::musicPlaybackPosition += filledBlockLen;
    }
  }

  if (Settings::highwayVUMeter)
  {
    f32 outputVolumeLeft = 0.0f;
    f32 outputVolumeRight = 0.0f;
    for (i32 i = 0; i < blockSize; ++i)
    {
      outputVolumeLeft = max_(outputVolumeLeft, abs(outBlock[0][i]));
      outputVolumeRight = max_(outputVolumeRight, abs(outBlock[1][i]));
    }
    Global::outputVolumeLeft = outputVolumeLeft;
    Global::outputVolumeRight = outputVolumeRight;
  }
}

#ifdef SHR3D_AUDIO_AAUDIO
namespace AAudio
{
  static f32 recordingToPlaybackBuffer[Const::audioMaximumPossibleBlockSize * 2];
  static std::condition_variable cv;
  static std::mutex mutex;
  static bool dataReady = true;

  static int deviceIdInput = AAUDIO_UNSPECIFIED;
  static bool initialized = false;

  static aaudio_data_callback_result_t audioCallbackInput(AAudioStream* stream, void* userData, void* audioData, i32 blockSize)
  {
    ASSERT(Global::androidRecordMicrophonePermissionGranted);

    {
      std::unique_lock lock(mutex);
      while (dataReady)
        cv.wait(lock);

      memcpy(recordingToPlaybackBuffer, audioData, 2 * blockSize * sizeof(f32));

      dataReady = true;
      cv.notify_one();
    }

    return AAUDIO_CALLBACK_RESULT_CONTINUE;
  }

  static aaudio_data_callback_result_t audioCallbackOutput(AAudioStream* stream, void* userData, void* audioData, i32 blockSize)
  {
    //const i32 blockSizeStereoInBytes = blockSize * sizeof(f32) * 2; // stereo
    f32* outputStream = reinterpret_cast<f32*>(audioData);
    //const i32 streamLen = blockSize * 2; // stereo

    if (Global::androidRecordMicrophonePermissionGranted)
    {
      std::unique_lock lock(mutex);
      while (!dataReady)
        cv.wait(lock);

      {
        deinterleaveFromMono(outputStream, recordingToPlaybackBuffer, blockSize, Settings::audioAAudioChannelInput, Settings::audioEffectVolume);
        f32 tempBuffer[Const::audioMaximumPossibleBlockSize * 2];
        f32* inStereo[] = { &outputStream[0], &outputStream[blockSize] };
        f32* outStereo[] = { &tempBuffer[0], &tempBuffer[blockSize] };
        process_main(inStereo, outStereo, blockSize);
        interleave(outputStream, tempBuffer, blockSize);
      }

      dataReady = false;
      cv.notify_one();
    }
    else
    {
      deinterleaveFromMono(outputStream, recordingToPlaybackBuffer, blockSize, Settings::audioAAudioChannelInput, Settings::audioEffectVolume);
      f32 tempBuffer[Const::audioMaximumPossibleBlockSize * 2];
      f32* inStereo[] = { &outputStream[0], &outputStream[blockSize] };
      f32* outStereo[] = { &tempBuffer[0], &tempBuffer[blockSize] };
      process_main(inStereo, outStereo, blockSize);
      interleave(outputStream, tempBuffer, blockSize);
    }

    return AAUDIO_CALLBACK_RESULT_CONTINUE;
  }

  static void initInput()
  {
    AAudioStreamBuilder* builder = nullptr;
    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
    ASSERT(result == AAUDIO_OK);

    AAudioStreamBuilder_setDeviceId(builder, deviceIdInput);
    AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_INPUT);
    AAudioStreamBuilder_setSharingMode(builder, Settings::audioAAudioExclusiveMode
      ? AAUDIO_SHARING_MODE_EXCLUSIVE
      : AAUDIO_SHARING_MODE_SHARED);
    AAudioStreamBuilder_setSampleRate(builder, Settings::audioAAudioSampleRate);
    AAudioStreamBuilder_setChannelCount(builder, 2);
    AAudioStreamBuilder_setFramesPerDataCallback(builder,
      Settings::audioAAudioBlockSize);
    AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT);
    AAudioStreamBuilder_setBufferCapacityInFrames(builder,
      Const::audioMaximumPossibleBlockSize * 2);
    const aaudio_performance_mode_t performanceMode = AAUDIO_PERFORMANCE_MODE_NONE +
      to_underlying_(
        Settings::audioAAudioPerformanceMode);
    AAudioStreamBuilder_setPerformanceMode(builder, performanceMode);
    AAudioStreamBuilder_setDataCallback(builder, AAudio::audioCallbackInput, nullptr);

    AAudioStream* stream = nullptr;
    result = AAudioStreamBuilder_openStream(builder, &stream);

    ASSERT(result == AAUDIO_OK); // if this fails the microphone permission is denied

    result = AAudioStreamBuilder_delete(builder);
    ASSERT(result == AAUDIO_OK);

    result = AAudioStream_requestStart(stream);
    ASSERT(result == AAUDIO_OK);
  }

  static const char* deviceTypeId2DeviceTypeName(int deviceTypeId)
  {
    static const char* deviceTypeNames[] =
    {
      "unknown",
      "builtin_earpiece",
      "builtin_speaker",
      "wired_headset",
      "wired_headphones",
      "line_analog",
      "line_digital",
      "bluetooth_sco",
      "bluetooth_a2dp",
      "hdmi",
      "hdmi_arc",
      "usb_device",
      "usb_accessory",
      "dock",
      "fm",
      "builtin_mic",
      "fm_tuner",
      "tv_tuner",
      "telephony",
      "aux_line",
      "ip",
      "bus",
      "usb_headset",
      "hearing_aid",
      "builtin_speaker_safe",
      "remote_submix",
      "ble_headset",
      "ble_speaker",
      "hdmi_earc",
      "ble_broadcast",
      "dock_analog"
    };

    ASSERT(deviceTypeId >= 0);
    ASSERT(deviceTypeId < ARRAY_SIZE(deviceTypeNames));

    return deviceTypeNames[deviceTypeId];
  }

  //  static int deviceTypeName2DeviceTypeId(const char* deviceTypeName)
  //  {
  //    for (i32 i = 0; i < ARRAY_SIZE(deviceTypeNames); ++i)
  //      if (strcmp(deviceTypeName, deviceTypeNames[i]) == 0)
  //        return i;
  //
  //    unreachable();
  //  }

  static void init()
  {
    // unlike input on android, we should be able to always start the output. There is no runtime permission for it.
    JNIEnv* env = nullptr;
    Global::g_JVM->GetEnv((void**)&env, JNI_VERSION_1_4);

    jclass context = env->FindClass("android/content/Context");

    jfieldID audioServiceField = env->GetStaticFieldID(context, "AUDIO_SERVICE", "Ljava/lang/String;");

    jstring jstr = (jstring)env->GetStaticObjectField(context, audioServiceField);

    jmethodID getSystemServiceID = env->GetMethodID(context, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;");

    jobject activity = (jobject)Global::androidActivity;

    jobject audioManagerObject = env->CallObjectMethod(activity, getSystemServiceID, jstr);

    jclass audioManagerClass = env->FindClass("android/media/AudioManager");

    jmethodID getDevicesId = env->GetMethodID(audioManagerClass, "getDevices", "(I)[Landroid/media/AudioDeviceInfo;");

    jclass audioDeviceInfoClass = env->FindClass("android/media/AudioDeviceInfo");

    jmethodID getProductNameId = env->GetMethodID(audioDeviceInfoClass, "getProductName", "()Ljava/lang/CharSequence;");
    jmethodID getTypeId = env->GetMethodID(audioDeviceInfoClass, "getType", "()I");
    jmethodID getId = env->GetMethodID(audioDeviceInfoClass, "getId", "()I");

    jclass charSequenceClass = env->FindClass("java/lang/CharSequence");

    jmethodID toStringId = env->GetMethodID(charSequenceClass, "toString", "()Ljava/lang/String;");

    int deviceIdOutput = AAUDIO_UNSPECIFIED;
    {
      {
        jint audioDevicesFlags = 2; // GET_DEVICES_OUTPUTS
        jobjectArray audioDeviceInfoArray = (jobjectArray)env->CallObjectMethod(audioManagerObject, getDevicesId, audioDevicesFlags);

        jsize arrayLength = env->GetArrayLength(audioDeviceInfoArray);
        for (jsize i = 0; i < arrayLength; ++i) {
          jobject audioDeviceInfoObj = env->GetObjectArrayElement(audioDeviceInfoArray, i);

          jobject productNameObj = env->CallObjectMethod(audioDeviceInfoObj, getProductNameId);
          jint deviceTypeId = env->CallIntMethod(audioDeviceInfoObj, getTypeId);
          jint deviceId = env->CallIntMethod(audioDeviceInfoObj, getId);

          jstring productNameString = (jstring)env->CallObjectMethod(productNameObj, toStringId);
          const char* productNameChars = env->GetStringUTFChars(productNameString, nullptr);

          std::string deviceName = deviceTypeId2DeviceTypeName(deviceTypeId);
          deviceName += ",";
          deviceName += productNameChars;
          deviceName += ",";
          deviceName += std::to_string(deviceId);

          env->ReleaseStringUTFChars(productNameString, productNameChars);
          env->DeleteLocalRef(productNameString);
          env->DeleteLocalRef(productNameObj);

          Global::audioAAudioDevicesOutput.push_back(deviceName);
          if (deviceIdOutput == AAUDIO_UNSPECIFIED && Settings::audioAAudioDeviceOutput == deviceName)
          {
            deviceIdOutput = deviceId;
          }

          env->DeleteLocalRef(audioDeviceInfoObj);
        }
      }
    }

    {
      AAudioStreamBuilder* builder;
      aaudio_result_t result = AAudio_createStreamBuilder(&builder);
      ASSERT(result == AAUDIO_OK);

      AAudioStreamBuilder_setDeviceId(builder, deviceIdOutput);
      AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
      AAudioStreamBuilder_setSharingMode(builder, Settings::audioAAudioExclusiveMode ? AAUDIO_SHARING_MODE_EXCLUSIVE : AAUDIO_SHARING_MODE_SHARED);
      AAudioStreamBuilder_setSampleRate(builder, Settings::audioAAudioSampleRate);
      AAudioStreamBuilder_setChannelCount(builder, 2);
      AAudioStreamBuilder_setFramesPerDataCallback(builder, Settings::audioAAudioBlockSize);
      AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT);
      AAudioStreamBuilder_setBufferCapacityInFrames(builder, Const::audioMaximumPossibleBlockSize * 2);
      const aaudio_performance_mode_t performanceMode = AAUDIO_PERFORMANCE_MODE_NONE + to_underlying_(Settings::audioAAudioPerformanceMode);
      AAudioStreamBuilder_setPerformanceMode(builder, performanceMode);
      AAudioStreamBuilder_setDataCallback(builder, audioCallbackOutput, nullptr);

      AAudioStream* stream;
      result = AAudioStreamBuilder_openStream(builder, &stream);
      ASSERT(result == AAUDIO_OK);

      result = AAudioStreamBuilder_delete(builder);
      ASSERT(result == AAUDIO_OK);

      result = AAudioStream_requestStart(stream);
      ASSERT(result == AAUDIO_OK);
    }

    {
      jint audioDevicesFlags = 1; // GET_DEVICES_INPUTS
      jobjectArray audioDeviceInfoArray = (jobjectArray)env->CallObjectMethod(
        audioManagerObject, getDevicesId, audioDevicesFlags);

      jsize arrayLength = env->GetArrayLength(audioDeviceInfoArray);
      for (jsize i = 0; i < arrayLength; ++i) {
        jobject audioDeviceInfoObj = env->GetObjectArrayElement(audioDeviceInfoArray, i);

        jobject productNameObj = env->CallObjectMethod(audioDeviceInfoObj, getProductNameId);
        jint deviceTypeId = env->CallIntMethod(audioDeviceInfoObj, getTypeId);
        jint deviceId = env->CallIntMethod(audioDeviceInfoObj, getId);

        jstring productNameString = (jstring)env->CallObjectMethod(productNameObj,
          toStringId);
        const char* productNameChars = env->GetStringUTFChars(productNameString, nullptr);

        std::string deviceName = deviceTypeId2DeviceTypeName(deviceTypeId);
        deviceName += ",";
        deviceName += productNameChars;
        deviceName += ",";
        deviceName += std::to_string(deviceId);

        env->ReleaseStringUTFChars(productNameString, productNameChars);
        env->DeleteLocalRef(productNameString);
        env->DeleteLocalRef(productNameObj);

        Global::audioAAudioDevicesInput.push_back(deviceName);
        if (deviceIdInput == AAUDIO_UNSPECIFIED && Settings::audioAAudioDeviceInput == deviceName)
        {
          deviceIdInput = deviceId;
        }

        env->DeleteLocalRef(audioDeviceInfoObj);
      }
    }

    initialized = true;
  }
}
extern "C" JNIEXPORT void JNICALL
#ifdef PLATFORM_OPENXR_ANDROID
Java_app_shr3d_MainActivity_recordAudioPermissionResult
#else // PLATFORM_OPENXR_ANDROID
Java_org_libsdl_app_SDLActivity_recordAudioPermissionResult
#endif // PLATFORM_OPENXR_ANDROID
(JNIEnv* env, jclass clazz, jboolean granted)
{
  if (Settings::audioSystem == AudioSystem::AAudio && granted)
  {
    // wait for the audio output to be initialized
    while (!AAudio::initialized)
      ; //std::this_thread::yield();

    AAudio::initInput();
    Global::androidRecordMicrophonePermissionGranted = granted;
  }
}
#endif // SHR3D_AUDIO_AAUDIO

#ifdef SHR3D_AUDIO_WEBAUDIO
namespace WebAudio
{
  EM_BOOL processAudio(int numInputs, const AudioSampleFrame* inputs,
    int numOutputs, AudioSampleFrame* outputs,
    int numParams, const AudioParamFrame* params,
    void* userData)
  {
    { // input
      const i32 channelOffset = Settings::audioWebAudioChannelInput0 * 128; // 0:left, 128:right
      for (i32 i = 0; i < 128; ++i)
      {
        outputs[0].data[i] = inputs[0].data[i + channelOffset] * Settings::audioEffectVolume;
        outputs[0].data[i + 128] = outputs[0].data[i];
      }
    }

    f32 tempBufferStereo[256];

    f32* outputStream[] = { &outputs[0].data[0], &outputs[0].data[128] };
    f32* tempBuffer[] = { &tempBufferStereo[0], &tempBufferStereo[128] };

    process_main(outputStream, tempBuffer, 128);

    memcpy(&outputs[0].data[0], &tempBufferStereo[0], 128 * sizeof(f32));
    memcpy(&outputs[0].data[128], &tempBufferStereo[128], 128 * sizeof(f32));

    return EM_TRUE; // Keep the graph output going
  }

  EM_BOOL OnCanvasClick(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData)
  {
    EMSCRIPTEN_WEBAUDIO_T audioContext = (EMSCRIPTEN_WEBAUDIO_T)userData;
    if (emscripten_audio_context_state(audioContext) != AUDIO_CONTEXT_STATE_RUNNING) {
      emscripten_resume_audio_context_sync(audioContext);
    }
    return EM_FALSE;
  }

  void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void* userData)
  {
    ASSERT(success);

    int outputChannelCounts[1] = { 2 };
    EmscriptenAudioWorkletNodeCreateOptions options = {
      .numberOfInputs = 1,
      .numberOfOutputs = 1,
      .outputChannelCounts = outputChannelCounts
    };

    // Create node
    EMSCRIPTEN_AUDIO_WORKLET_NODE_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "process-audio", &options, &processAudio, 0);

    EM_ASM({
        navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: false, autoGainControl : false, noiseSuppression : false } }) // disable all processing done by the browser
          .then(function(stream) {
            emscriptenGetAudioObject($1).streamNode = emscriptenGetAudioObject($1).createMediaStreamSource(stream);
            emscriptenGetAudioObject($1).streamNode.connect(emscriptenGetAudioObject($0));
          })
          .catch (function(error) {
        });
        emscriptenGetAudioObject($0).connect(emscriptenGetAudioObject($1).destination);
      }, wasmAudioWorklet, audioContext);

    // Resume context on mouse click
    emscripten_set_click_callback("canvas", (void*)audioContext, 0, OnCanvasClick);
  }

  static void AudioThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, EM_BOOL success, void* userData)
  {
    ASSERT(success);

    WebAudioWorkletProcessorCreateOptions opts = {
      .name = "process-audio",
    };
    emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, &AudioWorkletProcessorCreated, 0);
  }

  static void init()
  {
    static EMSCRIPTEN_WEBAUDIO_T context = emscripten_create_audio_context(0);
    static u8 audioThreadStack[32768];
    emscripten_start_wasm_audio_worklet_thread_async(context, audioThreadStack, sizeof(audioThreadStack), &AudioThreadInitialized, 0);
  }
}
#endif // SHR3D_AUDIO_WEBAUDIO

#ifdef SHR3D_AUDIO_JACK
namespace JACK
{
#define JACK_DEFAULT_AUDIO_TYPE "32 bit float mono audio"

#ifndef SHR3D_AUDIO_JACK_NO_DLOPEN
  extern "C"
  {
    enum JackOptions {
      JackNullOption = 0x00,
      JackNoStartServer = 0x01,
      JackUseExactName = 0x02,
      JackServerName = 0x04,
      JackLoadName = 0x08,
      JackLoadInit = 0x10,
      JackSessionID = 0x20
    };

    enum JackStatus {
      JackFailure = 0x01,
      JackInvalidOption = 0x02,
      JackNameNotUnique = 0x04,
      JackServerStarted = 0x08,
      JackServerFailed = 0x10,
      JackServerError = 0x20,
      JackNoSuchClient = 0x40,
      JackLoadFailure = 0x80,
      JackInitFailure = 0x100,
      JackShmFailure = 0x200,
      JackVersionError = 0x400,
      JackBackendError = 0x800,
      JackClientZombie = 0x1000
    };

    enum JackPortFlags {
      JackPortIsInput = 0x1,
      JackPortIsOutput = 0x2,
      JackPortIsPhysical = 0x4,
      JackPortCanMonitor = 0x8,
      JackPortIsTerminal = 0x10,
    };

    typedef enum JackStatus jack_status_t;
    typedef enum JackOptions jack_options_t;
    typedef struct _jack_port jack_port_t;
    typedef struct _jack_client jack_client_t;
    typedef uint32_t jack_nframes_t;
    typedef float jack_default_audio_sample_t;
    typedef int (*JackProcessCallback)(jack_nframes_t nframes, void* arg);
    typedef int (*JackBufferSizeCallback)(jack_nframes_t nframes, void* arg);
    typedef int (*JackSampleRateCallback)(jack_nframes_t nframes, void* arg);

    typedef jack_client_t* (*jack_client_openFunc)(const char*, jack_options_t, jack_status_t*, ...);
    static jack_client_openFunc jack_client_open;
    typedef char* (*jack_get_client_nameFunc)(jack_client_t* client);
    static jack_get_client_nameFunc jack_get_client_name;
    typedef const char** (*jack_get_portsFunc)(jack_client_t* client, const char* port_name_pattern, const char* type_name_pattern, u32 flags);
    static jack_get_portsFunc jack_get_ports;
    typedef int(*jack_set_process_callbackFunc)(jack_client_t* client, JackProcessCallback process_callback, void* arg);
    static jack_set_process_callbackFunc jack_set_process_callback;
    typedef int(*jack_set_buffer_size_callbackFunc)(jack_client_t* client, JackBufferSizeCallback bufsize_callback, void* arg);
    static jack_set_buffer_size_callbackFunc jack_set_buffer_size_callback;
    typedef int(*jack_set_sample_rate_callbackFunc)(jack_client_t* client, JackSampleRateCallback srate_callback, void* arg);
    static jack_set_sample_rate_callbackFunc jack_set_sample_rate_callback;
    typedef const char* (*jack_port_nameFunc)(const jack_port_t* port);
    static jack_port_nameFunc jack_port_name;
    typedef int(*jack_connectFunc)(jack_client_t* client, const char* source_port, const char* destination_port);
    static jack_connectFunc jack_connect;
    typedef void* (*jack_port_get_bufferFunc)(jack_port_t* port, jack_nframes_t);
    static jack_port_get_bufferFunc jack_port_get_buffer;
    typedef jack_port_t* (*jack_port_registerFunc)(jack_client_t* client, const char* port_name, const char* port_type, u32 flags, u32 buffer_size);
    static jack_port_registerFunc jack_port_register;
    typedef int(*jack_activateFunc)(jack_client_t* client);
    static jack_activateFunc jack_activate;
  }

  static void load_libjack()
  {
#ifdef PLATFORM_WINDOWS
    void* hinstLib = dlopen(L"libjack64.dll", RTLD_LAZY | RTLD_LOCAL);
#else // PLATFORM_WINDOWS
    void* hinstLib = dlopen("libjack.so.0", RTLD_LAZY | RTLD_LOCAL);
#endif // PLATFORM_WINDOWS

    if (hinstLib != nullptr)
    {
      jack_client_open = (jack_client_openFunc)dlsym(hinstLib, "jack_client_open");
      ASSERT(jack_client_open != nullptr);
      jack_get_client_name = (jack_get_client_nameFunc)dlsym(hinstLib, "jack_get_client_name");
      ASSERT(jack_get_client_name != nullptr);
      jack_get_ports = (jack_get_portsFunc)dlsym(hinstLib, "jack_get_ports");
      ASSERT(jack_get_ports != nullptr);
      jack_set_process_callback = (jack_set_process_callbackFunc)dlsym(hinstLib, "jack_set_process_callback");
      ASSERT(jack_set_process_callback != nullptr);
      jack_set_buffer_size_callback = (jack_set_buffer_size_callbackFunc)dlsym(hinstLib, "jack_set_buffer_size_callback");
      ASSERT(jack_set_buffer_size_callback != nullptr);
      jack_set_sample_rate_callback = (jack_set_sample_rate_callbackFunc)dlsym(hinstLib, "jack_set_sample_rate_callback");
      ASSERT(jack_set_sample_rate_callback != nullptr);
      jack_port_name = (jack_port_nameFunc)dlsym(hinstLib, "jack_port_name");
      ASSERT(jack_port_name != nullptr);
      jack_connect = (jack_connectFunc)dlsym(hinstLib, "jack_connect");
      ASSERT(jack_connect != nullptr);
      jack_port_get_buffer = (jack_port_get_bufferFunc)dlsym(hinstLib, "jack_port_get_buffer");
      ASSERT(jack_port_get_buffer != nullptr);
      jack_port_register = (jack_port_registerFunc)dlsym(hinstLib, "jack_port_register");
      ASSERT(jack_port_register != nullptr);
      jack_activate = (jack_activateFunc)dlsym(hinstLib, "jack_activate");
      ASSERT(jack_activate != nullptr);

      Global::audioJackLibraryLoaded = true;
    }
  }
#endif // SHR3D_AUDIO_JACK_NO_DLOPEN

  static jack_client_t* client;
  static jack_port_t* input_in0;
  static jack_port_t* input_in1;
#ifdef SHR3D_COOP
  static jack_port_t* input_in_coop0;
  static jack_port_t* input_in_coop1;
#endif // SHR3D_COOP
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
  static jack_port_t* input_string0;
  static jack_port_t* input_string1;
  static jack_port_t* input_string2;
  static jack_port_t* input_string3;
  static jack_port_t* input_string4;
  static jack_port_t* input_string5;
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
  static jack_port_t* output_out0;
  static jack_port_t* output_out1;

  static bool initJack()
  {
#ifndef SHR3D_AUDIO_JACK_NO_DLOPEN
    ASSERT(!Global::audioJackLibraryLoaded);
    load_libjack();
    if (!Global::audioJackLibraryLoaded)
      return false;
#endif // SHR3D_AUDIO_JACK_NO_DLOPEN

    jack_options_t options = JackNullOption;
    jack_status_t status;

    client = jack_client_open("Shr3D", options, &status, nullptr); // if you got a crash here on Windows, something is wrong with JACK. Delete %temp%/jack_db or temporarily rename %systemroot%/libjack64_.dll
    if (client == nullptr)
    {
      DEBUG_PRINT(stderr, "jack_client_open() failed, status = 0x%2.0x\n", status);
      if (status & JackServerFailed)
        DEBUG_PRINT(stderr, "Unable to connect to JACK server\n");
      Global::audioJackLibraryLoaded = false;
      return false;
    }

    if (status & JackServerStarted)
      DEBUG_PRINT("JACK server started\n");

    if (status & JackNameNotUnique)
    {
      const char* client_name = jack_get_client_name(client);
      DEBUG_PRINT("unique name `%s' assigned\n", client_name);
    }

    return true;
  }

  static void getDeviceNames()
  {
#ifndef SHR3D_AUDIO_JACK_NO_DLOPEN
    initJack();
    if (!Global::audioJackLibraryLoaded)
      return;
#endif // SHR3D_AUDIO_JACK_NO_DLOPEN

    {
      i32 i = 0;
      const char** capturePorts = jack_get_ports(client, nullptr, nullptr, JackPortIsPhysical | JackPortIsOutput);
      while (capturePorts[i] != nullptr)
      {
        Global::audioJackDevicesInput.push_back(capturePorts[i]);
        ++i;
      }
    }
    {
      i32 i = 0;
      const char** playbackPorts = jack_get_ports(client, nullptr, nullptr, JackPortIsPhysical | JackPortIsInput);
      while (playbackPorts[i] != nullptr)
      {
        Global::audioJackDevicesOutput.push_back(playbackPorts[i]);
        ++i;
      }
    }
  }

  static int process(const jack_nframes_t blockSize, void* /*arg*/)
  {
    f32* in0 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_in0, blockSize));
    f32* in1 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_in1, blockSize));

#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
    if (Settings::audioJackDividedPickup)
    {
      f32* inString0 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_string0, blockSize));
      f32* inString1 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_string1, blockSize));
      f32* inString2 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_string2, blockSize));
      f32* inString3 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_string3, blockSize));
      f32* inString4 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_string4, blockSize));
      f32* inString5 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_string5, blockSize));

      memcpy(TunerThread::inBlock0, inString0, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock1, inString1, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock2, inString2, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock3, inString3, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock4, inString4, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock5, inString5, blockSize * sizeof(f32));

      //TunerThread::tick();

      if (Settings::audioJackDividedPickupAsMainInput)
      {
        for (i32 i = 0; i < blockSize; ++i)
          in0[i] = 0.166666667f * (inString0[i] + inString1[i] + inString2[i] + inString3[i] + inString4[i] + inString5[i]);
        memcpy(in1, in0, blockSize * sizeof(f32));
      }
    }
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED

    f32* inBlock[] = { in0, in1 };

    f32* out0 = reinterpret_cast<f32*>(jack_port_get_buffer(output_out0, blockSize));
    f32* out1 = reinterpret_cast<f32*>(jack_port_get_buffer(output_out1, blockSize));
    f32* outBlock[] = { out0, out1 };

#ifdef SHR3D_COOP
    f32* inCoopBlock[2];
    if (Global::inputCoop.toggled)
    {
      f32* inCoop0 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_in_coop0, blockSize));
      f32* inCoop1 = reinterpret_cast<jack_default_audio_sample_t*>(jack_port_get_buffer(input_in_coop1, blockSize));
      if (Settings::audioJackInputDeviceCoop1.empty())
        memcpy(inCoop1, inCoop0, blockSize * sizeof(f32));
      inCoopBlock[0] = inCoop0;
      inCoopBlock[1] = inCoop1;
    }
#endif // SHR3D_COOP

    process_main(inBlock, outBlock, blockSize
#ifdef SHR3D_COOP
      , inCoopBlock
#endif // SHR3D_COOP
    );

    return 0;
  }

  static i32 bufferSizeCallback(jack_nframes_t nframes, void* /*arg*/)
  {
    Global::audioJackBlockSize = nframes;
    return 0;
  }

  static i32 sampleRateCallback(jack_nframes_t nframes, void* /*arg*/)
  {
    Global::audioJackSampleRate = nframes;
    return 0;
  }

  static void init()
  {
    if (client == nullptr)
      return;

    jack_set_buffer_size_callback(client, bufferSizeCallback, nullptr);
    jack_set_sample_rate_callback(client, sampleRateCallback, nullptr);

    jack_set_process_callback(client, process, nullptr);

    {
      input_in0 = jack_port_register(client, "in_0", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_in0 != nullptr);
      input_in1 = jack_port_register(client, "in_1", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_in1 != nullptr);
#ifdef SHR3D_COOP
      input_in_coop0 = jack_port_register(client, "in_coop_0", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_in_coop0 != nullptr);
      input_in_coop1 = jack_port_register(client, "in_coop_1", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_in_coop1 != nullptr);
#endif // SHR3D_COOP
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
      input_string0 = jack_port_register(client, "string_0", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_string0 != nullptr);
      input_string1 = jack_port_register(client, "string_1", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_string1 != nullptr);
      input_string2 = jack_port_register(client, "string_2", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_string2 != nullptr);
      input_string3 = jack_port_register(client, "string_3", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_string3 != nullptr);
      input_string4 = jack_port_register(client, "string_4", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_string4 != nullptr);
      input_string5 = jack_port_register(client, "string_5", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0);
      ASSERT(input_string5 != nullptr);
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
    }
    {
      output_out0 = jack_port_register(client, "out_0", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
      ASSERT(output_out0 != nullptr);
      output_out1 = jack_port_register(client, "out_1", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0);
      ASSERT(output_out1 != nullptr);
    }

    if (jack_activate(client) != 0)
      DEBUG_PRINT("cannot activate client\n");

    { // setup connections
      {
        i32 i = 0;
        const char** playbackPorts = jack_get_ports(client, nullptr, nullptr, JackPortIsPhysical | JackPortIsInput);
        while (playbackPorts[i] != nullptr)
        {
          if (Settings::audioJackOutputDevice0 == playbackPorts[i])
            jack_connect(client, jack_port_name(output_out0), playbackPorts[i]);
          if (Settings::audioJackOutputDevice1 == playbackPorts[i])
            jack_connect(client, jack_port_name(output_out1), playbackPorts[i]);
          ++i;
        }
      }
      {
        i32 i = 0;
        const char** capturePorts = jack_get_ports(client, nullptr, nullptr, JackPortIsPhysical | JackPortIsOutput);
        while (capturePorts[i] != nullptr)
        {
          if (Settings::audioJackInputDevice0 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_in0));
          if (Settings::audioJackInputDevice1 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_in1));
#ifdef SHR3D_COOP
          if (Settings::audioJackInputDeviceCoop0 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_in_coop0));
          if (Settings::audioJackInputDeviceCoop1 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_in_coop1));
#endif // SHR3D_COOP
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
          if (Settings::audioJackDividedPickupChannelString0 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_string0));
          if (Settings::audioJackDividedPickupChannelString1 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_string1));
          if (Settings::audioJackDividedPickupChannelString2 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_string2));
          if (Settings::audioJackDividedPickupChannelString3 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_string3));
          if (Settings::audioJackDividedPickupChannelString4 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_string4));
          if (Settings::audioJackDividedPickupChannelString5 == capturePorts[i])
            jack_connect(client, capturePorts[i], jack_port_name(input_string5));
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
          ++i;
        }
      }
    }
  }
}
#endif // SHR3D_AUDIO_JACK

#ifdef SHR3D_AUDIO_PIPEWIRE
namespace PipeWire
{
#define PIPEWIRE_DEFAULT_AUDIO_TYPE "32 bit float mono audio"

#ifndef SHR3D_AUDIO_PIPEWIRE_NO_DLOPEN
  extern "C"
  {
    typedef void (*pw_initFunc)(int* argc, char** argv[]);
    static pw_initFunc pw_init;
    typedef struct pw_thread_loop* (*pw_thread_loop_newFunc)(const char* name, const struct spa_dict* props);
    static pw_thread_loop_newFunc pw_thread_loop_new;
    typedef struct pw_filter* (*pw_filter_new_simpleFunc)(struct pw_loop* loop, const char* name, struct pw_properties* props, const struct pw_filter_events* events, void* data);
    static pw_filter_new_simpleFunc pw_filter_new_simple;
    typedef struct pw_loop* (*pw_thread_loop_get_loopFunc)(struct pw_thread_loop* loop);
    static pw_thread_loop_get_loopFunc pw_thread_loop_get_loop;
    typedef struct pw_properties* (*pw_properties_newFunc)(const char* key, ...) /*SPA_SENTINEL*/;
    static pw_properties_newFunc pw_properties_new;
    typedef void* (*pw_filter_add_portFunc)(struct pw_filter* filter, enum pw_direction direction, enum pw_filter_port_flags flags, size_t port_data_size, struct pw_properties* props, const struct spa_pod** params, uint32_t n_params);
    static pw_filter_add_portFunc pw_filter_add_port;
    typedef int (*pw_filter_connectFunc)(struct pw_filter* filter, enum pw_filter_flags flags, const struct spa_pod** params, uint32_t n_params);
    static pw_filter_connectFunc pw_filter_connect;
    typedef int (*pw_thread_loop_startFunc)(struct pw_thread_loop* loop);
    static pw_thread_loop_startFunc pw_thread_loop_start;
    typedef void* (*pw_filter_get_dsp_bufferFunc)(void* port_data, uint32_t n_samples);
    static pw_filter_get_dsp_bufferFunc pw_filter_get_dsp_buffer;

    static void load_libpipewire()
    {
      void* hinstLib = dlopen("libpipewire-0.3.so.0", RTLD_LAZY | RTLD_LOCAL);

      if (hinstLib != nullptr)
      {
        pw_init = (pw_initFunc)dlsym(hinstLib, "pw_init");
        ASSERT(pw_init != nullptr);
        pw_thread_loop_new = (pw_thread_loop_newFunc)dlsym(hinstLib, "pw_thread_loop_new");
        ASSERT(pw_thread_loop_new != nullptr);
        pw_filter_new_simple = (pw_filter_new_simpleFunc)dlsym(hinstLib, "pw_filter_new_simple");
        ASSERT(pw_filter_new_simple != nullptr);
        pw_thread_loop_get_loop = (pw_thread_loop_get_loopFunc)dlsym(hinstLib, "pw_thread_loop_get_loop");
        ASSERT(pw_thread_loop_get_loop != nullptr);
        pw_properties_new = (pw_properties_newFunc)dlsym(hinstLib, "pw_properties_new");
        ASSERT(pw_properties_new != nullptr);
        pw_filter_add_port = (pw_filter_add_portFunc)dlsym(hinstLib, "pw_filter_add_port");
        ASSERT(pw_filter_add_port != nullptr);
        pw_filter_connect = (pw_filter_connectFunc)dlsym(hinstLib, "pw_filter_connect");
        ASSERT(pw_filter_connect != nullptr);
        pw_thread_loop_start = (pw_thread_loop_startFunc)dlsym(hinstLib, "pw_thread_loop_start");
        ASSERT(pw_thread_loop_start != nullptr);
        pw_filter_get_dsp_buffer = (pw_filter_get_dsp_bufferFunc)dlsym(hinstLib, "pw_filter_get_dsp_buffer");
        ASSERT(pw_filter_get_dsp_buffer != nullptr);

        Global::audioPipewireLibraryLoaded = true;
      }
    }

    static bool initPipewire()
    {
      ASSERT(!Global::audioPipewireLibraryLoaded);
      load_libpipewire();
      if (!Global::audioPipewireLibraryLoaded)
        return false;

      return true;
    }
  }
#endif // SHR3D_AUDIO_PIPEWIRE_NO_DLOPEN

  using Port = void;

  struct Data {
    pw_thread_loop* loop;
    pw_filter* filter;
    Port* input_in0;
    Port* input_in1;
#ifdef SHR3D_COOP
    Port* input_in_coop0;
    Port* input_in_coop1;
#endif // SHR3D_COOP
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
    Port* input_string0;
    Port* input_string1;
    Port* input_string2;
    Port* input_string3;
    Port* input_string4;
    Port* input_string5;
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
    Port* output_out0;
    Port* output_out1;
  };

  static Data data;

  static void on_process(void* userdata, spa_io_position* position)
  {
    Data* data = reinterpret_cast<Data*>(userdata);
    const u32 blockSize = position->clock.duration;

    f32* in0 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_in0, blockSize));
    f32* in1 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_in1, blockSize));

    if (in0 == nullptr || in1 == nullptr)
      return;

#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
    if (Settings::audioPipewireDividedPickup)
    {
      f32* inString0 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_string0, blockSize));
      f32* inString1 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_string1, blockSize));
      f32* inString2 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_string2, blockSize));
      f32* inString3 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_string3, blockSize));
      f32* inString4 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_string4, blockSize));
      f32* inString5 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_string5, blockSize));

      if (inString0 == nullptr || inString1 == nullptr || inString2 == nullptr || inString3 == nullptr || inString4 == nullptr || inString5 == nullptr)
        return;

      memcpy(TunerThread::inBlock0, inString0, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock1, inString1, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock2, inString2, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock3, inString3, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock4, inString4, blockSize * sizeof(f32));
      memcpy(TunerThread::inBlock5, inString5, blockSize * sizeof(f32));

      //TunerThread::tick();

      if (Settings::audioPipewireDividedPickupAsMainInput)
      {
        for (i32 i = 0; i < blockSize; ++i)
          in0[i] = 0.166666667f * (inString0[i] + inString1[i] + inString2[i] + inString3[i] + inString4[i] + inString5[i]);
        memcpy(in1, in0, blockSize * sizeof(f32));
      }
    }
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED

    f32* inBlock[] = { in0, in1 };

    f32* out0 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->output_out0, blockSize));
    f32* out1 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->output_out1, blockSize));
    f32* outBlock[] = { out0, out1 };

    if (out0 == nullptr || out1 == nullptr)
      return;

#ifdef SHR3D_COOP
    f32* inCoopBlock[2];
    if (Global::inputCoop.toggled)
    {
      f32* inCoop0 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_in_coop0, blockSize));
      f32* inCoop1 = reinterpret_cast<f32*>(pw_filter_get_dsp_buffer(data->input_in_coop1, blockSize));
      if (Settings::audioPipewireInputDeviceCoop1.empty())
        memcpy(inCoop1, inCoop0, blockSize * sizeof(f32));
      inCoopBlock[0] = inCoop0;
      inCoopBlock[1] = inCoop1;
    }
#endif // SHR3D_COOP

    process_main(inBlock, outBlock, blockSize
#ifdef SHR3D_COOP
      , inCoopBlock
#endif // SHR3D_COOP
    );
  }

  static const struct pw_filter_events filter_events = {
          .version = PW_VERSION_FILTER_EVENTS,
          .process = on_process,
  };

  static void init()
  {
    pw_init(nullptr, nullptr);

    /* make a main loop. If you already have another main loop, you can add
     * the fd of this pipewire mainloop to it. */
    data.loop = pw_thread_loop_new("", nullptr);

    /* Create a simple filter, the simple filter manages the core and remote
     * objects for you if you don't need to deal with them.
     *
     * Pass your events and a user_data pointer as the last arguments. This
     * will inform you about the filter state. The most important event
     * you need to listen to is the process event where you need to process
     * the data.
     */
    data.filter = pw_filter_new_simple(
      pw_thread_loop_get_loop(data.loop),
      "Shr3D",
      pw_properties_new(
        PW_KEY_MEDIA_TYPE, "Audio",
        PW_KEY_MEDIA_CATEGORY, "Source",
        PW_KEY_MEDIA_ROLE, "DSP",
        PW_KEY_MEDIA_CLASS, "Stream/Output/Audio",
        PW_KEY_NODE_AUTOCONNECT, "true",
        NULL),
      &filter_events,
      &data);

    data.input_in0 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "in_0",
        nullptr),
      NULL, 0);

    data.input_in1 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "in_1",
        nullptr),
      NULL, 0);

#ifdef SHR3D_COOP
    data.input_in_coop0 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "in_coop_0",
        nullptr),
      NULL, 0);

    data.input_in_coop1 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "in_coop_1",
        nullptr),
      NULL, 0);
#endif // SHR3D_COOP
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
    data.input_string0 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "string_0",
        nullptr),
      NULL, 0);

    data.input_string1 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "string_1",
        nullptr),
      NULL, 0);

    data.input_string2 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "string_2",
        nullptr),
      NULL, 0);

    data.input_string3 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "string_3",
        nullptr),
      NULL, 0);

    data.input_string4 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "string_4",
        nullptr),
      NULL, 0);

    data.input_string5 = pw_filter_add_port(data.filter,
      PW_DIRECTION_INPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "string_5",
        nullptr),
      NULL, 0);
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED

    data.output_out0 = pw_filter_add_port(data.filter,
      PW_DIRECTION_OUTPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "out_0",
        NULL),
      NULL, 0);

    data.output_out1 = pw_filter_add_port(data.filter,
      PW_DIRECTION_OUTPUT,
      PW_FILTER_PORT_FLAG_MAP_BUFFERS,
      sizeof(Port*),
      pw_properties_new(
        PW_KEY_FORMAT_DSP, PIPEWIRE_DEFAULT_AUDIO_TYPE,
        PW_KEY_PORT_NAME, "out_1",
        NULL),
      NULL, 0);

    uint8_t buffer[1024];
    spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));

    const spa_process_latency_info latencyInfo = SPA_PROCESS_LATENCY_INFO_INIT(
      .ns = 10 * SPA_NSEC_PER_MSEC
    );

    const struct spa_pod* params[1]{
      spa_process_latency_build(&b, SPA_PARAM_ProcessLatency, &latencyInfo)
    };


    /* Now connect this filter. We ask that our process function is
     * called in a realtime thread. */
    if (pw_filter_connect(data.filter, PW_FILTER_FLAG_RT_PROCESS, params, 1) < 0) {
      ASSERT(false); // can't connect
      return;
    }



    pw_thread_loop_start(data.loop);
  }



  //#define PIPEWIRE_TODO_IMPLEMENT_AUTOCONNECT
#ifdef PIPEWIRE_TODO_IMPLEMENT_AUTOCONNECT
  struct data {
    struct pw_main_loop* loop;
    struct pw_registry* registry;
  };
  struct data data1 = { };

  static void node_param(void* object, int seq, uint32_t param_id, uint32_t index, uint32_t next, const struct spa_pod* param)
  {
    // it contains a pod object
    const struct spa_pod_prop* prop;
    prop = spa_pod_find_prop(param, NULL, SPA_PROP_channelVolumes);
    if (prop) {
      //fprintf(stderr, "prop key:%d\n", prop->key);
      if (spa_pod_is_array(&prop->value)) {
        struct spa_pod_array* arr = (struct spa_pod_array*)&prop->value;

        struct spa_pod* iter;
        //printf("id:%u\tname:%s\tclass:%s\t", id, node_name, class_name);
        SPA_POD_ARRAY_FOREACH(arr, iter) {
          // cube root to linear
          //printf("\tvolume:%f", cbrt(*(float*)iter));
        }
        puts("");
      }
    }

    // stop the roundtrip, we got our reply
    // TODO, one issue is that we might exit before getting all nodes
    pw_main_loop_quit(data1.loop);
  }

  static void port_event_info(void* data, const struct pw_port_info* info)
  {
  }

  struct Port1
  {
    std::string name;
    std::string type;
    std::string direction;
  };

  struct Node1
  {
    u32 id;
    std::string name;
    std::string nick;
    std::vector<Port1> ports;
  };
  static std::vector<Node1> nodeList;

  static void registry_event_global(void* userdata, uint32_t id, uint32_t permissions, const char* type, uint32_t version, const struct spa_dict* props)
  {
    //fprintf(stderr, "object: id:%u type:%s/%d\n", id, type, version);
    // it needs to at least have props
    if (!props) {
      return;
    }
    // uncomment to see all props
    //spa_debug_dict(4, props);


    if (::strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
      const char* node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
      const char* node_nick = spa_dict_lookup(props, PW_KEY_NODE_NICK);
      const char* class_name = spa_dict_lookup(props, "media.class");

      if (class_name == nullptr)
        return;

      Node1 node;
      node.id = id;
      if (node_name)
        node.name = node_name;
      if (node_nick)
        node.nick = node_nick;

      nodeList.push_back(node);
    }

    if (::strcmp(type, PW_TYPE_INTERFACE_Port) == 0)
    {
      const char* str = spa_dict_lookup(props, PW_KEY_NODE_ID);
      const uint node_id = (str ? uint(atoi(str)) : 0);

      for (Node1& node : nodeList)
      {
        if (node.id == node_id)
        {
          std::string port_name;
          str = spa_dict_lookup(props, PW_KEY_PORT_ALIAS);
          if (str == nullptr)
            str = spa_dict_lookup(props, PW_KEY_PORT_NAME);
          if (str == nullptr)
            str = "port";
          port_name += str;

          std::string port_type;
          str = spa_dict_lookup(props, PW_KEY_FORMAT_DSP);
          if (str)
            port_type = str;

          std::string port_direction;
          str = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION);
          if (str)
            port_direction = str;

          Port1 port;
          port.name = port_name;
          port.type = port_type;
          port.direction = port_direction;

          node.ports.push_back(port);
          break;
        }
      }
    }

    // we confirmed this is a PipeWire node
    // it follows the PW_TYPE_INTERFACE_Node interface
    // get a proxy to it to be able to send it message
    // see /usr/include/pipewire-0.3/pipewire/core.h
    struct data* data = (struct data*)userdata;
    struct pw_node* node = (struct pw_node*)pw_registry_bind(data->registry, id, PW_TYPE_INTERFACE_Node, version, 0);
    struct pw_port* port = (struct pw_port*)pw_registry_bind(data->registry, id, PW_TYPE_INTERFACE_Port, version, 0);
    // see /usr/include/pipewire/node.h for what we
    // can do with a pw_node
    // what we do here is subscribe to the param event
    // then request the list of params

    // the hook needs to be alive as long as we register for events
    // here we do a single rountrip and remove it afterwards
    struct spa_hook node_listener;
    struct spa_hook port_listener;

    const struct pw_node_events node_events = {
      .version = PW_VERSION_NODE_EVENTS,
      .param = node_param,
      //.info  = node_info,
    };

    const struct pw_port_events port_events = {
      .version = PW_VERSION_PORT_EVENTS,
      .info = port_event_info,
    };

    // not really needed but...
    spa_zero(node_listener);
    spa_zero(port_listener);
    // attach the listener for the enum param events
    pw_node_add_listener(node, &node_listener, &node_events, NULL);
    pw_port_add_listener(port, &port_listener, &port_events, NULL);

    // send the enum params event
    // here we only want the SPA_PARAM_Props and nothing else
    // see spa_param_type in /usr/include/spa/param/param.h
    pw_node_enum_params(node, 0,
      SPA_PARAM_Props, 0, //start index
      1, // max num of params
      NULL // filter
    );
    pw_port_enum_params(port, 0,
      SPA_PARAM_Props, 0, //start index
      1, // max num of params
      NULL // filter
    );

    // run the loop
    // this will block until we call pw_main_loop_quit in the callback
    pw_main_loop_run(data->loop);
    // safely remove the hook once we got the param we wanted
    spa_hook_remove(&node_listener);
    spa_hook_remove(&port_listener);
    pw_proxy_destroy((struct pw_proxy*)node);
    pw_proxy_destroy((struct pw_proxy*)port);
  }

  static const struct pw_registry_events registry_events = {
          .version = PW_VERSION_REGISTRY_EVENTS,
          .global = registry_event_global,
  };
#endif // PIPEWIRE_TODO_IMPLEMENT_AUTOCONNECT

  static void getDeviceNames()
  {
#ifndef SHR3D_AUDIO_PIPEWIRE_NO_DLOPEN
    initPipewire();
    if (!Global::audioPipewireLibraryLoaded)
      return;
#endif // SHR3D_AUDIO_PIPEWIRE_NO_DLOPEN

#ifdef PIPEWIRE_TODO_IMPLEMENT_AUTOCONNECT
    struct pw_context* context;
    struct spa_hook registry_listener;

    pw_init(&argc, &argv);

    data1.loop = pw_main_loop_new(NULL /* properties */);
    context = pw_context_new(pw_main_loop_get_loop(data1.loop),
      NULL /* properties */,
      0 /* user_data size */);

    pw_core* core = pw_context_connect(context,
      NULL /* properties */,
      0 /* user_data size */);

    data1.registry = pw_core_get_registry(core, PW_VERSION_REGISTRY,
      0 /* user_data size */);

    spa_zero(registry_listener);
    pw_registry_add_listener(data1.registry, &registry_listener,
      &registry_events, (void*)&data1);

    pw_main_loop_run(data1.loop);

    // destroy objects in reverse order
    // the registry is a proxy to the remote registry object on the
    // pw server
    pw_proxy_destroy((struct pw_proxy*)data1.registry);
    pw_core_disconnect(core);
    pw_context_destroy(context);
    pw_main_loop_destroy(data1.loop);

    for (const Node1& node : nodeList)
    {
      for (const Port1& port : node.ports)
      {
        if (port.type == PIPEWIRE_DEFAULT_AUDIO_TYPE)
        {
          if (port.direction == "out")
            Global::audioPipewireDevicesInput.push_back(port.name);
          else if (port.direction == "in")
            Global::audioPipewireDevicesOutput.push_back(port.name);
        }
      }
    }
#endif // PIPEWIRE_TODO_IMPLEMENT_AUTOCONNECT
  }
}
#endif // SHR3D_AUDIO_PIPEWIRE

#ifdef SHR3D_AUDIO_SDL
namespace SDL
{
  static SDL_AudioDeviceID devid_in = 0;
  //struct VstBuffer
  //{
  //  f32 left[Const::audioMaximumPossibleBlockSize]; // VST and VST3 format: L0, L1, ...
  //  f32 right[Const::audioMaximumPossibleBlockSize];  // VST and VST3 format: R0, R1, ...
  //};

  static SDL_AudioDeviceID devid_out;

  // TODO: std::mutex is not the best here in terms of latency
  // http://www.rossbencina.com/code/real-time-audio-programming-101-time-waits-for-nothing
  // https://timur.audio/using-locks-in-real-time-audio-processing-safely
  static std::condition_variable cv;
  static std::mutex mutex;
  static bool recordNext = true;

  static f32 recordingToPlaybackBuffer[Const::audioMaximumPossibleBlockSize * 2]; // SDL format: L0, R0, L1, R1, ...

#ifndef NDEBUG
  static u64 debugAudioCallbackRecordingCallCount;
#endif // NDEBUG

  static void audioRecordingCallback(void* /*userdata*/, u8* inputStreamU8, const i32 blockSizeStereoInBytes)
  {
#ifndef PLATFORM_EMSCRIPTEN
    std::unique_lock lock(mutex);
    cv.wait(lock, [] { return recordNext; });
#endif // PLATFORM_EMSCRIPTEN

#ifndef NDEBUG
    ++debugAudioCallbackRecordingCallCount;
#endif // NDEBUG

    memcpy(recordingToPlaybackBuffer, inputStreamU8, blockSizeStereoInBytes);

#ifdef SHR3D_ENVIRONMENT_MILK
    //Milk::processAudioFrame();
#endif // SHR3D_ENVIRONMENT_MILK

#ifndef PLATFORM_EMSCRIPTEN
    recordNext = !recordNext;
    cv.notify_one();
#endif // PLATFORM_EMSCRIPTEN
  }

  static void audioPlaybackCallback(void* /*userdata*/, u8* outputStreamU8, const i32 blockSizeStereoInBytes)
  {
    const i32 blockSize = (blockSizeStereoInBytes / sizeof(f32)) / 2; // mono

    // copy instrument input channel to stereo and change the format from SDL to VST:
    // SDL: L0, R0, L1, R1, ...
    // VST: L0, L1, ..., R0, R1, ...
    f32* outputStream = reinterpret_cast<f32*>(outputStreamU8);
#ifdef SHR3D_COOP
    f32 coopTempBuffer[Const::audioMaximumPossibleBlockSize * 2];
#endif // SHR3D_COOP

    {
#ifndef PLATFORM_EMSCRIPTEN
      std::unique_lock lock(mutex);
      cv.wait(lock, [] { return !recordNext; });
#endif // PLATFORM_EMSCRIPTEN

#ifndef NDEBUG
      static u64 debugAudioCallbackPlaybackCallCount;
      ++debugAudioCallbackPlaybackCallCount;
      ASSERT(debugAudioCallbackRecordingCallCount == debugAudioCallbackPlaybackCallCount); // check that the mutex works
#endif // NDEBUG

      deinterleaveFromMono(outputStream, recordingToPlaybackBuffer, blockSize, Settings::audioSdlChannelInput, Settings::audioEffectVolume);
#ifdef SHR3D_COOP
      if (Global::inputCoop.toggled)
        deinterleaveFromMono(coopTempBuffer, recordingToPlaybackBuffer, blockSize, Settings::audioSdlChannelInputCoop, Settings::audioEffectVolumeCoop);
#endif // SHR3D_COOP

#ifndef PLATFORM_EMSCRIPTEN
      recordNext = !recordNext;
      cv.notify_one();
#endif // PLATFORM_EMSCRIPTEN
    }



#ifdef SHR3D_COOP
    f32* inCoopStereo[] = { &coopTempBuffer[0], &coopTempBuffer[blockSize] };
#endif // SHR3D_COOP

    {
      f32* inStereo[] = { &outputStream[0], &outputStream[blockSize] };

      f32 tempBuffer[Const::audioMaximumPossibleBlockSize * 2];
      f32* outStereo[] = { &tempBuffer[0], &tempBuffer[blockSize] };
      process_main(inStereo, outStereo, blockSize
#ifdef SHR3D_COOP
        , inCoopStereo
#endif // SHR3D_COOP
      );
      interleave(outputStream, tempBuffer, blockSize);
    }
  }

  static void getDeviceNames()
  {
    {
      const i32 deviceInputCount = SDL_GetNumAudioDevices(SDL_TRUE);
      for (i32 i = 0; i < deviceInputCount; ++i)
      {
        const char* deviceName = SDL_GetAudioDeviceName(i, SDL_TRUE);
        Global::audioSdlDevicesInput.push_back(deviceName);
      }
    }
    {
      const i32 deviceInputCount = SDL_GetNumAudioDevices(SDL_FALSE);
      for (i32 i = 0; i < deviceInputCount; ++i)
      {
        const char* deviceName = SDL_GetAudioDeviceName(i, SDL_FALSE);
        Global::audioSdlDevicesOutput.push_back(deviceName);
      }
    }
  }

  static void init()
  {
    const char* selectedDeviceInput = nullptr;
    const char* selectedDeviceOutput = nullptr;

    {
      const i32 deviceInputCount = SDL_GetNumAudioDevices(SDL_TRUE);
      for (i32 i = 0; i < deviceInputCount; ++i)
      {
        const char* deviceName = SDL_GetAudioDeviceName(i, SDL_TRUE);

        if (deviceName == Settings::audioSdlDeviceInput)
        {
          selectedDeviceInput = deviceName;
          break;
        }
      }
    }
    {
      const i32 deviceInputCount = SDL_GetNumAudioDevices(SDL_FALSE);
      for (i32 i = 0; i < deviceInputCount; ++i)
      {
        const char* deviceName = SDL_GetAudioDeviceName(i, SDL_FALSE);

        if (deviceName == Settings::audioSdlDeviceOutput)
        {
          selectedDeviceOutput = deviceName;
          break;
        }
      }
    }

    { // Input
      SDL_AudioSpec desiredRecordingSpec
      {
        .freq = Settings::audioSdlSampleRate,
        .format = AUDIO_F32LSB,
        .channels = 2,
        .samples = u16(Settings::audioSdlBlockSize),
        .callback = audioRecordingCallback
      };

      SDL_AudioSpec receivedRecordingSpec{};
      devid_in = SDL_OpenAudioDevice(selectedDeviceInput, SDL_TRUE, &desiredRecordingSpec, &receivedRecordingSpec, 0);
      ASSERT(receivedRecordingSpec.freq == desiredRecordingSpec.freq);
      ASSERT(receivedRecordingSpec.format == desiredRecordingSpec.format);
      ASSERT(receivedRecordingSpec.channels == desiredRecordingSpec.channels);
      ASSERT(receivedRecordingSpec.samples == desiredRecordingSpec.samples);
      ASSERT(devid_in != 0);
      SDL_PauseAudioDevice(devid_in, false);
    }

    { // Output
      SDL_AudioSpec desiredPlaybackSpec
      {
        .freq = Settings::audioSdlSampleRate,
        .format = AUDIO_F32LSB,
        .channels = 2,
        .samples = u16(Settings::audioSdlBlockSize),
        .callback = audioPlaybackCallback
      };

      SDL_AudioSpec receivedPlaybackSpec{};
      devid_out = SDL_OpenAudioDevice(selectedDeviceOutput, SDL_FALSE, &desiredPlaybackSpec, &receivedPlaybackSpec, 0);
      ASSERT(receivedPlaybackSpec.freq == desiredPlaybackSpec.freq);
      ASSERT(receivedPlaybackSpec.format == desiredPlaybackSpec.format);
      ASSERT(receivedPlaybackSpec.channels == desiredPlaybackSpec.channels);
      ASSERT(receivedPlaybackSpec.samples == desiredPlaybackSpec.samples);
      ASSERT(devid_out != 0);
      SDL_PauseAudioDevice(devid_out, false);
    }
  }
}
#endif // SHR3D_AUDIO_SDL

#ifdef SHR3D_AUDIO_WASAPI
namespace WASAPI
{
  static void getDeviceNames()
  {
    HRESULT hr;

    hr = CoInitialize(nullptr);
    ASSERT(hr == S_OK || S_FALSE /*was already initialized*/);

    {
      IMMDeviceEnumerator* pDeviceEnumerator = nullptr;
      hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pDeviceEnumerator);
      ASSERT(hr == S_OK);

      IMMDeviceCollection* pDeviceCaptureCollection = nullptr;
      hr = pDeviceEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &pDeviceCaptureCollection);
      ASSERT(hr == S_OK);
      {
        u32 deviceInputCount;
        hr = pDeviceCaptureCollection->GetCount(&deviceInputCount);
        ASSERT(hr == S_OK);

        for (u32 i = 0; i < deviceInputCount; ++i)
        {
          IMMDevice* pDevice = nullptr;
          hr = pDeviceCaptureCollection->Item(i, &pDevice);
          ASSERT(hr == S_OK);
          LPWSTR deviceId = nullptr;
          hr = pDevice->GetId(&deviceId);
          ASSERT(hr == S_OK);
          IPropertyStore* propertyStore = nullptr;
          hr = pDevice->OpenPropertyStore(STGM_READ, &propertyStore);
          ASSERT(hr == S_OK);
          PROPVARIANT friendlyName;
          hr = propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
          ASSERT(hr == S_OK);

          const std::wstring wDeviceName(friendlyName.bstrVal);
          const std::string deviceName(wDeviceName.begin(), wDeviceName.end());
          Global::audioWasapiDevicesInput.push_back(deviceName);
        }
      }

      IMMDeviceCollection* pDeviceRenderCollection = nullptr;
      hr = pDeviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &pDeviceRenderCollection);
      ASSERT(hr == S_OK);
      {
        u32 deviceOutputCount;
        hr = pDeviceRenderCollection->GetCount(&deviceOutputCount);
        ASSERT(hr == S_OK);

        for (u32 i = 0; i < deviceOutputCount; ++i)
        {
          IMMDevice* pDevice = nullptr;
          hr = pDeviceRenderCollection->Item(i, &pDevice);
          ASSERT(hr == S_OK);
          LPWSTR deviceId = nullptr;
          hr = pDevice->GetId(&deviceId);
          ASSERT(hr == S_OK);
          IPropertyStore* propertyStore = nullptr;
          hr = pDevice->OpenPropertyStore(STGM_READ, &propertyStore);
          ASSERT(hr == S_OK);
          PROPVARIANT friendlyName;
          hr = propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
          ASSERT(hr == S_OK);

          const std::wstring wDeviceName(friendlyName.bstrVal);
          const std::string deviceName(wDeviceName.begin(), wDeviceName.end());
          Global::audioWasapiDevicesOutput.push_back(deviceName);
        }
      }
    }
  }

  static IMMDevice* getDevice(IMMDeviceEnumerator* deviceEnumerator, const bool isCaptureDevice)
  {
    IMMDevice* device = nullptr;
    IMMDeviceCollection* deviceCollection = nullptr;
    HRESULT hr = deviceEnumerator->EnumAudioEndpoints(isCaptureDevice ? eCapture : eRender, DEVICE_STATE_ACTIVE, &deviceCollection);
    ASSERT(hr == S_OK);
    {
      u32 deviceInputCount;
      hr = deviceCollection->GetCount(&deviceInputCount);
      ASSERT(hr == S_OK);

      for (u32 i = 0; i < deviceInputCount; ++i)
      {
        IMMDevice* pDevice = nullptr;
        hr = deviceCollection->Item(i, &pDevice);
        ASSERT(hr == S_OK);
        LPWSTR deviceId = nullptr;
        hr = pDevice->GetId(&deviceId);
        ASSERT(hr == S_OK);
        IPropertyStore* propertyStore = nullptr;
        hr = pDevice->OpenPropertyStore(STGM_READ, &propertyStore);
        ASSERT(hr == S_OK);
        PROPVARIANT friendlyName;
        hr = propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
        ASSERT(hr == S_OK);

        const std::wstring wDeviceName(friendlyName.bstrVal);
        const std::string deviceName(wDeviceName.begin(), wDeviceName.end());
        if (deviceName == (isCaptureDevice ? Settings::audioWasapiDeviceInput : Settings::audioWasapiDeviceOutput))
        {
          hr = deviceCollection->Item(i, &device);
          goto found_device;
        }
      }
    }
    hr = deviceEnumerator->GetDefaultAudioEndpoint(isCaptureDevice ? eCapture : eRender, eMultimedia, &device);
  found_device:
    ASSERT(hr == S_OK);
    deviceCollection->Release();
    return device;
  }

  static void checkAndModifyFormat(IAudioClient3* audioClient, WAVEFORMATEX* format)
  {
    if (Settings::audioWasapiExclusiveMode)
    {
      if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE && reinterpret_cast<WAVEFORMATEXTENSIBLE*>(format)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
      {
        WAVEFORMATEXTENSIBLE* waveFormatExtensible = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(format);
        waveFormatExtensible->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
        waveFormatExtensible->Format.wBitsPerSample = 32;
        waveFormatExtensible->Format.nBlockAlign = (format->wBitsPerSample / 8) * format->nChannels;
        waveFormatExtensible->Format.nAvgBytesPerSec = waveFormatExtensible->Format.nSamplesPerSec * waveFormatExtensible->Format.nBlockAlign;
        waveFormatExtensible->Samples.wValidBitsPerSample = 24; // 24 Bit PCM like most audio interfaces
        HRESULT hr = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, format, nullptr);
        ASSERT(hr == S_OK);
      }
      else
      {
        ASSERT(false);
      }
    }
    else
    {
      WAVEFORMATEX* matchFormat;
      HRESULT hr = audioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, format, &matchFormat);
      ASSERT(hr == S_OK);
      ASSERT(matchFormat == nullptr);
    }
  }

  static void worker()
  {
    {
      typedef HANDLE(__stdcall* TAvSetMmThreadCharacteristicsPtr)(LPCWSTR TaskName, LPDWORD TaskIndex);
      // Attempt to assign "Pro Audio" characteristic to thread
      HMODULE AvrtDll = LoadLibraryW(L"AVRT.dll");
      if (AvrtDll) {
        DWORD taskIndex = 0;
        TAvSetMmThreadCharacteristicsPtr AvSetMmThreadCharacteristicsPtr =
          (TAvSetMmThreadCharacteristicsPtr)(void(*)()) GetProcAddress(AvrtDll, "AvSetMmThreadCharacteristicsW");
        AvSetMmThreadCharacteristicsPtr(L"Pro Audio", &taskIndex);
        FreeLibrary(AvrtDll);
      }
    }

    HRESULT hr = CoInitialize(nullptr);
    ASSERT(hr == S_OK);

    IMMDeviceEnumerator* deviceEnumerator = nullptr;
    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnumerator);
    ASSERT(hr == S_OK);

    //IMMDevice* capture = nullptr;
    //hr = deviceEnumerator->GetDefaultAudioEndpoint(eCapture, eMultimedia, &capture);
    //ASSERT(hr == S_OK);

    //IMMDevice* renderer = nullptr;
    //hr = deviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &renderer);
    //ASSERT(hr == S_OK);

    IMMDevice* capture = getDevice(deviceEnumerator, true);
    IMMDevice* renderer = getDevice(deviceEnumerator, false);
    ASSERT(renderer != nullptr);

    deviceEnumerator->Release();

#ifdef SHR3D_WINDOW_SDL
    SDL_SysWMinfo wmInfo;
    SDL_VERSION(&wmInfo.version);
    SDL_GetWindowWMInfo(Global::window, &wmInfo);
#ifdef PLATFORM_WINDOWS
    HWND window = wmInfo.info.win.window;
#else // PLATFORM_WINDOWS
    void* window = (void*)wmInfo.info.x11.window;
#endif // PLATFORM_WINDOWS
#else // SHR3D_WINDOW_SDL
    HWND window = Global::window;
#endif // SHR3D_WINDOW_SDL

    if (capture == nullptr)
    {
      MessageBoxA(window, "No recording device found.", "WASAPI Audio problem detected", MB_ICONERROR);
      return;
    }
    if (renderer == nullptr)
    {
      MessageBoxA(window, "No playback device found.", "WASAPI Audio problem detected", MB_ICONERROR);
      return;
    }

    IAudioClient3* captureClient = nullptr;
    hr = capture->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr, (void**)&captureClient);
    ASSERT(hr == S_OK);

    IAudioClient3* renderClient = nullptr;
    hr = renderer->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr, (void**)&renderClient);
    ASSERT(hr == S_OK);

    WAVEFORMATEX* captureFormat = nullptr;
    hr = captureClient->GetMixFormat(&captureFormat);
    ASSERT(hr == S_OK);

    WAVEFORMATEX* renderFormat = nullptr;
    hr = renderClient->GetMixFormat(&renderFormat);
    ASSERT(hr == S_OK);

    if (captureFormat->nChannels != 2 ||
      captureFormat->nChannels != renderFormat->nChannels ||
      captureFormat->nSamplesPerSec != 48000 ||
      captureFormat->nSamplesPerSec != renderFormat->nSamplesPerSec ||
      captureFormat->nBlockAlign != 8 ||
      captureFormat->nBlockAlign != renderFormat->nBlockAlign ||
      captureFormat->wBitsPerSample != 32 ||
      captureFormat->wBitsPerSample != renderFormat->wBitsPerSample)
    {
      std::string errorMsg = "Looks like your audio playback and recording devices are not working well together.\nHere is what we are working with:";
      errorMsg += "\n\n - Playback device:";
      errorMsg += "\n    Channels: " + std::to_string(renderFormat->nChannels);
      errorMsg += "\n    SampleRate: " + std::to_string(renderFormat->nSamplesPerSec);
      errorMsg += "\n    BlockAlign: " + std::to_string(renderFormat->nBlockAlign);
      errorMsg += "\n    BitsPerSample: " + std::to_string(renderFormat->wBitsPerSample);
      errorMsg += "\n\n - Recording device:";
      errorMsg += "\n    Channels: " + std::to_string(captureFormat->nChannels);
      errorMsg += "\n    SampleRate: " + std::to_string(captureFormat->nSamplesPerSec);
      errorMsg += "\n    BlockAlign: " + std::to_string(captureFormat->nBlockAlign);
      errorMsg += "\n    BitsPerSample: " + std::to_string(captureFormat->wBitsPerSample);
      errorMsg += "\n\n - How both should look like:";
      errorMsg += "\n    Channels: 2";
      errorMsg += "\n    SampleRate: 48000";
      errorMsg += "\n    BlockAlign: 8";
      errorMsg += "\n    BitsPerSample: 32";
      errorMsg += "\n\nSolution: If you use professional audio hardware, switch the audio system in Shr3D to ASIO. Otherwise, check your audio devices.";
      MessageBoxA(window, errorMsg.c_str(), "Potential WASAPI Audio problem detected", MB_ICONWARNING);
    }

    checkAndModifyFormat(captureClient, captureFormat);
    checkAndModifyFormat(renderClient, renderFormat);

    if (Settings::audioWasapiExclusiveMode)
    {
      {
        REFERENCE_TIME hnsRequestedDuration = 0;
        hr = captureClient->GetDevicePeriod(NULL, &hnsRequestedDuration);
        ASSERT(hr == S_OK);
        hr = captureClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, 0, hnsRequestedDuration, hnsRequestedDuration, captureFormat, nullptr);
        ASSERT(hr == S_OK);
      }
      {
        REFERENCE_TIME hnsRequestedDuration = 0;
        hr = renderClient->GetDevicePeriod(NULL, &hnsRequestedDuration);
        ASSERT(hr == S_OK);
        hr = renderClient->Initialize(AUDCLNT_SHAREMODE_EXCLUSIVE, 0, hnsRequestedDuration, hnsRequestedDuration, renderFormat, nullptr);
        ASSERT(hr == S_OK);
      }
    }
    else
    {
      hr = captureClient->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 200000, 0, captureFormat, nullptr);
      ASSERT(hr == S_OK);
      hr = renderClient->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 200000, 0, renderFormat, nullptr);
      ASSERT(hr == S_OK);
    }

    IAudioCaptureClient* captureService = nullptr;
    hr = captureClient->GetService(__uuidof(IAudioCaptureClient), (void**)&captureService);
    ASSERT(hr == S_OK);

    IAudioRenderClient* renderService = nullptr;
    hr = renderClient->GetService(__uuidof(IAudioRenderClient), (void**)&renderService);
    ASSERT(hr == S_OK);

    hr = captureClient->Start();
    ASSERT(hr == S_OK);

    hr = renderClient->Start();
    ASSERT(hr == S_OK);

    const bool isCapture32BitPCM = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(captureService)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM;
    const bool isRender32BitPCM = reinterpret_cast<WAVEFORMATEXTENSIBLE*>(renderClient)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM;

    for (;;)
    {
      BYTE* captureBuffer;
      DWORD flags;
      hr = captureService->GetBuffer(&captureBuffer, &Global::audioWasapiBlockSize, &flags, nullptr, nullptr);
      if (hr == AUDCLNT_S_BUFFER_EMPTY)
        continue;
      ASSERT(hr == S_OK);

      hr = captureService->ReleaseBuffer(Global::audioWasapiBlockSize);
      ASSERT(hr == S_OK);

      BYTE* renderBuffer;
      hr = renderService->GetBuffer(Global::audioWasapiBlockSize, &renderBuffer);
      //if (hr == AUDCLNT_S_BUFFER_EMPTY)
      //  continue;
      if (hr == AUDCLNT_E_BUFFER_TOO_LARGE)
        continue;
      ASSERT(hr == S_OK);

      //REFERENCE_TIME latency;
      //hr = captureClient->GetStreamLatency(&latency);
      //if (latency != 0)
      //  latency = latency;
      //hr = renderClient->GetStreamLatency(&latency);
      //if (latency != 0)
      //  latency = latency;

      //REFERENCE_TIME minTime, maxTime, engineTime;
      //hr = captureClient->GetBufferSizeLimits(format, TRUE, &minTime, &maxTime);

      //hr = captureClient->GetBufferSize(&Global::audioWasapiInputLatencyFrames);
      //ASSERT(hr == S_OK);
      //hr = renderClient->GetBufferSize(&Global::audioWasapiOutputLatencyFrames);
      //ASSERT(hr == S_OK);

      f32* captureBuffer_f32 = reinterpret_cast<f32*>(captureBuffer);

      if (isCapture32BitPCM)
        for (u32 i = 0; i < Global::audioWasapiBlockSize * 2; ++i)
          captureBuffer_f32[i] = f32(reinterpret_cast<const i32*>(captureBuffer)[i] / f32(I32::max));

      f32* renderBuffer_f32 = reinterpret_cast<f32*>(renderBuffer);

      deinterleaveFromMono(renderBuffer_f32, captureBuffer_f32, Global::audioWasapiBlockSize, Settings::audioWasapiChannelInput0, Settings::audioEffectVolume);

      f32* inStereo[] = { &renderBuffer_f32[0], &renderBuffer_f32[Global::audioWasapiBlockSize] };
      f32* outStereo[] = { &captureBuffer_f32[0], &captureBuffer_f32[Global::audioWasapiBlockSize] };

#ifdef SHR3D_COOP
      f32 coopTempBuffer[Const::audioMaximumPossibleBlockSize * 2];
      if (Global::inputCoop.toggled)
        deinterleaveFromMono(coopTempBuffer, captureBuffer_f32, Global::audioWasapiBlockSize, Settings::audioWasapiChannelInput1, Settings::audioEffectVolumeCoop);
      f32* inCoopStereo[] = { &coopTempBuffer[0], &coopTempBuffer[Global::audioWasapiBlockSize] };
#endif // SHR3D_COOP

      process_main(inStereo, outStereo, Global::audioWasapiBlockSize
#ifdef SHR3D_COOP
        , inCoopStereo
#endif // SHR3D_COOP
      );

      interleave(renderBuffer_f32, captureBuffer_f32, Global::audioWasapiBlockSize);

      if (isRender32BitPCM)
        for (u32 i = 0; i < Global::audioWasapiBlockSize * 2; ++i)
          reinterpret_cast<i32*>(renderBuffer)[i] = i32(renderBuffer_f32[i] * f32(I32::max));

      hr = renderService->ReleaseBuffer(Global::audioWasapiBlockSize, 0);
      ASSERT(hr == S_OK);
    }
  }

  static void init()
  {
    static std::thread worker_(worker);
  }
}
#endif // SHR3D_AUDIO_WASAPI

#ifdef SHR3D_AUDIO_ASIO
static ASIO::IASIO* asioDriverDevice = nullptr;

#if defined(SHR3D_SFX_CORE_HEXFIN_DIVIDED) && defined(SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED)
static ASIO::IASIO* asioDriverSecondDeviceForTunerDivided = nullptr;
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED && SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED

namespace ASIO
{
  enum {
    // number of input and outputs supported by the host application
    // you can change these to higher or lower values
    kMaxInputChannels = 32,
    kMaxOutputChannels = 32
  };

  //struct AsioDriverInfo
  //{
  //  //bool postOutput;

  //  ASIO::BufferInfo bufferInfos[kMaxInputChannels + kMaxOutputChannels]; // buffer info's
  //  ASIO::ChannelInfo channelInfos[kMaxInputChannels + kMaxOutputChannels]; // channel info's
  //  // The above two arrays share the same indexing, as the data in them are linked together

  //  // Information from ASIOGetSamplePosition()
  //  // data is converted to double floats for easier use, however 64 bit integer can be used, too
  //  //f64         nanoSeconds;
  //  //f64         samples;
  //  //f64         tcSamples;	// time code samples

  //  // bufferSwitchTimeInfo()
  //  //ASIO::Time       tInfo;			// time info state
  //  //u32  sysRefTime;      // system reference time, when bufferSwitch() was called
  //};

  static u32 get_sys_reference_time()
  {	// get the system reference time
    return timeGetTime();
  }
#define TEST_RUN_TIME  20.0		// run for 20 seconds

  static ASIO::Manager asioManager;
  static ASIO::BufferInfo bufferInfos[kMaxInputChannels + kMaxOutputChannels];
  static ASIO::ChannelInfo channelInfos[kMaxInputChannels + kMaxOutputChannels];
#ifdef SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
  static ASIO::BufferInfo asioDriverInfoSecondDeviceForTunerDividedBufferInfos[kMaxInputChannels + kMaxOutputChannels];
  static ASIO::ChannelInfo asioDriverInfoSecondDeviceForTunerDividedChannelInfos[kMaxInputChannels + kMaxOutputChannels];
#endif // SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED

  static f32* buffer_into_f32(void* buffer, const ASIO::SampleType inputSampleType, const i32 blockSize)
  {
    // this function expects 4 bytes per sample so i32 or f32
    switch (inputSampleType)
    {
    case ASIO::SampleType::Int32LSB: // no idea if all audio interfaces are like mine. Probably not...
      for (i32 i = 0; i < blockSize; ++i)
        reinterpret_cast<f32*>(buffer)[i] = f32(reinterpret_cast<const i32*>(buffer)[i] / f32(I32::max));
      return reinterpret_cast<f32*>(buffer);
    case ASIO::SampleType::Float32LSB: // already in the right format!
      return reinterpret_cast<f32*>(buffer);
    default:
      unreachable();
    }
  }

  static void buffer_from_f32(f32* buffer, const ASIO::SampleType inputSampleType, const i32 blockSize)
  {
    switch (inputSampleType)
    {
    case ASIO::SampleType::Int32LSB: // no idea if all audio interfaces are like mine. Probably not...
      for (i32 i = 0; i < blockSize; ++i)
        reinterpret_cast<i32*>(buffer)[i] = i32(buffer[i] * f32(I32::max));
      return;
    case ASIO::SampleType::Float32LSB: // already in the right format!
      return;
    }
    ASSERT(false);
  }

#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
  static void asioTunerDivided(const ASIO::BufferInfo* bufferInfos_, const i32 index, const i32 blockSize)
  {
    memcpy(TunerThread::inBlock0, bufferInfos_[Settings::audioAsioDividedPickupChannelString0].buffers[index], blockSize * sizeof(f32));
    memcpy(TunerThread::inBlock1, bufferInfos_[Settings::audioAsioDividedPickupChannelString1].buffers[index], blockSize * sizeof(f32));
    memcpy(TunerThread::inBlock2, bufferInfos_[Settings::audioAsioDividedPickupChannelString2].buffers[index], blockSize * sizeof(f32));
    memcpy(TunerThread::inBlock3, bufferInfos_[Settings::audioAsioDividedPickupChannelString3].buffers[index], blockSize * sizeof(f32));
    memcpy(TunerThread::inBlock4, bufferInfos_[Settings::audioAsioDividedPickupChannelString4].buffers[index], blockSize * sizeof(f32));
    memcpy(TunerThread::inBlock5, bufferInfos_[Settings::audioAsioDividedPickupChannelString5].buffers[index], blockSize * sizeof(f32));

    //TunerThread::tick();
  }
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED

  static ASIO::Time* bufferSwitchTimeInfo(ASIO::Time* /*timeInfo*/, i32 index, ASIO::Bool /*processNow*/)
  {
    // store the timeInfo for later use
    //tInfo = *timeInfo;

    // get the time stamp of the buffer, not necessary if no
    // synchronization to other media is required
    //if (timeInfo->timeInfo.flags & kSystemTimeValid)
    //  nanoSeconds = timeInfo->timeInfo.systemTime;
    //else
    //  nanoSeconds = 0;

    //if (timeInfo->timeInfo.flags & kSamplePositionValid)
    //  samples = timeInfo->timeInfo.samplePosition;
    //else
    //  samples = 0;

    //if (timeInfo->timeCode.flags & kTcValid)
    //  tcSamples = timeInfo->timeCode.timeCodeSamples;
    //else
    //  tcSamples = 0;

    // get the system reference time
    //sysRefTime = get_sys_reference_time();

    Global::audioAsioBlockSize = Settings::audioAsioBlockSize >= 1 ? Settings::audioAsioBlockSize : Global::audioAsioBufferPreferredSize;

    {
      u32 convertF32Channels = 0;
      convertF32Channels |= 1 << Settings::audioAsioChannelInput0;
      convertF32Channels |= 1 << Settings::audioAsioChannelInput1;
#ifdef SHR3D_SFX_CORE_HEXFIN
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
      if (Settings::audioAsioDividedPickup
#ifdef SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
        && Settings::audioAsioSecondDeviceForTunerDivided.empty()
#endif // SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
        )
      {
        convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString0;
        convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString1;
        convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString2;
        convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString3;
        convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString4;
        convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString5;
      }
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
#endif // SHR3D_SFX_CORE_HEXFIN
      for (u8 i = 0; i < 32; ++i)
      {
        if (convertF32Channels & (1 << i))
        {
          buffer_into_f32(bufferInfos[i].buffers[index], channelInfos[i].type, Global::audioAsioBlockSize);
        }
      }
    }

    f32* inStereo[] = { reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioChannelInput0].buffers[index]), reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioChannelInput1].buffers[index]) };

#ifdef SHR3D_SFX_CORE_HEXFIN
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
    if (Settings::audioAsioDividedPickup)
    {
#ifdef SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
      if (Settings::audioAsioSecondDeviceForTunerDivided.empty())
#endif // SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
      {
        if (Settings::audioAsioDividedPickupAsMainInput)
        {
          for (i32 i = 0; i < Global::audioAsioBlockSize; ++i)
          {
            inStereo[0][i] = 0.166666667f * (
              reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioDividedPickupChannelString0].buffers[index])[i] +
              reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioDividedPickupChannelString1].buffers[index])[i] +
              reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioDividedPickupChannelString2].buffers[index])[i] +
              reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioDividedPickupChannelString3].buffers[index])[i] +
              reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioDividedPickupChannelString4].buffers[index])[i] +
              reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioDividedPickupChannelString5].buffers[index])[i]
              );
          }
        }

        asioTunerDivided(bufferInfos, index, Global::audioAsioBlockSize);
      }
    }
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
#endif // SHR3D_SFX_CORE_HEXFIN

    if (Settings::audioAsioChannelInput0 == Settings::audioAsioChannelInput1) // For external Amp use, you might already got a stereo signal.
    {
      static f32 inMonoRight[Const::audioMaximumPossibleBlockSize]; // This buffer is only needed for a mono input signal. Fx signal chain needs a stereo signal for panning effects and such.
      inStereo[1] = inMonoRight;
      memcpy(inStereo[1], inStereo[0], Global::audioAsioBlockSize * sizeof(f32)); // mono to stereo
    }

    void* outLeft_void = bufferInfos[Settings::audioAsioChannelOutput + Global::audioAsioInputCount].buffers[index];
    void* outRight_void = bufferInfos[Settings::audioAsioChannelOutput + Global::audioAsioInputCount + 1].buffers[index];

    f32* outStereo[] = { reinterpret_cast<f32*>(outLeft_void), reinterpret_cast<f32*>(outRight_void) };

#ifdef SHR3D_COOP
    f32* inCoopStereo[] = { reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioChannelInputCoop0].buffers[index]), reinterpret_cast<f32*>(bufferInfos[Settings::audioAsioChannelInputCoop1].buffers[index]) };
    if (Global::inputCoop.toggled)
    {
      if (Settings::audioAsioChannelInputCoop0 == Settings::audioAsioChannelInputCoop1) // For external Amp use, you might already got a stereo signal.
      {
        static f32 inCoopMonoRight[Const::audioMaximumPossibleBlockSize]; // fx signal chain needs a stereo signal for panning effects and such
        inCoopStereo[1] = inCoopMonoRight;
        memcpy(inCoopStereo[1], inCoopStereo[0], Global::audioAsioBlockSize * sizeof(f32)); // mono to stereo
      }
    }

#endif // SHR3D_COOP

    process_main(inStereo, outStereo, Global::audioAsioBlockSize
#ifdef SHR3D_COOP
      , inCoopStereo
#endif // SHR3D_COOP
    );

    buffer_from_f32(outStereo[0], channelInfos[Settings::audioAsioChannelOutput + Global::audioAsioInputCount].type, Global::audioAsioBlockSize);
    buffer_from_f32(outStereo[1], channelInfos[Settings::audioAsioChannelOutput + Global::audioAsioInputCount + 1].type, Global::audioAsioBlockSize);

    // finally if the driver supports the ASIOOutputReady() optimization, do it here, all data are in place
    //if (postOutput)
    asioDriverDevice->outputReady();

    return 0L;
  }

#if defined(SHR3D_SFX_CORE_HEXFIN) && defined(SHR3D_SFX_CORE_HEXFIN_DIVIDED) && defined(SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED)
  static ASIO::Time* bufferSwitchTimeInfo2(ASIO::Time* /*timeInfo*/, i32 index, ASIO::Bool /*processNow*/)
  {
    // store the timeInfo for later use
    //g_asioDriverInfoSecondDeviceForTunerDivided.tInfo = *timeInfo;

    // get the time stamp of the buffer, not necessary if no
    // synchronization to other media is required
    //if (timeInfo->timeInfo.flags & kSystemTimeValid)
    //  g_asioDriverInfoSecondDeviceForTunerDivided.nanoSeconds = timeInfo->timeInfo.systemTime;
    //else
    //  g_asioDriverInfoSecondDeviceForTunerDivided.nanoSeconds = 0;

    //if (timeInfo->timeInfo.flags & kSamplePositionValid)
    //  g_asioDriverInfoSecondDeviceForTunerDivided.samples = timeInfo->timeInfo.samplePosition;
    //else
    //  g_asioDriverInfoSecondDeviceForTunerDivided.samples = 0;

    //if (timeInfo->timeCode.flags & kTcValid)
    //  g_asioDriverInfoSecondDeviceForTunerDivided.tcSamples = timeInfo->timeCode.timeCodeSamples;
    //else
    //  g_asioDriverInfoSecondDeviceForTunerDivided.tcSamples = 0;

    // get the system reference time
    //g_asioDriverInfoSecondDeviceForTunerDivided.sysRefTime = get_sys_reference_time();

    {
      u32 convertF32Channels = 0;
      convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString0;
      convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString1;
      convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString2;
      convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString3;
      convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString4;
      convertF32Channels |= 1 << Settings::audioAsioDividedPickupChannelString5;

      for (u8 i = 0; i < 32; ++i)
      {
        if (convertF32Channels & (1 << i))
        {
          buffer_into_f32(asioDriverInfoSecondDeviceForTunerDividedBufferInfos[i].buffers[index], asioDriverInfoSecondDeviceForTunerDividedChannelInfos[i].type, Global::audioAsioBlockSize);
        }
      }
    }

    asioTunerDivided(asioDriverInfoSecondDeviceForTunerDividedBufferInfos, index, Global::audioAsioBlockSize);

    //if (g_asioDriverInfoSecondDeviceForTunerDivided.postOutput)
    asioDriverSecondDeviceForTunerDivided->outputReady();

    return 0L;
  }

  static void bufferSwitch2(i32 index, ASIO::Bool processNow)
  {	// the actual processing callback.
    // Beware that this is normally in a seperate thread, hence be sure that you take care
    // about thread synchronization. This is omitted here for simplicity.

    // as this is a "back door" into the bufferSwitchTimeInfo a timeInfo needs to be created
    // though it will only set the timeInfo.samplePosition and timeInfo.systemTime fields and the according flags
    //ASIO::Time timeInfo{};

    // get the time stamp of the buffer, not necessary if no
    // synchronization to other media is required
    //if (asioDriverSecondDeviceForTunerDivided->getSamplePosition(&timeInfo.timeInfo.samplePosition, &timeInfo.timeInfo.systemTime) == ASIO::Error::OK)
    //  timeInfo.timeInfo.flags = kSystemTimeValid | kSamplePositionValid;

    bufferSwitchTimeInfo2(/*&timeInfo*/nullptr, index, processNow);
  }
#endif // SHR3D_SFX_CORE_HEXFIN && SHR3D_SFX_CORE_HEXFIN_DIVIDED && SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED

  static void bufferSwitch(i32 index, ASIO::Bool processNow)
  {	// the actual processing callback.
    // Beware that this is normally in a seperate thread, hence be sure that you take care
    // about thread synchronization. This is omitted here for simplicity.

    // as this is a "back door" into the bufferSwitchTimeInfo a timeInfo needs to be created
    // though it will only set the timeInfo.samplePosition and timeInfo.systemTime fields and the according flags
    //ASIO::Time timeInfo{};

    // get the time stamp of the buffer, not necessary if no
    // synchronization to other media is required
    //if (asioDriverDevice->getSamplePosition(&timeInfo.timeInfo.samplePosition, &timeInfo.timeInfo.systemTime) == ASIO::Error::OK)
    //  timeInfo.timeInfo.flags = kSystemTimeValid | kSamplePositionValid;

    bufferSwitchTimeInfo(/*&timeInfo*/nullptr, index, processNow);
  }

  static void sampleRateChanged(ASIO::SampleRate /*sRate*/)
  {
    // do whatever you need to do if the sample rate changed
    // usually this only happens during external sync.
    // Audio processing is not stopped by the driver, actual sample rate
    // might not have even changed, maybe only the sample rate status of an
    // AES/EBU or S/PDIF digital input at the audio device.
    // You might have to update time/sample related conversion routines, etc.
  }

  static i32 asioMessages(i32 selector, i32 value, void* /*message*/, f64* /*opt*/)
  {
    // currently the parameters "value", "message" and "opt" are not used.
    i32 ret = 0;
    switch (selector)
    {
    case kAsioSelectorSupported:
      if (value == kAsioResetRequest
        || value == kAsioEngineVersion
        || value == kAsioResyncRequest
        || value == kAsioLatenciesChanged
        // the following three were added for ASIO 2.0, you don't necessarily have to support them
        || value == kAsioSupportsTimeInfo
        || value == kAsioSupportsTimeCode
        || value == kAsioSupportsInputMonitor)
        ret = 1L;
      break;
    case kAsioResetRequest:
      // defer the task and perform the reset of the driver during the next "safe" situation
      // You cannot reset the driver right now, as this code is called from the driver.
      // Reset the driver is done by completely destruct is. I.e. ASIOStop(), ASIODisposeBuffers(), Destruction
      // Afterwards you initialize the driver again.
      //stopped;  // In this sample the processing will just stop
      ret = 1L;
      break;
    case kAsioResyncRequest:
      // This informs the application, that the driver encountered some non fatal data loss.
      // It is used for synchronization purposes of different media.
      // Added mainly to work around the Win16Mutex problems in Windows 95/98 with the
      // Windows Multimedia system, which could loose data because the Mutex was hold too i32
      // by another thread.
      // However a driver can issue it in other situations, too.
      ret = 1L;
      break;
    case kAsioLatenciesChanged:
      // This will inform the host application that the drivers were latencies changed.
      // Beware, it this does not mean that the buffer sizes have changed!
      // You might need to update internal delay data.
      ret = 1L;
      break;
    case kAsioEngineVersion:
      // return the supported ASIO version of the host application
      // If a host applications does not implement this selector, ASIO 1.0 is assumed
      // by the driver
      ret = 2L;
      break;
    case kAsioSupportsTimeInfo:
      // informs the driver wether the asioCallbacks.bufferSwitchTimeInfo() callback
      // is supported.
      // For compatibility with ASIO 1.0 drivers the host application should always support
      // the "old" bufferSwitch method, too.
      ret = 1;
      break;
    case kAsioSupportsTimeCode:
      // informs the driver wether application is interested in time code info.
      // If an application does not need to know about time code, the driver has less work
      // to do.
      ret = 0;
      break;
    }
    return ret;
  }

  static void getDeviceNames()
  {
    for (i32 i = 0; i < asioManager.numDrivers; i++)
    {
      char deviceName[32];
      if (!asioManager.GetDriverName(i, deviceName, 32))
        Global::audioAsioDevices.push_back(deviceName);
    }
  }

  static void init()
  {
    std::string selectedDevice;
    for (i32 i = 0; i < asioManager.numDrivers; i++)
    {
      char deviceName[64];
      if (!asioManager.GetDriverName(i, deviceName, 32))
      {
        if (deviceName == Settings::audioAsioDevice)
        {
          selectedDevice = deviceName;
          break;
        }
      }
    }

    asioDriverDevice = asioManager.loadDriver(selectedDevice.c_str());
    if (asioDriverDevice == nullptr)
      return;

    if (!asioDriverDevice->init(nullptr))
      return;

    ASIO::Error asioError = ASIO::Error::OK;

    {
      asioError = asioDriverDevice->getChannels(&Global::audioAsioInputCount, &Global::audioAsioOutputCount);
      ASSERT(asioError == ASIO::Error::OK);
      ASSERT(Global::audioAsioInputCount >= 1);
      ASSERT(Global::audioAsioOutputCount >= 2);

      asioError = asioDriverDevice->getBufferSize(&Global::audioAsioBufferMinSize, &Global::audioAsioBufferMaxSize, &Global::audioAsioBufferPreferredSize, &Global::audioAsioBufferGranularity);
      ASSERT(asioError == ASIO::Error::OK);

      ASIO::SampleRate sampleRate;
      asioError = asioDriverDevice->getSampleRate(&sampleRate);
      Global::audioAsioSampleRate = i32(sampleRate);
      ASSERT(asioError == ASIO::Error::OK);
      if (Settings::audioAsioSampleRate > 0 && Global::audioAsioSampleRate != Settings::audioAsioSampleRate)
      {
        asioError = asioDriverDevice->canSampleRate(f64(Settings::audioAsioSampleRate));
        ASSERT(asioError == ASIO::Error::OK);

        asioError = asioDriverDevice->setSampleRate(f64(Settings::audioAsioSampleRate));
        ASSERT(asioError == ASIO::Error::OK);

        Global::audioAsioSampleRate = Settings::audioAsioSampleRate;
      }
      //if (asioDriverDevice->outputReady() == ASIO::Error::OK)
      //  postOutput = true;
      //else
      //  postOutput = false;
    }

    { // setup buffers
      i32 i = 0;
      for (; i < Global::audioAsioInputCount; ++i)
      {
        bufferInfos[i].isInput = ASIO::Bool::True;
        bufferInfos[i].channelNum = i;
      }

      if (Global::audioAsioOutputCount > kMaxOutputChannels)
        Global::audioAsioOutputCount = kMaxOutputChannels;
      else
        Global::audioAsioOutputCount = Global::audioAsioOutputCount;
      for (i32 o = 0; o < Global::audioAsioOutputCount; ++o)
      {
        bufferInfos[i + o].isInput = ASIO::Bool::False;
        bufferInfos[i + o].channelNum = o;
      }

      Settings::audioAsioChannelOutput = clamp(Settings::audioAsioChannelOutput, 0, i32(Global::audioAsioOutputCount) - 2);
      Settings::audioAsioChannelInput0 = clamp(Settings::audioAsioChannelInput0, 0, i32(Global::audioAsioInputCount) - 1);
      Settings::audioAsioChannelInput1 = clamp(Settings::audioAsioChannelInput1, 0, i32(Global::audioAsioInputCount) - 1);
#ifdef SHR3D_COOP
      Settings::audioAsioChannelInputCoop0 = clamp(Settings::audioAsioChannelInputCoop0, 0, i32(Global::audioAsioInputCount) - 1);
      Settings::audioAsioChannelInputCoop1 = clamp(Settings::audioAsioChannelInputCoop1, 0, i32(Global::audioAsioInputCount) - 1);
#endif // SHR3D_COOP
#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
#ifdef SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
      if (Settings::audioAsioSecondDeviceForTunerDivided.empty())
#endif // SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
      {
        Settings::audioAsioDividedPickupChannelString0 = clamp(Settings::audioAsioDividedPickupChannelString0, 0, i32(Global::audioAsioInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString1 = clamp(Settings::audioAsioDividedPickupChannelString1, 0, i32(Global::audioAsioInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString2 = clamp(Settings::audioAsioDividedPickupChannelString2, 0, i32(Global::audioAsioInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString3 = clamp(Settings::audioAsioDividedPickupChannelString3, 0, i32(Global::audioAsioInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString4 = clamp(Settings::audioAsioDividedPickupChannelString4, 0, i32(Global::audioAsioInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString5 = clamp(Settings::audioAsioDividedPickupChannelString5, 0, i32(Global::audioAsioInputCount) - 1);
      }
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED
    }

    {
      static ASIO::Callbacks asioCallbacks{
        .bufferSwitch = &bufferSwitch,
        .sampleRateDidChange = &sampleRateChanged,
        .asioMessage = &asioMessages,
        .bufferSwitchTimeInfo = &bufferSwitchTimeInfo
      };

      const i32 blockSize = Settings::audioAsioBlockSize >= 1 ? Settings::audioAsioBlockSize : Global::audioAsioBufferPreferredSize;

      asioError = asioDriverDevice->createBuffers(bufferInfos, Global::audioAsioInputCount + Global::audioAsioOutputCount, blockSize, &asioCallbacks);
      ASSERT(asioError == ASIO::Error::OK);
    }

    for (i32 i = 0; i < Global::audioAsioInputCount + Global::audioAsioOutputCount; ++i)
    {
      channelInfos[i].channel = bufferInfos[i].channelNum;
      channelInfos[i].isInput = bufferInfos[i].isInput;
      asioError = asioDriverDevice->getChannelInfo(&channelInfos[i]);
      ASSERT(asioError == ASIO::Error::OK);
    }

    asioError = asioDriverDevice->start();
    ASSERT(asioError == ASIO::Error::OK);

    asioError = asioDriverDevice->getLatencies(&Global::audioAsioInputLatencyFrames, &Global::audioAsioOutputLatencyFrames);
    ASSERT(asioError == ASIO::Error::OK);

#if defined(SHR3D_SFX_CORE_HEXFIN) && defined(SHR3D_SFX_CORE_HEXFIN_DIVIDED) && defined(SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED)
    if (Settings::audioAsioDividedPickup && !Settings::audioAsioSecondDeviceForTunerDivided.empty())
    {
      std::string selectedDevice2;
      for (i32 i = 0; i < asioManager.numDrivers; i++)
      {
        char deviceName[64];
        if (!asioManager.GetDriverName(i, deviceName, 32))
        {
          if (deviceName == Settings::audioAsioSecondDeviceForTunerDivided)
          {
            selectedDevice2 = deviceName;
            break;
          }
        }
      }

      asioDriverSecondDeviceForTunerDivided = asioManager.loadDriver(selectedDevice2.c_str());
      if (asioDriverSecondDeviceForTunerDivided == nullptr)
        return;

      if (!asioDriverSecondDeviceForTunerDivided->init(nullptr))
        return;

      {
        {
          i32 audioAsioSecondDeviceOutputCount;
          asioError = asioDriverSecondDeviceForTunerDivided->getChannels(&Global::audioAsioSecondDeviceForTunerDividedInputCount, &audioAsioSecondDeviceOutputCount);
          ASSERT(asioError == ASIO::Error::OK);
          ASSERT(Global::audioAsioSecondDeviceForTunerDividedInputCount >= 1);
          ASSERT(audioAsioSecondDeviceOutputCount >= 2);
        }

        //{
        //  i32 audioAsioSecondDeviceForTunerDividedMinSize;
        //  i32 audioAsioSecondDeviceForTunerDividedMaxSize;
        //  i32 audioAsioSecondDeviceForTunerDividedPreferredSize;
        //  i32 audioAsioSecondDeviceForTunerDividedGranularity;
        //  asioError = asioDriverSecondDeviceForTunerDivided->getBufferSize(&audioAsioSecondDeviceForTunerDividedMinSize, &audioAsioSecondDeviceForTunerDividedMaxSize, &audioAsioSecondDeviceForTunerDividedPreferredSize, &audioAsioSecondDeviceForTunerDividedGranularity);
        //  ASSERT(asioError == ASIO::Error::OK);
        //}

        //{
        //  ASIO::SampleRate audioAsioSecondDeviceForTunerSampleRate;
        //  asioError = asioDriverSecondDeviceForTunerDivided->getSampleRate(&audioAsioSecondDeviceForTunerSampleRate);
        //  ASSERT(asioError == ASIO::Error::OK);
        //}

        if (Settings::audioAsioSampleRate > 0 && Global::audioAsioSampleRate != Settings::audioAsioSampleRate)
        {
          asioError = asioDriverSecondDeviceForTunerDivided->canSampleRate(f64(Settings::audioAsioSampleRate));
          ASSERT(asioError == ASIO::Error::OK);

          asioError = asioDriverSecondDeviceForTunerDivided->setSampleRate(f64(Settings::audioAsioSampleRate));
          ASSERT(asioError == ASIO::Error::OK);

          //Global::audioAsioSampleRate = Settings::audioAsioSampleRate;
        }
      }

      { // setup buffers
        for (i32 i = 0; i < Global::audioAsioSecondDeviceForTunerDividedInputCount; ++i)
        {
          asioDriverInfoSecondDeviceForTunerDividedBufferInfos[i].isInput = ASIO::Bool::True;
          asioDriverInfoSecondDeviceForTunerDividedBufferInfos[i].channelNum = i;
        }

        Settings::audioAsioDividedPickupChannelString0 = clamp(Settings::audioAsioDividedPickupChannelString0, 0, i32(Global::audioAsioSecondDeviceForTunerDividedInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString1 = clamp(Settings::audioAsioDividedPickupChannelString1, 0, i32(Global::audioAsioSecondDeviceForTunerDividedInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString2 = clamp(Settings::audioAsioDividedPickupChannelString2, 0, i32(Global::audioAsioSecondDeviceForTunerDividedInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString3 = clamp(Settings::audioAsioDividedPickupChannelString3, 0, i32(Global::audioAsioSecondDeviceForTunerDividedInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString4 = clamp(Settings::audioAsioDividedPickupChannelString4, 0, i32(Global::audioAsioSecondDeviceForTunerDividedInputCount) - 1);
        Settings::audioAsioDividedPickupChannelString5 = clamp(Settings::audioAsioDividedPickupChannelString5, 0, i32(Global::audioAsioSecondDeviceForTunerDividedInputCount) - 1);

        {
          static ASIO::Callbacks asioCallbacks{
            .bufferSwitch = &bufferSwitch2,
            .sampleRateDidChange = &sampleRateChanged,
            .asioMessage = &asioMessages,
            .bufferSwitchTimeInfo = &bufferSwitchTimeInfo2
          };

          const i32 blockSize = Settings::audioAsioBlockSize >= 1 ? Settings::audioAsioBlockSize : Global::audioAsioBufferPreferredSize;

          asioError = asioDriverSecondDeviceForTunerDivided->createBuffers(asioDriverInfoSecondDeviceForTunerDividedBufferInfos, Global::audioAsioSecondDeviceForTunerDividedInputCount, blockSize, &asioCallbacks);
          ASSERT(asioError == ASIO::Error::OK);
        }

        for (i32 i = 0; i < Global::audioAsioSecondDeviceForTunerDividedInputCount; ++i)
        {
          asioDriverInfoSecondDeviceForTunerDividedChannelInfos[i].channel = asioDriverInfoSecondDeviceForTunerDividedBufferInfos[i].channelNum;
          asioDriverInfoSecondDeviceForTunerDividedChannelInfos[i].isInput = asioDriverInfoSecondDeviceForTunerDividedBufferInfos[i].isInput;
          asioError = asioDriverSecondDeviceForTunerDivided->getChannelInfo(&asioDriverInfoSecondDeviceForTunerDividedChannelInfos[i]);
          ASSERT(asioError == ASIO::Error::OK);
        }

        asioError = asioDriverSecondDeviceForTunerDivided->start();
        ASSERT(asioError == ASIO::Error::OK);

        //asioError = ASIOGetLatencies(&g_asioDriverInfoSecondDeviceForTunerDivided.inputLatency, &g_asioDriverInfoSecondDeviceForTunerDivided.outputLatency);
        //ASSERT(asioError == OK);
      }
    }
#endif // SHR3D_SFX_CORE_HEXFIN && SHR3D_SFX_CORE_HEXFIN_DIVIDED && SHR3D_AUDIO_ASIO_SECOND_DEVICE_FOR_TUNER_DIVIDED
  }
}
#endif // SHR3D_AUDIO_ASIO

#ifdef SHR3D_AUDIO_SUPERPOWERED
static void interleaveSuperpowered(f32* sdlStream, const f32* vstStream, const i32 blockSize, const i32 channel, const i32 numChannels)
{
  for (i32 i = 0; i < blockSize; ++i)
  {
    sdlStream[i * numChannels + channel] = vstStream[i];
    sdlStream[i * numChannels + channel + 1] = vstStream[blockSize + i];
  }
}

#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
static void tunerDividedSuperpowered(f32* audioIO, const i32 blockSize, const i32 numInputChannels)
{
  for (i32 i = 0; i < blockSize; ++i)
  {
    TunerThread::inBlock0[i] = audioIO[i * numInputChannels + Settings::audioSuperpoweredDividedPickupChannelString0];
    TunerThread::inBlock1[i] = audioIO[i * numInputChannels + Settings::audioSuperpoweredDividedPickupChannelString1];
    TunerThread::inBlock2[i] = audioIO[i * numInputChannels + Settings::audioSuperpoweredDividedPickupChannelString2];
    TunerThread::inBlock3[i] = audioIO[i * numInputChannels + Settings::audioSuperpoweredDividedPickupChannelString3];
    TunerThread::inBlock4[i] = audioIO[i * numInputChannels + Settings::audioSuperpoweredDividedPickupChannelString4];
    TunerThread::inBlock5[i] = audioIO[i * numInputChannels + Settings::audioSuperpoweredDividedPickupChannelString5];
  }
}
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED

static void superpoweredUpdateMutesAndVolumes(i32 deviceID)
{
  static std::vector<char> inputMutes;
  static std::vector<f32> inputVolumes;
  static std::vector<char> outputMutes;
  static std::vector<f32> outputVolumes;

  if (inputMutes != Settings::audioSuperpoweredInputMutes)
  {
    const i32 count = Settings::audioSuperpoweredInputMutes.size();
    for (i32 i = 0; i < count; ++i)
    {
      if (inputMutes.size() != count || inputMutes[i] != Settings::audioSuperpoweredInputMutes[i])
        Superpowered::AndroidUSBAudio::setMute(deviceID, *Global::audioSuperpoweredInputPathIndex, i, Settings::audioSuperpoweredInputMutes[i]);
    }
    inputMutes = Settings::audioSuperpoweredInputMutes;
  }
  if (inputVolumes != Settings::audioSuperpoweredInputVolume)
  {
    const i32 count = Settings::audioSuperpoweredInputVolume.size();
    for (i32 i = 0; i < count; ++i)
    {
      if (inputVolumes.size() != count || inputVolumes[i] != Settings::audioSuperpoweredInputVolume[i])
        Superpowered::AndroidUSBAudio::setVolume(deviceID, *Global::audioSuperpoweredInputPathIndex, i, Settings::audioSuperpoweredInputVolume[i]);
    }
    inputVolumes = Settings::audioSuperpoweredInputVolume;
  }
  if (outputMutes != Settings::audioSuperpoweredOutputMutes)
  {
    const i32 count = Settings::audioSuperpoweredOutputMutes.size();
    for (i32 i = 0; i < count; ++i)
    {
      if (outputMutes.size() != count || outputMutes[i] != Settings::audioSuperpoweredOutputMutes[i])
        Superpowered::AndroidUSBAudio::setMute(deviceID, *Global::audioSuperpoweredOutputPathIndex, i, Settings::audioSuperpoweredOutputMutes[i]);
    }
    outputMutes = Settings::audioSuperpoweredOutputMutes;
  }
  if (outputVolumes != Settings::audioSuperpoweredOutputVolume)
  {
    const i32 count = Settings::audioSuperpoweredOutputVolume.size();
    for (i32 i = 0; i < count; ++i)
    {
      if (outputVolumes.size() != count || outputVolumes[i] != Settings::audioSuperpoweredOutputVolume[i])
        Superpowered::AndroidUSBAudio::setVolume(deviceID, *Global::audioSuperpoweredOutputPathIndex, i, Settings::audioSuperpoweredOutputVolume[i]);
    }
    outputVolumes = Settings::audioSuperpoweredOutputVolume;
  }
}

static bool superpowered_audio_callback(
  void* clientdata,      // custom pointer
  i32 deviceID,  // device identifier
  f32* audioIO,         // buffer for input/output samples
  i32 blockSize,    // number of samples to process
  i32 samplerate,         // sampling rate
  i32 numInputChannels,   // number of input channels
  i32 numOutputChannels   // number of output channels
)
{
  superpoweredUpdateMutesAndVolumes(deviceID);

#ifdef SHR3D_SFX_CORE_HEXFIN_DIVIDED
  tunerDividedSuperpowered(audioIO, blockSize, numInputChannels);
#endif // SHR3D_SFX_CORE_HEXFIN_DIVIDED

  f32 tempBuffer[512 * 2]; // Superpowered does not support higher block sizes than 512

  deinterleaveFromMono(tempBuffer, audioIO, blockSize, Settings::audioSuperpoweredChannelInput, Settings::audioEffectVolume, numInputChannels);

  f32* inStereo[] = { &tempBuffer[0], &tempBuffer[blockSize] };
  f32* outStereo[] = { &audioIO[0], &audioIO[blockSize] };
  process_main(inStereo, outStereo, blockSize);

  memcpy(tempBuffer, audioIO, blockSize * sizeof(f32) * 2);
  memset(audioIO, 0, sizeof(f32) * blockSize * numOutputChannels);

  interleaveSuperpowered(audioIO, tempBuffer, blockSize, Settings::audioSuperpoweredChannelOutput, numOutputChannels);

  return true;
}

static void usbAudioDeviceConnectedCallback(void* clientdata, int deviceID, const char* manufacturer, const char* product, const char* info)
{
  int numConfigurations;
  char** configurationNames;
  Superpowered::AndroidUSBAudio::getConfigurationInfo(
    deviceID,               // Device identifier.
    &numConfigurations,     // Number of configurations.
    &configurationNames     // Names of each configuration.
  );

  if (numConfigurations <= 0)
    return;

  Global::audioSuperpoweredCofiguration.resize(numConfigurations);
  i32 configurationIndex = 0;
  for (i32 i = 0; i < numConfigurations; ++i)
  {
    Global::audioSuperpoweredCofiguration[i] = reinterpret_cast<const char8_t*>(configurationNames[i]);
    if (Settings::audioSuperpoweredCofiguration == Global::audioSuperpoweredCofiguration[i])
    {
      configurationIndex = i;
      break;
    }
  }

  Superpowered::AndroidUSBAudio::setConfiguration(deviceID, configurationIndex);

  i32 inputIOIndex = 0;
  {
    Superpowered::AndroidUSBAudioIOInfo* infos;
    const i32 numInputs = Superpowered::AndroidUSBAudio::getInputs(deviceID, &infos);
    Global::audioSuperpoweredDevicesInput.resize(numInputs);
    for (i32 i = 0; i < numInputs; ++i)
    {
      Global::audioSuperpoweredDevicesInput[i] = reinterpret_cast<const char8_t*>(infos[i].name);
      if (Settings::audioSuperpoweredDeviceInput == Global::audioSuperpoweredDevicesInput[i])
      {
        inputIOIndex = i;
        break;
      }
    }
    free(infos);
  }
  i32 outputIOIndex = 0;
  {
    Superpowered::AndroidUSBAudioIOInfo* infos;
    const i32 numOutputs = Superpowered::AndroidUSBAudio::getOutputs(deviceID, &infos);
    Global::audioSuperpoweredDevicesOutput.resize(numOutputs);
    for (i32 i = 0; i < numOutputs; ++i)
    {
      Global::audioSuperpoweredDevicesOutput[i] = reinterpret_cast<const char8_t*>(infos[i].name);
      if (Settings::audioSuperpoweredDeviceOutput == Global::audioSuperpoweredDevicesOutput[i])
      {
        outputIOIndex = i;
        break;
      }
    }
    free(infos);
  }

  if (!Global::audioSuperpoweredDevicesInput.empty())
  {
    i32 inputPathNums;
    i32* thruPathIndex;
    i32 truPathNums;
    char** inputPathNames;
    char** truPathNames;
    Superpowered::AndroidUSBAudio::getIOOptions(
      deviceID, // Device identifier.
      true, // True for input, false for output.
      inputIOIndex, // The index of the input or output.
      &Global::audioSuperpoweredInputPathIndex, // Returns the path indexes.
      &inputPathNames, // Returns the path names.
      &inputPathNums, // Returns the number of paths.
      &thruPathIndex, // Returns the audio-thru path indexes (input only).
      &truPathNames, // Returns the audio-thru path names (input only).
      &truPathNums // Returns the number of audio-thru paths (input only).
    );
    Global::audioSuperpoweredInputPaths.resize(inputPathNums);
    for (i32 i = 0; i < inputPathNums; ++i)
    {
      Global::audioSuperpoweredInputPaths[i] = reinterpret_cast<const char8_t*>(inputPathNames[i]);
      free(inputPathNames[i]);
    }
    Global::audioSuperpoweredTruPaths.resize(truPathNums);
    for (i32 i = 0; i < truPathNums; ++i)
    {
      Global::audioSuperpoweredTruPaths[i] = reinterpret_cast<const char8_t*>(truPathNames[i]);
      free(truPathNames[i]);
    }
  }
  if (!Global::audioSuperpoweredDevicesOutput.empty())
  {
    i32 outputPathNums;
    char** outputPathNames;
    Superpowered::AndroidUSBAudio::getIOOptions(
      deviceID, // Device identifier.
      false, // True for input, false for output.
      outputIOIndex, // The index of the input or output.
      &Global::audioSuperpoweredOutputPathIndex, // Returns the path indexes.
      &outputPathNames, // Returns the path names.
      &outputPathNums, // Returns the number of paths.
      NULL, // Returns the audio-thru path indexes (input only)
      NULL, // Returns the audio-thru path names (input only).
      NULL // Returns the number of audio-thru paths (input only).
    );
    Global::audioSuperpoweredOutputPaths.resize(outputPathNums);
    for (i32 i = 0; i < outputPathNums; ++i)
    {
      Global::audioSuperpoweredOutputPaths[i] = reinterpret_cast<const char8_t*>(outputPathNames[i]);
      free(outputPathNames[i]);
    }
  }

  Superpowered::AndroidUSBAudioBufferSize bufferSize;
  switch (Settings::audioSuperpoweredBlockSize) {
  case 128:
    bufferSize = Superpowered::AndroidUSBAudioBufferSize_VeryLow;
    break;
  case 256:
    bufferSize = Superpowered::AndroidUSBAudioBufferSize_Low;
    break;
  case 512:
    bufferSize = Superpowered::AndroidUSBAudioBufferSize_Mid;
    break;
  default:
    unreachable();
  }

  Superpowered::CPU::setSustainedPerformanceMode(Settings::audioSuperpoweredSustainedPerformanceMode);
  Superpowered::AndroidUSBAudio::startIO(
    deviceID, // deviceID
    inputIOIndex, // inputIOIndex
    outputIOIndex, // outputIOIndex
    bufferSize, // latency
    nullptr, // clientData
    superpowered_audio_callback // audio process callback
  );

  //  {
  //    const char *manufacurer, *product, *info;
  //    Superpowered::AndroidUSBAudio::getInfo (
  //            deviceID,      // Device identifier.
  //            &manufacurer,  // Manufacturer name.
  //            &product,      // Product name.
  //            &info          // Detailed USB information about the device.
  //    );
  //  }
  { // Output
    int numFeatures;
    float* minVolumes, * maxVolumes, * curVolumes;
    char* mutes;
    Superpowered::AndroidUSBAudio::getPathInfo(
      deviceID, // Device identifier.
      *Global::audioSuperpoweredInputPathIndex, // Path index.
      &numFeatures, // Returns the number of features (the size of the minVolumes, maxVolumes, curVolumes and mutes arrays).
      &minVolumes, // Minimum volume values in db.
      &maxVolumes, // Maximum volume values in db.
      &curVolumes, // Current volume values in db.
      &mutes // Current mute values (1 muted, 0 not muted).
    );

    Global::audioSuperpoweredInputMinVolumes.resize(numFeatures);
    Global::audioSuperpoweredInputMaxVolumes.resize(numFeatures);
    Settings::audioSuperpoweredInputVolume.resize(numFeatures);
    Settings::audioSuperpoweredInputMutes.resize(numFeatures);

    for (i32 i = 0; i < numFeatures; ++i)
    {
      Global::audioSuperpoweredInputMinVolumes[i] = minVolumes[i];
      Global::audioSuperpoweredInputMaxVolumes[i] = maxVolumes[i];
      if (Settings::audioSuperpoweredInputVolume[i] < minVolumes[i] || Settings::audioSuperpoweredInputVolume[i] > maxVolumes[i])
        Settings::audioSuperpoweredInputVolume[i] = curVolumes[i];
      Settings::audioSuperpoweredInputMutes[i] = mutes[i];
    }
    Global::audioSuperpoweredInputCount = numFeatures - 1;

    free(minVolumes);
    free(maxVolumes);
    free(curVolumes);
    free(mutes);
  }
  { // Input
    int numFeatures;
    float* minVolumes, * maxVolumes, * curVolumes;
    char* mutes;
    Superpowered::AndroidUSBAudio::getPathInfo(
      deviceID, // Device identifier.
      *Global::audioSuperpoweredOutputPathIndex, // Path index.
      &numFeatures, // Returns the number of features (the size of the minVolumes, maxVolumes, curVolumes and mutes arrays).
      &minVolumes, // Minimum volume values in db.
      &maxVolumes, // Maximum volume values in db.
      &curVolumes, // Current volume values in db.
      &mutes // Current mute values (1 muted, 0 not muted).
    );

    Global::audioSuperpoweredOutputMinVolumes.resize(numFeatures);
    Global::audioSuperpoweredOutputMaxVolumes.resize(numFeatures);
    Settings::audioSuperpoweredOutputVolume.resize(numFeatures);
    Settings::audioSuperpoweredOutputMutes.resize(numFeatures);

    for (i32 i = 0; i < numFeatures; ++i)
    {
      Global::audioSuperpoweredOutputMinVolumes[i] = minVolumes[i];
      Global::audioSuperpoweredOutputMaxVolumes[i] = maxVolumes[i];
      if (Settings::audioSuperpoweredOutputVolume[i] < minVolumes[i] || Settings::audioSuperpoweredOutputVolume[i] > maxVolumes[i])
        Settings::audioSuperpoweredOutputVolume[i] = curVolumes[i];
      Settings::audioSuperpoweredOutputMutes[i] = mutes[i];
    }
    Global::audioSuperpoweredOutputCount = numFeatures - 1;

    free(minVolumes);
    free(maxVolumes);
    free(curVolumes);
    free(mutes);
  }
  {
    //Superpowered::AndroidUSBAudio::setVolume(deviceID, pathIndex, channel, db)
  }
  {
    //Superpowered::AndroidUSBAudio::setMute(deviceID, pathIndex, channel, mute);
  }
}

namespace Superpowered
{
  void init()
  {
    Superpowered::Initialize("ExampleLicenseKey-WillExpire-OnNextUpdate");
    Superpowered::AndroidUSB::initialize(NULL, usbAudioDeviceConnectedCallback, NULL, NULL, NULL);
  }
}

int superpoweredDeviceID;

extern "C" JNIEXPORT jint JNICALL
#ifdef SHR3D_WINDOW_SDL
Java_org_libsdl_app_SuperpoweredUSBAudio_onConnect
#else // SHR3D_WINDOW_SDL
#ifdef PLATFORM_QUEST_3
Java_app_shr3d_SuperpoweredUSBAudio_onConnect
#else // PLATFORM_QUEST_3
Java_app_shr3d_SuperpoweredUSBAudio_onConnect
#endif // PLATFORM_QUEST_3
#endif // SHR3D_WINDOW_SDL
(JNIEnv* env, jobject __unused obj, jint deviceID, jint fd, jbyteArray rawDescriptor)
{
  superpoweredDeviceID = deviceID;
  jbyte* rd = env->GetByteArrayElements(rawDescriptor, NULL);
  int dataBytes = env->GetArrayLength(rawDescriptor);
  int r = Superpowered::AndroidUSB::onConnect(deviceID, fd, (unsigned char*)rd, dataBytes);
  env->ReleaseByteArrayElements(rawDescriptor, rd, JNI_ABORT);
  return r;
}

//extern "C" JNIEXPORT jobjectArray JNICALL Java_app_shr3d_SuperpoweredUSBAudio_getConfigurationInfo(
//  JNIEnv* env,
//  jobject thiz,
//  jint deviceID)
//{
//  int numConfigurations;
//  char** configurationNames;
//  Superpowered::AndroidUSBAudio::getConfigurationInfo(
//    deviceID,               // Device identifier.
//    &numConfigurations,     // Number of configurations.
//    &configurationNames     // Names of each configuration.
//  );
//
//  jobjectArray names =
//    env->NewObjectArray(numConfigurations, env->FindClass("java/lang/String"), NULL);
//  //Global::audioSuperpoweredCofiguration.resize(numConfigurations);
//  for (int n = 0; n < numConfigurations; n++) {
//    env->SetObjectArrayElement(names, n, env->NewStringUTF(configurationNames[n]));
//
//    //Global::audioSuperpoweredCofiguration[n] = configurationNames[n];
//
//    free(configurationNames[n]);
//  }
//
//  free(configurationNames);
//  return names;
//}

#endif // SHR3D_AUDIO_SUPERPOWERED

static void getDeviceNames()
{
  { // fill device name list
#ifdef SHR3D_AUDIO_AAUDIO
    //AAudio::getDeviceNames();
#endif // SHR3D_AUDIO_AAUDIO
#ifdef SHR3D_AUDIO_ASIO
    ASIO::getDeviceNames();
#endif // SHR3D_AUDIO_ASIO
#ifdef SHR3D_AUDIO_JACK
    JACK::getDeviceNames();
#endif // SHR3D_AUDIO_JACK
#ifdef SHR3D_AUDIO_PIPEWIRE
    PipeWire::getDeviceNames();
#endif // SHR3D_AUDIO_PIPEWIRE
#ifdef SHR3D_AUDIO_SDL
    SDL::getDeviceNames();
#endif // SHR3D_AUDIO_SDL
#ifdef SHR3D_AUDIO_WASAPI
    WASAPI::getDeviceNames();
#endif // SHR3D_AUDIO_WASAPI
#ifdef SHR3D_AUDIO_WEBAUDIO
    //WebAudio::getDeviceNames();
#endif // SHR3D_AUDIO_WEBAUDIO  
  }
}

static void useAudioSystem(const AudioSystem audioSystem)
{
  switch (audioSystem)
  {
#ifdef SHR3D_AUDIO_AAUDIO
  case AudioSystem::AAudio:
    AAudio::init();
    return;
#endif // SHR3D_AUDIO_AAUDIO
#ifdef SHR3D_AUDIO_ASIO
  case AudioSystem::ASIO:
    ASIO::init();
    return;
#endif // SHR3D_AUDIO_ASIO
#ifdef SHR3D_AUDIO_JACK
  case AudioSystem::JACK:
    JACK::init();
    return;
#endif // SHR3D_AUDIO_JACK
#ifdef SHR3D_AUDIO_PIPEWIRE
  case AudioSystem::PipeWire:
    PipeWire::init();
    return;
#endif // SHR3D_AUDIO_PIPEWIRE
#ifdef SHR3D_AUDIO_SDL
  case AudioSystem::SDL:
    SDL::init();
    return;
#endif // SHR3D_AUDIO_SDL  
#ifdef SHR3D_AUDIO_WASAPI
  case AudioSystem::WASAPI:
    WASAPI::init();
    return;
#endif // SHR3D_AUDIO_WASAPI
#ifdef SHR3D_AUDIO_WEBAUDIO
  case AudioSystem::WebAudio:
    WebAudio::init();
    return;
#endif // SHR3D_AUDIO_WEBAUDIO
#ifdef SHR3D_AUDIO_SUPERPOWERED
  case AudioSystem::Superpowered:
    Superpowered::init();
    return;
#endif // SHR3D_AUDIO_SUPERPOWERED
  default:
    unreachable();
  }
  ASSERT(false);
}

void Sound::init()
{
  getDeviceNames();
  useAudioSystem(Settings::audioSystem);
}

#ifdef SHR3D_AUDIO_SUPERPOWERED
void Sound::fini()
{
  if (Settings::audioSystem == AudioSystem::Superpowered)
  {
    //Superpowered::AndroidUSBAudio::stopIO(superpoweredDeviceID);
  }
}
#endif // SHR3D_AUDIO_SUPERPOWERED

#ifdef SHR3D_AUDIO_ASIO
void Sound::openControlPanel()
{
  asioDriverDevice->controlPanel();
}
#endif // SHR3D_AUDIO_ASIO
