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 file
package.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")