song.jsannotatedsong.jssource288 lines · 19.0 KB · raw

TASK FAILED SUCCESSFULLY — breakcore, 180bpm, 64 bars as written; Jev arranges up to 28 four-bar sections. Spec: SPEC.md Start from STOPPED so the song begins at bar 1 (re-running it mid-play keeps the cycle count).

3setcps(180/60/4)
4await samples('github:tidalcycles/dirt-samples')
5await samples('jev-samples/strudel.json')

── Jev: two of them, in TypeSafe's own shape (SPEC.md, "Jev in the booth") ── formJev picks the next section; djJev, asked once that is decided (questions in one request cannot see each other), says how much of each part plays it, how the song moves into it, and how hard. Every 4 bars. The form's rules (walk, at the bottom) narrow the sections Jev may pick and end the song; without Jev it plays as written.

12const song = {
13  title: 'TASK FAILED SUCCESSFULLY',
14  what: 'upbeat breakcore about the bad days of Jev: it picks among the answers it is given and never makes one up, and projects built on it still had bad days',
15  tempo: '180 bpm, 4/4, 4 bars a section',
16  key: 'C major',
17  chords: 'C C G G Am Am F F, two bars each, under the stabs, the bass and the XP error riff',
18  lyrics: {
19    verse1: 'No hallucinations / Just the wrong option',
20    verse1b: 'Ranked the files / Indistinguishable from noise',
21    chorus1: 'Jev is type safe. Jev is fine. / Task failed successfully',
22    verse2: 'Order by probability / Four of six failed / Oh no no no',
23    verse2b: 'Reversed the options / Thirty-two of two hundred',
24    chorus2: 'Jev is type safe. Jev is fine. / Emotional damage',
25  },
26}
28const formJev = jev({
29  state: song,
30  questions: {
31    section: choice('Pick the section of the song that plays next, from those allowed. The Weide theme (weide) ends the song: choose it once the song has built to its peak and made its point, before it drags', {
32      intro: 'the Windows XP error sound played as a riff, alone',
33      amenIn: 'the XP riff with a thin, high-passed amen break coming in',
34      verse1: 'verse 1, first half: the robot sings No hallucinations / Just the wrong option over a straight amen, the kick and a plain sub',
35      verse1b: 'verse 1, second half: Ranked the files / Indistinguishable from noise',
36      chorus1: 'the chorus: Jev is type safe, Jev is fine, then the Task Failed Successfully meme, and an XP error stutter up the chord into a drop',
37      drop1: 'drop 1: written-out amen edits with 32nd and 64th rolls in two of its bars, the reese, saw chord stabs and the XP riff, a crash on the downbeat',
38      drop1b: 'drop 1 continues: 32nd rolls in every bar and 64ths closing three of them',
39      drop1c: 'the end of drop 1: 64th rolls in every bar, the busiest edits of the first drop, then verse 2',
40      verse2: 'verse 2, first half: Order by probability / Four of six failed, answered by the Oh no no no meme',
41      verse2b: 'verse 2, second half: Reversed the options / Thirty-two of two hundred',
42      chorus2: 'the chorus again: Jev is type safe, Jev is fine, then the Emotional Damage meme, and the XP stutter into a drop',
43      drop2: 'drop 2: 32nd and 64th rolls in every bar, the reese, stabs and the XP riff, a crash on the downbeat',
44      drop2b: 'drop 2 builds: 64th and 128th rolls, and the XP riff doubled an octave up',
45      drop2c: 'drop 2 builds further: 128th rolls in every bar, and the reese filter opened wide',
46      drop2d: 'the peak of the song: the densest edits, a last bar of 64th and 128th rolls, and a second, busier layer of chord stabs on top',
47      weide: 'everything cuts and the Directed by Robert B. Weide theme plays out: the end',
48    }),
49  },
50})

how much of each part the section decidedThisSection has: out, back or full (a choice, so allow below asks only about the parts a section has)

