diff --git a/src/WavEncoder.cpp b/src/WavEncoder.cpp index 6bc9281..888837a 100644 --- a/src/WavEncoder.cpp +++ b/src/WavEncoder.cpp @@ -28,7 +28,18 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. using namespace nqr; -inline void toBytes(int value, char * arr) +static inline void to_bytes(uint8_t value, char * arr) +{ + arr[0] = (value) & 0xFF; +} + +static inline void to_bytes(uint16_t value, char * arr) +{ + arr[0] = (value) & 0xFF; + arr[1] = (value >> 8) & 0xFF; +} + +static inline void to_bytes(uint32_t value, char * arr) { arr[0] = (value) & 0xFF; arr[1] = (value >> 8) & 0xFF; @@ -105,7 +116,7 @@ int WavEncoder::WriteFile(const EncoderParams p, const AudioData * d, const std: return EncoderError::BufferTooBig; } - // Don't support PCM_64 or PCM_DBL + // Don't support PC64 or PCDBL if (GetFormatBitsPerSample(p.targetFormat) > 32) { return EncoderError::UnsupportedBitdepth; @@ -121,7 +132,7 @@ int WavEncoder::WriteFile(const EncoderParams p, const AudioData * d, const std: char * chunkSizeBuff = new char[4]; // Initial size (this is changed after we're done writing the file) - toBytes(36, chunkSizeBuff); + to_bytes(uint32_t(36), chunkSizeBuff); // RIFF file header fout.write(GenerateChunkCodeChar('R', 'I', 'F', 'F'), 4); @@ -154,7 +165,7 @@ int WavEncoder::WriteFile(const EncoderParams p, const AudioData * d, const std: fout.write(GenerateChunkCodeChar('d', 'a', 't', 'a'), 4); // + data chunk size - toBytes(int(samplesSizeInBytes), chunkSizeBuff); + to_bytes(uint32_t(samplesSizeInBytes), chunkSizeBuff); fout.write(chunkSizeBuff, 4); if (targetBits <= sourceBits && p.targetFormat != PCM_FLT) @@ -166,7 +177,7 @@ int WavEncoder::WriteFile(const EncoderParams p, const AudioData * d, const std: } else { - // Handle PCM_FLT. Coming in from AudioData ensures we start with 32 bits, so we're fine + // Handle PCFLT. Coming in from AudioData ensures we start with 32 bits, so we're fine // since we've also rejected formats with more than 32 bits above. fout.write(reinterpret_cast(sampleData), samplesSizeInBytes); } @@ -185,7 +196,7 @@ int WavEncoder::WriteFile(const EncoderParams p, const AudioData * d, const std: fout.seekp(4); // Total size of the file, less 8 bytes for the RIFF header - toBytes(int(totalSize - 8), chunkSizeBuff); + to_bytes(uint32_t(totalSize - 8), chunkSizeBuff); fout.write(chunkSizeBuff, 4); @@ -199,19 +210,201 @@ int WavEncoder::WriteFile(const EncoderParams p, const AudioData * d, const std: //////////////////////////// #include "opus/opusfile/include/opusfile.h" +#include "ogg/ogg.h" + +typedef std::pair metadata_type; + +class OggWriter +{ + void write_to_ostream(bool flush) + { + while (ogg_stream_pageout(&oss, &page)) + { + ostream->write(reinterpret_cast(page.header), static_cast(page.header_len)); + ostream->write(reinterpret_cast(page.body), static_cast(page.body_len)); + } + + if (flush && ogg_stream_flush(&oss, &page)) + { + ostream->write(reinterpret_cast(page.header), static_cast(page.header_len)); + ostream->write(reinterpret_cast(page.body), static_cast(page.body_len)); + } + } + + std::vector make_header(int channel_count, int preskip, long sample_rate, int gain) + { + std::vector header; + + std::array streacount = {{ 0x0, 0x1, 0x1, 0x2, 0x2, 0x3, 0x4, 0x5, 0x5 }}; + std::array coupled_streacount = {{ 0x0, 0x0, 0x1, 0x1, 0x2, 0x2, 0x2, 0x2, 0x3 }}; + + std::array chan_map_1 = {{ 0x0 }}; + std::array chan_map_2 = {{ 0x0, 0x1 }}; + std::array chan_map_3 = {{ 0x0, 0x2, 0x1 }}; + std::array chan_map_4 = {{ 0x0, 0x1, 0x2, 0x3 }}; + std::array chan_map_5 = {{ 0x0, 0x4, 0x1, 0x2, 0x3 }}; + std::array chan_map_6 = {{ 0x0, 0x4, 0x1, 0x2, 0x3, 0x5 }}; + std::array chan_map_7 = {{ 0x0, 0x4, 0x1, 0x2, 0x3, 0x5, 0x6 }}; + std::array chan_map_8 = {{ 0x0, 0x6, 0x1, 0x2, 0x3, 0x4, 0x5, 0x7 }}; + + std::array _preamble = {{ 'O', 'p', 'u', 's', 'H', 'e', 'a', 'd', 0x1 }}; + std::array _channel_count; + std::array _preskip; + std::array _sample_rate; + std::array _gain; + std::array _channel_family = {{ 0x1 }}; + + to_bytes(uint8_t(channel_count), _channel_count.data()); + to_bytes(uint16_t(preskip), _channel_count.data()); + to_bytes(uint32_t(sample_rate), _channel_count.data()); + to_bytes(uint16_t(gain), _channel_count.data()); + + header.insert(header.end(), _preamble.cbegin(), _preamble.cend()); + header.insert(header.end(), _channel_count.cbegin(), _channel_count.cend()); + header.insert(header.end(), _preskip.cbegin(), _preskip.cend()); + header.insert(header.end(), _sample_rate.cbegin(), _sample_rate.cend()); + header.insert(header.end(), _gain.cbegin(), _gain.cend()); + header.insert(header.end(), _channel_family.cbegin(), _channel_family.cend()); + header.push_back(streacount[channel_count]); + header.push_back(coupled_streacount[channel_count]); + + switch (channel_count) + { + case 1: header.insert(header.end(), chan_map_1.cbegin(), chan_map_1.cend()); break; + case 2: header.insert(header.end(), chan_map_2.cbegin(), chan_map_2.cend()); break; + case 3: header.insert(header.end(), chan_map_3.cbegin(), chan_map_3.cend()); break; + case 4: header.insert(header.end(), chan_map_4.cbegin(), chan_map_4.cend()); break; + case 5: header.insert(header.end(), chan_map_5.cbegin(), chan_map_5.cend()); break; + case 6: header.insert(header.end(), chan_map_6.cbegin(), chan_map_6.cend()); break; + case 7: header.insert(header.end(), chan_map_7.cbegin(), chan_map_7.cend()); break; + case 8: header.insert(header.end(), chan_map_8.cbegin(), chan_map_8.cend()); break; + default: throw std::runtime_error("couldn't map channel data"); + } + + return header; + } + + std::vector make_tags(const std::string & vendor, const std::vector & metadata) + { + std::vector tags; + + std::array _preamble = {{ 'O', 'p', 'u', 's', 'T', 'a', 'g', 's' }}; + std::array _vendor_length; + std::array _metadata_count; + + to_bytes(uint32_t(vendor.size()), _vendor_length.data()); + to_bytes(uint32_t(metadata.size()), _metadata_count.data()); + + tags.insert(tags.end(), _preamble.cbegin() , _preamble.cend()); + tags.insert(tags.end(), _vendor_length.cbegin() , _vendor_length.cend()); + tags.insert(tags.end(), vendor.cbegin() , vendor.cend()); + tags.insert(tags.end(), _metadata_count.cbegin() , _metadata_count.cend()); + + // Process metadata. + for (const auto & metadata_entry : metadata) + { + std::array _metadata_entry_length; + std::string entry = metadata_entry.first + "=" + metadata_entry.second; + to_bytes(uint32_t(entry.size()), _metadata_entry_length.data()); + tags.insert(tags.end(), _metadata_entry_length.cbegin(), _metadata_entry_length.cend()); + tags.insert(tags.end(), entry.cbegin() , entry.cend()); + } + + return tags; + } + + ogg_int64_t packet_number; + ogg_int64_t granule; + ogg_page page; + ogg_stream_state oss; + + int channel_count; + long sample_rate; + int bits_per_sample; + std::ofstream * ostream; + std::string description; + std::vector metadata; + +public: + + OggWriter(int channel_count, long sample_rate, int bits_per_sample, std::ofstream & stream, const std::vector & metadata) + { + channel_count = channel_count; + sample_rate = sample_rate; + bits_per_sample = bits_per_sample; + ostream = &stream; + metadata = metadata; + + int oss_init_error; + int packet_write_error; + ogg_packet header_packet; + ogg_packet tags_packet; + std::vector header_vector; + std::vector tags_vector; + + description = "Ogg"; + packet_number = 0; + granule = 0; + + // Validate parameters + if (channel_count < 1 && channel_count > 255) + throw std::runtime_error("Channel count must be between 1 and 255."); + + // Initialize the Ogg stream. + oss_init_error = ogg_stream_init(&oss, 12345); + if (oss_init_error) throw std::runtime_error("Could not initialize the Ogg stream state."); + + // Initialize the header packet. + header_vector = make_header(channel_count, 3840, sample_rate, 0); + header_packet.packet = reinterpret_cast(header_vector.data()); + header_packet.bytes = header_vector.size(); + header_packet.b_o_s = 1; + header_packet.e_o_s = 0; + header_packet.granulepos = 0; + header_packet.packetno = packet_number++; + + // Initialize tags + tags_vector = make_tags("libnyquist", metadata); + tags_packet.packet = reinterpret_cast(tags_vector.data()); + tags_packet.bytes = tags_vector.size(); + tags_packet.b_o_s = 0; + tags_packet.e_o_s = 0; + tags_packet.granulepos = 0; + tags_packet.packetno = packet_number++; + + // Write header page + packet_write_error = ogg_stream_packetin(&oss, &header_packet); + if (packet_write_error) throw std::runtime_error("Could not write header packet to the Ogg stream."); + write_to_ostream(true); + + // Write tags page + packet_write_error = ogg_stream_packetin(&oss, &tags_packet); + if (packet_write_error) throw std::runtime_error("Could not write tags packet to the Ogg stream."); + write_to_ostream(true); + } + + ~OggWriter() + { + write_to_ostream(true); + ogg_stream_clear(&oss); + } + + bool write(char * data, std::streamsize length, size_t sample_count, bool end); + +}; // Opus only supports a 48k samplerate... int OggOpusEncoder::WriteFile(const EncoderParams p, const AudioData * d, const std::string & path) { - assert(d->samples.size() > 0); + assert(d->samples.size() > 0); - OpusEncoder * enc; - enc = opus_encoder_create(48000, 1, OPUS_APPLICATION_AUDIO, nullptr); + OpusEncoder * enc; + enc = opus_encoder_create(48000, 1, OPUS_APPLICATION_AUDIO, nullptr); - // How big should this be? - std::vector encodedBuffer(1024); + // How big should this be? + std::vector encodedBuffer(1024); - auto encoded_size = opus_encode_float(enc, d->samples.data(), d->frameSize, encodedBuffer.data(), encodedBuffer.size()); + auto encoded_size = opus_encode_float(enc, d->samples.data(), d->frameSize, encodedBuffer.data(), encodedBuffer.size()); // Cast away const because we know what we are doing (Hopefully?) float * sampleData = const_cast(d->samples.data());