#include "media/audio_playback.hpp"

#include <algorithm>
#include <chrono>
#include <cmath>
#include <deque>
#include <iomanip>
#include <limits>
#include <sstream>
#include <thread>

#include "media/ffmpeg_raii.hpp"

#if defined(SMARTCUT_HAS_FFMPEG) && defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <mmsystem.h>
#endif

namespace smartcut::media {
namespace {
std::string pcm_hash(const std::vector<std::int16_t>& samples) {
  std::uint64_t hash = 14695981039346656037ULL;
  for (const auto sample : samples) {
    const auto value = static_cast<std::uint16_t>(sample);
    hash ^= value & 0xffU; hash *= 1099511628211ULL;
    hash ^= value >> 8U; hash *= 1099511628211ULL;
  }
  std::ostringstream output; output << std::hex << std::setfill('0') << std::setw(16) << hash; return output.str();
}
std::optional<std::uint64_t> scale_position(std::uint64_t value,
                                            std::uint64_t numerator,
                                            std::uint64_t denominator) {
  if (denominator == 0 || numerator == 0 ||
      value > std::numeric_limits<std::uint64_t>::max() / numerator)
    return std::nullopt;
  return value * numerator / denominator;
}
}  // namespace

std::optional<std::uint64_t> audio_device_position_us(
    std::uint64_t value, AudioDevicePositionUnit unit, int sample_rate,
    int block_align) {
  switch (unit) {
    case AudioDevicePositionUnit::samples:
      if (sample_rate <= 0) return std::nullopt;
      return scale_position(value, 1000000ULL,
                            static_cast<std::uint64_t>(sample_rate));
    case AudioDevicePositionUnit::bytes:
      if (block_align <= 0 || sample_rate <= 0) return std::nullopt;
      return scale_position(value / static_cast<std::uint64_t>(block_align),
                            1000000ULL,
                            static_cast<std::uint64_t>(sample_rate));
    case AudioDevicePositionUnit::milliseconds:
      return scale_position(value, 1000ULL, 1ULL);
  }
  return std::nullopt;
}

DecodedPcmAudio decode_audio_pcm(const std::filesystem::path& path, std::size_t max_sample_frames) {
  DecodedPcmAudio result;
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)max_sample_frames; return result;
#else
  auto format = ffmpeg::open_input(path);
  if (!format || avformat_find_stream_info(format.get(), nullptr) < 0) return result;
  int audio_stream = -1;
  for (unsigned i = 0; i < format->nb_streams; ++i)
    if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream = static_cast<int>(i); break; }
  if (audio_stream < 0) return result;
  auto* stream = format->streams[audio_stream];
  const auto* decoder = avcodec_find_decoder(stream->codecpar->codec_id);
  auto context = ffmpeg::make_codec_context(decoder);
  if (!context || avcodec_parameters_to_context(context.get(), stream->codecpar) < 0 || avcodec_open2(context.get(), decoder, nullptr) < 0) return result;
  if (context->ch_layout.nb_channels <= 0) av_channel_layout_default(&context->ch_layout, stream->codecpar->ch_layout.nb_channels > 0 ? stream->codecpar->ch_layout.nb_channels : 2);
  result.sample_rate = context->sample_rate > 0 ? context->sample_rate : 48000;
  result.channels = context->ch_layout.nb_channels == 1 ? 1 : 2;
  result.time_base_num = stream->time_base.num; result.time_base_den = stream->time_base.den;
  AVChannelLayout output_layout{}; av_channel_layout_default(&output_layout, result.channels);
  SwrContext* raw_swr = nullptr;
  if (swr_alloc_set_opts2(&raw_swr, &output_layout, AV_SAMPLE_FMT_S16, result.sample_rate,
                          &context->ch_layout, context->sample_fmt, context->sample_rate, 0, nullptr) < 0) {
    av_channel_layout_uninit(&output_layout); return {};
  }
  ffmpeg::Swr resampler(raw_swr); av_channel_layout_uninit(&output_layout);
  if (swr_init(resampler.get()) < 0) return {};
  auto packet = ffmpeg::make_packet(); auto frame = ffmpeg::make_frame();
  if (!packet || !frame) return {};
  bool have_pts = false;
  auto receive = [&] {
    while (avcodec_receive_frame(context.get(), frame.get()) >= 0) {
      if (!have_pts) { result.first_pts = frame->best_effort_timestamp; have_pts = true; }
      const auto capacity = static_cast<int>(av_rescale_rnd(swr_get_delay(resampler.get(), context->sample_rate) + frame->nb_samples,
                                                            result.sample_rate, context->sample_rate, AV_ROUND_UP));
      const auto old_size = result.samples.size(); result.samples.resize(old_size + static_cast<std::size_t>(capacity * result.channels));
      auto* destination = reinterpret_cast<std::uint8_t*>(result.samples.data() + old_size);
      const int converted = swr_convert(resampler.get(), &destination, capacity,
                                        const_cast<const std::uint8_t**>(frame->extended_data), frame->nb_samples);
      if (converted < 0) { result.samples.resize(old_size); }
      else result.samples.resize(old_size + static_cast<std::size_t>(converted * result.channels));
      if (max_sample_frames > 0 && result.samples.size() > max_sample_frames * static_cast<std::size_t>(result.channels))
        result.samples.resize(max_sample_frames * static_cast<std::size_t>(result.channels));
      av_frame_unref(frame.get());
      if (max_sample_frames > 0 && result.samples.size() >= max_sample_frames * static_cast<std::size_t>(result.channels)) break;
    }
  };
  while (av_read_frame(format.get(), packet.get()) >= 0) {
    if (packet->stream_index == audio_stream && avcodec_send_packet(context.get(), packet.get()) >= 0) receive();
    av_packet_unref(packet.get());
    if (max_sample_frames > 0 && result.samples.size() >= max_sample_frames * static_cast<std::size_t>(result.channels)) break;
  }
  if (max_sample_frames == 0 || result.samples.size() < max_sample_frames * static_cast<std::size_t>(result.channels)) { avcodec_send_packet(context.get(), nullptr); receive(); }
  return result;
