This commit is contained in:
2023-11-11 00:37:30 +02:00
51 changed files with 7239 additions and 842 deletions

View File

@ -42,9 +42,17 @@ jobs:
name: production-files
path: ./dist
- name: Checkout main branch for favicon
uses: actions/checkout@v3
with:
path: "main"
- name: Copy favicon folder
run: cp -r main/favicon ./dist/
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
cname: topos.raphaelforment.fr
cname: topos.live

2
CNAME
View File

@ -1 +1 @@
topos.raphaelforment.fr
topos.live

View File

@ -14,7 +14,7 @@
</p>
</p>
Topos is a web-based live coding environment. It lives [here](https://topos.raphaelforment.fr). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal.
Topos is a web-based live coding environment. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal.
![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif)

1
dev-dist/registerSW.js Normal file
View File

@ -0,0 +1 @@
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })

93
dev-dist/sw.js Normal file
View File

@ -0,0 +1,93 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-5357ef54'], (function (workbox) { 'use strict';
self.skipWaiting();
workbox.clientsClaim();
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "registerSW.js",
"revision": "3ca0b8505b4bec776b69afdba2768812"
}, {
"url": "index.html",
"revision": "0.aa2tv1h0in8"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
}));
//# sourceMappingURL=sw.js.map

1
dev-dist/sw.js.map Normal file

File diff suppressed because one or more lines are too long

3395
dev-dist/workbox-5357ef54.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
favicon/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
favicon/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
favicon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
favicon/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="850.000000pt" height="850.000000pt" viewBox="0 0 850.000000 850.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,850.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 4250 l0 -3770 4250 0 4250 0 0 3770 0 3770 -4250 0 -4250 0 0
-3770z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 603 B

19
favicon/site.webmanifest Normal file
View File

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

1
global.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite-plugin-pwa/client" />

View File

@ -1,10 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Topos is a live coding environment inspired by the Monome Teletype.">
<title>Topos</title>
<meta charset="UTF-8" />
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
<link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="/src/output.css" />
<link rel="stylesheet" href='/fonts/index.css' >
<script src="https://unpkg.com/hydra-synth"></script>
@ -133,13 +139,23 @@
<p rel="noopener noreferrer" id="docs_interface" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interface</p>
<p rel="noopener noreferrer" id="docs_interaction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interaction</p>
<p rel="noopener noreferrer" id="docs_shortcuts" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Keyboard</p>
<p rel="noopener noreferrer" id="docs_mouse" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Mouse</p>
<p rel="noopener noreferrer" id="docs_code" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Coding</p>
</div>
</details>
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl pb-1 pt-1 text-orange-300">Learning</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_time" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time/Rhythm</p>
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Time</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_time" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Dealing with time</p>
<p rel="noopener noreferrer" id="docs_linear" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Transport</p>
<p rel="noopener noreferrer" id="docs_cyclic" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Cycles</p>
<p rel="noopener noreferrer" id="docs_longform" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Structure</p>
</div>
</details>
<p rel="noopener noreferrer" id="docs_sound" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Audio Engine</p>
<p rel="noopener noreferrer" id="docs_samples" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Samples</p>
<p rel="noopener noreferrer" id="docs_synths" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synths</p>
@ -164,11 +180,21 @@
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">More</summary>
<div class="flex flex-col">
<a rel="noopener noreferrer" id="docs_synchronisation" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synchronisation</a>
<a rel="noopener noreferrer" id="docs_oscilloscope" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Oscilloscope</a>
<a rel="noopener noreferrer" id="docs_bonus" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Bonus/Trivia</a>
<a rel="noopener noreferrer" id="docs_about" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">About Topos</a>
</div>
</details>
<details class="" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">Community</summary>
<form action="https://github.com/Bubobubobubobubo/topos">
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="GitHub" />
</form>
<form action="https://discord.gg/6T67DqBNNT">
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Discord" />
</form>
</details>
</nav>
</aside>
<div id="documentation-content" class="w-full flex-grow-1 h-screen overflow-y-scroll lg:px-12 mx-2 my-2 break-words pb-32">

View File

