6 Commits

Author SHA1 Message Date
f3ddb39ab6 Reintroduce nudge 2023-11-20 11:12:16 +01:00
8b744e3d90 Delete TransportNode and TransportProcessor 2023-11-20 11:04:15 +01:00
424adeebc0 Cleaning Clock file 2023-11-20 11:03:46 +01:00
ed8bc21713 work in progress 2023-11-20 02:29:47 +01:00
9b7f980027 working on clock 2023-11-20 01:42:44 +01:00
05382bab6e tiny correction 2023-11-19 23:43:18 +01:00
94 changed files with 4645 additions and 16305 deletions

View File

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

View File

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

View File

@ -2,8 +2,8 @@
<p align="center"> |
<a href="https://discord.gg/aPgV7mSFZh">Discord</a> |
<a href="https://raphaelforment.fr/">BuboBubo</a> |
<a href="https://github.com/amiika">Amiika</a> |
<a href="https://raphaelforment.fr/">BuboBubo</a> | 
<a href="about:blank">Amiika</a> |
<a href="https://toplap.org/">About Live Coding</a> |
<br><br>
<h2 align="center"><b>Contributors</b></h2>
@ -12,32 +12,15 @@
<img src="https://contrib.rocks/image?repo=bubobubobubobubo/Topos" />
</a>
</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>
---
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/)
---
Topos is a web-based live coding environment. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal.
![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif)
## Disclaimer
**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.
**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.
## Installation (for devs and contributors)
@ -63,18 +46,15 @@ The `tauri` version is only here to quickstart future developments but nothing
## Docker
### Run the application
`docker run -p 8001:80 yassinsiouda/topos:latest`
### Build and run the prod image
`docker compose --profile prod up`
### Build and run the dev image
**First installation**
First you need to map node_modules to your local machine for your ide intellisense to work properly
```bash
docker compose --profile dev up -d
docker cp topos-dev:/app/node_modules .
@ -82,7 +62,7 @@ docker compose --profile dev down
```
**Then**
```bash
docker compose --profile dev up
```

View File

@ -1,36 +0,0 @@
const WebSocket = require("ws");
const osc = require("osc");
const cleanIncomingOSC = (oscMsg) => {
let data = oscMsg.args;
// Remove information about type of data
data = data.map((item) => {
return item.value;
});
return { data: data, address: oscMsg.address };
};
// ==============================================
// Receiving and forwarding OSC UDP messages
// Create an osc.js UDP Port listening on port 57121.
console.log("> OSC Input: 127.0.0.1:30000");
const wss = new WebSocket.Server({ port: 3001 });
var udpPort = new osc.UDPPort({
localAddress: "0.0.0.0",
localPort: 30000,
metadata: true,
});
udpPort.on("message", function (oscMsg, timeTag, info) {
console.log(
`> Incoming OSC to ${oscMsg.address}:`,
oscMsg.args.map((item) => {
return item.value;
}),
);
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(cleanIncomingOSC(oscMsg)));
}
});
});
udpPort.open();

View File

@ -1,83 +0,0 @@
const WebSocket = require("ws");
const osc = require("osc");
// Listening to WebSocket messages
const wss = new WebSocket.Server({ port: 3000 });
// 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(
formatAndTypeMessage(message),
message.address,
message.port,
);
console.log(
`> Message sent to ${message.address}:${message.port}: ${JSON.stringify(
message.args,
)}`,
);
} 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 = new osc.UDPPort({
localAddress: "0.0.0.0",
localPort: 3000,
metadata: true,
remoteAddress: "0.0.0.0",
remotePort: 57120,
});
udpPort.on("error", function (error) {
console.error("> UDP Port error:", error);
});
udpPort.on("ready", function () {
//console.log(`> UDP Receive: ${udpPort.options.localPort}`);
console.log("> WebSocket server: 127.0.0.1:3000");
});
udpPort.open();
function sendOscMessage(message, address, port) {
try {
udpPort.options.remotePort = port;
message.address = address;
udpPort.send(message);
} catch (error) {
console.error("> Error sending OSC message:", error);
}
}
const formatAndTypeMessage = (message) => {
let newMessage = {};
delete message.args["address"];
delete message.args["port"];
newMessage.address = message.address;
newMessage.timestamp = osc.timeTag(message.timetag);
args = [...Object.entries(message.args)].flat().map((arg) => {
if (typeof arg === "string") return { type: "s", value: arg };
if (typeof arg === "number") return { type: "f", value: arg };
if (typeof arg === "boolean")
return value ? { type: "s", value: 1 } : { type: "s", value: 0 };
});
newMessage.args = args;
return newMessage;
};

View File

@ -1,14 +0,0 @@
var pjson = require("./package.json");
let banner = `
┏┳┓ ┏┓┏┓┏┓
┃ ┏┓┏┓┏┓┏ ┃┃┗┓┃
┻ ┗┛┣┛┗┛┛ ┗┛┗┛┗┛
${pjson.version}\n`;
function greet() {
console.log(banner);
}
module.exports = {
greet: greet,
};

View File

@ -1,332 +0,0 @@
{
"name": "topos-server",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "topos-server",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc": "^2.4.4",
"ws": "^8.14.2"
}
},
"node_modules/@serialport/binding-mock": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-10.2.2.tgz",
"integrity": "sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "^1.2.1",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@serialport/bindings-cpp": {
"version": "10.8.0",
"resolved": "https://registry.npmjs.org/@serialport/bindings-cpp/-/bindings-cpp-10.8.0.tgz",
"integrity": "sha512-OMQNJz5kJblbmZN5UgJXLwi2XNtVLxSKmq5VyWuXQVsUIJD4l9UGHnLPqM5LD9u3HPZgDI5w7iYN7gxkQNZJUw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"@serialport/parser-readline": "^10.2.1",
"debug": "^4.3.2",
"node-addon-api": "^5.0.0",
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=12.17.0 <13.0 || >=14.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/bindings-interface": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz",
"integrity": "sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==",
"optional": true,
"engines": {
"node": "^12.22 || ^14.13 || >=16"
}
},
"node_modules/@serialport/parser-byte-length": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-10.5.0.tgz",
"integrity": "sha512-eHhr4lHKboq1OagyaXAqkemQ1XyoqbLQC8XJbvccm95o476TmEdW5d7AElwZV28kWprPW68ZXdGF2VXCkJgS2w==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-cctalk": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-10.5.0.tgz",
"integrity": "sha512-Iwsdr03xmCKAiibLSr7b3w6ZUTBNiS+PwbDQXdKU/clutXjuoex83XvsOtYVcNZmwJlVNhAUbkG+FJzWwIa4DA==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-delimiter": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz",
"integrity": "sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-inter-byte-timeout": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.5.0.tgz",
"integrity": "sha512-WPvVlSx98HmmUF9jjK6y9mMp3Wnv6JQA0cUxLeZBgS74TibOuYG3fuUxUWGJALgAXotOYMxfXSezJ/vSnQrkhQ==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-packet-length": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-packet-length/-/parser-packet-length-10.5.0.tgz",
"integrity": "sha512-jkpC/8w4/gUBRa2Teyn7URv1D7T//0lGj27/4u9AojpDVXsR6dtdcTG7b7dNirXDlOrSLvvN7aS5/GNaRlEByw==",
"optional": true,
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/@serialport/parser-readline": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-10.5.0.tgz",
"integrity": "sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ==",
"optional": true,
"dependencies": {
"@serialport/parser-delimiter": "10.5.0"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-ready": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-10.5.0.tgz",
"integrity": "sha512-QIf65LTvUoxqWWHBpgYOL+soldLIIyD1bwuWelukem2yDZVWwEjR288cLQ558BgYxH4U+jLAQahhqoyN1I7BaA==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-regex": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-10.5.0.tgz",
"integrity": "sha512-9jnr9+PCxRoLjtGs7uxwsFqvho+rxuJlW6ZWSB7oqfzshEZWXtTJgJRgac/RuLft4hRlrmRz5XU40i3uoL4HKw==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-slip-encoder": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.5.0.tgz",
"integrity": "sha512-wP8m+uXQdkWSa//3n+VvfjLthlabwd9NiG6kegf0fYweLWio8j4pJRL7t9eTh2Lbc7zdxuO0r8ducFzO0m8CQw==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/parser-spacepacket": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/parser-spacepacket/-/parser-spacepacket-10.5.0.tgz",
"integrity": "sha512-BEZ/HAEMwOd8xfuJSeI/823IR/jtnThovh7ils90rXD4DPL1ZmrP4abAIEktwe42RobZjIPfA4PaVfyO0Fjfhg==",
"optional": true,
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/@serialport/stream": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-10.5.0.tgz",
"integrity": "sha512-gbcUdvq9Kyv2HsnywS7QjnEB28g+6OGB5Z8TLP7X+UPpoMIWoUsoQIq5Kt0ZTgMoWn3JGM2lqwTsSHF+1qhniA==",
"optional": true,
"dependencies": {
"@serialport/bindings-interface": "1.2.2",
"debug": "^4.3.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"optional": true
},
"node_modules/node-gyp-build": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.7.0.tgz",
"integrity": "sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/osc": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/osc/-/osc-2.4.4.tgz",
"integrity": "sha512-YJr2bUCQMc9BIaq1LXgqYpt5Ii7wNy2n0e0BkQiCSziMNrrsYHhH5OlExNBgCrQsum60EgXZ32lFsvR4aUf+ew==",
"dependencies": {
"long": "4.0.0",
"slip": "1.0.2",
"wolfy87-eventemitter": "5.2.9",
"ws": "8.13.0"
},
"optionalDependencies": {
"serialport": "10.5.0"
}
},
"node_modules/osc/node_modules/ws": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/serialport": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/serialport/-/serialport-10.5.0.tgz",
"integrity": "sha512-7OYLDsu5i6bbv3lU81pGy076xe0JwpK6b49G6RjNvGibstUqQkI+I3/X491yBGtf4gaqUdOgoU1/5KZ/XxL4dw==",
"optional": true,
"dependencies": {
"@serialport/binding-mock": "10.2.2",
"@serialport/bindings-cpp": "10.8.0",
"@serialport/parser-byte-length": "10.5.0",
"@serialport/parser-cctalk": "10.5.0",
"@serialport/parser-delimiter": "10.5.0",
"@serialport/parser-inter-byte-timeout": "10.5.0",
"@serialport/parser-packet-length": "10.5.0",
"@serialport/parser-readline": "10.5.0",
"@serialport/parser-ready": "10.5.0",
"@serialport/parser-regex": "10.5.0",
"@serialport/parser-slip-encoder": "10.5.0",
"@serialport/parser-spacepacket": "10.5.0",
"@serialport/stream": "10.5.0",
"debug": "^4.3.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://opencollective.com/serialport/donate"
}
},
"node_modules/slip": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/slip/-/slip-1.0.2.tgz",
"integrity": "sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g=="
},
"node_modules/wolfy87-eventemitter": {
"version": "5.2.9",
"resolved": "https://registry.npmjs.org/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz",
"integrity": "sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw=="
},
"node_modules/ws": {
"version": "8.14.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz",
"integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@ -1,15 +0,0 @@
{
"name": "topos-server",
"version": "0.0.1",
"description": "Topos OSC Server",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Raphaël Forment",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc": "^2.4.4",
"ws": "^8.14.2"
}
}

View File

@ -1,8 +0,0 @@
const WebSocket = require("ws");
const osc = require("osc");
require("./banner").greet();
// Topos to OSC
require("./ToposToOSC");
// OSC to Topos
require("./OSCtoTopos");

View File

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

View File

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

View File

@ -11,13 +11,15 @@
<link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href='/fonts/index.css' >
<link rel="stylesheet" href="/src/output.css" />
<link rel="stylesheet" href='/fonts/index.css' >
<script src="https://unpkg.com/hydra-synth"></script>
</head>
<style>
body {
font-family: "Arial";
background-color: #111827;
overflow: hidden;
position: fixed;
width: 100vw;
@ -26,10 +28,11 @@
padding: 0;
}
.fluid-transition {
.fluid-bg-transition {
transition: background-color 0.05s ease-in-out;
}
.fullscreencanvas {
position: fixed; /* ignore margins */
top: 0px;
@ -63,89 +66,60 @@
z-index: 0;
}
.bar_button {
@apply mx-2 px-2 py-2 flex inline rounded-lg bg-background text-foreground hover:bg-foreground hover:text-background
}
.side_button {
@apply px-2 py-2 bg-background text-foreground rounded-lg hover:bg-foreground hover:text-background
}
.subtitle {
@apply bg-selection_foreground text-sm lg:text-xl border-b py-4 text-foreground
}
.tab_panel {
@apply inline-block lg:px-4 px-8 py-1 text-brightwhite
}
.doc_header {
@apply pl-2 pr-2 lg:text-xl text-sm py-1 my-1 rounded-lg text-white hover:text-brightwhite hover:bg-brightblack
}
.doc_subheader {
@apply pl-2 pr-2 lg:text-xl text-sm ml-6 py-1 my-1 rounded-lg text-white hover:text-brightwhite hover:bg-brightblack
}
</style>
<body id="all" class="z-0 bg-neutral-800 overflow-y-hidden">
<body id="all" class="z-0 overflow-y-hidden bg-black">
<!-- The header is hidden on smaller devices -->
<header class="py-0 block">
<div id="topbar" class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center bg-background">
<a class="flex title-font font-medium items-center mb-0">
<img id="topos-logo" src="topos_frog.svg" class="w-12 h-12 text-selection_foreground p-2 rounded-full bg-foreground" alt="Topos Frog Logo"/>
<input id="universe-viewer" class="hidden transparent xl:block ml-4 text-2xl bg-background text-brightwhite placeholder-brightwhite" id="renamer" type="text" placeholder="Topos">
<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">
<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" />
<input id="universe-viewer" class="hidden bg-transparent xl:block ml-4 text-2xl text-white placeholder-white" id="renamer" type="text" placeholder="Topos">
</a>
<nav class="py-2 flex flex-wrap items-center text-base absolute right-0">
<!-- Play Button -->
<a title="Play button (Ctrl+P)" id="play-button-1" class="bar_button">
<a title="Play button (Ctrl+P)" id="play-button-1" class="flex flex-row mr-2 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg id="play-icon" class="w-7 h-7" fill="currentColor" viewBox="0 0 14 16">
<path d="M0 .984v14.032a1 1 0 0 0 1.506.845l12.006-7.016a.974.974 0 0 0 0-1.69L1.506.139A1 1 0 0 0 0 .984Z"/>
</svg>
<svg id="pause-icon" class="hidden w-7 h-7" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<svg id="pause-icon" class="hidden w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9 13a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0v6Zm4 0a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0v6Z"/>
</svg>
<p id="play-label" class="hidden lg:block text-xl pl-2 inline-block">Play</p>
<p id="play-label" class="hidden lg:block text-xl pl-2 text-white inline-block">Play</p>
</a>
<!-- Stop button -->
<a title="Stop button (Ctrl+R)" id="stop-button-1" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Stop button (Ctrl+R)" id="stop-button-1" class="flex flex-row mr-2 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Z"/>
<rect x="6.5" y="6.5" width="7" height="7" fill="selection_background" rx="1" ry="1"/>
<rect x="6.5" y="6.5" width="7" height="7" fill="black" rx="1" ry="1"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Stop</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Stop</p>
</a>
<!-- Eval button -->
<a title="Eval button (Ctrl+Enter)" id="eval-button-1" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
<a title="Eval button (Ctrl+Enter)" id="eval-button-1" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 1v5h-5M2 19v-5h5m10-4a8 8 0 0 1-14.947 3.97M1 10a8 8 0 0 1 14.947-3.97"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Eval</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Eval</p>
</a>
<a title="Clear button" id="clear-button-1" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
<a title="Clear button" id="clear-button-1" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
<path d="M17 4h-4V2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v2H1a1 1 0 0 0 0 2h1v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1a1 1 0 1 0 0-2ZM7 2h4v2H7V2Zm1 14a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v8Zm4 0a1 1 0 0 1-2 0V8a1 1 0 0 1 2 0v8Z"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Clear</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Clear</p>
</a>
<a title="Share button" id="share-button" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
<a title="Share button" id="share-button" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.013 7.962a3.519 3.519 0 0 0-4.975 0l-3.554 3.554a3.518 3.518 0 0 0 4.975 4.975l.461-.46m-.461-4.515a3.518 3.518 0 0 0 4.975 0l3.553-3.554a3.518 3.518 0 0 0-4.974-4.975L10.3 3.7"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Share</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Share</p>
</a>
<a title="Open Documentation (Ctrl+D)" id="doc-button-1" class="bar_button">
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Open Documentation (Ctrl+D)" id="doc-button-1" class="flex flex-row hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
</svg>
<p class="hidden lg:block text-xl pl-2 inline-block">Docs</p>
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Docs</p>
</a>
</nav>
@ -154,133 +128,121 @@
</header>
<div id="documentation" class="hidden">
<div id="documentation-page" class="flex flex-row transparent">
<aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-background">
<nav class="text-xl sm:text-sm overflow-y-scroll mb-24 bg-background">
<details class="" open>
<div id="documentation-page" class="flex flex-row">
<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">
<details class="" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">Basics</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_introduction" class="doc_header">Welcome </p>
<p rel="noopener noreferrer" id="docs_interface" class="doc_header">Interface</p>
<p rel="noopener noreferrer" id="docs_interaction" class="doc_header">Interaction</p>
<p rel="noopener noreferrer" id="docs_shortcuts" class="doc_header">Keyboard</p>
<p rel="noopener noreferrer" id="docs_mouse" class="doc_header">Mouse</p>
<p rel="noopener noreferrer" id="docs_code" class="doc_header">Coding</p>
<p 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">Welcome </p>
<p rel="noopener noreferrer" id="docs_interface" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interface</p>
<p rel="noopener noreferrer" id="docs_interaction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interaction</p>
<p rel="noopener noreferrer" id="docs_shortcuts" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Keyboard</p>
<p rel="noopener noreferrer" id="docs_mouse" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Mouse</p>
<p rel="noopener noreferrer" id="docs_code" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Coding</p>
</div>
</details>
<details class="space-y-2" open>
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl pb-1 pt-1 text-orange-300">Learning</summary>
<div class="flex flex-col">
<!-- Time -->
<details class="space-y-2">
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header">Time</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_time" class="doc_subheader">Dealing with time</p>
<p rel="noopener noreferrer" id="docs_linear" class="doc_subheader">Time & Transport</p>
<p rel="noopener noreferrer" id="docs_cyclic" class="doc_subheader">Time & Cycles</p>
<p rel="noopener noreferrer" id="docs_longform" class="doc_subheader">Time & Structure</p>
</div>
</details>
<!-- Audio Engine -->
<details class="space-y-2">
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header">Audio Engine</summary>
<!-- Time -->
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Time</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_audio_basics" class="doc_subheader">Playing a sound</p>
<p rel="noopener noreferrer" id="docs_amplitude" class="doc_subheader">Amplitude</p>
<p rel="noopener noreferrer" id="docs_sampler" class="doc_subheader">Sampler</p>
<p rel="noopener noreferrer" id="docs_synths" class="doc_subheader">Synths</p>
<p rel="noopener noreferrer" id="docs_filters" class="doc_subheader">Filters</p>
<p rel="noopener noreferrer" id="docs_effects" class="doc_subheader">Effects</p>
<p rel="noopener noreferrer" id="docs_time" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Dealing with time</p>
<p rel="noopener noreferrer" id="docs_linear" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Transport</p>
<p rel="noopener noreferrer" id="docs_cyclic" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Cycles</p>
<p rel="noopener noreferrer" id="docs_longform" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Structure</p>
</div>
</details>
<!-- Samples -->
<details class="space-y-2">
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header ">Samples</summary>
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Audio Engine</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_sample_list" class="doc_subheader">List of samples</p>
<p rel="noopener noreferrer" id="docs_loading_samples" class="doc_subheader">External samples</p>
<p rel="noopener noreferrer" id="docs_audio_basics" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Playing a sound</p>
<p rel="noopener noreferrer" id="docs_amplitude" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Amplitude</p>
<p rel="noopener noreferrer" id="docs_sampler" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Sampler</p>
<p rel="noopener noreferrer" id="docs_synths" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synths</p>
<p rel="noopener noreferrer" id="docs_reverb_delay" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Effects</p>
</div>
</details>
<p rel="noopener noreferrer" id="docs_midi" class="doc_header">MIDI</p>
<p rel="noopener noreferrer" id="docs_osc" class="doc_header">OSC</p>
<!-- Audio Engine -->
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Samples</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_sample_list" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">List of samples</p>
<p rel="noopener noreferrer" id="docs_loading_samples" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Loading Samples</p>
</div>
</details>
<details class="space-y-2" open>
<p rel="noopener noreferrer" id="docs_patterns" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Patterns</p>
<p rel="noopener noreferrer" id="docs_midi" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">MIDI</p>
</div>
</details>
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl pb-1 pt-1 text-orange-300">Patterns</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_patterns" class="pl-2 pr-2 lg:text-xl text-sm hover:neutral-800 py-1 my-1 rounded-lg doc_header">Array patterns</p>
<!-- Ziffers -->
<details class="space-y-2">
<summary class="doc_header">Ziffers</summary>
<p rel="noopener noreferrer" id="docs_variables" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Global Variables</p>
<p rel="noopener noreferrer" id="docs_lfos" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Low Freq Oscs.</p>
<p rel="noopener noreferrer" id="docs_probabilities" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Probabilities</p>
<p rel="noopener noreferrer" id="docs_chaining" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Chaining</p>
<p rel="noopener noreferrer" id="docs_functions" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Functions</p>
<p rel="noopener noreferrer" id="docs_ziffers" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Ziffers</p>
<!--
<p rel="noopener noreferrer" id="docs_reference" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Reference</p>
-->
</div>
</details>
<details class="space-y-2" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">More</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_ziffers_basics" class="doc_subheader">Basics</p>
<p rel="noopener noreferrer" id="docs_ziffers_scales" class="doc_subheader">Scales</p>
<p rel="noopener noreferrer" id="docs_ziffers_rhythm" class="doc_subheader">Rhythm</p>
<p rel="noopener noreferrer" id="docs_ziffers_algorithmic" class="doc_subheader">Algorithmic</p>
<p rel="noopener noreferrer" id="docs_ziffers_tonnetz" class="doc_subheader">Tonnetz</p>
<p rel="noopener noreferrer" id="docs_ziffers_syncing" class="doc_subheader">Syncing</p>
<a rel="noopener noreferrer" id="docs_synchronisation" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synchronisation</a>
<a rel="noopener noreferrer" id="docs_oscilloscope" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Oscilloscope</a>
<a rel="noopener noreferrer" id="docs_bonus" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Bonus/Trivia</a>
<a rel="noopener noreferrer" id="docs_about" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">About Topos</a>
</div>
</details>
<p rel="noopener noreferrer" id="docs_variables" class="doc_header">Global Variables</p>
<p rel="noopener noreferrer" id="docs_lfos" class="doc_header">Low Freq Oscs.</p>
<p rel="noopener noreferrer" id="docs_probabilities" class="doc_header">Probabilities</p>
<p rel="noopener noreferrer" id="docs_chaining" class="doc_header">Chaining</p>
<p rel="noopener noreferrer" id="docs_functions" class="doc_header">Functions</p>
<p rel="noopener noreferrer" id="docs_generators" class="doc_header">Generators</p>
</div>
</details>
<details class="space-y-2" open>
<summary class="font-semibold lg:text-xl doc_header">More</summary>
<div class="flex flex-col">
<a rel="noopener noreferrer" id="docs_synchronisation" class="doc_subheader">Synchronisation</a>
<a rel="noopener noreferrer" id="docs_oscilloscope" class="doc_subheader">Oscilloscope</a>
<a rel="noopener noreferrer" id="docs_bonus" class="doc_header">Bonus/Trivia</a>
<a rel="noopener noreferrer" id="docs_about" class="doc_header">About Topos</a>
</div>
</details>
<details class="" open>
<details class="" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">Community</summary>
<form action="https://github.com/Bubobubobubobubo/topos">
<input rel="noopener noreferrer" id="github_link" class="doc_header" type="submit" value="GitHub" />
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="GitHub" />
</form>
<form action="https://discord.gg/6T67DqBNNT">
<input rel="noopener noreferrer" id="discord_link" class="doc_header" type="submit" value="Discord" />
</form>
<form action="https://ko-fi.com/raphaelbubo">
<input rel="noopener noreferrer" id="support_link" class="doc_header" type="submit" value="Support" />
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Discord" />
</form>
</details>
</nav>
</aside>
<div id="documentation-content" class="w-full flex-grow-1 h-screen overflow-y-scroll lg:px-12 mx-2 my-2 break-words pb-32 transparent"></div>
<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>
</div>
</div>
<div id="app">
<!-- This modal is used for switching between buffers -->
<div id="modal-buffers" class="invisible flex justify-center items-center absolute top-0 right-0 bottom-0 left-0">
<div id="start-button" class="lg:px-16 px-4 lg:pt-4 lg:pb-4 pt-2 pb-2 rounded-md text-center bg-foreground">
<p class="text-semibold lg:text-2xl text-sm pb-4 text-selection_foreground">Known universes</p>
<div id="modal-buffers" class="invisible bg-gray-900 bg-opacity-50 flex justify-center items-center absolute top-0 right-0 bottom-0 left-0">
<div id="start-button" class="lg:px-16 px-4 lg:pt-4 lg:pb-4 pt-2 pb-2 rounded-md text-center bg-white">
<p class="text-semibold lg:text-2xl text-sm pb-4">Known universes</p>
<p id="existing-universes" class="text-normal lg:h-auto h-48 overflow-y-auto mb-2"></p>
<div id="disclaimer" class="pb-4">
<form id="universe-creator">
<label for="search" class="mb-2 text-sm font-medium sr-only ">Search</label>
<label for="search" class="mb-2 text-sm font-medium text-gray-900 sr-only text-white">Search</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<svg class="w-4 h-4 text-gray-500 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
</svg>
</div>
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm border border-neutral-800 outline-0 rounded-lg neutral-800 " placeholder="Buffer..." required>
<button id="load-universe-button" class="bg-background text-selection_background absolute right-2.5 bottom-2.5 focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm border border-neutral-800 outline-0 rounded-lg bg-neutral-800 text-white" placeholder="Buffer..." required>
<button id="load-universe-button" class="text-black absolute right-2.5 bottom-2.5 bg-white hover:bg-white focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
</div>
</form>
<div class="mt-2 flex space-x-6 border-t rounded-b border-spacing-y-4">
<button id="close-universes-button" data-modal-hide="defaultModal" type="button" class="mt-2 focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center bg-background text-selection_background">Close</button>
<div class="mt-2 flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 border-spacing-y-4">
<button id="close-universes-button" data-modal-hide="defaultModal" type="button" class="mt-2 hover:bg-neutral-700 bg-neutral-800 text-white focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center">Close</button>
</div>
</div>
@ -295,20 +257,23 @@
md:top-0 md:bottom-0 h-screen w-full"
>
<div class="grid w-full grid-col-3">
<div class="white rounded-lg lg:mx-48 mx-0 lg:space-y-8 space-y-4 lg:px-8 bg-foreground">
<h1 class="lg:mt-12 mt-6 font-semibold rounded-lg justify-center lg:text-center lg:pl-0 pl-8 mx-4 subtitle">Topos Application Settings</h1>
<div class="bg-white rounded-lg lg:mx-48 mx-0 lg:space-y-8 space-y-4 lg:px-8">
<h1 class="lg:mt-12 mt-6 font-semibold rounded-lg
bg-gray-800 justify-center lg:text-center lg:pl-0 pl-8 text-white mx-4
text-sm lg:text-xl border-b border-gray-300 py-4">Topos Application Settings</h1>
<div class="flex lg:flex-row flex-col mr-4 ml-4">
<!-- Font Size Selection -->
<div class="rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 lg:pb-4 pb-2 underline underline-offset-4 text-selection_background">Theme Settings</p>
<div class="bg-gray-200 rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<p class="font-bold lg:text-xl text-sm ml-4 lg:pb-4 pb-2 underline underline-offset-4">Font Settings</p>
<div class="mb-6 mx-4 font-semibold">
<label for="default-input" class="block mb-2 ml-1 font-normal sd:text-sm text-foreground">Size:</label>
<input type="text" id="font-size-input" type="number" class="border
text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 focus:border-blue-500">
<label for="default-input" class="block mb-2 ml-1 font-normal sd:text-sm">Size:</label>
<input type="text" id="font-size-input" type="number" class="bg-gray-50 border border-gray-300 text-gray-900
text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700
dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
</div>
<label for="font" class="block ml-5 mb-2 font-medium sd:text-sm text-foreground">Font:</label>
<select id="font-family" class=" ml-4 border mb-2
text-sm rounded-lg focus:border-blue-500 block p-2.5">
<label for="font" class="block ml-5 mb-2 font-medium sd:text-sm">Font:</label>
<select id="font-family" class="bg-gray-50 ml-4 border border-gray-300 mb-2
text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="IBM Plex Mono">IBM Plex Mono</option>
<option value="Jet Brains">Jet Brains</option>
<option value="Courier">Courier</option>
@ -320,199 +285,186 @@
<option value="Steps Mono">Steps Mono</option>
<option value="Steps Mono Thin">Steps Mono Thin</option>
</select>
<div class="rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<label for="theme" class="block ml-5 mb-2 font-medium sd:text-sm text-foreground">Theme:</label>
<select id="theme-selector" class="ml-4 border mb-2
text-sm rounded-lg block p-2.5">
</select>
<div id="theme-previewer"></div>
</div>
</div>
<!-- Editor mode selection -->
<div class="rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">Editor options</p>
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">Editor options</p>
<!-- Checkboxes -->
<div class="pr-4">
<div class="flex items-center mb-4 ml-5">
<input id="vim-mode" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-selection_background">Vim Mode</label>
<input id="vim-mode" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Vim Mode</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-line-numbers" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Line Numbers</label>
<input id="show-line-numbers" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Line Numbers</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-time-position" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Time Position</label>
<input id="show-time-position" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Time Position</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Hovering Tips</label>
<input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Hovering Tips</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="show-completions" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Completions</label>
<input id="show-completions" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Completions</label>
</div>
<div class="flex items-center mb-4 ml-5">
<input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Load Demo Song</label>
<input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Load Demo Song</label>
</div>
</div>
</div>
<div class="rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">File Management</p>
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">File Management</p>
<div class="flex flex-col space-y-2 pb-2">
<button id="download-universes" class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
<button id="download-universes" class="bg-gray-800 hover:bg-gray-900 text-white font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<svg class="fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
<span class="text-selection_foreground">Download universes</span>
<span>Download universes</span>
</button>
<button id="upload-universes" class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
<button id="upload-universes" class="bg-gray-800 hover:bg-gray-900 text-white font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<svg class="rotate-180 fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
<span class="text-selection_foreground">Upload universes</span>
<span>Upload universes</span>
</button>
<button id="destroy-universes" class="bg-brightwhite font-bold lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<button id="destroy-universes" class="bg-red-800 hover:bg-red-900 text-white font-bold lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-6 mr-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
<span class="text-selection_foreground">Destroy universes</span>
<span>Destroy universes</span>
</button>
<!-- Upload audio samples -->
<p class="font-bold lg:text-xl text-sm ml-4 pb-2 pt-2 underline underline-offset-4 text-selection_background">Audio samples</p>
<label class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
<svg class="rotate-180 fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
<input id="upload-samples" type="file" class="hidden" accept="file" webkitdirectory directory multiple>
<span id="sample-indicator" class="text-selection_foreground">Import samples</span>
</label>
</div>
</div>
</div>
<!-- Midi settings -->
<div id="midi-settings-container" class="rounded-lg flex lg:flex-row flex-col mx-4 my-4 pt-4 bg-color bg-selection_foreground">
<div id="midi-settings-container" class="bg-gray-200 rounded-lg flex lg:flex-row flex-col mx-4 my-4 pt-4">
<div class="lg:flex lg:flex-row w-fit">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">MIDI I/O Settings</p>
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">MIDI I/O Settings</p>
<div class="flex items-center mb-4 ml-6">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">MIDI Clock:&nbsp;</label>
<select id="midi-clock-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">MIDI Clock:&nbsp;</label>
<select id="midi-clock-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
<option value="-1">Internal</option>
</select>
</div>
<div class="lg:flex block items-center mb-4 ml-6">
<label for="default-checkbox" class="ml-2 mr-2 text-sm font-medium text-foreground">Clock PPQN:&nbsp;</label>
<select id="midi-clock-ppqn-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Clock PPQN:&nbsp;</label>
<select id="midi-clock-ppqn-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
<option value="24">24</option>
<option value="48">48</option>
</select>
</div>
<div class="lg:flex block items-center mb-4 ml-6">
<input id="send-midi-clock" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Send MIDI Clock</label>
<input id="send-midi-clock" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Send MIDI Clock</label>
</div>
</div>
<div class="lg:flex block flex-row">
<div class="flex items-center mb-4 ml-6">
<label for="default-checkbox" class="ml-2 mr-2 text-sm font-medium text-foreground">MIDI input:&nbsp;</label>
<select id="default-midi-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600 focus:ring-2">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">MIDI input:&nbsp;</label>
<select id="default-midi-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
<option value="-1">None</option>
</select>
</div>
<div class="lg:flex block items-center mb-4 ml-6">
<input id="midi-channels-scripts" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 rounded focus:ring-blue-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Route channels to scripts</label>
<input id="midi-channels-scripts" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Route channels to scripts</label>
</div>
</div>
</div>
<!-- Audio nudge slider -->
<div id="midi-settings-container" class="rounded-lg flex flex-col mx-4 my-4 pt-4 pb-2 bg-selection_foreground">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">Audio/Event Nudging</p> <div class="flex flex-column pb-2">
<p class="pt-0.5 ml-4 text-foreground">Clock:</p>
<div id="midi-settings-container" class="bg-gray-200 rounded-lg flex flex-col mx-4 my-4 pt-4 pb-2">
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">Audio/Event Nudging</p>
<div class="flex flex-column pb-2">
<p class="pt-0.5 ml-4">Clock:</p>
<input
type="range" id="audio_nudge"
name="audiorangeInput"
min="-200" max="200"
value="0"
class="w-full ml-4 text-red"
class="w-full ml-4"
oninput="nudgenumber.value=audio_nudge.value"
>
<output
name="nudgenumber"
id="nudgenumber"
for="audiorangeInput"
class="rounded-lg ml-2 mr-4 px-4 py-1 text-foreground"
class="bg-gray-500 rounded-lg ml-2 mr-4 px-4 py-1 text-white"
>0</output>
</div>
<div class="flex flex-column">
<p class="pt-0.5 ml-4 text-foreground">Audio:</p>
<p class="pt-0.5 ml-4">Audio:</p>
<input
type="range" id="dough_nudge"
name="doughrangeInput"
min="0" max="100"
value="0"
class="w-full ml-4 text-foreground"
class="w-full ml-4"
oninput="doughnumber.value=dough_nudge.value"
>
<output
name="doughnumber"
id="doughnumber"
for="doughrangeInput"
class="rounded-lg ml-2 mr-4 px-4 py-1 text-foreground"
class="bg-gray-500 rounded-lg ml-2 mr-4 px-4 py-1 text-white"
>0</output>
</div>
</div>
<div class="flex space-x-6 border-t rounded-b mx-4 border-spacing-y-4 pb-36 lg:pb-0">
<div class="flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 mx-4 border-spacing-y-4 pb-36 lg:pb-0">
<button id="close-settings-button" data-modal-hide="defaultModal" type="button" class="
hover:bg-background bg-background mt-4 mb-4 focus:ring-4
font-medium rounded-lg text-sm px-5 py-2.5 text-center text-selection_background">OK</button>
hover:bg-gray-700 bg-gray-800 mt-4 mb-4 text-white focus:ring-4
font-medium rounded-lg text-sm px-5 py-2.5 text-center">OK</button>
</div>
</div>
</div>
</div>
<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. -->
<aside id="sidebar" class="
<aside class="
flex flex-col items-center w-14
h-screen py-2 border-r
rtl:border-l max-h-fit
rtl:border-r-0 bg-background
border-neutral-700 border-none"
rtl:border-r-0 bg-neutral-900
dark:border-neutral-700 border-none"
>
<nav class="flex flex-col space-y-6">
<a title="Local Scripts (Ctrl + L)" id="local-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 18">
<a title="Local Scripts (Ctrl + L)" id="local-button" class="pl-2 p-1.5 focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 18">
<path d="M18 5H0v11a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5Zm-7.258-2L9.092.8a2.009 2.009 0 0 0-1.6-.8H2.049a2 2 0 0 0-2 2v1h10.693Z"/>
</svg>
</svg>
</a>
<a title="Global Script (Ctrl + G)" id="global-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 16">
<a title="Global Script (Ctrl + G)" id="global-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 16">
<path d="M14.316.051A1 1 0 0 0 13 1v8.473A4.49 4.49 0 0 0 11 9c-2.206 0-4 1.525-4 3.4s1.794 3.4 4 3.4 4-1.526 4-3.4a2.945 2.945 0 0 0-.067-.566c.041-.107.064-.22.067-.334V2.763A2.974 2.974 0 0 1 16 5a1 1 0 0 0 2 0C18 1.322 14.467.1 14.316.051ZM10 3H1a1 1 0 0 1 0-2h9a1 1 0 1 1 0 2Z"/>
<path d="M10 7H1a1 1 0 0 1 0-2h9a1 1 0 1 1 0 2Zm-5 4H1a1 1 0 0 1 0-2h4a1 1 0 1 1 0 2Z"/>
</svg>
</a>
<a title="Initialisation Script (Ctrl + I)" id="init-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 14">
<a title="Initialisation Script (Ctrl + I)" id="init-button" class="pl-2 p-1.5 focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1v12m0 0 4-4m-4 4L1 9"/>
</svg>
</a>
<a title="Project notes (Ctrl + N)" id="note-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Project notes (Ctrl + N)" id="note-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="m13.835 7.578-.005.007-7.137 7.137 2.139 2.138 7.143-7.142-2.14-2.14Zm-10.696 3.59 2.139 2.14 7.138-7.137.007-.005-2.141-2.141-7.143 7.143Zm1.433 4.261L2 12.852.051 18.684a1 1 0 0 0 1.265 1.264L7.147 18l-2.575-2.571Zm14.249-14.25a4.03 4.03 0 0 0-5.693 0L11.7 2.611 17.389 8.3l1.432-1.432a4.029 4.029 0 0 0 0-5.689Z"/>
</svg>
</a>
<a title="Application Settings" id="settings-button" class="side_button">
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<a title="Application Settings" id="settings-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
@ -526,54 +478,55 @@
<!-- Tabs for local files -->
<div class="min-w-screen flex grow flex-col">
<ul id="local-script-tabs" class=" flex text-xl font-medium text-center bg-background space-x-1 lg:space-x-8">
<ul id="local-script-tabs" class=" flex text-xl font-medium text-center text-white bg-neutral-900 space-x-1 lg:space-x-8">
<li class="pl-5">
<a title="Local Script 1 (F1)" id="tab-1" class="tab_panel">1</a>
<a title="Local Script 1 (F1)" id="tab-1" class="bg-orange-300 inline-block lg:px-4 px-2 py-1 text-white hover:bg-gray-800">1</a>
</li>
<li class="">
<a title="Local Script 2 (F2)" id="tab-2" class="tab_panel">2</a>
<a title="Local Script 2 (F2)" id="tab-2" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">2</a>
</li>
<li class="">
<a title="Local Script 3 (F3)" id="tab-3" class="tab_panel">3</a>
<a title="Local Script 3 (F3)" id="tab-3" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">3</a>
</li>
<li class="">
<a title="Local Script 4 (F4)" id="tab-4" class="tab_panel">4</a>
<a title="Local Script 4 (F4)" id="tab-4" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">4</a>
</li>
<li class="">
<a title="Local Script 5 (F5)" id="tab-5" class="tab_panel">5</a>
<a title="Local Script 5 (F5)" id="tab-5" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">5</a>
</li>
<li class="">
<a title="Local Script 6 (F6)" id="tab-6" class="tab_panel">6</a>
<a title="Local Script 6 (F6)" id="tab-6" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">6</a>
</li>
<li class="">
<a title="Local Script 7 (F7)" id="tab-7" class="tab_panel">7</a>
<a title="Local Script 7 (F7)" id="tab-7" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">7</a>
</li>
<li class="">
<a title="Local Script 8 (F8)" id="tab-8" class="tab_panel">8</a>
<a title="Local Script 8 (F8)" id="tab-8" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">8</a>
</li>
<li class="">
<a title="Local Script 9 (F9)" id="tab-9" class="tab_panel">9</a>
<a title="Local Script 9 (F9)" id="tab-9" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">9</a>
</li>
</ul>
<!-- Here comes the editor itself -->
<div id="editor" class="relative flex flex-row h-screen overflow-y-hidden">
<canvas id="scope" class="fullscreencanvas"></canvas>
<canvas id="hydra-bg" class="fullscreencanvas"></canvas>
<canvas id="scope" class="fullscreencanvas"></canvas>
<canvas id="feedback" class="fullscreencanvas"></canvas>
</div>
<p id="error_line" class="hidden w-screen bg-background font-mono absolute bottom-0 pl-2 py-2">Hello kids</p>
<p id="error_line" class="hidden text-red-400 w-screen bg-neutral-900 font-mono absolute bottom-0 pl-2 py-2">Hello kids</p>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
<template id="ui-known-universe-item-template">
<!-- A known universe button in "opening" interface -->
<li class="py-2 px-4 flex justify-between text-brightwhite hover:bg-selection_background hover:text-selection_foreground">
<li class="hover:fill-black hover:bg-white py-2 hover:text-black flex justify-between px-4">
<button class="universe-name load-universe" title="Load this universe">Universe Name</button>
<button class="delete-universe" title="Delete this universe">🗑</button>
</li>
</template>
</body>
<p id="timeviewer" class="rounded-lg px-2 py-2 font-bold cursor-textpointer-events-none select-none text-sm absolute bottom-2 right-2 bg-foreground text-background"></p>
<p id="fillviewer" class="invisible rounded-lg px-2 py-2 font-bold cursor-textpointer-events-none select-none text-sm absolute right-2 bottom-12 bg-foreground text-background">/////// Fill ///////</p>
<p id="timeviewer" class="rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute bottom-2 right-2"></p>
<p id="fillviewer" class="invisible rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute right-2 bottom-12">/////// Fill ///////</p>
</html>

View File

@ -30,22 +30,19 @@
"autoprefixer": "^10.4.14",
"codemirror": "^6.0.1",
"fflate": "^0.8.0",
"highlight.js": "^11.9.0",
"jisg": "^0.9.7",
"lru-cache": "^10.0.1",
"marked": "^7.0.3",
"osc": "^2.4.4",
"postcss": "^8.4.27",
"showdown": "^2.1.0",
"showdown-highlight": "^3.1.0",
"superdough": "^0.9.12",
"superdough": "^0.9.11",
"tailwind-highlightjs": "^2.0.1",
"tailwindcss": "^3.3.3",
"tone": "^14.8.49",
"unique-names-generator": "^4.7.1",
"vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.55",
"zyklus": "^0.1.4",
"zifferjs": "^0.0.39",
"zzfx": "^1.2.0"
}
}