#endif
}

std::string audio_pcm_report(const std::filesystem::path& path, std::size_t max_sample_frames) {
  const auto audio = decode_audio_pcm(path, max_sample_frames);
  const auto frames = audio.channels > 0 ? audio.samples.size() / static_cast<std::size_t>(audio.channels) : 0;
  long double sum_squares = 0.0; std::uint16_t peak = 0;
  for (const auto sample : audio.samples) { const auto magnitude = static_cast<std::uint16_t>(std::min<std::int64_t>(32768, std::abs(static_cast<std::int64_t>(sample)))); peak = std::max(peak, magnitude); const auto normalized = static_cast<long double>(sample) / 32768.0L; sum_squares += normalized * normalized; }
  const double rms = audio.samples.empty() ? 0.0 : std::sqrt(static_cast<double>(sum_squares / audio.samples.size()));
  std::ostringstream output; output << "{\"schema\":\"audio-pcm-v1\",\"status\":\""
    << (frames > 0 ? "PASSED" : "FAILED") << "\",\"sample_rate\":" << audio.sample_rate
    << ",\"channels\":" << audio.channels << ",\"sample_frames\":" << frames
    << ",\"first_pts\":" << audio.first_pts << ",\"time_base_num\":" << audio.time_base_num
    << ",\"time_base_den\":" << audio.time_base_den << ",\"rms\":" << rms << ",\"peak\":" << (static_cast<double>(peak) / 32768.0)
    << ",\"pcm_hash\":\"" << pcm_hash(audio.samples) << "\"}";
  return output.str();
}

