From 2b7a1b04d0d012890fc9e9ef5a64ca598ac35402 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Tue, 6 Oct 2026 22:55:01 +0200 Subject: [PATCH] Describe the Chip node in Instruments (#329) - Instruments: new section Chips with the waves, inputs and measured cost, the Chip in the node table, and sustain 0 ending a note in the Envelope. - Sounds: what a Pitch does on a Chip. - Development: 1843 checks. Co-Authored-By: Claude Opus 5.5 --- Development.md | 2 +- Instruments.md | 66 +++++++++++++++++++++++++++++++++++++++++++++----- Sounds.md | 2 +- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/Development.md b/Development.md index 9a15c53..8da0339 100644 --- a/Development.md +++ b/Development.md @@ -18,7 +18,7 @@ php tests/php/accounts.test.php # the API's accoun # has and the web server's container has not (SKIP there) php tests/php/snapshots.test.php # the snapshot store (#231), the same way php tests/php/releases.test.php # the release store (#266), the same way -node tools/seed-e2e.mjs && node tools/e2e.mjs # 1841 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1843 editor checks, screenshots in /tmp/gm-e2e-*.png GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step ./e2e.sh start [GM_STEPS] # the same run detached, see below ./e2e.sh status # live progress, or the last run's result diff --git a/Instruments.md b/Instruments.md index 37bf429..880d617 100644 --- a/Instruments.md +++ b/Instruments.md @@ -1,6 +1,7 @@ An **instrument** (**+ → Instrument**, #331) makes a note a sound. It is a node graph like a -[sound FX](Sounds#sound-fx): **Notes** nodes bring the note in, a **Sample** plays a sound for it, -the effects of a sound FX shape it and the **Output** is what is heard. It is the first piece of +[sound FX](Sounds#sound-fx): **Notes** nodes bring the note in, a **Sample** plays a sound for it +or a **Chip** makes one the way old game consoles did (#329), the effects of a sound FX shape it +and the **Output** is what is heard. It is the first piece of the music tools planned in #330: a [pattern](Patterns) (#333) plays one, and a [song](Songs) (#334) lays patterns out. A behavior plays one with **Play note**, and the tab plays it from its keys. @@ -20,6 +21,7 @@ A at 440 Hz, 127 the highest. A note is named with its octave after it and no da | --- | --- | | **Notes** | Where the notes come in. `any` triggers what it is connected to for every note from **From** to **To**. `note` is the number of the note played, `frequency` its pitch in Hz (`440 * 2^((note - 69) / 12)`), `velocity` how hard it is struck, 0 to 1. With **Single notes** there is one trigger output for every note in the range as well, and each can have a **name** of its own (*Name of C2* in the inspector): the output shows it, and so do the keys of the [piano roll](Patterns#the-pattern-tab). | | **Sample** | Plays a sound asset when its `trigger` is on. With **Follow note** (on by default) it plays higher or lower by the distance from **Root**, the note it sounds as it is (C4 by default), and **keeps its length** — it is played by the same worklet as a Pitch (#317). Off, every note plays it as it is, as for a drum. **Play** says how it is played while the note is held and once it is let go (table below). **Loop start** and **Loop end** are in seconds, 0 at the end being the end of the sound, counted in the stretch the sound is [cut to](Sounds#the-sound-tab). The volume of the sound asset counts. | +| **Chip** | Makes a raw wave for every note that triggers it, the way sfxr makes its sounds: square, triangle, saw, sine or noise, with a slide, vibrato, an arpeggio and repeat. It needs no sound file. See [Chips](#chips). | | **Parameters** | The instrument's own parameters, each a number output with a default — like the parameters of a sound FX's Input. One per instrument. | | **Sound FX** | Sends the sound through a sound FX asset, as if it were the sound at the FX's Input. Every parameter of that FX is an input here; one left open is the FX's default. The node is titled after the FX, and a double click opens it. | | **Envelope** | Shapes a note as it is played: **Attack**, **Decay** and **Release** in seconds, **Sustain** from 0 to 1. On the way of the sound it is its volume; its output `value` moves a number with the same curve, from **From** to **To**. See [Envelopes](#envelopes). | @@ -85,7 +87,7 @@ voice 0. [Envelope](#envelopes) does not fade: it plays on through the release. - **Pitch.** A Sample that follows the note is shifted by the phase vocoder of #317, so a note far from the root sounds rough, as there. A note at its root is the sound itself, played without the - worklet. + worklet. On a Chip a Pitch moves its frequency itself, exactly and at no cost. - **Sound FX node and a Mix.** The arriving sound is set in wherever the FX's Input was. A Mix in the FX therefore plays the sound twice, one source per way, as in the sound FX itself. @@ -113,13 +115,18 @@ sound as it is. then release* one leaves its loop as well, and the note ends when the release has. A *One-shot* under an Envelope of its own plays through as it would, the Envelope held at its sustain. Echoes after it ring on as ever. +- **Sustain 0 ends the note** once its decay is over, however long the note is held (#329): every + source under that Envelope ends then, a *One-shot* and a [Chip](#chips) too. A laser or a jump + played by `Play note` with `duration` 0 therefore ends by itself rather than running on silently + until a `Stop sound`. - **`value`** moves a number with the same curve: at **From** while the note rests, at **To** at the top of the attack, and where the sustain puts it between them while held. Its pin is of a kind of its own, an **envelope**, drawn in coral. It fits only a number the sound itself holds while it plays: `cutoff` and `resonance` of a Filter, `semitones` of a Pitch, `gain` of a Gain, a band of - the 5-Band-EQ, `mix` of a Mix, and `bits`, `rate` and `mix` of a Bitcrusher. Not the time of a - Delay, not the times of an Envelope, not a parameter of a Sound FX node. A cutoff, a Gain and the - rate of a Bitcrusher move in equal ratios (octaves, decibels), everything else evenly. Both ends are held to the range of the number it moves. + the 5-Band-EQ, `mix` of a Mix, `bits`, `rate` and `mix` of a Bitcrusher, and `frequency` and + `duty` of a Chip. Not the time of a Delay, not the times of an Envelope, not a parameter of a + Sound FX node, not the slide or the other numbers of a Chip. A cutoff, a Gain, the rate of a + Bitcrusher and the frequency of a Chip move in equal ratios (octaves, decibels), everything else evenly. Both ends are held to the range of the number it moves. - **A filter sweep:** Sample → Filter → Envelope → Output, and a second Envelope with its `in` left open, its `value` into the Filter's `cutoff`, From 200, To 5000. Each Envelope is a curve of its own; one Envelope can feed several numbers. @@ -141,6 +148,53 @@ sound as it is. A Gain moved from 0.25 to 1 by that curve was 0.5 halfway up. Live, a looped note of 0.2 s through an Envelope with a release of 1 s was gone 1.32 s after it started, read every 150 ms. +## Chips + +A **Chip** (#329) makes the sound of a note itself, the way old game consoles did and sfxr does: +a raw wave, and what its frequency does over the note. Notes `any` into its `trigger`, its `out` +on through an Envelope, a Filter or a Bitcrusher like any Sample. Where sfxr counts every slider +from 0 to 1, the Chip counts in Hz, semitones and seconds. + +| Input | Default | What it does | +| --- | --- | --- | +| **Wave** (setting) | Square | *Square*, *Triangle*, *Saw*, *Sine*, *Noise* (sfxr's: 32 new values every period, so it gets lower with the frequency) or *Noise (periodic)* (the short noise of the NES, a 15-bit register repeating every 93 steps: metallic, with a pitch). A square and the periodic noise are half as loud as the others, as in sfxr. | +| **Follow note** (setting) | on | Plays at the pitch of the note. Off, it plays at **Frequency**. | +| **Frequency (Hz)** | 440 | The pitch without *Follow note*. A number connected here goes before both: `frequency` of a Notes node, a parameter, or an Envelope's `value` for a sweep in Hz. | +| **Duty** | 0.5 | How much of a square is up: 0.5 is even, 0.25 and 0.125 thinner and more nasal. Held to 0.01 to 0.99. | +| **Duty sweep (/s)** | 0 | Moves the duty by that much per second. | +| **Slide (st/s)** | 0 | Glides the pitch in semitones per second: -100 is a laser falling about eight octaves a second. | +| **Delta slide** | 0 | Changes the slide by that many semitones per second, every second. | +| **Min frequency (Hz)** | 0 | Where a slide falls below it, the note ends. 0 never. | +| **Vibrato depth (st)** | 0 | How far it wobbles up and down, in semitones. | +| **Vibrato speed (Hz)** | 6 | How often it wobbles a second. | +| **Arpeggio (st)** | 0 | Jumps the pitch by that many semitones once, **Arpeggio time** seconds into the note: the "pling" of a pickup. | +| **Arpeggio time (s)** | 0.1 | When it jumps. | +| **Repeat (s)** | 0 | Starts slide, duty sweep and arpeggio over every so many seconds: the trill of a powerup. 0 never. | + +- An input that does nothing as things stand is **grey** in the inspector: *Frequency* while it + follows the note, *Duty* and *Duty sweep* for a wave that is no square, *Vibrato speed* at depth + 0, *Arpeggio time* without an arpeggio. One that is connected counts as moving, whatever was typed + into it. *Follow note* is left off the node and the inspector while *Frequency* is connected. +- **Frequency and duty run on the audio thread** (AudioParams): an Envelope moves them while the + note plays, and `Set sound FX parameters` reaches one fed by a parameter, as with every other + number. A **Pitch** behind a Chip moves its frequency itself. The rest (slide, vibrato, arpeggio, + repeat, the wave) is fixed when the note starts; a parameter can still set it for every note. +- **It ends with its note**, faded out in 10 ms, or under an [Envelope](#envelopes) through its + release, and with *Sustain* 0 once the decay is over. Below *Min frequency* it ends by itself. + Envelope, Filter and Bitcrusher are nodes of their own; sfxr's punch is a short decay, its phaser + comes with #341. +- **How it is made:** a worklet of its own per note, next to the Pitch and the Bitcrusher in the + same module. Every sample is worked out 8 times and averaged, as sfxr does, which takes the worst + of the aliasing off a raw wave. Without the worklet a Chip is silent and its note ends at once. + The frequency follows sfxr (Tomas Pettersson, MIT licence) as jsfxr (public domain) ports it. +- **Measured** (2026-10-06, e2e 34, Chrome offline on the Windows runner): a square at 220 Hz with + a Pitch of 12 and a duty of 0.25, started at 0.25 s, was silent up to sample 12000 and then made + 329 periods in 0.75 s, up 0.249 of each, peak 0.5. Sliding from 440 Hz down two octaves a second + with a lowest frequency of 110 Hz it fell silent at 1 s and said so. 64 voices of noise with a + slide and vibrato rendered 2 s in 2.0 s: about 63 at once, where a Pitch manages about 24. Live, + a note of 0.2 s through an Envelope with a decay of 0.1 s and sustain 0 was gone 0.26 s after it + started, read every 150 ms. + ## The Instrument tab The graph, and below it **the keys**: all 128 notes, the ones that sound in white and black, the diff --git a/Sounds.md b/Sounds.md index 4f3c7cd..fd561e0 100644 --- a/Sounds.md +++ b/Sounds.md @@ -143,7 +143,7 @@ Input and the Output once each; their pins carry sound and fit nothing but each | Node | What it does | | --- | --- | | **Gain** | Louder or quieter, in dB (`Gain (dB)`): 0 leaves it alone, -6 is about half as loud, +6 twice. Above 0 the sound can clip. | -| **Pitch** | Higher or lower, in semitones (12 is an octave, 0.5 a quarter tone). The sound **keeps its length**, also when the pitch changes while it plays. The pitch is made where the sound is read, so an effect before the Pitch acts as if it came after it. | +| **Pitch** | Higher or lower, in semitones (12 is an octave, 0.5 a quarter tone). The sound **keeps its length**, also when the pitch changes while it plays. The pitch is made where the sound is read, so an effect before the Pitch acts as if it came after it. On a [Chip](Instruments#chips) of an instrument it moves the frequency of the Chip itself. | | **5-Band-EQ** | Five bands in dB: 80 Hz and 12 kHz as shelves, 250 Hz, 1 kHz and 4 kHz as peaks. A band at 0 does nothing. | | **Filter** | Takes away what is above `cutoff` (Hz, 1000 by default): **Type** *Low-pass*. Or what is below it (*High-pass*), or both, leaving a band around it (*Band-pass*). `resonance` from 0 to 1 raises what is right at the cutoff: a low- or high-pass has no bump at 0 (Q -3 dB) and rises by 20 dB at 1, a band-pass gets narrower (Q 0.7 to 20). In an [instrument](Instruments#envelopes) an Envelope can move both while a note plays. | | **Mix** | Blends the sounds at `a` and `b`: at 0 only `a` is heard, at 1 only `b` (0.5 is the default). **Curve** says what lies between: *Crossfade* (the default) gives half of each at 0.5, *Both at center* keeps each full up to the middle. See [below](#the-curve-of-a-mix). |