Commitaa86c684Recorded2 Apr 2026Repositorysigil-audio
Add OGG Vorbis encoding via libvorbis+libogg
Message
Adds write-ogg for encoding interleaved float32 sample buffers to .ogg files, with keyword args for sample-rate, channels, and quality. Also adds make-float-buffer for converting Scheme vectors to float32 bytevectors, bridging motif's render output to the encoder input.
Includes manifest.scm for guix dev environment and a test harness with mono/stereo round-trip verification.
Changed
manifest.scm | 14 +++++++++
package.sgl | 12 ++++----
src/c/audio.c | 6 ++++
src/c/ogg-encode.c | 267 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/sigil/audio.sgl | 32 ++++++++++++++++++++
test/main.c | 56 +++++++++++++++++++++++++++++++++++
test/test-ogg-encode.sgl | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 462 insertions(+), 6 deletions(-)Diff
manifest.scmadded
@@ -0,0 +1,14 @@
+1
;; sigil-audio Development Environment+2
;; Use with: guix shell -m manifest.scm+3
+4
(specifications->manifest+5
'("gcc-toolchain"+6
"pkg-config"+7
"binutils"+8
+9
;; Audio playback (Sokol backend)+10
"alsa-lib"+11
+12
;; OGG Vorbis encoding+13
"libvorbis"+14
"libogg")) 15
No newline at end of filepackage.sglmodified
@@ -29,14 +29,14 @@
29
libraries: (list 30
(library 31
name: 'sigil-audio−32
c-sources: '("src/c/sokol-audio.c" "src/c/audio.c")+32
c-sources: '("src/c/sokol-audio.c" "src/c/audio.c" "src/c/ogg-encode.c") 33
c-include-dirs: '("vendor/sokol" "vendor/stb") 34
native-init: "sigil__init_sigil_audio_module"−35
;; Platform-specific linker flags for audio+35
;; Platform-specific linker flags for audio + OGG Vorbis encoding 36
link-flags: (case (host-os)−37
((linux) '("-lasound"))−38
((darwin) '("-framework" "AudioToolbox"))−39
(else '()))))+37
((linux) '("-lasound" "-lvorbis" "-lvorbisenc" "-logg"))+38
((darwin) '("-framework" "AudioToolbox" "-lvorbis" "-lvorbisenc" "-logg"))+39
(else '("-lvorbis" "-lvorbisenc" "-logg"))))) 40
41
tasks: (list 42
(task@@ -44,7 +44,7 @@
44
description: "Build sigil-audio" 45
steps: (list 46
(compile-c-sources−47
sources: '("src/c/sokol-audio.c" "src/c/audio.c")+47
sources: '("src/c/sokol-audio.c" "src/c/audio.c" "src/c/ogg-encode.c") 48
include-dirs: '("../sigil/packages/sigil-lib/include" 49
"../sigil/packages/sigil-lib/src" 50
"vendor/sokol"src/c/audio.cmodified
@@ -567,6 +567,9 @@ static Value native_audio_muted(SigilVM *vm, int argc, Value *args)
567
* MODULE INITIALIZATION 568
* ============================================================ */ 569
+570
/* OGG encoding (ogg-encode.c) */+571
extern void sigil__register_ogg_encode(SigilVM *vm);+572
573
void sigil__init_sigil_audio_module(SigilVM *vm) 574
{ 575
SigilModule *module = sigil_begin_module(vm, "(sigil audio)");@@ -633,5 +636,8 @@ void sigil__init_sigil_audio_module(SigilVM *vm)
636
sigil_module_export(vm, "unmute-audio"); 637
sigil_module_export(vm, "audio-muted?"); 638
+639
/* OGG encoding */+640
sigil__register_ogg_encode(vm);+641
642
sigil_end_module(vm); 643
}src/c/ogg-encode.cadded
@@ -0,0 +1,267 @@
+1
/*+2
* ogg-encode.c - OGG Vorbis encoding for sigil-audio+3
*/+4
+5
#include "sigil-internal.h"+6
+7
#include <stdio.h>+8
#include <stdlib.h>+9
#include <string.h>+10
+11
#include <vorbis/vorbisenc.h>+12
#include <ogg/ogg.h>+13
+14
/* Monotonic serial counter for ogg streams (avoids polluting global RNG) */+15
static int g_ogg_serial = 1;+16
+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
*/+30
static Value native_write_ogg(SigilVM *vm, int argc, Value *args)+31
{+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
}+37
+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
}+42
+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
}+48
+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
}+54
+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
}+60
+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
}+66
+67
SigilString *path_str = (SigilString *)sigil_as_ptr(args[0]);+68
const char *path = path_str->data;+69
+70
uint8_t *bv_data = sigil_bytevector_data(args[1]);+71
size_t bv_len = sigil_bytevector_length(args[1]);+72
+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]);+76
+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
}+82
+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
}+88
+89
if (quality < 0.0f) quality = 0.0f;+90
if (quality > 1.0f) quality = 1.0f;+91
+92
size_t num_frames = (bv_len / sizeof(float)) / channels;+93
float *samples = (float *)bv_data;+94
+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
}+100
+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
}+107
+108
/* Initialize vorbis encoder */+109
vorbis_info vi;+110
vorbis_info_init(&vi);+111
+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
}+120
+121
vorbis_comment vc;+122
vorbis_comment_init(&vc);+123
vorbis_comment_add_tag(&vc, "ENCODER", "sigil-audio");+124
+125
vorbis_dsp_state vd;+126
vorbis_block vb;+127
vorbis_analysis_init(&vd, &vi);+128
vorbis_block_init(&vd, &vb);+129
+130
ogg_stream_state os;+131
ogg_stream_init(&os, g_ogg_serial++);+132
+133
/* Write Vorbis headers */+134
ogg_packet header;+135
ogg_packet header_comm;+136
ogg_packet header_code;+137
+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);+142
+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
}+148
+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;+154
+155
while (!eos) {+156
size_t frames_remaining = num_frames - frames_written;+157
+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;+166
+167
float **buffer = vorbis_analysis_buffer(&vd, (int)chunk);+168
+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
}+181
+182
vorbis_analysis_wrote(&vd, (int)chunk);+183
frames_written += chunk;+184
}+185
+186
/* Pull encoded blocks */+187
while (vorbis_analysis_blockout(&vd, &vb) == 1) {+188
vorbis_analysis(&vb, NULL);+189
vorbis_bitrate_addblock(&vb);+190
+191
ogg_packet op;+192
while (vorbis_bitrate_flushpacket(&vd, &op)) {+193
ogg_stream_packetin(&os, &op);+194
+195
while (!eos) {+196
if (!ogg_stream_pageout(&os, &og)) break;+197
+198
fwrite(og.header, 1, og.header_len, fp);+199
fwrite(og.body, 1, og.body_len, fp);+200
+201
if (ogg_page_eos(&og)) eos = 1;+202
}+203
}+204
}+205
}+206
+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);+214
+215
return SIGIL_TRUE;+216
}+217
+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
*/+224
static Value native_make_float_buffer(SigilVM *vm, int argc, Value *args)+225
{+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
}+231
+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);+235
+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
}+248
+249
return bv;+250
}+251
+252
/*+253
* Register OGG encoding functions into the (sigil audio) module.+254
* Called from sigil__init_sigil_audio_module in audio.c.+255
*/+256
void sigil__register_ogg_encode(SigilVM *vm)+257
{+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");+262
+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");+267
}src/sigil/audio.sglmodified
@@ -35,14 +35,46 @@
35
;; play-music, stop-music, pause-music, resume-music 36
;; music-playing?, set-music-volume 37
;; set-master-volume, mute-audio, unmute-audio, audio-muted?+38
;; make-float-buffer (ogg-encode.c)+39
;; %write-ogg (internal native, ogg-encode.c) 40
41
(export+42
;; OGG encoding (Scheme wrapper with keyword args)+43
write-ogg 44
;; Playlist utilities (Scheme-defined) 45
wait-music-end 46
play-playlist) 47
48
(begin 49
+50
;; ============================================================+51
;; OGG Encoding+52
;; ============================================================+53
+54
;;; Encode interleaved float32 samples to an OGG Vorbis file.+55
;;;+56
;;; Arguments:+57
;;; path: Output file path (string).+58
;;; sample-buffer: Bytevector of interleaved float32 samples.+59
;;;+60
;;; Keywords:+61
;;; sample-rate: Sample rate in Hz (default: 44100).+62
;;; channels: Number of channels, 1 or 2 (default: 2).+63
;;; quality: VBR quality 0.0-1.0 (default: 0.4, ~128kbps).+64
;;;+65
;;; Returns #t on success, #f on failure.+66
;;;+67
;;; Examples:+68
;;; ```scheme+69
;;; (write-ogg "output.ogg" sample-buffer)+70
;;; (write-ogg "output.ogg" sample-buffer sample-rate: 48000 quality: 0.6)+71
;;; ```+72
(define (write-ogg path sample-buffer+73
(keys: (sample-rate 44100)+74
(channels 2)+75
(quality 0.4)))+76
(%write-ogg path sample-buffer sample-rate channels quality))+77
78
;; ============================================================ 79
;; Playlist Utilities 80
;; ============================================================test/main.cadded
@@ -0,0 +1,56 @@
+1
/*+2
* main.c - Sigil Audio Test Harness+3
*+4
* Creates a VM, initializes the audio module (with OGG encoding),+5
* and runs a test script.+6
*/+7
+8
#include <sigil/sigil.h>+9
#include <stdio.h>+10
#include <stdlib.h>+11
+12
/* Audio module init function */+13
extern void sigil__init_sigil_audio_module(SigilVM *vm);+14
+15
int main(int argc, char *argv[])+16
{+17
const char *script_path = "test/test-ogg-encode.sgl";+18
+19
if (argc >= 2) {+20
script_path = argv[1];+21
}+22
+23
/* Create VM */+24
SigilVM *vm = sigil_vm_create();+25
if (!vm) {+26
fprintf(stderr, "Failed to create VM\n");+27
return 1;+28
}+29
+30
/* Add load paths */+31
sigil_vm_add_load_path(vm, "deps/sigil-stdlib/src");+32
sigil_vm_add_load_path(vm, "build/release/lib");+33
sigil_vm_add_load_path(vm, "build/dev/lib");+34
sigil_vm_add_load_path(vm, "src/sigil");+35
+36
/* Initialize audio native module */+37
sigil__init_sigil_audio_module(vm);+38
+39
/* Load and run the script */+40
printf("Loading %s...\n", script_path);+41
int result = sigil_eval_file(vm, script_path);+42
+43
if (result != 0) {+44
const char *err = sigil_error_message(vm);+45
if (err) {+46
fprintf(stderr, "Error: %s\n", err);+47
} else {+48
fprintf(stderr, "Script execution failed (no error message)\n");+49
}+50
}+51
+52
/* Cleanup */+53
sigil_vm_destroy(vm);+54
+55
return result;+56
}test/test-ogg-encode.sgladded
@@ -0,0 +1,81 @@
+1
;;; Test OGG Vorbis encoding+2
;;;+3
;;; Generates a sine wave, encodes to .ogg, then verifies the file+4
;;; can be read back.+5
+6
(import (sigil core)+7
(sigil math)+8
(sigil audio))+9
+10
(define pi 3.14159265358979)+11
+12
;; Generate a 1-second 440Hz sine wave (mono, 44100Hz)+13
(define sample-rate 44100)+14
(define num-samples sample-rate) ;; 1 second+15
(define frequency 440.0)+16
+17
(display "Generating mono sine wave... ")+18
(define samples+19
(let ((v (make-vector num-samples 0.0)))+20
(let loop ((i 0))+21
(when (< i num-samples)+22
(vector-set! v i (* 0.8 (sin (* 2.0 pi frequency (/ i sample-rate)))))+23
(loop (+ i 1))))+24
v))+25
(display "OK\n")+26
+27
;; Convert to float32 bytevector+28
(display "Converting to float buffer... ")+29
(define buffer (make-float-buffer samples))+30
(display (string-append (number->string (bytevector-length buffer)) " bytes\n"))+31
+32
;; Write OGG file using keyword API+33
(define output-path "/tmp/test-sigil-ogg-encode.ogg")+34
(display (string-append "Encoding mono to " output-path "... "))+35
(define result (write-ogg output-path buffer+36
sample-rate: sample-rate+37
channels: 1+38
quality: 0.4))+39
(if result+40
(display "OK\n")+41
(begin (display "FAILED\n") (exit 1)))+42
+43
;; Verify we can decode the file we just wrote+44
(display "Verifying: loading encoded file back... ")+45
(define loaded (load-sound output-path))+46
(if loaded+47
(display "OK\n")+48
(begin (display "FAILED - could not decode written file\n") (exit 1)))+49
+50
;; Test stereo encoding with defaults (sample-rate: 44100, channels: 2, quality: 0.4)+51
(display "\nGenerating stereo sine wave... ")+52
(define stereo-samples+53
(let ((v (make-vector (* num-samples 2) 0.0)))+54
(let loop ((i 0))+55
(when (< i num-samples)+56
(let ((s (* 0.8 (sin (* 2.0 pi frequency (/ i sample-rate))))))+57
(vector-set! v (* i 2) s)+58
(vector-set! v (+ (* i 2) 1)+59
(* 0.8 (sin (* 2.0 pi (* frequency 1.5) (/ i sample-rate))))))+60
(loop (+ i 1))))+61
v))+62
(display "OK\n")+63
+64
(define stereo-buffer (make-float-buffer stereo-samples))+65
(define stereo-path "/tmp/test-sigil-ogg-encode-stereo.ogg")+66
(display (string-append "Encoding stereo to " stereo-path "... "))+67
;; Use defaults for stereo (channels: 2 is default)+68
(define stereo-result (write-ogg stereo-path stereo-buffer+69
sample-rate: sample-rate))+70
(if stereo-result+71
(display "OK\n")+72
(begin (display "FAILED\n") (exit 1)))+73
+74
;; Verify stereo file decodes+75
(display "Verifying: loading stereo file back... ")+76
(define stereo-loaded (load-sound stereo-path))+77
(if stereo-loaded+78
(display "OK\n")+79
(begin (display "FAILED - could not decode stereo file\n") (exit 1)))+80
+81
(display "\nAll tests passed!\n")