AtlatestRepositorysigil-audio

sigil-audio / tree / src / cogg-encode.c

1/*
2 * ogg-encode.c - OGG Vorbis encoding for sigil-audio
3 */
4
5#include "sigil/sigil.h"
6
7#include <stdio.h>
8#include <stdlib.h>
9#include <string.h>
11#include <vorbis/vorbisenc.h>
12#include <ogg/ogg.h>
14/* Monotonic serial counter for ogg streams (avoids polluting global RNG) */
15static int g_ogg_serial = 1;
17/*
18 * (write-ogg path sample-buffer sample-rate channels quality) -> boolean
19 *
20 * Encode interleaved float32 samples to an OGG Vorbis file.
21 *
22 * path: Output file path (string)
23 * sample-buffer: Bytevector of interleaved float32 samples
24 * sample-rate: Sample rate in Hz (integer, e.g. 44100)
25 * channels: Number of channels (integer, 1 or 2)
26 * quality: VBR quality 0.0-1.0 (flonum, 0.4 ~ 128kbps)
27 *
28 * Returns #t on success, #f on failure.
29 */
30static Value native_write_ogg(SigilVM *vm, int argc, Value *args)
32 if (argc < 5) {
33 sigil__vm_set_error(vm, SIGIL_ERR_ARITY,
34 "write-ogg: requires path, sample-buffer, sample-rate, channels, quality");
35 return SIGIL_FALSE;
36 }
38 if (!sigil_is_string(args[0])) {
39 sigil__vm_set_error(vm, SIGIL_ERR_TYPE, "write-ogg: path must be a string");
40 return SIGIL_FALSE;
41 }
43 if (!sigil_is_bytevector(args[1])) {
44 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
45 "write-ogg: sample-buffer must be a bytevector");
46 return SIGIL_FALSE;
47 }
49 if (!sigil_is_fixnum(args[2])) {
50 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
51 "write-ogg: sample-rate must be an integer");
52 return SIGIL_FALSE;
53 }
55 if (!sigil_is_fixnum(args[3])) {
56 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
57 "write-ogg: channels must be an integer");
58 return SIGIL_FALSE;
59 }
61 if (!sigil_is_flonum(args[4])) {
62 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
63 "write-ogg: quality must be a flonum");
64 return SIGIL_FALSE;
65 }
67 SigilString *path_str = (SigilString *)sigil_as_ptr(args[0]);
68 const char *path = path_str->data;
70 uint8_t *bv_data = sigil_bytevector_data(args[1]);
71 size_t bv_len = sigil_bytevector_length(args[1]);
73 int sample_rate = (int)sigil_as_fixnum(args[2]);
74 int channels = (int)sigil_as_fixnum(args[3]);
75 float quality = (float)sigil_as_flonum(args[4]);
77 if (channels < 1 || channels > 2) {
78 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
79 "write-ogg: channels must be 1 or 2");
80 return SIGIL_FALSE;
81 }
83 if (sample_rate < 1 || sample_rate > 192000) {
84 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
85 "write-ogg: sample-rate out of range");
86 return SIGIL_FALSE;
87 }
89 if (quality < 0.0f) quality = 0.0f;
90 if (quality > 1.0f) quality = 1.0f;
92 size_t num_frames = (bv_len / sizeof(float)) / channels;
93 float *samples = (float *)bv_data;
95 if (num_frames == 0) {
96 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
97 "write-ogg: sample-buffer is empty");
98 return SIGIL_FALSE;
99 }
101 FILE *fp = fopen(path, "wb");
102 if (!fp) {
103 sigil__vm_set_error(vm, SIGIL_ERR_IO,
104 "write-ogg: cannot open output file");
105 return SIGIL_FALSE;
106 }
108 /* Initialize vorbis encoder */
109 vorbis_info vi;
110 vorbis_info_init(&vi);
112 int ret = vorbis_encode_init_vbr(&vi, channels, sample_rate, quality);
113 if (ret) {
114 vorbis_info_clear(&vi);
115 fclose(fp);
116 sigil__vm_set_error(vm, SIGIL_ERR_RUNTIME,
117 "write-ogg: vorbis encoder init failed");
118 return SIGIL_FALSE;
119 }
121 vorbis_comment vc;
122 vorbis_comment_init(&vc);
123 vorbis_comment_add_tag(&vc, "ENCODER", "sigil-audio");
125 vorbis_dsp_state vd;
126 vorbis_block vb;
127 vorbis_analysis_init(&vd, &vi);
128 vorbis_block_init(&vd, &vb);
130 ogg_stream_state os;
131 ogg_stream_init(&os, g_ogg_serial++);
133 /* Write Vorbis headers */
134 ogg_packet header;
135 ogg_packet header_comm;
136 ogg_packet header_code;
138 vorbis_analysis_headerout(&vd, &vc, &header, &header_comm, &header_code);
139 ogg_stream_packetin(&os, &header);
140 ogg_stream_packetin(&os, &header_comm);
141 ogg_stream_packetin(&os, &header_code);
143 ogg_page og;
144 while (ogg_stream_flush(&os, &og)) {
145 fwrite(og.header, 1, og.header_len, fp);
146 fwrite(og.body, 1, og.body_len, fp);
147 }
149 /* Encode samples in chunks */
150 int eos = 0;
151 int eos_signaled = 0;
152 size_t frames_written = 0;
153 const size_t CHUNK_FRAMES = 4096;
155 while (!eos) {
156 size_t frames_remaining = num_frames - frames_written;
158 if (frames_remaining == 0) {
159 if (!eos_signaled) {
160 vorbis_analysis_wrote(&vd, 0);
161 eos_signaled = 1;
162 }
163 } else {
164 size_t chunk = frames_remaining < CHUNK_FRAMES ?
165 frames_remaining : CHUNK_FRAMES;
167 float **buffer = vorbis_analysis_buffer(&vd, (int)chunk);
169 /* Deinterleave samples into vorbis channel buffers */
170 if (channels == 1) {
171 memcpy(buffer[0], samples + frames_written, chunk * sizeof(float));
172 } else {
173 float *left = buffer[0];
174 float *right = buffer[1];
175 const float *src = samples + frames_written * 2;
176 for (size_t f = 0; f < chunk; f++) {
177 left[f] = src[f * 2];
178 right[f] = src[f * 2 + 1];
179 }
180 }
182 vorbis_analysis_wrote(&vd, (int)chunk);
183 frames_written += chunk;
184 }
186 /* Pull encoded blocks */
187 while (vorbis_analysis_blockout(&vd, &vb) == 1) {
188 vorbis_analysis(&vb, NULL);
189 vorbis_bitrate_addblock(&vb);
191 ogg_packet op;
192 while (vorbis_bitrate_flushpacket(&vd, &op)) {
193 ogg_stream_packetin(&os, &op);
195 while (!eos) {
196 if (!ogg_stream_pageout(&os, &og)) break;
198 fwrite(og.header, 1, og.header_len, fp);
199 fwrite(og.body, 1, og.body_len, fp);
201 if (ogg_page_eos(&og)) eos = 1;
202 }
203 }
204 }
205 }
207 /* Clean up */
208 ogg_stream_clear(&os);
209 vorbis_block_clear(&vb);
210 vorbis_dsp_clear(&vd);
211 vorbis_comment_clear(&vc);
212 vorbis_info_clear(&vi);
213 fclose(fp);
215 return SIGIL_TRUE;
218/*
219 * (make-float-buffer vec) -> bytevector
220 *
221 * Convert a Scheme vector of numbers to a bytevector of float32 data.
222 * Useful for preparing sample data for write-ogg.
223 */
224static Value native_make_float_buffer(SigilVM *vm, int argc, Value *args)
226 if (argc < 1 || !sigil_is_vector(args[0])) {
227 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
228 "make-float-buffer: expected vector of numbers");
229 return SIGIL_FALSE;
230 }
232 size_t len = sigil_vector_length(args[0]);
233 Value bv = sigil_make_bytevector(vm, len * sizeof(float));
234 float *data = (float *)sigil_bytevector_data(bv);
236 for (size_t i = 0; i < len; i++) {
237 Value elem = sigil_vector_ref(args[0], i);
238 if (sigil_is_flonum(elem)) {
239 data[i] = (float)sigil_as_flonum(elem);
240 } else if (sigil_is_fixnum(elem)) {
241 data[i] = (float)sigil_as_fixnum(elem);
242 } else {
243 sigil__vm_set_error(vm, SIGIL_ERR_TYPE,
244 "make-float-buffer: vector element is not a number");
245 return SIGIL_FALSE;
246 }
247 }
249 return bv;
252/*
253 * Register OGG encoding functions into the (sigil audio) module.
254 * Called from sigil__init_sigil_audio_module in audio.c.
255 */
256void sigil__register_ogg_encode(SigilVM *vm)
258 sigil_module_register_native(vm, "%write-ogg", native_write_ogg,
259 SIGIL_ARITY_EXACT(5),
260 "Encode float samples to OGG Vorbis file (internal)");
261 sigil_module_export(vm, "%write-ogg");
263 sigil_module_register_native(vm, "make-float-buffer", native_make_float_buffer,
264 SIGIL_ARITY_EXACT(1),
265 "Convert vector of numbers to float32 bytevector");
266 sigil_module_export(vm, "make-float-buffer");