/**
 * Looking Glass
 * Copyright © 2017-2026 The Looking Glass Authors
 * https://looking-glass.io
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 59
 * Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

#include "interface/audiodev.h"

#include <spa/param/audio/format-utils.h>
#include <spa/param/buffers.h>
#include <spa/param/props.h>
#include <spa/utils/result.h>
#include <pipewire/pipewire.h>
#include <errno.h>
#include <limits.h>
#include <math.h>
#include <semaphore.h>
#include <stddef.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>

#include "common/debug.h"
#include "common/stringutils.h"
#include "common/util.h"
#include "common/option.h"
#include "common/ringbuffer.h"
#include "common/thread.h"
#include "common/time.h"

typedef enum
{
  STREAM_STATE_INACTIVE,
  STREAM_STATE_ACTIVE
}
StreamState;

typedef enum
{
  RECORD_LATEST_FREE,
  RECORD_LATEST_WRITING,
  RECORD_LATEST_READY,
  RECORD_LATEST_READING
}
RecordLatestState;

typedef struct
{
  int           frames;
  bool          clockValid;
  LG_AudioClock clock;
}
RecordBlock;

#define RECORD_LATEST_SLOTS 2
#define RECORD_CLOCK_TOLERANCE_FRAMES 2.0
#define RECORD_ERROR_REPORT_INTERVAL_NS INT64_C(5000000000)
#define PIPEWIRE_CONNECT_TIMEOUT_NS INT64_C(5000000000)

struct PipeWire
{
  struct pw_loop        * loop;
  struct pw_context     * context;
  struct pw_core        * core;
  struct pw_thread_loop * thread;

  struct
  {
    struct pw_stream * stream;
    _Atomic(struct spa_io_rate_match *) rateMatch;
    _Atomic(int64_t) latencyNs;
    atomic_bool      latencyUpdateRequested;
    atomic_uint      bufferErrors;
    atomic_uint      pullErrors;
    atomic_uint      timingErrors;
    atomic_int       streamError;
#if PW_CHECK_VERSION(1, 4, 0)
    double           appliedResampleRatio;
    atomic_int       resampleError;
#endif
    enum pw_stream_state connectionState;
    bool                 resamplerEnabled;

    LG_AudioFormat format;
    int            stride;
    LG_AudioPullFn pullFn;
    LG_AudioFailureFn failureFn;
    uint32_t          failureCookie;
    int            maxPeriodFrames;
    int            startFrames;

    atomic_uint state;
  }
  playback;

  struct
  {
    struct pw_stream * stream;
    enum pw_stream_state connectionState;

    LG_AudioFormat format;
    int            stride;
    LG_AudioPushFn pushFn;
    LG_AudioFailureFn failureFn;
    uint32_t          failureCookie;

    RingBuffer  sendQueue;
    RingBuffer  sendBlocks;
    uint8_t *   sendBuffer;
    int         sendBufferFrames;
    int         maxQueuedFrames;
    sem_t       sendWake;
    bool        sendWakeInitialized;
    LGThread *  sendThread;
    atomic_bool sendStop;
    atomic_bool sendWakePending;
    atomic_bool latestMode;
    atomic_uint_fast64_t latestSerial;
    uint8_t *   latestBuffer;
    struct
    {
      atomic_uint          state;
      atomic_uint_fast64_t serial;
      int                  frames;
      bool                 clockValid;
      LG_AudioClock        clock;
    }
    latest[RECORD_LATEST_SLOTS];
    atomic_uint bufferErrors;
    atomic_uint droppedFrames;
    atomic_uint rejectedBatches;
    atomic_uint rejectedFrames;
    atomic_uint signalErrors;
    atomic_int  recycleError;

    uint64_t            clockPosition;
    uint64_t            clockLastPosition;
    uint64_t            clockLastTicks;
    int64_t             clockLastTime;
    struct spa_fraction clockLastRate;
    bool                clockLastValid;
    bool                clockLastTimingValid;
    bool                clockDiscontinuityPending;
    bool active;
  }
  record;
};

static struct PipeWire pw = {0};

static inline void pipewire_storeError(atomic_int * target, int result)
{
  if (result >= 0)
    return;

  int expected = 0;
  atomic_compare_exchange_strong_explicit(
      target, &expected, result,
      memory_order_relaxed, memory_order_relaxed);
}

static bool pipewire_audioFormatEqual(const LG_AudioFormat * a,
    const LG_AudioFormat * b)
{
  return
    a->sampleFormat == b->sampleFormat &&
    a->sampleRate   == b->sampleRate &&
    a->channelCount == b->channelCount &&
    memcmp(a->channels, b->channels,
        a->channelCount * sizeof(*a->channels)) == 0;
}

static enum spa_audio_channel pipewire_channel(
    LG_AudioChannel channel, uint8_t index)
{
  switch (channel)
  {
    case LG_AUDIO_CH_UNKNOWN:
      return (enum spa_audio_channel)(SPA_AUDIO_CHANNEL_AUX0 + index);
    case LG_AUDIO_CH_MONO               : return SPA_AUDIO_CHANNEL_MONO;
    case LG_AUDIO_CH_FRONT_LEFT         : return SPA_AUDIO_CHANNEL_FL;
    case LG_AUDIO_CH_FRONT_RIGHT        : return SPA_AUDIO_CHANNEL_FR;
    case LG_AUDIO_CH_FRONT_CENTER       : return SPA_AUDIO_CHANNEL_FC;
    case LG_AUDIO_CH_LFE                : return SPA_AUDIO_CHANNEL_LFE;
    case LG_AUDIO_CH_REAR_LEFT          : return SPA_AUDIO_CHANNEL_RL;
    case LG_AUDIO_CH_REAR_RIGHT         : return SPA_AUDIO_CHANNEL_RR;
    case LG_AUDIO_CH_FRONT_LEFT_CENTER  : return SPA_AUDIO_CHANNEL_FLC;
    case LG_AUDIO_CH_FRONT_RIGHT_CENTER : return SPA_AUDIO_CHANNEL_FRC;
    case LG_AUDIO_CH_REAR_CENTER        : return SPA_AUDIO_CHANNEL_RC;
    case LG_AUDIO_CH_SIDE_LEFT          : return SPA_AUDIO_CHANNEL_SL;
    case LG_AUDIO_CH_SIDE_RIGHT         : return SPA_AUDIO_CHANNEL_SR;
    case LG_AUDIO_CH_TOP_CENTER         : return SPA_AUDIO_CHANNEL_TC;
    case LG_AUDIO_CH_TOP_FRONT_LEFT     : return SPA_AUDIO_CHANNEL_TFL;
    case LG_AUDIO_CH_TOP_FRONT_CENTER   : return SPA_AUDIO_CHANNEL_TFC;
    case LG_AUDIO_CH_TOP_FRONT_RIGHT    : return SPA_AUDIO_CHANNEL_TFR;
    case LG_AUDIO_CH_TOP_REAR_LEFT      : return SPA_AUDIO_CHANNEL_TRL;
    case LG_AUDIO_CH_TOP_REAR_CENTER    : return SPA_AUDIO_CHANNEL_TRC;
    case LG_AUDIO_CH_TOP_REAR_RIGHT     : return SPA_AUDIO_CHANNEL_TRR;
  }

  return (enum spa_audio_channel)(SPA_AUDIO_CHANNEL_AUX0 + index);
}

static enum spa_audio_format pipewire_sampleFormat(
    LG_AudioSampleFormat format)
{
  switch (format)
  {
    case LG_AUDIO_FMT_U8     : return SPA_AUDIO_FORMAT_U8;
    case LG_AUDIO_FMT_S16_LE : return SPA_AUDIO_FORMAT_S16_LE;
    case LG_AUDIO_FMT_S24_LE : return SPA_AUDIO_FORMAT_S24_LE;
    case LG_AUDIO_FMT_S32_LE : return SPA_AUDIO_FORMAT_S32_LE;
    case LG_AUDIO_FMT_F32_LE : return SPA_AUDIO_FORMAT_F32_LE;
    case LG_AUDIO_FMT_F32_NE : return SPA_AUDIO_FORMAT_F32;
    case LG_AUDIO_FMT_F64_LE : return SPA_AUDIO_FORMAT_F64_LE;
  }

  return SPA_AUDIO_FORMAT_UNKNOWN;
}

static int pipewire_sampleSize(LG_AudioSampleFormat format)
{
  switch (format)
  {
    case LG_AUDIO_FMT_U8     : return 1;
    case LG_AUDIO_FMT_S16_LE : return 2;
    case LG_AUDIO_FMT_S24_LE : return 3;
    case LG_AUDIO_FMT_S32_LE :
    case LG_AUDIO_FMT_F32_LE :
    case LG_AUDIO_FMT_F32_NE : return 4;
    case LG_AUDIO_FMT_F64_LE : return 8;
  }

  return 0;
}

static struct spa_audio_info_raw pipewire_audioInfo(
    const LG_AudioFormat * format, enum spa_audio_format sampleFormat)
{
  struct spa_audio_info_raw info = SPA_AUDIO_INFO_RAW_INIT(
    .format   = sampleFormat,
    .channels = format->channelCount,
    .rate     = format->sampleRate
  );

  for (uint8_t i = 0; i < format->channelCount; ++i)
    info.position[i] = pipewire_channel(format->channels[i], i);

  return info;
}

static void pipewire_reportPlaybackErrors(void)
{
  const unsigned int bufferErrors = atomic_exchange_explicit(
      &pw.playback.bufferErrors, 0, memory_order_relaxed);
  const unsigned int pullErrors = atomic_exchange_explicit(
      &pw.playback.pullErrors, 0, memory_order_relaxed);
  const unsigned int timingErrors = atomic_exchange_explicit(
      &pw.playback.timingErrors, 0, memory_order_relaxed);
  const int streamError = atomic_exchange_explicit(
      &pw.playback.streamError, 0, memory_order_relaxed);
#if PW_CHECK_VERSION(1, 4, 0)
  const int resampleError = atomic_exchange_explicit(
      &pw.playback.resampleError, 0, memory_order_relaxed);
#endif

  if (bufferErrors)
    DEBUG_WARN("PipeWire playback ran out of buffers %u time(s)",
        bufferErrors);
  if (pullErrors)
    DEBUG_WARN("PipeWire playback returned an invalid frame count %u time(s)",
        pullErrors);
  if (timingErrors)
    DEBUG_WARN("PipeWire playback timing query failed %u time(s)",
        timingErrors);
  if (streamError)
    DEBUG_WARN("PipeWire playback stream operation failed: %s",
        spa_strerror(streamError));
#if PW_CHECK_VERSION(1, 4, 0)
  if (resampleError)
    DEBUG_WARN("PipeWire resampler rate update failed: %s",
        spa_strerror(resampleError));
#endif
}

static void pipewire_reportRecordErrors(void)
{
  const unsigned int bufferErrors = atomic_exchange_explicit(
      &pw.record.bufferErrors, 0, memory_order_relaxed);
  const unsigned int droppedFrames = atomic_exchange_explicit(
      &pw.record.droppedFrames, 0, memory_order_relaxed);
  const unsigned int rejectedBatches = atomic_exchange_explicit(
      &pw.record.rejectedBatches, 0, memory_order_relaxed);
  const unsigned int rejectedFrames = atomic_exchange_explicit(
      &pw.record.rejectedFrames, 0, memory_order_relaxed);
  const unsigned int signalErrors = atomic_exchange_explicit(
      &pw.record.signalErrors, 0, memory_order_relaxed);
  const int recycleError = atomic_exchange_explicit(
      &pw.record.recycleError, 0, memory_order_relaxed);

  if (bufferErrors)
    DEBUG_WARN("PipeWire recording encountered %u buffer error(s)",
        bufferErrors);
  if (droppedFrames)
    DEBUG_WARN("PipeWire recording dropped %u frame(s)", droppedFrames);
  if (rejectedFrames)
    DEBUG_WARN("PipeWire recording provider rejected %u frame(s) "
        "in %u batch(es)", rejectedFrames, rejectedBatches);
  if (signalErrors)
    DEBUG_WARN("PipeWire recording worker notification failed %u time(s)",
        signalErrors);
  if (recycleError)
    DEBUG_WARN("PipeWire recording buffer recycle failed: %s",
        spa_strerror(recycleError));
}

static void pipewire_reportRecordErrorsDue(uint64_t * nextReport)
{
  const uint64_t now = nanotime();
  if (now < *nextReport)
    return;

  pipewire_reportRecordErrors();
  *nextReport = now + RECORD_ERROR_REPORT_INTERVAL_NS;
}

#if !PW_CHECK_VERSION(1, 1, 0)
static int64_t pipewire_monotonicTime(void)
{
  struct timespec time;
  clock_gettime(CLOCK_MONOTONIC, &time);
  return SPA_TIMESPEC_TO_NSEC(&time);
}
#endif

static inline void pipewire_updatePlaybackLatency(void)
{
  if (!atomic_exchange_explicit(
        &pw.playback.latencyUpdateRequested, false,
        memory_order_acquire))
    return;

  struct pw_time time;
#if PW_CHECK_VERSION(0, 3, 50)
  if (pw_stream_get_time_n(pw.playback.stream, &time, sizeof(time)) < 0)
#else
  if (pw_stream_get_time(pw.playback.stream, &time) < 0)
#endif
  {
    atomic_fetch_add_explicit(
        &pw.playback.timingErrors, 1, memory_order_relaxed);
    return;
  }

  if (time.rate.num == 0 || time.rate.denom == 0)
    return;

#if PW_CHECK_VERSION(1, 1, 0)
  const int64_t now = (int64_t)pw_stream_get_nsec(pw.playback.stream);
#else
  const int64_t now = pipewire_monotonicTime();
#endif
  const int64_t elapsedNs = max(INT64_C(0), now - time.now);
  const double graphLatencyNs =
    (double)time.delay * time.rate.num *
      SPA_NSEC_PER_SEC / time.rate.denom - elapsedNs;
#if PW_CHECK_VERSION(0, 3, 50)
  const double streamFrames = time.queued + time.buffered;
#else
  const double streamFrames =
    (double)time.queued / pw.playback.stride;
#endif
  const double streamLatencyNs = streamFrames * SPA_NSEC_PER_SEC /
    pw.playback.format.sampleRate;
  const int64_t latencyNs =
    llrint(max(0.0, graphLatencyNs + streamLatencyNs));
  atomic_store_explicit(
      &pw.playback.latencyNs, latencyNs, memory_order_release);
}

#if PW_CHECK_VERSION(1, 4, 0)
static bool pipewire_playbackSetRate(double * ratio)
{
  if (!ratio || *ratio <= 0.0 ||
      !pw.playback.resamplerEnabled)
    return false;
  if (*ratio == pw.playback.appliedResampleRatio)
    return true;

  const int result = pw_stream_set_rate(pw.playback.stream, *ratio);
  if (result < 0)
  {
    int expected = 0;
    atomic_compare_exchange_strong_explicit(
        &pw.playback.resampleError, &expected, result,
        memory_order_relaxed, memory_order_relaxed);
    return false;
  }

  pw.playback.appliedResampleRatio = *ratio;
  return true;
}

static bool pipewire_configurePlaybackResampler(
    bool enable, bool * reusable)
{
  const bool wasEnabled = pw.playback.resamplerEnabled;
  const int result = pw_stream_set_rate(
      pw.playback.stream, enable ? 1.0 : 0.0);
  if (reusable)
    *reusable = result >= 0 || !wasEnabled;
  if (result < 0)
    return false;

  pw.playback.resamplerEnabled     = enable;
  pw.playback.appliedResampleRatio = enable ? 1.0 : 0.0;
  return enable;
}
#endif

static void pipewire_onPlaybackIoChanged(void * userdata, uint32_t id,
  void * data, uint32_t size)
{
  switch (id)
  {
    case SPA_IO_RateMatch:
      atomic_store_explicit(&pw.playback.rateMatch,
          data && size >= offsetof(struct spa_io_rate_match, size) +
            sizeof(((struct spa_io_rate_match *)0)->size) ? data : NULL,
          memory_order_release);
      break;
  }
}

static void pipewire_onPlaybackStateChanged(void * userdata,
    enum pw_stream_state old, enum pw_stream_state state,
    const char * error)
{
  (void)error;
  pw.playback.connectionState = state;
  if ((state == PW_STREAM_STATE_ERROR ||
       state == PW_STREAM_STATE_UNCONNECTED) &&
      pw.playback.failureFn)
  {
    LG_AudioFailureFn failureFn = pw.playback.failureFn;
    const uint32_t failureCookie = pw.playback.failureCookie;
    pw.playback.failureFn     = NULL;
    pw.playback.failureCookie = 0;
    failureFn(failureCookie);
  }
  pw_thread_loop_signal(pw.thread, false);
}

static void pipewire_onPlaybackProcess(void * userdata)
{
  struct pw_buffer * pbuf;

  if (!(pbuf = pw_stream_dequeue_buffer(pw.playback.stream)))
  {
    atomic_fetch_add_explicit(
        &pw.playback.bufferErrors, 1, memory_order_relaxed);
    return;
  }

  struct spa_buffer * sbuf = pbuf->buffer;
  uint8_t * dst;

  if (!sbuf || sbuf->n_datas == 0 || !sbuf->datas ||
      !sbuf->datas[0].chunk ||
      !(dst = sbuf->datas[0].data))
  {
#if PW_CHECK_VERSION(1, 4, 0)
    const int recycleResult =
      pw_stream_return_buffer(pw.playback.stream, pbuf);
#else
    const int recycleResult =
      pw_stream_queue_buffer(pw.playback.stream, pbuf);
#endif
    pipewire_storeError(&pw.playback.streamError, recycleResult);
    return;
  }

  int                        frames    =
    sbuf->datas[0].maxsize / pw.playback.stride;
  struct spa_io_rate_match * rateMatch = atomic_load_explicit(
      &pw.playback.rateMatch, memory_order_acquire);
  if (rateMatch && rateMatch->size > 0)
    frames = min(frames, rateMatch->size);
#if PW_CHECK_VERSION(0, 3, 50)
  else if (pbuf->requested > 0)
    frames = min(frames, pbuf->requested);
#endif
  else
  {
#if PW_CHECK_VERSION(1, 1, 0)
    struct pw_time time = {0};
    if (pw_stream_get_time_n(
          pw.playback.stream, &time, sizeof(time)) >= 0 && time.size > 0)
      frames = (int)min((uint64_t)frames, time.size);
    else
#endif
      frames = min(frames, pw.playback.maxPeriodFrames);
  }

  const int requestedFrames = frames;
  frames = pw.playback.pullFn(dst, requestedFrames);
  if (frames < 0 || frames > requestedFrames)
  {
    atomic_fetch_add_explicit(
        &pw.playback.pullErrors, 1, memory_order_relaxed);
    frames = clamp(frames, 0, requestedFrames);
  }
  pipewire_updatePlaybackLatency();
  if (!frames)
  {
    pbuf->size = 0;
    sbuf->datas[0].chunk->offset = 0;
    sbuf->datas[0].chunk->stride = pw.playback.stride;
    sbuf->datas[0].chunk->size = 0;
    pipewire_storeError(&pw.playback.streamError,
        pw_stream_queue_buffer(pw.playback.stream, pbuf));
    return;
  }

  pbuf->size = frames;
  sbuf->datas[0].chunk->offset = 0;
  sbuf->datas[0].chunk->stride = pw.playback.stride;
  sbuf->datas[0].chunk->size   = frames * pw.playback.stride;

  pipewire_storeError(&pw.playback.streamError,
      pw_stream_queue_buffer(pw.playback.stream, pbuf));
}

static struct Option pipewire_options[] =
{
  {
    .module       = "pipewire",
    .name         = "outDevice",
    .description  = "The default playback device to use",
    .type         = OPTION_TYPE_STRING
  },
  {
    .module       = "pipewire",
    .name         = "recDevice",
    .description  = "The default record device to use",
    .type         = OPTION_TYPE_STRING
  },

  {0}
};

static void pipewire_earlyInit(void)
{
  option_register(pipewire_options);
}

static bool pipewire_waitForStream(enum pw_stream_state * state)
{
  int result = 0;
#if PW_CHECK_VERSION(0, 3, 7)
  struct timespec deadline;
  result = pw_thread_loop_get_time(
      pw.thread, &deadline, PIPEWIRE_CONNECT_TIMEOUT_NS);
  if (result < 0)
    return false;

  while (*state == PW_STREAM_STATE_CONNECTING && result >= 0)
    result = pw_thread_loop_timed_wait_full(pw.thread, &deadline);
#else
  while (*state == PW_STREAM_STATE_CONNECTING && result >= 0)
    result = pw_thread_loop_timed_wait(pw.thread, 5);
#endif
  return result >= 0;
}

static bool pipewire_init(void)
{
  pw_init(NULL, NULL);

  pw.loop = pw_loop_new(NULL);
  if (!pw.loop)
  {
    DEBUG_ERROR("Failed to create a PipeWire loop");
    goto err;
  }
#if PW_CHECK_VERSION(1, 3, 81)
  pw.context = pw_context_new(
    pw.loop,
    NULL,
    0);
#else
  pw.context = pw_context_new(
    pw.loop,
    pw_properties_new(
      // Request real-time priority on the PipeWire threads
      PW_KEY_CONFIG_NAME, "client-rt.conf",
      NULL
    ),
    0);
#endif
  if (!pw.context)
  {
    DEBUG_ERROR("Failed to create a context");
    goto err;
  }

  /* this is just to test for PipeWire availabillity */
  pw.core = pw_context_connect(pw.context, NULL, 0);
  if (!pw.core)
    goto err_context;

  /* PipeWire is available so create the loop thread and start it */
  pw.thread = pw_thread_loop_new_full(pw.loop, "PipeWire", NULL);
  if (!pw.thread)
  {
    DEBUG_ERROR("Failed to create the thread loop");
    goto err_core;
  }

  const int result = pw_thread_loop_start(pw.thread);
  if (result < 0)
  {
    DEBUG_ERROR("Failed to start the PipeWire thread loop: %s",
        spa_strerror(result));
    pw_thread_loop_destroy(pw.thread);
    pw.thread = NULL;
    goto err_core;
  }
  return true;