std::string audio_waveform_report(const std::filesystem::path& path, std::uint64_t start_us,
                                  std::uint64_t end_us, std::size_t bucket_count) {
  std::vector<double> peaks(bucket_count, 0.0);
  bool decoded_audio = false;
#if !defined(SMARTCUT_HAS_FFMPEG)
  (void)path;
#else
  if (end_us > start_us && bucket_count > 0) {
    auto format = ffmpeg::open_input(path);
    if (format && avformat_find_stream_info(format.get(), nullptr) >= 0) {
      int audio_stream = -1;
      for (unsigned i = 0; i < format->nb_streams; ++i) {
        if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
          audio_stream = static_cast<int>(i);
          break;
        }
      }
      if (audio_stream >= 0) {
        auto* stream = format->streams[audio_stream];
        const auto* decoder = avcodec_find_decoder(stream->codecpar->codec_id);
        auto context = ffmpeg::make_codec_context(decoder);
        if (context && avcodec_parameters_to_context(context.get(), stream->codecpar) >= 0 &&
            avcodec_open2(context.get(), decoder, nullptr) >= 0) {
          if (context->ch_layout.nb_channels <= 0) {
            av_channel_layout_default(&context->ch_layout,
                                      stream->codecpar->ch_layout.nb_channels > 0
                                          ? stream->codecpar->ch_layout.nb_channels
                                          : 2);
          }
          const int sample_rate = context->sample_rate > 0 ? context->sample_rate : 48000;
          AVChannelLayout mono{};
          av_channel_layout_default(&mono, 1);
          SwrContext* raw_swr = nullptr;
          const bool resampler_allocated = swr_alloc_set_opts2(
              &raw_swr, &mono, AV_SAMPLE_FMT_S16, sample_rate, &context->ch_layout,
              context->sample_fmt, context->sample_rate, 0, nullptr) >= 0;
          av_channel_layout_uninit(&mono);
          ffmpeg::Swr resampler(raw_swr);
          if (resampler_allocated && resampler && swr_init(resampler.get()) >= 0) {
            const auto safe_start = static_cast<std::int64_t>(std::min<std::uint64_t>(
                start_us, static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())));
            const auto safe_end = static_cast<std::int64_t>(std::min<std::uint64_t>(
                end_us, static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())));
            if (safe_start > 0) {
              // Matroska commonly indexes video clusters but not every audio
              // packet. A stream-specific audio seek can therefore fall back
              // to the beginning and decode tens of minutes for a sub-second
              // waveform. Global timestamps use the container's indexed
              // clusters and land beside the requested video position.
              auto seek_result = avformat_seek_file(format.get(), -1,
                  std::numeric_limits<std::int64_t>::min(), safe_start,
                  safe_start, AVSEEK_FLAG_BACKWARD);
              if (seek_result < 0) {
                int video_stream = -1;
                for (unsigned i = 0; i < format->nb_streams; ++i)
                  if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream = static_cast<int>(i); break; }
                if (video_stream >= 0) {
                  const auto target = av_rescale_q(safe_start, AVRational{1, 1000000}, format->streams[video_stream]->time_base);
                  seek_result = av_seek_frame(format.get(), video_stream, target, AVSEEK_FLAG_BACKWARD);
                }
              }
              if (seek_result >= 0) { avformat_flush(format.get()); avcodec_flush_buffers(context.get()); }
            }
            auto packet = ffmpeg::make_packet();
            auto frame = ffmpeg::make_frame();
            bool reached_end = false;
            std::int64_t fallback_us = safe_start;
            auto receive = [&] {
              while (!reached_end && avcodec_receive_frame(context.get(), frame.get()) >= 0) {
                const auto pts = frame->best_effort_timestamp != AV_NOPTS_VALUE
                                     ? frame->best_effort_timestamp
                                     : frame->pts;
                const auto frame_start_us = pts == AV_NOPTS_VALUE
                                                ? fallback_us
                                                : av_rescale_q(pts, stream->time_base,
                                                               AVRational{1, 1000000});
                const int capacity = static_cast<int>(av_rescale_rnd(
                    swr_get_delay(resampler.get(), context->sample_rate) + frame->nb_samples,
                    sample_rate, context->sample_rate, AV_ROUND_UP));
                std::vector<std::int16_t> converted(static_cast<std::size_t>(std::max(capacity, 0)));
                auto* destination = reinterpret_cast<std::uint8_t*>(converted.data());
                const int count = capacity > 0
                                      ? swr_convert(resampler.get(), &destination, capacity,
                                                    const_cast<const std::uint8_t**>(frame->extended_data),
                                                    frame->nb_samples)
                                      : 0;
                av_frame_unref(frame.get());
                if (count < 0) continue;
                decoded_audio = decoded_audio || count > 0;
                for (int sample = 0; sample < count; ++sample) {
                  const auto sample_us = frame_start_us +
                                         static_cast<std::int64_t>(sample) * 1000000LL / sample_rate;
                  if (sample_us < safe_start) continue;
                  if (sample_us >= safe_end) {
                    reached_end = true;
                    break;
                  }
                  const auto elapsed = static_cast<std::uint64_t>(sample_us - safe_start);
                  const auto duration = static_cast<std::uint64_t>(safe_end - safe_start);
                  const auto bucket = std::min<std::size_t>(
                      static_cast<std::size_t>(elapsed * bucket_count / duration), bucket_count - 1);
                  const auto magnitude = std::abs(static_cast<int>(converted[static_cast<std::size_t>(sample)])) /
                                         32768.0;
                  peaks[bucket] = std::max(peaks[bucket], magnitude);
                }
                fallback_us = frame_start_us + static_cast<std::int64_t>(count) * 1000000LL / sample_rate;
              }
            };
            while (!reached_end && av_read_frame(format.get(), packet.get()) >= 0) {
              if (packet->stream_index == audio_stream &&
                  avcodec_send_packet(context.get(), packet.get()) >= 0) {
                receive();
              }
              av_packet_unref(packet.get());
            }
            if (!reached_end) {
              avcodec_send_packet(context.get(), nullptr);
              receive();
            }
          }
        }
      }
    }
  }
