Search code examples
c++audioportaudiolibsndfile

Playing a stereo .wav file with PortAudio & sndfile, output is fuzzy and pitched down / slowed


I've been writing some code to play back a stereo .wav file in c++ using PortAudio and sndfile, but the output sound is fuzzy and pitched down (pitch down is less of an issue for me but it may be part of the problem). It almost seems like its playing partially garbage data with the fuzz, but I don't believe there's any variable I'm using that this could happen where I'm not clearing the data first. I've tried to follow some of PortAudios examples with stereo playback but due to the input being from a .wav file rather than generated I've not been able to follow it perfectly. I've also compiled and ran some PortAudio examples (that use stereo sound) and those work fine. I'm not sure where the issue is.

Audio.h

struct AudioFile {
  SNDFILE* file = nullptr;
  SF_INFO  info;
  int      buffer_size = 512;
  int      readHead = 0;
  sf_count_t count = 1;
};

/*
 Class for handling basic audio functions
*/
class Audio {
 protected:
 public:
  /// Constructor
  Audio();
  /// Destructor
  ~Audio();
  /// Load an audio file
  AudioFile loadFile(const char* path);
  /// Play an audio file
  void playFile(AudioFile* file);
};

Audio.cpp

/// Audio constructor
Audio::Audio() {
  PaError err = Pa_Initialize();
  if (err != paNoError) std::cerr << "PAError: " << err << std::endl;

#ifdef DEBUG
  std::cout << "Initialising PortAudio" << std::endl;
  std::cout << "----------------------" << std::endl;
  std::cout << "Version: " << Pa_GetVersion << std::endl;
  std::cout << "Devices:" << std::endl;
  std::cout << "----------------------" << std::endl;

  int numDevices = Pa_GetDeviceCount();
  for (int i=0; i < numDevices; i++) {
    auto deviceInfo = Pa_GetDeviceInfo(i);
    std::cout << "Name: " << deviceInfo->name << std::endl;
    std::cout << "HostApi: " << deviceInfo->hostApi << std::endl;
    std::cout << "SampleRate: " << deviceInfo->defaultSampleRate << std::endl;
    std::cout << "InputChannels: " << deviceInfo->maxInputChannels << std::endl;
    std::cout << "OutputChannels: " << deviceInfo->maxOutputChannels << std::endl;
    std::cout << "----------------------" << std::endl;
  }
#endif
}

Audio::~Audio() {
  PaError err = Pa_Terminate();
  if (err != paNoError) std::cerr << "PAError: " << err << std::endl;
}

/* Loads an audiofile */
AudioFile Audio::loadFile(const char* path) {
  AudioFile file;
  ::memset(&file.info, 0, sizeof(file.info));
  file.file = sf_open(path, SFM_READ, &file.info);
  return file;
}

static int patestCallback(const void* inputBuffer, void* outputBuffer,
                          unsigned long                   framesPerBuffer,
                          const PaStreamCallbackTimeInfo* timeInfo,
                          PaStreamCallbackFlags statusFlags, void* userData) {
  /// Prevent warnings
  (void)inputBuffer;
  (void)timeInfo;
  (void)statusFlags;

  /// an AudioFile gets passed as userData
  AudioFile* file = (AudioFile*)userData;
  float*            out  = (float*)outputBuffer;

  sf_seek(file->file, file->readHead, SF_SEEK_SET);

  auto data   = std::make_unique<float[]>(framesPerBuffer);
  file->count = sf_read_float(file->file, data.get(), framesPerBuffer);

  for (int i = 0; i < framesPerBuffer; i++) { 
      *out++ = data[i];
  }

  file->readHead += file->buffer_size;

  if (file->count > 0) return paContinue;
  else return paComplete;
}

void Audio::playFile(AudioFile* file) {
  PaStream*          stream = nullptr;
  PaStreamParameters params;
  params.device       = Pa_GetDefaultOutputDevice();
  params.channelCount = file->info.channels;
  params.sampleFormat = paFloat32;
  params.suggestedLatency =
    Pa_GetDeviceInfo(params.device)->defaultLowOutputLatency;
  params.hostApiSpecificStreamInfo = nullptr;

  /// Check if params work
  PaError err = Pa_IsFormatSupported(nullptr, &params, file->info.samplerate);
  if (err != paFormatIsSupported) {
    std::cerr << "PAError: " << Pa_GetErrorText(err) << std::endl;
    return;
  }

  err = Pa_OpenStream(&stream, nullptr, &params, file->info.samplerate,
                      file->buffer_size * params.channelCount, paClipOff,
                      &patestCallback, file);
  if (err != paNoError) std::cerr << "PAError: " << Pa_GetErrorText(err) << std::endl;

  err = Pa_StartStream(stream);
  if (err != paNoError)
    std::cerr << "PAError: " << Pa_GetErrorText(err) << std::endl;

  /// wait until file finishes playing
  while (file->count > 0) {}

  err = Pa_StopStream(stream);
  if (err != paNoError)
    std::cerr << "PAError: " << Pa_GetErrorText(err) << std::endl;

  err = Pa_CloseStream(stream);
  if (err != paNoError)
    std::cerr << "PAError: " << Pa_GetErrorText(err) << std::endl;

}

I've also tried without the data pointer (using this does seem to produce a cleaner, but still fuzzy sound) and passing the audio file by value into the playFile function. Any help is appreciated.


Solution

  • Ended up figuring it out, I had one primary issue, here:

    err = Pa_OpenStream(&stream, nullptr, &params, file->info.samplerate,
                          file->buffer_size * params.channelCount, paClipOff,
                          &patestCallback, file);
    

    I gave the Pa_OpenStream the buffersize * the number of channels, however I should've just been giving it the buffersize, and make the channel adjustment to the framesPerBuffer in the callback function directly:

    static int patestCallback(const void* inputBuffer, void* outputBuffer,
                              unsigned long                   framesPerBuffer,
                              const PaStreamCallbackTimeInfo* timeInfo,
                              PaStreamCallbackFlags statusFlags, void* userData) {
      /// Prevent warnings
      (void)inputBuffer;
      (void)timeInfo;
      (void)statusFlags;
    
      /// an AudioFile gets passed as userData
      velox::AudioFile* file = (velox::AudioFile*)userData;
      float*            out  = (float*)outputBuffer;
    
      sf_seek(file->file, file->readHead, SF_SEEK_SET);
    
      auto data = std::make_unique<float[]>(framesPerBuffer * file->info.channels);
      file->count = sf_read_float(file->file, data.get(),
                                  framesPerBuffer * file->info.channels);
    
      for (int i = 0; i < framesPerBuffer * file->info.channels; i++) { 
          *out++ = data[i];
      }
    
      file->readHead += file->buffer_size;
    
      if (file->count > 0) return paContinue;
      else return paComplete;
    }
    

    This change fixed both the pitch and the fuzz, which makes sense in hindsight.