Skip to content

Commit 9b9e98c

Browse files
committed
fix: route portamento notes through setNote path, not fast-path triggerAttackRelease
1 parent 338362b commit 9b9e98c

2 files changed

Lines changed: 73 additions & 1 deletion

File tree

js/utils/__tests__/synthutils.test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,77 @@ describe("Utility Functions (logic-only)", () => {
15121512
});
15131513
});
15141514

1515+
describe("_performNotes glide/portamento setNote routing ", () => {
1516+
it("should call setNote (continuous glide) instead of triggerAttackRelease when doPortamento and setNote are true", async () => {
1517+
const mockSynth = {
1518+
oscillator: {},
1519+
toDestination: jest.fn().mockReturnThis(),
1520+
triggerAttackRelease: jest.fn(),
1521+
setNote: jest.fn(),
1522+
chain: jest.fn().mockReturnThis(),
1523+
disconnect: jest.fn(),
1524+
connect: jest.fn()
1525+
};
1526+
Synth.inTemperament = "equal";
1527+
1528+
const paramsEffects = {
1529+
doPortamento: true,
1530+
portamento: 0.05
1531+
};
1532+
1533+
await _performNotes.call(Synth, mockSynth, "D4", 0.5, paramsEffects, null, true, 0);
1534+
1535+
// This is the regression check: glide notes must continue the same
1536+
// voice via setNote, not retrigger a fresh attack+release.
1537+
expect(mockSynth.setNote).toHaveBeenCalledWith("D4");
1538+
expect(mockSynth.triggerAttackRelease).not.toHaveBeenCalled();
1539+
});
1540+
1541+
it("should still call triggerAttackRelease for a portamento note when setNote is false", async () => {
1542+
const mockSynth = {
1543+
oscillator: {},
1544+
toDestination: jest.fn().mockReturnThis(),
1545+
triggerAttackRelease: jest.fn(),
1546+
setNote: jest.fn(),
1547+
chain: jest.fn().mockReturnThis(),
1548+
disconnect: jest.fn(),
1549+
connect: jest.fn()
1550+
};
1551+
Synth.inTemperament = "equal";
1552+
1553+
const paramsEffects = {
1554+
doPortamento: true,
1555+
portamento: 0.05
1556+
};
1557+
1558+
await _performNotes.call(Synth, mockSynth, "C4", 0.5, paramsEffects, null, false, 0);
1559+
1560+
// Non-continuation notes (setNote=false) should still play normally.
1561+
expect(mockSynth.triggerAttackRelease).toHaveBeenCalled();
1562+
expect(mockSynth.setNote).not.toHaveBeenCalled();
1563+
});
1564+
1565+
it("should route plain (non-portamento) notes through the fast path unaffected", async () => {
1566+
const mockSynth = {
1567+
toDestination: jest.fn().mockReturnThis(),
1568+
triggerAttackRelease: jest.fn(),
1569+
setNote: jest.fn()
1570+
};
1571+
Synth.inTemperament = "equal";
1572+
1573+
const paramsEffects = {
1574+
doPartials: true,
1575+
partials: [1]
1576+
};
1577+
1578+
await _performNotes.call(Synth, mockSynth, "E4", 0.5, paramsEffects, null, true, 0);
1579+
1580+
// No portamento involved — fast path is fine here, no regression expected.
1581+
expect(mockSynth.triggerAttackRelease).toHaveBeenCalled();
1582+
expect(mockSynth.setNote).not.toHaveBeenCalled();
1583+
});
1584+
});
1585+
15151586
describe("_performNotes effects routing and cleanup", () => {
15161587
it("should reconnect synth to destination and disconnect old routing when effects complete", async () => {
15171588
jest.useFakeTimers();

js/utils/synthutils.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1872,7 +1872,8 @@ function Synth() {
18721872
paramsEffects.doTremolo ||
18731873
paramsEffects.doPhaser ||
18741874
paramsEffects.doChorus ||
1875-
paramsEffects.doNeighbor));
1875+
paramsEffects.doNeighbor ||
1876+
(paramsEffects.doPortamento && setNote)));
18761877

18771878
if (!_needsGraphRewire) {
18781879
// Apply in-place property mutations then take the fast path.

0 commit comments

Comments
 (0)