54const level = (part) => choice('How much of ' + part + ' should the next section (decidedThisSection) have? Drop parts to build tension or make space, pull them back to let others speak, and bring them in full for the payoff', {
55  full: 'full: as written',
56  back: 'back: at half velocity, under the rest of the mix',
57  out: 'out: silent for the section',
58})
59const djJev = jev({
60  state: song,
61  questions: {
62    xp: level('the Windows XP error riff (the XP error sound played as notes on the chords: with the amen coming in and through every drop, doubled an octave up from drop2b; and its stutter up the chord at the end of each chorus)'),
63    drums: level('the amen break and the kick (a thin, high-passed amen with the XP riff, a straight amen and the kick in the verses and choruses, written-out edits with the kick and a crash in the drops)'),
64    stabs: level('the chord stabs (square in the choruses, saw in the drops, with a second, busier layer at the peak)'),
65    bass: level('the bass (a plain sub on the chord roots in the verses and choruses, the reese and sub in the drops)'),
66    into: choice('How should the song move into the next section (decidedThisSection)? It plays over the last three beats of the section before it', {
67      cut: 'straight in, no transition',
68      fill: 'an amen snare roll, doubling up into the first beat',
69      riser: 'the XP error sound stuttering and rising an octave over the last bar',
70      dropout: 'every part stops before the last beat, a silent beat before the section hits',
71    }),
72    energy: score('How intense should the next section be? It scales the band, not the vocals. Shape the energy of the whole song, following its history, how it measured and how the listener reacted', [
73      'held back: the band at about half velocity (0.55)',
74      'steady: about 70%',
75      'driving: about 85%',
76      'all out: full velocity, as written',
77    ]),
78  },
79})

a little chance in the form: the section is drawn from Jev's probabilities

82const { section } = formJev.every(4, { sample: 0.7 })

Jev is asked only about what a section has (SPEC.md, "The parts" and "Into the next section"); a single allowed option plays without asking.

85const levels = ['full', 'back', 'out']
86const defining = ['full', 'back']
87const drop = { xp: levels, drums: defining, stabs: levels, bass: levels }
88const verse = { drums: defining, bass: levels }
89const chorus = { xp: levels, drums: levels, stabs: defining, bass: levels }
90const partsIn = {
91  amenIn: { xp: levels, drums: defining },
92  verse1: verse, verse1b: verse, verse2: verse, verse2b: verse,
93  chorus1: chorus, chorus2: chorus,
94  drop1: drop, drop1b: drop, drop1c: drop, drop2: drop, drop2b: drop, drop2c: drop, drop2d: drop,
95}
96const has = (name) => ({ section: next }) => partsIn[next]?.[name] ?? ['full']

out of a chorus the stutter is the riser, out of verse2 "Oh no" fills the bar

98const intoAllowed = ({ section: next, playingNow: now = '' }) =>
99  next === 'intro' || ['chorus1', 'chorus2', 'verse2'].includes(now) ? ['cut'] : ['cut', 'fill', 'riser', 'dropout']
100const dj = djJev.every(4, {
101  after: { section },
102  forget: ['into'],
103  allow: { xp: has('xp'), drums: has('drums'), stabs: has('stabs'), bass: has('bass'), into: intoAllowed },
104  sample: { into: 1 },
105  fallback: { xp: 'full', drums: 'full', stabs: 'full', bass: 'full', into: 'cut', energy: 3 },
106})

A part at Jev's level: silent when out, half velocity when back. Each part has its own orbit, named for its question, so the meter hears it alone (measuredSound).

111const part = (name, pat) =>
112  pat.orbit(name).mask(dj[name].choice.fmap((c) => c !== 'out'))
113    .mul(velocity(dj[name].choice.fmap((c) => (c === 'back' ? .5 : 1))))

bars: 1-8 intro | 9-16 verse 1 | 17-20 chorus | 21-32 DROP 1 | 33-40 verse 2 41-44 chorus | 45-60 DROP 2 | 61-64 the Weide theme (each mask = 64 steps)

