From 54f8f1418282d0fbdf806bdaa357206c16f0927d Mon Sep 17 00:00:00 2001 From: Sebastian Reimers Date: Sun, 18 Jan 2026 16:26:21 +0100 Subject: [PATCH 1/5] libs: add flac recording --- libsl/CMakeLists.txt | 4 +- libsl/include/studiolink.h | 18 ++++ libsl/src/flac.c | 164 +++++++++++++++++++++++++++++ libsl/src/http/server.c | 10 ++ libsl/src/meters.c | 2 +- libsl/src/record.c | 207 +++++++++++++++++++++++++++++++++++++ libsl/src/ws.c | 2 +- 7 files changed, 404 insertions(+), 3 deletions(-) create mode 100644 libsl/src/flac.c create mode 100644 libsl/src/record.c diff --git a/libsl/CMakeLists.txt b/libsl/CMakeLists.txt index 5c5c03d..dbac49d 100644 --- a/libsl/CMakeLists.txt +++ b/libsl/CMakeLists.txt @@ -22,10 +22,12 @@ set(SRCS src/audio.c src/conf.c src/db.c + src/flac.c src/http/client.c src/http/server.c src/main.c src/meters.c + src/record.c src/tracks.c src/ws.c ) @@ -83,7 +85,7 @@ LIST(APPEND COMPILED_RESOURCES ${OUTPUT_FILE}) # # Main target object # -set(LINKLIBS baresip re ${LMDB_LIBRARIES} ${OPENH264_LIBRARIES} stdc++) +set(LINKLIBS baresip re FLAC ${LMDB_LIBRARIES} stdc++) if(WIN32) list(APPEND LINKLIBS winmm setupapi) diff --git a/libsl/include/studiolink.h b/libsl/include/studiolink.h index bb67cae..5bc077e 100644 --- a/libsl/include/studiolink.h +++ b/libsl/include/studiolink.h @@ -220,6 +220,24 @@ int sl_account_close(void); struct ua *sl_account_ua(void); +/****************************************************************************** + * record.c + */ +uint64_t sl_record_msecs(void); +void sl_record_toggle(const char *folder); +int sl_record_start(const char *folder); +void sl_record(struct auframe *af); +int sl_record_close(void); + + +/****************************************************************************** + * flac.c + */ +struct flac; +int sl_flac_init(struct flac **flacp, struct auframe *af, char *file); +int sl_flac_record(struct flac *flac, struct auframe *af, uint64_t offset); + + #ifdef __cplusplus } #endif diff --git a/libsl/src/flac.c b/libsl/src/flac.c new file mode 100644 index 0000000..e0510c0 --- /dev/null +++ b/libsl/src/flac.c @@ -0,0 +1,164 @@ +#include +#include +#include + +#include +#include +#include +#include + +struct flac { + FLAC__StreamEncoder *enc; + FLAC__StreamMetadata *m[2]; + FLAC__int32 *pcm; +}; + + +static void flac_destruct(void *arg) +{ + struct flac *flac = arg; + + mem_deref(flac->pcm); + FLAC__stream_encoder_finish(flac->enc); + FLAC__stream_encoder_delete(flac->enc); + FLAC__metadata_object_delete(flac->m[0]); + FLAC__metadata_object_delete(flac->m[1]); +} + + +int sl_flac_init(struct flac **flacp, struct auframe *af, char *file) +{ + + struct flac *flac; + FLAC__bool ret; + FLAC__StreamEncoderInitStatus init; + FLAC__StreamMetadata_VorbisComment_Entry entry; + + + if (!flacp || !af || !file) + return EINVAL; + + flac = mem_zalloc(sizeof(struct flac), flac_destruct); + if (!flac) + return ENOMEM; + + flac->pcm = mem_zalloc(af->sampc * sizeof(FLAC__int32), NULL); + + + flac->enc = FLAC__stream_encoder_new(); + if (!flac->enc) + return ENOMEM; + + ret = FLAC__stream_encoder_set_verify(flac->enc, true); + ret &= FLAC__stream_encoder_set_compression_level(flac->enc, 5); + ret &= FLAC__stream_encoder_set_channels(flac->enc, af->ch); + ret &= FLAC__stream_encoder_set_bits_per_sample(flac->enc, 16); + ret &= FLAC__stream_encoder_set_sample_rate(flac->enc, af->srate); + ret &= FLAC__stream_encoder_set_total_samples_estimate(flac->enc, 0); + + if (!ret) { + warning("record: FLAC__stream_encoder_set\n"); + return EINVAL; + } + + /* METADATA */ + flac->m[0] = + FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT); + flac->m[1] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_PADDING); + + ret = FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair( + &entry, "ENCODED_BY", "STUDIO LINK MIX"); + + ret &= FLAC__metadata_object_vorbiscomment_append_comment( + flac->m[0], entry, /*copy=*/false); + + if (!ret) { + warning("record: FLAC METADATA ERROR: out of memory or tag " + "error\n"); + return ENOMEM; + } + + flac->m[1]->length = 1234; /* padding length */ + + ret = FLAC__stream_encoder_set_metadata(flac->enc, flac->m, 2); + + if (!ret) { + warning("record: FLAC__stream_encoder_set_metadata\n"); + return ENOMEM; + } + + init = FLAC__stream_encoder_init_file(flac->enc, file, NULL, NULL); + + if (init != FLAC__STREAM_ENCODER_INIT_STATUS_OK) { + warning("record: FLAC ERROR: initializing encoder: %s\n", + FLAC__StreamEncoderInitStatusString[init]); + return ENOMEM; + } + + *flacp = flac; + + return 0; +} + + +int sl_flac_record(struct flac *flac, struct auframe *af, uint64_t offset) +{ + FLAC__StreamEncoderState state; + FLAC__bool ret; + + if (!flac || !af || !af->ch) + return EINVAL; + + if (offset > 24 * 3600 * 1000) { + warning("flac_record: ignoring high >24h offset (%llu)\n", + offset); + offset = 0; + } + + if (offset < 40 /*ms*/) + offset = 0; + + if (offset) { + info("flac_record: offset %llu id %u\n", offset, af->id); + memset(flac->pcm, 0, af->sampc * sizeof(FLAC__int32)); + uint64_t offsampc = af->srate * af->ch * offset / 1000; + + while (offsampc) { + ret = FLAC__stream_encoder_process_interleaved( + flac->enc, flac->pcm, + (uint32_t)af->sampc / af->ch); + if (!ret) + goto err; + + if (offsampc >= af->sampc) + offsampc -= af->sampc; + else { + ret = FLAC__stream_encoder_process_interleaved( + flac->enc, flac->pcm, + (uint32_t)offsampc / af->ch); + if (!ret) + goto err; + offsampc = 0; + } + } + } + + int16_t *sampv = (int16_t *)af->sampv; + + for (size_t i = 0; i < af->sampc; i++) { + flac->pcm[i] = sampv[i]; + } + + ret = FLAC__stream_encoder_process_interleaved( + flac->enc, flac->pcm, (uint32_t)af->sampc / af->ch); + if (ret) + return 0; + + +err: + state = FLAC__stream_encoder_get_state(flac->enc); + warning("record: FLAC ENCODE ERROR: %s\n", + FLAC__StreamEncoderStateString[state]); + + return EBADFD; +} diff --git a/libsl/src/http/server.c b/libsl/src/http/server.c index a00d2ee..10ed0de 100644 --- a/libsl/src/http/server.c +++ b/libsl/src/http/server.c @@ -344,6 +344,16 @@ static void http_req_handler(struct http_conn *conn, } + if (0 == pl_strcasecmp(&msg->path, "/api/v1/record") && + 0 == pl_strcasecmp(&msg->met, "POST")) { + + sl_record_toggle("/tmp"); + http_sreply(conn, 200, "OK", "text/html", "", 0); + + goto out; + } + + #ifndef RELEASE /* Default return OPTIONS - needed on dev for preflight CORS Check * @TODO: add release test */ diff --git a/libsl/src/meters.c b/libsl/src/meters.c index c870c5f..1dc9067 100644 --- a/libsl/src/meters.c +++ b/libsl/src/meters.c @@ -38,7 +38,7 @@ static void write_ws(void) p[0] = '\0'; /* Record time */ - re_snprintf(one_peak, sizeof(one_peak), "0 0 "); + re_snprintf(one_peak, sizeof(one_peak), "%llu ", sl_record_msecs()); strcat((char *)p, one_peak); for (i = 0; i < MAX_METERS; i++) { diff --git a/libsl/src/record.c b/libsl/src/record.c new file mode 100644 index 0000000..f7e7929 --- /dev/null +++ b/libsl/src/record.c @@ -0,0 +1,207 @@ +/** + * @file record.c generic recording + * + * Copyright (C) 2026 Sebastian Reimers + */ + +#include +#include +#include +#include +#include + + +static struct { + struct list tracks; + RE_ATOMIC bool run; + thrd_t thread; + struct aubuf *ab; + char *folder; + RE_ATOMIC uint64_t start_time; +} record = {.tracks = LIST_INIT, .run = false}; + +struct record_entry { + struct le le; + struct mbuf *mb; + size_t size; +}; + +enum { + SRATE = 48000, + CH = 1, + PTIME = 20, +}; + + +uint64_t sl_record_msecs(void) +{ + if (!re_atomic_rlx(&record.start_time)) + return 0; + + return tmr_jiffies() - re_atomic_rlx(&record.start_time); +} + + +struct track { + struct le le; + uint16_t id; + char file[512]; + uint64_t last; + struct flac *flac; +}; + + +static void track_destruct(void *arg) +{ + struct track *track = arg; + + list_unlink(&track->le); + mem_deref(track->flac); +} + + +static int record_track(struct auframe *af) +{ + struct le *le; + struct track *track = NULL; + uint64_t offset; + int err; + + LIST_FOREACH(&record.tracks, le) + { + struct track *t = le->data; + + if (t->id == af->id) { + track = t; + break; + } + } + + if (!track) { + track = mem_zalloc(sizeof(struct track), track_destruct); + if (!track) + return ENOMEM; + + track->id = af->id; + track->last = re_atomic_rlx(&record.start_time); + + /* TODO: add user->name */ + re_snprintf(track->file, sizeof(track->file), + "%s/audio_id%u.flac", record.folder, track->id); + + err = sl_flac_init(&track->flac, af, track->file); + if (err) { + mem_deref(track); + return err; + } + + list_append(&record.tracks, &track->le, track); + } + + offset = af->timestamp - track->last; + track->last = af->timestamp; + + sl_flac_record(track->flac, af, offset); + + return 0; +} + + +static int record_thread(void *arg) +{ + struct auframe af; + int err; + (void)arg; + + int16_t *sampv; + size_t sampc = SRATE * CH * PTIME / 1000; + + sampv = mem_zalloc(sampc * sizeof(int16_t), NULL); + if (!sampv) + return ENOMEM; + + auframe_init(&af, AUFMT_S16LE, sampv, sampc, SRATE, CH); + + re_atomic_rlx_set(&record.start_time, tmr_jiffies()); + + while (re_atomic_rlx(&record.run)) { + sys_msleep(4); + while (aubuf_cur_size(record.ab) > sampc) { + aubuf_read_auframe(record.ab, &af); + err = record_track(&af); + if (err) + goto out; + } + } + +out: + re_atomic_rlx_set(&record.start_time, 0); + mem_deref(sampv); + + return 0; +} + + +void sl_record(struct auframe *af) +{ + if (!re_atomic_rlx(&record.run) || !af->id) + return; + + af->timestamp = tmr_jiffies(); + aubuf_write_auframe(record.ab, af); +} + + +void sl_record_toggle(const char *folder) +{ + if (!re_atomic_rlx(&record.run)) + sl_record_start(folder); + else + sl_record_close(); + +} + + +int sl_record_start(const char *folder) +{ + int err; + + if (!folder) + return EINVAL; + + if (re_atomic_rlx(&record.run)) + return EALREADY; + + re_atomic_rlx_set(&record.start_time, 0); + str_dup(&record.folder, folder); + + err = aubuf_alloc(&record.ab, 0, 0); + if (err) { + return err; + } + + re_atomic_rlx_set(&record.run, true); + info("aumix: record started\n"); + + thread_create_name(&record.thread, "sl_record", record_thread, NULL); + + return 0; +} + + +int sl_record_close(void) +{ + if (!re_atomic_rlx(&record.run)) + return EINVAL; + + re_atomic_rlx_set(&record.run, false); + info("aumix: record close\n"); + thrd_join(record.thread, NULL); + + mem_deref(record.ab); + + record.folder = mem_deref(record.folder); + list_flush(&record.tracks); + + return 0; +} diff --git a/libsl/src/ws.c b/libsl/src/ws.c index 2940dcb..d189833 100644 --- a/libsl/src/ws.c +++ b/libsl/src/ws.c @@ -138,7 +138,7 @@ int sl_ws_init(void) return err; } - err = mutex_alloc(&wsl_lock); + err = mutex_alloc_tp(&wsl_lock, mtx_recursive); if (err) { warning("sl_ws_init: mutex_alloc failed\n"); mem_deref(ws); From a72853d76df8f6ed3f3a8edacd0af0ffd13d4432 Mon Sep 17 00:00:00 2001 From: Sebastian Reimers Date: Sun, 18 Jan 2026 16:26:35 +0100 Subject: [PATCH 2/5] webui: add recording --- webui/src/api.ts | 4 +++ webui/src/components/BottomActions.vue | 16 +++++----- webui/src/states/meters.ts | 41 +++++++++++++++++++++----- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/webui/src/api.ts b/webui/src/api.ts index 1dc2fe5..10330a8 100644 --- a/webui/src/api.ts +++ b/webui/src/api.ts @@ -32,6 +32,10 @@ export default { api_request('POST', '/track/mute?track=' + String(id), null); }, + record() { + api_request('POST', '/record', null); + }, + audio_device(track: number, mic: number, speaker: number) { api_request('PUT', '/audio/device?track=' + String(track), String(mic) + ";" + String(speaker)); }, diff --git a/webui/src/components/BottomActions.vue b/webui/src/components/BottomActions.vue index 4c52872..9ffd880 100644 --- a/webui/src/components/BottomActions.vue +++ b/webui/src/components/BottomActions.vue @@ -5,21 +5,19 @@ @click="api.track_mute(1)"> Mute - diff --git a/webui/src/states/meters.ts b/webui/src/states/meters.ts index b6899fc..45533ff 100644 --- a/webui/src/states/meters.ts +++ b/webui/src/states/meters.ts @@ -1,9 +1,4 @@ -interface Meters { - socket?: WebSocket - websocket(ws_host: string): void - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - update(message: any): void -} +import { Ref, ref } from 'vue' function iec_scale(db: number) { let def = 0.0; @@ -38,10 +33,26 @@ function update_meters(peak: string, index: number) { document.getElementById("level" + (index - 2))?.style.setProperty("--my-level", val + "% 0 0 0") } +interface Meters { + socket?: WebSocket + record_timer: Ref + record: Ref + websocket(ws_host: string): void + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + update(message: any): void +} + +function pad(num: number, size: number) { + const s = '0000' + num + return s.substring(s.length - size) +} + export const meters: Meters = { + record_timer: ref("0:00:00"), + record: ref(false), websocket(ws_host): void { this.socket = new WebSocket('ws://' + ws_host + '/ws/v1/meters') - this.socket.onerror = function() { + this.socket.onerror = function () { console.log('Websocket error') } this.socket.onmessage = (message) => { @@ -50,6 +61,22 @@ export const meters: Meters = { }, update(message): void { const peaks = message.data.split(" ") + let time = parseInt(peaks.shift()) + if (time) { + this.record.value = true + time = time / 1000 + const h = Math.floor(time / (60 * 60)) + time = time % (60 * 60) + const m = Math.floor(time / 60) + time = time % 60 + const s = Math.floor(time) + + this.record_timer.value = pad(h, 1) + ':' + pad(m, 2) + ':' + pad(s, 2) + } + else { + this.record.value = false + } + peaks.forEach(update_meters) } } From 46d7e6ad27cb07c5726bf5531168519e8872aa4e Mon Sep 17 00:00:00 2001 From: Sebastian Reimers Date: Sun, 18 Jan 2026 17:14:59 +0100 Subject: [PATCH 3/5] mk: add flac and upgrade opus --- Makefile | 17 ++++++++++++++++- versions.mk | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index db604cb..04fdcac 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,9 @@ lmdb: third_party/lmdb .PHONY: libvpx libvpx: third_party/libvpx +.PHONY: flac +flac: third_party/flac + .PHONY: cacert cacert: third_party/cacert.pem @@ -100,7 +103,19 @@ third_party_dir: .PHONY: third_party third_party: third_party_dir libvpx openssl opus samplerate portaudio lmdb \ - cacert + flac cacert + + +third_party/flac: + $(HIDE)cd third_party && \ + wget ${FLAC_MIRROR}/flac-${FLAC_VERSION}.tar.xz && \ + tar -xvf flac-${FLAC_VERSION}.tar.xz && \ + mv flac-${FLAC_VERSION} flac && cd flac && \ + $(_ARCH_CONFIGURE) \ + --disable-ogg --enable-static --disable-cpplibs && \ + make -j4 && \ + cp src/libFLAC/.libs/libFLAC.a ../lib/ && \ + cp -a include/FLAC ../include third_party/libvpx: $(HIDE)cd third_party && \ diff --git a/versions.mk b/versions.mk index a6bb413..f071c80 100644 --- a/versions.mk +++ b/versions.mk @@ -1,7 +1,9 @@ OPENSSL_VERSION := 3.6.0 OPENSSL_MIRROR := https://www.openssl.org/source -OPUS_VERSION := 1.6 +OPUS_VERSION := 1.6.1 OPUS_MIRROR := https://downloads.xiph.org/releases/opus +FLAC_VERSION := 1.5.0 +FLAC_MIRROR := https://ftp.osuosl.org/pub/xiph/releases/flac VPX_VERSION := v1.15.2 VPX_MIRROR := https://chromium.googlesource.com/webm/libvpx PORTAUDIO_VERSION := master From 9bb97d3a740804af4ec34e09c18e0b6223fa4b02 Mon Sep 17 00:00:00 2001 From: Sebastian Reimers Date: Sun, 18 Jan 2026 17:27:02 +0100 Subject: [PATCH 4/5] cmake: add FindFLAC --- cmake/FindFLAC.cmake | 9 +++++++++ libsl/CMakeLists.txt | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 cmake/FindFLAC.cmake diff --git a/cmake/FindFLAC.cmake b/cmake/FindFLAC.cmake new file mode 100644 index 0000000..c465f89 --- /dev/null +++ b/cmake/FindFLAC.cmake @@ -0,0 +1,9 @@ +FIND_PATH(FLAC_INCLUDE_DIR FLAC/metadata.h HINTS include) + +FIND_LIBRARY(FLAC_LIBRARIES NAMES FLAC HINTS lib) + +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(FLAC DEFAULT_MSG FLAC_LIBRARIES FLAC_INCLUDE_DIR) + +# show the FLAC_INCLUDE_DIR and FLAC_LIBRARIES variables only in the advanced view +MARK_AS_ADVANCED(FLAC_INCLUDE_DIR FLAC_LIBRARIES) diff --git a/libsl/CMakeLists.txt b/libsl/CMakeLists.txt index dbac49d..6f351f9 100644 --- a/libsl/CMakeLists.txt +++ b/libsl/CMakeLists.txt @@ -11,6 +11,7 @@ project(sl LANGUAGES C) find_package(LMDB REQUIRED) +find_package(FLAC REQUIRED) ############################################################################## # @@ -85,7 +86,7 @@ LIST(APPEND COMPILED_RESOURCES ${OUTPUT_FILE}) # # Main target object # -set(LINKLIBS baresip re FLAC ${LMDB_LIBRARIES} stdc++) +set(LINKLIBS baresip re ${FLAC_LIBRARIES} ${LMDB_LIBRARIES} stdc++) if(WIN32) list(APPEND LINKLIBS winmm setupapi) From c0dc8954e50695c259b2e6cd4448bcc5949981a3 Mon Sep 17 00:00:00 2001 From: Sebastian Reimers Date: Sun, 18 Jan 2026 17:31:41 +0100 Subject: [PATCH 5/5] use EPROTO --- libsl/src/flac.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libsl/src/flac.c b/libsl/src/flac.c index e0510c0..9b8c637 100644 --- a/libsl/src/flac.c +++ b/libsl/src/flac.c @@ -160,5 +160,5 @@ int sl_flac_record(struct flac *flac, struct auframe *af, uint64_t offset) warning("record: FLAC ENCODE ERROR: %s\n", FLAC__StreamEncoderStateString[state]); - return EBADFD; + return EPROTO; }