// Copyright (c) Charles J. Cliffe // SPDX-License-Identifier: GPL-2.0+ #include "AudioFileWAV.h" #include "CubicSDR.h" #include //limit file size to 2GB (- margin) for maximum compatibility. #define MAX_WAV_FILE_SIZE (0x7FFFFFFF - 1024) // Simple endian io read/write handling from // http://www.cplusplus.com/forum/beginner/31584/#msg171056 namespace little_endian_io { template std::ostream& write_word(std::ostream& outs, Word value, unsigned size = sizeof(Word)) { for (; size; --size, value >>= 8) { outs.put(static_cast (value & 0xFF)); } return outs; } template std::istream& read_word(std::istream& ins, Word& value, unsigned size = sizeof(Word)) { value = 0; for (unsigned n = 0; n < size; ++n) { value |= ins.get() << (8 * n); } return ins; } } namespace big_endian_io { template std::ostream& write_word(std::ostream& outs, Word value, unsigned size = sizeof(Word)) { while (size) { outs.put(static_cast ((value >> (8 * --size)) & 0xFF)); } return outs; } template std::istream& read_word(std::istream& ins, Word& value, unsigned size = sizeof(Word)) { for (value = 0; size; --size) { value = (value << 8) | ins.get(); } return ins; } } using namespace little_endian_io; AudioFileWAV::AudioFileWAV() : AudioFile(), dataChunkPos(0) { } AudioFileWAV::~AudioFileWAV() = default; std::string AudioFileWAV::getExtension() { return "wav"; } bool AudioFileWAV::writeToFile(AudioThreadInputPtr input) { if (!outputFileStream.is_open()) { std::string ofName = getOutputFileName(); outputFileStream.open(ofName.c_str(), std::ios::binary); currentFileSize = 0; writeHeaderToFileStream(input); } size_t maxRoomInCurrentFileInSamples = getMaxWritableNumberOfSamples(input); if (maxRoomInCurrentFileInSamples >= input->data.size()) { writePayloadToFileStream(input, 0, input->data.size()); } else { //we complete the current file and open another: writePayloadToFileStream(input, 0, maxRoomInCurrentFileInSamples); closeFile(); // Open a new file with the next sequence number, and dump the rest of samples in it. currentSequenceNumber++; currentFileSize = 0; std::string ofName = getOutputFileName(); outputFileStream.open(ofName.c_str(), std::ios::binary); writeHeaderToFileStream(input); writePayloadToFileStream(input, maxRoomInCurrentFileInSamples, input->data.size()); } return true; } bool AudioFileWAV::closeFile() { if (outputFileStream.is_open()) { size_t file_length = outputFileStream.tellp(); // Fix the data chunk header to contain the data size outputFileStream.seekp(dataChunkPos + 4); write_word(outputFileStream, file_length - (dataChunkPos + 8), 4); // Fix the file header to contain the proper RIFF chunk size, which is (file size - 8) bytes outputFileStream.seekp(0 + 4); write_word(outputFileStream, file_length - 8, 4); outputFileStream.close(); currentFileSize = 0; } return true; } void AudioFileWAV::writeHeaderToFileStream(const AudioThreadInputPtr& input) { // Based on simple wav file output code from // http://www.cplusplus.com/forum/beginner/166954/ // Write the wav file headers outputFileStream << "RIFF----WAVEfmt "; // (chunk size to be filled in later) write_word(outputFileStream, 16, 4); // no extension data write_word(outputFileStream, 1, 2); // PCM - integer samples write_word(outputFileStream, input->channels, 2); // channels write_word(outputFileStream, input->sampleRate, 4); // samples per second (Hz) write_word(outputFileStream, (input->sampleRate * 16 * input->channels) / 8, 4); // (Sample Rate * BitsPerSample * Channels) / 8 write_word(outputFileStream, input->channels * 2, 2); // data block size (size of integer samples, one for each channel, in bytes) write_word(outputFileStream, 16, 2); // number of bits per sample (use a multiple of 8) // Write the data chunk header dataChunkPos = outputFileStream.tellp(); currentFileSize = dataChunkPos; outputFileStream << "data----"; // (chunk size to be filled in later) } void AudioFileWAV::writePayloadToFileStream(const AudioThreadInputPtr& input, size_t startInputPosition, size_t endInputPosition) { // Prevent clipping float intScale = (input->peak < 1.0) ? 32767.0f : (32767.0f / input->peak); if (input->channels == 1) { for (size_t i = startInputPosition, iMax = endInputPosition; i < iMax; i++) { write_word(outputFileStream, int(input->data[i] * intScale), 2); currentFileSize += 2; } } else if (input->channels == 2) { for (size_t i = startInputPosition, iMax = endInputPosition / 2; i < iMax; i++) { write_word(outputFileStream, int(input->data[i * 2] * intScale), 2); write_word(outputFileStream, int(input->data[i * 2 + 1] * intScale), 2); currentFileSize += 4; } } } size_t AudioFileWAV::getMaxWritableNumberOfSamples(const AudioThreadInputPtr& input) const { long long remainingBytesInFile = (long long)(MAX_WAV_FILE_SIZE) - currentFileSize; return (size_t)(remainingBytesInFile / (input->channels * 2)); } void AudioFileWAV::setOutputFileName(std::string filename) { if (filename != filenameBase) { currentSequenceNumber = 0; } AudioFile::setOutputFileName(filename); } std::string AudioFileWAV::getOutputFileName() { std::string recPath = wxGetApp().getConfig()->getRecordingPath(); // Strip any invalid characters from the name std::string stripChars("<>:\"/\\|?*"); std::string filenameBaseSafe = filenameBase; for (size_t i = 0, iMax = filenameBaseSafe.length(); i < iMax; i++) { if (stripChars.find(filenameBaseSafe[i]) != std::string::npos) { filenameBaseSafe.replace(i, 1, "_"); } } // Create output file name std::stringstream outputFileName; outputFileName << recPath << filePathSeparator << filenameBaseSafe; //customized part: append a sequence number. if (currentSequenceNumber > 0) { outputFileName << "_" << std::setfill('0') << std::setw(3) << currentSequenceNumber; } int idx = 0; // If the file exists; then find the next non-existing file in sequence. std::string fileNameCandidate = outputFileName.str(); while (FILE *file = fopen((fileNameCandidate + "." + getExtension()).c_str(), "r")) { fclose(file); fileNameCandidate = outputFileName.str() + "-" + std::to_string(++idx); } return fileNameCandidate + "." + getExtension(); }