@ -12,7 +12,9 @@
"@tauri-apps/cli": "^1.4.0",
"@types/audioworklet": "^0.0.49",
"typescript": "^5.2.2",
"vite": "^4.4.5"
"vite": "^4.4.5",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^0.16.7"
},
"dependencies": {
"@codemirror/lang-javascript": "^6.1.9",

2
robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Allow: /

View File

@ -1,4 +1,4 @@
import { EditorView } from '@codemirror/view';
import { EditorView } from "@codemirror/view";
import { getAllScaleNotes, nearScales, seededRandom } from "zifferjs";
import {
MidiCCEvent,
@ -28,7 +28,7 @@ import {
import { Speaker } from "./extensions/StringExtensions";
import { getScaleNotes } from "zifferjs";
import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation";
import { SkipEvent } from './classes/SkipEvent';
import { SkipEvent } from "./classes/SkipEvent";
interface ControlChange {
channel: number;
@ -151,9 +151,13 @@ export class UserAPI {
const stackLines = error.stack?.split("\n");
if (stackLines) {
for (const line of stackLines) {
if (line.includes('<anonymous>')) {
if (line.includes("<anonymous>")) {
const match = line.match(/<anonymous>:(\d+):(\d+)/);
if (match) return { line: parseInt(match[1], 10), column: parseInt(match[2], 10) };
if (match)
return {
line: parseInt(match[1], 10),
column: parseInt(match[2], 10),
};
}
}
}
@ -161,16 +165,17 @@ export class UserAPI {
};
const { line, column } = extractLineAndColumn(error);
const errorMessage = line && column
? `${error.message} (Line: ${line - 2}, Column: ${column})`
: error.message;
const errorMessage =
line && column
? `${error.message} (Line: ${line - 2}, Column: ${column})`
: error.message;
clearTimeout(this.errorTimeoutID);
clearTimeout(this.printTimeoutID);
this.app.interface.error_line.innerHTML = errorMessage;
this.app.interface.error_line.style.color = "color-red-800";
this.app.interface.error_line.classList.remove("hidden");
// @ts-ignore
this.errorTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
2000
@ -184,6 +189,7 @@ export class UserAPI {
this.app.interface.error_line.innerHTML = message as string;
this.app.interface.error_line.style.color = "white";
this.app.interface.error_line.classList.remove("hidden");
// @ts-ignore
this.printTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
4000
@ -404,7 +410,7 @@ export class UserAPI {
* { channel: 0, velocity: 100, duration: 0.5 }
*/
const event = {note: value, velocity, channel, port} as MidiParams
const event = { note: value, velocity, channel, port } as MidiParams;
return new MidiEvent(event, this.app);
};
@ -673,7 +679,7 @@ export class UserAPI {
public resetAllFromCache = (): void => {
this.patternCache.forEach((player) => (player as Player).reset());
}
};
public removePatternFromCache = (id: string): void => {
this.patternCache.delete(id);
@ -1011,74 +1017,6 @@ export class UserAPI {
};
cmp = this.clamp;
// =============================================================
// Transport functions
// =============================================================
public nudge = (nudge?: number): number => {
/**
* Sets or returns the current clock nudge.
*
* @param nudge - [optional] the nudge to set
* @returns The current nudge
*/
if (nudge) {
this.app.clock.nudge = nudge;
}
return this.app.clock.nudge;
};
public bpm = (n?: number): number => {
/**
* Sets or returns the current bpm.
*
* @param bpm - [optional] The bpm to set
* @returns The current bpm
*/
if (n === undefined) return this.app.clock.bpm;
if (n < 1 || n > 500) console.log(`Setting bpm to ${n}`);
this.app.clock.bpm = n;
return n;
};
tempo = this.bpm;
public bpb = (n?: number): number => {
/**
* Sets or returns the number of beats per bar.
*
* @param bpb - [optional] The number of beats per bar to set
* @returns The current bpb
*/
if (n === undefined) return this.app.clock.time_signature[0];
if (n < 1) console.log(`Setting bpb to ${n}`);
this.app.clock.time_signature[0] = n;
return n;
};
public ppqn = (n?: number) => {
/**
* Sets or returns the number of pulses per quarter note.
*/
if (n === undefined) return this.app.clock.ppqn;
if (n < 1) console.log(`Setting ppqn to ${n}`);
this.app.clock.ppqn = n;
return n;
};
public time_signature = (numerator: number, denominator: number): void => {
/**
* Sets the time signature.
*
* @param numerator - The numerator of the time signature
* @param denominator - The denominator of the time signature
* @returns The current time signature
*/
this.app.clock.time_signature = [numerator, denominator];
};
// =============================================================
// Probability functions
// =============================================================
@ -1299,30 +1237,29 @@ export class UserAPI {
// =============================================================
public fullseq = (sequence: string, duration: number) => {
if (sequence.split('').every(c => c === 'x' || c === 'o')) {
return [...sequence].map(c => c === 'x').beat(duration);
if (sequence.split("").every((c) => c === "x" || c === "o")) {
return [...sequence].map((c) => c === "x").beat(duration);
} else {
return false
return false;
}
}
};
public seq = (expr: string, duration: number = 0.5): boolean => {
let len = expr.length * duration
let len = expr.length * duration;
let output: number[] = [];
for (let i = 1; i <= len + 1; i += duration) {
output.push(Math.floor(i * 10) / 10);
}
output.pop()
output.pop();
output = output.filter((_, idx) => {
const exprIdx = idx % expr.length;
return expr[exprIdx] === 'x';
return expr[exprIdx] === "x";
});
return this.oncount(output, len)
}
return this.oncount(output, len);
};
public beat = (n: number | number[] = 1, nudge: number = 0): boolean => {
/**
@ -1335,7 +1272,7 @@ export class UserAPI {
const results: boolean[] = nArray.map(
(value) =>
(this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) %
Math.floor(value * this.ppqn()) ===
Math.floor(value * this.ppqn()) ===
0
);
return results.some((value) => value === true);
@ -1355,7 +1292,7 @@ export class UserAPI {
const results: boolean[] = nArray.map(
(value) =>
(this.app.clock.pulses_since_origin - nudgeInPulses) %
Math.floor(value * barLength) ===
Math.floor(value * barLength) ===
0
);
return results.some((value) => value === true);
@ -1889,7 +1826,7 @@ export class UserAPI {
// =============================================================
sound = (sound: string | string[] | null | undefined) => {
if(sound) return new SoundEvent(sound, this.app);
if (sound) return new SoundEvent(sound, this.app);
else return new SkipEvent();
};
@ -2040,7 +1977,7 @@ export class UserAPI {
effects: this.app.fontSize.reconfigure(
EditorView.theme({
"&": { fontFamily: mainFont },
".cm-gutters": { fontFamily: mainFont, },
".cm-gutters": { fontFamily: mainFont },
".cm-content": {
fontFamily: mainFont,
},
@ -2050,5 +1987,140 @@ export class UserAPI {
})
),
});
}
};
// =============================================================
// Resolution
// =============================================================
public gif = (options: any) => {
/**
* Displays a GIF on the webpage with customizable options including rotation and timed fade-out.
* @param {Object} options - The configuration object for displaying the GIF.
* @param {string} options.url - The URL of the GIF to display.
* @param {number} [options.posX=0] - The X-coordinate to place the GIF at.
* @param {number} [options.posY=0] - The Y-coordinate to place the GIF at.
* @param {number} [options.opacity=1] - The initial opacity level of the GIF.
* @param {string} [options.size='auto'] - The size of the GIF (can be 'cover', 'contain', or specific dimensions).
* @param {boolean} [options.center=false] - Whether to center the GIF in the window.
* @param {number} [options.rotation=0] - The rotation angle of the GIF in degrees.
* @param {string} [options.filter='none'] - The CSS filter function to apply for color alterations.
* @param {number} [options.duration=10] - The total duration the GIF is displayed, in pulses.
*/
const {
url,
posX = 0,
posY = 0,
opacity = 1,
size = "auto",
center = false,
rotation = 0,
filter = "none",
dur = 1,
} = options;
let real_duration =
dur * this.app.clock.pulse_duration * this.app.clock.ppqn;
let fadeOutDuration = real_duration * 0.1;
let visibilityDuration = real_duration - fadeOutDuration;
const gifElement = document.createElement("img");
gifElement.src = url;
gifElement.style.position = "fixed";
gifElement.style.left = center ? "50%" : `${posX}px`;
gifElement.style.top = center ? "50%" : `${posY}px`;
gifElement.style.opacity = `${opacity}`;
gifElement.style.zIndex = "-1";
if (size !== "auto") {
gifElement.style.width = size;
gifElement.style.height = size;
}
const transformRules = [`rotate(${rotation}deg)`];
if (center) {
transformRules.unshift("translate(-50%, -50%)");
}
gifElement.style.transform = transformRules.join(" ");
gifElement.style.filter = filter;
gifElement.style.transition = `opacity ${fadeOutDuration}s ease`;
document.body.appendChild(gifElement);
// Start the fade-out at the end of the visibility duration
setTimeout(() => {
gifElement.style.opacity = "0";
}, visibilityDuration * 1000);
// Remove the GIF from the DOM after the fade-out duration
setTimeout(() => {
if (document.body.contains(gifElement)) {
document.body.removeChild(gifElement);
}
}, real_duration * 1000);
};
// =============================================================
// Transport functions
// =============================================================
public nudge = (nudge?: number): number => {
/**
* Sets or returns the current clock nudge.
*
* @param nudge - [optional] the nudge to set
* @returns The current nudge
*/
if (nudge) {
this.app.clock.nudge = nudge;
}
return this.app.clock.nudge;
};
public bpm = (n?: number): number => {
/**
* Sets or returns the current bpm.
*
* @param bpm - [optional] The bpm to set
* @returns The current bpm
*/
if (n === undefined) return this.app.clock.bpm;
if (n < 1 || n > 500) console.log(`Setting bpm to ${n}`);
this.app.clock.bpm = n;
return n;
};
tempo = this.bpm;
public bpb = (n?: number): number => {
/**
* Sets or returns the number of beats per bar.
*
* @param bpb - [optional] The number of beats per bar to set
* @returns The current bpb
*/
if (n === undefined) return this.app.clock.time_signature[0];
if (n < 1) console.log(`Setting bpb to ${n}`);
this.app.clock.time_signature[0] = n;
return n;
};
public ppqn = (n?: number) => {
/**
* Sets or returns the number of pulses per quarter note.
*/
if (n === undefined) return this.app.clock.ppqn;
if (n < 1) console.log(`Setting ppqn to ${n}`);
this.app.clock.ppqn = n;
return n;
};
public time_signature = (numerator: number, denominator: number): void => {
/**
* Sets the time signature.
*
* @param numerator - The numerator of the time signature
* @param denominator - The denominator of the time signature
* @returns The current time signature
*/
this.app.clock.time_signature = [numerator, denominator];
};
}

View File

@ -81,6 +81,7 @@ export const blinkScript = (
_drawBlinker(shiftAmount);
// Save timeout ID for later clearing
// @ts-ignore
app.blinkTimeouts[shiftAmount] = setTimeout(() => {
_clearBlinker(shiftAmount);
// Clear the canvas before drawing new blinkers

View File

@ -54,7 +54,7 @@ export class Clock {
this.logicalTime = 0;
this.tick = 0;
this._bpm = 120;
this._ppqn = 48;
this._ppqn = 96;
this.transportNode = null;
this.ctx = ctx;
this.running = true;
@ -164,7 +164,7 @@ export class Clock {
}
get realTime(): number {
return this.app.audioContext.currentTime - this.totalPauseTime;
return this.app.audioContext.currentTime - this.totalPauseTime;
}
get deviation(): number {
@ -175,6 +175,7 @@ export class Clock {
if (ppqn > 0 && this._ppqn !== ppqn) {
this._ppqn = ppqn;
this.transportNode?.setPPQN(ppqn);
this.logicalTime = this.realTime;
}
}
@ -192,15 +193,12 @@ export class Clock {
*/
const pulseDuration = this.pulse_duration;
const nudgedTime = time + nudge;
const nextTickTime = Math.ceil(nudgedTime /
pulseDuration) * pulseDuration;
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
const remainingTime = nextTickTime - nudgedTime;
return remainingTime;
}
public convertPulseToSecond(n: number): number {
/**
* Converts a pulse to a second.
@ -218,7 +216,7 @@ export class Clock {
this.running = true;
this.app.api.MidiConnection.sendStartMessage();
this.lastPlayPressTime = this.app.audioContext.currentTime;
this.totalPauseTime += (this.lastPlayPressTime - this.lastPauseTime);
this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime;
this.transportNode?.start();
}

View File

@ -1,16 +1,21 @@
import { type Editor } from "./main";
import { introduction } from "./documentation/introduction";
import { oscilloscope } from "./documentation/oscilloscope";
import { synchronisation } from "./documentation/synchronisation";
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 { linear_time } from "./documentation/linear_time";
import { cyclical_time } from "./documentation/cyclical_time";
import { long_forms } from "./documentation/long_forms";
import { midi } from "./documentation/midi";
import { code } from "./documentation/code";
import { about } from "./documentation/about";
import { sound } from "./documentation/engine";
import { shortcuts } from "./documentation/keyboard";
import { mouse } from "./documentation/mouse";
import { patterns } from "./documentation/patterns";
import { functions } from "./documentation/functions";
import { variables } from "./documentation/variables";
@ -44,9 +49,9 @@ export const makeExampleFactory = (application: Editor): Function => {
return `
<details ${open ? "open" : ""}>
<summary >${description}
<button class="py-1 align-top text-base rounded-lg pl-2 pr-2 hover:bg-green-700 bg-green-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
<button class="py-1 text-base rounded-lg pr-2 hover:bg-neutral-600 bg-neutral-500 inline-block pl-2" onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base rounded-lg pr-2 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
<button class="ml-4 py-1 align-top text-base px-4 hover:bg-green-700 bg-emerald-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
<button class="py-1 text-base px-4 hover:bg-neutral-600 bg-neutral-500 inline-block " onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base px-4 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
</summary>
\`\`\`javascript
${code}
@ -58,7 +63,6 @@ export const makeExampleFactory = (application: Editor): Function => {
};
export const documentation_factory = (application: Editor) => {
// Initialize a data structure to store code examples by their unique IDs
application.api.codeExamples = {};
@ -68,6 +72,9 @@ export const documentation_factory = (application: Editor) => {
interaction: interaction(application),
code: code(application),
time: time(application),
linear: linear_time(application),
cyclic: cyclical_time(application),
longform: long_forms(application),
sound: sound(application),
samples: samples(application),
synths: synths(application),
@ -80,15 +87,16 @@ export const documentation_factory = (application: Editor) => {
probabilities: probabilities(application),
functions: functions(application),
reference: reference(),
shortcuts: shortcuts(),
shortcuts: shortcuts(application),
mouse: mouse(application),
oscilloscope: oscilloscope(application),
synchronisation: synchronisation(application),
bonus: bonus(application),
about: about(),
};
};
export const showDocumentation = (app: Editor) => {
if (document.getElementById("app")?.classList.contains("hidden")) {
document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden");

View File

@ -61,12 +61,13 @@ export const buttonGroups = {
clear_buttons: ["clear-button-1"],
};
//@ts-ignore
export const createDocumentationStyle = (app: Editor) => {
return {
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 underline underline-offset-8 pt-2 pb-3 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 underline underline-offset-8 pt-2 pb-3 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-900 rounded-lg underline underline-offset-8 pt-2 pb-3 px-2 lg:mt-16",
ul: "text-underline pl-6",
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 border-b-4 pt-4 pb-3 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 border-b-2 pt-12 pb-3 px-2",
h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 border-l-2 border-b-2 lg:mb-4 mb-4 pb-2 px-2 lg:mt-16",
ul: "text-underline ml-12",
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",
warning:
@ -78,7 +79,7 @@ export const createDocumentationStyle = (app: Editor) => {
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4",
details:
"lg:mx-12 py-2 px-6 lg:text-2xl text-white rounded-lg bg-neutral-600",
"lg:mx-20 py-2 px-6 lg:text-2xl text-white border-l-8 box-border bg-neutral-900",
summary: "font-semibold text-xl",
table:
"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",
@ -87,6 +88,7 @@ export const createDocumentationStyle = (app: Editor) => {
th: "",
td: "",
tr: "",
box: "border bg-red-500",
};
}

View File

@ -578,8 +578,7 @@ export class MidiConnection {
if (typeof output === "number") {
if (output < 0 || output >= this.midiOutputs.length) {
console.error(
`Invalid MIDI output index. Index must be in the range 0-${
this.midiOutputs.length - 1
`Invalid MIDI output index. Index must be in the range 0-${this.midiOutputs.length - 1
}.`
);
return this.currentOutputIndex;
@ -608,8 +607,7 @@ export class MidiConnection {
if (typeof input === "number") {
if (input < 0 || input >= this.midiInputs.length) {
console.error(
`Invalid MIDI input index. Index must be in the range 0-${
this.midiInputs.length - 1
`Invalid MIDI input index. Index must be in the range 0-${this.midiInputs.length - 1
}.`
);
return -1;
@ -676,6 +674,7 @@ export class MidiConnection {
delete this.scheduledNotes[noteNumber];
}, (duration - 0.02) * 1000);
// @ts-ignore
this.scheduledNotes[noteNumber] = timeoutId;
} else {
console.error("MIDI output not available.");

View File

@ -39,7 +39,8 @@ export const installInterfaceLogic = (app: Editor) => {
(app.interface.time_position_checkbox as HTMLInputElement).checked =
app.settings.time_position;
(app.interface.tips_checkbox as HTMLInputElement).checked = app.settings.tips;
(app.interface.completion_checkbox as HTMLInputElement).checked = app.settings.completions;
(app.interface.completion_checkbox as HTMLInputElement).checked =
app.settings.completions;
(app.interface.midi_clock_checkbox as HTMLInputElement).checked =
app.settings.send_clock;
@ -392,7 +393,8 @@ export const installInterfaceLogic = (app: Editor) => {
});
app.interface.completion_checkbox.addEventListener("change", () => {
let checked = (app.interface.completion_checkbox as HTMLInputElement).checked
let checked = (app.interface.completion_checkbox as HTMLInputElement)
.checked
? true
: false;
app.settings.completions = checked;
@ -458,6 +460,9 @@ export const installInterfaceLogic = (app: Editor) => {
"interaction",
"code",
"time",
"linear",
"cyclic",
"longform",
"sound",
"samples",
"synths",
@ -470,6 +475,8 @@ export const installInterfaceLogic = (app: Editor) => {
"probabilities",
"variables",
// "reference",
"synchronisation",
"mouse",
"shortcuts",
"about",
"bonus",

View File

@ -1,5 +1,4 @@
class TransportProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.port.addEventListener("message", this.handleMessage);
@ -7,7 +6,7 @@ class TransportProcessor extends AudioWorkletProcessor {
this.nudge = 0;
this.started = false;
this.bpm = 120;
this.ppqn = 48;
this.ppqn = 96;
this.currentPulsePosition = 0;
}
@ -20,19 +19,20 @@ class TransportProcessor extends AudioWorkletProcessor {
this.started = false;
} else if (message.data.type === "stop") {
this.started = false;
} else if (message.data.type === 'bpm') {
} else if (message.data.type === "bpm") {
this.bpm = message.data.value;
this.currentPulsePosition = currentTime;
} else if (message.data.type === 'ppqn') {
} else if (message.data.type === "ppqn") {
this.ppqn = message.data.value;
} else if (message.data.type === 'nudge') {
this.currentPulsePosition = currentTime;
} else if (message.data.type === "nudge") {
this.nudge = message.data.value;
}
}
};
process(inputs, outputs, parameters) {
if (this.started) {
const adjustedCurrentTime = currentTime + (this.nudge / 100);
const adjustedCurrentTime = currentTime + this.nudge / 100;
const beatNumber = adjustedCurrentTime / (60 / this.bpm);
const currentPulsePosition = Math.ceil(beatNumber * this.ppqn);
if (currentPulsePosition > this.currentPulsePosition) {
@ -44,7 +44,4 @@ class TransportProcessor extends AudioWorkletProcessor {
}
}
registerProcessor(
"transport",
TransportProcessor
);
registerProcessor("transport", TransportProcessor);

View File

@ -16,14 +16,22 @@ const handleResize = (canvas: HTMLCanvasElement) => {
}
};
export const saveBeforeExit = (app: Editor): null => {
// @ts-ignore
event.preventDefault();
// Iterate over all local files and set the candidate to the committed
app.currentFile().candidate = app.view.state.doc.toString();
app.currentFile().committed = app.view.state.doc.toString();
app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
return null;
};
export const installWindowBehaviors = (
app: Editor,
window: Window,
preventMultipleTabs: boolean = false
) => {
window.addEventListener("resize", () =>
handleResize(app.interface.scope as HTMLCanvasElement)
);
@ -31,14 +39,10 @@ export const installWindowBehaviors = (
handleResize(app.interface.feedback as HTMLCanvasElement)
);
window.addEventListener("beforeunload", () => {
// @ts-ignore
event.preventDefault();
// Iterate over all local files and set the candidate to the committed
app.currentFile().candidate = app.view.state.doc.toString();
app.currentFile().committed = app.view.state.doc.toString();
app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
app.clock.stop();
return null;
saveBeforeExit(app)
});
window.addEventListener("visibilitychange", () => {
saveBeforeExit(app)
});
if (preventMultipleTabs) {

View File

@ -432,7 +432,6 @@ export class SoundEvent extends AudibleEvent {
};
out = (): void => {
console.log(this.app.clock.time_position.pulse)
const events = objectWithArraysToArrayOfObjects(this.values, [
"parsedScale",
]);

View File

@ -7,7 +7,7 @@ export const bonus = (application: Editor): string => {
return `
# Bonus features
Some features are not part of the core of Topos but are still very useful. They are not described in the main documentation but are still available in the API. These features are sometimes coming from personal experiments, from a thinking-out-loud process or from a sudden desire to hack things. This bonus set of functionalities is not guaranteed to be stable.
Some features are here "just for fun" or "just because I can". They are not very interesting per se but are still available nonetheless. They mostly gravitate towards manipulating visuals or patterning other multimedia formats.
## Hydra Visual Live Coding
@ -18,10 +18,10 @@ Some features are not part of the core of Topos but are still very useful. They
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows the metaphor of analog synthesizer patching to allow its user to create complex live visuals from a web browser window. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance. Topos provides a simple way to integrate Hydra into a live coding session and to blend it with regular Topos code.
${makeExample(
"Hydra integration",
`beat(4) :: app.hydra.osc(3, 0.5, 2).out()`,
true
)}
"Hydra integration",
`beat(4) :: app.hydra.osc(3, 0.5, 2).out()`,
true
)}
You may feel like it's doing nothing! Press ${key_shortcut(
"Ctrl+D"
@ -32,17 +32,38 @@ Be careful not to call <ic>app.hydra</ic> too often as it can impact performance
Stopping **Hydra** is simple:
${makeExample(
"Stopping Hydra",
`
"Stopping Hydra",
`
beat(4) :: stop_hydra() // this one
beat(4) :: app.hydra.hush() // or this one
`,
true
)}
true
)}
I won't teach you how to play with Hydra. You can find some great resources on the [Hydra website](https://hydra.ojack.xyz/):
- [Hydra interactive documentation](https://hydra.ojack.xyz/docs/)
- [List of Hydra Functions](https://hydra.ojack.xyz/api/)
- [Source code on GitHub](https://github.com/hydra-synth/hydra)
## GIF player
Topos embeds a small <ic>.gif</ic> picture player with a small API. GIFs are automatically fading out after the given duration. Look at the following example:
${makeExample(
"Playing many gifs",
`
beat(0.25)::gif({
url:v('gif')[$(1)%6], // Any URL will do!
opacity: r(0.5, 1), // Opacity (0-1)
size:"300px", // CSS size property
center:false, // Centering on the screen?
filter:'none', // CSS Filter
dur: 2, // In beats (Topos unit)
rotation: ir(1, 360), // Rotation (in degrees)
posX: ir(1,1200), // CSS Horizontal Position
posY: ir(1, 800), // CSS Vertical Position
`, true
)}
`;
};

View File

@ -6,29 +6,29 @@ export const code = (application: Editor): string => {
return `
# Code
Topos is using the [JavaScript](https://en.wikipedia.org/wiki/JavaScript) syntax because it lives in a web browser where JS is the default programming language. It is also a language that you can learn to speak quite fast if you are already familiar with other programming languages. You are not going to write a lot of code anyway but familiarity with the language can help. Here are some good resources:
Topos scripts are using the [JavaScript](https://en.wikipedia.org/wiki/JavaScript) syntax. This is the language used to write pretty much anything in a web browser. JavaScript is easy to learn, and even faster to learn if you are already familiar with other programming languages. Here are some good resources if you want to learn more about it:
- [MDN (Mozilla Web Docs)](https://developer.mozilla.org/): it covers pretty much anything and is considered to be a reliable source to learn how the web currently works. We use it quite a lot to develop Topos.
- [MDN (Mozilla Web Docs)](https://developer.mozilla.org/): it covers pretty much anything and is considered to be a reliable source to learn how the web currently works. Any web developer knows about it.
- [Learn JS in Y Minutes](https://learnxinyminutes.com/docs/javascript/): a good tour of the language. Can be useful as a refresher.
- [The Modern JavaScript Tutorial](https://javascript.info/): another well known source to learn the language.
You **do not need to have any prior knowledge of programming** to use Topos. It can also be used as a **valuable resource** to learn some basic programming.
**You do not need to have any prior knowledge of programming** to use Topos**.
## How is the code evaluated?
# How is the code evaluated?
The code you enter in any of the scripts is evaluated in strict mode. This tells your browser that the code you run can be optimized quite agressively. We need this because by default, **the global script is evaluated 48 times per beat**. It also means that you can crash at the speed of light :smile:. The local and initialisation scripts are evaluated on demand, one run at a time. There are some things to keep in mind:
The code you enter in any of the scripts is evaluated in strict mode. This tells your browser that the code you run can be optimized quite agressively. We need this because by default, **the global script is evaluated 48 times per beat**. It also means that you can crash at the speed of light :smile:. There are some things to keep in mind:
- **about variables:** the state of your variables is not kept between iterations. If you write <ic>let a = 2</ic> and change the value later on, the value will be reset to <ic>2</ic> after each run! There are other ways to deal with variables and to share variables between scripts! Some variables like **iterators** can keep their state between iterations because they are saved **with the file itself**.
- **about errors and printing:** your code will crash! Don't worry, it will hopefully try to crash in the most gracious way possible. To check if your code is erroring, you will have to open the dev console with ${key_shortcut(
- **about variables:** the state of your variables is not kept between iterations. If you write <ic>let a = 2</ic> and remove that value from your script, **it will crash**! Variable and state is not preserved between each run of the script. There are other ways to deal with variables and to share variables between scripts! Some variables like **iterators** can keep their state between iterations because they are saved **with the file itself**. There is also **global variables**.
- **about errors and printing:** your code will crash! Don't worry, we do our best to make it crash in the most gracious way possible. Most errors are caught and displayed in the interface. For weirder bugs, open the dev console with ${key_shortcut(
"Ctrl + Shift + I"
)}. You cannot directly use <ic>console.log('hello, world')</ic> in the interface but you can use <ic>log(message)</ic> to print a one line message. You will have to open the console as well to see your messages being printed there!
- **about new syntax:** sometimes, we have taken liberties with the JavaScript syntax in order to make it easier/faster to write on stage. <ic>&&</ic> can also be written <ic>::</ic> or <ic>-></ic> because it is faster to type or better for the eyes!
- **about new syntax:** sometimes, we had some fun with JavaScript's syntax in order to make it easier/faster to write on stage. <ic>&&</ic> can also be written <ic>::</ic> or <ic>-></ic> because it is faster to type or better for the eyes!
## Common idioms
# Common idioms
There are some techniques that Topos players are using to keep their JavaScript short and tidy. Don't try to write the shortest possible code but use shortcuts when it makes sense. It's sometimes very comforting to take time to write utilities and scripts that you will often reuse. Take a look at the following examples:
There are some techniques to keep code short and tidy. Don't try to write the shortest possible code! Use shortcuts when it makes sense. Take a look at the following examples:
${makeExample(
"Shortening your if conditions",
@ -66,16 +66,17 @@ beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
false
)}
## About crashes and bugs
# About crashes and bugs
Things will crash, that's also part of the show. You will learn progressively to avoid mistakes and to write safer code. Do not hesitate to kill the page or to stop the transport if you feel overwhelmed by an algorithm blowing up. There are no safeties in place to save you. This is to ensure that you have all the available possible room to write bespoke code and experiment with your ideas through code.
Things will crash! It's part of the show! You will learn progressively to avoid mistakes and to write safer code. Do not hesitate to kill the page or to stop the transport if you feel overwhelmed by an algorithm blowing up. There is no safeguard to stop you from doing most things. This is to ensure that you have all the available possible room to write bespoke code and experiment with your ideas through code.
${makeExample(
"This example will crash! Who cares?",
`// This is crashing. Open your console!
`
// This is crashing. See? No harm!
qjldfqsdklqsjdlkqjsdlqkjdlksjd
`,
false
true
)}
`;

View File

@ -0,0 +1,204 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const cyclical_time = (app: Editor): string => {
// @ts-ignore
let makeExample = makeExampleFactory(app);
return `
# Cyclical time
Time as a cycle. A cycle can be quite long (a few bars) or very short (a few pulses). Cyclical time is extremely interesting for _live coders_ since it allows you to control a process that will eventually repeat. If your time constructs are repeating, you are able to hear them again and again. Since you can react and alter the code to change the loops, you become part of a complex feedback system between the computer and yourself.
## Simple rhythms
- <ic>beat(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ beats.
- <ic>number</ic>: if <ic>number = 1</ic>, the function will return <ic>true</ic> every beat. Lists can be used too.
- <ic>offset</ic>: offset (in beats) to apply. An offset of <ic>0.5</ic> will return true against the beat.
${makeExample(
"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
)}
${makeExample(
"Some sort of ringtone",
`
// Blip generator :)
let blip = (freq) => {
return sound('wt_piano')
.gain(1)
.sustain(0.1)
.freq(freq)
.cutoff(1500)
.lpadsr(4, 0, .25, 0, 0)
};
beat(1) :: blip(200).pan(r(0,1)).vib(0.5).vibmod(2).out();
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
)}
${makeExample(
"Beat can match multiple values",
`
beat([.5, 1.25])::sound('hat').out()
`,
false
)}
- <ic>pulse(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ pulses. A pulse is the tiniest possible rhythmic value.
- <ic>number</ic>: if <ic>number = 1</ic>, the function will return <ic>true</ic> every pulse. Lists can be used too.
- <ic>offset</ic>: offset (in pulses) to apply.
${makeExample(
"Intriguing rhythms",
`
pulse([24, 16])::sound('hat').ad(0, .02).out()
pulse([48, [36,24].dur(4, 1)])::sound('fhardkick').ad(0, .1).out()
`,
true
)}
${makeExample(
"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
)}
- <ic>bar(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ bars.
- <ic>number</ic>: if <ic>number = 1</ic>, the function will return <ic>true</ic> every bar. Lists can be used too.
- <ic>offset</ic>: offset (in bars) to apply.
${makeExample(
"Four beats per bar: proof",
`
bar(1)::sound('kick').out()
beat(1)::sound('hat').speed(2).out()
`,
true
)}
${makeExample(
"Offsetting beat and bar",
`
bar(1)::sound('kick').out()
beat(1)::sound('hat').speed(2).out()
beat(1, 0.5)::sound('hat').speed(4).out()
bar(1, 0.5)::sound('sn').out()
`,
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",
`
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
)}
## Cyclical rhythm generators
We included a bunch of popular rhythm generators in Topos such as the euclidian rhythms algorithms or the one to generate rhythms based on a binary sequence. They all work using _iterators_ that you will gradually learn to use for iterating over lists. Note that they are levaraging <ic>mod(...n:number[])</ic> that you just learned about!
- <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(.5, 4, 8)::sound('sine')
.fmi(2)
.room(0.5).size(8)
.freq(250).ad(0, .2).out()
rhythm(.5, 7, 8)::sound('sine')
.freq(125).ad(0, .2).out()
rhythm(.5, 3, 8)::sound('sine').freq(500).ad(0, .5).out()
`,
true
)}
- <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",
`
// 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()
`,
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!",
`
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
)}
${makeExample(
"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()
binrhythm([.5, .25].beat(1), 30) && snd('wt_granular').n(a)
.cutoff(800).lpadsr(4, 0, 0.125, 0.5, 0.25)
.adsr(0, r(.1, .4), 0, 0).freq([50, 60, 72].beat(4))
.room(1).size(1).out()
`,
true
)}
${makeExample(
"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
)}
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!",
`
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
)}
`;
};

View File

@ -1,10 +1,9 @@
import { hoverTooltip } from "@codemirror/view";
import { type EditorView } from "@codemirror/view";
import { CompletionContext } from "@codemirror/autocomplete"
import { CompletionContext } from "@codemirror/autocomplete";
// @ts-ignore
import { soundMap } from "superdough";
interface InlineCompletion {
name: string;
category: string;
@ -562,6 +561,13 @@ const completionDatabase: CompletionDatabase = {
description: "Get or set the current beats per minute.",
example: "bpm(135) // set the bpm to 135",
},
tempo: {
name: "tempo",
category: "time",
description: "Get or set the current beats per minute.",
example: "tempo(135) // set the bpm to 135",
},
out: {
name: "out",
category: "audio",
@ -974,30 +980,28 @@ export const inlineHoveringTips = hoverTooltip(
);
export const toposCompletions = (context: CompletionContext) => {
let word = context.matchBefore(/\w*/)
let word = context.matchBefore(/\w*/);
if (word) {
if (word.from == word.to && !context.explicit)
return null
if (word.from == word.to && !context.explicit) return null;
return {
from: word.from,
options: Object.keys(completionDatabase).map((key) => ({
label: key,
type: completionDatabase[key].category,
info: () => {
let div = document.createElement('div');
let div = document.createElement("div");
div.innerHTML = `
<h1 class="text-orange-300 text-base pb-1">${completionDatabase[key].name} [<em class="text-white">${completionDatabase[key].category}</em>]</h1>
<p class="text-base pl-4">${completionDatabase[key].description}</p>
<div class="overflow-hidden overflow-scroll rounded px-2 ml-4 mt-2 bg-neutral-800"><code class="text-sm">${completionDatabase[key].example}</code></div>
`
`;
div.classList.add("px-4", "py-2", "rounded-lg", "w-92");
return div
}
}))
}
return div;
},
})),
};
}
}
};
export const soundCompletions = (context: CompletionContext) => {
let map = soundMap.get();
@ -1007,10 +1011,10 @@ export const soundCompletions = (context: CompletionContext) => {
let from = match.from + "sound(".length;
return {
from,
options: Object.keys(map).map(key => ({
options: Object.keys(map).map((key) => ({
label: key,
type: map[key].data.type,
apply: `"${key}"`
apply: `"${key}"`,
})),
};
}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../main";
import { key_shortcut, makeExampleFactory } from "../Documentation";
import { makeExampleFactory } from "../Documentation";
// @ts-ignore
export const interaction = (application: Editor): string => {
@ -7,18 +7,8 @@ export const interaction = (application: Editor): string => {
return `
# Interaction
Topos can interact with the physical world or react to events coming from outside the system (_MIDI_, physical control, etc).
Topos can interact with the physical world or react to events coming from outside the system (_MIDI_, physical control, etc). By creating interactions with your mouse, keyboard or controllers, you can make your performance more dynamic and/or easier to control.
## Fill
By pressing the ${key_shortcut('Alt')} key, you can trigger the <ic>Fill</ic> mode which can either be <ic>true</ic> or <ic>false</ic>. The fill will be set to <ic>true</ic> as long as the key is held. Try pressing ${key_shortcut('Alt')} when playing this example:
${makeExample(
"Claping twice as fast with fill",
`
beat(fill() ? 1/4 : 1/2)::sound('cp').out()
`, true)
}
## MIDI input
@ -28,74 +18,81 @@ Topos can use MIDI input to estimate the BPM from incoming Clock messages and to
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 60 if no other 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",
`
"Play active notes as chords",
`
beat(1) && active_notes() && sound('sine').chord(active_notes()).out()
`,
true
)}
true
)}
${makeExample(
"Play active notes as arpeggios",
`
"Play active notes as arpeggios",
`
beat(0.25) && active_notes() && sound('juno').note(
active_notes().beat(0.5)+[12,24].beat(0.25)
).cutoff(300 + usine(1/4) * 2000).out()
`,
false
)}
false
)}
* <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.
${makeExample(
"Play continous arpeggio with sticky notes",
`
"Play continous arpeggio with sticky notes",
`
beat(0.25) && sticky_notes() && sound('arp')
.note(sticky_notes().palindrome().beat(0.25)).out()
`,
false
)}
true
)}
* <ic>last_note(channel?: number)</ic>: returns the last note that has been received. Returns 60 if no other notes have been received.
${makeExample(
"Play last note",
`
"Play last note",
`
beat(0.5) && sound('sawtooth').note(last_note())
.vib([1, 3, 5].beat(1))
.vibmod([1,3,2,4].beat(2)).out()
`,
false
)}
false
)}
* <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 buffered note",
`
"Play buffered note",
`
beat(1) && buffer() && sound('sine').note(buffer_note()).out()
`,
false
)}
false
)}
### MIDI CC Input
Midi CC messages can be used to control any value in Topos. MIDI input can be defined in Settings and last received CC message can be used to control any numeric value within Topos.
Currently supported methods for CC input are:
* <ic>last_cc(control: number, channel?: number): Returns last received CC value for given control number (and optional channel). By default last CC value is last value from ANY channel or 64 if no CC messages have been received.
* <ic>last_cc(control: number, channel?: number)</ic>: Returns last received CC value for given control number (and optional channel). By default last CC value is last value from ANY channel or 64 if no CC messages have been received.
${makeExample(
"Play notes with cc",
`
"Play notes with cc",
`
beat(0.5) && sound('arp').note(last_cc(74)).out()
`,
true
)}
true
)}
${makeExample(
"Control everything with CCs",
`
"Control everything with CCs",
`
beat(0.5) :: sound('sine')
.freq(last_cc(75)*3)
.cutoff(last_cc(76)*2*usine())
@ -108,59 +105,20 @@ beat(last_cc(74)/127*.5) :: sound('sine')
.sustain(last_cc(74)/127*.25)
.out()
`,
false
)}
false
)}
### Run scripts with MIDI
## Run scripts with MIDI
MIDI note messages with channels can also be used to trigger scripts.
This can be enabled in the settings panel by setting _Route channels to scripts_.
MIDI note messages with channels can also be used to trigger scripts. This can be enabled in the settings panel by setting _Route channels to scripts_.
### MIDI clock Synchronisation
## MIDI clock Synchronisation
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 <ic>Start</ic>, <ic>Stop</ic> and <ic>Continue</ic> 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
)}
## Scale output for lighted keys
Topos can output scales to external keyboards lighted keys using the following functions:
@ -168,18 +126,13 @@ Topos can output scales to external keyboards lighted keys using the following f
- <ic>show_scale(key: string, scale: string|int, channel?: number, port?: string|number, soundOff?: boolean): void</ic>: sends the scale as midi on messages to specified port and channel to light the keys of external keyboard. If soundOff is true, all sound off message will be sent after every note on message. This can be useful with some keyboards not supporting external channel for lightning or routing for the midi in to suppress the sound from incoming note on messages.
${makeExample(
"Show scale on external keyboard",
`show_scale("F","aeolian",0,4)`,
true
)}
"Show scale on external keyboard",
`show_scale("F","aeolian",0,4)`,
true
)}
${makeExample(
"Hide scale",
`hide_scale("F","aeolian",0,4)`,
true
)}
${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)}
`
}
`;
};

View File

@ -1,21 +1,21 @@
import { key_shortcut, makeExampleFactory } from "../Documentation";
import { type Editor } from "../main";
import topos_arch from './topos_arch.svg';
import many_universes from './many_universes.svg';
import topos_arch from "./topos_arch.svg";
import many_universes from "./many_universes.svg";
export const software_interface = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Interface
The Topos interface is entirely dedicated to highlight the core concepts at play: _scripts_ and _universes_. By understanding the interface, you will already understand quite a lot about Topos and how to play music with it. Make sure to learn the dedicated keybindings as well and you will fly!
The Topos interface is designed on a simple concept: _scripts_ and _universes_. By understanding how the interface works, you will already understand quite a lot. Make sure to learn the dedicated keybindings as well. They will give you extra powers!
<object type="image/svg+xml" data=${topos_arch} style="width: 100%; height: auto; background-color: transparent"></object>
## Scripts
# Scripts
Every Topos session is composed of several small scripts. A set of scripts is called a _universe_. Every script is written using the JavaScript programming language and describes a musical or algorithmic process that takes place over time.
Every Topos session is composed of **local**, **global** and **init** scripts. These scripts form a structure called a "_universe_". The scripts can describe whatever you want: songs, sketches, small tools, or whatever. All the scripts are written using the JavaScript programming language. They describe a musical or algorithmic process. You can call them anytime.
- **the global script** (${key_shortcut(
"Ctrl + G"
@ -38,54 +38,65 @@ Every Topos session is composed of several small scripts. A set of scripts is ca
${makeExample(
"To take the most out of Topos...",
`// Write your code in multiple scripts. Use all the code buffers!
beat(1) :: script(1)
flip(4) :: beat(.5) :: script(2)
"Calling scripts to form a musical piece",
`
beat(1) :: script(1) // Calling local script n°1
flip(4) :: beat(.5) :: script(2) // Calling script n°2
`,
true
)}
${makeExample(
"Script execution can become musical too!",
`// You can play your scripts... algorithmically.
beat(1) :: script([1,3,5].pick())
flip(4) :: beat([.5, .25].beat(16)) :: script([5,6,7,8].loop($(2)))
`
// Use algorithms to pick a script.
beat(1) :: script([1, 3, 5].pick())
flip(4) :: beat([.5, .25].beat(16)) :: script(
[5, 6, 7, 8].beat())
`,
false
)}
### Navigating the interface
The interface is centered around the manipulation of scripts. Take a look at the left bar:
- **pencil icon:** notes. Used to take project notes about your "_universe_".
- **down arrow**: init script. Runs once when the project is loaded.
- **text with note**: global script, it acts as the **conductor** for your piece.
- **folder icon**: local scripts (from 1 to 9).
### Managing scripts programatically
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
# Universes
<object type="image/svg+xml" data=${many_universes} style="width: 100%; height: auto; background-color: transparent"></object>
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(
A set of files is called a _universe_. You can switch between universes immediately immediately by pressing ${key_shortcut(
"Ctrl + B"
)}. You can also create a new universe by entering a name that has never been used before. _Universes_ are only referenced by their names. Once a universe is loaded, it is not possible to call any data/code from any other universe.
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!
)}. You can also create a new universe by entering a name. Load a universe by typing its name. Once a universe is loaded, it is not possible to call any data/code from any other universe. Switching between universes does not stop the transport nor reset the clock. The context switches but time keeps flowing. This can be useful for transitioning between songs / parts.
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>copy_universe(from: string, to: string)</ic>: copy the content of a universe to another.
- <ic>delete_universe(name: string)</ic>: delete a universe. Warning: this is irreversible!
# Sharing your work
**Click on the Topos logo in the top bar**. Your URL will change to something much longer and complex. The same URL will be copied to your clipboard. Send this link to your friends to share the universe you are currently working on with them.
**Click the share button**. The URL of the website will change to something much longer. This URL will automatically be copied to your clipboard. Send this link to your friends to share the universe you are currently working on with them. You can use a service like [tinyurl](https://tinyurl.com/) to shorten your links.
- The imported universe will always get a randomly generated name such as: <ic>random_silly_llama</ic>.
- Topos will automatically fetch and switch to the universe that was sent to you. Your previous universe is still accessible if you switch to it, don't worry!
- Topos will automatically fetch and switch to the universe that was sent to you.
**Note:** links are currently super long and unsharable! Sorry about that, minifying takes a server and we don't have one yet. We will fix that soon. In the meantime, you can use a service like [tinyurl](https://tinyurl.com/) to shorten your links.
`;
};

View File

@ -7,9 +7,9 @@ export const introduction = (application: Editor): string => {
return `
# Welcome
Welcome to the Topos documentation. These pages are offering you an introduction to the software and to the ideas behind it. You can jump here anytime by pressing ${key_shortcut(
Welcome to the **Topos** documentation. You can jump here anytime by pressing ${key_shortcut(
"Ctrl + D"
)}. Press again to make the documentation disappear. All your contributions are welcome!
)}. Press again to make the documentation disappear. Contributions are much appreciated! The documentation [lives here](https://github.com/Bubobubobubobubo/topos/tree/main/src/documentation).
${makeExample(
"Welcome! Eval to get started",
@ -17,26 +17,36 @@ ${makeExample(
true
)}
## What is Topos?
# 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.
Topos is an _algorithmic_ sequencer. Topos is also a _live coding_ environment. To sum it up, think: "_making music in real time through code_". Code used as an expressive medium for musical improvisation! Topos uses small algorithms to represent musical sequences and processes.
${makeExample(
"Small algorithms for direct musical expression",
`
beat(1) :: sound(['kick', 'hat', 'snare', 'hat'].beat(1)).out()
beat(.5) :: sound('jvbass').note(35 + [0,12].beat()).out()
beat([0.5, 0.25].beat(1)) :: sound('east')
.room(.9).speed(flip(4) ? 1 : 0.95).size(0.9).o(2).n($(1)).out()`,
rhythm(.5, 4, 8) :: sound('drum').out()
rhythm(.25, [5, 7].beat(2), 8) :: sound(['hc', 'fikea', 'hat'].pick(1))
.lpf([500, 4000+usine(1/2)*2000]).pan(r(0, 1)).ad(0, [1, .5])
.db(-ir(1,8)).speed([1,[0.5, 2].pick()]).room(0.5).size(3).o(4).out()
beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare')
.n(0).speed([1, 0.5]).o(4).out()`,
false
)}
${makeExample(
"Computer music should be immediate and intuitive",
`beat(.5)::snd('sine')
.delay(0.5).delayt(0.25).delayfb(0.7)
.room(0.8).size(0.8)
.freq(mouseX()).out()`,
`
let chord_prog = [0, 0, 5].bar() // Chord progression
beat(.25)::snd('sine')
.note(chord_prog + [60, 64, 67, 71].mouseX()
+ [-12,0,12].beat(0.25)) // Notes
.fmi([1, 1.5, 2, 4].beat()) // FM synthesis
.ad(0, r(0.1, .25)) // Envelope
.lpf(500 + usine(1/4)*1500) // Filter Envelope
.lpad(4, 0, .125)
.delay(0.5).delayt(0.25).delayfb(0.7) // Delay
.room(0.5).size(8) // Reverb
.out()`,
false
)}
@ -51,9 +61,7 @@ beat(.25) :: sound('sid').note(
false
)}
Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Teletype is/was an open source hardware module for Eurorack synthesizers. While the Teletype was initially born as an hardware module, Topos aims to be a web-browser based software sequencer from the same family! It is a sequencer, a scriptable interface, a companion for algorithmic music-making. Topos wishes to fullfill the same goal as the Teletype, keeping the same spirit alive on the web. It is free, open-source, and made to be shared and used by everyone.
Learn more about live coding on [livecoding.fr](https://livecoding.fr).
Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Teletype is/was an open source hardware module for Eurorack synthesizers. While the Teletype was initially born as an hardware module, Topos aims to be a web-browser based cousin of it! It is a sequencer, a scriptable interface, a companion for algorithmic music-making. Topos wishes to fullfill the same goal as the Teletype, keeping the same spirit alive on the web. It is free, open-source, and made to be shared and used by everyone. Learn more about live coding on [livecoding.fr](https://livecoding.fr).
## Demo Songs

View File

@ -1,12 +1,15 @@
import { key_shortcut } from "../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const shortcuts = (): string => {
export const shortcuts = (app: Editor): string => {
let makeExample = makeExampleFactory(app);
return `
# Keybindings
Topos is made to be controlled entirely with a keyboard. It is recommanded to stop using the mouse as much as possible when you are _live coding_. Here is a list of the most important keybindings:
## Transport
### Transport
| Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------|
@ -17,7 +20,7 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
"Ctrl + S"
)}|Stop and rewind audio playback|
## Moving in the interface
### Moving in the interface
| Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------|
@ -35,18 +38,39 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
)}|Switch to a specific local script|
|Documentation|${key_shortcut("Ctrl + D")}|Open the documentation|
## Evaluating code
### Evaluating code
| Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------|
|Evaluate|${key_shortcut("Ctrl + Enter")}| Evaluate the current script |
|Local Eval|${key_shortcut("Ctrl + F1")} to ${key_shortcut("Ctrl + F9")}|Local File Evaluation|
|Force Eval|${key_shortcut("Ctrl + Shift + Enter")}|Force evaluation of the current script|
|Local Eval|${key_shortcut("Ctrl + F1")} to ${key_shortcut(
"Ctrl + F9"
)}|Local File Evaluation|
|Force Eval|${key_shortcut(
"Ctrl + Shift + Enter"
)}|Force evaluation of the current script|
## Special
### Special
| Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------|
|Vim Mode|${key_shortcut("Ctrl + V")}| Switch between Vim and Normal Mode|
# Keyboard Fill
By pressing the ${key_shortcut(
"Alt"
)} key, you can trigger the <ic>Fill</ic> mode which can either be <ic>true</ic> or <ic>false</ic>. The fill will be set to <ic>true</ic> as long as the key is held. Try pressing ${key_shortcut(
"Alt"
)} when playing this example:
${makeExample(
"Claping twice as fast with fill",
`
beat(fill() ? 1/4 : 1/2)::sound('cp').out()
`,
true
)}
`;
};

View File

@ -0,0 +1,192 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
import pulses from "./pulses.svg";
export const linear_time = (app: Editor): string => {
// @ts-ignore
let makeExample = makeExampleFactory(app);
return `
# Linear time
**Topos** time is flowing just like in your typical computer music program, with _bars_, _beats_, _pulses_ and so on. The transport can be **paused**, **resumed** and/or **stopped**. There are interface buttons to handle these tasks. The tiniest unit of time is the **pulse**. There is a finite number of **pulses** per **beat** (by default, <ic>48</ic> **PPQN**). Beats are passing at a given **BPM** (_beats per minute_). You can change the **BPM** anytime you want. You can also change the granularity of time.
<object type="image/svg+xml" data=${pulses} style="width: 100%; height: auto; background-color: transparent"></object>
### Beats, bar, pulses
**Topos** is using three core values to deal with time:
- **bars**: how many bars have elapsed since the origin of time.
- **beats**: how many beats have elapsed since the beginning of the bar.
- **pulse**: how many pulses have elapsed since the last beat.
There is a tiny widget at the bottom right of the screen showing you the current BPM and the status of the transport. You can turn it on or off in the settings menu.
${makeExample(
"Printing the transport",
`
log(\`\$\{cbar()}\, \$\{cbeat()\}, \$\{cpulse()\}\`)
`,
true
)}
### BPM and PPQN
The base functions to control time are:
- <ic>bpm(number?)</ic> : get or set the current tempo.
- <ic>tempo(number?)</ic> : alias to <ic>bpm</ic>.
- <ic>ppqn(number?)</ic> : get or set the granularity of time
- The function name comes from [PPQN](https://en.wikipedia.org/wiki/Pulses_per_quarter_note) (_pulses per quarter notes_).
### Controlling time
Note that it is preferable to use the keyboard shortcuts to manipulate the transport system. You can also use the <ic>play()</ic>, <ic>pause()</ic> and <ic>stop()</ic> functions. It is generally preferable to program things instead of relying on the interface!
<br>
## Time Primitives
Every script can access the current time by using the following functions:
- <ic>cbar(n: number)</ic>: current bar since the origin of time.
- <ic>cbeat(n: number)</ic>: current beat since the beginning of the bar.
- <ic>ebeat()</ic>: current beat since the origin of time (counting from 1).
- <ic>cpulse()</ic>: current bar since the origin of the beat.
- <ic>ppqn()</ic>: current **PPQN** (see above).
- <ic>bpm()</ic>: current **BPM** (see above).
- <ic>time()</ic>: current wall clock time, the real time of the system.
These values are **extremely useful** to craft more complex syntax or to write musical scores. However, it means that you have to write more to be more precise. There is a tradeoff between _live-codeability_ and dealing with time manually (verbose). Topos is offering high-level functions to deal with that issue, don't worry :)
You can use time primitives 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 time condition
if((cbar() % 4) > 1) {
beat(2) && sound('kick').out()
rarely() && beat(.5) && sound('sd').out()
beat([.5, .25].beat()) && sound('jvbass')
.freq(100 * [2, 1].pick()).dec(2)
.room(0.9).size(0.9).orbit(2).out()
} else {
beat(.5) && sound('hh').out()
beat(2) && sound('cp').out()
beat([.5, .5, .25].beat(.5)) && sound('jvbass')
.freq(100 * [3, 1].pick()).dec(2)
.room(0.9).size(0.9).orbit(2).out()
}
// This is always playing no matter what happens
beat([.5, .5, 1, .25].beat(0.5)) :: sound('shaker').out()
`,
true
)}
## Time Warping
Time generally flows from the past to the future. However, you can manipulate it to jump back and forth. Think about looping a specific part of your current pattern or song or jumping all of the sudden in the future. This is entirely possible thanks to two simple functions: <ic>warp(n: number)</ic> and <ic>beat_warp(n: number)</ic>. They are both very easy to use and very powerful. Let's see how they work.
- <ic>warp(n: number)</ic>: this function jumps to the _n_ tick of the clock. <ic>1</ic> is the first pulsation ever and the number keeps increasing indefinitely. You are most likely currently listening to tick n°<ic>12838123</ic>.
${makeExample(
"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))
.gain(0.5).room(0.9).size(0.9)
.sustain(0.1).out()
beat(1) :: sound('kick').out()
beat(2) :: sound('dr').n(5).out()
flip(3) :: beat([.25,.5].beat(.5)) :: sound('dr')
.n([8,9].pick()).gain([.8,.5,.25,.1,.0].beat(.25)).out()
// Jumping back and forth in time
beat(.25) :: warp([12, 48, 24, 1, 120, 30].pick())
`,
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",
`
// 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()
// Comment me to stop warping!
beat(1) :: beat_warp([2,4,5,10,11].pick())
`,
true
)}
## Transport-based rhythm generators
- <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",
`
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
)}
${makeExample(
"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
)}
- <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",
`
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)
.delay(0.5).delayt(0.25).room(0.9).size(0.9).out()
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
)}
${makeExample(
"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
)}
`;
};

View File

@ -0,0 +1,162 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const long_forms = (app: Editor): string => {
// @ts-ignore
let makeExample = makeExampleFactory(app);
return `
# Long forms
Now you know how to play some basic rhythms but in any case, you are stuck in a loop. It's time to learn how to compose larger/longer musical structures. The functions you are going to learn are all about mastering the flow of time on longer periods. **Read and experiment a lot with the following examples**.
## Using scripts and universes
- **Use the nine local scripts as containers** for sections of your composition. When you start playing with **Topos**, it's easy to forget that there are multiple scripts you can play with. Each script can store a different section or part from your composition. Here is a simple example:
${makeExample(
"Eight bars per section",
`
// Playing each script for 8 bars in succession
script([1,2,3,4].bar(8))
`,
true
)}
You can also give a specific duration to each section using <ic>.dur</ic>:
${makeExample(
"N beats per section",
`
script([1,2,3,4].dur(8, 2, 16, 4))
`,
true
)}
- **Use universes as well**. Transitions between universes are _seamless_, instantaneous. Just switch to different content if you ever hit the limitations of the current _universe_.
## Long-form Functions
- <ic>flip(n: number, ratio: number = 50)</ic>: the <ic>flip</ic> method is a temporal switch. If the value <ic>2</ic> is given, the function will return <ic>true</ic> for two beats and <ic>false</ic> for two beats. There are multiple ways to use it effectively. You can pass an integer or a floating point number.
- <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",
`
flip(4) :: beat(1) :: snd('kick').out()
`,
true
)}
${makeExample(
"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
)}
${makeExample(
"Good old true and false",
`
if (flip(4, 75)) {
beat(1) :: snd('kick').out()
} else {
beat(.5) :: snd('snare').out()
}
`,
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",
`
// Rap God VS Lil Wild -- Adel Faure
if (flip(8)) {
// Playing this part for two bars
beat(1.5)::snd('kick').out()
beat(2)::snd('snare').out()
beat(.5)::snd('hh').out()
} else {
// Now adding some birds and tablas
beat(1.5)::snd('kick').out()
beat(2)::snd('snare').out()
beat(.5)::snd('hh').out()
beat(.5)::snd('tabla').speed([1,2].pick()).end(0.5).out()
beat(2.34)::snd('birds').n(irand(1,10))
.delay(0.5).delaytime(0.5).delayfb(0.25).out()
beat(.5)::snd('diphone').end(0.5).n([1,2,3,4].pick()).out()
}
`,
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(2) ? 'kick' : 'hat').out()
`,
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",
`
let roomy = (n) => n.room(1).size(1).cutoff(500 + usaw(1/8) * 5000);
function a() {
beat(1) && roomy(sound('kick')).out()
beat(.5) && roomy(sound('hat')).out()
}
function b() {
beat(1/4) && roomy(sound('shaker')).out()
}
flipbar(2) && a()
flipbar(3) && b()
`,
true
)}
${makeExample(
"Alternating over four bars",
`
flipbar(2)
? beat(.5) && snd(['kick', 'hh'].beat(1)).out()
: beat(.5) && snd(['east', 'east:2'].beat(1)).out()
`,
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",
`
bpm(150);
// Only play on the third and fourth bar of the cycle.
onbar([3,4], 4)::beat(.25)::snd('hh').out();
// Using JavaScript regular control flow
if (onbar([1,2], 4)) {
beat(.5) :: sometimes() :: sound('east').out()
rhythm(.5, 3, 7) :: snd('kick').out();
rhythm(.5, 1, 7) :: snd('jvbass').out();
rhythm(.5, 2, 7) :: snd('snare').n(5).out();
} else {
beat(.5) :: rarely() :: sound('east').n($(1)).out()
rhythm(.5, 3, 7) :: snd('kick').n(4).out();
rhythm(.5, 1, 7) :: snd('jvbass').n(2).out();
rhythm(.5, 2, 7) :: snd('snare').n(3).out();
}`,
true
)}
`;
};

View File

@ -0,0 +1,72 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const mouse = (app: Editor): string => {
let makeExample = makeExampleFactory(app);
return `
# Mouse
Using the mouse is a fun way to control your code. It's basically an X/Y controller that you don't have to pay for! There are clever actions you can do with the mouse from generating notes to activating scripts conditionally!
## Mouse position
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
)}
<br>
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
)}
## Mouse and Arrays
You can use the mouse to explore the valuesq contained in an Array:
- <ic>mouseX()</ic>: returns a value from a list by splitting the horizontal space of the screen in _n_ sections.
- <ic>mouseY()</ic>: returns a value from a list by splitting the vertical space of the screen in _n_ sections.
${makeExample(
"Taking values out of an Array with the mouse",
`
log([1,2,3,4].mouseX())
log([4,5,6,7].mouseY())
`,
true
)}
`;
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,30 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const synchronisation = (app: Editor): string => {
// @ts-ignore
let makeExample = makeExampleFactory(app);
return `
# Synchronisation
Synchronisation is currently a work in progress. If you are a programmer and if you know something about the topic, please help us to make it work! In the meantime, Topos can already be synchronised but it takes some getting used to.
## MIDI clock Synchronisation
Topos can be controlled from external hadware or software using MIDI clock messages. The options to do so are located in the settings menu. You will need to connect an external MIDI controller or to ready virtual MIDI port.
1) Connect a MIDI input as MIDI Clock in the settings panel.
2) Topos will listen to incoming Clock messages and will use them to estimate the current BPM.
3) Topos will also listen to <ic>Start</ic>, <ic>Stop</ic> and <ic>Continue</ic> messages.
Different MIDI devices can send clock at different resolution, define Clock PPQN in settings to match the resolution of your device.
## Clock nudge
In the settings menu, you will find two ways to _nudge_ the clock, allowing you to finetune synchronisation:
- **clock nudge**: nudge the event clock backwards or forward in time (in milliseconds).
- **audio nudge**: nudge the **synths** and **sampler** forward (in milliseconds).
- note that you need to give some time to the system (2ms+) in order to give it enough time to load and play sounds.
`;
};

View File

@ -1,511 +1,35 @@
import { makeExampleFactory } from "../Documentation";
import { type Editor } from "../main";
import times from "./times.svg";
export const time = (application: Editor): string => {
//@ts-ignore
const makeExample = makeExampleFactory(application);
return `
# Time
Time in Topos is flowing just like on a drum machine. Topos is counting bars, beats and pulses. The time can be **paused**, **resumed** and/or **resetted**. Pulses are flowing at a given **BPM** (_beats per minute_). There are three core values that you will often interact with in one form or another:
- **bars**: how many bars have elapsed since the origin of time.
- **beats**: how many beats have elapsed since the beginning of the bar.
- **pulse**: how many pulses have elapsed since the last beat.
To change the tempo, use the <ic>bpm(number)</ic> function. The transport is controlled by the interface buttons, by the keyboard shortcuts or using the <ic>play()</ic>, <ic>pause()</ic> and <ic>stop()</ic> functions. You will soon learn how to manipulate time to your liking for backtracking, jumping forward, etc... The traditional timeline model has little value when you can script everything.
**Note:** the <ic>bpm(number)</ic> function can serve both for getting and setting the **BPM** value.
## Simple rhythms
- <ic>beat(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ beats.
- <ic>number</ic>: if <ic>number = 1</ic>, the function will return <ic>true</ic> every beat. Lists can be used too.
- <ic>offset</ic>: offset (in beats) to apply. An offset of <ic>0.5</ic> will return true against the beat.
${makeExample(
"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
)}
# What is time?
${makeExample(
"Some sort of ringtone",
`
// Blip generator :)
let blip = (freq) => {
return sound('wt_piano')
.gain(1)
.sustain(0.1)
.freq(freq)
.cutoff(1500)
.lpadsr(4, 0, .25, 0, 0)
};
beat(1) :: blip(200).pan(r(0,1)).vib(0.5).vibmod(2).out();
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
)}
There are two ways to think _intuitively_ about time:
${makeExample(
"Multiple values for creating rhythms", `
beat([1,2.5,3.25], 0)::sound('hat').out()
beat([1,2.25,4])::sound('kick').speed(
[1.25,1].beat(0.5)).out()
onbeat(3, 3.25, 3.125)::sound('shaker').out()
`, false,
)}
- **linear time:** the _arrow_ of time, minutes/days/years passing. Time moving forward. In musical terms, a _piece_, _song_.
- **cyclical time:** seasons, cycles, etc. In musical terms, repetitions, _beats_, _sections_, etc.
- <ic>pulse(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ pulses. A pulse is the tiniest possible rhythmic value.
- <ic>number</ic>: if <ic>number = 1</ic>, the function will return <ic>true</ic> every pulse. Lists can be used too.
- <ic>offset</ic>: offset (in pulses) to apply.
A musician's job is to interweave cyclical and linear time, repetition and continuity.
<object type="image/svg+xml" data=${times} style="width: 100%; height: auto; background-color: transparent"></object>
${makeExample(
"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
)}
${makeExample(
"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
)};
# Time and programming
When you program on a computer, you can adopt a similar mindset to think about time, where time is sometimes cyclic, sometimes linear:
- **linear:** _runtime_, _process time_, _wall clock time_, etc...
- **cyclic:** _recursion_, repeating function, routine, etc.
# Time and Topos
By making music with **Topos**, you will mingle repetitive structures of different scale and deal with composition as well:
- **linear time:** using _bars_, _beats_, _pulses_, transport (_start_/_pause_/_stop_), etc...
- **cyclical time:** euclidean rhythms, beats, pulsed time, etc...
- <ic>bar(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ bars.
- <ic>number</ic>: if <ic>number = 1</ic>, the function will return <ic>true</ic> every bar. Lists can be used too.
- <ic>offset</ic>: offset (in bars) to apply.
${makeExample(
"Four beats per bar: proof",
`
bar(1)::sound('kick').out()
beat(1)::sound('hat').speed(2).out()
`, true
)}
${makeExample(
"Offsettings trigger signal",
`
bar(1)::sound('kick').out()
beat(1)::sound('hat').speed(2).out()
beat(1,0.5)::sound('hat').speed(4).out()
bar(1,0.5)::sound('sn').out()
`, 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",
`
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
)}
${makeExample(
"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
)}
- <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",
`
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)
.delay(0.5).delayt(0.25).room(0.9).size(0.9).out()
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
)}
${makeExample(
"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
)}
## Rhythm generators
We included a bunch of popular rhythm generators in Topos such as the euclidian rhythms algorithms or the one to generate rhythms based on a binary sequence. They all work using _iterators_ that you will gradually learn to use for iterating over lists. Note that they are levaraging <ic>mod(...n:number[])</ic> that you just learned about!
- <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",
`
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')
.gain(r(0.7, 1)).cutoff(1000 + usine(1/8) * 3000)
.n(11).out()
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
)}
${makeExample(
"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
)}
${makeExample(
"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()
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
)}
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",
`
// 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
)}
- <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!",
`
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
)}
- <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!",
`
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
)}
${makeExample(
"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()
binrhythm([.5, .25].beat(1), 30) && snd('wt_granular').n(a)
.cutoff(800).lpadsr(4, 0, 0.125, 0.5, 0.25)
.adsr(0, r(.1, .4), 0, 0).freq([50, 60, 72].beat(4))
.room(1).size(1).out()
`,
true
)}
${makeExample(
"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
)}
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!",
`
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
)}
## Time Warping
Time generally flows from the past to the future. However, it's even cooler when you can manipulate it to your liking by jumping back and forth. Think about looping a specific part of your current pattern or song or jumping all of the sudden in the future. This is entirely possible thanks to two simple functions: <ic>warp(n: number)</ic> and <ic>beat_warp(n: number)</ic>. They are both very easy to use and very powerful. Let's see how they work.
- <ic>warp(n: number)</ic>: this function jumps to the _n_ tick of the clock. <ic>1</ic> is the first pulsation ever and the number keeps increasing indefinitely. You are most likely currently listening to tick n°<ic>12838123</ic>.
${makeExample(
"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))
.gain(0.5).room(0.9).size(0.9)
.sustain(0.1).out()
beat(1) :: sound('kick').out()
beat(2) :: sound('dr').n(5).out()
flip(3) :: beat([.25,.5].beat(.5)) :: sound('dr')
.n([8,9].pick()).gain([.8,.5,.25,.1,.0].beat(.25)).out()
// Jumping back and forth in time
beat(.25) :: warp([12, 48, 24, 1, 120, 30].pick())
`,
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",
`
// 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()
// Comment me to stop warping!
beat(1) :: beat_warp([2,4,5,10,11].pick())
`,
true
)}
## Larger time divisions
Now you know how to play some basic rhythmic music but you are a bit stuck in a one-bar long loop. Let's see how we can think about time flowing on longer periods. The functions you are going to learn now are _very fundamental_ and all the fun comes from mastering them. **Read and experiment a lot with the following examples**.
- <ic>flip(n: number, ratio: number = 50)</ic>: the <ic>flip</ic> method is a temporal switch. If the value <ic>2</ic> is given, the function will return <ic>true</ic> for two beats and <ic>false</ic> for two beats. There are multiple ways to use it effectively. You can pass an integer or a floating point number.
- <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",
`
flip(4) :: beat(1) :: snd('kick').out()
`,
true
)}
${makeExample(
"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
)}
${makeExample(
"Good old true and false",
`
if (flip(4, 75)) {
beat(1) :: snd('kick').out()
} else {
beat(.5) :: snd('snare').out()
}
`,
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",
`
// Rap God VS Lil Wild -- Adel Faure
if (flip(8)) {
// Playing this part for two bars
beat(1.5)::snd('kick').out()
beat(2)::snd('snare').out()
beat(.5)::snd('hh').out()
} else {
// Now adding some birds and tablas
beat(1.5)::snd('kick').out()
beat(2)::snd('snare').out()
beat(.5)::snd('hh').out()
beat(.5)::snd('tabla').speed([1,2].pick()).end(0.5).out()
beat(2.34)::snd('birds').n(irand(1,10))
.delay(0.5).delaytime(0.5).delayfb(0.25).out()
beat(.5)::snd('diphone').end(0.5).n([1,2,3,4].pick()).out()
}
`,
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(2) ? 'kick' : 'hat').out()
`,
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",
`
let roomy = (n) => n.room(1).size(1).cutoff(500 + usaw(1/8) * 5000);
function a() {
beat(1) && roomy(sound('kick')).out()
beat(.5) && roomy(sound('hat')).out()
}
function b() {
beat(1/4) && roomy(sound('shaker')).out()
}
flipbar(2) && a()
flipbar(3) && b()
`,
true
)}
${makeExample(
"Alternating over four bars",
`
flipbar(2)
? beat(.5) && snd(['kick', 'hh'].beat(1)).out()
: beat(.5) && snd(['east', 'east:2'].beat(1)).out()
`,
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",
`
bpm(150);
// Only play on the third and fourth bar of the cycle.
onbar([3,4], 4)::beat(.25)::snd('hh').out();
// Using JavaScript regular control flow
if (onbar([1,2], 4)) {
beat(.5) :: sometimes() :: sound('east').out()
rhythm(.5, 3, 7) :: snd('kick').out();
rhythm(.5, 1, 7) :: snd('jvbass').out();
rhythm(.5, 2, 7) :: snd('snare').n(5).out();
} else {
beat(.5) :: rarely() :: sound('east').n($(1)).out()
rhythm(.5, 3, 7) :: snd('kick').n(4).out();
rhythm(.5, 1, 7) :: snd('jvbass').n(2).out();
rhythm(.5, 2, 7) :: snd('snare').n(3).out();
}`,
true
)}
## What are pulses?
To make a beat, you need a certain number of time grains or **pulses**. The **pulse** is also known as the [PPQN](https://en.wikipedia.org/wiki/Pulses_per_quarter_note). By default, Topos is using a _pulses per quarter note_ of 48. You can change it by using the <ic>ppqn(number)</ic> function. It means that the lowest possible rhythmic value is 1/48 of a quarter note. That's plenty of time already.
**Note:** the <ic>ppqn(number)</ic> function can serve both for getting and setting the **PPQN** value.
## Time Primitives
Every script can access the current time by using the following functions:
- <ic>cbar(n: number)</ic>: returns the current bar since the origin of time.
- <ic>cbeat(n: number)</ic>: returns the current beat since the beginning of the bar.
- <ic>ebeat()</ic>: returns the current beat since the origin of time (counting from 1).
- <ic>cpulse()</ic>: returns the current bar since the origin of the beat.
- <ic>ppqn()</ic>: returns the current **PPQN** (see above).
- <ic>bpm()</ic>: returns the current **BPM** (see above).
- <ic>time()</ic>: returns the current wall clock time, the real time of the system.
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 time condition
if((cbar() % 4) > 1) {
beat(2) && sound('kick').out()
rarely() && beat(.5) && sound('sd').out()
beat([.5, .25].beat()) && sound('jvbass')
.freq(100 * [2, 1].pick()).dec(2)
.room(0.9).size(0.9).orbit(2).out()
} else {
beat(.5) && sound('hh').out()
beat(2) && sound('cp').out()
beat([.5, .5, .25].beat(.5)) && sound('jvbass')
.freq(100 * [3, 1].pick()).dec(2)
.room(0.9).size(0.9).orbit(2).out()
}
// This is always playing no matter what happens
beat([.5, .5, 1, .25].beat(0.5)) :: sound('shaker').out()
`,
true
)}
`;
};

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Do not edit this file with editors other than draw.io -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="501px" height="151px" viewBox="-0.5 -0.5 501 151" class="ge-export-svg-dark" content="&lt;mxfile host=&quot;app.diagrams.net&quot; modified=&quot;2023-11-06T09:00:40.296Z&quot; agent=&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36&quot; etag=&quot;C_foan-BfT9OTEfKR49b&quot; version=&quot;22.0.8&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Page-1&quot; id=&quot;SVK7qbBq6eghmxk_gXBK&quot;&gt;7Zddb5swFIZ/DZeVAAfSXK5Jt2lN1Wq5aHs1efgULBlMjVOgv35mGIhjsqKoH7vIFebF59jnfYxsO2iZVt8EzpNrToA5vksqB60c3/dcd6YejVK3yjwMWiEWlOhOg7ChL9BFanVLCRRGR8k5kzQ3xYhnGUTS0LAQvDS7PXJmjprjGCxhE2Fmq3eUyESrXrgYPnwHGifd0GGgK05x11uXUiSY8HJHQpcOWgrOZdtKqyWwxr3OmDbu64Gv/cwEZHJKQDm/pz8u8mj+MydXv182kK/omc7yjNlWV6wnK+vOAsG3GYEmieugizKhEjY5jpqvpYKutESmTL15qqnTgZBQHZyn11ev1g3wFKSoVZdyMDhwtWnJrreBFrGGGvexQ9mqoSsfd+Hp4e7hHv1aXxU1ytcgrtObcIoLil7eNB8ZVF+ahaWKhYzo5ipiuChoZJphOnfAB98FYqzBV4yxfek0AQxL+myu3DGv9Ai3nKqZ+G7/s7YR+k8N98wu+FZEoGN2l9lemtni33kkFjFIK89fbn3Nx6P0TygtlJ5/JEsPvZLonWGiE8wJEKbS7Cf0STSDE80JEKbSRPPPpRmeaE6AcOyu+dE0FxbNNc0Ai+a8TVOw0B5xKByhaJ0TbY7jO1rnzg7l2fkI5n0X3+7EaG9NyzpiVN0c/lPLzt/PMvU6XGPaFTncBtHlHw==&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(18, 18, 18);"><defs><style type="text/css">svg.ge-export-svg-dark &gt; * { filter: invert(100%) hue-rotate(180deg); }&#xa;svg.ge-export-svg-dark image { filter: invert(100%) hue-rotate(180deg) }</style></defs><g><rect x="0" y="0" width="500" height="150" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><path d="M 10.5 65 L 10.5 55 L 470.5 55 L 470.5 44.5 L 489.5 60 L 470.5 75.5 L 470.5 65 Z" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 10.5 125 L 10.5 115 L 110.5 115 L 110.5 104.5 L 129.5 120 L 110.5 135.5 L 110.5 125 Z" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 130.5 125 L 130.5 115 L 230.5 115 L 230.5 104.5 L 249.5 120 L 230.5 135.5 L 230.5 125 Z" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 250.5 125 L 250.5 115 L 350.5 115 L 350.5 104.5 L 369.5 120 L 350.5 135.5 L 350.5 125 Z" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><path d="M 370.5 125 L 370.5 115 L 470.5 115 L 470.5 104.5 L 489.5 120 L 470.5 135.5 L 470.5 125 Z" fill="none" stroke="rgb(0, 0, 0)" stroke-miterlimit="10" pointer-events="all"/><rect x="10" y="20" width="480" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 478px; height: 1px; padding-top: 30px; margin-left: 11px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Linear time</div></div></div></foreignObject><text x="250" y="34" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Linear time</text></switch></g><rect x="10" y="80" width="480" height="20" fill="rgb(255, 255, 255)" stroke="rgb(0, 0, 0)" pointer-events="all"/><g transform="translate(-0.5 -0.5)"><switch><foreignObject pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility" style="overflow: visible; text-align: left;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 478px; height: 1px; padding-top: 90px; margin-left: 11px;"><div data-drawio-colors="color: rgb(0, 0, 0); " style="box-sizing: border-box; font-size: 0px; text-align: center;"><div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; pointer-events: all; white-space: normal; overflow-wrap: normal;">Cyclical time</div></div></div></foreignObject><text x="250" y="94" fill="rgb(0, 0, 0)" font-family="Helvetica" font-size="12px" text-anchor="middle">Cyclical time</text></switch></g></g><switch><g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/><a transform="translate(0,-5)" xlink:href="https://www.drawio.com/doc/faq/svg-export-text-problems" target="_blank"><text text-anchor="middle" font-size="10px" x="50%" y="100%">Text is not SVG - cannot display</text></a></switch></svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -88,8 +88,9 @@ if (flip(8, 75)) {
.end(1).out()
beat(.5) :: sound('ST01').note(melody).n($(1)).gain(0.4).end(1).out()
beat(1) :: sound('ST02').note(melody).n($(1)).gain(0.4).end(1).out()
}`, `// Race day - Bubobubobubo
bpm(125);
}`,
`// Race day - Bubobubobubo
tempo(125);
beat(.5) :: sound('STB6').n(irand(1,10)).gain(1).out()
rhythm(flip(4) ? 1 : .5, 5, 8) :: sound('kick').out()
rhythm(flip(2) ? .5 : .25, 7, 8) :: sound('click')
@ -113,7 +114,7 @@ rhythm(flip(2) ? .5 : .25, flip(4) ? 8 : 11, 12) :: sound('hat')
.orbit(3).room(0.5).size(0.5).n(0).out()
`,
`// Part-Dieu - Bubobubobubo
bpm(90);
tempo(90);
beat(rarely(12) ? .5 : .25) :: sound('ST22')
.note([30, 30, 30, 31].repeat(8).beat(.5))
.cut(1).n([19, 21].beat(.75))
@ -127,10 +128,11 @@ beat(.5) :: snd('dr')
.gain(1).out()
beat(flip(2) ? 1 : 0.75) :: snd('bd').n(2).out()
beat(4) :: snd('snare').n(5)
.delay(0.5).delayt(bpm() / 60 / 8)
.delay(0.5).delayt(tempo() / 60 / 8)
.delayfb(0.25).out()
`, `// Atarism - Bubobubobubo
bpm(85);
`,
`// Atarism - Bubobubobubo
tempo(85);
let modifier = [.5, 1, 2].beat(8);
let othermod = [1, .5, 4].beat(4);
beat(modifier / 2):: sound('STA9').n([0,2].beat(.5)).vel(0.5).out()
@ -159,7 +161,7 @@ beat(1/4)::snd(['sawtooth', 'triangle', 'square'].beat(1))
.sustain(0.01 + usine(.25) / 10).out()
beat(4)::snd('amencutup').n($(19)).cut(1).orbit(2).pan(rand(0.0,1.0)).out()`,
`// Crazy arpeggios - Bubobubobubo
bpm(110)
tempo(110)
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())
@ -194,7 +196,8 @@ beat([.25,.125, .5].beat(4))::snd('arpy:4')
.cutoff(100 + usine(1/8) * 800).lpadsr(5, 0, [1/8, 1.16].beat(), 0, 0)
.resonance(0.2).gain(0.4).end(0.8).room(0.9).size(0.9).n(3).out();
beat(.5) :: snd('arpy').note([30, 33, 35].repeat(4).beat(1) - [24,12].beat(0.5))
.cutoff(500).lpadsr(8, 0.05, .125, 0, 0).out()`, `// Naïf et agréable -- Bubobubobubo
.cutoff(500).lpadsr(8, 0.05, .125, 0, 0).out()`,
`// Naïf et agréable -- Bubobubobubo
z1('1/8 024!3 035 024 0124').sound('wt_stereo')
.adsr(0, .4, 0.5, .4).gain(0.1)
.lpadsr(4, 0, .2, 0, 0)
@ -211,7 +214,7 @@ z4('1/4 kick kick snare kick').sound().gain(1).cutoff(osci).out()`,
/*
`// Numerology - Bubobubobubo
bpm(130);
tempo(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)",
@ -219,7 +222,7 @@ let mel = [
z0(mel)
.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)
.delay(tempo() / 60 / 9).delayt(0.25).delayfb(0.5)
.fmsus(0.3).fmrel(0.3).rel(rand(0.5,0.8))
.sus(rand(0.05, 0.1)).out();
beat(1) :: sound(flip(2) ? 'kick' : ['sd', 'cp'].beat(3)).out();