117const intro   = "<1!4 0!60>"
118const amenIn  = "<0!4 1!4 0!56>"
119const verses  = "<0!8 1!8 0!16 1!8 0!24>"
120const verse1  = "<0!8 1!8 0!48>"
121const verse2  = "<0!32 1!8 0!24>"
122const choruses = "<0!16 1!4 0!20 1!4 0!20>"
123const chorus1 = "<0!16 1!4 0!44>"
124const chorus2 = "<0!40 1!4 0!20>"
125const drops   = "<0!20 1!12 0!12 1!16 0!4>"
126const drop1   = "<0!20 1!4 0!40>"
127const drop1b  = "<0!24 1!4 0!36>"
128const drop1c  = "<0!28 1!4 0!32>"
129const drop2   = "<0!44 1!4 0!16>"
130const drop2b  = "<0!48 1!4 0!12>"
131const drop2c  = "<0!52 1!4 0!8>"
132const drop2d  = "<0!56 1!4 0!4>"
133const xpRiff  = "<0!4 1!4 0!12 1!12 0!12 1!16 0!4>"   // amenIn and the drops
134const octave  = "<0!48 1!12 0!4>"                     // drop2b to drop2d
135const reese   = "<0!20 1!12 0!12 1!8 0!12>"           // its filter sweeping
136const opened  = "<0!52 1!8 0!4>"                      // drop2c and drop2d: opened wide
137const kick    = "<0!8 1!52 0!4>"
138const sub     = "<0!8 1!12 0!12 1!12 0!20>"           // verses and choruses
139const stutter = "<0!19 1 0!23 1 0!20>"
140const crash   = "<0!20 1 0!23 1 0!19>"
141const weide   = "<0!60 1 0!3>"

two bars per chord: C C G G Am Am F F

144const chords = "<[c4,e4,g4]!2 [b3,d4,g4]!2 [a3,c4,e4]!2 [a3,c4,f4]!2>"
145const roots  = "<36!2 31!2 33!2 41!2>"   // C2 G1 A1 F2, as MIDI numbers (F1 is below laptop range)

the XP error sound is centred on C, so these speeds are notes: C G C' ... per chord

147const xpNotes = "<[1 ~ ~ 1.498 ~ ~ 2 ~ 1 ~ ~ 1.498 ~ ~ 1.26 ~]!2 [.749 ~ ~ 1.122 ~ ~ 1.498 ~ .749 ~ ~ 1.122 ~ ~ .944 ~]!2 [.841 ~ ~ 1.26 ~ ~ 1.682 ~ .841 ~ ~ 1.26 ~ ~ 1 ~]!2 [.667 ~ ~ 1 ~ ~ 1.335 ~ .667 ~ ~ 1 ~ ~ .841 ~]!2>"

amen: straight, and each drop section's own written-out edits (never random). A section starts on a multiple of 4, so each 4-bar row plays in its own section. [a a] in a step is a 32nd roll, [a a a a] a 64th, eight a 128th.

