diff --git a/src/engine/SynthEngine.ts b/src/engine/SynthEngine.ts index be087f8d..e3a997ef 100644 --- a/src/engine/SynthEngine.ts +++ b/src/engine/SynthEngine.ts @@ -69,6 +69,10 @@ function linearGainToDb(value: number): number { return 20 * Math.log10(Math.max(MIN_LINEAR_GAIN, value)); } +function getLegacySlidePortamentoSeconds(duration: number): number { + return Math.max(0.03, Math.min(0.12, duration * 0.35)); +} + function toLegacySubtractivePreset(preset: SynthPreset): LegacySynthVoicePreset { return preset === 'sampler' ? 'piano' : preset; } @@ -223,6 +227,17 @@ export function createSynthRuntimeSpec(source: SynthSource): SynthRuntimeSpec { return createSubtractiveRuntimeSpec(resolveSubtractiveInstrument(source)); } +export function resolveSlidePortamentoSeconds(source: SynthSource, duration: number): number { + const spec = createSynthRuntimeSpec(source); + const configured = typeof spec.options.portamento === 'number' + ? Math.max(0, spec.options.portamento) + : 0; + + return configured > 0 + ? configured + : getLegacySlidePortamentoSeconds(duration); +} + export function createSynthModulationSpec(source: SynthSource): SynthModulationSpec | null { const instrument = resolveSubtractiveLfoInstrument(source); if (!instrument) return null; @@ -506,7 +521,7 @@ class SynthEngine { triggerAttackRelease: (note: number, duration: number, time?: string | number, velocity?: number) => void; }; restartPlaybackModulation(this.synths.get(trackId)); - const glideTime = Math.max(0.03, Math.min(0.12, duration * 0.35)); + const glideTime = resolveSlidePortamentoSeconds(source, duration); const fromFreq = Tone.Frequency(fromPitch, 'midi').toFrequency(); const toFreq = Tone.Frequency(toPitch, 'midi').toFrequency(); synth.set({ portamento: glideTime }); diff --git a/src/engine/__tests__/SynthEngine.test.ts b/src/engine/__tests__/SynthEngine.test.ts index b673fe40..007fc397 100644 --- a/src/engine/__tests__/SynthEngine.test.ts +++ b/src/engine/__tests__/SynthEngine.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { createSynthModulationSpec, createSynthRuntimeSpec } from '../SynthEngine'; +import { createSynthModulationSpec, createSynthRuntimeSpec, resolveSlidePortamentoSeconds } from '../SynthEngine'; import { createDefaultFmInstrument, createDefaultSamplerInstrument, @@ -244,3 +244,28 @@ describe('createSynthModulationSpec', () => { expect(options.depth).toBeCloseTo(0.85, 5); }); }); + +describe('resolveSlidePortamentoSeconds', () => { + it('uses canonical subtractive glide time when a positive glide value is configured', () => { + const instrument = createDefaultSubtractiveInstrument('lead', { + settings: { + glideTime: 0.48, + }, + }); + + expect(resolveSlidePortamentoSeconds(instrument, 0.2)).toBeCloseTo(0.48, 5); + }); + + it('falls back to the legacy slide heuristic for zero-glide subtractive, FM, and preset sources', () => { + const subtractive = createDefaultSubtractiveInstrument('pad', { + settings: { + glideTime: 0, + }, + }); + const fm = createDefaultFmInstrument(); + + expect(resolveSlidePortamentoSeconds(subtractive, 0.5)).toBeCloseTo(0.12, 5); + expect(resolveSlidePortamentoSeconds(fm, 0.4)).toBeCloseTo(0.12, 5); + expect(resolveSlidePortamentoSeconds('lead', 0.04)).toBeCloseTo(0.03, 5); + }); +});