text: // Released under the MIT License. See LICENSE for details.
  
  #include "ballistica/audio/ogg_stream.h"
  
  #include "ballistica/platform/platform.h"
  
  namespace ballistica {
  
  #if BA_ENABLE_AUDIO
  
  static auto CallbackRead(void* ptr, size_t size, size_t nmemb,
                           void* data_source) -> size_t {
    return fread(ptr, size, nmemb, static_cast<FILE*>(data_source));
  }
  
  static auto CallbackSeek(void* data_source, ogg_int64_t offset, int whence)
      -> int {
    return fseek(static_cast<FILE*>(data_source),
                 static_cast_check_fit<long>(offset), whence);  // NOLINT
  }
  static auto CallbackClose(void* data_source) -> int {
    return fclose(static_cast<FILE*>(data_source));
  }
  static long CallbackTell(void* data_source) {  // NOLINT (ogg wants long)
    return ftell(static_cast<FILE*>(data_source));
  }
  
  OggStream::OggStream(const char* file_name, ALuint source, bool loop)
      : AudioStreamer(file_name, source, loop), have_ogg_file_(false) {
    int result;
    FILE* f;
    if (!(f = g_platform->FOpen(file_name, "rb"))) {
      throw Exception("can't open ogg file: '" + std::string(file_name) + "'");
    }
    ov_callbacks callbacks;
    callbacks.read_func = CallbackRead;
    callbacks.seek_func = CallbackSeek;
    callbacks.close_func = CallbackClose;
    callbacks.tell_func = CallbackTell;
  
    // Have to use callbacks here as codewarrior's FILE struct doesn't
    // seem to agree with what vorbis expects... oh well.
    // Ericf note Aug 2019: Wow I have comments here old enough to be referencing
    // codewarrior; that's awesome!
    result = ov_open_callbacks(f, &ogg_file_, nullptr, 0, callbacks);
    if (result < 0) {
      fclose(f);
      throw Exception(GetErrorString(result));
    }
    have_ogg_file_ = true;
  
    vorbis_info_ = ov_info(&ogg_file_, -1);
    if (vorbis_info_->channels == 1) {
      set_format(MONO16_FORMAT);
    } else {
      set_format(STEREO16_FORMAT);
    }
  }
  
  OggStream::~OggStream() {
    if (have_ogg_file_) {
      ov_clear(&ogg_file_);
    }
  }
  
  void OggStream::DoStop() {
    if (have_ogg_file_) ov_pcm_seek(&ogg_file_, 0);
  }
  
  void OggStream::DoStream(char* pcm, int* size, unsigned int* rate) {
    int section;
    int result;
    while ((*size) < kAudioStreamBufferSize) {
      // tremor's ov_read takes fewer args
  #if (BA_OSTYPE_IOS_TVOS || BA_OSTYPE_ANDROID)
      result = static_cast<int>(ov_read(
          &ogg_file_, pcm + (*size), kAudioStreamBufferSize - (*size), &section));
  #else
      result = static_cast<int>(ov_read(&ogg_file_, pcm + (*size),
                                        kAudioStreamBufferSize - (*size), 0, 2, 1,
                                        &section));
  #endif  // BA_OSTYPE_IOS_TVOS
  
      if (result > 0) {
        (*size) += result;
      } else {
        if (result < 0) {
          static bool reported_error = false;
          if (!reported_error) {
            reported_error = true;
            Log("Error streaming ogg file: '" + file_name() + "'.");
          }
          if (loops()) {
            ov_pcm_seek(&ogg_file_, 0);
          } else {
            return;
          }
        } else {
          // we hit the end of the file; either reset and keep reading if we're
          // looping or just return what we got
          if (loops()) {
            ov_pcm_seek(&ogg_file_, 0);
          } else {
            return;
          }
        }
      }
    }
    if ((*size) == 0 && loops()) {
      throw Exception();
    }
    (*rate) = static_cast<unsigned int>(vorbis_info_->rate);
  }
  
  auto OggStream::GetErrorString(int code) -> std::string {
    switch (code) {
      case OV_EREAD:
        return std::string("Read from media.");
      case OV_ENOTVORBIS:
        return std::string("Not Vorbis data.");
      case OV_EVERSION:
        return std::string("Vorbis version mismatch.");
      case OV_EBADHEADER:
        return std::string("Invalid Vorbis header.");
      case OV_EFAULT:
        return std::string("Internal logic fault (bug or heap/stack corruption.");
      default:
        return std::string("Unknown Ogg error.");
    }
  }
  
  #endif  // BA_ENABLE_AUDIO
  
  }  // namespace ballistica
  