152const straight = "<[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] [16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31]>"
153const edits = {
154  drop1: "<[0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15] [0 1 2 3 [4 4] [4 4] 6 7 8 9 10 11 [12 12 12 12] 14 15] [16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31] [0 0 2 [3 3] 4 [4 4 4 4] 6 7 [8 8] 9 [10 10] 11 [12 12 12 12] [13 13 13 13] [14 14 14 14] [15 15 15 15]]>",
155  drop1b: "<[0 1 2 3 [4 4] 5 6 7 8 9 [10 10] 11 [12 12] 13 [14 14] 15] [16 17 [18 18] 19 [20 20] 21 [22 22] 23 24 25 [26 26] 27 [28 28 28 28] 29 [30 30] 31] [0 1 2 3 4 5 6 7 [8 8] [8 8] 10 11 [12 12 12 12] [12 12 12 12] 14 15] [0 0 2 [3 3] [4 4] [4 4 4 4] 6 [7 7] [8 8] 9 [10 10] [11 11] [12 12 12 12] [13 13 13 13] [14 14 14 14] [15 15 15 15]]>",
156  drop1c: "<[0 [1 1] 2 [3 3] [4 4 4 4] [5 5] 6 [7 7] 8 [9 9] 10 [11 11] [12 12 12 12] [13 13] 14 [15 15]] [16 16 18 [19 19] 20 [20 20 20 20] 22 23 [24 24] 25 [26 26] 27 [28 28 28 28] [29 29 29 29] [30 30 30 30] [31 31 31 31]] [0 1 [2 2] 3 [4 4 4 4] 5 [6 6] 7 [8 8 8 8] 9 [10 10] 11 [12 12 12 12] 13 [14 14] 15] [[0 0] [0 0] 2 [3 3] [4 4 4 4] [4 4 4 4] 6 7 [8 8 8 8] [8 8 8 8] [12 12 12 12] [12 12 12 12] [4 4 4 4] [4 4 4 4] [12 12 12 12] [12 12 12 12]]>",
157  drop2: "<[[0 0] [1 1] 2 3 [4 4] [4 4] 6 7 [8 8] [9 9] 10 11 [12 12 12 12] [12 12 12 12] 14 15] [16 17 [18 18] 19 [20 20 20 20] 21 [22 22] 23 24 25 [26 26] 27 [28 28 28 28] [29 29 29 29] [30 30 30 30] [31 31 31 31]] [0 1 2 3 [4 4] 5 [6 6] 7 [8 8] 9 [10 10] 11 [12 12 12 12] 13 [14 14] 15] [0 0 2 [3 3] 4 [4 4 4 4] 6 7 [8 8] 9 [10 10] 11 [12 12 12 12] [13 13 13 13] [14 14 14 14] [15 15 15 15]]>",
158  drop2b: "<[0 1 2 3 4 5 6 7 [8 8 8 8] [8 8 8 8] [12 12 12 12] [12 12 12 12] [4 4 4 4 4 4 4 4] [4 4 4 4 4 4 4 4] 14 15] [16 [17 17] 18 [19 19] [20 20 20 20] 21 [22 22 22 22] 23 [24 24] 25 [26 26 26 26] 27 [28 28 28 28 28 28 28 28] [29 29 29 29] [30 30 30 30] [31 31 31 31]] [[0 0] [1 1] [2 2] [3 3] [4 4 4 4] [4 4 4 4] 6 7 [8 8] [9 9] [10 10] [11 11] [12 12 12 12] [12 12 12 12] [14 14 14 14] [15 15 15 15]] [0 0 2 [3 3] 4 [4 4 4 4] 6 7 [8 8 8 8] [8 8 8 8] [10 10] 11 [12 12 12 12 12 12 12 12] [13 13 13 13 13 13 13 13] [14 14 14 14] [15 15 15 15]]>",
159  drop2c: "<[[0 0] 1 [2 2] 3 [4 4 4 4] 5 [6 6] 7 [8 8 8 8] 9 [10 10] 11 [12 12 12 12 12 12 12 12] 13 [14 14 14 14] 15] [16 16 [18 18] 19 [20 20 20 20 20 20 20 20] 21 [22 22] 23 [24 24 24 24] 25 [26 26] 27 [28 28 28 28 28 28 28 28] [29 29 29 29] [30 30 30 30 30 30 30 30] [31 31 31 31]] [0 1 2 3 [4 4] [4 4] [4 4 4 4] [4 4 4 4] [8 8] 9 [10 10 10 10] 11 [12 12 12 12] [12 12 12 12 12 12 12 12] [14 14] 15] [[0 0] [0 0] [2 2] [3 3] [4 4 4 4] [4 4 4 4] [4 4 4 4 4 4 4 4] [4 4 4 4 4 4 4 4] [8 8 8 8] [8 8 8 8] [12 12 12 12] [12 12 12 12] [12 12 12 12 12 12 12 12] [12 12 12 12 12 12 12 12] [15 15 15 15 15 15 15 15] [15 15 15 15 15 15 15 15]]>",
160  drop2d: "<[[0 0] [1 1] [2 2 2 2] 3 [4 4 4 4] [4 4 4 4 4 4 4 4] 6 [7 7] [8 8 8 8] [9 9] [10 10 10 10] 11 [12 12 12 12 12 12 12 12] [12 12 12 12 12 12 12 12] [14 14 14 14] [15 15]] [16 [16 16] [18 18 18 18] 19 [20 20 20 20 20 20 20 20] [20 20 20 20] [22 22 22 22] 23 [24 24 24 24] 25 [26 26 26 26] [27 27] [28 28 28 28 28 28 28 28] [29 29 29 29 29 29 29 29] [30 30 30 30 30 30 30 30] [31 31 31 31 31 31 31 31]] [[0 0] [0 0] [2 2 2 2] [3 3] [4 4 4 4 4 4 4 4] [4 4 4 4 4 4 4 4] [6 6] [7 7] [8 8 8 8 8 8 8 8] [8 8 8 8 8 8 8 8] [10 10 10 10] [11 11] [12 12 12 12 12 12 12 12] [12 12 12 12 12 12 12 12] [4 4 4 4 4 4 4 4] [4 4 4 4 4 4 4 4]] [[0 0 0 0] [0 0 0 0] [0 0 0 0 0 0 0 0] [0 0 0 0 0 0 0 0] [4 4 4 4 4 4 4 4] [4 4 4 4 4 4 4 4] [4 4 4 4 4 4 4 4] [4 4 4 4 4 4 4 4] [12 12 12 12 12 12 12 12] [12 12 12 12 12 12 12 12] [12 12 12 12 12 12 12 12] [12 12 12 12 12 12 12 12] [15 15 15 15 15 15 15 15] [15 15 15 15 15 15 15 15] [15 15 15 15 15 15 15 15] [15 15 15 15 15 15 15 15]]>",
161}
162const dropMasks = { drop1, drop1b, drop1c, drop2, drop2b, drop2c, drop2d }

