Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5b908c2
Initial plan
Copilot Sep 29, 2025
c6ac04e
Implement global portamento time mode feature
Copilot Sep 29, 2025
5260789
Add functional test and complete portamento feature implementation
Copilot Sep 29, 2025
eadb9b3
Merge remote-tracking branch 'origin/master' into copilot/fix-14b2d64…
derselbst Sep 29, 2025
6f78dc1
Address some review findings
derselbst Sep 29, 2025
b70ce6d
Merge remote-tracking branch 'origin/master' into copilot/fix-14b2d64…
derselbst Sep 29, 2025
6a8f5f6
continue
derselbst Sep 29, 2025
422cb2a
Merge remote-tracking branch 'origin/master' into copilot/fix-14b2d64…
derselbst Sep 29, 2025
60bc4d3
implement portamento time correctly
derselbst Sep 29, 2025
cd99101
make it realtime setings
derselbst Sep 29, 2025
7d87004
update gitignore
derselbst Sep 30, 2025
0a78347
document
derselbst Sep 30, 2025
41828d0
finish test
derselbst Sep 30, 2025
3806c6d
rename synth.portamento-mode -> synth.portamento-time
derselbst Sep 30, 2025
c56b36f
implement porta time scale hack
derselbst Sep 30, 2025
dfcce35
Change default portamento mode
derselbst Sep 30, 2025
d877255
reduce duplicate code a bit
derselbst Sep 30, 2025
405f3db
change scaling to 36 semitones
derselbst Oct 1, 2025
a26946a
tweak curve again
derselbst Oct 1, 2025
4852406
yet better tweak
derselbst Oct 1, 2025
ce1acb0
Update src/synth/fluid_chan.c
derselbst Oct 1, 2025
85c9343
fix portamento test
derselbst Oct 3, 2025
c30d2c5
Merge remote-tracking branch 'origin/master' into copilot/fix-14b2d64…
derselbst Oct 4, 2025
05cc085
update api docs
derselbst Oct 4, 2025
5e6aac9
microopt
derselbst Oct 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.DS_Store
build/
install/

