diff --git a/Development.md b/Development.md index 7de5e69..362ead7 100644 --- a/Development.md +++ b/Development.md @@ -19,7 +19,7 @@ php tests/php/accounts.test.php # the API's accoun 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 php tests/php/files.test.php # uploaded files in a folder per account (#344), the same way -node tools/seed-e2e.mjs && node tools/e2e.mjs # 1852 editor checks, screenshots in /tmp/gm-e2e-*.png +node tools/seed-e2e.mjs && node tools/e2e.mjs # 1853 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 314b997..e669593 100644 --- a/Instruments.md +++ b/Instruments.md @@ -26,7 +26,7 @@ A at 440 Hz, 127 the highest. A note is named with its octave after it and no da | **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). | | **Output** | What is heard. It takes **one** sound, like every other sound input; a second edge replaces the first. To play several together, put a **Mix** in front of it (several Mixes in a row for more than two). One per instrument. | -| Gain, Pitch, 5-Band-EQ, Filter, Mix, Delay, Bitcrusher | The effects of a [sound FX](Sounds#sound-fx), the same nodes. | +| Gain, Pitch, 5-Band-EQ, Filter, Mix, Delay, Bitcrusher, Phaser, Flanger | The effects of a [sound FX](Sounds#sound-fx), the same nodes. | **Play** of a Sample: @@ -123,10 +123,11 @@ sound as it is. 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, `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. + the 5-Band-EQ, `mix` of a Mix, `bits`, `rate` and `mix` of a Bitcrusher, `frequency` and `mix` + of a Phaser, `offset` and `mix` of a Flanger, 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, not the LFO of a Phaser or Flanger. A cutoff, a Gain, the rate of a + Bitcrusher and the frequency of a Phaser or 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. @@ -182,7 +183,7 @@ from 0 to 1, the Chip counts in Hz, semitones and seconds. - **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. + is the [Flanger](Sounds#phaser-and-flanger) (#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. @@ -229,14 +230,17 @@ plays the note last played at once (C4 before any), for up to 2 s. follow the note, so every key plays at its own pitch, and most hold the note while the key is held. They come out the same every time they are picked: Randomize and Mutate make a variant. - **Effects** below the line: *Pickup*, *Laser*, *Explosion*, *Powerup*, *Hit*, *Jump*, *Blip*. - Each is Notes for all 128 notes → Chip → Envelope → Filter → Bitcrusher → Output. Every click - draws the sound anew within its kind, with sfxr's generator for it. The Chip plays at its own + Each is Notes for all 128 notes → Chip → Envelope → Filter → Bitcrusher → Output. One + *Explosion* in two has sfxr's phaser as well, a [Flanger](Sounds#phaser-and-flanger) after the + Filter, with sfxr's offset and sweep and no LFO (#341). Every click draws the sound anew within its kind, with sfxr's generator for it. The Chip plays at its own frequency, *Follow note* off, so every key plays the same sound, as an effect does; turn it on to play the sound as a melody. The Envelope has *Sustain* 0, so the sound ends by itself. -- **Randomize** draws every number of the Chips, Envelopes, Filters and Bitcrushers in the graph - anew, as sfxr's randomizer does: most of them wild. It leaves the wave, *Follow note*, the type - of a Filter, the *Mix* of a Bitcrusher, an Envelope's *From* and *To* and every other node as - they are. An Envelope gets *Sustain* 0. An input with an edge into it keeps what it is fed. +- **Randomize** draws every number of the Chips, Envelopes, Filters, Bitcrushers, Phasers and + Flangers in the graph anew, as sfxr's randomizer does: most of them wild. A Flanger's offset and + sweep come from sfxr's phaser, its LFO and feedback and all of a Phaser, which sfxr has not, + within bounds of their own (a rate of 0.05 to 5 Hz, a Phaser at 200 Hz to 4 kHz). It leaves the + wave, *Follow note*, the type of a Filter, the *Stages* of a Phaser, every *Mix*, an Envelope's + *From* and *To* and every other node as they are. An Envelope gets *Sustain* 0. An input with an edge into it keeps what it is fed. - **Mutate** moves the numbers of those nodes a little, each one in two: a frequency, cutoff or rate by up to a quarter of an octave, the rest by a step and a tenth of its value. A 0 stays 0, so nothing that was off comes on, and *Sustain* stays as it is. @@ -256,6 +260,7 @@ The instruments, all on every note unless the table says otherwise: | *Synth Brass* | Saw → Envelope with a slow attack → Low-pass that an Envelope opens from 400 Hz to 3.5 kHz | held | | *Detuned Saws* | Two saws a tenth of a semitone apart (Pitch 0.1) → Mix → Envelope → Low-pass 7 kHz | held | | *String Pad* | As *Detuned Saws* with vibrato, attack 0.5 s, release 1 s, Low-pass 2.2 kHz | held | +| *Phased Pad* | Saw with vibrato → Envelope, attack 0.3 s, release 0.8 s → Low-pass 3 kHz → Phaser of 6 stages at 600 Hz, 2.5 octaves wide, 0.3 times a second | held | | *Organ* | Square and the square an octave up → Mix → Envelope at full sustain → Low-pass 4.5 kHz | held | | *Flute* | Triangle with vibrato, a breath of noise through a Band-pass at 2.5 kHz → Mix → Envelope | held | | *Bell* | Two sines 17.6 semitones apart (a ratio of 2.76), each with its own Envelope → Mix → Delay | dies away in 2 s | diff --git a/Sounds.md b/Sounds.md index 9566897..d0607a9 100644 --- a/Sounds.md +++ b/Sounds.md @@ -172,13 +172,17 @@ Input and the Output once each; their pins carry sound and fit nothing but each | **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). | | **Delay** | An echo: the sound comes back after `time`, again and again, each time `feedback` times as loud. **Sync** says what `time` counts: *Seconds*, or *Beats* in quarter notes at the tempo of the voice. `damping` makes every echo duller than the last, `mix` is how loud the echoes are next to the sound, which stays as it is. **Ping-pong** sends the echoes left and right in turn. See [below](#delay-and-the-tail-of-a-voice). | | **Bitcrusher** | Makes the sound rough, like an old console. `bits` rounds it to fewer steps (8 by default), `rate` takes a sample only so many times a second and holds it until the next (8000 Hz by default), `mix` is how much of the crushed sound is heard instead of the sound itself (1 by default). See [below](#bitcrusher). | +| **Phaser** | Sweeps notches through the sound, the swirl of a phaser pedal. **Stages** (2, 4, 6, 8 or 12, 4 by default) is how many allpass stages it has, every two make one notch. They sit around `frequency` (800 Hz by default), and an LFO moves them `depth` octaves wide (2), `rate` times a second (0.5 Hz). `feedback` (0.4) sharpens the notches, `mix` (1) is how much is heard. See [below](#phaser-and-flanger). | +| **Flanger** | Blends the sound with a copy a few ms late: the jet sound of a flanger, and what sfxr calls its phaser. `offset` is how late the copy is (1 ms by default), `sweep` moves that on by so many ms a second from the start of the sound (0), an LFO adds up to `depth` ms (2) `rate` times a second (0.25 Hz). `feedback` (0.5) and `mix` (1) as on the Phaser. See [below](#phaser-and-flanger). | Every number of these nodes is an **input**: typed in while nothing is connected, or fed from a parameter of the Input. Each is held to its range (Gain -120 to +24 dB, Pitch -64 to +64, the EQ bands -60 to +24 dB, Filter `cutoff` 20 to 20000 Hz and `resonance` 0 to 1, Mix 0 to 1, Delay `time` 0.01 to 8 and never more than 2 s, `feedback` 0 to 0.95, `damping` and `mix` 0 to 1, -Bitcrusher `bits` 1 to 16, `rate` 100 to 48000 Hz and `mix` 0 to 1), whichever way the value -arrives. +Bitcrusher `bits` 1 to 16, `rate` 100 to 48000 Hz and `mix` 0 to 1, Phaser `frequency` 20 to +20000 Hz, `rate` 0 to 10 Hz, `depth` 0 to 6 octaves, Flanger `offset` 0 to 10 ms, `sweep` -125 to +125 ms/s, `rate` 0 to 10 Hz, `depth` 0 to 10 ms, both with `feedback` 0 to 0.95 and `mix` 0 to 1), +whichever way the value arrives. **How Pitch keeps the length** (#317). A way with a Pitch is played by an `AudioWorklet` on the audio thread, a **phase vocoder**: the sound is cut into frames of about 43 ms (2048 samples at @@ -313,6 +317,45 @@ A **Bitcrusher** (#322) runs on the same worklet as Pitch, sample by sample: through a Bitcrusher for 2 s in 1.9 s, so about **68 of them fill the audio thread** (2026-10-06). There is no limit on how many play at once. +### Phaser and Flanger + +Both (#341) run on the same worklet as Pitch, sample by sample, and both blend the sound with a +shifted copy of itself, so some frequencies cancel. **`mix` 0 leaves the sound as it is, 1 is half +sound and half copy**: there the notches are deepest, and the peaks stay at the level of the sound, +so neither ever makes it louder. A `mix` typed in at 0 leaves the node out, fed from a parameter it +stays. + +- **Phaser**: a chain of first-order allpasses, each turning the phase by 90° at its frequency. + Where the copy is turned by 180° it cancels the sound: two stages make one notch at `frequency`, + four make two, around it, with a peak at `frequency` itself. A sine LFO moves the frequency of + every stage `depth` octaves wide around `frequency` (half up, half down), `rate` times a second; + at `depth` 0 or `rate` 0 the notches stand still. `feedback` sends what leaves the chain back in, + a sample later, which sharpens the notches into a ring. **Stages** is a setting, not an input. +- **Flanger**: a delay line, read with linear interpolation. The delay is `offset`, plus what + `sweep` has added since the sound started (in ms per second, either way), plus up to `depth` ms + from an LFO that starts at 0. **As in sfxr the delay is mirrored at 0**: an `offset` of 1 ms + sweeping down at 10 ms/s is 0 after 0.1 s and 1 ms again after 0.2 s. It never goes past 20 ms. + At `depth` 0 the copy only sweeps, as sfxr's does. `feedback` sends the copy back into the line, + every round exactly one delay later (the worklet needs no render quantum per round, unlike a + Delay). +- **sfxr's phaser** is this Flanger with `depth`, `feedback` and `rate` at 0. sfxr adds the copy + at the same level as the sound, the Flanger at `mix` 1 at half of each, so the same sound comes + out 6 dB quieter. sfxr's delay ends at 2.9 ms, the Flanger's at 20 ms, so a long sweep goes on + further. +- `frequency` and `offset` move in what they hold (the frequency in octaves), and in an + [instrument](Instruments#envelopes) an Envelope can move `frequency`, `offset` and `mix` while a + note plays. The other numbers move with *Set sound FX parameters* as well. +- Without `AudioWorklet` the sound goes through both as it is, as through the Bitcrusher. +- **Measured** (2026-10-07, e2e 34, Chrome offline on the Windows runner): a 1 kHz sine at 0.8 + through a Phaser at 1 kHz came out at an RMS of 0.0000 with two stages and 0.5657 with four + (0.8 * 0.707, untouched). Swept 4 octaves wide once a second, the RMS over 25 ms windows ran + between 0.07 and 0.501 (0.8 * 0.707 * cos(28°) = 0.496 two octaves off). A Flanger at 1 ms gave + an impulse back after 48 samples at 0.5. Sweeping 100 ms/s from the start of its voice, an + impulse 0.1 s in came back 533.3 samples later, as worked out. +- **Cost**: in the same run 64 stereo voices rendered for 2 s say about **47 Phasers** of 12 + stages with feedback, or about **63 Flangers** with an LFO, fill the audio thread. There is no + limit on how many play at once. + **Listening in the tab.** The header picks one of the project's sounds and plays it through the graph as it stands — the same library as a running game, so it sounds the way the game will. `Stop` there ends only what the tab started. Play is offered only while the graph compiles.