every slice cut to its own step, so a roll never stacks copies of a slice

164const amen = (pat) => n(pat).s("amencutup").clip(1)

── parts ───────────────────────────────────────── the XP error riff (amenIn, the drops, an octave up from drop2b), and its stutter at the end of each chorus, up the bar's chord (G: G B D G), from halfway through the second beat, after the meme's last word

170const riff = (speeds) => s("tfxperror").struct("x ~ ~ x ~ ~ x ~ x ~ ~ x ~ ~ x ~").speed(speeds).end(.3)
171const xp = stack(
172  riff(xpNotes).cut(1).gain(.55).mask(xpRiff),
173  riff(xpNotes.mul(2)).cut(3).gain(.3).mask(octave),
174  s("tfxperror").struct("~@6 x x x x x x x x x x")
175    .speed("~@6 .749 .749 .944 .944 1.122 1.122 1.498 1.498 1.498 1.498")
176    .end(.2).cut(1).gain(.6).mask(stutter)
177)

bars 1-4: the riff alone, always as written (not a part)

179const introRiff = riff(xpNotes).cut(1).gain(.55).mask(intro).orbit('xp')
181const drums = stack(
182  amen(straight).hpf(2200).gain(.26).mask(amenIn),
183  amen(straight).hpf(150).gain(.26).mask(verses),
184  amen(straight).hpf(150).gain(.28).mask(choruses),
185  ...Object.entries(edits).map(([name, pat]) => amen(pat).hpf(120).gain(.3).mask(dropMasks[name])),
186  // the kick, held to a beat so a dropout's last beat is silent
187  s("[bd:3,808bd:1]").struct("x ~ ~ ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~").clip(4).gain(.5).mask(kick),
188  s("hh27:1").gain(2.75).mask(crash)
189)

chord stabs in the choruses and the drops, and a busier layer at the peak

192const stabRhythm = "x ~ ~ x ~ ~ x ~ ~ ~ x ~ ~ ~ ~ ~"
193const stabs = stack(
194  note(chords).struct(stabRhythm).s("sawtooth").lpf(2600).decay(.12).sustain(0).gain(.62).mask(drops),
195  note(chords).struct(stabRhythm).s("square").lpf(2000).decay(.1).sustain(0).gain(.47).mask(choruses),
196  note(chords).struct("~ x ~ ~ x ~ ~ x ~ x ~ x ~ x x ~").s("square").lpf(3200).decay(.08).sustain(0).gain(.39).mask(drop2d)
197)

the reese and sub in the drops (its filter opened wide in drop2c and drop2d); a plain sub in the verses and choruses

201const reeseNotes = note(roots).struct("x@3 x@3 x@4 x@2 x@4").s("supersaw").detune(.6).lpq(3).shape(.3).gain(.54)
202const bass = stack(
203  reeseNotes.lpf(sine.range(250, 1100).slow(4)).mask(reese),
204  reeseNotes.lpf(sine.range(1200, 2800).slow(4)).mask(opened),
205  note(roots).struct("x@3 x@3 x@4 x@2 x@4").s("sine").gain(.72).mask(drops),
206  note(roots).struct("x ~ ~ ~ ~ ~ ~ ~ x ~ ~ ~ ~ ~ ~ ~").s("sine")
207    .decay(.6).sustain(.3).gain(.66).mask(sub)
208)