err_core:
  pw_core_disconnect(pw.core);
  pw.core = NULL;

err_context:
  pw_context_destroy(pw.context);
  pw.context = NULL;

err:
  if (pw.loop)
    pw_loop_destroy(pw.loop);
  pw.loop = NULL;
  pw_deinit();
  return false;
}

static void pipewire_playbackStopStream(void)
{
  if (!pw.playback.stream)
  {
    pipewire_reportPlaybackErrors();
    return;
  }

  pw_thread_loop_lock(pw.thread);
  atomic_store_explicit(
      &pw.playback.rateMatch, NULL, memory_order_release);
  pw.playback.failureFn     = NULL;
  pw.playback.failureCookie = 0;
  pw_stream_destroy(pw.playback.stream);
  pw.playback.stream = NULL;
  pw.playback.resamplerEnabled = false;
  atomic_store_explicit(&pw.playback.state,
      STREAM_STATE_INACTIVE, memory_order_release);
  atomic_store_explicit(
      &pw.playback.latencyNs, 0, memory_order_release);
  atomic_store_explicit(
      &pw.playback.latencyUpdateRequested, false, memory_order_relaxed);
  pw_thread_loop_unlock(pw.thread);
  pipewire_reportPlaybackErrors();
}

static bool pipewire_playbackSetup(const LG_AudioFormat * format,
    int requestedPeriodFrames, bool requestResampler,
    bool * resamplerEnabled, int * maxPeriodFrames, int * startFrames,
    LG_AudioPullFn pullFn)
{
  *resamplerEnabled = false;

  const int channels   = format->channelCount;
  const int sampleRate = format->sampleRate;
  const int sampleSize =
    pipewire_sampleSize(format->sampleFormat);
  const enum spa_audio_format sampleFormat =
    pipewire_sampleFormat(format->sampleFormat);
  if (!sampleSize || sampleFormat == SPA_AUDIO_FORMAT_UNKNOWN)
    return false;

  const struct spa_pod * params[1];
  uint8_t buffer[1024];
  struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
  static const struct pw_stream_events events =
  {
    .version       = PW_VERSION_STREAM_EVENTS,
    .state_changed = pipewire_onPlaybackStateChanged,
    .io_changed    = pipewire_onPlaybackIoChanged,
    .process       = pipewire_onPlaybackProcess
  };

  bool reuse = false;
  pw_thread_loop_lock(pw.thread);
  if (pw.playback.stream &&
      pw.playback.connectionState == PW_STREAM_STATE_PAUSED &&
      atomic_load_explicit(
        &pw.playback.state, memory_order_acquire) == STREAM_STATE_INACTIVE &&
      pipewire_audioFormatEqual(&pw.playback.format, format))
  {
    pw.playback.pullFn = pullFn;
    bool resamplerReusable = true;
#if PW_CHECK_VERSION(1, 4, 0)
    atomic_store_explicit(
        &pw.playback.resampleError, 0, memory_order_relaxed);
    *resamplerEnabled =
      pipewire_configurePlaybackResampler(
          requestResampler, &resamplerReusable);
#endif
    if (resamplerReusable)
    {
      *maxPeriodFrames = pw.playback.maxPeriodFrames;
      *startFrames     = pw.playback.startFrames;
      reuse = true;
    }
  }
  pw_thread_loop_unlock(pw.thread);
  if (reuse)
    return true;

  pipewire_playbackStopStream();

  char requestedNodeLatency[32];
  snprintf(requestedNodeLatency, sizeof(requestedNodeLatency), "%d/%d",
    requestedPeriodFrames, sampleRate);

  pw.playback.format = *format;
  pw.playback.stride = sampleSize * channels;
  pw.playback.pullFn = pullFn;

  pw_thread_loop_lock(pw.thread);
  atomic_store_explicit(
      &pw.playback.rateMatch, NULL, memory_order_release);

  struct pw_properties * props =
    pw_properties_new(
      PW_KEY_APP_NAME      , "Looking Glass",
      PW_KEY_NODE_NAME     , "Looking Glass",
      PW_KEY_MEDIA_TYPE    , "Audio",
      PW_KEY_MEDIA_CATEGORY, "Playback",
      PW_KEY_MEDIA_ROLE    , "Music",
      PW_KEY_NODE_LATENCY  , requestedNodeLatency,
      NULL
    );
  if (!props)
  {
    pw_thread_loop_unlock(pw.thread);
    DEBUG_ERROR("Failed to create playback stream properties");
    return false;
  }

  const char * device = option_get_string("pipewire", "outDevice");
  if (device)
  {
#ifdef PW_KEY_TARGET_OBJECT
    pw_properties_set(props, PW_KEY_TARGET_OBJECT, device);
#else
    pw_properties_set(props, PW_KEY_NODE_TARGET, device);
#endif
  }

  pw.playback.stream = pw_stream_new_simple(
    pw.loop,
    "Looking Glass",
    props,
    &events,
    NULL
  );

  if (!pw.playback.stream)
  {
    pw_thread_loop_unlock(pw.thread);
    DEBUG_ERROR("Failed to create the stream");
    return false;
  }

  // The user can override the default node latency with the PIPEWIRE_LATENCY
  // environment variable, so get the actual node latency value from the stream.
  // The actual quantum size may be lower than this value depending on what else
  // is using the audio device, but we can treat this value as a maximum
  const struct pw_properties * properties =
    pw_stream_get_properties(pw.playback.stream);
  const char * actualNodeLatency = properties ?
    pw_properties_get(properties, PW_KEY_NODE_LATENCY) : NULL;

  unsigned num, denom;
  uint64_t actualPeriodFrames = 0;
  if (!actualNodeLatency ||
      sscanf(actualNodeLatency, "%u/%u", &num, &denom) != 2 ||
      num == 0 || denom == 0 ||
      (actualPeriodFrames =
        ((uint64_t)num * sampleRate + denom - 1) / denom) > INT_MAX)
  {
    DEBUG_WARN(
      "PIPEWIRE_LATENCY value '%s' is invalid; using %d/%d",
      actualNodeLatency ? actualNodeLatency : "(unset)", requestedPeriodFrames,
      sampleRate);

    struct spa_dict_item items[] = {
      { PW_KEY_NODE_LATENCY, requestedNodeLatency }
    };
    pw_stream_update_properties(pw.playback.stream,
      &SPA_DICT_INIT_ARRAY(items));

    pw.playback.maxPeriodFrames = requestedPeriodFrames;
  }
  else
    pw.playback.maxPeriodFrames = (int)actualPeriodFrames;

  // If the previous quantum size was very small, PipeWire can request two full
  // periods almost immediately at the start of playback
  pw.playback.startFrames = (int)min(
      (int64_t)pw.playback.maxPeriodFrames * 2, (int64_t)INT_MAX);

  *maxPeriodFrames = pw.playback.maxPeriodFrames;
  *startFrames     = pw.playback.startFrames;

  struct spa_audio_info_raw info =
    pipewire_audioInfo(format, sampleFormat);
  params[0] = spa_format_audio_raw_build(
      &b, SPA_PARAM_EnumFormat, &info);

  pw.playback.connectionState = PW_STREAM_STATE_CONNECTING;
  const int result = pw_stream_connect(
      pw.playback.stream,
      PW_DIRECTION_OUTPUT,
      PW_ID_ANY,
      PW_STREAM_FLAG_AUTOCONNECT |
      PW_STREAM_FLAG_MAP_BUFFERS |
      PW_STREAM_FLAG_RT_PROCESS  |
      PW_STREAM_FLAG_INACTIVE,
      params, 1);

  if (result < 0)
  {
    DEBUG_ERROR("Failed to connect playback stream: %s", spa_strerror(result));
    atomic_store_explicit(
        &pw.playback.rateMatch, NULL, memory_order_release);
    pw_stream_destroy(pw.playback.stream);
    pw.playback.stream = NULL;
    pw_thread_loop_unlock(pw.thread);
    return false;
  }

  if (!pipewire_waitForStream(&pw.playback.connectionState) ||
      pw.playback.connectionState != PW_STREAM_STATE_PAUSED)
  {
    DEBUG_ERROR("PipeWire playback stream did not become ready");
    atomic_store_explicit(
        &pw.playback.rateMatch, NULL, memory_order_release);
    pw_stream_destroy(pw.playback.stream);
    pw.playback.stream = NULL;
    pw_thread_loop_unlock(pw.thread);
    return false;
  }

  atomic_store_explicit(&pw.playback.state,
      STREAM_STATE_INACTIVE, memory_order_release);
  atomic_store_explicit(
      &pw.playback.latencyNs, 0, memory_order_release);
  atomic_store_explicit(
      &pw.playback.latencyUpdateRequested, false, memory_order_relaxed);
  atomic_store_explicit(
      &pw.playback.bufferErrors, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.playback.pullErrors, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.playback.timingErrors, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.playback.streamError, 0, memory_order_relaxed);
#if PW_CHECK_VERSION(1, 4, 0)
  atomic_store_explicit(
      &pw.playback.resampleError, 0, memory_order_relaxed);
  *resamplerEnabled =
    pipewire_configurePlaybackResampler(
        requestResampler, NULL);
#else
  (void)requestResampler;
#endif
  pw_thread_loop_unlock(pw.thread);
  return true;
}

