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,
};
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;
pending_seek_req_ = false;
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()
{
while(!request_fifo_.IsEmpty())
{
auto req = request_fifo_.PopFront();
switch(req.type)
{
case IoRequest::Type::Read:
{
size_t bytes_requested = req.data * sizeof(int16_t);
// Align to full frames
bytes_requested -= (bytes_requested % frame_bytes_);
if(bytes_requested == 0)
{
pending_read_req_ = false;
break;
}
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;
}
}
}
// Push into FIFO; align to full frames
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);
for(size_t i = 0; i < samps_to_write; i++)
{
if(!buff_fifo_.PushBack(buff_raw_[i]))
{
pending_read_req_ = false;
return Result::PrepareOverrun;
}
}
pending_read_req_ = false;
}
break;
case IoRequest::Type::Seek:
{
size_t dest_bytes = req.data * sizeof(int16_t);
// Clamp and align to frame boundary
if(dest_bytes > file_info_.data_size_bytes)
dest_bytes = file_info_.data_size_bytes;
dest_bytes -= (dest_bytes % frame_bytes_);
if(f_lseek(&file_, file_info_.data_start + dest_bytes)
!= FR_OK)
return Result::DiskError;
bytes_left_in_chunk_
= file_info_.data_size_bytes - dest_bytes;
pending_seek_req_ = false;
}
break;
default: break;
}
}
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;
if(!buff_fifo_.IsEmpty() && playing_)
{
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 Result::PlaybackUnderrun;
else
return Result::Ok;
}
void Restart()
{
buff_fifo_.Clear();
request_fifo_.Clear();
pos_acc_ = 0.f;
for(size_t i = 0; i < kMaxAudioChannels; i++)
current_sample_[i] = previous_sample_[i] = 0.f;
bytes_left_in_chunk_ = file_info_.data_size_bytes;
request_fifo_.PushBack(IoRequest(IoRequest::Type::Seek, 0));
// Request a frame-aligned quantity
size_t req_samps = kRxSizeSamples;
if(file_info_.channels > 0)
req_samps -= (req_samps % file_info_.channels);
if(req_samps > 0)
request_fifo_.PushBack(IoRequest(IoRequest::Type::Read, req_samps));
pending_read_req_ = true;
pending_seek_req_ = true;
position_ = 0;
playing_ = true;
}
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) { 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:
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_;
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