#pragma once #include #include #include #include #include #include "Concepts/AudioWriterConcept.h" #include namespace snoop { class OggAudioWriter { std::ofstream m_outputFile; ogg_stream_state m_oggStream = {}; bool m_isInWriting = false; long m_packetNumber = 0; int64_t m_granulePos = 0; int m_sampleRate; int m_channels; std::mutex m_mutex; public: OggAudioWriter( int sampleRate, int channels ) : m_sampleRate( sampleRate ), m_channels( channels ) { } ~OggAudioWriter() { this->StopWriting(); } void StartWriting( const std::string& filePath ) { std::lock_guard l( this->m_mutex ); StartWritingInternal( filePath ); } void StopWriting() { std::lock_guard l( this->m_mutex ); StopWritingInternal(); } void Write( const char* data, size_t size, size_t frames ) { std::lock_guard l( this->m_mutex ); WriteInternal( data, size, frames ); } private: void StartWritingInternal( std::string filePath ) { if( this->m_isInWriting ) { this->StopWritingInternal(); } m_isInWriting = true; this->m_packetNumber = 0; this->m_granulePos = 0; this->m_oggStream = {}; this->m_outputFile = {}; ogg_stream_init( &this->m_oggStream, 1 ); this->m_outputFile.open( filePath, std::ios::binary ); if( !this->m_outputFile.is_open() ) { throw std::runtime_error( "Failed to open file for writing: " + filePath ); } this->WriteOpusHeader(); this->WriteOpusTags(); spdlog::info( "Started writing Ogg audio to {}", filePath ); } void StopWritingInternal() { if( !m_isInWriting ) { return; } if( this->m_outputFile.is_open() ) { ogg_packet packet = {}; packet.e_o_s = 1; packet.granulepos = this->m_granulePos; packet.packetno = this->m_packetNumber++; ogg_stream_packetin( &m_oggStream, &packet ); this->WriteOggPages( true ); this->m_outputFile.close(); spdlog::info( "Stopped writing Ogg audio" ); } ogg_stream_clear( &this->m_oggStream ); this->m_isInWriting = false; } void WriteInternal( const char* data, size_t size, size_t frames ) { if( !this->m_isInWriting ) { return; } ogg_packet packet; packet.packet = reinterpret_cast(const_cast(data)); packet.bytes = static_cast(size); packet.b_o_s = this->m_packetNumber == 0 ? 1 : 0; packet.e_o_s = 0; packet.granulepos = this->m_granulePos += static_cast(frames) * m_channels; packet.packetno = this->m_packetNumber++; ogg_stream_packetin( &this->m_oggStream, &packet ); this->WriteOggPages(); } void WriteOpusHeader() { static constexpr char magic[ ] = "OpusHead"; unsigned char header[ 19 ] = { 0 }; memcpy( header, reinterpret_cast(magic), 8 ); header[ 8 ] = 1; // version header[ 9 ] = m_channels; *reinterpret_cast( &header[ 12 ] ) = m_sampleRate; this->WriteInternal( reinterpret_cast(header), sizeof( header ), 0 ); } void WriteOpusTags() { constexpr char tags[ ] = "OpusTags\0\0\0\0\0\0\0\0"; this->WriteInternal( tags, sizeof( tags ) - 1, 0 ); } void WriteOggPages( bool flush = false ) { ogg_page page; while( ogg_stream_pageout( &m_oggStream, &page ) ) { this->m_outputFile.write( reinterpret_cast(page.header), page.header_len ); this->m_outputFile.write( reinterpret_cast(page.body), page.body_len ); } if( flush ) { while( ogg_stream_flush( &m_oggStream, &page ) ) { this->m_outputFile.write( reinterpret_cast(page.header), page.header_len ); this->m_outputFile.write( reinterpret_cast(page.body), page.body_len ); } } } }; static_assert( AudioWriterConcept, "OggAudioWriter does not satisfy AudioWriterConcept" ); }