CMakeCache.txt
CMakeFiles
Expand Down Expand Up @@ -40,3 +41,4 @@ install_manifest.txt
# ProjectFiles
*.pro.user*
*.user
*.vscode
15 changes: 15 additions & 0 deletions doc/fluidsettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,21 @@ Developers:
<desc>
The polyphony defines how many voices can be played in parallel. A note event produces one or more voices. Its good to set this to a value which the system can handle and will thus limit FluidSynth's CPU usage. When FluidSynth runs out of voices it will begin terminating lower priority voices for new note events.</desc>
</setting>
<setting>
<name>portamento-time</name>
<type>str</type>
<def>auto</def>
<realtime/>
<vals>auto, linear, xg-gs</vals>
<desc>
This setting was introduced in 2.5.0 to specify how to handle portamento time CC MSB and LSB.
<ul>
<li>'linear' - portamento time is 14 bits wide and calculated as <code>CC5 * 128 + CC37</code> in milliseconds (this was fluidsynth's behavior before this setting was introduced).</li>
<li>'xg-gs' - portamento time is 7 bits wide, using only CC5, which is mapped onto a concave curve ranging from 0s to 480s. The exact mapping was derived through listening tests and may be subject to change between different versions of fluidsynth. This setting should be used for older MIDI files, like Descent Game08.</li>
<li>'auto' - the synth starts in 'xg-gs' mode. If it detects a CC37 on any MIDI channel, it switches to 'linear' mode.</li>
</ul>
</desc>
</setting>
<setting>
<name>reverb.active</name>
<type>bool</type>
Expand Down
1 change: 1 addition & 0 deletions doc/recent_changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- fluid_mod_get_transform() now receives a <code>const</code> argument
- fluid_version_str() now returns a <code>const</code> char array
- An API for manipulating fluid_sfont_t specific default modulators has been added: fluid_sfont_get_default_mod() and fluid_sfont_set_default_mod()
- Support for specifying a synth-wide mode to interpret portamento time has been added, see #fluid_portamento_time_mode, fluid_synth_get_portamento_time_mode(), fluid_synth_set_portamento_time_mode() and setting "synth.portamento-time"

\section NewIn2_4_5 What's new in 2.4.5?
- In order to use the sdl3 audio driver, the downstream application is responsible for calling <code>SDL_Init()</code> and <code>SDL_Quit()</code>, just like it was practice for the sdl2 audio driver. Fluidsynth may raise a warning if this isn't done, see \ref CreatingAudioDriver
Expand Down
21 changes: 21 additions & 0 deletions include/fluidsynth/synth.h
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,27 @@ FLUIDSYNTH_API int fluid_synth_set_breath_mode(fluid_synth_t *synth,
FLUIDSYNTH_API int fluid_synth_get_breath_mode(fluid_synth_t *synth,
int chan, int *breathmode);
/** @} Breath Mode */

/** @name Portamento Time Mode
* @{
*/

/**
* Indicates the portamento time mode the synthesizer is set to
*/
enum fluid_portamento_time_mode
{
FLUID_PORTAMENTO_TIME_MODE_AUTO, /**< Auto mode - Start with 7-bit MSB, switch to 14-bit when LSB seen */
FLUID_PORTAMENTO_TIME_MODE_XG_GS, /**< XG/GS mode - Always use 7-bit MSB only */
FLUID_PORTAMENTO_TIME_MODE_LINEAR, /**< Linear mode - Always use 14-bit MSB+LSB */
FLUID_PORTAMENTO_TIME_MODE_LAST /**< @internal Value defines the count of portamento time modes
@warning This symbol is not part of the public API and ABI
stability guarantee and may change at any time! */
};

FLUIDSYNTH_API int fluid_synth_set_portamento_time_mode(fluid_synth_t *synth, int mode);
FLUIDSYNTH_API int fluid_synth_get_portamento_time_mode(fluid_synth_t *synth, int *mode);
/** @} Portamento Time Mode */
/** @} MIDI Channel Setup */


Expand Down
68 changes: 67 additions & 1 deletion src/synth/fluid_chan.c
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ fluid_channel_init(fluid_channel_t *chan)
/*---*/
chan->key_mono_sustained = INVALID_NOTE; /* No previous mono note sustained */
chan->legatomode = FLUID_CHANNEL_LEGATO_MODE_MULTI_RETRIGGER; /* Default mode */
chan->portamentomode = FLUID_CHANNEL_PORTAMENTO_MODE_LEGATO_ONLY; /* Default mode */
chan->portamentomode = FLUID_CHANNEL_PORTAMENTO_MODE_EACH_NOTE; /* Default mode */
/*--- End of poly/mono initialization --------------------------------------*/

chan->channel_type = (chan->channum == 9) ? CHANNEL_TYPE_DRUM : CHANNEL_TYPE_MELODIC;
Expand Down Expand Up @@ -758,3 +758,69 @@ void fluid_channel_set_override_gen_default(fluid_channel_t *chan, int gen, flui
chan->override_gen_default[gen].flags = GEN_SET;
chan->override_gen_default[gen].val = val;
}

/* Calculate portamento time in milliseconds considering the synthesizer's portamento time mode */
unsigned int fluid_channel_portamentotime_with_mode(fluid_channel_t *chan, enum fluid_portamento_time_mode time_mode, int lsb_seen, int fromkey, int tokey)
{
int msb = fluid_channel_get_cc(chan, PORTAMENTO_TIME_MSB);
int lsb = fluid_channel_get_cc(chan, PORTAMENTO_TIME_LSB);
int res;
fluid_real_t tmp;
static const int Max = 480*1000; /*ms*/

/* Use 7-bit MSB initially, switch to 14-bit if LSB has been seen */
if(time_mode == FLUID_PORTAMENTO_TIME_MODE_AUTO)
{
if(lsb_seen)
{
time_mode = FLUID_PORTAMENTO_TIME_MODE_LINEAR;
}
else
{
time_mode = FLUID_PORTAMENTO_TIME_MODE_XG_GS;
}
}

switch(time_mode)
{
case FLUID_PORTAMENTO_TIME_MODE_XG_GS:
// Produce a curve similar to:
/*
CC 5 value Portamento time
---------- ---------------
0 0.000 s
1 0.006 s
2 0.023 s
4 0.050 s
8 0.110 s
16 0.250 s
32 0.500 s
64 2.060 s
80 4.200 s
96 8.400 s
112 19.500 s
116 26.700 s
120 40.000 s
124 80.000 s
127 480.000 s
*/
// Tests were performed by John Novak
// https://github.com/dosbox-staging/dosbox-staging/pull/2705
tmp = fluid_concave(msb);
res = (fluid_real_t)(Max/2 * 2.5) * tmp * fluid_concave(128 * tmp) + 400 * fluid_convex(msb * (fluid_real_t)(1/4.0));
res = res < Max ? res : Max;
// Apply a similar scaling hack as SpessaSynth to fix Descent Game08, it's unclear why exactly
// https://github.com/spessasus/spessasynth_core/blob/5a8730a80f8c0b74733ec193a968b36e2a0c0aee/src/synthesizer/audio_engine/engine_methods/portamento_time.ts#L84-87
// https://github.com/FluidSynth/fluidsynth/pull/1656#issuecomment-3355759938
res = (unsigned int)(res * abs(tokey - fromkey) / 36.0f + 0.5f);
return res;

case FLUID_PORTAMENTO_TIME_MODE_LINEAR:
/* Always use 14-bit MSB+LSB */
return msb * 128 + lsb;

default:
FLUID_LOG(FLUID_ERR, "THIS SHOULD NEVER HAPPEN! unknown portamento time mode %d", time_mode);
}
return 0;
}
6 changes: 3 additions & 3 deletions src/synth/fluid_chan.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,6 @@ fluid_real_t fluid_channel_get_key_pitch(fluid_channel_t *chan, int key);
((chan)->tuning_prog)
#define fluid_channel_set_tuning_prog(chan, prog) \
((chan)->tuning_prog = (prog))
#define fluid_channel_portamentotime(_c) \
((_c)->cc[PORTAMENTO_TIME_MSB] * 128 + (_c)->cc[PORTAMENTO_TIME_LSB])
#define fluid_channel_portamento(_c) ((_c)->cc[PORTAMENTO_SWITCH] >= 64)
#define fluid_channel_breath_msb(_c) ((_c)->cc[BREATH_MSB] > 0)
#define fluid_channel_clear_portamento(_c) ((_c)->cc[PORTAMENTO_CTRL] = INVALID_NOTE)
Expand Down Expand Up @@ -286,8 +284,10 @@ void fluid_channel_cc_breath_note_on_off(fluid_channel_t *chan, int value);
int fluid_channel_get_override_gen_default(fluid_channel_t *chan, int gen, fluid_real_t *val);
void fluid_channel_set_override_gen_default(fluid_channel_t *chan, int gen, fluid_real_t val);

/* Portamento time calculation with mode support */
unsigned int fluid_channel_portamentotime_with_mode(fluid_channel_t *chan, enum fluid_portamento_time_mode time_mode, int lsb_seen, int, int);

#ifdef __cplusplus
}
#endif

#endif /* _FLUID_CHAN_H */
141 changes: 139 additions & 2 deletions src/synth/fluid_synth.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,15 @@ static int fluid_synth_set_important_channels(fluid_synth_t *synth, const char *

static void fluid_synth_process_awe32_nrpn_LOCAL(fluid_synth_t *synth, int chan, int gen, int data, int data_lsb);

static int fluid_parse_portamento_time_str(const char* value);

/* Callback handlers for real-time settings */
static void fluid_synth_handle_gain(void *data, const char *name, double value);
static void fluid_synth_handle_polyphony(void *data, const char *name, int value);
static void fluid_synth_handle_device_id(void *data, const char *name, int value);
static void fluid_synth_handle_overflow(void *data, const char *name, double value);
static void fluid_synth_handle_important_channels(void *data, const char *name,
const char *value);
static void fluid_synth_handle_important_channels(void *data, const char *name, const char *value);
static void fluid_synth_handle_portamento_mode(void *data, const char *name, const char *value);
static void fluid_synth_handle_reverb_chorus_num(void *data, const char *name, double value);
static void fluid_synth_handle_reverb_chorus_int(void *data, const char *name, int value);

Expand Down Expand Up @@ -261,6 +263,11 @@ void fluid_synth_settings(fluid_settings_t *settings)

fluid_settings_register_int(settings, "synth.dynamic-sample-loading", 0, 0, 1, FLUID_HINT_TOGGLED);
fluid_settings_register_int(settings, "synth.note-cut", 0, 0, 2, 0);

fluid_settings_register_str(settings, "synth.portamento-time", "auto", 0);
fluid_settings_add_option(settings, "synth.portamento-time", "auto");
fluid_settings_add_option(settings, "synth.portamento-time", "xg-gs");
fluid_settings_add_option(settings, "synth.portamento-time", "linear");
}

/**
Expand Down Expand Up @@ -766,6 +773,8 @@ new_fluid_synth(fluid_settings_t *settings)
fluid_synth_handle_reverb_chorus_num, synth);
fluid_settings_callback_num(settings, "synth.chorus.speed",
fluid_synth_handle_reverb_chorus_num, synth);
fluid_settings_callback_str(settings, "synth.portamento-time",
fluid_synth_handle_portamento_mode, synth);

/* do some basic sanity checking on the settings */

Expand Down Expand Up @@ -848,6 +857,29 @@ new_fluid_synth(fluid_settings_t *settings)

synth->fromkey_portamento = INVALID_NOTE; /* disable portamento */

/* Initialize portamento time mode */
{
char *portamento_mode_str;
if(fluid_settings_dupstr(settings, "synth.portamento-time", &portamento_mode_str) == FLUID_OK)
{
int res = fluid_parse_portamento_time_str(portamento_mode_str);
if(res == FLUID_FAILED)
{
synth->portamento_time_mode = FLUID_PORTAMENTO_TIME_MODE_AUTO;
}
else
{
synth->portamento_time_mode = res;
}
FLUID_FREE(portamento_mode_str);
}
else
{
synth->portamento_time_mode = FLUID_PORTAMENTO_TIME_MODE_AUTO;
}
synth->portamento_time_has_seen_lsb = 0; /* Start in xg-gs mode for auto */
}

fluid_atomic_int_set(&synth->ticks_since_start, 0);
synth->tuning = NULL;
fluid_private_init(synth->tuning_iter);
Expand Down Expand Up @@ -2040,6 +2072,13 @@ fluid_synth_cc_LOCAL(fluid_synth_t *synth, int channum, int num)
fluid_channel_cc_breath_note_on_off(chan, value);

/* fall-through */
case PORTAMENTO_TIME_LSB:
/* Track LSB reception for auto portamento mode */
if(num == PORTAMENTO_TIME_LSB && synth->portamento_time_mode == FLUID_PORTAMENTO_TIME_MODE_AUTO)
{
synth->portamento_time_has_seen_lsb = 1;
}
/* fall-through */
default:
/* CC lsb shouldn't allowed to modulate (spec SF 2.01 - 8.2.1) */
/* However, as long fluidsynth will use only CC 7 bits resolution, it
Expand Down Expand Up @@ -2823,6 +2862,12 @@ fluid_synth_system_reset_LOCAL(fluid_synth_t *synth)
fluid_synth_update_mixer(synth, fluid_rvoice_mixer_reset_reverb, 0, 0.0f);
fluid_synth_update_mixer(synth, fluid_rvoice_mixer_reset_chorus, 0, 0.0f);

/* Reset auto portamento mode tracking */
if(synth->portamento_time_mode == FLUID_PORTAMENTO_TIME_MODE_AUTO)
{
synth->portamento_time_has_seen_lsb = 0;
}

return FLUID_OK;
}

