Skip to content

Commit 9a738c9

Browse files
committed
add acap/fluidsynth
To replace sdl3_mixer that does no longer support MIDI playback, thus unusable for our use case. - song1 needs to be static included potentially from 2 compilation units - this and sdl_mixer
1 parent 755ee51 commit 9a738c9

File tree

4 files changed

+385
-2
lines changed

4 files changed

+385
-2
lines changed

configure.ac

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3444,6 +3444,28 @@ fi
34443444

34453445
ENSURE_FEATURE_PRESENT([$sdl_mixer_req], [$sdl_mixer], [SDL_mixer deps not found!])
34463446

3447+
# ---------------------------------------------------------------------
3448+
# fluidsynt audio capture synthesizer
3449+
# ---------------------------------------------------------------------
3450+
AC_ARG_ENABLE(fluidsynth,
3451+
AS_HELP_STRING([--disable-fluidsynth],
3452+
[disable fluidtynth audio capture (default is auto)]),
3453+
[fluidsynth_req=$enableval],
3454+
[fluidsynth_req=$build_default])
3455+
fluidsynth=no
3456+
3457+
if test "${fluidsynth_req?}" != no; then
3458+
PKG_CHECK_MODULES([FLUIDSYNTH], [fluidsynth], [found_fluidsynth=yes],
3459+
[found_fluidsynth=no])
3460+
if test "${found_fluidsynth?}" = yes; then
3461+
add_module acap_fluidsynth src/audio/capture/fluidsynth.o "$FLUIDSYNTH_LIBS"
3462+
INC="$INC${FLUIDSYNTH_CFLAGS:+ $FLUIDSYNTH_CFLAGS}"
3463+
fluidsynth=yes
3464+
fi
3465+
fi
3466+
3467+
ENSURE_FEATURE_PRESENT([$fluidsynth_req], [$fluidsynth], [fluidsynth not found!])
3468+
34473469
# -----------------------------------------------------------------------------
34483470
# Reflector
34493471
# -----------------------------------------------------------------------------
@@ -3620,6 +3642,7 @@ RESULT=`end_section "$RESULT"`
36203642
RESULT=`start_section "$RESULT" "Audio"`
36213643
RESULT=`add_column "$RESULT" "ALSA" $alsa $?`
36223644
RESULT=`add_column "$RESULT" "CoreAudio" $coreaudio $?`
3645+
RESULT=`add_column "$RESULT" "FluidSynth" $fluidsynth $?`
36233646
RESULT=`add_column "$RESULT" "JACK" $jack $?`
36243647
RESULT=`add_column "$RESULT" "JACK transport" $jack_trans $?`
36253648
RESULT=`add_column "$RESULT" "SDL_mixer" $sdl_mixer $?`