View File

@ -26,9 +26,15 @@ import { tryEvaluate } from "./Evaluator";
import showdown from "showdown";
import { makeStringExtensions } from "./extensions/StringExtensions";
import { installInterfaceLogic } from "./InterfaceLogic";
import { installWindowBehaviors } from "./WindowBehavior";
import { installWindowBehaviors, saveBeforeExit } from "./WindowBehavior";
import { drawEmptyBlinkers } from "./AudioVisualisation";
import { makeNumberExtensions } from "./extensions/NumberExtensions";
// @ts-ignore
import { registerSW } from "virtual:pwa-register";
if ("serviceWorker" in navigator) {
registerSW();
}
export class Editor {
// Universes and settings
@ -95,9 +101,13 @@ export class Editor {
this.initializeElements();
this.initializeButtonGroups();
this.initializeHydra();
this.setCanvas(this.interface.feedback as HTMLCanvasElement);
this.setCanvas(this.interface.scope as HTMLCanvasElement);
try {
this.loadHydraSynthAsync();
} catch (error) {
console.log("Couldn't start Hydra: ", error);
}
// ================================================================================
// Loading the universe from local storage
@ -174,6 +184,8 @@ export class Editor {
// Loading universe from URL (if needed)
loadUniverserFromUrl(this);
this.setPeriodicSave(5000);
}
private getBuffer(type: string): any {
@ -462,8 +474,22 @@ export class Editor {
}
}
private loadHydraSynthAsync(): void {
var script = document.createElement("script");
script.src = "https://unpkg.com/hydra-synth";
script.async = true;
script.onload = () => {
console.log("Hydra loaded successfully");
this.initializeHydra();
};
script.onerror = function() {
console.error("Error loading Hydra script");
};
document.head.appendChild(script);
}
private initializeHydra(): void {
//@ts-ignore
// @ts-ignore
this.hydra_backend = new Hydra({
canvas: this.interface.hydra_canvas as HTMLCanvasElement,
detectAudio: false,
@ -486,6 +512,10 @@ export class Editor {
ctx.scale(dpr, dpr);
}
}
private setPeriodicSave(interval: number): void {
setInterval(() => saveBeforeExit(this), interval)
}
}
let app = new Editor();

View File

@ -1,9 +1,72 @@
import { defineConfig } from "vite";
// import * as mdPlugin from 'vite-plugin-markdown';
import { VitePWA } from "vite-plugin-pwa";
import viteCompression from "vite-plugin-compression";
const webManifest = {
name: "Topos",
short_name: "Topos",
description: "Live coding environment",
theme_color: "#ffffff",
icons: [
{
src: "./favicon/android-chrome-192x192.png",
sizes: "192x192",
type: "image/png",
purpose: "any maskable",
},
{
src: "./favicon/android-chrome-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
],
};
const vitePWAconfiguration = {
devOptions: {
enabled: true,
},
workbox: {
sourcemap: true,
cleanupOutdatedCaches: true,
globPatterns: ["**/*.{js,css,html,json,ogg,wav,mp3,ico,png,svg}"],
// Thanks Froos :)
runtimeCaching: [
{
urlPattern: ({ url }) =>
[
/^https:\/\/raw\.githubusercontent\.com\/.*/i,
/^https:\/\/shabda\.ndre\.gr\/.*/i,
].some((regex) => regex.test(url)),
handler: 'CacheFirst',
options: {
cacheName: 'external-samples',
expiration: {
maxEntries: 5000,
maxAgeSeconds: 60 * 60 * 24 * 30, // <== 14 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
includeAssets: [
"favicon/favicon.icon",
"favicon/apple-touch-icon.png",
"mask-icon.svg",
],
manifest: webManifest,
registerType: "autoUpdate",
injectRegister: "auto",
};
export default defineConfig(({ command, mode, ssrBuild }) => {
if (command === "serve") {
return {
plugins: [viteCompression(), VitePWA(vitePWAconfiguration)],
assetsInclude: ["**/*.md"],
server: {
port: 8000,
@ -12,6 +75,7 @@ export default defineConfig(({ command, mode, ssrBuild }) => {
};
} else {
return {
plugins: [viteCompression(), VitePWA(vitePWAconfiguration)],
chunkSizeWarningLimit: 1600 * 2,
build: {
outDir: "dist",

2434
yarn.lock

File diff suppressed because it is too large Load Diff