Expand Down Expand Up @@ -8327,6 +8372,41 @@ static void fluid_synth_handle_important_channels(void *data, const char *name,
fluid_synth_api_exit(synth);
}

static int fluid_parse_portamento_time_str(const char* value)
{
int mode;
if(FLUID_STRCMP(value, "auto") == 0)
{
mode = FLUID_PORTAMENTO_TIME_MODE_AUTO;
}
else if(FLUID_STRCMP(value, "xg-gs") == 0)
{
mode = FLUID_PORTAMENTO_TIME_MODE_XG_GS;
}
else if(FLUID_STRCMP(value, "linear") == 0)
{
mode = FLUID_PORTAMENTO_TIME_MODE_LINEAR;
}
else
{
FLUID_LOG(FLUID_ERR, "Invalid portamento time mode '%s'. Valid modes: auto, xg-gs, linear", value);
mode = FLUID_FAILED;
}
return mode;
}

static void fluid_synth_handle_portamento_mode(void *data, const char *name, const char *value)
{
int mode;
fluid_synth_t *synth = (fluid_synth_t *)data;

mode = fluid_parse_portamento_time_str(value);
if(mode != FLUID_FAILED)
{
fluid_synth_set_portamento_time_mode(synth, mode);
}
}


/* API legato mode *********************************************************/

