|
| 1 | +#pragma once |
| 2 | + |
| 3 | + |
| 4 | +#include "AudioTools/AudioCodecs/AudioCodecsBase.h" |
| 5 | +#include "AudioTools/CoreAudio/Buffers.h" |
| 6 | + |
| 7 | +#include <vorbis.h> |
| 8 | + |
| 9 | +namespace audio_tools { |
| 10 | + |
| 11 | +/** |
| 12 | + * @brief Vorbis Audio Decoder using low-level libvorbis API |
| 13 | + * |
| 14 | + * This decoder expects Ogg Vorbis packets to be provided via the write() |
| 15 | + * method. It parses the Vorbis headers, initializes the decoder, and outputs |
| 16 | + * PCM audio. |
| 17 | + * |
| 18 | + * Usage: |
| 19 | + * 1. Call begin() to reset the decoder. |
| 20 | + * 2. Feed the first three Vorbis header packets via write(). |
| 21 | + * 3. Feed subsequent audio packets via write(). |
| 22 | + * 4. Use setOutput() to set the PCM output destination. |
| 23 | + * 5. Call audioInfo() to retrieve stream parameters after header parsing. |
| 24 | + * |
| 25 | + * @author Phil Schatzmann |
| 26 | + * @ingroup codecs |
| 27 | + * @ingroup decoder |
| 28 | + * @copyright GPLv3 |
| 29 | + */ |
| 30 | +class VorbisDecoder : public AudioDecoder { |
| 31 | + public: |
| 32 | + /** |
| 33 | + * @brief Constructor for VorbisDecoder |
| 34 | + * @param buffer_size Size of the PCM output buffer (default: 256) |
| 35 | + * @param header_packets Number of Vorbis header packets (default: 3) |
| 36 | + * |
| 37 | + * Initializes the decoder and allocates the PCM output buffer. |
| 38 | + */ |
| 39 | + VorbisDecoder(size_t buffer_size = 256, int header_packets = 3) |
| 40 | + : pcm_buffer_size(buffer_size), num_header_packets(header_packets) {} |
| 41 | + |
| 42 | + /** |
| 43 | + * @brief Destructor for VorbisDecoder |
| 44 | + * |
| 45 | + * Cleans up all decoder resources. |
| 46 | + */ |
| 47 | + ~VorbisDecoder() { end(); } |
| 48 | + |
| 49 | + /** |
| 50 | + * @brief Resets decoder state and prepares for new Vorbis stream |
| 51 | + * |
| 52 | + * This method clears all decoder state, resizes the PCM output buffer, |
| 53 | + * and initializes Vorbis structures. Call this before feeding header packets. |
| 54 | + * @return true if successful |
| 55 | + */ |
| 56 | + bool begin() override { |
| 57 | + end(); |
| 58 | + pcmout_buffer.resize(pcm_buffer_size); |
| 59 | + vorbis_info_init(&vi); |
| 60 | + vorbis_comment_init(&vc); |
| 61 | + active = true; |
| 62 | + return true; |
| 63 | + } |
| 64 | + |
| 65 | + /** |
| 66 | + * @brief Cleans up all Vorbis decoder structures |
| 67 | + */ |
| 68 | + void end() override { |
| 69 | + vorbis_block_clear(&vb); |
| 70 | + vorbis_dsp_clear(&vd); |
| 71 | + vorbis_comment_clear(&vc); |
| 72 | + vorbis_info_clear(&vi); |
| 73 | + header_packets = 0; |
| 74 | + decoder_initialized = false; |
| 75 | + active = false; |
| 76 | + } |
| 77 | + |
| 78 | + /** |
| 79 | + * @brief Feeds a Vorbis packet (header or audio) to the decoder |
| 80 | + * |
| 81 | + * The first three packets must be Vorbis headers. Subsequent packets are |
| 82 | + * audio. PCM output is written to the Print stream set via setOutput(). |
| 83 | + * |
| 84 | + * @param data Pointer to packet data |
| 85 | + * @param len Length of packet data |
| 86 | + * @return Number of PCM bytes written to output |
| 87 | + */ |
| 88 | + size_t write(const uint8_t *data, size_t len) override { |
| 89 | + ogg_packet packet; |
| 90 | + packet.packet = (unsigned char *)data; |
| 91 | + packet.bytes = len; |
| 92 | + packet.b_o_s = (header_packets == 0) ? 1 : 0; |
| 93 | + packet.e_o_s = 0; |
| 94 | + packet.granulepos = 0; |
| 95 | + packet.packetno = header_packets; |
| 96 | + |
| 97 | + if (num_header_packets == 0 && !decoder_initialized) { |
| 98 | + if (!initDecoder()) return 0; |
| 99 | + decoder_initialized = true; |
| 100 | + } |
| 101 | + if (header_packets < num_header_packets) { |
| 102 | + if (!parseHeaderPacket(packet, header_packets)) return 0; |
| 103 | + header_packets++; |
| 104 | + if (header_packets == num_header_packets) { |
| 105 | + if (!initDecoder()) return 0; |
| 106 | + decoder_initialized = true; |
| 107 | + } |
| 108 | + return 0; |
| 109 | + } |
| 110 | + if (header_packets == num_header_packets) { |
| 111 | + notifyAudioChange(audioInfo()); |
| 112 | + } |
| 113 | + if (!decoder_initialized) return 0; |
| 114 | + return decodeAudioPacket(packet); |
| 115 | + } |
| 116 | + |
| 117 | + /** |
| 118 | + * @brief Returns audio stream info (sample rate, channels, bits per sample) |
| 119 | + * @return AudioInfo struct with stream parameters |
| 120 | + */ |
| 121 | + AudioInfo audioInfo() override { |
| 122 | + AudioInfo info; |
| 123 | + if (vi.channels > 0 && vi.rate > 0) { |
| 124 | + info.sample_rate = vi.rate; |
| 125 | + info.channels = vi.channels; |
| 126 | + info.bits_per_sample = 16; |
| 127 | + } |
| 128 | + return info; |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * @brief Returns true if decoder is active |
| 133 | + */ |
| 134 | + operator bool() override { return active; } |
| 135 | + |
| 136 | + protected: |
| 137 | + /** @brief Vorbis stream info (channels, sample rate, etc.) */ |
| 138 | + vorbis_info vi{}; |
| 139 | + /** @brief Vorbis comment metadata */ |
| 140 | + vorbis_comment vc{}; |
| 141 | + /** @brief Decoder state for synthesis */ |
| 142 | + vorbis_dsp_state vd{}; |
| 143 | + /** @brief Block structure for synthesis */ |
| 144 | + vorbis_block vb{}; |
| 145 | + /** @brief Output stream for PCM audio */ |
| 146 | + Print *p_print = nullptr; |
| 147 | + /** @brief Decoder active state */ |
| 148 | + bool active = false; |
| 149 | + /** @brief PCM output buffer size */ |
| 150 | + size_t pcm_buffer_size = 256; |
| 151 | + /** @brief Number of Vorbis header packets */ |
| 152 | + int num_header_packets = 3; |
| 153 | + /** @brief Buffer for interleaved PCM output */ |
| 154 | + Vector<int16_t> pcmout_buffer; |
| 155 | + int header_packets = 0; |
| 156 | + bool decoder_initialized = false; |
| 157 | + /** |
| 158 | + * @brief Parses a Vorbis header packet |
| 159 | + * @param packet Ogg Vorbis header packet |
| 160 | + * @param header_packets Index of header packet (0, 1, 2) |
| 161 | + * @return true if successful |
| 162 | + */ |
| 163 | + bool parseHeaderPacket(ogg_packet &packet, int header_packets) { |
| 164 | + if (vorbis_synthesis_headerin(&vi, &vc, &packet) != 0) { |
| 165 | + LOGE("Header packet %d invalid", header_packets); |
| 166 | + return false; |
| 167 | + } |
| 168 | + return true; |
| 169 | + } |
| 170 | + |
| 171 | + /** |
| 172 | + * @brief Initializes the Vorbis decoder after header parsing |
| 173 | + * @return true if successful |
| 174 | + */ |
| 175 | + bool initDecoder() { |
| 176 | + if (vorbis_synthesis_init(&vd, &vi) != 0) { |
| 177 | + LOGE("vorbis_synthesis_init failed"); |
| 178 | + return false; |
| 179 | + } |
| 180 | + vorbis_block_init(&vd, &vb); |
| 181 | + return true; |
| 182 | + } |
| 183 | + |
| 184 | + /** |
| 185 | + * @brief Decodes an audio packet and writes PCM to output |
| 186 | + * @param packet Ogg Vorbis audio packet |
| 187 | + * @return Number of PCM bytes written |
| 188 | + */ |
| 189 | + size_t decodeAudioPacket(ogg_packet &packet) { |
| 190 | + size_t total_written = 0; |
| 191 | + if (vorbis_synthesis(&vb, &packet) == 0) { |
| 192 | + vorbis_synthesis_blockin(&vd, &vb); |
| 193 | + float **pcm = nullptr; |
| 194 | + int samples = vorbis_synthesis_pcmout(&vd, &pcm); |
| 195 | + while (samples > 0 && pcm) { |
| 196 | + int chunk = (samples > pcm_buffer_size) ? pcm_buffer_size : samples; |
| 197 | + convertFloatToInt16PCM(pcm, chunk, vi.channels); |
| 198 | + if (!pcmout_buffer.empty() && p_print) { |
| 199 | + p_print->write((uint8_t *)pcmout_buffer.data(), |
| 200 | + pcmout_buffer.size() * sizeof(int16_t)); |
| 201 | + total_written += pcmout_buffer.size() * sizeof(int16_t); |
| 202 | + pcmout_buffer.clear(); |
| 203 | + } |
| 204 | + vorbis_synthesis_read(&vd, chunk); |
| 205 | + samples = vorbis_synthesis_pcmout(&vd, &pcm); |
| 206 | + } |
| 207 | + } |
| 208 | + return total_written; |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * @brief Converts float PCM to interleaved int16 PCM and stores in |
| 213 | + * pcmout_buffer |
| 214 | + * @param pcm Pointer to float PCM array [channels][samples] |
| 215 | + * @param samples Number of samples |
| 216 | + * @param channels Number of channels |
| 217 | + */ |
| 218 | + void convertFloatToInt16PCM(float **pcm, int samples, int channels) { |
| 219 | + for (int i = 0; i < samples; ++i) { |
| 220 | + for (int ch = 0; ch < channels; ++ch) { |
| 221 | + float val = pcm[ch][i]; |
| 222 | + int16_t sample = (int16_t)(val * 32767.0f); |
| 223 | + if (sample > 32767) sample = 32767; |
| 224 | + if (sample < -32768) sample = -32768; |
| 225 | + pcmout_buffer.push_back(sample); |
| 226 | + } |
| 227 | + } |
| 228 | + } |
| 229 | +}; |
| 230 | + |
| 231 | +} // namespace audio_tools |
0 commit comments