/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
 * SPDX-FileCopyrightText: 2025 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup bke
 */

#include <chrono>
#include <condition_variable>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <numeric>
#include <optional>
#include <thread>

#ifdef WITH_FFTW3
#  include <fftw3.h>
#endif

#include "MEM_guardedalloc.h"

#include "BLI_build_config.h"
#include "BLI_enum_flags.hh"
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BLI_math_rotation.h"
#include "BLI_path_utils.hh"
#include "BLI_string.h"
#include "BLI_threads.h"

#include "BLT_translation.hh"

/* Allow using deprecated functionality for .blend file I/O. */
#define DNA_DEPRECATED_ALLOW

#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "DNA_packedFile_types.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_sequence_types.h"
#include "DNA_sound_types.h"
#include "DNA_speaker_types.h"
#include "DNA_userdef_types.h"

#ifdef WITH_AUDASPACE
#  include "BLI_set.hh"

#  include <Exception.h>
#  include <IReader.h>
#  include <devices/DeviceManager.h>
#  include <devices/IDeviceFactory.h>
#  include <devices/IHandle.h>
#  include <devices/NULLDevice.h>
#  include <devices/ReadDevice.h>
#  include <file/File.h>
#  include <file/FileManager.h>
#  include <file/FileWriter.h>
#  include <fx/Accumulator.h>
#  include <fx/AnimateableTimeStretchPitchScale.h>
#  include <fx/Envelope.h>
#  include <fx/Highpass.h>
#  include <fx/Limiter.h>
#  include <fx/Lowpass.h>
#  include <fx/Sum.h>
#  include <fx/Threshold.h>
#  include <generator/Silence.h>
#  include <plugin/PluginManager.h>
#  include <respec/ChannelMapper.h>
#  include <respec/LinearResample.h>
#  include <sequence/Sequence.h>
#  include <sequence/SequenceEntry.h>
#  include <util/StreamBuffer.h>

#  include <fmt/format.h>

#endif

#include "BKE_bpath.hh"
#include "BKE_global.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_lib_query.hh"
#include "BKE_library.hh"
#include "BKE_main.hh"
#include "BKE_packedFile.hh"
#include "BKE_scene_runtime.hh"
#include "BKE_sound.hh"
#include "BKE_sound_sample.hh"

#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_query.hh"

#include "BLO_read_write.hh"

#include "SEQ_sequencer.hh"
#include "SEQ_sound.hh"

#include "BLI_concurrent_map.hh"

#include "CLG_log.h"

namespace blender {

namespace bke {
struct SceneAudioRuntime;

enum class SoundTags {
  None = 0,
  /* Do not free/reset waveform on sound load, only used by undo code. */
  WaveformNoReload = 1 << 0,
  WaveformLoading = 1 << 1,
};
ENUM_OPERATORS(SoundTags);

using bSoundFrequencySamplerMap =
    ConcurrentMap<bSoundFrequencySampler::Key, std::shared_ptr<bSoundFrequencySampler>>;

struct SoundRuntime {
  AUD_Sound handle;
  AUD_Sound cache;
  /* The audaspace handle that should actually be played back.
   * Should be cache if cache != NULL; otherwise its handle. */
  AUD_Sound playback_handle;
  /* Spin-lock for asynchronous loading of sounds. */
  SpinLock spinlock;

  /* Note: not by-value since #sound_foreach_cache can only
   * save/restore a pointer. */
  Vector<float> *waveform = nullptr;
  SoundTags tags = SoundTags::None;

  /** Caches frequency samplers for this sound. */
  bSoundFrequencySamplerMap samplers;
};

}  // namespace bke

static void sound_free_audio(bSound *sound);

static void sound_init_runtime(bSound *sound)
{
  sound->runtime = MEM_new<bke::SoundRuntime>(__func__);
  BLI_spin_init(&sound->runtime->spinlock);
}

static void sound_free_waveform(bSound *sound)
{
  bke::SoundRuntime *runtime = sound->runtime;
  if (!flag_is_set(runtime->tags, bke::SoundTags::WaveformNoReload)) {
    MEM_SAFE_DELETE(runtime->waveform);
  }
  /* This tag is only valid once. */
  runtime->tags &= ~bke::SoundTags::WaveformNoReload;
}

static void sound_copy_data(Main * /*bmain*/,
                            std::optional<Library *> /*owner_library*/,
                            ID *id_dst,
                            const ID *id_src,
                            const int /*flag*/)
{
  bSound *sound_dst = id_cast<bSound *>(id_dst);
  const bSound *sound_src = id_cast<const bSound *>(id_src);

  /* Just to be sure, should not have any value actually after reading time. */
  sound_dst->newpackedfile = nullptr;

  if (sound_src->packedfile != nullptr) {
    sound_dst->packedfile = BKE_packedfile_duplicate(sound_src->packedfile);
  }

  sound_init_runtime(sound_dst);
}

static void sound_free_data(ID *id)
{
  bSound *sound = id_cast<bSound *>(id);

  if (sound->packedfile) {
    BKE_packedfile_free(sound->packedfile);
    sound->packedfile = nullptr;
  }

  if (sound->runtime) {
    sound_free_audio(sound);
    sound_free_waveform(sound);
    BLI_spin_end(&sound->runtime->spinlock);
    MEM_delete(sound->runtime);
  }
}

static void sound_foreach_cache(ID *id,
                                IDTypeForeachCacheFunctionCallback function_callback,
                                void *user_data)
{
  bSound *sound = id_cast<bSound *>(id);
  IDCacheKey key = {id->session_uid, 1};
  function_callback(id, &key, reinterpret_cast<void **>(&sound->runtime->waveform), 0, user_data);
}

static void sound_foreach_path(ID *id, BPathForeachPathData *bpath_data)
{
  bSound *sound = id_cast<bSound *>(id);
  if (sound->packedfile != nullptr && (bpath_data->flag & BKE_BPATH_FOREACH_PATH_SKIP_PACKED) != 0)
  {
    return;
  }

  /* FIXME: This does not check for empty path... */
  BKE_bpath_foreach_path_fixed_process(bpath_data, sound->filepath, sizeof(sound->filepath));
}

static void sound_blend_write(BlendWriter *writer, ID *id, const void *id_address)
{
  bSound *sound = id_cast<bSound *>(id);
  const bool is_undo = BLO_write_is_undo(writer);

  /* Clean up, important in undo case to reduce false detection of changed datablocks. */
  sound->runtime = nullptr;

  /* Do not store packed files in case this is a library override ID. */
  if (ID_IS_OVERRIDE_LIBRARY(sound) && !is_undo) {
    sound->packedfile = nullptr;
  }

  /* write LibData */
  writer->write_id_struct(id_address, sound);
  BKE_id_blend_write(writer, &sound->id);

  BKE_packedfile_blend_write(writer, sound->packedfile);
}

static void sound_blend_read_data(BlendDataReader *reader, ID *id)
{
  bSound *sound = id_cast<bSound *>(id);
  sound_init_runtime(sound);
  if (BLO_read_data_is_undo(reader)) {
    sound->runtime->tags |= bke::SoundTags::WaveformNoReload;
  }

  BKE_packedfile_blend_read(reader, &sound->packedfile, sound->filepath);
  BKE_packedfile_blend_read(reader, &sound->newpackedfile, sound->filepath);
}

IDTypeInfo IDType_ID_SO = {
    .id_code = bSound::id_type,
    .id_filter = FILTER_ID_SO,
    .dependencies_id_types = 0,
    .main_listbase_index = INDEX_ID_SO,
    .struct_size = sizeof(bSound),
    .name = "Sound",
    .name_plural = N_("sounds"),
    .translation_context = BLT_I18NCONTEXT_ID_SOUND,
    .flags = IDTYPE_FLAGS_NO_ANIMDATA | IDTYPE_FLAGS_APPEND_IS_REUSABLE,
    .asset_type_info = nullptr,

    /* A fuzzy case, think NULLified content is OK here... */
    .init_data = nullptr,
    .copy_data = sound_copy_data,
    .free_data = sound_free_data,
    .make_local = nullptr,
    .foreach_id = nullptr,
    .foreach_cache = sound_foreach_cache,
    .foreach_path = sound_foreach_path,
    .foreach_working_space_color = nullptr,
    .owner_pointer_get = nullptr,

    .blend_write = sound_blend_write,
    .blend_read_data = sound_blend_read_data,
    .blend_read_after_liblink = nullptr,

    .blend_read_undo_preserve = nullptr,

    .lib_override_apply_post = nullptr,
};

#ifdef WITH_AUDASPACE
/* evil globals ;-) */
static char **audio_device_names = nullptr;
#endif

BLI_INLINE void sound_verify_evaluated_id(const ID *id)
{
  UNUSED_VARS_NDEBUG(id);
  /* This is a bit tricky and not quite reliable, but good enough check.
   *
   * We don't want audio system handles to be allocated on an original data-blocks, and only want
   * them to be allocated on a data-blocks which are result of dependency graph evaluation.
   *
   * Data-blocks which are covered by a copy-on-evaluation system of dependency graph will have
   * ID_TAG_COPIED_ON_EVAL tag set on them. But if some of data-blocks during its evaluation
   * decides to re-allocate its nested one (for example, object evaluation could re-allocate mesh
   * when evaluating modifier stack). Such data-blocks will have
   * ID_TAG_COPIED_ON_EVAL_FINAL_RESULT tag set on them.
   *
   * Additionally, we also allow data-blocks outside of main database. Those can not be "original"
   * and could be used as a temporary evaluated result during operations like baking.
   *
   * NOTE: We consider ID evaluated if ANY of those flags is set. We do NOT require ALL of them.
   */
  BLI_assert(id->tag &
             (ID_TAG_COPIED_ON_EVAL | ID_TAG_COPIED_ON_EVAL_FINAL_RESULT | ID_TAG_NO_MAIN));
}

bSound *BKE_sound_new_file(Main *bmain, const char *filepath, short stream_index)
{
  bSound *sound;
  const char *blendfile_path = BKE_main_blendfile_path(bmain);
  char filepath_abs[FILE_MAX];

  STRNCPY(filepath_abs, filepath);
  BLI_path_abs(filepath_abs, blendfile_path);

  sound = static_cast<bSound *>(BKE_libblock_alloc(bmain, ID_SO, BLI_path_basename(filepath), 0));
  STRNCPY(sound->filepath, filepath);
  sound_init_runtime(sound);
  sound->stream_index = stream_index;

  /* Extract sound specs for bSound */
  SoundInfo info;
  bool success = BKE_sound_info_get(bmain, sound, &info);
  if (success) {
    sound->samplerate = info.specs.samplerate;
    sound->audio_channels = info.specs.channels;
  }

  return sound;
}

static bSound *sound_new_file_exists_ex(Main *bmain, const char *filepath, short stream_index)
{
  bSound *sound;
  char filepath_abs[FILE_MAX], filepath_test[FILE_MAX];

  STRNCPY(filepath_abs, filepath);
  BLI_path_abs(filepath_abs, BKE_main_blendfile_path(bmain));

  /* Search for an existing sound matching both filepath and stream index. */
  for (sound = static_cast<bSound *>(bmain->sounds.first); sound;
       sound = static_cast<bSound *>(sound->id.next))
  {
    if (sound->stream_index != stream_index) {
      continue;
    }
    STRNCPY(filepath_test, sound->filepath);
    BLI_path_abs(filepath_test, ID_BLEND_PATH(bmain, &sound->id));

    if (BLI_path_cmp(filepath_test, filepath_abs) == 0) {
      id_us_plus(&sound->id); /* officially should not, it doesn't link here! */
      return sound;
    }
  }

  return BKE_sound_new_file(bmain, filepath, stream_index);
}

bSound *BKE_sound_new_file_exists(Main *bmain, const char *filepath, short stream_index)
{
  return sound_new_file_exists_ex(bmain, filepath, stream_index);
}

static void sound_free_audio(bSound *sound)
{
#ifdef WITH_AUDASPACE
  bke::SoundRuntime *runtime = sound->runtime;
  runtime->handle.reset();
  runtime->playback_handle.reset();
  runtime->cache.reset();
#else
  UNUSED_VARS(sound);
#endif /* WITH_AUDASPACE */
}

#ifdef WITH_AUDASPACE
static CLG_LogRef LOG = {"sound"};

namespace {

struct GlobalState {
  const char *force_device = nullptr;

