First version for midi clock and inputs

This commit is contained in:
2023-10-06 23:31:16 +03:00
12 changed files with 875 additions and 616 deletions

View File

@ -7,6 +7,7 @@ import { SoundEvent } from "./classes/SoundEvent";
import { MidiEvent } from "./classes/MidiEvent";
import { LRUCache } from "lru-cache";
import { InputOptions, Player } from "./classes/ZPlayer";
import { template_universes } from "./AppSettings";
import {
samples,
initAudioOnFirstClick,
@ -266,7 +267,7 @@ export class UserAPI {
};
s = this.script;
clear_script = (script: number): void => {
delete_script = (script: number): void => {
/**
* Clears a local script
*
@ -278,7 +279,7 @@ export class UserAPI {
evaluations: 0,
};
};
cs = this.clear_script;
cs = this.delete_script;
copy_script = (from: number, to: number): void => {
/**
@ -293,6 +294,42 @@ export class UserAPI {
};
cps = this.copy_script;
copy_universe = (from: string, to: string): void => {
this.app.universes[to] = {
...this.app.universes[from],
};
};
delete_universe = (universe: string): void => {
if (this.app.selected_universe === universe) {
this.app.selected_universe = "Default";
}
delete this.app.universes[universe];
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings
);
this.app.updateKnownUniversesView();
};
big_bang = (): void => {
/**
* Clears all universes
* TODO: add documentation. This doesn't work super well.
*/
if (confirm("Are you sure you want to delete all universes?")) {
this.app.universes = {
...template_universes,
};
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings
);
}
this.app.selected_universe = "Default";
this.app.updateKnownUniversesView();
};
// =============================================================
// MIDI related functions
// =============================================================
@ -436,13 +473,8 @@ export class UserAPI {
/**
* @returns A list of currently active MIDI notes
*/
let notes;
if(channel) {
notes = this.MidiConnection.activeNotesFromChannel(channel).map((note) => note.note);
} else {
notes = this.MidiConnection.activeNotes.map((note) => note.note);
}
if(notes.length > 0) return notes;
const notes = this.active_note_events(channel);
if(notes && notes.length > 0) return notes.map((e) => e.note);
else return undefined;
}
@ -473,69 +505,68 @@ export class UserAPI {
this.MidiConnection.stickyNotes = [];
}
public last_event = (): MidiNoteEvent|undefined => {
public buffer = (channel?: number): boolean => {
/**
* Return true if there is last note event
*/
if(channel) return this.MidiConnection.findNoteFromBufferInChannel(channel) !== undefined;
else return this.MidiConnection.noteInputBuffer.length > 0;
}
public buffer_event = (channel?: number): MidiNoteEvent|undefined => {
/**
* @returns Returns latest unlistened note event
*/
return this.MidiConnection.popNoteFromBuffer();
if(channel) return this.MidiConnection.findNoteFromBufferInChannel(channel);
else return this.MidiConnection.noteInputBuffer.shift();
}
public last_note = (): number|undefined => {
public buffer_note = (channel?: number): number|undefined => {
/**
* @returns Returns latest received note
*/
const note = this.MidiConnection.popNoteFromBuffer();
const note = this.buffer_event(channel);
return note ? note.note : undefined;
}
public first_event = (): MidiNoteEvent|undefined => {
public last_note_event = (channel?: number): MidiNoteEvent|undefined => {
/**
* @returns Returns first unlistened note event
* @returns Returns last received note
*/
return this.MidiConnection.shiftNoteFromBuffer();
if(channel) return this.MidiConnection.lastNoteInChannel[channel];
else return this.MidiConnection.lastNote;
}
public first_note = (): number|undefined => {
public last_note = (channel?: number): number|undefined => {
/**
* @returns Returns first received note
* @returns Returns last received note
*/
const note = this.MidiConnection.shiftNoteFromBuffer();
const note = this.last_note_event(channel);
return note ? note.note : undefined;
}
public last_channel_note = (channel: number): MidiNoteEvent|undefined => {
public last_cc = (control: number, channel?: number): number|undefined => {
/**
* @returns Returns first unlistened note event on a specific channel
* @returns Returns last received cc
*/
return this.MidiConnection.findNoteFromBufferInChannel(channel);
if(channel) return this.MidiConnection.lastCCInChannel[channel][control];
else return this.MidiConnection.lastCC[control];
}
public find_channel_note = (channel: number): MidiNoteEvent|undefined => {
public has_cc = (channel?: number): boolean => {
/**
* @returns Returns first unlistened note event on a specific channel
* Return true if there is last cc event
*/
return this.MidiConnection.findNoteFromBufferInChannel(channel);
if(channel) return this.MidiConnection.findCCFromBufferInChannel(channel) !== undefined;
else return this.MidiConnection.ccInputBuffer.length > 0;
}
public first_cc = (): MidiCCEvent|undefined => {
/**
* @returns Returns first unlistened cc event
*/
return this.MidiConnection.popCCFromBuffer();
}
public last_cc = (): MidiCCEvent|undefined => {
public buffer_cc = (channel?: number): MidiCCEvent|undefined => {
/**
* @returns Returns latest unlistened cc event
*/
return this.MidiConnection.shiftCCFromBuffer();
}
public find_channel_cc = (channel: number): MidiCCEvent|undefined => {
/**
* @returns Returns first unlistened cc event on a specific channel
*/
return this.MidiConnection.findCCFromBufferInChannel(channel);
if(channel) return this.MidiConnection.findCCFromBufferInChannel(channel);
else return this.MidiConnection.ccInputBuffer.shift();
}
// =============================================================
@ -1175,8 +1206,9 @@ export class UserAPI {
* @param ratio Optional ratio to influence the true/false output (0-100)
* @returns Whether the function returns true or false based on ratio and time chunk
*/
let realChunk = chunk * 2;
const time_pos = this.app.clock.pulses_since_origin;
const full_chunk = Math.floor(chunk * this.ppqn());
const full_chunk = Math.floor(realChunk * this.ppqn());
// const current_chunk = Math.floor(time_pos / full_chunk);
const threshold = Math.floor((ratio / 100) * full_chunk);
const pos_within_chunk = time_pos % full_chunk;
@ -1184,8 +1216,9 @@ export class UserAPI {
};
public flipbar = (chunk: number = 1): boolean => {
let realFlip = chunk * 2;
const time_pos = this.app.clock.time_position.bar;
const current_chunk = Math.floor(time_pos / chunk);
const current_chunk = Math.floor(time_pos / realFlip);
return current_chunk % 2 === 0;
};

View File

@ -3,6 +3,7 @@ import { introduction } from "./documentation/introduction";
import { samples } from "./documentation/samples";
import { chaining } from "./documentation/chaining";
import { software_interface } from "./documentation/interface";
import { interaction } from "./documentation/interaction";
import { time } from "./documentation/time";
import { midi } from "./documentation/midi";
import { code } from "./documentation/code";
@ -11,6 +12,9 @@ import { sound } from "./documentation/engine";
import { shortcuts } from "./documentation/keyboard";
import { patterns } from "./documentation/patterns";
import { functions } from "./documentation/functions";
import { variables } from "./documentation/variables";
import { probabilities } from "./documentation/probabilities";
import { lfos } from "./documentation/lfos";
import { ziffers } from "./documentation/ziffers";
import { reference } from "./documentation/reference";
import { synths } from "./documentation/synths";
@ -53,6 +57,7 @@ export const documentation_factory = (application: Editor) => {
return {
introduction: introduction(application),
interface: software_interface(application),
interaction: interaction(application),
code: code(application),
time: time(application),
sound: sound(application),
@ -62,6 +67,9 @@ export const documentation_factory = (application: Editor) => {
patterns: patterns(application),
ziffers: ziffers(application),
midi: midi(application),
lfos: lfos(application),
variables: variables(application),
probabilities: probabilities(application),
functions: functions(application),
reference: reference(),
shortcuts: shortcuts(),

View File

@ -42,6 +42,10 @@ export class MidiConnection {
public ccInputBuffer: MidiCCEvent[] = [];
public activeNotes: MidiNoteEvent[] = [];
public stickyNotes: MidiNoteEvent[] = [];
public lastNote: MidiNoteEvent|undefined = undefined;
public lastCC: { [control: number]: number } = {};
public lastNoteInChannel: { [channel: number]: MidiNoteEvent } = {};
public lastCCInChannel: { [channel: number]: { [control: number]: number } } = {};
/* MIDI clock stuff */
private midiClockInputIndex: number|undefined = undefined;
@ -292,8 +296,9 @@ export class MidiConnection {
const channel = message.data[0] - 0x90 + 1;
const note = message.data[1];
const velocity = message.data[2];
this.api.variable(`channel_${channel}_note`, note);
this.api.variable(`channel_${channel}_velocity`, velocity);
this.lastNote = {note, velocity, channel, timestamp: event.timeStamp};
this.lastNoteInChannel[channel] = {note, velocity, channel, timestamp: event.timeStamp};
if(this.settings.midi_channels_scripts) this.api.script(channel);
@ -318,8 +323,10 @@ export class MidiConnection {
const channel = message.data[0] - 0xB0 + 1;
const control = message.data[1];
const value = message.data[2];
this.api.variable(`channel_${channel}_control`, control);
this.api.variable(`channel_${channel}_value`, value);
this.lastCC[control] = value;
this.lastCCInChannel[channel][control] = value;
//console.log(`CC: ${control} VALUE: ${value} CHANNEL: ${channel}`);
@ -381,30 +388,6 @@ export class MidiConnection {
}
}
public shiftNoteFromBuffer(): MidiNoteEvent|undefined {
const event = this.noteInputBuffer.shift();
if(event) return event;
else return undefined;
}
public popNoteFromBuffer(): MidiNoteEvent|undefined {
const event = this.noteInputBuffer.pop();
if(event) return event;
else return undefined;
}
public popCCFromBuffer(): MidiCCEvent|undefined {
const event = this.ccInputBuffer.pop();
if(event) return event;
else return undefined;
}
public shiftCCFromBuffer(): MidiCCEvent|undefined {
const event = this.ccInputBuffer.shift();
if(event) return event;
else return undefined;
}
public findNoteFromBufferInChannel(channel: number|undefined) {
const index = this.noteInputBuffer.findIndex((e) => e.channel===channel);
if(index>=0) {

View File

@ -6,75 +6,6 @@ export const functions = (application: Editor): string => {
return `
# Functions
## Global Shared Variables
By default, each script is independant from each other. Scripts live in their own bubble and you cannot get or set variables affecting a script from any other script. **However**, everybody knows that global variables are cool and should be used everywhere. This is an incredibely powerful tool to use for radically altering a composition in a few lines of code.
- <ic>variable(a: number | string, b?: any)</ic>: if only one argument is provided, the value of the variable will be returned through its name, denoted by the first argument. If a second argument is used, it will be saved as a global variable under the name of the first argument.
- <ic>delete_variable(name: string)</ic>: deletes a global variable from storage.
- <ic>clear_variables()</ic>: clear **ALL** variables. **This is a destructive operation**!
**Note:** since this example is running in the documentation, we cannot take advantage of the multiple scripts paradigm. Try to send a variable from the global file to the local file n°6.
${makeExample(
"Setting a global variable",
`
v('my_cool_variable', 2)
`,
true
)}
${makeExample(
"Getting that variable back and printing!",
`
// Note that we just use one argument
log(v('my_cool_variable'))
`,
false
)}
## Counter and iterators
You will often need to use iterators and/or counters to index over data structures (getting a note from a list of notes, etc...). There are functions ready to be used for this. Each script also comes with its own iterator that you can access using the <ic>i</ic> variable. **Note:** the script iteration count is **not** resetted between sessions. It will continue to increase the more you play, even if you just picked up an old project.
- <ic>counter(name: number | string, limit?: number, step?: number)</ic>: reads the value of the counter <ic>name</ic>. You can also call this function using the dollar symbol: <ic>$</ic>.
- <ic>limit?</ic>: counter upper limit before wrapping up.
- <ic>step?</ic>: incrementor. If step is <ic>2</ic>, the iterator will go: <ic>0, 2, 4, 6</ic>, etc...
- <ic>drunk(n?: number)</ic>: returns the value of the internal drunk walk counter. This iterator will sometimes go up, sometimes go down. It comes with companion functions that you can use to finetune its behavior.
- <ic>drunk_max(max: number)</ic>: sets the maximum value.
- <ic>drunk_min(min: number)</ic>: sets the minimum value.
- <ic>drunk_wrap(wrap: boolean)</ic>: whether to wrap the drunk walk to 0 once the upper limit is reached or not.
**Note:** Counters also come with a secret syntax. They can be called with the **$** symbol!
${makeExample(
"Iterating over a list of samples using a counter",
`
rhythm(.25, 6, 8) :: sound('dr').n($(1)).end(.25).out()
`,
true
)}
${makeExample(
"Using a more complex counter",
`
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n($(1, 20, 5)).end(.25).out()
`,
false
)}
${makeExample(
"Calling the drunk mechanism",
`
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n(drunk()).end(.25).out()
`,
false
)}
## Scripts
You can control scripts programatically. This is the core concept of Topos after all!
@ -84,199 +15,20 @@ You can control scripts programatically. This is the core concept of Topos after
- <ic>copy_script(from: number, to: number)</ic>: copies a local script denoted by its number to another local script. **This is a destructive operation!**
${makeExample(
"Calling a script! The most important feature!",
`
"Calling a script! The most important feature!",
`
beat(1) :: script(1)
`,
true
)}
true
)}
${makeExample(
"Calling mutliple scripts at the same time.",
`
"Calling mutliple scripts at the same time.",
`
beat(1) :: script(1, 3, 5)
`,
false
)}
## Mouse
You can get the current position of the mouse on the screen by using the following functions:
- <ic>mouseX()</ic>: the horizontal position of the mouse on the screen (as a floating point number).
- <ic>mouseY()</ic>: the vertical position of the mouse on the screen (as a floating point number).
${makeExample(
"FM Synthesizer controlled using the mouse",
`
beat(.25) :: sound('sine')
.fmi(mouseX() / 100)
.fmh(mouseY() / 100)
.vel(0.2)
.room(0.9).out()
`,
true
)}
Current mouse position can also be used to generate notes:
- <ic>noteX()</ic>: returns a MIDI note number (0-127) based on the horizontal position of the mouse on the screen.
- <ic>noteY()</ic>: returns a MIDI note number (0-127) based on the vertical position of the mouse on the screen.
${makeExample(
"The same synthesizer, with note control!",
`
beat(.25) :: sound('sine')
.fmi(mouseX() / 100)
.note(noteX())
.fmh(mouseY() / 100)
.vel(0.2)
.room(0.9).out()
`,
true
)}
## Low Frequency Oscillators
Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio workstation or synthesizer. Topos implements some basic waveforms you can play with to automatically modulate your paremeters.
- <ic>sine(freq: number = 1, offset: number= 0): number</ic>: returns a sinusoïdal oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>usine(freq: number = 1, offset: number= 0): number</ic>: returns a sinusoïdal oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a sine LFO",
`beat(.25) && snd('cp').speed(1 + usine(0.25) * 2).out()`,
true
)};
- <ic>triangle(freq: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>utriangle(freq: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a triangle LFO",
`beat(.25) && snd('cp').speed(1 + utriangle(0.25) * 2).out()`,
true
)}
- <ic>saw(freq: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>usaw(freq: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a saw LFO",
`beat(.25) && snd('cp').speed(1 + usaw(0.25) * 2).out()`,
true
)}
- <ic>square(freq: number = 1, offset: number= 0, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>-1</ic> and <ic>1</ic>. You can also control the duty cycle using the <ic>duty</ic> parameter.
- <ic>usquare(freq: number = 1, offset: number= 0, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_. You can also control the duty cycle using the <ic>duty</ic> parameter.
${makeExample(
"Modulating the speed of a sample player using a square LFO",
`beat(.25) && snd('cp').speed(1 + usquare(0.25, 0, 0.25) * 2).out()`,
true
)};
- <ic>noise()</ic>: returns a random value between -1 and 1.
${makeExample(
"Modulating the speed of a sample player using noise",
`beat(.25) && snd('cp').speed(1 + noise() * 2).out()`,
true
)};
## Probabilities
There are some simple functions to play with probabilities.
- <ic>rand(min: number, max:number)</ic>: returns a random number between <ic>min</ic> and <ic>max</ic>. Shorthand _r()_.
- <ic>irand(min: number, max:number)</ic>: returns a random integer between <ic>min</ic> and <ic>max</ic>. Shorthands _ir()_ or _rI()_.
${makeExample(
"Bleep bloop, what were you expecting?",
`
rhythm(0.125, 10, 16) :: sound('sid').n(4).note(50 + irand(50, 62) % 8).out()
`,
true
)}
- <ic>prob(p: number)</ic>: return <ic>true</ic> _p_% of time, <ic>false</ic> in other cases.
- <ic>toss()</ic>: throwing a coin. Head (<ic>true</ic>) or tails (<ic>false</ic>).
${makeExample(
"The Teletype experience!",
`
prob(50) :: script(1);
prob(60) :: script(2);
prob(80) :: script(toss() ? script(3) : script(4))
`,
true
)}
- <ic>seed(val: number|string)</ic>: sets the seed of the random number generator. You can use a number or a string. The same seed will always return the same sequence of random numbers.
## Chance operators
Chance operators returning a boolean value are also available. They are super important because they also exist for another mechanism called **chaining**. Checkout the **Chaining** page to learn how to use them in different contexts!
By default chance operators will be evaluated 48 times within a beat. You can change this value by providing a number of beats as an argument. Default value is 1. Change operators can also be used to randomly apply other operators.
- <ic>odds(n: number, beats?: number)</ic>: returns true for every n (odds) (eg. 1/4 = 0.25) in given number of beats
- <ic>never(beats?: number)</ic>: returns false. Can be handy when switching between different probabilities
- <ic>almostNever(beats?: number)</ic>: returns true 0.1% of the time in given number of beats
- <ic>rarely(beats?: number)</ic>: returns true 1% of the time in given number of beats
- <ic>scarcely(beats?: number)</ic>: returns true 10% of the time in given number of beats
- <ic>sometimes(beats?: number)</ic>: returns true 50% of the time in given number of beats
- <ic>often(beats?: number)</ic>: returns true 75% of the time in given number of beats
- <ic>frequently(beats?: number)</ic>: returns true 90% of the time in given number of beats
- <ic>almostAlways(beats?: number)</ic>: returns true 99% of the time in given number of beats
- <ic>always(beats?: number)</ic>: returns true. Can be handy when switching between different probabilities
Examples:
${makeExample(
"Using chance operators",
`
rarely() :: sound('hh').out(); // Rarely 48 times is still a lot
rarely(4) :: sound('bd').out(); // Rarely in 4 beats is bit less
rarely(8) :: sound('east').out(); // Rarely in 8 beats is even less
`,
true
)}
${makeExample(
"Using chance with other operators",
`
frequently() :: beat(1) :: sound('kick').out();
often() :: beat(0.5) :: sound('hh').out();
sometimes() :: onbeat(1,3) :: sound('snare').out();
`,
true
)}
${makeExample(
"Using chance with chaining",
`
beat(0.5) && sound("bd")
.freq(100)
.sometimes(s=>s.crush(2.5))
.out()
beat(0.5) && sound('arp').freq(100)
.sometimes(n=>n.freq(200).delay(0.5))
.rarely(n=>n.freq(300).delay(2.5))
.almostNever(n=>n.freq(400))
.out()
`,
true
)}
false
)}
## Math functions
@ -290,24 +42,24 @@ ${makeExample(
- <ic>delay(ms: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds.
${makeExample(
"Phased woodblocks",
`
"Phased woodblocks",
`
// Some very low-budget version of phase music
beat(.5) :: delay(usine(.125) * 80, () => sound('east').out())
beat(.5) :: delay(50, () => sound('east').out())
`,
true
)}
true
)}
- <ic>delayr(ms: number, nb: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds, repeated a given number of times.
${makeExample(
"Another woodblock texture",
`
"Another woodblock texture",
`
beat(1) :: delayr(50, 4, () => sound('east').speed([0.5,.25].beat()).out())
flip(2) :: beat(2) :: delayr(150, 4, () => sound('east').speed([0.5,.25].beat() * 4).out())
`,
true
)};
true
)};
`;
};

View File

@ -0,0 +1,108 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
// @ts-ignore
export const interaction = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Interaction
Topos can interact with the physical world or react to events coming from outside the system (_MIDI_, physical control, etc).
## Midi input
Topos can use MIDI input to estimate the bpm from incoming clock messages and to control sounds with incoming note and cc messages. Sending MIDI messages to Topos is like sending messages to far away universe; you can't expect a quick response, but the messages will be received and processed eventually.
Midi input can be enabled in the settings panel. Once you have done that, you can use the following functions to control values. All methods have channel parameter as optional value to receive only notes from a certain channel:
* <ic>active_notes(channel?: number)</ic>: returns array of the active notes / pressed keys as an array of MIDI note numbers (0-127). Returns undefined if no notes are active.
* <ic>sticky_notes(channel?: number)</ic>: returns array of the last pressed keys as an array of MIDI note numbers (0-127). Notes are added and removed from the list with the "Note on"-event. Returns undefined if no keys have been pressed.
* <ic>last_note(channel?: number)</ic>: returns the last note that has been received. Returns undefined if no notes have been received.
* <ic>buffer()</ic>: return true if there are notes in the buffer.
* <ic>buffer_note(channel?: number)</ic>: returns last unread note that has been received. Note is fetched and removed from start of the buffer once this is called. Returns undefined if no notes have been received.
${makeExample(
"Play active notes as chords",
`
beat(1) && active_notes() && sound('sine').chord(active_notes()).out()
`,
true
)}
${makeExample(
"Play continous arpeggio with sticky notes",
`
beat(0.25) && sticky_notes() && sound('arp')
.note(sticky_notes().palindrome().beat(0.25)).out()
`,
true
)}
${makeExample(
"Play last note",
`
beat(0.5) && last_note() :: sound('sawtooth').note(last_note())
.vib([1, 3, 5].beat(1))
.vibmod([1,3,2,4].beat(2)).out()
`,
true
)}
${makeExample(
"Play buffered note",
`
beat(1) && buffer() && sound('sine').note(buffer_note()).out()
`,
true
)}
### Run scripts with mini note messages and channels
Midi note messages with channels can also be used to run scripts. This can be enabled in the settings panel by setting "Route channels to scripts".
### Midi clock
Topos can controlled from external hadware or software using Midi clock messages. To enable this feature, you need to connect a MIDI input as Midi Clock in the settings panel. Once you have done that, Topos will listen to incoming clock messages and will use them to estimate the current bpm. Topos will also listen to Start, Stop and Continue messages to start and stop the evaluation. Different MIDI devices can send clock at different resolution, define Clock PPQN in settings to match the resolution of your device.
## Mouse input
You can get the current position of the mouse on the screen by using the following functions:
- <ic>mouseX()</ic>: the horizontal position of the mouse on the screen (as a floating point number).
- <ic>mouseY()</ic>: the vertical position of the mouse on the screen (as a floating point number).
${makeExample(
"FM Synthesizer controlled using the mouse",
`
beat(.25) :: sound('sine')
.fmi(mouseX() / 100)
.fmh(mouseY() / 100)
.vel(0.2)
.room(0.9).out()
`,
true
)}
Current mouse position can also be used to generate notes:
- <ic>noteX()</ic>: returns a MIDI note number (0-127) based on the horizontal position of the mouse on the screen.
- <ic>noteY()</ic>: returns a MIDI note number (0-127) based on the vertical position of the mouse on the screen.
${makeExample(
"The same synthesizer, with note control!",
`
beat(.25) :: sound('sine')
.fmi(mouseX() / 100)
.note(noteX())
.fmh(mouseY() / 100)
.vel(0.2)
.room(0.9).out()
`,
true
)}
`
}

View File

@ -51,6 +51,11 @@ flip(4) :: beat([.5, .25].beat(16)) :: script([5,6,7,8].loop($(2)))
)}
There are some useful functions to help you manage your scripts:
- <ic>copy_script(from: number, to: number)</ic>: copy the content of a script to another.
- <ic>delete_script(index: number)</ic>: clear the content of a script. Warning: this is irreversible!
## Universes
A set of files is called a _universe_. Topos can store several universes and switch immediately from one to another. You can switch between universes by pressing ${key_shortcut(
@ -60,6 +65,11 @@ A set of files is called a _universe_. Topos can store several universes and swi
Switching between universes will not stop the transport nor reset the clock. You are switching the context but time keeps flowing. This can be useful to prepare immediate transitions between songs and parts. Think of universes as an algorithmic set of music. All scripts in a given universe are aware about how many times they have been runned already. You can reset that value programatically.
You can clear the current universe by pressing the flame button on the top right corner of the interface. This will clear all the scripts and the note file. **Note:** there is no shortcut for clearing a universe. We do not want to loose your work by mistake!
There are some useful functions to help you manage your universes:
- <ic>copy_universe(from: string, to: string)</ic>: copy the content of a universe to another. This is useful to create a backup of your work.
- <ic>delete_universe(name: string)</ic>: delete a universe. Warning: this is irreversible!
# Sharing your work

58
src/documentation/lfos.ts Normal file
View File

@ -0,0 +1,58 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const lfos = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Low Frequency Oscillators
Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio workstation or synthesizer. Topos implements some basic waveforms you can play with to automatically modulate your paremeters.
- <ic>sine(freq: number = 1, offset: number= 0): number</ic>: returns a sinusoïdal oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>usine(freq: number = 1, offset: number= 0): number</ic>: returns a sinusoïdal oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a sine LFO",
`beat(.25) && snd('cp').speed(1 + usine(0.25) * 2).out()`,
true
)};
- <ic>triangle(freq: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>utriangle(freq: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a triangle LFO",
`beat(.25) && snd('cp').speed(1 + utriangle(0.25) * 2).out()`,
true
)}
- <ic>saw(freq: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>usaw(freq: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_.
${makeExample(
"Modulating the speed of a sample player using a saw LFO",
`beat(.25) && snd('cp').speed(1 + usaw(0.25) * 2).out()`,
true
)}
- <ic>square(freq: number = 1, offset: number= 0, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>-1</ic> and <ic>1</ic>. You can also control the duty cycle using the <ic>duty</ic> parameter.
- <ic>usquare(freq: number = 1, offset: number= 0, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>0</ic> and <ic>1</ic>. The <ic>u</ic> stands for _unipolar_. You can also control the duty cycle using the <ic>duty</ic> parameter.
${makeExample(
"Modulating the speed of a sample player using a square LFO",
`beat(.25) && snd('cp').speed(1 + usquare(0.25, 0, 0.25) * 2).out()`,
true
)};
- <ic>noise()</ic>: returns a random value between -1 and 1.
${makeExample(
"Modulating the speed of a sample player using noise",
`beat(.25) && snd('cp').speed(1 + noise() * 2).out()`,
true
)};
`
}

View File

@ -0,0 +1,98 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const probabilities = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Probabilities
There are some simple functions to play with probabilities.
- <ic>rand(min: number, max:number)</ic>: returns a random number between <ic>min</ic> and <ic>max</ic>. Shorthand _r()_.
- <ic>irand(min: number, max:number)</ic>: returns a random integer between <ic>min</ic> and <ic>max</ic>. Shorthands _ir()_ or _rI()_.
${makeExample(
"Bleep bloop, what were you expecting?",
`
rhythm(0.125, 10, 16) :: sound('sid').n(4).note(50 + irand(50, 62) % 8).out()
`,
true
)}
- <ic>prob(p: number)</ic>: return <ic>true</ic> _p_% of time, <ic>false</ic> in other cases.
- <ic>toss()</ic>: throwing a coin. Head (<ic>true</ic>) or tails (<ic>false</ic>).
${makeExample(
"The Teletype experience!",
`
prob(50) :: script(1);
prob(60) :: script(2);
prob(80) :: script(toss() ? script(3) : script(4))
`,
true
)}
- <ic>seed(val: number|string)</ic>: sets the seed of the random number generator. You can use a number or a string. The same seed will always return the same sequence of random numbers.
## Chance operators
Chance operators returning a boolean value are also available. They are super important because they also exist for another mechanism called **chaining**. Checkout the **Chaining** page to learn how to use them in different contexts!
By default chance operators will be evaluated 48 times within a beat. You can change this value by providing a number of beats as an argument. Default value is 1. Change operators can also be used to randomly apply other operators.
- <ic>odds(n: number, beats?: number)</ic>: returns true for every n (odds) (eg. 1/4 = 0.25) in given number of beats
- <ic>never(beats?: number)</ic>: returns false. Can be handy when switching between different probabilities
- <ic>almostNever(beats?: number)</ic>: returns true 0.1% of the time in given number of beats
- <ic>rarely(beats?: number)</ic>: returns true 1% of the time in given number of beats
- <ic>scarcely(beats?: number)</ic>: returns true 10% of the time in given number of beats
- <ic>sometimes(beats?: number)</ic>: returns true 50% of the time in given number of beats
- <ic>often(beats?: number)</ic>: returns true 75% of the time in given number of beats
- <ic>frequently(beats?: number)</ic>: returns true 90% of the time in given number of beats
- <ic>almostAlways(beats?: number)</ic>: returns true 99% of the time in given number of beats
- <ic>always(beats?: number)</ic>: returns true. Can be handy when switching between different probabilities
Examples:
${makeExample(
"Using chance operators",
`
rarely() :: sound('hh').out(); // Rarely 48 times is still a lot
rarely(4) :: sound('bd').out(); // Rarely in 4 beats is bit less
rarely(8) :: sound('east').out(); // Rarely in 8 beats is even less
`,
true
)}
${makeExample(
"Using chance with other operators",
`
frequently() :: beat(1) :: sound('kick').out();
often() :: beat(0.5) :: sound('hh').out();
sometimes() :: onbeat(1,3) :: sound('snare').out();
`,
false
)}
${makeExample(
"Using chance with chaining",
`
beat(0.5) && sound("bd")
.freq(100)
.sometimes(s=>s.crush(2.5))
.out()
beat(0.5) && sound('arp').freq(100)
.sometimes(n=>n.freq(200).delay(0.5))
.rarely(n=>n.freq(300).delay(2.5))
.almostNever(n=>n.freq(400))
.out()
`,
false
)}
`
}

View File

@ -23,17 +23,17 @@ Let's study two very simple rhythmic functions, <ic>mod(n: ...number[])</ic> and
- <ic>beat(...n: number[])</ic>: this function will return true every _n_ beats. The value <ic>1</ic> will return <ic>true</ic> at the beginning of each beat. Floating point numbers like <ic>0.5</ic> or <ic>0.25</ic> are also accepted. Multiple values can be passed to <ic>beat</ic> to generate more complex rhythms.
${makeExample(
"Using different mod values",
`
"Using different mod values",
`
// This code is alternating between different mod values
beat([1,1/2,1/4,1/8].beat(2)) :: sound('hat').n(0).out()
`,
true
)}
true
)}
${makeExample(
"Some sort of ringtone",
`
"Some sort of ringtone",
`
// Blip generator :)
let blip = (freq) => {
return sound('wt_piano')
@ -48,61 +48,61 @@ beat(1/3) :: blip(400).pan(r(0,1)).out();
flip(3) :: beat(1/6) :: blip(800).pan(r(0,1)).out();
beat([1,0.75].beat(2)) :: blip([50, 100].beat(2)).pan(r(0,1)).out();
`,
false
)}
false
)}
- <ic>pulse(...n: number[])</ic>: faster version of the <ic>beat</ic> function. Instead of returning true for every beat, this function is returning true every _n_ clock ticks! It can be used to generate very unexpected rhythms.
${makeExample(
"Intriguing rhythms",
`
"Intriguing rhythms",
`
pulse([24,48].beat(2)) :: snd('hand')
.cut(1).room(0.9).size(0.9)
.n([2,4].beat(2)).out()
pulse([48/2, 48/3].beat(4)) :: snd('hand')
.n([2,4].add(5).beat(1)).out()
`,
true
)}
true
)}
${makeExample(
"pulse is the OG rhythmic function in Topos",
`
"pulse is the OG rhythmic function in Topos",
`
pulse([48, 24, 16].beat(4)) :: sound('linnhats').out()
beat(1)::snd(['bd', '808oh'].beat(1)).out()
`,
false
)};
false
)};
- <ic>onbeat(...n: number[])</ic>: The <ic>onbeat</ic> function allows you to lock on to a specific beat from the clock to execute code. It can accept multiple arguments. It's usage is very straightforward and not hard to understand. You can pass either integers or floating point numbers. By default, topos is using a <ic>4/4</ic> bar meaning that you can target any of these beats (or in-between) with this function.
${makeExample(
"Some simple yet detailed rhythms",
`
"Some simple yet detailed rhythms",
`
onbeat(1,2,3,4)::snd('kick').out() // Bassdrum on each beat
onbeat(2,4)::snd('snare').n([8,4].beat(4)).out() // Snare on acccentuated beats
onbeat(1.5,2.5,3.5, 3.75)::snd('hat').gain(r(0.9,1.1)).out() // Cool high-hats
`,
true
)}
true
)}
${makeExample(
"Let's do something more complex",
`
"Let's do something more complex",
`
onbeat(0.5, 2, 3, 3.75)::snd('kick').n(2).out()
onbeat(2, [1.5, 3, 4].pick(), 4)::snd('snare').n(8).out()
beat([.25, 1/8].beat(1.5))::snd('hat').n(2)
.gain(rand(0.4, 0.7)).end(0.05)
.pan(usine()).out()
`,
false
)}
false
)}
- <ic>oncount(beats: number[], meter: number)</ic>: This function is similar to <ic>onbeat</ic> but it allows you to specify a custom number of beats as the last argument.
${makeExample(
"Using oncount to create more variation in the rhythm",
`
"Using oncount to create more variation in the rhythm",
`
z1('1/16 (0 2 3 4)+(0 2 4 6)').scale('pentatonic').sound('sawtooth')
.cutoff([400,500,1000,2000].beat(1))
.lpadsr(2, 0, .2, 0, 0)
@ -110,20 +110,20 @@ z1('1/16 (0 2 3 4)+(0 2 4 6)').scale('pentatonic').sound('sawtooth')
onbeat(1,1.5,2,3,4) :: sound('bd').gain(2.0).out()
oncount([1,3,5.5,7,7.5,8],8) :: sound('hh').gain(irand(1.0,4.0)).out()
`,
true
)}
true
)}
${makeExample(
"Using oncount to create rhythms with a custom meter",
`
"Using oncount to create rhythms with a custom meter",
`
bpm(200)
oncount([1, 5, 9, 13],16) :: sound('808bd').n(4).shape(0.5).gain(1.0).out()
oncount([5, 6, 13],16) :: sound('shaker').room(0.25).gain(0.9).out()
oncount([2, 3, 3.5, 6, 7, 10, 15],16) :: sound('hh').n(8).gain(0.8).out()
oncount([1, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16],16) :: sound('hh').out()
`,
true
)}
true
)}
## Rhythm generators
@ -132,8 +132,8 @@ We included a bunch of popular rhythm generators in Topos such as the euclidian
- <ic>euclid(iterator: number, pulses: number, length: number, rotate: number): boolean</ic>: generates <ic>true</ic> or <ic>false</ic> values from an euclidian rhythm sequence. This algorithm is very popular in the electronic music making world.
${makeExample(
"Classic euclidian club music patterns",
`
"Classic euclidian club music patterns",
`
beat(.5) && euclid($(1), 4, 8) && snd('kick').n(4).out()
beat(.25) && euclid($(2), 5, 8) && snd('dr').n(21).out()
beat(.25) && euclid($(3), 3, 8) && snd('shaker')
@ -143,24 +143,24 @@ beat(.25) && euclid($(3), 6, 8) && snd('shaker')
.gain(r(0.7, 1)).cutoff(1000 + usine(1/4) * 4000)
.speed(2).n(11).out()
`,
true
)}
true
)}
${makeExample(
"And now something a bit more complex",
`
"And now something a bit more complex",
`
bpm(145); // Setting a faster BPM
beat(.5) && euclid($(1), 5, 8) :: sound('bd').out()
beat(.5) && euclid($(2), [1,0].beat(8), 8)
:: sound('ST03').n(5).room(1).size(1).o(1).out()
beat(.5) && euclid($(6), [6,7].beat(8), 8) :: sound('hh').out()
`,
false
)}
false
)}
${makeExample(
"Adding more rhythmic density",
`
"Adding more rhythmic density",
`
beat(.5) && euclid($(1), 5, 9) && snd('kick').shape(r(0.2,0.5)).out()
beat(.5) && euclid($(2), 2, 3, 1) && snd('dr').end(0.5).n([8,9,13].beat(0.25))
.gain(r(0.5,1)).speed(1).out()
@ -168,52 +168,52 @@ beat(.5) && euclid($(3), 6, 9, 1) && snd('dr').end(0.5).n(2).freq(200).speed(1)
.gain(r(0.5,1)).out()
beat(.25) && euclid($(4), 7, 9, 1) && snd('hh').out()
`,
false
)}
false
)}
Alternatively, you can <ic>oneuclid</ic> or <ic>rhythm</ic> without the _iterators_:
- <ic>oneuclid(pulses: number, length: number, rotate: number): boolean</ic>: generates <ic>true</ic> or <ic>false</ic> values from an euclidian rhythm sequence. This is another version of <ic>euclid</ic> that does not take an iterator.
${makeExample(
"Using oneuclid to create a rhythm without iterators",
`
"Using oneuclid to create a rhythm without iterators",
`
// Change speed using bpm
bpm(250)
oneuclid(5, 9) :: snd('kick').out()
oneuclid(7,16) :: snd('east').end(0.5).n(irand(3,5)).out()
`,
false
)}
false
)}
- <ic>rhythm(divisor: number, pulses: number, length: number, rotate: number): boolean</ic>: generates <ic>true</ic> or <ic>false</ic> values from an euclidian rhythm sequence. This is another version of <ic>euclid</ic> that does not take an iterator.
${makeExample(
"rhythm is a beginner friendly rhythmic function!",
`
"rhythm is a beginner friendly rhythmic function!",
`
let speed = [1, 0.5].beat(8); bpm(140);
rhythm(speed, 5, 12) :: snd('linnhats').n(2).pan(noise()).out()
rhythm(speed, 2, 12) :: snd('east').out()
rhythm(speed, 3, 12) :: snd('linnhats').n(4).pan(noise()).out()
rhythm(speed, 7, 12) :: snd('east').n(9).out()
`,
true
)}
true
)}
- <ic>bin(iterator: number, n: number): boolean</ic>: a binary rhythm generator. It transforms the given number into its binary representation (_e.g_ <ic>34</ic> becomes <ic>100010</ic>). It then returns a boolean value based on the iterator in order to generate a rhythm.
- <ic>binrhythm(divisor: number, n: number): boolean: boolean</ic>: iterator-less version of the binary rhythm generator.
${makeExample(
"Change the integers for a surprise rhythm!",
`
"Change the integers for a surprise rhythm!",
`
bpm(135);
beat(.5) && bin($(1), 12) && snd('kick').n([4,9].beat(1.5)).out()
beat(.5) && bin($(2), 34) && snd('snare').n([3,5].beat(1)).out()
`,
true
)}
true
)}
${makeExample(
"binrhythm for fast cool binary rhythms!",
`
"binrhythm for fast cool binary rhythms!",
`
let a = 0;
a = beat(4) ? irand(1,20) : a;
binrhythm(.5, 6) && snd(['kick', 'snare'].beat(0.5)).n(11).out()
@ -222,34 +222,34 @@ binrhythm([.5, .25].beat(1), 30) && snd('wt_granular').n(a)
.adsr(0, r(.1, .4), 0, 0).freq([50, 60, 72].beat(4))
.room(1).size(1).out()
`,
true
)}
true
)}
${makeExample(
"Submarine jungle music",
`
"Submarine jungle music",
`
bpm(145);
beat(.5) && bin($(1), 911) && snd('ST69').n([2,3,4].beat())
.delay(0.125).delayt(0.25).end(0.25).speed(1/3)
.room(1).size(1).out()
beat(.5) && sound('amencutup').n(irand(2,7)).shape(0.3).out()
`,
false
)}
false
)}
If you don't find it spicy enough, you can add some more probabilities to your rhythms by taking advantage of the probability functions. See the functions documentation page to learn more about them.
${makeExample(
"Probablistic drums in one line!",
`
"Probablistic drums in one line!",
`
prob(60)::beat(.5) && euclid($(1), 5, 8) && snd('kick').out()
prob(60)::beat(.5) && euclid($(2), 3, 8) && snd('mash')
.n([1,2,3].beat(1))
.pan(usine(1/4)).out()
prob(80)::beat(.5) && sound(['hh', 'hat'].pick()).out()
`,
true
)}
true
)}
## Time Warping
@ -259,8 +259,8 @@ Time generally flows from the past to the future. However, it's even cooler when
${makeExample(
"Time is now super elastic!",
`
"Time is now super elastic!",
`
// Obscure Shenanigans - Bubobubobubo
beat([1/4,1/8,1/16].beat(8)):: sound('sine')
.freq([100,50].beat(16) + 50 * ($(1)%10))
@ -273,14 +273,14 @@ flip(3) :: beat([.25,.5].beat(.5)) :: sound('dr')
// Jumping back and forth in time
beat(.25) :: warp([12, 48, 24, 1, 120, 30].pick())
`,
true
)}
true
)}
- <ic>beat_warp(beat: number)</ic>: this function jumps to the _n_ beat of the clock. The first beat is <ic>1</ic>.
${makeExample(
"Jumping back and forth with beats",
`
"Jumping back and forth with beats",
`
// Resonance bliss - Bubobubobubo
beat(.25)::snd('arpy')
.note(30 + [0,3,7,10].beat())
@ -295,8 +295,8 @@ beat(.5) :: snd('arpy').note(
// Comment me to stop warping!
beat(1) :: beat_warp([2,4,5,10,11].pick())
`,
true
)}
true
)}
## Larger time divisions
@ -306,44 +306,44 @@ Now you know how to play some basic rhythmic music but you are a bit stuck in a
- <ic>ratio: number = 50</ic>: this argument is ratio expressed in %. It determines how much of the period should be true or false. A ratio of <ic>75</ic> means that 75% of the period will be true. A ratio of <ic>25</ic> means that 25% of the period will be true.
${makeExample(
"Two beats of silence, two beats of playing",
`
"Two beats of silence, two beats of playing",
`
flip(4) :: beat(1) :: snd('kick').out()
`,
true
)}
true
)}
${makeExample(
"Clapping on the edge",
`
"Clapping on the edge",
`
flip(2.5, 10) :: beat(.25) :: snd('cp').out()
flip(2.5, 75) :: beat(.25) :: snd('click')
.speed(2).end(0.2).out()
flip(2.5) :: beat(.5) :: snd('bd').out()
beat(.25) :: sound('hat').end(0.1).cutoff(1200).pan(usine(1/4)).out()
`,
false
)}
false
)}
${makeExample(
"Good old true and false",
`
"Good old true and false",
`
if (flip(4, 75)) {
beat(1) :: snd('kick').out()
} else {
beat(.5) :: snd('snare').out()
}
`,
true
)}
true
)}
<ic>flip</ic> is extremely powerful and is used internally for a lot of other Topos functions. You can also use it to think about **longer durations** spanning over multiple bars. Here is a silly composition that is using <ic>flip</ic> to generate a 4 bars long pattern.
${makeExample(
"Clunky algorithmic rap music",
`
"Clunky algorithmic rap music",
`
// Rap God VS Lil Wild -- Adel Faure
if (flip(16)) {
if (flip(8)) {
// Playing this part for two bars
beat(1.5)::snd('kick').out()
beat(2)::snd('snare').out()
@ -359,24 +359,24 @@ if (flip(16)) {
beat(.5)::snd('diphone').end(0.5).n([1,2,3,4].pick()).out()
}
`,
true
)}
true
)}
You can use it everywhere to spice things up, including as a method parameter picker:
${makeExample(
"flip is great for parameter variation",
`
beat(.5)::snd(flip(4) ? 'kick' : 'hat').out()
"flip is great for parameter variation",
`
beat(.5)::snd(flip(2) ? 'kick' : 'hat').out()
`,
true
)}
true
)}
- <ic>flipbar(n: number = 1)</ic>: this method works just like <ic>flip</ic> but counts in bars instead of beats. It allows you to think about even larger time cycles. You can also pair it with regular <ic>flip</ic> for writing complex and long-spanning algorithmic beats.
${makeExample(
"Thinking music over bars",
`
"Thinking music over bars",
`
let roomy = (n) => n.room(1).size(1).cutoff(500 + usaw(1/8) * 5000);
function a() {
beat(1) && roomy(sound('kick')).out()
@ -388,24 +388,24 @@ function b() {
flipbar(2) && a()
flipbar(3) && b()
`,
true
)}
true
)}
${makeExample(
"Alternating over four bars",
`
"Alternating over four bars",
`
flipbar(2)
? beat(.5) && snd(['kick', 'hh'].beat(1)).out()
: beat(.5) && snd(['east', 'east:2'].beat(1)).out()
`,
false
)};
false
)};
- <ic>onbar(bars: number | number[], n: number)</ic>: The second argument, <ic>n</ic>, is used to divide the time in a period of <ic>n</ic> consecutive bars. The first argument should be a bar number or a list of bar numbers to play on. For example, <ic>onbar([1, 4], 5)</ic> will return <ic>true</ic> on bar <ic>1</ic> and <ic>4</ic> but return <ic>false</ic> the rest of the time. You can easily divide time that way.
${makeExample(
"Using onbar for filler drums",
`
"Using onbar for filler drums",
`
bpm(150);
// Only play on the third and fourth bar of the cycle.
onbar([3,4], 4)::beat(.25)::snd('hh').out();
@ -421,8 +421,8 @@ if (onbar([1,2], 4)) {
rhythm(.5, 1, 7) :: snd('jvbass').n(2).out();
rhythm(.5, 2, 7) :: snd('snare').n(3).out();
}`,
true
)}
true
)}
## What are pulses?
@ -451,8 +451,8 @@ Every script can access the current time by using the following functions:
These values are **extremely useful** to craft more complex syntax or to write musical scores. However, Topos is also offering more high-level sequencing functions to make it easier to play music. You can use the time functions as conditionals. The following example will play a pattern A for 2 bars and a pattern B for 2 bars:
${makeExample(
"Manual mode: using time primitives!",
`
"Manual mode: using time primitives!",
`
// Manual time condition
if((cbar() % 4) > 1) {
beat(2) && sound('kick').out()
@ -470,7 +470,7 @@ if((cbar() % 4) > 1) {
// This is always playing no matter what happens
beat([.5, .5, 1, .25].beat(0.5)) :: sound('shaker').out()
`,
true
)}
true
)}
`;
};

View File

@ -0,0 +1,80 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const variables = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Variables
By default, each script is independant from each other. Scripts live in their own bubble and you cannot get or set variables affecting a script from any other script. **However**, everybody knows that global variables are cool and should be used everywhere. This is an incredibely powerful tool to use for radically altering a composition in a few lines of code.
- <ic>variable(a: number | string, b?: any)</ic>: if only one argument is provided, the value of the variable will be returned through its name, denoted by the first argument. If a second argument is used, it will be saved as a global variable under the name of the first argument.
- <ic>delete_variable(name: string)</ic>: deletes a global variable from storage.
- <ic>clear_variables()</ic>: clear **ALL** variables. **This is a destructive operation**!
**Note:** since this example is running in the documentation, we cannot take advantage of the multiple scripts paradigm. Try to send a variable from the global file to the local file n°6.
${makeExample(
"Setting a global variable",
`
v('my_cool_variable', 2)
`,
true
)}
${makeExample(
"Getting that variable back and printing!",
`
// Note that we just use one argument
log(v('my_cool_variable'))
`,
false
)}
## Counter and iterators
You will often need to use iterators and/or counters to index over data structures (getting a note from a list of notes, etc...). There are functions ready to be used for this. Each script also comes with its own iterator that you can access using the <ic>i</ic> variable. **Note:** the script iteration count is **not** resetted between sessions. It will continue to increase the more you play, even if you just picked up an old project.
- <ic>counter(name: number | string, limit?: number, step?: number)</ic>: reads the value of the counter <ic>name</ic>. You can also call this function using the dollar symbol: <ic>$</ic>.
- <ic>limit?</ic>: counter upper limit before wrapping up.
- <ic>step?</ic>: incrementor. If step is <ic>2</ic>, the iterator will go: <ic>0, 2, 4, 6</ic>, etc...
- <ic>drunk(n?: number)</ic>: returns the value of the internal drunk walk counter. This iterator will sometimes go up, sometimes go down. It comes with companion functions that you can use to finetune its behavior.
- <ic>drunk_max(max: number)</ic>: sets the maximum value.
- <ic>drunk_min(min: number)</ic>: sets the minimum value.
- <ic>drunk_wrap(wrap: boolean)</ic>: whether to wrap the drunk walk to 0 once the upper limit is reached or not.
**Note:** Counters also come with a secret syntax. They can be called with the **$** symbol!
${makeExample(
"Iterating over a list of samples using a counter",
`
rhythm(.25, 6, 8) :: sound('dr').n($(1)).end(.25).out()
`,
true
)}
${makeExample(
"Using a more complex counter",
`
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n($(1, 20, 5)).end(.25).out()
`,
false
)}
${makeExample(
"Calling the drunk mechanism",
`
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n(drunk()).end(.25).out()
`,
false
)}
`
}

View File

@ -33,19 +33,24 @@ showdown.setFlavor("github");
import showdownHighlight from "showdown-highlight";
import { makeStringExtensions } from "./StringExtensions";
// Broadcast that you're opening a page.
localStorage.openpages = Date.now();
window.addEventListener('storage', function(e) {
if (e.key == "openpages") {
// Listen if anybody else is opening the same page!
localStorage.page_available = Date.now();
}
if (e.key == "page_available") {
document.getElementById("all")!.classList.add("invisible")
alert("Topos is already opened in another tab. Close this tab now to prevent data loss.");
}
}, false);
window.addEventListener(
"storage",
function(e) {
if (e.key == "openpages") {
// Listen if anybody else is opening the same page!
localStorage.page_available = Date.now();
}
if (e.key == "page_available") {
document.getElementById("all")!.classList.add("invisible");
alert(
"Topos is already opened in another tab. Close this tab now to prevent data loss."
);
}
},
false
);
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",
@ -135,6 +140,18 @@ export class Editor {
"load-universe-button"
) as HTMLButtonElement;
download_universe_button: HTMLButtonElement = document.getElementById(
"download-universes"
) as HTMLButtonElement;
upload_universe_button: HTMLButtonElement = document.getElementById(
"upload-universes"
) as HTMLButtonElement;
destroy_universes_button: HTMLButtonElement = document.getElementById(
"destroy-universes"
) as HTMLButtonElement;
documentation_button: HTMLButtonElement = document.getElementById(
"doc-button-1"
) as HTMLButtonElement;
@ -186,12 +203,19 @@ export class Editor {
) as HTMLDivElement;
// Font Size Slider
font_size_slider: HTMLInputElement = document.getElementById(
"font-size-slider"
font_size_input: HTMLInputElement = document.getElementById(
"font-size-input"
) as HTMLInputElement;
// Font Family Selector
font_family_selector: HTMLSelectElement = document.getElementById(
"font-family"
) as HTMLSelectElement;
// Vim mode checkbox
vim_mode_checkbox: HTMLInputElement = document.getElementById(
"vim-mode"
) as HTMLInputElement;
font_size_witness: HTMLSpanElement = document.getElementById(
"font-size-witness"
) as HTMLSpanElement;
// Line Numbers checkbox
line_numbers_checkbox: HTMLInputElement = document.getElementById(
@ -257,7 +281,6 @@ export class Editor {
public hydra: any = this.hydra_backend.synth;
constructor() {
// ================================================================================
// Loading the settings
// ================================================================================
@ -279,22 +302,23 @@ export class Editor {
this.universes = {
...this.settings.universes,
...template_universes
...template_universes,
};
if (this.settings.load_demo_songs) {
let random_example = examples[Math.floor(Math.random() * examples.length)];
this.selected_universe = "Welcome"
let random_example =
examples[Math.floor(Math.random() * examples.length)];
this.selected_universe = "Welcome";
this.universes[this.selected_universe].global.committed = random_example;
this.universes[this.selected_universe].global.candidate = random_example;
} else {
this.selected_universe = this.settings.selected_universe;
if (this.universes[this.selected_universe] === undefined)
this.universes[this.selected_universe] = structuredClone(template_universe)
this.universes[this.selected_universe] =
structuredClone(template_universe);
}
this.universe_viewer.innerHTML = `Topos: ${this.selected_universe}`;
// ================================================================================
// Audio context and clock
// ================================================================================
@ -549,7 +573,8 @@ export class Editor {
button.addEventListener("click", () => {
this.setButtonHighlighting("clear", true);
if (confirm("Do you want to reset the current universe?")) {
this.universes[this.selected_universe] = structuredClone(template_universe);
this.universes[this.selected_universe] =
structuredClone(template_universe);
this.updateEditorView();
}
});
@ -559,6 +584,72 @@ export class Editor {
this.showDocumentation();
});
this.destroy_universes_button.addEventListener("click", () => {
if (confirm("Do you want to destroy all universes?")) {
this.universes = {
...template_universes,
};
this.updateKnownUniversesView();
}
});
this.upload_universe_button.addEventListener("click", () => {
const fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.accept = ".json";
fileInput.addEventListener("change", (event) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) {
const reader = new FileReader();
reader.readAsText(file, "UTF-8");
reader.onload = (evt) => {
const data = JSON.parse(evt.target!.result as string);
for (const [key, value] of Object.entries(data)) {
this.universes[key] = value as Universe;
}
};
reader.onerror = (evt) => {
console.error("An error occurred reading the file:", evt);
};
}
});
document.body.appendChild(fileInput);
fileInput.click();
document.body.removeChild(fileInput);
});
this.download_universe_button.addEventListener("click", () => {
// Trigger save of the universe before downloading
this.settings.saveApplicationToLocalStorage(
this.universes,
this.settings
);
// Generate a file name based on timestamp
let fileName = `topos-universes-${Date.now()}.json`;
// Create Blob and Object URL
const blob = new Blob([JSON.stringify(this.settings.universes)], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
// Create a temporary anchor and trigger download
const a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Revoke the Object URL to free resources
URL.revokeObjectURL(url);
});
this.load_universe_button.addEventListener("click", () => {
let query = this.buffer_search.value;
if (query.length > 2 && query.length < 20 && !query.includes(" ")) {
@ -597,13 +688,48 @@ export class Editor {
this.changeModeFromInterface("notes")
);
this.font_family_selector.addEventListener("change", () => {
let new_font = this.font_family_selector.value;
this.settings.font = new_font;
let new_font_size = EditorView.theme({
"&": { fontSize: this.settings.font_size + "px" },
"&content": {
fontFamily: new_font,
fontSize: this.settings.font_size + "px",
},
".cm-gutters": { fontSize: this.settings.font_size + "px" },
});
this.view.dispatch({
effects: this.fontSize.reconfigure(new_font_size),
});
});
this.font_size_input.addEventListener("input", () => {
let new_value: string | number = this.font_size_input.value;
this.settings.font_size = Math.max(8, Math.min(48, parseInt(new_value)));
let new_font_size = EditorView.theme({
"&": { fontSize: new_value + "px" },
"&content": {
fontFamily: this.settings.font,
fontSize: new_value + "px",
},
".cm-gutters": { fontSize: new_value + "px" },
});
this.view.dispatch({
effects: this.fontSize.reconfigure(new_font_size),
});
this.settings.font_size = parseInt(new_value);
});
this.settings_button.addEventListener("click", () => {
this.font_size_slider.value = this.settings.font_size.toString();
this.font_size_witness.innerHTML = `Font Size: ${this.settings.font_size}px`;
this.font_size_witness?.setAttribute(
"style",
`font-size: ${this.settings.font_size}px;`
);
// Populate the font family selector
this.font_family_selector.value = this.settings.font;
if (this.settings.font_size === null) {
this.settings.font_size = 12;
}
this.font_size_input.value = this.settings.font_size.toString();
// Get the right value to update graphical widgets
this.line_numbers_checkbox.checked = this.settings.line_numbers;
@ -613,22 +739,7 @@ export class Editor {
this.midi_channels_scripts.checked = this.settings.midi_channels_scripts;
this.midi_clock_ppqn.value = this.settings.midi_clock_ppqn.toString();
this.load_demo_songs.checked = this.settings.load_demo_songs;
if (this.settings.vimMode) {
let vim = document.getElementById("vim-mode-radio") as HTMLInputElement;
let normal = document.getElementById(
"normal-mode-radio"
) as HTMLInputElement;
vim.checked = true;
normal.checked = false;
} else {
let vim = document.getElementById("vim-mode-radio") as HTMLInputElement;
let normal = document.getElementById(
"normal-mode-radio"
) as HTMLInputElement;
normal.checked = true;
vim.checked = false;
}
this.vim_mode_checkbox.checked = this.settings.vimMode;
let modal_settings = document.getElementById("modal-settings");
let editor = document.getElementById("editor");
@ -642,29 +753,25 @@ export class Editor {
let editor = document.getElementById("editor");
modal_settings?.classList.add("invisible");
editor?.classList.remove("invisible");
// Update the font size once again
this.view.dispatch({
effects: this.fontSize.reconfigure(
EditorView.theme({
"&": { fontSize: this.settings.font_size + "px" },
"&content": {
fontFamily: this.settings.font,
fontSize: this.settings.font_size + "px",
},
".cm-gutters": { fontSize: this.settings.font_size + "px" },
})
),
});
});
this.close_universes_button.addEventListener("click", () => {
this.openBuffersModal();
});
this.font_size_slider.addEventListener("input", () => {
const new_value = this.font_size_slider.value;
this.settings.font_size = parseInt(new_value);
this.font_size_witness.style.fontSize = `${new_value}px`;
this.font_size_witness.innerHTML = `Font Size: ${new_value}px`;
let new_font_size = EditorView.theme({
"&": { fontSize: new_value + "px" },
"&content": { fontFamily: this.settings.font },
".cm-gutters": { fontSize: new_value + "px" },
});
this.view.dispatch({
effects: this.fontSize.reconfigure(new_font_size),
});
this.settings.font_size = parseInt(new_value);
});
this.share_button.addEventListener("click", async () => {
// trigger a manual save
this.currentFile().candidate = app.view.state.doc.toString();
@ -674,9 +781,12 @@ export class Editor {
await this.share();
});
this.normal_mode_button.addEventListener("click", () => {
this.settings.vimMode = false;
this.view.dispatch({ effects: this.vimModeCompartment.reconfigure([]) });
this.vim_mode_checkbox.addEventListener("change", () => {
let checked = this.vim_mode_checkbox.checked ? true : false;
this.settings.vimMode = checked;
this.view.dispatch({
effects: this.vimModeCompartment.reconfigure(checked ? vim() : []),
});
});
this.line_numbers_checkbox.addEventListener("change", () => {
@ -728,14 +838,6 @@ export class Editor {
this.settings.load_demo_songs = checked;
});
this.vim_mode_button.addEventListener("click", () => {
this.settings.vimMode = true;
this.view.dispatch({
effects: this.vimModeCompartment.reconfigure(vim()),
});
});
this.universe_creator.addEventListener("submit", (event) => {
event.preventDefault();
@ -758,6 +860,7 @@ export class Editor {
[
"introduction",
"interface",
"interaction",
"code",
"time",
"sound",
@ -768,6 +871,9 @@ export class Editor {
"ziffers",
"midi",
"functions",
"lfos",
"probabilities",
"variables",
// "reference",
"shortcuts",
"about",
@ -1220,4 +1326,3 @@ window.addEventListener("beforeunload", () => {
app.clock.stop();
return null;
});