Skip to content

Commit 4bd45bf

Browse files
Copilotarkq
andcommitted
Rewrite Opus writer to use libopus instead of libopusenc
Per maintainer request, replaced libopusenc dependency with libopus. The implementation now manually handles OGG muxing using libogg, similar to the existing Vorbis writer. Co-authored-by: arkq <6979798+arkq@users.noreply.github.com>
1 parent 1fb991b commit 4bd45bf

2 files changed

Lines changed: 269 additions & 50 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ if(ENABLE_VORBIS)
8585
endif()
8686

8787
if(ENABLE_OPUS)
88-
pkg_check_modules(Opus REQUIRED IMPORTED_TARGET libopusenc ogg)
88+
pkg_check_modules(Opus REQUIRED IMPORTED_TARGET opus ogg)
8989
target_sources(svarcore PRIVATE src/writer-opus.c)
9090
target_link_libraries(svarcore PkgConfig::Opus)
9191
endif()

src/writer-opus.c

Lines changed: 268 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,51 +12,196 @@
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+
2227
struct 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+
32125
static 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

77253
static 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

92312
static 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

Comments
 (0)