static int pipewire_playbackControl(bool active,
    LG_AudioFailureFn failureFn, uint32_t failureCookie)
{
  int error = 0;
  pw_thread_loop_lock(pw.thread);
  if (!active)
  {
    pw.playback.failureFn     = NULL;
    pw.playback.failureCookie = 0;
  }

  if (!pw.playback.stream)
    error = active ? -ENODEV : 0;
  else if (active)
  {
    if (pw.playback.connectionState == PW_STREAM_STATE_ERROR ||
        pw.playback.connectionState == PW_STREAM_STATE_UNCONNECTED)
      error = -EPIPE;
    else
    {
      pw.playback.failureFn     = failureFn;
      pw.playback.failureCookie = failureCookie;
      if (atomic_load_explicit(
            &pw.playback.state, memory_order_acquire) != STREAM_STATE_ACTIVE)
      {
        /* A stopped stream must not replay data queued before reactivation. */
        error = pw_stream_flush(pw.playback.stream, false);
        if (error >= 0)
          error = pw_stream_set_active(pw.playback.stream, true);
      }
      if (error >= 0)
      {
        atomic_store_explicit(&pw.playback.state,
            STREAM_STATE_ACTIVE, memory_order_release);
      }
    }

    if (error < 0 ||
        pw.playback.connectionState == PW_STREAM_STATE_ERROR ||
        pw.playback.connectionState == PW_STREAM_STATE_UNCONNECTED)
    {
      pw.playback.failureFn     = NULL;
      pw.playback.failureCookie = 0;
      if (error >= 0)
        error = -EPIPE;
    }
  }
  else if (atomic_load_explicit(
        &pw.playback.state, memory_order_acquire) == STREAM_STATE_ACTIVE)
  {
    const int activeError =
      pw_stream_set_active(pw.playback.stream, false);
    const int flushError =
      pw_stream_flush(pw.playback.stream, false);

    error = activeError < 0 ? activeError : flushError;
    if (activeError >= 0)
      atomic_store_explicit(&pw.playback.state,
          STREAM_STATE_INACTIVE, memory_order_release);
  }

  pw_thread_loop_unlock(pw.thread);
  if (error < 0)
  {
    int expected = 0;
    atomic_compare_exchange_strong_explicit(
        &pw.playback.streamError, &expected, error,
        memory_order_relaxed, memory_order_relaxed);
  }
  return error;
}

