File WavPlayer.h¶
File List > external-docs > libDaisy > src > util > WavPlayer.h
Go to the documentation of this file
Source Code¶
#pragma once
#include "daisy.h"
#include "ff.h"
#include "WavParser.h"
#include "FileReader.h"
namespace daisy
{
template <size_t workspace_bytes>
class WavPlayer
{
public:
WavPlayer() {}
~WavPlayer() {}
enum class Result
{
Ok,
FileNotFoundError,
PlaybackUnderrun,
PrepareOverrun,
NewSamplesRequested,
DiskError,
InitialLatency,
};
struct FileInfo
{
size_t channels, length, samplerate, data_start;
size_t data_size_bytes;
};
Result Init(const char* name)
{
auto res = Open(name);
if(res != Result::Ok)
return res;
for(size_t i = 0; i < kMaxAudioChannels; i++)
{
current_sample_[i] = 0.f;
previous_sample_[i] = 0.f;
}
pos_acc_ = 0.f;
playback_speed_ = 1.f;
looping_ = false;
playing_ = false;
return Result::Ok;
}
Result Open(const char* name)
{
if(is_open_)
f_close(&file_);
auto sta = f_open(&file_, name, (FA_OPEN_EXISTING | FA_READ));
switch(sta)
{
case FR_OK: break;
case FR_NO_FILE:
case FR_NO_PATH: return Result::FileNotFoundError;
default: return Result::DiskError;
}
is_open_ = true;
daisy::FileReader reader(&file_);
daisy::WavParser parser;
if(!parser.parse(reader))
return Result::DiskError;
const auto& info = parser.info();
file_info_.channels = info.numChannels;
file_info_.samplerate = info.sampleRate;
auto bd = info.bitsPerSample;
file_info_.data_size_bytes = parser.dataSize();
file_info_.length
= file_info_.data_size_bytes / ((bd / 8) * file_info_.channels);
file_info_.data_start = parser.dataOffset();
// Compute frame size (bytes per sample frame)
frame_bytes_ = (file_info_.channels) * (bd / 8);
if(frame_bytes_ == 0)
return Result::DiskError;
// Seek to start of data
if(f_lseek(&file_, file_info_.data_start) != FR_OK)
return Result::DiskError;
// Prime FIFO with frame-aligned read
std::fill(buff_raw_, buff_raw_ + kRxSizeSamples, 0);
UINT bytes_read = 0;
size_t bytes_to_read
= std::min((size_t)workspace_bytes, file_info_.data_size_bytes);
// Align down to full frames
bytes_to_read -= (bytes_to_read % frame_bytes_);
if(bytes_to_read > 0)
{
if(f_read(&file_, (void*)buff_raw_, bytes_to_read, &bytes_read)
!= FR_OK)
{
f_close(&file_);
return Result::DiskError;
}
if(bytes_read != bytes_to_read)
{
f_close(&file_);
return Result::DiskError;
}
}
buff_fifo_.Clear();
size_t samps_to_write = bytes_read / sizeof(int16_t);
// Align push count to whole frames (multiples of channels)
if(file_info_.channels > 0)
samps_to_write -= (samps_to_write % file_info_.channels);
for(size_t i = 0; i < samps_to_write; i++)
{
if(!buff_fifo_.PushBack(buff_raw_[i]))
break;
}
position_ = 0;
pending_read_req_ = false;
awaiting_first_data_ = false;
pending_seek_req_ = false;
handled_gen_ = restart_gen_;
bytes_left_in_chunk_ = (bytes_read <= file_info_.data_size_bytes)
? (file_info_.data_size_bytes - bytes_read)
: 0;
return Result::Ok;
}
Result Close()
{
f_close(&file_);
file_info_.channels = 0;
file_info_.data_start = 0;
file_info_.length = 0;
file_info_.samplerate = 0;
is_open_ = false;
playing_ = false;
return Result::Ok;
}
Result Prepare()
{
for(;;)
{
// Capture the restart generation, used to discard unwanted data
// if a restart occurs in the callback during Disk I/O.
const uint32_t gen = restart_gen_;
if(gen != handled_gen_)
{
handled_gen_ = gen;
request_fifo_.Clear();
if(f_lseek(&file_, file_info_.data_start) != FR_OK)
{
Close();
return Result::DiskError;
}
bytes_left_in_chunk_ = file_info_.data_size_bytes;
// Smaller first read gets audio started sooner (less underrun)
size_t first = kRxSizeSamples / 4;
auto res = ReadIntoFifo(first, gen);
if(res != Result::Ok)
{
if(res == Result::DiskError)
Close();
return res;
}
continue;
}
if(request_fifo_.IsEmpty())
break;
auto req = request_fifo_.PopFront();
if(req.type != IoRequest::Type::Read)
continue;
auto res = ReadIntoFifo(req.data, gen);
if(res != Result::Ok)
{
if(res == Result::DiskError)
Close();
return res;
}
}
return Result::Ok;
}
Result Stream(float* samples, size_t num_channels)
{
auto channels = file_info_.channels;
for(size_t i = 0; i < num_channels; i++)
samples[i] = 0.f;
// immediately return if no file is open.
// could probably use a different result name that's more specific
if(!is_open_)
return Result::FileNotFoundError;
if(!buff_fifo_.IsEmpty() && playing_)
{
awaiting_first_data_ = false;
size_t ch_out = std::min(channels, num_channels);
for(size_t i = 0; i < ch_out; i++)
{
samples[i]
= previous_sample_[i]
+ pos_acc_ * (current_sample_[i] - previous_sample_[i]);
}
pos_acc_ += playback_speed_;
while(pos_acc_ >= 1.f)
{
position_ += 1;
pos_acc_ -= 1.f;
for(size_t i = 0; i < channels; i++)
{
previous_sample_[i] = current_sample_[i];
current_sample_[i] = s162f(buff_fifo_.PopFront());
}
}
}
if(position_ >= (file_info_.length > 0 ? file_info_.length : 1))
{
position_ = 0;
if(!looping_)
{
playing_ = false;
}
else
{
pos_acc_ = 0.f;
for(size_t i = 0; i < kMaxAudioChannels; i++)
previous_sample_[i] = current_sample_[i];
}
}
// Request new samples in whole frames
bool requested_new_samps = false;
if(buff_fifo_.GetNumElements() < kRxFifoThreshold && !pending_read_req_)
{
size_t free_slots = (kRxSizeSamples - buff_fifo_.GetNumElements());
size_t rx_qty = (free_slots > 1) ? (free_slots - 1) : 0;
// Align to multiples of channels
if(file_info_.channels > 0)
rx_qty -= (rx_qty % file_info_.channels);
if(rx_qty > 0)
{
request_fifo_.PushBack(
IoRequest(IoRequest::Type::Read, rx_qty));
pending_read_req_ = true;
requested_new_samps = true;
}
}
if(requested_new_samps)
return Result::NewSamplesRequested;
else if(buff_fifo_.IsEmpty() && playing_)
return awaiting_first_data_ ? Result::InitialLatency
: Result::PlaybackUnderrun;
else
return Result::Ok;
}
void Restart()
{
pos_acc_ = 0.f;
for(size_t i = 0; i < kMaxAudioChannels; i++)
current_sample_[i] = previous_sample_[i] = 0.f;
if(!is_open_)
return;
pending_read_req_ = true;
awaiting_first_data_ = true;
position_ = 0;
playing_ = true;
buff_fifo_.Clear();
// Restart generation increments will flag the `Prepare()` method to
// discard any stale data that is in progress.
restart_gen_ = restart_gen_ + 1;
}
inline size_t GetDurationInSamples() const
{
return file_info_.length > 0 ? file_info_.length : 1;
}
inline size_t GetChannels() const { return file_info_.channels; }
inline uint32_t GetPosition() const { return position_; }
inline float GetNormalizedPosition() const
{
size_t duration = GetDurationInSamples();
return static_cast<float>(position_) / static_cast<float>(duration);
}
inline void SetLooping(bool state) { looping_ = state; }
inline bool GetLooping() const { return looping_; }
inline void SetPlaying(bool state)
{
if(!is_open_)
return;
playing_ = state;
}
inline bool GetPlaying() const { return playing_; }
inline void SetPlaybackSpeedRatio(const float speed)
{
if(speed >= 0.f)
{
playback_speed_ = speed;
}
}
inline void SetPlaybackSpeedSemitones(const float semitones)
{
playback_speed_ = std::pow(2.f, semitones / 12.f);
}
private:
Result ReadIntoFifo(size_t samples, uint32_t gen)
{
size_t bytes_requested = samples * sizeof(int16_t);
// Align to full frames
bytes_requested -= (bytes_requested % frame_bytes_);
if(bytes_requested == 0)
{
if(gen == restart_gen_)
pending_read_req_ = false;
return Result::Ok;
}
UINT total_bytes_read = 0;
UINT bytes_read = 0;
std::fill(buff_raw_, buff_raw_ + kRxSizeSamples, 0);
// Read up to end of data chunk
size_t first_span = std::min(bytes_requested, bytes_left_in_chunk_);
// Align first_span to frame boundary as well
first_span -= (first_span % frame_bytes_);
if(first_span > 0)
{
if(f_read(&file_, (void*)buff_raw_, first_span, &bytes_read)
!= FR_OK)
return Result::DiskError;
if(bytes_read != first_span)
return Result::DiskError;
total_bytes_read += bytes_read;
bytes_left_in_chunk_ -= bytes_read;
}
// If need more and looping, wrap and read more (frame-aligned)
if(total_bytes_read < bytes_requested && looping_)
{
if(f_lseek(&file_, file_info_.data_start) != FR_OK)
return Result::DiskError;
size_t remaining_bytes = bytes_requested - total_bytes_read;
// Align remaining as well (it already is, but keep consistent)
remaining_bytes -= (remaining_bytes % frame_bytes_);
if(remaining_bytes > 0)
{
UINT bytes_read2 = 0;
char* tbuff = ((char*)(buff_raw_) + total_bytes_read);
// Do not exceed the chunk size on wrap
size_t span2
= std::min(remaining_bytes, file_info_.data_size_bytes);
// Align span2
span2 -= (span2 % frame_bytes_);
if(span2 > 0)
{
if(f_read(&file_, (void*)tbuff, span2, &bytes_read2)
!= FR_OK)
return Result::DiskError;
if(bytes_read2 != span2)
return Result::DiskError;
total_bytes_read += bytes_read2;
bytes_left_in_chunk_
= (bytes_read2 <= file_info_.data_size_bytes)
? (file_info_.data_size_bytes - bytes_read2)
: 0;
}
}
}
// Count total samples to write to FIFO
size_t samps_to_write = total_bytes_read / sizeof(int16_t);
if(file_info_.channels > 0)
samps_to_write -= (samps_to_write % file_info_.channels);
// Push samples to FIFO in chunks, disabling IRQs during each
// chunk. A Restart (from the audio callback) will cause the
// remaining data to be discarded to avoid unwanted, stale data.
constexpr size_t kPushChunk = 128;
size_t i = 0;
while(i < samps_to_write)
{
size_t end = std::min(i + kPushChunk, samps_to_write);
uint32_t primask = __get_PRIMASK();
__disable_irq();
if(gen != restart_gen_)
{
__set_PRIMASK(primask);
return Result::Ok; // stale: discard
}
bool full = false;
for(; i < end; i++)
{
if(!buff_fifo_.PushBack(buff_raw_[i]))
{
full = true;
break;
}
}
__set_PRIMASK(primask);
if(full)
{
pending_read_req_ = false;
return Result::PrepareOverrun;
}
}
pending_read_req_ = false;
return Result::Ok;
}
struct IoRequest
{
enum class Type
{
Read,
Seek,
Unknown,
};
Type type;
size_t data;
IoRequest(Type t, size_t val)
{
type = t;
data = val;
}
IoRequest() : type(Type::Unknown), data(0) {}
~IoRequest() {}
};
static const constexpr size_t kRxSizeSamples
= (workspace_bytes / sizeof(int16_t));
static const constexpr size_t kRxFifoThreshold = ((kRxSizeSamples / 4) * 3);
static const constexpr size_t kMaxAudioChannels = 8;
daisy::FIFO<IoRequest, 8> request_fifo_;
daisy::FIFO<int16_t, kRxSizeSamples> buff_fifo_;
int16_t buff_raw_[kRxSizeSamples];
size_t position_; //< position within audio data (in samples)
FileInfo file_info_; //< Info for the currently open file
bool looping_, playing_;
float playback_speed_;
float current_sample_[kMaxAudioChannels];
float previous_sample_[kMaxAudioChannels];
FIL file_;
bool is_open_;
bool pending_read_req_;
bool pending_seek_req_;
volatile bool awaiting_first_data_ = false;
volatile uint32_t restart_gen_ = 0; //< bumped by Restart() (audio context)
uint32_t handled_gen_ = 0; //< last generation serviced by Prepare()
float pos_acc_;
size_t bytes_left_in_chunk_; //< remaining bytes in WAV data chunk
size_t frame_bytes_; //< bytes per sample frame (channels*bytes-per-sample)
};
} // namespace daisy