diff --git a/misc_modules/recorder/src/main.cpp b/misc_modules/recorder/src/main.cpp index eee452c3..25287518 100644 --- a/misc_modules/recorder/src/main.cpp +++ b/misc_modules/recorder/src/main.cpp @@ -43,13 +43,14 @@ public: // Define option lists formats.define("WAV", wav::FORMAT_WAV); // formats.define("RF64", wav::FORMAT_RF64); // Disabled for now - sampleDepths.define(wav::SAMP_DEPTH_8BIT, "8-Bit", wav::SAMP_DEPTH_8BIT); - sampleDepths.define(wav::SAMP_DEPTH_16BIT, "16-Bit", wav::SAMP_DEPTH_16BIT); - sampleDepths.define(wav::SAMP_DEPTH_32BIT, "32-Bit", wav::SAMP_DEPTH_32BIT); + sampleTypes.define(wav::SAMP_TYPE_UINT8, "Uint8", wav::SAMP_TYPE_UINT8); + sampleTypes.define(wav::SAMP_TYPE_INT16, "Int16", wav::SAMP_TYPE_INT16); + sampleTypes.define(wav::SAMP_TYPE_INT32, "Int32", wav::SAMP_TYPE_INT32); + sampleTypes.define(wav::SAMP_TYPE_FLOAT32, "Float32", wav::SAMP_TYPE_FLOAT32); // Load default config for option lists formatId = formats.valueId(wav::FORMAT_WAV); - sampleDepthId = sampleDepths.valueId(wav::SAMP_DEPTH_16BIT); + sampleTypeId = sampleTypes.valueId(wav::SAMP_TYPE_INT16); // Load config config.acquire(); @@ -62,8 +63,8 @@ public: if (config.conf[name].contains("format") && formats.keyExists(config.conf[name]["format"])) { formatId = formats.keyId(config.conf[name]["format"]); } - if (config.conf[name].contains("sampleDepth") && sampleDepths.keyExists(config.conf[name]["sampleDepth"])) { - sampleDepthId = sampleDepths.keyId(config.conf[name]["sampleDepth"]); + if (config.conf[name].contains("sampleType") && sampleTypes.keyExists(config.conf[name]["sampleType"])) { + sampleTypeId = sampleTypes.keyId(config.conf[name]["sampleType"]); } if (config.conf[name].contains("audioStream")) { selectedStreamName = config.conf[name]["audioStream"]; @@ -143,7 +144,7 @@ public: } writer.setFormat(formats[formatId]); writer.setChannels((recMode == RECORDER_MODE_AUDIO && !stereo) ? 1 : 2); - writer.setSampleDepth(sampleDepths[sampleDepthId]); + writer.setSampleType(sampleTypes[sampleTypeId]); writer.setSamplerate(samplerate); // Open file @@ -238,11 +239,11 @@ private: config.release(true); } - ImGui::LeftLabel("Sample depth"); + ImGui::LeftLabel("Sample type"); ImGui::FillWidth(); - if (ImGui::Combo(CONCAT("##_recorder_bits_", _this->name), &_this->sampleDepthId, _this->sampleDepths.txt)) { + if (ImGui::Combo(CONCAT("##_recorder_st_", _this->name), &_this->sampleTypeId, _this->sampleTypes.txt)) { config.acquire(); - config.conf[_this->name]["sampleDepth"] = _this->sampleDepths.key(_this->sampleDepthId); + config.conf[_this->name]["sampleType"] = _this->sampleTypes.key(_this->sampleTypeId); config.release(true); } @@ -431,12 +432,12 @@ private: std::string root; OptionList formats; - OptionList sampleDepths; + OptionList sampleTypes; FolderSelect folderSelect; int recMode = RECORDER_MODE_AUDIO; int formatId; - int sampleDepthId; + int sampleTypeId; bool stereo = true; std::string selectedStreamName = ""; float audioVolume = 1.0f; diff --git a/misc_modules/recorder/src/riff.cpp b/misc_modules/recorder/src/riff.cpp new file mode 100644 index 00000000..4c1fab75 --- /dev/null +++ b/misc_modules/recorder/src/riff.cpp @@ -0,0 +1,86 @@ +#include "riff.h" +#include + +namespace riff { + bool Writer::open(std::string path, char form[4]) { + // TODO: Open file + + beginRIFF(form); + + return false; + } + + bool Writer::isOpen() { + + return false; + } + + void Writer::close() { + if (!isOpen()) { return; } + + endRIFF(); + + file.close(); + } + + void Writer::beginChunk(char id[4]) { + // Create and write header + ChunkDesc desc; + desc.pos = file.tellp(); + memcpy(desc.hdr.id, id, sizeof(desc.hdr.id)); + desc.hdr.size = 0; + file.write((char*)&desc.hdr, sizeof(ChunkHeader)); + + // Save descriptor + chunks.push(desc); + } + + void Writer::endChunk() { + if (!chunks.empty()) { + throw std::runtime_error("No chunk to end"); + } + + // Get descriptor + ChunkDesc desc = chunks.top(); + chunks.pop(); + + // Write size + auto pos = file.tellp(); + file.seekp(desc.pos + 4); + file.write((char*)&desc.hdr.size, sizeof(desc.hdr.size)); + file.seekp(pos); + + // If parent chunk, increment its size + if (!chunks.empty()) { + chunks.top().hdr.size += desc.hdr.size; + } + } + + void Writer::write(void* data, size_t len) { + if (!chunks.empty()) { + throw std::runtime_error("No chunk to write into"); + } + file.write((char*)data, len); + chunks.top().hdr.size += len; + } + + void Writer::beginRIFF(char form[4]) { + if (!chunks.empty()) { + throw std::runtime_error("Can't create RIFF chunk on an existing RIFF file"); + } + + // Create chunk with RIFF ID and write form + beginChunk("RIFF"); + write(form, sizeof(form)); + } + + void Writer::endRIFF() { + if (!chunks.empty()) { + throw std::runtime_error("No chunk to end"); + } + if (memcmp(chunks.top().hdr.id, "RIFF", 4)) { + throw std::runtime_error("Top chunk not RIFF chunk"); + } + endChunk(); + } +} \ No newline at end of file diff --git a/misc_modules/recorder/src/riff.h b/misc_modules/recorder/src/riff.h new file mode 100644 index 00000000..6bfbaaf3 --- /dev/null +++ b/misc_modules/recorder/src/riff.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include + +namespace riff { +#pragma pack(push, 1) + struct ChunkHeader { + char id[4]; + uint32_t size; + }; +#pragma pack(pop) + + struct ChunkDesc { + ChunkHeader hdr; + std::streampos pos; + }; + + class Writer { + public: + bool open(std::string path, char form[4]); + bool isOpen(); + void close(); + + void beginList(); + void endList(); + + void beginChunk(char id[4]); + void endChunk(); + + void write(void* data, size_t len); + + private: + void beginRIFF(char form[4]); + void endRIFF(); + + std::ofstream file; + std::stack chunks; + }; +} \ No newline at end of file diff --git a/misc_modules/recorder/src/wav.cpp b/misc_modules/recorder/src/wav.cpp index 2cd47243..5a6a38a1 100644 --- a/misc_modules/recorder/src/wav.cpp +++ b/misc_modules/recorder/src/wav.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace wav { const char* RIFF_SIGNATURE = "RIFF"; @@ -11,8 +12,15 @@ namespace wav { const char* DATA_MARKER = "data"; const uint32_t FORMAT_HEADER_LEN = 16; const uint16_t SAMPLE_TYPE_PCM = 1; + + std::map SAMP_BITS = { + { SAMP_TYPE_UINT8, 8 }, + { SAMP_TYPE_INT16, 16 }, + { SAMP_TYPE_INT32, 32 }, + { SAMP_TYPE_FLOAT32, 32 } + }; - Writer::Writer(int channels, uint64_t samplerate, Format format, SampleDepth depth) { + Writer::Writer(int channels, uint64_t samplerate, Format format, SampleType type) { // Validate channels and samplerate if (channels < 1) { throw std::runtime_error("Channel count must be greater or equal to 1"); } if (!samplerate) { throw std::runtime_error("Samplerate must be non-zero"); } @@ -21,14 +29,13 @@ namespace wav { _channels = channels; _samplerate = samplerate; _format = format; - _depth = depth; + _type = type; // Initialize header with constants memcpy(hdr.signature, RIFF_SIGNATURE, 4); memcpy(hdr.fileType, WAVE_FILE_TYPE, 4); memcpy(hdr.formatMarker, FORMAT_MARKER, 4); hdr.formatHeaderLength = FORMAT_HEADER_LEN; - hdr.sampleType = SAMPLE_TYPE_PCM; memcpy(hdr.dataMarker, DATA_MARKER, 4); } @@ -43,16 +50,19 @@ namespace wav { samplesWritten = 0; // Precompute sizes and allocate buffers - bytesPerSamp = (_depth / 8) * _channels; - switch (_depth) { - case SAMP_DEPTH_8BIT: - buf8 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + // TODO: Get number of bits for each sample type + bytesPerSamp = (SAMP_BITS[_type] / 8) * _channels; // THIS IS WRONG + switch (_type) { + case SAMP_TYPE_UINT8: + bufU8 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); break; - case SAMP_DEPTH_16BIT: - buf16 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + case SAMP_TYPE_INT16: + bufI16 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); break; - case SAMP_DEPTH_32BIT: - buf32 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + case SAMP_TYPE_INT32: + bufI32 = dsp::buffer::alloc(STREAM_BUFFER_SIZE * _channels); + break; + case SAMP_TYPE_FLOAT32: break; default: return false; @@ -89,18 +99,17 @@ namespace wav { file.close(); // Destroy buffers - switch (_depth) { - case SAMP_DEPTH_8BIT: - dsp::buffer::free(buf8); - break; - case SAMP_DEPTH_16BIT: - dsp::buffer::free(buf16); - break; - case SAMP_DEPTH_32BIT: - dsp::buffer::free(buf32); - break; - default: - break; + if (bufU8) { + dsp::buffer::free(bufU8); + bufU8 = NULL; + } + if (bufI16) { + dsp::buffer::free(bufI16); + bufI16 = NULL; + } + if (bufI32) { + dsp::buffer::free(bufI32); + bufI32 = NULL; } // Mark as closed @@ -133,11 +142,11 @@ namespace wav { _format = format; } - void Writer::setSampleDepth(SampleDepth depth) { + void Writer::setSampleType(SampleType type) { std::lock_guard lck(mtx); // Do not allow settings to change while open if (_isOpen) { throw std::runtime_error("Cannot change parameters while file is open"); } - _depth = depth; + _type = type; } void Writer::write(float* samples, int count) { @@ -146,22 +155,25 @@ namespace wav { // Select different writer function depending on the chose depth int tcount; - switch (_depth) { - case SAMP_DEPTH_8BIT: + switch (_type) { + case SAMP_TYPE_UINT8: tcount = count * _channels; // Volk doesn't support unsigned ints :/ for (int i = 0; i < tcount; i++) { - buf8[i] = (samples[i] * 127.0f) + 128.0f; + bufU8[i] = (samples[i] * 127.0f) + 128.0f; } - file.write((char*)buf8, count * bytesPerSamp); + file.write((char*)bufU8, count * bytesPerSamp); break; - case SAMP_DEPTH_16BIT: - volk_32f_s32f_convert_16i(buf16, samples, 32767.0f, count * _channels); - file.write((char*)buf16, count * bytesPerSamp); + case SAMP_TYPE_INT16: + volk_32f_s32f_convert_16i(bufI16, samples, 32767.0f, count * _channels); + file.write((char*)bufI16, count * bytesPerSamp); break; - case SAMP_DEPTH_32BIT: - volk_32f_s32f_convert_32i(buf32, samples, 2147483647.0f, count * _channels); - file.write((char*)buf32, count * bytesPerSamp); + case SAMP_TYPE_INT32: + volk_32f_s32f_convert_32i(bufI32, samples, 2147483647.0f, count * _channels); + file.write((char*)bufI32, count * bytesPerSamp); + break; + case SAMP_TYPE_FLOAT32: + file.write((char*)samples, count * bytesPerSamp); break; default: break; @@ -173,9 +185,10 @@ namespace wav { void Writer::finalize() { file.seekp(file.beg); + hdr.codec = (_type == SAMP_TYPE_FLOAT32) ? CODEC_FLOAT : CODEC_PCM; hdr.channelCount = _channels; hdr.sampleRate = _samplerate; - hdr.bitDepth = _depth; + hdr.bitDepth = SAMP_BITS[_type]; hdr.bytesPerSample = bytesPerSamp; hdr.bytesPerSecond = bytesPerSamp * _samplerate; hdr.dataSize = samplesWritten * bytesPerSamp; diff --git a/misc_modules/recorder/src/wav.h b/misc_modules/recorder/src/wav.h index 7823b5d5..f18e7097 100644 --- a/misc_modules/recorder/src/wav.h +++ b/misc_modules/recorder/src/wav.h @@ -12,7 +12,7 @@ namespace wav { char fileType[4]; // "WAVE" char formatMarker[4]; // "fmt " uint32_t formatHeaderLength; // Always 16 - uint16_t sampleType; // PCM (1) + uint16_t codec; // PCM (1) uint16_t channelCount; uint32_t sampleRate; uint32_t bytesPerSecond; @@ -28,15 +28,21 @@ namespace wav { FORMAT_RF64 }; - enum SampleDepth { - SAMP_DEPTH_8BIT = 8, - SAMP_DEPTH_16BIT = 16, - SAMP_DEPTH_32BIT = 32 + enum SampleType { + SAMP_TYPE_UINT8, + SAMP_TYPE_INT16, + SAMP_TYPE_INT32, + SAMP_TYPE_FLOAT32 + }; + + enum Codec { + CODEC_PCM = 1, + CODEC_FLOAT = 3 }; class Writer { public: - Writer(int channels = 2, uint64_t samplerate = 48000, Format format = FORMAT_WAV, SampleDepth depth = SAMP_DEPTH_16BIT); + Writer(int channels = 2, uint64_t samplerate = 48000, Format format = FORMAT_WAV, SampleType type = SAMP_TYPE_INT16); ~Writer(); bool open(std::string path); @@ -46,7 +52,7 @@ namespace wav { void setChannels(int channels); void setSamplerate(uint64_t samplerate); void setFormat(Format format); - void setSampleDepth(SampleDepth depth); + void setSampleType(SampleType type); size_t getSamplesWritten() { return samplesWritten; } @@ -63,12 +69,12 @@ namespace wav { int _channels; uint64_t _samplerate; Format _format; - SampleDepth _depth; + SampleType _type; size_t bytesPerSamp; - int8_t* buf8; - int16_t* buf16; - int32_t* buf32; + uint8_t* bufU8 = NULL; + int16_t* bufI16 = NULL; + int32_t* bufI32 = NULL; size_t samplesWritten = 0; }; }