static bool pipewire_playbackStart(
    LG_AudioFailureFn failureFn, uint32_t failureCookie)
{
  return pipewire_playbackControl(
      true, failureFn, failureCookie) >= 0;
}

static void pipewire_playbackStop(void)
{
  if (pipewire_playbackControl(false, NULL, 0) < 0)
  {
    pipewire_playbackStopStream();
    return;
  }
  atomic_store_explicit(
      &pw.playback.latencyNs, 0, memory_order_release);
  atomic_store_explicit(
      &pw.playback.latencyUpdateRequested, false, memory_order_relaxed);
}

static void pipewire_playbackVolume(int channels, const uint16_t volume[])
{
  if (channels <= 0 || channels > LG_AUDIO_MAX_CHANNELS)
    return;

  float param[channels];
  for(int i = 0; i < channels; ++i)
    param[i] = max(0.0,
        9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787);

  int result = 0;
  pw_thread_loop_lock(pw.thread);
  if (pw.playback.stream &&
      channels == pw.playback.format.channelCount)
    result = pw_stream_set_control(pw.playback.stream,
        SPA_PROP_channelVolumes, channels, param, 0);
  pw_thread_loop_unlock(pw.thread);

  if (result < 0)
    DEBUG_WARN("Failed to set PipeWire playback volume: %s",
        spa_strerror(result));
}

