Skip to content

File WavWriter.h

File List > external-docs > libDaisy > src > util > WavWriter.h

Go to the documentation of this file

Source Code

#pragma once
#pragma once
#include "fatfs.h"
#include "util/wav_format.h"

namespace daisy
{
template <size_t transfer_size>
class WavWriter
{
  public:
    WavWriter() {}
    ~WavWriter() {}

    enum class Result
    {
        OK,
        ERROR,
    };

    struct Config
    {
        float   samplerate;
        int32_t channels;
        int32_t bitspersample;
    };

    enum class BufferState
    {
        IDLE,
        FLUSH0,
        FLUSH1,
    };

    void Init(const Config &cfg)
    {
        cfg_       = cfg;
        num_samps_ = 0;
        wptr_      = 0;
        bstate_    = BufferState::IDLE;
        recording_ = false;
        memset(transfer_buff, 0, sizeof(transfer_buff));
        // Prep the wav header according to config.
        // Certain things (i.e. Size, etc. will have to wait until the finalization of the file, or be updated while streaming).
        wavheader_.ChunkId       = kWavFileChunkId;     
        wavheader_.FileFormat    = kWavFileWaveId;      
        wavheader_.SubChunk1ID   = kWavFileSubChunk1Id; 
        wavheader_.SubChunk1Size = 16;                  // for PCM
        wavheader_.AudioFormat   = WAVE_FORMAT_PCM;
        wavheader_.NbrChannels   = cfg.channels;
        wavheader_.SampleRate    = static_cast<int>(cfg.samplerate);
        wavheader_.ByteRate      = CalcByteRate();
        wavheader_.BlockAlign    = cfg_.channels * cfg_.bitspersample / 8;
        wavheader_.BitPerSample  = cfg_.bitspersample;
        wavheader_.SubChunk2ID   = kWavFileSubChunk2Id; 
        wavheader_.FileSize = CalcFileSize();
        // This is calculated as part of the subchunk size
    }

    void Sample(const float *in)
    {
        for(int i = 0; i < cfg_.channels; i++)
        {
            switch(cfg_.bitspersample)
            {
                case 16:
                {
                    int16_t *tp;
                    tp            = (int16_t *)transfer_buff;
                    tp[wptr_ + i] = f2s16(in[i]);
                }
                break;
                case 32: transfer_buff[wptr_ + i] = f2s32(in[i]); break;
                default: break;
            }
        }
        num_samps_++;
        wptr_ += cfg_.channels;
        size_t cap_point
            = cfg_.bitspersample == 16 ? kTransferSamps * 2 : kTransferSamps;
        if(wptr_ == cap_point)
        {
            bstate_ = BufferState::FLUSH0;
        }
        if(wptr_ >= cap_point * 2)
        {
            wptr_   = 0;
            bstate_ = BufferState::FLUSH1;
        }
    }

    void Write()
    {
        if(bstate_ != BufferState::IDLE && IsRecording())
        {
            uint32_t     offset;
            unsigned int bw = 0;
            //offset          = bstate_ == BufferState::FLUSH0 ? 0 : transfer_size;
            offset  = bstate_ == BufferState::FLUSH0 ? 0 : kTransferSamps;
            bstate_ = BufferState::IDLE;
            f_write(&fp_, &transfer_buff[offset], transfer_size, &bw);
        }
    }

    void SaveFile()
    {
        unsigned int bw = 0;

        // 1. Flush any pending full half-buffer first
        Write();

        // 2. Flush remaining partial data (if any)
        if(wptr_ > 0)
        {
            uint32_t bytes_per_sample = cfg_.bitspersample / 8;
            uint32_t cap_point = cfg_.bitspersample == 16 ? kTransferSamps * 2
                                                          : kTransferSamps;

            uint32_t offset_samples = 0;
            uint32_t local_wptr     = wptr_;

            // If we've crossed cap_point, the partial data sits in the 2nd half
            if(local_wptr >= cap_point)
            {
                offset_samples = cap_point;
                local_wptr -= cap_point;
            }

            uint32_t offset_bytes   = offset_samples * bytes_per_sample;
            uint32_t remaining_size = local_wptr * bytes_per_sample;

            // Clamp for safety
            uint32_t max_bytes = sizeof(transfer_buff) - offset_bytes;
            if(remaining_size > max_bytes)
                remaining_size = max_bytes;

            uint8_t *base = reinterpret_cast<uint8_t *>(transfer_buff);
            f_write(&fp_, base + offset_bytes, remaining_size, &bw);
        }

        // 3. Finalize header and close
        recording_          = false;
        wavheader_.FileSize = CalcFileSize();
        f_lseek(&fp_, 0);
        f_write(&fp_, &wavheader_, sizeof(wavheader_), &bw);
        f_close(&fp_);

        // 4. Reset internal state
        memset(transfer_buff, 0, sizeof(transfer_buff));
        bstate_    = BufferState::IDLE;
        wptr_      = 0;
        num_samps_ = 0;
        recording_ = false;
    }

    void OpenFile(const char *name)
    {
        if(f_open(&fp_, name, FA_WRITE | FA_CREATE_ALWAYS) == FR_OK)
        {
            unsigned int bw = 0;
            if(f_write(&fp_, &wavheader_, sizeof(wavheader_), &bw) == FR_OK)
            {
                recording_ = true;
                num_samps_ = 0;
                wptr_      = 0;
                bstate_    = BufferState::IDLE;
                memset(transfer_buff, 0, sizeof(transfer_buff));
            }
        }
    }

    inline bool IsRecording() const { return recording_; }

    inline uint32_t GetLengthSamps() { return num_samps_; }

    inline float GetLengthSeconds()
    {
        return (float)num_samps_ / (float)cfg_.samplerate;
    }

  private:
    inline uint32_t CalcFileSize()
    {
        wavheader_.SubCHunk2Size
            = num_samps_ * cfg_.channels * cfg_.bitspersample / 8;
        return 36 + wavheader_.SubCHunk2Size;
    }

    inline uint32_t CalcByteRate()
    {
        return cfg_.samplerate * cfg_.channels * cfg_.bitspersample / 8;
    }

    static constexpr int kTransferSamps = transfer_size / sizeof(int32_t);

    WAV_FormatTypeDef wavheader_;
    uint32_t          num_samps_, wptr_;
    Config            cfg_;
    int32_t           transfer_buff[kTransferSamps * 2];
    BufferState       bstate_;
    bool              recording_;
    FIL               fp_;
};

} // namespace daisy