  /* Parameters of the opened device */
  const char *device_name = nullptr;
  aud::DeviceSpecs initialized_specs;

  /* Device handle and its synchronization mutex. */
  AUD_Device sound_device;
  int buffer_size = 0;
  std::mutex sound_device_mutex;

  bool need_exit = false;
  bool use_delayed_close = true;
  std::thread delayed_close_thread;
  std::condition_variable delayed_close_cv;

  int num_device_users = 0;
  std::chrono::time_point<std::chrono::steady_clock> last_user_disconnect_time_point;

  ~GlobalState()
  {
    /* Ensure that we don't end up in a deadlock if the global state is being cleaned up
     * before BKE_sound_exit_once has been called. (For example if someone called exit()
     * to quickly close the program without cleaning up)
     *
     * If we don't do this, we could end up in a state where this destructor is waiting for
     * other threads to let go of delayed_close_cv forever. See #146640.
     */
    exit_threads();
  }

  void exit_threads()
  {
    {
      std::unique_lock lock(sound_device_mutex);
      need_exit = true;
    }

    if (delayed_close_thread.joinable()) {
      delayed_close_cv.notify_all();
      delayed_close_thread.join();
    }
  }
};

GlobalState g_state;
}  // namespace

static void sound_device_close_no_lock()
{
  if (g_state.sound_device) {
    CLOG_DEBUG(&LOG, "Closing audio device");
    bke::sound_device_exit();
    g_state.sound_device = nullptr;
  }
}

static void sound_device_open_no_lock(const aud::DeviceSpecs &requested_specs)
{
  BLI_assert(!g_state.sound_device);

  CLOG_DEBUG(&LOG, "Opening audio device name:%s", g_state.device_name);

  g_state.sound_device = bke::sound_device_init(
      g_state.device_name, requested_specs, g_state.buffer_size, "Blender");
  if (!g_state.sound_device) {
    g_state.sound_device = bke::sound_device_init(
        "None", requested_specs, g_state.buffer_size, "Blender");
  }

  g_state.initialized_specs = g_state.sound_device->getSpecs();
}

static void sound_device_use_begin()
{
  ++g_state.num_device_users;

  if (g_state.sound_device) {
    return;
  }

  sound_device_open_no_lock(g_state.initialized_specs);
}

static void sound_device_use_end_after(const std::chrono::milliseconds after_ms)
{
  BLI_assert(g_state.num_device_users > 0);
  if (g_state.num_device_users == 0) {
    return;
  }

  --g_state.num_device_users;
  if (g_state.num_device_users == 0) {
    g_state.last_user_disconnect_time_point = std::chrono::steady_clock::now() + after_ms;
    g_state.delayed_close_cv.notify_one();
  }
}

static void sound_device_use_end()
{
  sound_device_use_end_after(std::chrono::milliseconds(0));
}

/* Return true if we need a thread which checks for device usage and closes it when it is inactive.
 * Only runtime-invariant checks are done here, such as possible platform-specific requirements.
 */
static bool sound_use_close_thread()
{
  /* No point starting a thread if sound is disabled and we're running headless. */
  if (g_state.force_device && STREQ(g_state.force_device, "None")) {
#  if defined(WITH_PYTHON_MODULE) || defined(WITH_HEADLESS)
    return false;
#  endif
    if (G.background) {
      return false;
    }
  }

#  if OS_MAC
  /* Closing audio device on macOS prior to 15.2 could lead to interference with other software.
   * See #121911 for details. */
  if (__builtin_available(macOS 15.2, *)) {
    return true;
  }
  return false;
#  else
  return true;
#  endif
}

static void delayed_close_thread_run()
{
  constexpr std::chrono::milliseconds device_close_delay{30000};

  std::unique_lock lock(g_state.sound_device_mutex);

  while (!g_state.need_exit) {
    if (!g_state.use_delayed_close) {
      CLOG_DEBUG(&LOG, "Delayed device close is disabled");
      /* Don't do anything here as delayed close is disabled.
       * Wait so that we don't spin around in the while loop. */
      g_state.delayed_close_cv.wait(lock);
      continue;
    }

    if (g_state.num_device_users == 0) {
      if (g_state.sound_device == nullptr) {
        /* There are no device users, wait until there is device to be waited for to close. */
        g_state.delayed_close_cv.wait(lock);
      }
      else {
        g_state.delayed_close_cv.wait_until(
            lock, g_state.last_user_disconnect_time_point + device_close_delay);
      }
    }
    else {
      /* If there are active device users wait indefinitely, until the system is requested to be
       * closed or the user stops using device.
       * It is not really guaranteed that the CV is notified for every user that stops using
       * device, only the last one is guaranteed to notify the CV. */
      g_state.delayed_close_cv.wait(lock);
    }

    if (g_state.need_exit) {
      CLOG_DEBUG(&LOG, "System exit requested");
      break;
    }

    if (!g_state.use_delayed_close) {
      /* Take into account corner case where you switch from a delayed close device while Blender
       * is running and a delayed close has already been queued up. */
      continue;
    }

    if (!g_state.sound_device) {
      CLOG_DEBUG(&LOG, "Device is not open, nothing to do");
      continue;
    }

    CLOG_DEBUG(&LOG, "Checking last device usage and timestamp");

    if (g_state.num_device_users) {
      CLOG_DEBUG(&LOG, "Device is used by %d user(s)", g_state.num_device_users);
      continue;
    }

    const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
    if ((now - g_state.last_user_disconnect_time_point) >= device_close_delay) {
      sound_device_close_no_lock();
    }
  }

  CLOG_DEBUG(&LOG, "Delayed device close thread finished");
}

static SoundJackSyncCallback sound_jack_sync_callback = nullptr;

static void sound_sync_callback(void *data, int mode, float time)
{
  if (sound_jack_sync_callback == nullptr) {
    return;
  }
  Main *bmain = (Main *)data;
  sound_jack_sync_callback(bmain, mode, time);
}

void BKE_sound_force_device(const char *device)
{
  g_state.force_device = device;
}

void BKE_sound_init_once()
{
  bke::sound_system_initialize();
  if (sound_use_close_thread()) {
    CLOG_DEBUG(&LOG, "Using delayed device close thread");
    g_state.delayed_close_thread = std::thread(delayed_close_thread_run);
  }
}

void BKE_sound_exit_once()
{
  g_state.exit_threads();

  std::lock_guard lock(g_state.sound_device_mutex);
  sound_device_close_no_lock();

  if (audio_device_names != nullptr) {
    int i;
    for (i = 0; audio_device_names[i]; i++) {
      free(audio_device_names[i]);
    }
    free(audio_device_names);
    audio_device_names = nullptr;
  }
}

void BKE_sound_init(Main *bmain)
{
  std::lock_guard lock(g_state.sound_device_mutex);
  /* Make sure no instance of the sound system is running, otherwise we get leaks. */
  sound_device_close_no_lock();

  aud::DeviceSpecs requested_specs;
  requested_specs.channels = aud::Channels(U.audiochannels);
  requested_specs.format = aud::SampleFormat(U.audioformat);
  requested_specs.rate = U.audiorate;

  if (g_state.force_device == nullptr) {
    char **names = BKE_sound_get_device_names();
    g_state.device_name = names[0];

    /* make sure device is within the bounds of the array */
    for (int i = 0; names[i]; i++) {
      if (i == U.audiodevice) {
        g_state.device_name = names[i];
      }
    }
  }
  else {
    g_state.device_name = g_state.force_device;
  }

  g_state.buffer_size = U.mixbufsize < 128 ? 1024 : U.mixbufsize;

  if (requested_specs.rate < double(aud::RATE_8000)) {
    requested_specs.rate = aud::RATE_48000;
  }

  if (requested_specs.format <= aud::FORMAT_INVALID) {
    requested_specs.format = aud::FORMAT_S16;
  }

  if (requested_specs.channels <= aud::CHANNELS_INVALID) {
    requested_specs.channels = aud::CHANNELS_STEREO;
  }

  /* Make sure that we have our initalized_specs */
  sound_device_open_no_lock(requested_specs);
  if (STR_ELEM(g_state.device_name, "JACK", "PulseAudio", "PipeWire")) {
    /* JACK:
     * Do not close the device when using JACK. If we close it, we will not be able to
     * respond to JACK audio bus commands.
     *
     * PulseAudio, PipeWire:
     * These APIs are built around the idea that the program using them keeps the device open.
     * Instead it uses audio streams to determine if something is playing back audio or not.
     * These streams are only active when Audaspace is playing back, so we don't need to
     * do anything manually.
     * If we close these devices, it will become very hard and tedious for end users to
     * control the volume or route audio from Blender.
     */
    g_state.use_delayed_close = false;
    aud::DeviceManager::getDevice()->setSyncCallback(sound_sync_callback, bmain);
  }
  else {
    g_state.use_delayed_close = true;
    sound_device_close_no_lock();
  }
}

void BKE_sound_refresh_callback_bmain(Main *bmain)
{
  std::lock_guard lock(g_state.sound_device_mutex);
  if (g_state.sound_device) {
    aud::DeviceManager::getDevice()->setSyncCallback(sound_sync_callback, bmain);
  }
}

static void sound_load_audio(Main *bmain, bSound *sound, bool free_waveform)
{
  bke::SoundRuntime *runtime = sound->runtime;
  runtime->cache.reset();
  runtime->handle.reset();
  runtime->playback_handle.reset();
  if (free_waveform) {
    sound_free_waveform(sound);
  }

  {
    char fullpath[FILE_MAX];

    /* load sound */
    PackedFile *pf = sound->packedfile;

    /* Don't modify `sound->filepath`, only change a copy. */
    STRNCPY(fullpath, sound->filepath);
    BLI_path_abs(fullpath, ID_BLEND_PATH(bmain, &sound->id));

    /* but we need a packed file then */
    if (pf) {
      runtime->handle = AUD_Sound(new aud::File((uchar *)pf->data, pf->size, sound->stream_index));
    }
    else {
      /* or else load it from disk */
      runtime->handle = AUD_Sound(new aud::File(fullpath, sound->stream_index));
    }
  }
  if (sound->flags & SOUND_FLAGS_MONO) {
    aud::DeviceSpecs specs;
    specs.channels = aud::CHANNELS_MONO;
    specs.rate = aud::RATE_INVALID;
    specs.format = aud::FORMAT_INVALID;
    runtime->handle = AUD_Sound(new aud::ChannelMapper(runtime->handle, specs));
  }

  if (sound->flags & SOUND_FLAGS_CACHING) {
    try {
      runtime->cache = AUD_Sound(new aud::StreamBuffer(runtime->handle));
    }
    catch (aud::Exception &) {
    }
  }

  if (runtime->cache) {
    runtime->playback_handle = runtime->cache;
  }
  else {
    runtime->playback_handle = runtime->handle;
  }
}

void BKE_sound_load(Main *bmain, bSound *sound)
{
  sound_verify_evaluated_id(&sound->id);
  sound_load_audio(bmain, sound, true);
}

void BKE_sound_packfile_ensure(Main *bmain, bSound *sound, ReportList *reports)
{
  if (sound->packedfile != nullptr) {
    /* Sound is already packed and considered unmodified, do not attempt to repack it, since its
     * original file may not be available anymore on the current FS.
     *
     * See #152638.
     */
    return;
  }

  sound->packedfile = BKE_packedfile_new(
      reports, sound->filepath, ID_BLEND_PATH(bmain, &sound->id));
}

AUD_Device BKE_sound_mixdown(const Scene *scene,
                             const aud::DeviceSpecs &specs,
                             int start,
                             float volume)
{
  sound_verify_evaluated_id(&scene->id);
  try {
    std::shared_ptr<aud::ReadDevice> device(new aud::ReadDevice(specs));
    device->setQuality(aud::ResampleQuality::MEDIUM);
    device->setVolume(volume);

    aud::Sequence *f = dynamic_cast<aud::Sequence *>(scene->runtime->audio.sound_scene.get());
    f->setSpecs(specs.specs);

    AUD_Handle handle = device->play(f->createQualityReader(aud::ResampleQuality::MEDIUM));
    if (handle.get()) {
      handle->seek(start / scene->frames_per_second());
    }

    return device;
  }
  catch (aud::Exception &) {
    return nullptr;
  }
}

void BKE_sound_create_scene(Scene *scene)
{
  sound_verify_evaluated_id(&scene->id);

  /* should be done in version patch, but this gets called before */
  if (scene->r.frs_sec_base == 0) {
    scene->r.frs_sec_base = 1;
  }

  bke::SceneAudioRuntime &audio = scene->runtime->audio;

  aud::Specs specs;
  specs.channels = aud::CHANNELS_STEREO;
  specs.rate = aud::RATE_48000;
  audio.sound_scene = AUD_Sequence(
      new aud::Sequence(specs, scene->frames_per_second(), scene->audio.flag & AUDIO_MUTE));
  audio.sound_scene->setSpeedOfSound(scene->audio.speed_of_sound);
  audio.sound_scene->setDopplerFactor(scene->audio.doppler_factor);
  audio.sound_scene->setDistanceModel(aud::DistanceModel(scene->audio.distance_model));
  audio.playback_handle = nullptr;
  audio.sound_scrub_handle = nullptr;
  audio.speaker_handles.clear();
}

void BKE_sound_destroy_scene(Scene *scene)
{
  bke::SceneAudioRuntime &audio = scene->runtime->audio;
  if (audio.playback_handle) {
    audio.playback_handle->stop();
    audio.playback_handle.reset();
  }
  if (audio.sound_scrub_handle) {
    audio.sound_scrub_handle->stop();
    audio.sound_scrub_handle.reset();
  }
  for (AUD_SequenceEntry handle : audio.speaker_handles) {
    audio.sound_scene->remove(handle);
  }
  audio.speaker_handles.clear();
  audio.sound_scene.reset();
}

void BKE_sound_lock()
{
  g_state.sound_device_mutex.lock();
  if (g_state.sound_device != nullptr) {
    g_state.sound_device->lock();
  }
}

void BKE_sound_unlock()
{
  g_state.sound_device_mutex.unlock();
  if (g_state.sound_device != nullptr) {
    g_state.sound_device->unlock();
  }
}

void BKE_sound_reset_scene_specs(Scene *scene)
{
  sound_verify_evaluated_id(&scene->id);

  if (scene->runtime->audio.sound_scene) {
    scene->runtime->audio.sound_scene->setSpecs(g_state.initialized_specs.specs);
  }
}

void BKE_sound_mute_scene(Scene *scene, bool muted)
{
  sound_verify_evaluated_id(&scene->id);
  if (scene->runtime->audio.sound_scene) {
    scene->runtime->audio.sound_scene->mute(muted);
  }
}

void BKE_sound_update_fps(Main *bmain, Scene *scene)
{
  sound_verify_evaluated_id(&scene->id);

  if (scene->runtime->audio.sound_scene) {
    scene->runtime->audio.sound_scene->setFPS(scene->frames_per_second());
  }

  seq::sound_update_length(bmain, scene);
}

void BKE_sound_update_scene_listener(Scene *scene)
{
  sound_verify_evaluated_id(&scene->id);

  AUD_Sequence sound = scene->runtime->audio.sound_scene;
  sound->setSpeedOfSound(scene->audio.speed_of_sound);
  sound->setDopplerFactor(scene->audio.doppler_factor);
  sound->setDistanceModel(aud::DistanceModel(scene->audio.distance_model));
}

AUD_SequenceEntry BKE_sound_scene_add_scene_sound(Scene *scene, Strip *strip)
{
  sound_verify_evaluated_id(&scene->id);
  if (strip->scene && scene != strip->scene) {
    int startframe = strip->left_handle();
    int endframe = strip->right_handle(scene);
    int frameskip = strip->startofs + strip->anim_startofs;
    const double fps = scene->frames_per_second();
    return AUD_SequenceEntry(
        scene->runtime->audio.sound_scene->add(strip->scene->runtime->audio.sound_scene,
                                               startframe / fps,
                                               endframe / fps,
                                               frameskip / fps));
  }
  return nullptr;
}

AUD_SequenceEntry BKE_sound_add_scene_sound(Scene *scene, Strip *strip)
{
  sound_verify_evaluated_id(&scene->id);
  /* Happens when sequence's sound data-block was removed. */
  if (strip->sound == nullptr) {
    return nullptr;
  }
  sound_verify_evaluated_id(&strip->sound->id);

  int startframe = strip->left_handle();
  int endframe = strip->right_handle(scene);
  int frameskip = strip->startofs + strip->anim_startofs;

  const double fps = scene->frames_per_second();
  const double offset_time = strip->sound->offset_time + strip->sound_offset - frameskip / fps;
  if (offset_time >= 0.0f) {
    return AUD_SequenceEntry(
        scene->runtime->audio.sound_scene->add(strip->sound->runtime->playback_handle,
                                               startframe / fps + offset_time,
                                               endframe / fps,
                                               0.0f));
  }
  return AUD_SequenceEntry(scene->runtime->audio.sound_scene->add(
      strip->sound->runtime->playback_handle, startframe / fps, endframe / fps, -offset_time));
}

void BKE_sound_remove_scene_sound(Scene *scene, AUD_SequenceEntry handle)
{
  scene->runtime->audio.sound_scene->remove(handle);
}

void BKE_sound_mute_scene_sound(AUD_SequenceEntry handle, bool mute)
{
  handle->mute(mute);
}

void BKE_sound_move_scene_sound(const Scene *scene,
                                AUD_SequenceEntry handle,
                                int startframe,
                                int endframe,
                                int frameskip,
                                double audio_offset)
{
  sound_verify_evaluated_id(&scene->id);
  const double fps = scene->frames_per_second();
  const double offset_time = audio_offset - frameskip / fps;
  if (offset_time >= 0.0f) {
    handle->move(startframe / fps + offset_time, endframe / fps, 0.0f);
  }
  else {
    handle->move(startframe / fps, endframe / fps, -offset_time);
  }
}

void BKE_sound_move_scene_sound_defaults(Scene *scene, Strip *strip)
{
  sound_verify_evaluated_id(&scene->id);
  if (strip->runtime->scene_sound) {
    double offset_time = 0.0f;
    if (strip->sound != nullptr) {
      offset_time = strip->sound->offset_time + strip->sound_offset;
    }
    BKE_sound_move_scene_sound(scene,
                               strip->runtime->scene_sound,
                               strip->left_handle(),
                               strip->right_handle(scene),
                               strip->startofs + strip->anim_startofs,
                               offset_time);
  }
}

void BKE_sound_update_scene_sound(AUD_SequenceEntry handle, bSound *sound)
{
  handle->setSound(sound->runtime->playback_handle);
}

#endif /* WITH_AUDASPACE */

void BKE_sound_update_sequence_handle(AUD_SequenceEntry handle, AUD_Sound sound_handle)
{
#ifdef WITH_AUDASPACE
  handle->setSound(sound_handle);
#else
  UNUSED_VARS(handle, sound_handle);
#endif
}

#ifdef WITH_AUDASPACE

template<typename T>
static void set_audaspace_anim_property(std::shared_ptr<T> sound,
                                        aud::AnimateablePropertyType type,
                                        const int frame,
                                        float *value,
                                        bool animated)
{
  aud::AnimateableProperty *prop = sound->getAnimProperty(type);
  if (animated) {
    if (frame >= 0) {
      prop->write(value, frame, 1);
    }
  }
  else {
    prop->write(value);
  }
}

void BKE_sound_set_scene_volume(Scene *scene, float volume)
{
  sound_verify_evaluated_id(&scene->id);
  if (scene->runtime->audio.sound_scene == nullptr) {
    return;
  }
  const bool animated = (scene->audio.flag & AUDIO_VOLUME_ANIMATED) != 0;
  const int frame = scene->r.cfra;
  set_audaspace_anim_property(
      scene->runtime->audio.sound_scene, aud::AP_VOLUME, frame, &volume, animated);
}

void BKE_sound_set_scene_sound_volume_at_frame(AUD_SequenceEntry handle,
                                               const int frame,
                                               float volume,
                                               bool animated)
{
  set_audaspace_anim_property(handle, aud::AP_VOLUME, frame, &volume, animated);
}

void BKE_sound_set_scene_sound_pitch_at_frame(AUD_SequenceEntry handle,
                                              const int frame,
                                              float pitch,
                                              bool animated)
{
  set_audaspace_anim_property(handle, aud::AP_PITCH, frame, &pitch, animated);
}

void BKE_sound_set_scene_sound_pitch_constant_range(AUD_SequenceEntry handle,
                                                    int frame_start,
                                                    int frame_end,
                                                    float pitch)
{
  frame_start = max_ii(0, frame_start);
  frame_end = max_ii(0, frame_end);
  aud::AnimateableProperty *prop = handle->getAnimProperty(aud::AP_PITCH);
  prop->writeConstantRange(&pitch, frame_start, frame_end);
}

void BKE_sound_set_scene_sound_pan_at_frame(AUD_SequenceEntry handle,
                                            const int frame,
                                            float pan,
                                            bool animated)
{
  set_audaspace_anim_property(handle, aud::AP_PANNING, frame, &pan, animated);
}

void BKE_sound_update_sequencer(Main *main, bSound *sound)
{
  BLI_assert_msg(0, "is not supposed to be used, is weird function.");

  Scene *scene;

  for (scene = static_cast<Scene *>(main->scenes.first); scene;
       scene = static_cast<Scene *>(scene->id.next))
  {
    seq::sound_update(scene, sound);
  }
}

/* This function assumes that you have already held the g_state.sound_device mutex. */
static void sound_start_play_scene(Scene *scene)
{
  sound_verify_evaluated_id(&scene->id);

  bke::SceneAudioRuntime &audio = scene->runtime->audio;
  if (audio.playback_handle) {
    audio.playback_handle->stop();
    audio.playback_handle.reset();
  }

  BKE_sound_reset_scene_specs(scene);

  audio.playback_handle = bke::sound_device_play(g_state.sound_device, audio.sound_scene);
  if (audio.playback_handle) {
    audio.playback_handle->setLoopCount(-1);
  }
}

void BKE_sound_play_scene(Scene *scene)
{
  std::lock_guard lock(g_state.sound_device_mutex);
  sound_device_use_begin();
  sound_verify_evaluated_id(&scene->id);

  const double cur_time = FRA2TIME(scene->r.cfra + scene->r.subframe);

  g_state.sound_device->lock();

  bke::SceneAudioRuntime &audio = scene->runtime->audio;
  if (audio.sound_scrub_handle && audio.sound_scrub_handle->getStatus() != aud::STATUS_INVALID) {
    /* If the audio scrub handle is playing back, stop to make sure it is not active.
     * Otherwise, it will trigger a callback that will stop audio playback. */
    audio.sound_scrub_handle->stop();
    audio.sound_scrub_handle = nullptr;
    /* The scrub_handle started playback with playback_handle, stop it so we can
     * properly restart it. */
    audio.playback_handle->pause();
  }

  aud::Status status = audio.playback_handle ? audio.playback_handle->getStatus() :
                                               aud::STATUS_INVALID;

  if (status == aud::STATUS_INVALID) {
    sound_start_play_scene(scene);

    if (!audio.playback_handle) {
      g_state.sound_device->unlock();
      return;
    }
  }

  if (status != aud::STATUS_PLAYING) {
    /* Seeking the synchronizer will also seek the playback handle.
     * Even if we don't have A/V sync on, keep the synchronizer and handle seek time in sync. */
    aud::DeviceManager::getDevice()->seekSynchronizer(cur_time);
    audio.playback_handle->seek(cur_time);
    audio.playback_handle->resume();
  }

  if (scene->audio.flag & AUDIO_SYNC) {
    aud::DeviceManager::getDevice()->playSynchronizer();
  }

  g_state.sound_device->unlock();
}

void BKE_sound_stop_scene(Scene *scene)
{
  std::lock_guard lock(g_state.sound_device_mutex);
  BLI_assert(g_state.sound_device);
  if (scene->runtime->audio.playback_handle) {
    scene->runtime->audio.playback_handle->pause();

    if (scene->audio.flag & AUDIO_SYNC) {
      aud::DeviceManager::getDevice()->stopSynchronizer();
    }
  }
  sound_device_use_end();
}

void BKE_sound_seek_scene(Main *bmain, Scene *scene)
{
  std::lock_guard lock(g_state.sound_device_mutex);
  bool animation_playing = false;
  for (bScreen *screen = static_cast<bScreen *>(bmain->screens.first); screen;
       screen = static_cast<bScreen *>(screen->id.next))
  {
    if (screen->animtimer) {
      animation_playing = true;
      break;
    }
  }

  bool do_audio_scrub = scene->audio.flag & AUDIO_SCRUB && !animation_playing;

  if (do_audio_scrub) {
    /* Make sure the sound device is open for scrubbing. */
    sound_device_use_begin();
  }
  else if (g_state.sound_device == nullptr) {
    /* Nothing to do if there is no sound device and we are not doing audio scrubbing. */
    return;
  }
  sound_verify_evaluated_id(&scene->id);

  g_state.sound_device->lock();

  bke::SceneAudioRuntime &audio = scene->runtime->audio;
  aud::Status status = audio.playback_handle ? audio.playback_handle->getStatus() :
                                               aud::STATUS_INVALID;
  if (status == aud::STATUS_INVALID) {
    sound_start_play_scene(scene);

    if (!audio.playback_handle) {
      g_state.sound_device->unlock();
      if (do_audio_scrub) {
        sound_device_use_end();
      }
      return;
    }

    audio.playback_handle->pause();
  }

  const double one_frame = 1.0 / scene->frames_per_second() +
                           (U.audiorate > 0 ? U.mixbufsize / double(U.audiorate) : 0.0);
  const double cur_time = FRA2TIME(scene->r.cfra);

  if (do_audio_scrub) {
    /* Playback one frame of audio without advancing the timeline. */
    audio.playback_handle->seek(cur_time);
    audio.playback_handle->resume();
    if (audio.sound_scrub_handle && audio.sound_scrub_handle->getStatus() != aud::STATUS_INVALID) {
      audio.sound_scrub_handle->seek(0);
    }
    else {
      if (audio.sound_scrub_handle) {
        audio.sound_scrub_handle->stop();
      }
      audio.sound_scrub_handle = bke::sound_pause_after(audio.playback_handle, one_frame);
    }
    sound_device_use_end_after(std::chrono::milliseconds(int(one_frame * 1000)));
  }
  else if (status == aud::STATUS_PLAYING) {
    /* Seeking the synchronizer will also seek the playback handle.
     * Even if we don't have A/V sync on, keep the synchronizer and handle
     * seek time in sync.
     */
    aud::DeviceManager::getDevice()->seekSynchronizer(cur_time);
    audio.playback_handle->seek(cur_time);
  }

  g_state.sound_device->unlock();
}

double BKE_sound_sync_scene(Scene *scene)
{
  sound_verify_evaluated_id(&scene->id);

  /* Ugly: Blender doesn't like it when the animation is played back during rendering */
  if (G.is_rendering) {
    return NAN_FLT;
  }

  if (scene->runtime->audio.playback_handle) {
    if (scene->audio.flag & AUDIO_SYNC) {
      return aud::DeviceManager::getDevice()->getSynchronizerPosition();
    }

    return scene->runtime->audio.playback_handle->getPosition();
  }
  return NAN_FLT;
}

static int sound_read(
    AUD_Sound sound, float *buffer, int length, int samples_per_second, bool *interrupt)
{
  using namespace aud;
  DeviceSpecs specs;
  float *buf;
  Buffer aBuffer;

  specs.rate = RATE_INVALID;
  specs.channels = CHANNELS_MONO;
  specs.format = FORMAT_INVALID;

  std::shared_ptr<IReader> reader = ChannelMapper(sound, specs).createReader();

  specs.specs = reader->getSpecs();
  int len;
  float samplejump = specs.rate / samples_per_second;
  float min, max, power, overallmax;
  bool eos;

  overallmax = 0;

  for (int i = 0; i < length; i++) {
    len = floor(samplejump * (i + 1)) - floor(samplejump * i);

    if (*interrupt) {
      return 0;
    }

    aBuffer.assureSize(len * AUD_SAMPLE_SIZE(specs));
    buf = aBuffer.getBuffer();

    reader->read(len, eos, buf);

    max = min = *buf;
    power = *buf * *buf;
    for (int j = 1; j < len; j++) {
      if (buf[j] < min) {
        min = buf[j];
      }
      if (buf[j] > max) {
        max = buf[j];
      }
      power += buf[j] * buf[j];
    }

    buffer[i * 3] = min;
    buffer[i * 3 + 1] = max;
    buffer[i * 3 + 2] = std::sqrt(power / len);

    if (overallmax < max) {
      overallmax = max;
    }
    if (overallmax < -min) {
      overallmax = -min;
    }

    if (eos) {
      length = i;
      break;
    }
  }

  if (overallmax > 1.0f) {
    for (int i = 0; i < length * 3; i++) {
      buffer[i] /= overallmax;
    }
  }

  return length;
}

void BKE_sound_read_waveform(Main *bmain, bSound *sound, bool *stop)
{
  bool need_close_audio_handles = false;
  bke::SoundRuntime *runtime = sound->runtime;
  if (runtime->playback_handle == nullptr) {
    /* TODO(sergey): Make it fully independent audio handle. */
    sound_load_audio(bmain, sound, true);
    need_close_audio_handles = true;
  }

  SoundInfo info = bke::sound_info_get(runtime->playback_handle);

  Vector<float> *waveform = MEM_new<Vector<float>>(__func__);
  if (info.length > 0) {
    int length = info.length * SOUND_WAVE_SAMPLES_PER_SECOND;

    waveform->resize(3 * length);
    length = sound_read(
        runtime->playback_handle, waveform->data(), length, SOUND_WAVE_SAMPLES_PER_SECOND, stop);
    waveform->resize(3 * length);
  }

  if (*stop) {
    MEM_SAFE_DELETE(runtime->waveform);
    BLI_spin_lock(&runtime->spinlock);
    runtime->tags &= ~bke::SoundTags::WaveformLoading;
    BLI_spin_unlock(&runtime->spinlock);
    return;
  }

  sound_free_waveform(sound);

  BLI_spin_lock(&runtime->spinlock);
  runtime->waveform = waveform;
  runtime->tags &= ~bke::SoundTags::WaveformLoading;
  BLI_spin_unlock(&runtime->spinlock);

  if (need_close_audio_handles) {
    sound_free_audio(sound);
  }
}

static void sound_update_base(Scene *scene, Object *object, Set<AUD_SequenceEntry> &new_set)
{
  Speaker *speaker;
  float quat[4];

  sound_verify_evaluated_id(&scene->id);
  sound_verify_evaluated_id(&object->id);

  if ((object->type != OB_SPEAKER) || !object->adt) {
    return;
  }

  for (NlaTrack &track : object->adt->nla_tracks) {
    for (NlaStrip &strip : track.strips) {
      if (strip.type != NLASTRIP_TYPE_SOUND) {
        continue;
      }
      speaker = (Speaker *)object->data;

      bke::NlaStripRuntime &strip_runtime = strip.runtime_get();

      if ((strip_runtime.speaker_handle != nullptr) &&
          scene->runtime->audio.speaker_handles.remove(strip_runtime.speaker_handle))
      {
        if (speaker->sound) {
          strip_runtime.speaker_handle->move(
              double(strip.start) / scene->frames_per_second(), FLT_MAX, 0);
        }
        else {
          scene->runtime->audio.sound_scene->remove(strip_runtime.speaker_handle);
          strip_runtime.speaker_handle = nullptr;
        }
      }
      else {
        if (speaker->sound) {
          strip_runtime.speaker_handle = AUD_SequenceEntry(scene->runtime->audio.sound_scene->add(
              speaker->sound->runtime->playback_handle,
              double(strip.start) / scene->frames_per_second(),
              FLT_MAX,
              0));
          strip_runtime.speaker_handle->setRelative(false);
        }
      }

      if (strip_runtime.speaker_handle) {
        const bool mute = ((strip.flag & NLASTRIP_FLAG_MUTED) || (speaker->flag & SPK_MUTED));
        new_set.add(strip_runtime.speaker_handle);
        strip_runtime.speaker_handle->setVolumeMaximum(speaker->volume_max);
        strip_runtime.speaker_handle->setVolumeMinimum(speaker->volume_min);
        strip_runtime.speaker_handle->setDistanceMaximum(speaker->distance_max);
        strip_runtime.speaker_handle->setDistanceReference(speaker->distance_reference);
        strip_runtime.speaker_handle->setAttenuation(speaker->attenuation);
        strip_runtime.speaker_handle->setConeAngleOuter(speaker->cone_angle_outer);
        strip_runtime.speaker_handle->setConeAngleInner(speaker->cone_angle_inner);
        strip_runtime.speaker_handle->setConeVolumeOuter(speaker->cone_volume_outer);

        mat4_to_quat(quat, object->object_to_world().ptr());
        float3 location = object->object_to_world().location();
        set_audaspace_anim_property(
            strip_runtime.speaker_handle, aud::AP_LOCATION, scene->r.cfra, location, true);
        set_audaspace_anim_property(
            strip_runtime.speaker_handle, aud::AP_ORIENTATION, scene->r.cfra, quat, true);
        set_audaspace_anim_property(
            strip_runtime.speaker_handle, aud::AP_VOLUME, scene->r.cfra, &speaker->volume, true);
        set_audaspace_anim_property(
            strip_runtime.speaker_handle, aud::AP_PITCH, scene->r.cfra, &speaker->pitch, true);
        strip_runtime.speaker_handle->setSound(speaker->sound->runtime->playback_handle);
        strip_runtime.speaker_handle->mute(mute);
      }
    }
  }
}

void BKE_sound_update_scene(Depsgraph *depsgraph, Scene *scene)
{
  sound_verify_evaluated_id(&scene->id);

  Set<AUD_SequenceEntry> new_set;
  float quat[4];

  /* cheap test to skip looping over all objects (no speakers is a common case) */
  if (DEG_id_type_any_exists(depsgraph, ID_SPK)) {
    DEGObjectIterSettings deg_iter_settings = {nullptr};
    deg_iter_settings.depsgraph = depsgraph;
    deg_iter_settings.flags = DEG_ITER_OBJECT_FLAG_LINKED_DIRECTLY |
                              DEG_ITER_OBJECT_FLAG_LINKED_INDIRECTLY |
                              DEG_ITER_OBJECT_FLAG_LINKED_VIA_SET;
    DEG_OBJECT_ITER_BEGIN (&deg_iter_settings, object) {
      sound_update_base(scene, object, new_set);
    }
    DEG_OBJECT_ITER_END;
  }

  bke::SceneAudioRuntime &audio = scene->runtime->audio;
  for (AUD_SequenceEntry handle : audio.speaker_handles) {
    audio.sound_scene->remove(handle);
  }
  audio.speaker_handles.clear();

  if (scene->camera) {
    mat4_to_quat(quat, scene->camera->object_to_world().ptr());
    float3 location = scene->camera->object_to_world().location();
    set_audaspace_anim_property(
        audio.sound_scene, aud::AP_LOCATION, scene->r.cfra, location, true);
    set_audaspace_anim_property(audio.sound_scene, aud::AP_ORIENTATION, scene->r.cfra, quat, true);
  }

  audio.speaker_handles = new_set;
}

AUD_Sound BKE_sound_get_factory(void *sound)
{
  return ((bSound *)sound)->runtime->playback_handle;
}

float BKE_sound_get_length(Main *bmain, bSound *sound)
{
  if (sound->runtime->playback_handle != nullptr) {
    SoundInfo info = bke::sound_info_get(sound->runtime->playback_handle);
    return info.length;
  }
  SoundInfo info;
  if (!BKE_sound_info_get(bmain, sound, &info)) {
    return 0.0f;
  }
  return info.length;
}

char **BKE_sound_get_device_names()
{
  if (audio_device_names == nullptr) {

    std::vector<std::string> v_names = aud::DeviceManager::getAvailableDeviceNames();
    char **names = (char **)malloc(sizeof(char *) * (v_names.size() + 1));

    for (int i = 0; i < v_names.size(); i++) {
      std::string name = v_names[i];
      const size_t name_size = sizeof(char) * (name.length() + 1);
      names[i] = (char *)malloc(name_size);
      memcpy(names[i], name.c_str(), name_size);
    }
    names[v_names.size()] = nullptr;
    audio_device_names = names;
  }

  return audio_device_names;
}

bool BKE_sound_info_get(Main *main, bSound *sound, SoundInfo *sound_info)
{
  if (sound->runtime->playback_handle != nullptr) {
    *sound_info = bke::sound_info_get(sound->runtime->playback_handle);
    return true;
  }
  /* TODO(sergey): Make it fully independent audio handle. */
  /* Don't free waveforms during non-destructive queries.
   * This causes unnecessary recalculation - see #69921 */
  sound_load_audio(main, sound, false);
  const bool result = sound->runtime->playback_handle != nullptr;
  if (result) {
    *sound_info = bke::sound_info_get(sound->runtime->playback_handle);
  }
  sound_free_audio(sound);
  return result;
}

bool BKE_sound_stream_info_get(Main *main,
                               const char *filepath,
                               int stream,
                               SoundStreamInfo *sound_info)
{
  char filepath_abs[FILE_MAX];
  STRNCPY(filepath_abs, filepath);
  const char *blendfile_path = BKE_main_blendfile_path(main);
  BLI_path_abs(filepath_abs, blendfile_path);

  std::vector<aud::StreamInfo> streams;
  try {
    streams = aud::FileManager::queryStreams(filepath_abs);
  }
  catch (aud::Exception &) {
    return false;
  }

  if (stream < 0 || stream >= streams.size()) {
    return false;
  }

  sound_info->start = streams[stream].start;
  sound_info->duration = streams[stream].duration;
  return true;
}

int BKE_sound_stream_count(Main *main, const char *filepath)
{
  char filepath_abs[FILE_MAX];
  STRNCPY(filepath_abs, filepath);
  BLI_path_abs(filepath_abs, BKE_main_blendfile_path(main));

  std::vector<aud::StreamInfo> streams;
  try {
    streams = aud::FileManager::queryStreams(filepath_abs);
    return streams.size();
  }
  catch (aud::Exception &) {
    return 0;
  }
}

#  ifdef WITH_RUBBERBAND
AUD_Sound BKE_sound_ensure_time_stretch_effect(const Strip *strip, float fps)
{
  seq::StripRuntime &runtime = *strip->runtime;

  /* If we already have a time stretch effect with the same frame-rate, use that. */
  if (runtime.sound_time_stretch != nullptr && runtime.sound_time_stretch_fps == fps) {
    return runtime.sound_time_stretch;
  }

  /* Otherwise create the time stretch effect. */
  runtime.clear_sound_time_stretch();
  runtime.sound_time_stretch = AUD_Sound(
      new aud::AnimateableTimeStretchPitchScale(BKE_sound_playback_handle_get(strip->sound),
                                                fps,
                                                1.0f,
                                                1.0f,
                                                aud::StretcherQuality::HIGH,
                                                false));
  runtime.sound_time_stretch_fps = fps;
  return runtime.sound_time_stretch;
}

void BKE_sound_set_scene_sound_time_stretch_at_frame(AUD_Sound handle,
                                                     int frame,
                                                     float time_stretch,
                                                     bool animated)
{
  std::shared_ptr<aud::AnimateableProperty> prop =
      std::dynamic_pointer_cast<aud::AnimateableTimeStretchPitchScale>(handle)->getAnimProperty(
          aud::AP_TIME_STRETCH);
  if (animated) {
    if (frame >= 0) {
      prop->write(&time_stretch, frame, 1);
    }
  }
  else {
    prop->write(&time_stretch);
  }
}

void BKE_sound_set_scene_sound_time_stretch_constant_range(AUD_Sound handle,
                                                           int frame_start,
                                                           int frame_end,
                                                           float time_stretch)
{
  frame_start = max_ii(0, frame_start);
  frame_end = max_ii(0, frame_end);
  std::shared_ptr<aud::AnimateableProperty> prop =
      std::dynamic_pointer_cast<aud::AnimateableTimeStretchPitchScale>(handle)->getAnimProperty(
          aud::AP_TIME_STRETCH);
  prop->writeConstantRange(&time_stretch, frame_start, frame_end);
}
#  endif /* WITH_RUBBERBAND */

SoundInfo bke::sound_info_get(AUD_Sound sound)
{
  SoundInfo res;
  res.length = 0.0f;
  res.specs.channels = SOUND_CHANNELS_INVALID;
  res.specs.samplerate = 0;

  try {
    std::shared_ptr<aud::IReader> reader = sound->createReader();
    if (reader.get()) {
      aud::Specs specs = reader->getSpecs();
      res.specs.channels = eSoundChannels(specs.channels);
      res.specs.samplerate = specs.rate;
      res.length = reader->getLength() / float(specs.rate);
    }
  }
  catch (aud::Exception &) {
  }
  return res;
}

void bke::sound_system_initialize()
{
  aud::PluginManager::loadPlugins();
  aud::NULLDevice::registerPlugin();
}

AUD_Device bke::sound_device_init(const char *device,
                                  const aud::DeviceSpecs &specs,
                                  int buffersize,
                                  const char *name)
{
  using namespace aud;
  try {
    std::shared_ptr<IDeviceFactory> factory = device ? DeviceManager::getDeviceFactory(device) :
                                                       DeviceManager::getDefaultDeviceFactory();

    if (factory) {
      factory->setName(name);
      factory->setBufferSize(buffersize);
      factory->setSpecs(specs);
      auto device = factory->openDevice();
      DeviceManager::setDevice(device);

      return AUD_Device(device);
    }
  }
  catch (Exception &) {
  }
  return nullptr;
}

void bke::sound_device_exit()
{
  aud::DeviceManager::releaseDevice();
}

AUD_Handle bke::sound_device_play(AUD_Device device, AUD_Sound sound)
{
  if (!device) {
    device = aud::DeviceManager::getDevice();
  }
  try {
    return device->play(sound, true);
  }
  catch (aud::Exception &) {
  }
  return nullptr;
}

bool bke::sound_device_read(AUD_Device device, uchar *buffer, int length)
{
  BLI_assert(buffer);
  auto read_device = std::dynamic_pointer_cast<aud::ReadDevice>(device);
  if (!read_device) {
    return false;
  }
  try {
    return read_device->read(buffer, length);
  }
  catch (aud::Exception &) {
    return false;
  }
}

AUD_Handle bke::sound_pause_after(AUD_Handle handle, double seconds)
{
  auto device = aud::DeviceManager::getDevice();

  AUD_Sound silence = AUD_Sound(new aud::Silence(device->getSpecs().rate));
  AUD_Sound limiter = AUD_Sound(new aud::Limiter(silence, 0, seconds));

  std::lock_guard<aud::ILockable> lock(*device);

  try {
    AUD_Handle handle2 = device->play(limiter);
    if (handle2.get()) {
      AUD_Handle *data = new AUD_Handle(handle);
      handle2->setStopCallback(
          [](void *data) {
            AUD_Handle *handle = (AUD_Handle *)data;
            (*handle)->pause();
            delete handle;
          },
          data);
      return handle2;
    }
  }
  catch (aud::Exception &) {
  }
  return nullptr;
}

float *bke::sound_read_file_buffer(const char *filename,
                                   float low,
                                   float high,
                                   float attack,
                                   float release,
                                   float threshold,
                                   bool accumulate,
                                   bool additive,
                                   bool square,
                                   float sthreshold,
                                   double samplerate,
                                   int stream,
                                   int *length)
{
  using namespace aud;

  DeviceSpecs specs;
  specs.channels = CHANNELS_MONO;
  specs.rate = samplerate;

  AUD_Sound file = AUD_Sound(new File(filename, stream));
  int position = 0;

  Buffer buffer;

  try {
    std::shared_ptr<IReader> reader = file->createReader();
    SampleRate rate = reader->getSpecs().rate;

    AUD_Sound sound = AUD_Sound(new ChannelMapper(file, specs));

    if (high < rate) {
      sound = AUD_Sound(new Lowpass(sound, high));
    }
    if (low > 0) {
      sound = AUD_Sound(new Highpass(sound, low));
    }

    sound = AUD_Sound(new Envelope(sound, attack, release, threshold, 0.1f));
    sound = AUD_Sound(new LinearResample(sound, specs));

    if (square) {
      sound = AUD_Sound(new Threshold(sound, sthreshold));
    }

    if (accumulate) {
      sound = AUD_Sound(new Accumulator(sound, additive));
    }
    else if (additive) {
      sound = AUD_Sound(new Sum(sound));
    }

    reader = sound->createReader();

    if (!reader.get()) {
      return nullptr;
    }

    int len;
    bool eos;
    do {
      len = samplerate;
      buffer.resize((position + len) * sizeof(float), true);
      reader->read(len, eos, buffer.getBuffer() + position);
      position += len;
    } while (!eos);
  }
  catch (Exception &) {
    return nullptr;
  }

  float *result = MEM_new_array_uninitialized<float>(position, __func__);
  memcpy(result, buffer.getBuffer(), position * sizeof(float));
  *length = position;
  return result;
}

bool bke::sound_mixdown(AUD_Sequence sequence,
                        uint start,
                        uint length,
                        uint buffersize,
                        const char *filename,
                        const aud::DeviceSpecs &specs,
                        aud::Container format,
                        aud::Codec codec,
                        uint bitrate,
                        bool split_channels,
                        std::string &r_error,
                        bool (*progress_callback)(float, void *),
                        void *progress_data)
{
  using namespace aud;
  try {
    sequence->setSpecs(specs.specs);
    std::shared_ptr<IReader> reader = sequence->createQualityReader(ResampleQuality::MEDIUM);
    reader->seek(start);

    if (!split_channels) {
      std::shared_ptr<IWriter> writer = FileWriter::createWriter(
          filename, specs, format, codec, bitrate);
      FileWriter::writeReader(
          reader, writer, length, buffersize, progress_callback, progress_data);
    }
    else {
      std::vector<std::shared_ptr<IWriter>> writers;
      int channels = specs.channels;

      aud::DeviceSpecs specs_mono = specs;
      specs_mono.channels = CHANNELS_MONO;
      for (int i = 0; i < channels; i++) {
        std::string stream;
        std::string fn = filename;
        size_t index = fn.find_last_of('.');
        size_t index_slash = fn.find_last_of('/');
        size_t index_backslash = fn.find_last_of('\\');

        if ((index == std::string::npos) ||
            ((index < index_slash) && (index_slash != std::string::npos)) ||
            ((index < index_backslash) && (index_backslash != std::string::npos)))
        {
          stream = fmt::format("{}_{}", filename, i + 1);
        }
        else {
          stream = fmt::format("{}_{}{}", fn.substr(0, index), i + 1, fn.substr(index));
        }
        writers.push_back(FileWriter::createWriter(stream, specs_mono, format, codec, bitrate));
      }
      FileWriter::writeReader(
          reader, writers, length, buffersize, progress_callback, progress_data);
    }
    return true;
  }
  catch (Exception &e) {
    r_error = e.getMessage();
    return false;
  }
}

#else /* WITH_AUDASPACE */

#  include "BLI_utildefines.h"

void BKE_sound_force_device(const char * /*device*/) {}
void BKE_sound_init_once() {}
void BKE_sound_init(Main * /*bmain*/) {}
void BKE_sound_exit_once() {}
void BKE_sound_load(Main * /*bmain*/, bSound * /*sound*/) {}
void BKE_sound_packfile_ensure(Main * /*bmain*/, bSound * /*sound*/, ReportList * /*reports*/) {}
void BKE_sound_create_scene(Scene * /*scene*/) {}
void BKE_sound_destroy_scene(Scene * /*scene*/) {}
void BKE_sound_lock() {}
void BKE_sound_unlock() {}
void BKE_sound_refresh_callback_bmain(Main * /*bmain*/) {}
void BKE_sound_reset_scene_specs(Scene * /*scene*/) {}
void BKE_sound_mute_scene(Scene * /*scene*/, bool /*muted*/) {}
AUD_SequenceEntry BKE_sound_scene_add_scene_sound(Scene * /*scene*/, Strip * /*strip*/)
{
  return nullptr;
}
AUD_SequenceEntry BKE_sound_add_scene_sound(Scene * /*scene*/, Strip * /*strip*/)
{
  return nullptr;
}
void BKE_sound_remove_scene_sound(Scene * /*scene*/, AUD_SequenceEntry /*handle*/) {}
void BKE_sound_mute_scene_sound(AUD_SequenceEntry /*handle*/, bool /*mute*/) {}
void BKE_sound_move_scene_sound(const Scene * /*scene*/,
                                AUD_SequenceEntry /*handle*/,
                                int /*startframe*/,
                                int /*endframe*/,
                                int /*frameskip*/,
                                double /*audio_offset*/)
{
}
void BKE_sound_move_scene_sound_defaults(Scene * /*scene*/, Strip * /*strip*/) {}
void BKE_sound_play_scene(Scene * /*scene*/) {}
void BKE_sound_stop_scene(Scene * /*scene*/) {}
void BKE_sound_seek_scene(Main * /*bmain*/, Scene * /*scene*/) {}
double BKE_sound_sync_scene(Scene * /*scene*/)
{
  return NAN_FLT;
}
void BKE_sound_read_waveform(Main *bmain,
                             bSound *sound,
                             /* NOLINTNEXTLINE: readability-non-const-parameter. */
                             bool *stop)
{
  UNUSED_VARS(sound, stop, bmain);
}

void BKE_sound_update_sequencer(Main * /*main*/, bSound * /*sound*/) {}
void BKE_sound_update_scene(Depsgraph * /*depsgraph*/, Scene * /*scene*/) {}
void BKE_sound_update_scene_sound(AUD_SequenceEntry /*handle*/, bSound * /*sound*/) {}
void BKE_sound_update_scene_listener(Scene * /*scene*/) {}
void BKE_sound_update_fps(Main * /*bmain*/, Scene * /*scene*/) {}
void BKE_sound_set_scene_sound_volume_at_frame(AUD_SequenceEntry /*handle*/,
                                               int /*frame*/,
                                               float /*volume*/,
                                               bool /*animated*/)
{
}
void BKE_sound_set_scene_sound_pan_at_frame(AUD_SequenceEntry /*handle*/,
                                            int /*frame*/,
                                            float /*pan*/,
                                            bool /*animated*/)
{
}
void BKE_sound_set_scene_volume(Scene * /*scene*/, float /*volume*/) {}
void BKE_sound_set_scene_sound_pitch_at_frame(AUD_SequenceEntry /*handle*/,
                                              int /*frame*/,
                                              float /*pitch*/,
                                              bool /*animated*/)
{
}
void BKE_sound_set_scene_sound_pitch_constant_range(AUD_SequenceEntry /*handle*/,
                                                    int /*frame_start*/,
                                                    int /*frame_end*/,
                                                    float /*pitch*/)
{
}
float BKE_sound_get_length(Main * /*bmain*/, bSound * /*sound*/)
{
  return 0;
}
char **BKE_sound_get_device_names()
{
  static char *names[1] = {nullptr};
  return names;
}

bool BKE_sound_info_get(Main * /*main*/, bSound * /*sound*/, SoundInfo * /*sound_info*/)
{
  return false;
}

bool BKE_sound_stream_info_get(Main * /*main*/,
                               const char * /*filepath*/,
                               int /*stream*/,
                               SoundStreamInfo * /*sound_info*/)
{
  return false;
}

int BKE_sound_stream_count(Main * /*main*/, const char * /*filepath*/)
{
  return 0;
}

#endif /* WITH_AUDASPACE */

#if !defined(WITH_AUDASPACE) || !defined(WITH_RUBBERBAND)
AUD_Sound BKE_sound_ensure_time_stretch_effect(const Strip * /*strip*/, float /*fps*/)
{
  return nullptr;
}

void BKE_sound_set_scene_sound_time_stretch_at_frame(AUD_Sound /*handle*/,
                                                     int /*frame*/,
                                                     float /*time_stretch*/,
                                                     bool /*animated*/)
{
}
void BKE_sound_set_scene_sound_time_stretch_constant_range(AUD_Sound /*handle*/,
                                                           int /*frame_start*/,
                                                           int /*frame_end*/,
                                                           float /*time_stretch*/)
{
}
#endif

void BKE_sound_ensure_scene(Scene *scene)
{
  if (scene->runtime->audio.sound_scene != nullptr) {
    return;
  }
  BKE_sound_create_scene(scene);
}

static void sound_ensure_loaded(Main *bmain, bSound *sound)
{
  if (sound->runtime->cache != nullptr) {
    return;
  }
  BKE_sound_load(bmain, sound);
}

void BKE_sound_jack_sync_callback_set(SoundJackSyncCallback callback)
{
#if defined(WITH_AUDASPACE)
  sound_jack_sync_callback = callback;
#else
  UNUSED_VARS(callback);
#endif
}

void BKE_sound_jack_scene_update(Scene *scene, int mode, double time)
{
  sound_verify_evaluated_id(&scene->id);

  /* Ugly: Blender doesn't like it when the animation is played back during rendering. */
  if (G.is_rendering) {
    return;
  }
#ifdef WITH_AUDASPACE
  g_state.sound_device->lock();

  if (mode) {
    BKE_sound_play_scene(scene);
  }
  else {
    BKE_sound_stop_scene(scene);
  }
  if (scene->runtime->audio.playback_handle != nullptr) {
    scene->runtime->audio.playback_handle->seek(time);
  }
  g_state.sound_device->unlock();
#else
  UNUSED_VARS(mode, time);
#endif
}

void BKE_sound_evaluate(Depsgraph *depsgraph, Main *bmain, bSound *sound)
{
  DEG_debug_print_eval(depsgraph, __func__, sound->id.name, sound);
  if (sound->id.recalc & ID_RECALC_SOURCE) {
    /* Sequencer checks this flag to see if the strip sound is to be updated from the Audaspace
     * side. */
    sound->id.recalc |= ID_RECALC_AUDIO;
  }

  if (sound->id.recalc & ID_RECALC_AUDIO) {
    BKE_sound_load(bmain, sound);
    return;
  }
  sound_ensure_loaded(bmain, sound);
}

void BKE_sound_runtime_state_get_and_clear(const bSound *sound,
                                           AUD_Sound *r_cache,
                                           AUD_Sound *r_playback_handle,
                                           Vector<float> **r_waveform)
{
  bke::SoundRuntime *runtime = sound->runtime;
  *r_cache = runtime->cache;
  *r_playback_handle = runtime->playback_handle;
  *r_waveform = runtime->waveform;
  runtime->cache = nullptr;
  runtime->playback_handle = nullptr;
  runtime->waveform = nullptr;
}

void BKE_sound_runtime_state_set(const bSound *sound,
                                 AUD_Sound cache,
                                 AUD_Sound playback_handle,
                                 Vector<float> *waveform)
{
  bke::SoundRuntime *runtime = sound->runtime;
  runtime->cache = cache;
  runtime->playback_handle = playback_handle;
  runtime->waveform = waveform;
}

AUD_Sound BKE_sound_playback_handle_get(const bSound *sound)
{
  if (sound == nullptr) {
    return nullptr;
  }
  return sound->runtime->playback_handle;
}

void BKE_sound_runtime_clear_waveform_loading_tag(bSound *sound)
{
  bke::SoundRuntime *runtime = sound->runtime;
  BLI_spin_lock(&runtime->spinlock);
  runtime->tags &= ~bke::SoundTags::WaveformLoading;
  BLI_spin_unlock(&runtime->spinlock);
}

bool BKE_sound_runtime_start_waveform_loading(bSound *sound)
{
  bke::SoundRuntime *runtime = sound->runtime;
  bool result = false;
  BLI_spin_lock(&runtime->spinlock);
  if (runtime->waveform == nullptr) {
    /* Load the waveform data if it hasn't been loaded and cached already. */
    if (!flag_is_set(runtime->tags, bke::SoundTags::WaveformLoading)) {
      /* Prevent sounds from reloading. */
      runtime->tags |= bke::SoundTags::WaveformLoading;
      result = true;
    }
  }
  BLI_spin_unlock(&runtime->spinlock);
  return result;
}

const Vector<float> *BKE_sound_runtime_get_waveform(const bSound *sound)
{
  return sound->runtime->waveform;
}

namespace bke {

const bSoundFrequencySampler *bSoundFrequencySampler::get_cached(const bSound &sound,
                                                                 const Key &key)
{
#ifdef WITH_AUDASPACE
  {
    /* Fast common case when the sampler has been created already. */
    bSoundFrequencySamplerMap::ConstAccessor accessor;
    if (sound.runtime->samplers.lookup(accessor, key)) {
      return accessor->second.get();
    }
  }
  AUD_Sound sound_handle = sound.runtime->handle;
  if (!sound_handle) {
    /* Maybe try to load the sound in this case instead of relying on cache. */
    return nullptr;
  }
  /* Slower case when the sampler is newly created. */
  bSoundFrequencySamplerMap::MutableAccessor accessor;
  if (sound.runtime->samplers.add(accessor, key)) {
    if (key.channel.has_value()) {
      const SoundInfo info = sound_info_get(sound_handle);
      const int channel = *key.channel;
      if (channel < 0 || channel >= info.specs.channels) {
        return nullptr;
      }
    }
    accessor->second = std::make_shared<bSoundFrequencySampler>(sound_handle, key);
  }
  return accessor->second.get();
#else
  UNUSED_VARS(sound, key);
  return nullptr;
#endif
}

static bSoundFrequencySampler::WindowWeights compute_window_function_weights(
    const bSoundFrequencySampler::WindowFunction window, const int size)
{
  Array<float> weights(size);
  switch (window) {
    case bSoundFrequencySampler::WindowFunction::Hann: {
      for (const int i : IndexRange(size)) {
        weights[i] = 0.5f - 0.5f * math::cos((2.0f * std::numbers::pi * i) / (size - 1));
      }
      break;
    }
    case bSoundFrequencySampler::WindowFunction::Hamming: {
      for (const int i : IndexRange(size)) {
        weights[i] = 0.54f - 0.46f * math::cos((2.0f * std::numbers::pi * i) / (size - 1));
      }
      break;
    }
    case bSoundFrequencySampler::WindowFunction::Blackman: {
      for (const int i : IndexRange(size)) {
        weights[i] = 0.42f - 0.5f * math::cos((2.0f * std::numbers::pi * i) / (size - 1)) +
                     0.08f * math::cos((4.0f * std::numbers::pi * i) / (size - 1));
      }
      break;
    }
    case bSoundFrequencySampler::WindowFunction::Rectangular: {
      weights.fill(1.0f);
      break;
    }
  }
  const float sum = std::accumulate(weights.begin(), weights.end(), 0.0f);
  return bSoundFrequencySampler::WindowWeights{std::move(weights), sum};
}

/** Caches the window function weights so that each combination is only computed once. */
static const bSoundFrequencySampler::WindowWeights &get_window_function_weights(
    const bSoundFrequencySampler::WindowFunction window, const int size)
{
  static Mutex mutex;
  static Map<std::pair<bSoundFrequencySampler::WindowFunction, int>,
             std::unique_ptr<bSoundFrequencySampler::WindowWeights>>
      map;
  std::lock_guard lock{mutex};
  return *map.lookup_or_add_cb({window, size}, [&]() {
    return std::make_unique<bSoundFrequencySampler::WindowWeights>(
        compute_window_function_weights(window, size));
  });
}

bSoundFrequencySampler::bSoundFrequencySampler(AUD_Sound sound, const Key &key)
    : sound_(sound),
      key_(key),
      window_weights_(get_window_function_weights(key.window_function, key.fft_size))
{
#ifdef WITH_AUDASPACE
  const SoundInfo info = bke::sound_info_get(sound_);
  samples_per_second_ = info.specs.samplerate;
  /* This could be a parameter but a single fixed value seems fine for now and makes caching much
   * simpler. */
  window_cache_stride_ = key.fft_size / 8;
  const int window_caches_num = std::ceil(info.length * info.specs.samplerate /
                                          window_cache_stride_);
  window_caches_.reinitialize(window_caches_num);
#else
  UNUSED_VARS(sound, key);
  BLI_assert_unreachable();
#endif
}

std::optional<Array<float>> bSoundFrequencySampler::compute_fft(const int start_sample) const
{
  /* Since the result of the dft algorithm is symmetric in this case, only the first half is
   * computed. */
  const int frequencies_num = key_.fft_size / 2;

#if defined(WITH_AUDASPACE) && defined(WITH_FFTW3)
  /* Read some extra samples before the ones we are actually interested in here. This is done
   * because the #read function may sometimes give invalid data for the first samples. */
  const int warmup_samples = std::min(2000, start_sample);

  /* Prepare the reader. */
  std::shared_ptr<aud::IReader> reader = sound_->createReader();
  const aud::Specs specs = reader->getSpecs();
  const int channels_num = specs.channels;

  /* Read the raw samples from the audio stream. */
  Array<float> read_buffer_extra((key_.fft_size + warmup_samples) * channels_num);
  bool is_end_of_stream = false;
  int length = key_.fft_size + warmup_samples;
  reader->seek(std::max(start_sample - warmup_samples, 0));
  reader->read(length, is_end_of_stream, read_buffer_extra.data());
  const Span<float> read_buffer = read_buffer_extra.as_span().drop_front(warmup_samples *
                                                                         channels_num);
  const int read_length = read_buffer.size() / channels_num;

  /* Pull out the samples for the requested channel(s). */
  Array<float> buffer(key_.fft_size, 0.0f);
  if (key_.channel.has_value()) {
    const int channel = *key_.channel;
    if (channel < 0 || channel >= channels_num) {
      return std::nullopt;
    }
    for (const int i : IndexRange(read_length)) {
      buffer[i] = read_buffer[i * channels_num + channel];
    }
  }
  else {
    for (const int i : IndexRange(read_length)) {
      for (const int c : IndexRange(channels_num)) {
        buffer[i] += read_buffer[i * channels_num + c];
      }
      buffer[i] /= channels_num;
    }
  }

  /* Apply window function which avoids spectral leakage (depending on the function). */
  for (const int i : IndexRange(read_length)) {
    buffer[i] *= window_weights_.weights[i];
  }

  /* Set up the fftw plan. */
  fftwf_complex *fftwf_buffer = static_cast<fftwf_complex *>(
      fftwf_malloc(sizeof(fftwf_complex) * (frequencies_num + 1)));
  fftwf_plan plan = fftwf_plan_dft_r2c_1d(
      key_.fft_size, buffer.data(), fftwf_buffer, FFTW_ESTIMATE);
  BLI_SCOPED_DEFER([&]() {
    fftwf_destroy_plan(plan);
    fftwf_free(fftwf_buffer);
  });

  /* Actually perform the fourier transform. */
  fftwf_execute(plan);

  /* Compute the amplitudes of the frequencies. */
  Array<float> frequency_amplitudes(frequencies_num);
  /* The scaling factor is applied so that changing the fft size or window function does not affect
   * the magnitude of the result. */
  const float scaling_factor = 1.0f / window_weights_.weights_sum;
  for (const int i : IndexRange(frequencies_num)) {
    const fftwf_complex &c = fftwf_buffer[i];
    /* Take real and imaginary parts into account which correspond to the sin and cos component of
     * the frequency. */
    frequency_amplitudes[i] = sqrt(pow2f(c[0]) + pow2f(c[1])) * scaling_factor;
  }

  return frequency_amplitudes;
#else
  UNUSED_VARS(start_sample);
  return Array<float>(frequencies_num, 0.0f);
#endif
}

static float catmul_rom_interpolation(
    const float p0, const float p1, const float p2, const float p3, const float t)
{
  const float t2 = t * t;
  const float t3 = t2 * t;

  const float value = 0.5 * ((2 * p1) + (-p0 + p2) * t + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t2 +
                             (-p0 + 3 * p1 - 3 * p2 + p3) * t3);
  return value;
}

static float bspline_interpolation(
    const float p0, const float p1, const float p2, const float p3, const float t)
{
  const float t2 = t * t;
  const float t3 = t2 * t;

  const float b0 = (1.0f - t) * (1.0f - t) * (1.0f - t) / 6.0f;
  const float b1 = (3.0f * t3 - 6.0f * t2 + 4.0f) / 6.0f;
  const float b2 = (-3.0f * t3 + 3.0f * t2 + 3.0f * t + 1.0f) / 6.0f;
  const float b3 = t3 / 6.0f;

  return (b0 * p0 + b1 * p1 + b2 * p2 + b3 * p3);
}

float bSoundFrequencySampler::sample(const float time,
                                     const float low,
                                     const float high,
                                     const InterpolationMethod time_interpolation,
                                     const InterpolationMethod frequency_interpolation) const
{
  if (low >= high) {
    return 0.0f;
  }
  const float i_float = std::max(
      0.0f, (time * samples_per_second_ - key_.fft_size / 2) / window_cache_stride_);
  const int i_pre = floorf(i_float);
  const int i_post = i_pre + 1;
  const float t = fractf(i_float);

  switch (time_interpolation) {
    case InterpolationMethod::Linear: {
      const float v_prev = this->sample_frequency_range_in_window(
          i_pre, low, high, frequency_interpolation);
      const float v_next = this->sample_frequency_range_in_window(
          i_post, low, high, frequency_interpolation);

      return math::interpolate(v_prev, v_next, t);
    }
    case InterpolationMethod::CatmullRom: {
      const int i_pre2 = i_pre - 1;
      const int i_post2 = i_post + 1;

      const float p0 = this->sample_frequency_range_in_window(
          i_pre2, low, high, frequency_interpolation);
      const float p1 = this->sample_frequency_range_in_window(
          i_pre, low, high, frequency_interpolation);
      const float p2 = this->sample_frequency_range_in_window(
          i_post, low, high, frequency_interpolation);
      const float p3 = this->sample_frequency_range_in_window(
          i_post2, low, high, frequency_interpolation);

      return catmul_rom_interpolation(p0, p1, p2, p3, t);
    }
    default:
    case InterpolationMethod::BSpline: {
      const int i_pre2 = i_pre - 1;
      const int i_post2 = i_post + 1;

      const float p0 = this->sample_frequency_range_in_window(
          i_pre2, low, high, frequency_interpolation);
      const float p1 = this->sample_frequency_range_in_window(
          i_pre, low, high, frequency_interpolation);
      const float p2 = this->sample_frequency_range_in_window(
          i_post, low, high, frequency_interpolation);
      const float p3 = this->sample_frequency_range_in_window(
          i_post2, low, high, frequency_interpolation);

      return bspline_interpolation(p0, p1, p2, p3, t);
    }
  }
}

float bSoundFrequencySampler::sample_frequency_range_in_window(int window_i,
                                                               float low,
                                                               float high,
                                                               InterpolationMethod method) const
{
  const std::optional<Span<float>> window_opt = this->ensure_window_cache(window_i);
  if (!window_opt.has_value()) {
    return 0.0f;
  }
  const Span<float> window_values = *window_opt;
  const float cumulative_low = this->sample_cumulative_frequency(window_values, low, method);
  const float cumulative_high = this->sample_cumulative_frequency(window_values, high, method);
  return cumulative_high - cumulative_low;
}

float bSoundFrequencySampler::sample_cumulative_frequency(const Span<float> window_values,
                                                          const float frequency,
                                                          const InterpolationMethod method) const
{
  const int max_i = window_values.size() - 1;
  const float i_float = frequency * key_.fft_size / samples_per_second_;
  const int i_pre = std::clamp<int>(std::floor(i_float), 0, max_i);
  const int i_post = std::min(i_pre + 1, max_i);
  const float t = fractf(i_float);

  switch (method) {
    case InterpolationMethod::Linear: {
      return math::interpolate(window_values[i_pre], window_values[i_post], t);
    }
    case InterpolationMethod::CatmullRom: {
      const int i_pre2 = std::max(i_pre - 1, 0);
      const int i_post2 = std::min(i_post + 1, max_i);

      const float p0 = window_values[i_pre2];
      const float p1 = window_values[i_pre];
      const float p2 = window_values[i_post];
      const float p3 = window_values[i_post2];

      return catmul_rom_interpolation(p0, p1, p2, p3, t);
    }
    case InterpolationMethod::BSpline: {
      const int i_pre2 = std::max(i_pre - 1, 0);
      const int i_post2 = std::min(i_post + 1, max_i);

      const float p0 = window_values[i_pre2];
      const float p1 = window_values[i_pre];
      const float p2 = window_values[i_post];
      const float p3 = window_values[i_post2];

      return bspline_interpolation(p0, p1, p2, p3, t);
    }
  }
  return 0.0f;
}

std::optional<Span<float>> bSoundFrequencySampler::ensure_window_cache(int window_i) const
{
  /* Clamp window so that too low or high indices get mapped to the closest valid index. */
  window_i = std::clamp<int>(window_i, 0, window_caches_.size() - 1);

  const WindowCache &window = window_caches_[window_i];
  /* Compute the FFT of that window if that wasn't done already. */
  window.mutex.ensure([&]() {
    std::optional<Array<float>> fft_array = this->compute_fft(window_i * window_cache_stride_);
    if (!fft_array.has_value()) {
      return;
    }
    /* Compute prefix sum for the fft values. */
    const Span<float> fft_values = fft_array->as_span();
    window.cumulative_amplitudes.emplace(fft_values.size() + 1);
    float sum = 0.0f;
    for (const int i : fft_array->index_range()) {
      const float value = std::abs(fft_values[i]);
      (*window.cumulative_amplitudes)[i] = sum;
      sum += value;
    }
    window.cumulative_amplitudes->last() = sum;
  });
  return window.cumulative_amplitudes;
}

}  // namespace bke

}  // namespace blender