static void pipewire_playbackMute(bool mute)
{
  int result = 0;
  pw_thread_loop_lock(pw.thread);
  if (pw.playback.stream)
  {
    float val = mute ? 1.0f : 0.0f;
    result = pw_stream_set_control(
        pw.playback.stream, SPA_PROP_mute, 1, &val, 0);
  }
  pw_thread_loop_unlock(pw.thread);

  if (result < 0)
    DEBUG_WARN("Failed to set PipeWire playback mute: %s",
        spa_strerror(result));
}

static uint64_t pipewire_playbackLatency(void)
{
  atomic_store_explicit(
      &pw.playback.latencyUpdateRequested, true, memory_order_release);

  const int64_t latencyNs = atomic_load_explicit(
      &pw.playback.latencyNs, memory_order_acquire);
  if (latencyNs <= 0)
    return 0;

  return latencyNs / 1000;
}

static void pipewire_recordSignalSender(void)
{
  if (atomic_exchange_explicit(
        &pw.record.sendWakePending, true, memory_order_acq_rel))
    return;

  if (sem_post(&pw.record.sendWake) < 0)
  {
    atomic_fetch_add_explicit(
        &pw.record.signalErrors, 1, memory_order_relaxed);
    if (errno != EOVERFLOW)
      atomic_store_explicit(
          &pw.record.sendWakePending, false, memory_order_release);
  }
}

static void pipewire_recordRejected(int frames)
{
  atomic_fetch_add_explicit(
      &pw.record.rejectedBatches, 1, memory_order_relaxed);
  atomic_fetch_add_explicit(
      &pw.record.rejectedFrames, frames, memory_order_relaxed);
}

static void pipewire_recordAdvanceClock(
    LG_AudioClock * clock, int frames)
{
  const double rate = clock->rate > 0.0 ?
    clock->rate : pw.record.format.sampleRate;
  clock->position += frames;
  if (rate > 0.0)
    clock->time += (int64_t)llrint(
        (double)frames * SPA_NSEC_PER_SEC / rate);
  clock->discontinuity = false;
}

static bool pipewire_recordClockJump(
    double elapsedNs, double expectedNs)
{
  const double tolerance =
    RECORD_CLOCK_TOLERANCE_FRAMES * SPA_NSEC_PER_SEC /
    pw.record.format.sampleRate;
  return elapsedNs <= 0 ||
    fabs(elapsedNs - expectedNs) > tolerance;
}

static RecordBlock pipewire_recordMakeBlock(
    struct pw_buffer * pbuf, const struct spa_buffer * sbuf, int frames)
{
  RecordBlock block =
  {
    .frames = frames
  };
  struct pw_time timing = {0};
#if PW_CHECK_VERSION(0, 3, 50)
  const int timingResult = pw_stream_get_time_n(
      pw.record.stream, &timing, sizeof(timing));
#else
  const int timingResult = pw_stream_get_time(
      pw.record.stream, &timing);
#endif
  const bool timingValid = timingResult >= 0 &&
    timing.rate.num != 0 && timing.rate.denom != 0;

  int64_t clockTime = 0;
#if PW_CHECK_VERSION(1, 0, 5)
  if (pbuf->time)
    clockTime = (int64_t)pbuf->time;
#endif
  if (!clockTime && timingResult >= 0)
    clockTime = timing.now;

  const uint64_t position = pw.record.clockPosition;
  pw.record.clockPosition += frames;

  bool discontinuity = pw.record.clockDiscontinuityPending;
  const struct spa_meta_header * header = spa_buffer_find_meta_data(
      sbuf, SPA_META_Header, sizeof(*header));
  if (header &&
      (header->flags & (SPA_META_HEADER_FLAG_DISCONT |
          SPA_META_HEADER_FLAG_CORRUPTED)))
    discontinuity = true;
  if (sbuf->datas[0].chunk->flags & SPA_CHUNK_FLAG_CORRUPTED)
    discontinuity = true;

  block.clockValid = clockTime > 0;
  if (block.clockValid)
  {
    const bool clockStable = pw.record.clockLastValid;
    if (pw.record.clockLastValid)
    {
      const uint64_t elapsedFrames =
        position - pw.record.clockLastPosition;
      const double expectedNs =
        (double)elapsedFrames * SPA_NSEC_PER_SEC /
        pw.record.format.sampleRate;
      if (pipewire_recordClockJump(
            clockTime - pw.record.clockLastTime, expectedNs))
        discontinuity = true;
    }

    if (timingValid && pw.record.clockLastTimingValid)
    {
      if (timing.rate.num != pw.record.clockLastRate.num ||
          timing.rate.denom != pw.record.clockLastRate.denom ||
          timing.ticks <= pw.record.clockLastTicks)
        discontinuity = true;
      else
      {
        const double elapsedTickNs =
          (double)(timing.ticks - pw.record.clockLastTicks) *
          timing.rate.num * SPA_NSEC_PER_SEC / timing.rate.denom;
        const double expectedNs =
          (double)(position - pw.record.clockLastPosition) *
          SPA_NSEC_PER_SEC / pw.record.format.sampleRate;
        if (pipewire_recordClockJump(elapsedTickNs, expectedNs))
          discontinuity = true;
      }
    }

    block.clock = (LG_AudioClock)
    {
      .position      = position,
      .time          = clockTime,
      .rate          = 0.0,
      .stable        = clockStable && !discontinuity,
      .discontinuity = discontinuity
    };
    pw.record.clockDiscontinuityPending = false;
    pw.record.clockLastPosition = position;
    pw.record.clockLastTime     = clockTime;
    pw.record.clockLastValid    = true;
  }
  else
  {
    pw.record.clockDiscontinuityPending |= discontinuity;
    pw.record.clockLastValid = false;
  }

  if (timingValid)
  {
    pw.record.clockLastTicks       = timing.ticks;
    pw.record.clockLastRate        = timing.rate;
    pw.record.clockLastTimingValid = block.clockValid;
  }
  else
    pw.record.clockLastTimingValid = false;

  return block;
}

static bool pipewire_recordLatestPending(void)
{
  for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
    if (atomic_load_explicit(
          &pw.record.latest[i].state, memory_order_acquire) !=
        RECORD_LATEST_FREE)
      return true;

  return false;
}