Expand Down Expand Up @@ -8434,6 +8514,63 @@ int fluid_synth_get_portamento_mode(fluid_synth_t *synth, int chan,
FLUID_API_RETURN(FLUID_OK);
}

/* API portamento time mode ************************************************/

/**
* Sets the global portamento time mode of the synthesizer.
*
* @param synth the synth instance.
* @param mode The portamento time mode as indicated by #fluid_portamento_time_mode.
*
* @return
* - #FLUID_OK on success.
* - #FLUID_FAILED
* - \a synth is NULL.
* - \a mode is invalid.
*/
int fluid_synth_set_portamento_time_mode(fluid_synth_t *synth, int mode)
{
/* checks parameters first */
fluid_return_val_if_fail(synth != NULL, FLUID_FAILED);
fluid_synth_api_enter(synth);

if(mode < 0 || mode >= FLUID_PORTAMENTO_TIME_MODE_LAST)
{
FLUID_API_RETURN(FLUID_FAILED);
}

synth->portamento_time_mode = (enum fluid_portamento_time_mode)mode;

/* Reset auto mode tracking when mode changes */
synth->portamento_time_has_seen_lsb = 0;

FLUID_API_RETURN(FLUID_OK);
}

/**
* Gets the global portamento time mode of the synthesizer.
*
* @param synth the synth instance.
* @param mode the address to store the portamento time mode to.
*
* @return
* - #FLUID_OK on success.
* - #FLUID_FAILED
* - \a synth is NULL.
* - \a mode is NULL.
*/
int fluid_synth_get_portamento_time_mode(fluid_synth_t *synth, int *mode)
{
/* checks parameters first */
fluid_return_val_if_fail(mode != NULL, FLUID_FAILED);
fluid_return_val_if_fail(synth != NULL, FLUID_FAILED);
fluid_synth_api_enter(synth);

*mode = synth->portamento_time_mode;

FLUID_API_RETURN(FLUID_OK);
}

/* API breath mode *********************************************************/

/**
Expand Down
4 changes: 4 additions & 0 deletions src/synth/fluid_synth.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ struct _fluid_synth_t
enum fluid_iir_filter_flags custom_filter_flags; /**< filter type of the user-defined filter currently used for all voices */
enum fluid_msgs_note_cut msgs_note_cut_mode;

/** Portamento time mode settings */
enum fluid_portamento_time_mode portamento_time_mode; /**< Global portamento time mode */
int portamento_time_has_seen_lsb; /**< Flag to track if LSB has been seen (for auto mode) */

fluid_iir_sincos_t iir_sincos_table[SINCOS_TAB_SIZE]; /**< Table of sin/cos values for IIR filter */
};

Expand Down
Loading