Merge pull request #50 from Bubobubobubobubo/48-superdough-features

Adding new Superdough Features
This commit is contained in:
Raphaël Forment
2023-09-18 17:56:29 +01:00
committed by GitHub
12 changed files with 624 additions and 243 deletions

View File

@ -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",

View File

@ -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"),
]);
}

View File

@ -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 };
@ -54,16 +54,15 @@ 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 dur = (value: number) => this.updateValue("dur", value);
public zzfx = (value: number[]) => this.updateValue("zzfx", value);
// ================================================================================
// 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,125 @@ 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;
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("lpenv", value);
public lpattack = (value: number) => this.updateValue("lpattack", value);
public lpa = this.lpattack;
public lpdecay = (value: number) => this.updateValue("lpdecay", value);
public lpd = this.lpdecay;
public lpsustain = (value: number) => this.updateValue("lpsustain", value);
public lps = 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;
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
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;
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
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 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;
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 +208,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 +255,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
@ -181,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);
}
};
}

View File

@ -111,7 +111,9 @@ export class Player extends Event {
(this.current &&
this.pulseToSecond(this.origin()) >=
this.pulseToSecond(this.lastCallTime) +
this.current.duration * 4 * this.pulseToSecond(this.app.api.ppqn()) &&
this.current.duration *
4 *
this.pulseToSecond(this.app.api.ppqn()) &&
this.origin() >= this.waitTime);
// Increment index of how many times call is skipped
@ -143,6 +145,7 @@ export class Player extends Event {
"octave",
"parsedScale"
);
obj.dur = event.duration;
return new SoundEvent(obj, this.app).sound(name);
} else if (event instanceof Chord) {
const pitches = event.freqs();

View File

@ -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!**

View File

@ -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... <ic>sound(name: string)</ic> (you can also write <ic>snd</ic> 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... <ic>sound(name: string)</ic> (you can also write <ic>snd</ic> 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 <ic>beat</ic> instruction, you will end up with a deluge of kick drums and high-hats. <ic>beat</ic> 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 <ic>.out()</ic> 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_ <ic>sound('...').blabla(...)..something(...).out()</ic>.
- Sounds are **composed** by adding qualifiers/parameters that will modify the sound or synthesizer being played (_e.g_ <ic>sound('...').blabla(...)..something(...).out()</ic>. 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 <ic>sound('sample_name')</ic> 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 <ic>0</ic> to infinity) |
| begin | | Beginning of the sample playback (between <ic>0</ic> and <ic>1</ic>) |
| end | | End of the sample (between <ic>0</ic> and <ic>1</ic>) |
| loopBegin | | Beginning of the loop section (between <ic>0</ic> and <ic>1</ic>) |
| loopEnd | | End of the loop section (between <ic>0</ic> and <ic>1</ic>) |
| loop | | Whether to loop or not the audio sample |
| speed | | Playback speed (<ic>2</ic> = twice as fast) |
| cut | | Set with <ic>0</ic> or <ic>1</ic>. 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 |

View File

@ -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,25 +13,10 @@ 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?
Topos is an _algorithmic_ sequencer. Topos uses small algorithms to represent musical sequences and processes. These can be written in just a few lines of code. Topos is made to be _live-coded_. The _live coder_ strives for the constant interaction with algorithms and sound during a musical performance. Topos is aiming to be a digital playground for live algorithmic music.

View File

@ -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).beat()).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
- <ic>add()</ic>: add a given amount to every list element.
- <ic>sub()</ic>: add a given amount to every list element.
- <ic>mult()</ic>: add a given amount to every list element.
- <ic>division()</ic>: add a given amount to every list element. The method is named <ic>division</ic> because obviously <ic>div</ic> is already taken.
- <ic>div()</ic>: add a given amount to every list element.
${makeExample("Simple addition", `[1, 2 ,3].add(2).beat()`, true)}

View File

@ -6,15 +6,173 @@ 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
# Timbre, pitch and frequency
The <ic>sound</ic> function can take the name of a synthesizer as first argument.
- <ic>sine</ic>, <ic>sawtooth</ic>,<ic>triangle</ic>, <ic>square</ic> for the waveform selection.
- <ic>cutoff</ic> and <ic>resonance</ic> for adding a low-pass filter with cutoff frequency and filter resonance.
- <ic>hcutoff</ic> or <ic>bandf</ic> to switch to a high-pass or bandpass filter.
- <ic>hresonance</ic> and <ic>bandq</ic> for the resonance parameter of these filters.
The <ic>sound</ic> 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. <ic>sine</ic>, <ic>sawtooth</ic>,<ic>triangle</ic>, <ic>square</ic> 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:
- <ic>freq(hz: number)</ic>: sets the frequency of the oscillator.
- <ic>note(note: number)</ic>: 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 <ic>vib</ic> and <ic>vibmod</ic> 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",
`
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
)}
## 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:
- <ic>gain(gain: number)</ic>: sets the gain of the oscillator.
- <ic>velocity(velocity: number)</ic>: 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
)}
<div class="mt-4 mb-4 lg:grid lg:grid-cols-4 lg:gap-4">
<img class="col-span-1 lg:ml-12 bg-gray-100 rounded-lg px-2 py-2", src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ADSR_Envelope_Graph.svg/1280px-ADSR_Envelope_Graph.svg.png" width="400" />
<z class="pl-8 lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal col-span-3 ">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: <ic>attack</ic>, <ic>decay</ic>, <ic>sustain</ic> and <ic>release</ic>:</z>
</div>
- <ic>attack(attack: number)</ic> / <ic>atk(atk: number)</ic>: sets the attack time of the envelope.
- <ic>decay(decay: number)</ic> / <ic>dec(dec: number)</ic>: sets the decay time of the envelope.
- <ic>sustain(sustain: number)</ic> / <ic>sus(sus: number)</ic>: sets the sustain time of the envelope.
- <ic>release(release: number)</ic> / <ic>rel(rel: number)</ic>: sets the release time of the envelope.
${makeExample(
"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
)}
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 <ic>adsr()</ic> combines all the parameters together. It is a shortcut for setting the ADSR envelope:
- <ic>adsr(attack: number, decay: number, sustain: number, release: number)</ic>: 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
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
)}
## Filter order (type)
You can also use the <ic>ftype</ic> method to change the filter type (order). There are two types by default, <ic>12db</ic> for a gentle slope or <ic>24db</ic> for a really steep filtering slope. The <ic>24db</ic> type is particularly useful for substractive synthesis if you are trying to emulate some of the Moog or Prophet sounds:
- <ic>ftype(type: string)</ic>: sets the filter type (order), either <ic>12db</ic> or <ic>24db</ic>.
${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(
"Simple synthesizer voice with filter",
@ -27,16 +185,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,9 +196,8 @@ beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`,
false
)}
${makeExample(
"Ghost carillon",
"Ghost carillon (move your mouse!)",
`
beat(1/8)::sound('sine')
.velocity(rand(0.0, 1.0))
@ -63,42 +210,147 @@ beat(1/8)::sound('sine')
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 |
| lpadsr | | (**takes five arguments**) set all the parameters |
${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 |
| 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())
.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 |
| bpadsr | | (**takes five arguments**) set all the parameters |
${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
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 <ic>wt_</ic> 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(1) :: sound('kick').n(4).out()
beat(2) :: sound('snare').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)
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**.
- <ic>fmi</ic> (_frequency modulation index_): a floating point value between <ic>1</ic> and <ic>n</ic>.
- <ic>fmh</ic> (_frequency modulation harmonic ratio_): a floating point value between <ic>1</ic> and <ic>n</ic>.
- <ic>fmwave</ic> (_frequency modulation waveform_): a waveform name (_sine_, _triangle_, _sawtooth_ or _pulse_).
There is also an additional parameter, <ic>fm</ic> that combines <ic>fmi</ic> and <ic>fmh</ic> using strings: <ic>fm('2:4')</ic>. 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())
@ -108,7 +360,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 <ic>fm</ic> argument. You will have to feed both as a string: <ic>fm('2:4')</ic>. If you only feed one number, only the _modulation index_ will be updated.
@ -251,7 +503,7 @@ beat(4) :: speak("Hello world!")
)}
${makeExample(
"Different voices",
"Let's hear people talking about Topos",
`
beat(2) :: speak("Topos!","fr",irand(0,5))
`,
@ -262,7 +514,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()
`,
@ -287,7 +539,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")

View File

@ -1,25 +1,38 @@
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)
.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))
@ -31,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)
@ -81,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')
@ -162,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)
@ -172,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))
@ -194,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()`,
];

View File

@ -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());

View File

@ -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"