static bool pipewire_recordSendLatest(void)
{
  int chosen = -1;
  uint64_t chosenSerial = 0;
  for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
  {
    if (atomic_load_explicit(
          &pw.record.latest[i].state, memory_order_acquire) !=
        RECORD_LATEST_READY)
      continue;

    const uint64_t serial = atomic_load_explicit(
        &pw.record.latest[i].serial, memory_order_relaxed);
    if (chosen < 0 || serial > chosenSerial)
    {
      chosen       = i;
      chosenSerial = serial;
    }
  }

  if (chosen < 0)
    return false;

  unsigned int expected = RECORD_LATEST_READY;
  if (!atomic_compare_exchange_strong_explicit(
        &pw.record.latest[chosen].state, &expected,
        RECORD_LATEST_READING, memory_order_acq_rel, memory_order_acquire))
    return true;

  /* Only the newest overload block is useful. Drop older blocks before
   * delivering it so recovery cannot build another backlog. */
  for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
  {
    if (i == chosen ||
        atomic_load_explicit(
          &pw.record.latest[i].serial, memory_order_relaxed) > chosenSerial)
      continue;

    expected = RECORD_LATEST_READY;
    if (atomic_compare_exchange_strong_explicit(
          &pw.record.latest[i].state, &expected,
          RECORD_LATEST_READING,
          memory_order_acq_rel, memory_order_acquire))
    {
      const int frames = pw.record.latest[i].frames;
      atomic_store_explicit(&pw.record.latest[i].state,
          RECORD_LATEST_FREE, memory_order_release);
      atomic_fetch_add_explicit(&pw.record.droppedFrames,
          frames, memory_order_relaxed);
    }
  }

  const int frames = pw.record.latest[chosen].frames;
  const LG_AudioClock * clock =
    pw.record.latest[chosen].clockValid ?
      &pw.record.latest[chosen].clock : NULL;
  if (!pw.record.pushFn(
        pw.record.latestBuffer +
          (size_t)chosen * pw.record.sendBufferFrames * pw.record.stride,
        frames, clock))
    pipewire_recordRejected(frames);
  atomic_store_explicit(&pw.record.latest[chosen].state,
      RECORD_LATEST_FREE, memory_order_release);
  return true;
}

static int pipewire_recordSendThread(void * opaque)
{
  uint64_t nextErrorReport = nanotime() +
    RECORD_ERROR_REPORT_INTERVAL_NS;

  for (;;)
  {
    int result;
    do
      result = sem_wait(&pw.record.sendWake);
    while (result < 0 && errno == EINTR);

    if (result < 0)
    {
      DEBUG_ERROR("Failed to wait for the PipeWire recording worker");
      break;
    }

    /* Clear before draining. A producer racing before this point is covered by
     * this wake, while a producer racing after it publishes the sole next
     * wake. */
    atomic_store_explicit(
        &pw.record.sendWakePending, false, memory_order_release);
    if (atomic_load_explicit(&pw.record.sendStop, memory_order_acquire))
      break;

    for (;;)
    {
      pipewire_reportRecordErrorsDue(&nextErrorReport);

      if (atomic_load_explicit(
            &pw.record.sendStop, memory_order_acquire))
        break;

      if (atomic_load_explicit(
            &pw.record.latestMode, memory_order_acquire))
      {
        const int queued = ringbuffer_getCount(pw.record.sendQueue);
        if (queued > 0)
        {
          ringbuffer_consume(pw.record.sendQueue, NULL, queued);
          atomic_fetch_add_explicit(
              &pw.record.droppedFrames, queued, memory_order_relaxed);
        }
        const int queuedBlocks = ringbuffer_getCount(pw.record.sendBlocks);
        if (queuedBlocks > 0)
          ringbuffer_consume(
              pw.record.sendBlocks, NULL, queuedBlocks);

        while (!atomic_load_explicit(
                 &pw.record.sendStop, memory_order_acquire) &&
               pipewire_recordSendLatest())
          ;

        atomic_store_explicit(
            &pw.record.latestMode, false, memory_order_release);
        if (pipewire_recordLatestPending())
        {
          atomic_store_explicit(
              &pw.record.latestMode, true, memory_order_release);
          continue;
        }
      }

      RecordBlock block;
      if (ringbuffer_consume(
            pw.record.sendBlocks, &block, 1) != 1)
        break;

      LG_AudioClock clock = block.clock;
      int remaining = block.frames;
      while (remaining > 0 && !atomic_load_explicit(
               &pw.record.sendStop, memory_order_acquire))
      {
        const int frames = min(
            remaining, pw.record.sendBufferFrames);
        const int consumed = ringbuffer_consume(
            pw.record.sendQueue, pw.record.sendBuffer, frames);
        DEBUG_ASSERT(consumed == frames);
        if (consumed != frames)
        {
          atomic_fetch_add_explicit(
              &pw.record.bufferErrors, 1, memory_order_relaxed);
          break;
        }

        if (!pw.record.pushFn(pw.record.sendBuffer, frames,
              block.clockValid ? &clock : NULL))
          pipewire_recordRejected(frames);
        remaining -= frames;
        if (block.clockValid)
          pipewire_recordAdvanceClock(&clock, frames);
      }
    }

    if (atomic_load_explicit(
          &pw.record.sendStop, memory_order_acquire))
      break;
  }

  pipewire_reportRecordErrors();
  return 0;
}

static void pipewire_recordStopSender(void)
{
  const bool hadThread = pw.record.sendThread != NULL;
  if (pw.record.sendThread)
  {
    atomic_store_explicit(
        &pw.record.sendStop, true, memory_order_release);
    pipewire_recordSignalSender();
    lgJoinThread(pw.record.sendThread, NULL);
    pw.record.sendThread = NULL;
  }

  if (pw.record.sendWakeInitialized)
  {
    sem_destroy(&pw.record.sendWake);
    pw.record.sendWakeInitialized = false;
  }

  ringbuffer_free(&pw.record.sendQueue);
  ringbuffer_free(&pw.record.sendBlocks);
  free(pw.record.sendBuffer);
  free(pw.record.latestBuffer);
  pw.record.sendBuffer       = NULL;
  pw.record.latestBuffer     = NULL;
  pw.record.sendBufferFrames = 0;
  pw.record.maxQueuedFrames  = 0;
  if (!hadThread)
    pipewire_reportRecordErrors();
}

static bool pipewire_recordStartSender(int sampleRate)
{
  /* This is overload capacity, not a prebuffer. The sender drains whatever is
   * available as soon as the RT callback transitions the queue from empty. */
  const int queueFrames = max(sampleRate / 10, 1);
  const int sendFrames = max(sampleRate / 100, 1);

  pw.record.sendQueue = ringbuffer_new(
      queueFrames, pw.record.stride);
  pw.record.sendBlocks = ringbuffer_new(
      queueFrames, sizeof(RecordBlock));
  pw.record.sendBuffer = malloc(
      (size_t)sendFrames * pw.record.stride);
  pw.record.latestBuffer = malloc(
      (size_t)RECORD_LATEST_SLOTS * sendFrames * pw.record.stride);
  pw.record.sendBufferFrames = sendFrames;
  pw.record.maxQueuedFrames  = max(sampleRate / 50, 1);
  if (!pw.record.sendQueue || !pw.record.sendBlocks ||
      !pw.record.sendBuffer || !pw.record.latestBuffer)
  {
    DEBUG_ERROR("Failed to allocate the PipeWire recording queue");
    pipewire_recordStopSender();
    return false;
  }

  if (sem_init(&pw.record.sendWake, 0, 0) < 0)
  {
    DEBUG_ERROR("Failed to create the PipeWire recording worker semaphore");
    pipewire_recordStopSender();
    return false;
  }
  pw.record.sendWakeInitialized = true;

  atomic_store_explicit(
      &pw.record.sendStop, false, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.sendWakePending, false, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.latestMode, false, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.latestSerial, 0, memory_order_relaxed);
  for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
  {
    atomic_store_explicit(&pw.record.latest[i].state,
        RECORD_LATEST_FREE, memory_order_relaxed);
    atomic_store_explicit(
        &pw.record.latest[i].serial, 0, memory_order_relaxed);
    pw.record.latest[i].frames     = 0;
    pw.record.latest[i].clockValid = false;
    pw.record.latest[i].clock      = (LG_AudioClock) {0};
  }
  pw.record.clockPosition             = 0;
  pw.record.clockLastPosition         = 0;
  pw.record.clockLastTicks            = 0;
  pw.record.clockLastTime             = 0;
  pw.record.clockLastRate             = (struct spa_fraction) {0};
  pw.record.clockLastValid            = false;
  pw.record.clockLastTimingValid      = false;
  pw.record.clockDiscontinuityPending = false;
  atomic_store_explicit(
      &pw.record.bufferErrors, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.droppedFrames, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.rejectedBatches, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.rejectedFrames, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.signalErrors, 0, memory_order_relaxed);
  atomic_store_explicit(
      &pw.record.recycleError, 0, memory_order_relaxed);
  if (!lgCreateThread("pwRecordSend", pipewire_recordSendThread,
        NULL, &pw.record.sendThread))
  {
    pipewire_recordStopSender();
    return false;
  }

  return true;
}

