From c4deff6a3ec50e748e5db5e4310f1a064d827987 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 17 Sep 2023 13:53:31 +0200 Subject: [PATCH 1/7] Declaration of new controls and refactoring --- package.json | 2 +- src/classes/SoundEvent.ts | 123 +++++++++++++++++++++++++++++--------- yarn.lock | 8 +-- 3 files changed, 99 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 5c8230d..7aea213 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "postcss": "^8.4.27", "showdown": "^2.1.0", "showdown-highlight": "^3.1.0", - "superdough": "^0.9.6", + "superdough": "^0.9.8", "tailwind-highlightjs": "^2.0.1", "tailwindcss": "^3.3.3", "tone": "^14.8.49", diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 748df3e..6b47c92 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -54,8 +54,6 @@ export class SoundEvent extends AudibleEvent { public zdelay = (value: number) => this.updateValue("zdelay", value); public sustainVolume = (value: number) => this.updateValue("sustainVolume", value); - public decay = (value: number) => this.updateValue("decay", value); - public dec = this.decay; public tremolo = (value: number) => this.updateValue("tremolo", value); public duration = (value: number) => this.updateValue("duration", value); public zzfx = (value: number[]) => this.updateValue("zzfx", value); @@ -64,6 +62,7 @@ export class SoundEvent extends AudibleEvent { // Basic Audio Engine Parameters // ================================================================================ + // FM Synthesis public fmi = (value: number) => this.updateValue("fmi", value); public fmh = (value: number) => this.updateValue("fmh", value); public fmenv = (value: "lin" | "exp") => this.updateValue("fmenv", value); @@ -80,16 +79,76 @@ export class SoundEvent extends AudibleEvent { public fmwave = (value: "sine" | "triangle" | "sawtooth" | "square") => this.updateValue("fmwave", value); public fmw = this.fmwave; + + // Filter type + public ftype = (value: "12db" | "24db") => this.updateValue("ftype", value); + public fanchor = (value: number) => this.updateValue("fanchor", value); + + // Amplitude Envelope public attack = (value: number) => this.updateValue("attack", value); public atk = this.attack; - public release = (value: number) => this.updateValue("release", value); - public rel = this.release; + public decay = (value: number) => this.updateValue("decay", value); + public dec = this.decay; public sustain = (value: number) => this.updateValue("sustain", value); public sus = this.sustain; - public unit = (value: number) => this.updateValue("unit", value); - public u = this.unit; + public release = (value: number) => this.updateValue("release", value); + public rel = this.release; + + // Lowpass filter + public lpenv = (value: number) => this.updateValue("lpenv", value); + public lpe = (value: number) => this.updateValue("lpe", value); + public lpattack = (value: number) => this.updateValue("lpattack", value); + public lpa = this.lpattack; + public lbdecay = (value: number) => this.updateValue("lbdecay", value); + public lbd = this.lbdecay; + public lpsustain = (value: number) => this.updateValue("lpsustain", value); + public lpsus = this.lpsustain; + public lprelease = (value: number) => this.updateValue("lprelease", value); + public lpr = this.lprelease; + public cutoff = (value: number) => this.updateValue("cutoff", value); + public lpf = this.cutoff; + public resonance = (value: number) => + this.updateValue("resonance", Math.min(Math.max(value, 0), 50)); + public lpq = this.resonance; + + // Highpass filter + + public hpenv = (value: number) => this.updateValue("hpenv", value); + public hpe = (value: number) => this.updateValue("hpe", value); + public hpattack = (value: number) => this.updateValue("hpattack", value); + public hpa = this.hpattack; + public hpdecay = (value: number) => this.updateValue("hpdecay", value); + public hpd = this.hpdecay; + public hpsustain = (value: number) => this.updateValue("hpsustain", value); + public hpsus = this.hpsustain; + public hprelease = (value: number) => this.updateValue("hprelease", value); + public hpr = this.hprelease; + public hcutoff = (value: number) => this.updateValue("hcutoff", value); + public hpf = this.hcutoff; + public hresonance = (value: number) => this.updateValue("hresonance", value); + public hpq = this.hresonance; + + // Bandpass filter + + public bpenv = (value: number) => this.updateValue("bpenv", value); + public bpe = (value: number) => this.updateValue("bpe", value); + public bpattack = (value: number) => this.updateValue("bpattack", value); + public bpa = this.bpattack; + public bpdecay = (value: number) => this.updateValue("bpdecay", value); + public bpd = this.bpdecay; + public bpsustain = (value: number) => this.updateValue("bpsustain", value); + public bps = this.bpsustain; + public bprelease = (value: number) => this.updateValue("bprelease", value); + public bpr = this.bprelease; + public bandf = (value: number) => this.updateValue("bandf", value); + public bpf = this.bandf; + public bandq = (value: number) => this.updateValue("bandq", value); + public bpq = this.bandq; + public freq = (value: number) => this.updateValue("freq", value); public f = this.freq; + public vib = (value: number) => this.updateValue("vib", value); + public vibmod = (value: number) => this.updateValue("vibmod", value); public fm = (value: number | string) => { if (typeof value === "number") { this.values["fmi"] = value; @@ -100,42 +159,46 @@ export class SoundEvent extends AudibleEvent { } return this; }; + + // Sampler looping + public loop = (value: number) => this.updateValue("loop", value); + public loopBegin = (value: number) => this.updateValue("loopBegin", value); + public loopEnd = (value: number) => this.updateValue("loopEnd", value); + public begin = (value: number) => this.updateValue("begin", value); + public end = (value: number) => this.updateValue("end", value); + + // Gain management + public gain = (value: number) => this.updateValue("gain", value); + public dbgain = (value: number) => + this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); + public db = this.dbgain; + public velocity = (value: number) => this.updateValue("velocity", value); + public vel = this.velocity; + + // Panoramic control (stereo) + public pan = (value: number) => this.updateValue("pan", value); + + // Frequency management + public sound = (value: string) => this.updateValue("s", value); public chord = (value: number[]) => this.updateValue("chord", value); public snd = this.sound; public nudge = (value: number) => this.updateValue("nudge", value); public cut = (value: number) => this.updateValue("cut", value); - public loop = (value: number) => this.updateValue("loop", value); public clip = (value: number) => this.updateValue("clip", value); public n = (value: number) => this.updateValue("n", value); public note = (value: number) => this.updateValue("note", value); public speed = (value: number) => this.updateValue("speed", value); public spd = this.speed; - public begin = (value: number) => this.updateValue("begin", value); - public end = (value: number) => this.updateValue("end", value); - public gain = (value: number) => this.updateValue("gain", value); - public dbgain = (value: number) => - this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); - public db = this.dbgain; - public cutoff = (value: number) => this.updateValue("cutoff", value); - public lpf = this.cutoff; - public resonance = (value: number) => - this.updateValue("resonance", Math.min(Math.max(value, 0), 50)); - public lpq = this.resonance; - public hcutoff = (value: number) => this.updateValue("hcutoff", value); - public hpf = this.hcutoff; - public hresonance = (value: number) => this.updateValue("hresonance", value); - public hpq = this.hresonance; - public bandf = (value: number) => this.updateValue("bandf", value); - public bpf = this.bandf; - public bandq = (value: number) => this.updateValue("bandq", value); - public bpq = this.bandq; + + // Creative sampler effects public coarse = (value: number) => this.updateValue("coarse", value); public crush = (value: number) => this.updateValue("crush", value); public shape = (value: number) => this.updateValue("shape", value); - public pan = (value: number) => this.updateValue("pan", value); public vowel = (value: number) => this.updateValue("vowel", value); public vow = this.vowel; + + // Delay control public delay = (value: number) => this.updateValue("delay", value); public del = this.delay; public delayfeedback = (value: number) => @@ -143,14 +206,16 @@ export class SoundEvent extends AudibleEvent { public delayfb = this.delayfeedback; public delaytime = (value: number) => this.updateValue("delaytime", value); public delayt = this.delaytime; + + // Orbit management public orbit = (value: number) => this.updateValue("orbit", value); public o = this.orbit; + + // Reverb management public room = (value: number) => this.updateValue("room", value); public rm = this.room; public size = (value: number) => this.updateValue("size", value); public sz = this.size; - public velocity = (value: number) => this.updateValue("velocity", value); - public vel = this.velocity; // ================================================================================ // AbstactEvent overrides diff --git a/yarn.lock b/yarn.lock index ec0ad2f..bd0a155 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1272,10 +1272,10 @@ sucrase@^3.32.0: pirates "^4.0.1" ts-interface-checker "^0.1.9" -superdough@^0.9.6: - version "0.9.6" - resolved "https://registry.yarnpkg.com/superdough/-/superdough-0.9.6.tgz#4955827d01d6fd56a24beca50cd310b690519e14" - integrity sha512-f6Aey9+qDJ+NHTuIRO3hXBAhuxg1Z7TeSAdKbG9yrSbfDGcqOWN3Gmfy5ZKe3s34djHWnMotRIHCfzuJcuabuA== +superdough@^0.9.8: + version "0.9.8" + resolved "https://registry.yarnpkg.com/superdough/-/superdough-0.9.8.tgz#de30e364b2613a15a46f35f359a2a0ce30d52611" + integrity sha512-3xy2LXAH4K0JMwwY4G0eJz+u1VIhSFxIE4YZL7E4isjUQZTN9Y0jpX17EzQgvFIW1V3PUJMkDiDgEDWZTRCTKg== dependencies: nanostores "^0.8.1" From b7acee90a11e8ea7d8416a0cd846cdc54d405096 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 17 Sep 2023 19:27:41 +0200 Subject: [PATCH 2/7] commit current state of documentation --- src/API.ts | 1 + src/classes/SoundEvent.ts | 57 +++++++- src/documentation/introduction.ts | 18 +-- src/documentation/synths.ts | 229 +++++++++++++++++++++++++++--- src/examples/excerpts.ts | 11 ++ src/main.ts | 7 +- 6 files changed, 280 insertions(+), 43 deletions(-) diff --git a/src/API.ts b/src/API.ts index 0a916b5..871b99e 100644 --- a/src/API.ts +++ b/src/API.ts @@ -33,6 +33,7 @@ export async function loadSamples() { registerZZFXSounds(), samples("github:Bubobubobubobubo/Dough-Samples/main"), samples("github:Bubobubobubobubo/Dough-Amiga/main"), + samples("github:Bubobubobubobubo/Dough-Waveforms/main"), ]); } diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 6b47c92..7111518 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -93,16 +93,23 @@ export class SoundEvent extends AudibleEvent { public sus = this.sustain; public release = (value: number) => this.updateValue("release", value); public rel = this.release; + public adsr = (a: number, d: number, s: number, r: number) => { + this.attack(a); + this.decay(d); + this.sustain(s); + this.release(r); + return this; + }; // Lowpass filter public lpenv = (value: number) => this.updateValue("lpenv", value); - public lpe = (value: number) => this.updateValue("lpe", value); + public lpe = (value: number) => this.updateValue("lpenv", value); public lpattack = (value: number) => this.updateValue("lpattack", value); public lpa = this.lpattack; - public lbdecay = (value: number) => this.updateValue("lbdecay", value); - public lbd = this.lbdecay; + public lpdecay = (value: number) => this.updateValue("lbdecay", value); + public lpd = this.lpdecay; public lpsustain = (value: number) => this.updateValue("lpsustain", value); - public lpsus = this.lpsustain; + public lps = this.lpsustain; public lprelease = (value: number) => this.updateValue("lprelease", value); public lpr = this.lprelease; public cutoff = (value: number) => this.updateValue("cutoff", value); @@ -110,6 +117,20 @@ export class SoundEvent extends AudibleEvent { public resonance = (value: number) => this.updateValue("resonance", Math.min(Math.max(value, 0), 50)); public lpq = this.resonance; + public lpadsr = ( + depth: number, + a: number, + d: number, + s: number, + r: number + ) => { + this.lpenv(depth); + this.lpattack(a); + this.lpdecay(d); + this.lpsustain(s); + this.lprelease(r); + return this; + }; // Highpass filter @@ -127,6 +148,20 @@ export class SoundEvent extends AudibleEvent { public hpf = this.hcutoff; public hresonance = (value: number) => this.updateValue("hresonance", value); public hpq = this.hresonance; + public hpadsr = ( + depth: number, + a: number, + d: number, + s: number, + r: number + ) => { + this.hpenv(depth); + this.hpattack(a); + this.hpdecay(d); + this.hpsustain(s); + this.hprelease(r); + return this; + }; // Bandpass filter @@ -144,6 +179,20 @@ export class SoundEvent extends AudibleEvent { public bpf = this.bandf; public bandq = (value: number) => this.updateValue("bandq", value); public bpq = this.bandq; + public bpadsr = ( + depth: number, + a: number, + d: number, + s: number, + r: number + ) => { + this.bpenv(depth); + this.bpattack(a); + this.bpdecay(d); + this.bpsustain(s); + this.bprelease(r); + return this; + }; public freq = (value: number) => this.updateValue("freq", value); public f = this.freq; diff --git a/src/documentation/introduction.ts b/src/documentation/introduction.ts index d7e0a13..28a48c5 100644 --- a/src/documentation/introduction.ts +++ b/src/documentation/introduction.ts @@ -1,5 +1,6 @@ import { makeExampleFactory, key_shortcut } from "../Documentation"; import { type Editor } from "../main"; +import { examples } from "../examples/excerpts"; export const introduction = (application: Editor): string => { const makeExample = makeExampleFactory(application); @@ -12,24 +13,9 @@ Welcome to the Topos documentation. These pages are offering you an introduction ${makeExample( "Welcome! Eval to get started", - ` - - -bpm(110) -beat(0.125) && sound('sawtooth') - .note([60, 62, 63, 67, 70].beat(.125) + - [-12,0,12].beat() + [0, 0, 5, 7].bar()) - .sustain(0.1).fmi(0.25).fmh(2).room(0.9) - .gain(0.75).cutoff(500 + usine(8) * [500, 1000, 2000].bar()) - .delay(0.5).delayt(0.25).delayfb(0.25) - .out(); -beat(1) && snd('kick').out(); -beat(2) && snd('snare').out(); -beat(.5) && snd('hat').out(); -`, + examples[Math.floor(Math.random() * examples.length)], true )} - ## What is Topos? diff --git a/src/documentation/synths.ts b/src/documentation/synths.ts index 2d1a98c..867c742 100644 --- a/src/documentation/synths.ts +++ b/src/documentation/synths.ts @@ -6,16 +6,145 @@ export const synths = (application: Editor): string => { return ` # Synthesizers -Topos comes with a small number of basic synthesizers. These synths are based on a basic [WebAudio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) design. For heavy synthesis duties, please use MIDI and speak to more complex instruments. +Topos comes by default with a forever-increasing number of synthesis capabilities. These synths are based on basic [WebAudio](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) designs. For heavy synthesis duties, I recommend you to user other synthesizers or softwares through MIDI. There is already a lot you can do with the built-in synths though! -# Substractive Synthesis - -The sound function can take the name of a synthesizer as first argument. -- sine, sawtooth,triangle, square for the waveform selection. -- cutoff and resonance for adding a low-pass filter with cutoff frequency and filter resonance. - - hcutoff or bandf to switch to a high-pass or bandpass filter. - - hresonance and bandq for the resonance parameter of these filters. +# Timbre, pitch and frequency +The sound function can take the name of a synthesizer or waveform as first argument. This has for effect to turn the sampler we all know and love into a synthesizer. sine, sawtooth,triangle, square are the names used to select classic oscillator waveforms. Note that you can also make use of filters and envelopes to shape the sound to your liking. + +${makeExample( + "Listening to the different waveforms from the sweetest to the harshest", + ` +beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out() +`, + true +)} + +Two functions are primarily used to control the frequency of the synthesizer: +- freq(hz: number): sets the frequency of the oscillator. +- note(note: number): sets the MIDI note of the oscillator (MIDI note converted to hertz). + +${makeExample( + "Selecting a pitch or note", + ` +beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out() +`, + true +)} + +## Vibrato + +You can also add some amount of vibrato to the sound using the vib and vibmod methods. These can turn any oscillator into something more lively and/or into a sound effect when used with a high amount of modulation. + +${makeExample( + "Different vibrato settings", + `beat(1) :: sound('triangle') + .freq(400).release(0.2) + .vib([1/2, 1, 2, 4].beat()) + .vibmod([1,2,4,8].beat(2)) + .out()`, + true +)} + +## Controlling the amplitude + +Controlling the amplitude and duration of the sound can be done using various techniques. The most important thing to learn is probably how set the amplitude (volume) of your synthesizer: +- gain(gain: number): sets the gain of the oscillator. +- velocity(velocity: number): sets the velocity of the oscillator (velocity is a multiple of gain). + +${makeExample( + "Setting the gain", + `beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, + true +)} + +${makeExample( + "Setting the velocity", + `beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, + true +)} + +
+ + Synthesizers typically come with an amplitude envelope that can help you to shape the sound with a slow attack or long release. This is done in Topos using the amplitude envelope, composed of four parameters: attack, decay, sustain and release: +
+ +- attack(attack: number) / atk(atk: number): sets the attack time of the envelope. +- decay(decay: number) / dec(dec: number): sets the decay time of the envelope. +- sustain(sustain: number) / sus(sus: number): sets the sustain time of the envelope. +- release(release: number) / rel(rel: number): sets the release time of the envelope. + +${makeExample( + "Using decay and sustain to set the ADSR envelope", + ` +beat(0.5) :: sound('sawtooth') + .cutoff(1000 + usine() * 4000) + .freq([50,100,150,300].pick()) + .decay(.1).sustain([0.25,0.5].pick()) + .out()`, + true +)} + +This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. + +## Substractive synthesis using filters + +The most basic synthesis technique used since the 1970s is called substractive synthesis. This technique is based on the use of rich sound sources (oscillators) as a base to build rich and moving timbres. Because rich sources contain a lot of different harmonics, you might want to filter some of them to obtain the timbre you are looking for. To do so, Topos comes with a set of basic filters that can be used to shape the sound exactly to your liking. There are three filter types by defaut, with more to be added in the future: + +- **lowpass filter**: filters the high frequencies, keeping the low frequencies. +- **highpass filter**: filtering the low frequencies, keeping the high frequencies. +- **bandpass filter**: filters the low and high frequencies around a frequency band, keeping what's in the middle. + +${makeExample( + "Filtering the high frequencies of an oscillator", + `beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`, + true +)} + +These filters all come with their own set of parameters. Note that we are describing the parameters of the three different filter types here. Choose the right parameters depending on the filter type you are using: + + +### Lowpass filter + +| Method | Alias | Description | +|------------|-----------|---------------------------------| +| cutoff | lpf | cutoff frequency of the lowpass filter | +| resonance | lpq | resonance of the lowpass filter | + +${makeExample( + "Filtering a bass", + `beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`, + true +)} + +### Highpass filter + +| Method | Alias | Description | +|------------|-----------|---------------------------------| +| hcutoff | hpf | cutoff frequency of the highpass filter | +| hresonance | hpq | resonance of the highpass filter | + +${makeExample( + "Filtering a noise source", + `beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`, + true +)} + +### Bandpass filter + +| Method | Alias | Description | +|------------|-----------|---------------------------------| +| bandf | bpf | cutoff frequency of the bandpass filter | +| bandq | bpq | resonance of the bandpass filter | + +${makeExample( + "Sweeping the filter on the same guitar sample", + `beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`, + true +)} + +I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers: + ${makeExample( "Simple synthesizer voice with filter", ` @@ -27,16 +156,6 @@ beat(.5) && snd('sawtooth') true )} -${makeExample( - "Listening to the different waveforms from the sweetest to the harshest", - ` -beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out() - .freq(50) - .out()`, - false -)} - - ${makeExample( "Blessed by the square wave", ` @@ -48,7 +167,6 @@ beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`, false )} - ${makeExample( "Ghost carillon", ` @@ -62,6 +180,79 @@ beat(1/8)::sound('sine') .out()`, false )} + +## Filter envelopes + +The examples we have studied so far are static. They filter the sound around a fixed cutoff frequency. To make the sound more interesting, you can use the ADSR filter envelopes to shape the filter cutoff frequency over time. You will always find amplitude and filter envelopes on most commercial synthesizers. This is done using the following methods: + +### Lowpass envelope + +| Method | Alias | Description | +|------------|-----------|---------------------------------| +| lpenv | lpe | lowpass frequency modulation amount (negative or positive) | +| lpattack | lpa | attack of the lowpass filter | +| lpdecay | lpd | decay of the lowpass filter | +| lpsustain | lps | sustain of the lowpass filter | +| lprelease | lpr | release of the lowpass filter | + +${makeExample( + "Filtering a sawtooth wave dynamically", + `beat(.5) :: sound('sawtooth').note([48,60].beat()) + .cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2)) + .lpenv(-8).lpq(10).out()`, + true +)} + +### Highpass envelope + +| Method | Alias | Description | +|------------|-----------|---------------------------------| +| hpenv | hpe | highpass frequency modulation amount (negative or positive) | +| hpattack | hpa | attack of the highpass filter | +| hpdecay | hpd | decay of the highpass filter | +| hpsustain | hps | sustain of the highpass filter | +| hprelease | hpr | release of the highpass filter | + +${makeExample( + "Let's use another filter using the same example", + `beat(.5) :: sound('sawtooth').note([48,60].beat()) + .hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2)) + .hpenv(8).hpq(10).out()`, + true +)} + +### Bandpass envelope + +| Method | Alias | Description | +|------------|-----------|---------------------------------| +| bpenv | bpe | bandpass frequency modulation amount (negative or positive) | +| bpattack | bpa | attack of the bandpass filter | +| bpdecay | bpd | decay of the bandpass filter | +| bpsustain | bps | sustain of the bandpass filter | +| bprelease | bpr | release of the bandpass filter | + +${makeExample( + "And the bandpass filter, just for fun", + `beat(.5) :: sound('sawtooth').note([48,60].beat()) + .bandf([500,1000,2000].beat(2)) + .bpa([0.25, 0.125, 0.5].beat(2) * 4) + .bpenv(-4).release(2).out() + `, + true +)} + + +## Wavetable synthesis + +${makeExample( + "Acidity test", + ` +beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0].pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0) + .cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0).room(0.9).size(0.9).resonance(20).gain(0.3).out() +beat(1) :: sound('kick').n(4).out() +beat(2) :: sound('snare').out() +beat(.5) :: sound('hh').out()` +)} # Frequency Modulation Synthesis (FM) diff --git a/src/examples/excerpts.ts b/src/examples/excerpts.ts index 171d374..2d66099 100644 --- a/src/examples/excerpts.ts +++ b/src/examples/excerpts.ts @@ -1,4 +1,15 @@ export const examples = [ + `// Acidity test - BuboBuboBubo +beat(.25) :: sound('wt_symetric:8') + .note([50,55,57,60].beat(.25) - [12,0].pick()) + .ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0) + .cutoff(1500 + usine(1/8) * 5000) + .lpadsr(16, 0.2, 0.2, 0.125/2, 0) + .room(0.9).size(0.9).resonance(20) + .gain(0.3).out() +beat(1) :: sound('kick').n(4).out() +beat(2) :: sound('snare').out() +beat(.5) :: sound('hh').out()`, `// Entering the secret room - Bubobubobubo let frequencies = [200,400,600,800,1000,2000].beat(2); beat(2) :: sound('sine').freq(frequencies) diff --git a/src/main.ts b/src/main.ts index 4af2fa8..6ae6b33 100644 --- a/src/main.ts +++ b/src/main.ts @@ -35,6 +35,7 @@ import { makeStringExtensions } from "./StringExtensions"; const classMap = { h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2", h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2", + h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-700 rounded-lg py-2 px-2 lg:mt-16", ul: "text-underline pl-6", li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal", p: "lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal", @@ -51,7 +52,7 @@ const classMap = { "lg:mx-12 py-2 px-6 lg:text-2xl text-white rounded-lg bg-neutral-600", summary: "font-semibold text-xl", table: - "justify-center lg:my-8 my-2 lg:mx-8 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse", + "justify-center lg:my-12 my-2 lg:mx-12 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse", thead: "text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400", th: "", @@ -367,9 +368,7 @@ export class Editor { } // Shift + Ctrl + Enter: Evaluate the currently visible code block - if ( - (event.key === "Enter" && event.shiftKey && event.ctrlKey)) - { + if (event.key === "Enter" && event.shiftKey && event.ctrlKey) { event.preventDefault(); this.currentFile().candidate = this.view.state.doc.toString(); tryEvaluate(this, this.currentFile()); From f089f1088d20896321dcb1ef5c328b2dc84863b0 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 17 Sep 2023 20:35:36 +0200 Subject: [PATCH 3/7] update about wavetable synthesis --- src/documentation/about.ts | 1 + src/documentation/synths.ts | 33 ++++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/documentation/about.ts b/src/documentation/about.ts index 0a1b718..fe51e15 100644 --- a/src/documentation/about.ts +++ b/src/documentation/about.ts @@ -18,6 +18,7 @@ Topos is a free and open-source software distributed under [GPL-3.0](https://git - Felix Roos for the [SuperDough](https://www.npmjs.com/package/superdough) audio engine. - Frank Force for the [ZzFX](https://github.com/KilledByAPixel/ZzFX) synthesizer. +- Kristoffer Ekstrand for the [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) waveforms. - All the [Topos](https//github.com/Bubobubobubobubo/Topos) contributors! **Have fun!** diff --git a/src/documentation/synths.ts b/src/documentation/synths.ts index 867c742..e5cf049 100644 --- a/src/documentation/synths.ts +++ b/src/documentation/synths.ts @@ -168,7 +168,7 @@ beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`, )} ${makeExample( - "Ghost carillon", + "Ghost carillon (move your mouse!)", ` beat(1/8)::sound('sine') .velocity(rand(0.0, 1.0)) @@ -244,16 +244,39 @@ ${makeExample( ## Wavetable synthesis +Topos can also do wavetable synthesis. Wavetable synthesis allows you to use any sound file as a source to build an oscillator. By default, Topos comes with more than 1000 waveforms thanks to the awesome [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) pack made by Kristoffer Ekstrand. Any sample name that contains wt_ as a prefix will be interpreted by the sampler as a wavetable and thus as an oscillator. See for yourself: + ${makeExample( "Acidity test", - ` -beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0].pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0) - .cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0).room(0.9).size(0.9).resonance(20).gain(0.3).out() + `beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0] + .pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0) + .cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0) + .room(0.9).size(0.9).resonance(20).gain(0.3).out() beat(1) :: sound('kick').n(4).out() beat(2) :: sound('snare').out() -beat(.5) :: sound('hh').out()` +beat(.5) :: sound('hh').out()`, + true )} + +Let's explore the galaxy of possible waveforms. It can be hard to explore them all, there is a **lot** of them: + +${makeExample( + "Let's explore some wavetables", + ` +// Exploring a vast galaxy of waveforms +let collection = [ + 'wt_sinharm', 'wt_linear', 'wt_bw_sawrounded', + 'wt_eorgan', 'wt_theremin', 'wt_overtone', + 'wt_fmsynth', 'wt_bitreduced', 'wt_bw_squrounded']; +beat(2) :: v('selec', irand(1, 100)) +beat(2) :: v('swave', collection.pick()) +beat(0.5) :: sound(v('swave')).n(v('selec')).out() +`, + true +)} + +You can work with them just like with any other waveform. Having so many of them makes them also very useful for generating sound effects, percussive, sounds, etc... # Frequency Modulation Synthesis (FM) From 907b3c18009d0a03fbba33db4ca6af8bfcfafe45 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 17 Sep 2023 21:19:57 +0200 Subject: [PATCH 4/7] Temp push --- src/documentation/engine.ts | 29 ++++++++------ src/documentation/synths.ts | 78 +++++++++++++++++++++++++++---------- 2 files changed, 74 insertions(+), 33 deletions(-) diff --git a/src/documentation/engine.ts b/src/documentation/engine.ts index 6d60303..7d450ce 100644 --- a/src/documentation/engine.ts +++ b/src/documentation/engine.ts @@ -6,11 +6,11 @@ export const sound = (application: Editor): string => { return ` # Audio engine -The Topos audio engine is based on the [SuperDough](https://www.npmjs.com/package/superdough) audio backend, leveraging the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). The engine is capable of playing multiple samples, synths and effects at once. It is a very powerful and almost limitless tool to create complex sounds and textures. A set of default sounds are already provided but you can also load your own audio samples and synths! +The Topos audio engine is based on the [SuperDough](https://www.npmjs.com/package/superdough) audio backend that takes advantage of the [Web Audio API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API). The engine is capable of many things such as playing samples, synths and effects all at once. It is a very powerful and almost limitless tool to create complex sounds and textures. A set of default sounds are already provided but you can also load your own audio samples if you wish! ## Sound basics -The basic function to play a sound is... sound(name: string) (you can also write snd to save some precious time). If the given sound or synthesizer exists in the database, it will be automatically queried/started and will start playing. Evaluate the following script in the global window: +The basic function to play a sound is... sound(name: string) (you can also write snd to save some precious time). If the given sound (or synthesizer) is already declared, it will be automatically queried/started and will start playing. Evaluate the following script in the global window: ${makeExample( "Playing sounds is easy", @@ -23,8 +23,8 @@ beat(0.5) && sound('hh').out() In plain english, this translates to: -> Every 48 pulses, play a kick drum. -> Every 24 pulses, play a high-hat. +> Every beat, play a kick drum. +> Every half-beat, play a high-hat. Let's make it slightly more complex: @@ -39,14 +39,15 @@ beat(0.5) && sound('hh').delay(0.25).delaytime(0.125).out(); Now, it reads as follow: -> Every 48 pulses, play a kick drum with some amount of distortion. -> Every 24 pulses, play a high-hat with 25% of the sound injected in +> Every beat, play a kick drum with some amount of distortion. +> Every half-beat, play a high-hat with 25% of the sound injected in > a delay unit, with a delay time of 0.125 seconds. -Let's pause for a moment to explain what we just wrote. There are many things to be said: -- If you remove the **mod** instruction, you will end up with a deluge of kick drums and high-hats. The **mod** instruction is used to filter out pulses. It is a very useful instruction to create basic rhythms. Check out the **Time** page if you haven't read it already. +Let's pause for a moment and explain what is going on: + +- If you remove beat instruction, you will end up with a deluge of kick drums and high-hats. beat in that case, is used to filter time. It is a very useful instruction to create basic rhythms. Check out the **Time** page if you haven't read it already. - Playing a sound always ends up with the .out() method that gives the instruction to send a message to the audio engine. -- Sounds are **composed** by adding qualifiers that will modify the sound or synthesizer being played (_e.g_ sound('...').blabla(...)..something(...).out(). +- Sounds are **composed** by adding qualifiers/parameters that will modify the sound or synthesizer being played (_e.g_ sound('...').blabla(...)..something(...).out(). Think of it as _audio chains_. ${makeExample( '"Composing" a sound or making a sound chain', @@ -106,11 +107,12 @@ beat(.25) && sound('numbers').n(Math.floor(mouseX())).out()`, As we said earlier, the sound('sample_name') function can be chained to _specify_ a sound more. For instance, you can add a filter and some effects to your high-hat: ${makeExample( - "Learning through repetition", + "Let's make something more complex", ` -beat(0.5) && sound('hh') +beat(0.25) && sound('jvbass') .sometimes(s=>s.speed([1,5,10].pick())) .room(0.5) + .gain(1) .cutoff(usine(2) * 5000) .out()`, true @@ -129,7 +131,7 @@ There is a special method to choose the _orbit_ that your sound is going to use: | Method | Alias | Description | |----------|-------|------------------------------------------------------------| -| orbit | | Orbit number | +| orbit | o | Orbit number | ## Amplitude @@ -183,6 +185,9 @@ There are some basic controls over the playback of each sample. This allows you | n | | Select a sample in the current folder (from 0 to infinity) | | begin | | Beginning of the sample playback (between 0 and 1) | | end | | End of the sample (between 0 and 1) | +| loopBegin | | Beginning of the loop section (between 0 and 1) | +| loopEnd | | End of the loop section (between 0 and 1) | +| loop | | Whether to loop or not the audio sample | | speed | | Playback speed (2 = twice as fast) | | cut | | Set with 0 or 1. Will cut the sample as soon as another sample is played on the same bus | | clip | | Multiply the duration of the sample with the given number | diff --git a/src/documentation/synths.ts b/src/documentation/synths.ts index e5cf049..93b0aae 100644 --- a/src/documentation/synths.ts +++ b/src/documentation/synths.ts @@ -38,7 +38,9 @@ You can also add some amount of vibrato to the sound using the vib and ${makeExample( "Different vibrato settings", - `beat(1) :: sound('triangle') + ` +bpm(140); +beat(1) :: sound('triangle') .freq(400).release(0.2) .vib([1/2, 1, 2, 4].beat()) .vibmod([1,2,4,8].beat(2)) @@ -77,15 +79,30 @@ ${makeExample( ${makeExample( "Using decay and sustain to set the ADSR envelope", ` -beat(0.5) :: sound('sawtooth') +beat(0.5) :: sound('wt_piano') .cutoff(1000 + usine() * 4000) - .freq([50,100,150,300].pick()) - .decay(.1).sustain([0.25,0.5].pick()) + .freq(100).decay(.2) + .sustain([0.1,0.5].beat(4)) .out()`, true )} -This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. +This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The adsr() combines all the parameters together. It is a shortcut for setting the ADSR envelope: + +- adsr(attack: number, decay: number, sustain: number, release: number): sets the ADSR envelope. + +${makeExample( + "Replacing the previous example with the adsr() method", + ` +beat(0.5) :: sound('wt_piano') + .cutoff(1000 + usine() * 4000) + .freq(100) + .adsr(0, .2, [0.1,0.5].beat(4), 0) + .out() +`, + true +)} + ## Substractive synthesis using filters @@ -143,6 +160,18 @@ ${makeExample( true )} +## Filter order (type) + +You can also use the ftype method to change the filter type (order). There are two types by default, 12db for a gentle slope or 24db for a really steep filtering slope. The 24db type is particularly useful for substractive synthesis if you are trying to emulate some of the Moog or Prophet sounds: + +- ftype(type: string): sets the filter type (order), either 12db or 24db. + +${makeExample( + "Filtering a bass", + `beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`, + true +)} + I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers: ${makeExample( @@ -248,7 +277,8 @@ Topos can also do wavetable synthesis. Wavetable synthesis allows you to use any ${makeExample( "Acidity test", - `beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0] + ` +beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0] .pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0) .cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0) .room(0.9).size(0.9).resonance(20).gain(0.3).out() @@ -280,39 +310,41 @@ You can work with them just like with any other waveform. Having so many of them # Frequency Modulation Synthesis (FM) -The same basic waveforms can take additional methods to switch to a basic two operators FM synth design (with _carrier_ and _modulator_). FM Synthesis is a complex topic but take this advice: simple ratios will yield stable and harmonic sounds, complex ratios will generate noises, percussions and gritty sounds. +Another really useful technique to know about is FM synthesis, FM standing for _frequency modulation_. Our basic waveforms can take some additional parameters to be transformed into a two operators FM synthesizer (with _carrier_ and _modulator_). FM Synthesis is a very complex and fascinating topic. There are a lot of things you can design using this technique but keep in mind this advice: **simple ratios will yield stable and harmonic sounds, complex ratios will generate noises, percussions and gritty sounds**. - fmi (_frequency modulation index_): a floating point value between 1 and n. - fmh (_frequency modulation harmonic ratio_): a floating point value between 1 and n. - fmwave (_frequency modulation waveform_): a waveform name (_sine_, _triangle_, _sawtooth_ or _pulse_). +There is also an additional parameter, fm that combines fmi and fmh using strings: fm('2:4'). Think of it as a static shortcut for getting some timbres more quickly. + ${makeExample( "80s nostalgia", ` -beat(.25) && snd('sine') - .fmi([1,2,4,8].pick()) - .fmh([1,2,4,8].beat(8)) - .freq([100,150].pick()) - .sustain(0.1) - .out() +beat([.5, 1].beat(8)) && snd('triangle').adsr(0.02, 0.5, 0.5, 0.25) + .fmi(2).fmh(1.5).note([60,55, 60, 63].beat() - 12) + .pan(noise()).out() +beat(.25) && snd('triangle').adsr(0.02, 0.1, 0.1, 0.1) + .fmi([2,4].beat(4)).fmh(1.5) + .pan(noise()).note([60,55, 60, 63].beat() + [0, 7].pick()).out() +beat(2) :: sound('cp').room(1).sz(1).out() `, true )} ${makeExample( - "Giving some love to weird ratios", + "Giving some love to ugly inharmonic sounds", ` beat([.5, .25].bar()) :: sound('sine').fm('2.2183:3.18293').sustain(0.05).out() beat([4].bar()) :: sound('sine').fm('5.2183:4.5').sustain(0.05).out() beat(.5) :: sound('sine') .fmh([1, 1.75].beat()) .fmi($(1) % 30).orbit(2).room(0.5).out()`, - false + true )} - ${makeExample( - "Some peace and serenity", + "Peace and serenity through FM synthesis", ` beat(0.25) :: sound('sine') .note([60, 67, 70, 72, 77].beat() - [0,12].bar()) @@ -322,7 +354,7 @@ beat(0.25) :: sound('sine') .cutoff(1500).delay(0.5).delayt(0.125) .delayfb(0.8).fmh(Math.floor(usine(.5) * 4)) .out()`, - false + true )} **Note:** you can also set the _modulation index_ and the _harmonic ratio_ with the fm argument. You will have to feed both as a string: fm('2:4'). If you only feed one number, only the _modulation index_ will be updated. @@ -465,7 +497,7 @@ beat(4) :: speak("Hello world!") )} ${makeExample( - "Different voices", + "Let's hear people talking about Topos", ` beat(2) :: speak("Topos!","fr",irand(0,5)) `, @@ -476,7 +508,7 @@ beat(2) :: speak("Topos!","fr",irand(0,5)) You can also use speech by chaining methods to a string: ${makeExample( - "Chaining string", + "Foobaba is the real deal", ` onbeat(4) :: "Foobaba".voice(irand(0,10)).speak() `, @@ -501,7 +533,11 @@ ${makeExample( ` bpm(70) - const croissant = ["Volant", "Arc-en-ciel", "Chocolat", "Dansant", "Nuage", "Tournant", "Galaxie", "Chatoyant", "Flamboyant", "Cosmique","Croissant!"]; + const croissant = [ + "Volant", "Arc-en-ciel", "Chocolat", "Dansant", + "Nuage", "Tournant", "Galaxie", "Chatoyant", + "Flamboyant", "Cosmique", "Croissant!" + ]; onbeat(4) :: croissant.bar() .lang("fr") From b96c670015f459ecc7624851b35bdb48d75b6b47 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 17 Sep 2023 22:10:11 +0200 Subject: [PATCH 5/7] temp push 2 --- src/classes/SoundEvent.ts | 12 +++++++----- src/documentation/patterns.ts | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/classes/SoundEvent.ts b/src/classes/SoundEvent.ts index 7111518..3ca63e2 100644 --- a/src/classes/SoundEvent.ts +++ b/src/classes/SoundEvent.ts @@ -15,7 +15,7 @@ export class SoundEvent extends AudibleEvent { this.values = { s: sound.split(":")[0], n: sound.split(":")[1], - dur: 0.5, + dur: app.clock.convertPulseToSecond(app.clock.ppqn), }; } else { this.values = { s: sound, dur: 0.5 }; @@ -55,7 +55,7 @@ export class SoundEvent extends AudibleEvent { public sustainVolume = (value: number) => this.updateValue("sustainVolume", value); public tremolo = (value: number) => this.updateValue("tremolo", value); - public duration = (value: number) => this.updateValue("duration", value); + public dur = (value: number) => this.updateValue("dur", value); public zzfx = (value: number[]) => this.updateValue("zzfx", value); // ================================================================================ @@ -106,7 +106,7 @@ export class SoundEvent extends AudibleEvent { public lpe = (value: number) => this.updateValue("lpenv", value); public lpattack = (value: number) => this.updateValue("lpattack", value); public lpa = this.lpattack; - public lpdecay = (value: number) => this.updateValue("lbdecay", value); + public lpdecay = (value: number) => this.updateValue("lpdecay", value); public lpd = this.lpdecay; public lpsustain = (value: number) => this.updateValue("lpsustain", value); public lps = this.lpsustain; @@ -295,10 +295,12 @@ export class SoundEvent extends AudibleEvent { this.values.chord.forEach((freq: number) => { const copy = { ...this.values }; copy.freq = freq; - superdough(copy, 1 / 4, this.values.dur || 0.5); + // This is pure non-sense but I need to adapt somehow + superdough(copy, this.values.dur * 2, this.values.dur); }); } else { - superdough(this.values, 1 / 4, this.values.dur || 0.5); + // This is pure non-sense but I need to adapt somehow + superdough(this.values, this.values.dur * 2, this.values.dur); } }; } diff --git a/src/documentation/patterns.ts b/src/documentation/patterns.ts index 0f73585..ffeeb48 100644 --- a/src/documentation/patterns.ts +++ b/src/documentation/patterns.ts @@ -111,7 +111,7 @@ beat([.5, 1].random() / 2) :: snd( ${makeExample( "Generate a list of random numbers", - `beat(0.5) && sound('arp').freq([].gen(300,600,10).div(3)).out()`, + `beat(0.5) && sound('arp').freq([].gen(300,600,10).beat(3)).out()`, true )} @@ -188,7 +188,7 @@ beat(1)::snd('sine').sustain(0.1).freq([100,100,100,100,200].unique().beat()).ou - add(): add a given amount to every list element. - sub(): add a given amount to every list element. - mult(): add a given amount to every list element. -- division(): add a given amount to every list element. The method is named division because obviously div is already taken. +- div(): add a given amount to every list element. ${makeExample("Simple addition", `[1, 2 ,3].add(2).beat()`, true)} From fa0f786252e4d065bd893133d74178d58abeda46 Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Sun, 17 Sep 2023 22:25:25 +0200 Subject: [PATCH 6/7] Fix Ziffers breaking with no dur key --- src/classes/ZPlayer.ts | 43 ++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/classes/ZPlayer.ts b/src/classes/ZPlayer.ts index 75313f9..dc2b87b 100644 --- a/src/classes/ZPlayer.ts +++ b/src/classes/ZPlayer.ts @@ -71,7 +71,7 @@ export class Player extends Event { }; origin = (): number => { - return this.app.clock.pulses_since_origin+1; + return this.app.clock.pulses_since_origin + 1; }; pulse = (): number => { @@ -105,14 +105,16 @@ export class Player extends Event { const howAboutNow = // If pattern is just starting (this.notStarted() && - (this.pulse() === 0 || this.origin() >= this.nextBeatInTicks()) && - this.origin() >= this.waitTime) || + (this.pulse() === 0 || this.origin() >= this.nextBeatInTicks()) && + this.origin() >= this.waitTime) || // If pattern is already playing (this.current && - this.pulseToSecond(this.origin()) >= - this.pulseToSecond(this.lastCallTime) + - this.current.duration * 4 * this.pulseToSecond(this.app.api.ppqn()) && - this.origin() >= this.waitTime); + this.pulseToSecond(this.origin()) >= + this.pulseToSecond(this.lastCallTime) + + this.current.duration * + 4 * + this.pulseToSecond(this.app.api.ppqn()) && + this.origin() >= this.waitTime); // Increment index of how many times call is skipped this.skipIndex = howAboutNow ? 0 : this.skipIndex + 1; @@ -143,8 +145,9 @@ export class Player extends Event { "octave", "parsedScale" ); + obj.dur = event.duration; return new SoundEvent(obj, this.app).sound(name); - } else if(event instanceof Chord) { + } else if (event instanceof Chord) { const pitches = event.freqs(); return new SoundEvent(event, this.app).chord(pitches).sound(name); } else if (event instanceof ZRest) { @@ -214,22 +217,22 @@ export class Player extends Event { return this; } - sync(value: string|Function) { - if(this.atTheBeginning() && this.notStarted()) { + sync(value: string | Function) { + if (this.atTheBeginning() && this.notStarted()) { const origin = this.app.clock.pulses_since_origin; - const syncId = typeof value === "function" ? value.name : value; - if(origin>0) { - const syncPattern = this.app.api.patternCache.get(syncId) as Player; - if(syncPattern) { - const syncPatternDuration = syncPattern.ziffers.duration; - const syncPatternStart = syncPattern.startCallTime; - const syncInPulses = syncPatternDuration*4*this.app.clock.ppqn; - this.waitTime = syncPatternStart + syncInPulses; - } + const syncId = typeof value === "function" ? value.name : value; + if (origin > 0) { + const syncPattern = this.app.api.patternCache.get(syncId) as Player; + if (syncPattern) { + const syncPatternDuration = syncPattern.ziffers.duration; + const syncPatternStart = syncPattern.startCallTime; + const syncInPulses = syncPatternDuration * 4 * this.app.clock.ppqn; + this.waitTime = syncPatternStart + syncInPulses; } + } } return this; - } + } out = (): void => { // TODO? From 04d8d7fe6db2ded77c670140aa5095b6bf6723ac Mon Sep 17 00:00:00 2001 From: Raphael Forment Date: Mon, 18 Sep 2023 18:55:14 +0200 Subject: [PATCH 7/7] updating examples with new features --- src/documentation/synths.ts | 286 ++++++++++++++++++------------------ src/examples/excerpts.ts | 101 +++++++------ 2 files changed, 198 insertions(+), 189 deletions(-) diff --git a/src/documentation/synths.ts b/src/documentation/synths.ts index 93b0aae..63c0702 100644 --- a/src/documentation/synths.ts +++ b/src/documentation/synths.ts @@ -13,40 +13,40 @@ Topos comes by default with a forever-increasing number of synthesis capabilitie The sound function can take the name of a synthesizer or waveform as first argument. This has for effect to turn the sampler we all know and love into a synthesizer. sine, sawtooth,triangle, square are the names used to select classic oscillator waveforms. Note that you can also make use of filters and envelopes to shape the sound to your liking. ${makeExample( - "Listening to the different waveforms from the sweetest to the harshest", - ` + "Listening to the different waveforms from the sweetest to the harshest", + ` beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out() `, - true -)} + true + )} Two functions are primarily used to control the frequency of the synthesizer: - freq(hz: number): sets the frequency of the oscillator. - note(note: number): sets the MIDI note of the oscillator (MIDI note converted to hertz). ${makeExample( - "Selecting a pitch or note", - ` + "Selecting a pitch or note", + ` beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out() `, - true -)} + true + )} ## Vibrato You can also add some amount of vibrato to the sound using the vib and vibmod methods. These can turn any oscillator into something more lively and/or into a sound effect when used with a high amount of modulation. ${makeExample( - "Different vibrato settings", - ` + "Different vibrato settings", + ` bpm(140); beat(1) :: sound('triangle') .freq(400).release(0.2) .vib([1/2, 1, 2, 4].beat()) .vibmod([1,2,4,8].beat(2)) .out()`, - true -)} + true + )} ## Controlling the amplitude @@ -55,16 +55,16 @@ Controlling the amplitude and duration of the sound can be done using various te - velocity(velocity: number): sets the velocity of the oscillator (velocity is a multiple of gain). ${makeExample( - "Setting the gain", - `beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, - true -)} + "Setting the gain", + `beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, + true + )} ${makeExample( - "Setting the velocity", - `beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, - true -)} + "Setting the velocity", + `beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, + true + )}
@@ -77,31 +77,31 @@ ${makeExample( - release(release: number) / rel(rel: number): sets the release time of the envelope. ${makeExample( - "Using decay and sustain to set the ADSR envelope", - ` + "Using decay and sustain to set the ADSR envelope", + ` beat(0.5) :: sound('wt_piano') .cutoff(1000 + usine() * 4000) .freq(100).decay(.2) .sustain([0.1,0.5].beat(4)) .out()`, - true -)} + true + )} This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The adsr() combines all the parameters together. It is a shortcut for setting the ADSR envelope: - adsr(attack: number, decay: number, sustain: number, release: number): sets the ADSR envelope. ${makeExample( - "Replacing the previous example with the adsr() method", - ` + "Replacing the previous example with the adsr() method", + ` beat(0.5) :: sound('wt_piano') .cutoff(1000 + usine() * 4000) .freq(100) .adsr(0, .2, [0.1,0.5].beat(4), 0) .out() `, - true -)} + true + )} ## Substractive synthesis using filters @@ -113,10 +113,10 @@ The most basic synthesis technique used since the 1970s is called substractive s - **bandpass filter**: filters the low and high frequencies around a frequency band, keeping what's in the middle. ${makeExample( - "Filtering the high frequencies of an oscillator", - `beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`, - true -)} + "Filtering the high frequencies of an oscillator", + `beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`, + true + )} These filters all come with their own set of parameters. Note that we are describing the parameters of the three different filter types here. Choose the right parameters depending on the filter type you are using: @@ -129,10 +129,10 @@ These filters all come with their own set of parameters. Note that we are descri | resonance | lpq | resonance of the lowpass filter | ${makeExample( - "Filtering a bass", - `beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`, - true -)} + "Filtering a bass", + `beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`, + true + )} ### Highpass filter @@ -142,10 +142,10 @@ ${makeExample( | hresonance | hpq | resonance of the highpass filter | ${makeExample( - "Filtering a noise source", - `beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`, - true -)} + "Filtering a noise source", + `beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`, + true + )} ### Bandpass filter @@ -155,10 +155,10 @@ ${makeExample( | bandq | bpq | resonance of the bandpass filter | ${makeExample( - "Sweeping the filter on the same guitar sample", - `beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`, - true -)} + "Sweeping the filter on the same guitar sample", + `beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`, + true + )} ## Filter order (type) @@ -167,38 +167,38 @@ You can also use the ftype method to change the filter type (order). Th - ftype(type: string): sets the filter type (order), either 12db or 24db. ${makeExample( - "Filtering a bass", - `beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`, - true -)} + "Filtering a bass", + `beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`, + true + )} I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers: ${makeExample( - "Simple synthesizer voice with filter", - ` + "Simple synthesizer voice with filter", + ` beat(.5) && snd('sawtooth') .cutoff([2000,500].pick() + usine(.5) * 4000) .resonance(0.9).freq([100,150].pick()) .out() `, - true -)} + true + )} ${makeExample( - "Blessed by the square wave", - ` + "Blessed by the square wave", + ` beat(4) :: [100,101].forEach((freq) => sound('square').freq(freq).sustain(0.1).out()) beat(.5) :: [100,101].forEach((freq) => sound('square').freq(freq*2).sustain(0.01).out()) beat([.5, .75, 2].beat()) :: [100,101].forEach((freq) => sound('square') .freq(freq*4 + usquare(2) * 200).sustain(0.125).out()) beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`, - false -)} + false + )} ${makeExample( - "Ghost carillon (move your mouse!)", - ` + "Ghost carillon (move your mouse!)", + ` beat(1/8)::sound('sine') .velocity(rand(0.0, 1.0)) .delay(0.75).delayt(.5) @@ -207,8 +207,8 @@ beat(1/8)::sound('sine') .freq(mouseX()) .gain(0.25) .out()`, - false -)} + false + )} ## Filter envelopes @@ -223,14 +223,16 @@ The examples we have studied so far are static. They filter the sound around a f | lpdecay | lpd | decay of the lowpass filter | | lpsustain | lps | sustain of the lowpass filter | | lprelease | lpr | release of the lowpass filter | +| lpadsr | | (**takes five arguments**) set all the parameters | + ${makeExample( - "Filtering a sawtooth wave dynamically", - `beat(.5) :: sound('sawtooth').note([48,60].beat()) + "Filtering a sawtooth wave dynamically", + `beat(.5) :: sound('sawtooth').note([48,60].beat()) .cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2)) .lpenv(-8).lpq(10).out()`, - true -)} + true + )} ### Highpass envelope @@ -241,14 +243,16 @@ ${makeExample( | hpdecay | hpd | decay of the highpass filter | | hpsustain | hps | sustain of the highpass filter | | hprelease | hpr | release of the highpass filter | +| hpadsr | | (**takes five arguments**) set all the parameters | + ${makeExample( - "Let's use another filter using the same example", - `beat(.5) :: sound('sawtooth').note([48,60].beat()) + "Let's use another filter using the same example", + `beat(.5) :: sound('sawtooth').note([48,60].beat()) .hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2)) .hpenv(8).hpq(10).out()`, - true -)} + true + )} ### Bandpass envelope @@ -259,16 +263,18 @@ ${makeExample( | bpdecay | bpd | decay of the bandpass filter | | bpsustain | bps | sustain of the bandpass filter | | bprelease | bpr | release of the bandpass filter | +| bpadsr | | (**takes five arguments**) set all the parameters | + ${makeExample( - "And the bandpass filter, just for fun", - `beat(.5) :: sound('sawtooth').note([48,60].beat()) + "And the bandpass filter, just for fun", + `beat(.5) :: sound('sawtooth').note([48,60].beat()) .bandf([500,1000,2000].beat(2)) .bpa([0.25, 0.125, 0.5].beat(2) * 4) .bpenv(-4).release(2).out() `, - true -)} + true + )} ## Wavetable synthesis @@ -276,8 +282,8 @@ ${makeExample( Topos can also do wavetable synthesis. Wavetable synthesis allows you to use any sound file as a source to build an oscillator. By default, Topos comes with more than 1000 waveforms thanks to the awesome [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) pack made by Kristoffer Ekstrand. Any sample name that contains wt_ as a prefix will be interpreted by the sampler as a wavetable and thus as an oscillator. See for yourself: ${makeExample( - "Acidity test", - ` + "Acidity test", + ` beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0] .pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0) .cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0) @@ -285,15 +291,15 @@ beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0] beat(1) :: sound('kick').n(4).out() beat(2) :: sound('snare').out() beat(.5) :: sound('hh').out()`, - true -)} + true + )} Let's explore the galaxy of possible waveforms. It can be hard to explore them all, there is a **lot** of them: ${makeExample( - "Let's explore some wavetables", - ` + "Let's explore some wavetables", + ` // Exploring a vast galaxy of waveforms let collection = [ 'wt_sinharm', 'wt_linear', 'wt_bw_sawrounded', @@ -303,8 +309,8 @@ beat(2) :: v('selec', irand(1, 100)) beat(2) :: v('swave', collection.pick()) beat(0.5) :: sound(v('swave')).n(v('selec')).out() `, - true -)} + true + )} You can work with them just like with any other waveform. Having so many of them makes them also very useful for generating sound effects, percussive, sounds, etc... @@ -319,8 +325,8 @@ Another really useful technique to know about is FM synthesis, FM standing for _ There is also an additional parameter, fm that combines fmi and fmh using strings: fm('2:4'). Think of it as a static shortcut for getting some timbres more quickly. ${makeExample( - "80s nostalgia", - ` + "80s nostalgia", + ` beat([.5, 1].beat(8)) && snd('triangle').adsr(0.02, 0.5, 0.5, 0.25) .fmi(2).fmh(1.5).note([60,55, 60, 63].beat() - 12) .pan(noise()).out() @@ -329,23 +335,23 @@ beat(.25) && snd('triangle').adsr(0.02, 0.1, 0.1, 0.1) .pan(noise()).note([60,55, 60, 63].beat() + [0, 7].pick()).out() beat(2) :: sound('cp').room(1).sz(1).out() `, - true -)} + true + )} ${makeExample( - "Giving some love to ugly inharmonic sounds", - ` + "Giving some love to ugly inharmonic sounds", + ` beat([.5, .25].bar()) :: sound('sine').fm('2.2183:3.18293').sustain(0.05).out() beat([4].bar()) :: sound('sine').fm('5.2183:4.5').sustain(0.05).out() beat(.5) :: sound('sine') .fmh([1, 1.75].beat()) .fmi($(1) % 30).orbit(2).room(0.5).out()`, - true -)} + true + )} ${makeExample( - "Peace and serenity through FM synthesis", - ` + "Peace and serenity through FM synthesis", + ` beat(0.25) :: sound('sine') .note([60, 67, 70, 72, 77].beat() - [0,12].bar()) .attack(0.2).release(0.5).gain(0.25) @@ -354,8 +360,8 @@ beat(0.25) :: sound('sine') .cutoff(1500).delay(0.5).delayt(0.125) .delayfb(0.8).fmh(Math.floor(usine(.5) * 4)) .out()`, - true -)} + true + )} **Note:** you can also set the _modulation index_ and the _harmonic ratio_ with the fm argument. You will have to feed both as a string: fm('2:4'). If you only feed one number, only the _modulation index_ will be updated. @@ -366,8 +372,8 @@ There is also a more advanced set of parameters you can use to control the envel - fmrelease / fmrel: release time of the modulator envelope. ${makeExample( - "FM Synthesis with envelope control", - ` + "FM Synthesis with envelope control", + ` beat(.5) :: sound('sine') .note([50,53,55,57].beat(.5) - 12) .fmi(0.5 + usine(.25) * 1.5) @@ -375,8 +381,8 @@ beat(.5) :: sound('sine') .fmwave('triangle') .fmsus(0).fmdec(0.2).out() `, - true -)} + true + )} ## ZzFX @@ -385,15 +391,15 @@ beat(.5) :: sound('sine') ZZfX can be triggered by picking a default ZZfX waveform in the following list: z_sine, z_triangle, z_sawtooth, z_tan, z_noise. ${makeExample( - "Picking a waveform", - ` + "Picking a waveform", + ` beat(.5) :: sound(['z_sine', 'z_triangle', 'z_sawtooth', 'z_tan', 'z_noise'].beat()).out() `, - true -)} + true + )} ${makeExample( - "Minimalist chiptune", - ` + "Minimalist chiptune", + ` beat(.5) :: sound('z_triangle') .note([60, 67, 72, 63, 65, 70].beat(.5)) .zrand(0).curve([1,2,3,4].beat(1)) @@ -403,8 +409,8 @@ beat(.5) :: sound('z_triangle') .room(0.5).size(0.9) .pitchJumpTime(0.01).out() `, - true -)} + true + )} It comes with a set of parameters that can be used to tweak the sound. Don't underestimate this synth! It is very powerful for generating anything ranging from chaotic noise sources to lush pads: @@ -430,8 +436,8 @@ It comes with a set of parameters that can be used to tweak the sound. Don't und |duration|| Total sound duration (overrides envelope) | ${makeExample( - "Chaotic Noise source", - ` + "Chaotic Noise source", + ` beat(.25) :: sound('z_tan') .note(40).noise(rand(0.0, 1.0)) .pitchJump(84).pitchJumpTime(rand(0.0, 1.0)) @@ -441,21 +447,21 @@ beat(.25) :: sound('z_tan') .sustain(0).decay([0.2, 0.1].pick()) .out() `, - true -)} + true + )} ${makeExample( - "What is happening to me?", - ` + "What is happening to me?", + ` beat(1) :: snd('zzfx').zzfx([ [4.77,,25,,.15,.2,3,.21,,2.4,,,,,,,.23,.35], [1.12,,97,.11,.16,.01,4,.77,,,30,.17,,,-1.9,,.01,.67,.2] ].beat()).out() `, - false -)} + false + )} ${makeExample( - "Les voitures dans le futur", - ` + "Les voitures dans le futur", + ` beat(1) :: sound(['z_triangle', 'z_sine'].pick()) .note([60,63,72,75].pick()).tremolo(16) .zmod([0, 1/2, 1/8].div(2).pick()) @@ -463,18 +469,18 @@ beat(1) :: sound(['z_triangle', 'z_sine'].pick()) .room(0.9).size(0.9) .delayt(0.75).delayfb(0.5).out() `, - false -)} + false + )} Note that you can also design sounds [on this website](https://killedbyapixel.github.io/ZzFX/) and copy the generated code in Topos. To do so, please use the zzfx method with the generated array: ${makeExample( - "Designing a sound on the ZzFX website", - ` + "Designing a sound on the ZzFX website", + ` beat(2) :: sound('zzfx').zzfx([3.62,,452,.16,.1,.21,,2.5,,,403,.05,.29,,,,.17,.34,.22,.68]).out() `, - true -)} + true + )} # Speech synthesis @@ -489,35 +495,35 @@ Topos can also speak using the [Web Speech API](https://developer.mozilla.org/en - volume(number): speaking volume, from 0.0 to 1.0. ${makeExample( - "Hello world!", - ` + "Hello world!", + ` beat(4) :: speak("Hello world!") `, - true -)} + true + )} ${makeExample( - "Let's hear people talking about Topos", - ` + "Let's hear people talking about Topos", + ` beat(2) :: speak("Topos!","fr",irand(0,5)) `, - true -)} + true + )} You can also use speech by chaining methods to a string: ${makeExample( - "Foobaba is the real deal", - ` + "Foobaba is the real deal", + ` onbeat(4) :: "Foobaba".voice(irand(0,10)).speak() `, - true -)} + true + )} ${makeExample( - "Building string and chaining", - ` + "Building string and chaining", + ` const subject = ["coder","user","loser"].pick() const verb = ["is", "was", "isnt"].pick() const object = ["happy","sad","tired"].pick() @@ -525,12 +531,12 @@ ${makeExample( beat(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).speak() `, - true -)} + true + )} ${makeExample( - "Live coded poetry with array and string chaining", - ` + "Live coded poetry with array and string chaining", + ` bpm(70) const croissant = [ @@ -545,7 +551,7 @@ ${makeExample( .rate(rand(.4,.6)) .speak(); `, - true -)} + true + )} `; }; diff --git a/src/examples/excerpts.ts b/src/examples/excerpts.ts index 2d66099..8c916de 100644 --- a/src/examples/excerpts.ts +++ b/src/examples/excerpts.ts @@ -13,24 +13,26 @@ beat(.5) :: sound('hh').out()`, `// Entering the secret room - Bubobubobubo let frequencies = [200,400,600,800,1000,2000].beat(2); beat(2) :: sound('sine').freq(frequencies) - .delay(0.5).delayt(usine(.5)).delayfb(0.8).size(0.9).room(0.9).out() + .delay(0.25).delayt(usine(.25)).delayfb(0.25).size(0.9).room(0.9).out() beat(2) :: app.hydra.osc(frequencies/100, 0.25, 0.5) .posterize([32,4,8,16].beat(2)).rotate(cpulse()) .kaleid([1,2,3].beat()).out()`, `// The real internet of things - Bubobubobubo beat(.5) :: sound('STA6').cut(1).vel(0.4) .orbit(2).room(0.5).size(0.5).n(irand(1,4)) - .speed([0.15, 0.30].beat()).out() + .speed([0.15, 0.30].beat() * 1.5).loop([1,0] + .beat(.125)).loopEnd([1,0.5].beat(2)).out() binrhythm(.5, 50) :: sound('shaker').out() binrhythm(.5, 52) :: sound('808bd').n(3).out() rhythm(.25, 6, 8) :: sound('808sd').out()`, `// Numerology - Bubobubobubo +bpm(130); let mel = [ "0.125 _ (0 3 7 0 3 5 0 3 9)+(0 2)", "0.125 (0 7 0 10 0 5)+(0 3)", "0.125 (0 3 7 0 3 5 0 3 9)+(0 2)", "0.125 (0 2 4 5 9 10)+(0 2)", ].beat(4); z0(mel) - .scale('minor').sound('square').cutoff(800 + usine(.5) * 5000) + .scale('minor').sound('wt_piano').cutoff(800 + usine(.5) * 5000) .fmi([2, 4, 8].beat(2)).fmh(flip(2) ? 2 : 4) .delay(bpm() / 60 / 9).delayt(0.25).delayfb(0.5) .fmsus(0.3).fmrel(0.3).rel(rand(0.5,0.8)) @@ -42,34 +44,35 @@ beat([.25, .5].beat(4)) :: flip(6) && sound('dr') `// Harmonic Leaps and Gaps -- Bubobubobubo let oscillation = quant(usine(.25) * 20, [35, 40, 38, 50, 55]); let tonal = [0, 5, 0, 0, 7].palindrome().bar() + 40 - 24; -[tonal, tonal + 7, tonal + [12,3].bar()].forEach((e) => { - flip(2) :: beat(.5) :: sound('square').fmi(2) - .cutoff(500 + usine(1/2)).n(irand(1,10)) +[tonal, tonal + 7, tonal + [12, 3, 24].bar()].forEach((e) => { + flip(2) :: beat(.5) :: sound('square').fmi(1.5).fmh(0.99) + .cutoff(500 + usine(1/2)).n($(1) % 20).cut(.1) .note(e + oscillation + [0, 5].beat(.5)).out() - !flip(2) :: beat(.5) :: sound('sawtooth').fmi(2) - .cutoff(500 + usine(1/2) * 5000).n(irand(1,10)) + !flip(2) :: beat(.5) :: sound('sawtooth').fmi(1.49).fmh(1.01) + .cutoff(500 + usine(1/2) * 5000).n(irand(1,10)).cut(.1) .note(e + oscillation + [0, 5].beat(.5)).out() }); -oncount([2, 3.5, [5,0].pick()], 6) :: sound('snare').out() - `, +oncount([2, 4, 4.5], 6) :: sound('snare').n(4).out() +rhythm(.25, 6, 8) :: sound('shaker').end(0.25).out() +rhythm(.5,4,8) :: sound('bd').out()`, `// Computer Music Classroom, Monday (8AM) -- Bubobubobubo -let ur = [0, 12, 7].beat(24), +let ur = [0, 5, 12, 7, 5].beat(24), fundamental = [0, 5, 10, 8, 6].repeatAll(4).bar(); -beat(.25) :: sound('square') - .note(ur + fundamental + 40).n(1 + $(1) % 8) +beat(.25) :: sound('triangle') + .note(ur + fundamental + 40).n(1 + $(1) % 16) .atk(0.05).sustain(0.1).release(0.1) - .room(0.9).size(0.9) + .room(0.9).size(0.9).vib(2).vibmod(0.125) .out() -beat(.25) :: sound('sawtooth') - .note(ur + fundamental + 47).n(1 + $(2) % 8) +beat(.25) :: sound('triangle') + .note(ur + fundamental + 47).n(1 + $(2) % 16) .atk(0.05).sustain(0.1).release(0.1) - .room(0.9).size(0.9) + .room(0.9).size(0.9).vib(4).vibmod(0.125) .out() beat(.25) :: sound(['sawtooth', 'square'].bar()) - .note(ur + fundamental + 40+[10,12].bar()).n(1 + $(3) % 8) - .atk(0.05).sustain(0.1).release(0.1) - .room(0.9).size(0.9).out() - `, + .note(ur + fundamental + 40+[10,12].bar()).n(1 + $(3) % 16) + .atk(0.05).sustain(0.1).release(0.1).vib([0, 0, 0, 0.5]) + .vibmod(often() ? 1 : [16,8,4].pick()) + .room(0.9).size(0.9).out()`, `// Lamento for Digital Harpists -- Bubobubobubo beat(4) :: sound('triangle') .note(60).fmwave('triangle').fmi(3.95) @@ -92,21 +95,18 @@ beat([4, 2, 8].pick() / [2,1].bar()) :: sound('triangle') let melody = [30,30,34,35,37].palindrome() .beat() + [0, -12].repeatAll(2).beat(2) if (flip(8, 75)) { - log('first section') - rhythm(.5, 4, 8) :: sound('ST12').n([0,1,2].beat(0.5)).speed(0.5).out() - rhythm(.5, 6, 8) :: sound('ST20').n([0,1,2].beat(0.5) + 20) + rhythm(.5, 4, 8) :: sound('ST12').n([0,1,2].beat(0.5)).speed(0.5).gain(0.4).out() + rhythm(.5, 6, 8) :: sound('ST20').n([0,1,2].beat(0.5) + 20).gain(0.4) .speed(0.25).end(0.1).orbit(2).room(0.5).size(0.5).out() - beat(.5) :: sound('ST01').note(melody) + beat(.5) :: sound('ST01').note(melody).gain(0.4) .n($(1)).speed(0.5).room(0.5).size(0.5).out() } else { - log('second section') rhythm(.5, 2, 8) :: sound('ST20') - .n([0,1,2].beat(0.5)).speed(0.5) + .n([0,1,2].beat(0.5)).speed(0.5).gain(0.4) .end(0.1).out() - beat(.5) :: sound('ST01').note(melody).n($(1)).speed(0.5).end(0.1).out() - beat(1) :: sound('ST02').note(melody).n($(1)).speed(0.5).end(0.1).out() -} - `, + beat(.5) :: sound('ST01').note(melody).n($(1)).speed(0.5).gain(0.4).end(0.1).out() + beat(1) :: sound('ST02').note(melody).n($(1)).speed(0.5).gain(0.4).end(0.1).out() +}`, `// Race day - Bubobubobubo bpm(125); beat(.5) :: sound('STB6') @@ -173,6 +173,7 @@ beat(0.25) :: snd('hh').out(); beat(2) :: snd('square') .cutoff(500).note(50-12).resonance(20).sustain(0.2).out() beat(1/4)::snd(['sawtooth', 'triangle', 'square'].beat(1)) + .vib(2).vibmod(0.5) .note([50, 53, 55, 50, 50, 52, 58, 50+12, 50+15].beat(4) + [0, 12, 24].beat(0.5)) .cutoff(usine(.5)*10000).resonance([10,20].beat(2)) .fmi($(1) % 10).fmh($(2) % 5) @@ -183,17 +184,20 @@ beat(1/4)::snd(['sawtooth', 'triangle', 'square'].beat(1)) beat(4)::snd('amencutup').n($(19)).cut(1).orbit(2).pan(rand(0.0,1.0)).out()`, `// Crazy arpeggios - Bubobubobubo bpm(110) -beat(0.125) && sound('sawtooth') +beat([0.25, 0.5].beat(4)) && sound('sawtooth') .note([60, 62, 63, 67, 70].beat(.125) + [-12,0,12].beat() + [0, 0, 5, 7].bar()) .sustain(0.1).fmi(0.25).fmh(2).room(0.9) - .gain(0.75).cutoff(500 + usine(8) * [500, 1000, 2000].bar()) - .delay(0.5).delayt(0.25).delayfb(0.25) + .gain(0.75).cutoff([500,1000].beat(2)) + .lpadsr([6,8].beat(), 1/16, .125, 0, 0) + .delay(0.5).delayt(0.25) + .delayfb(0.25) .out(); -beat(1) && snd('kick').out(); -beat(2) && snd('snare').out(); -beat(.5) && snd('hat').out(); -`, +binrhythm([.25, .5].beat(), 11) :: often() :: sound('dr') + .n([0, 1].beat()).gain(0.4).out() +binrhythm([.5, .25, .5, .5, .25].beat(), 122) :: often() :: sound('dr') + .n([9, 24].beat()).gain(0.4).out() +rhythm([.5, .25, 1].beat(2), 4, 8) :: sound('kick').gain(0.6).out()`, `// Obscure Shenanigans - Bubobubobubo beat([1/4,1/8,1/16].beat(8)):: sound('sine') .freq([100,50].beat(16) + 50 * ($(1)%10)) @@ -205,15 +209,14 @@ flip(3) :: beat([.25,.5].beat(.5)) :: sound('dr') .n([8,9].pick()).gain([.8,.5,.25,.1,.0].beat(.25)).out() `, `// Resonance bliss - Bubobubobubo -beat(.25)::snd('arpy') - .note(30 + [0,3,7,10].beat()) - .cutoff(usine(.5) * 5000).resonance(10).gain(0.3) - .end(0.8).room(0.9).size(0.9).n(0).out(); -beat([.25,.125].beat(2))::snd('arpy') - .note(30 + [0,3,7,10].beat()) - .cutoff(usine(.5) * 5000).resonance(20).gain(0.3) - .end(0.8).room(0.9).size(0.9).n(3).out(); -beat(.5) :: snd('arpy').note( - [30, 33, 35].repeatAll(4).beat(1) - [12,0].beat(0.5)).out() -`, +beat([.25,.125].beat(4))::snd('arpy:4') + .note(30 + [0,3,7,10, 12, 5, 7].beat()).speed(0.999).pan(0) + .cutoff(1000 + usine(1/8) * 400).lpadsr(8, 1/8, 1/16, 0, 0) + .resonance(1).gain(0.4).end(0.8).room(0.9).size(0.9).n(0).out(); +beat([.25,.125, .5].beat(4))::snd('arpy:4') + .note(30 + [0,3,7,10, 12, 5, 7].beat()).speed(1.001).pan(1) + .cutoff(100 + usine(1/8) * 800).lpadsr(5, 0, [1/8, 1.16].beat(), 0, 0) + .resonance(5).gain(0.4).end(0.8).room(0.9).size(0.9).n(3).out(); +beat(.5) :: snd('arpy').note([30, 33, 35].repeatAll(4).beat(1) - [24,12].beat(0.5)) + .cutoff(500).lpadsr(8, 0.05, .125, 0, 0).out()`, ];