1212#include <stdio.h>
1313#include <stdlib.h>
1414#include <string.h>
15+ #include <time.h>
1516
16- #include <opusenc.h>
17+ #include <ogg/ogg.h>
18+ #include <opus/opus.h>
1719
1820#include "log.h"
1921#include "pcm.h"
2022#include "writer.h"
2123
24+ /* Opus frame size in milliseconds (20ms is recommended) */
25+ #define OPUS_FRAME_MS 20
26+
2227struct writer_opus {
23- OggOpusEnc * enc ;
24- OggOpusComments * comments ;
25- char * pathname ;
26- int bitrate_min ;
27- int bitrate_max ;
28+ ogg_stream_state ogg_s ;
29+ OpusEncoder * enc ;
30+ FILE * fp ;
2831 unsigned int channels ;
2932 unsigned int sampling ;
33+ int bitrate ;
34+ /* Buffer for Opus encoding */
35+ int16_t * pcm_buffer ;
36+ size_t pcm_buffer_size ; /* in frames */
37+ size_t pcm_buffer_used ; /* in frames */
38+ /* Packet number for granulepos calculation */
39+ int64_t packet_count ;
40+ int64_t granule_pos ;
41+ /* Comment string */
42+ char * comment ;
3043};
3144
45+ /* Create OpusHead header packet according to RFC 7845 */
46+ static int create_opus_header (unsigned char * header , unsigned int channels , unsigned int sampling ) {
47+ memset (header , 0 , 19 );
48+ memcpy (header , "OpusHead" , 8 );
49+ header [8 ] = 1 ; /* Version */
50+ header [9 ] = channels ;
51+ /* Pre-skip: number of samples at 48kHz to discard from start */
52+ uint16_t preskip = 0 ;
53+ header [10 ] = preskip & 0xFF ;
54+ header [11 ] = (preskip >> 8 ) & 0xFF ;
55+ /* Input sample rate */
56+ header [12 ] = sampling & 0xFF ;
57+ header [13 ] = (sampling >> 8 ) & 0xFF ;
58+ header [14 ] = (sampling >> 16 ) & 0xFF ;
59+ header [15 ] = (sampling >> 24 ) & 0xFF ;
60+ /* Output gain in dB (Q7.8 format) */
61+ uint16_t gain = 0 ;
62+ header [16 ] = gain & 0xFF ;
63+ header [17 ] = (gain >> 8 ) & 0xFF ;
64+ /* Channel mapping family */
65+ header [18 ] = 0 ; /* 0 = mono/stereo */
66+ return 19 ;
67+ }
68+
69+ /* Create OpusTags header packet */
70+ static int create_opus_tags (unsigned char * * tags , const char * comment ) {
71+ const char * vendor = "libopus" ;
72+ uint32_t vendor_len = strlen (vendor );
73+ uint32_t comment_len = comment ? strlen ("ENCODER=" ) + strlen (comment ) : 0 ;
74+ uint32_t user_comment_count = comment ? 1 : 0 ;
75+
76+ /* Calculate total size */
77+ size_t total_size = 8 + 4 + vendor_len + 4 ; /* "OpusTags" + vendor_len + vendor + count */
78+ if (comment )
79+ total_size += 4 + comment_len ; /* comment_len + comment */
80+
81+ * tags = malloc (total_size );
82+ if (* tags == NULL )
83+ return -1 ;
84+
85+ unsigned char * p = * tags ;
86+ memcpy (p , "OpusTags" , 8 );
87+ p += 8 ;
88+
89+ /* Vendor string length (little-endian) */
90+ memcpy (p , & vendor_len , 4 );
91+ p += 4 ;
92+
93+ /* Vendor string */
94+ memcpy (p , vendor , vendor_len );
95+ p += vendor_len ;
96+
97+ /* User comment list length */
98+ memcpy (p , & user_comment_count , 4 );
99+ p += 4 ;
100+
101+ if (comment ) {
102+ char * tag ;
103+ size_t tag_len = 8 + strlen (comment ) + 1 ;
104+ if ((tag = malloc (tag_len )) == NULL ) {
105+ free (* tags );
106+ return -1 ;
107+ }
108+ snprintf (tag , tag_len , "ENCODER=%s" , comment );
109+ comment_len = strlen (tag );
110+
111+ /* Comment length */
112+ memcpy (p , & comment_len , 4 );
113+ p += 4 ;
114+
115+ /* Comment */
116+ memcpy (p , tag , comment_len );
117+ p += comment_len ;
118+
119+ free (tag );
120+ }
121+
122+ return p - * tags ;
123+ }
124+
32125static int writer_opus_open (struct writer * writer , const char * pathname ) {
33126 struct writer_opus * w = writer -> w ;
34127
35128 writer -> close (writer );
36129
37- /* Store the pathname for later use when creating the encoder */
38- free (w -> pathname );
39- if ((w -> pathname = strdup (pathname )) == NULL )
130+ if ((w -> fp = fopen (pathname , "w" )) == NULL )
40131 return -1 ;
41132
133+ /* Create Opus encoder */
42134 int error ;
43- /* Create encoder with file path, comments, sample rate, channels, and family
44- * Family 0 is used for mono/stereo (up to 2 channels) */
45- int family = w -> channels <= 2 ? 0 : 1 ;
46- w -> enc = ope_encoder_create_file (w -> pathname , w -> comments , w -> sampling , w -> channels , family , & error );
47-
48- if (w -> enc == NULL ) {
49- error ("Couldn't create Opus encoder: %s" , ope_strerror (error ));
135+ w -> enc = opus_encoder_create (48000 , w -> channels , OPUS_APPLICATION_AUDIO , & error );
136+ if (w -> enc == NULL || error != OPUS_OK ) {
137+ error ("Couldn't create Opus encoder: %d" , error );
138+ fclose (w -> fp );
139+ w -> fp = NULL ;
50140 errno = EINVAL ;
51141 return -1 ;
52142 }
53143
54- /* Set bitrate constraints using encoder control interface */
55- if (w -> bitrate_max > 0 ) {
56- if (ope_encoder_ctl (w -> enc , OPUS_SET_BITRATE (w -> bitrate_max )) != OPE_OK )
57- warn ("Couldn't set Opus max bitrate" );
144+ /* Set bitrate */
145+ if (w -> bitrate > 0 ) {
146+ if (opus_encoder_ctl (w -> enc , OPUS_SET_BITRATE (w -> bitrate )) != OPUS_OK )
147+ warn ("Couldn't set Opus bitrate" );
58148 }
59149
150+ /* Initialize OGG stream */
151+ ogg_stream_init (& w -> ogg_s , time (NULL ));
152+
153+ /* Create and write OpusHead header */
154+ unsigned char header [19 ];
155+ int header_len = create_opus_header (header , w -> channels , w -> sampling );
156+
157+ ogg_packet op ;
158+ op .packet = header ;
159+ op .bytes = header_len ;
160+ op .b_o_s = 1 ; /* Beginning of stream */
161+ op .e_o_s = 0 ;
162+ op .granulepos = 0 ;
163+ op .packetno = 0 ;
164+
165+ ogg_stream_packetin (& w -> ogg_s , & op );
166+
167+ /* Write header pages */
168+ ogg_page og ;
169+ while (ogg_stream_flush (& w -> ogg_s , & og )) {
170+ fwrite (og .header , 1 , og .header_len , w -> fp );
171+ fwrite (og .body , 1 , og .body_len , w -> fp );
172+ }
173+
174+ /* Create and write OpusTags header */
175+ unsigned char * tags = NULL ;
176+ int tags_len = create_opus_tags (& tags , w -> comment );
177+ if (tags_len < 0 ) {
178+ opus_encoder_destroy (w -> enc );
179+ w -> enc = NULL ;
180+ ogg_stream_clear (& w -> ogg_s );
181+ fclose (w -> fp );
182+ w -> fp = NULL ;
183+ return -1 ;
184+ }
185+
186+ op .packet = tags ;
187+ op .bytes = tags_len ;
188+ op .b_o_s = 0 ;
189+ op .e_o_s = 0 ;
190+ op .granulepos = 0 ;
191+ op .packetno = 1 ;
192+
193+ ogg_stream_packetin (& w -> ogg_s , & op );
194+ free (tags );
195+
196+ while (ogg_stream_flush (& w -> ogg_s , & og )) {
197+ fwrite (og .header , 1 , og .header_len , w -> fp );
198+ fwrite (og .body , 1 , og .body_len , w -> fp );
199+ }
200+
201+ w -> packet_count = 2 ; /* We've written 2 header packets */
202+ w -> granule_pos = 0 ;
203+ w -> pcm_buffer_used = 0 ;
204+
60205 writer -> opened = true;
61206 return 0 ;
62207}
@@ -65,38 +210,112 @@ static void writer_opus_close(struct writer * writer) {
65210 struct writer_opus * w = writer -> w ;
66211 writer -> opened = false;
67212
68- if (w -> enc == NULL )
213+ if (w -> fp == NULL )
69214 return ;
70215
71- /* Finalize the stream */
72- ope_encoder_drain (w -> enc );
73- ope_encoder_destroy (w -> enc );
74- w -> enc = NULL ;
216+ /* Encode any remaining PCM data */
217+ if (w -> pcm_buffer_used > 0 ) {
218+ unsigned char packet [4000 ];
219+ int nbBytes = opus_encode (w -> enc , w -> pcm_buffer , w -> pcm_buffer_used , packet , sizeof (packet ));
220+
221+ if (nbBytes > 0 ) {
222+ /* Calculate granule position (samples at 48kHz) */
223+ w -> granule_pos += w -> pcm_buffer_used * 48000 / w -> sampling ;
224+
225+ ogg_packet op ;
226+ op .packet = packet ;
227+ op .bytes = nbBytes ;
228+ op .b_o_s = 0 ;
229+ op .e_o_s = 1 ; /* End of stream */
230+ op .granulepos = w -> granule_pos ;
231+ op .packetno = w -> packet_count ++ ;
232+
233+ ogg_stream_packetin (& w -> ogg_s , & op );
234+ }
235+ }
236+
237+ /* Flush remaining pages */
238+ ogg_page og ;
239+ while (ogg_stream_flush (& w -> ogg_s , & og )) {
240+ fwrite (og .header , 1 , og .header_len , w -> fp );
241+ fwrite (og .body , 1 , og .body_len , w -> fp );
242+ }
243+
244+ if (w -> enc != NULL ) {
245+ opus_encoder_destroy (w -> enc );
246+ w -> enc = NULL ;
247+ }
248+ ogg_stream_clear (& w -> ogg_s );
249+ fclose (w -> fp );
250+ w -> fp = NULL ;
75251}
76252
77253static ssize_t writer_opus_write (struct writer * writer , const void * buffer , size_t frames ) {
78254 struct writer_opus * w = writer -> w ;
255+ const int16_t * pcm = (const int16_t * )buffer ;
256+ size_t frames_written = 0 ;
79257
80- /* Write 16-bit PCM samples to the encoder */
81- int ret = ope_encoder_write (w -> enc , (const opus_int16 * )buffer , frames );
82-
83- if (ret != OPE_OK ) {
84- error ("Opus encoding error: %s" , ope_strerror (ret ));
85- return -1 ;
258+ while (frames > 0 ) {
259+ /* Calculate how many frames we can add to buffer */
260+ size_t frames_to_copy = w -> pcm_buffer_size - w -> pcm_buffer_used ;
261+ if (frames_to_copy > frames )
262+ frames_to_copy = frames ;
263+
264+ /* Copy PCM data to buffer */
265+ memcpy (w -> pcm_buffer + w -> pcm_buffer_used * w -> channels ,
266+ pcm + frames_written * w -> channels ,
267+ frames_to_copy * w -> channels * sizeof (int16_t ));
268+
269+ w -> pcm_buffer_used += frames_to_copy ;
270+ frames_written += frames_to_copy ;
271+ frames -= frames_to_copy ;
272+
273+ /* If buffer is full, encode it */
274+ if (w -> pcm_buffer_used >= w -> pcm_buffer_size ) {
275+ unsigned char packet [4000 ];
276+ int nbBytes = opus_encode (w -> enc , w -> pcm_buffer , w -> pcm_buffer_size , packet , sizeof (packet ));
277+
278+ if (nbBytes < 0 ) {
279+ error ("Opus encoding error: %d" , nbBytes );
280+ return -1 ;
281+ }
282+
283+ if (nbBytes > 0 ) {
284+ /* Update granule position (samples at 48kHz) */
285+ w -> granule_pos += w -> pcm_buffer_size * 48000 / w -> sampling ;
286+
287+ ogg_packet op ;
288+ op .packet = packet ;
289+ op .bytes = nbBytes ;
290+ op .b_o_s = 0 ;
291+ op .e_o_s = 0 ;
292+ op .granulepos = w -> granule_pos ;
293+ op .packetno = w -> packet_count ++ ;
294+
295+ ogg_stream_packetin (& w -> ogg_s , & op );
296+
297+ /* Write out any pages */
298+ ogg_page og ;
299+ while (ogg_stream_pageout (& w -> ogg_s , & og )) {
300+ fwrite (og .header , 1 , og .header_len , w -> fp );
301+ fwrite (og .body , 1 , og .body_len , w -> fp );
302+ }
303+ }
304+
305+ w -> pcm_buffer_used = 0 ;
306+ }
86307 }
87308
88- /* Return the number of frames written (libopusenc doesn't return byte count) */
89- return frames ;
309+ return frames_written ;
90310}
91311
92312static void writer_opus_free (struct writer * writer ) {
93313 if (writer == NULL )
94314 return ;
95315 struct writer_opus * w = writer -> w ;
96316 writer -> close (writer );
97- if (w -> comments != NULL )
98- ope_comments_destroy (w -> comments );
99- free (w -> pathname );
317+ free (w -> pcm_buffer );
318+ free (w -> comment );
100319 free (writer -> w );
101320 free (writer );
102321}
@@ -127,30 +346,30 @@ struct writer * writer_opus_new(
127346 return NULL ;
128347 }
129348
130- w -> bitrate_min = bitrate_min ; /* Currently unused - Opus uses single bitrate setting */
131- w -> bitrate_max = bitrate_max ;
349+ w -> bitrate = bitrate_max > 0 ? bitrate_max : 64000 ;
132350 w -> channels = channels ;
133351 w -> sampling = sampling ;
134352
135- /* Create comments object */
136- w -> comments = ope_comments_create ();
137- if (w -> comments == NULL ) {
138- writer_opus_free (writer );
353+ /* Calculate frame size in samples at the input sample rate
354+ * Opus always works at 48kHz internally, so we need to buffer data */
355+ w -> pcm_buffer_size = (sampling * OPUS_FRAME_MS ) / 1000 ;
356+ w -> pcm_buffer = malloc (w -> pcm_buffer_size * channels * sizeof (int16_t ));
357+ if (w -> pcm_buffer == NULL ) {
358+ free (writer -> w );
359+ free (writer );
139360 return NULL ;
140361 }
362+ w -> pcm_buffer_used = 0 ;
141363
142364 if (comment != NULL ) {
143- /* Use dynamic allocation to avoid VLA stack overflow issues */
144- size_t tag_len = 8 + strlen (comment ) + 1 ;
145- char * tag = malloc (tag_len );
146- if (tag == NULL ) {
365+ w -> comment = strdup (comment );
366+ if (w -> comment == NULL ) {
147367 writer_opus_free (writer );
148368 return NULL ;
149369 }
150- snprintf (tag , tag_len , "ENCODER=%s" , comment );
151- ope_comments_add_string (w -> comments , tag );
152- free (tag );
153370 }
154371
372+ (void )bitrate_min ; /* Unused */
373+
155374 return writer ;
156375}
0 commit comments