static void pipewire_recordStopStream(void)
{
  pw_thread_loop_lock(pw.thread);
  pw.record.failureFn     = NULL;
  pw.record.failureCookie = 0;
  if (pw.record.stream)
  {
    pw_stream_destroy(pw.record.stream);
    pw.record.stream = NULL;
  }
  pw.record.active = false;
  pw_thread_loop_unlock(pw.thread);

  pipewire_recordStopSender();
  pw.record.pushFn = NULL;
}

static void pipewire_recordQueueFrames(
    const void * data, const RecordBlock * block)
{
  const int frames = block->frames;
  int       chosen = -1;
  if (atomic_load_explicit(
        &pw.record.latestMode, memory_order_acquire))
    goto latest;

  const int occupancy = ringbuffer_getCount(pw.record.sendQueue);
  const int available =
    max(0, ringbuffer_getLength(pw.record.sendQueue) - occupancy);
  const bool exceedsBacklog = occupancy > 0 &&
    (occupancy >= pw.record.maxQueuedFrames ||
     frames > pw.record.maxQueuedFrames - occupancy);
  if (frames > available || exceedsBacklog)
    goto latest;

  const int advanced =
    ringbuffer_append(pw.record.sendQueue, data, frames);
  DEBUG_ASSERT(advanced == frames);
  if (advanced == frames)
  {
    const int blocks = ringbuffer_append(
        pw.record.sendBlocks, block, 1);
    DEBUG_ASSERT(blocks == 1);
    if (blocks == 1)
    {
      pipewire_recordSignalSender();
      return;
    }
  }

latest:
  for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
  {
    unsigned int expected = RECORD_LATEST_FREE;
    if (atomic_compare_exchange_strong_explicit(
          &pw.record.latest[i].state, &expected,
          RECORD_LATEST_WRITING, memory_order_acq_rel, memory_order_acquire))
    {
      chosen = i;
      break;
    }
  }

  if (chosen < 0)
  {
    int oldest = -1;
    uint64_t oldestSerial = UINT64_MAX;
    for (int i = 0; i < RECORD_LATEST_SLOTS; ++i)
    {
      if (atomic_load_explicit(
            &pw.record.latest[i].state, memory_order_acquire) !=
          RECORD_LATEST_READY)
        continue;

      const uint64_t serial = atomic_load_explicit(
          &pw.record.latest[i].serial, memory_order_relaxed);
      if (serial < oldestSerial)
      {
        oldest       = i;
        oldestSerial = serial;
      }
    }

    if (oldest >= 0)
    {
      unsigned int expected = RECORD_LATEST_READY;
      if (atomic_compare_exchange_strong_explicit(
            &pw.record.latest[oldest].state, &expected,
            RECORD_LATEST_WRITING,
            memory_order_acq_rel, memory_order_acquire))
      {
        chosen = oldest;
        const int dropped = pw.record.latest[oldest].frames;
        atomic_fetch_add_explicit(&pw.record.droppedFrames,
            dropped, memory_order_relaxed);
      }
    }
  }

  if (chosen < 0)
  {
    atomic_fetch_add_explicit(
        &pw.record.droppedFrames, frames, memory_order_relaxed);
    pw.record.clockDiscontinuityPending = true;
    atomic_store_explicit(
        &pw.record.latestMode, true, memory_order_release);
    pipewire_recordSignalSender();
    return;
  }

  const int keep = min(frames, pw.record.sendBufferFrames);
  memcpy(
      pw.record.latestBuffer +
        (size_t)chosen * pw.record.sendBufferFrames * pw.record.stride,
      (const uint8_t *)data + (size_t)(frames - keep) * pw.record.stride,
      (size_t)keep * pw.record.stride);
  pw.record.latest[chosen].frames     = keep;
  pw.record.latest[chosen].clockValid = block->clockValid;
  if (block->clockValid)
  {
    pw.record.latest[chosen].clock = block->clock;
    pipewire_recordAdvanceClock(
        &pw.record.latest[chosen].clock, frames - keep);
    pw.record.latest[chosen].clock.discontinuity = true;
    pw.record.latest[chosen].clock.stable        = false;
  }
  atomic_store_explicit(&pw.record.latest[chosen].serial,
      atomic_fetch_add_explicit(
        &pw.record.latestSerial, 1, memory_order_relaxed) + 1,
      memory_order_relaxed);
  atomic_store_explicit(&pw.record.latest[chosen].state,
      RECORD_LATEST_READY, memory_order_release);
  atomic_store_explicit(
      &pw.record.latestMode, true, memory_order_release);

  if (keep != frames)
    atomic_fetch_add_explicit(
        &pw.record.droppedFrames, frames - keep, memory_order_relaxed);
  pipewire_recordSignalSender();
}

static void pipewire_onRecordProcess(void * userdata)
{
  struct pw_buffer * pbuf;

  if (!(pbuf = pw_stream_dequeue_buffer(pw.record.stream)))
  {
    atomic_fetch_add_explicit(
        &pw.record.bufferErrors, 1, memory_order_relaxed);
    pw.record.clockDiscontinuityPending = true;
    return;
  }

  struct spa_buffer * sbuf = pbuf->buffer;
  if (!sbuf || sbuf->n_datas == 0 || !sbuf->datas[0].data ||
      !sbuf->datas[0].chunk ||
      sbuf->datas[0].chunk->offset > sbuf->datas[0].maxsize)
  {
    atomic_fetch_add_explicit(
        &pw.record.bufferErrors, 1, memory_order_relaxed);
    pw.record.clockDiscontinuityPending = true;
#if PW_CHECK_VERSION(1, 4, 0)
    const int recycleResult =
      pw_stream_return_buffer(pw.record.stream, pbuf);
#else
    const int recycleResult =
      pw_stream_queue_buffer(pw.record.stream, pbuf);
#endif
    pipewire_storeError(&pw.record.recycleError, recycleResult);
    return;
  }

  const uint32_t offset = sbuf->datas[0].chunk->offset;
  const uint32_t flags  = sbuf->datas[0].chunk->flags;
  const bool     empty  = flags & SPA_CHUNK_FLAG_EMPTY;
  if (flags & (SPA_CHUNK_FLAG_EMPTY | SPA_CHUNK_FLAG_CORRUPTED))
    pw.record.clockDiscontinuityPending = true;

  const uint32_t bytes = empty ? 0 : min(
      sbuf->datas[0].chunk->size,
      sbuf->datas[0].maxsize - offset);
  const int frames = bytes / pw.record.stride;
  if (bytes % pw.record.stride)
    pw.record.clockDiscontinuityPending = true;
  if (frames > 0)
  {
    const RecordBlock block =
      pipewire_recordMakeBlock(pbuf, sbuf, frames);
    pipewire_recordQueueFrames(
        (uint8_t *)sbuf->datas[0].data + offset, &block);
  }

  pipewire_storeError(&pw.record.recycleError,
      pw_stream_queue_buffer(pw.record.stream, pbuf));
}

static void pipewire_onRecordStateChanged(void * userdata,
    enum pw_stream_state old, enum pw_stream_state state,
    const char * error)
{
  (void)error;
  pw.record.connectionState = state;
  if (state == PW_STREAM_STATE_ERROR ||
      state == PW_STREAM_STATE_UNCONNECTED)
  {
    LG_AudioFailureFn failureFn = pw.record.failureFn;
    const uint32_t failureCookie = pw.record.failureCookie;
    pw.record.failureFn     = NULL;
    pw.record.failureCookie = 0;

    if (failureFn && failureCookie)
      failureFn(failureCookie);
  }
  pw_thread_loop_signal(pw.thread, false);
}

