Skip to content

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