temp work

This commit is contained in:
2023-12-04 12:18:34 +01:00
75 changed files with 3079 additions and 3357 deletions

View File

@ -3,28 +3,23 @@ name: Build and Push Docker Images
on: on:
push: push:
branches: branches:
- 'main' - "main"
jobs: jobs:
topos: topos:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: Checkout
name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
- - name: Set up QEMU
name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- - name: Set up Docker Buildx
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- - name: Login to Docker Hub
name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- - name: Build and push
name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
push: true push: true

View File

@ -1,2 +1 @@
{ {}
}

View File

@ -2,8 +2,8 @@
<p align="center"> | <p align="center"> |
<a href="https://discord.gg/aPgV7mSFZh">Discord</a> | <a href="https://discord.gg/aPgV7mSFZh">Discord</a> |
<a href="https://raphaelforment.fr/">BuboBubo</a> |  <a href="https://raphaelforment.fr/">BuboBubo</a> |
<a href="about:blank">Amiika</a> | <a href="https://github.com/amiika">Amiika</a> |
<a href="https://toplap.org/">About Live Coding</a> | <a href="https://toplap.org/">About Live Coding</a> |
<br><br> <br><br>
<h2 align="center"><b>Contributors</b></h2> <h2 align="center"><b>Contributors</b></h2>
@ -12,15 +12,32 @@
<img src="https://contrib.rocks/image?repo=bubobubobubobubo/Topos" /> <img src="https://contrib.rocks/image?repo=bubobubobubobubo/Topos" />
</a> </a>
</p> </p>
<p align="center">
<a href='https://ko-fi.com/I2I2RSBHF' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
</p>
</p> </p>
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. ---------------------
Topos is a web based live coding environment. Topos is capable of many things:
- it is a music sequencer made for improvisation and composition alike
- it is a synthesizer capable of additive, substractive, FM and wavetable
synthesis, backed up by a [powerful web based audio engine](https://www.npmjs.com/package/superdough)
- it can also generate video thanks to [Hydra](https://hydra.ojack.xyz/) and
custom oscilloscopes, frequency visualizers and image sequencing capabilities
- it can be used to sequence other MIDI devices (and soon.. OSC!)
- it is made to be used without the need of installing anything, always ready at
[https://topos.live](https://topos.live)
- Topos is also an emulation and personal extension of the [Monome Teletype](https://monome.org/docs/teletype/)
---------------------
![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif) ![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif)
## Disclaimer ## Disclaimer
**Topos** is a fairly young project developed by two part time hobbyists :) Do not expect stable features and/or user support in the initial development stage. Contributors and curious people are welcome! The software is working quite well and we are continuously striving to improve it. **Topos** is still a young project developed by two hobbyists :) Contributions are welcome! We wish to be as inclusive and welcoming as possible to your ideas and suggestions! The software is working quite well and we are continuously striving to improve it.
## Installation (for devs and contributors) ## Installation (for devs and contributors)
@ -46,15 +63,18 @@ The `tauri` version is only here to quickstart future developments but nothing
## Docker ## Docker
### Run the application ### Run the application
`docker run -p 8001:80 yassinsiouda/topos:latest` `docker run -p 8001:80 yassinsiouda/topos:latest`
### Build and run the prod image ### Build and run the prod image
`docker compose --profile prod up` `docker compose --profile prod up`
### Build and run the dev image ### Build and run the dev image
**First installation** **First installation**
First you need to map node_modules to your local machine for your ide intellisense to work properly First you need to map node_modules to your local machine for your ide intellisense to work properly
```bash ```bash
docker compose --profile dev up -d docker compose --profile dev up -d
docker cp topos-dev:/app/node_modules . docker cp topos-dev:/app/node_modules .
@ -62,7 +82,7 @@ docker compose --profile dev down
``` ```
**Then** **Then**
```bash ```bash
docker compose --profile dev up docker compose --profile dev up
``` ```

View File

@ -1,35 +0,0 @@
const WebSocket = require("ws");
const osc = require("osc");
const wss = new WebSocket.Server({ port: 3000 });
console.log("WebSocket server started on ws://localhost:3000");
wss.on("connection", function (ws) {
console.log("> Client connected");
ws.on("message", function (data) {
try {
const message = JSON.parse(data);
sendOscMessage(message);
} catch (error) {
console.error("> Error processing message:", error);
}
});
});
function sendOscMessage(message) {
const udpPort = new osc.UDPPort({
localAddress: "127.0.0.1",
localPort: 3000,
remoteAddress: "127.0.0.1",
remotePort: message.port,
});
udpPort.on("ready", function () {
console.log("> OSC Message:", message);
udpPort.send(message);
udpPort.close();
});
udpPort.open();
}

View File

@ -1,6 +1,6 @@
{ {
"name": "topos-server", "name": "topos-server",
"version": "1.0.0", "version": "0.0.1",
"description": "Topos OSC Server", "description": "Topos OSC Server",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

66
ToposServer/server.js Normal file
View File

@ -0,0 +1,66 @@
const WebSocket = require("ws");
const osc = require("osc");
var pjson = require('./package.json');
// ==========================================================
// SERVER SIDE OSC FORWARDING: WebSocket => OSC
// ==========================================================
// Listening to WebSocket messages
let banner = `
┏┳┓ ┏┓┏┓┏┓
┃ ┏┓┏┓┏┓┏ ┃┃┗┓┃
┻ ┗┛┣┛┗┛┛ ┗┛┗┛┗┛
${pjson.version}\n`
const wss = new WebSocket.Server({ port: 3000 });
console.log(banner)
console.log("Listening to: ws://localhost:3000. Open Topos.\n");
// Setting up for message broadcasting
wss.on("connection", function (ws) {
console.log("> Client connected");
ws.on("message", function (data) {
try {
const message = JSON.parse(data);
sendOscMessage(message);
} catch (error) {
console.error("> Error processing message:", error);
}
});
});
wss.on("error", function (error) {
console.error("> Server error:", error);
})
wss.on("close", function () {
// Close the websocket server
wss.close();
console.log("> Closing websocket server")
});
let udpPort;
function sendOscMessage(message) {
console.log("sendOscMessage")
try {
if (!message.port === udpPort.remotePort) {
udpPort = new osc.UDPPort({
localAddress: "127.0.0.1",
localPort: 3000,
remoteAddress: "127.0.0.1",
remotePort: message.port,
});
udpPort.open();
}
udpPort.on("ready", function () {
console.log("> OSC Message:", message);
udpPort.send(message);
});
} catch (error) {
console.log(error)
}
}

View File

@ -1,4 +1,4 @@
version: '3.7' version: "3.7"
services: services:
topos-dev: topos-dev:
container_name: topos-dev container_name: topos-dev

View File

@ -1,6 +1,7 @@
@font-face { @font-face {
font-family: "IBM Plex Mono"; font-family: "IBM Plex Mono";
src: url("woff2/IBMPlexMono-Regular.woff2") format("woff2"), src:
url("woff2/IBMPlexMono-Regular.woff2") format("woff2"),
url("woff/IBMPlexMono-Regular.woff") format("woff"); url("woff/IBMPlexMono-Regular.woff") format("woff");
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
@ -9,7 +10,8 @@
@font-face { @font-face {
font-family: "IBM PLex Mono"; font-family: "IBM PLex Mono";
src: url("woff2/IBMPlexMono-Italic.woff2") format("woff2"), src:
url("woff2/IBMPlexMono-Italic.woff2") format("woff2"),
url("woff/IBMPlexMono-Italic.woff") format("woff"); url("woff/IBMPlexMono-Italic.woff") format("woff");
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
@ -18,7 +20,8 @@
@font-face { @font-face {
font-family: "IBM PLex Mono"; font-family: "IBM PLex Mono";
src: url("woff2/IBMPlexMono-Bold.woff2") format("woff2"), src:
url("woff2/IBMPlexMono-Bold.woff2") format("woff2"),
url("woff/IBMPlexMono-Bold.woff") format("woff"); url("woff/IBMPlexMono-Bold.woff") format("woff");
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
@ -27,7 +30,8 @@
@font-face { @font-face {
font-family: "IBM Plex Mono"; font-family: "IBM Plex Mono";
src: url("woff2/IBMPlexMono-BoldItalic.woff2") format("woff2"), src:
url("woff2/IBMPlexMono-BoldItalic.woff2") format("woff2"),
url("woff/IBMPlexMono-BoldItalic.woff") format("woff"); url("woff/IBMPlexMono-BoldItalic.woff") format("woff");
font-weight: 700; font-weight: 700;
font-style: italic; font-style: italic;
@ -37,83 +41,84 @@
@font-face { @font-face {
font-family: "Comic Mono"; font-family: "Comic Mono";
font-weight: normal; font-weight: normal;
src: url(./woff/ComicMono.woff) format("woff"), src:
url(./woff/ComicMono.woff) format("woff"),
url(./woff2/ComicMono.woff2) format("wooff2"); url(./woff2/ComicMono.woff2) format("wooff2");
} }
@font-face { @font-face {
font-family: "Comic Mono"; font-family: "Comic Mono";
font-weight: bold; font-weight: bold;
src: url(./woff/ComicMono-Bold.woff) format("woff"), src:
url(./woff/ComicMono-Bold.woff2) format("woff2"), url(./woff/ComicMono-Bold.woff) format("woff"),
url(./woff/ComicMono-Bold.woff2) format("woff2");
} }
@font-face { @font-face {
font-family: 'jgs7'; font-family: "jgs7";
src: url('./woff2/jgs7.woff2') format('woff2'), src:
url('./woff/jgs7.woff') format('woff'); url("./woff2/jgs7.woff2") format("woff2"),
url("./woff/jgs7.woff") format("woff");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'jgs5'; font-family: "jgs5";
src: url('./woff2/jgs5.woff2') format('woff2'), src:
url('./woff/jgs5.woff') format('woff'); url("./woff2/jgs5.woff2") format("woff2"),
url("./woff/jgs5.woff") format("woff");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'jgs9'; font-family: "jgs9";
src: url('./woff2/jgs9.woff2') format('woff2'), src:
url('./woff/jgs9.woff') format('woff'); url("./woff2/jgs9.woff2") format("woff2"),
font-weight: normal; url("./woff/jgs9.woff") format("woff");
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'jgs_vecto';
src: url('./woff2/jgs_vecto.woff2') format('woff2');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'Steps Mono'; font-family: "jgs_vecto";
src: url('./woff2/Steps-Mono.woff2') format('woff2'); src: url("./woff2/jgs_vecto.woff2") format("woff2");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'Steps Mono Thin'; font-family: "Steps Mono";
src: url('./woff2/Steps-Mono-Thin.woff2') format('woff2'); src: url("./woff2/Steps-Mono.woff2") format("woff2");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: 'Jet Brains'; font-family: "Steps Mono Thin";
src: url('./woff2/JetBrainsMono-Regular.woff2') format('woff2'); src: url("./woff2/Steps-Mono-Thin.woff2") format("woff2");
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face {
font-family: "Jet Brains";
src: url("./woff2/JetBrainsMono-Regular.woff2") format("woff2");
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face { @font-face {
font-family: 'Jet Brains'; font-family: "Jet Brains";
src: url('./woff2/JetBrainsMono-Bold.woff2') format('woff2'); src: url("./woff2/JetBrainsMono-Bold.woff2") format("woff2");
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;

View File

@ -71,7 +71,7 @@
<!-- The header is hidden on smaller devices --> <!-- The header is hidden on smaller devices -->
<header class="py-0 block text-white bg-neutral-900"> <header class="py-0 block text-white bg-neutral-900">
<div class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center"> <div id="topbar" class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center">
<a class="flex title-font font-medium items-center text-black mb-0"> <a class="flex title-font font-medium items-center text-black mb-0">
<img id="topos-logo" src="topos_frog.svg" class="w-12 h-12 text-black p-2 bg-white rounded-full" alt="Topos Frog Logo" /> <img id="topos-logo" src="topos_frog.svg" class="w-12 h-12 text-black p-2 bg-white rounded-full" alt="Topos Frog Logo" />
<input id="universe-viewer" class="hidden bg-transparent xl:block ml-4 text-2xl text-white placeholder-white" id="renamer" type="text" placeholder="Topos"> <input id="universe-viewer" class="hidden bg-transparent xl:block ml-4 text-2xl text-white placeholder-white" id="renamer" type="text" placeholder="Topos">
@ -128,7 +128,7 @@
</header> </header>
<div id="documentation" class="hidden"> <div id="documentation" class="hidden">
<div id="documentation-page" class="flex flex-row"> <div id="documentation-page" class="flex flex-row bg-transparent">
<aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-neutral-900 text-white"> <aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-neutral-900 text-white">
<nav class="text-xl sm:text-sm overflow-y-scroll mb-24"> <nav class="text-xl sm:text-sm overflow-y-scroll mb-24">
<details class="" open=true> <details class="" open=true>
@ -209,15 +209,18 @@
<details class="" open=true> <details class="" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">Community</summary> <summary class="font-semibold lg:text-xl text-orange-300">Community</summary>
<form action="https://github.com/Bubobubobubobubo/topos"> <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" /> <input rel="noopener noreferrer" id="github_link" 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>
<form action="https://discord.gg/6T67DqBNNT"> <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" /> <input rel="noopener noreferrer" id="discord_link" 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>
<form action="https://ko-fi.com/raphaelbubo">
<input rel="noopener noreferrer" id="support_link" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Support" />
</form> </form>
</details> </details>
</nav> </nav>
</aside> </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"> <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 bg-transparent"></div>
</div> </div>
</div> </div>
</div> </div>
@ -429,7 +432,7 @@
<div class="flex flex-row max-h-fit"> <div class="flex flex-row max-h-fit">
<!-- This is a lateral bar that will inherit the header buttons if the window is too small. --> <!-- This is a lateral bar that will inherit the header buttons if the window is too small. -->
<aside class=" <aside id="sidebar" class="
flex flex-col items-center w-14 flex flex-col items-center w-14
h-screen py-2 border-r h-screen py-2 border-r
rtl:border-l max-h-fit rtl:border-l max-h-fit

View File

@ -43,7 +43,8 @@
"tone": "^14.8.49", "tone": "^14.8.49",
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"vite-plugin-markdown": "^2.1.0", "vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.39", "zifferjs": "^0.0.44",
"zyklus": "^0.1.4",
"zzfx": "^1.2.0" "zzfx": "^1.2.0"
} }
} }

View File

@ -3,4 +3,4 @@ export default {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},
}, },
} };

View File

@ -27,7 +27,8 @@ import {
} from "superdough"; } from "superdough";
import { Speaker } from "./extensions/StringExtensions"; import { Speaker } from "./extensions/StringExtensions";
import { getScaleNotes } from "zifferjs"; import { getScaleNotes } from "zifferjs";
import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation"; import { OscilloscopeConfig } from "./Visuals/Oscilloscope";
import { blinkScript } from "./Visuals/Blinkers";
import { SkipEvent } from "./classes/SkipEvent"; import { SkipEvent } from "./classes/SkipEvent";
import { AbstractEvent, EventOperation } from "./classes/AbstractEvents"; import { AbstractEvent, EventOperation } from "./classes/AbstractEvents";
import drums from "./tidal-drum-machines.json"; import drums from "./tidal-drum-machines.json";
@ -41,16 +42,31 @@ interface ControlChange {
export async function loadSamples() { export async function loadSamples() {
return Promise.all([ return Promise.all([
initAudioOnFirstClick(), initAudioOnFirstClick(),
samples("github:tidalcycles/Dirt-Samples/master", undefined, { tag: "Tidal" }).then(() => samples("github:tidalcycles/Dirt-Samples/master", undefined, {
registerSynthSounds() tag: "Tidal",
), }).then(() => registerSynthSounds()),
registerZZFXSounds(), registerZZFXSounds(),
samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", { tag: "Machines" }), samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", {
samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, { tag: "FoxDot" }), tag: "Machines",
samples("github:Bubobubobubobubo/Dough-Samples/main", undefined, { tag: "Pack" }), }),
samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, { tag: "Amiga" }), samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, {
samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, { tag: "Amen" }), tag: "FoxDot",
samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, { tag: "Waveforms" }), }),
samples("github:Bubobubobubobubo/Dough-Samples/main", undefined, {
tag: "Pack",
}),
samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, {
tag: "Amiga",
}),
samples("github:Bubobubobubobubo/Dough-Juj/main", undefined, {
tag: "Juliette",
}),
samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, {
tag: "Amen",
}),
samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, {
tag: "Waveforms",
}),
]); ]);
} }
@ -74,6 +90,7 @@ export class UserAPI {
private printTimeoutID: number = 0; private printTimeoutID: number = 0;
public MidiConnection: MidiConnection; public MidiConnection: MidiConnection;
public scale_aid: string | number | undefined = undefined; public scale_aid: string | number | undefined = undefined;
public hydra: any;
load: samples; load: samples;
constructor(public app: Editor) { constructor(public app: Editor) {
@ -95,7 +112,7 @@ export class UserAPI {
} }
this.app.settings.saveApplicationToLocalStorage( this.app.settings.saveApplicationToLocalStorage(
this.app.universes, this.app.universes,
this.app.settings this.app.settings,
); );
this.app.updateKnownUniversesView(); this.app.updateKnownUniversesView();
}; };
@ -187,7 +204,7 @@ export class UserAPI {
// @ts-ignore // @ts-ignore
this.errorTimeoutID = setTimeout( this.errorTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"), () => this.app.interface.error_line.classList.add("hidden"),
2000 2000,
); );
}; };
@ -201,7 +218,7 @@ export class UserAPI {
// @ts-ignore // @ts-ignore
this.printTimeoutID = setTimeout( this.printTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"), () => this.app.interface.error_line.classList.add("hidden"),
4000 4000,
); );
}; };
@ -252,7 +269,7 @@ export class UserAPI {
*/ */
this.app.clock.tick = beat * this.app.clock.ppqn; this.app.clock.tick = beat * this.app.clock.ppqn;
this.app.clock.time_position = this.app.clock.convertTicksToTimeposition( this.app.clock.time_position = this.app.clock.convertTicksToTimeposition(
beat * this.app.clock.ppqn beat * this.app.clock.ppqn,
); );
}; };
@ -309,7 +326,7 @@ export class UserAPI {
blinkScript(this.app, "local", arg); blinkScript(this.app, "local", arg);
tryEvaluate( tryEvaluate(
this.app, this.app,
this.app.universes[this.app.selected_universe].locals[arg] this.app.universes[this.app.selected_universe].locals[arg],
); );
} }
}); });
@ -356,7 +373,7 @@ export class UserAPI {
delete this.app.universes[universe]; delete this.app.universes[universe];
this.app.settings.saveApplicationToLocalStorage( this.app.settings.saveApplicationToLocalStorage(
this.app.universes, this.app.universes,
this.app.settings this.app.settings,
); );
this.app.updateKnownUniversesView(); this.app.updateKnownUniversesView();
}; };
@ -372,7 +389,7 @@ export class UserAPI {
}; };
this.app.settings.saveApplicationToLocalStorage( this.app.settings.saveApplicationToLocalStorage(
this.app.universes, this.app.universes,
this.app.settings this.app.settings,
); );
} }
this.app.selected_universe = "Default"; this.app.selected_universe = "Default";
@ -409,7 +426,7 @@ export class UserAPI {
value: number | number[] = 60, value: number | number[] = 60,
velocity?: number | number[], velocity?: number | number[],
channel?: number | number[], channel?: number | number[],
port?: number | string | number[] | string[] port?: number | string | number[] | string[],
): MidiEvent => { ): MidiEvent => {
/** /**
* Sends a MIDI note to the current MIDI output. * Sends a MIDI note to the current MIDI output.
@ -484,7 +501,7 @@ export class UserAPI {
}; };
public active_note_events = ( public active_note_events = (
channel?: number channel?: number,
): MidiNoteEvent[] | undefined => { ): MidiNoteEvent[] | undefined => {
/** /**
* @returns A list of currently active MIDI notes * @returns A list of currently active MIDI notes
@ -621,7 +638,7 @@ export class UserAPI {
scale: number | string, scale: number | string,
channel: number = 0, channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0, port: number | string = this.MidiConnection.currentOutputIndex || 0,
soundOff: boolean = false soundOff: boolean = false,
): void => { ): void => {
/** /**
* Sends given scale to midi output for visual aid * Sends given scale to midi output for visual aid
@ -645,7 +662,7 @@ export class UserAPI {
// @ts-ignore // @ts-ignore
scale: number | string = 0, scale: number | string = 0,
channel: number = 0, channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0 port: number | string = this.MidiConnection.currentOutputIndex || 0,
): void => { ): void => {
/** /**
* Hides all notes by sending all notes off to midi output * Hides all notes by sending all notes off to midi output
@ -660,7 +677,7 @@ export class UserAPI {
midi_notes_off = ( midi_notes_off = (
channel: number = 0, channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0 port: number | string = this.MidiConnection.currentOutputIndex || 0,
): void => { ): void => {
/** /**
* Sends all notes off to midi output * Sends all notes off to midi output
@ -670,7 +687,7 @@ export class UserAPI {
midi_sound_off = ( midi_sound_off = (
channel: number = 0, channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0 port: number | string = this.MidiConnection.currentOutputIndex || 0,
): void => { ): void => {
/** /**
* Sends all sound off to midi output * Sends all sound off to midi output
@ -697,7 +714,7 @@ export class UserAPI {
public z = ( public z = (
input: string | Generator<number>, input: string | Generator<number>,
options: InputOptions = {}, options: InputOptions = {},
id: number | string = "" id: number | string = "",
): Player => { ): Player => {
const zid = "z" + id.toString(); const zid = "z" + id.toString();
const key = id === "" ? this.generateCacheKey(input, options) : zid; const key = id === "" ? this.generateCacheKey(input, options) : zid;
@ -774,7 +791,7 @@ export class UserAPI {
public counter = ( public counter = (
name: string | number, name: string | number,
limit?: number, limit?: number,
step?: number step?: number,
): number => { ): number => {
/** /**
* Returns the current value of a counter, and increments it by the step value. * Returns the current value of a counter, and increments it by the step value.
@ -1282,7 +1299,7 @@ export class UserAPI {
(value) => (value) =>
(this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) % (this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) %
Math.floor(value * this.ppqn()) === Math.floor(value * this.ppqn()) ===
0 0,
); );
return results.some((value) => value === true); return results.some((value) => value === true);
}; };
@ -1302,7 +1319,7 @@ export class UserAPI {
(value) => (value) =>
(this.app.clock.pulses_since_origin - nudgeInPulses) % (this.app.clock.pulses_since_origin - nudgeInPulses) %
Math.floor(value * barLength) === Math.floor(value * barLength) ===
0 0,
); );
return results.some((value) => value === true); return results.some((value) => value === true);
}; };
@ -1317,7 +1334,7 @@ export class UserAPI {
*/ */
const nArray = Array.isArray(n) ? n : [n]; const nArray = Array.isArray(n) ? n : [n];
const results: boolean[] = nArray.map( const results: boolean[] = nArray.map(
(value) => (this.app.clock.pulses_since_origin - nudge) % value === 0 (value) => (this.app.clock.pulses_since_origin - nudge) % value === 0,
); );
return results.some((value) => value === true); return results.some((value) => value === true);
}; };
@ -1326,7 +1343,7 @@ export class UserAPI {
public tick = (tick: number | number[], offset: number = 0): boolean => { public tick = (tick: number | number[], offset: number = 0): boolean => {
const nArray = Array.isArray(tick) ? tick : [tick]; const nArray = Array.isArray(tick) ? tick : [tick];
const results: boolean[] = nArray.map( const results: boolean[] = nArray.map(
(value) => this.app.clock.time_position.pulse === value + offset (value) => this.app.clock.time_position.pulse === value + offset,
); );
return results.some((value) => value === true); return results.some((value) => value === true);
}; };
@ -1375,7 +1392,7 @@ export class UserAPI {
public onbar = ( public onbar = (
bars: number[] | number, bars: number[] | number,
n: number = this.app.clock.time_signature[0] n: number = this.app.clock.time_signature[0],
): boolean => { ): boolean => {
let current_bar = (this.app.clock.time_position.bar % n) + 1; let current_bar = (this.app.clock.time_position.bar % n) + 1;
return typeof bars === "number" return typeof bars === "number"
@ -1403,7 +1420,7 @@ export class UserAPI {
if (decimal_part <= 0) if (decimal_part <= 0)
decimal_part = decimal_part + this.ppqn() * this.nominator(); decimal_part = decimal_part + this.ppqn() * this.nominator();
final_pulses.push( final_pulses.push(
integral_part === this.cbeat() && this.cpulse() === decimal_part integral_part === this.cbeat() && this.cpulse() === decimal_part,
); );
}); });
return final_pulses.some((p) => p == true); return final_pulses.some((p) => p == true);
@ -1485,7 +1502,7 @@ export class UserAPI {
iterator: number, iterator: number,
pulses: number, pulses: number,
length: number, length: number,
rotate: number = 0 rotate: number = 0,
): boolean => { ): boolean => {
/** /**
* Returns a euclidean cycle of size length, with n pulses, rotated or not. * Returns a euclidean cycle of size length, with n pulses, rotated or not.
@ -1504,7 +1521,7 @@ export class UserAPI {
div: number, div: number,
pulses: number, pulses: number,
length: number, length: number,
rotate: number = 0 rotate: number = 0,
): boolean => { ): boolean => {
return ( return (
this.beat(div) && this._euclidean_cycle(pulses, length, rotate).beat(div) this.beat(div) && this._euclidean_cycle(pulses, length, rotate).beat(div)
@ -1514,7 +1531,7 @@ export class UserAPI {
_euclidean_cycle( _euclidean_cycle(
pulses: number, pulses: number,
length: number, length: number,
rotate: number = 0 rotate: number = 0,
): boolean[] { ): boolean[] {
if (pulses == length) return Array.from({ length }, () => true); if (pulses == length) return Array.from({ length }, () => true);
function startsDescent(list: number[], i: number): boolean { function startsDescent(list: number[], i: number): boolean {
@ -1525,7 +1542,7 @@ export class UserAPI {
if (pulses >= length) return [true]; if (pulses >= length) return [true];
const resList = Array.from( const resList = Array.from(
{ length }, { length },
(_, i) => (((pulses * (i - 1)) % length) + length) % length (_, i) => (((pulses * (i - 1)) % length) + length) % length,
); );
let cycle = resList.map((_, i) => startsDescent(resList, i)); let cycle = resList.map((_, i) => startsDescent(resList, i));
if (rotate != 0) { if (rotate != 0) {
@ -1564,7 +1581,9 @@ export class UserAPI {
// Low Frequency Oscillators // Low Frequency Oscillators
// ============================================================= // =============================================================
line = (start: number, end: number, step: number = 1): number[] => { public range = (v: number, a: number, b: number): number => v * (b - a) + a;
public line = (start: number, end: number, step: number = 1): number[] => {
/** /**
* Returns an array of values between start and end, with a given step. * Returns an array of values between start and end, with a given step.
* *
@ -1586,7 +1605,11 @@ export class UserAPI {
return result; return result;
}; };
sine = (freq: number = 1, times: number = 1, offset: number = 0): number => { public sine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
/** /**
* Returns a sine wave between -1 and 1. * Returns a sine wave between -1 and 1.
* *
@ -1600,7 +1623,11 @@ export class UserAPI {
); );
}; };
usine = (freq: number = 1, times: number = 1, offset: number = 0): number => { public usine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
/** /**
* Returns a sine wave between 0 and 1. * Returns a sine wave between 0 and 1.
* *
@ -1644,7 +1671,7 @@ export class UserAPI {
triangle = ( triangle = (
freq: number = 1, freq: number = 1,
times: number = 1, times: number = 1,
offset: number = 0 offset: number = 0,
): number => { ): number => {
/** /**
* Returns a triangle wave between -1 and 1. * Returns a triangle wave between -1 and 1.
@ -1661,7 +1688,7 @@ export class UserAPI {
utriangle = ( utriangle = (
freq: number = 1, freq: number = 1,
times: number = 1, times: number = 1,
offset: number = 0 offset: number = 0,
): number => { ): number => {
/** /**
* Returns a triangle wave between 0 and 1. * Returns a triangle wave between 0 and 1.
@ -1678,7 +1705,7 @@ export class UserAPI {
freq: number = 1, freq: number = 1,
times: number = 1, times: number = 1,
offset: number = 0, offset: number = 0,
duty: number = 0.5 duty: number = 0.5,
): number => { ): number => {
/** /**
* Returns a square wave with a specified duty cycle between -1 and 1. * Returns a square wave with a specified duty cycle between -1 and 1.
@ -1698,7 +1725,7 @@ export class UserAPI {
freq: number = 1, freq: number = 1,
times: number = 1, times: number = 1,
offset: number = 0, offset: number = 0,
duty: number = 0.5 duty: number = 0.5,
): number => { ): number => {
/** /**
* Returns a square wave between 0 and 1. * Returns a square wave between 0 and 1.
@ -1758,23 +1785,11 @@ export class UserAPI {
*/ */
const sum = values.reduce( const sum = values.reduce(
(accumulator, currentValue) => accumulator + currentValue, (accumulator, currentValue) => accumulator + currentValue,
0 0,
); );
return sum / values.length; return sum / values.length;
}; };
public range = (
inputY: number,
yMin: number,
yMax: number,
xMin: number,
xMax: number
): number => {
const percent = (inputY - yMin) / (yMax - yMin);
const outputX = percent * (xMax - xMin) + xMin;
return outputX;
};
limit = (value: number, min: number, max: number): number => { limit = (value: number, min: number, max: number): number => {
/** /**
* Limits a value between a minimum and a maximum. * Limits a value between a minimum and a maximum.
@ -1798,7 +1813,7 @@ export class UserAPI {
lang: string = "en-US", lang: string = "en-US",
voice: number = 0, voice: number = 0,
rate: number = 1, rate: number = 1,
pitch: number = 1 pitch: number = 1,
): void => { ): void => {
/* /*
* Speaks the given text using the browser's speech synthesis API. * Speaks the given text using the browser's speech synthesis API.
@ -1873,7 +1888,7 @@ export class UserAPI {
const elements = args.slice(1); // Get the rest of the arguments as an array const elements = args.slice(1); // Get the rest of the arguments as an array
const timepos = this.app.clock.pulses_since_origin; const timepos = this.app.clock.pulses_since_origin;
const slice_count = Math.floor( const slice_count = Math.floor(
timepos / Math.floor(chunk_size * this.ppqn()) timepos / Math.floor(chunk_size * this.ppqn()),
); );
return elements[slice_count % elements.length]; return elements[slice_count % elements.length];
}; };
@ -1901,10 +1916,13 @@ export class UserAPI {
// ============================================================= // =============================================================
register = (name: string, operation: EventOperation<AbstractEvent>): void => { register = (name: string, operation: EventOperation<AbstractEvent>): void => {
AbstractEvent.prototype[name] = function(this: AbstractEvent, ...args: any[]) { AbstractEvent.prototype[name] = function(
this: AbstractEvent,
...args: any[]
) {
return operation(this, ...args); return operation(this, ...args);
}; };
} };
public shuffle = <T>(array: T[]): T[] => { public shuffle = <T>(array: T[]): T[] => {
/** /**
@ -2005,7 +2023,7 @@ export class UserAPI {
".cm-comment": { ".cm-comment": {
fontFamily: commentFont, fontFamily: commentFont,
}, },
}) }),
), ),
}); });
}; };
@ -2081,19 +2099,6 @@ export class UserAPI {
// Transport functions // 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 tempo = (n?: number): number => { public tempo = (n?: number): number => {
/** /**
* Sets or returns the current bpm. * Sets or returns the current bpm.

View File

@ -1,7 +1,11 @@
// @ts-ignore
import { TransportNode } from "./TransportNode";
import TransportProcessor from "./TransportProcessor?worker&url";
import { Editor } from "./main"; import { Editor } from "./main";
import { tryEvaluate } from "./Evaluator";
// @ts-ignore
import { getAudioContext } from "superdough";
// @ts-ignore
import "zyklus";
const zeroPad = (num: number, places: number) =>
String(num).padStart(places, "0");
export interface TimePosition { export interface TimePosition {
/** /**
@ -18,62 +22,92 @@ export interface TimePosition {
export class Clock { export class Clock {
/** /**
* The Clock Class is responsible for keeping track of the current time.
* It is also responsible for starting and stopping the Clock TransportNode.
* *
* @param app - The main application instance * @param app - main application instance
* @param ctx - The current AudioContext used by app * @param clock - zyklus clock
* @param transportNode - The TransportNode helper * @param ctx - current AudioContext used by app
* @param bpm - The current beats per minute value * @param bpm - current beats per minute value
* @param time_signature - The time signature * @param time_signature - time signature
* @param time_position - The current time position * @param time_position - current time position
* @param ppqn - The pulses per quarter note * @param ppqn - pulses per quarter note
* @param tick - The current tick since origin * @param tick - current tick since origin
* @param running - Is the clock running? * @param running - Is the clock running?
* @param lastPauseTime - The last time the clock was paused
* @param lastPlayPressTime - The last time the clock was started
* @param totalPauseTime - The total time the clock has been paused / stopped
*/ */
private _bpm: number;
private _ppqn: number;
clock: any;
ctx: AudioContext; ctx: AudioContext;
logicalTime: number; logicalTime: number;
transportNode: TransportNode | null;
private _bpm: number;
time_signature: number[]; time_signature: number[];
time_position: TimePosition; time_position: TimePosition;
private _ppqn: number;
tick: number; tick: number;
running: boolean; running: boolean;
lastPauseTime: number; timeviewer: HTMLElement;
lastPlayPressTime: number; deadline: number;
totalPauseTime: number;
constructor(public app: Editor, ctx: AudioContext) { constructor(
public app: Editor,
ctx: AudioContext,
) {
this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.time_signature = [4, 4]; this.time_signature = [4, 4];
this.logicalTime = 0; this.logicalTime = 0;
this.tick = 0; this.tick = 0;
this._bpm = 120; this._bpm = 120;
this._ppqn = 48; this._ppqn = 48;
this.transportNode = null;
this.ctx = ctx; this.ctx = ctx;
this.running = true; this.running = true;
this.lastPauseTime = 0; this.deadline = 0;
this.lastPlayPressTime = 0; this.timeviewer = document.getElementById("timeviewer")!;
this.totalPauseTime = 0; this.clock = getAudioContext().createClock(
ctx.audioWorklet this.clockCallback,
.addModule(TransportProcessor) this.pulse_duration,
.then((e) => { );
this.transportNode = new TransportNode(ctx, {}, this.app);
this.transportNode.connect(ctx.destination);
return e;
})
.catch((e) => {
console.log("Error loading TransportProcessor.js:", e);
});
} }
// @ts-ignore
clockCallback = (time: number, duration: number, tick: number) => {
/**
* Callback function for the zyklus clock. Updates the clock info and sends a
* MIDI clock message if the setting is enabled. Also evaluates the global buffer.
*
* @param time - precise AudioContext time when the tick should happen
* @param duration - seconds between each tick
* @param tick - count of the current tick
*/
let deadline = time - getAudioContext().currentTime;
this.deadline = deadline;
this.tick = tick;
if (this.app.clock.running) {
if (this.app.settings.send_clock) {
this.app.api.MidiConnection.sendMidiClock();
}
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick,
);
this.app.clock.time_position = futureTimeStamp;
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1
} / ${this.app.clock.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
}
// Implement TransportNode clock callback and update clock info with it
};
convertTicksToTimeposition(ticks: number): TimePosition { convertTicksToTimeposition(ticks: number): TimePosition {
/**
* Converts ticks to a time position.
*
* @param ticks - ticks to convert
* @returns TimePosition
*/
const beatsPerBar = this.app.clock.time_signature[0]; const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn; const ppqnPosition = ticks % this.app.clock.ppqn;
const beatNumber = Math.floor(ticks / this.app.clock.ppqn); const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
@ -84,10 +118,9 @@ export class Clock {
get ticks_before_new_bar(): number { get ticks_before_new_bar(): number {
/** /**
* This function returns the number of ticks separating the current moment * Calculates the number of ticks before the next bar.
* from the beginning of the next bar.
* *
* @returns number of ticks until next bar * @returns number - ticks before the next bar
*/ */
const ticskMissingFromBeat = this.ppqn - this.time_position.pulse; const ticskMissingFromBeat = this.ppqn - this.time_position.pulse;
const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat; const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat;
@ -96,10 +129,9 @@ export class Clock {
get next_beat_in_ticks(): number { get next_beat_in_ticks(): number {
/** /**
* This function returns the number of ticks separating the current moment * Calculates the number of ticks before the next beat.
* from the beginning of the next beat.
* *
* @returns number of ticks until next beat * @returns number - ticks before the next beat
*/ */
return this.app.clock.pulses_since_origin + this.time_position.pulse; return this.app.clock.pulses_since_origin + this.time_position.pulse;
} }
@ -107,6 +139,8 @@ export class Clock {
get beats_per_bar(): number { get beats_per_bar(): number {
/** /**
* Returns the number of beats per bar. * Returns the number of beats per bar.
*
* @returns number - beats per bar
*/ */
return this.time_signature[0]; return this.time_signature[0];
} }
@ -115,7 +149,7 @@ export class Clock {
/** /**
* Returns the number of beats since the origin. * Returns the number of beats since the origin.
* *
* @returns number of beats since origin * @returns number - beats since the origin
*/ */
return Math.floor(this.tick / this.ppqn); return Math.floor(this.tick / this.ppqn);
} }
@ -124,7 +158,7 @@ export class Clock {
/** /**
* Returns the number of pulses since the origin. * Returns the number of pulses since the origin.
* *
* @returns number of pulses since origin * @returns number - pulses since the origin
*/ */
return this.tick; return this.tick;
} }
@ -132,119 +166,112 @@ export class Clock {
get pulse_duration(): number { get pulse_duration(): number {
/** /**
* Returns the duration of a pulse in seconds. * Returns the duration of a pulse in seconds.
* @returns number - duration of a pulse in seconds
*/ */
return 60 / this.bpm / this.ppqn; return 60 / this.bpm / this.ppqn;
} }
public pulse_duration_at_bpm(bpm: number = this.bpm): number { public pulse_duration_at_bpm(bpm: number = this.bpm): number {
/** /**
* Returns the duration of a pulse in seconds at a specific bpm. * Returns the duration of a pulse in seconds at a given bpm.
*
* @param bpm - bpm to calculate the pulse duration for
* @returns number - duration of a pulse in seconds
*/ */
return 60 / bpm / this.ppqn; return 60 / bpm / this.ppqn;
} }
get bpm(): number { get bpm(): number {
/**
* Returns the current bpm.
* @returns number - current bpm
*/
return this._bpm; return this._bpm;
} }
set nudge(nudge: number) { get tickDuration(): number {
this.transportNode?.setNudge(nudge); /**
* Returns the duration of a tick in seconds.
* @returns number - duration of a tick in seconds
*/
return 1 / this.ppqn;
} }
set bpm(bpm: number) { set bpm(bpm: number) {
/**
* Sets the bpm.
* @param bpm - bpm to set
*/
if (bpm > 0 && this._bpm !== bpm) { if (bpm > 0 && this._bpm !== bpm) {
this.transportNode?.setBPM(bpm);
this._bpm = bpm; this._bpm = bpm;
this.logicalTime = this.realTime; this.clock.setDuration(() => (this.tickDuration * 60) / this.bpm);
} }
} }
get ppqn(): number { get ppqn(): number {
/**
* Returns the current ppqn.
* @returns number - current ppqn
*/
return this._ppqn; return this._ppqn;
} }
get realTime(): number {
return this.app.audioContext.currentTime - this.totalPauseTime;
}
get deviation(): number {
return Math.abs(this.logicalTime - this.realTime);
}
set ppqn(ppqn: number) { set ppqn(ppqn: number) {
/**
* Sets the ppqn.
* @param ppqn - ppqn to set
* @returns number - current ppqn
*/
if (ppqn > 0 && this._ppqn !== ppqn) { if (ppqn > 0 && this._ppqn !== ppqn) {
this._ppqn = ppqn; this._ppqn = ppqn;
this.transportNode?.setPPQN(ppqn);
this.logicalTime = this.realTime;
} }
} }
public incrementTick(bpm: number) {
this.tick++;
this.logicalTime += this.pulse_duration_at_bpm(bpm);
}
public nextTickFrom(time: number, nudge: number): number { public nextTickFrom(time: number, nudge: number): number {
/**
* Compute the time remaining before the next clock tick.
* @param time - audio context currentTime
* @param nudge - nudge in the future (in seconds)
* @returns remainingTime
*/
const pulseDuration = this.pulse_duration; const pulseDuration = this.pulse_duration;
const nudgedTime = time + nudge; const nudgedTime = time + nudge;
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration; const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
const remainingTime = nextTickTime - nudgedTime; const remainingTime = nextTickTime - nudgedTime;
return remainingTime; return remainingTime;
} }
public convertPulseToSecond(n: number): number { public convertPulseToSecond(n: number): number {
/**
* Converts a pulse to a second.
*/
return n * this.pulse_duration; return n * this.pulse_duration;
} }
public start(): void { public start(): void {
/** /**
* Starts the TransportNode (starts the clock). * Start the clock
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
*/ */
this.app.audioContext.resume(); this.app.audioContext.resume();
this.running = true; this.running = true;
this.app.api.MidiConnection.sendStartMessage(); this.app.api.MidiConnection.sendStartMessage();
this.lastPlayPressTime = this.app.audioContext.currentTime; this.clock.start();
this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime;
this.transportNode?.start();
} }
public pause(): void { public pause(): void {
/** /**
* Pauses the TransportNode (pauses the clock). * Pause the clock.
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
*/ */
this.running = false; this.running = false;
this.transportNode?.pause();
this.app.api.MidiConnection.sendStopMessage(); this.app.api.MidiConnection.sendStopMessage();
this.lastPauseTime = this.app.audioContext.currentTime; this.clock.pause();
this.logicalTime = this.realTime;
} }
public stop(): void { public stop(): void {
/** /**
* Stops the TransportNode (stops the clock). * Stops the clock.
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
*/ */
this.running = false; this.running = false;
this.tick = 0; this.tick = 0;
this.lastPauseTime = this.app.audioContext.currentTime;
this.logicalTime = this.realTime;
this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.app.api.MidiConnection.sendStopMessage(); this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.stop(); this.clock.stop();
} }
} }

View File

@ -47,7 +47,7 @@ export const makeExampleFactory = (application: Editor): Function => {
const make_example = ( const make_example = (
description: string, description: string,
code: string, code: string,
open: boolean = false open: boolean = false,
) => { ) => {
const codeId = `codeExample${application.exampleCounter++}`; const codeId = `codeExample${application.exampleCounter++}`;
// Store the code snippet in the data structure // Store the code snippet in the data structure
@ -70,7 +70,11 @@ export const makeExampleFactory = (application: Editor): Function => {
}; };
export const documentation_factory = (application: Editor) => { export const documentation_factory = (application: Editor) => {
// Initialize a data structure to store code examples by their unique IDs /**
* Creates the documentation for the given application.
* @param application The editor application.
* @returns An object containing various documentation sections.
*/
application.api.codeExamples = {}; application.api.codeExamples = {};
return { return {
@ -109,6 +113,10 @@ export const documentation_factory = (application: Editor) => {
}; };
export const showDocumentation = (app: Editor) => { export const showDocumentation = (app: Editor) => {
/**
* Shows or hides the documentation based on the current state of the app.
* @param app - The Editor instance.
*/
if (document.getElementById("app")?.classList.contains("hidden")) { if (document.getElementById("app")?.classList.contains("hidden")) {
document.getElementById("app")?.classList.remove("hidden"); document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden"); document.getElementById("documentation")?.classList.add("hidden");
@ -129,6 +137,9 @@ export const showDocumentation = (app: Editor) => {
}; };
export const hideDocumentation = () => { export const hideDocumentation = () => {
/**
* Hides the documentation section and shows the main application.
*/
if (document.getElementById("app")?.classList.contains("hidden")) { if (document.getElementById("app")?.classList.contains("hidden")) {
document.getElementById("app")?.classList.remove("hidden"); document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden"); document.getElementById("documentation")?.classList.add("hidden");
@ -136,6 +147,12 @@ export const hideDocumentation = () => {
}; };
export const updateDocumentationContent = (app: Editor, bindings: any) => { export const updateDocumentationContent = (app: Editor, bindings: any) => {
/**
* Updates the content of the documentation pane with the converted markdown.
*
* @param app - The editor application.
* @param bindings - Additional bindings for the showdown converter.
*/
const converter = new showdown.Converter({ const converter = new showdown.Converter({
emoji: true, emoji: true,
moreStyling: true, moreStyling: true,
@ -143,7 +160,7 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => {
extensions: [showdownHighlight({ auto_detection: true }), ...bindings], extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
}); });
const converted_markdown = converter.makeHtml( const converted_markdown = converter.makeHtml(
app.docs[app.currentDocumentationPane] app.docs[app.currentDocumentationPane],
); );
document.getElementById("documentation-content")!.innerHTML = document.getElementById("documentation-content")!.innerHTML =
converted_markdown; converted_markdown;

View File

@ -1,6 +1,5 @@
import { type Editor } from "./main"; import { type Editor } from "./main";
export type ElementMap = { export type ElementMap = {
[key: string]: [key: string]:
| HTMLElement | HTMLElement
@ -10,8 +9,7 @@ export type ElementMap = {
| HTMLSelectElement | HTMLSelectElement
| HTMLCanvasElement | HTMLCanvasElement
| HTMLFormElement | HTMLFormElement
| HTMLInputElement | HTMLInputElement;
;
}; };
export const singleElements = { export const singleElements = {
@ -65,6 +63,12 @@ export const buttonGroups = {
//@ts-ignore //@ts-ignore
export const createDocumentationStyle = (app: Editor) => { export const createDocumentationStyle = (app: Editor) => {
/**
* Creates a documentation style object.
* @param {Editor} app - The editor object.
* @returns {Object} - The documentation style object.
*/
return { 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 border-b-4 pt-4 pb-3 px-2", 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", 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",
@ -92,6 +96,4 @@ export const createDocumentationStyle = (app: Editor) => {
tr: "", tr: "",
box: "border bg-red-500", box: "border bg-red-500",
}; };
} };

View File

@ -20,7 +20,7 @@ import {
bracketMatching, bracketMatching,
} from "@codemirror/language"; } from "@codemirror/language";
import { defaultKeymap, historyKeymap, history } from "@codemirror/commands"; import { defaultKeymap, historyKeymap, history } from "@codemirror/commands";
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search" import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
import { import {
autocompletion, autocompletion,
closeBrackets, closeBrackets,
@ -34,15 +34,15 @@ import { toposTheme } from "./themes/toposTheme";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { inlineHoveringTips } from "./documentation/inlineHelp"; import { inlineHoveringTips } from "./documentation/inlineHelp";
import { toposCompletions, soundCompletions } from "./documentation/inlineHelp"; import { toposCompletions, soundCompletions } from "./documentation/inlineHelp";
import { javascriptLanguage } from "@codemirror/lang-javascript" import { javascriptLanguage } from "@codemirror/lang-javascript";
export const jsCompletions = javascriptLanguage.data.of({ export const jsCompletions = javascriptLanguage.data.of({
autocomplete: toposCompletions autocomplete: toposCompletions,
}) });
export const toposSoundCompletions = javascriptLanguage.data.of({ export const toposSoundCompletions = javascriptLanguage.data.of({
autocomplete: soundCompletions autocomplete: soundCompletions,
}) });
export const editorSetup: Extension = (() => [ export const editorSetup: Extension = (() => [
highlightActiveLineGutter(), highlightActiveLineGutter(),
@ -95,7 +95,9 @@ export const installEditor = (app: Editor) => {
app.withLineNumbers.of(lines), app.withLineNumbers.of(lines),
app.fontSize.of(fontModif), app.fontSize.of(fontModif),
app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []), app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []),
app.completionsCompartment.of(app.settings.completions ? [jsCompletions, toposSoundCompletions] : []), app.completionsCompartment.of(
app.settings.completions ? [jsCompletions, toposSoundCompletions] : [],
),
editorSetup, editorSetup,
toposTheme, toposTheme,
app.chosenLanguage.of(javascript()), app.chosenLanguage.of(javascript()),
@ -114,7 +116,7 @@ export const installEditor = (app: Editor) => {
return true; return true;
}, },
}, },
]) ]),
), ),
keymap.of([indentWithTab]), keymap.of([indentWithTab]),
], ],
@ -139,7 +141,7 @@ export const installEditor = (app: Editor) => {
".cm-gutters": { ".cm-gutters": {
fontSize: `${app.settings.font_size}px`, fontSize: `${app.settings.font_size}px`,
}, },
}) }),
), ),
}); });
}; };

View File

@ -1,42 +1,45 @@
import type { Editor } from "./main"; import type { Editor } from "./main";
import type { File } from "./FileManagement"; import type { File } from "./FileManagement";
const delay = (ms: number) => const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Operation took too long")), ms)
);
const codeReplace = (code: string): string => { const codeReplace = (code: string): string => {
let new_code = code.replace(/->/g, "&&").replace(/::/g, "&&"); return code.replace(/->|::/g, "&&");
return new_code;
}; };
const tryCatchWrapper = ( const tryCatchWrapper = async (
application: Editor, application: Editor,
code: string code: string,
): Promise<boolean> => { ): Promise<boolean> => {
return new Promise((resolve, _) => { /**
* Wraps the provided code in a try-catch block and executes it.
*
* @param application - The editor application.
* @param code - The code to be executed.
* @returns A promise that resolves to a boolean indicating whether the code executed successfully or not.
*/
try { try {
Function( await new Function(`"use strict"; ${codeReplace(code)}`).call(
`"use strict";try{ application.api,
${codeReplace(code)}; /* break block comments */; );
} catch (e) {console.log(e); _reportError(e);};` return true;
).call(application.api);
resolve(true);
} catch (error) { } catch (error) {
application.interface.error_line.innerHTML = error as string; application.interface.error_line.innerHTML = error as string;
application.api._reportError(error as string) application.api._reportError(error as string);
resolve(false); return false;
} }
});
}; };
const cache = new Map<string, Function>(); const cache = new Map<string, Function>();
const MAX_CACHE_SIZE = 20; const MAX_CACHE_SIZE = 40;
const addFunctionToCache = (code: string, fn: Function) => { const addFunctionToCache = (code: string, fn: Function) => {
/**
* Adds a function to the cache.
* @param code - The code associated with the function.
* @param fn - The function to be added to the cache.
*/
if (cache.size >= MAX_CACHE_SIZE) { if (cache.size >= MAX_CACHE_SIZE) {
// Delete the first item if cache size exceeds max size
cache.delete(cache.keys().next().value); cache.delete(cache.keys().next().value);
} }
cache.set(code, fn); cache.set(code, fn);
@ -45,28 +48,36 @@ const addFunctionToCache = (code: string, fn: Function) => {
export const tryEvaluate = async ( export const tryEvaluate = async (
application: Editor, application: Editor,
code: File, code: File,
timeout = 5000 timeout = 5000,
): Promise<void> => { ): Promise<void> => {
try { /**
* Tries to evaluate the provided code within a specified timeout period.
* Increments the evaluation count of the code file.
* If the code is valid, updates the committed code and adds the evaluated function to the cache.
* If the code is invalid, retries the evaluation.
* @param application - The editor application.
* @param code - The code file to evaluate.
* @param timeout - The timeout period in milliseconds (default: 5000).
* @returns A Promise that resolves when the evaluation is complete.
*/
code.evaluations!++; code.evaluations!++;
const candidateCode = code.candidate; const candidateCode = code.candidate;
if (cache.has(candidateCode)) { try {
// If the code is already in cache, use it const cachedFunction = cache.get(candidateCode);
cache.get(candidateCode)!.call(application.api); if (cachedFunction) {
cachedFunction.call(application.api);
} else { } else {
const wrappedCode = `let i = ${code.evaluations};` + candidateCode; const wrappedCode = `let i = ${code.evaluations}; ${candidateCode}`;
// Otherwise, evaluate the code and if valid, add it to the cache
const isCodeValid = await Promise.race([ const isCodeValid = await Promise.race([
tryCatchWrapper(application, wrappedCode as string), tryCatchWrapper(application, wrappedCode),
delay(timeout), delay(timeout),
]); ]);
if (isCodeValid) { if (isCodeValid) {
code.committed = code.candidate; code.committed = code.candidate;
const newFunction = new Function( const newFunction = new Function(
`"use strict";try{${codeReplace( `"use strict"; ${codeReplace(wrappedCode)}`,
wrappedCode
)}} catch (e) {console.log(e); _reportError(e);};`
); );
addFunctionToCache(candidateCode, newFunction); addFunctionToCache(candidateCode, newFunction);
} else { } else {
@ -75,15 +86,23 @@ export const tryEvaluate = async (
} }
} catch (error) { } catch (error) {
application.interface.error_line.innerHTML = error as string; application.interface.error_line.innerHTML = error as string;
application.api._reportError(error as string) application.api._reportError(error as string);
} }
}; };
export const evaluate = async ( export const evaluate = async (
application: Editor, application: Editor,
code: File, code: File,
timeout = 1000 timeout = 1000,
): Promise<void> => { ): Promise<void> => {
/**
* Evaluates the given code using the provided application and timeout.
* @param application The editor application.
* @param code The code file to evaluate.
* @param timeout The timeout value in milliseconds (default: 1000).
* @returns A Promise that resolves when the evaluation is complete.
*/
try { try {
await Promise.race([ await Promise.race([
tryCatchWrapper(application, code.committed as string), tryCatchWrapper(application, code.committed as string),
@ -98,7 +117,7 @@ export const evaluate = async (
export const evaluateOnce = async ( export const evaluateOnce = async (
application: Editor, application: Editor,
code: string code: string,
): Promise<void> => { ): Promise<void> => {
/** /**
* Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper. * Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper.

View File

@ -154,7 +154,7 @@ export class AppSettings {
constructor() { constructor() {
const settingsFromStorage = JSON.parse( const settingsFromStorage = JSON.parse(
localStorage.getItem("topos") || "{}" localStorage.getItem("topos") || "{}",
); );
if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) { if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) {
@ -210,7 +210,7 @@ export class AppSettings {
saveApplicationToLocalStorage( saveApplicationToLocalStorage(
universes: Universes, universes: Universes,
settings: Settings settings: Settings,
): void { ): void {
/** /**
* Main method to store the application to local storage. * Main method to store the application to local storage.
@ -263,7 +263,9 @@ export const initializeSelectedUniverse = (app: Editor): void => {
app.universes[app.selected_universe] = structuredClone(template_universe); app.universes[app.selected_universe] = structuredClone(template_universe);
} }
} }
(app.interface.universe_viewer as HTMLInputElement).placeholder! = `${app.selected_universe}`; (
app.interface.universe_viewer as HTMLInputElement
).placeholder! = `${app.selected_universe}`;
}; };
export const emptyUrl = () => { export const emptyUrl = () => {
@ -271,6 +273,11 @@ export const emptyUrl = () => {
}; };
export const share = async (app: Editor) => { export const share = async (app: Editor) => {
/**
* Shares the current state of the app by generating a URL with encoded data and copying it to the clipboard.
* @param app - The Editor instance representing the app.
* @returns A Promise that resolves to void.
*/
async function bufferToBase64(buffer: Uint8Array) { async function bufferToBase64(buffer: Uint8Array) {
const base64url: string = await new Promise((r) => { const base64url: string = await new Promise((r) => {
const reader = new FileReader(); const reader = new FileReader();
@ -321,8 +328,20 @@ export const loadUniverserFromUrl = (app: Editor): void => {
export const loadUniverse = ( export const loadUniverse = (
app: Editor, app: Editor,
universeName: string, universeName: string,
universe: Universe = template_universe universe: Universe = template_universe,
): void => { ): void => {
/**
* Loads a universe into the application.
* If the universe does not exist, a fresh clone of the template universe is created and added to the application.
* The references to the selected universe are updated in the application settings.
* The editor view is updated to reflect the selected universe.
* The initialization script for the selected universe is evaluated.
*
* @param app - The Editor application instance.
* @param universeName - The name of the universe to load.
* @param universe - The template universe to clone if the specified universe does not exist.
*/
let selectedUniverse = universeName.trim(); let selectedUniverse = universeName.trim();
if (app.universes[selectedUniverse] === undefined) { if (app.universes[selectedUniverse] === undefined) {
// Pushing a freshly cloned template universe to: // Pushing a freshly cloned template universe to:
@ -334,7 +353,9 @@ export const loadUniverse = (
// Updating references to the currently selected universe // Updating references to the currently selected universe
app.settings.selected_universe = selectedUniverse; app.settings.selected_universe = selectedUniverse;
app.selected_universe = selectedUniverse; app.selected_universe = selectedUniverse;
(app.interface.universe_viewer as HTMLInputElement).placeholder! = `${selectedUniverse}`; (
app.interface.universe_viewer as HTMLInputElement
).placeholder! = `${selectedUniverse}`;
// Updating the editor View to reflect the selected universe // Updating the editor View to reflect the selected universe
app.updateEditorView(); app.updateEditorView();
// Evaluating the initialisation script for the selected universe // Evaluating the initialisation script for the selected universe
@ -342,7 +363,11 @@ export const loadUniverse = (
}; };
export const openUniverseModal = (): void => { export const openUniverseModal = (): void => {
// If the modal is hidden, unhide it and hide the editor /**
* Opens the universe modal.
* If the modal is hidden, it unhides it and hides the editor.
* If the modal is already visible, it closes the modal.
*/
if ( if (
document.getElementById("modal-buffers")!.classList.contains("invisible") document.getElementById("modal-buffers")!.classList.contains("invisible")
) { ) {
@ -355,6 +380,9 @@ export const openUniverseModal = (): void => {
}; };
export const closeUniverseModal = (): void => { export const closeUniverseModal = (): void => {
/**
* Closes the universe modal and performs necessary actions.
*/
// @ts-ignore // @ts-ignore
document.getElementById("buffer-search")!.value = ""; document.getElementById("buffer-search")!.value = "";
document.getElementById("editor")!.classList.remove("invisible"); document.getElementById("editor")!.classList.remove("invisible");
@ -362,6 +390,9 @@ export const closeUniverseModal = (): void => {
}; };
export const openSettingsModal = (): void => { export const openSettingsModal = (): void => {
/**
* Opens the settings modal.
*/
if ( if (
document.getElementById("modal-settings")!.classList.contains("invisible") document.getElementById("modal-settings")!.classList.contains("invisible")
) { ) {
@ -373,6 +404,9 @@ export const openSettingsModal = (): void => {
}; };
export const closeSettingsModal = (): void => { export const closeSettingsModal = (): void => {
/**
* Closes the settings modal and performs necessary actions.
*/
document.getElementById("editor")!.classList.remove("invisible"); document.getElementById("editor")!.classList.remove("invisible");
document.getElementById("modal-settings")!.classList.add("invisible"); document.getElementById("modal-settings")!.classList.add("invisible");
}; };

View File

@ -175,10 +175,10 @@ export class MidiConnection {
*/ */
if (this.midiInputs.length > 0) { if (this.midiInputs.length > 0) {
const midiClockSelect = document.getElementById( const midiClockSelect = document.getElementById(
"midi-clock-input" "midi-clock-input",
) as HTMLSelectElement; ) as HTMLSelectElement;
const midiInputSelect = document.getElementById( const midiInputSelect = document.getElementById(
"default-midi-input" "default-midi-input",
) as HTMLSelectElement; ) as HTMLSelectElement;
midiClockSelect.innerHTML = ""; midiClockSelect.innerHTML = "";
@ -207,7 +207,7 @@ export class MidiConnection {
if (this.settings.midi_clock_input) { if (this.settings.midi_clock_input) {
const clockMidiInputIndex = this.getMidiInputIndex( const clockMidiInputIndex = this.getMidiInputIndex(
this.settings.midi_clock_input this.settings.midi_clock_input,
); );
midiClockSelect.value = clockMidiInputIndex.toString(); midiClockSelect.value = clockMidiInputIndex.toString();
if (clockMidiInputIndex > 0) { if (clockMidiInputIndex > 0) {
@ -220,7 +220,7 @@ export class MidiConnection {
if (this.settings.default_midi_input) { if (this.settings.default_midi_input) {
const defaultMidiInputIndex = this.getMidiInputIndex( const defaultMidiInputIndex = this.getMidiInputIndex(
this.settings.default_midi_input this.settings.default_midi_input,
); );
midiInputSelect.value = defaultMidiInputIndex.toString(); midiInputSelect.value = defaultMidiInputIndex.toString();
if (defaultMidiInputIndex > 0) { if (defaultMidiInputIndex > 0) {
@ -400,14 +400,14 @@ export class MidiConnection {
public removeFromActiveNotes(note: number, channel: number): void { public removeFromActiveNotes(note: number, channel: number): void {
const index = this.activeNotes.findIndex( const index = this.activeNotes.findIndex(
(e) => e.note === note && e.channel === channel (e) => e.note === note && e.channel === channel,
); );
if (index >= 0) this.activeNotes.splice(index, 1); if (index >= 0) this.activeNotes.splice(index, 1);
} }
public removeFromStickyNotes(note: number, channel: number): boolean { public removeFromStickyNotes(note: number, channel: number): boolean {
const index = this.stickyNotes.findIndex( const index = this.stickyNotes.findIndex(
(e) => e.note === note && e.channel === channel (e) => e.note === note && e.channel === channel,
); );
if (index >= 0) { if (index >= 0) {
this.stickyNotes.splice(index, 1); this.stickyNotes.splice(index, 1);
@ -578,8 +578,9 @@ export class MidiConnection {
if (typeof output === "number") { if (typeof output === "number") {
if (output < 0 || output >= this.midiOutputs.length) { if (output < 0 || output >= this.midiOutputs.length) {
console.error( 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; return this.currentOutputIndex;
} else { } else {
@ -607,8 +608,9 @@ export class MidiConnection {
if (typeof input === "number") { if (typeof input === "number") {
if (input < 0 || input >= this.midiInputs.length) { if (input < 0 || input >= this.midiInputs.length) {
console.error( 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; return -1;
} else { } else {
@ -642,7 +644,7 @@ export class MidiConnection {
velocity: number, velocity: number,
duration: number, duration: number,
port: number | string = this.currentOutputIndex, port: number | string = this.currentOutputIndex,
bend: number | undefined = undefined bend: number | undefined = undefined,
): void { ): void {
/** /**
* Sending a MIDI Note on/off message with the same note number and channel. Automatically manages * Sending a MIDI Note on/off message with the same note number and channel. Automatically manages
@ -668,11 +670,14 @@ export class MidiConnection {
if (bend) this.sendPitchBend(bend, channel, port); if (bend) this.sendPitchBend(bend, channel, port);
// Schedule Note Off // Schedule Note Off
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(
() => {
output.send(noteOffMessage); output.send(noteOffMessage);
if (bend) this.sendPitchBend(8192, channel, port); if (bend) this.sendPitchBend(8192, channel, port);
delete this.scheduledNotes[noteNumber]; delete this.scheduledNotes[noteNumber];
}, (duration - 0.02) * 1000); },
(duration - 0.02) * 1000,
);
// @ts-ignore // @ts-ignore
this.scheduledNotes[noteNumber] = timeoutId; this.scheduledNotes[noteNumber] = timeoutId;
@ -685,7 +690,7 @@ export class MidiConnection {
note: number, note: number,
channel: number, channel: number,
velocity: number, velocity: number,
port: number | string = this.currentOutputIndex port: number | string = this.currentOutputIndex,
) { ) {
/** /**
* Sending Midi Note on message * Sending Midi Note on message
@ -704,7 +709,7 @@ export class MidiConnection {
sendMidiOff( sendMidiOff(
note: number, note: number,
channel: number, channel: number,
port: number | string = this.currentOutputIndex port: number | string = this.currentOutputIndex,
) { ) {
/** /**
* Sending Midi Note off message * Sending Midi Note off message
@ -722,7 +727,7 @@ export class MidiConnection {
sendAllNotesOff( sendAllNotesOff(
channel: number, channel: number,
port: number | string = this.currentOutputIndex port: number | string = this.currentOutputIndex,
) { ) {
/** /**
* Sending Midi Note off message * Sending Midi Note off message
@ -739,7 +744,7 @@ export class MidiConnection {
sendAllSoundOff( sendAllSoundOff(
channel: number, channel: number,
port: number | string = this.currentOutputIndex port: number | string = this.currentOutputIndex,
) { ) {
/** /**
* Sending all sound off * Sending all sound off
@ -775,7 +780,7 @@ export class MidiConnection {
public sendPitchBend( public sendPitchBend(
value: number, value: number,
channel: number, channel: number,
port: number | string = this.currentOutputIndex port: number | string = this.currentOutputIndex,
): void { ): void {
/** /**
* Sends a MIDI Pitch Bend message to the currently selected MIDI output. * Sends a MIDI Pitch Bend message to the currently selected MIDI output.
@ -786,7 +791,7 @@ export class MidiConnection {
*/ */
if (value < 0 || value > 16383) { if (value < 0 || value > 16383) {
console.error( console.error(
"Invalid pitch bend value. Value must be in the range 0-16383." "Invalid pitch bend value. Value must be in the range 0-16383.",
); );
} }
if (channel < 0 || channel > 15) { if (channel < 0 || channel > 15) {
@ -825,7 +830,7 @@ export class MidiConnection {
public sendMidiControlChange( public sendMidiControlChange(
controlNumber: number, controlNumber: number,
value: number, value: number,
channel: number channel: number,
): void { ): void {
/** /**
* Sends a MIDI Control Change message to the currently selected MIDI output. * Sends a MIDI Control Change message to the currently selected MIDI output.

View File

@ -12,8 +12,8 @@ socket.onopen = function (event) {
// Send an OSC-like message // Send an OSC-like message
socket.send( socket.send(
JSON.stringify({ JSON.stringify({
address: "/connected", address: "/successful_connexion",
args: [1], args: true,
}) })
); );

View File

@ -118,32 +118,41 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.universe_viewer.addEventListener("keydown", (event: any) => { app.interface.universe_viewer.addEventListener("keydown", (event: any) => {
if (event.key === "Enter") { if (event.key === "Enter") {
let content = (app.interface.universe_viewer as HTMLInputElement).value.trim(); let content = (
app.interface.universe_viewer as HTMLInputElement
).value.trim();
if (content.length > 2 && content.length < 40) { if (content.length > 2 && content.length < 40) {
if (content !== app.selected_universe) { if (content !== app.selected_universe) {
Object.defineProperty(app.universes, content, Object.defineProperty(
app.universes,
content,
// @ts-ignore // @ts-ignore
Object.getOwnPropertyDescriptor(app.universes, app.selected_universe)); Object.getOwnPropertyDescriptor(
app.universes,
app.selected_universe,
),
);
delete app.universes[app.selected_universe]; delete app.universes[app.selected_universe];
} }
app.selected_universe = content; app.selected_universe = content;
loadUniverse(app, app.selected_universe); loadUniverse(app, app.selected_universe);
(app.interface.universe_viewer as HTMLInputElement).placeholder = content; (app.interface.universe_viewer as HTMLInputElement).placeholder =
(app.interface.universe_viewer as HTMLInputElement).value = ''; content;
(app.interface.universe_viewer as HTMLInputElement).value = "";
} }
} }
}); });
app.interface.audio_nudge_range.addEventListener("input", () => { app.interface.audio_nudge_range.addEventListener("input", () => {
app.clock.nudge = parseInt( // TODO: rebuild this
(app.interface.audio_nudge_range as HTMLInputElement).value // app.clock.nudge = parseInt(
); // (app.interface.audio_nudge_range as HTMLInputElement).value,
// );
}); });
app.interface.dough_nudge_range.addEventListener("input", () => { app.interface.dough_nudge_range.addEventListener("input", () => {
app.dough_nudge = parseInt( app.dough_nudge = parseInt(
(app.interface.dough_nudge_range as HTMLInputElement).value (app.interface.dough_nudge_range as HTMLInputElement).value,
); );
}); });
@ -227,16 +236,16 @@ export const installInterfaceLogic = (app: Editor) => {
}); });
app.interface.local_button.addEventListener("click", () => app.interface.local_button.addEventListener("click", () =>
app.changeModeFromInterface("local") app.changeModeFromInterface("local"),
); );
app.interface.global_button.addEventListener("click", () => app.interface.global_button.addEventListener("click", () =>
app.changeModeFromInterface("global") app.changeModeFromInterface("global"),
); );
app.interface.init_button.addEventListener("click", () => app.interface.init_button.addEventListener("click", () =>
app.changeModeFromInterface("init") app.changeModeFromInterface("init"),
); );
app.interface.note_button.addEventListener("click", () => app.interface.note_button.addEventListener("click", () =>
app.changeModeFromInterface("notes") app.changeModeFromInterface("notes"),
); );
app.interface.font_family_selector.addEventListener("change", () => { app.interface.font_family_selector.addEventListener("change", () => {
@ -255,7 +264,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px", fontSize: app.settings.font_size + "px",
}, },
".cm-gutters": { fontSize: app.settings.font_size + "px" }, ".cm-gutters": { fontSize: app.settings.font_size + "px" },
}) }),
), ),
}); });
}); });
@ -275,7 +284,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px", fontSize: app.settings.font_size + "px",
}, },
".cm-gutters": { fontSize: app.settings.font_size + "px" }, ".cm-gutters": { fontSize: app.settings.font_size + "px" },
}) }),
), ),
}); });
}); });
@ -283,7 +292,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.settings_button.addEventListener("click", () => { app.interface.settings_button.addEventListener("click", () => {
// Populate the font selector // Populate the font selector
const fontFamilySelect = document.getElementById( const fontFamilySelect = document.getElementById(
"font-family" "font-family",
) as HTMLSelectElement | null; ) as HTMLSelectElement | null;
if (fontFamilySelect) { if (fontFamilySelect) {
fontFamilySelect.value = app.settings.font; fontFamilySelect.value = app.settings.font;
@ -294,7 +303,7 @@ export const installInterfaceLogic = (app: Editor) => {
doughNudgeRange.value = app.dough_nudge.toString(); doughNudgeRange.value = app.dough_nudge.toString();
// @ts-ignore // @ts-ignore
const doughNumber = document.getElementById( const doughNumber = document.getElementById(
"doughnumber" "doughnumber",
) as HTMLInputElement; ) as HTMLInputElement;
doughNumber.value = app.dough_nudge.toString(); doughNumber.value = app.dough_nudge.toString();
if (app.settings.font_size === null) { if (app.settings.font_size === null) {
@ -350,7 +359,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px", fontSize: app.settings.font_size + "px",
}, },
".cm-gutters": { fontSize: app.settings.font_size + "px" }, ".cm-gutters": { fontSize: app.settings.font_size + "px" },
}) }),
), ),
}); });
}); });
@ -408,7 +417,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.tips = checked; app.settings.tips = checked;
app.view.dispatch({ app.view.dispatch({
effects: app.hoveringCompartment.reconfigure( effects: app.hoveringCompartment.reconfigure(
checked ? inlineHoveringTips : [] checked ? inlineHoveringTips : [],
), ),
}); });
}); });
@ -421,7 +430,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.completions = checked; app.settings.completions = checked;
app.view.dispatch({ app.view.dispatch({
effects: app.completionsCompartment.reconfigure( effects: app.completionsCompartment.reconfigure(
checked ? jsCompletions : [] checked ? jsCompletions : [],
), ),
}); });
}); });
@ -444,7 +453,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.midi_clock_ppqn.addEventListener("change", () => { app.interface.midi_clock_ppqn.addEventListener("change", () => {
let value = parseInt( let value = parseInt(
(app.interface.midi_clock_ppqn as HTMLInputElement).value (app.interface.midi_clock_ppqn as HTMLInputElement).value,
); );
app.settings.midi_clock_ppqn = value; app.settings.midi_clock_ppqn = value;
}); });

View File

@ -26,6 +26,31 @@ export const registerOnKeyDown = (app: Editor) => {
event.preventDefault(); event.preventDefault();
} }
if (event.ctrlKey && event.key === "m") {
event.preventDefault();
let topbar = document.getElementById("topbar");
let sidebar = document.getElementById("sidebar");
console.log("oui ok");
if (app.hidden_interface) {
// Sidebar
sidebar?.classList.remove("flex");
sidebar?.classList.remove("flex-col");
sidebar?.classList.add("hidden");
// Topbar
topbar?.classList.add("hidden");
topbar?.classList.remove("flex");
} else {
// Sidebar
sidebar?.classList.remove("hidden");
sidebar?.classList.add("flex");
sidebar?.classList.add("flex-col");
// Topbar
topbar?.classList.remove("hidden");
topbar?.classList.add("flex");
}
app.hidden_interface = !app.hidden_interface;
}
if (event.ctrlKey && event.key === "s") { if (event.ctrlKey && event.key === "s") {
event.preventDefault(); event.preventDefault();
app.setButtonHighlighting("stop", true); app.setButtonHighlighting("stop", true);

View File

@ -1,67 +0,0 @@
import { tryEvaluate } from "./Evaluator";
const zeroPad = (num, places) => String(num).padStart(places, "0");
export class TransportNode extends AudioWorkletNode {
constructor(context, options, application) {
super(context, "transport", options);
this.app = application;
this.port.addEventListener("message", this.handleMessage);
this.port.start();
this.timeviewer = document.getElementById("timeviewer");
}
/** @type {(this: MessagePort, ev: MessageEvent<any>) => any} */
handleMessage = (message) => {
if (message.data) {
if (message.data.type === "bang") {
if (this.app.clock.running) {
if (this.app.settings.send_clock) {
this.app.api.MidiConnection.sendMidiClock();
}
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick
);
this.app.clock.time_position = futureTimeStamp;
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${futureTimeStamp.beat + 1
} / ${this.app.clock.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
this.app.clock.incrementTick(message.data.bpm);
}
}
}
};
start() {
this.port.postMessage({ type: "start" });
}
pause() {
this.port.postMessage({ type: "pause" });
}
resume() {
this.port.postMessage({ type: "resume" });
}
setBPM(bpm) {
this.port.postMessage({ type: "bpm", value: bpm });
}
setPPQN(ppqn) {
this.port.postMessage({ type: "ppqn", value: ppqn });
}
setNudge(nudge) {
this.port.postMessage({ type: "nudge", value: nudge });
}
stop() {
this.port.postMessage({ type: "stop" });
}
}

View File

@ -1,47 +0,0 @@
class TransportProcessor extends AudioWorkletProcessor {
constructor(options) {
super(options);
this.port.addEventListener("message", this.handleMessage);
this.port.start();
this.nudge = 0;
this.started = false;
this.bpm = 120;
this.ppqn = 48;
this.currentPulsePosition = 0;
}
handleMessage = (message) => {
if (message.data && message.data.type === "ping") {
this.port.postMessage(message.data);
} else if (message.data.type === "start") {
this.started = true;
} else if (message.data.type === "pause") {
this.started = false;
} else if (message.data.type === "stop") {
this.started = false;
} else if (message.data.type === "bpm") {
this.bpm = message.data.value;
this.currentPulsePosition = currentTime;
} else if (message.data.type === "ppqn") {
this.ppqn = message.data.value;
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 beatNumber = adjustedCurrentTime / (60 / this.bpm);
const currentPulsePosition = Math.ceil(beatNumber * this.ppqn);
if (currentPulsePosition > this.currentPulsePosition) {
this.currentPulsePosition = currentPulsePosition;
this.port.postMessage({ type: "bang", bpm: this.bpm });
}
}
return true;
}
}
registerProcessor("transport", TransportProcessor);

View File

@ -1,3 +1,7 @@
export function objectWithArraysToArrayOfObjects(
input: Record<string, any>,
arraysToArrays: string[],
): Record<string, any>[] {
/* /*
* Transforms object with arrays into array of objects * Transforms object with arrays into array of objects
* *
@ -6,37 +10,42 @@
* @returns {Record<string, any>[]} Array of objects * @returns {Record<string, any>[]} Array of objects
* *
*/ */
export function objectWithArraysToArrayOfObjects(input: Record<string, any>, arraysToArrays: string[]): Record<string, any>[] { const inputCopy = { ...input };
arraysToArrays.forEach((k) => { arraysToArrays.forEach((k) => {
// Transform single array to array of arrays and keep array of arrays as is if (Array.isArray(inputCopy[k]) && !Array.isArray(inputCopy[k][0])) {
if (Array.isArray(input[k]) && !Array.isArray(input[k][0])) { inputCopy[k] = [inputCopy[k]];
input[k] = [input[k]];
} }
}); });
const keys = Object.keys(input);
const maxLength = Math.max( const keysAndLengths = Object.entries(inputCopy).reduce(
...keys.map((k) => (acc, [key, value]) => {
Array.isArray(input[k]) ? (input[k] as any[]).length : 1 const length = Array.isArray(value) ? (value as any[]).length : 1;
) acc.maxLength = Math.max(acc.maxLength, length);
acc.keys.push(key);
return acc;
},
{ keys: [] as string[], maxLength: 0 },
); );
const output: Record<string, any>[] = []; const output: Record<string, any>[] = [];
for (let i = 0; i < keysAndLengths.maxLength; i++) {
for (let i = 0; i < maxLength; i++) {
const event: Record<string, any> = {}; const event: Record<string, any> = {};
for (const k of keys) { for (const k of keysAndLengths.keys) {
if (Array.isArray(input[k])) { if (Array.isArray(inputCopy[k])) {
event[k] = (input[k] as any[])[i % (input[k] as any[]).length]; event[k] = (inputCopy[k] as any[])[i % (inputCopy[k] as any[]).length];
} else { } else {
event[k] = input[k]; event[k] = inputCopy[k];
} }
} }
output.push(event); output.push(event);
} }
return output; return output;
}; }
export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(
array: T[],
mergeObject: Record<string, any> = {},
): Record<string, any> {
/* /*
* Transforms array of objects into object with arrays * Transforms array of objects into object with arrays
* *
@ -45,21 +54,25 @@ export function objectWithArraysToArrayOfObjects(input: Record<string, any>, arr
* @returns {object} Merged object with arrays * @returns {object} Merged object with arrays
* *
*/ */
export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(array: T[], mergeObject: Record<string, any> = {}): Record<string, any> { return array.reduce(
return array.reduce((acc, obj) => { (acc, obj) => {
Object.keys(mergeObject).forEach((key) => { const mergedObj = { ...obj, ...mergeObject };
obj[key as keyof T] = mergeObject[key]; Object.keys(mergedObj).forEach((key) => {
}); if (!acc[key]) {
Object.keys(obj).forEach((key) => { acc[key] = [];
if (!acc[key as keyof T]) {
acc[key as keyof T] = [];
} }
(acc[key as keyof T] as unknown[]).push(obj[key]); acc[key].push(mergedObj[key]);
}); });
return acc; return acc;
}, {} as Record<keyof T, any[]>); },
{} as Record<string, any>,
);
} }
export function filterObject(
obj: Record<string, any>,
filter: string[],
): Record<string, any> {
/* /*
* Filter certain keys from object * Filter certain keys from object
* *
@ -68,6 +81,7 @@ export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(
* @returns {object} Filtered object * @returns {object} Filtered object
* *
*/ */
export function filterObject(obj: Record<string, any>, filter: string[]): Record<string, any> { return Object.fromEntries(
return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key))); Object.entries(obj).filter(([key]) => filter.includes(key)),
);
} }

118
src/Visuals/Blinkers.ts Normal file
View File

@ -0,0 +1,118 @@
import { type Editor } from "../main";
export const drawCircle = (
/**
* Draw a circle at a specific position on the canvas.
* @param {number} x - The x-coordinate of the circle's center.
* @param {number} y - The y-coordinate of the circle's center.
* @param {number} radius - The radius of the circle.
* @param {string} color - The fill color of the circle.
*/
app: Editor,
x: number,
y: number,
radius: number,
color: string,
): void => {
// @ts-ignore
const canvas: HTMLCanvasElement = app.interface.feedback;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
};
export const blinkScript = (
/**
* Blinks a script indicator circle.
* @param script - The type of script.
* @param no - The shift amount multiplier.
*/
app: Editor,
script: "local" | "global" | "init",
no?: number,
) => {
if (no !== undefined && no < 1 && no > 9) return;
const blinkDuration =
(app.clock.bpm / 60 / app.clock.time_signature[1]) * 200;
// @ts-ignore
const ctx = app.interface.feedback.getContext("2d"); // Assuming a canvas context
/**
* Draws a circle at a given shift.
* @param shift - The pixel distance from the origin.
*/
const _drawBlinker = (shift: number) => {
const horizontalOffset = 50;
drawCircle(
app,
horizontalOffset + shift,
app.interface.feedback.clientHeight - 15,
8,
"#fdba74",
);
};
const _clearBlinker = (shift: number) => {
/**
* Clears the circle at a given shift.
* @param shift - The pixel distance from the origin.
*/
const x = 50 + shift;
const y = app.interface.feedback.clientHeight - 15;
const radius = 8;
ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2);
};
if (script === "local" && no !== undefined) {
const shiftAmount = no * 25;
// Clear existing timeout if any
if (app.blinkTimeouts[shiftAmount]) {
clearTimeout(app.blinkTimeouts[shiftAmount]);
}
_drawBlinker(shiftAmount);
// Save timeout ID for later clearing
// @ts-ignore
app.blinkTimeouts[shiftAmount] = setTimeout(() => {
_clearBlinker(shiftAmount);
// Clear the canvas before drawing new blinkers
(app.interface.feedback as HTMLCanvasElement)
.getContext("2d")!
.clearRect(
0,
0,
(app.interface.feedback as HTMLCanvasElement).width,
(app.interface.feedback as HTMLCanvasElement).height,
);
}, blinkDuration);
}
};
export const scriptBlinkers = () => {
/**
* Manages animation updates using requestAnimationFrame.
* @param app - The Editor application context.
*/
let lastFrameTime = Date.now();
const frameRate = 10;
const minFrameDelay = 1000 / frameRate;
const update = () => {
const now = Date.now();
const timeSinceLastFrame = now - lastFrameTime;
if (timeSinceLastFrame >= minFrameDelay) {
lastFrameTime = now;
}
requestAnimationFrame(update);
};
requestAnimationFrame(update);
};

View File

@ -1,122 +1,6 @@
// @ts-ignore // @ts-ignore
import { getAnalyser } from "superdough"; import { getAnalyser } from "superdough";
import { type Editor } from "./main"; import { Editor } from "../main";
/**
* Draw a circle at a specific position on the canvas.
* @param {number} x - The x-coordinate of the circle's center.
* @param {number} y - The y-coordinate of the circle's center.
* @param {number} radius - The radius of the circle.
* @param {string} color - The fill color of the circle.
*/
export const drawCircle = (
app: Editor,
x: number,
y: number,
radius: number,
color: string
): void => {
// @ts-ignore
const canvas: HTMLCanvasElement = app.interface.feedback;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
};
/**
* Blinks a script indicator circle.
* @param script - The type of script.
* @param no - The shift amount multiplier.
*/
export const blinkScript = (
app: Editor,
script: "local" | "global" | "init",
no?: number
) => {
if (no !== undefined && no < 1 && no > 9) return;
const blinkDuration =
(app.clock.bpm / 60 / app.clock.time_signature[1]) * 200;
// @ts-ignore
const ctx = app.interface.feedback.getContext("2d"); // Assuming a canvas context
/**
* Draws a circle at a given shift.
* @param shift - The pixel distance from the origin.
*/
const _drawBlinker = (shift: number) => {
const horizontalOffset = 50;
drawCircle(
app,
horizontalOffset + shift,
app.interface.feedback.clientHeight - 15,
8,
"#fdba74"
);
};
/**
* Clears the circle at a given shift.
* @param shift - The pixel distance from the origin.
*/
const _clearBlinker = (shift: number) => {
const x = 50 + shift;
const y = app.interface.feedback.clientHeight - 15;
const radius = 8;
ctx.clearRect(x - radius, y - radius, radius * 2, radius * 2);
};
if (script === "local" && no !== undefined) {
const shiftAmount = no * 25;
// Clear existing timeout if any
if (app.blinkTimeouts[shiftAmount]) {
clearTimeout(app.blinkTimeouts[shiftAmount]);
}
_drawBlinker(shiftAmount);
// Save timeout ID for later clearing
// @ts-ignore
app.blinkTimeouts[shiftAmount] = setTimeout(() => {
_clearBlinker(shiftAmount);
// Clear the canvas before drawing new blinkers
(app.interface.feedback as HTMLCanvasElement)
.getContext("2d")!
.clearRect(
0,
0,
(app.interface.feedback as HTMLCanvasElement).width,
(app.interface.feedback as HTMLCanvasElement).height
);
}, blinkDuration);
}
};
/**
* Manages animation updates using requestAnimationFrame.
* @param app - The Editor application context.
*/
export const scriptBlinkers = () => {
let lastFrameTime = Date.now();
const frameRate = 10;
const minFrameDelay = 1000 / frameRate;
const update = () => {
const now = Date.now();
const timeSinceLastFrame = now - lastFrameTime;
if (timeSinceLastFrame >= minFrameDelay) {
lastFrameTime = now;
}
requestAnimationFrame(update);
};
requestAnimationFrame(update);
};
export interface OscilloscopeConfig { export interface OscilloscopeConfig {
enabled: boolean; enabled: boolean;
@ -134,15 +18,16 @@ export interface OscilloscopeConfig {
let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg' let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg'
let lastRenderTime: number = 0; let lastRenderTime: number = 0;
/**
* Initializes and runs an oscilloscope using an AnalyzerNode.
* @param {HTMLCanvasElement} canvas - The canvas element to draw the oscilloscope.
* @param {OscilloscopeConfig} config - Configuration for the oscilloscope's appearance and behavior.
*/
export const runOscilloscope = ( export const runOscilloscope = (
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
app: Editor app: Editor,
): void => { ): void => {
/**
* Runs the oscilloscope visualization on the provided canvas element.
*
* @param canvas - The HTMLCanvasElement on which to render the visualization.
* @param app - The Editor object containing the configuration for the oscilloscope.
*/
let config = app.osc; let config = app.osc;
let analyzer = getAnalyser(config.fftSize); let analyzer = getAnalyser(config.fftSize);
let dataArray = new Float32Array(analyzer.frequencyBinCount); let dataArray = new Float32Array(analyzer.frequencyBinCount);
@ -155,7 +40,7 @@ export const runOscilloscope = (
width: number, width: number,
height: number, height: number,
offset_height: number, offset_height: number,
offset_width: number offset_width: number,
) { ) {
const maxFPS = 30; const maxFPS = 30;
const now = performance.now(); const now = performance.now();
@ -169,10 +54,12 @@ export const runOscilloscope = (
canvasCtx.clearRect(0, 0, width, height); canvasCtx.clearRect(0, 0, width, height);
const performanceFactor = 1; const performanceFactor = 1;
const reducedDataSize = Math.floor(freqDataArray.length * performanceFactor); const reducedDataSize = Math.floor(
freqDataArray.length * performanceFactor,
);
const numBars = Math.min( const numBars = Math.min(
reducedDataSize, reducedDataSize,
app.osc.orientation === "horizontal" ? width : height app.osc.orientation === "horizontal" ? width : height,
); );
const barWidth = const barWidth =
app.osc.orientation === "horizontal" ? width / numBars : height / numBars; app.osc.orientation === "horizontal" ? width / numBars : height / numBars;
@ -184,7 +71,8 @@ export const runOscilloscope = (
for (let i = 0; i < numBars; i++) { for (let i = 0; i < numBars; i++) {
barHeight = Math.floor( barHeight = Math.floor(
freqDataArray[Math.floor(i * freqDataArray.length / numBars)] * ((height / 256) * app.osc.size) freqDataArray[Math.floor((i * freqDataArray.length) / numBars)] *
((height / 256) * app.osc.size),
); );
if (app.osc.orientation === "horizontal") { if (app.osc.orientation === "horizontal") {
@ -192,7 +80,7 @@ export const runOscilloscope = (
x + offset_width, x + offset_width,
(height - barHeight) / 2 + offset_height, (height - barHeight) / 2 + offset_height,
barWidth + 1, barWidth + 1,
barHeight barHeight,
); );
x += barWidth; x += barWidth;
} else { } else {
@ -200,14 +88,13 @@ export const runOscilloscope = (
(width - barHeight) / 2 + offset_width, (width - barHeight) / 2 + offset_width,
y + offset_height, y + offset_height,
barHeight, barHeight,
barWidth + 1 barWidth + 1,
); );
y += barWidth; y += barWidth;
} }
} }
} }
function draw() { function draw() {
// Update the canvas position on each cycle // Update the canvas position on each cycle
const WIDTH = canvas.width; const WIDTH = canvas.width;
@ -230,12 +117,19 @@ export const runOscilloscope = (
-OFFSET_WIDTH, -OFFSET_WIDTH,
-OFFSET_HEIGHT, -OFFSET_HEIGHT,
WIDTH + 2 * OFFSET_WIDTH, WIDTH + 2 * OFFSET_WIDTH,
HEIGHT + 2 * OFFSET_HEIGHT HEIGHT + 2 * OFFSET_HEIGHT,
); );
return; return;
} }
if (analyzer.fftSize !== app.osc.fftSize) { if (analyzer.fftSize !== app.osc.fftSize) {
// Disconnect and release the old analyzer if it exists
if (analyzer) {
analyzer.disconnect();
analyzer = null; // Release the reference for garbage collection
}
// Create a new analyzer with the updated FFT size
analyzer = getAnalyser(app.osc.fftSize); analyzer = getAnalyser(app.osc.fftSize);
dataArray = new Float32Array(analyzer.frequencyBinCount); dataArray = new Float32Array(analyzer.frequencyBinCount);
} }
@ -250,7 +144,7 @@ export const runOscilloscope = (
-OFFSET_WIDTH, -OFFSET_WIDTH,
-OFFSET_HEIGHT, -OFFSET_HEIGHT,
WIDTH + 2 * OFFSET_WIDTH, WIDTH + 2 * OFFSET_WIDTH,
HEIGHT + 2 * OFFSET_HEIGHT HEIGHT + 2 * OFFSET_HEIGHT,
); );
} }
canvasCtx.lineWidth = app.osc.thickness; canvasCtx.lineWidth = app.osc.thickness;

View File

@ -1,4 +1,5 @@
import { type Editor } from "./main"; import { type Editor } from "./main";
import { socket } from "./IO/OSC";
const handleResize = (canvas: HTMLCanvasElement) => { const handleResize = (canvas: HTMLCanvasElement) => {
if (!canvas) return; if (!canvas) return;
@ -26,19 +27,21 @@ export const saveBeforeExit = (app: Editor): null => {
app.currentFile().candidate = app.view.state.doc.toString(); app.currentFile().candidate = app.view.state.doc.toString();
app.currentFile().committed = app.view.state.doc.toString(); app.currentFile().committed = app.view.state.doc.toString();
app.settings.saveApplicationToLocalStorage(app.universes, app.settings); app.settings.saveApplicationToLocalStorage(app.universes, app.settings);
// Close the websocket
socket.close();
return null; return null;
}; };
export const installWindowBehaviors = ( export const installWindowBehaviors = (
app: Editor, app: Editor,
window: Window, window: Window,
preventMultipleTabs: boolean = false preventMultipleTabs: boolean = false,
) => { ) => {
window.addEventListener("resize", () => window.addEventListener("resize", () =>
handleResize(app.interface.scope as HTMLCanvasElement) handleResize(app.interface.scope as HTMLCanvasElement),
); );
window.addEventListener("resize", () => window.addEventListener("resize", () =>
handleResize(app.interface.feedback as HTMLCanvasElement) handleResize(app.interface.feedback as HTMLCanvasElement),
); );
window.addEventListener("beforeunload", (event) => { window.addEventListener("beforeunload", (event) => {
event.preventDefault(); event.preventDefault();
@ -61,11 +64,11 @@ export const installWindowBehaviors = (
if (e.key == "page_available") { if (e.key == "page_available") {
document.getElementById("all")!.classList.add("invisible"); document.getElementById("all")!.classList.add("invisible");
alert( alert(
"Topos is already opened in another tab. Close this tab now to prevent data loss." "Topos is already opened in another tab. Close this tab now to prevent data loss.",
); );
} }
}, },
false false,
); );
} }
}; };

View File

@ -1,14 +1,17 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { import {
freqToMidi, freqToMidi,
chord as parseChord,
noteNameToMidi,
resolvePitchBend, resolvePitchBend,
safeScale safeScale,
} from "zifferjs"; } from "zifferjs";
import { SkipEvent } from "./SkipEvent";
export type EventOperation<T> = (instance: T, ...args: any[]) => void; export type EventOperation<T> = (instance: T, ...args: any[]) => void;
export interface AbstractEvent { export interface AbstractEvent {
[key: string]: any [key: string]: any;
} }
export class AbstractEvent { export class AbstractEvent {
@ -208,19 +211,26 @@ export class AbstractEvent {
return this.modify(func); return this.modify(func);
}; };
noteLength = (value: number | number[], ...kwargs: number[]): AbstractEvent => { noteLength = (
value: number | number[],
...kwargs: number[]
): AbstractEvent => {
/** /**
* This function is used to set the note length of the Event. * This function is used to set the note length of the Event.
*/ */
if (kwargs.length > 0) { if (kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
this.values["noteLength"] = value; this.values["noteLength"] = value;
this.values.dur = value.map((v) => this.app.clock.convertPulseToSecond(v*4*this.app.clock.ppqn)); this.values.dur = value.map((v) =>
this.app.clock.convertPulseToSecond(v * 4 * this.app.clock.ppqn),
);
} else { } else {
this.values["noteLength"] = value; this.values["noteLength"] = value;
this.values.dur = this.app.clock.convertPulseToSecond(value*4*this.app.clock.ppqn); this.values.dur = this.app.clock.convertPulseToSecond(
value * 4 * this.app.clock.ppqn,
);
} }
return this; return this;
}; };
@ -238,12 +248,12 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if (kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
} }
this.values["pitch"] = value; this.values["pitch"] = value;
if (this.values.key && this.values.parsedScale) this.update(); if (this.values.key && this.values.parsedScale) this.update();
return this; return this;
} };
pc = this.pitch; pc = this.pitch;
@ -254,10 +264,15 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if (kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
} }
this.values["octave"] = value; this.values["octave"] = value;
if(this.values.key && (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update(); if (
this.values.key &&
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
)
this.update();
return this; return this;
}; };
@ -268,21 +283,28 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if (kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
} }
this.values["key"] = value; this.values["key"] = value;
if((this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update(); if (
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
)
this.update();
return this; return this;
}; };
scale = (value: string | number | (number|string)[], ...kwargs: (string|number)[]): this => { scale = (
value: string | number | (number | string)[],
...kwargs: (string | number)[]
): this => {
/* /*
* This function is used to set the scale of the Event. * This function is used to set the scale of the Event.
* @param value - The scale value * @param value - The scale value
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if (kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
} }
if (typeof value === "string" || typeof value === "number") { if (typeof value === "string" || typeof value === "number") {
this.values.parsedScale = safeScale(value) as number[]; this.values.parsedScale = safeScale(value) as number[];
@ -295,6 +317,49 @@ export abstract class AudibleEvent extends AbstractEvent {
return this; return this;
}; };
protected updateValue<T>(key: string, value: T | T[] | null): this {
if (value == null) return this;
this.values[key] = value;
return this;
}
public note = (
value: number | string | null,
...kwargs: number[] | string[]
) => {
if (typeof value === "string") {
const parsedNote = noteNameToMidi(value);
return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity));
} else if (typeof value == null || value == undefined) {
return new SkipEvent();
} else {
return this.updateValue("note", [value, ...kwargs].flat(Infinity));
}
};
public chord = (value: number | string, ...kwargs: number[]) => {
if (typeof value === "string") {
const chord = parseChord(value);
return this.updateValue("note", chord);
} else {
const chord = [value, ...kwargs].flat(Infinity);
return this.updateValue("note", chord);
}
};
public invert = (howMany: number = 0) => {
if (this.values.note) {
let notes = [...this.values.note];
notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12;
}
return this.updateValue("note", notes);
} else {
return this;
}
};
freq = (value: number | number[], ...kwargs: number[]): this => { freq = (value: number | number[], ...kwargs: number[]): this => {
/* /*
* This function is used to set the frequency of the Event. * This function is used to set the frequency of the Event.
@ -302,7 +367,7 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if (kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]); value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
} }
this.values["freq"] = value; this.values["freq"] = value;
if (Array.isArray(value)) { if (Array.isArray(value)) {

View File

@ -1,8 +1,12 @@
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from "../main"; import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection"; import { MidiConnection } from "../IO/MidiConnection";
import { noteFromPc, chord as parseChord } from "zifferjs"; import { noteFromPc } from "zifferjs";
import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic"; import {
filterObject,
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
} from "../Utils/Generic";
export type MidiParams = { export type MidiParams = {
note: number; note: number;
@ -11,27 +15,20 @@ export type MidiParams = {
port?: number; port?: number;
sustain?: number; sustain?: number;
velocity?: number; velocity?: number;
} };
export class MidiEvent extends AudibleEvent { export class MidiEvent extends AudibleEvent {
midiConnection: MidiConnection; midiConnection: MidiConnection;
constructor(input: MidiParams, public app: Editor) { constructor(
input: MidiParams,
public app: Editor,
) {
super(app); super(app);
this.values = input; this.values = input;
this.midiConnection = app.api.MidiConnection; this.midiConnection = app.api.MidiConnection;
} }
public chord = (value: string) => {
this.values.note = parseChord(value);
return this;
};
note = (value: number | number[]): this => {
this.values["note"] = value;
return this;
};
sustain = (value: number | number[]): this => { sustain = (value: number | number[]): this => {
this.values["sustain"] = value; this.values["sustain"] = value;
return this; return this;
@ -40,7 +37,7 @@ export class MidiEvent extends AudibleEvent {
velocity = (value: number | number[]): this => { velocity = (value: number | number[]): this => {
this.values["velocity"] = value; this.values["velocity"] = value;
return this; return this;
} };
channel = (value: number | number[]): this => { channel = (value: number | number[]): this => {
this.values["channel"] = value; this.values["channel"] = value;
@ -51,7 +48,9 @@ export class MidiEvent extends AudibleEvent {
if (typeof value === "string") { if (typeof value === "string") {
this.values["port"] = this.midiConnection.getMidiOutputIndex(value); this.values["port"] = this.midiConnection.getMidiOutputIndex(value);
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
this.values["port"] = value.map((v) => typeof v === "string" ? this.midiConnection.getMidiOutputIndex(v) : v); this.values["port"] = value.map((v) =>
typeof v === "string" ? this.midiConnection.getMidiOutputIndex(v) : v,
);
} }
return this; return this;
}; };
@ -86,16 +85,23 @@ export class MidiEvent extends AudibleEvent {
update = (): void => { update = (): void => {
// Get key, pitch, parsedScale and octave from this.values object // Get key, pitch, parsedScale and octave from this.values object
const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]); const filteredValues = filterObject(this.values, [
"key",
"pitch",
"parsedScale",
"octave",
]);
const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]); const events = objectWithArraysToArrayOfObjects(filteredValues, [
"parsedScale",
]);
events.forEach((event) => { events.forEach((event) => {
const [note, bend] = noteFromPc( const [note, bend] = noteFromPc(
event.key as number || "C4", (event.key as number) || "C4",
event.pitch as number || 0, (event.pitch as number) || 0,
event.parsedScale as number[] || event.scale || "MAJOR", (event.parsedScale as number[]) || event.scale || "MAJOR",
event.octave as number || 0 (event.octave as number) || 0,
); );
event.note = note; event.note = note;
if (bend) event.bend = bend; if (bend) event.bend = bend;
@ -114,9 +120,7 @@ export class MidiEvent extends AudibleEvent {
const note = params.note ? params.note : 60; const note = params.note ? params.note : 60;
const sustain = params.sustain const sustain = params.sustain
? params.sustain * ? params.sustain * event.app.clock.pulse_duration * event.app.api.ppqn()
event.app.clock.pulse_duration *
event.app.api.ppqn()
: event.app.clock.pulse_duration * event.app.api.ppqn(); : event.app.clock.pulse_duration * event.app.api.ppqn();
const bend = params.bend ? params.bend : undefined; const bend = params.bend ? params.bend : undefined;
@ -131,15 +135,16 @@ export class MidiEvent extends AudibleEvent {
velocity, velocity,
sustain, sustain,
port, port,
bend bend,
); );
} }
const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]) as MidiParams[]; const events = objectWithArraysToArrayOfObjects(this.values, [
"parsedScale",
]) as MidiParams[];
events.forEach((p: MidiParams) => { events.forEach((p: MidiParams) => {
play(this, p); play(this, p);
}); });
}; };
} }

View File

@ -11,10 +11,7 @@ export class RestEvent extends AbstractEvent {
return RestEvent.createRestProxy(this.values["noteLength"], this.app); return RestEvent.createRestProxy(this.values["noteLength"], this.app);
}; };
public static createRestProxy = ( public static createRestProxy = (length: number, app: Editor): RestEvent => {
length: number,
app: Editor
): RestEvent => {
const instance = new RestEvent(length, app); const instance = new RestEvent(length, app);
return new Proxy(instance, { return new Proxy(instance, {
// @ts-ignore // @ts-ignore

View File

@ -6,17 +6,13 @@ import {
arrayOfObjectsToObjectWithArrays, arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects, objectWithArraysToArrayOfObjects,
} from "../Utils/Generic"; } from "../Utils/Generic";
import { import { midiToFreq, noteFromPc } from "zifferjs";
chord as parseChord,
midiToFreq,
noteFromPc,
noteNameToMidi,
} from "zifferjs";
import { import {
superdough, superdough,
// @ts-ignore // @ts-ignore
} from "superdough"; } from "superdough";
// import { Sound } from "zifferjs/src/types";
export type SoundParams = { export type SoundParams = {
dur: number | number[]; dur: number | number[];
@ -36,7 +32,7 @@ export class SoundEvent extends AudibleEvent {
nudge: number; nudge: number;
sound: any; sound: any;
private methodMap = { private static methodMap = {
volume: ["volume", "vol"], volume: ["volume", "vol"],
zrand: ["zrand", "zr"], zrand: ["zrand", "zr"],
curve: ["curve"], curve: ["curve"],
@ -70,17 +66,23 @@ export class SoundEvent extends AudibleEvent {
phaserDepth: ["phaserDepth", "phasdepth"], phaserDepth: ["phaserDepth", "phasdepth"],
phaserSweep: ["phaserSweep", "phassweep"], phaserSweep: ["phaserSweep", "phassweep"],
phaserCenter: ["phaserCenter", "phascenter"], phaserCenter: ["phaserCenter", "phascenter"],
fmadsr: (a: number, d: number, s: number, r: number) => { fmadsr: function (
this.updateValue("fmattack", a); self: SoundEvent,
this.updateValue("fmdecay", d); a: number,
this.updateValue("fmsustain", s); d: number,
this.updateValue("fmrelease", r); s: number,
return this; r: number,
) {
self.updateValue("fmattack", a);
self.updateValue("fmdecay", d);
self.updateValue("fmsustain", s);
self.updateValue("fmrelease", r);
return self;
}, },
fmad: (a: number, d: number) => { fmad: function (self: SoundEvent, a: number, d: number) {
this.updateValue("fmattack", a); self.updateValue("fmattack", a);
this.updateValue("fmdecay", d); self.updateValue("fmdecay", d);
return this; return self;
}, },
ftype: ["ftype"], ftype: ["ftype"],
fanchor: ["fanchor"], fanchor: ["fanchor"],
@ -88,147 +90,185 @@ export class SoundEvent extends AudibleEvent {
decay: ["decay", "dec"], decay: ["decay", "dec"],
sustain: ["sustain", "sus"], sustain: ["sustain", "sus"],
release: ["release", "rel"], release: ["release", "rel"],
adsr: (a: number, d: number, s: number, r: number) => { adsr: function (
this.updateValue("attack", a); self: SoundEvent,
this.updateValue("decay", d); a: number,
this.updateValue("sustain", s); d: number,
this.updateValue("release", r); s: number,
return this; r: number,
) {
self.updateValue("attack", a);
self.updateValue("decay", d);
self.updateValue("sustain", s);
self.updateValue("release", r);
return self;
}, },
ad: (a: number, d: number) => { ad: function (self: SoundEvent, a: number, d: number) {
this.updateValue("attack", a); self.updateValue("attack", a);
this.updateValue("decay", d); self.updateValue("decay", d);
this.updateValue("sustain", 0.0); self.updateValue("sustain", 0.0);
this.updateValue("release", 0.0); self.updateValue("release", 0.0);
return this; return self;
},
scope: function (self: SoundEvent) {
self.updateValue("analyze", true);
return self;
},
debug: function (self: SoundEvent, callback?: Function) {
self.updateValue("debug", true);
if (callback) {
self.updateValue("debugFunction", callback);
}
return self;
}, },
lpenv: ["lpenv", "lpe"], lpenv: ["lpenv", "lpe"],
lpattack: ["lpattack", "lpa"], lpattack: ["lpattack", "lpa"],
lpdecay: ["lpdecay", "lpd"], lpdecay: ["lpdecay", "lpd"],
lpsustain: ["lpsustain", "lps"], lpsustain: ["lpsustain", "lps"],
lprelease: ["lprelease", "lpr"], lprelease: ["lprelease", "lpr"],
cutoff: (value: number, resonance?: number) => { cutoff: function (self: SoundEvent, value: number, resonance?: number) {
this.updateValue("cutoff", value); self.updateValue("cutoff", value);
if (resonance) { if (resonance) {
this.updateValue("resonance", resonance); self.updateValue("resonance", resonance);
} }
return this; return self;
}, },
lpf: (value: number, resonance?: number) => { lpf: function (self: SoundEvent, value: number, resonance?: number) {
this.updateValue("cutoff", value); self.updateValue("cutoff", value);
if (resonance) { if (resonance) {
this.updateValue("resonance", resonance); self.updateValue("resonance", resonance);
} }
return this; return self;
}, },
resonance: (value: number) => { resonance: function (self: SoundEvent, value: number) {
if (value >= 0 && value <= 1) { if (value >= 0 && value <= 1) {
this.updateValue("resonance", 50 * value); self.updateValue("resonance", 50 * value);
} }
return this; return self;
}, },
lpadsr: (depth: number, a: number, d: number, s: number, r: number) => { lpadsr: function (
this.updateValue("lpenv", depth); self: SoundEvent,
this.updateValue("lpattack", a); depth: number,
this.updateValue("lpdecay", d); a: number,
this.updateValue("lpsustain", s); d: number,
this.updateValue("lprelease", r); s: number,
return this; r: number,
) {
self.updateValue("lpenv", depth);
self.updateValue("lpattack", a);
self.updateValue("lpdecay", d);
self.updateValue("lpsustain", s);
self.updateValue("lprelease", r);
return self;
}, },
lpad: (depth: number, a: number, d: number) => { lpad: function (self: SoundEvent, depth: number, a: number, d: number) {
this.updateValue("lpenv", depth); self.updateValue("lpenv", depth);
this.updateValue("lpattack", a); self.updateValue("lpattack", a);
this.updateValue("lpdecay", d); self.updateValue("lpdecay", d);
this.updateValue("lpsustain", 0); self.updateValue("lpsustain", 0);
this.updateValue("lprelease", 0); self.updateValue("lprelease", 0);
return this; return self;
}, },
hpenv: ["hpenv", "hpe"], hpenv: ["hpenv", "hpe"],
hpattack: ["hpattack", "hpa"], hpattack: ["hpattack", "hpa"],
hpdecay: ["hpdecay", "hpd"], hpdecay: ["hpdecay", "hpd"],
hpsustain: ["hpsustain", "hpsus"], hpsustain: ["hpsustain", "hpsus"],
hprelease: ["hprelease", "hpr"], hprelease: ["hprelease", "hpr"],
hcutoff: (value: number, resonance?: number) => { hcutoff: function (self: SoundEvent, value: number, resonance?: number) {
this.updateValue("hcutoff", value); self.updateValue("hcutoff", value);
if (resonance) { if (resonance) {
this.updateValue("hresonance", resonance); self.updateValue("hresonance", resonance);
} }
return this; return self;
}, },
hpf: (value: number, resonance?: number) => { hpf: function (self: SoundEvent, value: number, resonance?: number) {
this.updateValue("hcutoff", value); self.updateValue("hcutoff", value);
if (resonance) { if (resonance) {
this.updateValue("hresonance", resonance); self.updateValue("hresonance", resonance);
} }
return this; return self;
}, },
hpq: (value: number) => { hpq: function (self: SoundEvent, value: number) {
this.updateValue("hresonance", value); self.updateValue("hresonance", value);
return this; return self;
}, },
hpadsr: (depth: number, a: number, d: number, s: number, r: number) => { hpadsr: function (
this.updateValue("hpenv", depth); self: SoundEvent,
this.updateValue("hpattack", a); depth: number,
this.updateValue("hpdecay", d); a: number,
this.updateValue("hpsustain", s); d: number,
this.updateValue("hprelease", r); s: number,
return this; r: number,
) {
self.updateValue("hpenv", depth);
self.updateValue("hpattack", a);
self.updateValue("hpdecay", d);
self.updateValue("hpsustain", s);
self.updateValue("hprelease", r);
return self;
}, },
hpad: (depth: number, a: number, d: number) => { hpad: function (self: SoundEvent, depth: number, a: number, d: number) {
this.updateValue("hpenv", depth); self.updateValue("hpenv", depth);
this.updateValue("hpattack", a); self.updateValue("hpattack", a);
this.updateValue("hpdecay", d); self.updateValue("hpdecay", d);
this.updateValue("hpsustain", 0); self.updateValue("hpsustain", 0);
this.updateValue("hprelease", 0); self.updateValue("hprelease", 0);
return this; return self;
}, },
bpenv: ["bpenv", "bpe"], bpenv: ["bpenv", "bpe"],
bpattack: ["bpattack", "bpa"], bpattack: ["bpattack", "bpa"],
bpdecay: ["bpdecay", "bpd"], bpdecay: ["bpdecay", "bpd"],
bpsustain: ["bpsustain", "bps"], bpsustain: ["bpsustain", "bps"],
bprelease: ["bprelease", "bpr"], bprelease: ["bprelease", "bpr"],
bandf: (value: number, resonance?: number) => { bandf: function (self: SoundEvent, value: number, resonance?: number) {
this.updateValue("bandf", value); self.updateValue("bandf", value);
if (resonance) { if (resonance) {
this.updateValue("bandq", resonance); self.updateValue("bandq", resonance);
} }
return this; return self;
}, },
bpf: (value: number, resonance?: number) => { bpf: function (self: SoundEvent, value: number, resonance?: number) {
this.updateValue("bandf", value); self.updateValue("bandf", value);
if (resonance) { if (resonance) {
this.updateValue("bandq", resonance); self.updateValue("bandq", resonance);
} }
return this; return self;
}, },
bandq: ["bandq", "bpq"], bandq: ["bandq", "bpq"],
bpadsr: (depth: number, a: number, d: number, s: number, r: number) => { bpadsr: function (
this.updateValue("bpenv", depth); self: SoundEvent,
this.updateValue("bpattack", a); depth: number,
this.updateValue("bpdecay", d); a: number,
this.updateValue("bpsustain", s); d: number,
this.updateValue("bprelease", r); s: number,
return this; r: number,
) {
self.updateValue("bpenv", depth);
self.updateValue("bpattack", a);
self.updateValue("bpdecay", d);
self.updateValue("bpsustain", s);
self.updateValue("bprelease", r);
return self;
}, },
bpad: (depth: number, a: number, d: number) => { bpad: function (self: SoundEvent, depth: number, a: number, d: number) {
this.updateValue("bpenv", depth); self.updateValue("bpenv", depth);
this.updateValue("bpattack", a); self.updateValue("bpattack", a);
this.updateValue("bpdecay", d); self.updateValue("bpdecay", d);
this.updateValue("bpsustain", 0); self.updateValue("bpsustain", 0);
this.updateValue("bprelease", 0); self.updateValue("bprelease", 0);
return this; return self;
}, },
vib: ["vib"], vib: ["vib"],
vibmod: ["vibmod"], vibmod: ["vibmod"],
fm: (value: number | string) => { fm: function (self: SoundEvent, value: number | string) {
if (typeof value === "number") { if (typeof value === "number") {
this.values["fmi"] = value; self.values["fmi"] = value;
} else { } else {
let values = value.split(":"); let values = value.split(":");
this.values["fmi"] = parseFloat(values[0]); self.values["fmi"] = parseFloat(values[0]);
if (values.length > 1) this.values["fmh"] = parseFloat(values[1]); if (values.length > 1) self.values["fmh"] = parseFloat(values[1]);
} }
return this; return self;
}, },
loop: ["loop"], loop: ["loop"],
loopBegin: ["loopBegin", "loopb"], loopBegin: ["loopBegin", "loopb"],
@ -236,13 +276,13 @@ export class SoundEvent extends AudibleEvent {
begin: ["begin"], begin: ["begin"],
end: ["end"], end: ["end"],
gain: ["gain"], gain: ["gain"],
dbgain: (value: number) => { dbgain: function (self: SoundEvent, value: number) {
this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return this; return self;
}, },
db: (value: number) => { db: function (self: SoundEvent, value: number) {
this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return this; return self;
}, },
velocity: ["velocity", "vel"], velocity: ["velocity", "vel"],
pan: ["pan"], pan: ["pan"],
@ -263,59 +303,74 @@ export class SoundEvent extends AudibleEvent {
roomlp: ["roomlp", "rlp"], roomlp: ["roomlp", "rlp"],
roomdim: ["roomdim", "rdim"], roomdim: ["roomdim", "rdim"],
sound: ["s", "sound"], sound: ["s", "sound"],
size: (value: number) => { size: function (self: SoundEvent, value: number) {
this.updateValue("roomsize", value); self.updateValue("roomsize", value);
return this; return self;
}, },
sz: (value: number) => { sz: function (self: SoundEvent, value: number) {
this.updateValue("roomsize", value); self.updateValue("roomsize", value);
return this; return self;
}, },
comp: ["compressor", "cmp"], comp: ["compressor", "cmp"],
ratio: (value: number) => { ratio: function (self: SoundEvent, value: number) {
this.updateValue("compressorRatio", value); self.updateValue("compressorRatio", value);
return this; return self;
}, },
knee: (value: number) => { knee: function (self: SoundEvent, value: number) {
this.updateValue("compressorKnee", value); self.updateValue("compressorKnee", value);
return this; return self;
}, },
compAttack: (value: number) => { compAttack: function (self: SoundEvent, value: number) {
this.updateValue("compressorAttack", value); self.updateValue("compressorAttack", value);
return this; return self;
}, },
compRelease: (value: number) => { compRelease: function (self: SoundEvent, value: number) {
this.updateValue("compressorRelease", value); self.updateValue("compressorRelease", value);
return this; return self;
}, },
stretch: (beat: number) => { stretch: function (self: SoundEvent, beat: number) {
this.updateValue("unit", "c"); self.updateValue("unit", "c");
this.updateValue("speed", 1 / beat); self.updateValue("speed", 1 / beat);
this.updateValue("cut", beat); self.updateValue("cut", beat);
return this; return self;
}, },
}; };
constructor(sound: string | string[] | SoundParams, public app: Editor) { constructor(
sound: string | string[] | SoundParams,
public app: Editor,
) {
super(app); super(app);
this.nudge = app.dough_nudge / 100; this.nudge = app.dough_nudge / 100;
for (const [methodName, keys] of Object.entries(this.methodMap)) { for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) {
if (Symbol.iterator in Object(keys)) { if (typeof keys === "object" && Symbol.iterator in Object(keys)) {
for (const key of keys as string[]) { for (const key of keys as string[]) {
// @ts-ignore // Using arrow function to maintain 'this' context
this[key] = (value: number) => this.updateValue(keys[0], value); this[key] = (value: number) => this.updateValue(keys[0], value);
} }
} else { } else {
// @ts-ignore // @ts-ignore
this[methodName] = keys; this[methodName] = (...args) => keys(this, ...args);
} }
} }
// for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) {
// if (typeof keys === "object" && Symbol.iterator in Object(keys)) {
// for (const key of keys as string[]) {
// // @ts-ignore
// this[key] = (value: number) => this.updateValue(this, keys[0], value);
// }
// } else {
// // @ts-ignore
// this[methodName] = keys;
// }
// }
this.values = this.processSound(sound); this.values = this.processSound(sound);
} }
private processSound = ( private processSound = (
sound: string | string[] | SoundParams | SoundParams[] sound: string | string[] | SoundParams | SoundParams[],
): SoundParams => { ): SoundParams => {
if (Array.isArray(sound) && typeof sound[0] === "string") { if (Array.isArray(sound) && typeof sound[0] === "string") {
const s: string[] = []; const s: string[] = [];
@ -331,12 +386,10 @@ export class SoundEvent extends AudibleEvent {
s, s,
n: n.length > 0 ? n : undefined, n: n.length > 0 ? n : undefined,
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true,
}; };
} else if (typeof sound === "object") { } else if (typeof sound === "object") {
const validatedObj: SoundParams = { const validatedObj: SoundParams = {
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true,
...(sound as Partial<SoundParams>), ...(sound as Partial<SoundParams>),
}; };
return validatedObj; return validatedObj;
@ -349,23 +402,13 @@ export class SoundEvent extends AudibleEvent {
s, s,
n, n,
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn), dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true,
}; };
} else { } else {
return { s: sound, dur: 0.5, analyze: true }; return { s: sound, dur: 0.5 };
} }
} }
}; };
private updateValue<T>(
key: string,
value: T | T[] | SoundParams[] | null
): this {
if (value == null) return this;
this.values[key] = value;
return this;
}
// ================================================================================ // ================================================================================
// AbstactEvent overrides // AbstactEvent overrides
// ================================================================================ // ================================================================================
@ -395,7 +438,7 @@ export class SoundEvent extends AudibleEvent {
(event.key as number) || "C4", (event.key as number) || "C4",
(event.pitch as number) || 0, (event.pitch as number) || 0,
(event.parsedScale as number[]) || event.scale || "MAJOR", (event.parsedScale as number[]) || event.scale || "MAJOR",
(event.octave as number) || 0 (event.octave as number) || 0,
); );
event.note = note; event.note = note;
event.freq = midiToFreq(note); event.freq = midiToFreq(note);
@ -407,38 +450,6 @@ export class SoundEvent extends AudibleEvent {
this.values.freq = newArrays.freq; this.values.freq = newArrays.freq;
}; };
public chord = (value: string) => {
const chord = parseChord(value);
return this.updateValue("note", chord);
};
public invert = (howMany: number = 0) => {
if (this.values.chord) {
let notes = this.values.chord.map(
(obj: { [key: string]: number }) => obj.note
);
notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12;
}
const chord = notes.map((note: number) => {
return { note: note, freq: midiToFreq(note) };
});
return this.updateValue("chord", chord);
} else {
return this;
}
};
public note = (value: number | string | null) => {
if (typeof value === "string") {
return this.updateValue("note", noteNameToMidi(value));
} else if (typeof value == null || value == undefined) {
return this.updateValue("note", 0).updateValue("gain", 0);
} else {
return this.updateValue("note", value);
}
};
out = (orbit?: number | number[]): void => { out = (orbit?: number | number[]): void => {
if (orbit) this.values["orbit"] = orbit; if (orbit) this.values["orbit"] = orbit;
const events = objectWithArraysToArrayOfObjects(this.values, [ const events = objectWithArraysToArrayOfObjects(this.values, [
@ -456,7 +467,7 @@ export class SoundEvent extends AudibleEvent {
} }
superdough( superdough(
filteredEvent, filteredEvent,
this.nudge - this.app.clock.deviation, this.nudge - this.app.clock.deadline,
filteredEvent.dur filteredEvent.dur
); );
} }
@ -482,7 +493,7 @@ export class SoundEvent extends AudibleEvent {
address: oscAddress, address: oscAddress,
port: oscPort, port: oscPort,
message: event, message: event,
timetag: Math.round(Date.now() + this.nudge - this.app.clock.deviation), timetag: Math.round(Date.now() + this.nudge - this.app.clock.deadline),
} as OSCMessage); } as OSCMessage);
} }
}; };

View File

@ -29,7 +29,7 @@ export class Player extends AbstractEvent {
input: string | number | Generator<number>, input: string | number | Generator<number>,
options: InputOptions, options: InputOptions,
public app: Editor, public app: Editor,
zid: string = "" zid: string = "",
) { ) {
super(app); super(app);
this.options = options; this.options = options;
@ -159,7 +159,7 @@ export class Player extends AbstractEvent {
if (this.areWeThereYet()) { if (this.areWeThereYet()) {
const event = this.next() as Pitch | Chord | ZRest; const event = this.next() as Pitch | Chord | ZRest;
const noteLengthInSeconds = this.app.clock.convertPulseToSecond( const noteLengthInSeconds = this.app.clock.convertPulseToSecond(
event.duration * 4 * this.app.clock.ppqn event.duration * 4 * this.app.clock.ppqn,
); );
if (event instanceof Pitch) { if (event instanceof Pitch) {
const obj = event.getExisting( const obj = event.getExisting(
@ -169,7 +169,7 @@ export class Player extends AbstractEvent {
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale" "parsedScale",
) as SoundParams; ) as SoundParams;
if (event.sound) name = event.sound as string; if (event.sound) name = event.sound as string;
if (event.soundIndex) obj.n = event.soundIndex as number; if (event.soundIndex) obj.n = event.soundIndex as number;
@ -184,14 +184,14 @@ export class Player extends AbstractEvent {
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale" "parsedScale",
); );
}) as SoundParams[]; }) as SoundParams[];
const add = { dur: noteLengthInSeconds } as SoundParams; const add = { dur: noteLengthInSeconds } as SoundParams;
if (name) add.s = name; if (name) add.s = name;
let sound = arrayOfObjectsToObjectWithArrays( let sound = arrayOfObjectsToObjectWithArrays(
pitches, pitches,
add add,
) as SoundParams; ) as SoundParams;
return new SoundEvent(sound, this.app); return new SoundEvent(sound, this.app);
} else if (event instanceof ZRest) { } else if (event instanceof ZRest) {
@ -212,7 +212,7 @@ export class Player extends AbstractEvent {
"key", "key",
"scale", "scale",
"octave", "octave",
"parsedScale" "parsedScale",
) as MidiParams; ) as MidiParams;
if (event instanceof Pitch) { if (event instanceof Pitch) {
if (event.soundIndex) obj.channel = event.soundIndex as number; if (event.soundIndex) obj.channel = event.soundIndex as number;
@ -245,6 +245,12 @@ export class Player extends AbstractEvent {
return this; return this;
} }
tonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) {
// @ts-ignore
if (this.atTheBeginning()) this.ziffers.tonnetz(transform, tonnetz);
return this;
}
triadTonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) { triadTonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.triadTonnetz(transform, tonnetz); if (this.atTheBeginning()) this.ziffers.triadTonnetz(transform, tonnetz);
return this; return this;

View File

@ -20,7 +20,7 @@ ${makeExample(
"Velocity manipulated by a counter", "Velocity manipulated by a counter",
` `
beat(.5)::snd('cp').vel($(1)%10 / 10).out()`, beat(.5)::snd('cp').vel($(1)%10 / 10).out()`,
true true,
)} )}
## Amplitude Enveloppe ## Amplitude Enveloppe
@ -50,7 +50,7 @@ beat(.25)::smooth(sound('sawtooth')
beat(.25)::smooth(sound('sawtooth') beat(.25)::smooth(sound('sawtooth')
.note([50,57,55,60].add(12).beat(1.5))).out(); .note([50,57,55,60].add(12).beat(1.5))).out();
`, `,
true true,
)}; )};
Sometimes, using a full ADSR envelope is a bit overkill. There are other simpler controls to manipulate the envelope like the <ic>.ad</ic> method: Sometimes, using a full ADSR envelope is a bit overkill. There are other simpler controls to manipulate the envelope like the <ic>.ad</ic> method:
@ -70,9 +70,8 @@ beat(.25)::smooth(sound('sawtooth')
beat(.25)::smooth(sound('sawtooth') beat(.25)::smooth(sound('sawtooth')
.note([50,57,55,60].add(12).beat(1.5))).out(); .note([50,57,55,60].add(12).beat(1.5))).out();
`, `,
true true,
)}; )};
`} `;
};

View File

@ -22,7 +22,7 @@ ${makeExample(
beat(1) && sound('bd').out() beat(1) && sound('bd').out()
beat(0.5) && sound('hh').out() beat(0.5) && sound('hh').out()
`, `,
true true,
)} )}
These commands, in plain english, can be translated to: These commands, in plain english, can be translated to:
@ -38,7 +38,7 @@ ${makeExample(
beat(1) && sound('bd').coarse(0.25).room(0.5).orbit(2).out(); beat(1) && sound('bd').coarse(0.25).room(0.5).orbit(2).out();
beat(0.5) && sound('hh').delay(0.25).delaytime(0.125).out(); beat(0.5) && sound('hh').delay(0.25).delaytime(0.125).out();
`, `,
true true,
)} )}
Now, it translates as follows: Now, it translates as follows:
@ -53,11 +53,13 @@ If you remove <ic>beat</ic> instruction, you will end up with a deluge of kick d
To play a sound, you always need the <ic>.out()</ic> method at the end of your chain. THis method tells **Topos** to send the chain to the audio engine. The <ic>.out</ic> method can take an optional argument to send the sound to a numbered effect bus, from <ic>0</ic> to <ic>n</ic> : To play a sound, you always need the <ic>.out()</ic> method at the end of your chain. THis method tells **Topos** to send the chain to the audio engine. The <ic>.out</ic> method can take an optional argument to send the sound to a numbered effect bus, from <ic>0</ic> to <ic>n</ic> :
${makeExample("Using the .out method", ${makeExample(
"Using the .out method",
` `
// Playing a clap on the third bus (0-indexed) // Playing a clap on the third bus (0-indexed)
beat(1)::sound('cp').out(2) beat(1)::sound('cp').out(2)
`, true `,
true,
)} )}
Try to remove <ic>.out</ic>. You will see that no sound is playing at all! Try to remove <ic>.out</ic>. You will see that no sound is playing at all!
@ -67,7 +69,7 @@ Try to remove <ic>.out</ic>. You will see that no sound is playing at all!
- Sounds are **composed** by adding qualifiers/parameters that modify the sound or synthesizer you have picked (_e.g_ <ic>sound('...').blabla(...)..something(...).out()</ic>. Think of it as _audio chains_. - Sounds are **composed** by adding qualifiers/parameters that modify the sound or synthesizer you have picked (_e.g_ <ic>sound('...').blabla(...)..something(...).out()</ic>. Think of it as _audio chains_.
${makeExample( ${makeExample(
'Complex sonic object', "Complex sonic object",
` `
beat(1) :: sound('pad').n(1) beat(1) :: sound('pad').n(1)
.begin(rand(0, 0.4)) .begin(rand(0, 0.4))
@ -75,7 +77,7 @@ beat(1) :: sound('pad').n(1)
.size(0.9).room(0.9) .size(0.9).room(0.9)
.velocity(0.25) .velocity(0.25)
.pan(usine()).release(2).out()`, .pan(usine()).release(2).out()`,
true true,
)} )}
## Picking a specific sound ## Picking a specific sound
@ -104,7 +106,7 @@ ${makeExample(
` `
beat(1) && sound('kick').n([1,2,3,4,5,6,7,8].pick()).out() beat(1) && sound('kick').n([1,2,3,4,5,6,7,8].pick()).out()
`, `,
true true,
)} )}
You can also use the <ic>:</ic> to pick a sample number directly from the <ic>sound</ic> function: You can also use the <ic>:</ic> to pick a sample number directly from the <ic>sound</ic> function:
@ -114,7 +116,7 @@ You can also use the <ic>:</ic> to pick a sample number directly from the <ic>so
` `
beat(1) && sound('kick:3').out() beat(1) && sound('kick:3').out()
`, `,
true true,
)} )}
You can use any number to pick a sound. Don't be afraid of using a number too big. If the number exceeds the number of available samples, it will simply wrap around and loop infinitely over the folder. Let's demonstrate this by using the mouse over a very large sample folder: You can use any number to pick a sound. Don't be afraid of using a number too big. If the number exceeds the number of available samples, it will simply wrap around and loop infinitely over the folder. Let's demonstrate this by using the mouse over a very large sample folder:
@ -124,7 +126,7 @@ ${makeExample(
` `
// Move your mouse to change the sample being used! // Move your mouse to change the sample being used!
beat(.25) && sound('ST09').n(Math.floor(mouseX())).out()`, beat(.25) && sound('ST09').n(Math.floor(mouseX())).out()`,
true true,
)} )}
@ -147,14 +149,18 @@ There is a special method to choose the _orbit_ that your sound is going to use:
You can play a sound _dry_ and another sound _wet_. Take a look at this example where the reverb is only affecting one of the sounds: You can play a sound _dry_ and another sound _wet_. Take a look at this example where the reverb is only affecting one of the sounds:
${makeExample("Dry and wet", ` ${makeExample(
"Dry and wet",
`
// This sound is dry // This sound is dry
beat(1)::sound('hh').out() beat(1)::sound('hh').out()
// This sound is wet (reverb) // This sound is wet (reverb)
beat(2)::sound('cp').orbit(2).room(0.5).size(8).out() beat(2)::sound('cp').orbit(2).room(0.5).size(8).out()
`, true)} `,
true,
)}
## The art of chaining ## The art of chaining
@ -168,10 +174,9 @@ beat(0.25) && sound('fhh')
.room(0.9).size(0.9).gain(1) .room(0.9).size(0.9).gain(1)
.cutoff(usine(1/2) * 5000) .cutoff(usine(1/2) * 5000)
.out()`, .out()`,
true true,
)} )}
Most audio parameters can be used both for samples and synthesizers. This is quite unconventional if you are familiar with a more traditional music software. Most audio parameters can be used both for samples and synthesizers. This is quite unconventional if you are familiar with a more traditional music software.
`} `;
};

View File

@ -23,9 +23,8 @@ ${makeExample(
beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me
beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out() beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out()
`, `,
true true,
)}; )};
`} `;
};

View File

@ -28,7 +28,7 @@ ${makeExample(
` `
beat(2)::snd('cp').room(0.5).size(4).out() beat(2)::snd('cp').room(0.5).size(4).out()
`, `,
true true,
)}; )};
## Delay ## Delay
@ -42,10 +42,13 @@ A good sounding delay unit that can go into feedback territory. Use it without m
| <ic>delayfeedback</ic> | delayfb | Delay feedback (between <ic>0</ic> and <ic>1</ic>) | | <ic>delayfeedback</ic> | delayfb | Delay feedback (between <ic>0</ic> and <ic>1</ic>) |
${makeExample( ${makeExample(
"Who doesn't like delay?", ` "Who doesn't like delay?",
`
beat(2)::snd('cp').delay(0.5).delaytime(0.75).delayfb(0.8).out() beat(2)::snd('cp').delay(0.5).delaytime(0.75).delayfb(0.8).out()
beat(4)::snd('snare').out() beat(4)::snd('snare').out()
beat(1)::snd('kick').out()`, true)} beat(1)::snd('kick').out()`,
true,
)}
## Phaser ## Phaser
@ -56,13 +59,17 @@ beat(1)::snd('kick').out()`, true)}
| <ic>phaserSweep</ic> | <ic>phassweep</ic> | Phaser frequency sweep (in hertz) | | <ic>phaserSweep</ic> | <ic>phassweep</ic> | Phaser frequency sweep (in hertz) |
| <ic>phaserCenter</ic> | <ic>phascenter</ic> | Phaser center frequency (default to 1000) | | <ic>phaserCenter</ic> | <ic>phascenter</ic> | Phaser center frequency (default to 1000) |
${makeExample("Super cool phaser lick", ` ${makeExample(
"Super cool phaser lick",
`
rhythm(.5, 7, 8)::sound('wt_stereo') rhythm(.5, 7, 8)::sound('wt_stereo')
.phaser(0.75).phaserSweep(3000) .phaser(0.75).phaserSweep(3000)
.phaserCenter(1500).phaserDepth(1) .phaserCenter(1500).phaserDepth(1)
.note([0, 1, 2, 3, 4, 5, 6].scale('pentatonic', 50).beat(0.25)) .note([0, 1, 2, 3, 4, 5, 6].scale('pentatonic', 50).beat(0.25))
.room(0.5).size(4).out() .room(0.5).size(4).out()
`, true)} `,
true,
)}
## Distorsion, saturation, destruction ## Distorsion, saturation, destruction
@ -81,6 +88,7 @@ ${makeExample(
beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me
beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out() beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out()
`, `,
true true,
)}; )};
`} `;
};

View File

@ -38,7 +38,7 @@ beat(.5)::snd('pad').begin(0.2)
.room(0.8).size(0.5) .room(0.8).size(0.5)
.clip(1).out() .clip(1).out()
`, `,
true true,
)}; )};
@ -46,37 +46,53 @@ beat(.5)::snd('pad').begin(0.2)
Let's play with the <ic>speed</ic> parameter to control the pitch of sample playback: Let's play with the <ic>speed</ic> parameter to control the pitch of sample playback:
${makeExample("Controlling the playback speed", ` ${makeExample(
"Controlling the playback speed",
`
beat(0.5)::sound('notes') beat(0.5)::sound('notes')
.speed([1,2,3,4].palindrome().beat(0.5)).out() .speed([1,2,3,4].palindrome().beat(0.5)).out()
`, true)} `,
true,
)}
It also works by using negative values. It reverses the playback: It also works by using negative values. It reverses the playback:
${makeExample("Playing samples backwards", ` ${makeExample(
"Playing samples backwards",
`
beat(0.5)::sound('notes') beat(0.5)::sound('notes')
.speed(-[1,2,3,4].palindrome().beat(0.5)).out() .speed(-[1,2,3,4].palindrome().beat(0.5)).out()
`, true)} `,
true,
)}
Of course you can play melodies using samples: Of course you can play melodies using samples:
${makeExample("Playing melodies using samples", ` ${makeExample(
"Playing melodies using samples",
`
beat(0.5)::sound('notes') beat(0.5)::sound('notes')
.room(0.5).size(4) .room(0.5).size(4)
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.5)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.5)).out()
`, true)} `,
true,
)}
## Panning ## Panning
To pan samples, use the <ic>.pan</ic> method with a number between <ic>0</ic> and <ic>1</ic>. To pan samples, use the <ic>.pan</ic> method with a number between <ic>0</ic> and <ic>1</ic>.
${makeExample("Playing melodies using samples", ` ${makeExample(
"Playing melodies using samples",
`
beat(0.25)::sound('notes') beat(0.25)::sound('notes')
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out()
`, true)} `,
true,
)}
## Looping over a sample ## Looping over a sample
@ -84,13 +100,17 @@ beat(0.25)::sound('notes')
Using <ic>loop</ic> (<ic>1</ic> for looping), <ic>loopBegin</ic> and <ic>loopEnd</ic> (between <ic>0</ic> and <ic>1</ic>), you can loop over the length of a sample. It can be super effective to create granular effects. Using <ic>loop</ic> (<ic>1</ic> for looping), <ic>loopBegin</ic> and <ic>loopEnd</ic> (between <ic>0</ic> and <ic>1</ic>), you can loop over the length of a sample. It can be super effective to create granular effects.
${makeExample("Granulation using loop", ` ${makeExample(
"Granulation using loop",
`
beat(0.25)::sound('fikea').loop(1) beat(0.25)::sound('fikea').loop(1)
.lpf(ir(2000, 5000)) .lpf(ir(2000, 5000))
.loopBegin(0).loopEnd(r(0, 1)) .loopBegin(0).loopEnd(r(0, 1))
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out()
`, true)} `,
true,
)}
## Stretching a sample ## Stretching a sample
@ -111,34 +131,45 @@ Sometimes, you will find it necessary to cut a sample. It can be because the sam
Know about the <ic>begin</ic> and <ic>end</ic> parameters. They are not related to the sampler itself, but to the length of the event you are playing. Let's cut the granular example: Know about the <ic>begin</ic> and <ic>end</ic> parameters. They are not related to the sampler itself, but to the length of the event you are playing. Let's cut the granular example:
${makeExample("Cutting a sample using end", ` ${makeExample(
"Cutting a sample using end",
`
beat(0.25)::sound('notes') beat(0.25)::sound('notes')
.end(usine(1/2)/0.5) .end(usine(1/2)/0.5)
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out()
`, true)} `,
true,
)}
You can also use <ic>clip</ic> to cut the sample everytime a new sample comes in: You can also use <ic>clip</ic> to cut the sample everytime a new sample comes in:
${makeExample("Cutting a sample using end", ` ${makeExample(
"Cutting a sample using end",
`
beat(0.125)::sound('notes') beat(0.125)::sound('notes')
.cut(1) .cut(1)
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.125) .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.125)
+ [-12,12].beat()).out() + [-12,12].beat()).out()
`, true)} `,
true,
)}
## Adding vibrato to samples ## Adding vibrato to samples
You can add vibrato to any sample using <ic>vib</ic> and <ic>vibmod</ic>: You can add vibrato to any sample using <ic>vib</ic> and <ic>vibmod</ic>:
${makeExample("Adding vibrato to a sample", ` ${makeExample(
"Adding vibrato to a sample",
`
beat(1)::sound('fhang').vib([1, 2, 4].bar()).vibmod([0.5, 2].beat()).out() beat(1)::sound('fhang').vib([1, 2, 4].bar()).vibmod([0.5, 2].beat()).out()
`, true)} `,
true,
)}
`}
`;
};

View File

@ -22,7 +22,7 @@ The code you enter in any of the scripts is evaluated in strict mode. This tells
- **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 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( - **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" "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! )}. 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 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! - **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!
@ -42,7 +42,7 @@ beat(1) :: snd('bd').out()
//// beat(1) :: snd('bd').out() //// beat(1) :: snd('bd').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -52,7 +52,7 @@ ${makeExample(
beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out() beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
// (true) ? log('very true') : log('very false') // (true) ? log('very true') : log('very false')
`, `,
false false,
)} )}
@ -63,7 +63,7 @@ ${makeExample(
beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out() beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
!beat(2) :: beat(0.5) :: snd('clap').out() !beat(2) :: beat(0.5) :: snd('clap').out()
`, `,
false false,
)} )}
# About crashes and bugs # About crashes and bugs
@ -76,7 +76,7 @@ ${makeExample(
// This is crashing. See? No harm! // This is crashing. See? No harm!
qjldfqsdklqsjdlkqjsdlqkjdlksjd qjldfqsdklqsjdlkqjsdlqkjdlksjd
`, `,
true true,
)} )}
`; `;

View File

@ -18,22 +18,22 @@ The Topos interface is designed on a simple concept: _scripts_ and _universes_.
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. 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( - **the global script** (${key_shortcut(
"Ctrl + G" "Ctrl + G",
)}): _Evaluated for every clock pulse_. The central piece, acting as the conductor for all the other scripts. You can also jam directly from the global script to test your ideas before pushing them to a separate script. You can also access that script using the ${key_shortcut( )}): _Evaluated for every clock pulse_. The central piece, acting as the conductor for all the other scripts. You can also jam directly from the global script to test your ideas before pushing them to a separate script. You can also access that script using the ${key_shortcut(
"F10" "F10",
)} key. )} key.
- **the local scripts** (${key_shortcut( - **the local scripts** (${key_shortcut(
"Ctrl + L" "Ctrl + L",
)}): _Evaluated on demand_. Local scripts are used to store anything too complex to sit in the global script. It can be a musical process, a whole section of your composition, a complex controller that you've built for your hardware, etc... You can also switch to one of the local scripts by using the function keys (${key_shortcut( )}): _Evaluated on demand_. Local scripts are used to store anything too complex to sit in the global script. It can be a musical process, a whole section of your composition, a complex controller that you've built for your hardware, etc... You can also switch to one of the local scripts by using the function keys (${key_shortcut(
"F1" "F1",
)} to ${key_shortcut("F9")}). )} to ${key_shortcut("F9")}).
- **the init script** (${key_shortcut( - **the init script** (${key_shortcut(
"Ctrl + I" "Ctrl + I",
)}): _Evaluated on program load_. Used to set up the software the session to the desired state before playing, for example changing bpm or to initialize global variables (See Functions). You can also access that script using the ${key_shortcut( )}): _Evaluated on program load_. Used to set up the software the session to the desired state before playing, for example changing bpm or to initialize global variables (See Functions). You can also access that script using the ${key_shortcut(
"F11" "F11",
)} key. )} key.
- **the note file** (${key_shortcut( - **the note file** (${key_shortcut(
"Ctrl + N" "Ctrl + N",
)}): _Not evaluated_. Used to store your thoughts or commentaries about the session you are currently playing. It is nothing more than a scratchpad really! )}): _Not evaluated_. Used to store your thoughts or commentaries about the session you are currently playing. It is nothing more than a scratchpad really!
@ -43,7 +43,7 @@ ${makeExample(
beat(1) :: script(1) // Calling local script n°1 beat(1) :: script(1) // Calling local script n°1
flip(4) :: beat(.5) :: script(2) // Calling script n°2 flip(4) :: beat(.5) :: script(2) // Calling script n°2
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -54,7 +54,7 @@ beat(1) :: script([1, 3, 5].pick())
flip(4) :: beat([.5, .25].beat(16)) :: script( flip(4) :: beat([.5, .25].beat(16)) :: script(
[5, 6, 7, 8].beat()) [5, 6, 7, 8].beat())
`, `,
false false,
)} )}
### Navigating the interface ### Navigating the interface
@ -81,7 +81,7 @@ There are some useful functions to help you manage your scripts:
A set of files is called a _universe_. You can switch between universes immediately immediately 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" "Ctrl + B",
)}. 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. )}. 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: There are some useful functions to help you manage your universes:

View File

@ -14,10 +14,10 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
| Shortcut | Key | Description | | Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|**Start/Pause** transport|${key_shortcut( |**Start/Pause** transport|${key_shortcut(
"Ctrl + P" "Ctrl + P",
)}|Start or pause audio playback| )}|Start or pause audio playback|
|**Stop** the transport |${key_shortcut( |**Stop** the transport |${key_shortcut(
"Ctrl + S" "Ctrl + S",
)}|Stop and rewind audio playback| )}|Stop and rewind audio playback|
### Moving in the interface ### Moving in the interface
@ -26,15 +26,15 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|Universe switch|${key_shortcut("Ctrl + B")}|Switch to a new universe| |Universe switch|${key_shortcut("Ctrl + B")}|Switch to a new universe|
|Global Script|${key_shortcut("Ctrl + G")} or ${key_shortcut( |Global Script|${key_shortcut("Ctrl + G")} or ${key_shortcut(
"F10" "F10",
)}|Switch to global script | )}|Switch to global script |
|Local scripts|${key_shortcut("Ctrl + L")} or ${key_shortcut( |Local scripts|${key_shortcut("Ctrl + L")} or ${key_shortcut(
"F11" "F11",
)}|Switch to local scripts | )}|Switch to local scripts |
|Init script|${key_shortcut("Ctrl + L")}|Switch to init script| |Init script|${key_shortcut("Ctrl + L")}|Switch to init script|
|Note File|${key_shortcut("Ctrl + N")}|Switch to note file| |Note File|${key_shortcut("Ctrl + N")}|Switch to note file|
|Local Script|${key_shortcut("F1")} to ${key_shortcut( |Local Script|${key_shortcut("F1")} to ${key_shortcut(
"F9" "F9",
)}|Switch to a specific local script| )}|Switch to a specific local script|
|Documentation|${key_shortcut("Ctrl + D")}|Open the documentation| |Documentation|${key_shortcut("Ctrl + D")}|Open the documentation|
@ -44,10 +44,10 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|Evaluate|${key_shortcut("Ctrl + Enter")}| Evaluate the current script | |Evaluate|${key_shortcut("Ctrl + Enter")}| Evaluate the current script |
|Local Eval|${key_shortcut("Ctrl + F1")} to ${key_shortcut( |Local Eval|${key_shortcut("Ctrl + F1")} to ${key_shortcut(
"Ctrl + F9" "Ctrl + F9",
)}|Local File Evaluation| )}|Local File Evaluation|
|Force Eval|${key_shortcut( |Force Eval|${key_shortcut(
"Ctrl + Shift + Enter" "Ctrl + Shift + Enter",
)}|Force evaluation of the current script| )}|Force evaluation of the current script|
### Special ### Special
@ -55,13 +55,14 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
| Shortcut | Key | Description | | Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|Vim Mode|${key_shortcut("Ctrl + V")}| Switch between Vim and Normal Mode| |Vim Mode|${key_shortcut("Ctrl + V")}| Switch between Vim and Normal Mode|
|Maximize|${key_shortcut("Ctrl + M")}| Show/Hide the interface|
# Keyboard Fill # Keyboard Fill
By pressing the ${key_shortcut( By pressing the ${key_shortcut(
"Alt" "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( )} 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" "Alt",
)} when playing this example: )} when playing this example:
${makeExample( ${makeExample(
@ -69,7 +70,7 @@ ${makeExample(
` `
beat(fill() ? 1/4 : 1/2)::sound('cp').out() beat(fill() ? 1/4 : 1/2)::sound('cp').out()
`, `,
true true,
)} )}
`; `;

View File

@ -27,7 +27,7 @@ beat(.25) :: sound('sine')
.pan(r(0, 1)) .pan(r(0, 1))
.room(0.35).size(4).out() .room(0.35).size(4).out()
`, `,
true true,
)} )}
<br> <br>
@ -46,7 +46,7 @@ beat(.25) :: sound('sine')
.delay(0.5).delayt(1/6).delayfb(0.2) .delay(0.5).delayt(1/6).delayfb(0.2)
.note(noteX()) .note(noteX())
.room(0.35).size(4).out()`, .room(0.35).size(4).out()`,
true true,
)} )}
## Mouse and Arrays ## Mouse and Arrays
@ -64,7 +64,7 @@ log([1,2,3,4].mouseX())
log([4,5,6,7].mouseY()) log([4,5,6,7].mouseY())
`, `,
true true,
)} )}

View File

@ -8,13 +8,13 @@ export const introduction = (application: Editor): string => {
# Welcome # Welcome
Welcome to the **Topos** documentation. 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" "Ctrl + D",
)}. Press again to make the documentation disappear. Contributions are much appreciated! The documentation [lives here](https://github.com/Bubobubobubobubo/topos/tree/main/src/documentation). )}. 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( ${makeExample(
"Welcome! Eval to get started", "Welcome! Eval to get started",
examples[Math.floor(Math.random() * examples.length)], examples[Math.floor(Math.random() * examples.length)],
true true,
)} )}
# What is Topos? # What is Topos?
@ -30,7 +30,7 @@ rhythm(.25, [5, 7].beat(2), 8) :: sound(['hc', 'fikea', 'hat'].pick(1))
.db(-ir(1,8)).speed([1,[0.5, 2].pick()]).room(0.5).size(3).o(4).out() .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') beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare')
.n(0).speed([1, 0.5]).o(4).out()`, .n(0).speed([1, 0.5]).o(4).out()`,
false false,
)} )}
${makeExample( ${makeExample(
@ -47,7 +47,7 @@ beat(.25)::snd('sine')
.delay(0.5).delayt(0.25).delayfb(0.7) // Delay .delay(0.5).delayt(0.25).delayfb(0.7) // Delay
.room(0.5).size(8) // Reverb .room(0.5).size(8) // Reverb
.out()`, .out()`,
false false,
)} )}
${makeExample( ${makeExample(
@ -58,7 +58,7 @@ beat(.5) :: sound('sid').n($(2))
beat(.25) :: sound('sid').note( beat(.25) :: sound('sid').note(
[34, 36, 41].beat(.25) + [[0,-24].pick(),12].beat()) [34, 36, 41].beat(.25) + [[0,-24].pick(),12].beat())
.room(0.9).size(0.9).n(4).out()`, .room(0.9).size(0.9).n(4).out()`,
false 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 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). 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).
@ -66,7 +66,13 @@ Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Tele
## Demo Songs ## Demo Songs
Reloading the application will get you one random song example to study every time. Press ${key_shortcut( Reloading the application will get you one random song example to study every time. Press ${key_shortcut(
"F5" "F5",
)} and listen to them all! The demo songs are also used a bit everywhere in the documentation to illustrate some of the working principles :). )} and listen to them all! The demo songs are also used a bit everywhere in the documentation to illustrate some of the working principles :).
## Support
<p>You can <a href='https://ko-fi.com/I2I2RSBHF' target='_blank'><img height='36' style='display: inline; border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a> to support the development :) </p>
`; `;
}; };

View File

@ -12,7 +12,8 @@ ${makeExample(
"Method chaining", "Method chaining",
` `
beat(1)::sound('bd').speed(2).lpf(500).out() beat(1)::sound('bd').speed(2).lpf(500).out()
`, true `,
true,
)} )}
Method chains become fun if you add just a little bit of complexity to them. You can start to add conditions, start to register complex chains to be re-used later on, etc.. We will not remind you how to write basic chains. The whole documentation is full of examples! Let's explore more delicate patterns! Method chains become fun if you add just a little bit of complexity to them. You can start to add conditions, start to register complex chains to be re-used later on, etc.. We will not remind you how to write basic chains. The whole documentation is full of examples! Let's explore more delicate patterns!
@ -29,7 +30,8 @@ register('juxrev', n=>n.pan([0, 1]).speed([1, -1]))
// Using our new abstraction // Using our new abstraction
beat(1)::sound('fhh').juxrev().out() beat(1)::sound('fhh').juxrev().out()
`, true `,
true,
)} )}
This is an extremely powerful construct. For example, you can use it to create synthesizer presets and reuse them later on. You can also define parameters for your registered functions. For example: This is an extremely powerful construct. For example, you can use it to create synthesizer presets and reuse them later on. You can also define parameters for your registered functions. For example:
@ -48,7 +50,8 @@ register('sub', (n,x=4,y=80)=>n.ad(0, .25)
// Using it with an arpeggio // Using it with an arpeggio
rhythm(.25, [6, 8].beat(), 12)::sound('sine') rhythm(.25, [6, 8].beat(), 12)::sound('sine')
.note([0, 2, 4, 5].scale('minor', 50).beat(0.5)) .note([0, 2, 4, 5].scale('minor', 50).beat(0.5))
.sub(8).out()`, true .sub(8).out()`,
true,
)} )}
@ -65,7 +68,7 @@ beat(.5) && sound('fhh')
.odds(1/4, s => s.speed(irand(1,4))) .odds(1/4, s => s.speed(irand(1,4)))
.rarely(s => s.room(0.5).size(8).speed(0.5)) .rarely(s => s.room(0.5).size(8).speed(0.5))
.out()`, .out()`,
true true,
)} )}
${makeExample( ${makeExample(
"Chance to play a random note", "Chance to play a random note",
@ -77,7 +80,7 @@ beat(.5) && sound('pluck').note(60)
.note(62) .note(62)
.room(0.5).size(3) .room(0.5).size(3)
.out()`, .out()`,
false false,
)} )}
There is a growing collection of probability and chance methods you can use: There is a growing collection of probability and chance methods you can use:
@ -111,7 +114,7 @@ ${makeExample(
.often(n => n.note+=4) .often(n => n.note+=4)
.sometimes(s => s.velocity(irand(50,100))) .sometimes(s => s.velocity(irand(50,100)))
.out()`, .out()`,
true true,
)}; )};
## Ziffers ## Ziffers
@ -134,7 +137,7 @@ z1('s 0 5 7 0 3 7 0 2 7 0 1 7 0 1 6 5 4 3 2')
.odds(1/2, n => n.speed(0.5)) .odds(1/2, n => n.speed(0.5))
.room(0.5).size(0.5).out() .room(0.5).size(0.5).out()
`, `,
true true,
)}; )};
`; `;
}; };

View File

@ -35,7 +35,7 @@ beat(.5)::snd('pad').begin(0.2)
.room(0.8).size(0.5) .room(0.8).size(0.5)
.clip(1).out() .clip(1).out()
`, `,
true true,
)}; )};
${makeExample( ${makeExample(
@ -70,7 +70,7 @@ beat(.5) && snd('sawtooth')
.resonance(0.9).freq([100,150].pick()) .resonance(0.9).freq([100,150].pick())
.out() .out()
`, `,
true true,
)}; )};

View File

@ -19,7 +19,7 @@ ${makeExample(
` `
beat(1) :: script(1) beat(1) :: script(1)
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -27,7 +27,7 @@ ${makeExample(
` `
beat(1) :: script(1, 3, 5) beat(1) :: script(1, 3, 5)
`, `,
false false,
)} )}
## Math functions ## Math functions
@ -48,7 +48,7 @@ ${makeExample(
beat(.5) :: delay(usine(.125) * 80, () => sound('east').out()) beat(.5) :: delay(usine(.125) * 80, () => sound('east').out())
beat(.5) :: delay(50, () => sound('east').out()) beat(.5) :: delay(50, () => sound('east').out())
`, `,
true true,
)} )}
- <ic>delayr(ms: number, nb: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds, repeated a given number of times. - <ic>delayr(ms: number, nb: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds, repeated a given number of times.
@ -59,7 +59,7 @@ ${makeExample(
beat(1) :: delayr(50, 4, () => sound('east').speed([0.5,.25].beat()).out()) beat(1) :: delayr(50, 4, () => sound('east').speed([0.5,.25].beat()).out())
flip(2) :: beat(2) :: delayr(150, 4, () => sound('east').speed([0.5,.25].beat() * 4).out()) flip(2) :: beat(2) :: delayr(150, 4, () => sound('east').speed([0.5,.25].beat() * 4).out())
`, `,
true true,
)}; )};
`; `;
}; };

View File

@ -969,7 +969,7 @@ export const inlineHoveringTips = hoverTooltip(
return { dom }; return { dom };
}, },
}; };
} },
); );
export const toposCompletions = (context: CompletionContext) => { export const toposCompletions = (context: CompletionContext) => {

View File

@ -24,7 +24,7 @@ ${makeExample(
` `
beat(1) && active_notes() && sound('sine').chord(active_notes()).out() beat(1) && active_notes() && sound('sine').chord(active_notes()).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -34,7 +34,7 @@ ${makeExample(
active_notes().beat(0.5)+[12,24].beat(0.25) active_notes().beat(0.5)+[12,24].beat(0.25)
).cutoff(300 + usine(1/4) * 2000).out() ).cutoff(300 + usine(1/4) * 2000).out()
`, `,
false false,
)} )}
@ -46,7 +46,7 @@ ${makeExample(
beat(0.25) && sticky_notes() && sound('arp') beat(0.25) && sticky_notes() && sound('arp')
.note(sticky_notes().palindrome().beat(0.25)).out() .note(sticky_notes().palindrome().beat(0.25)).out()
`, `,
true true,
)} )}
* <ic>last_note(channel?: number)</ic>: returns the last note that has been received. Returns 60 if no other notes have been received. * <ic>last_note(channel?: number)</ic>: returns the last note that has been received. Returns 60 if no other notes have been received.
@ -58,7 +58,7 @@ ${makeExample(
.vib([1, 3, 5].beat(1)) .vib([1, 3, 5].beat(1))
.vibmod([1,3,2,4].beat(2)).out() .vibmod([1,3,2,4].beat(2)).out()
`, `,
false false,
)} )}
* <ic>buffer()</ic>: return true if there are notes in the buffer. * <ic>buffer()</ic>: return true if there are notes in the buffer.
@ -69,7 +69,7 @@ ${makeExample(
` `
beat(1) && buffer() && sound('sine').note(buffer_note()).out() beat(1) && buffer() && sound('sine').note(buffer_note()).out()
`, `,
false false,
)} )}
@ -87,7 +87,7 @@ ${makeExample(
` `
beat(0.5) && sound('arp').note(last_cc(74)).out() beat(0.5) && sound('arp').note(last_cc(74)).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -105,7 +105,7 @@ beat(last_cc(74)/127*.5) :: sound('sine')
.sustain(last_cc(74)/127*.25) .sustain(last_cc(74)/127*.25)
.out() .out()
`, `,
false false,
)} )}
@ -128,7 +128,7 @@ Topos can output scales to external keyboards lighted keys using the following f
${makeExample( ${makeExample(
"Show scale on external keyboard", "Show scale on external keyboard",
`show_scale("F","aeolian",0,4)`, `show_scale("F","aeolian",0,4)`,
true true,
)} )}
${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)} ${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)}

View File

@ -17,7 +17,7 @@ Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio w
${makeExample( ${makeExample(
"Modulating the speed of a sample player using a sine LFO", "Modulating the speed of a sample player using a sine LFO",
`beat(.25) && snd('cp').speed(1 + usine(0.25) * 2).out()`, `beat(.25) && snd('cp').speed(1 + usine(0.25) * 2).out()`,
true true,
)}; )};
- <ic>triangle(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>-1</ic> and <ic>1</ic>. - <ic>triangle(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a triangle oscillation between <ic>-1</ic> and <ic>1</ic>.
@ -26,7 +26,7 @@ ${makeExample(
${makeExample( ${makeExample(
"Modulating the speed of a sample player using a triangle LFO", "Modulating the speed of a sample player using a triangle LFO",
`beat(.25) && snd('cp').speed(1 + utriangle(0.25) * 2).out()`, `beat(.25) && snd('cp').speed(1 + utriangle(0.25) * 2).out()`,
true true,
)} )}
- <ic>saw(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>-1</ic> and <ic>1</ic>. - <ic>saw(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a sawtooth-like oscillation between <ic>-1</ic> and <ic>1</ic>.
@ -35,7 +35,7 @@ ${makeExample(
${makeExample( ${makeExample(
"Modulating the speed of a sample player using a saw LFO", "Modulating the speed of a sample player using a saw LFO",
`beat(.25) && snd('cp').speed(1 + usaw(0.25) * 2).out()`, `beat(.25) && snd('cp').speed(1 + usaw(0.25) * 2).out()`,
true true,
)} )}
- <ic>square(freq: number = 1, times: number = 1, offset: number= 0, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>-1</ic> and <ic>1</ic>. You can also control the duty cycle using the <ic>duty</ic> parameter. - <ic>square(freq: number = 1, times: number = 1, offset: number= 0, duty: number = .5): number</ic>: returns a square wave oscillation between <ic>-1</ic> and <ic>1</ic>. You can also control the duty cycle using the <ic>duty</ic> parameter.
@ -44,7 +44,7 @@ ${makeExample(
${makeExample( ${makeExample(
"Modulating the speed of a sample player using a square LFO", "Modulating the speed of a sample player using a square LFO",
`beat(.25) && snd('cp').speed(1 + usquare(0.25, 0, 0.25) * 2).out()`, `beat(.25) && snd('cp').speed(1 + usquare(0.25, 0, 0.25) * 2).out()`,
true true,
)}; )};
- <ic>noise(times: number = 1)</ic>: returns a random value between -1 and 1. - <ic>noise(times: number = 1)</ic>: returns a random value between -1 and 1.
@ -52,8 +52,8 @@ ${makeExample(
${makeExample( ${makeExample(
"Modulating the speed of a sample player using noise", "Modulating the speed of a sample player using noise",
`beat(.25) && snd('cp').speed(1 + noise() * 2).out()`, `beat(.25) && snd('cp').speed(1 + noise() * 2).out()`,
true true,
)}; )};
` `;
} };

View File

@ -19,7 +19,7 @@ ${makeExample(
// Playing each script for 8 bars in succession // Playing each script for 8 bars in succession
script([1,2,3,4].bar(8)) script([1,2,3,4].bar(8))
`, `,
true true,
)} )}
You can also give a specific duration to each section using <ic>.dur</ic>: You can also give a specific duration to each section using <ic>.dur</ic>:
@ -29,7 +29,7 @@ ${makeExample(
` `
script([1,2,3,4].dur(8, 2, 16, 4)) script([1,2,3,4].dur(8, 2, 16, 4))
`, `,
true 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_. - **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_.
@ -44,7 +44,7 @@ ${makeExample(
` `
flip(4) :: beat(1) :: snd('kick').out() flip(4) :: beat(1) :: snd('kick').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -56,7 +56,7 @@ flip(2.5, 75) :: beat(.25) :: snd('click')
flip(2.5) :: beat(.5) :: snd('bd').out() flip(2.5) :: beat(.5) :: snd('bd').out()
beat(.25) :: sound('hat').end(0.1).cutoff(1200).pan(usine(1/4)).out() beat(.25) :: sound('hat').end(0.1).cutoff(1200).pan(usine(1/4)).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -68,7 +68,7 @@ if (flip(4, 75)) {
beat(.5) :: snd('snare').out() beat(.5) :: snd('snare').out()
} }
`, `,
true true,
)} )}
<ic>flip</ic> is extremely powerful and is used internally for a lot of other Topos functions. You can also use it to think about **longer durations** spanning over multiple bars. Here is a silly composition that is using <ic>flip</ic> to generate a 4 bars long pattern. <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.
@ -93,7 +93,7 @@ if (flip(8)) {
beat(.5)::snd('diphone').end(0.5).n([1,2,3,4].pick()).out() beat(.5)::snd('diphone').end(0.5).n([1,2,3,4].pick()).out()
} }
`, `,
true true,
)} )}
You can use it everywhere to spice things up, including as a method parameter picker: You can use it everywhere to spice things up, including as a method parameter picker:
@ -103,7 +103,7 @@ ${makeExample(
` `
beat(.5)::snd(flip(2) ? 'kick' : 'hat').out() beat(.5)::snd(flip(2) ? 'kick' : 'hat').out()
`, `,
true true,
)} )}
- <ic>flipbar(n: number = 1)</ic>: this method works just like <ic>flip</ic> but counts in bars instead of beats. It allows you to think about even larger time cycles. You can also pair it with regular <ic>flip</ic> for writing complex and long-spanning algorithmic beats. - <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.
@ -122,7 +122,7 @@ function b() {
flipbar(2) && a() flipbar(2) && a()
flipbar(3) && b() flipbar(3) && b()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
"Alternating over four bars", "Alternating over four bars",
@ -131,7 +131,7 @@ flipbar(2)
? beat(.5) && snd(['kick', 'hh'].beat(1)).out() ? beat(.5) && snd(['kick', 'hh'].beat(1)).out()
: beat(.5) && snd(['east', 'east:2'].beat(1)).out() : beat(.5) && snd(['east', 'east:2'].beat(1)).out()
`, `,
false false,
)}; )};
@ -155,7 +155,7 @@ if (onbar([1,2], 4)) {
rhythm(.5, 1, 7) :: snd('jvbass').n(2).out(); rhythm(.5, 1, 7) :: snd('jvbass').n(2).out();
rhythm(.5, 2, 7) :: snd('snare').n(3).out(); rhythm(.5, 2, 7) :: snd('snare').n(3).out();
}`, }`,
true true,
)} )}
`; `;

View File

@ -15,9 +15,9 @@ You can use Topos to play MIDI thanks to the [WebMIDI API](https://developer.moz
Your web browser is capable of sending and receiving MIDI information through the [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API). The support for MIDI on browsers is a bit shaky. Please, take some time to configure and test. To our best knowledge, **Chrome** is currently leading on this feature, followed closely by **Firefox**. The other major web browsers are also starting to support this API. **There are two important functions for configuration:** Your web browser is capable of sending and receiving MIDI information through the [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API). The support for MIDI on browsers is a bit shaky. Please, take some time to configure and test. To our best knowledge, **Chrome** is currently leading on this feature, followed closely by **Firefox**. The other major web browsers are also starting to support this API. **There are two important functions for configuration:**
- <ic>midi_outputs()</ic>: prints the list of available MIDI devices on the screen. You will have to open the web console using ${key_shortcut( - <ic>midi_outputs()</ic>: prints the list of available MIDI devices on the screen. You will have to open the web console using ${key_shortcut(
"Ctrl+Shift+I" "Ctrl+Shift+I",
)} or sometimes ${key_shortcut( )} or sometimes ${key_shortcut(
"F12" "F12",
)}. You can also open it from the menu of your web browser. **Note:** close the docs to see it printed. )}. You can also open it from the menu of your web browser. **Note:** close the docs to see it printed.
@ -26,7 +26,7 @@ ${makeExample(
` `
midi_outputs() midi_outputs()
`, `,
true true,
)} )}
- <ic>midi_output(output_name: string)</ic>: enter your desired output to connect to it. - <ic>midi_output(output_name: string)</ic>: enter your desired output to connect to it.
@ -36,7 +36,7 @@ ${makeExample(
` `
midi_output("MIDI Rocket-Trumpet") midi_output("MIDI Rocket-Trumpet")
`, `,
true true,
)} )}
That's it! You are now ready to play with MIDI. That's it! You are now ready to play with MIDI.
@ -54,7 +54,7 @@ ${makeExample(
// => midi_output("MIDI Bus 1") // => midi_output("MIDI Bus 1")
rhythm(.5, 5, 8) :: midi(50).out() rhythm(.5, 5, 8) :: midi(50).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -63,7 +63,7 @@ ${makeExample(
// MIDI Note 50, Velocity 50 + LFO, Channel 0 // MIDI Note 50, Velocity 50 + LFO, Channel 0
rhythm(.5, 5, 8) :: midi(50, 50 + usine(.5) * 20, 0).out() rhythm(.5, 5, 8) :: midi(50, 50 + usine(.5) * 20, 0).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -72,7 +72,7 @@ ${makeExample(
// MIDI Note 50, Velocity 50 + LFO, Channel 0 // MIDI Note 50, Velocity 50 + LFO, Channel 0
rhythm(.5, 5, 8) :: midi({note: 50, velocity: 50 + usine(.5) * 20, channel: 0}).out() rhythm(.5, 5, 8) :: midi({note: 50, velocity: 50 + usine(.5) * 20, channel: 0}).out()
`, `,
false false,
)} )}
We can now have some fun and starting playing a small piano piece: We can now have some fun and starting playing a small piano piece:
@ -86,7 +86,7 @@ beat(.25) && midi([64, 76].pick()).sustain(0.05).out()
beat(.75) && midi([64, 67, 69].beat()).sustain(0.05).out() beat(.75) && midi([64, 67, 69].beat()).sustain(0.05).out()
beat(.25) && midi([64, 67, 69].beat() + 24).sustain(0.05).out() beat(.25) && midi([64, 67, 69].beat() + 24).sustain(0.05).out()
`, `,
true true,
)} )}
## Control and Program Changes ## Control and Program Changes
@ -99,7 +99,7 @@ ${makeExample(
control_change({control: [24,25].pick(), value: irand(1,120), channel: 1}) control_change({control: [24,25].pick(), value: irand(1,120), channel: 1})
control_change({control: [30,35].pick(), value: irand(1,120) / 2, channel: 1}) control_change({control: [30,35].pick(), value: irand(1,120) / 2, channel: 1})
`, `,
true true,
)} )}
- <ic>program_change(program: number, channel: number)</ic>: send a MIDI Program Change. This function takes two arguments to specify the program and the channel (_e.g._ <ic>program_change(1, 1)</ic>). - <ic>program_change(program: number, channel: number)</ic>: send a MIDI Program Change. This function takes two arguments to specify the program and the channel (_e.g._ <ic>program_change(1, 1)</ic>).
@ -109,7 +109,7 @@ ${makeExample(
` `
program_change([1,2,3,4,5,6,7,8].pick(), 1) program_change([1,2,3,4,5,6,7,8].pick(), 1)
`, `,
true true,
)} )}
@ -123,7 +123,7 @@ ${makeExample(
` `
sysex(0x90, 0x40, 0x7f) sysex(0x90, 0x40, 0x7f)
`, `,
true true,
)} )}
## Clock ## Clock
@ -135,7 +135,7 @@ ${makeExample(
` `
beat(.25) && midi_clock() // Sending clock to MIDI device from the global buffer beat(.25) && midi_clock() // Sending clock to MIDI device from the global buffer
`, `,
true true,
)} )}
## Using midi with ziffers ## Using midi with ziffers
@ -147,7 +147,7 @@ ${makeExample(
` `
z1('0 2 e 5 2 q 4 2').midi().port(2).channel(4).out() z1('0 2 e 5 2 q 4 2').midi().port(2).channel(4).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -155,7 +155,7 @@ ${makeExample(
` `
z1('(0 2 e 5 2):0 (4 2):1').midi().out() z1('(0 2 e 5 2):0 (4 2):1').midi().out()
`, `,
true true,
)} )}
`; `;

View File

@ -14,6 +14,21 @@ Topos is an experimental web based algorithmic sequencer programmed by **BuboBub
Topos is a free and open-source software distributed under [GPL-3.0](https://github.com/Bubobubobubobubo/Topos/blob/main/LICENSE) licence. We welcome all contributions and ideas. You can find the source code on [GitHub](https://github.com/Bubobubobubobubo/topos). You can also join us on [Discord](https://discord.gg/dnUTPbu6bN) to discuss about the project and live coding in general. Topos is a free and open-source software distributed under [GPL-3.0](https://github.com/Bubobubobubobubo/Topos/blob/main/LICENSE) licence. We welcome all contributions and ideas. You can find the source code on [GitHub](https://github.com/Bubobubobubobubo/topos). You can also join us on [Discord](https://discord.gg/dnUTPbu6bN) to discuss about the project and live coding in general.
## Support the project
You can support the project by making a small donation on [Kofi](https://ko-fi.com/Manage/).
<div style="display: flex; justify-content: center;">
<iframe
id='kofiframe'
src='https://ko-fi.com/raphaelbubo/?hidefeed=true&widget=true&embed=true&preview=true'
style='border:none;width:40%;padding:4px;background:#f9f9f9;'
height='590'
title='raphaelbubo'>
</iframe>
</div>
## Credits ## Credits
- Felix Roos for the [SuperDough](https://www.npmjs.com/package/superdough) audio engine. - Felix Roos for the [SuperDough](https://www.npmjs.com/package/superdough) audio engine.

View File

@ -7,7 +7,7 @@ export const bonus = (application: Editor): string => {
return ` return `
# Bonus features # Bonus features
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. Some features have been included as a bonus. These features are often about patterning over things that are not directly related to sound: pictures, video, etc.
## Hydra Visual Live Coding ## Hydra Visual Live Coding
@ -15,19 +15,19 @@ Some features are here "just for fun" or "just because I can". They are not very
<warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning> <warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning>
</div> </div>
[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. [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 an analog synthesizer patching metaphor to encourage live coding complex shaders. 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.
${makeExample( ${makeExample(
"Hydra integration", "Hydra integration",
`beat(4) :: app.hydra.osc(3, 0.5, 2).out()`, `beat(4) :: hydra.osc(3, 0.5, 2).out()`,
true true,
)} )}
You may feel like it's doing nothing! Press ${key_shortcut( Close the documentation to see the effect: ${key_shortcut(
"Ctrl+D" "Ctrl+D",
)} to close the documentation. **Boom, all shiny!** )}! **Boom, all shiny!**
Be careful not to call <ic>app.hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>mod()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches. Be careful not to call <ic>hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>beat()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches.
Stopping **Hydra** is simple: Stopping **Hydra** is simple:
@ -35,16 +35,35 @@ ${makeExample(
"Stopping Hydra", "Stopping Hydra",
` `
beat(4) :: stop_hydra() // this one beat(4) :: stop_hydra() // this one
beat(4) :: app.hydra.hush() // or this one beat(4) :: 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/):
### Changing the resolution
You can change Hydra resolution using this simple method:
${makeExample(
"Changing Hydra resolution",
`hydra.setResolution(1024, 768)`,
true,
)}
### Documentation
I won't teach Hydra. You can find some great resources directly on the [Hydra website](https://hydra.ojack.xyz/):
- [Hydra interactive documentation](https://hydra.ojack.xyz/docs/) - [Hydra interactive documentation](https://hydra.ojack.xyz/docs/)
- [List of Hydra Functions](https://hydra.ojack.xyz/api/) - [List of Hydra Functions](https://hydra.ojack.xyz/api/)
- [Source code on GitHub](https://github.com/hydra-synth/hydra) - [Source code on GitHub](https://github.com/hydra-synth/hydra)
### The Hydra namespace
In comparison with the basic Hydra editor, please note that you have to prefix all Hydra functions with <ic>hydra.</ic> to avoid conflicts with Topos functions. For example, <ic>osc()</ic> becomes <ic>hydra.osc()</ic>.
${makeExample("Hydra namespace", `hydra.voronoi(20).out()`, true)}
## GIF player ## 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: 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:
@ -62,8 +81,8 @@ beat(0.25)::gif({
rotation: ir(1, 360), // Rotation (in degrees) rotation: ir(1, 360), // Rotation (in degrees)
posX: ir(1,1200), // CSS Horizontal Position posX: ir(1,1200), // CSS Horizontal Position
posY: ir(1, 800), // CSS Vertical Position posY: ir(1, 800), // CSS Vertical Position
`, true `,
true,
)} )}
`; `;
}; };

View File

@ -5,7 +5,19 @@ export const oscilloscope = (application: Editor): string => {
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
return `# Oscilloscope return `# Oscilloscope
You can turn on the oscilloscope to generate interesting visuals or to inspect audio. Use the <ic>scope()</ic> function to turn it on and off. The oscilloscope is off by default. You can turn on the oscilloscope to generate interesting visuals or to inspect audio. Use the <ic>scope()</ic> function to turn on/off the oscilloscope and to configure it. The oscilloscope is off by default.
You need to manually feed the scope with the sounds you want to inspect:
${makeExample(
"Feeding a sine to the oscilloscope",
`
beat(1)::sound('sine').freq(200).ad(0, .2).scope().out()
`,
true,
)}
Here is a layout of the scope configuration options:
${makeExample( ${makeExample(
"Oscilloscope configuration", "Oscilloscope configuration",
@ -23,7 +35,7 @@ scope({
refresh: 1 // refresh rate (in pulses) refresh: 1 // refresh rate (in pulses)
}) })
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -44,7 +56,7 @@ scope({enabled: true, thickness: 8,
color: ['purple', 'green', 'random'].beat(), color: ['purple', 'green', 'random'].beat(),
size: 0.5, fftSize: 2048}) size: 0.5, fftSize: 2048})
`, `,
true true,
)} )}
Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :) Note that these values can be patterned as well! You can transform the oscilloscope into its own light show if you want. The picture is not stable anyway so you won't have much use of it for precision work :)

View File

@ -14,7 +14,7 @@ ${makeExample(
` `
beat(1)::sound('kick').out() beat(1)::sound('kick').out()
`, `,
true true,
)} )}
can be turned into something more interesting like this easily: can be turned into something more interesting like this easily:
@ -26,7 +26,7 @@ let c = [1,2].dur(3, 1)
beat([1, 0.5, 0.25].dur(0.75, 0.25, 1) / c)::sound(['kick', 'fsoftsnare'].beat(0.75)) beat([1, 0.5, 0.25].dur(0.75, 0.25, 1) / c)::sound(['kick', 'fsoftsnare'].beat(0.75))
.ad(0, .25).shape(usine(1/2)*0.5).speed([1, 2, 4].beat(0.5)).out() .ad(0, .25).shape(usine(1/2)*0.5).speed([1, 2, 4].beat(0.5)).out()
`, `,
true true,
)} )}
@ -44,7 +44,7 @@ beat([1, 0.75].beat(4)) :: sound('cp').out()
beat([0.5, 1].beat(4)) :: sound('kick').out() beat([0.5, 1].beat(4)) :: sound('kick').out()
beat(2)::snd('snare').shape(.5).out() beat(2)::snd('snare').shape(.5).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
"Using beat to create arpeggios", "Using beat to create arpeggios",
@ -62,7 +62,7 @@ beat([.5, .25].beat(0.5)) :: sound('sine')
.delayfb(0.5) .delayfb(0.5)
.out() .out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
"Cool ambiance", "Cool ambiance",
@ -73,7 +73,7 @@ flip(2)::beat(1)::snd('froomy').out()
flip(4)::beat(2)::snd('pad').n(2).shape(.5) flip(4)::beat(2)::snd('pad').n(2).shape(.5)
.orbit(2).room(0.9).size(0.9).release(0.5).out() .orbit(2).room(0.9).size(0.9).release(0.5).out()
`, `,
false false,
)} )}
- <ic>bar(value: number = 1)</ic>: returns the next value every bar (if <ic>value = 1</ic>). Using a larger value will return the next value every <ic>n</ic> bars. - <ic>bar(value: number = 1)</ic>: returns the next value every bar (if <ic>value = 1</ic>). Using a larger value will return the next value every <ic>n</ic> bars.
@ -88,7 +88,7 @@ beat([1/4, 1/2].dur(1.5, 0.5))::sound(['jvbass', 'fikea'].bar())
* [1, 2].bar()) * [1, 2].bar())
.out() .out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -101,7 +101,7 @@ beat([1, 0.5].beat()) :: sound(['bass3'].bar())
.pan(r(0, 1)) .pan(r(0, 1))
.speed([1,2,3].beat()) .speed([1,2,3].beat())
.out() .out()
` `,
)} )}
- <ic>dur(...list: numbers[])</ic> : keeps the same value for a duration of <ic>n</ic> beats corresponding to the <ic>nth</ic> number of the list you provide. - <ic>dur(...list: numbers[])</ic> : keeps the same value for a duration of <ic>n</ic> beats corresponding to the <ic>nth</ic> number of the list you provide.
@ -117,7 +117,7 @@ beat(0.5)::sound('notes').n([1,2].dur(1, 2))
beat(1)::sound(['kick', 'fsnare'].dur(3, 1)) beat(1)::sound(['kick', 'fsnare'].dur(3, 1))
.n([0,3].dur(3, 1)).out() .n([0,3].dur(3, 1)).out()
`, `,
true true,
)} )}
## Manipulating notes and scales ## Manipulating notes and scales
@ -133,7 +133,7 @@ beat(0.25) :: snd('sine')
.key(["F4","F3"].beat(2.0)) .key(["F4","F3"].beat(2.0))
.scale("minor").ad(0, .25).out() .scale("minor").ad(0, .25).out()
`, `,
true true,
)} )}
- <ic>scale(scale: string, base note: number)</ic>: Map each element of the list to the closest note of the slected scale. [0, 2, 3, 5 ].scale("major", 50) returns [50, 52, <ic>54</ic>, 55]. You can use western scale names like (Major, Minor, Minor pentatonic ...) or [zeitler](https://ianring.com/musictheory/scales/traditions/zeitler) scale names. Alternatively you can also use the integers as used by Ian Ring in his [study of scales](https://ianring.com/musictheory/scales/). - <ic>scale(scale: string, base note: number)</ic>: Map each element of the list to the closest note of the slected scale. [0, 2, 3, 5 ].scale("major", 50) returns [50, 52, <ic>54</ic>, 55]. You can use western scale names like (Major, Minor, Minor pentatonic ...) or [zeitler](https://ianring.com/musictheory/scales/traditions/zeitler) scale names. Alternatively you can also use the integers as used by Ian Ring in his [study of scales](https://ianring.com/musictheory/scales/).
@ -145,7 +145,7 @@ beat(1) :: snd('gtr')
.note([0, 5, 2, 1, 7].scale("Major", 52).beat()) .note([0, 5, 2, 1, 7].scale("Major", 52).beat())
.out() .out()
`, `,
true true,
)} )}
- <ic>scaleArp(scale: string, mask: number)</ic>: extrapolate a custom-masked scale from each list elements. [0].scale("major", 3) returns [0,2,4]. <ic>scaleArp</ic> supports the same scales as <ic>scale</ic>. - <ic>scaleArp(scale: string, mask: number)</ic>: extrapolate a custom-masked scale from each list elements. [0].scale("major", 3) returns [0,2,4]. <ic>scaleArp</ic> supports the same scales as <ic>scale</ic>.
@ -157,7 +157,7 @@ beat(1) :: snd('gtr')
.note([0, 5].scaleArp("mixolydian", 3).beat() + 50) .note([0, 5].scaleArp("mixolydian", 3).beat() + 50)
.out() .out()
`, `,
true true,
)} )}
## Iteration using the mouse ## Iteration using the mouse
@ -173,7 +173,7 @@ beat(0.25)::sound('wt_piano')
.room(0.5).size(4).lpad(-2, .2).lpf(500, 0.3) .room(0.5).size(4).lpad(-2, .2).lpf(500, 0.3)
.ad(0, .2).out() .ad(0, .2).out()
`, `,
true true,
)} )}
## Simple data operations ## Simple data operations
@ -191,7 +191,7 @@ beat([1,.5,.25].beat()) :: snd('wt_stereo')
.lpf([500,1000,2000,4000].palindrome().beat()) .lpf([500,1000,2000,4000].palindrome().beat())
.lpad(4, 0, .25).sustain(0.125).out() .lpad(4, 0, .25).sustain(0.125).out()
`, `,
true true,
)} )}
- <ic>random(index: number)</ic>: pick a random element in the given list. - <ic>random(index: number)</ic>: pick a random element in the given list.
@ -210,7 +210,7 @@ beat([.5, 1].rand() / 2) :: snd(
.lpf([5000,3000,2000].pick()) .lpf([5000,3000,2000].pick())
.end(0.5).out() .end(0.5).out()
`, `,
true true,
)} )}
- <ic>pick()</ic>: pick a random element in the list. - <ic>pick()</ic>: pick a random element in the list.
@ -222,7 +222,7 @@ beat(0.25)::sound(['ftabla', 'fwood'].pick())
.speed([1,2,3,4].pick()).ad(0, .125).n(ir(1,10)) .speed([1,2,3,4].pick()).ad(0, .125).n(ir(1,10))
.room(0.5).size(1).out() .room(0.5).size(1).out()
`, `,
true true,
)} )}
- <ic>degrade(amount: number)</ic>: removes _n_% of the list elements. Lists can be degraded as long as one element remains. The amount of degradation is given as a percentage. - <ic>degrade(amount: number)</ic>: removes _n_% of the list elements. Lists can be degraded as long as one element remains. The amount of degradation is given as a percentage.
@ -233,7 +233,7 @@ ${makeExample(
// Tweak the value to degrade this amen break even more! // Tweak the value to degrade this amen break even more!
beat(.25)::snd('amencutup').n([1,2,3,4,5,6,7,8,9].degrade(20).loop($(1))).out() beat(.25)::snd('amencutup').n([1,2,3,4,5,6,7,8,9].degrade(20).loop($(1))).out()
`, `,
true true,
)} )}
- <ic>repeat(amount: number)</ic>: repeat every list elements _n_ times. - <ic>repeat(amount: number)</ic>: repeat every list elements _n_ times.
@ -245,7 +245,7 @@ ${makeExample(
` `
beat(.25)::sound('amencutup').n([1,2,3,4,5,6,7,8].repeat(4).beat(.25)).out() beat(.25)::sound('amencutup').n([1,2,3,4,5,6,7,8].repeat(4).beat(.25)).out()
`, `,
true true,
)} )}
- <ic>loop(index: number)</ic>: loop takes one argument, the _index_. It allows you to iterate over a list using an iterator such as a counter. This is super useful to control how you are accessing values in a list without relying on a temporal method such as <ic>.beat()</ic> or </ic>.bar()</ic>. - <ic>loop(index: number)</ic>: loop takes one argument, the _index_. It allows you to iterate over a list using an iterator such as a counter. This is super useful to control how you are accessing values in a list without relying on a temporal method such as <ic>.beat()</ic> or </ic>.bar()</ic>.
@ -255,7 +255,7 @@ ${makeExample(
` `
beat(1) :: sound('numbers').n([1,2,3,4,5].loop($(3, 10, 2))).out() beat(1) :: sound('numbers').n([1,2,3,4,5].loop($(3, 10, 2))).out()
`, `,
true true,
)} )}
- <ic>shuffle(): this</ic>: shuffles a list! Simple enough! - <ic>shuffle(): this</ic>: shuffles a list! Simple enough!
@ -265,7 +265,7 @@ ${makeExample(
` `
beat(1) :: sound('numbers').n([1,2,3,4,5].shuffle().loop($(1)).out() beat(1) :: sound('numbers').n([1,2,3,4,5].shuffle().loop($(1)).out()
`, `,
true true,
)} )}
- <ic>rotate(steps: number)</ic>: rotate a list to the right _n_ times. The last value become the first, rinse and repeat. - <ic>rotate(steps: number)</ic>: rotate a list to the right _n_ times. The last value become the first, rinse and repeat.
@ -283,7 +283,7 @@ beat(.25) :: snd('sine').fmi([1.99, 2])
.beat(.25)) // while the index changes .beat(.25)) // while the index changes
.out() .out()
`, `,
true true,
)} )}
## Filtering ## Filtering
@ -296,7 +296,7 @@ ${makeExample(
// Remove unique and 100 will repeat four times! // Remove unique and 100 will repeat four times!
beat(1)::snd('sine').sustain(0.1).freq([100,100,100,100,200].unique().beat()).out() beat(1)::snd('sine').sustain(0.1).freq([100,100,100,100,200].unique().beat()).out()
`, `,
true true,
)} )}
## Simple math operations ## Simple math operations

View File

@ -17,7 +17,7 @@ ${makeExample(
` `
rhythm(0.125, 10, 16) :: sound('sid').n(4).note(50 + irand(50, 62) % 8).out() rhythm(0.125, 10, 16) :: sound('sid').n(4).note(50 + irand(50, 62) % 8).out()
`, `,
true true,
)} )}
@ -32,7 +32,7 @@ prob(50) :: script(1);
prob(60) :: script(2); prob(60) :: script(2);
prob(80) :: script(toss() ? script(3) : script(4)) prob(80) :: script(toss() ? script(3) : script(4))
`, `,
true true,
)} )}
- <ic>seed(val: number|string)</ic>: sets the seed of the random number generator. You can use a number or a string. The same seed will always return the same sequence of random numbers. - <ic>seed(val: number|string)</ic>: sets the seed of the random number generator. You can use a number or a string. The same seed will always return the same sequence of random numbers.
@ -64,7 +64,7 @@ ${makeExample(
rarely(4) :: sound('bd').out(); // Rarely in 4 beats is bit less rarely(4) :: sound('bd').out(); // Rarely in 4 beats is bit less
rarely(8) :: sound('east').out(); // Rarely in 8 beats is even less rarely(8) :: sound('east').out(); // Rarely in 8 beats is even less
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -74,7 +74,7 @@ ${makeExample(
often() :: beat(0.5) :: sound('hh').out(); often() :: beat(0.5) :: sound('hh').out();
sometimes() :: onbeat(1,3) :: sound('snare').out(); sometimes() :: onbeat(1,3) :: sound('snare').out();
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -91,8 +91,7 @@ ${makeExample(
.almostNever(n=>n.freq(400)) .almostNever(n=>n.freq(400))
.out() .out()
`, `,
false false,
)} )}
` `;
} };

View File

@ -18,7 +18,7 @@ ${makeExample(
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'], sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'], hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, 'github:tidalcycles/Dirt-Samples/master/');`, }, 'github:tidalcycles/Dirt-Samples/master/');`,
true true,
)} )}
This example is loading two samples from each folder declared in the original repository (in the <ic>strudel.json</ic> file). You can then play with them using the syntax you are already used to: This example is loading two samples from each folder declared in the original repository (in the <ic>strudel.json</ic> file). You can then play with them using the syntax you are already used to:
@ -27,7 +27,7 @@ ${makeExample(
"Playing with the loaded samples", "Playing with the loaded samples",
`rhythm(.5, 5, 8)::sound('bd').n(ir(1,2)).end(1).out() `rhythm(.5, 5, 8)::sound('bd').n(ir(1,2)).end(1).out()
`, `,
true true,
)} )}
Internally, Topos is loading samples using a different technique where sample maps are directly taken from the previously mentioned <ic>strudel.json</ic> file that lives in each repository: Internally, Topos is loading samples using a different technique where sample maps are directly taken from the previously mentioned <ic>strudel.json</ic> file that lives in each repository:
@ -40,7 +40,7 @@ samples("github:tidalcycles/Dirt-Samples/master");
samples("github:Bubobubobubobubo/Dough-Samples/main"); samples("github:Bubobubobubobubo/Dough-Samples/main");
samples("github:Bubobubobubobubo/Dough-Amiga/main"); samples("github:Bubobubobubobubo/Dough-Amiga/main");
`, `,
true true,
)} )}
To learn more about the audio sample loading mechanism, please refer to [this page](https://strudel.tidalcycles.org/learn/samples) written by Felix Roos who has implemented the sample loading mechanism. The API is absolutely identic in Topos! To learn more about the audio sample loading mechanism, please refer to [this page](https://strudel.tidalcycles.org/learn/samples) written by Felix Roos who has implemented the sample loading mechanism. The API is absolutely identic in Topos!
@ -57,9 +57,10 @@ samples("shabda:ocean")
// Use the sound without 'shabda:' // Use the sound without 'shabda:'
beat(1)::sound('ocean').clip(1).out() beat(1)::sound('ocean').clip(1).out()
`, true `,
true,
)} )}
You can also use the <ic>.n</ic> attribute like usual to load a different sample. You can also use the <ic>.n</ic> attribute like usual to load a different sample.
` `;
} };

View File

@ -9,5 +9,5 @@ export const sample_banks = (application: Editor): string => {
There is a <ic>bank</ic> attribute that can help you to sort audio samples from large collections. There is a <ic>bank</ic> attribute that can help you to sort audio samples from large collections.
**AJKPercusyn**, **AkaiLinn**, **AkaiMPC60**, **AkaiXR10**, **AlesisHR16**, **AlesisSR16**, **BossDR110**, **BossDR220**, **BossDR55**, **BossDR550**, **BossDR660**, **CasioRZ1**, **CasioSK1**, **CasioVL1**, **DoepferMS404**, **EmuDrumulator**, **EmuModular**, **EmuSP12**, **KorgDDM110**, **KorgKPR77**, **KorgKR55**, **KorgKRZ**, **KorgM1**, **KorgMinipops**, **KorgPoly800**, **KorgT3**, **Linn9000**, **LinnDrum**, **LinnLM1**, **LinnLM2**, **MFB512**, **MPC1000**, **MoogConcertMateMG1**, **OberheimDMX**, **RhodesPolaris**, **RhythmAce**, **RolandCompurhythm1000**, **RolandCompurhythm78**, **RolandCompurhythm8000**, **RolandD110**, **RolandD70**, **RolandDDR30**, **RolandJD990**, **RolandMC202**, **RolandMC303**, **RolandMT32**, **RolandR8**, **RolandS50**, **RolandSH09**, **RolandSystem100**, **RolandTR505**, **RolandTR606**, **RolandTR626**, **RolandTR707**, **RolandTR727**, **RolandTR808**, **RolandTR909**, **SakataDPM48**, **SequentialCircuitsDrumtracks**, **SequentialCircuitsTom**, **SergeModular**, **SimmonsSDS400**, **SimmonsSDS5**, **SoundmastersR88**, **UnivoxMicroRhythmer12**, **ViscoSpaceDrum**, **XdrumLM8953**, **YamahaRM50**, **YamahaRX21**, **YamahaRX5**, **YamahaRY30**, **YamahaTG33**. **AJKPercusyn**, **AkaiLinn**, **AkaiMPC60**, **AkaiXR10**, **AlesisHR16**, **AlesisSR16**, **BossDR110**, **BossDR220**, **BossDR55**, **BossDR550**, **BossDR660**, **CasioRZ1**, **CasioSK1**, **CasioVL1**, **DoepferMS404**, **EmuDrumulator**, **EmuModular**, **EmuSP12**, **KorgDDM110**, **KorgKPR77**, **KorgKR55**, **KorgKRZ**, **KorgM1**, **KorgMinipops**, **KorgPoly800**, **KorgT3**, **Linn9000**, **LinnDrum**, **LinnLM1**, **LinnLM2**, **MFB512**, **MPC1000**, **MoogConcertMateMG1**, **OberheimDMX**, **RhodesPolaris**, **RhythmAce**, **RolandCompurhythm1000**, **RolandCompurhythm78**, **RolandCompurhythm8000**, **RolandD110**, **RolandD70**, **RolandDDR30**, **RolandJD990**, **RolandMC202**, **RolandMC303**, **RolandMT32**, **RolandR8**, **RolandS50**, **RolandSH09**, **RolandSystem100**, **RolandTR505**, **RolandTR606**, **RolandTR626**, **RolandTR707**, **RolandTR727**, **RolandTR808**, **RolandTR909**, **SakataDPM48**, **SequentialCircuitsDrumtracks**, **SequentialCircuitsTom**, **SergeModular**, **SimmonsSDS400**, **SimmonsSDS5**, **SoundmastersR88**, **UnivoxMicroRhythmer12**, **ViscoSpaceDrum**, **XdrumLM8953**, **YamahaRM50**, **YamahaRX21**, **YamahaRX5**, **YamahaRY30**, **YamahaTG33**.
` `;
} };

View File

@ -1,7 +1,10 @@
import { type Editor } from "../../main"; import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation"; import { makeExampleFactory } from "../../Documentation";
export const samples_to_markdown = (application: Editor, tag_filter?: string) => { export const samples_to_markdown = (
application: Editor,
tag_filter?: string,
) => {
let samples = application.api._all_samples(); let samples = application.api._all_samples();
let markdownList = ""; let markdownList = "";
let keys = Object.keys(samples); let keys = Object.keys(samples);
@ -44,13 +47,11 @@ export const injectAllSamples = (application: Editor): string => {
return generatedPage; return generatedPage;
}; };
export const injectDrumMachineSamples = (application: Editor): string => { export const injectDrumMachineSamples = (application: Editor): string => {
let generatedPage = samples_to_markdown(application, "Machines"); let generatedPage = samples_to_markdown(application, "Machines");
return generatedPage; return generatedPage;
}; };
export const sample_list = (application: Editor): string => { export const sample_list = (application: Editor): string => {
// @ts-ignore // @ts-ignore
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
@ -63,9 +64,13 @@ On this page, you will find an exhaustive list of all the samples currently load
A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with <ic>wt_</ic> will be looped. Look at this demo: A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with <ic>wt_</ic> will be looped. Look at this demo:
${makeExample("Wavetable synthesis made easy :)", ` ${makeExample(
"Wavetable synthesis made easy :)",
`
beat(0.5)::sound('wt_stereo').n([0, 1].pick()).ad(0, .25).out() beat(0.5)::sound('wt_stereo').n([0, 1].pick()).ad(0, .25).out()
`, true)} `,
true,
)}
Pick one folder and spend some time exploring it. There is a lot of different waveforms. Pick one folder and spend some time exploring it. There is a lot of different waveforms.
@ -79,9 +84,12 @@ ${samples_to_markdown(application, "Waveforms")}
A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the <ic>.bank()</ic> parameter like so: A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the <ic>.bank()</ic> parameter like so:
${makeExample( ${makeExample(
"Using a classic drum machine", ` "Using a classic drum machine",
`
beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out() beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out()
`, true)} `,
true,
)}
Here is the complete list of available machines: Here is the complete list of available machines:
@ -111,9 +119,11 @@ ${samples_to_markdown(application, "Amiga")}
A collection of many different amen breaks. Use <ic>.stretch()</ic> to play with these: A collection of many different amen breaks. Use <ic>.stretch()</ic> to play with these:
${makeExample( ${makeExample(
"Stretching an amen break", ` "Stretching an amen break",
`
beat(4)::sound('amen1').stretch(4).out() beat(4)::sound('amen1').stretch(4).out()
`, true, `,
true,
)} )}
The stretch should be adapted based on the length of each amen break. The stretch should be adapted based on the length of each amen break.
@ -130,5 +140,13 @@ Many live coders are expecting to find the Tidal sample library wherever they go
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll"> <div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
${samples_to_markdown(application, "Tidal")} ${samples_to_markdown(application, "Tidal")}
</div> </div>
`
} ## Juliette's voice
This sample pack is only one folder full of french phonems! It sounds super nice.
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
${samples_to_markdown(application, "Juliette")}
</div>
`;
};

View File

@ -17,7 +17,7 @@ ${makeExample(
` `
beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out() beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out()
`, `,
true true,
)} )}
Note that you can also use noise if you do not want to use a periodic oscillator: Note that you can also use noise if you do not want to use a periodic oscillator:
@ -28,7 +28,7 @@ ${makeExample(
` `
beat(.5) && snd(['brown', 'pink', 'white'].beat()).adsr(0,.1,0,0).out() beat(.5) && snd(['brown', 'pink', 'white'].beat()).adsr(0,.1,0,0).out()
`, `,
true true,
)} )}
Two functions are primarily used to control the frequency of the synthesizer: Two functions are primarily used to control the frequency of the synthesizer:
@ -40,7 +40,7 @@ ${makeExample(
` `
beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out() beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -48,7 +48,7 @@ beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out()
` `
beat(.5) && snd('triangle').note([60,"F4"].pick()).out() beat(.5) && snd('triangle').note([60,"F4"].pick()).out()
`, `,
true true,
)} )}
Chords can also played using different parameters: Chords can also played using different parameters:
@ -60,7 +60,7 @@ ${makeExample(
` `
beat(1) && snd('triangle').chord(["C","Em7","Fmaj7","Emin"].beat(2)).adsr(0,.2).out() beat(1) && snd('triangle').chord(["C","Em7","Fmaj7","Emin"].beat(2)).adsr(0,.2).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -68,7 +68,7 @@ ${makeExample(
` `
beat(.5) && snd('triangle').chord(60,64,67,72).invert([1,-3,4,-5].pick()).adsr(0,.2).out() beat(.5) && snd('triangle').chord(60,64,67,72).invert([1,-3,4,-5].pick()).adsr(0,.2).out()
`, `,
true true,
)} )}
## Vibrato ## Vibrato
@ -84,7 +84,7 @@ beat(1) :: sound('triangle')
.vib([1/2, 1, 2, 4].beat()) .vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2)) .vibmod([1,2,4,8].beat(2))
.out()`, .out()`,
true true,
)} )}
## Noise ## Noise
@ -101,7 +101,7 @@ beat(1) :: sound('triangle')
.vib([1/2, 1, 2, 4].beat()) .vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2)) .vibmod([1,2,4,8].beat(2))
.out()`, .out()`,
true true,
)} )}
@ -114,13 +114,13 @@ Controlling the amplitude and duration of the sound can be done using various te
${makeExample( ${makeExample(
"Setting the gain", "Setting the gain",
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, `beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true true,
)} )}
${makeExample( ${makeExample(
"Setting the velocity", "Setting the velocity",
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`, `beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true true,
)} )}
<div class="mt-4 mb-4 lg:grid lg:grid-cols-4 lg:gap-4"> <div class="mt-4 mb-4 lg:grid lg:grid-cols-4 lg:gap-4">
@ -141,7 +141,7 @@ beat(0.5) :: sound('wt_piano')
.freq(100).decay(.2) .freq(100).decay(.2)
.sustain([0.1,0.5].beat(4)) .sustain([0.1,0.5].beat(4))
.out()`, .out()`,
true true,
)} )}
This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The <ic>adsr()</ic> combines all the parameters together. It is a shortcut for setting the ADSR envelope: This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The <ic>adsr()</ic> combines all the parameters together. It is a shortcut for setting the ADSR envelope:
@ -157,7 +157,7 @@ beat(0.5) :: sound('wt_piano')
.adsr(0, .2, [0.1,0.5].beat(4), 0) .adsr(0, .2, [0.1,0.5].beat(4), 0)
.out() .out()
`, `,
true true,
)} )}
- <ic>ad(attack: number, decay: number)</ic>: sets the attack and decay phases, setting sustain and release to <ic>0</ic>. - <ic>ad(attack: number, decay: number)</ic>: sets the attack and decay phases, setting sustain and release to <ic>0</ic>.
@ -171,7 +171,7 @@ beat(0.5) :: sound('wt_piano')
.ad(0, .2) .ad(0, .2)
.out() .out()
`, `,
true true,
)} )}
## Substractive synthesis using filters ## Substractive synthesis using filters
@ -185,7 +185,7 @@ The most basic synthesis technique used since the 1970s is called substractive s
${makeExample( ${makeExample(
"Filtering the high frequencies of an oscillator", "Filtering the high frequencies of an oscillator",
`beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`, `beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`,
true true,
)} )}
These filters all come with their own set of parameters. Note that we are describing the parameters of the three different filter types here. Choose the right parameters depending on the filter type you are using: These filters all come with their own set of parameters. Note that we are describing the parameters of the three different filter types here. Choose the right parameters depending on the filter type you are using:
@ -201,7 +201,7 @@ These filters all come with their own set of parameters. Note that we are descri
${makeExample( ${makeExample(
"Filtering a bass", "Filtering a bass",
`beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`, `beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`,
true true,
)} )}
### Highpass filter ### Highpass filter
@ -214,7 +214,7 @@ ${makeExample(
${makeExample( ${makeExample(
"Filtering a noise source", "Filtering a noise source",
`beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`, `beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`,
true true,
)} )}
### Bandpass filter ### Bandpass filter
@ -227,7 +227,7 @@ ${makeExample(
${makeExample( ${makeExample(
"Sweeping the filter on the same guitar sample", "Sweeping the filter on the same guitar sample",
`beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`, `beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`,
true true,
)} )}
Alternatively, <ic>lpf</ic>, <ic>hpf</ic> and <ic>bpf</ic> can take a second argument, the **resonance**. Alternatively, <ic>lpf</ic>, <ic>hpf</ic> and <ic>bpf</ic> can take a second argument, the **resonance**.
@ -241,7 +241,7 @@ You can also use the <ic>ftype</ic> method to change the filter type (order). Th
${makeExample( ${makeExample(
"Filtering a bass", "Filtering a bass",
`beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`, `beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`,
true true,
)} )}
I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers: I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers:
@ -254,7 +254,7 @@ beat(.5) && snd('sawtooth')
.resonance(0.2).freq([100,150].pick()) .resonance(0.2).freq([100,150].pick())
.out() .out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -265,7 +265,7 @@ beat(.5) :: [100,101].forEach((freq) => sound('square').freq(freq*2).sustain(0.0
beat([.5, .75, 2].beat()) :: [100,101].forEach((freq) => sound('square') beat([.5, .75, 2].beat()) :: [100,101].forEach((freq) => sound('square')
.freq(freq*4 + usquare(2) * 200).sustain(0.125).out()) .freq(freq*4 + usquare(2) * 200).sustain(0.125).out())
beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`, beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`,
false false,
)} )}
${makeExample( ${makeExample(
@ -279,7 +279,7 @@ beat(1/8)::sound('sine')
.freq(mouseX()) .freq(mouseX())
.gain(0.25) .gain(0.25)
.out()`, .out()`,
false false,
)} )}
## Filter envelopes ## Filter envelopes
@ -303,7 +303,7 @@ ${makeExample(
`beat(.5) :: sound('sawtooth').note([48,60].beat()) `beat(.5) :: sound('sawtooth').note([48,60].beat())
.cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2)) .cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2))
.lpenv(-8).lpq(10).out()`, .lpenv(-8).lpq(10).out()`,
true true,
)} )}
### Highpass envelope ### Highpass envelope
@ -323,7 +323,7 @@ ${makeExample(
`beat(.5) :: sound('sawtooth').note([48,60].beat()) `beat(.5) :: sound('sawtooth').note([48,60].beat())
.hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2)) .hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2))
.hpenv(8).hpq(10).out()`, .hpenv(8).hpq(10).out()`,
true true,
)} )}
### Bandpass envelope ### Bandpass envelope
@ -345,7 +345,7 @@ ${makeExample(
.bpa([0.25, 0.125, 0.5].beat(2) * 4) .bpa([0.25, 0.125, 0.5].beat(2) * 4)
.bpenv(-4).release(2).out() .bpenv(-4).release(2).out()
`, `,
true true,
)} )}
@ -363,7 +363,7 @@ beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0]
beat(1) :: sound('kick').n(4).out() beat(1) :: sound('kick').n(4).out()
beat(2) :: sound('snare').out() beat(2) :: sound('snare').out()
beat(.5) :: sound('hh').out()`, beat(.5) :: sound('hh').out()`,
true true,
)} )}
@ -381,7 +381,7 @@ beat(2) :: v('selec', irand(1, 100))
beat(2) :: v('swave', collection.pick()) beat(2) :: v('swave', collection.pick())
beat(0.5) :: sound(v('swave')).n(v('selec')).out() beat(0.5) :: sound(v('swave')).n(v('selec')).out()
`, `,
true true,
)} )}
You can work with them just like with any other waveform. Having so many of them makes them also very useful for generating sound effects, percussive, sounds, etc... You can work with them just like with any other waveform. Having so many of them makes them also very useful for generating sound effects, percussive, sounds, etc...
@ -407,7 +407,7 @@ beat(.25) && snd('triangle').adsr(0.02, 0.1, 0.1, 0.1)
.pan(noise()).note([60,55, 60, 63].beat() + [0, 7].pick()).out() .pan(noise()).note([60,55, 60, 63].beat() + [0, 7].pick()).out()
beat(2) :: sound('cp').room(1).sz(1).out() beat(2) :: sound('cp').room(1).sz(1).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -418,7 +418,7 @@ beat([4].bar()) :: sound('sine').fm('5.2183:4.5').sustain(0.05).out()
beat(.5) :: sound('sine') beat(.5) :: sound('sine')
.fmh([1, 1.75].beat()) .fmh([1, 1.75].beat())
.fmi($(1) % 30).orbit(2).room(0.5).out()`, .fmi($(1) % 30).orbit(2).room(0.5).out()`,
true true,
)} )}
${makeExample( ${makeExample(
@ -432,7 +432,7 @@ beat(0.25) :: sound('sine')
.cutoff(1500).delay(0.5).delayt(0.125) .cutoff(1500).delay(0.5).delayt(0.125)
.delayfb(0.8).fmh(Math.floor(usine(.5) * 4)) .delayfb(0.8).fmh(Math.floor(usine(.5) * 4))
.out()`, .out()`,
true true,
)} )}
**Note:** you can also set the _modulation index_ and the _harmonic ratio_ with the <ic>fm</ic> argument. You will have to feed both as a string: <ic>fm('2:4')</ic>. If you only feed one number, only the _modulation index_ will be updated. **Note:** you can also set the _modulation index_ and the _harmonic ratio_ with the <ic>fm</ic> argument. You will have to feed both as a string: <ic>fm('2:4')</ic>. If you only feed one number, only the _modulation index_ will be updated.
@ -453,7 +453,7 @@ beat(.5) :: sound('sine')
.fmwave('triangle') .fmwave('triangle')
.fmsus(0).fmdec(0.2).out() .fmsus(0).fmdec(0.2).out()
`, `,
true true,
)} )}
## ZzFX ## ZzFX
@ -467,7 +467,7 @@ ${makeExample(
` `
beat(.5) :: sound(['z_sine', 'z_triangle', 'z_sawtooth', 'z_tan', 'z_noise'].beat()).out() beat(.5) :: sound(['z_sine', 'z_triangle', 'z_sawtooth', 'z_tan', 'z_noise'].beat()).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
"Minimalist chiptune", "Minimalist chiptune",
@ -481,7 +481,7 @@ beat(.5) :: sound('z_triangle')
.room(0.5).size(0.9) .room(0.5).size(0.9)
.pitchJumpTime(0.01).out() .pitchJumpTime(0.01).out()
`, `,
true true,
)} )}
It comes with a set of parameters that can be used to tweak the sound. Don't underestimate this synth! It is very powerful for generating anything ranging from chaotic noise sources to lush pads: It comes with a set of parameters that can be used to tweak the sound. Don't underestimate this synth! It is very powerful for generating anything ranging from chaotic noise sources to lush pads:
@ -519,7 +519,7 @@ beat(.25) :: sound('z_tan')
.sustain(0).decay([0.2, 0.1].pick()) .sustain(0).decay([0.2, 0.1].pick())
.out() .out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
"What is happening to me?", "What is happening to me?",
@ -529,7 +529,7 @@ beat(1) :: snd('zzfx').zzfx([
[1.12,,97,.11,.16,.01,4,.77,,,30,.17,,,-1.9,,.01,.67,.2] [1.12,,97,.11,.16,.01,4,.77,,,30,.17,,,-1.9,,.01,.67,.2]
].beat()).out() ].beat()).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
"Les voitures dans le futur", "Les voitures dans le futur",
@ -541,7 +541,7 @@ beat(1) :: sound(['z_triangle', 'z_sine'].pick())
.room(0.9).size(0.9) .room(0.9).size(0.9)
.delayt(0.75).delayfb(0.5).out() .delayt(0.75).delayfb(0.5).out()
`, `,
false false,
)} )}
Note that you can also design sounds [on this website](https://killedbyapixel.github.io/ZzFX/) and copy the generated code in Topos. To do so, please use the <ic>zzfx</ic> method with the generated array: Note that you can also design sounds [on this website](https://killedbyapixel.github.io/ZzFX/) and copy the generated code in Topos. To do so, please use the <ic>zzfx</ic> method with the generated array:
@ -551,7 +551,7 @@ ${makeExample(
beat(2) :: sound('zzfx').zzfx([3.62,,452,.16,.1,.21,,2.5,,,403,.05,.29,,,,.17,.34,.22,.68]).out() beat(2) :: sound('zzfx').zzfx([3.62,,452,.16,.1,.21,,2.5,,,403,.05,.29,,,,.17,.34,.22,.68]).out()
`, `,
true true,
)} )}
# Speech synthesis # Speech synthesis
@ -571,7 +571,7 @@ ${makeExample(
` `
beat(4) :: speak("Hello world!") beat(4) :: speak("Hello world!")
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -579,7 +579,7 @@ ${makeExample(
` `
beat(2) :: speak("Topos!","fr",irand(0,5)) beat(2) :: speak("Topos!","fr",irand(0,5))
`, `,
true true,
)} )}
@ -590,7 +590,7 @@ ${makeExample(
` `
onbeat(4) :: "Foobaba".voice(irand(0,10)).speak() onbeat(4) :: "Foobaba".voice(irand(0,10)).speak()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -603,7 +603,7 @@ ${makeExample(
beat(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).speak() beat(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).speak()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -623,7 +623,7 @@ ${makeExample(
.rate(rand(.4,.6)) .rate(rand(.4,.6))
.speak(); .speak();
`, `,
true true,
)} )}
`; `;
}; };

View File

@ -21,7 +21,7 @@ ${makeExample(
// This code is alternating between 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() beat([1,1/2,1/4,1/8].beat(2)) :: sound('hat').n(0).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -41,7 +41,7 @@ beat(1/3) :: blip(400).pan(r(0,1)).out();
flip(3) :: beat(1/6) :: blip(800).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(); beat([1,0.75].beat(2)) :: blip([50, 100].beat(2)).pan(r(0,1)).out();
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -49,7 +49,7 @@ ${makeExample(
` `
beat([.5, 1.25])::sound('hat').out() beat([.5, 1.25])::sound('hat').out()
`, `,
false 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>pulse(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ pulses. A pulse is the tiniest possible rhythmic value.
@ -63,7 +63,7 @@ ${makeExample(
pulse([24, 16])::sound('hat').ad(0, .02).out() pulse([24, 16])::sound('hat').ad(0, .02).out()
pulse([48, [36,24].dur(4, 1)])::sound('fhardkick').ad(0, .1).out() pulse([48, [36,24].dur(4, 1)])::sound('fhardkick').ad(0, .1).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
"pulse is the OG rhythmic function in Topos", "pulse is the OG rhythmic function in Topos",
@ -71,7 +71,7 @@ ${makeExample(
pulse([48, 24, 16].beat(4)) :: sound('linnhats').out() pulse([48, 24, 16].beat(4)) :: sound('linnhats').out()
beat(1)::snd(['bd', '808oh'].beat(1)).out() beat(1)::snd(['bd', '808oh'].beat(1)).out()
`, `,
false false,
)} )}
@ -85,7 +85,7 @@ ${makeExample(
bar(1)::sound('kick').out() bar(1)::sound('kick').out()
beat(1)::sound('hat').speed(2).out() beat(1)::sound('hat').speed(2).out()
`, `,
true true,
)} )}
@ -97,7 +97,7 @@ beat(1)::sound('hat').speed(2).out()
beat(1, 0.5)::sound('hat').speed(4).out() beat(1, 0.5)::sound('hat').speed(4).out()
bar(1, 0.5)::sound('sn').out() bar(1, 0.5)::sound('sn').out()
`, `,
false false,
)} )}
- <ic>onbeat(...n: number[])</ic>: The <ic>onbeat</ic> function allows you to lock on to a specific beat from the clock to execute code. It can accept multiple arguments. It's usage is very straightforward and not hard to understand. You can pass either integers or floating point numbers. By default, topos is using a <ic>4/4</ic> bar meaning that you can target any of these beats (or in-between) with this function. - <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.
@ -109,7 +109,7 @@ 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(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 onbeat(1.5,2.5,3.5, 3.75)::snd('hat').gain(r(0.9,1.1)).out() // Cool high-hats
`, `,
true true,
)} )}
## XOX Style sequencers ## XOX Style sequencers
@ -125,7 +125,7 @@ seq('xoxo')::sound('fhardkick').out()
seq('ooxo')::sound('fsoftsnare').out() seq('ooxo')::sound('fsoftsnare').out()
seq('xoxo', 0.25)::sound('fhh').out() seq('xoxo', 0.25)::sound('fhh').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -135,7 +135,7 @@ seq('xoxooxxoo', [0.5, 0.25].dur(2, 1))::sound('fhardkick').out()
seq('ooxo', [1, 2].bar())::sound('fsoftsnare').speed(0.5).out() seq('ooxo', [1, 2].bar())::sound('fsoftsnare').speed(0.5).out()
seq(['xoxoxoxx', 'xxoo'].bar())::sound('fhh').out() seq(['xoxoxoxx', 'xxoo'].bar())::sound('fhh').out()
`, `,
true true,
)} )}
- <ic>fullseq(expr: string, duration: number = 0.5): boolean</ic> : a variant. Will return <ic>true</ic> or <ic>false</ic> for a whole period, depending on the symbol. Useful for long structure patterns. - <ic>fullseq(expr: string, duration: number = 0.5): boolean</ic> : a variant. Will return <ic>true</ic> or <ic>false</ic> for a whole period, depending on the symbol. Useful for long structure patterns.
@ -159,7 +159,7 @@ function complexPat() {
} }
fullseq('xooxooxx', 4) ? simplePat() : complexPat() fullseq('xooxooxx', 4) ? simplePat() : complexPat()
`, `,
true true,
)} )}
@ -181,7 +181,7 @@ rhythm(.5, 7, 8)::sound('sine')
.freq(125).ad(0, .2).out() .freq(125).ad(0, .2).out()
rhythm(.5, 3, 8)::sound('sine').freq(500).ad(0, .5).out() rhythm(.5, 3, 8)::sound('sine').freq(500).ad(0, .5).out()
`, `,
true true,
)} )}
@ -194,7 +194,7 @@ ${makeExample(
oneuclid(5, 9) :: snd('kick').out() oneuclid(5, 9) :: snd('kick').out()
oneuclid(7,16) :: snd('east').end(0.5).n(irand(3,5)).out() oneuclid(7,16) :: snd('east').end(0.5).n(irand(3,5)).out()
`, `,
true true,
)} )}
- <ic>bin(iterator: number, n: number): boolean</ic>: a binary rhythm generator. It transforms the given number into its binary representation (_e.g_ <ic>34</ic> becomes <ic>100010</ic>). It then returns a boolean value based on the iterator in order to generate a rhythm. - <ic>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.
@ -207,7 +207,7 @@ bpm(135);
beat(.5) && bin($(1), 12) && snd('kick').n([4,9].beat(1.5)).out() 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() beat(.5) && bin($(2), 34) && snd('snare').n([3,5].beat(1)).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -221,7 +221,7 @@ binrhythm([.5, .25].beat(1), 30) && snd('wt_granular').n(a)
.adsr(0, r(.1, .4), 0, 0).freq([50, 60, 72].beat(4)) .adsr(0, r(.1, .4), 0, 0).freq([50, 60, 72].beat(4))
.room(1).size(1).out() .room(1).size(1).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -233,7 +233,7 @@ beat(.5) && bin($(1), 911) && snd('ST69').n([2,3,4].beat())
.room(1).size(1).out() .room(1).size(1).out()
beat(.5) && sound('amencutup').n(irand(2,7)).shape(0.3).out() beat(.5) && sound('amencutup').n(irand(2,7)).shape(0.3).out()
`, `,
false false,
)} )}
If you don't find it spicy enough, you can add some more probabilities to your rhythms by taking advantage of the probability functions. See the functions documentation page to learn more about them. 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.
@ -247,7 +247,7 @@ prob(60)::beat(.5) && euclid($(2), 3, 8) && snd('mash')
.pan(usine(1/4)).out() .pan(usine(1/4)).out()
prob(80)::beat(.5) && sound(['hh', 'hat'].pick()).out() prob(80)::beat(.5) && sound(['hh', 'hat'].pick()).out()
`, `,
true true,
)} )}

View File

@ -26,7 +26,7 @@ ${makeExample(
` `
log(\`\$\{cbar()}\, \$\{cbeat()\}, \$\{cpulse()\}\`) log(\`\$\{cbar()}\, \$\{cbeat()\}, \$\{cpulse()\}\`)
`, `,
true true,
)} )}
### BPM and PPQN ### BPM and PPQN
@ -83,7 +83,7 @@ if((cbar() % 4) > 1) {
// This is always playing no matter what happens // This is always playing no matter what happens
beat([.5, .5, 1, .25].beat(0.5)) :: sound('shaker').out() beat([.5, .5, 1, .25].beat(0.5)) :: sound('shaker').out()
`, `,
true true,
)} )}
## Time Warping ## Time Warping
@ -108,7 +108,7 @@ flip(3) :: beat([.25,.5].beat(.5)) :: sound('dr')
// Jumping back and forth in time // Jumping back and forth in time
beat(.25) :: warp([12, 48, 24, 1, 120, 30].pick()) beat(.25) :: warp([12, 48, 24, 1, 120, 30].pick())
`, `,
true true,
)} )}
- <ic>beat_warp(beat: number)</ic>: this function jumps to the _n_ beat of the clock. The first beat is <ic>1</ic>. - <ic>beat_warp(beat: number)</ic>: this function jumps to the _n_ beat of the clock. The first beat is <ic>1</ic>.
@ -130,7 +130,7 @@ beat(.5) :: snd('arpy').note(
// Comment me to stop warping! // Comment me to stop warping!
beat(1) :: beat_warp([2,4,5,10,11].pick()) beat(1) :: beat_warp([2,4,5,10,11].pick())
`, `,
true true,
)} )}
## Transport-based rhythm generators ## Transport-based rhythm generators
@ -144,7 +144,7 @@ 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(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 onbeat(1.5,2.5,3.5, 3.75)::snd('hat').gain(r(0.9,1.1)).out() // Cool high-hats
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -156,7 +156,7 @@ beat([.25, 1/8].beat(1.5))::snd('hat').n(2)
.gain(rand(0.4, 0.7)).end(0.05) .gain(rand(0.4, 0.7)).end(0.05)
.pan(usine()).out() .pan(usine()).out()
`, `,
false false,
)} )}
- <ic>oncount(beats: number[], meter: number)</ic>: This function is similar to <ic>onbeat</ic> but it allows you to specify a custom number of beats as the last argument. - <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.
@ -171,7 +171,7 @@ z1('1/16 (0 2 3 4)+(0 2 4 6)').scale('pentatonic').sound('sawtooth')
onbeat(1,1.5,2,3,4) :: sound('bd').gain(2.0).out() 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() oncount([1,3,5.5,7,7.5,8],8) :: sound('hh').gain(irand(1.0,4.0)).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -183,7 +183,7 @@ 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([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() oncount([1, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16],16) :: sound('hh').out()
`, `,
true true,
)} )}

View File

@ -22,7 +22,7 @@ ${makeExample(
` `
v('my_cool_variable', 2) v('my_cool_variable', 2)
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -31,7 +31,7 @@ ${makeExample(
// Note that we just use one argument // Note that we just use one argument
log(v('my_cool_variable')) log(v('my_cool_variable'))
`, `,
false false,
)} )}
@ -55,7 +55,7 @@ ${makeExample(
` `
rhythm(.25, 6, 8) :: sound('dr').n($(1)).end(.25).out() rhythm(.25, 6, 8) :: sound('dr').n($(1)).end(.25).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -64,7 +64,7 @@ ${makeExample(
// Limit is 20, step is 5 // Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n($(1, 20, 5)).end(.25).out() rhythm(.25, 6, 8) :: sound('dr').n($(1, 20, 5)).end(.25).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -73,10 +73,10 @@ ${makeExample(
// Limit is 20, step is 5 // Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n(drunk()).end(.25).out() rhythm(.25, 6, 8) :: sound('dr').n(drunk()).end(.25).out()
`, `,
false false,
)} )}
` `;
} };

View File

@ -12,7 +12,9 @@ Ziffers is a **musical number based notation** tuned for _live coding_. It is a
- exploring **generative / aleatoric / stochastic** melodies and applying them to sounds and synths. - exploring **generative / aleatoric / stochastic** melodies and applying them to sounds and synths.
- embracing a different mindset and approach to time and **patterning**. - embracing a different mindset and approach to time and **patterning**.
${makeExample("Super Fancy Ziffers example", ` ${makeExample(
"Super Fancy Ziffers example",
`
z1('1/8 024!3 035 024 0124').sound('wt_stereo') z1('1/8 024!3 035 024 0124').sound('wt_stereo')
.adsr(0, .4, 0.5, .4).gain(0.1) .adsr(0, .4, 0.5, .4).gain(0.1)
.lpadsr(4, 0, .2, 0, 0) .lpadsr(4, 0, .2, 0, 0)
@ -26,7 +28,9 @@ z2('<1/8 1/16> __ 0 <(^) (^ ^)> (0,8)').sound('wt_stereo')
let osci = 1500 + usine(1/2) * 2000; let osci = 1500 + usine(1/2) * 2000;
z3('can can:2').sound().gain(1).cutoff(osci).out() z3('can can:2').sound().gain(1).cutoff(osci).out()
z4('1/4 kick kick snare kick').sound().gain(1).cutoff(osci).out() z4('1/4 kick kick snare kick').sound().gain(1).cutoff(osci).out()
`, true)} `,
true,
)}
## Notation ## Notation
@ -53,7 +57,7 @@ ${makeExample(
` `
z1('0.25 0 1 2 3 4 5 6 7 8 9').sound('wt_stereo') z1('0.25 0 1 2 3 4 5 6 7 8 9').sound('wt_stereo')
.adsr(0, .1, 0, 0).out()`, .adsr(0, .1, 0, 0).out()`,
true true,
)} )}
${makeExample( ${makeExample(
@ -62,7 +66,7 @@ ${makeExample(
.sound('wt_05').pan(r(0,1)) .sound('wt_05').pan(r(0,1))
.cutoff(usaw(1/2) * 4000) .cutoff(usaw(1/2) * 4000)
.room(0.9).size(0.9).out()`, .room(0.9).size(0.9).out()`,
false false,
)} )}
${makeExample( ${makeExample(
@ -72,7 +76,7 @@ z1('1/8 0 2 4 0 2 4 1/4 0 3 5 0.25 _ 0 7 0 7')
.sound('square').delay(0.5).delayt(1/8) .sound('square').delay(0.5).delayt(1/8)
.adsr(0, .1, 0, 0).delayfb(0.45).out() .adsr(0, .1, 0, 0).delayfb(0.45).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -82,7 +86,7 @@ z1('e _ _ 0 ^ 0 _ 0 ^ 0').sound('jvbass').out()
beat(1)::snd('bd').out(); beat(2)::snd('sd').out() beat(1)::snd('bd').out(); beat(2)::snd('sd').out()
beat(3) :: snd('cp').room(0.5).size(0.5).orbit(2).out() beat(3) :: snd('cp').room(0.5).size(0.5).orbit(2).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -94,7 +98,7 @@ z1('^ 1/8 0 1 b2 3 4 _ 4 b5 4 3 b2 1 0')
.fmi(0.5).fmh(2).delay(0.5).delayt(1/3) .fmi(0.5).fmh(2).delay(0.5).delayt(1/3)
.adsr(0, .1, 0, 0).out() .adsr(0, .1, 0, 0).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -107,7 +111,7 @@ z2('1/8 _ 0!4 5!4 4!2 7!2')
.scale('major').sound('wt_oboe') .scale('major').sound('wt_oboe')
.shape(0.2).sustain(0.1).out() .shape(0.2).sustain(0.1).out()
`, `,
false false,
)} )}
${makeExample( ${makeExample(
@ -117,7 +121,7 @@ z1('w [0 [5 [3 7]]] h [0 4]')
.scale('major').sound('sine') .scale('major').sound('sine')
.fmi(usine(.5)).fmh(2).out() .fmi(usine(.5)).fmh(2).out()
`, `,
false false,
)} )}
## Chords ## Chords
@ -131,7 +135,7 @@ z1('1.0 024 045 058 046 014')
.sound('sine').adsr(0.5, 1, 0, 0) .sound('sine').adsr(0.5, 1, 0, 0)
.room(0.5).size(0.9) .room(0.5).size(0.9)
.scale("minor").out() .scale("minor").out()
` `,
)} )}
${makeExample( ${makeExample(
@ -140,7 +144,7 @@ ${makeExample(
z1('2/4 i vi ii v') z1('2/4 i vi ii v')
.sound('triangle').adsr(0.2, 0.3, 0, 0) .sound('triangle').adsr(0.2, 0.3, 0, 0)
.room(0.5).size(0.9).scale("major").out() .room(0.5).size(0.9).scale("major").out()
` `,
)} )}
${makeExample( ${makeExample(
@ -150,7 +154,7 @@ z1('0.25 Bmaj7!2 D7!2 _ Gmaj7!2 Bb7!2 ^ Ebmaj7!2')
.sound('square').room(0.5).cutoff(500) .sound('square').room(0.5).cutoff(500)
.lpadsr(4, 0, .4, 0, 0).size(0.9) .lpadsr(4, 0, .4, 0, 0).size(0.9)
.scale("major").out() .scale("major").out()
` `,
)} )}
${makeExample( ${makeExample(
@ -158,7 +162,7 @@ ${makeExample(
` `
z1('q Amin!2').key(["A2", "E2"].beat(4)) z1('q Amin!2').key(["A2", "E2"].beat(4))
.sound('sawtooth').cutoff(500) .sound('sawtooth').cutoff(500)
.lpadsr(2, 0, .5, 0, 0, 0).out()` .lpadsr(2, 0, .5, 0, 0, 0).out()`,
)} )}
${makeExample( ${makeExample(
@ -169,7 +173,7 @@ z1('i i v%-4 v%-2 vi%-5 vi%-3 iv%-2 iv%-1')
.delay(0.5).delayt([1/8, 1/4].beat(4)) .delay(0.5).delayt([1/8, 1/4].beat(4))
.delayfb(0.5).out() .delayfb(0.5).out()
beat(4) :: sound('breaks165').stretch(4).out() beat(4) :: sound('breaks165').stretch(4).out()
` `,
)} )}
${makeExample( ${makeExample(
@ -178,7 +182,7 @@ ${makeExample(
z1('1/4 Cmin!3 Fmin!3 Fmin%-1 Fmin%-2 Fmin%-1') z1('1/4 Cmin!3 Fmin!3 Fmin%-1 Fmin%-2 Fmin%-1')
.sound("sine").bpf(500 + usine(1/4) * 2000) .sound("sine").bpf(500 + usine(1/4) * 2000)
.out() .out()
` `,
)} )}
${makeExample( ${makeExample(
@ -187,7 +191,7 @@ ${makeExample(
z1('1/6 i v 1/3 vi iv').invert([1,-1,-2,0].beat(4)) z1('1/6 i v 1/3 vi iv').invert([1,-1,-2,0].beat(4))
.sound("sawtooth").cutoff(1000) .sound("sawtooth").cutoff(1000)
.lpadsr(2, 0, .2, 0, 0).out() .lpadsr(2, 0, .2, 0, 0).out()
` `,
)} )}
## Algorithmic operations ## Algorithmic operations
@ -204,7 +208,7 @@ z1("1/8 _ 0 (0 1 3)+(1 2) 0 (2 3 5)-(1 2)").sound('sine')
.room(0.9).size(0.9).sustain(0.1).delay(0.5).delay(0.125) .room(0.9).size(0.9).sustain(0.1).delay(0.5).delay(0.125)
.delayfb(0.25).out(); .delayfb(0.25).out();
`, `,
true true,
)} )}
* **Random numbers:** <ic>(4,6)</ic> Random number between 4 and 6 * **Random numbers:** <ic>(4,6)</ic> Random number between 4 and 6
@ -219,7 +223,7 @@ z1("s _ (0,8) 0 0 (0,5) 0 0").sound('sine')
.delay(0.125).delayfb(0.25).out(); .delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out() beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`, `,
true true,
)} )}
## Keys and scales ## Keys and scales
@ -249,7 +253,7 @@ z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.delay(0.125).delayfb(0.25).out(); .delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out() beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`, `,
true true,
)} )}
@ -282,7 +286,7 @@ z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.delay(0.25).delayfb(0.5).out(); .delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out() beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`, `,
true true,
)} )}
Microtonal scales can be defined using <a href="https://www.huygens-fokker.org/scala/scl_format.html" target="_blank">Scala format</a> or by extended notation defined by Sevish <a href="https://sevish.com/scaleworkshop/" target="_blank">Scale workshop</a>, for example: Microtonal scales can be defined using <a href="https://www.huygens-fokker.org/scala/scl_format.html" target="_blank">Scala format</a> or by extended notation defined by Sevish <a href="https://sevish.com/scaleworkshop/" target="_blank">Scale workshop</a>, for example:
@ -300,7 +304,7 @@ z1("s ^ (0,8) 0 0 _ (0,5) 0 0").sound('sine')
.delay(0.25).delayfb(0.5).out(); .delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out() beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`, `,
true true,
)} )}
## Synchronization ## Synchronization
@ -315,7 +319,7 @@ ${makeExample(
z0('w 0 8').sound('peri').out() z0('w 0 8').sound('peri').out()
z1('e 0 4 5 9').sound('bell').out() z1('e 0 4 5 9').sound('bell').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -324,7 +328,7 @@ ${makeExample(
z1('w 0 5').sound('pluck').release(0.1).sustain(0.25).out() z1('w 0 5').sound('pluck').release(0.1).sustain(0.25).out()
z2('q 6 3').wait(z1).sound('sine').release(0.16).sustain(0.55).out() z2('q 6 3').wait(z1).sound('sine').release(0.16).sustain(0.55).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -333,7 +337,7 @@ ${makeExample(
z1('w __ 0 5 9 3').sound('bin').out() z1('w __ 0 5 9 3').sound('bin').out()
z2('q __ 4 2 e 6 3 q 6').sync(z1).sound('east').out() z2('q __ 4 2 e 6 3 q 6').sync(z1).sound('east').out()
`, `,
true true,
)} )}
## Examples ## Examples
@ -346,7 +350,7 @@ ${makeExample(
z1('0 1 2 3').key('G3') z1('0 1 2 3').key('G3')
.scale('minor').sound('sine').out() .scale('minor').sound('sine').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -354,7 +358,7 @@ ${makeExample(
` `
z1('0 1 2 3 4').key('G3').scale('minor').sound('sine').often(n => n.pitch+=3).rarely(s => s.delay(0.5)).out() z1('0 1 2 3 4').key('G3').scale('minor').sound('sine').often(n => n.pitch+=3).rarely(s => s.delay(0.5)).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -362,7 +366,7 @@ ${makeExample(
` `
z1('0 3 2 4',{key: 'D3', scale: 'minor pentatonic'}).sound('sine').out() z1('0 3 2 4',{key: 'D3', scale: 'minor pentatonic'}).sound('sine').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -370,7 +374,7 @@ ${makeExample(
` `
z1('q 0 0 4 4 5 5 h4 q 3 3 2 2 1 1 h0').sound('sine').out() z1('q 0 0 4 4 5 5 h4 q 3 3 2 2 1 1 h0').sound('sine').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -379,7 +383,7 @@ ${makeExample(
z1('1/4 0 0 4 4 5 5 2/4 4 1/4 3 3 2 2 1 1 2/4 0') z1('1/4 0 0 4 4 5 5 2/4 4 1/4 3 3 2 2 1 1 2/4 0')
.sound('sine').out() .sound('sine').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -388,7 +392,7 @@ ${makeExample(
z1('0.25 5 1 2 6 0.125 3 8 0.5 4 1.0 0') z1('0.25 5 1 2 6 0.125 3 8 0.5 4 1.0 0')
.sound('sine').scale("galian").out() .sound('sine').scale("galian").out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -397,7 +401,7 @@ ${makeExample(
z1('q 0 ^ e0 r _ 0 _ r 4 ^4 4') z1('q 0 ^ e0 r _ 0 _ r 4 ^4 4')
.sound('sine').scale("godian").out() .sound('sine').scale("godian").out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -406,7 +410,7 @@ ${makeExample(
z1('q 0 4 e^r 3 e3 0.5^r h4 1/4^r e 5 r 0.125^r 0') z1('q 0 4 e^r 3 e3 0.5^r h4 1/4^r e 5 r 0.125^r 0')
.sound('sine').scale("aeryptian").out() .sound('sine').scale("aeryptian").out()
`, `,
true true,
)} )}
- Scales - Scales
@ -419,7 +423,7 @@ z1('q 0 3 {10 14} e 8 4 {5 10 12 14 7 0}').sound('sine')
.scale("17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1") .scale("17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1")
.out() .out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -437,7 +441,7 @@ ${makeExample(
onbeat(1,1.5,3) :: sound('bd').cutoff(100 + usine(.25) * 1000).out() onbeat(1,1.5,3) :: sound('bd').cutoff(100 + usine(.25) * 1000).out()
`, `,
true true,
)} )}
- Algorithmic operations - Algorithmic operations
@ -449,7 +453,7 @@ z1('q 0 (2,4) 4 (5,9)').sound('sine')
.scale("Bebop minor") .scale("Bebop minor")
.out() .out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -459,7 +463,7 @@ z1('q (0 3 1 5)+(2 5) e (0 5 2)*(2 3) (0 5 2)>>(2 3) (0 5 2)%(2 3)').sound('sine
.scale("Bebop major") .scale("Bebop major")
.out() .out()
`, `,
true true,
)} )}
## Samples ## Samples
@ -471,7 +475,7 @@ ${makeExample(
` `
z1('bd [hh hh]').octave(-2).sound('sine').out() z1('bd [hh hh]').octave(-2).sound('sine').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -479,7 +483,7 @@ ${makeExample(
` `
z1('bd [hh <hh <cp cp:2>>]').octave(-2).sound('sine').out() z1('bd [hh <hh <cp cp:2>>]').octave(-2).sound('sine').out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -489,7 +493,7 @@ ${makeExample(
.octave(-1).sound() .octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out() .adsr(0.25,0.125,0.125,0.25).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -500,7 +504,7 @@ ${makeExample(
.scale('110 220 320 450') .scale('110 220 320 450')
.sound().out() .sound().out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -510,7 +514,7 @@ ${makeExample(
.octave(-1).sound() .octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out() .adsr(0.25,0.125,0.125,0.25).out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -519,7 +523,7 @@ ${makeExample(
z1('e 1:2 4:3 6:2') z1('e 1:2 4:3 6:2')
.octave(-1).sound("east").out() .octave(-1).sound("east").out()
`, `,
true true,
)} )}
${makeExample( ${makeExample(
@ -527,7 +531,7 @@ ${makeExample(
` `
z1('_e 1@east:2 4@bd:3 6@arp:2 9@baa').sound().out() z1('_e 1@east:2 4@bd:3 6@arp:2 9@baa').sound().out()
`, `,
true true,
)} )}
@ -543,7 +547,7 @@ ${makeExample(
"q 2 7 8 6".z1().octave(-1).sound('sine').out() "q 2 7 8 6".z1().octave(-1).sound('sine').out()
"q 2 7 8 6".z2({key: "C2", scale: "aeolian"}).sound('sine').scale("minor").out() "q 2 7 8 6".z2({key: "C2", scale: "aeolian"}).sound('sine').scale("minor").out()
`, `,
true true,
)} )}
`; `;

View File

@ -141,7 +141,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
} }
return Array.from( return Array.from(
{ length: times }, { length: times },
() => Math.floor(api.randomGen() * (max - min + 1)) + min () => Math.floor(api.randomGen() * (max - min + 1)) + min,
); );
}; };
@ -164,7 +164,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
const chunk_size = divisor; // Get the first argument (chunk size) const chunk_size = divisor; // Get the first argument (chunk size)
const timepos = api.app.clock.pulses_since_origin; const timepos = api.app.clock.pulses_since_origin;
const slice_count = Math.floor( const slice_count = Math.floor(
timepos / Math.floor(chunk_size * api.ppqn()) timepos / Math.floor(chunk_size * api.ppqn()),
); );
return this[slice_count % this.length]; return this[slice_count % this.length];
}; };
@ -174,12 +174,12 @@ export const makeArrayExtensions = (api: UserAPI) => {
const timepos = api.app.clock.pulses_since_origin; const timepos = api.app.clock.pulses_since_origin;
const ppqn = api.ppqn(); const ppqn = api.ppqn();
const adjustedDurations: number[] = this.map( const adjustedDurations: number[] = this.map(
(_, index) => durations[index % durations.length] (_, index) => durations[index % durations.length],
); );
const totalDurationInPulses = adjustedDurations.reduce( const totalDurationInPulses = adjustedDurations.reduce(
// @ts-ignore // @ts-ignore
(acc, duration) => acc + duration * ppqn, (acc, duration) => acc + duration * ppqn,
0 0,
); );
const loopPosition = timepos % totalDurationInPulses; const loopPosition = timepos % totalDurationInPulses;
let cumulativeDuration = 0; let cumulativeDuration = 0;
@ -402,7 +402,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
Array.prototype.scale = function ( Array.prototype.scale = function (
scale: string = "major", scale: string = "major",
base_note: number = 0 base_note: number = 0,
) { ) {
/** /**
* @param scale - the scale name * @param scale - the scale name
@ -426,7 +426,7 @@ Array.prototype.scale = function (
Array.prototype.scaleArp = function ( Array.prototype.scaleArp = function (
scaleName: string = "major", scaleName: string = "major",
boundary: number = 0 boundary: number = 0,
) { ) {
/* /*
* @param scaleName - the scale name * @param scaleName - the scale name

View File

@ -2,6 +2,7 @@ import { type UserAPI } from "../API";
import { MidiEvent } from "../classes/MidiEvent"; import { MidiEvent } from "../classes/MidiEvent";
import { Player } from "../classes/ZPlayer"; import { Player } from "../classes/ZPlayer";
import { SoundEvent } from "../classes/SoundEvent"; import { SoundEvent } from "../classes/SoundEvent";
import { SkipEvent } from "../classes/SkipEvent";
declare global { declare global {
interface Number { interface Number {
@ -24,12 +25,11 @@ declare global {
z15(): Player; z15(): Player;
z16(): Player; z16(): Player;
midi(): MidiEvent; midi(): MidiEvent;
sound(name: string): SoundEvent; sound(name: string): SoundEvent | SkipEvent;
} }
} }
export const makeNumberExtensions = (api: UserAPI) => { export const makeNumberExtensions = (api: UserAPI) => {
Number.prototype.z0 = function (options: { [key: string]: any } = {}) { Number.prototype.z0 = function (options: { [key: string]: any } = {}) {
return api.z0(this.valueOf().toString().split("").join(" "), options); return api.z0(this.valueOf().toString().split("").join(" "), options);
}; };
@ -100,14 +100,13 @@ export const makeNumberExtensions = (api: UserAPI) => {
Number.prototype.midi = function (...kwargs: any[]) { Number.prototype.midi = function (...kwargs: any[]) {
return api.midi(this.valueOf(), ...kwargs); return api.midi(this.valueOf(), ...kwargs);
} };
Number.prototype.sound = function (name: string) { Number.prototype.sound = function (name: string): SoundEvent | SkipEvent {
if (Number.isInteger(this.valueOf())) { if (Number.isInteger(this.valueOf())) {
return (api.sound(name) as SoundEvent).note(this.valueOf()); return (api.sound(name) as SoundEvent).note(this.valueOf());
} else { } else {
return (api.sound(name) as SoundEvent).freq(this.valueOf()); return (api.sound(name) as SoundEvent).freq(this.valueOf());
} }
} };
};
}

View File

@ -36,8 +36,8 @@ declare global {
} }
const isJsonString = (str: string): boolean => { const isJsonString = (str: string): boolean => {
return str[0] === '{' && str[str.length - 1] === '}' return str[0] === "{" && str[str.length - 1] === "}";
} };
const stringObject = (str: string, params: object) => { const stringObject = (str: string, params: object) => {
if (isJsonString(str)) { if (isJsonString(str)) {
@ -46,14 +46,17 @@ const stringObject = (str: string, params: object) => {
} else { } else {
return JSON.stringify({ ...params, text: str }); return JSON.stringify({ ...params, text: str });
} }
} };
export const makeStringExtensions = (api: UserAPI) => { export const makeStringExtensions = (api: UserAPI) => {
String.prototype.speak = function () { String.prototype.speak = function () {
const options = JSON.parse(this.valueOf()); const options = JSON.parse(this.valueOf());
new Speaker({ ...options, text: options.text }).speak().then(() => { new Speaker({ ...options, text: options.text })
.speak()
.then(() => {
// Done // Done
}).catch((e) => { })
.catch((e) => {
console.log("Error speaking:", e); console.log("Error speaking:", e);
}); });
}; };
@ -157,7 +160,7 @@ export const makeStringExtensions = (api: UserAPI) => {
return noteNameToMidi(this.valueOf()); return noteNameToMidi(this.valueOf());
} }
}; };
} };
type SpeechOptions = { type SpeechOptions = {
text?: string; text?: string;
@ -166,14 +169,12 @@ type SpeechOptions = {
volume?: number; volume?: number;
voice?: number; voice?: number;
lang?: string; lang?: string;
} };
let speakerTimeout: number; let speakerTimeout: number;
export class Speaker { export class Speaker {
constructor( constructor(public options: SpeechOptions) {}
public options: SpeechOptions
) {}
speak = () => { speak = () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
@ -191,12 +192,14 @@ export class Speaker {
if (this.options.lang) { if (this.options.lang) {
// Check if language has country code // Check if language has country code
if (this.options.lang.length === 2) { if (this.options.lang.length === 2) {
utterance.lang = `${this.options.lang}-${this.options.lang.toUpperCase()}` utterance.lang = `${
this.options.lang
}-${this.options.lang.toUpperCase()}`;
} else if (this.options.lang.length === 5) { } else if (this.options.lang.length === 5) {
utterance.lang = this.options.lang; utterance.lang = this.options.lang;
} else { } else {
// Fallback to en us // Fallback to en us
utterance.lang = 'en-US'; utterance.lang = "en-US";
} }
} }
@ -220,11 +223,9 @@ export class Speaker {
} else { } else {
synth.speak(utterance); synth.speak(utterance);
} }
} else { } else {
reject("No text provided"); reject("No text provided");
} }
}); });
} };
} }

View File

@ -1,5 +1,6 @@
import { OscilloscopeConfig, runOscilloscope, scriptBlinkers } from "./AudioVisualisation"; import { OscilloscopeConfig, runOscilloscope } from "./Visuals/Oscilloscope";
import { EditorState, Compartment } from "@codemirror/state"; import { EditorState, Compartment } from "@codemirror/state";
import { scriptBlinkers } from "./Visuals/Blinkers";
import { javascript } from "@codemirror/lang-javascript"; import { javascript } from "@codemirror/lang-javascript";
import { markdown } from "@codemirror/lang-markdown"; import { markdown } from "@codemirror/lang-markdown";
import { Extension } from "@codemirror/state"; import { Extension } from "@codemirror/state";
@ -47,6 +48,7 @@ export class Editor {
// Editor logic // Editor logic
editor_mode: "global" | "local" | "init" | "notes" = "global"; editor_mode: "global" | "local" | "init" | "notes" = "global";
hidden_interface: boolean = false;
fontSize!: Compartment; fontSize!: Compartment;
withLineNumbers!: Compartment; withLineNumbers!: Compartment;
vimModeCompartment!: Compartment; vimModeCompartment!: Compartment;
@ -100,6 +102,19 @@ export class Editor {
public hydra: any; public hydra: any;
constructor() { constructor() {
/**
* This is the entry point of the application. The Editor instance is created when the page is loaded.
* It is responsible for:
* - Initializing the user interface
* - Loading the universe from local storage
* - Initializing the audio context and the clock
* - Building the user API
* - Building the documentation
* - Installing event listeners
* - Building the CodeMirror editor
* - Evaluating the init file
*/
// ================================================================================ // ================================================================================
// Build user interface // Build user interface
// ================================================================================ // ================================================================================
@ -194,6 +209,11 @@ export class Editor {
} }
private getBuffer(type: string): any { private getBuffer(type: string): any {
/**
* Retrieves the buffer based on the specified type.
* @param type - The type of buffer to retrieve.
* @returns The buffer object.
*/
const universe = this.universes[this.selected_universe.toString()]; const universe = this.universes[this.selected_universe.toString()];
return type === "locals" return type === "locals"
? universe[type][this.local_index] ? universe[type][this.local_index]
@ -221,24 +241,27 @@ export class Editor {
} }
updateKnownUniversesView = () => { updateKnownUniversesView = () => {
/**
* Updates the known universes view.
* This function generates and populates a list of known universes based on the data stored in the 'universes' property.
* It retrieves the necessary HTML elements and template, creates the list, and attaches event listeners to the generated items.
* If any required elements or templates are missing, warning messages are logged and the function returns early.
*/
let itemTemplate = document.getElementById( let itemTemplate = document.getElementById(
"ui-known-universe-item-template" "ui-known-universe-item-template",
) as HTMLTemplateElement; ) as HTMLTemplateElement;
if (!itemTemplate) { if (!itemTemplate) {
console.warn("Missing template #ui-known-universe-item-template");
return; return;
} }
let existing_universes = document.getElementById("existing-universes"); let existing_universes = document.getElementById("existing-universes");
if (!existing_universes) { if (!existing_universes) {
console.warn("Missing element #existing-universes");
return; return;
} }
let list = document.createElement("ul"); let list = document.createElement("ul");
list.className = list.className =
"lg:h-80 lg:text-normal text-sm h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800"; "lg:h-80 lg:text-normal text-sm h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800";
list.append( list.append(
...Object.keys(this.universes).map((it) => { ...Object.keys(this.universes).map((it) => {
let item = itemTemplate.content.cloneNode(true) as DocumentFragment; let item = itemTemplate.content.cloneNode(true) as DocumentFragment;
@ -250,10 +273,10 @@ export class Editor {
item item
.querySelector(".delete-universe") .querySelector(".delete-universe")
?.addEventListener("click", () => ?.addEventListener("click", () =>
api._deleteUniverseFromInterface(it) api._deleteUniverseFromInterface(it),
); );
return item; return item;
}) }),
); );
existing_universes.innerHTML = ""; existing_universes.innerHTML = "";
@ -261,7 +284,13 @@ export class Editor {
}; };
changeToLocalBuffer(i: number) { changeToLocalBuffer(i: number) {
// Updating the CSS accordingly /**
* Changes the local buffer based on the provided index.
* Updates the CSS accordingly by adding a specific class to the selected tab and removing it from other tabs.
* Updates the local index and updates the editor view.
*
* @param i The index of the tab to change the local buffer to.
*/
const tabs = document.querySelectorAll('[id^="tab-"]'); const tabs = document.querySelectorAll('[id^="tab-"]');
const tab = tabs[i] as HTMLElement; const tab = tabs[i] as HTMLElement;
tab.classList.add("bg-orange-300"); tab.classList.add("bg-orange-300");
@ -274,6 +303,11 @@ export class Editor {
} }
changeModeFromInterface(mode: "global" | "local" | "init" | "notes") { changeModeFromInterface(mode: "global" | "local" | "init" | "notes") {
/**
* Changes the mode of the interface.
*
* @param mode - The mode to change to. Can be one of "global", "local", "init", or "notes".
*/
const interface_buttons: HTMLElement[] = [ const interface_buttons: HTMLElement[] = [
this.interface.local_button, this.interface.local_button,
this.interface.global_button, this.interface.global_button,
@ -334,7 +368,7 @@ export class Editor {
this.view.dispatch({ this.view.dispatch({
effects: this.chosenLanguage.reconfigure( effects: this.chosenLanguage.reconfigure(
this.editor_mode == "notes" ? [markdown()] : [javascript()] this.editor_mode == "notes" ? [markdown()] : [javascript()],
), ),
}); });
@ -343,8 +377,14 @@ export class Editor {
setButtonHighlighting( setButtonHighlighting(
button: "play" | "pause" | "stop" | "clear", button: "play" | "pause" | "stop" | "clear",
highlight: boolean highlight: boolean,
) { ) {
/**
* Sets the highlighting for a specific button.
*
* @param button - The button to highlight ("play", "pause", "stop", or "clear").
* @param highlight - A boolean indicating whether to highlight the button or not.
*/
document.getElementById("play-label")!.textContent = document.getElementById("play-label")!.textContent =
button !== "pause" ? "Pause" : "Play"; button !== "pause" ? "Pause" : "Play";
if (button !== "pause") { if (button !== "pause") {
@ -391,7 +431,7 @@ export class Editor {
// All other buttons must lose the highlighting // All other buttons must lose the highlighting
document document
.querySelectorAll( .querySelectorAll(
possible_selectors.filter((_, index) => index != selector).join(",") possible_selectors.filter((_, index) => index != selector).join(","),
) )
.forEach((button) => { .forEach((button) => {
button.children[0].classList.remove("animate-pulse"); button.children[0].classList.remove("animate-pulse");
@ -429,36 +469,36 @@ export class Editor {
} }
} }
flashBackground(color: string, duration: number): void {
/** /**
* Flashes the background of the view and its gutters. * Flashes the background of the view and its gutters.
* @param {string} color - The color to set. * @param {string} color - The color to set.
* @param {number} duration - Duration in milliseconds to maintain the color. * @param {number} duration - Duration in milliseconds to maintain the color.
*/ */
flashBackground(color: string, duration: number): void {
const domElement = this.view.dom; const domElement = this.view.dom;
const gutters = domElement.getElementsByClassName( const gutters = domElement.getElementsByClassName(
"cm-gutter" "cm-gutter",
) as HTMLCollectionOf<HTMLElement>; ) as HTMLCollectionOf<HTMLElement>;
domElement.classList.add("fluid-bg-transition"); domElement.classList.add("fluid-bg-transition");
Array.from(gutters).forEach((gutter) => Array.from(gutters).forEach((gutter) =>
gutter.classList.add("fluid-bg-transition") gutter.classList.add("fluid-bg-transition"),
); );
domElement.style.backgroundColor = color; domElement.style.backgroundColor = color;
Array.from(gutters).forEach( Array.from(gutters).forEach(
(gutter) => (gutter.style.backgroundColor = color) (gutter) => (gutter.style.backgroundColor = color),
); );
setTimeout(() => { setTimeout(() => {
domElement.style.backgroundColor = ""; domElement.style.backgroundColor = "";
Array.from(gutters).forEach( Array.from(gutters).forEach(
(gutter) => (gutter.style.backgroundColor = "") (gutter) => (gutter.style.backgroundColor = ""),
); );
domElement.classList.remove("fluid-bg-transition"); domElement.classList.remove("fluid-bg-transition");
Array.from(gutters).forEach((gutter) => Array.from(gutters).forEach((gutter) =>
gutter.classList.remove("fluid-bg-transition") gutter.classList.remove("fluid-bg-transition"),
); );
}, duration); }, duration);
} }
@ -466,7 +506,7 @@ export class Editor {
private initializeElements(): void { private initializeElements(): void {
for (const [key, value] of Object.entries(singleElements)) { for (const [key, value] of Object.entries(singleElements)) {
this.interface[key] = document.getElementById( this.interface[key] = document.getElementById(
value value,
) as ElementMap[keyof ElementMap]; ) as ElementMap[keyof ElementMap];
} }
} }
@ -474,12 +514,18 @@ export class Editor {
private initializeButtonGroups(): void { private initializeButtonGroups(): void {
for (const [key, ids] of Object.entries(buttonGroups)) { for (const [key, ids] of Object.entries(buttonGroups)) {
this.buttonElements[key] = ids.map( this.buttonElements[key] = ids.map(
(id) => document.getElementById(id) as HTMLButtonElement (id) => document.getElementById(id) as HTMLButtonElement,
); );
} }
} }
private loadHydraSynthAsync(): void { private loadHydraSynthAsync(): void {
/**
* Loads the Hydra Synth asynchronously by creating a script element
* and appending it to the document head. * Once the script is
* loaded successfully, it initializes the Hydra Synth. If there
* is an error loading the script, it logs an error message.
*/
var script = document.createElement("script"); var script = document.createElement("script");
script.src = "https://unpkg.com/hydra-synth"; script.src = "https://unpkg.com/hydra-synth";
script.async = true; script.async = true;
@ -494,6 +540,9 @@ export class Editor {
} }
private initializeHydra(): void { private initializeHydra(): void {
/**
* Initializes the Hydra backend and sets up the Hydra synth.
*/
// @ts-ignore // @ts-ignore
this.hydra_backend = new Hydra({ this.hydra_backend = new Hydra({
canvas: this.interface.hydra_canvas as HTMLCanvasElement, canvas: this.interface.hydra_canvas as HTMLCanvasElement,
@ -501,18 +550,22 @@ export class Editor {
enableStreamCapture: false, enableStreamCapture: false,
}); });
this.hydra = this.hydra_backend.synth; this.hydra = this.hydra_backend.synth;
(globalThis as any).hydra = this.hydra;
this.hydra.setResolution(1024, 768);
} }
private setCanvas(canvas: HTMLCanvasElement): void { private setCanvas(canvas: HTMLCanvasElement): void {
/**
* Sets the canvas element and configures its size and context.
*
* @param canvas - The HTMLCanvasElement to set.
*/
if (!canvas) return; if (!canvas) return;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1; const dpr = window.devicePixelRatio || 1;
// Assuming the canvas takes up the whole window // Assuming the canvas takes up the whole window
canvas.width = window.innerWidth * dpr; canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr; canvas.height = window.innerHeight * dpr;
if (ctx) { if (ctx) {
ctx.scale(dpr, dpr); ctx.scale(dpr, dpr);
} }

View File

@ -22,7 +22,7 @@
::before, ::before,
::after { ::after {
--tw-content: ''; --tw-content: "";
} }
/* /*
@ -44,7 +44,21 @@ html {
-o-tab-size: 4; -o-tab-size: 4;
tab-size: 4; tab-size: 4;
/* 3 */ /* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
"Noto Sans",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji";
/* 4 */ /* 4 */
font-feature-settings: normal; font-feature-settings: normal;
/* 5 */ /* 5 */
@ -129,7 +143,8 @@ code,
kbd, kbd,
samp, samp,
pre { pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
/* 1 */ /* 1 */
font-size: 1em; font-size: 1em;
/* 2 */ /* 2 */
@ -224,9 +239,9 @@ select {
*/ */
button, button,
[type='button'], [type="button"],
[type='reset'], [type="reset"],
[type='submit'] { [type="submit"] {
-webkit-appearance: button; -webkit-appearance: button;
/* 1 */ /* 1 */
background-color: transparent; background-color: transparent;
@ -273,7 +288,7 @@ Correct the cursor style of increment and decrement buttons in Safari.
2. Correct the outline style in Safari. 2. Correct the outline style in Safari.
*/ */
[type='search'] { [type="search"] {
-webkit-appearance: textfield; -webkit-appearance: textfield;
/* 1 */ /* 1 */
outline-offset: -2px; outline-offset: -2px;
@ -366,7 +381,8 @@ textarea {
2. Set the default placeholder color to the user's configured gray 400 color. 2. Set the default placeholder color to the user's configured gray 400 color.
*/ */
input::-moz-placeholder, textarea::-moz-placeholder { input::-moz-placeholder,
textarea::-moz-placeholder {
opacity: 1; opacity: 1;
/* 1 */ /* 1 */
color: #9ca3af; color: #9ca3af;
@ -434,7 +450,9 @@ video {
display: none; display: none;
} }
*, ::before, ::after { *,
::before,
::after {
--tw-border-spacing-x: 0; --tw-border-spacing-x: 0;
--tw-border-spacing-y: 0; --tw-border-spacing-y: 0;
--tw-translate-x: 0; --tw-translate-x: 0;
@ -538,7 +556,7 @@ video {
display: block; display: block;
overflow-x: auto; overflow-x: auto;
padding: 0.5em; padding: 0.5em;
background: #F0F0F0; background: #f0f0f0;
} }
.hljs, .hljs,
@ -583,11 +601,11 @@ video {
.hljs-link, .hljs-link,
.hljs-selector-attr, .hljs-selector-attr,
.hljs-selector-pseudo { .hljs-selector-pseudo {
color: #BC6060; color: #bc6060;
} }
.hljs-literal { .hljs-literal {
color: #78A960; color: #78a960;
} }
.hljs-built_in, .hljs-built_in,
@ -1333,8 +1351,10 @@ video {
.shadow { .shadow {
--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color),
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 0 1px 2px -1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
} }
.outline { .outline {
@ -1350,11 +1370,14 @@ video {
} }
.filter { .filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast)
var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate)
var(--tw-sepia) var(--tw-drop-shadow);
} }
.transition-colors { .transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-property: color, background-color, border-color,
text-decoration-color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms; transition-duration: 150ms;
} }
@ -1399,15 +1422,21 @@ video {
} }
.focus\:ring-2:focus { .focus\:ring-2:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); var(--tw-ring-offset-width) var(--tw-ring-offset-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0
calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
var(--tw-shadow, 0 0 #0000);
} }
.focus\:ring-4:focus { .focus\:ring-4:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color); var(--tw-ring-offset-width) var(--tw-ring-offset-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0
calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow),
var(--tw-shadow, 0 0 #0000);
} }
.focus\:ring-blue-500:focus { .focus\:ring-blue-500:focus {
@ -1431,7 +1460,7 @@ video {
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
@keyframes pulse { @keyframes pulse {
50% { 50% {
opacity: .5; opacity: 0.5;
} }
} }

View File

@ -3,8 +3,8 @@
@tailwind utilities; @tailwind utilities;
@layer utilities { @layer utilities {
.striped .col-span-3, .striped .col-span-2 { .striped .col-span-3,
@apply bg-neutral-300 .striped .col-span-2 {
@apply bg-neutral-300;
} }
} }

View File

@ -113,7 +113,7 @@ export const toposDarkTheme = EditorView.theme(
}, },
}, },
}, },
{ dark: true } { dark: true },
); );
/// The highlighting style for code in the Material Dark theme. /// The highlighting style for code in the Material Dark theme.

File diff suppressed because it is too large Load Diff

View File

@ -4028,10 +4028,15 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
zifferjs@^0.0.39: zifferjs@^0.0.44:
version "0.0.39" version "0.0.44"
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.39.tgz#a3916ca1b38d493edea14bf4f29948f2f6f1572e" resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.44.tgz#c6b425166488ec05e349867e3de2460b74204449"
integrity sha512-WMSJ9SPGA/OP/9Z936anUUOM66qzuwPZaE99Qix+Q7jr4fFuoZ/Xw76m2/1C2UVk85sauAecnSfjcK3zo6nA3Q== integrity sha512-Q+0affxeUZwl+oJpsa1nb4hqHV6V4VX+pkejCQq/e9+/0H6ooTpcDQ9IDopvrWBnhA8E11k0tbwUee5TJtE8UQ==
zyklus@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/zyklus/-/zyklus-0.1.4.tgz#229b2966fd1126ef72c6004697269118762bdcd5"
integrity sha512-hbv2cyy4nOI7P8nL8b3ki1jswoLzkUzewPgCLDdDfABryDkV5iO8DAbU25OgO5ShRZHLjXJIylwv5PJQPl3Mpw==
zzfx@^1.2.0: zzfx@^1.2.0:
version "1.2.0" version "1.2.0"