static int pipewire_recordControl(bool active)
{
  int error = 0;
  pw_thread_loop_lock(pw.thread);
  if (!pw.record.stream)
    error = active ? -ENODEV : 0;
  else if (pw.record.active != active)
  {
    error = pw_stream_set_active(pw.record.stream, active);
    if (error >= 0)
      pw.record.active = active;
  }
  pw_thread_loop_unlock(pw.thread);
  return error;
}

static bool pipewire_recordStart(const LG_AudioFormat * format,
    LG_AudioPushFn pushFn, LG_AudioFailureFn failureFn,
    uint32_t failureCookie)
{
  pipewire_recordStopStream();

  const int channels     = format->channelCount;
  const int sampleRate   = format->sampleRate;
  const int sampleSize   = pipewire_sampleSize(format->sampleFormat);
  const enum spa_audio_format sampleFormat =
    pipewire_sampleFormat(format->sampleFormat);
  if (!sampleSize || sampleFormat == SPA_AUDIO_FORMAT_UNKNOWN)
    return false;

  const int periodFrames = max(sampleRate / 1000, 1);
  char requestedNodeLatency[32];
  snprintf(requestedNodeLatency, sizeof(requestedNodeLatency), "%d/%d",
      periodFrames, sampleRate);
  const struct spa_pod * params[2];
  uint8_t buffer[1024];
  struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer));
  static const struct pw_stream_events events =
  {
    .version       = PW_VERSION_STREAM_EVENTS,
    .state_changed = pipewire_onRecordStateChanged,
    .process       = pipewire_onRecordProcess
  };

  pw.record.format = *format;
  pw.record.stride = sampleSize * channels;
  pw.record.pushFn = pushFn;
  if (!pipewire_recordStartSender(sampleRate))
    return false;

  struct pw_properties * props =
    pw_properties_new(
      PW_KEY_NODE_NAME     , "Looking Glass",
      PW_KEY_MEDIA_TYPE    , "Audio",
      PW_KEY_MEDIA_CATEGORY, "Capture",
      PW_KEY_MEDIA_ROLE    , "Music",
      PW_KEY_NODE_LATENCY  , requestedNodeLatency,
      NULL
    );
  if (!props)
  {
    DEBUG_ERROR("Failed to create recording stream properties");
    pipewire_recordStopSender();
    return false;
  }

  const char * device = option_get_string("pipewire", "recDevice");
  if (device)
  {
#ifdef PW_KEY_TARGET_OBJECT
    pw_properties_set(props, PW_KEY_TARGET_OBJECT, device);
#else
    pw_properties_set(props, PW_KEY_NODE_TARGET, device);
#endif
  }

  pw_thread_loop_lock(pw.thread);
  pw.record.failureFn     = failureFn;
  pw.record.failureCookie = failureCookie;
  pw.record.stream = pw_stream_new_simple(
    pw.loop,
    "Looking Glass",
    props,
    &events,
    NULL
  );

  if (!pw.record.stream)
  {
    pw.record.failureFn     = NULL;
    pw.record.failureCookie = 0;
    pw_thread_loop_unlock(pw.thread);
    DEBUG_ERROR("Failed to create the stream");
    pipewire_recordStopSender();
    return false;
  }

  struct spa_audio_info_raw info =
    pipewire_audioInfo(format, sampleFormat);
  params[0] = spa_format_audio_raw_build(
      &b, SPA_PARAM_EnumFormat, &info);
  params[1] = spa_pod_builder_add_object(&b,
      SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta,
      SPA_PARAM_META_type, SPA_POD_Id(SPA_META_Header),
      SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_header)));

  pw.record.connectionState = PW_STREAM_STATE_CONNECTING;
  int result = pw_stream_connect(
      pw.record.stream,
      PW_DIRECTION_INPUT,
      PW_ID_ANY,
      PW_STREAM_FLAG_AUTOCONNECT |
      PW_STREAM_FLAG_MAP_BUFFERS |
      PW_STREAM_FLAG_RT_PROCESS  |
      PW_STREAM_FLAG_INACTIVE,
      params, 2);

  if (result < 0)
  {
    DEBUG_ERROR("Failed to connect recording stream: %s",
        spa_strerror(result));
    pw.record.failureFn     = NULL;
    pw.record.failureCookie = 0;
    pw_stream_destroy(pw.record.stream);
    pw.record.stream = NULL;
    pw_thread_loop_unlock(pw.thread);
    pipewire_recordStopSender();
    return false;
  }

  if (!pipewire_waitForStream(&pw.record.connectionState) ||
      pw.record.connectionState != PW_STREAM_STATE_PAUSED)
  {
    DEBUG_ERROR("PipeWire recording stream did not become ready");
    pw.record.failureFn     = NULL;
    pw.record.failureCookie = 0;
    pw_stream_destroy(pw.record.stream);
    pw.record.stream = NULL;
    pw_thread_loop_unlock(pw.thread);
    pipewire_recordStopSender();
    return false;
  }

  pw_thread_loop_unlock(pw.thread);
  result = pipewire_recordControl(true);
  if (result < 0)
  {
    DEBUG_ERROR("Failed to activate recording stream: %s",
        spa_strerror(result));
    pipewire_recordStopStream();
    return false;
  }

  return true;
}

static void pipewire_recordStop(void)
{
  pipewire_recordStopStream();
}

static void pipewire_recordVolume(int channels, const uint16_t volume[])
{
  if (channels <= 0 || channels > LG_AUDIO_MAX_CHANNELS)
    return;

  float param[channels];
  for(int i = 0; i < channels; ++i)
    param[i] = max(0.0,
        9.3234e-7 * pow(1.000211902, volume[i]) - 0.000172787);

  int result = 0;
  pw_thread_loop_lock(pw.thread);
  if (pw.record.stream &&
      channels == pw.record.format.channelCount)
    result = pw_stream_set_control(pw.record.stream,
        SPA_PROP_channelVolumes, channels, param, 0);
  pw_thread_loop_unlock(pw.thread);

  if (result < 0)
    DEBUG_WARN("Failed to set PipeWire recording volume: %s",
        spa_strerror(result));
}

static void pipewire_recordMute(bool mute)
{
  int result = 0;
  pw_thread_loop_lock(pw.thread);
  if (pw.record.stream)
  {
    float val = mute ? 1.0f : 0.0f;
    result = pw_stream_set_control(
        pw.record.stream, SPA_PROP_mute, 1, &val, 0);
  }
  pw_thread_loop_unlock(pw.thread);

  if (result < 0)
    DEBUG_WARN("Failed to set PipeWire recording mute: %s",
        spa_strerror(result));
}

static void pipewire_free(void)
{
  pipewire_playbackStopStream();
  pipewire_recordStopStream();

  pw_thread_loop_lock(pw.thread);
  if (pw.core)
  {
    pw_core_disconnect(pw.core);
    pw.core = NULL;
  }
  pw_thread_loop_unlock(pw.thread);
  pw_thread_loop_stop(pw.thread);
  pw_thread_loop_destroy(pw.thread);
  pw_context_destroy(pw.context);
  pw_loop_destroy(pw.loop);

  pw.loop    = NULL;
  pw.context = NULL;
  pw.thread  = NULL;

  pw_deinit();
}

struct LG_AudioDevOps LGAD_PipeWire =
{
  .name      = "PipeWire",
  .earlyInit = pipewire_earlyInit,
  .init      = pipewire_init,
  .free      = pipewire_free,
  .playback =
  {
    .setup   = pipewire_playbackSetup,
    .start   = pipewire_playbackStart,
    .stop    = pipewire_playbackStop,
    .volume  = pipewire_playbackVolume,
    .mute    = pipewire_playbackMute,
#if PW_CHECK_VERSION(1, 4, 0)
    .setRate = pipewire_playbackSetRate,
#endif
    .latency = pipewire_playbackLatency
  },
  .record =
  {
    .start  = pipewire_recordStart,
    .stop   = pipewire_recordStop,
    .volume = pipewire_recordVolume,
    .mute   = pipewire_recordMute
  }
};
