As the title says, I'm a bit stumped on this. I'm not sure what to do for the write_callback of libFLAC++. I have implemented the rest of the callbacks correctly (I think). So that way, libFLAC can decode using an ifstream rather than a C-style FILE*.
Below is my implementation of the decoder and its callbacks.
#include <FLAC++/decoder.h>
#include <fstream>
#include <iostream>
class FLACStreamDecoder : public FLAC::Decoder::Stream {
private:
std::ifstream& input;
uint32_t sample_rate;
uint32_t channels;
uint32_t bits_per_sample;
public:
~FLACStreamDecoder();
// The FLAC decoder will take ownership of the ifstream.
FLACStreamDecoder(std::ifstream& arg) : FLAC::Decoder::Stream(), input(arg) {}
uint32_t getSampleRate() { return sample_rate; }
uint32_t getChannels() { return channels; }
uint32_t getBitsPerSample() { return bits_per_sample; }
virtual void metadata_callback(const FLAC__StreamMetadata *);
virtual ::FLAC__StreamDecoderReadStatus read_callback(FLAC__byte *, size_t *);
virtual ::FLAC__StreamDecoderWriteStatus write_callback(const FLAC__Frame *, const FLAC__int32 * const *);
virtual void error_callback(FLAC__StreamDecoderErrorStatus);
virtual ::FLAC__StreamDecoderSeekStatus seek_callback(FLAC__uint64 absolute_byte_offset);
virtual ::FLAC__StreamDecoderTellStatus tell_callback(FLAC__uint64 *absolute_byte_offset);
virtual ::FLAC__StreamDecoderLengthStatus length_callback(FLAC__uint64 *stream_length);
virtual bool eof_callback();
};
FLACStreamDecoder::~FLACStreamDecoder() { input.close(); }
void FLACStreamDecoder::metadata_callback(const FLAC__StreamMetadata * metadata) {
std::cerr << "metadata callback called!" << std::endl;
if (FLAC__METADATA_TYPE_STREAMINFO == metadata->type) {
std::cerr << "streaminfo found!" << std::endl;
sample_rate = metadata->data.stream_info.sample_rate;
channels = metadata->data.stream_info.channels;
bits_per_sample = metadata->data.stream_info.bits_per_sample;
}
}
static_assert(sizeof(char) == sizeof(FLAC__byte), "invalid char size");
FLAC__StreamDecoderReadStatus FLACStreamDecoder::read_callback(FLAC__byte * buffer, size_t * nbytes) {
if (nbytes && *nbytes > 0) {
input.read(reinterpret_cast<char *>(buffer), *nbytes);
*nbytes = input.gcount();
if (input.fail()) {
return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
}
else if (input.eof()) {
return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM;
}
else {
return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE;
}
}
else {
return FLAC__STREAM_DECODER_READ_STATUS_ABORT;
}
}
::FLAC__StreamDecoderSeekStatus FLACStreamDecoder::seek_callback(FLAC__uint64 absolute_byte_offset) {
if (input.is_open()) {
input.seekg(absolute_byte_offset);
return FLAC__StreamDecoderSeekStatus::FLAC__STREAM_DECODER_SEEK_STATUS_OK;
}
return FLAC__StreamDecoderSeekStatus::FLAC__STREAM_DECODER_SEEK_STATUS_ERROR;
}
::FLAC__StreamDecoderTellStatus FLACStreamDecoder::tell_callback(FLAC__uint64 *absolute_byte_offset) {
if (input.is_open()) {
*absolute_byte_offset = input.tellg();
return FLAC__StreamDecoderTellStatus::FLAC__STREAM_DECODER_TELL_STATUS_OK;
}
return FLAC__StreamDecoderTellStatus::FLAC__STREAM_DECODER_TELL_STATUS_ERROR;
}
::FLAC__StreamDecoderLengthStatus FLACStreamDecoder::length_callback(FLAC__uint64 *stream_length) {
if (input.is_open()) {
std::streampos currentPos = input.tellg();
input.seekg(std::ios::end);
*stream_length = input.tellg();
input.seekg(currentPos);
return FLAC__StreamDecoderLengthStatus::FLAC__STREAM_DECODER_LENGTH_STATUS_OK;
}
return FLAC__StreamDecoderLengthStatus::FLAC__STREAM_DECODER_LENGTH_STATUS_ERROR;
}
bool FLACStreamDecoder::eof_callback() {
return input.eof();
}
// This is called for every audio frame.
FLAC__StreamDecoderWriteStatus FLACStreamDecoder::write_callback(const FLAC__Frame * frame, const FLAC__int32 * const * buffer) {
// A the size of a FLAC Frame is frame->header.channels * frame->header.blocksize. That is, the size of the buffer array is the number of channels in the current frame, times the number of samples per channel (blocksize).
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;
}
void FLACStreamDecoder::error_callback(FLAC__StreamDecoderErrorStatus status) {
std::string msg;
switch (status) {
case FLAC__STREAM_DECODER_ERROR_STATUS_BAD_HEADER:
msg = "BAD HEADER";
break;
case FLAC__STREAM_DECODER_ERROR_STATUS_LOST_SYNC:
msg = "LOST SYNC";
break;
case FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH:
msg = "FRAME CRC MISMATCH";
break;
case FLAC__STREAM_DECODER_ERROR_STATUS_UNPARSEABLE_STREAM:
msg = "UNPARSEABLE STREAM";
break;
default:
msg = "UNKNOWN ERROR";
break;
}
std::cerr << msg << std::endl;
}
As you see, I have no idea what to do for the write_callback. Any help with regards to that would be appreciated. To be a bit more clear, the problem is that WASAPI has a frame size of numChannels * bitsPerSample bits, or numChannels * bitsPerSample / 8 bytes. I can't seem to figure out how to go from a FLAC frame to a WASAPI frame.
I'll also paste my WASAPI playback code below:
#pragma once
#include <iostream>
#define NOMINMAX
#include <Mmdeviceapi.h>
#include <Audioclient.h>
#include <fstream>
#include <algorithm>
class WASAPIBackend
{
public:
WASAPIBackend();
~WASAPIBackend();
private:
HRESULT hr;
IMMDeviceEnumerator* pDeviceEnumerator;
IMMDevice* pDevice;
IAudioClient3* pAudioClient;
IAudioRenderClient* pAudioRenderClient;
//WAVEFORMATEX* pMixFormat;
WAVEFORMATEX mixFormat;
uint32_t defaultPeriodInFrames, fundamentalPeriodInFrames, minPeriodInFrames, maxPeriodInFrames;
HANDLE audioSamplesReadyEvent;
};
#include "WASAPIBackend.h"
constexpr void SafeRelease(IUnknown** p) {
if (p) {
(*p)->Release();
}
}
WASAPIBackend::WASAPIBackend() : hr(0), pDeviceEnumerator(nullptr), pDevice(nullptr), pAudioClient(nullptr), pAudioRenderClient(nullptr)/*, pMixFormat(nullptr)*/
{
try
{
// COM result
hr = S_OK;
hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (FAILED(hr)) throw std::runtime_error("CoInitialize error");
hr = CoCreateInstance(
__uuidof(MMDeviceEnumerator),
nullptr,
CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator),
reinterpret_cast<void**>(&pDeviceEnumerator));
if (FAILED(hr)) throw std::runtime_error("CoCreateInstance error");
hr = pDeviceEnumerator->GetDefaultAudioEndpoint(EDataFlow::eRender, ERole::eConsole, &pDevice);
if (FAILED(hr)) throw std::runtime_error("IMMDeviceEnumerator.GetDefaultAudioEndpoint error");
std::cout << "IMMDeviceEnumerator.GetDefaultAudioEndpoint()->OK" << std::endl;
hr = pDevice->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr, reinterpret_cast<void**>(&pAudioClient));
if (FAILED(hr)) throw std::runtime_error("IMMDevice.Activate error");
std::cout << "IMMDevice.Activate()->OK" << std::endl;
WAVEFORMATEX wave_format = {};
wave_format.wFormatTag = WAVE_FORMAT_PCM;
wave_format.nChannels = 2;
wave_format.nSamplesPerSec = 44100;
//nSamplesPerSec * nBlockAlign
wave_format.nAvgBytesPerSec = 44100 * 2 * 16 / 8;
wave_format.nBlockAlign = 2 * 16 / 8;
wave_format.wBitsPerSample = 16;
//pAudioClient->GetMixFormat(reinterpret_cast<WAVEFORMATEX**>(&wave_format));
hr = pAudioClient->GetSharedModeEnginePeriod(&wave_format, &defaultPeriodInFrames, &fundamentalPeriodInFrames, &minPeriodInFrames, &maxPeriodInFrames);
hr = pAudioClient->GetSharedModeEnginePeriod(&wave_format, &defaultPeriodInFrames, &fundamentalPeriodInFrames, &minPeriodInFrames, &maxPeriodInFrames);
if (FAILED(hr)) throw std::runtime_error("IAudioClient.GetDevicePeriod error");
std::cout << "default device period=" << defaultPeriodInFrames << "[nano seconds]" << std::endl;
std::cout << "minimum device period=" << minPeriodInFrames << "[nano seconds]" << std::endl;
hr = pAudioClient->InitializeSharedAudioStream(AUDCLNT_STREAMFLAGS_EVENTCALLBACK, minPeriodInFrames, &wave_format, nullptr);
if (FAILED(hr)) throw std::runtime_error("IAudioClient.Initialize error");
std::cout << "IAudioClient.Initialize()->OK" << std::endl;
// event
audioSamplesReadyEvent = CreateEvent(nullptr, false, false, nullptr);
if (FAILED(hr)) throw std::runtime_error("CreateEvent error");
hr = pAudioClient->SetEventHandle(audioSamplesReadyEvent);
if (FAILED(hr)) throw std::runtime_error("IAudioClient.SetEventHandle error");
UINT32 numBufferFrames = 0;
hr = pAudioClient->GetBufferSize(&numBufferFrames);
if (FAILED(hr)) throw std::runtime_error("IAudioClient.GetBufferSize error");
std::cout << "buffer frame size=" << numBufferFrames << "[frames]" << std::endl;
hr = pAudioClient->GetService(__uuidof(IAudioRenderClient), reinterpret_cast<void**>(&pAudioRenderClient));
std::cout << std::hex << hr << std::endl;
if (FAILED(hr)) throw std::runtime_error("IAudioClient.GetService error");
BYTE *pData = nullptr;
hr = pAudioRenderClient->GetBuffer(numBufferFrames, &pData);
if (FAILED(hr)) throw std::runtime_error("IAudioRenderClient.GetBuffer error");
const char* flac_filename = "audio/07 DaMonz - Choose Your Destiny (Super Smash Bros. Melee).flac";
std::ifstream stream(flac_filename, std::ifstream::binary);
FLACStreamDecoder streamer(stream);
auto initStatus = streamer.init();
if (FLAC__STREAM_DECODER_INIT_STATUS_OK != initStatus) {
std::cerr << "ERROR INITIALIZING" << std::endl;
}
else {
streamer.process_until_end_of_metadata();
if (!streamer.process_single()) {
std::cerr << "FAILED PROCESSING" << std::endl;
}
else {
std::cerr << "SUCCEEDED PROCESSING" << std::endl;
}
}
hr = pAudioRenderClient->ReleaseBuffer(numBufferFrames, 0);
if (FAILED(hr)) throw std::runtime_error("IAudioRenderClient.ReleaseBuffer error");
AudioClientProperties audioClientProp = {};
audioClientProp.cbSize = sizeof(AudioClientProperties);
audioClientProp.bIsOffload = true;
audioClientProp.eCategory = AUDIO_STREAM_CATEGORY::AudioCategory_GameMedia;
audioClientProp.Options = AUDCLNT_STREAMOPTIONS::AUDCLNT_STREAMOPTIONS_MATCH_FORMAT;
pAudioClient->SetClientProperties(&audioClientProp);
hr = pAudioClient->Start();
if (FAILED(hr)) throw std::runtime_error("IAudioClient.Start error");
std::cout << "IAudioClient.Start()->OK" << std::endl;
//bool playing = (streamer.get_total_samples() > numBufferFrames);
while (/*playing*/ true)
{
WaitForSingleObject(audioSamplesReadyEvent, INFINITE);
uint32_t numPaddingFrames = 0;
hr = pAudioClient->GetCurrentPadding(&numPaddingFrames);
if (FAILED(hr)) throw std::runtime_error("IAudioClient.GetCurrentPadding error");
uint32_t numAvailableFrames = numBufferFrames - numPaddingFrames;
if (numAvailableFrames == 0) continue;
hr = pAudioRenderClient->GetBuffer(numAvailableFrames, &pData);
if (FAILED(hr)) throw std::runtime_error("IAudioRenderClient.GetBuffer error");
for (size_t i = 0; i < numAvailableFrames; ++i) {
streamer.process_single();
memcpy(&pData[i], &m_audioFrame, streamer.get_blocksize() * streamer.getChannels());
}
hr = pAudioRenderClient->ReleaseBuffer((uint32_t)numAvailableFrames, 0);
if (FAILED(hr)) throw std::runtime_error("IAudioRenderClient.ReleaseBuffer error");
//playing = (streamer.get_total_samples() < numAvailableFrames);
}
do
{
// wait for buffer to be empty
WaitForSingleObject(audioSamplesReadyEvent, INFINITE);
uint32_t numPaddingFrames = 0;
hr = pAudioClient->GetCurrentPadding(&numPaddingFrames);
if (FAILED(hr)) throw std::runtime_error("IAudioClient.GetCurrentPadding error");
if (numPaddingFrames == 0)
{
std::cout << "current buffer padding=0[frames]" << std::endl;
break;
}
} while (true);
hr = pAudioClient->Stop();
if (FAILED(hr)) throw std::runtime_error("IAudioClient.Stop error");
std::cout << "IAudioClient.Stop()->OK" << std::endl;
}
catch (std::exception& ex)
{
std::cout << "error:" << ex.what() << std::endl;
}
}
WASAPIBackend::~WASAPIBackend()
{
//CoTaskMemFree(pMixFormat);
if (audioSamplesReadyEvent) CloseHandle(audioSamplesReadyEvent);
SafeRelease(reinterpret_cast<IUnknown**>(&pAudioRenderClient));
SafeRelease(reinterpret_cast<IUnknown**>(&pAudioClient));
SafeRelease(reinterpret_cast<IUnknown**>(&pDevice));
SafeRelease(reinterpret_cast<IUnknown**>(&pDeviceEnumerator));
CoUninitialize();
}
The playback loop is most definitely broken, but I can't fix it if the decoder isn't working. Please note that I'm doing this for learning purposes, to get a better understanding of how libraries like SDL_Mixer and libsndfile work at a more fundamental level.