the lines and the memes: the story, so they always play, at full level, outside Jev's parts and the energy

212const lines = stack(
213  // verses: vocoder, one line per 2 bars, each sung on its chord;
214  // "Oh no no no" answers "Four of six failed" in bar 36
215  s("<tfnohalluc ~ tfwrongopt ~ tfranked ~ tfnoise ~>").gain(1.35).room(.2).mask(verse1),
216  s("<tforderby ~ tffourofsix tfohno tfreversed ~ tfthirtytwo ~>").gain(1.35).room(.2).mask(verse2),
217  // choruses
218  s("<tftypesafe ~ tftaskfailed ~>").gain(1.35).room(.25).mask(chorus1),
219  s("<tftypesafe ~ tfemodamage ~>").gain(1.35).room(.25).mask(chorus2)
220).orbit('vocals')

the ending: the Weide theme, at the voices' level. The clip is 10 s and the section 4 bars (5.3 s): it holds 3 bars and fades out over the last, so the song ends with its section.

225const ending = s("tfweide").gain(2).clip(3).release(1.3).mask(weide)
227const once = (p) => p.mask("<1!64 0!936>")   // play once, then silence

── the form, as rules in code: which sections may follow which ── at is where each starts as written; an empty next ends the song. The written order is always allowed, and is what plays without Jev. Every path plays both verses. times: drop 2 goes round once more at most (SPEC.md, "Loops end").

234const form = walk(section, {
235  max: 28,
236  sections: {
237    intro: { at: 0, next: ['amenIn'] },
238    amenIn: { at: 4, next: ['verse1'] },
239    verse1: { at: 8, next: ['verse1b'] },
240    verse1b: { at: 12, next: ['chorus1'] },
241    chorus1: { at: 16, next: ['drop1'] },
242    drop1: { at: 20, next: ['drop1b'] },
243    drop1b: { at: 24, next: ['drop1c', 'verse2'] },
244    drop1c: { at: 28, next: ['verse2'] },
245    verse2: { at: 32, next: ['verse2b'] },
246    verse2b: { at: 36, next: ['chorus2'] },
247    chorus2: { at: 40, next: ['drop2'] },
248    drop2: { at: 44, next: ['drop2b'], times: 2 },
249    drop2b: { at: 48, next: ['drop2c'] },
250    drop2c: { at: 52, next: ['drop2d', 'drop2'] },
251    drop2d: { at: 56, next: ['weide', 'drop2', 'chorus1', 'verse2'] },
252    weide: { at: 60, next: [] },
253  },
254})

── the song: Jev's section, parts and energy, composed ── Each section plays its own bars, each part at the level Jev chose.

258const band = form.arrange(once(stack(
259  part('xp', xp),
260  part('drums', drums),
261  part('stabs', stabs),
262  part('bass', bass),
263)))

── into the next section: over the last bar of each, the next one's answer ── It plays from the bar's second beat, after any line's tail. Past the last section there is no next one, so nothing plays there.

268const into = dj.into.choice.early(1)
269const transition = into.pick({
270  cut: silence,
271  fill: amen("[12 12] [12 12] [12 12 12 12] [12 12 12 12 12 12 12 12]")
272    .hpf(120).gain(saw.range(.25, .45)).orbit('drums'),
273  riser: part('xp', s("tfxperror*16").speed(saw.range(1, 2)).end(.1).cut(2).gain(saw.range(.15, .55))),
274  dropout: silence,
275}).mask("<0!3 [0 1 1 1]>")

dropout: every part's notes stop from the second half of the third beat, so nothing rings into the last beat and it is silent

278const tail = "<0!3 [0!10 1!6]>"
279const gap = stack("<1!3 [1!10 0!6]>", into.fmap((c) => c !== 'dropout').mask(tail))

Energy scales the band and the transitions; the intro riff, the lines and the Weide theme always play at their own level. One postgain for the whole mix keeps the master's peak under -1 dBFS (SPEC.md, "Mix").

284stack(
285  stack(band.mask(gap), transition).mul(velocity(dj.energy.score.div(3).range(.55, 1))),
286  form.arrange(once(introRiff)).mask(gap),
287  form.arrange(once(stack(lines, ending))),
288).mul(postgain(.5))