#endif
  std::ostringstream output;
  output << "{\"schema\":\"audio-waveform-v1\",\"status\":\""
         << (decoded_audio ? "PASSED" : "FAILED") << "\",\"start_us\":" << start_us
         << ",\"end_us\":" << end_us << ",\"peaks\":[";
  output << std::fixed << std::setprecision(5);
  for (std::size_t i = 0; i < peaks.size(); ++i) {
    if (i > 0) output << ',';
    output << peaks[i];
  }
  output << "]}";
  return output.str();
}

std::string play_audio_device(const std::filesystem::path& path, std::uint64_t start_us,
                               std::optional<std::uint64_t> end_us,
                               std::stop_token stop, const std::function<void(std::uint64_t)>& progress,
                              const std::function<bool()>& cancel_requested,
                              const std::function<double()>& volume_gain) {
#if !defined(SMARTCUT_HAS_FFMPEG) || !defined(_WIN32)
  (void)path; (void)start_us; (void)end_us; (void)stop; (void)progress; (void)cancel_requested; (void)volume_gain;
  return "{\"schema\":\"audio-playback-v1\",\"status\":\"FAILED\",\"error\":\"audio playback unavailable\"}";
#else
  auto format = ffmpeg::open_input(path);
  if (!format || avformat_find_stream_info(format.get(), nullptr) < 0)
    return "{\"schema\":\"audio-playback-v1\",\"status\":\"FAILED\",\"error\":\"audio input failed\"}";
  int audio_stream = -1;
  for (unsigned i = 0; i < format->nb_streams; ++i)
    if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audio_stream = static_cast<int>(i); break; }
  if (audio_stream < 0)
    return "{\"schema\":\"audio-playback-v1\",\"status\":\"FAILED\",\"error\":\"audio stream missing\"}";
  auto* stream = format->streams[audio_stream];
  const auto* decoder = avcodec_find_decoder(stream->codecpar->codec_id);
  auto context = ffmpeg::make_codec_context(decoder);
  if (!context || avcodec_parameters_to_context(context.get(), stream->codecpar) < 0 || avcodec_open2(context.get(), decoder, nullptr) < 0)
    return "{\"schema\":\"audio-playback-v1\",\"status\":\"FAILED\",\"error\":\"audio decoder failed\"}";
  if (context->ch_layout.nb_channels <= 0) av_channel_layout_default(&context->ch_layout, stream->codecpar->ch_layout.nb_channels > 0 ? stream->codecpar->ch_layout.nb_channels : 2);
  const int sample_rate = context->sample_rate > 0 ? context->sample_rate : 48000;
  const int channels = context->ch_layout.nb_channels == 1 ? 1 : 2;
  AVChannelLayout output_layout{}; av_channel_layout_default(&output_layout, channels);
  SwrContext* raw_swr = nullptr;
  if (swr_alloc_set_opts2(&raw_swr, &output_layout, AV_SAMPLE_FMT_S16, sample_rate,
                          &context->ch_layout, context->sample_fmt, context->sample_rate, 0, nullptr) < 0) {
    av_channel_layout_uninit(&output_layout);
    return "{\"schema\":\"audio-playback-v1\",\"status\":\"FAILED\",\"error\":\"audio resampler failed\"}";
  }
  ffmpeg::Swr resampler(raw_swr); av_channel_layout_uninit(&output_layout);
  if (swr_init(resampler.get()) < 0)
    return "{\"schema\":\"audio-playback-v1\",\"status\":\"FAILED\",\"error\":\"audio resampler failed\"}";
  WAVEFORMATEX wave{}; wave.wFormatTag = WAVE_FORMAT_PCM; wave.nChannels = static_cast<WORD>(channels);
  wave.nSamplesPerSec = static_cast<DWORD>(sample_rate); wave.wBitsPerSample = 16;
  wave.nBlockAlign = static_cast<WORD>(wave.nChannels * wave.wBitsPerSample / 8U);
  wave.nAvgBytesPerSec = wave.nSamplesPerSec * wave.nBlockAlign;
  HWAVEOUT device = nullptr;
  if (waveOutOpen(&device, WAVE_MAPPER, &wave, 0, 0, CALLBACK_NULL) != MMSYSERR_NOERROR)
    return "{\"schema\":\"audio-playback-v1\",\"status\":\"FAILED\",\"error\":\"waveOutOpen failed\"}";
  struct WaveBlock { std::vector<std::int16_t> samples; WAVEHDR header{}; bool prepared{}; };
  std::deque<WaveBlock> queued;
  const auto is_cancelled = [&] { return stop.stop_requested() || (cancel_requested && cancel_requested()); };
  bool cancelled = false; bool failed = false; bool wrote_audio = false;
  std::uint64_t last_progress = std::numeric_limits<std::uint64_t>::max();
  auto report_progress = [&] {
    MMTIME position{}; position.wType = TIME_SAMPLES;
    if (waveOutGetPosition(device, &position, sizeof(position)) == MMSYSERR_NOERROR) {
      std::optional<std::uint64_t> offset_us;
      if (position.wType == TIME_SAMPLES)
        offset_us = audio_device_position_us(position.u.sample, AudioDevicePositionUnit::samples,
                                             sample_rate, wave.nBlockAlign);
      else if (position.wType == TIME_BYTES)
        offset_us = audio_device_position_us(position.u.cb, AudioDevicePositionUnit::bytes,
                                             sample_rate, wave.nBlockAlign);
      else if (position.wType == TIME_MS)
        offset_us = audio_device_position_us(position.u.ms, AudioDevicePositionUnit::milliseconds,
                                             sample_rate, wave.nBlockAlign);
      if (offset_us) {
        const auto played_us = *offset_us > std::numeric_limits<std::uint64_t>::max() - start_us
                                   ? std::numeric_limits<std::uint64_t>::max()
                                   : start_us + *offset_us;
        if (progress && (last_progress == std::numeric_limits<std::uint64_t>::max() ||
                         played_us > last_progress)) {
          progress(played_us);
          last_progress = played_us;
        }
      }
    }
  };
  auto drain_front = [&] {
    while (!queued.empty() && (queued.front().header.dwFlags & WHDR_DONE) == 0 && !is_cancelled()) {
      report_progress(); std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
    if (is_cancelled()) { cancelled = true; return false; }
    if (!queued.empty()) {
      waveOutUnprepareHeader(device, &queued.front().header, sizeof(WAVEHDR)); queued.pop_front();
    }
    return true;
  };
  const auto safe_start_us = static_cast<std::int64_t>(std::min<std::uint64_t>(start_us, static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())));
  if (start_us > 0) {
    // Large Matroska files commonly index video clusters but not every audio
    // packet. Seeking the audio stream directly can therefore scan gigabytes
    // before playback starts. Use the container's global index first, then the
    // indexed video stream as a fallback; decoded audio is still trimmed to the
    // exact requested timestamp below.
    auto seek_result = avformat_seek_file(
        format.get(), -1, std::numeric_limits<std::int64_t>::min(), safe_start_us,
        safe_start_us, AVSEEK_FLAG_BACKWARD);
    if (seek_result < 0) {
      int video_stream = -1;
      for (unsigned i = 0; i < format->nb_streams; ++i) {
        if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
          video_stream = static_cast<int>(i);
          break;
        }
      }
      if (video_stream >= 0) {
        const auto video_target = av_rescale_q(
            safe_start_us, AVRational{1, 1000000},
            format->streams[video_stream]->time_base);
        seek_result = av_seek_frame(format.get(), video_stream, video_target,
                                    AVSEEK_FLAG_BACKWARD);
      }
    }
    if (seek_result < 0) {
      const auto audio_target = av_rescale_q(
          safe_start_us, AVRational{1, 1000000}, stream->time_base);
      seek_result = av_seek_frame(format.get(), audio_stream, audio_target,
                                  AVSEEK_FLAG_BACKWARD);
    }
    if (seek_result >= 0) {
      avformat_flush(format.get());
      avcodec_flush_buffers(context.get());
    }
  }
  bool reached_start = start_us == 0;
  bool reached_end = end_us && *end_us <= start_us;
  auto packet = ffmpeg::make_packet(); auto frame = ffmpeg::make_frame();
  auto receive = [&] {
    while (!failed && !cancelled && !reached_end && avcodec_receive_frame(context.get(), frame.get()) >= 0) {
      const auto decoded_pts = frame->best_effort_timestamp != AV_NOPTS_VALUE ? frame->best_effort_timestamp : frame->pts;
      const auto capacity = static_cast<int>(av_rescale_rnd(swr_get_delay(resampler.get(), context->sample_rate) + frame->nb_samples,
                                                            sample_rate, context->sample_rate, AV_ROUND_UP));
      std::vector<std::int16_t> converted(static_cast<std::size_t>(capacity * channels));
      auto* destination = reinterpret_cast<std::uint8_t*>(converted.data());
      const int count = swr_convert(resampler.get(), &destination, capacity,
                                    const_cast<const std::uint8_t**>(frame->extended_data), frame->nb_samples);
      const auto frame_start_us = decoded_pts == AV_NOPTS_VALUE ? std::optional<std::int64_t>{} : std::optional<std::int64_t>{av_rescale_q(decoded_pts, stream->time_base, AVRational{1, 1000000})};
      av_frame_unref(frame.get());
      if (count < 0) { failed = true; break; }
      std::size_t first = 0;
      if (!reached_start) { if (frame_start_us) { const auto frame_end_us = *frame_start_us + static_cast<std::int64_t>(count) * 1000000LL / sample_rate; if (frame_end_us <= safe_start_us) first = static_cast<std::size_t>(count); else { if (*frame_start_us < safe_start_us) first = std::min<std::size_t>(static_cast<std::size_t>((safe_start_us - *frame_start_us) * sample_rate / 1000000LL), static_cast<std::size_t>(count)); reached_start = true; } } else reached_start = true; }
      if (first == static_cast<std::size_t>(count)) continue;
      std::size_t last = static_cast<std::size_t>(count);
      if (end_us && frame_start_us) {
        const auto safe_end_us = static_cast<std::int64_t>(std::min<std::uint64_t>(*end_us, static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())));
        if (*frame_start_us >= safe_end_us) { reached_end = true; break; }
        const auto samples_to_end = static_cast<std::size_t>(std::max<std::int64_t>(0, (safe_end_us - *frame_start_us) * sample_rate / 1000000LL));
        last = std::min(last, samples_to_end);
        if (last <= first) { reached_end = true; break; }
        if (last < static_cast<std::size_t>(count)) reached_end = true;
      }
      while (queued.size() >= 4 && drain_front()) {}
      if (cancelled || failed) break;
      const auto gain = std::clamp(volume_gain ? volume_gain() : 1.0, 0.0, 1.0);
      if (gain < 1.0) {
        for (auto& sample : converted) {
          sample = static_cast<std::int16_t>(std::clamp(
              static_cast<double>(sample) * gain,
              static_cast<double>(std::numeric_limits<std::int16_t>::min()),
              static_cast<double>(std::numeric_limits<std::int16_t>::max())));
        }
      }
      queued.emplace_back(); auto& block = queued.back();
      block.samples.assign(converted.begin() + static_cast<std::ptrdiff_t>(first * channels), converted.begin() + static_cast<std::ptrdiff_t>(last * channels));
      block.header.lpData = reinterpret_cast<LPSTR>(block.samples.data());
      block.header.dwBufferLength = static_cast<DWORD>(block.samples.size() * sizeof(std::int16_t));
      block.prepared = waveOutPrepareHeader(device, &block.header, sizeof(WAVEHDR)) == MMSYSERR_NOERROR;
      if (!block.prepared || waveOutWrite(device, &block.header, sizeof(WAVEHDR)) != MMSYSERR_NOERROR) { failed = true; break; }
      wrote_audio = true;
    }
  };
  while (!failed && !cancelled && !reached_end && av_read_frame(format.get(), packet.get()) >= 0) {
    if (is_cancelled()) { cancelled = true; break; }
    if (packet->stream_index == audio_stream && avcodec_send_packet(context.get(), packet.get()) >= 0) receive();
    av_packet_unref(packet.get());
  }
  if (!failed && !cancelled && !reached_end) { avcodec_send_packet(context.get(), nullptr); receive(); }
  while (!failed && !cancelled && !queued.empty()) drain_front();
  if (cancelled || failed) waveOutReset(device);
  for (auto& block : queued) if (block.prepared) waveOutUnprepareHeader(device, &block.header, sizeof(WAVEHDR));
  queued.clear();
  waveOutClose(device);
  std::ostringstream output; output << "{\"schema\":\"audio-playback-v1\",\"status\":\""
    << (cancelled ? "CANCELLED" : (wrote_audio && !failed ? "PASSED" : "FAILED")) << "\",\"start_us\":" << start_us
    << ",\"end_us\":" << (end_us ? std::to_string(*end_us) : "null")
    << ",\"sample_rate\":" << sample_rate << ",\"channels\":" << channels << "}"; return output.str();
#endif
}

}  // namespace smartcut::media