src/audio/capture/fluidsynth.c

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
/**
2+
* @file audio/capture/fluidsynth.c
3+
* @author Martin Pulec <[email protected]>
4+
*/
5+
/*
6+
* Copyright (c) 2025 CESNET
7+
* All rights reserved.
8+
*
9+
* Redistribution and use in source and binary forms, with or without
10+
* modification, is permitted provided that the following conditions
11+
* are met:
12+
*
13+
* 1. Redistributions of source code must retain the above copyright
14+
* notice, this list of conditions and the following disclaimer.
15+
*
16+
* 2. Redistributions in binary form must reproduce the above copyright
17+
* notice, this list of conditions and the following disclaimer in the
18+
* documentation and/or other materials provided with the distribution.
19+
*
20+
* 3. Neither the name of CESNET nor the names of its contributors may be
21+
* used to endorse or promote products derived from this software without
22+
* specific prior written permission.
23+
*
24+
* THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
25+
* "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
26+
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
27+
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
28+
* EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
29+
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
30+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
31+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
32+
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
34+
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
35+
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36+
*/
37+
38+
#include <assert.h> // for assert
39+
#include <fluidsynth.h> // for fluid_player_play, fluid_synth_writ...
40+
#include <fluidsynth/types.h> // for fluid_player_t, fluid_settings_t
41+
#include <stdbool.h> // for bool
42+
#include <stdio.h> // for NULL, fclose, size_t, FILE, fopen
43+
#include <stdlib.h> // for free, getenv, malloc, calloc
44+
#include <string.h> // for strdup, strlen, strncat, strcmp
45+
#include <unistd.h> // for unlink
46+
47+
#include "audio/audio_capture.h" // for AUDIO_CAPTURE_ABI_VERSION, audio_ca...
48+
#include "audio/types.h" // for audio_frame
49+
#include "audio/utils.h" // for mux_channel
50+
#include "debug.h" // for LOG_LEVEL_ERROR, MSG, log_msg, LOG_...
51+
#include "host.h" // for audio_capture_sample_rate, INIT_NOERR
52+
#include "lib_common.h" // for REGISTER_MODULE, library_class
53+
#include "song1.h" // for song1
54+
#include "tv.h" // for get_time_in_ns, time_ns_t, NS_IN_SE...
55+
#include "types.h" // for device_info
56+
#include "utils/color_out.h" // for color_printf, TBOLD, TRED
57+
#include "utils/fs.h" // for get_install_root, get_temp_file
58+
#include "utils/macros.h" // for IS_KEY_PREFIX
59+
60+
struct module;
61+
62+
enum {
63+
FLUIDSYNTH_BPS = 2,
64+
DEFAULT_FLUIDSYNTH_SAMPLE_RATE = 48000,
65+
CHUNK_SIZE = 480,
66+
};
67+
68+
#define MOD_NAME "[fluidsynth] "
69+
70+
struct state_fluidsynth_capture {
71+
struct audio_frame audio;
72+
unsigned char *left;
73+
unsigned char *right;
74+
75+
char *req_filename;
76+
const char *tmp_filename;
77+
78+
time_ns_t next_frame_time;
79+
time_ns_t frame_interval;
80+
;
81+
82+
fluid_settings_t *settings;
83+
fluid_synth_t *synth;
84+
fluid_player_t *player;
85+
};
86+
87+
static void audio_cap_fluidsynth_done(void *state);
88+
89+
static void
90+
audio_cap_fluidsynth_probe(struct device_info **available_devices, int *count,
91+
void (**deleter)(void *))
92+
{
93+
*deleter = free;
94+
*count = 1;
95+
*available_devices = calloc(1, sizeof **available_devices);
96+
strncat((*available_devices)[0].dev, "fluidsynth",
97+
sizeof(*available_devices)[0].dev - 1);
98+
strncat((*available_devices)[0].name, "Sample midi song",
99+
sizeof(*available_devices)[0].name - 1);
100+
}
101+
102+
static void
103+
usage()
104+
{
105+
color_printf(
106+
TBOLD("fluidsynth") " is a capture device capable playing MIDI.\n\n"
107+
"The main functional difference to " TBOLD(
108+
"file") " video capture (that is able to "
109+
"play audio\n"
110+
"files as well) is the support "
111+
"for " TBOLD(
112+
"MIDI") " (and also having one "
113+
"song bundled).\n\n");
114+
color_printf("Usage:\n");
115+
color_printf(TBOLD(TRED("\t-s fluidsynth") "[:file=<filename>]") "\n");
116+
color_printf("where\n");
117+
color_printf(TBOLD("\t<filename>") " - name of file to be used\n");
118+
color_printf("\n");
119+
color_printf(TBOLD(
120+
"FLUIDSYNTH_SF") " - environment variable with path to "
121+
"sound fonts for MIDI playback (eg. freepats)\n");
122+
color_printf(
123+
TBOLD("ULTRAGRID_BUNDLED_SF") " - set this environment variable to "
124+
"1 to skip loading system default "
125+
"sound font\n\n");
126+
}
127+
128+
static int
129+
parse_opts(struct state_fluidsynth_capture *s, char *cfg)
130+
{
131+
char *save_ptr = NULL;
132+
char *item = NULL;
133+
while ((item = strtok_r(cfg, ":", &save_ptr)) != NULL) {
134+
cfg = NULL;
135+
if (strcmp(item, "help") == 0) {
136+
usage();
137+
return 1;
138+
}
139+
if (IS_KEY_PREFIX(item, "file")) {
140+
s->req_filename = strdup(strchr(item, '=') + 1);
141+
} else {
142+
log_msg(LOG_LEVEL_ERROR, MOD_NAME "Wrong option: %s!\n",
143+
item);
144+
color_printf("Use " TBOLD(
145+
"-s fluidsynth:help") " to see available "
146+
"options.\n");
147+
return -1;
148+
}
149+
}
150+
return 0;
151+
}
152+
153+
static const char *
154+
load_song1()
155+
{
156+
const char *filename = NULL;
157+
FILE *f = get_temp_file(&filename);
158+
if (f == NULL) {
159+
perror("fopen audio");
160+
return NULL;
161+
}
162+
size_t nwritten = fwrite(song1, sizeof song1, 1, f);
163+
fclose(f);
164+
if (nwritten != 1) {
165+
unlink(filename);
166+
return NULL;
167+
}
168+
return filename;
169+
}
170+
171+
/**
172+
* Try to preload a sound font.
173+
*
174+
* This is mainly intended to allow loading sound fonts from application bundle
175+
* on various platforms (get_install_root is relative to executable). But use
176+
* to system default font, if available.
177+
*/
178+
static char *
179+
get_soundfont()
180+
{
181+
const char *env_fs = getenv("FLUIDSYNTH_SF");
182+
if (env_fs != NULL) {
183+
return strdup(env_fs);
184+
}
185+
const bool force_bundled_sf =
186+
getenv("ULTRAGRID_BUNDLED_SF") != NULL &&
187+
strcmp(getenv("ULTRAGRID_BUNDLED_SF"), "1") == 0;
188+
const char *roots[2] = { "/usr", get_install_root() };
189+
if (force_bundled_sf) {
190+
roots[0] = get_install_root();
191+
roots[1] = "/usr";
192+
}
193+
const char *sf_candidates[] = {
194+
// without install prefix
195+
"/share/soundfonts/default.sf2",
196+
"/share/soundfonts/default.sf3",
197+
"/share/sounds/sf2/default-GM.sf2",
198+
"/share/sounds/sf2/default-GM.sf3", // Ubuntu
199+
};
200+
for (size_t i = 0; i < sizeof roots / sizeof roots[0]; ++i) {
201+
for (size_t j = 0;
202+
j < sizeof sf_candidates / sizeof sf_candidates[0]; ++j) {
203+
const char *root = roots[i];
204+
const size_t len =
205+
strlen(root) + strlen(sf_candidates[j]) + 1;
206+
char path[len];
207+
strncpy(path, root, len - 1);
208+
strncat(path, sf_candidates[j], len - strlen(path) - 1);
209+
FILE *f = fopen(path, "rb");
210+
debug_msg(MOD_NAME
211+
"Trying to open sound font '%s': %s\n",
212+
path, f ? "success, setting" : "failed");
213+
if (!f) {
214+
continue;
215+
}
216+
fclose(f);
217+
return strdup(path);
218+
}
219+
}
220+
MSG(ERROR, "Cannot find any suitable sound font!\n");
221+
return NULL;
222+
}
223+
224+
static void *
225+
audio_cap_fluidsynth_init(struct module *parent, const char *cfg)
226+
{
227+
(void) parent;
228+
struct state_fluidsynth_capture *s = calloc(1, sizeof *s);
229+
char *ccfg = strdup(cfg);
230+
int ret = parse_opts(s, ccfg);
231+
free(ccfg);
232+
if (ret != 0) {
233+
audio_cap_fluidsynth_done(s);
234+
return ret < 0 ? NULL : INIT_NOERR;
235+
}
236+
237+
char *sf = get_soundfont();
238+
if (sf == NULL) {
239+
audio_cap_fluidsynth_done(s);
240+
return NULL;
241+
}
242+
243+
s->audio.bps = FLUIDSYNTH_BPS;
244+
s->audio.ch_count = audio_capture_channels < 2 ? 1 : 2;
245+
s->audio.sample_rate = audio_capture_sample_rate > 0
246+
? audio_capture_sample_rate
247+
: DEFAULT_FLUIDSYNTH_SAMPLE_RATE;
248+
249+
const char *filename = s->req_filename;
250+
if (!filename) {
251+
filename = s->tmp_filename = load_song1();
252+
if (!filename) {
253+
goto error;
254+
}
255+
}
256+
257+
s->settings = new_fluid_settings();
258+
fluid_settings_setnum(s->settings, "synth.sample-rate",
259+
s->audio.sample_rate);
260+
261+
s->synth = new_fluid_synth(s->settings);
262+
if (fluid_synth_sfload(s->synth, sf, 1) < 0) {
263+
MSG(ERROR, "Failed to load SF2: %s\n", sf);
264+
goto error;
265+
}
266+
s->player = new_fluid_player(s->synth);
267+
if (fluid_player_add(s->player, filename) != FLUID_OK) {
268+
MSG(ERROR, "Failed to add MIDI: %s\n", s->req_filename);
269+
goto error;
270+
}
271+
fluid_player_play(s->player);
272+
273+
s->audio.max_size = s->audio.data_len =
274+
s->audio.ch_count * s->audio.bps * CHUNK_SIZE;
275+
s->audio.data = malloc(s->audio.data_len);
276+
s->left = malloc(s->audio.data_len / s->audio.ch_count);
277+
s->right = malloc(s->audio.data_len / s->audio.ch_count);
278+
279+
s->frame_interval = CHUNK_SIZE * NS_IN_SEC_DBL / s->audio.sample_rate;
280+
s->next_frame_time = get_time_in_ns() + s->frame_interval;
281+
282+
log_msg(LOG_LEVEL_NOTICE, MOD_NAME "Initialized fluidsynth\n");
283+
284+
free(sf);
285+
return s;
286+
error:
287+
audio_cap_fluidsynth_done(s);
288+
free(sf);
289+
return NULL;
290+
}
291+
292+
static struct audio_frame *
293+
audio_cap_fluidsynth_read(void *state)
294+
{
295+
struct state_fluidsynth_capture *s = state;
296+
297+
if (fluid_player_get_status(s->player) == FLUID_PLAYER_DONE) {
298+
MSG(VERBOSE, "Rewinding...\n");
299+
fluid_player_play(s->player);
300+
}
301+
302+
if (s->audio.ch_count == 1) {
303+
fluid_synth_write_s16(s->synth, CHUNK_SIZE, s->audio.data, 0, 1,
304+
s->right, 0, 1);
305+
} else {
306+
assert(s->audio.ch_count == 2);
307+
fluid_synth_write_s16(s->synth, CHUNK_SIZE, s->left, 0, 1,
308+
s->right, 0, 1);
309+
mux_channel(s->audio.data, (char *) s->left, s->audio.bps,
310+
s->audio.bps * CHUNK_SIZE, s->audio.ch_count, 0,
311+
1.);
312+
mux_channel(s->audio.data, (char *) s->right, s->audio.bps,
313+
s->audio.bps * CHUNK_SIZE, s->audio.ch_count, 1,
314+
1.);
315+
}
316+
317+
time_ns_t t = get_time_in_ns();
318+
if (t > s->next_frame_time + s->frame_interval) {
319+
MSG(WARNING, "Some data missed!\n");
320+
t = s->next_frame_time;
321+
} else {
322+
while (t < s->next_frame_time) {
323+
t = get_time_in_ns();
324+
}
325+
}
326+
s->next_frame_time += s->frame_interval;
327+
328+
return &s->audio;
329+
}
330+
331+
static void
332+
audio_cap_fluidsynth_done(void *state)
333+
{
334+
struct state_fluidsynth_capture *s = state;
335+
free(s->audio.data);
336+
free(s->req_filename);
337+
free(s->left);
338+
free(s->right);
339+
if (s->tmp_filename) {
340+
unlink(s->tmp_filename);
341+
}
342+
free(s);
343+
}
344+
345+
static const struct audio_capture_info acap_fluidsynth_info = {
346+
audio_cap_fluidsynth_probe, audio_cap_fluidsynth_init,
347+
audio_cap_fluidsynth_read, audio_cap_fluidsynth_done
348+
};
349+
350+
REGISTER_MODULE(fluidsynth, &acap_fluidsynth_info, LIBRARY_CLASS_AUDIO_CAPTURE,
351+
AUDIO_CAPTURE_ABI_VERSION);

src/audio/capture/song1.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* THIS CHUNK OF BYTES IS AUTOMATICALLY GENERATED */
2-
unsigned char song1[] =
2+
static const unsigned char song1[] =
33
{
44
0x4d, 0x54, 0x68, 0x64, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0x0d, 0x00, 0x60, 0x4d, 0x54,
55
0x72, 0x6b, 0x00, 0x00, 0x00, 0x1a, 0x00, 0xff, 0x7f, 0x03, 0x00, 0x00, 0x41, 0x00, 0xff, 0x58,

0 commit comments

Comments
 (0)