View File

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

View File

@ -1,7 +1,5 @@
import { EditorView } from "@codemirror/view";
import { sendToServer, type OSCMessage, oscMessages } from "./IO/OSC";
import { getAllScaleNotes, nearScales, seededRandom } from "zifferjs";
import colorschemes from "./colors.json";
import {
MidiCCEvent,
MidiConnection,
@ -14,7 +12,6 @@ import { SoundEvent } from "./classes/SoundEvent";
import { MidiEvent, MidiParams } from "./classes/MidiEvent";
import { LRUCache } from "lru-cache";
import { InputOptions, Player } from "./classes/ZPlayer";
import { isGenerator, isGeneratorFunction } from "./Utils/Generic";
import {
loadUniverse,
openUniverseModal,
@ -30,8 +27,7 @@ import {
} from "superdough";
import { Speaker } from "./extensions/StringExtensions";
import { getScaleNotes } from "zifferjs";
import { OscilloscopeConfig } from "./Visuals/Oscilloscope";
import { blinkScript } from "./Visuals/Blinkers";
import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation";
import { SkipEvent } from "./classes/SkipEvent";
import { AbstractEvent, EventOperation } from "./classes/AbstractEvents";
import drums from "./tidal-drum-machines.json";
@ -45,31 +41,16 @@ interface ControlChange {
export async function loadSamples() {
return Promise.all([
initAudioOnFirstClick(),
samples("github:tidalcycles/Dirt-Samples/master", undefined, {
tag: "Tidal",
}).then(() => registerSynthSounds()),
samples("github:tidalcycles/Dirt-Samples/master", undefined, { tag: "Tidal" }).then(() =>
registerSynthSounds()
),
registerZZFXSounds(),
samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", {
tag: "Machines",
}),
samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, {
tag: "FoxDot",
}),
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",
}),
samples(drums, "github:ritchse/tidal-drum-machines/main/machines/", { tag: "Machines" }),
samples("github:Bubobubobubobubo/Dough-Fox/main", undefined, { tag: "FoxDot" }),
samples("github:Bubobubobubobubo/Dough-Samples/main", undefined, { tag: "Pack" }),
samples("github:Bubobubobubobubo/Dough-Amiga/main", undefined, { tag: "Amiga" }),
samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, { tag: "Amen" }),
samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, { tag: "Waveforms" }),
]);
}
@ -88,14 +69,11 @@ export class UserAPI {
public randomGen = Math.random;
public currentSeed: string | undefined = undefined;
public localSeeds = new Map<string, Function>();
public patternCache = new LRUCache({ max: 10000, ttl: 10000 * 60 * 5 });
public invalidPatterns: {[key: string]: boolean} = {};
public cueTimes: { [key: string]: number } = {};
public patternCache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 });
private errorTimeoutID: number = 0;
private printTimeoutID: number = 0;
public MidiConnection: MidiConnection;
public scale_aid: string | number | undefined = undefined;
public hydra: any;
load: samples;
constructor(public app: Editor) {
@ -117,7 +95,7 @@ export class UserAPI {
}
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings,
this.app.settings
);
this.app.updateKnownUniversesView();
};
@ -147,7 +125,6 @@ export class UserAPI {
? code
: (this.app.selectedExample as string);
}
this.clearPatternCache();
this.stop();
this.play();
};
@ -159,7 +136,6 @@ export class UserAPI {
current_universe.example.candidate! = "";
current_universe.example.committed! = "";
}
this.clearPatternCache();
this.stop();
};
@ -169,7 +145,6 @@ export class UserAPI {
current_universe.example.candidate! = "";
current_universe.example.committed! = "";
}
this.clearPatternCache();
this.stop();
this.play();
this.app.exampleIsPlaying = true;
@ -212,7 +187,7 @@ export class UserAPI {
// @ts-ignore
this.errorTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
2000,
2000
);
};
@ -221,12 +196,12 @@ export class UserAPI {
clearTimeout(this.printTimeoutID);
clearTimeout(this.errorTimeoutID);
this.app.interface.error_line.innerHTML = message as string;
this.app.interface.error_line.style.color = "red";
this.app.interface.error_line.style.color = "white";
this.app.interface.error_line.classList.remove("hidden");
// @ts-ignore
this.printTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
4000,
4000
);
};
@ -277,7 +252,7 @@ export class UserAPI {
*/
this.app.clock.tick = beat * this.app.clock.ppqn;
this.app.clock.time_position = this.app.clock.convertTicksToTimeposition(
beat * this.app.clock.ppqn,
beat * this.app.clock.ppqn
);
};
@ -334,7 +309,7 @@ export class UserAPI {
blinkScript(this.app, "local", arg);
tryEvaluate(
this.app,
this.app.universes[this.app.selected_universe].locals[arg],
this.app.universes[this.app.selected_universe].locals[arg]
);
}
});
@ -381,7 +356,7 @@ export class UserAPI {
delete this.app.universes[universe];
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings,
this.app.settings
);
this.app.updateKnownUniversesView();
};
@ -397,7 +372,7 @@ export class UserAPI {
};
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings,
this.app.settings
);
}
this.app.selected_universe = "Default";
@ -434,7 +409,7 @@ export class UserAPI {
value: number | number[] = 60,
velocity?: number | number[],
channel?: number | number[],
port?: number | string | number[] | string[],
port?: number | string | number[] | string[]
): MidiEvent => {
/**
* Sends a MIDI note to the current MIDI output.
@ -509,7 +484,7 @@ export class UserAPI {
};
public active_note_events = (
channel?: number,
channel?: number
): MidiNoteEvent[] | undefined => {
/**
* @returns A list of currently active MIDI notes
@ -646,7 +621,7 @@ export class UserAPI {
scale: number | string,
channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0,
soundOff: boolean = false,
soundOff: boolean = false
): void => {
/**
* Sends given scale to midi output for visual aid
@ -670,7 +645,7 @@ export class UserAPI {
// @ts-ignore
scale: number | string = 0,
channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0
): void => {
/**
* Hides all notes by sending all notes off to midi output
@ -685,7 +660,7 @@ export class UserAPI {
midi_notes_off = (
channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0
): void => {
/**
* Sends all notes off to midi output
@ -695,7 +670,7 @@ export class UserAPI {
midi_sound_off = (
channel: number = 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0,
port: number | string = this.MidiConnection.currentOutputIndex || 0
): void => {
/**
* Sends all sound off to midi output
@ -704,7 +679,7 @@ export class UserAPI {
};
// =============================================================
// Cache functions
// Ziffers related functions
// =============================================================
public generateCacheKey = (...args: any[]): string => {
@ -715,111 +690,30 @@ export class UserAPI {
this.patternCache.forEach((player) => (player as Player).reset());
};
public clearPatternCache = (): void => {
this.patternCache.clear();
}
public removePatternFromCache = (id: string): void => {
this.patternCache.delete(id);
};
maybeToNumber = (something: any): number|any => {
// If something is BigInt
if(typeof something === "bigint") {
return Number(something);
} else {
return something;
}
}
cache = (key: string, value: any) => {
/**
* Gets or sets a value in the cache.
*
* @param key - The key of the value to get or set
* @param value - The value to set
* @returns The value of the key
*/
if(value !== undefined) {
if(isGenerator(value)) {
if(this.patternCache.has(key)) {
const cachedValue = (this.patternCache.get(key) as Generator<any>).next().value
if(cachedValue!==0 && !cachedValue) {
const generator = value as unknown as Generator<any>
this.patternCache.set(key, generator);
return this.maybeToNumber(generator.next().value);
}
return this.maybeToNumber(cachedValue);
} else {
const generator = value as unknown as Generator<any>
this.patternCache.set(key, generator);
return this.maybeToNumber(generator.next().value);
}
} else if(isGeneratorFunction(value)) {
if(this.patternCache.has(key)) {
const cachedValue = (this.patternCache.get(key) as Generator<any>).next().value;
if(cachedValue || cachedValue===0 || cachedValue===0n) {
return this.maybeToNumber(cachedValue);
} else {
const generator = value();
this.patternCache.set(key, generator);
return this.maybeToNumber(generator.next().value);
}
} else {
const generator = value();
this.patternCache.set(key, generator);
return this.maybeToNumber(generator.next().value);
}
} else {
this.patternCache.set(key, value);
return this.maybeToNumber(value);
}
} else {
return this.maybeToNumber(this.patternCache.get(key));
}
}
// =============================================================
// Ziffers related functions
// =============================================================
public z = (
input: string | Generator<number>,
options: InputOptions = {},
id: number | string = "",
id: number | string = ""
): Player => {
const zid = "z" + id.toString();
const key = id === "" ? this.generateCacheKey(input, options) : zid;
const validSyntax = typeof input === "string" && !this.invalidPatterns[input]
let player;
let replace = false;
if (this.app.api.patternCache.has(key)) {
player = this.app.api.patternCache.get(key) as Player;
if (typeof input === "string" &&
player.input !== input &&
player.atTheBeginning()) {
replace = true;
if (typeof input === "string" && player.input !== input) {
player = undefined;
}
}
if ((typeof input !== "string" || validSyntax) && (!player || replace)) {
const newPlayer = new Player(input, options, this.app, zid);
if(newPlayer.isValid()) {
player = newPlayer
this.patternCache.set(key, player);
} else if(typeof input === "string") {
this.invalidPatterns[input] = true;
}
}
if(player) {
if(player.atTheBeginning()) {
if(typeof input === "string" && !validSyntax) this.app.api.log(`Invalid syntax: ${input}`);
if (!player) {
player = new Player(input, options, this.app, zid);
this.app.api.patternCache.set(key, player);
}
if (player.ziffers.generator && player.ziffers.generatorDone) {
@ -832,13 +726,10 @@ export class UserAPI {
if (id !== "" && zid !== "z0") {
// Sync named patterns to z0 by default
player.sync("z0", false);
player.sync("z0");
}
return player;
} else {
throw new Error(`Invalid syntax: ${input}`);
}
};
public z0 = (input: string, opts: InputOptions = {}) =>
@ -883,7 +774,7 @@ export class UserAPI {
public counter = (
name: string | number,
limit?: number,
step?: number,
step?: number
): number => {
/**
* Returns the current value of a counter, and increments it by the step value.
@ -1391,7 +1282,7 @@ export class UserAPI {
(value) =>
(this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) %
Math.floor(value * this.ppqn()) ===
0,
0
);
return results.some((value) => value === true);
};
@ -1411,7 +1302,7 @@ export class UserAPI {
(value) =>
(this.app.clock.pulses_since_origin - nudgeInPulses) %
Math.floor(value * barLength) ===
0,
0
);
return results.some((value) => value === true);
};
@ -1426,7 +1317,7 @@ export class UserAPI {
*/
const nArray = Array.isArray(n) ? n : [n];
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);
};
@ -1435,7 +1326,7 @@ export class UserAPI {
public tick = (tick: number | number[], offset: number = 0): boolean => {
const nArray = Array.isArray(tick) ? tick : [tick];
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);
};
@ -1484,7 +1375,7 @@ export class UserAPI {
public onbar = (
bars: number[] | number,
n: number = this.app.clock.time_signature[0],
n: number = this.app.clock.time_signature[0]
): boolean => {
let current_bar = (this.app.clock.time_position.bar % n) + 1;
return typeof bars === "number"
@ -1512,7 +1403,7 @@ export class UserAPI {
if (decimal_part <= 0)
decimal_part = decimal_part + this.ppqn() * this.nominator();
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);
@ -1594,7 +1485,7 @@ export class UserAPI {
iterator: number,
pulses: number,
length: number,
rotate: number = 0,
rotate: number = 0
): boolean => {
/**
* Returns a euclidean cycle of size length, with n pulses, rotated or not.
@ -1613,7 +1504,7 @@ export class UserAPI {
div: number,
pulses: number,
length: number,
rotate: number = 0,
rotate: number = 0
): boolean => {
return (
this.beat(div) && this._euclidean_cycle(pulses, length, rotate).beat(div)
@ -1623,7 +1514,7 @@ export class UserAPI {
_euclidean_cycle(
pulses: number,
length: number,
rotate: number = 0,
rotate: number = 0
): boolean[] {
if (pulses == length) return Array.from({ length }, () => true);
function startsDescent(list: number[], i: number): boolean {
@ -1634,7 +1525,7 @@ export class UserAPI {
if (pulses >= length) return [true];
const resList = Array.from(
{ length },
(_, i) => (((pulses * (i - 1)) % length) + length) % length,
(_, i) => (((pulses * (i - 1)) % length) + length) % length
);
let cycle = resList.map((_, i) => startsDescent(resList, i));
if (rotate != 0) {
@ -1673,9 +1564,7 @@ export class UserAPI {
// Low Frequency Oscillators
// =============================================================
public range = (v: number, a: number, b: number): number => v * (b - a) + a;
public line = (start: number, end: number, step: number = 1): number[] => {
line = (start: number, end: number, step: number = 1): number[] => {
/**
* Returns an array of values between start and end, with a given step.
*
@ -1697,11 +1586,7 @@ export class UserAPI {
return result;
};
public sine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
sine = (freq: number = 1, times: number = 1, offset: number = 0): number => {
/**
* Returns a sine wave between -1 and 1.
*
@ -1715,11 +1600,7 @@ export class UserAPI {
);
};
public usine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
usine = (freq: number = 1, times: number = 1, offset: number = 0): number => {
/**
* Returns a sine wave between 0 and 1.
*
@ -1763,7 +1644,7 @@ export class UserAPI {
triangle = (
freq: number = 1,
times: number = 1,
offset: number = 0,
offset: number = 0
): number => {
/**
* Returns a triangle wave between -1 and 1.
@ -1780,7 +1661,7 @@ export class UserAPI {
utriangle = (
freq: number = 1,
times: number = 1,
offset: number = 0,
offset: number = 0
): number => {
/**
* Returns a triangle wave between 0 and 1.
@ -1797,7 +1678,7 @@ export class UserAPI {
freq: number = 1,
times: number = 1,
offset: number = 0,
duty: number = 0.5,
duty: number = 0.5
): number => {
/**
* Returns a square wave with a specified duty cycle between -1 and 1.
@ -1817,7 +1698,7 @@ export class UserAPI {
freq: number = 1,
times: number = 1,
offset: number = 0,
duty: number = 0.5,
duty: number = 0.5
): number => {
/**
* Returns a square wave between 0 and 1.
@ -1877,11 +1758,23 @@ export class UserAPI {
*/
const sum = values.reduce(
(accumulator, currentValue) => accumulator + currentValue,
0,
0
);
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 => {
/**
* Limits a value between a minimum and a maximum.
@ -1905,7 +1798,7 @@ export class UserAPI {
lang: string = "en-US",
voice: number = 0,
rate: number = 1,
pitch: number = 1,
pitch: number = 1
): void => {
/*
* Speaks the given text using the browser's speech synthesis API.
@ -1980,7 +1873,7 @@ export class UserAPI {
const elements = args.slice(1); // Get the rest of the arguments as an array
const timepos = this.app.clock.pulses_since_origin;
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];
};
@ -2008,13 +1901,10 @@ export class UserAPI {
// =============================================================
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);
};
};
}
public shuffle = <T>(array: T[]): T[] => {
/**
@ -2115,7 +2005,7 @@ export class UserAPI {
".cm-comment": {
fontFamily: commentFont,
},
}),
})
),
});
};
@ -2187,36 +2077,23 @@ export class UserAPI {
}, real_duration * 1000);
};
// =============================================================
// OSC Functions
// =============================================================
public osc = (address: string, port: number, ...args: any[]): void => {
sendToServer({
address: address,
port: port,
args: args,
timetag: Math.round(Date.now() + this.app.clock.deadline),
} as OSCMessage);
};
public getOSC = (address?: string): any[] => {
/**
* Give access to incoming OSC messages. If no address is specified, returns the raw oscMessages array. If an address is specified, returns only the messages who contain the address and filter the address itself.
*/
if (address) {
let messages = oscMessages.filter((msg) => msg.address === address);
messages = messages.map((msg) => msg.data);
return messages;
} else {
return oscMessages;
}
};
// =============================================================
// 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 => {
/**
* Sets or returns the current bpm.
@ -2267,40 +2144,4 @@ export class UserAPI {
*/
this.app.clock.time_signature = [numerator, denominator];
};
public cue = (functionName: string|Function): void => {
functionName = typeof functionName === "function" ? functionName.name : functionName;
this.cueTimes[functionName] = this.app.clock.pulses_since_origin;
};
public theme = (color_scheme: string): void => {
this.app.readTheme(color_scheme);
console.log("Changing color scheme for: ", color_scheme)
}
public themeName = (): string => {
return this.app.currentThemeName;
}
public randomTheme = (): void => {
let theme_names = this.getThemes();
let selected_theme = theme_names[Math.floor(Math.random() * theme_names.length)];
this.app.readTheme(selected_theme);
this.app.api.log(selected_theme);
}
public nextTheme = (): void => {
let theme_names = this.getThemes();
let current_theme = this.app.api.themeName();
let current_theme_idx = theme_names.indexOf(current_theme);
let next_theme_idx = (current_theme_idx + 1) % theme_names.length;
let next_theme = theme_names[next_theme_idx];
this.app.readTheme(next_theme);
this.app.api.log(next_theme);
}
public getThemes = (): string[] => {
return Object.keys(colorschemes);
}
}

View File

@ -1,6 +1,122 @@
// @ts-ignore
import { getAnalyser } from "superdough";
import { Editor } from "../main";
import { type 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 {
enabled: boolean;
@ -18,16 +134,15 @@ export interface OscilloscopeConfig {
let lastZeroCrossingType: string | null = null; // 'negToPos' or 'posToNeg'
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 = (
canvas: HTMLCanvasElement,
app: Editor,
app: Editor
): 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 analyzer = getAnalyser(config.fftSize);
let dataArray = new Float32Array(analyzer.frequencyBinCount);
@ -40,7 +155,7 @@ export const runOscilloscope = (
width: number,
height: number,
offset_height: number,
offset_width: number,
offset_width: number
) {
const maxFPS = 30;
const now = performance.now();
@ -54,12 +169,10 @@ export const runOscilloscope = (
canvasCtx.clearRect(0, 0, width, height);
const performanceFactor = 1;
const reducedDataSize = Math.floor(
freqDataArray.length * performanceFactor,
);
const reducedDataSize = Math.floor(freqDataArray.length * performanceFactor);
const numBars = Math.min(
reducedDataSize,
app.osc.orientation === "horizontal" ? width : height,
app.osc.orientation === "horizontal" ? width : height
);
const barWidth =
app.osc.orientation === "horizontal" ? width / numBars : height / numBars;
@ -71,8 +184,7 @@ export const runOscilloscope = (
for (let i = 0; i < numBars; i++) {
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") {
@ -80,7 +192,7 @@ export const runOscilloscope = (
x + offset_width,
(height - barHeight) / 2 + offset_height,
barWidth + 1,
barHeight,
barHeight
);
x += barWidth;
} else {
@ -88,13 +200,14 @@ export const runOscilloscope = (
(width - barHeight) / 2 + offset_width,
y + offset_height,
barHeight,
barWidth + 1,
barWidth + 1
);
y += barWidth;
}
}
}
function draw() {
// Update the canvas position on each cycle
const WIDTH = canvas.width;
@ -117,19 +230,12 @@ export const runOscilloscope = (
-OFFSET_WIDTH,
-OFFSET_HEIGHT,
WIDTH + 2 * OFFSET_WIDTH,
HEIGHT + 2 * OFFSET_HEIGHT,
HEIGHT + 2 * OFFSET_HEIGHT
);
return;
}
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);
dataArray = new Float32Array(analyzer.frequencyBinCount);
}
@ -144,7 +250,7 @@ export const runOscilloscope = (
-OFFSET_WIDTH,
-OFFSET_HEIGHT,
WIDTH + 2 * OFFSET_WIDTH,
HEIGHT + 2 * OFFSET_HEIGHT,
HEIGHT + 2 * OFFSET_HEIGHT
);
}
canvasCtx.lineWidth = app.osc.thickness;

View File

@ -1,9 +1,6 @@
// @ts-ignore
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");
@ -22,75 +19,118 @@ export interface TimePosition {
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 - main application instance
* @param clock - zyklus clock
* @param ctx - current AudioContext used by app
* @param bpm - current beats per minute value
* @param time_signature - time signature
* @param time_position - current time position
* @param ppqn - pulses per quarter note
* @param tick - current tick since origin
* @param app - The main application instance
* @param ctx - The current AudioContext used by app
* @param bpm - The current beats per minute value
* @param time_signature - The time signature
* @param time_position - The current time position
* @param ppqn - The pulses per quarter note
* @param tick - The current tick since origin
* @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
* @param _nudge - The current nudge value
*/
private _bpm: number;
private _ppqn: number;
clock: any;
ctx: AudioContext;
logicalTime: number;
private _bpm: number;
time_signature: number[];
time_position: TimePosition;
private _ppqn: number;
tick: number;
running: boolean;
timeviewer: HTMLElement;
deadline: number;
private timerWorker: Worker | null = null;
private timeAtStart: number;
_nudge: number;
constructor(
public app: Editor,
ctx: AudioContext,
) {
timeviewer: HTMLElement;
constructor(public app: Editor, ctx: AudioContext) {
this.timeviewer = document.getElementById("timeviewer")!;
this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.time_signature = [4, 4];
this.logicalTime = 0;
this.tick = 0;
this._bpm = 120;
this._ppqn = 48;
this._nudge = 0;
this.ctx = ctx;
this.running = true;
this.deadline = 0;
this.timeviewer = document.getElementById("timeviewer")!;
this.clock = getAudioContext().createClock(
this.clockCallback,
this.pulse_duration,
);
this.timeAtStart = ctx.currentTime;
this.initializeWorker();
}
// @ts-ignore
clockCallback = (time: number, duration: number, tick: number) => {
private initializeWorker(): void {
/**
* 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.
* Initializes the worker responsible for sending clock pulses. The worker
* is responsible for sending clock pulses at a regular interval. The
* interval is set by the `setWorkerInterval` function. The worker is
* restarted when the BPM is changed. The worker is terminated when the
* clock is stopped.
*
* @param time - precise AudioContext time when the tick should happen
* @param duration - seconds between each tick
* @param tick - count of the current tick
* @returns void
*/
let deadline = time - getAudioContext().currentTime;
this.deadline = deadline;
this.tick = tick;
if (this.app.clock.running) {
const workerScript =
"onmessage = (e) => { setInterval(() => { postMessage(true) }, e.data)}";
const blob = new Blob([workerScript], { type: "text/javascript" });
this.timerWorker = new Worker(URL.createObjectURL(blob));
this.timerWorker.onmessage = () => {
this.run();
};
}
private setWorkerInterval(): void {
/**
* Sets the interval for the worker responsible for sending clock pulses.
* The interval is set by calculating the duration of one pulse. The
* duration of one pulse is calculated by dividing the duration of one beat
* by the number of pulses per quarter note.
*
* @remark The BPM is off constantly by 3~5 BPM.
* @returns void
*/
const beatDurationMs = 60000 / this._bpm;
const pulseDurationMs = beatDurationMs / this._ppqn;
this.timerWorker?.postMessage(pulseDurationMs);
}
private run = () => {
/**
* This function is called by the worker responsible for sending clock
* pulses. It is called at a regular interval. The interval is set by the
* `setWorkerInterval` function. This function is responsible for updating
* the time position and sending MIDI clock messages. It is also responsible
* for evaluating the global buffer. The global buffer is evaluated at the
* beginning of each pulse.
*
* @returns void
*/
if (this.running) {
const adjustedCurrentTime = this.ctx.currentTime + this._nudge / 1000;
const beatNumber = adjustedCurrentTime / (60 / this._bpm);
const currentPulsePosition = Math.ceil(beatNumber * this._ppqn);
if (currentPulsePosition > this.time_position.pulse) {
const futureTimeStamp = this.convertTicksToTimeposition(this.tick);
this.app.clock.incrementTick(this.bpm);
this.time_position.pulse = currentPulsePosition;
if (this.app.settings.send_clock) {
if (futureTimeStamp.pulse % 2 == 0)
// TODO: Why?
this.app.api.MidiConnection.sendMidiClock();
}
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick,
);
this.app.clock.time_position = futureTimeStamp;
this.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}`;
} / ${this.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
@ -98,16 +138,14 @@ export class Clock {
tryEvaluate(this.app, this.app.global_buffer);
}
}
// Implement TransportNode clock callback and update clock info with it
}
};
convertTicksToTimeposition(ticks: number): TimePosition {
/**
* Converts ticks to a time position.
*
* @param ticks - ticks to convert
* @returns TimePosition
* This function converts a number of ticks to a time position.
* @param ticks - number of ticks
* @returns time position
*/
const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn;
@ -119,9 +157,10 @@ export class Clock {
get ticks_before_new_bar(): number {
/**
* Calculates the number of ticks before the next bar.
* This function returns the number of ticks separating the current moment
* from the beginning of the next bar.
*
* @returns number - ticks before the next bar
* @returns number of ticks until next bar
*/
const ticskMissingFromBeat = this.ppqn - this.time_position.pulse;
const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat;
@ -130,9 +169,10 @@ export class Clock {
get next_beat_in_ticks(): number {
/**
* Calculates the number of ticks before the next beat.
* This function returns the number of ticks separating the current moment
* from the beginning of the next beat.
*
* @returns number - ticks before the next beat
* @returns number of ticks until next beat
*/
return this.app.clock.pulses_since_origin + this.time_position.pulse;
}
@ -141,7 +181,7 @@ export class Clock {
/**
* Returns the number of beats per bar.
*
* @returns number - beats per bar
* @returns number of beats per bar
*/
return this.time_signature[0];
}
@ -150,7 +190,7 @@ export class Clock {
/**
* Returns the number of beats since the origin.
*
* @returns number - beats since the origin
* @returns number of beats since origin
*/
return Math.floor(this.tick / this.ppqn);
}
@ -159,7 +199,7 @@ export class Clock {
/**
* Returns the number of pulses since the origin.
*
* @returns number - pulses since the origin
* @returns number of pulses since origin
*/
return this.tick;
}
@ -167,112 +207,210 @@ export class Clock {
get pulse_duration(): number {
/**
* Returns the duration of a pulse in seconds.
* @returns number - duration of a pulse in seconds
*
* @returns 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 {
/**
* Returns the duration of a pulse in seconds at a given bpm.
* Returns the duration of a pulse in seconds at a specific bpm.
*
* @param bpm - bpm to calculate the pulse duration for
* @returns number - duration of a pulse in seconds
* @param bpm - beats per minute
* @returns duration of a pulse in seconds
*/
return 60 / bpm / this.ppqn;
}
get bpm(): number {
/**
* Returns the current bpm.
* @returns number - current bpm
* Returns the current BPM.
*
* @returns current BPM
*/
return this._bpm;
}
get tickDuration(): number {
set nudge(nudge: number) {
/**
* Returns the duration of a tick in seconds.
* @returns number - duration of a tick in seconds
* Sets the nudge.
*
* @param nudge - nudge in seconds
* @returns void
*/
return 1 / this.ppqn;
this._nudge = nudge;
}
get nudge(): number {
/**
* Returns the current nudge.
*
* @returns current nudge
*/
return this._nudge;
}
set bpm(bpm: number) {
/**
* Sets the bpm.
* @param bpm - bpm to set
* Sets the BPM.
*
* @param bpm - beats per minute
* @returns void
*/
if (bpm > 0 && this._bpm !== bpm) {
this._bpm = bpm;
this.clock.setDuration(() => (this.tickDuration * 60) / this.bpm);
// Restart the worker with the new BPM if the clock is running
if (this.running) {
this.restartWorker();
}
}
}
private restartWorker(): void {
/**
* Restarts the worker responsible for sending clock pulses.
*
* @returns void
*/
if (this.timerWorker) {
this.timerWorker.terminate();
}
this.initializeWorker();
this.setWorkerInterval();
}
get ppqn(): number {
/**
* Returns the current ppqn.
* @returns number - current ppqn
* Returns the current PPQN.
*
* @returns current PPQN
*/
return this._ppqn;
}
get realTime(): number {
/**
* Returns the current time of the audio context.
*
* @returns current time of the audio context
* @remark This is the time of the audio context, not the time of the clock.
*/
return this.app.audioContext.currentTime;
}
get deviation(): number {
/**
* Returns the deviation between the logical time and the real time.
*
* @returns deviation between the logical time and the real time
*/
return this.logicalTime - this.realTime;
}
set ppqn(ppqn: number) {
/**
* Sets the ppqn.
* @param ppqn - ppqn to set
* @returns number - current ppqn
* Sets the PPQN.
*
* @param ppqn - pulses per quarter note
* @returns void
*/
if (ppqn > 0 && this._ppqn !== ppqn) {
this._ppqn = ppqn;
}
}
public incrementTick(bpm: number) {
/**
* Increments the tick by one.
*
* @param bpm - beats per minute
* @returns void
*/
this.tick++;
this.logicalTime += this.pulse_duration_at_bpm(bpm);
}
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 nudgedTime = time + nudge;
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
const remainingTime = nextTickTime - nudgedTime;
return remainingTime;
}
public convertPulseToSecond(n: number): number {
/**
* Converts a number of pulses to a number of seconds.
*
* @param n - number of pulses
* @returns number of seconds
*/
return n * this.pulse_duration;
}
public start(): void {
/**
* Start the clock
* This function starts the worker.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.app.audioContext.resume();
if (this.running) {
return;
}
this.running = true;
this.app.audioContext.resume();
this.app.api.MidiConnection.sendStartMessage();
this.clock.start();
if (!this.timerWorker) {
this.initializeWorker();
}
this.setWorkerInterval();
this.timeAtStart = this.ctx.currentTime;
this.logicalTime = this.timeAtStart;
}
public pause(): void {
/**
* Pause the clock.
* Pauses the Transport worker.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.running = false;
this.app.api.MidiConnection.sendStopMessage();
this.clock.pause();
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
}
public stop(): void {
/**
* Stops the clock.
* Stops the Transport worker and resets the tick to 0. The time position
* is also reset to 0. The clock is stopped by terminating the worker
* responsible for sending clock pulses.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.running = false;
this.tick = 0;
this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.app.api.MidiConnection.sendStopMessage();
this.clock.stop();
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
}
}

View File

@ -1,13 +1,13 @@
import { type Editor } from "./main";
// Basics
import { introduction } from "./documentation/basics/welcome";
import { loading_samples } from "./documentation/learning/samples/loading_samples";
import { amplitude } from "./documentation/learning/audio_engine/amplitude";
import { effects } from "./documentation/learning/audio_engine/effects";
import { sampler } from "./documentation/learning/audio_engine/sampler";
import { sample_banks } from "./documentation/learning/samples/sample_banks";
import { audio_basics } from "./documentation/learning/audio_engine/audio_basics";
import { sample_list } from "./documentation/learning/samples/sample_list";
import { loading_samples } from "./documentation/samples/loading_samples";
import { amplitude } from "./documentation/audio_engine/amplitude";
import { reverb } from "./documentation/audio_engine/reverb_delay";
import { sampler } from "./documentation/audio_engine/sampler";
import { sample_banks } from "./documentation/samples/sample_banks";
import { audio_basics } from "./documentation/audio_engine/audio_basics";
import { sample_list } from "./documentation/samples/sample_list";
import { software_interface } from "./documentation/basics/interface";
import { shortcuts } from "./documentation/basics/keyboard";
import { code } from "./documentation/basics/code";
@ -17,44 +17,37 @@ import { oscilloscope } from "./documentation/more/oscilloscope";
import { synchronisation } from "./documentation/more/synchronisation";
import { about } from "./documentation/more/about";
import { bonus } from "./documentation/more/bonus";
import { chaining } from "./documentation/patterns/chaining";
import { interaction } from "./documentation/basics/interaction";
import { time } from "./documentation/learning/time/time";
import { linear_time } from "./documentation/learning/time/linear_time";
import { cyclical_time } from "./documentation/learning/time/cyclical_time";
import { long_forms } from "./documentation/learning/time/long_forms";
import { midi } from "./documentation/learning/midi";
import { osc } from "./documentation/learning/osc";
import { patterns } from "./documentation/patterns/patterns";
import { functions } from "./documentation/patterns/functions";
import { generators } from "./documentation/patterns/generators";
import { variables } from "./documentation/patterns/variables";
import { probabilities } from "./documentation/patterns/probabilities";
import { lfos } from "./documentation/patterns/lfos";
import { ziffers_basics } from "./documentation/patterns/ziffers/ziffers_basics";
import { ziffers_scales } from "./documentation/patterns/ziffers/ziffers_scales";
import { ziffers_rhythm } from "./documentation/patterns/ziffers/ziffers_rhythm";
import { ziffers_algorithmic } from "./documentation/patterns/ziffers/ziffers_algorithmic";
import { ziffers_tonnetz } from "./documentation/patterns/ziffers/ziffers_tonnetz";
import { ziffers_syncing } from "./documentation/patterns/ziffers/ziffers_syncing";
import { synths } from "./documentation/learning/audio_engine/synths";
import { chaining } from "./documentation/chaining";
import { interaction } from "./documentation/interaction";
import { time } from "./documentation/time/time";
import { linear_time } from "./documentation/time/linear_time";
import { cyclical_time } from "./documentation/time/cyclical_time";
import { long_forms } from "./documentation/long_forms";
import { midi } from "./documentation/midi";
import { sound } from "./documentation/engine";
import { patterns } from "./documentation/patterns";
import { functions } from "./documentation/functions";
import { variables } from "./documentation/variables";
import { probabilities } from "./documentation/probabilities";
import { lfos } from "./documentation/lfos";
import { ziffers } from "./documentation/ziffers";
import { synths } from "./documentation/synths";
// Setting up the Markdown converter with syntax highlighting
import showdown from "showdown";
import showdownHighlight from "showdown-highlight";
import "highlight.js/styles/atom-one-dark-reasonable.min.css";
import { createDocumentationStyle } from "./DomElements";
import { filters } from "./documentation/learning/audio_engine/filters";
showdown.setFlavor("github");
export const key_shortcut = (shortcut: string): string => {
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-brightwhite bg-brightblack border border-black rounded-lg">${shortcut}</kbd>`;
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">${shortcut}</kbd>`;
};
export const makeExampleFactory = (application: Editor): Function => {
const make_example = (
description: string,
code: string,
open: boolean = false,
open: boolean = false
) => {
const codeId = `codeExample${application.exampleCounter++}`;
// Store the code snippet in the data structure
@ -63,11 +56,13 @@ export const makeExampleFactory = (application: Editor): Function => {
return `
<details ${open ? "open" : ""}>
<summary >${description}
<button class="ml-4 py-1 align-top text-base px-4 hover:bg-brightgreen bg-green inline-block text-selection_foreground" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
<button class="py-1 text-base px-4 hover:brightyellow bg-yellow text-selection_foreground inline-block" onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base px-4 hover:bg-brightmagenta bg-magenta text-selection_foreground inline-block" onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
<button class="ml-4 py-1 align-top text-base px-4 hover:bg-green-700 bg-emerald-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
<button class="py-1 text-base px-4 hover:bg-neutral-600 bg-neutral-500 inline-block " onclick="app.api._stopDocExample()">&#x23f8;&#xFE0F; Pause</button>
<button class="py-1 text-base px-4 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
</summary>
<pre><code class="hljs language-javascript">${code.trim()}</code></pre>
\`\`\`javascript
${code}
\`\`\`
</details>
`;
};
@ -75,11 +70,7 @@ export const makeExampleFactory = (application: Editor): Function => {
};
export const documentation_factory = (application: Editor) => {
/**
* Creates the documentation for the given application.
* @param application The editor application.
* @returns An object containing various documentation sections.
*/
// Initialize a data structure to store code examples by their unique IDs
application.api.codeExamples = {};
return {
@ -91,26 +82,19 @@ export const documentation_factory = (application: Editor) => {
linear: linear_time(application),
cyclic: cyclical_time(application),
longform: long_forms(application),
sound: sound(application),
synths: synths(application),
filters: filters(application),
chaining: chaining(application),
patterns: patterns(application),
ziffers_basics: ziffers_basics(application),
ziffers_scales: ziffers_scales(application),
ziffers_algorithmic: ziffers_algorithmic(application),
ziffers_rhythm: ziffers_rhythm(application),
ziffers_tonnetz: ziffers_tonnetz(application),
ziffers_syncing: ziffers_syncing(application),
ziffers: ziffers(application),
midi: midi(application),
osc: osc(application),
lfos: lfos(application),
variables: variables(application),
probabilities: probabilities(application),
functions: functions(application),
generators: generators(application),
shortcuts: shortcuts(application),
amplitude: amplitude(application),
effects: effects(application),
reverb_delay: reverb(application),
sampler: sampler(application),
mouse: mouse(application),
oscilloscope: oscilloscope(application),
@ -125,10 +109,6 @@ export const documentation_factory = (application: 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")) {
document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden");
@ -138,24 +118,17 @@ export const showDocumentation = (app: Editor) => {
document.getElementById("documentation")?.classList.remove("hidden");
// Load and convert Markdown content from the documentation file
let style = createDocumentationStyle(app);
function update_and_assign(callback: Function) {
let bindings = Object.keys(style).map((key) => ({
type: "output",
regex: new RegExp(`<${key}([^>]*)>`, "g"),
//@ts-ignore
replace: (match, p1) => `<${key} class="${style[key]}" ${p1}>`,
}));
callback(bindings)
}
update_and_assign((e: Object) => updateDocumentationContent(app, e));
updateDocumentationContent(app, bindings);
}
};
export const hideDocumentation = () => {
/**
* Hides the documentation section and shows the main application.
*/
if (document.getElementById("app")?.classList.contains("hidden")) {
document.getElementById("app")?.classList.remove("hidden");
document.getElementById("documentation")?.classList.add("hidden");
@ -163,37 +136,15 @@ export const hideDocumentation = () => {
};
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.
*/
let loading_message: string = "<h1 class='border-4 py-2 px-2 mx-48 mt-48 text-center text-2xl text-brightwhite'>Loading! <b class='text-red'>Clic to refresh!</b></h1>";
const converter = new showdown.Converter({
emoji: true,
moreStyling: true,
backslashEscapesHTMLTags: true,
extensions: [showdownHighlight({
pre: true,
auto_detection: false
}), ...bindings],
extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
});
console.log(app.currentDocumentationPane);
function _update_and_assign(callback: Function) {
const converted_markdown = converter.makeHtml(
app.docs[app.currentDocumentationPane],
app.docs[app.currentDocumentationPane]
);
callback(converted_markdown)
}
_update_and_assign((e: string)=> {
let display_content = e === undefined ? loading_message : e;
document.getElementById("documentation-content")!.innerHTML = display_content;
})
if (document.getElementById("documentation-content")!.innerHTML.replace(/"/g, "'") == loading_message.replace(/"/g, "'")) {
setTimeout(() => {
updateDocumentationContent(app, bindings);
}, 100);
}
}
document.getElementById("documentation-content")!.innerHTML =
converted_markdown;
};

View File

@ -1,5 +1,6 @@
import { type Editor } from "./main";
export type ElementMap = {
[key: string]:
| HTMLElement
@ -9,7 +10,8 @@ export type ElementMap = {
| HTMLSelectElement
| HTMLCanvasElement
| HTMLFormElement
| HTMLInputElement;
| HTMLInputElement
;
};
export const singleElements = {
@ -18,8 +20,6 @@ export const singleElements = {
load_universe_button: "load-universe-button",
download_universe_button: "download-universes",
upload_universe_button: "upload-universes",
upload_samples_button: "upload-samples",
sample_indicator: "sample-indicator",
destroy_universes_button: "destroy-universes",
documentation_button: "doc-button-1",
eval_button: "eval-button-1",
@ -45,8 +45,6 @@ export const singleElements = {
midi_clock_checkbox: "send-midi-clock",
midi_channels_scripts: "midi-channels-scripts",
midi_clock_ppqn: "midi-clock-ppqn-input",
theme_selector: "theme-selector",
theme_previewer: "theme-previewer",
load_demo_songs: "load-demo-songs",
normal_mode_button: "normal-mode",
vim_mode_button: "vim-mode",
@ -67,37 +65,33 @@ export const buttonGroups = {
//@ts-ignore
export const createDocumentationStyle = (app: Editor) => {
/**
* Creates a documentation style object.
* @param {Editor} app - The editor object.
* @returns {Object} - The documentation style object.
*/
return {
h1: "text-brightwhite 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-brightwhite lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-2 pt-12 pb-3 px-2",
h3: "text-brightwhite lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 border-l-2 border-b-2 lg:mb-4 mb-4 pb-2 px-2 lg:mt-16",
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-4 pt-4 pb-3 px-2",
h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-2 pt-12 pb-3 px-2",
h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 border-l-2 border-b-2 lg:mb-4 mb-4 pb-2 px-2 lg:mt-16",
ul: "text-underline ml-12",
li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal",
p: "lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal",
warning:
"animate-pulse lg:text-2xl font-bold text-brightred lg:mx-6 mx-2 my-4 leading-normal",
a: "lg:text-2xl text-base text-white",
"animate-pulse lg:text-2xl font-bold text-rose-600 lg:mx-6 mx-2 my-4 leading-normal",
a: "lg:text-2xl text-base text-orange-300",
code: `lg:my-4 sm:my-1 text-base lg:text-xl block whitespace-pre overflow-x-hidden`,
icode:
"lg:my-1 my-1 lg:text-xl sm:text-xs text-brightwhite font-mono bg-brightblack",
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-brightwhite font-mono bg-brightblack",
blockquote: "text-brightwhite border-l-4 border-white pl-4 my-4 mx-4",
"lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4",
details:
"lg:mx-20 py-2 px-6 lg:text-2xl text-white border-l-8 box-border bg-selection_foreground",
"lg:mx-20 py-2 px-6 lg:text-2xl text-white border-l-8 box-border bg-neutral-900",
summary: "font-semibold text-xl",
table:
"justify-center lg:my-12 my-2 lg:mx-12 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse",
thead:
"text-xs text-gray-700 uppercase",
"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400",
th: "",
td: "",
tr: "",
box: "border bg-red",
};
box: "border bg-red-500",
};
}

View File

@ -1,6 +1,5 @@
import { Prec } from "@codemirror/state";
import { indentWithTab } from "@codemirror/commands";
import { tags as t } from "@lezer/highlight";
import {
keymap,
lineNumbers,
@ -8,6 +7,8 @@ import {
drawSelection,
highlightActiveLine,
dropCursor,
// rectangularSelection,
// crosshairCursor,
highlightActiveLineGutter,
} from "@codemirror/view";
import { Extension, EditorState } from "@codemirror/state";
@ -17,10 +18,9 @@ import {
syntaxHighlighting,
indentOnInput,
bracketMatching,
HighlightStyle,
} from "@codemirror/language";
import { defaultKeymap, historyKeymap, history } from "@codemirror/commands";
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search"
import {
autocompletion,
closeBrackets,
@ -30,222 +30,19 @@ import { lintKeymap } from "@codemirror/lint";
import { Compartment } from "@codemirror/state";
import { Editor } from "./main";
import { EditorView } from "codemirror";
import { toposTheme } from "./themes/toposTheme";
import { javascript } from "@codemirror/lang-javascript";
import { inlineHoveringTips } from "./documentation/inlineHelp";
import { toposCompletions, soundCompletions } from "./documentation/inlineHelp";
import { javascriptLanguage } from "@codemirror/lang-javascript";
export const getCodeMirrorTheme = (theme: {[key: string]: string}): Extension => {
// @ts-ignore
const black = theme["black"],
red = theme["red"],
green = theme["green"],
yellow = theme["yellow"],
blue = theme["blue"],
magenta = theme["magenta"],
cyan = theme["cyan"],
white = theme["white"],
// @ts-ignore
brightblack = theme["brightblack"],
// @ts-ignore
brightred = theme["brightred"],
brightgreen = theme["brightgreen"],
// @ts-ignore
brightyellow = theme["brightyellow"],
// @ts-ignore
brightblue = theme["brightblue"],
// @ts-ignore
brightmagenta = theme["brightmagenta"],
// @ts-ignore
brightcyan = theme["brightcyan"],
brightwhite = theme["brightwhite"],
background = theme["background"],
selection_foreground = theme["selection_foreground"],
cursor = theme["cursor"],
foreground = theme["foreground"],
selection_background = theme["selection_background"];
const toposTheme = EditorView.theme( {
"&": {
color: background,
// backgroundColor: background,
backgroundColor: "transparent",
fontSize: "24px",
fontFamily: "IBM Plex Mono",
},
".cm-content": {
caretColor: cursor,
fontFamily: "IBM Plex Mono",
},
".cm-cursor, .cm-dropCursor": {
borderLeftColor: cursor,
},
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{
backgroundColor: selection_foreground,
border: `0.5px solid ${selection_background}`,
},
".cm-panels": {
backgroundColor: selection_background,
color: red,
},
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
".cm-search.cm-panel": { backgroundColor: "transparent" },
".cm-searchMatch": {
outline: `1px solid ${magenta}`,
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: red,
},
".cm-activeLine": {
// backgroundColor: highlightBackground
backgroundColor: `${selection_foreground}`,
},
".cm-selectionMatch": {
backgroundColor: yellow,
outline: `1px solid ${red}`,
},
"&.cm-focused .cm-matchingBracket": {
color: yellow,
// outline: `1px solid ${base02}`,
},
"&.cm-focused .cm-nonmatchingBracket": {
color: yellow,
},
".cm-gutters": {
//backgroundColor: base00,
backgroundColor: "transparent",
color: foreground,
},
".cm-activeLineGutter": {
backgroundColor: selection_background,
color: selection_foreground,
},
".cm-foldPlaceholder": {
border: "none",
color: `${blue}`,
},
".cm-tooltip": {
border: "none",
backgroundColor: background,
},
".cm-tooltip .cm-tooltip-arrow:before": {},
".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: background,
borderBottomColor: background,
},
".cm-tooltip-autocomplete": {
"& > ul > li[aria-selected]": {
backgroundColor: background,
color: background,
},
},
},
{ dark: true },
);
let toposHighlightStyle = HighlightStyle.define([
{ tag: t.paren, color: brightwhite },
{ tag: [t.propertyName, t.punctuation, t.variableName], color: brightwhite },
{ tag: t.keyword, color: yellow },
{ tag: [t.name, t.deleted, t.character, t.macroName], color: red, },
{ tag: [t.function(t.variableName)], color: blue },
{ tag: [t.labelName], color: red },
{ tag: [t.color, t.constant(t.name), t.standard(t.name)], color: cyan, },
{ tag: [t.definition(t.name), t.separator], color: magenta },
{ tag: [t.brace], color: white },
{ tag: [t.annotation], color: blue, },
{ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: yellow, },
{ tag: [t.typeName, t.className], color: magenta, },
{ tag: [t.operator, t.operatorKeyword], color: blue, },
{ tag: [t.tagName], color: blue, },
{ tag: [t.squareBracket], color: blue, },
{ tag: [t.angleBracket], color: blue, },
{ tag: [t.attributeName], color: red, },
{ tag: [t.regexp], color: brightgreen, },
{ tag: [t.quote], color: green, },
{ tag: [t.string], color: green },
{
tag: t.link,
color: green,
textDecoration: "underline",
textUnderlinePosition: "under",
},
{
tag: [t.url, t.escape, t.special(t.string)],
color: green,
},
{ tag: [t.meta], color: brightwhite },
{ tag: [t.comment], color: brightwhite, fontStyle: "italic" },
{ tag: t.monospace, color: brightwhite },
{ tag: t.strong, fontWeight: "bold", color: white },
{ tag: t.emphasis, fontStyle: "italic", color: white },
{ tag: t.strikethrough, textDecoration: "line-through" },
{ tag: t.heading, fontWeight: "bold", color: white },
{ tag: t.heading1, fontWeight: "bold", color: white },
{
tag: [t.heading2, t.heading3, t.heading4],
fontWeight: "bold",
color: yellow,
},
{
tag: [t.heading5, t.heading6],
color: red,
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: green},
{
tag: [t.processingInstruction, t.inserted],
color: green,
},
{
tag: [t.contentSeparator],
color: green,
},
{ tag: t.invalid, color: red, borderBottom: `1px dotted ${red}` },
]);
return [ toposTheme, syntaxHighlighting(toposHighlightStyle),
]
}
// const debugTheme = EditorView.theme({
// ".cm-line span": {
// position: "relative",
// },
// ".cm-line span:hover::after": {
// position: "absolute",
// bottom: "100%",
// left: 0,
// background: "black",
// color: "white",
// border: "solid 2px",
// borderRadius: "5px",
// content: "var(--tags)",
// width: `max-content`,
// padding: "1px 4px",
// zIndex: 10,
// pointerEvents: "none",
// },
// });
// const debugHighlightStyle = HighlightStyle.define(
// // @ts-ignore
// Object.entries(t).map(([key, value]) => {
// return { tag: value, "--tags": `"tag.${key}"` };
// })
// );
// const debug = [debugTheme, syntaxHighlighting(debugHighlightStyle)];
import { javascriptLanguage } from "@codemirror/lang-javascript"
export const jsCompletions = javascriptLanguage.data.of({
autocomplete: toposCompletions,
});
autocomplete: toposCompletions
})
export const toposSoundCompletions = javascriptLanguage.data.of({
autocomplete: soundCompletions,
});
autocomplete: soundCompletions
})
export const editorSetup: Extension = (() => [
highlightActiveLineGutter(),
@ -273,7 +70,6 @@ export const editorSetup: Extension = (() => [
export const installEditor = (app: Editor) => {
app.vimModeCompartment = new Compartment();
app.hoveringCompartment = new Compartment();
app.themeCompartment = new Compartment();
app.completionsCompartment = new Compartment();
app.withLineNumbers = new Compartment();
app.chosenLanguage = new Compartment();
@ -299,14 +95,9 @@ export const installEditor = (app: Editor) => {
app.withLineNumbers.of(lines),
app.fontSize.of(fontModif),
app.hoveringCompartment.of(app.settings.tips ? inlineHoveringTips : []),
app.completionsCompartment.of(
app.settings.completions ? [jsCompletions, toposSoundCompletions] : [],
),
app.completionsCompartment.of(app.settings.completions ? [jsCompletions, toposSoundCompletions] : []),
editorSetup,
app.themeCompartment.of(
getCodeMirrorTheme(app.getColorScheme("Tomorrow Night Burns")),
// debug
),
toposTheme,
app.chosenLanguage.of(javascript()),
];
app.dynamicPlugins = new Compartment();
@ -323,7 +114,7 @@ export const installEditor = (app: Editor) => {
return true;
},
},
]),
])
),
keymap.of([indentWithTab]),
],
@ -348,7 +139,7 @@ export const installEditor = (app: Editor) => {
".cm-gutters": {
fontSize: `${app.settings.font_size}px`,
},
}),
})
),
});
};

View File

@ -1,45 +1,42 @@
import type { Editor } from "./main";
import type { File } from "./FileManagement";
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const delay = (ms: number) =>
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Operation took too long")), ms)
);
const codeReplace = (code: string): string => {
return code.replace(/->|::/g, "&&");
let new_code = code.replace(/->/g, "&&").replace(/::/g, "&&");
return new_code;
};
const tryCatchWrapper = async (
const tryCatchWrapper = (
application: Editor,
code: string,
code: string
): Promise<boolean> => {
/**
* 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.
*/
return new Promise((resolve, _) => {
try {
await new Function(`"use strict"; ${codeReplace(code)}`).call(
application.api,
);
return true;
Function(
`"use strict";try{
${codeReplace(code)}; /* break block comments */;
} catch (e) {console.log(e); _reportError(e);};`
).call(application.api);
resolve(true);
} catch (error) {
application.interface.error_line.innerHTML = error as string;
application.api._reportError(error as string);
return false;
application.api._reportError(error as string)
resolve(false);
}
});
};
const cache = new Map<string, Function>();
const MAX_CACHE_SIZE = 40;
const MAX_CACHE_SIZE = 20;
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) {
// Delete the first item if cache size exceeds max size
cache.delete(cache.keys().next().value);
}
cache.set(code, fn);
@ -48,36 +45,28 @@ const addFunctionToCache = (code: string, fn: Function) => {
export const tryEvaluate = async (
application: Editor,
code: File,
timeout = 5000,
timeout = 5000
): Promise<void> => {
/**
* 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.
*/
try {
code.evaluations!++;
const candidateCode = code.candidate;
try {
const cachedFunction = cache.get(candidateCode);
if (cachedFunction) {
cachedFunction.call(application.api);
if (cache.has(candidateCode)) {
// If the code is already in cache, use it
cache.get(candidateCode)!.call(application.api);
} 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([
tryCatchWrapper(application, wrappedCode),
tryCatchWrapper(application, wrappedCode as string),
delay(timeout),
]);
if (isCodeValid) {
code.committed = code.candidate;
const newFunction = new Function(
`"use strict"; ${codeReplace(wrappedCode)}`,
`"use strict";try{${codeReplace(
wrappedCode
)}} catch (e) {console.log(e); _reportError(e);};`
);
addFunctionToCache(candidateCode, newFunction);
} else {
@ -86,23 +75,15 @@ export const tryEvaluate = async (
}
} catch (error) {
application.interface.error_line.innerHTML = error as string;
application.api._reportError(error as string);
application.api._reportError(error as string)
}
};
export const evaluate = async (
application: Editor,
code: File,
timeout = 1000,
timeout = 1000
): 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 {
await Promise.race([
tryCatchWrapper(application, code.committed as string),
@ -117,7 +98,7 @@ export const evaluate = async (
export const evaluateOnce = async (
application: Editor,
code: string,
code: string
): Promise<void> => {
/**
* Evaluates the code once without any caching or error-handling mechanisms besides the tryCatchWrapper.

View File

@ -136,7 +136,7 @@ export class AppSettings {
*/
public vimMode: boolean = false;
public theme: string = "Everblush";
public theme: string = "toposTheme";
public font: string = "IBM Plex Mono";
public font_size: number = 24;
public universes: Universes;
@ -154,7 +154,7 @@ export class AppSettings {
constructor() {
const settingsFromStorage = JSON.parse(
localStorage.getItem("topos") || "{}",
localStorage.getItem("topos") || "{}"
);
if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) {
@ -210,7 +210,7 @@ export class AppSettings {
saveApplicationToLocalStorage(
universes: Universes,
settings: Settings,
settings: Settings
): void {
/**
* Main method to store the application to local storage.
@ -263,9 +263,7 @@ export const initializeSelectedUniverse = (app: Editor): void => {
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 = () => {
@ -273,11 +271,6 @@ export const emptyUrl = () => {
};
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) {
const base64url: string = await new Promise((r) => {
const reader = new FileReader();
@ -328,20 +321,8 @@ export const loadUniverserFromUrl = (app: Editor): void => {
export const loadUniverse = (
app: Editor,
universeName: string,
universe: Universe = template_universe,
universe: Universe = template_universe
): 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();
if (app.universes[selectedUniverse] === undefined) {
// Pushing a freshly cloned template universe to:
@ -353,9 +334,7 @@ export const loadUniverse = (
// Updating references to the currently selected universe
app.settings.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
app.updateEditorView();
// Evaluating the initialisation script for the selected universe
@ -363,11 +342,7 @@ export const loadUniverse = (
};
export const openUniverseModal = (): void => {
/**
* 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 the modal is hidden, unhide it and hide the editor
if (
document.getElementById("modal-buffers")!.classList.contains("invisible")
) {
@ -380,9 +355,6 @@ export const openUniverseModal = (): void => {
};
export const closeUniverseModal = (): void => {
/**
* Closes the universe modal and performs necessary actions.
*/
// @ts-ignore
document.getElementById("buffer-search")!.value = "";
document.getElementById("editor")!.classList.remove("invisible");
@ -390,9 +362,6 @@ export const closeUniverseModal = (): void => {
};
export const openSettingsModal = (): void => {
/**
* Opens the settings modal.
*/
if (
document.getElementById("modal-settings")!.classList.contains("invisible")
) {
@ -404,9 +373,6 @@ export const openSettingsModal = (): void => {
};
export const closeSettingsModal = (): void => {
/**
* Closes the settings modal and performs necessary actions.
*/
document.getElementById("editor")!.classList.remove("invisible");
document.getElementById("modal-settings")!.classList.add("invisible");
};

View File

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

View File

@ -1,62 +0,0 @@
export interface OSCMessage {
address: string;
port: number;
args: object;
timetag: number;
}
// Send/receive messages from websocket
export let outputSocket = new WebSocket("ws://localhost:3000");
export let inputSocket = new WebSocket("ws://localhost:3001");
export let oscMessages: any[] = [];
inputSocket.addEventListener("message", (event) => {
let data = JSON.parse(event.data);
if (oscMessages.length >= 1000) {
oscMessages.shift();
}
oscMessages.push(data);
});
// @ts-ignore
outputSocket.onopen = function (event) {
console.log("Connected to WebSocket Server");
// Send an OSC-like message
outputSocket.send(
JSON.stringify({
address: "/successful_connexion",
port: 3000,
args: {},
}),
);
outputSocket.onerror = function (error) {
console.log("Websocket Error:", error);
};
outputSocket.onmessage = function (event) {
console.log("Received: ", event.data);
};
};
export function sendToServer(message: OSCMessage) {
if (outputSocket.readyState === WebSocket.OPEN) {
outputSocket.send(JSON.stringify(message));
} else {
console.log("WebSocket is not open. Attempting to reconnect...");
if (
outputSocket.readyState === WebSocket.CONNECTING ||
outputSocket.readyState === WebSocket.OPEN
) {
outputSocket.close();
}
// Create a new WebSocket connection
outputSocket = new WebSocket("ws://localhost:3000");
// Send the message once the socket is open
outputSocket.onopen = () => {
outputSocket.send(JSON.stringify(message));
};
}
}

View File

@ -1,155 +0,0 @@
/**
* This code is taken from https://github.com/tidalcycles/strudel/pull/839. The logic is written by
* daslyfe (Jade Rose Rowland). I have tweaked it a bit to fit the needs of this project (TypeScript),
* etc... Many thanks for this piece of code! This code is initially part of the Strudel project:
* https://github.com/tidalcycles/strudel.
*/
// @ts-ignore
import { registerSound, onTriggerSample } from "superdough";
export const isAudioFile = (filename: string) => ['wav', 'mp3'].includes(filename.split('.').slice(-1)[0]);
interface samplesDBConfig {
dbName: string,
table: string,
columns: string[],
version: number
}
export const samplesDBConfig = {
dbName: 'samples',
table: 'usersamples',
columns: ['data_url', 'title'],
version: 1
}
async function bufferToDataUrl(buf: Buffer) {
return new Promise((resolve) => {
var blob = new Blob([buf], { type: 'application/octet-binary' });
var reader = new FileReader();
reader.onload = function (event: Event) {
// @ts-ignore
resolve(event.target.result);
};
reader.readAsDataURL(blob);
});
}
const processFilesForIDB = async (files: FileList) => {
return await Promise.all(
Array.from(files)
.map(async (s: File) => {
const title = s.name;
if (!isAudioFile(title)) {
return;
}
//create obscured url to file system that can be fetched
const sUrl = URL.createObjectURL(s);
//fetch the sound and turn it into a buffer array
const buf = await fetch(sUrl).then((res) => res.arrayBuffer());
//create a url blob containing all of the buffer data
// @ts-ignore
// TODO: conversion to do here, remove ts-ignore
const base64 = await bufferToDataUrl(buf);
return {
title,
blob: base64,
id: s.webkitRelativePath,
};
})
.filter(Boolean),
).catch((error) => {
console.log('Something went wrong while processing uploaded files', error);
});
};
export const registerSamplesFromDB = (config: samplesDBConfig, onComplete = () => {}) => {
openDB(config, (objectStore: IDBObjectStore) => {
let query = objectStore.getAll();
query.onsuccess = (event: Event) => {
// @ts-ignore
const soundFiles = event.target.result;
if (!soundFiles?.length) {
return;
}
const sounds = new Map();
[...soundFiles]
.sort((a, b) => a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }))
.forEach((soundFile) => {
const title = soundFile.title;
if (!isAudioFile(title)) {
return;
}
const splitRelativePath = soundFile.id?.split('/');
const parentDirectory = splitRelativePath[splitRelativePath.length - 2];
const soundPath = soundFile.blob;
const soundPaths = sounds.get(parentDirectory) ?? new Set();
soundPaths.add(soundPath);
sounds.set(parentDirectory, soundPaths);
});
sounds.forEach((soundPaths, key) => {
const value = Array.from(soundPaths);
// @ts-ignore
registerSound(key, (t, hapValue, onended) => onTriggerSample(t, hapValue, onended, value), {
type: 'sample',
samples: value,
baseUrl: undefined,
prebake: false,
tag: "user",
});
});
onComplete();
};
});
};
export const openDB = (config: samplesDBConfig, onOpened: Function) => {
const { dbName, version, table, columns } = config
if (!('indexedDB' in window)) {
console.log('This browser doesn\'t support IndexedDB')
return
}
const dbOpen = indexedDB.open(dbName, version);
dbOpen.onupgradeneeded = (_event) => {
const db = dbOpen.result;
const objectStore = db.createObjectStore(table, { keyPath: 'id', autoIncrement: false });
columns.forEach((c: any) => {
objectStore.createIndex(c, c, { unique: false });
});
};
dbOpen.onerror = function (err: Event) {
console.log('Error opening DB: ', (err.target as IDBOpenDBRequest).error);
}
dbOpen.onsuccess = function (_event: Event) {
const db = dbOpen.result;
db.onversionchange = function() {
db.close();
alert("Database is outdated, please reload the page.")
};
const writeTransaction = db.transaction([table], 'readwrite'),
objectStore = writeTransaction.objectStore(table);
// Writing in the database here!
onOpened(objectStore)
}
}
export const uploadSamplesToDB = async (config: samplesDBConfig, files: FileList) => {
await processFilesForIDB(files).then((files) => {
const onOpened = (objectStore: IDBObjectStore, _db: IDBDatabase) => {
// @ts-ignore
files.forEach((file: File) => {
if (file == null) {
return;
}
objectStore.put(file);
});
};
openDB(config, onOpened);
});
};

View File

@ -1,7 +1,6 @@
import { EditorView } from "codemirror";
import { vim } from "@replit/codemirror-vim";
import { type Editor } from "./main";
import colors from "./colors.json";
import {
documentation_factory,
hideDocumentation,
@ -25,7 +24,6 @@ import { lineNumbers } from "@codemirror/view";
import { jsCompletions } from "./EditorSetup";
import { createDocumentationStyle } from "./DomElements";
import { saveState } from "./WindowBehavior";
import { registerSamplesFromDB, samplesDBConfig, uploadSamplesToDB } from "./IO/SampleLoading";
export const installInterfaceLogic = (app: Editor) => {
// Initialize style
@ -59,11 +57,9 @@ export const installInterfaceLogic = (app: Editor) => {
for (let i = 0; i < tabs.length; i++) {
tabs[i].addEventListener("click", (event) => {
// Updating the CSS accordingly
tabs[i].classList.add("bg-foreground");
tabs[i].classList.add("text-selection_foreground");
tabs[i].classList.add("bg-orange-300");
for (let j = 0; j < tabs.length; j++) {
if (j != i) tabs[j].classList.remove("bg-foreground");
if (j != i) tabs[j].classList.remove("text-selection_foreground");
if (j != i) tabs[j].classList.remove("bg-orange-300");
}
app.currentFile().candidate = app.view.state.doc.toString();
@ -122,59 +118,35 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.universe_viewer.addEventListener("keydown", (event: any) => {
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 !== app.selected_universe) {
Object.defineProperty(
app.universes,
content,
Object.defineProperty(app.universes, content,
// @ts-ignore
Object.getOwnPropertyDescriptor(
app.universes,
app.selected_universe,
),
);
Object.getOwnPropertyDescriptor(app.universes, app.selected_universe));
delete app.universes[app.selected_universe];
}
app.selected_universe = content;
loadUniverse(app, app.selected_universe);
(app.interface.universe_viewer as HTMLInputElement).placeholder =
content;
(app.interface.universe_viewer as HTMLInputElement).value = "";
(app.interface.universe_viewer as HTMLInputElement).placeholder = content;
(app.interface.universe_viewer as HTMLInputElement).value = '';
}
}
});
app.interface.audio_nudge_range.addEventListener("input", () => {
// TODO: rebuild this
// app.clock.nudge = parseInt(
// (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.dough_nudge = parseInt(
(app.interface.dough_nudge_range as HTMLInputElement).value,
(app.interface.dough_nudge_range as HTMLInputElement).value
);
});
app.interface.upload_samples_button.addEventListener("input", async (event) => {
let fileInput = event.target as HTMLInputElement;
if (!fileInput.files?.length) {
return;
}
app.interface.sample_indicator.innerText = "Loading...";
app.interface.sample_indicator.classList.add("animate-pulse");
await uploadSamplesToDB(samplesDBConfig, fileInput.files).then(() => {
registerSamplesFromDB(samplesDBConfig, () => {
app.interface.sample_indicator.innerText = "Import samples";
app.interface.sample_indicator.classList.remove("animate-pulse");
});
});
});
app.interface.upload_universe_button.addEventListener("click", () => {
const fileInput = document.createElement("input");
fileInput.type = "file";
@ -255,16 +227,16 @@ export const installInterfaceLogic = (app: Editor) => {
});
app.interface.local_button.addEventListener("click", () =>
app.changeModeFromInterface("local"),
app.changeModeFromInterface("local")
);
app.interface.global_button.addEventListener("click", () =>
app.changeModeFromInterface("global"),
app.changeModeFromInterface("global")
);
app.interface.init_button.addEventListener("click", () =>
app.changeModeFromInterface("init"),
app.changeModeFromInterface("init")
);
app.interface.note_button.addEventListener("click", () =>
app.changeModeFromInterface("notes"),
app.changeModeFromInterface("notes")
);
app.interface.font_family_selector.addEventListener("change", () => {
@ -283,7 +255,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
}),
})
),
});
});
@ -303,58 +275,26 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
}),
})
),
});
});
app.interface.theme_selector.addEventListener("change", () => {
app.settings.theme = (app.interface.theme_selector as HTMLSelectElement).value;
app.readTheme(app.settings.theme);
// @ts-ignore
let selected_theme = colors[app.settings.theme as string];
let theme_preview = "";
for (const [key, _] of Object.entries(selected_theme)) {
theme_preview += `<p class="inline text-${key} bg-${key}">█</div>`;
}
app.interface.theme_previewer.innerHTML = theme_preview;
});
app.interface.settings_button.addEventListener("click", () => {
// Populate the font selector
const fontFamilySelect = document.getElementById(
"font-family",
"font-family"
) as HTMLSelectElement | null;
if (fontFamilySelect) {
fontFamilySelect.value = app.settings.font;
}
app.interface.theme_selector.innerHTML = "";
let all_themes = Object.keys(colors);
all_themes.sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
app.interface.theme_selector.innerHTML = all_themes.map((color) => {
return `<option value="${color}">${color}</option>`
}).join("");
// Set the selected theme in the selector to app.settings.theme
// @ts-ignore
app.interface.theme_selector.value = app.settings.theme;
// @ts-ignore
let selected_theme = colors[app.settings.theme as string];
let theme_preview = "<div class='ml-6'>";
for (const [key, _] of Object.entries(selected_theme)) {
theme_preview += `<p class="inline text-${key} bg-${key}">█</p>`;
}
theme_preview += "</div>";
app.interface.theme_previewer.innerHTML = theme_preview;
// Populate the font family selector
const doughNudgeRange = app.interface.dough_nudge_range as HTMLInputElement;
doughNudgeRange.value = app.dough_nudge.toString();
// @ts-ignore
const doughNumber = document.getElementById(
"doughnumber",
"doughnumber"
) as HTMLInputElement;
doughNumber.value = app.dough_nudge.toString();
if (app.settings.font_size === null) {
@ -410,7 +350,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
}),
})
),
});
});
@ -468,7 +408,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.tips = checked;
app.view.dispatch({
effects: app.hoveringCompartment.reconfigure(
checked ? inlineHoveringTips : [],
checked ? inlineHoveringTips : []
),
});
});
@ -481,7 +421,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.completions = checked;
app.view.dispatch({
effects: app.completionsCompartment.reconfigure(
checked ? jsCompletions : [],
checked ? jsCompletions : []
),
});
});
@ -504,7 +444,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.midi_clock_ppqn.addEventListener("change", () => {
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;
});
@ -542,8 +482,7 @@ export const installInterfaceLogic = (app: Editor) => {
"sampler",
"amplitude",
"audio_basics",
"filters",
"effects",
"reverb_delay",
"interface",
"interaction",
"code",
@ -551,19 +490,13 @@ export const installInterfaceLogic = (app: Editor) => {
"linear",
"cyclic",
"longform",
// "sound",
"synths",
"chaining",
"patterns",
"ziffers_basics",
"ziffers_scales",
"ziffers_rhythm",
"ziffers_algorithmic",
"ziffers_tonnetz",
"ziffers_syncing",
"ziffers",
"midi",
"osc",
"functions",
"generators",
"lfos",
"probabilities",
"variables",
@ -577,12 +510,8 @@ export const installInterfaceLogic = (app: Editor) => {
"loading_samples",
].forEach((e) => {
let name = `docs_` + e;
// Check if the element exists
let element = document.getElementById(name);
if (element) {
element.addEventListener("click", async () => {
if (name !== "docs_sample_list") {
document.getElementById(name)!.addEventListener("click", async () => {
if (name !== "docs_samples") {
app.currentDocumentationPane = e;
updateDocumentationContent(app, bindings);
} else {
@ -594,8 +523,5 @@ export const installInterfaceLogic = (app: Editor) => {
});
}
});
} else {
console.log("Could not find element " + name);
}
});
};

View File

@ -26,31 +26,6 @@ export const registerOnKeyDown = (app: Editor) => {
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") {
event.preventDefault();
app.setButtonHighlighting("stop", true);
@ -109,15 +84,6 @@ export const registerOnKeyDown = (app: Editor) => {
app.flashBackground("#404040", 200);
}
// Force eval with clearing cache
if (event.ctrlKey && event.shiftKey && (event.key === "Backspace" || event.key === "Delete")) {
event.preventDefault();
app.api.clearPatternCache();
app.currentFile().candidate = app.view.state.doc.toString();
tryEvaluate(app, app.currentFile());
app.flashBackground("#404040", 200);
}
// app is the modal to switch between universes
if (event.ctrlKey && event.key === "b") {
event.preventDefault();

View File

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

View File

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

View File

@ -1,20 +1,14 @@
import { type Editor } from "../main";
import {
freqToMidi,
chord as parseChord,
noteNameToMidi,
resolvePitchBend,
safeScale,
safeScale
} from "zifferjs";
import { SkipEvent } from "./SkipEvent";
import { SoundParams } from "./SoundEvent";
import { centsToSemitones, edoToSemitones, ratiosToSemitones } from "zifferjs/src/scale";
import { safeMod } from "zifferjs/src/utils";
export type EventOperation<T> = (instance: T, ...args: any[]) => void;
export interface AbstractEvent {
[key: string]: any;
[key: string]: any
}
export class AbstractEvent {
@ -211,79 +205,25 @@ export class AbstractEvent {
* @param func - The function to be applied to the Event
* @returns The transformed Event
*/
return this.modify(func).update();
return this.modify(func);
};
mod = (value: number): AbstractEvent => {
this.values.originalPitch = safeMod(this.values.originalPitch, value);
return this.update();
}
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.
*/
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)) {
this.values.dur = value.map((v) =>
this.app.clock.convertPulseToSecond(v * 4 * this.app.clock.ppqn),
);
this.values["noteLength"] = value;
this.values.dur = value.map((v) => this.app.clock.convertPulseToSecond(v*4*this.app.clock.ppqn));
} else {
this.values.dur = this.app.clock.convertPulseToSecond(
value * 4 * this.app.clock.ppqn,
);
}
if(this.current) {
value = Array.isArray(value) ? value[this.index%value.length] : value;
this.current.duration = value;
this.values["noteLength"] = value;
this.values.dur = this.app.clock.convertPulseToSecond(value*4*this.app.clock.ppqn);
}
return this;
};
protected processSound = (
sound: string | string[] | SoundParams | SoundParams[],
): SoundParams => {
if (Array.isArray(sound) && typeof sound[0] === "string") {
const s: string[] = [];
const n: number[] = [];
sound.forEach((str) => {
const parts = (str as string).split(":");
s.push(parts[0]);
if (parts[1]) {
n.push(parseInt(parts[1]));
}
});
return {
s,
n: n.length > 0 ? n : undefined,
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
};
} else if (typeof sound === "object") {
const validatedObj: SoundParams = {
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
...(sound as Partial<SoundParams>),
};
return validatedObj;
} else {
if (sound.includes(":")) {
const vals = sound.split(":");
const s = vals[0];
const n = parseInt(vals[1]);
return {
s,
n,
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
};
} else {
return { s: sound, dur: 0.5 };
}
}
};
}
export abstract class AudibleEvent extends AbstractEvent {
@ -298,13 +238,12 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event
*/
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["originalPitch"] = value;
this.defaultPitchKeyScale();
return this.update();
};
if(this.values.key && this.values.parsedScale) this.update();
return this;
}
pc = this.pitch;
@ -315,17 +254,10 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event
*/
if(kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
}
this.values["paramOctave"] = value;
if (
this.values.key &&
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
) {
return this.update();
}
this.values["octave"] = value;
if(this.values.key && (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update();
return this;
};
@ -336,136 +268,32 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event
*/
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;
if (
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
) {
return this.update();
}
if((this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update();
return this;
};
defaultPitchKeyScale() {
if (!this.values.key) this.values.key = 60;
if (!(this.values.pitch || this.values.pitch === 0)) this.values.pitch = 0;
if (!this.values.parsedScale) this.values.parsedScale = safeScale("major");
}
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.
* @param value - The scale value
* @returns The Event
*/
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") {
this.values.parsedScale = safeScale(value) as number[];
} else if(Array.isArray(value)) {
this.values.parsedScale = value.map((v) => safeScale(v));
}
this.defaultPitchKeyScale();
return this.update();
};
semitones(values: number|number[], ...rest: number[]) {
const scaleValues = typeof values === "number" ? [values, ...rest] : values;
this.values.parsedScale = safeScale(scaleValues);
this.defaultPitchKeyScale();
return this.update();
}
steps = this.semitones;
cents(values: number|number[], ...rest: number[]) {
const scaleValues = typeof values === "number" ? [values, ...rest] : values;
this.values.parsedScale = safeScale(centsToSemitones(scaleValues));
this.defaultPitchKeyScale();
return this.update();
}
ratios(values: number|number[], ...rest: number[]) {
const scaleValues = typeof values === "number" ? [values, ...rest] : values;
this.values.parsedScale = safeScale(ratiosToSemitones(scaleValues));
this.defaultPitchKeyScale();
return this.update();
}
edo(value: number, intervals: string|number[] = new Array(value).fill(1)) {
this.values.parsedScale = edoToSemitones(value, intervals);
this.defaultPitchKeyScale();
return this.update();
}
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(howMany === 0) return this;
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;
}
};
public log = (key: string|string[], ...args: string[]) => {
/*
* Log values from values using log()
*
* @param key - The key(s) to log
* @returns this and logs the values
*/
if (typeof key === "string") {
if(args && args.length > 0) {
this.app.api.log([key, ...args].map((k) => this.values[k]));
} else {
this.app.api.log(this.values[key]);
}
} else {
this.app.api.log([...key, ...args].map((k) => this.values[k]));
if(this.values.key && (this.values.pitch || this.values.pitch === 0)) {
this.update();
}
return this;
}
};
freq = (value: number | number[], ...kwargs: number[]): this => {
/*
@ -474,7 +302,7 @@ export abstract class AudibleEvent extends AbstractEvent {
* @returns The Event
*/
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;
if(Array.isArray(value)) {
@ -502,13 +330,7 @@ export abstract class AudibleEvent extends AbstractEvent {
return this;
};
update = (): this => {
update = (): void => {
// Overwrite in subclasses
return this;
};
cue = (functionName: string|Function): this => {
this.app.api.cue(functionName);
return this;
}
}

View File

@ -1,12 +1,8 @@
import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection";
import { resolvePitchClass } from "zifferjs";
import {
filterObject,
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
} from "../Utils/Generic";
import { noteFromPc, chord as parseChord } from "zifferjs";
import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic";
export type MidiParams = {
note: number;
@ -15,20 +11,27 @@ export type MidiParams = {
port?: number;
sustain?: number;
velocity?: number;
};
}
export class MidiEvent extends AudibleEvent {
midiConnection: MidiConnection;
constructor(
input: MidiParams,
public app: Editor,
) {
constructor(input: MidiParams, public app: Editor) {
super(app);
this.values = input;
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 => {
this.values["sustain"] = value;
return this;
@ -37,7 +40,7 @@ export class MidiEvent extends AudibleEvent {
velocity = (value: number | number[]): this => {
this.values["velocity"] = value;
return this;
};
}
channel = (value: number | number[]): this => {
this.values["channel"] = value;
@ -48,9 +51,7 @@ export class MidiEvent extends AudibleEvent {
if(typeof value === "string"){
this.values["port"] = this.midiConnection.getMidiOutputIndex(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;
};
@ -66,7 +67,8 @@ export class MidiEvent extends AudibleEvent {
return funcResult;
} else {
func(this.values);
return this.update();
this.update();
return this;
}
};
@ -82,36 +84,27 @@ export class MidiEvent extends AudibleEvent {
return this;
};
update = (): this => {
const filteredValues = filterObject(this.values, [
"key",
"pitch",
"originalPitch",
"parsedScale",
"addedOctave"
]);
update = (): void => {
// Get key, pitch, parsedScale and octave from this.values object
const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]);
const events = objectWithArraysToArrayOfObjects(filteredValues, [
"parsedScale",
]);
const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]);
events.forEach((soundEvent) => {
const resolvedPitchClass = resolvePitchClass(
(soundEvent.key || "C4"),
(soundEvent.originalPitch || soundEvent.pitch || 0),
(soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
(soundEvent.addedOctave || 0)
events.forEach((event) => {
const [note, bend] = noteFromPc(
event.key as number || "C4",
event.pitch as number || 0,
event.parsedScale as number[] || event.scale || "MAJOR",
event.octave as number || 0
);
soundEvent.note = resolvedPitchClass.note;
soundEvent.pitch = resolvedPitchClass.pitch;
soundEvent.octave = resolvedPitchClass.octave;
event.note = note;
if(bend) event.bend = bend;
});
const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams;
this.values.note = newArrays.note;
if(newArrays.bend) this.values.bend = newArrays.bend;
return this;
};
out = (): void => {
@ -121,7 +114,9 @@ export class MidiEvent extends AudibleEvent {
const note = params.note ? params.note : 60;
const sustain = params.sustain
? params.sustain * event.app.clock.pulse_duration * event.app.api.ppqn()
? params.sustain *
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;
@ -136,16 +131,15 @@ export class MidiEvent extends AudibleEvent {
velocity,
sustain,
port,
bend,
bend
);
}
const events = objectWithArraysToArrayOfObjects(this.values, [
"parsedScale",
]) as MidiParams[];
const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]) as MidiParams[];
events.forEach((p: MidiParams) => {
play(this,p);
});
};
}

View File

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

View File

@ -1,18 +1,21 @@
import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents";
import { sendToServer, type OSCMessage } from "../IO/OSC";
import {
filterObject,
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
} from "../Utils/Generic";
import { midiToFreq, resolvePitchClass } from "zifferjs";
import {
chord as parseChord,
midiToFreq,
noteFromPc,
noteNameToMidi,
} from "zifferjs";
import {
superdough,
// @ts-ignore
} from "superdough";
// import { Sound } from "zifferjs/src/types";
export type SoundParams = {
dur: number | number[];
@ -22,20 +25,17 @@ export type SoundParams = {
note?: number | number[];
freq?: number | number[];
pitch?: number | number[];
originalPitch?: number | number[];
key?: string;
scale?: string;
parsedScale?: number[];
octave?: number | number[];
addedOctave?: number | number[];
pitchOctave?: number | number[];
};
export class SoundEvent extends AudibleEvent {
nudge: number;
sound: any;
private static methodMap = {
private methodMap = {
volume: ["volume", "vol"],
zrand: ["zrand", "zr"],
curve: ["curve"],
@ -46,8 +46,6 @@ export class SoundEvent extends AudibleEvent {
pitchJumpTime: ["pitchJumpTime", "pjt"],
lfo: ["lfo"],
znoise: ["znoise"],
address: ["address", "add"],
port: ["port"],
noise: ["noise"],
zmod: ["zmod"],
zcrush: ["zcrush"],
@ -69,23 +67,17 @@ export class SoundEvent extends AudibleEvent {
phaserDepth: ["phaserDepth", "phasdepth"],
phaserSweep: ["phaserSweep", "phassweep"],
phaserCenter: ["phaserCenter", "phascenter"],
fmadsr: function (
self: SoundEvent,
a: number,
d: number,
s: number,
r: number,
) {
self.updateValue("fmattack", a);
self.updateValue("fmdecay", d);
self.updateValue("fmsustain", s);
self.updateValue("fmrelease", r);
return self;
fmadsr: (a: number, d: number, s: number, r: number) => {
this.updateValue("fmattack", a);
this.updateValue("fmdecay", d);
this.updateValue("fmsustain", s);
this.updateValue("fmrelease", r);
return this;
},
fmad: function (self: SoundEvent, a: number, d: number) {
self.updateValue("fmattack", a);
self.updateValue("fmdecay", d);
return self;
fmad: (a: number, d: number) => {
this.updateValue("fmattack", a);
this.updateValue("fmdecay", d);
return this;
},
ftype: ["ftype"],
fanchor: ["fanchor"],
@ -93,185 +85,147 @@ export class SoundEvent extends AudibleEvent {
decay: ["decay", "dec"],
sustain: ["sustain", "sus"],
release: ["release", "rel"],
adsr: function (
self: SoundEvent,
a: number,
d: number,
s: number,
r: number,
) {
self.updateValue("attack", a);
self.updateValue("decay", d);
self.updateValue("sustain", s);
self.updateValue("release", r);
return self;
adsr: (a: number, d: number, s: number, r: number) => {
this.updateValue("attack", a);
this.updateValue("decay", d);
this.updateValue("sustain", s);
this.updateValue("release", r);
return this;
},
ad: function (self: SoundEvent, a: number, d: number) {
self.updateValue("attack", a);
self.updateValue("decay", d);
self.updateValue("sustain", 0.0);
self.updateValue("release", 0.0);
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;
ad: (a: number, d: number) => {
this.updateValue("attack", a);
this.updateValue("decay", d);
this.updateValue("sustain", 0.0);
this.updateValue("release", 0.0);
return this;
},
lpenv: ["lpenv", "lpe"],
lpattack: ["lpattack", "lpa"],
lpdecay: ["lpdecay", "lpd"],
lpsustain: ["lpsustain", "lps"],
lprelease: ["lprelease", "lpr"],
cutoff: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("cutoff", value);
cutoff: (value: number, resonance?: number) => {
this.updateValue("cutoff", value);
if (resonance) {
self.updateValue("resonance", resonance);
this.updateValue("resonance", resonance);
}
return self;
return this;
},
lpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("cutoff", value);
lpf: (value: number, resonance?: number) => {
this.updateValue("cutoff", value);
if (resonance) {
self.updateValue("resonance", resonance);
this.updateValue("resonance", resonance);
}
return self;
return this;
},
resonance: function (self: SoundEvent, value: number) {
resonance: (value: number) => {
if (value >= 0 && value <= 1) {
self.updateValue("resonance", 50 * value);
this.updateValue("resonance", 50 * value);
}
return self;
return this;
},
lpadsr: function (
self: SoundEvent,
depth: number,
a: number,
d: number,
s: number,
r: number,
) {
self.updateValue("lpenv", depth);
self.updateValue("lpattack", a);
self.updateValue("lpdecay", d);
self.updateValue("lpsustain", s);
self.updateValue("lprelease", r);
return self;
lpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
this.updateValue("lpenv", depth);
this.updateValue("lpattack", a);
this.updateValue("lpdecay", d);
this.updateValue("lpsustain", s);
this.updateValue("lprelease", r);
return this;
},
lpad: function (self: SoundEvent, depth: number, a: number, d: number) {
self.updateValue("lpenv", depth);
self.updateValue("lpattack", a);
self.updateValue("lpdecay", d);
self.updateValue("lpsustain", 0);
self.updateValue("lprelease", 0);
return self;
lpad: (depth: number, a: number, d: number) => {
this.updateValue("lpenv", depth);
this.updateValue("lpattack", a);
this.updateValue("lpdecay", d);
this.updateValue("lpsustain", 0);
this.updateValue("lprelease", 0);
return this;
},
hpenv: ["hpenv", "hpe"],
hpattack: ["hpattack", "hpa"],
hpdecay: ["hpdecay", "hpd"],
hpsustain: ["hpsustain", "hpsus"],
hprelease: ["hprelease", "hpr"],
hcutoff: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("hcutoff", value);
hcutoff: (value: number, resonance?: number) => {
this.updateValue("hcutoff", value);
if (resonance) {
self.updateValue("hresonance", resonance);
this.updateValue("hresonance", resonance);
}
return self;
return this;
},
hpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("hcutoff", value);
hpf: (value: number, resonance?: number) => {
this.updateValue("hcutoff", value);
if (resonance) {
self.updateValue("hresonance", resonance);
this.updateValue("hresonance", resonance);
}
return self;
return this;
},
hpq: function (self: SoundEvent, value: number) {
self.updateValue("hresonance", value);
return self;
hpq: (value: number) => {
this.updateValue("hresonance", value);
return this;
},
hpadsr: function (
self: SoundEvent,
depth: number,
a: number,
d: number,
s: number,
r: number,
) {
self.updateValue("hpenv", depth);
self.updateValue("hpattack", a);
self.updateValue("hpdecay", d);
self.updateValue("hpsustain", s);
self.updateValue("hprelease", r);
return self;
hpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
this.updateValue("hpenv", depth);
this.updateValue("hpattack", a);
this.updateValue("hpdecay", d);
this.updateValue("hpsustain", s);
this.updateValue("hprelease", r);
return this;
},
hpad: function (self: SoundEvent, depth: number, a: number, d: number) {
self.updateValue("hpenv", depth);
self.updateValue("hpattack", a);
self.updateValue("hpdecay", d);
self.updateValue("hpsustain", 0);
self.updateValue("hprelease", 0);
return self;
hpad: (depth: number, a: number, d: number) => {
this.updateValue("hpenv", depth);
this.updateValue("hpattack", a);
this.updateValue("hpdecay", d);
this.updateValue("hpsustain", 0);
this.updateValue("hprelease", 0);
return this;
},
bpenv: ["bpenv", "bpe"],
bpattack: ["bpattack", "bpa"],
bpdecay: ["bpdecay", "bpd"],
bpsustain: ["bpsustain", "bps"],
bprelease: ["bprelease", "bpr"],
bandf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("bandf", value);
bandf: (value: number, resonance?: number) => {
this.updateValue("bandf", value);
if (resonance) {
self.updateValue("bandq", resonance);
this.updateValue("bandq", resonance);
}
return self;
return this;
},
bpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("bandf", value);
bpf: (value: number, resonance?: number) => {
this.updateValue("bandf", value);
if (resonance) {
self.updateValue("bandq", resonance);
this.updateValue("bandq", resonance);
}
return self;
return this;
},
bandq: ["bandq", "bpq"],
bpadsr: function (
self: SoundEvent,
depth: number,
a: number,
d: number,
s: number,
r: number,
) {
self.updateValue("bpenv", depth);
self.updateValue("bpattack", a);
self.updateValue("bpdecay", d);
self.updateValue("bpsustain", s);
self.updateValue("bprelease", r);
return self;
bpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
this.updateValue("bpenv", depth);
this.updateValue("bpattack", a);
this.updateValue("bpdecay", d);
this.updateValue("bpsustain", s);
this.updateValue("bprelease", r);
return this;
},
bpad: function (self: SoundEvent, depth: number, a: number, d: number) {
self.updateValue("bpenv", depth);
self.updateValue("bpattack", a);
self.updateValue("bpdecay", d);
self.updateValue("bpsustain", 0);
self.updateValue("bprelease", 0);
return self;
bpad: (depth: number, a: number, d: number) => {
this.updateValue("bpenv", depth);
this.updateValue("bpattack", a);
this.updateValue("bpdecay", d);
this.updateValue("bpsustain", 0);
this.updateValue("bprelease", 0);
return this;
},
vib: ["vib"],
vibmod: ["vibmod"],
fm: function (self: SoundEvent, value: number | string) {
fm: (value: number | string) => {
if (typeof value === "number") {
self.values["fmi"] = value;
this.values["fmi"] = value;
} else {
let values = value.split(":");
self.values["fmi"] = parseFloat(values[0]);
if (values.length > 1) self.values["fmh"] = parseFloat(values[1]);
this.values["fmi"] = parseFloat(values[0]);
if (values.length > 1) this.values["fmh"] = parseFloat(values[1]);
}
return self;
return this;
},
loop: ["loop"],
loopBegin: ["loopBegin", "loopb"],
@ -279,13 +233,13 @@ export class SoundEvent extends AudibleEvent {
begin: ["begin"],
end: ["end"],
gain: ["gain"],
dbgain: function (self: SoundEvent, value: number) {
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return self;
dbgain: (value: number) => {
this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return this;
},
db: function (self: SoundEvent, value: number) {
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return self;
db: (value: number) => {
this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return this;
},
velocity: ["velocity", "vel"],
pan: ["pan"],
@ -306,70 +260,107 @@ export class SoundEvent extends AudibleEvent {
roomlp: ["roomlp", "rlp"],
roomdim: ["roomdim", "rdim"],
sound: ["s", "sound"],
size: function (self: SoundEvent, value: number) {
self.updateValue("roomsize", value);
return self;
size: (value: number) => {
this.updateValue("roomsize", value);
return this;
},
sz: function (self: SoundEvent, value: number) {
self.updateValue("roomsize", value);
return self;
sz: (value: number) => {
this.updateValue("roomsize", value);
return this;
},
comp: ["comp","compressor", "cmp"],
ratio: function (self: SoundEvent, value: number) {
self.updateValue("compressorRatio", value);
return self;
comp: ["compressor", "cmp"],
ratio: (value: number) => {
this.updateValue("compressorRatio", value);
return this;
},
knee: function (self: SoundEvent, value: number) {
self.updateValue("compressorKnee", value);
return self;
knee: (value: number) => {
this.updateValue("compressorKnee", value);
return this;
},
compAttack: function (self: SoundEvent, value: number) {
self.updateValue("compressorAttack", value);
return self;
compAttack: (value: number) => {
this.updateValue("compressorAttack", value);
return this;
},
compRelease: function (self: SoundEvent, value: number) {
self.updateValue("compressorRelease", value);
return self;
compRelease: (value: number) => {
this.updateValue("compressorRelease", value);
return this;
},
stretch: function (self: SoundEvent, beat: number) {
self.updateValue("unit", "c");
self.updateValue("speed", 1 / beat);
self.updateValue("cut", beat);
return self;
stretch: (beat: number) => {
this.updateValue("unit", "c");
this.updateValue("speed", 1 / beat);
this.updateValue("cut", beat);
return this;
},
};
constructor(
sound: string | string[] | SoundParams,
public app: Editor,
) {
constructor(sound: string | string[] | SoundParams, public app: Editor) {
super(app);
this.nudge = app.dough_nudge / 100;
for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) {
if (typeof keys === "object" && Symbol.iterator in Object(keys)) {
for (const [methodName, keys] of Object.entries(this.methodMap)) {
if (Symbol.iterator in Object(keys)) {
for (const key of keys as string[]) {
// Using arrow function to maintain 'this' context
// @ts-ignore
this[key] = (value: number) => this.updateValue(keys[0], value);
}
} else {
// @ts-ignore
this[methodName] = (...args) => keys(this, ...args);
this[methodName] = keys;
}
}
this.values = this.processSound(sound);
}
// 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);
private processSound = (
sound: string | string[] | SoundParams | SoundParams[]
): SoundParams => {
if (Array.isArray(sound) && typeof sound[0] === "string") {
const s: string[] = [];
const n: number[] = [];
sound.forEach((str) => {
const parts = (str as string).split(":");
s.push(parts[0]);
if (parts[1]) {
n.push(parseInt(parts[1]));
}
});
return {
s,
n: n.length > 0 ? n : undefined,
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true,
};
} else if (typeof sound === "object") {
const validatedObj: SoundParams = {
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true,
...(sound as Partial<SoundParams>),
};
return validatedObj;
} else {
if (sound.includes(":")) {
const vals = sound.split(":");
const s = vals[0];
const n = parseInt(vals[1]);
return {
s,
n,
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
analyze: true,
};
} else {
return { s: sound, dur: 0.5, analyze: true };
}
}
};
private updateValue<T>(
key: string,
value: T | T[] | SoundParams[] | null
): this {
if (value == null) return this;
this.values[key] = value;
return this;
}
// ================================================================================
@ -381,87 +372,85 @@ export class SoundEvent extends AudibleEvent {
if (funcResult instanceof Object) return funcResult;
else {
func(this.values);
return this.update();
this.update();
return this;
}
};
update = (): this => {
update = (): void => {
const filteredValues = filterObject(this.values, [
"key",
"pitch",
"originalPitch",
"parsedScale",
"addedOctave",
"octave",
"paramOctave"
]);
const events = objectWithArraysToArrayOfObjects(filteredValues, [
"parsedScale",
]);
events.forEach((soundEvent) => {
const resolvedPitchClass = resolvePitchClass(
(soundEvent.key || "C4"),
(soundEvent.originalPitch || soundEvent.pitch || 0),
(soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
(soundEvent.paramOctave || 0)+(soundEvent.addedOctave || 0)
events.forEach((event) => {
const [note, _] = noteFromPc(
(event.key as number) || "C4",
(event.pitch as number) || 0,
(event.parsedScale as number[]) || event.scale || "MAJOR",
(event.octave as number) || 0
);
soundEvent.note = resolvedPitchClass.note;
soundEvent.freq = midiToFreq(resolvedPitchClass.note);
soundEvent.pitch = resolvedPitchClass.pitch;
soundEvent.octave = resolvedPitchClass.octave;
event.note = note;
event.freq = midiToFreq(note);
});
const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams;
this.values.note = newArrays.note;
this.values.freq = newArrays.freq;
this.values.pitch = newArrays.pitch;
this.values.octave = newArrays.octave;
this.values.pitchOctave = newArrays.pitchOctave;
return this;
};
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 => {
if (orbit) this.values["orbit"] = orbit;
const events = objectWithArraysToArrayOfObjects(this.values, [
"parsedScale",
]);
for (const event of events) {
// Filter non superdough parameters
// TODO: Should filter relevant fields for superdough
// const filteredEvent = filterObject(event, ["analyze","note","dur","freq","s"]);
const filteredEvent = event;
// No need for note if there is freq
if (filteredEvent.freq) {
delete filteredEvent.note;
}
superdough(filteredEvent, this.app.clock.deadline, filteredEvent.dur);
}
};
osc = (orbit?: number | number[]): void => {
if (orbit) this.values["orbit"] = orbit;
const events = objectWithArraysToArrayOfObjects(this.values, [
"parsedScale",
]);
for (const event of events) {
const filteredEvent = event;
let oscAddress = "address" in event ? event.address : "/topos";
oscAddress = oscAddress?.startsWith("/") ? oscAddress : "/" + oscAddress;
let oscPort = "port" in event ? event.port : 57120;
if (filteredEvent.freq) {
delete filteredEvent.note;
}
sendToServer({
address: oscAddress,
port: oscPort,
args: event,
timetag: Math.round(Date.now() + this.app.clock.deadline),
} as OSCMessage);
if (filteredEvent.freq) { delete filteredEvent.note; }
// const correction = Math.max(this.nudge - this.app.clock.deviation, 0);
superdough(filteredEvent, this.nudge, filteredEvent.dur);
}
};
}

View File

@ -5,7 +5,7 @@ import { SkipEvent } from "./SkipEvent";
import { SoundEvent, SoundParams } from "./SoundEvent";
import { MidiEvent, MidiParams } from "./MidiEvent";
import { RestEvent } from "./RestEvent";
import { arrayOfObjectsToObjectWithArrays, isGenerator } from "../Utils/Generic";
import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic";
import { TonnetzSpaces } from "zifferjs/src/tonnetz";
export type InputOptions = { [key: string]: string | number };
@ -17,7 +17,6 @@ export class Player extends AbstractEvent {
startCallTime: number = 0;
lastCallTime: number = 0;
waitTime = 0;
cueName: string|undefined = undefined;
played: boolean = false;
current!: Pitch | Chord | ZRest;
retro: boolean = false;
@ -30,7 +29,7 @@ export class Player extends AbstractEvent {
input: string|number|Generator<number>,
options: InputOptions,
public app: Editor,
zid: string = "",
zid: string = ""
) {
super(app);
this.options = options;
@ -40,19 +39,13 @@ export class Player extends AbstractEvent {
} else if (typeof input === "number") {
this.input = input;
this.ziffers = Ziffers.fromNumber(input,options);
} else if (isGenerator(input)) {
} else {
this.ziffers = Ziffers.fromGenerator(input,options);
this.input = this.ziffers.input;
} else {
throw new Error("Invalid input");
}
this.zid = zid;
}
isValid() {
return this.ziffers.values.length > 0;
}
reset() {
this.initCallTime = 0;
this.startCallTime = 0;
@ -132,13 +125,11 @@ export class Player extends AbstractEvent {
const patternIsStarting =
this.notStarted() &&
this.waitTime >= 0 &&
this.origin() >= this.waitTime &&
(this.pulse() === 0 || this.origin() >= this.nextBeatInTicks());
(this.pulse() === 0 || this.origin() >= this.nextBeatInTicks()) &&
this.origin() >= this.waitTime;
const timeToPlayNext =
this.current &&
this.waitTime >= 0 &&
this.pulseToSecond(this.origin()) >=
this.pulseToSecond(this.lastCallTime) +
this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) &&
@ -164,67 +155,43 @@ export class Player extends AbstractEvent {
return areWeThereYet;
};
checkCue() {
if(this.ziffers.atLast()) {
if(this.cueName && this.app.api.cueTimes[this.cueName]) {
delete this.app.api.cueTimes[this.cueName];
this.cueName = undefined;
this.waitTime = -1;
}
}
}
sound(name?: string | string[] | SoundParams | SoundParams[]) {
sound(name?: string) {
if (this.areWeThereYet()) {
this.checkCue();
const event = this.next() as Pitch | Chord | ZRest;
const noteLengthInSeconds = this.app.clock.convertPulseToSecond(
event.duration * 4 * this.app.clock.ppqn,
event.duration * 4 * this.app.clock.ppqn
);
if (event instanceof Pitch) {
let obj = event.getExisting(
const obj = event.getExisting(
"freq",
"note",
"pitch",
"originalPitch",
"key",
"scale",
"octave",
"pitchOctave",
"addedOctave",
"parsedScale",
"parsedScale"
) as SoundParams;
if (event.sound) name = event.sound as string;
if(name) obj = {...obj, ...this.processSound(name)};
else obj.s = "sine";
if (event.soundIndex) obj.n = event.soundIndex as number;
obj.dur = noteLengthInSeconds;
return new SoundEvent(obj, this.app);
return new SoundEvent(obj, this.app).sound(name || "sine");
} else if (event instanceof Chord) {
const pitches = event.pitches.map((p) => {
return p.getExisting(
"freq",
"note",
"pitch",
"originalPitch",
"key",
"scale",
"octave",
"pitchOctave",
"addedOctave",
"parsedScale",
"parsedScale"
);
}) as SoundParams[];
let add = { dur: noteLengthInSeconds} as SoundParams;
if(name) add = {...add, ...this.processSound(name)};
else add.s = "sine";
const add = { dur: noteLengthInSeconds } as SoundParams;
if (name) add.s = name;
let sound = arrayOfObjectsToObjectWithArrays(
pitches,
add,
add
) as SoundParams;
return new SoundEvent(sound, this.app);
} else if (event instanceof ZRest) {
@ -237,19 +204,15 @@ export class Player extends AbstractEvent {
midi(value: number | undefined = undefined) {
if (this.areWeThereYet()) {
this.checkCue();
const event = this.next() as Pitch | Chord | ZRest;
const obj = event.getExisting(
"note",
"pitch",
"originalPitch",
"bend",
"key",
"scale",
"octave",
"pitchOctave",
"addedOctave",
"parsedScale",
"parsedScale"
) as MidiParams;
if (event instanceof Pitch) {
if (event.soundIndex) obj.channel = event.soundIndex as number;
@ -267,34 +230,11 @@ export class Player extends AbstractEvent {
}
}
scale(name: string|number[]) {
scale(name: string) {
if (this.atTheBeginning()) this.ziffers.scale(name);
return this;
}
semitones(values: number|number[], ...rest: number[]) {
values = typeof values === "number" ? [values, ...rest] : values;
if (this.atTheBeginning()) this.ziffers.semitones(values);
return this;
}
cents(values: number|number[], ...rest: number[]) {
values = typeof values === "number" ? [values, ...rest] : values;
if (this.atTheBeginning()) this.ziffers.cents(values);
return this;
}
ratios(values: number|number[], ...rest: number[]) {
values = typeof values === "number" ? [values, ...rest] : values;
if (this.atTheBeginning()) this.ziffers.ratios(values);
return this;
}
edo(value: number, scale: string|number[] = new Array(value).fill(1)) {
if (this.atTheBeginning()) this.ziffers.edo(value, scale);
return this;
}
key(name: string) {
if (this.atTheBeginning()) this.ziffers.key(name);
return this;
@ -305,12 +245,6 @@ export class Player extends AbstractEvent {
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]) {
if (this.atTheBeginning()) this.ziffers.triadTonnetz(transform, tonnetz);
return this;
@ -321,18 +255,18 @@ export class Player extends AbstractEvent {
return this;
}
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 4) {
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz, repeats);
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz);
return this;
}
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz, repeats);
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz);
return this;
}
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz, repeats);
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz);
return this;
}
@ -348,12 +282,6 @@ export class Player extends AbstractEvent {
lead = () => this.voiceleading();
arpeggio(indexes: string|number[], ...rest: number[]) {
if(typeof indexes === "number") indexes = [indexes, ...rest];
if (this.atTheBeginning()) this.ziffers.arpeggio(indexes);
return this;
}
invert = (n: number) => {
if (this.atTheBeginning()) {
this.ziffers.invert(n);
@ -366,65 +294,25 @@ export class Player extends AbstractEvent {
return this;
}
listen(value: string) {
if(typeof value === "string") {
const cueTime = this.app.api.cueTimes[value];
this.cueName = value;
if(cueTime && this.app.clock.pulses_since_origin <= cueTime) {
this.waitTime = cueTime;
} else {
this.waitTime = -1;
}
return this;
}
}
wait(value: number | string | Function) {
if(typeof value === "string") {
const cueTime = this.app.api.cueTimes[value];
this.cueName = value;
if(cueTime && this.app.clock.pulses_since_origin <= cueTime) {
this.waitTime = cueTime;
} else if(this.atTheBeginning()){
this.waitTime = -1;
}
return this;
}
wait(value: number | Function) {
if (this.atTheBeginning()) {
if (typeof value === "function") {
const refPat = this.app.api.patternCache.get(value.name) as Player;
if (refPat) this.waitTime = refPat.nextEndTime();
return this;
} else if(typeof value === "number") {
}
this.waitTime =
this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn);
return this;
}
}
return this;
}
sync(value: string | Function, manualSync: boolean = true) {
if(typeof value === "string") {
if(manualSync) {
const cueTime = this.app.api.cueTimes[value];
if(cueTime) {
this.waitTime = cueTime;
} else {
this.waitTime = -1;
}
}
return this;
}
sync(value: string | Function) {
if (this.atTheBeginning() && this.notStarted()) {
const origin = this.app.clock.pulses_since_origin;
const syncId = typeof value === "function" ? value.name : value;
if (origin > 0) {
const syncPattern = this.app.api.patternCache.get(value.name) as Player;
const syncPattern = this.app.api.patternCache.get(syncId) as Player;
if (syncPattern) {
const syncPatternDuration = syncPattern.ziffers.duration;
const syncPatternStart = syncPattern.startCallTime;
@ -436,13 +324,6 @@ export class Player extends AbstractEvent {
return this;
}
log(key: string, ...args: string[]) {
this.app.api.log(this.ziffers.evaluated.map((p) => {
return Object.values(p.getExisting(...[key,...args]));
}).join(" "));
return this;
}
out = (): void => {
// TODO?
};

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const amplitude = (application: Editor): string => {
// @ts-ignore
@ -20,7 +20,7 @@ ${makeExample(
"Velocity manipulated by a counter",
`
beat(.5)::snd('cp').vel($(1)%10 / 10).out()`,
true,
true
)}
## Amplitude Enveloppe
@ -33,25 +33,24 @@ beat(.5)::snd('cp').vel($(1)%10 / 10).out()`,
| <ic>decay</ic> | dec | Decay value (time to decay to sustain level) |
| <ic>sustain</ic> | sus | Sustain value (gain when sound is held) |
| <ic>release</ic> | rel | Release value (time for the sound to die off) |
| <ic>adsr</ic> | | Shortcut that combines all the parameters together |
Note that the **sustain** value is not a duration but an amplitude value (how loud). The other values are the time for each stage to take place. Here is a fairly complete example using the <ic>sawtooth</ic> basic waveform.
${makeExample(
"Simple synthesizer",
`
register("smooth", x => x.cutoff(r(100,500))
let smooth = (sound) => {
return sound.cutoff(r(100,500))
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
.gain(r(0.25, 0.4)).adsr(0, r(.2,.4), r(0,0.5), 0)
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125))
beat(.25)::sound('sawtooth')
.note([50,57,55,60].beat(1))
.smooth().out();
beat(.25)::sound('sawtooth')
.note([50,57,55,60].add(12).beat(1.5))
.smooth().out();
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125)
}
beat(.25)::smooth(sound('sawtooth')
.note([50,57,55,60].beat(1))).out();
beat(.25)::smooth(sound('sawtooth')
.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:
@ -60,19 +59,20 @@ Sometimes, using a full ADSR envelope is a bit overkill. There are other simpler
${makeExample(
"Replacing .adsr by .ad",
`
register("smooth", x => x.cutoff(r(100,500))
let smooth = (sound) => {
return sound.cutoff(r(100,500))
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
.gain(r(0.25, 0.4)).ad(0, 0.25)
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125))
beat(.25)::sound('sawtooth')
.note([50,57,55,60].beat(1))
.smooth().out();
beat(.25)::sound('sawtooth')
.note([50,57,55,60].add(12).beat(1.5))
.smooth().out();
.gain(r(0.25, 0.4)).ad(0, .25)
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125)
}
beat(.25)::smooth(sound('sawtooth')
.note([50,57,55,60].beat(1))).out();
beat(.25)::smooth(sound('sawtooth')
.note([50,57,55,60].add(12).beat(1.5))).out();
`,
true,
true
)};
`;
};
`}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const audio_basics = (application: Editor): string => {
// @ts-ignore
@ -22,7 +22,7 @@ ${makeExample(
beat(1) && sound('bd').out()
beat(0.5) && sound('hh').out()
`,
true,
true
)}
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(0.5) && sound('hh').delay(0.25).delaytime(0.125).out();
`,
true,
true
)}
Now, it translates as follows:
@ -53,13 +53,11 @@ 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> :
${makeExample(
"Using the .out method",
${makeExample("Using the .out method",
`
// Playing a clap on the third bus (0-indexed)
beat(1)::sound('cp').out(2)
`,
true,
`, true
)}
Try to remove <ic>.out</ic>. You will see that no sound is playing at all!
@ -69,7 +67,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_.
${makeExample(
"Complex sonic object",
'Complex sonic object',
`
beat(1) :: sound('pad').n(1)
.begin(rand(0, 0.4))
@ -77,7 +75,7 @@ beat(1) :: sound('pad').n(1)
.size(0.9).room(0.9)
.velocity(0.25)
.pan(usine()).release(2).out()`,
true,
true
)}
## Picking a specific sound
@ -106,7 +104,7 @@ ${makeExample(
`
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:
@ -116,7 +114,7 @@ You can also use the <ic>:</ic> to pick a sample number directly from the <ic>so
`
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:
@ -126,7 +124,7 @@ ${makeExample(
`
// Move your mouse to change the sample being used!
beat(.25) && sound('ST09').n(Math.floor(mouseX())).out()`,
true,
true
)}
@ -149,18 +147,14 @@ 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:
${makeExample(
"Dry and wet",
`
${makeExample("Dry and wet", `
// This sound is dry
beat(1)::sound('hh').out()
// This sound is wet (reverb)
beat(2)::sound('cp').orbit(2).room(0.5).size(8).out()
`,
true,
)}
`, true)}
## The art of chaining
@ -174,9 +168,10 @@ beat(0.25) && sound('fhh')
.room(0.9).size(0.9).gain(1)
.cutoff(usine(1/2) * 5000)
.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.
`;
};
`}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const distortion = (application: Editor): string => {
// @ts-ignore
@ -23,8 +23,9 @@ ${makeExample(
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()
`,
true,
true
)};
`;
};
`}

View File

@ -1,7 +1,7 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const effects = (application: Editor): string => {
export const reverb = (application: Editor): string => {
// @ts-ignore
const makeExample = makeExampleFactory(application);
return `
@ -28,7 +28,7 @@ ${makeExample(
`
beat(2)::snd('cp').room(0.5).size(4).out()
`,
true,
true
)};
## Delay
@ -42,13 +42,10 @@ 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>) |
${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(4)::snd('snare').out()
beat(1)::snd('kick').out()`,
true,
)}
beat(1)::snd('kick').out()`, true)}
## Phaser
@ -59,17 +56,13 @@ beat(1)::snd('kick').out()`,
| <ic>phaserSweep</ic> | <ic>phassweep</ic> | Phaser frequency sweep (in hertz) |
| <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')
.phaser(0.75).phaserSweep(3000)
.phaserCenter(1500).phaserDepth(1)
.note([0, 1, 2, 3, 4, 5, 6].scale('pentatonic', 50).beat(0.25))
.room(0.5).size(4).out()
`,
true,
)}
`, true)}
## Distorsion, saturation, destruction
@ -88,36 +81,6 @@ ${makeExample(
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()
`,
true,
true
)};
## Vibrato
You can also add some amount of vibrato to the sound using the <ic>vib</ic> and <ic>vibmod</ic> methods. These can turn any oscillator into something more lively and/or into a sound effect when used with a high amount of modulation.
${makeExample(
"Different vibrato settings",
`
tempo(140);
beat(1) :: sound('triangle')
.freq(400).release(0.2)
.vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2))
.out()`,
true,
)}
## Compression
This effect is leveraging the basic WebAudio compressor. More information can be found about it on the [DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) page. This can be come quite complex :)
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>comp</ic> | cmp | Compressor threshold value (dB) over which compressor operates |
| <ic>ratio</ic> | rt | Compressor ratio: input amount in dB needed for 1dB change in the output |
| <ic>knee</ic> | kn | dB value defining the range over which the signal transitions to compressed section |
| <ic>compAttack</ic> | cmpa | In seconds, time to decrease the gain by 10db |
| <ic>compRelease</ic> | cmpr | In seconds, time to increase the gain by 10db |
`;
};
`}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const sampler = (application: Editor): string => {
// @ts-ignore
@ -38,7 +38,7 @@ beat(.5)::snd('pad').begin(0.2)
.room(0.8).size(0.5)
.clip(1).out()
`,
true,
true
)};
@ -46,53 +46,37 @@ beat(.5)::snd('pad').begin(0.2)
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')
.speed([1,2,3,4].palindrome().beat(0.5)).out()
`,
true,
)}
`, true)}
It also works by using negative values. It reverses the playback:
${makeExample(
"Playing samples backwards",
`
${makeExample("Playing samples backwards", `
beat(0.5)::sound('notes')
.speed(-[1,2,3,4].palindrome().beat(0.5)).out()
`,
true,
)}
`, true)}
Of course you can play melodies using samples:
${makeExample(
"Playing melodies using samples",
`
${makeExample("Playing melodies using samples", `
beat(0.5)::sound('notes')
.room(0.5).size(4)
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.5)).out()
`,
true,
)}
`, true)}
## Panning
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')
.room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out()
`,
true,
)}
`, true)}
## Looping over a sample
@ -100,17 +84,13 @@ 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.
${makeExample(
"Granulation using loop",
`
${makeExample("Granulation using loop", `
beat(0.25)::sound('fikea').loop(1)
.lpf(ir(2000, 5000))
.loopBegin(0).loopEnd(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()
`,
true,
)}
`, true)}
## Stretching a sample
@ -131,45 +111,34 @@ 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:
${makeExample(
"Cutting a sample using end",
`
${makeExample("Cutting a sample using end", `
beat(0.25)::sound('notes')
.end(usine(1/2)/0.5)
.room(0.5).size(4).pan(r(0, 1))
.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:
${makeExample(
"Cutting a sample using end",
`
${makeExample("Cutting a sample using end", `
beat(0.125)::sound('notes')
.cut(1)
.room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.125)
+ [-12,12].beat()).out()
`,
true,
)}
`, true)}
## Adding vibrato to samples
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()
`,
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 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!
- **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()
`,
true,
true
)}
${makeExample(
@ -52,7 +52,7 @@ ${makeExample(
beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
// (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(2) :: beat(0.5) :: snd('clap').out()
`,
false,
false
)}
# About crashes and bugs
@ -76,7 +76,7 @@ ${makeExample(
// This is crashing. See? No harm!
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.
- **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(
"F10",
"F10"
)} key.
- **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(
"F1",
"F1"
)} to ${key_shortcut("F9")}).
- **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(
"F11",
"F11"
)} key.
- **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!
@ -43,7 +43,7 @@ ${makeExample(
beat(1) :: script(1) // Calling local script n°1
flip(4) :: beat(.5) :: script(2) // Calling script n°2
`,
true,
true
)}
${makeExample(
@ -54,7 +54,7 @@ beat(1) :: script([1, 3, 5].pick())
flip(4) :: beat([.5, .25].beat(16)) :: script(
[5, 6, 7, 8].beat())
`,
false,
false
)}
### 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(
"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.
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 |
|----------|-------|------------------------------------------------------------|
|**Start/Pause** transport|${key_shortcut(
"Ctrl + P",
"Ctrl + P"
)}|Start or pause audio playback|
|**Stop** the transport |${key_shortcut(
"Ctrl + S",
"Ctrl + S"
)}|Stop and rewind audio playback|
### 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|
|Global Script|${key_shortcut("Ctrl + G")} or ${key_shortcut(
"F10",
"F10"
)}|Switch to global script |
|Local scripts|${key_shortcut("Ctrl + L")} or ${key_shortcut(
"F11",
"F11"
)}|Switch to local scripts |
|Init script|${key_shortcut("Ctrl + L")}|Switch to init script|
|Note File|${key_shortcut("Ctrl + N")}|Switch to note file|
|Local Script|${key_shortcut("F1")} to ${key_shortcut(
"F9",
"F9"
)}|Switch to a specific local script|
|Documentation|${key_shortcut("Ctrl + D")}|Open the documentation|
@ -44,28 +44,24 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|----------|-------|------------------------------------------------------------|
|Evaluate|${key_shortcut("Ctrl + Enter")}| Evaluate the current script |
|Local Eval|${key_shortcut("Ctrl + F1")} to ${key_shortcut(
"Ctrl + F9",
"Ctrl + F9"
)}|Local File Evaluation|
|Force Eval|${key_shortcut(
"Ctrl + Shift + Enter",
"Ctrl + Shift + Enter"
)}|Force evaluation of the current script|
|Clear cache & Eval|${key_shortcut(
"Ctrl + Shift + Backspace (Delete)",
)}|Clears cache and forces evaluation to update cached scripts|
### Special
| Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------|
|Vim Mode|${key_shortcut("Ctrl + V")}| Switch between Vim and Normal Mode|
|Maximize|${key_shortcut("Ctrl + M")}| Show/Hide the interface|
# Keyboard Fill
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(
"Alt",
"Alt"
)} when playing this example:
${makeExample(
@ -73,7 +69,7 @@ ${makeExample(
`
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))
.room(0.35).size(4).out()
`,
true,
true
)}
<br>
@ -46,7 +46,7 @@ beat(.25) :: sound('sine')
.delay(0.5).delayt(1/6).delayfb(0.2)
.note(noteX())
.room(0.35).size(4).out()`,
true,
true
)}
## Mouse and Arrays
@ -64,7 +64,7 @@ log([1,2,3,4].mouseX())
log([4,5,6,7].mouseY())
`,
true,
true
)}

View File

@ -8,13 +8,13 @@ export const introduction = (application: Editor): string => {
# Welcome
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).
${makeExample(
"Welcome! Eval to get started",
examples[Math.floor(Math.random() * examples.length)],
true,
true
)}
# 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()
beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare')
.n(0).speed([1, 0.5]).o(4).out()`,
false,
false
)}
${makeExample(
@ -47,7 +47,7 @@ beat(.25)::snd('sine')
.delay(0.5).delayt(0.25).delayfb(0.7) // Delay
.room(0.5).size(8) // Reverb
.out()`,
false,
false
)}
${makeExample(
@ -58,7 +58,7 @@ beat(.5) :: sound('sid').n($(2))
beat(.25) :: sound('sid').note(
[34, 36, 41].beat(.25) + [[0,-24].pick(),12].beat())
.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).
@ -66,12 +66,7 @@ Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Tele
## Demo Songs
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 :).
## 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

@ -1,5 +1,5 @@
import { makeExampleFactory } from "../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../Documentation";
import { type Editor } from "../main";
export const chaining = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
@ -12,8 +12,7 @@ ${makeExample(
"Method chaining",
`
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!
@ -30,8 +29,7 @@ register('juxrev', n=>n.pan([0, 1]).speed([1, -1]))
// Using our new abstraction
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:
@ -50,29 +48,9 @@ register('sub', (n,x=4,y=80)=>n.ad(0, .25)
// Using it with an arpeggio
rhythm(.25, [6, 8].beat(), 12)::sound('sine')
.note([0, 2, 4, 5].scale('minor', 50).beat(0.5))
.sub(8).out()`,
true,
.sub(8).out()`, true
)}
## Logging values from the chain
You can use the <ic>log()</ic> function to print values from the current event. This can be useful to debug your code. Useful parameters to log could be **note**, **pitch**, **dur**, **octave** etc...
${makeExample(
"Logging values from the chain",
`
beat(1) :: sound("sine").pitch(rI(1,6)).log("note").out()
`,
true,
)}
${makeExample(
"Logging values from ziffers pattern",
`
z1("0 3 2 5").scale("rocritonic").sound("sine").log("pitch","note","key").out()
`,
true,
)}
## Conditional chaining
@ -87,7 +65,7 @@ beat(.5) && sound('fhh')
.odds(1/4, s => s.speed(irand(1,4)))
.rarely(s => s.room(0.5).size(8).speed(0.5))
.out()`,
true,
true
)}
${makeExample(
"Chance to play a random note",
@ -99,7 +77,7 @@ beat(.5) && sound('pluck').note(60)
.note(62)
.room(0.5).size(3)
.out()`,
false,
false
)}
There is a growing collection of probability and chance methods you can use:
@ -133,7 +111,7 @@ ${makeExample(
.often(n => n.note+=4)
.sometimes(s => s.velocity(irand(50,100)))
.out()`,
true,
true
)};
## Ziffers
@ -156,7 +134,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))
.room(0.5).size(0.5).out()
`,
true,
true
)};
`;
};

View File

@ -0,0 +1,91 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const sound = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
## Sample Controls
There are some basic controls over the playback of each sample. This allows you to get into more serious sampling if you take the time to really work with your audio materials.
| Method | Alias | Description |
|---------|-------|--------------------------------------------------------|
| <ic>n</ic> | | Select a sample in the current folder (from <ic>0</ic> to infinity) |
| <ic>begin</ic> | | Beginning of the sample playback (between <ic>0</ic> and <ic>1</ic>) |
| <ic>end</ic> | | End of the sample (between <ic>0</ic> and <ic>1</ic>) |
| <ic>loopBegin</ic> | | Beginning of the loop section (between <ic>0</ic> and <ic>1</ic>) |
| <ic>loopEnd</ic> | | End of the loop section (between <ic>0</ic> and <ic>1</ic>) |
| <ic>loop</ic> | | Whether to loop or not the audio sample |
| <ic>stretch</ic> | | Stretches the audio playback rate of a sample over <ic>n</ic> beats |
| <ic>speed</ic> | | Playback speed (<ic>2</ic> = twice as fast) |
| <ic>cut</ic> | | Set with <ic>0</ic> or <ic>1</ic>. Will cut the sample as soon as another sample is played on the same bus |
| <ic>clip</ic> | | Multiply the duration of the sample with the given number |
| <ic>pan</ic> | | Stereo position of the audio playback (<ic>0</ic> = left, <ic>1</ic> = right)|
${makeExample(
"Complex sampling duties",
`
// Using some of the modifiers described above :)
beat(.5)::snd('pad').begin(0.2)
.speed([1, 0.9, 0.8].beat(4))
.n(2).pan(usine(.5))
.end(rand(0.3,0.8))
.room(0.8).size(0.5)
.clip(1).out()
`,
true
)};
${makeExample(
"Playing an amen break",
`
// Note that stretch has the same value as beat
beat(4) :: sound('amen1').n(11).stretch(4).out()
beat(1) :: sound('kick').shape(0.35).out()`,
true,
)};
## Filters
There are three basic filters: a _lowpass_, _highpass_ and _bandpass_ filters with rather soft slope. Each of them can take up to two arguments. You can also use only the _cutoff_ frequency and the resonance will stay to its default nominal value. You will learn more about the usage of filters in the synths page!
| Method | Alias | Description |
|------------|-------|-----------------------------------------|
| <ic>cutoff</ic> | lpf | Cutoff frequency of the lowpass filter |
| <ic>resonance</ic> | lpq | Resonance of the lowpass filter |
| <ic>hcutoff</ic> | hpf | Cutoff frequency of the highpass filter |
| <ic>hresonance</ic> | hpq | Resonance of the highpass filter |
| <ic>bandf</ic> | bpf | Cutoff frequency of the bandpass filter |
| <ic>bandq</ic> | bpq | Resonance of the bandpass filter |
| <ic>vowel</ic> | | Formant filter with (vocal quality) |
${makeExample(
"Filter sweep using a low frequency oscillator",
`
beat(.5) && snd('sawtooth')
.cutoff([2000,500].pick() + usine(.5) * 4000)
.resonance(0.9).freq([100,150].pick())
.out()
`,
true
)};
## Compression
This effect is leveraging the basic WebAudio compressor. More information can be found about it on the [DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) page. This can be come quite complex :)
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>comp</ic> | cmp | Compressor threshold value (dB) over which compressor operates |
| <ic>ratio</ic> | rt | Compressor ratio: input amount in dB needed for 1dB change in the output |
| <ic>knee</ic> | kn | dB value defining the range over which the signal transitions to compressed section |
| <ic>compAttack</ic> | cmpa | In seconds, time to decrease the gain by 10db |
| <ic>compRelease</ic> | cmpr | In seconds, time to increase the gain by 10db |
`;
};

View File

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

View File

@ -898,6 +898,12 @@ const completionDatabase: CompletionDatabase = {
description: "Detects if the Alt key is pressed",
example: "fill() ? 1 : 0.5",
},
comp: {
name: "comp",
category: "synthesis",
description: "Compressor threshold (dB)",
example: "sound('sine').comp(-4).out()",
},
ratio: {
name: "ratio",
category: "synthesis",
@ -963,7 +969,7 @@ export const inlineHoveringTips = hoverTooltip(
return { dom };
},
};
},
}
);
export const toposCompletions = (context: CompletionContext) => {

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
// @ts-ignore
export const interaction = (application: Editor): string => {
@ -24,7 +24,7 @@ ${makeExample(
`
beat(1) && active_notes() && sound('sine').chord(active_notes()).out()
`,
true,
true
)}
${makeExample(
@ -34,7 +34,7 @@ ${makeExample(
active_notes().beat(0.5)+[12,24].beat(0.25)
).cutoff(300 + usine(1/4) * 2000).out()
`,
false,
false
)}
@ -46,7 +46,7 @@ ${makeExample(
beat(0.25) && sticky_notes() && sound('arp')
.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.
@ -58,7 +58,7 @@ ${makeExample(
.vib([1, 3, 5].beat(1))
.vibmod([1,3,2,4].beat(2)).out()
`,
false,
false
)}
* <ic>buffer()</ic>: return true if there are notes in the buffer.
@ -69,7 +69,7 @@ ${makeExample(
`
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()
`,
true,
true
)}
${makeExample(
@ -105,7 +105,7 @@ beat(last_cc(74)/127*.5) :: sound('sine')
.sustain(last_cc(74)/127*.25)
.out()
`,
false,
false
)}
@ -128,7 +128,7 @@ Topos can output scales to external keyboards lighted keys using the following f
${makeExample(
"Show scale on external keyboard",
`show_scale("F","aeolian",0,4)`,
true,
true
)}
${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)}

View File

@ -1,145 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const filters = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Filters
Filters can be applied to both synthesizers and samples. They are used to shape the sound by removing or emphasizing certain frequencies. They are also used to create movement in the sound by modulating the cutoff frequency of the filter over time.
- **lowpass filter**: filters the high frequencies, keeping the low frequencies.
- **highpass filter**: filtering the low frequencies, keeping the high frequencies.
- **bandpass filter**: filters the low and high frequencies around a frequency band, keeping what's in the middle.
${makeExample(
"Filtering the high frequencies of an oscillator",
`beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`,
true,
)}
These filters all come with their own set of parameters. Note that we are describing the parameters of the three different filter types here. Choose the right parameters depending on the filter type you are using:
### Lowpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>cutoff</ic> | <ic>lpf</ic> | cutoff frequency of the lowpass filter |
| <ic>resonance</ic> | <ic>lpq</ic> | resonance of the lowpass filter (0-1) |
${makeExample(
"Filtering a bass",
`beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`,
true,
)}
### Highpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>hcutoff</ic> | <ic>hpf</ic> | cutoff frequency of the highpass filter |
| <ic>hresonance</ic> | <ic>hpq</ic> | resonance of the highpass filter (0-1) |
${makeExample(
"Filtering a noise source",
`beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`,
true,
)}
### Bandpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>bandf</ic> | <ic>bpf</ic> | cutoff frequency of the bandpass filter |
| <ic>bandq</ic> | <ic>bpq</ic> | resonance of the bandpass filter (0-1) |
${makeExample(
"Sweeping the filter on the same guitar sample",
`beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`,
true,
)}
Alternatively, <ic>lpf</ic>, <ic>hpf</ic> and <ic>bpf</ic> can take a second argument, the **resonance**.
## Filter order (type)
You can also use the <ic>ftype</ic> method to change the filter type (order). There are two types by default, <ic>12db</ic> for a gentle slope or <ic>24db</ic> for a really steep filtering slope. The <ic>24db</ic> type is particularly useful for substractive synthesis if you are trying to emulate some of the Moog or Prophet sounds:
- <ic>ftype(type: string)</ic>: sets the filter type (order), either <ic>12db</ic> or <ic>24db</ic>.
${makeExample(
"Filtering a bass",
`beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`,
true,
)}
## Filter envelopes
The examples we have studied so far are static. They filter the sound around a fixed cutoff frequency. To make the sound more interesting, you can use the ADSR filter envelopes to shape the filter cutoff frequency over time. You will always find amplitude and filter envelopes on most commercial synthesizers. This is done using the following methods:
### Lowpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>lpenv</ic> | <ic>lpe</ic> | lowpass frequency modulation amount (negative or positive) |
| <ic>lpattack</ic> | <ic>lpa</ic> | attack of the lowpass filter |
| <ic>lpdecay</ic> | <ic>lpd</ic> | decay of the lowpass filter |
| <ic>lpsustain</ic> | <ic>lps</ic> | sustain of the lowpass filter |
| <ic>lprelease</ic> | <ic>lpr</ic> | release of the lowpass filter |
| <ic>lpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"Filtering a sawtooth wave dynamically",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2))
.lpenv(-8).lpq(10).out()`,
true,
)}
### Highpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>hpenv</ic> | <ic>hpe</ic> | highpass frequency modulation amount (negative or positive) |
| <ic>hpattack</ic> | <ic>hpa</ic> | attack of the highpass filter |
| <ic>hpdecay</ic> | <ic>hpd</ic> | decay of the highpass filter |
| <ic>hpsustain</ic> | <ic>hps</ic> | sustain of the highpass filter |
| <ic>hprelease</ic> | <ic>hpr</ic> | release of the highpass filter |
| <ic>hpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"Let's use another filter using the same example",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2))
.hpenv(8).hpq(10).out()`,
true,
)}
### Bandpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>bpenv</ic> | <ic>bpe</ic> | bandpass frequency modulation amount (negative or positive) |
| <ic>bpattack</ic> | <ic>bpa</ic> | attack of the bandpass filter |
| <ic>bpdecay</ic> | <ic>bpd</ic> | decay of the bandpass filter |
| <ic>bpsustain</ic> | <ic>bps</ic> | sustain of the bandpass filter |
| <ic>bprelease</ic> | <ic>bpr</ic> | release of the bandpass filter |
| <ic>bpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"And the bandpass filter, just for fun",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.bandf([500,1000,2000].beat(2))
.bpa([0.25, 0.125, 0.5].beat(2) * 4)
.bpenv(-4).release(2).out()
`,
true,
)}
`;
};

View File

@ -1,75 +0,0 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const osc = (application: Editor): string => {
// @ts-ignore
const makeExample = makeExampleFactory(application);
return `
# Open Sound Control
Topos is a sandboxed web application. It cannot speak with your computer directly or only through a secure connexion. You can use the [Open Sound Control](https://en.wikipedia.org/wiki/Open_Sound_Control) protocol to send and receive data from your computer. This protocol is used by many softwares and hardware devices. You can use it to control your favorite DAW, your favorite synthesizer, your favorite robot, or anything really! To use **OSC** with Topos, you will need to download the <ic>ToposServer</ic> by [following this link](https://github.com/Bubobubobubobubo/Topos). You can download everything as a zip file or clone the project if you know what you are doing. Here is a quick guide to get you started:
- 1) Download <ic>Topos</ic> and navigate to the <ic>ToposServer</ic> folder.
- 2) Install the dependencies using <ic>npm install</ic>. Start the server using <ic>npm start</ic>.
- 3) Open the <ic>Topos</ic> application in your web browser (server first, then application).
The <ic>ToposServer</ic> server is used both for **OSC** _input_ and _output_.
## Input
Send an **OSC** message to the server from another application or device at the address <ic>localhost:30000</ic>. Topos will store the last 1000 messages in a queue. You can access this queue using the <ic>getOsc()</ic> function.
### Unfiltered messages
You can access the last 1000 messages using the <ic>getOsc()</ic> function without any argument. This is raw data, you will need to parse it yourself:
${makeExample(
"Reading the last OSC messages",
`
beat(1)::getOsc()
// 0 : {data: Array(2), address: '/lala'}
// 1 : {data: Array(2), address: '/lala'}
// 2 : {data: Array(2), address: '/lala'}`,
true,
)}
### Filtered messages
The <ic>getOsc()</ic> can receive an address filter as an argument. This will return only the messages that match the filter:
${makeExample(
"Reading the last OSC messages (filtered)",
`
beat(1)::getOsc("/lala")
// 0 : (2) [89, 'bob']
// 1 : (2) [84, 'bob']
// 2 : (2) [82, 'bob']
`,
true,
)}
## Output
Once the server is loaded, you are ready to send an **OSC** message:
${makeExample(
"Sending a simple OSC message",
`
beat(1)::sound('cp').speed(2).vel(0.5).osc()
`,
true,
)}
This is a simple **OSC** message that will inherit all the properties of the sound. You can also send customized OSC messages using the <ic>osc()</ic> function:
${makeExample(
"Sending a customized OSC message",
`
// osc(address, port, ...message)
osc('/my/osc/address', 5000, 1, 2, 3)
`,
true,
)}
`;
};

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const lfos = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
@ -17,7 +17,7 @@ Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio w
${makeExample(
"Modulating the speed of a sample player using a sine LFO",
`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>.
@ -26,7 +26,7 @@ ${makeExample(
${makeExample(
"Modulating the speed of a sample player using a triangle LFO",
`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>.
@ -35,7 +35,7 @@ ${makeExample(
${makeExample(
"Modulating the speed of a sample player using a saw LFO",
`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.
@ -44,7 +44,7 @@ ${makeExample(
${makeExample(
"Modulating the speed of a sample player using a square LFO",
`beat(.25) && snd('cp').speed(1 + usquare(0.25, 0, 0.25) * 2).out()`,
true,
true
)};
- <ic>noise(times: number = 1)</ic>: returns a random value between -1 and 1.
@ -52,8 +52,8 @@ ${makeExample(
${makeExample(
"Modulating the speed of a sample player using noise",
`beat(.25) && snd('cp').speed(1 + noise() * 2).out()`,
true,
true
)};
`;
};
`
}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const long_forms = (app: Editor): string => {
// @ts-ignore
@ -19,7 +19,7 @@ ${makeExample(
// Playing each script for 8 bars in succession
script([1,2,3,4].bar(8))
`,
true,
true
)}
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))
`,
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_.
@ -44,7 +44,7 @@ ${makeExample(
`
flip(4) :: beat(1) :: snd('kick').out()
`,
true,
true
)}
${makeExample(
@ -56,7 +56,7 @@ flip(2.5, 75) :: beat(.25) :: snd('click')
flip(2.5) :: beat(.5) :: snd('bd').out()
beat(.25) :: sound('hat').end(0.1).cutoff(1200).pan(usine(1/4)).out()
`,
false,
false
)}
${makeExample(
@ -68,7 +68,7 @@ if (flip(4, 75)) {
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.
@ -93,7 +93,7 @@ if (flip(8)) {
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:
@ -103,7 +103,7 @@ ${makeExample(
`
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.
@ -122,7 +122,7 @@ function b() {
flipbar(2) && a()
flipbar(3) && b()
`,
true,
true
)}
${makeExample(
"Alternating over four bars",
@ -131,7 +131,7 @@ flipbar(2)
? beat(.5) && snd(['kick', 'hh'].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, 2, 7) :: snd('snare').n(3).out();
}`,
true,
true
)}
`;

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main";
import { makeExampleFactory, key_shortcut } from "../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory, key_shortcut } from "../Documentation";
export const midi = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
@ -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:**
- <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(
"F12",
"F12"
)}. 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()
`,
true,
true
)}
- <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")
`,
true,
true
)}
That's it! You are now ready to play with MIDI.
@ -54,7 +54,7 @@ ${makeExample(
// => midi_output("MIDI Bus 1")
rhythm(.5, 5, 8) :: midi(50).out()
`,
true,
true
)}
${makeExample(
@ -63,7 +63,7 @@ ${makeExample(
// MIDI Note 50, Velocity 50 + LFO, Channel 0
rhythm(.5, 5, 8) :: midi(50, 50 + usine(.5) * 20, 0).out()
`,
false,
false
)}
${makeExample(
@ -72,7 +72,7 @@ ${makeExample(
// MIDI Note 50, Velocity 50 + LFO, Channel 0
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:
@ -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(.25) && midi([64, 67, 69].beat() + 24).sustain(0.05).out()
`,
true,
true
)}
## Control and Program Changes
@ -99,7 +99,7 @@ ${makeExample(
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})
`,
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>).
@ -109,7 +109,7 @@ ${makeExample(
`
program_change([1,2,3,4,5,6,7,8].pick(), 1)
`,
true,
true
)}
@ -123,7 +123,7 @@ ${makeExample(
`
sysex(0x90, 0x40, 0x7f)
`,
true,
true
)}
## Clock
@ -135,7 +135,7 @@ ${makeExample(
`
beat(.25) && midi_clock() // Sending clock to MIDI device from the global buffer
`,
true,
true
)}
## Using midi with ziffers
@ -147,7 +147,7 @@ ${makeExample(
`
z1('0 2 e 5 2 q 4 2').midi().port(2).channel(4).out()
`,
true,
true
)}
${makeExample(
@ -155,7 +155,7 @@ ${makeExample(
`
z1('(0 2 e 5 2):0 (4 2):1').midi().out()
`,
true,
true
)}
`;

View File

@ -14,21 +14,6 @@ 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.
## 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
- Felix Roos for the [SuperDough](https://www.npmjs.com/package/superdough) audio engine.

View File

@ -7,25 +7,7 @@ export const bonus = (application: Editor): string => {
return `
# Bonus features
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.
## Editor theme configuration
The editor theme can be changed using the <ic>theme</ic> and <ic>randomTheme</ic> functions. The following example will use a random color scheme for every beat:
${makeExample(
"Random theme on each beat",
`
beat(1)::randomTheme()
`, true)}
You can also pick a theme using the <ic>theme</ic> function with a string as only argument:
${makeExample(
"Picking a theme",
`
beat(1)::theme("Batman")
`, true)}
Some features are here "just for fun" or "just because I can". They are not very interesting per se but are still available nonetheless. They mostly gravitate towards manipulating visuals or patterning other multimedia formats.
## Hydra Visual Live Coding
@ -33,19 +15,19 @@ beat(1)::theme("Batman")
<warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning>
</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 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.
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows the metaphor of analog synthesizer patching to allow its user to create complex live visuals from a web browser window. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance. Topos provides a simple way to integrate Hydra into a live coding session and to blend it with regular Topos code.
${makeExample(
"Hydra integration",
`beat(4) :: hydra.osc(3, 0.5, 2).out()`,
true,
`beat(4) :: app.hydra.osc(3, 0.5, 2).out()`,
true
)}
Close the documentation to see the effect: ${key_shortcut(
"Ctrl+D",
)}! **Boom, all shiny!**
You may feel like it's doing nothing! Press ${key_shortcut(
"Ctrl+D"
)} to close the documentation. **Boom, all shiny!**
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.
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.
Stopping **Hydra** is simple:
@ -53,35 +35,16 @@ ${makeExample(
"Stopping Hydra",
`
beat(4) :: stop_hydra() // this one
beat(4) :: hydra.hush() // or this one
beat(4) :: app.hydra.hush() // or this one
`,
true,
true
)}
### 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/):
I won't teach you how to play with Hydra. You can find some great resources on the [Hydra website](https://hydra.ojack.xyz/):
- [Hydra interactive documentation](https://hydra.ojack.xyz/docs/)
- [List of Hydra Functions](https://hydra.ojack.xyz/api/)
- [Source code on GitHub](https://github.com/hydra-synth/hydra)
### 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
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:
@ -99,8 +62,8 @@ beat(0.25)::gif({
rotation: ir(1, 360), // Rotation (in degrees)
posX: ir(1,1200), // CSS Horizontal Position
posY: ir(1, 800), // CSS Vertical Position
`,
true,
`, true
)}
`;
};

View File

@ -5,19 +5,7 @@ export const oscilloscope = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `# Oscilloscope
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:
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.
${makeExample(
"Oscilloscope configuration",
@ -35,7 +23,7 @@ scope({
refresh: 1 // refresh rate (in pulses)
})
`,
true,
true
)}
${makeExample(
@ -56,7 +44,7 @@ scope({enabled: true, thickness: 8,
color: ['purple', 'green', 'random'].beat(),
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 :)

View File

@ -1,10 +1,10 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const patterns = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Array patterns
# Patterns
**Topos** is using arrays as a way to make dynamic patterns of data (rhythms, melodies, etc).
It means that the following:
@ -14,7 +14,7 @@ ${makeExample(
`
beat(1)::sound('kick').out()
`,
true,
true
)}
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))
.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(2)::snd('snare').shape(.5).out()
`,
true,
true
)}
${makeExample(
"Using beat to create arpeggios",
@ -62,7 +62,7 @@ beat([.5, .25].beat(0.5)) :: sound('sine')
.delayfb(0.5)
.out()
`,
false,
false
)}
${makeExample(
"Cool ambiance",
@ -73,7 +73,7 @@ flip(2)::beat(1)::snd('froomy').out()
flip(4)::beat(2)::snd('pad').n(2).shape(.5)
.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.
@ -88,7 +88,7 @@ beat([1/4, 1/2].dur(1.5, 0.5))::sound(['jvbass', 'fikea'].bar())
* [1, 2].bar())
.out()
`,
true,
true
)}
${makeExample(
@ -101,7 +101,7 @@ beat([1, 0.5].beat()) :: sound(['bass3'].bar())
.pan(r(0, 1))
.speed([1,2,3].beat())
.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.
@ -117,11 +117,12 @@ beat(0.5)::sound('notes').n([1,2].dur(1, 2))
beat(1)::sound(['kick', 'fsnare'].dur(3, 1))
.n([0,3].dur(3, 1)).out()
`,
true,
true
)}
## Manipulating notes and scales
- <ic>pitch()</ic>: convert a list of integers to pitch classes
${makeExample(
@ -132,64 +133,9 @@ beat(0.25) :: snd('sine')
.key(["F4","F3"].beat(2.0))
.scale("minor").ad(0, .25).out()
`,
true,
true
)}
- <ic>semitones(number[], ...args?)</ic>: Create scale from semitone intervals.
${makeExample(
"Play pitches from scale created from semitone intervals",
`
beat(1) :: sound('gtr').pitch([0, 4, 3, 2].beat()).key(64)
.semitones(1, 1, 3, 1, 1, 2, 3).out()
`,
true,
)}
- <ic>cents(number[], ...args?)</ic>: Create scale from cent intervals.
${makeExample(
"Play pitches from scale created from cent intervals",
`
rhythm([0.5,0.25].beat(1),14,16) :: sound('pluck')
.stretch(r(1,5)).pitch(r(0,6)).key(57)
.cents(120,270,540,670,785,950,1215).out()
`,
true,
)}
- <ic>ratios(number[], ...args?)</ic>: Create scale from ratios.
${makeExample(
"Play pitches from scale created from ratios",
`
rhythm([0.5,0.25].beat(0.25),5,7) :: sound('east:3')
.pitch([0,1,2,3,4,5,6,7,8,9,10,11].beat(0.25)).key(67)
.ratios(2/11,4/11,6/11,8/11,10/11,11/11).out()
`,
true,
)}
- <ic>edo(number, scale?: string|number[])</ic>: Create scale from equal divisions of the octave. Creates chromatic scale by default.
${makeExample(
"Play pitches from scale created from equal divisions of the octave",
`
z0("e bd bd <bd bd [bd bd] [bd bd bd bd]>").sound().out()
flipbar(1) :: rhythm(.25,14,16) :: sound("ST10:30").stretch(3).gain(0.5)
.pitch([0,10,r(20,40),r(100,200),r(-200,200),r(200,300),200,r(3,666)].beat([1.0,0.5,0.25].bar(6)))
.octave(r(-6,6))
.edo(666,"rocritonic")
.out()
rhythm(2.0,26,32) :: sound("ST20").n([22,5,24,34,31,5,11,19].pick()).stretch(rI(1,6))
.pitch(rI(127,300))
.edo(666)
.out()
`,
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/).
${makeExample(
@ -199,7 +145,7 @@ beat(1) :: snd('gtr')
.note([0, 5, 2, 1, 7].scale("Major", 52).beat())
.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>.
@ -211,7 +157,7 @@ beat(1) :: snd('gtr')
.note([0, 5].scaleArp("mixolydian", 3).beat() + 50)
.out()
`,
true,
true
)}
## Iteration using the mouse
@ -227,7 +173,7 @@ beat(0.25)::sound('wt_piano')
.room(0.5).size(4).lpad(-2, .2).lpf(500, 0.3)
.ad(0, .2).out()
`,
true,
true
)}
## Simple data operations
@ -245,7 +191,7 @@ beat([1,.5,.25].beat()) :: snd('wt_stereo')
.lpf([500,1000,2000,4000].palindrome().beat())
.lpad(4, 0, .25).sustain(0.125).out()
`,
true,
true
)}
- <ic>random(index: number)</ic>: pick a random element in the given list.
@ -264,7 +210,7 @@ beat([.5, 1].rand() / 2) :: snd(
.lpf([5000,3000,2000].pick())
.end(0.5).out()
`,
true,
true
)}
- <ic>pick()</ic>: pick a random element in the list.
@ -276,7 +222,7 @@ beat(0.25)::sound(['ftabla', 'fwood'].pick())
.speed([1,2,3,4].pick()).ad(0, .125).n(ir(1,10))
.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.
@ -287,7 +233,7 @@ ${makeExample(
// 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()
`,
true,
true
)}
- <ic>repeat(amount: number)</ic>: repeat every list elements _n_ times.
@ -299,7 +245,7 @@ ${makeExample(
`
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>.
@ -309,7 +255,7 @@ ${makeExample(
`
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!
@ -319,7 +265,7 @@ ${makeExample(
`
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.
@ -337,7 +283,7 @@ beat(.25) :: snd('sine').fmi([1.99, 2])
.beat(.25)) // while the index changes
.out()
`,
true,
true
)}
## Filtering
@ -350,7 +296,7 @@ ${makeExample(
// Remove unique and 100 will repeat four times!
beat(1)::snd('sine').sustain(0.1).freq([100,100,100,100,200].unique().beat()).out()
`,
true,
true
)}
## Simple math operations

View File

@ -1,119 +0,0 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const generators = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Generator functions
JavaScript <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator" target="_blank">generators</a> are powerful functions for generating value sequences. They can be used to generate melodies, rhythms or control parameters.
In Topos generator functions should be called using the <ic>cache(key, function)</ic> function to store the current state of the generator. This function takes two arguments: the name for the cache and the generator instance.
Once the generator is cached the values will be returned from the named cache even if the generator function is modified. To clear the current cache and to re-evaluate the modified generator use the **Shift+Ctrl+Backspace** shortcut. Alternatively you can cache the modified generator using a different name.
The resulted values can be played using either <ic>pitch()</ic> or <ic>freq()</ic> or as Ziffers patterns. When playing the values using <ic>pitch()</ic> different scales and chained methods can be used to alter the result, for example <ic>mod(value: number)</ic> to limit the integer range or <ic>scale(name: string)</ic> etc. to change the resulting note.
${makeExample(
"Simple looping generator function",
`
function* simple() {
let x = 0;
while (x < 12) {
yield x+x;
x+=1;
}
}
beat(.25) && sound("triangle").pitch(cache("simple",simple())).scale("minor").out()
`,
true,
)};
${makeExample(
"Infinite frequency generator",
`
function* poly(x=0) {
while (true) {
const s = Math.tan(x/10)+Math.sin(x/20);
yield 2 * Math.pow(s, 3) - 6 * Math.pow(s, 2) + 5 * s + 200;
x++;
}
}
beat(.125) && sound("triangle").freq(cache("mathyshit",poly())).out()
`,
true,
)};
${makeExample(
"Truly scale free chaos inspired by Lorentz attractor",
`
function* strange(x = 0.1, y = 0, z = 0, rho = 28, beta = 8 / 3, zeta = 10) {
while (true) {
const dx = 10 * (y - x);
const dy = x * (rho - z) - y;
const dz = x * y - beta * z;
x += dx * 0.01;
y += dy * 0.01;
z += dz * 0.01;
const value = 300 + 30 * (Math.sin(x) + Math.tan(y) + Math.cos(z))
yield value;
}
}
beat(0.25) :: sound("triangle")
.freq(cache("stranger",strange(3,5,2)))
.adsr(.15,.1,.1,.1)
.log("freq").out()
`,
true,
)};
## OEIS integer sequences
To find some inspiration - or to enter into the void - one can visit <a href="https://oeis.org/" target="_blank">The On-Line Encyclopedia of Integer Sequences (OEIS)</a> to find some interesting integer sequences.
Many of the sequences are implemented by <a href="https://github.com/acerix/jisg/tree/main/src/oeis" target="_blank">JISG</a> (Javascript Integer Sequence Generators) project. Those sequences can be referenced directly with the identifiers using the cache function.
One of these implemented generators is the Inventory sequence <a href="https://github.com/acerix/jisg/blob/main/src/oeis/A342585.ts" target="_blank">A342585</a> made famous by <a href="https://www.youtube.com/watch?v=rBU9E-ZOZAI" target="_blank">Neil Sloane</a>.
${makeExample(
"Inventory sequence",
`
rhythm(0.5,[8,7,5,6].bar(4),9) :: sound("triangle")
.pitch(cache("Inventory",A342585))
.mod(11).scale("minor")
.adsr(.25,.05,.5,.5)
.room(2.0).size(0.5)
.gain(1).out()
`,
true,
)};
## Using generators with Ziffers
Alternatively generators can be used with Ziffers to generate longer patterns. In this case the generator function should be passed as an argument to the ziffers function. Ziffers patterns are cached separately so there is no need for using **cache()** function. Ziffers expects values from the generators to be integers or strings in ziffers syntax.
${makeExample(
"Ziffers patterns using a generator functions",
`
function* poly(x) {
while (true) {
yield 64 * Math.pow(x, 6) - 480 * Math.pow(x, 4) + 720 * Math.pow(x, 2);
x++;
}
}
z0(poly(1)).noteLength(0.5).semitones(2,2,3,2,2,2).sound("sine").out()
z1(poly(8)).noteLength(0.25).semitones(2,1,2,1,2,2).sound("sine").out()
z2(poly(-3)).noteLength(1.0).semitones(2,2,2,1,3,2).sound("sine").out()
`,
true,
)};
`
};

View File

@ -1,85 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_algorithmic = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Algorithmic operations
Ziffers provides shorthands for **many** numeric and algorithimic operations such as evaluating random numbers and creating sequences using list operations:
* **List operations:** Cartesian operation (_e.g._ <ic>(3 2 1)+(2 5)</ic>) using the <ic>+</ic> operator. All the arithmetic operators are supported.
${makeExample(
"Element-wise operations for melodic generation",
`
z1("1/8 _ 0 (0 1 3)+(1 2) 0 (2 3 5)-(1 2)").sound('sine')
.scale('pentatonic').fmi([0.25,0.5].beat(2)).fmh([2,4].beat(2))
.room(0.9).size(0.9).sustain(0.1).delay(0.125)
.delayfb(0.25).out();
`,
true,
)}
${makeExample(
"List operations",
`
z1('q (0 3 1 5)+(2 5) e (0 5 2)*(2 1) (0 5 2)%(2 3)')
.sound('sine')
.room(.5).size(1.0).adsr(.15,.15,.25,.1)
.scale("Bebop major")
.out()
`,
true,
)}
* **Random numbers:** <ic>(4,6)</ic> Random number between 4 and 6
${makeExample(
"Random numbers, true computer music at last!",
`
z1("s _ (0,8) 0 0 (0,5) 0 0").sound('sine')
.adsr(0, .1, 0, 0).scale('minor')
.fmdec(0.25).fmi(2).fmh([0.5, 0.25].beat(2))
.room(0.9).size(0.5).sustain(0.1) .delay(0.5)
.delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`,
true,
)}
## Variables
* <ic>A=(0 2 3 (1,4))</ic> Assign pre-evaluated list to a variable
* <ic>B~(0 2 3 (1,4))</ic> Assign list with operations to a variable
${makeExample(
"Assign lists to variables",
`
z1("s A=(0 (1,4)) B~(2 (3,8)) A B A B A")
.scale("modimic")
.sound("triangle")
.adsr(0.01,0.15,0.25,0)
.gain(0.5)
.out()
`,
true,
)}
${makeExample(
"Combine variables into lists and do operations",
`
z1("s A=(0 3) B=(3 8) C=(((A+B)+A)*B) D=(C-B) A A+C D")
.sound("sawtooth")
.ad(0.05,1.0)
.gain(0.5)
.out()
`,
true,
)}
`;
};

View File

@ -1,296 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_basics = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Ziffers
Ziffers is a **musical number based notation** tuned for _live coding_. It is a very powerful and flexible notation for describing musical patterns in very few characters. Number based musical notation has a long history and has been used for centuries as a shorthand technique for music notation. Amiika has written [papers](https://zenodo.org/record/7841945) and other documents describing his system. It is currently implemented for many live coding platforms including [Sardine](https://sardine.raphaelforment.fr) (Raphaël Forment) and [Sonic Pi](https://sonic-pi.net/) (Sam Aaron). Ziffers can be used for:
- composing melodies using using **classical music notation and concepts**.
- exploring **generative / aleatoric / stochastic** melodies and applying them to sounds and synths.
- embracing a different mindset and approach to time and **patterning**.
${makeExample(
"Super Fancy Ziffers example",
`
z1('1/8 024!3 035 024 0124').sound('wt_stereo')
.adsr(0, .4, 0.5, .4).gain(0.1)
.lpadsr(4, 0, .2, 0, 0)
.cutoff(5000 + usine(1/2) * 2000)
.n([1,2,4].beat(4)).out()
z2('<1/8 1/16> __ 0 <(^) (^ ^)> (0,8)').sound('wt_stereo')
.adsr(0, .5, 0.5, .4).gain(0.2)
.lpadsr(4, 0, .2, 0, 0).n(14)
.cutoff(200 + usine(1/2) * 4000)
.n([1,2,4].beat(4)).o(2).room(0.9).out()
let osci = 1500 + usine(1/2) * 2000;
z3('can can:2').sound().gain(1).cutoff(osci).out()
z4('1/4 kick kick snare kick').sound().gain(1).cutoff(osci).out()
`,
true,
)}
## Notation
The basic Ziffer notation is entirely written in JavaScript strings (_e.g_ <ic>"0 1 2"</ic>). It consists mostly of numbers and letters. The whitespace character is used as a separator. Instead of note names, Ziffer is using numbers to represent musical pitch and letters to represent musical durations. Alternatively, _floating point numbers_ can also be used to represent durations.
| Syntax | Symbol | Description |
|------------ |--------|------------------------|
| **Pitches** | <ic>0-9</ic> <ic>{10 11 21}</ic> | Numbers or escaped numbers in curly brackets |
| **Duration** | <ic>0.25</ic>, <ic>0.5</ic> | Floating point numbers can also be used as durations |
| **Duration** | <ic>1/4</ic>, <ic>1/16</ic> | Fractions can be used as durations |
| **Subdivision** | <ic>[1 [2 3]]</ic> | Durations can be subdivided using square brackets |
| **Cycles** | <ic>1 <2 4></ic> | Cycle values within the pattern |
| **Octave** | <ic>^ _</ic> | <ic>^</ic> for octave up and <ic>_</ic> for octave down |
| **Accidentals** | <ic># b</ic> | Sharp and flats, just like with regular music notation :smile: |
| **Rest** | <ic>r</ic> | Rest / silences |
| **Repeat** | <ic>!1-9</ic> | Repeat the item 1 to 9 times |
| **Chords** | <ic>[1-9]+ / [iv]+ / [AG]+name</ic> | Multiple pitches grouped together, roman numerals or named chords |
| **Samples** | <ic>[a-z0-9_]+</ic> | Samples can be used pitched or unpitched |
| **Index/Channel** | <ic>[a-z0-9]+:[0-9]*</ic> | Samples or midi channel can be changed using a colon |
**Note:** Some features are experimental and some are still unsupported. For full / prior syntax see article about <a href="https://zenodo.org/record/7841945" target="_blank">Ziffers</a>.
${makeExample(
"Pitches from 0 to 9",
`
z1('0.25 0 1 2 3 4 5 6 7 8 9').sound('wt_stereo')
.adsr(0, .1, 0, 0).out()`,
true,
)}
${makeExample(
"Escaped pitches using curly brackets",
`z1('_ _ 0 {9 10 11} 4 {12 13 14}')
.sound('wt_05').pan(r(0,1))
.cutoff(usaw(1/2) * 4000)
.room(0.9).size(0.9).out()`,
false,
)}
${makeExample(
"Durations using fractions and floating point numbers",
`
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)
.adsr(0, .1, 0, 0).delayfb(0.45).out()
`,
false,
)}
${makeExample(
"Disco was invented thanks to Ziffers",
`
z1('e _ _ 0 ^ 0 _ 0 ^ 0').sound('jvbass').out()
beat(1)::snd('bd').out(); beat(2)::snd('sd').out()
beat(3) :: snd('cp').room(0.5).size(0.5).orbit(2).out()
`,
false,
)}
${makeExample(
"Accidentals and rests for nice melodies",
`
z1('^ 1/8 0 1 b2 3 4 _ 4 b5 4 3 b2 1 0')
.scale('major').sound('triangle')
.cutoff(500).lpadsr(5, 0, 1/12, 0, 0)
.fmi(0.5).fmh(2).delay(0.5).delayt(1/3)
.adsr(0, .1, 0, 0).out()
`,
false,
)}
${makeExample(
"Repeat items n-times",
`
z1('1/8 _ _ 0!4 3!4 4!4 3!4')
.scale('major').sound('wt_oboe')
.shape(0.2).sustain(0.1).out()
z2('1/8 _ 0!4 5!4 4!2 7!2')
.scale('major').sound('wt_oboe')
.shape(0.2).sustain(0.1).out()
`,
false,
)}
${makeExample(
"Subdivided durations",
`
z1('w [0 [5 [3 7]]] h [0 4]')
.scale('major').sound('sine')
.fmi(usine(.5)).fmh(2).out()
`,
false,
)}
## Rests
${makeExample(
"Rest and octaves",
`
z1('q 0 ^ e0 r _ 0 _ r 4 ^4 4')
.sound('sine').scale("godian").out()
`,
true,
)}
${makeExample(
"Rests with durations",
`
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()
`,
true,
)}
## Chords
Chords can be build by grouping pitches or using roman numeral notation, or by using named chords.
${makeExample(
"Chords from pitches",
`
z1('1.0 024 045 058 046 014')
.sound('sine').adsr(0.5, 1, 0, 0)
.room(0.5).size(0.9)
.scale("minor").out()
`,
true
)}
${makeExample(
"Chords from roman numerals",
`
z1('2/4 i vi ii v')
.sound('triangle').adsr(0.2, 0.3, 0, 0)
.room(0.5).size(0.9).scale("major").out()
`,
true
)}
${makeExample(
"Named chords with repeats",
`
z1('0.25 Bmaj7!2 D7!2 _ Gmaj7!2 Bb7!2 ^ Ebmaj7!2')
.sound('square').room(0.5).cutoff(500)
.lpadsr(4, 0, .4, 0, 0).size(0.9)
.scale("major").out()
`,
true
)}
${makeExample(
"Transposing chords",
`
z1('q Amin!2').key(["A2", "E2"].beat(4))
.sound('sawtooth').cutoff(500)
.lpadsr(2, 0, .5, 0, 0, 0).out()`,
)}
${makeExample(
"Chord transposition with roman numerals",
`
z1('i i v%-4 v%-2 vi%-5 vi%-3 iv%-2 iv%-1')
.sound('triangle').adsr(1/16, 1/5, 0.1, 0)
.delay(0.5).delayt([1/8, 1/4].beat(4))
.delayfb(0.5).out()
beat(4) :: sound('breaks165').stretch(4).out()
`,
)}
${makeExample(
"Chord transposition with named chords",
`
z1('1/4 Cmin!3 Fmin!3 Fmin%-1 Fmin%-2 Fmin%-1')
.sound("sine").bpf(500 + usine(1/4) * 2000)
.out()
`,
)}
${makeExample(
"Programmatic inversions",
`
z1('1/6 i v 1/3 vi iv').invert([1,-1,-2,0].beat(4))
.sound("sawtooth").cutoff(1000)
.lpadsr(2, 0, .2, 0, 0).out()
`,
)}
## Arpeggios
Chords can be arpeggiated using the @-character within the ziffers notation or by using <ic>arpeggio</ic> method.
${makeExample(
"Arpeggio using the mini-notation",
`
z1("(i v vi%-3 iv%-2)@(s 0 2 0 1 2 1 0 2)")
.sound("sine").out()
`,
)}
${makeExample(
"Arpeggio from named chords with durations",
`
z1("_ Gm7 ^ C9 D7 Gm7")
.arpeggio("e 0 2 q 3 e 1 2")
.sound("sine").out()
`,
)}
${makeExample(
"Arpeggio from roman chords with inversions",
`
z1("i v%-1 vi%-1 iv%-2")
.arpeggio(0,2,1,2)
.noteLength(0.125)
.sound("sine").out()
`,
)}
## Chaining
- Basic notation
${makeExample(
"Simple method chaining",
`
z1('0 1 2 3').key('G3')
.scale('minor').sound('sine').out()
`,
true,
)}
${makeExample(
"More complex chaining",
`
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,
)}
${makeExample(
"Alternative way for inputting options",
`
z1('0 3 2 4',{key: 'D3', scale: 'minor pentatonic'}).sound('sine').out()
`,
true,
)}
## String prototypes
You can also use string prototypes as an alternative syntax for creating Ziffers patterns
${makeExample(
"String prototypes",
`
"q 0 e 5 2 6 2 q 3".z0().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()
`,
true,
)}
`;
};

View File

@ -1,157 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_rhythm = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Rhythm
Ziffers combines rhythmic and melodic notation into a single pattern language. This means that you can use the same pattern to describe both the rhythm and the melody of a musical phrase similarly to the way it is done in traditional music notation.
${makeExample(
"Duration chars",
`
z1('q 0 0 4 4 5 5 h4 q 3 3 2 2 1 1 h0').sound('sine').out()
`,
true,
)}
${makeExample(
"Fraction durations",
`
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()
`,
true,
)}
${makeExample(
"Decimal durations",
`
z1('0.25 5 1 2 6 0.125 3 8 0.5 4 1.0 0')
.sound('sine').scale("galian").out()
`,
true,
)}
## List of all duration characters
Ziffers maps the following duration characters to the corresponding note lengths.
| Character | Fraction | Duration | Name (US) | Name (UK) |
| ----- | ----- | ------- | ----- |
| m.. | 14/1 | 14.0 | Double dotted maxima | Double dotted Large
| m. | 12/1 | 12.0 | Dotted maxima | Dotted Large |
| m | 8/1 | 8.0 | Maxima | Large |
| l.. | 7/1 | 7.0 | Double dotted long note | Double dotted longa |
| l. | 6/1 | 6.0 | Long dotted note | Longa dotted |
| l | 4/1 | 4.0 | Long | Longa |
| d.. | 7/2 | 3.5 | Double dotted long note | Double dotted breve |
| d. | 3/3 | 3.0 | Dotted whole note | Double breve |
| n | 8/3 | 2.6666 | Triplet Long | Triplet longa |
| d | 2/1 | 2.0 | Double whole note | Breve |
| w.. | 7/4 | 1.75 | Double dotted whole note | Double dotted breve |
| w. | 3/2 | 1.5 | Dotted whole note | Dotted breve |
| k | 4/3 | 1.3333 | Triplet double whole | Triplet breve |
| w | 1/1 | 1.0 | Whole note | Semibreve |
| h.. | 7/8 | 0.875 | Double dotted half note | Double dotted minim |
| h. | 3/4 | 0.75 | Dotted half note | Dotted minim |
| c | 2/3 | 0.6666 | Triplet whole | Triplet semibreve |
| h | 1/2 | 0.5 | Half note  | Minim |
| p | 1/3 | 0.3333 | Triplet half | Triplet minim |
| q.. | 7/16 | 0.4375 | Double dotted quarter note | Double dotted crotchet |
| q. | 3/8 | 0.375 | Dotted quarter note | Dotted crotchet |
| q | 1/4 | 0.25 | Quarter note | Crotchet |
| e.. | 7/32 | 0.2187 | Double dotted 8th | Double dotted quaver |
| e. | 3/16 | 0.1875 | Dotted 8th | Dotted quaver |
| g | 1/6 | 0.1666 | Triplet quarter | Triplet crochet  |
| e | 1/8 | 0.125 | 8th note | Quaver |
| s.. | 7/64 | 0.1093 | Double dotted 16th | Double dotted semiquaver |
| a | 1/12 | 0.0833 | Triplet 8th | Triplet quaver |
| s. | 3/32 | 0.0937 | Dotted 16th | Dotted semiquaver |
| s | 1/16 | 0.0625 | 16th note | Semiquaver |
| t.. | 7/128 | 0.0546 | Double dotted 32th | Double dotted demisemiquaver |
| t. | 3/64 | 0.0468 | Dotted 32th | Dotted demisemiquaver |
| f | 1/24 | 0.0416 | Triplet 16th | Triplet semiquaver |
| t | 1/32 | 0.0312 | 32th note | Demisemiquaver |
| u.. | 7/256 | 0.0273 | Double dotted 64th | Double dotted hemidemisemiquaver |
| u. | 3/128 | 0.0234 | Dotted 64th | Dotted hemidemisemiquaver |
| x | 1/48 | 0.0208 | Triplet 32th | Triplet demi-semiquaver |
| u | 1/64 | 0.0156 | 64th note | Hemidemisemiquaver |
| o.. | 7/512 | 0.0136 | Double dotted 128th note | Double dotted semihemidemisemiquaver |
| y | 1/96 | 0.0104 | Triplet 64th | Triplet hemidemisemiquaver |
| o. | 3/256 | 0.0117 | Dotted 128th note | Dotted semihemidemisemiquaver |
| o | 1/128 | 0.0078 | 128th note | Semihemidemisemiquaver |
| j | 1/192 | 0.0052 | Triplet 128th | Triplet semihemidemisemiquaver |
| z | 0/1 | 0.0 | No length | No length |
## Samples
Samples can be patterned using the sample names or using <c>@</c>-operator for assigning sample to a pitch. Sample index can be changed using the <c>:</c> operator.
${makeExample(
"Sampled drums",
`
z1('bd [hh hh]').octave(-2).sound('sine').out()
`,
true,
)}
${makeExample(
"More complex pattern",
`
z1('bd [hh <hh <cp cp:2>>]').octave(-2).sound('sine').out()
`,
true,
)}
${makeExample(
"Pitched samples",
`
z1('0@sax 3@sax 2@sax 6@sax')
.octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out()
`,
true,
)}
${makeExample(
"Pitched samples from list operation",
`
z1('e (0 3 -1 4)+(-1 0 2 1)@sine')
.key('G4')
.scale('110 220 320 450')
.sound().out()
`,
true,
)}
${makeExample(
"Pitched samples with list notation",
`
z1('e (0 2 6 3 5 -2)@sax (0 2 6 3 5 -2)@arp')
.octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out()
`,
true,
)}
${makeExample(
"Sample indices",
`
z1('e 1:2 4:3 6:2')
.octave(-1).sound("east").out()
`,
true,
)}
${makeExample(
"Pitched samples with sample indices",
`
z1('_e 1@east:2 4@bd:3 6@arp:2 9@baa').sound().out()
`,
true,
)}
`;
};

View File

@ -1,101 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_scales = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Scales
Ziffers supports all the keys and scales. Keys can be defined by using [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation), for example <ic>F3</ic>. Western style (1490 scales) can be with scale names named after greek modes and extended by <a href="https://allthescales.org/intro.php" target="_blank">William Zeitler</a>. You will never really run out of scales to play with using Ziffers. Here is a short list of some possible scales that you can play with:
| Scale name | Intervals |
|------------|------------------------|
| Lydian | <ic>2221221</ic> |
| Mixolydian | <ic>2212212</ic> |
| Aeolian | <ic>2122122</ic> |
| Locrian | <ic>1221222</ic> |
| Ionian | <ic>2212221</ic> |
| Dorian | <ic>2122212</ic> |
| Phrygian | <ic>1222122</ic> |
| Soryllic | <ic>11122122</ic>|
| Modimic | <ic>412122</ic> |
| Ionalian | <ic>1312122</ic> |
| ... | And it goes on for <a href="https://ianring.com/musictheory/scales/traditions/zeitler" target="_blank">**1490** scales (See full list here)</a>. |
${makeExample(
"What the hell is the Modimic scale?",
`
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.scale('modimic').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.1) .delay(0.5)
.delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`,
true,
)}
You can also use more traditional <a href="https://ianring.com/musictheory/scales/traditions/western" target="_blank">western names</a>:
| Scale name | Intervals |
|------------|------------------------|
| Major | <ic>2212221</ic> |
| Minor | <ic>2122122</ic> |
| Minor pentatonic | <ic>32232</ic> |
| Harmonic minor | <ic>2122131</ic>|
| Harmonic major | <ic>2212131</ic>|
| Melodic minor | <ic>2122221</ic>|
| Melodic major | <ic>2212122</ic>|
| Whole | <ic>222222</ic> |
| Blues minor | <ic>321132</ic> |
| Blues major | <ic>211323</ic> |
${makeExample(
"Let's fall back to a classic blues minor scale",
`
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.scale('blues minor').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.25).delay(0.25)
.delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`,
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:
- **Young:** 106. 198. 306.2 400.1 502. 604. 697.9 806.1 898.1 1004.1 1102. 1200.
- **Wendy carlos:** 17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1
${makeExample(
"Wendy Carlos, here we go!",
`
z1("s ^ (0,8) 0 0 _ (0,5) 0 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').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.15).delay(0.1)
.delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`,
true,
)}
${makeExample(
"Werckmeister scale in Scala format",
`
const werckmeister = "107.82 203.91 311.72 401.955 503.91 605.865 701.955 809.775 900. 1007.82 1103.91 1200."
z0('s (0,3) ^ 0 3 ^ 0 (3,6) 0 _ (3,5) 0 _ 3 ^ 0 (3,5) ^ 0 6 0 _ 3 0')
.key('C3')
.scale(werckmeister)
.sound('sine')
.fmi(1 + usine(0.5) * irand(1,10))
.cutoff(100 + usine(.5) * 100)
.out()
onbeat(1,1.5,3) :: sound('bd').cutoff(100 + usine(.25) * 1000).out()
`,
true,
)}
`;
};

View File

@ -1,112 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_syncing = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Synchronization
Ziffers patterns can be synced to any event by using **cue**, **sync**, **wait** and **listen** methods.
## Sync with cue
The <ic>cue(name: string)</ic> methods can be used to send cue messages for ziffers patterns. The <ic>wait(name: string)</ic> method is used to wait for the cue message to be received before starting the next cycle.
${makeExample(
"Sending cue from event and wait",
`
beat(4.0) :: sound("bd").cue("foo").out()
z1("e 0 3 2 1 2 1").wait("foo").sound("sine").out()
`,
true,
)}
The <ic>sync(name: string)</ic> method is used to sync the ziffers pattern to the cue message.
${makeExample(
"Delayed start using individual cue",
`
register('christmas', n=>n.room(0.25).size(2).speed([0.5, 0.25, 0.125])
.delay(0.5).delayt(1/3).delayfb(0.5).bpf(200+usine(1/3)*500).out())
onbar(1) :: cue("bar")
onbar(2) :: cue('baz')
z1("<0.25 0.125> 0 4 2 -2").sync("bar").sound("ST40:25").christmas()
z2("<0.25 0.125> 0 6 4 -4").sync("baz").sound("ST40:25").christmas()
`,
true,
)}
The <ic>listen(name: string)</ic> method can be used to listen for the cue messages and play one event from the pattern for every cue.
${makeExample(
"Delayed start using individual cue",
`
beat(1.0) :: cue("boom")
z1("bd <hh ho>").listen("boom")
.sound().out()
`,
true,
)}
## Sync with beat
Patterns can also be synced using beat and setting the note length of events to zero using **z** duration character or <ic>noteLength(number)</ic> method.
${makeExample(
"Syncing with beat",
`
beat(.5) :: z1("<bd sn:3> hh:5").noteLength(0)
.sound().out()
beat([2.0,0.5,1.5].bar(1)) ::
z2("z _ 0 0 <2 1>").sound("bass:5")
.dur(0.5).out()
`,
true,
)}
## Automatic sync for ziffers patterns
Numbered methods **(z0-z16)** are synced automatically to **z0** method if it exsists. Syncing can also be done manually by using either the <ic>wait</ic> method, which will always wait for the current pattern to finish before starting the next cycle, or the <ic>sync</ic> method will only wait for the synced pattern to finish on the first time.
${makeExample(
"Automatic sync to z0",
`
z0('w 0 8').sound('peri').out()
z1('e 0 4 5 9').sound('bell').out()
`,
true,
)}
## Syncing patterns to each other
Patterns can also be synced together using the <ic>sync(name: Function)</ic> method. This will sync the pattern to the start of the referenced pattern. Copy this example and first run z1 and then z2 at random position.
${makeExample(
"Sync on first run",
`
z1('w __ 0 5 9 3').sound('bin').out()
z2('q __ 4 2 e 6 3 q 6').sync(z1).sound('east').out()
`,
true,
)}
## Sync with wait
Syncing can also be done using <ic>wait(name: Function)</ic> method. This will wait for the referenced pattern to finish before starting the next cycle.
${makeExample(
"Sync with wait",
`
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()
`,
true,
)}
`;
};

View File

@ -1,283 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_tonnetz = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Tonnetz
The Riemannian Tonnetz is a geometric representation of tonal relationships for applying mathematical operations to analyze harmonic and melodic relationships in tonal music. Ziffers includes an implementation of live coding tonnetz developed together with <a href="https://github.com/edelveart/TypeScriptTonnetz" target="_blank">Edgar Delgado Vega</a>. Live coding tonnetz implementation **combines 67 transformations** to **new explorative notation** that includes all of the traditional triad transformations (PLR functions), extended PLR* transformations, film music transformations and seventh transformations (PLRQ, PLRQ*, ST).
Tonnetz can be visualized as an <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numeric lattice</a> that represents the twelve pitch classes of the chromatic scale. The numeric visualization is a fork of <a href="https://hal.science/hal-03250334/" target="_blank">Web tonnetz</a> by Corentin Guichaou et al. (2021). The lattice can be arranged into multiple tonal pitch spaces which are all supported in Ziffers implementation.
In addition, we have included common graphs and cycles in Neo-Riemmanian theory: HexaCycles (<ic>pl</ic>), OctaCycles (<ic>pr</ic>), Enneacycles (seventh chords), Weitzmann Regions (triad chords), Boretz Regions (triad chords) and OctaTowers (tetrachords). You can explore each of these graphs in great generality over different Tonnetz.
## Explorative notation
Ziffers implements explorative live coding notation that indexes all of the transformations for triad and seventh chords. For more detailed transformations see Triad and Tetra chapters.
Transformations are applied by grouping operations into a **parameter string** which applies the **transformations** to the chord. The parameter string is a **sequence** of transformations **separated by whitespace**, for example <ic>plr rl2 p3lr</ic>. The numbers after the characters defines the **index for the operation**, as there can be multiple operations of the same type.
Indexed transformations <ic>[plrfsntq][1-9]*</ic>:
* p: Parallel
* l: Leading-tone exchange
* r: Relative
* f: Film transformation - Far-fifth
* n: Film transformation - Near-fifth (Nebenverdwandt) or PLRQ* transformation
* s: Film transformation - Slide
* h: Film transformation - Hexatonic Pole
* t: Film transformation - Tritone transposition
* q: PLR* transformation or PLRQ* transformation
### Examples:
${makeExample(
"Explorative transformations with roman chords",
`
z1('i i7').tonnetz("p1 p2 plr2")
.sound('wt_stereo')
.adsr(0, .1, 0, 0)
.out()`,
true,
)}
${makeExample(
"Arpeggiated explorative transformations",
`
z1("i7")
.tonnetz("p l2 r3 rp4l")
.arpeggio("e _ 0 1 s ^ 0 2 1 3 h _ 012 s ^ 2 1")
.sound("sine")
.out()`,
true,
)}
${makeExample(
"Arpeggios and note lengths with parameters",
`
z1("024")
.tonnetz("p lr rp lrp")
.arpeggio(0,2,1,2)
.noteLength(1/16,1/8)
.sound("sine")
.out()`,
true,
)}
## Triad transformations
Triad transformations can be defined explicitly using the <ic>triadTonnetz(transformation: string, tonnetz: number[])</ic> method. This method will only apply specific transformations to triad chords.
In the table below, we write the transformations types available for triads followed by the **transposition in semitones (+/-)** that we must perform to the **root of the chord**: <ic>0,4,3,7,5,1,8,6</ic>.
Second, you should know that the numbers next to the names of the transformations represent the **chord types to be exchanged**: <ic>1 = major, 2 = minor, 3 = diminished, 4 = augmented</ic>.
In fact, the functions <ic>p,l,r</ic> and the so-called [**film music transformations**](https://alpof.wordpress.com/2021/10/09/neo-riemannian-examples-in-music/), could have the numbers <ic>p12, l12, r12, f12 </ic> and so on, since they transform major and minor chords with their respective root transpositions. It must be clarified that the <ic>t6</ic> function is the only one among all that maintains the same type of chord.
Therefore, you will see that paying attention to the examples will allow you to infer whether you should **raise or lower the root** depending on the **type of chord**.
| Function | Function type | Root transposition | Example |
| :------: | :-------------------------------: | :----------------: | :-----------: |
| p | Parallel | 0 | C <-> Cm |
| l | Leading-tone | 4 | C <-> Em |
| r | Relative | 3 | C <-> Am |
| f | Film music: Far-fifth | 7 | C <-> Gm |
| n | Film music: Near-fifth | 5 | C <-> Fm |
| s | Film music: Slide | 1 | C <-> C#m |
| h | Film music: Hexatonic pole | 8 | C <-> G#m |
| t6 | Film music: Tritone Transposition | 6 | C <-> F# |
| p32 | Parallel | 0 | Cdim <-> Cm |
| p41 | Parallel | 0 | Caug <-> C |
| lt13 | Leading-tone | 4 | C <-> Edim |
| l41 | Leading-tone | 4 | Caug <-> Edim |
| l14 | Leading-tone | 4 | C <-> Eaug |
| rt23 | Relative | 3 | Cm <-> Adim |
| rt42 | Relative | 3 | Caug <-> Am |
| q13 | PLR* | 1 | C <-> C#dim |
| q42 | PLR* | 1 | Caug <-> C#m |
| n42 | PLR* | 5 | Caug <-> Fm |
* Remark A: We add <ic>t</ic> to <ic>l.13</ic>, <ic>r.23</ic>, <ic>r.42</ic> because we have similar syntax for sevenths transformations <ic>l13</ic>, <ic>r23</ic> and <ic>r42</ic>, although the meaning of the numbers (chord types) is different. See in the next section.
* Remark B: For those curious about mathematics, what we have implemented at Ziffers is the group called **PLR\*** [(Cannas, 2018, pp. 93-100)](https://publication-theses.unistra.fr/public/theses_doctorat/2018/CANNAS_Sonia_2018_ED269.pdf).
### Examples:
${makeExample(
"Synthetic 'Morton'",
`
z0('3/4 0 _ q 6 h 4 3 w 2 0 3/4 ^^ 0 _q 6 h 4 3 3/4 2 5/4 0 w r')
.scale("minor").sound('sawtooth').key("A")
.room(0.9).size(9).phaser(0.25).phaserDepth(0.8)
.vib(4).vibmod(0.15).out()
z1('w 904')
.scale("chromatic")
.tonnetz('o f l l o f l l o')
.sound('sine').adsr(0.1, 1, 1, 1.9).out()
z2('w 904')
.scale("chromatic")
.tonnetz('o f l l o f l l o')
.arpeggio('s 0 2 1 0 1 2 1 0 2 1 0 1 2 0 1 0')
.sound('sine').pan(rand(1, 0)).adsr(0, 0.125, 0.15, 0.25).out()
z3('e __ 4 s 0 e 1 2 s')
.sound('hat').delay(0.5).delayfb(0.35).out()`,
)}
## Different Tonnetz
At Ziffers we have strived to have fun and inspire you by exploring new sounds that Neo-Riemannian functions can offer you by changing only one parameter: The Tonnetz in which your chords move. By default, the Tonnetz has this form: <ic>[3, 4, 5]</ic>. Let's try an example as it will clarify this idea for us.
The <ic>Cm</ic> chord has the tone classes: <ic>037</ic>. Notice that the distance between the third of the chord and the root of the chord is <ic>3</ic> <ic>(3-0)</ic>. In turn, the distance of the fifth from the third is <ic>4</ic> semitones <ic>(7-3)</ic>. Finally, the distance left to get from the fifth to the root is <ic>5</ic> <ic>(7+5=0)</ic>. These distances are known as **intervalic structure**. In this regard, the array <ic>[x = 3, y = 4, z = 5]</ic> of a Tonnetz tells us the intervallic structure of the chords to which we apply the Neo-Riemannian functions.
:warning: To have a geometric intuition of the chords that we are going to describe, we suggest you see the <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numerical Tonnetz.</a>
In the next three Tonnetz we consider that we go from a minor chord to a major one by inversion (we change <ic>x</ic> and <ic>y</ic>).
* For the Tonnetz <ic>[3, 4, 5]</ic> we have minor chords <ic>037</ic> and major chords <ic>047</ic>
* For a Tonnetz <ic>[2, 3, 7]</ic> we have the "minor" chords <ic>025</ic> and the "major" chords <ic>035</ic>
* For a Tonnetz <ic>[1, 4, 7]</ic> we have the "minor" chords <ic>015</ic> and the "major" chords <ic>045</ic>
Are those all the Tonnetz? In fact, there are <ic>12</ic> spaces that comply with symmetries by **transposition and inversion**:
<ic>[3, 4, 5], [1, 1, 10], [1, 2, 9], [1, 3, 8], [1, 4, 7], [1, 5, 6], [ 2, 3, 7], [ 2, 5, 5]</ic>
<ic>[2, 4, 6], [2, 2, 8], [3, 3, 6], [4, 4, 4]</ic>
What do augmented chords or seventh chords sound like on a Tonnetz <ic>[1,5,6]</ic>? It is up to you to **explore all the transformations in different spaces**.
What if I want to place another type of Tonnetz that is not on the list? No problem, everyone is invited to the party.
* Remark C: If you want to know more about the topology and mathematics behind Tonnetz, you can refer to [Bigo (2013)](https://theses.hal.science/tel-01326827).
## Tetra transformations
Did you want to experiment with more functions? At Ziffers we have brought you Neo-Riemannian functions for seventh chords. The possibilities of sound exploration increase considerably, even more so if you **include different Tonnetz**.
Tetra transformations can be applied to seventh chords using the <ic>tetraTonnetz(transformation: string, tonnetz: number[])</ic> method. This method will apply specific transformations to certain type of chords. If the **chord is not the correct type**, the **transformation will not be applied**.
:warning: If you are here without having read the **triad chords transformations section**, we highly suggest you skip to it. The ideas and notation shown in this section are nothing more than an extension of what was developed above.
First, here we will deal with **9 interchangeable chord types** to which we will assign a number:
<ic>1 = 7, 2 = m7, 3 = hdim7, 4 = maj7, 5 = dim7, 6 = minMaj7, 7 = maj7#5, 8 = 7#5, 9 = 7b5</ic>.
Second, the **transpositions** that carry out the functions <ic>p = 0, l = 4, r = 3, q = 1, n = 5</ic>, raise or lower the **root of the chord** in semitones in equal measure. However, there are two new types of functions whose transpositions are the following: <ic>rr = 6</ic> and <ic>qq = 2</ic>.
You are ready, these have been all the requirements. Now a couple of examples will be enough for you to know how these functions operate.
* The <ic>p14</ic> function **does not move the root (0 semitones)** and changes a dominant 7th chord to a major 7th chord. For example: <ic>C7 <-> Cmaj7</ic>.
* The <ic>rr39</ic> function **moves the root 6 semitones** and swaps an hdim7 chord for a 7b5 chord. For example: <ic>Cm7b5 <-> F#7b5</ic>.
So that you can incorporate this new musical machinery into your game, all the possible transformations according to the type of seventh chord are listed below. You already know what each one will do.
* Remark D: For those curious about the mathematics behind it, we have implemented a group called **PLRQ** and another group called **PLRQ\*** extended [(Cannas, 2018, pp. 71-92)](https://publication-theses.unistra.fr/public/theses_doctorat/2018/CANNAS_Sonia_2018_ED269.pdf).
| Chord type | P functions | L functions | R functions | Q functions | N functions |
| :--------: | :----------------: | :-----------: | :----------------------------: | :---------: | :---------: |
| 7 | p12, p14, p18, p19 | l13, l15, l71 | r12, rr19 | q15, qq51 | n51 |
| m7 | p12, p23, p26 | l42 | r12, r23, r42 | q62 | |
| hdim7 | p23, p35, p39 | l13 | r23, r35, r53, r63, rr35, rr39 | q43, qq38 | |
| maj7 | p14, p47, p64 | l42 | r42 | q43 | |
| dim7 | p35 | l15 | r35, r53 | q15, qq51 | n51 |
| minMaj7 | p26, p64 | | r63, r76, r86 | q62, q76 | |
| maj7#5 | p47, p87 | l71 | r76 | q76 | |
| dom7#5 | p18, p87, p98 | l89 | r86, rr98 | qq38, qq98 | |
| dom7b5 | p19, p39, p98 | l89 | rr19, rr39, rr98 | qq98 | |
### Examples
${makeExample(
"Transform seventh chord from chromatic scale",
`
z1("1.0 047{10}")
.scale('chromatic')
.tetraTonnetz("o p18 q15 l13 n51 p19 q15")
.sound("sawtooth")
.cutoff(500 + usine(1/8) * 2000)
.adsr(.5,0.05,0.25,0.5)
.dur(2.0)
.out()`,
true,
)}
## Cyclic methods
In addition to the transformations, Ziffers implements cyclic methods that can be used to cycle through the tonnetz space. Cyclic methods turns individual pitch classes to chords using the tonnetz. The cyclic methods are:
* <ic>hexaCycle(tonnetz: number[], repeats: number = 3)</ic>: Cycles through chords in the hexa cycle
* <ic>octaCycle(tonnetz: number[], repeats: number = 4)</ic>: Cycles through chords in the octa cycle
* <ic>enneaCycle(tonnetz: number[], repeats: number = 3)</ic>: Cycles through chords in the ennea cycle
HexaCycles are sequences of major and minor triads generated by the <ic>p</ic> and <ic>l</ic> transformations . Let's take the following example starting with a <ic>C</ic> chord: <ic>C -> Cm -> Ab -> Abm -> E -> Em</ic>. You can start on the chord of your choice.
OctaCycles are sequences of major and minor triads generated using <ic>p</ic> and <ic>r</ic> transformations. Starting at <ic>C</ic>, we have the following sequence: <ic>C -> Cm -> Eb -> Ebm -> F# -> F#m -> A -> Am</ic>.
Unlike HexaCycles and OctaCycles, EnneaCycles are four-note chord sequences. Considering the functions implemented for tetrachords in Ziffers, we can interpret these sequences as generated by <ic>p12, p23, and l13</ic> transformations repeatedly: <ic>C7 -> Cm7 -> Cm7b5 -> Ab7 -> Abm7 -> Abm7b5 -> E7 -> Em7 -> Em7b5</ic>.
### Examples:
${makeExample(
"Arpeggio with ennea cycle",
`
z1("0 2 -1 3")
.enneaCycle()
.arpeggio(0,2,1)
.scale("modimic")
.noteLength(0.15,0.05,0.05,0.25)
.sound("sine")
.adsr(0.1,0.15,0.25,0.1)
.out()`,
true,
)}
${makeExample(
"Variating arpeggios",
`
z1("s 0 3 2 1")
.octaCycle()
.arpeggio([0,[0,2],[1,0],[0,1,2]].beat(0.15))
.sound("triangle")
.adsr(0.1,0.1,0.13,0.15)
.out()`,
true,
)}
## Cycles with vitamins and repetitions
Finally, cyclic methods in Ziffers can also be vitaminized with doses of different Tonnetz. However, this opens the way to different behavior with cycles.
We have the Tonnetz <ic>[2, 3, 7]</ic>, so <ic>hexaCycle([2, 3, 7])</ic>. The generated chords we hear are:
<ic>035 -> 025 -> 902 -> 9{11}2 -> 69{11} -> 68{11} </ic>
Apparently, everything operates as we expect: six chords and we return to the first. However, here comes the unexpected and perhaps somewhat obscure question:
* If we look at the graphs of the [numeric Tonnetz](https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/), our hexaCycle over <ic>[2, 3, 7]</ic> which starts with the chord <ic>035</ic> goes through all the intermediate chords generated by <ic>p</ic> and <ic>l</ic> functions until reaching <ic>035</ic> again?
As you can verify it manually, you will see that this is not the case. Upon reaching the <ic>68{11} </ic> chord, the cycle makes a jump of two chords (<ic>368 358</ic>) towards the <ic>035</ic> chord. This does not happen with the cycles in the Tonnetz <ic>[3, 4, 5]</ic>, since all the intermediate chords are played there.
To play the chords without jumps in our hexaCycle (although the prefix "hexa" would no longer have a precise meaning), we add a number of repetitions.
${makeExample(
"HexaCycles with vitamins",
`
z1("0")
.scale("chromatic")
.hexaCycle([2,3,7],4)
.sound("sine").out()
`,
true
)}
By default hexaCycles and enneaCycles have <ic>3</ic> repetitions, while octaCycles has <ic>4</ic> repetitions. We have specified a **chromatic scale** although this is the **default scale**. Try changing the **repeats and scales** when playing with different Tonnetz.
* Remark E: These cycles in Tonnetz <ic>[3, 4, 5]</ic> are implemented based on the work of [Douthett & Steinbach (1998, pp. 245-247)](https://www.jstor.org/stable/843877)
## :construction: Regions and OctaTowers
TBD: Implement and write about Weitzmann Regions, Boretz Regions, OctaTowers
`;
};

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const probabilities = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
@ -17,7 +17,7 @@ ${makeExample(
`
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(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.
@ -64,7 +64,7 @@ ${makeExample(
rarely(4) :: sound('bd').out(); // Rarely in 4 beats is bit less
rarely(8) :: sound('east').out(); // Rarely in 8 beats is even less
`,
true,
true
)}
${makeExample(
@ -74,7 +74,7 @@ ${makeExample(
often() :: beat(0.5) :: sound('hh').out();
sometimes() :: onbeat(1,3) :: sound('snare').out();
`,
false,
false
)}
${makeExample(
@ -91,7 +91,8 @@ ${makeExample(
.almostNever(n=>n.freq(400))
.out()
`,
false,
false
)}
`;
};
`
}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const loading_samples = (application: Editor): string => {
// @ts-ignore
@ -18,7 +18,7 @@ ${makeExample(
sd: ['sd/rytm-01-classic.wav','sd/rytm-00-hard.wav'],
hh: ['hh27/000_hh27closedhh.wav','hh/000_hh3closedhh.wav'],
}, '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:
@ -27,7 +27,7 @@ ${makeExample(
"Playing with the loaded samples",
`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:
@ -40,7 +40,7 @@ samples("github:tidalcycles/Dirt-Samples/master");
samples("github:Bubobubobubobubo/Dough-Samples/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!
@ -57,10 +57,9 @@ samples("shabda:ocean")
// Use the sound without 'shabda:'
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.
`;
};
`
}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const sample_banks = (application: Editor): string => {
// @ts-ignore
@ -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.
**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,10 +1,7 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
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 markdownList = "";
let keys = Object.keys(samples);
@ -32,7 +29,7 @@ export const samples_to_markdown = (
markdownList += `
<button
class="hover:bg-foreground inline px-4 py-2 bg-black text-brightwhite hover:text-background text-xl"
class="hover:bg-neutral-500 inline px-4 py-2 bg-neutral-700 text-orange-300 text-xl"
onclick="app.api._playDocExampleOnce(app.api.codeExamples['${codeId}'])"
>
${keys[i]}
@ -47,11 +44,13 @@ export const injectAllSamples = (application: Editor): string => {
return generatedPage;
};
export const injectDrumMachineSamples = (application: Editor): string => {
let generatedPage = samples_to_markdown(application, "Machines");
return generatedPage;
};
export const sample_list = (application: Editor): string => {
// @ts-ignore
const makeExample = makeExampleFactory(application);
@ -64,18 +63,14 @@ 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:
${makeExample(
"Wavetable synthesis made easy :)",
`
${makeExample("Wavetable synthesis made easy :)", `
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.
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background 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, "Waveforms")}
</div>
@ -84,17 +79,14 @@ ${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:
${makeExample(
"Using a classic drum machine",
`
"Using a classic drum machine", `
beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out()
`,
true,
)}
`, true)}
Here is the complete list of available machines:
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background 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, "Machines")}
</div>
@ -102,7 +94,7 @@ ${samples_to_markdown(application, "Machines")}
The default sample pack used by Ryan Kirkbride's [FoxDot](https://github.com/Qirky/FoxDot). It is a nice curated sample pack that covers all the basic sounds you could want.
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background 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, "FoxDot")}
</div>
@ -110,7 +102,7 @@ ${samples_to_markdown(application, "FoxDot")}
This set of audio samples is taken from [this wonderful collection](https://archive.org/details/AmigaSoundtrackerSamplePacksst-xx) of **Ultimate Tracker Amiga samples**. They were initially made by Karsten Obarski. These files were processed: pitched down one octave, gain down 6db. The audio has been processed with [SoX](https://github.com/chirlu/sox). The script used to do so is also included in this repository.
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background 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, "Amiga")}
</div>
@ -119,16 +111,14 @@ ${samples_to_markdown(application, "Amiga")}
A collection of many different amen breaks. Use <ic>.stretch()</ic> to play with these:
${makeExample(
"Stretching an amen break",
`
"Stretching an amen break", `
beat(4)::sound('amen1').stretch(4).out()
`,
true,
`, true,
)}
The stretch should be adapted based on the length of each amen break.
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background 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, "Amen")}
</div>
@ -137,24 +127,8 @@ ${samples_to_markdown(application, "Amen")}
Many live coders are expecting to find the Tidal sample library wherever they go, so here it is :)
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background 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")}
</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-background 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>
## Your samples
These samples are the one you have loaded for the duration of the session using the <ic>Import Samples</ic> button in the configuration menu.
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background 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, "user")}
</div>
`;
};
`
}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const synths = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
@ -17,17 +17,18 @@ ${makeExample(
`
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:
${makeExample(
"Listening to the different types of noise",
`
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:
@ -39,7 +40,7 @@ ${makeExample(
`
beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out()
`,
true,
true
)}
${makeExample(
@ -47,7 +48,7 @@ beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out()
`
beat(.5) && snd('triangle').note([60,"F4"].pick()).out()
`,
true,
true
)}
Chords can also played using different parameters:
@ -59,7 +60,7 @@ ${makeExample(
`
beat(1) && snd('triangle').chord(["C","Em7","Fmaj7","Emin"].beat(2)).adsr(0,.2).out()
`,
true,
true
)}
${makeExample(
@ -67,10 +68,44 @@ ${makeExample(
`
beat(.5) && snd('triangle').chord(60,64,67,72).invert([1,-3,4,-5].pick()).adsr(0,.2).out()
`,
true,
true
)}
# Controlling amplitude
## Vibrato
You can also add some amount of vibrato to the sound using the <ic>vib</ic> and <ic>vibmod</ic> methods. These can turn any oscillator into something more lively and/or into a sound effect when used with a high amount of modulation.
${makeExample(
"Different vibrato settings",
`
tempo(140);
beat(1) :: sound('triangle')
.freq(400).release(0.2)
.vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2))
.out()`,
true
)}
## Noise
A certain amount of brown noise can be added by using the <ic>.noise</ic> key:
${makeExample(
"Different vibrato settings",
`
tempo(140);
beat(1) :: sound('triangle')
.freq(400).release(0.2)
.noise([0.2,0.4,0.5].bar())
.vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2))
.out()`,
true
)}
## Controlling the amplitude
Controlling the amplitude and duration of the sound can be done using various techniques. The most important thing to learn is probably how set the amplitude (volume) of your synthesizer:
- <ic>gain(gain: number)</ic>: sets the gain of the oscillator.
@ -79,17 +114,15 @@ Controlling the amplitude and duration of the sound can be done using various te
${makeExample(
"Setting the gain",
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true,
true
)}
${makeExample(
"Setting the velocity",
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true,
true
)}
## Envelopes
<div class="mt-4 mb-4 lg:grid lg:grid-cols-4 lg:gap-4">
<img class="col-span-1 lg:ml-12 bg-gray-100 rounded-lg px-2 py-2", src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ADSR_Envelope_Graph.svg/1280px-ADSR_Envelope_Graph.svg.png" width="400" />
<z class="pl-8 lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal col-span-3 ">Synthesizers typically come with an amplitude envelope that can help you to shape the sound with a slow attack or long release. This is done in Topos using the amplitude envelope, composed of four parameters: <ic>attack</ic>, <ic>decay</ic>, <ic>sustain</ic> and <ic>release</ic>:</z>
@ -108,7 +141,7 @@ beat(0.5) :: sound('wt_piano')
.freq(100).decay(.2)
.sustain([0.1,0.5].beat(4))
.out()`,
true,
true
)}
This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The <ic>adsr()</ic> combines all the parameters together. It is a shortcut for setting the ADSR envelope:
@ -124,7 +157,7 @@ beat(0.5) :: sound('wt_piano')
.adsr(0, .2, [0.1,0.5].beat(4), 0)
.out()
`,
true,
true
)}
- <ic>ad(attack: number, decay: number)</ic>: sets the attack and decay phases, setting sustain and release to <ic>0</ic>.
@ -138,21 +171,81 @@ beat(0.5) :: sound('wt_piano')
.ad(0, .2)
.out()
`,
true,
true
)}
## Substractive synthesis using filters
The most basic synthesis technique used since the 1970s is called substractive synthesis. This technique is based on the use of rich sound sources (oscillators) as a base to build rich and moving timbres. Because rich sources contain a lot of different harmonics, you might want to filter some of them to obtain the timbre you are looking for. To do so, Topos comes with a set of basic filters that can be used to shape the sound exactly to your liking. There are three filter types by defaut, with more to be added in the future:
See the Filters page for details on lowpass, highpass and bandpass filters. I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers:
- **lowpass filter**: filters the high frequencies, keeping the low frequencies.
- **highpass filter**: filtering the low frequencies, keeping the high frequencies.
- **bandpass filter**: filters the low and high frequencies around a frequency band, keeping what's in the middle.
${makeExample(
"Filtering the high frequencies of an oscillator",
`beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`,
true,
true
)}
These filters all come with their own set of parameters. Note that we are describing the parameters of the three different filter types here. Choose the right parameters depending on the filter type you are using:
### Lowpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>cutoff</ic> | <ic>lpf</ic> | cutoff frequency of the lowpass filter |
| <ic>resonance</ic> | <ic>lpq</ic> | resonance of the lowpass filter (0-1) |
${makeExample(
"Filtering a bass",
`beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`,
true
)}
### Highpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>hcutoff</ic> | <ic>hpf</ic> | cutoff frequency of the highpass filter |
| <ic>hresonance</ic> | <ic>hpq</ic> | resonance of the highpass filter (0-1) |
${makeExample(
"Filtering a noise source",
`beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`,
true
)}
### Bandpass filter
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>bandf</ic> | <ic>bpf</ic> | cutoff frequency of the bandpass filter |
| <ic>bandq</ic> | <ic>bpq</ic> | resonance of the bandpass filter (0-1) |
${makeExample(
"Sweeping the filter on the same guitar sample",
`beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`,
true
)}
Alternatively, <ic>lpf</ic>, <ic>hpf</ic> and <ic>bpf</ic> can take a second argument, the **resonance**.
## Filter order (type)
You can also use the <ic>ftype</ic> method to change the filter type (order). There are two types by default, <ic>12db</ic> for a gentle slope or <ic>24db</ic> for a really steep filtering slope. The <ic>24db</ic> type is particularly useful for substractive synthesis if you are trying to emulate some of the Moog or Prophet sounds:
- <ic>ftype(type: string)</ic>: sets the filter type (order), either <ic>12db</ic> or <ic>24db</ic>.
${makeExample(
"Filtering a bass",
`beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`,
true
)}
I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers:
${makeExample(
"Simple synthesizer voice with filter",
`
@ -161,7 +254,7 @@ beat(.5) && snd('sawtooth')
.resonance(0.2).freq([100,150].pick())
.out()
`,
true,
true
)}
${makeExample(
@ -172,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')
.freq(freq*4 + usquare(2) * 200).sustain(0.125).out())
beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`,
false,
false
)}
${makeExample(
@ -186,26 +279,76 @@ beat(1/8)::sound('sine')
.freq(mouseX())
.gain(0.25)
.out()`,
false,
false
)}
## Noise
## Filter envelopes
The examples we have studied so far are static. They filter the sound around a fixed cutoff frequency. To make the sound more interesting, you can use the ADSR filter envelopes to shape the filter cutoff frequency over time. You will always find amplitude and filter envelopes on most commercial synthesizers. This is done using the following methods:
### Lowpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>lpenv</ic> | <ic>lpe</ic> | lowpass frequency modulation amount (negative or positive) |
| <ic>lpattack</ic> | <ic>lpa</ic> | attack of the lowpass filter |
| <ic>lpdecay</ic> | <ic>lpd</ic> | decay of the lowpass filter |
| <ic>lpsustain</ic> | <ic>lps</ic> | sustain of the lowpass filter |
| <ic>lprelease</ic> | <ic>lpr</ic> | release of the lowpass filter |
| <ic>lpadsr</ic> | | (**takes five arguments**) set all the parameters |
A certain amount of brown noise can be added by using the <ic>.noise</ic> key:
${makeExample(
"Different vibrato settings",
`
tempo(140);
beat(1) :: sound('triangle')
.freq(400).release(0.2)
.noise([0.2,0.4,0.5].bar())
.vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2))
.out()`,
true,
"Filtering a sawtooth wave dynamically",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2))
.lpenv(-8).lpq(10).out()`,
true
)}
### Highpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>hpenv</ic> | <ic>hpe</ic> | highpass frequency modulation amount (negative or positive) |
| <ic>hpattack</ic> | <ic>hpa</ic> | attack of the highpass filter |
| <ic>hpdecay</ic> | <ic>hpd</ic> | decay of the highpass filter |
| <ic>hpsustain</ic> | <ic>hps</ic> | sustain of the highpass filter |
| <ic>hprelease</ic> | <ic>hpr</ic> | release of the highpass filter |
| <ic>hpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"Let's use another filter using the same example",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2))
.hpenv(8).hpq(10).out()`,
true
)}
### Bandpass envelope
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>bpenv</ic> | <ic>bpe</ic> | bandpass frequency modulation amount (negative or positive) |
| <ic>bpattack</ic> | <ic>bpa</ic> | attack of the bandpass filter |
| <ic>bpdecay</ic> | <ic>bpd</ic> | decay of the bandpass filter |
| <ic>bpsustain</ic> | <ic>bps</ic> | sustain of the bandpass filter |
| <ic>bprelease</ic> | <ic>bpr</ic> | release of the bandpass filter |
| <ic>bpadsr</ic> | | (**takes five arguments**) set all the parameters |
${makeExample(
"And the bandpass filter, just for fun",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
.bandf([500,1000,2000].beat(2))
.bpa([0.25, 0.125, 0.5].beat(2) * 4)
.bpenv(-4).release(2).out()
`,
true
)}
## Wavetable synthesis
Topos can also do wavetable synthesis. Wavetable synthesis allows you to use any sound file as a source to build an oscillator. By default, Topos comes with more than 1000 waveforms thanks to the awesome [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) pack made by Kristoffer Ekstrand. Any sample name that contains <ic>wt_</ic> as a prefix will be interpreted by the sampler as a wavetable and thus as an oscillator. See for yourself:
@ -220,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(2) :: sound('snare').out()
beat(.5) :: sound('hh').out()`,
true,
true
)}
@ -238,7 +381,7 @@ beat(2) :: v('selec', irand(1, 100))
beat(2) :: v('swave', collection.pick())
beat(0.5) :: sound(v('swave')).n(v('selec')).out()
`,
true,
true
)}
You can work with them just like with any other waveform. Having so many of them makes them also very useful for generating sound effects, percussive, sounds, etc...
@ -264,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()
beat(2) :: sound('cp').room(1).sz(1).out()
`,
true,
true
)}
${makeExample(
@ -275,7 +418,7 @@ beat([4].bar()) :: sound('sine').fm('5.2183:4.5').sustain(0.05).out()
beat(.5) :: sound('sine')
.fmh([1, 1.75].beat())
.fmi($(1) % 30).orbit(2).room(0.5).out()`,
true,
true
)}
${makeExample(
@ -289,7 +432,7 @@ beat(0.25) :: sound('sine')
.cutoff(1500).delay(0.5).delayt(0.125)
.delayfb(0.8).fmh(Math.floor(usine(.5) * 4))
.out()`,
true,
true
)}
**Note:** you can also set the _modulation index_ and the _harmonic ratio_ with the <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.
@ -310,7 +453,7 @@ beat(.5) :: sound('sine')
.fmwave('triangle')
.fmsus(0).fmdec(0.2).out()
`,
true,
true
)}
## ZzFX
@ -324,7 +467,7 @@ ${makeExample(
`
beat(.5) :: sound(['z_sine', 'z_triangle', 'z_sawtooth', 'z_tan', 'z_noise'].beat()).out()
`,
true,
true
)}
${makeExample(
"Minimalist chiptune",
@ -338,7 +481,7 @@ beat(.5) :: sound('z_triangle')
.room(0.5).size(0.9)
.pitchJumpTime(0.01).out()
`,
true,
true
)}
It comes with a set of parameters that can be used to tweak the sound. Don't underestimate this synth! It is very powerful for generating anything ranging from chaotic noise sources to lush pads:
@ -376,7 +519,7 @@ beat(.25) :: sound('z_tan')
.sustain(0).decay([0.2, 0.1].pick())
.out()
`,
true,
true
)}
${makeExample(
"What is happening to me?",
@ -386,7 +529,7 @@ beat(1) :: snd('zzfx').zzfx([
[1.12,,97,.11,.16,.01,4,.77,,,30,.17,,,-1.9,,.01,.67,.2]
].beat()).out()
`,
false,
false
)}
${makeExample(
"Les voitures dans le futur",
@ -398,7 +541,7 @@ beat(1) :: sound(['z_triangle', 'z_sine'].pick())
.room(0.9).size(0.9)
.delayt(0.75).delayfb(0.5).out()
`,
false,
false
)}
Note that you can also design sounds [on this website](https://killedbyapixel.github.io/ZzFX/) and copy the generated code in Topos. To do so, please use the <ic>zzfx</ic> method with the generated array:
@ -408,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()
`,
true,
true
)}
# Speech synthesis
@ -428,7 +571,7 @@ ${makeExample(
`
beat(4) :: speak("Hello world!")
`,
true,
true
)}
${makeExample(
@ -436,7 +579,7 @@ ${makeExample(
`
beat(2) :: speak("Topos!","fr",irand(0,5))
`,
true,
true
)}
@ -447,7 +590,7 @@ ${makeExample(
`
onbeat(4) :: "Foobaba".voice(irand(0,10)).speak()
`,
true,
true
)}
${makeExample(
@ -460,7 +603,7 @@ ${makeExample(
beat(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).speak()
`,
true,
true
)}
${makeExample(
@ -480,7 +623,7 @@ ${makeExample(
.rate(rand(.4,.6))
.speak();
`,
true,
true
)}
`;
};

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const cyclical_time = (app: Editor): string => {
// @ts-ignore
@ -21,7 +21,7 @@ ${makeExample(
// This code is alternating between different mod values
beat([1,1/2,1/4,1/8].beat(2)) :: sound('hat').n(0).out()
`,
true,
true
)}
${makeExample(
@ -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();
beat([1,0.75].beat(2)) :: blip([50, 100].beat(2)).pan(r(0,1)).out();
`,
false,
false
)}
${makeExample(
@ -49,7 +49,7 @@ ${makeExample(
`
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.
@ -63,7 +63,7 @@ ${makeExample(
pulse([24, 16])::sound('hat').ad(0, .02).out()
pulse([48, [36,24].dur(4, 1)])::sound('fhardkick').ad(0, .1).out()
`,
true,
true
)}
${makeExample(
"pulse is the OG rhythmic function in Topos",
@ -71,7 +71,7 @@ ${makeExample(
pulse([48, 24, 16].beat(4)) :: sound('linnhats').out()
beat(1)::snd(['bd', '808oh'].beat(1)).out()
`,
false,
false
)}
@ -85,7 +85,7 @@ ${makeExample(
bar(1)::sound('kick').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()
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.
@ -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(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
@ -125,7 +125,7 @@ seq('xoxo')::sound('fhardkick').out()
seq('ooxo')::sound('fsoftsnare').out()
seq('xoxo', 0.25)::sound('fhh').out()
`,
true,
true
)}
${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(['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.
@ -159,7 +159,7 @@ function complexPat() {
}
fullseq('xooxooxx', 4) ? simplePat() : complexPat()
`,
true,
true
)}
@ -181,7 +181,7 @@ rhythm(.5, 7, 8)::sound('sine')
.freq(125).ad(0, .2).out()
rhythm(.5, 3, 8)::sound('sine').freq(500).ad(0, .5).out()
`,
true,
true
)}
@ -194,7 +194,7 @@ ${makeExample(
oneuclid(5, 9) :: snd('kick').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.
@ -207,7 +207,7 @@ bpm(135);
beat(.5) && bin($(1), 12) && snd('kick').n([4,9].beat(1.5)).out()
beat(.5) && bin($(2), 34) && snd('snare').n([3,5].beat(1)).out()
`,
true,
true
)}
${makeExample(
@ -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))
.room(1).size(1).out()
`,
true,
true
)}
${makeExample(
@ -233,7 +233,7 @@ beat(.5) && bin($(1), 911) && snd('ST69').n([2,3,4].beat())
.room(1).size(1).out()
beat(.5) && sound('amencutup').n(irand(2,7)).shape(0.3).out()
`,
false,
false
)}
If you don't find it spicy enough, you can add some more probabilities to your rhythms by taking advantage of the probability functions. See the functions documentation page to learn more about them.
@ -247,7 +247,7 @@ prob(60)::beat(.5) && euclid($(2), 3, 8) && snd('mash')
.pan(usine(1/4)).out()
prob(80)::beat(.5) && sound(['hh', 'hat'].pick()).out()
`,
true,
true
)}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
import pulses from "./pulses.svg";
export const linear_time = (app: Editor): string => {
@ -26,7 +26,7 @@ ${makeExample(
`
log(\`\$\{cbar()}\, \$\{cbeat()\}, \$\{cpulse()\}\`)
`,
true,
true
)}
### BPM and PPQN
@ -83,7 +83,7 @@ if((cbar() % 4) > 1) {
// This is always playing no matter what happens
beat([.5, .5, 1, .25].beat(0.5)) :: sound('shaker').out()
`,
true,
true
)}
## Time Warping
@ -108,7 +108,7 @@ flip(3) :: beat([.25,.5].beat(.5)) :: sound('dr')
// Jumping back and forth in time
beat(.25) :: warp([12, 48, 24, 1, 120, 30].pick())
`,
true,
true
)}
- <ic>beat_warp(beat: number)</ic>: this function jumps to the _n_ beat of the clock. The first beat is <ic>1</ic>.
@ -130,7 +130,7 @@ beat(.5) :: snd('arpy').note(
// Comment me to stop warping!
beat(1) :: beat_warp([2,4,5,10,11].pick())
`,
true,
true
)}
## 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(1.5,2.5,3.5, 3.75)::snd('hat').gain(r(0.9,1.1)).out() // Cool high-hats
`,
true,
true
)}
${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)
.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.
@ -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()
oncount([1,3,5.5,7,7.5,8],8) :: sound('hh').gain(irand(1.0,4.0)).out()
`,
true,
true
)}
${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([1, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16],16) :: sound('hh').out()
`,
true,
true
)}

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,5 +1,5 @@
import { makeExampleFactory } from "../../../Documentation";
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../Documentation";
import { type Editor } from "../../main";
import times from "./times.svg";
export const time = (application: Editor): string => {

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const variables = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
@ -22,7 +22,7 @@ ${makeExample(
`
v('my_cool_variable', 2)
`,
true,
true
)}
${makeExample(
@ -31,7 +31,7 @@ ${makeExample(
// Note that we just use one argument
log(v('my_cool_variable'))
`,
false,
false
)}
@ -55,7 +55,7 @@ ${makeExample(
`
rhythm(.25, 6, 8) :: sound('dr').n($(1)).end(.25).out()
`,
true,
true
)}
${makeExample(
@ -64,7 +64,7 @@ ${makeExample(
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n($(1, 20, 5)).end(.25).out()
`,
false,
false
)}
${makeExample(
@ -73,10 +73,10 @@ ${makeExample(
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n(drunk()).end(.25).out()
`,
false,
false
)}
`;
};
`
}

View File

@ -0,0 +1,550 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const ziffers = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Ziffers
Ziffers is a **musical number based notation** tuned for _live coding_. It is a very powerful and flexible notation for describing musical patterns in very few characters. Number based musical notation has a long history and has been used for centuries as a shorthand technique for music notation. Amiika has written [papers](https://zenodo.org/record/7841945) and other documents describing his system. It is currently implemented for many live coding platforms including [Sardine](https://sardine.raphaelforment.fr) (Raphaël Forment) and [Sonic Pi](https://sonic-pi.net/) (Sam Aaron). Ziffers can be used for:
- composing melodies using using **classical music notation and concepts**.
- exploring **generative / aleatoric / stochastic** melodies and applying them to sounds and synths.
- embracing a different mindset and approach to time and **patterning**.
${makeExample("Super Fancy Ziffers example", `
z1('1/8 024!3 035 024 0124').sound('wt_stereo')
.adsr(0, .4, 0.5, .4).gain(0.1)
.lpadsr(4, 0, .2, 0, 0)
.cutoff(5000 + usine(1/2) * 2000)
.n([1,2,4].beat(4)).out()
z2('<1/8 1/16> __ 0 <(^) (^ ^)> (0,8)').sound('wt_stereo')
.adsr(0, .5, 0.5, .4).gain(0.2)
.lpadsr(4, 0, .2, 0, 0).n(14)
.cutoff(200 + usine(1/2) * 4000)
.n([1,2,4].beat(4)).o(2).room(0.9).out()
let osci = 1500 + usine(1/2) * 2000;
z3('can can:2').sound().gain(1).cutoff(osci).out()
z4('1/4 kick kick snare kick').sound().gain(1).cutoff(osci).out()
`, true)}
## Notation
The basic Ziffer notation is entirely written in JavaScript strings (_e.g_ <ic>"0 1 2"</ic>). It consists mostly of numbers and letters. The whitespace character is used as a separator. Instead of note names, Ziffer is using numbers to represent musical pitch and letters to represent musical durations. Alternatively, _floating point numbers_ can also be used to represent durations.
| Syntax | Symbol | Description |
|------------ |--------|------------------------|
| **Pitches** | <ic>0-9</ic> <ic>{10 11 21}</ic> | Numbers or escaped numbers in curly brackets |
| **Duration** | <ic>0.25</ic>, <ic>0.5</ic> | Floating point numbers can also be used as durations |
| **Duration** | <ic>1/4</ic>, <ic>1/16</ic> | Fractions can be used as durations |
| **Subdivision** | <ic>[1 [2 3]]</ic> | Durations can be subdivided using square brackets |
| **Octave** | <ic>^ _</ic> | <ic>^</ic> for octave up and <ic>_</ic> for octave down |
| **Accidentals** | <ic># b</ic> | Sharp and flats, just like with regular music notation :smile: |
| **Rest** | <ic>r</ic> | Rest / silences |
| **Repeat** | <ic>!1-9</ic> | Repeat the item 1 to 9 times |
| **Chords** | <ic>[1-9]+ / [iv]+ / [AG]+name</ic> | Multiple pitches grouped together, roman numerals or named chords |
| **Samples** | <ic>[a-z0-9_]+</ic> | Samples can be used pitched or unpitched |
| **Index/Channel** | <ic>[a-z0-9]+:[0-9]*</ic> | Samples or midi channel can be changed using a colon |
**Note:** Some features are experimental and some are still unsupported. For full / prior syntax see article about <a href="https://zenodo.org/record/7841945" target="_blank">Ziffers</a>.
${makeExample(
"Pitches from 0 to 9",
`
z1('0.25 0 1 2 3 4 5 6 7 8 9').sound('wt_stereo')
.adsr(0, .1, 0, 0).out()`,
true
)}
${makeExample(
"Escaped pitches using curly brackets",
`z1('_ _ 0 {9 10 11} 4 {12 13 14}')
.sound('wt_05').pan(r(0,1))
.cutoff(usaw(1/2) * 4000)
.room(0.9).size(0.9).out()`,
false
)}
${makeExample(
"Durations using fractions and floating point numbers",
`
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)
.adsr(0, .1, 0, 0).delayfb(0.45).out()
`,
false
)}
${makeExample(
"Disco was invented thanks to Ziffers",
`
z1('e _ _ 0 ^ 0 _ 0 ^ 0').sound('jvbass').out()
beat(1)::snd('bd').out(); beat(2)::snd('sd').out()
beat(3) :: snd('cp').room(0.5).size(0.5).orbit(2).out()
`,
false
)}
${makeExample(
"Accidentals and rests for nice melodies",
`
z1('^ 1/8 0 1 b2 3 4 _ 4 b5 4 3 b2 1 0')
.scale('major').sound('triangle')
.cutoff(500).lpadsr(5, 0, 1/12, 0, 0)
.fmi(0.5).fmh(2).delay(0.5).delayt(1/3)
.adsr(0, .1, 0, 0).out()
`,
false
)}
${makeExample(
"Repeat items n-times",
`
z1('1/8 _ _ 0!4 3!4 4!4 3!4')
.scale('major').sound('wt_oboe')
.shape(0.2).sustain(0.1).out()
z2('1/8 _ 0!4 5!4 4!2 7!2')
.scale('major').sound('wt_oboe')
.shape(0.2).sustain(0.1).out()
`,
false
)}
${makeExample(
"Subdivided durations",
`
z1('w [0 [5 [3 7]]] h [0 4]')
.scale('major').sound('sine')
.fmi(usine(.5)).fmh(2).out()
`,
false
)}
## Chords
Chords can be build by grouping pitches or using roman numeral notation, or by using named chords.
${makeExample(
"Chords from pitches",
`
z1('1.0 024 045 058 046 014')
.sound('sine').adsr(0.5, 1, 0, 0)
.room(0.5).size(0.9)
.scale("minor").out()
`
)}
${makeExample(
"Chords from roman numerals",
`
z1('2/4 i vi ii v')
.sound('triangle').adsr(0.2, 0.3, 0, 0)
.room(0.5).size(0.9).scale("major").out()
`
)}
${makeExample(
"Named chords with repeats",
`
z1('0.25 Bmaj7!2 D7!2 _ Gmaj7!2 Bb7!2 ^ Ebmaj7!2')
.sound('square').room(0.5).cutoff(500)
.lpadsr(4, 0, .4, 0, 0).size(0.9)
.scale("major").out()
`
)}
${makeExample(
"Transposing chords",
`
z1('q Amin!2').key(["A2", "E2"].beat(4))
.sound('sawtooth').cutoff(500)
.lpadsr(2, 0, .5, 0, 0, 0).out()`
)}
${makeExample(
"Chord transposition with roman numerals",
`
z1('i i v%-4 v%-2 vi%-5 vi%-3 iv%-2 iv%-1')
.sound('triangle').adsr(1/16, 1/5, 0.1, 0)
.delay(0.5).delayt([1/8, 1/4].beat(4))
.delayfb(0.5).out()
beat(4) :: sound('breaks165').stretch(4).out()
`
)}
${makeExample(
"Chord transposition with named chords",
`
z1('1/4 Cmin!3 Fmin!3 Fmin%-1 Fmin%-2 Fmin%-1')
.sound("sine").bpf(500 + usine(1/4) * 2000)
.out()
`
)}
${makeExample(
"Programmatic inversions",
`
z1('1/6 i v 1/3 vi iv').invert([1,-1,-2,0].beat(4))
.sound("sawtooth").cutoff(1000)
.lpadsr(2, 0, .2, 0, 0).out()
`
)}
## Algorithmic operations
Ziffers provides shorthands for **many** numeric and algorithimic operations such as evaluating random numbers and creating sequences using list operations:
* **List operations:** Cartesian operation (_e.g._ <ic>(3 2 1)+(2 5)</ic>) using the <ic>+</ic> operator. All the arithmetic operators are supported.
${makeExample(
"Element-wise operations for melodic generation",
`
z1("1/8 _ 0 (0 1 3)+(1 2) 0 (2 3 5)-(1 2)").sound('sine')
.scale('pentatonic').fmi([0.25,0.5].beat(2)).fmh([2,4].beat(2))
.room(0.9).size(0.9).sustain(0.1).delay(0.5).delay(0.125)
.delayfb(0.25).out();
`,
true
)}
* **Random numbers:** <ic>(4,6)</ic> Random number between 4 and 6
${makeExample(
"Random numbers, true computer music at last!",
`
z1("s _ (0,8) 0 0 (0,5) 0 0").sound('sine')
.adsr(0, .1, 0, 0).scale('minor')
.fmdec(0.25).fmi(2).fmh([0.5, 0.25].beat(2))
.room(0.9).size(0.5).sustain(0.1) .delay(0.5)
.delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`,
true
)}
## Keys and scales
Ziffers supports all the keys and scales. Keys can be defined by using [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation), for example <ic>F3</ic>. Western style (1490 scales) can be with scale names named after greek modes and extended by [William Zeitler](https://ianring.com/musictheory/scales/traditions/zeitler). You will never really run out of scales to play with using Ziffers. Here is a short list of some possible scales that you can play with:
| Scale name | Intervals |
|------------|------------------------|
| Lydian | <ic>2221221</ic> |
| Mixolydian | <ic>2212212</ic> |
| Aeolian | <ic>2122122</ic> |
| Locrian | <ic>1221222</ic> |
| Ionian | <ic>2212221</ic> |
| Dorian | <ic>2122212</ic> |
| Phrygian | <ic>1222122</ic> |
| Soryllic | <ic>11122122</ic>|
| Modimic | <ic>412122</ic> |
| Ionalian | <ic>1312122</ic> |
| ... | And it goes on for **1490** scales |
${makeExample(
"What the hell is the Modimic scale?",
`
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.scale('modimic').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.1) .delay(0.5)
.delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`,
true
)}
<ic></ic>
You can also use more traditional <a href="https://ianring.com/musictheory/scales/traditions/western" target="_blank">western names</a>:
| Scale name | Intervals |
|------------|------------------------|
| Major | <ic>2212221</ic> |
| Minor | <ic>2122122</ic> |
| Minor pentatonic | <ic>32232</ic> |
| Harmonic minor | <ic>2122131</ic>|
| Harmonic major | <ic>2212131</ic>|
| Melodic minor | <ic>2122221</ic>|
| Melodic major | <ic>2212122</ic>|
| Whole | <ic>222222</ic> |
| Blues minor | <ic>321132</ic> |
| Blues major | <ic>211323</ic> |
${makeExample(
"Let's fall back to a classic blues minor scale",
`
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
.scale('blues minor').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.25).delay(0.25)
.delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`,
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:
- **Young:** 106. 198. 306.2 400.1 502. 604. 697.9 806.1 898.1 1004.1 1102. 1200.
- **Wendy carlos:** 17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1
${makeExample(
"Wendy Carlos, here we go!",
`
z1("s ^ (0,8) 0 0 _ (0,5) 0 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').fmi(2).fmh(2).room(0.5)
.size(0.5).sustain(0.15).delay(0.1)
.delay(0.25).delayfb(0.5).out();
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
`,
true
)}
## Synchronization
Ziffers numbered methods **(z0-z16)** can be used to parse and play patterns. Each method is individually cached and can be used to play multiple patterns simultaneously. By default, each Ziffers expression can have a different duration. This system is thus necessary to make everything fit together in a loop-based environment like Topos.
Numbered methods are synced automatically to **z0** method if it exsists. Syncing can also be done manually by using either the <ic>wait</ic> method, which will always wait for the current pattern to finish before starting the next cycle, or the <ic>sync</ic> method will only wait for the synced pattern to finish on the first time.
${makeExample(
"Automatic sync to z0",
`
z0('w 0 8').sound('peri').out()
z1('e 0 4 5 9').sound('bell').out()
`,
true
)}
${makeExample(
"Sync with wait",
`
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()
`,
true
)}
${makeExample(
"Sync on first run",
`
z1('w __ 0 5 9 3').sound('bin').out()
z2('q __ 4 2 e 6 3 q 6').sync(z1).sound('east').out()
`,
true
)}
## Examples
- Basic notation
${makeExample(
"Simple method chaining",
`
z1('0 1 2 3').key('G3')
.scale('minor').sound('sine').out()
`,
true
)}
${makeExample(
"More complex chaining",
`
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
)}
${makeExample(
"Simple options",
`
z1('0 3 2 4',{key: 'D3', scale: 'minor pentatonic'}).sound('sine').out()
`,
true
)}
${makeExample(
"Duration chars",
`
z1('q 0 0 4 4 5 5 h4 q 3 3 2 2 1 1 h0').sound('sine').out()
`,
true
)}
${makeExample(
"Fraction durations",
`
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()
`,
true
)}
${makeExample(
"Decimal durations",
`
z1('0.25 5 1 2 6 0.125 3 8 0.5 4 1.0 0')
.sound('sine').scale("galian").out()
`,
true
)}
${makeExample(
"Rest and octaves",
`
z1('q 0 ^ e0 r _ 0 _ r 4 ^4 4')
.sound('sine').scale("godian").out()
`,
true
)}
${makeExample(
"Rests with durations",
`
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()
`,
true
)}
- Scales
${makeExample(
"Microtonal scales",
`
z1('q 0 3 {10 14} e 8 4 {5 10 12 14 7 0}').sound('sine')
.fmi([1,2,4,8].pick())
.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()
`,
true
)}
${makeExample(
"Scala scale from variable",
`
const werckmeister = "107.82 203.91 311.72 401.955 503.91 605.865 701.955 809.775 900. 1007.82 1103.91 1200."
z0('s (0,3) ^ 0 3 ^ 0 (3,6) 0 _ (3,5) 0 _ 3 ^ 0 (3,5) ^ 0 6 0 _ 3 0')
.key('C3')
.scale(werckmeister)
.sound('sine')
.fmi(1 + usine(0.5) * irand(1,10))
.cutoff(100 + usine(.5) * 100)
.out()
onbeat(1,1.5,3) :: sound('bd').cutoff(100 + usine(.25) * 1000).out()
`,
true
)}
- Algorithmic operations
${makeExample(
"Random numbers",
`
z1('q 0 (2,4) 4 (5,9)').sound('sine')
.scale("Bebop minor")
.out()
`,
true
)}
${makeExample(
"List operations",
`
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")
.out()
`,
true
)}
## Samples
Samples can be patterned using the sample names or using <c>@</c>-operator for assigning sample to a pitch. Sample index can be changed using the <c>:</c> operator.
${makeExample(
"Sampled drums",
`
z1('bd [hh hh]').octave(-2).sound('sine').out()
`,
true
)}
${makeExample(
"More complex pattern",
`
z1('bd [hh <hh <cp cp:2>>]').octave(-2).sound('sine').out()
`,
true
)}
${makeExample(
"Pitched samples",
`
z1('0@sax 3@sax 2@sax 6@sax')
.octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out()
`,
true
)}
${makeExample(
"Pitched samples from list operation",
`
z1('e (0 3 -1 4)+(-1 0 2 1)@sine')
.key('G4')
.scale('110 220 320 450')
.sound().out()
`,
true
)}
${makeExample(
"Pitched samples with list notation",
`
z1('e (0 2 6 3 5 -2)@sax (0 2 6 3 5 -2)@arp')
.octave(-1).sound()
.adsr(0.25,0.125,0.125,0.25).out()
`,
true
)}
${makeExample(
"Sample indices",
`
z1('e 1:2 4:3 6:2')
.octave(-1).sound("east").out()
`,
true
)}
${makeExample(
"Pitched samples with sample indices",
`
z1('_e 1@east:2 4@bd:3 6@arp:2 9@baa').sound().out()
`,
true
)}
## String prototypes
You can also use string prototypes as an alternative syntax for creating Ziffers patterns
${makeExample(
"String prototypes",
`
"q 0 e 5 2 6 2 q 3".z0().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()
`,
true
)}
`;
};

View File

@ -141,7 +141,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
}
return Array.from(
{ 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 timepos = api.app.clock.pulses_since_origin;
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];
};
@ -174,12 +174,12 @@ export const makeArrayExtensions = (api: UserAPI) => {
const timepos = api.app.clock.pulses_since_origin;
const ppqn = api.ppqn();
const adjustedDurations: number[] = this.map(
(_, index) => durations[index % durations.length],
(_, index) => durations[index % durations.length]
);
const totalDurationInPulses = adjustedDurations.reduce(
// @ts-ignore
(acc, duration) => acc + duration * ppqn,
0,
0
);
const loopPosition = timepos % totalDurationInPulses;
let cumulativeDuration = 0;
@ -402,7 +402,7 @@ export const makeArrayExtensions = (api: UserAPI) => {
Array.prototype.scale = function (
scale: string = "major",
base_note: number = 0,
base_note: number = 0
) {
/**
* @param scale - the scale name
@ -426,7 +426,7 @@ Array.prototype.scale = function (
Array.prototype.scaleArp = function (
scaleName: string = "major",
boundary: number = 0,
boundary: number = 0
) {
/*
* @param scaleName - the scale name

View File

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

View File

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

View File

@ -1,11 +1,8 @@
import { OscilloscopeConfig, runOscilloscope } from "./Visuals/Oscilloscope";
import { OscilloscopeConfig, runOscilloscope, scriptBlinkers } from "./AudioVisualisation";
import { EditorState, Compartment } from "@codemirror/state";
import { scriptBlinkers } from "./Visuals/Blinkers";
import { javascript } from "@codemirror/lang-javascript";
import { markdown } from "@codemirror/lang-markdown";
import { Extension } from "@codemirror/state";
import { outputSocket } from "./IO/OSC";
import { getCodeMirrorTheme } from "./EditorSetup";
import {
initializeSelectedUniverse,
AppSettings,
@ -33,7 +30,6 @@ import { installWindowBehaviors } from "./WindowBehavior";
import { makeNumberExtensions } from "./extensions/NumberExtensions";
// @ts-ignore
import { registerSW } from "virtual:pwa-register";
import colors from "./colors.json";
if ("serviceWorker" in navigator) {
registerSW();
@ -50,10 +46,8 @@ export class Editor {
// Editor logic
editor_mode: "global" | "local" | "init" | "notes" = "global";
hidden_interface: boolean = false;
fontSize!: Compartment;
withLineNumbers!: Compartment;
themeCompartment!: Compartment;
vimModeCompartment!: Compartment;
hoveringCompartment!: Compartment;
completionsCompartment!: Compartment;
@ -71,7 +65,6 @@ export class Editor {
public _mouseX: number = 0;
public _mouseY: number = 0;
show_error: boolean = false;
currentThemeName: string = "Everblush";
buttonElements: Record<string, HTMLButtonElement[]> = {};
interface: ElementMap = {};
blinkTimeouts: Record<number, number> = {};
@ -98,27 +91,11 @@ export class Editor {
manualPlay: boolean = false;
isPlaying: boolean = false;
// OSC
outputSocket: WebSocket = outputSocket;
// Hydra
public hydra_backend: any;
public hydra: any;
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
// ================================================================================
@ -209,23 +186,9 @@ export class Editor {
// Loading universe from URL (if needed)
loadUniverserFromUrl(this);
// Set the color scheme for the application
let available_themes = Object.keys(colors);
if (this.settings.theme in available_themes) {
this.readTheme(this.settings.theme);
} else {
this.settings.theme = "Everblush";
this.readTheme(this.settings.theme);
}
}
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()];
return type === "locals"
? universe[type][this.local_index]
@ -253,27 +216,24 @@ export class Editor {
}
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(
"ui-known-universe-item-template",
"ui-known-universe-item-template"
) as HTMLTemplateElement;
if (!itemTemplate) {
console.warn("Missing template #ui-known-universe-item-template");
return;
}
let existing_universes = document.getElementById("existing-universes");
if (!existing_universes) {
console.warn("Missing element #existing-universes");
return;
}
let list = document.createElement("ul");
list.className =
"lg:h-80 lg:text-normal text-normal h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-brightwhite bg-background lg:mb-4 border rounded-lg";
"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(
...Object.keys(this.universes).map((it) => {
let item = itemTemplate.content.cloneNode(true) as DocumentFragment;
@ -285,10 +245,10 @@ export class Editor {
item
.querySelector(".delete-universe")
?.addEventListener("click", () =>
api._deleteUniverseFromInterface(it),
api._deleteUniverseFromInterface(it)
);
return item;
}),
})
);
existing_universes.innerHTML = "";
@ -296,18 +256,12 @@ export class Editor {
};
changeToLocalBuffer(i: number) {
/**
* 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.
*/
// Updating the CSS accordingly
const tabs = document.querySelectorAll('[id^="tab-"]');
const tab = tabs[i] as HTMLElement;
tab.classList.add("bg-foreground");
tab.classList.add("bg-orange-300");
for (let j = 0; j < tabs.length; j++) {
if (j != i) tabs[j].classList.remove("bg-foreground");
if (j != i) tabs[j].classList.remove("bg-orange-300");
}
let tab_id = tab.id.split("-")[1];
this.local_index = parseInt(tab_id);
@ -315,11 +269,6 @@ export class Editor {
}
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[] = [
this.interface.local_button,
this.interface.global_button,
@ -330,15 +279,15 @@ export class Editor {
let changeColor = (button: HTMLElement) => {
interface_buttons.forEach((button) => {
let svg = button.children[0] as HTMLElement;
if (svg.classList.contains("text-foreground_selection")) {
svg.classList.remove("text-foreground_selection");
button.classList.remove("text-foreground_selection");
if (svg.classList.contains("text-orange-300")) {
svg.classList.remove("text-orange-300");
button.classList.remove("text-orange-300");
}
});
button.children[0].classList.remove("text-white");
button.children[0].classList.add("text-foreground_selection");
button.classList.add("text-foreground_selection");
button.classList.add("fill-foreground_selection");
button.children[0].classList.add("text-orange-300");
button.classList.add("text-orange-300");
button.classList.add("fill-orange-300");
};
switch (mode) {
@ -380,7 +329,7 @@ export class Editor {
this.view.dispatch({
effects: this.chosenLanguage.reconfigure(
this.editor_mode == "notes" ? [markdown()] : [javascript()],
this.editor_mode == "notes" ? [markdown()] : [javascript()]
),
});
@ -389,14 +338,8 @@ export class Editor {
setButtonHighlighting(
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 =
button !== "pause" ? "Pause" : "Play";
if (button !== "pause") {
@ -443,7 +386,7 @@ export class Editor {
// All other buttons must lose the highlighting
document
.querySelectorAll(
possible_selectors.filter((_, index) => index != selector).join(","),
possible_selectors.filter((_, index) => index != selector).join(",")
)
.forEach((button) => {
button.children[0].classList.remove("animate-pulse");
@ -453,7 +396,7 @@ export class Editor {
unfocusPlayButtons() {
document.querySelectorAll('[id^="play-button-"]').forEach((button) => {
button.children[0].classList.remove("fill-foreground_selection");
button.children[0].classList.remove("fill-orange-300");
button.children[0].classList.remove("animate-pulse");
});
}
@ -481,36 +424,36 @@ export class Editor {
}
}
flashBackground(color: string, duration: number): void {
/**
* Flashes the background of the view and its gutters.
* @param {string} color - The color to set.
* @param {number} duration - Duration in milliseconds to maintain the color.
*/
flashBackground(color: string, duration: number): void {
const domElement = this.view.dom;
const gutters = domElement.getElementsByClassName(
"cm-gutter",
"cm-gutter"
) as HTMLCollectionOf<HTMLElement>;
domElement.classList.add("fluid-bg-transition");
Array.from(gutters).forEach((gutter) =>
gutter.classList.add("fluid-bg-transition"),
gutter.classList.add("fluid-bg-transition")
);
domElement.style.backgroundColor = color;
Array.from(gutters).forEach(
(gutter) => (gutter.style.backgroundColor = color),
(gutter) => (gutter.style.backgroundColor = color)
);
setTimeout(() => {
domElement.style.backgroundColor = "";
Array.from(gutters).forEach(
(gutter) => (gutter.style.backgroundColor = ""),
(gutter) => (gutter.style.backgroundColor = "")
);
domElement.classList.remove("fluid-bg-transition");
Array.from(gutters).forEach((gutter) =>
gutter.classList.remove("fluid-bg-transition"),
gutter.classList.remove("fluid-bg-transition")
);
}, duration);
}
@ -518,7 +461,7 @@ export class Editor {
private initializeElements(): void {
for (const [key, value] of Object.entries(singleElements)) {
this.interface[key] = document.getElementById(
value,
value
) as ElementMap[keyof ElementMap];
}
}
@ -526,18 +469,12 @@ export class Editor {
private initializeButtonGroups(): void {
for (const [key, ids] of Object.entries(buttonGroups)) {
this.buttonElements[key] = ids.map(
(id) => document.getElementById(id) as HTMLButtonElement,
(id) => document.getElementById(id) as HTMLButtonElement
);
}
}
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");
script.src = "https://unpkg.com/hydra-synth";
script.async = true;
@ -552,9 +489,6 @@ export class Editor {
}
private initializeHydra(): void {
/**
* Initializes the Hydra backend and sets up the Hydra synth.
*/
// @ts-ignore
this.hydra_backend = new Hydra({
canvas: this.interface.hydra_canvas as HTMLCanvasElement,
@ -562,65 +496,22 @@ export class Editor {
enableStreamCapture: false,
});
this.hydra = this.hydra_backend.synth;
(globalThis as any).hydra = this.hydra;
this.hydra.setResolution(1024, 768);
}
private setCanvas(canvas: HTMLCanvasElement): void {
/**
* Sets the canvas element and configures its size and context.
*
* @param canvas - The HTMLCanvasElement to set.
*/
if (!canvas) return;
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1;
// Assuming the canvas takes up the whole window
canvas.width = window.innerWidth * dpr;
canvas.height = window.innerHeight * dpr;
if (ctx) {
ctx.scale(dpr, dpr);
}
}
private updateInterfaceTheme(selected_theme: {[key: string]: string}): void {
function hexToRgb(hex: string): {r: number, g: number, b: number} | null {
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
for (const [key, value] of Object.entries(selected_theme)) {
let color = hexToRgb(value);
if (color) {
let colorString = `${color.r} ${color.g} ${color.b}`
document.documentElement.style.setProperty("--" + key, colorString);
}
}
}
getColorScheme(theme_name: string): {[key: string]: string} {
// Check if the theme exists in colors.json
let themes: Record<string, { [key: string]: any }> = colors;
return themes[theme_name];
}
readTheme(theme_name: string): void {
// Check if the theme exists in colors.json
let themes: Record<string, { [key: string]: any }> = colors;
let selected_theme = themes[theme_name];
if (selected_theme) {
this.currentThemeName = theme_name;
this.updateInterfaceTheme(selected_theme);
let codeMirrorTheme = getCodeMirrorTheme(selected_theme);
// Reconfigure the view with the new theme
this.view.dispatch({
effects: this.themeCompartment.reconfigure(codeMirrorTheme),
});
}
}
}
let app = new Editor();

View File

@ -22,7 +22,7 @@
::before,
::after {
--tw-content: "";
--tw-content: '';
}
/*
@ -44,21 +44,7 @@ html {
-o-tab-size: 4;
tab-size: 4;
/* 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 */
font-feature-settings: normal;
/* 5 */
@ -143,8 +129,7 @@ code,
kbd,
samp,
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 */
font-size: 1em;
/* 2 */
@ -239,9 +224,9 @@ select {
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
@ -288,7 +273,7 @@ Correct the cursor style of increment and decrement buttons in Safari.
2. Correct the outline style in Safari.
*/
[type="search"] {
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
@ -381,8 +366,7 @@ textarea {
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;
/* 1 */
color: #9ca3af;
@ -450,9 +434,7 @@ video {
display: none;
}
*,
::before,
::after {
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
@ -556,7 +538,7 @@ video {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #f0f0f0;
background: #F0F0F0;
}
.hljs,
@ -601,11 +583,11 @@ video {
.hljs-link,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #bc6060;
color: #BC6060;
}
.hljs-literal {
color: #78a960;
color: #78A960;
}
.hljs-built_in,
@ -1351,10 +1333,8 @@ video {
.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-colored: 0 1px 3px 0 var(--tw-shadow-color),
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);
--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 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 {
@ -1370,14 +1350,11 @@ video {
}
.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-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-duration: 150ms;
}
@ -1422,21 +1399,15 @@ video {
}
.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-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);
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--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 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0
var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--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);
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--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 {
@ -1460,7 +1431,7 @@ video {
@media (prefers-reduced-motion: no-preference) {
@keyframes pulse {
50% {
opacity: 0.5;
opacity: .5;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--black: 40 42 54;
--red: 68 71 90;
--green: 248 248 242;
--yellow: 98 114 164;
--blue: 139 233 253;
--magenta: 80 250 123;
--cyan: 255 184 108;
--white: 255 121 198;
--brightblack: 189 147 249;
--brightred: 255 85 85;
--brightgreen: 241 250 140;
--brightyellow: 139 233 253;
--brightblue: 80 250 123;
--brightmagenta: 255 184 108;
--brightcyan: 255 121 198;
--brightwhite: 189 147 249;
--background: 40 42 54;
--selection_foreground: 68 71 90;
--cursor: 139 233 253;
--foreground: 248 248 242;
--selection_background: 189 147 249;
}
}

221
src/themes/toposTheme.ts Normal file
View File

@ -0,0 +1,221 @@
import { EditorView } from "@codemirror/view";
import { Extension } from "@codemirror/state";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags as t } from "@lezer/highlight";
const base00 = "#262626",
base01 = "#3B4252",
base02 = "#BBBBBB",
base03 = "#4C566A",
base04 = "#D8DEE9",
base05 = "#E5E9F0",
base07 = "#8FBCBB",
base_red = "#BF616A",
base_deeporange = "#D08770",
base_pink = "#B48EAD",
base_cyan = "#FBCF8B",
base_yellow = "#88C0D0",
base_orange = "#D08770",
base_indigo = "#5E81AC",
base_purple = "#B48EAD",
base_green = "#A3BE8C",
base_lightgreen = "#A3BE8C";
const invalid = base_red,
darkBackground = "#262626",
highlightBackground = "#252525",
// background = base00,
tooltipBackground = base01,
cursor = base04;
/// The editor theme styles for Material Dark.
export const toposDarkTheme = EditorView.theme(
{
"&": {
color: base05,
// backgroundColor: background,
backgroundColor: "transparent",
fontSize: "24px",
fontFamily: "IBM Plex Mono",
},
".cm-content": {
caretColor: cursor,
fontFamily: "IBM Plex Mono",
},
".cm-cursor, .cm-dropCursor": {
borderLeftColor: cursor,
},
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{
backgroundColor: base00,
border: `0.5px solid ${base00}`,
},
".cm-panels": {
backgroundColor: darkBackground,
color: base05,
},
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
".cm-search.cm-panel": { backgroundColor: "transparent" },
".cm-searchMatch": {
outline: `1px solid ${base_cyan}`,
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor: highlightBackground,
},
".cm-activeLine": {
// backgroundColor: highlightBackground
backgroundColor: "rgb(76,76,106, 0.1)",
},
".cm-selectionMatch": {
backgroundColor: base04,
outline: `1px solid ${base_red}`,
},
"&.cm-focused .cm-matchingBracket": {
color: base02,
// outline: `1px solid ${base02}`,
},
"&.cm-focused .cm-nonmatchingBracket": {
color: base_red,
},
".cm-gutters": {
//backgroundColor: base00,
backgroundColor: "transparent",
color: base02,
},
".cm-activeLineGutter": {
backgroundColor: highlightBackground,
color: base02,
},
".cm-foldPlaceholder": {
border: "none",
color: `${base07}`,
},
".cm-tooltip": {
border: "none",
backgroundColor: tooltipBackground,
},
".cm-tooltip .cm-tooltip-arrow:before": {},
".cm-tooltip .cm-tooltip-arrow:after": {
borderTopColor: tooltipBackground,
borderBottomColor: tooltipBackground,
},
".cm-tooltip-autocomplete": {
"& > ul > li[aria-selected]": {
backgroundColor: highlightBackground,
color: base03,
},
},
},
{ dark: true }
);
/// The highlighting style for code in the Material Dark theme.
export const toposDarkHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: base_purple },
{
tag: [t.name, t.deleted, t.character, t.macroName],
color: base_cyan,
},
{ tag: [t.propertyName], color: base_yellow },
{ tag: [t.variableName], color: base05 },
{ tag: [t.function(t.variableName)], color: base_cyan },
{ tag: [t.labelName], color: base_purple },
{
tag: [t.color, t.constant(t.name), t.standard(t.name)],
color: base_yellow,
},
{ tag: [t.definition(t.name), t.separator], color: base_pink },
{ tag: [t.brace], color: base_purple },
{
tag: [t.annotation],
color: invalid,
},
{
tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
color: base_orange,
},
{
tag: [t.typeName, t.className],
color: base_orange,
},
{
tag: [t.operator, t.operatorKeyword],
color: base_indigo,
},
{
tag: [t.tagName],
color: base_deeporange,
},
{
tag: [t.squareBracket],
color: base_red,
},
{
tag: [t.angleBracket],
color: base02,
},
{
tag: [t.attributeName],
color: base05,
},
{
tag: [t.regexp],
color: invalid,
},
{
tag: [t.quote],
color: base_green,
},
{ tag: [t.string], color: base_lightgreen },
{
tag: t.link,
color: base_cyan,
textDecoration: "underline",
textUnderlinePosition: "under",
},
{
tag: [t.url, t.escape, t.special(t.string)],
color: base_yellow,
},
{ tag: [t.meta], color: base03 },
{ tag: [t.comment], color: base02, fontStyle: "italic" },
{ tag: t.monospace, color: base05 },
{ tag: t.strong, fontWeight: "bold", color: base_red },
{ tag: t.emphasis, fontStyle: "italic", color: base_lightgreen },
{ tag: t.strikethrough, textDecoration: "line-through" },
{ tag: t.heading, fontWeight: "bold", color: base_yellow },
{ tag: t.heading1, fontWeight: "bold", color: base_yellow },
{
tag: [t.heading2, t.heading3, t.heading4],
fontWeight: "bold",
color: base_yellow,
},
{
tag: [t.heading5, t.heading6],
color: base_yellow,
},
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: base_cyan },
{
tag: [t.processingInstruction, t.inserted],
color: base_red,
},
{
tag: [t.contentSeparator],
color: base_cyan,
},
{ tag: t.invalid, color: base02, borderBottom: `1px dotted ${base_red}` },
]);
/// Extension to enable the Material Dark theme (both the editor theme and
/// the highlight style).
export const toposTheme: Extension = [
toposDarkTheme,
syntaxHighlighting(toposDarkHighlightStyle),
];

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,21 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.html", "./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
colors: {
black: "rgb(var(--black) / <alpha-value>)",
red: "rgb(var(--red) / <alpha-value>)",
green: "rgb(var(--green) / <alpha-value>)",
yellow: "rgb(var(--yellow) / <alpha-value>)",
blue: "rgb(var(--blue) / <alpha-value>)",
magenta: "rgb(var(--magenta) / <alpha-value>)",
cyan: "rgb(var(--cyan) / <alpha-value>)",
white: "rgb(var(--white) / <alpha-value>)",
brightblack: "rgb(var(--brightblack) / <alpha-value>)",
brightred: "rgb(var(--brightred) / <alpha-value>)",
brightgreen: "rgb(var(--brightgreen) / <alpha-value>)",
brightyellow: "rgb(var(--brightyellow) / <alpha-value>)",
brightblue: "rgb(var(--brightblue) / <alpha-value>)",
brightmagenta: "rgb(var(--brightmagenta) / <alpha-value>)",
brightcyan: "rgb(var(--brightcyan) / <alpha-value>)",
brightwhite: "rgb(var(--brightwhite) / <alpha-value>)",
background: "rgb(var(--background) / <alpha-value>)",
selection_foreground: "rgb(var(--selection_foreground) / <alpha-value>)",
cursor: "rgb(var(--cursor) / <alpha-value>)",
foreground: "rgb(var(--foreground) / <alpha-value>)",
selection_background: "rgb(var(--selection_background) / <alpha-value>)",
}
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
safelist: [
{
pattern: /hljs+/,
},
],
theme: {
extend: {},
safelist: [{
pattern: /(bg|text|border)-(transparent|color0|color1|color2|color3|color4|color5|color6|color7|color8|color9|color10|color11|color12|color13|color14|color15|background|selection_background|cursor|foreground|selection_background)/,
}],
hljs: {
theme: "nord",
custom: {
general: {
comment: "#FEFEFE",
},
},
},
},
plugins: [require("tailwind-highlightjs")],
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

View File

@ -10,10 +10,11 @@ const vitePWAconfiguration = {
workbox: {
sourcemap: false,
cleanupOutdatedCaches: false,
cleanupOutdatedCaches: true,
globPatterns: [
"**/*.{js,js.gz,css,html,gif,png,json,woff,woff2,json,ogg,wav,mp3,ico,png,svg}",
"**/*.{js,css,html,gif,png,json,woff,woff2,json,ogg,wav,mp3,ico,png,svg}",
],
// Thanks Froos :)
runtimeCaching: [
{
urlPattern: ({ url }) =>

174
yarn.lock
View File

@ -1362,90 +1362,6 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@serialport/binding-mock@10.2.2":
version "10.2.2"
resolved "https://registry.yarnpkg.com/@serialport/binding-mock/-/binding-mock-10.2.2.tgz#d322a8116a97806addda13c62f50e73d16125874"
integrity sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==
dependencies:
"@serialport/bindings-interface" "^1.2.1"
debug "^4.3.3"
"@serialport/bindings-cpp@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@serialport/bindings-cpp/-/bindings-cpp-10.8.0.tgz#79507b57022ac264e963e7fbf3647a3821569a20"
integrity sha512-OMQNJz5kJblbmZN5UgJXLwi2XNtVLxSKmq5VyWuXQVsUIJD4l9UGHnLPqM5LD9u3HPZgDI5w7iYN7gxkQNZJUw==
dependencies:
"@serialport/bindings-interface" "1.2.2"
"@serialport/parser-readline" "^10.2.1"
debug "^4.3.2"
node-addon-api "^5.0.0"
node-gyp-build "^4.3.0"
"@serialport/bindings-interface@1.2.2", "@serialport/bindings-interface@^1.2.1":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz#c4ae9c1c85e26b02293f62f37435478d90baa460"
integrity sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==
"@serialport/parser-byte-length@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-byte-length/-/parser-byte-length-10.5.0.tgz#f3d4c1c7923222df2f3d3c7c8aaaa207fe373b49"
integrity sha512-eHhr4lHKboq1OagyaXAqkemQ1XyoqbLQC8XJbvccm95o476TmEdW5d7AElwZV28kWprPW68ZXdGF2VXCkJgS2w==
"@serialport/parser-cctalk@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-cctalk/-/parser-cctalk-10.5.0.tgz#0ee88db0768a361b7cfb9a394b74e480c38e1992"
integrity sha512-Iwsdr03xmCKAiibLSr7b3w6ZUTBNiS+PwbDQXdKU/clutXjuoex83XvsOtYVcNZmwJlVNhAUbkG+FJzWwIa4DA==
"@serialport/parser-delimiter@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-delimiter/-/parser-delimiter-10.5.0.tgz#b0d93100cdfd0619d020a427d652495073f3b828"
integrity sha512-/uR/yT3jmrcwnl2FJU/2ySvwgo5+XpksDUR4NF/nwTS5i3CcuKS+FKi/tLzy1k8F+rCx5JzpiK+koqPqOUWArA==
"@serialport/parser-inter-byte-timeout@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-10.5.0.tgz#8665ee5e6138f794ac055e83ef2d1c3653a577c0"
integrity sha512-WPvVlSx98HmmUF9jjK6y9mMp3Wnv6JQA0cUxLeZBgS74TibOuYG3fuUxUWGJALgAXotOYMxfXSezJ/vSnQrkhQ==
"@serialport/parser-packet-length@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-packet-length/-/parser-packet-length-10.5.0.tgz#4c4d733bdff8cc4749f2bd750e42e66f8f478def"
integrity sha512-jkpC/8w4/gUBRa2Teyn7URv1D7T//0lGj27/4u9AojpDVXsR6dtdcTG7b7dNirXDlOrSLvvN7aS5/GNaRlEByw==
"@serialport/parser-readline@10.5.0", "@serialport/parser-readline@^10.2.1":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-readline/-/parser-readline-10.5.0.tgz#df23365ae7f45679b1735deae26f72ba42802862"
integrity sha512-0aXJknodcl94W9zSjvU+sLdXiyEG2rqjQmvBWZCr8wJZjWEtv3RgrnYiWq4i2OTOyC8C/oPK8ZjpBjQptRsoJQ==
dependencies:
"@serialport/parser-delimiter" "10.5.0"
"@serialport/parser-ready@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-ready/-/parser-ready-10.5.0.tgz#1d9029f57b1abd664cb468e21bfccf7b44c6e8ea"
integrity sha512-QIf65LTvUoxqWWHBpgYOL+soldLIIyD1bwuWelukem2yDZVWwEjR288cLQ558BgYxH4U+jLAQahhqoyN1I7BaA==
"@serialport/parser-regex@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-regex/-/parser-regex-10.5.0.tgz#f98eab6e3d9bc99086269e9acf39a82db36d245f"
integrity sha512-9jnr9+PCxRoLjtGs7uxwsFqvho+rxuJlW6ZWSB7oqfzshEZWXtTJgJRgac/RuLft4hRlrmRz5XU40i3uoL4HKw==
"@serialport/parser-slip-encoder@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-slip-encoder/-/parser-slip-encoder-10.5.0.tgz#cb79ac0fda1fc87f049690ff7b498c787da67991"
integrity sha512-wP8m+uXQdkWSa//3n+VvfjLthlabwd9NiG6kegf0fYweLWio8j4pJRL7t9eTh2Lbc7zdxuO0r8ducFzO0m8CQw==
"@serialport/parser-spacepacket@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/parser-spacepacket/-/parser-spacepacket-10.5.0.tgz#2fc077c0ec16a9532c511ad5f2ab12d588796bc7"
integrity sha512-BEZ/HAEMwOd8xfuJSeI/823IR/jtnThovh7ils90rXD4DPL1ZmrP4abAIEktwe42RobZjIPfA4PaVfyO0Fjfhg==
"@serialport/stream@10.5.0":
version "10.5.0"
resolved "https://registry.yarnpkg.com/@serialport/stream/-/stream-10.5.0.tgz#cda8fb3e8d03094b0962a3d14b73adfcd591be58"
integrity sha512-gbcUdvq9Kyv2HsnywS7QjnEB28g+6OGB5Z8TLP7X+UPpoMIWoUsoQIq5Kt0ZTgMoWn3JGM2lqwTsSHF+1qhniA==
dependencies:
"@serialport/bindings-interface" "1.2.2"
debug "^4.3.2"
"@strudel.cycles/core@0.8.2":
version "0.8.2"
resolved "https://registry.yarnpkg.com/@strudel.cycles/core/-/core-0.8.2.tgz#62e957a3636b39938d1c4ecc3fd766d02fc523bb"
@ -1969,7 +1885,7 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -2476,11 +2392,6 @@ highlight.js@^11.5.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.8.0.tgz#966518ea83257bae2e7c9a48596231856555bb65"
integrity sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==
highlight.js@^11.9.0:
version "11.9.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.9.0.tgz#04ab9ee43b52a41a047432c8103e2158a1b8b5b0"
integrity sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==
html-encoder-decoder@^1.3.9:
version "1.3.9"
resolved "https://registry.yarnpkg.com/html-encoder-decoder/-/html-encoder-decoder-1.3.9.tgz#d5ec7d249cd525709f7640ae9340f482cc86e94a"
@ -2807,11 +2718,6 @@ lodash@^4.17.20:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
long@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
lru-cache@^10.0.1:
version "10.0.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a"
@ -2908,11 +2814,6 @@ nanostores@^0.8.1:
resolved "https://registry.yarnpkg.com/nanostores/-/nanostores-0.8.1.tgz#963577028ac10eeb50bec376535f4762ab5af9be"
integrity sha512-1ZCfQtII2XeFDrtqXL2cdQ/diGrLxzRB3YMyQjn8m7GSGQrJfGST2iuqMpWnS/ZlifhtjgR/SX0Jy6Uij6lRLA==
node-addon-api@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762"
integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==
node-fetch@^2.6.1:
version "2.6.13"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.13.tgz#a20acbbec73c2e09f9007de5cda17104122e0010"
@ -2920,11 +2821,6 @@ node-fetch@^2.6.1:
dependencies:
whatwg-url "^5.0.0"
node-gyp-build@^4.3.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.7.0.tgz#749f0033590b2a89ac8edb5e0775f95f5ae86d15"
integrity sha512-PbZERfeFdrHQOOXiAKOY0VPbykZy90ndPKk0d+CFDegTKmWp1VgOTz2xACVbr1BjCWxrQp68CXtvNsveFhqDJg==
node-releases@^2.0.13:
version "2.0.13"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"
@ -2977,18 +2873,6 @@ once@^1.3.0:
dependencies:
wrappy "1"
osc@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/osc/-/osc-2.4.4.tgz#52b2e914253b04a792d28ee892b0fa1ca8bdc1a2"
integrity sha512-YJr2bUCQMc9BIaq1LXgqYpt5Ii7wNy2n0e0BkQiCSziMNrrsYHhH5OlExNBgCrQsum60EgXZ32lFsvR4aUf+ew==
dependencies:
long "4.0.0"
slip "1.0.2"
wolfy87-eventemitter "5.2.9"
ws "8.13.0"
optionalDependencies:
serialport "10.5.0"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@ -3265,26 +3149,6 @@ serialize-javascript@^4.0.0:
dependencies:
randombytes "^2.1.0"
serialport@10.5.0:
version "10.5.0"
resolved "https://registry.yarnpkg.com/serialport/-/serialport-10.5.0.tgz#b85f614def6e8914e5865c798b0555330903a0f8"
integrity sha512-7OYLDsu5i6bbv3lU81pGy076xe0JwpK6b49G6RjNvGibstUqQkI+I3/X491yBGtf4gaqUdOgoU1/5KZ/XxL4dw==
dependencies:
"@serialport/binding-mock" "10.2.2"
"@serialport/bindings-cpp" "10.8.0"
"@serialport/parser-byte-length" "10.5.0"
"@serialport/parser-cctalk" "10.5.0"
"@serialport/parser-delimiter" "10.5.0"
"@serialport/parser-inter-byte-timeout" "10.5.0"
"@serialport/parser-packet-length" "10.5.0"
"@serialport/parser-readline" "10.5.0"
"@serialport/parser-ready" "10.5.0"
"@serialport/parser-regex" "10.5.0"
"@serialport/parser-slip-encoder" "10.5.0"
"@serialport/parser-spacepacket" "10.5.0"
"@serialport/stream" "10.5.0"
debug "^4.3.3"
set-function-length@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed"
@ -3329,11 +3193,6 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
slip@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/slip/-/slip-1.0.2.tgz#ba45a923034d6cf41b1a27aebe7128282c8d551f"
integrity sha512-XrcHe3NAcyD3wO+O4I13RcS4/3AF+S9RvGNj9JhJeS02HyImwD2E3QWLrmn9hBfL+fB6yapagwxRkeyYzhk98g==
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
@ -3460,10 +3319,10 @@ sucrase@^3.32.0:
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
superdough@^0.9.12:
version "0.9.12"
resolved "https://registry.yarnpkg.com/superdough/-/superdough-0.9.12.tgz#455f8860bc13cffbe1d8f391919e8f1dba1ff0b5"
integrity sha512-rsdCoYk5rLYster4tE5mSGjotf/TNP3gPpsuK4hxTZNxL92TkdEcbPFLnJfky5oMQJtpRY1XqAXUx3htLbHEZA==
superdough@^0.9.11:
version "0.9.11"
resolved "https://registry.yarnpkg.com/superdough/-/superdough-0.9.11.tgz#3a3842a47d6340477f77d39077303f05e15274dd"
integrity sha512-s0SNSg/EJHwp2sUnE2A7pTZ0G2luiSEq9NVKJvodjJw11Tn0fOp9XcnegNXINYz3U6mAsUYRoeaj4NmuTL13fA==
dependencies:
nanostores "^0.8.1"
@ -3850,11 +3709,6 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.13:
gopd "^1.0.1"
has-tostringtag "^1.0.0"
wolfy87-eventemitter@5.2.9:
version "5.2.9"
resolved "https://registry.yarnpkg.com/wolfy87-eventemitter/-/wolfy87-eventemitter-5.2.9.tgz#e879f770b30fbb6512a8afbb330c388591099c2a"
integrity sha512-P+6vtWyuDw+MB01X7UeF8TaHBvbCovf4HPEMF/SV7BdDc1SMTiBy13SRD71lQh4ExFTG1d/WNzDGDCyOKSMblw==
workbox-background-sync@7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.0.0.tgz#2b84b96ca35fec976e3bd2794b70e4acec46b3a5"
@ -4018,11 +3872,6 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
ws@8.13.0:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
yallist@^3.0.2:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
@ -4033,15 +3882,10 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
zifferjs@^0.0.55:
version "0.0.55"
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.55.tgz#ff7d08c9afde6cb78649f585b5a2c97ee4c97f22"
integrity sha512-QO/xWN3RugMbusIYxB7H1aHSm1w8OD1leEseJcDwxBx9VxTBWZF9SrxGbNdRowFAIfFg9b4hpOYmMSQYqi87EA==
zyklus@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/zyklus/-/zyklus-0.1.4.tgz#229b2966fd1126ef72c6004697269118762bdcd5"
integrity sha512-hbv2cyy4nOI7P8nL8b3ki1jswoLzkUzewPgCLDdDfABryDkV5iO8DAbU25OgO5ShRZHLjXJIylwv5PJQPl3Mpw==
zifferjs@^0.0.39:
version "0.0.39"
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.39.tgz#a3916ca1b38d493edea14bf4f29948f2f6f1572e"
integrity sha512-WMSJ9SPGA/OP/9Z936anUUOM66qzuwPZaE99Qix+Q7jr4fFuoZ/Xw76m2/1C2UVk85sauAecnSfjcK3zo6nA3Q==
zzfx@^1.2.0:
version "1.2.0"