Shape notes with an Envelope and move numbers with its curve #335
game-maker / gate (push) Successful in 3m19s
game-maker / suite (push) Successful in 15m58s

Without an envelope a note set in hard and broke off 10 ms after its
end. An instrument now has an Envelope node with attack, decay,
sustain and release. On the way of the sound it is the volume. Its
output "value" moves a number of the note with the same curve, from
"From" to "To", for a filter sweep among others. A new Filter node
gives that sweep something to move.

- Envelope (instrument only): attack a straight line, decay and
  release ease out and are 60 dB of the way at their time. A release
  takes 10 ms at least. Defaults 0.005 / 0.1 / 1 / 0.1.
- The end of a note goes to the Envelope instead of the 10 ms fade. A
  Gate or Loop Sample under it plays on through the release, "Loop,
  then release" leaves its loop too, and the voice ends with the
  release. A One-shot under an Envelope of its own plays through.
- A note ending before attack and decay releases from the level it
  has reached. Chrome holds it with cancelAndHoldAtTime, Firefox gets
  the level worked out from the shape.
- New port type "envelope" (coral). It fits only a number input that
  says it can follow one (PortDef.followsEnvelope): Filter cutoff and
  resonance, Pitch, Gain, the EQ bands and Mix. Not the Delay, not the
  Envelope's own numbers. portsMeet() decides it for ConnectCommand,
  the drag and the drawn edges.
- On the audio thread every Envelope of a note is one
  ConstantSourceNode. A number it moves gets the curve through a
  WaveShaper, in equal ratios for a cutoff and a gain. A number an
  Envelope moves is no longer reached by a parameter.
- Filter (sound FX and instrument): low-, high- or band-pass, cutoff
  in Hz, resonance 0 to 1 as a Q. A low- or high-pass is flat at 0
  and rises 20 dB at 1, a band-pass goes from Q 0.7 to 20.
- Stop sound with "let ring" lets a note with an Envelope fall away
  in its release. Without it, and with Stop in the tab, it is cut.
- The same note again during a release is a voice of its own.
- The Envelope's numbers are set when the note starts. They can come
  from velocity or a parameter, later changes do not bend the note.

Tests:
- envelope.test.ts (new, 12): curve, ratios, Filter Q, compiling an
  Envelope, its value into each number, a Delay refused
- sound-library +7: attack and decay on the clock, release from the
  level reached, One-shot held, Stop sound with and without let ring,
  a cutoff and a pitch moved by the curve
- e2e 34: ALL PASSED, 50 checks (+2). Offline in Chrome the curve
  rendered 0.5 / 1 / 0.5005 at 0.05 / 0.1 / 0.3 s, released at 0.5 s
  it was 0.0158 at 0.65 s and 0.0005 at 0.8 s, released mid-attack it
  fell from 0.5 to 0.05 in 0.1 s. A Gain moved 0.25 to 1 was 0.5
  halfway. A looped note of 0.2 s with a release of 1 s was gone
  after 1.32 s.
- Five counter-checks (release end, One-shot hold, the follows check,
  ratios, pitch sum) failed 3, 1, 1, 2 and 2 tests.
- Gate on melektaus-atx: CHECK EXIT 0, 245 files, 3812 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 13:54:46 +02:00
1 parent fb4d65a2ab
commit 9c71eb1a0d
19 files changed
+1118 -105

No files matched your search

+18 -2
View File
@@ -2,9 +2,16 @@ import type { Application } from 'pixi.js';
import { physicsStatus } from '../runtime/physics/engine';
import type { SchedulerOptions } from '../core/music/Scheduler';
import type { Sequence } from '../core/music/sequence';
import type { FxStage } from '../core/soundfx/nodes';
import type { FxEnvelope, FxStage } from '../core/soundfx/nodes';
import { Music } from '../viewport/music';
import { bothAtCenter, soundLibrary, stageNodes } from '../viewport/SoundLibrary';
import {
bothAtCenter,
envelopeDriver,
followCurve,
releaseEnvelope,
soundLibrary,
stageNodes,
} from '../viewport/SoundLibrary';
import { curveHooks } from './debug/curveHooks';
import { documentHooks } from './debug/documentHooks';
import { graphHooks } from './debug/graphHooks';
@@ -61,6 +68,15 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
/** What drives the two shares of a Mix "Both at center", in any context (unstarted). */
bothAtCenter: (context: BaseAudioContext, mix: number, a: GainNode, b: GainNode) =>
bothAtCenter(context, mix, a, b),
/** The curve of an Envelope from `at` (unstarted), its release, and how it moves a number (#335). */
envelopeDriver: (context: BaseAudioContext, envelope: FxEnvelope, at: number) =>
envelopeDriver(context, envelope, at),
releaseEnvelope: (level: AudioParam, envelope: FxEnvelope, startedAt: number, at: number) =>
releaseEnvelope(level, envelope, startedAt, at),
stageWith: (context: BaseAudioContext, stage: FxStage, driver: ConstantSourceNode) =>
stageNodes(context, stage, (mod, param, ratio, map) =>
followCurve(context, driver, mod, param, ratio, map),
),
},
/**
* The scheduler (#332), which nothing in the editor plays yet: a check hands it a sequence of