59 Commits

Author SHA1 Message Date
819cca4385 New zifferjs documentation 2023-12-09 23:47:12 +02:00
204a5ae2ab Moved processSound() to AbstractEvent for Ziffers 2023-12-09 04:41:34 +02:00
35c8c1beaa New zifferjs version and fixes for arpeggio 2023-12-09 01:56:40 +02:00
657bde733d Update zifferjs 2023-12-07 01:57:39 +02:00
faed3f8868 Update zifferjs 2023-12-07 01:50:04 +02:00
65fccac799 add more to workbox 2023-12-04 23:07:36 +01:00
750516d2d2 Merge pull request #98 from Bubobubobubobubo/osc
Support for OSC Input/Output
2023-12-04 18:36:42 +01:00
98c71953a4 lint topos 2023-12-04 18:35:36 +01:00
0aa6039f17 corrections 2023-12-04 18:33:59 +01:00
4cdde35835 add more documentation 2023-12-04 18:32:35 +01:00
e68ac4fcac improvements on osc input 2023-12-04 18:23:38 +01:00
cc963ac54f prepare for osc input 2023-12-04 16:28:07 +01:00
04a4f28f68 working OSC output 2023-12-04 15:44:25 +01:00
4c0eb8c043 small typing correction 2023-12-04 15:08:24 +01:00
583b3cb104 typing does nothing at all 2023-12-04 14:38:19 +01:00
d353d6cc1f fixing bad logic 2023-12-04 13:23:31 +01:00
2b609c4dcb temp work 2023-12-04 12:18:34 +01:00
c68a090e02 update readme again 2023-12-03 20:39:25 +01:00
9031f7b87d update readme 2023-12-03 20:38:56 +01:00
1bc7fcd3cb adding links 2023-12-03 20:37:55 +01:00
34c68c2f8a Merge pull request #95 from Bubobubobubobubo/topas
Experimental Workshop Branch
2023-12-02 10:48:13 +01:00
0e63f87271 prepare version 2023-12-02 10:44:12 +01:00
bcb0ddc1cb document clock 2023-12-01 12:30:33 +01:00
e5a331c6cf clean 2023-12-01 12:19:42 +01:00
a34f1a33eb lint 2023-12-01 11:16:16 +01:00
31adc17a36 cleaning clock file a bit 2023-12-01 10:59:50 +01:00
dada6c1614 connecting more stuff before fixing 2023-12-01 10:52:42 +01:00
a905d9b2df connect deadline to output 2023-12-01 10:49:29 +01:00
bb5dd6b348 first boom boom 2023-12-01 10:45:35 +01:00
5b9a59effe WIP: Zyklus callback 2023-12-01 09:26:03 +01:00
2309bcd95c remove nodeprocessor and import zyklus 2023-12-01 09:16:35 +01:00
53821983e9 renaming analyze to scope and documenting 2023-11-30 22:59:22 +01:00
c192988e70 better debug behavior + optional analyze 2023-11-30 22:54:40 +01:00
49f7998425 debug callback 2023-11-30 22:46:12 +01:00
50ace56de8 debug function 2023-11-30 22:31:29 +01:00
077e7acb4a Merge branch 'main' of github.com:Bubobubobubobubo/Topos 2023-11-30 15:01:45 +01:00
ee6dbf9e29 clean audiovisualisation file 2023-11-30 15:01:19 +01:00
278dce0196 Fix chord & note issues 2023-11-28 07:50:03 +02:00
22508acb9f lint 2023-11-26 23:06:49 +01:00
fc47d598ac Adding Juliette sample pack 2023-11-26 21:22:04 +01:00
b935cda91a Correct typing errors 2023-11-26 13:50:23 +01:00
70c20b2d4a Pushing the experimental SoundEvent refactoring 2023-11-26 13:43:46 +01:00
c56d6b1688 Optimizing generics file 2023-11-26 13:35:40 +01:00
d717fc8410 Add code documentation 2023-11-26 13:32:38 +01:00
626a8be77c Hydra looks better by default 2023-11-26 02:24:47 +01:00
eb8ef879e7 Rewrite part of evaluation logic, run prettier 2023-11-26 01:49:34 +01:00
22b52456fc Trying to optimize generics 2023-11-24 03:04:24 +01:00
7119080be2 Merge branch 'main' of github.com:Bubobubobubobubo/Topos 2023-11-24 01:49:29 +01:00
6e05c3927a test: lower canvas size 2023-11-24 01:49:21 +01:00
bbba63365c New zifferjs version 2023-11-24 01:41:45 +02:00
060cddd82c New zifferjs version 2023-11-23 00:13:22 +02:00
80a7bc9dc8 Merge branch 'main' of github.com:Bubobubobubobubo/Topos 2023-11-22 21:16:31 +01:00
dbacc913e2 New special key: Ctrl+M to hide the interface 2023-11-22 21:15:55 +01:00
565fc60113 add port 2023-11-22 20:57:51 +01:00
ea0c7f3165 fleshing out a bit 2023-11-22 15:37:36 +01:00
8195511332 fleshing out a bit 2023-11-22 15:34:21 +01:00
fa67fdc2e5 initial support for osc (buggy) 2023-11-22 12:12:36 +01:00
b9c59ab948 Update zifferjs 2023-11-22 00:55:11 +02:00
f6c86712aa Update zifferjs 2023-11-22 00:22:54 +02:00
84 changed files with 4399 additions and 3795 deletions

View File

@ -3,28 +3,23 @@ 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,2 +1 @@
{
}
{}

View File

@ -1,9 +1,9 @@
# Topos: A Web-Based Algorithmic Sequencer
<p align="center"> |
<a href="https://discord.gg/aPgV7mSFZh">Discord</a> |
<a href="https://raphaelforment.fr/">BuboBubo</a> | 
<a href="about:blank">Amiika</a> |
<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://toplap.org/">About Live Coding</a> |
<br><br>
<h2 align="center"><b>Contributors</b></h2>
@ -12,15 +12,32 @@
<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. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal.
---
Topos is a web based live coding environment. Topos is capable of many things:
- it is a music sequencer made for improvisation and composition alike
- it is a synthesizer capable of additive, substractive, FM and wavetable
synthesis, backed up by a [powerful web based audio engine](https://www.npmjs.com/package/superdough)
- it can also generate video thanks to [Hydra](https://hydra.ojack.xyz/) and
custom oscilloscopes, frequency visualizers and image sequencing capabilities
- it can be used to sequence other MIDI devices (and soon.. OSC!)
- it is made to be used without the need of installing anything, always ready at
[https://topos.live](https://topos.live)
- Topos is also an emulation and personal extension of the [Monome Teletype](https://monome.org/docs/teletype/)
---
![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif)
## Disclaimer
**Topos** is a fairly young project developed by two part time hobbyists :) Do not expect stable features and/or user support in the initial development stage. Contributors and curious people are welcome! The software is working quite well and we are continuously striving to improve it.
**Topos** is still a young project developed by two hobbyists :) Contributions are welcome! We wish to be as inclusive and welcoming as possible to your ideas and suggestions! The software is working quite well and we are continuously striving to improve it.
## Installation (for devs and contributors)
@ -46,15 +63,18 @@ 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 .
@ -62,7 +82,7 @@ docker compose --profile dev down
```
**Then**
```bash
docker compose --profile dev up
```

36
ToposServer/OSCtoTopos.js Normal file
View File

@ -0,0 +1,36 @@
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();

83
ToposServer/ToposToOSC.js Normal file
View File

@ -0,0 +1,83 @@
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;
};

14
ToposServer/banner.js Normal file
View File

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

332
ToposServer/package-lock.json generated Normal file
View File

@ -0,0 +1,332 @@
{
"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
}
}
}
}
}

15
ToposServer/package.json Normal file
View File

@ -0,0 +1,15 @@
{
"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"
}
}

8
ToposServer/server.js Normal file
View File

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

View File

@ -1,9 +1,9 @@
version: '3.7'
version: "3.7"
services:
topos-dev:
container_name: topos-dev
profiles: ["dev"]
build:
build:
context: .
target: "dev"
volumes:
@ -21,8 +21,8 @@ services:
topos-prod:
container_name: topos-prod
profiles: ["prod"]
build:
build:
context: .
target: "prod"
ports:
- "8001:80"
- "8001:80"

View File

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

View File

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

View File

@ -71,7 +71,7 @@
<!-- The header is hidden on smaller devices -->
<header class="py-0 block text-white bg-neutral-900">
<div class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center">
<div id="topbar" class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center">
<a class="flex title-font font-medium items-center text-black mb-0">
<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">
@ -128,7 +128,7 @@
</header>
<div id="documentation" class="hidden">
<div id="documentation-page" class="flex flex-row">
<div id="documentation-page" class="flex flex-row bg-transparent">
<aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-neutral-900 text-white">
<nav class="text-xl sm:text-sm overflow-y-scroll mb-24">
<details class="" open=true>
@ -178,8 +178,9 @@
</div>
</details>
<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_patterns" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Array 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>
<p rel="noopener noreferrer" id="docs_osc" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">OSC</p>
</div>
</details>
<details class="space-y-2" open=true>
@ -191,7 +192,19 @@
<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>
<!-- Ziffers -->
<details class="space-y-2" open=false>
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Ziffers</summary>
<div class="flex flex-col">
<p rel="noopener noreferrer" id="docs_ziffers_basics" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Basics</p>
<p rel="noopener noreferrer" id="docs_ziffers_scales" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Scales</p>
<p rel="noopener noreferrer" id="docs_ziffers_rhythm" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Rhythm</p>
<p rel="noopener noreferrer" id="docs_ziffers_algorithmic" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Algorithmic</p>
<p rel="noopener noreferrer" id="docs_ziffers_tonnetz" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Tonnetz</p>
</div>
</details>
<!--
<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>
-->
@ -209,15 +222,18 @@
<details class="" open=true>
<summary class="font-semibold lg:text-xl text-orange-300">Community</summary>
<form action="https://github.com/Bubobubobubobubo/topos">
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="GitHub" />
<input rel="noopener noreferrer" id="github_link" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="GitHub" />
</form>
<form action="https://discord.gg/6T67DqBNNT">
<input rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Discord" />
<input rel="noopener noreferrer" id="discord_link" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Discord" />
</form>
<form action="https://ko-fi.com/raphaelbubo">
<input rel="noopener noreferrer" id="support_link" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Support" />
</form>
</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">
<div id="documentation-content" class="w-full flex-grow-1 h-screen overflow-y-scroll lg:px-12 mx-2 my-2 break-words pb-32 bg-transparent"></div>
</div>
</div>
</div>
@ -429,7 +445,7 @@
<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 class="
<aside id="sidebar" class="
flex flex-col items-center w-14
h-screen py-2 border-r
rtl:border-l max-h-fit

View File

@ -33,6 +33,7 @@
"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",
@ -42,7 +43,8 @@
"tone": "^14.8.49",
"unique-names-generator": "^4.7.1",
"vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.39",
"zifferjs": "^0.0.47",
"zyklus": "^0.1.4",
"zzfx": "^1.2.0"
}
}

View File

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

View File

@ -1,4 +1,5 @@
import { EditorView } from "@codemirror/view";
import { sendToServer, type OSCMessage, oscMessages } from "./IO/OSC";
import { getAllScaleNotes, nearScales, seededRandom } from "zifferjs";
import {
MidiCCEvent,
@ -27,7 +28,8 @@ import {
} from "superdough";
import { Speaker } from "./extensions/StringExtensions";
import { getScaleNotes } from "zifferjs";
import { OscilloscopeConfig, blinkScript } from "./AudioVisualisation";
import { OscilloscopeConfig } from "./Visuals/Oscilloscope";
import { blinkScript } from "./Visuals/Blinkers";
import { SkipEvent } from "./classes/SkipEvent";
import { AbstractEvent, EventOperation } from "./classes/AbstractEvents";
import drums from "./tidal-drum-machines.json";
@ -41,16 +43,31 @@ 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-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-Juj/main", undefined, {
tag: "Juliette",
}),
samples("github:Bubobubobubobubo/Dough-Amen/main", undefined, {
tag: "Amen",
}),
samples("github:Bubobubobubobubo/Dough-Waveforms/main", undefined, {
tag: "Waveforms",
}),
]);
}
@ -74,6 +91,7 @@ export class UserAPI {
private printTimeoutID: number = 0;
public MidiConnection: MidiConnection;
public scale_aid: string | number | undefined = undefined;
public hydra: any;
load: samples;
constructor(public app: Editor) {
@ -95,7 +113,7 @@ export class UserAPI {
}
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings
this.app.settings,
);
this.app.updateKnownUniversesView();
};
@ -187,7 +205,7 @@ export class UserAPI {
// @ts-ignore
this.errorTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
2000
2000,
);
};
@ -201,7 +219,7 @@ export class UserAPI {
// @ts-ignore
this.printTimeoutID = setTimeout(
() => this.app.interface.error_line.classList.add("hidden"),
4000
4000,
);
};
@ -252,7 +270,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,
);
};
@ -309,7 +327,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],
);
}
});
@ -356,7 +374,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();
};
@ -372,7 +390,7 @@ export class UserAPI {
};
this.app.settings.saveApplicationToLocalStorage(
this.app.universes,
this.app.settings
this.app.settings,
);
}
this.app.selected_universe = "Default";
@ -409,7 +427,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.
@ -484,7 +502,7 @@ export class UserAPI {
};
public active_note_events = (
channel?: number
channel?: number,
): MidiNoteEvent[] | undefined => {
/**
* @returns A list of currently active MIDI notes
@ -621,7 +639,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
@ -645,7 +663,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
@ -660,7 +678,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
@ -670,7 +688,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
@ -697,7 +715,7 @@ export class UserAPI {
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;
@ -774,7 +792,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.
@ -1281,8 +1299,8 @@ export class UserAPI {
const results: boolean[] = nArray.map(
(value) =>
(this.app.clock.pulses_since_origin - Math.floor(nudge * this.ppqn())) %
Math.floor(value * this.ppqn()) ===
0
Math.floor(value * this.ppqn()) ===
0,
);
return results.some((value) => value === true);
};
@ -1301,8 +1319,8 @@ export class UserAPI {
const results: boolean[] = nArray.map(
(value) =>
(this.app.clock.pulses_since_origin - nudgeInPulses) %
Math.floor(value * barLength) ===
0
Math.floor(value * barLength) ===
0,
);
return results.some((value) => value === true);
};
@ -1317,7 +1335,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);
};
@ -1326,7 +1344,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);
};
@ -1375,7 +1393,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"
@ -1403,7 +1421,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);
@ -1485,7 +1503,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.
@ -1504,7 +1522,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)
@ -1514,7 +1532,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 {
@ -1525,7 +1543,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) {
@ -1564,7 +1582,9 @@ export class UserAPI {
// Low Frequency Oscillators
// =============================================================
line = (start: number, end: number, step: number = 1): number[] => {
public range = (v: number, a: number, b: number): number => v * (b - a) + a;
public line = (start: number, end: number, step: number = 1): number[] => {
/**
* Returns an array of values between start and end, with a given step.
*
@ -1586,7 +1606,11 @@ export class UserAPI {
return result;
};
sine = (freq: number = 1, times: number = 1, offset: number = 0): number => {
public sine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
/**
* Returns a sine wave between -1 and 1.
*
@ -1600,7 +1624,11 @@ export class UserAPI {
);
};
usine = (freq: number = 1, times: number = 1, offset: number = 0): number => {
public usine = (
freq: number = 1,
times: number = 1,
offset: number = 0,
): number => {
/**
* Returns a sine wave between 0 and 1.
*
@ -1644,7 +1672,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.
@ -1661,7 +1689,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.
@ -1678,7 +1706,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.
@ -1698,7 +1726,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.
@ -1758,23 +1786,11 @@ 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.
@ -1798,7 +1814,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.
@ -1873,7 +1889,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];
};
@ -1901,10 +1917,13 @@ 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[] => {
/**
@ -2005,7 +2024,7 @@ export class UserAPI {
".cm-comment": {
fontFamily: commentFont,
},
})
}),
),
});
};
@ -2078,22 +2097,35 @@ export class UserAPI {
};
// =============================================================
// Transport functions
// OSC 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 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 tempo = (n?: number): number => {
/**
* Sets or returns the current bpm.

View File

@ -1,6 +1,9 @@
// @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");
@ -19,133 +22,92 @@ 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 - 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 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 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;
private timerWorker: Worker | null = null;
private timeAtStart: number;
_nudge: number;
timeviewer: HTMLElement;
deadline: number;
constructor(public app: Editor, ctx: AudioContext) {
this.timeviewer = document.getElementById("timeviewer")!;
constructor(
public app: Editor,
ctx: AudioContext,
) {
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.timeAtStart = ctx.currentTime;
this.initializeWorker();
this.deadline = 0;
this.timeviewer = document.getElementById("timeviewer")!;
this.clock = getAudioContext().createClock(
this.clockCallback,
this.pulse_duration,
);
}
private initializeWorker(): void {
// @ts-ignore
clockCallback = (time: number, duration: number, tick: number) => {
/**
* 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.
* 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.
*
* @returns void
* @param time - precise AudioContext time when the tick should happen
* @param duration - seconds between each tick
* @param tick - count of the current tick
*/
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();
}
this.time_position = futureTimeStamp;
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
futureTimeStamp.beat + 1
} / ${this.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
let deadline = time - getAudioContext().currentTime;
this.deadline = deadline;
this.tick = tick;
if (this.app.clock.running) {
if (this.app.settings.send_clock) {
this.app.api.MidiConnection.sendMidiClock();
}
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
this.app.clock.tick,
);
this.app.clock.time_position = futureTimeStamp;
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
futureTimeStamp.beat + 1
} / ${this.app.clock.bpm}`;
}
if (this.app.exampleIsPlaying) {
tryEvaluate(this.app, this.app.example_buffer);
} else {
tryEvaluate(this.app, this.app.global_buffer);
}
}
// Implement TransportNode clock callback and update clock info with it
};
convertTicksToTimeposition(ticks: number): TimePosition {
/**
* This function converts a number of ticks to a time position.
* @param ticks - number of ticks
* @returns time position
* Converts ticks to a time position.
*
* @param ticks - ticks to convert
* @returns TimePosition
*/
const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn;
@ -157,10 +119,9 @@ export class Clock {
get ticks_before_new_bar(): number {
/**
* This function returns the number of ticks separating the current moment
* from the beginning of the next bar.
* Calculates the number of ticks before the next bar.
*
* @returns number of ticks until next bar
* @returns number - ticks before the next bar
*/
const ticskMissingFromBeat = this.ppqn - this.time_position.pulse;
const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat;
@ -169,10 +130,9 @@ export class Clock {
get next_beat_in_ticks(): number {
/**
* This function returns the number of ticks separating the current moment
* from the beginning of the next beat.
* Calculates the number of ticks before the next beat.
*
* @returns number of ticks until next beat
* @returns number - ticks before the next beat
*/
return this.app.clock.pulses_since_origin + this.time_position.pulse;
}
@ -181,7 +141,7 @@ export class Clock {
/**
* Returns the number of beats per bar.
*
* @returns number of beats per bar
* @returns number - beats per bar
*/
return this.time_signature[0];
}
@ -190,7 +150,7 @@ export class Clock {
/**
* Returns the number of beats since the origin.
*
* @returns number of beats since origin
* @returns number - beats since the origin
*/
return Math.floor(this.tick / this.ppqn);
}
@ -199,7 +159,7 @@ export class Clock {
/**
* Returns the number of pulses since the origin.
*
* @returns number of pulses since origin
* @returns number - pulses since the origin
*/
return this.tick;
}
@ -207,210 +167,112 @@ export class Clock {
get pulse_duration(): number {
/**
* Returns the duration of a pulse in seconds.
*
* @returns duration of a pulse in seconds
* @returns number - duration of a pulse in seconds
*/
return 60 / this._bpm / this.ppqn;
return 60 / this.bpm / this.ppqn;
}
public pulse_duration_at_bpm(bpm: number = this.bpm): number {
/**
* Returns the duration of a pulse in seconds at a specific bpm.
* Returns the duration of a pulse in seconds at a given bpm.
*
* @param bpm - beats per minute
* @returns duration of a pulse in seconds
* @param bpm - bpm to calculate the pulse duration for
* @returns number - duration of a pulse in seconds
*/
return 60 / bpm / this.ppqn;
}
get bpm(): number {
/**
* Returns the current BPM.
*
* @returns current BPM
* Returns the current bpm.
* @returns number - current bpm
*/
return this._bpm;
}
set nudge(nudge: number) {
get tickDuration(): number {
/**
* Sets the nudge.
*
* @param nudge - nudge in seconds
* @returns void
* Returns the duration of a tick in seconds.
* @returns number - duration of a tick in seconds
*/
this._nudge = nudge;
}
get nudge(): number {
/**
* Returns the current nudge.
*
* @returns current nudge
*/
return this._nudge;
return 1 / this.ppqn;
}
set bpm(bpm: number) {
/**
* Sets the BPM.
*
* @param bpm - beats per minute
* @returns void
* Sets the bpm.
* @param bpm - bpm to set
*/
if (bpm > 0 && this._bpm !== bpm) {
this._bpm = bpm;
// Restart the worker with the new BPM if the clock is running
if (this.running) {
this.restartWorker();
}
this.clock.setDuration(() => (this.tickDuration * 60) / this.bpm);
}
}
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 current PPQN
* Returns the current ppqn.
* @returns number - 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 - pulses per quarter note
* @returns void
* Sets the ppqn.
* @param ppqn - ppqn to set
* @returns number - current ppqn
*/
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 {
/**
* This function starts the worker.
* Start the clock
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
if (this.running) {
return;
}
this.running = true;
this.app.audioContext.resume();
this.running = true;
this.app.api.MidiConnection.sendStartMessage();
if (!this.timerWorker) {
this.initializeWorker();
}
this.setWorkerInterval();
this.timeAtStart = this.ctx.currentTime;
this.logicalTime = this.timeAtStart;
this.clock.start();
}
public pause(): void {
/**
* Pauses the Transport worker.
* Pause the clock.
*
* @remark also sends a MIDI message if a port is declared
* @returns void
*/
this.running = false;
this.app.api.MidiConnection.sendStopMessage();
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
this.clock.pause();
}
public stop(): void {
/**
* 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.
* Stops the clock.
*
* @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();
if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
this.clock.stop();
}
}

View File

@ -24,13 +24,20 @@ 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 { osc } from "./documentation/osc";
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 { 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 { synths } from "./documentation/synths";
// Setting up the Markdown converter with syntax highlighting
@ -47,7 +54,7 @@ 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
@ -70,7 +77,11 @@ export const makeExampleFactory = (application: Editor): Function => {
};
export const documentation_factory = (application: Editor) => {
// Initialize a data structure to store code examples by their unique IDs
/**
* Creates the documentation for the given application.
* @param application The editor application.
* @returns An object containing various documentation sections.
*/
application.api.codeExamples = {};
return {
@ -86,8 +97,13 @@ export const documentation_factory = (application: Editor) => {
synths: synths(application),
chaining: chaining(application),
patterns: patterns(application),
ziffers: ziffers(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),
midi: midi(application),
osc: osc(application),
lfos: lfos(application),
variables: variables(application),
probabilities: probabilities(application),
@ -109,6 +125,10 @@ 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");
@ -129,6 +149,9 @@ export const showDocumentation = (app: Editor) => {
};
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");
@ -136,6 +159,12 @@ 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.
*/
const converter = new showdown.Converter({
emoji: true,
moreStyling: true,
@ -143,7 +172,7 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => {
extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
});
const converted_markdown = converter.makeHtml(
app.docs[app.currentDocumentationPane]
app.docs[app.currentDocumentationPane],
);
document.getElementById("documentation-content")!.innerHTML =
converted_markdown;

View File

@ -1,17 +1,15 @@
import { type Editor } from "./main";
export type ElementMap = {
[key: string]:
| HTMLElement
| HTMLButtonElement
| HTMLDivElement
| HTMLInputElement
| HTMLSelectElement
| HTMLCanvasElement
| HTMLFormElement
| HTMLInputElement
;
| HTMLElement
| HTMLButtonElement
| HTMLDivElement
| HTMLInputElement
| HTMLSelectElement
| HTMLCanvasElement
| HTMLFormElement
| HTMLInputElement;
};
export const singleElements = {
@ -65,6 +63,12 @@ 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-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",
@ -92,6 +96,4 @@ export const createDocumentationStyle = (app: Editor) => {
tr: "",
box: "border bg-red-500",
};
}
};

View File

@ -20,7 +20,7 @@ import {
bracketMatching,
} 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,
@ -34,15 +34,15 @@ 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"
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(),
@ -95,7 +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,
toposTheme,
app.chosenLanguage.of(javascript()),
@ -114,7 +116,7 @@ export const installEditor = (app: Editor) => {
return true;
},
},
])
]),
),
keymap.of([indentWithTab]),
],
@ -139,7 +141,7 @@ export const installEditor = (app: Editor) => {
".cm-gutters": {
fontSize: `${app.settings.font_size}px`,
},
})
}),
),
});
};

View File

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

@ -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,7 +263,9 @@ 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 = () => {
@ -271,6 +273,11 @@ 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();
@ -321,8 +328,20 @@ 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:
@ -334,7 +353,9 @@ 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
@ -342,7 +363,11 @@ export const loadUniverse = (
};
export const openUniverseModal = (): void => {
// If the modal is hidden, unhide it and hide the editor
/**
* Opens the universe modal.
* If the modal is hidden, it unhides it and hides the editor.
* If the modal is already visible, it closes the modal.
*/
if (
document.getElementById("modal-buffers")!.classList.contains("invisible")
) {
@ -355,6 +380,9 @@ 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");
@ -362,6 +390,9 @@ export const closeUniverseModal = (): void => {
};
export const openSettingsModal = (): void => {
/**
* Opens the settings modal.
*/
if (
document.getElementById("modal-settings")!.classList.contains("invisible")
) {
@ -373,6 +404,9 @@ 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,8 +578,9 @@ 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 {
@ -607,8 +608,9 @@ 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 {
@ -642,7 +644,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
@ -668,11 +670,14 @@ export class MidiConnection {
if (bend) this.sendPitchBend(bend, channel, port);
// Schedule Note Off
const timeoutId = setTimeout(() => {
output.send(noteOffMessage);
if (bend) this.sendPitchBend(8192, channel, port);
delete this.scheduledNotes[noteNumber];
}, (duration - 0.02) * 1000);
const timeoutId = setTimeout(
() => {
output.send(noteOffMessage);
if (bend) this.sendPitchBend(8192, channel, port);
delete this.scheduledNotes[noteNumber];
},
(duration - 0.02) * 1000,
);
// @ts-ignore
this.scheduledNotes[noteNumber] = timeoutId;
@ -685,7 +690,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
@ -704,7 +709,7 @@ export class MidiConnection {
sendMidiOff(
note: number,
channel: number,
port: number | string = this.currentOutputIndex
port: number | string = this.currentOutputIndex,
) {
/**
* Sending Midi Note off message
@ -722,7 +727,7 @@ export class MidiConnection {
sendAllNotesOff(
channel: number,
port: number | string = this.currentOutputIndex
port: number | string = this.currentOutputIndex,
) {
/**
* Sending Midi Note off message
@ -739,7 +744,7 @@ export class MidiConnection {
sendAllSoundOff(
channel: number,
port: number | string = this.currentOutputIndex
port: number | string = this.currentOutputIndex,
) {
/**
* Sending all sound off
@ -775,7 +780,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.
@ -786,7 +791,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) {
@ -825,7 +830,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.

62
src/IO/OSC.ts Normal file
View File

@ -0,0 +1,62 @@
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

@ -118,32 +118,41 @@ 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", () => {
app.clock.nudge = parseInt(
(app.interface.audio_nudge_range as HTMLInputElement).value
);
// TODO: rebuild this
// 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,
);
});
@ -227,16 +236,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", () => {
@ -255,7 +264,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
})
}),
),
});
});
@ -275,7 +284,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
})
}),
),
});
});
@ -283,7 +292,7 @@ export const installInterfaceLogic = (app: Editor) => {
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;
@ -294,7 +303,7 @@ export const installInterfaceLogic = (app: Editor) => {
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) {
@ -350,7 +359,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px",
},
".cm-gutters": { fontSize: app.settings.font_size + "px" },
})
}),
),
});
});
@ -408,7 +417,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.tips = checked;
app.view.dispatch({
effects: app.hoveringCompartment.reconfigure(
checked ? inlineHoveringTips : []
checked ? inlineHoveringTips : [],
),
});
});
@ -421,7 +430,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.completions = checked;
app.view.dispatch({
effects: app.completionsCompartment.reconfigure(
checked ? jsCompletions : []
checked ? jsCompletions : [],
),
});
});
@ -444,7 +453,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;
});
@ -490,12 +499,16 @@ export const installInterfaceLogic = (app: Editor) => {
"linear",
"cyclic",
"longform",
// "sound",
"synths",
"chaining",
"patterns",
"ziffers",
"ziffers_basics",
"ziffers_scales",
"ziffers_rhythm",
"ziffers_algorithmic",
"ziffers_tonnetz",
"midi",
"osc",
"functions",
"lfos",
"probabilities",
@ -511,7 +524,7 @@ export const installInterfaceLogic = (app: Editor) => {
].forEach((e) => {
let name = `docs_` + e;
document.getElementById(name)!.addEventListener("click", async () => {
if (name !== "docs_samples") {
if (name !== "docs_sample_list") {
app.currentDocumentationPane = e;
updateDocumentationContent(app, bindings);
} else {

View File

@ -26,6 +26,31 @@ 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);

View File

@ -1,66 +1,79 @@
/*
* Transforms object with arrays into array of objects
*
* @param {Record<string, any>} input - Object with arrays
* @param {string[]} ignoredKeys - Keys to ignore
* @returns {Record<string, any>[]} Array of objects
*
*/
export function objectWithArraysToArrayOfObjects(input: Record<string, any>, arraysToArrays: string[]): Record<string, any>[] {
arraysToArrays.forEach((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 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 < maxLength; i++) {
const event: Record<string, any> = {};
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] = input[k];
}
}
output.push(event);
export function objectWithArraysToArrayOfObjects(
input: Record<string, any>,
arraysToArrays: string[],
): Record<string, any>[] {
/*
* Transforms object with arrays into array of objects
*
* @param {Record<string, any>} input - Object with arrays
* @param {string[]} ignoredKeys - Keys to ignore
* @returns {Record<string, any>[]} Array of objects
*
*/
const inputCopy = { ...input };
arraysToArrays.forEach((k) => {
if (Array.isArray(inputCopy[k]) && !Array.isArray(inputCopy[k][0])) {
inputCopy[k] = [inputCopy[k]];
}
return output;
};
});
/*
* Transforms array of objects into object with arrays
*
* @param {Record<string, any>[]} array - Array of objects
* @param {Record<string, any>} mergeObject - Object that is merged to each object in the array
* @returns {object} Merged object with arrays
*
*/
export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(array: T[], mergeObject: Record<string, any> = {}): Record<string, any> {
return array.reduce((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] = [];
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 output: Record<string, any>[] = [];
for (let i = 0; i < keysAndLengths.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];
} else {
event[k] = inputCopy[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
*
* @param {Record<string, any>[]} array - Array of objects
* @param {Record<string, any>} mergeObject - Object that is merged to each object in the array
* @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] = [];
}
(acc[key as keyof T] as unknown[]).push(obj[key]);
acc[key].push(mergedObj[key]);
});
return acc;
}, {} as Record<keyof T, any[]>);
}
},
{} as Record<string, any>,
);
}
/*
export function filterObject(
obj: Record<string, any>,
filter: string[],
): Record<string, any> {
/*
* Filter certain keys from object
*
* @param {Record<string, any>} obj - Object to filter
@ -68,6 +81,10 @@ export function arrayOfObjectsToObjectWithArrays<T extends Record<string, any>>(
* @returns {object} Filtered object
*
*/
export function filterObject(obj: Record<string, any>, filter: string[]): Record<string, any> {
return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key)));
}
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;

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

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

View File

@ -1,122 +1,6 @@
// @ts-ignore
import { getAnalyser } from "superdough";
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);
};
import { Editor } from "../main";
export interface OscilloscopeConfig {
enabled: boolean;
@ -134,15 +18,16 @@ 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);
@ -155,7 +40,7 @@ export const runOscilloscope = (
width: number,
height: number,
offset_height: number,
offset_width: number
offset_width: number,
) {
const maxFPS = 30;
const now = performance.now();
@ -169,10 +54,12 @@ 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;
@ -184,7 +71,8 @@ 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") {
@ -192,7 +80,7 @@ export const runOscilloscope = (
x + offset_width,
(height - barHeight) / 2 + offset_height,
barWidth + 1,
barHeight
barHeight,
);
x += barWidth;
} else {
@ -200,14 +88,13 @@ 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;
@ -230,12 +117,19 @@ 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);
}
@ -250,7 +144,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,4 +1,5 @@
import { type Editor } from "./main";
import { outputSocket, inputSocket } from "./IO/OSC";
const handleResize = (canvas: HTMLCanvasElement) => {
if (!canvas) return;
@ -26,19 +27,22 @@ 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();
@ -61,11 +65,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,14 +1,18 @@
import { type Editor } from "../main";
import {
freqToMidi,
chord as parseChord,
noteNameToMidi,
resolvePitchBend,
safeScale
safeScale,
} from "zifferjs";
import { SkipEvent } from "./SkipEvent";
import { SoundParams } from "./SoundEvent";
export type EventOperation<T> = (instance: T, ...args: any[]) => void;
export interface AbstractEvent {
[key: string]: any
[key: string]: any;
}
export class AbstractEvent {
@ -208,22 +212,71 @@ export class AbstractEvent {
return this.modify(func);
};
noteLength = (value: number | number[], ...kwargs: number[]): AbstractEvent => {
noteLength = (
value: number | number[],
...kwargs: number[]
): AbstractEvent => {
/**
* This function is used to set the note length of the Event.
*/
if(kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
if (kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
}
if(Array.isArray(value)) {
this.values["noteLength"] = value;
this.values.dur = value.map((v) => this.app.clock.convertPulseToSecond(v*4*this.app.clock.ppqn));
if (Array.isArray(value)) {
this.values.dur = value.map((v) =>
this.app.clock.convertPulseToSecond(v * 4 * this.app.clock.ppqn),
);
} else {
this.values["noteLength"] = value;
this.values.dur = this.app.clock.convertPulseToSecond(value*4*this.app.clock.ppqn);
this.values.dur = this.app.clock.convertPulseToSecond(
value * 4 * this.app.clock.ppqn,
);
}
if(this.current) {
value = Array.isArray(value) ? value[this.index%value.length] : value;
this.current.duration = value;
}
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 {
@ -232,83 +285,138 @@ export abstract class AudibleEvent extends AbstractEvent {
}
pitch = (value: number | number[], ...kwargs: number[]): this => {
/*
* This function is used to set the pitch of the Event.
* @param value - The pitch value
* @returns The Event
*/
if(kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
/*
* This function is used to set the pitch of the Event.
* @param value - The pitch value
* @returns The Event
*/
if (kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
}
this.values["pitch"] = value;
if(this.values.key && this.values.parsedScale) this.update();
if (this.values.key && this.values.parsedScale) this.update();
return this;
}
};
pc = this.pitch;
octave = (value: number | number[], ...kwargs: number[]): this => {
/*
* This function is used to set the octave of the Event.
* @param value - The octave value
* @returns The Event
*/
if(kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
* This function is used to set the octave of the Event.
* @param value - The octave value
* @returns The Event
*/
if (kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
}
this.values["octave"] = value;
if(this.values.key && (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update();
if (
this.values.key &&
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
)
this.update();
return this;
};
key = (value: string | string[], ...kwargs: string[]): this => {
/*
* This function is used to set the key of the Event.
* @param value - The key value
* @returns The Event
*/
if(kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
/*
* This function is used to set the key of the Event.
* @param value - The key value
* @returns The Event
*/
if (kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
}
this.values["key"] = value;
if((this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update();
if (
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
)
this.update();
return this;
};
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]);
* 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];
}
if (typeof value === "string" || typeof value === "number") {
this.values.parsedScale = safeScale(value) as number[];
} else if(Array.isArray(value)) {
} else if (Array.isArray(value)) {
this.values.parsedScale = value.map((v) => safeScale(v));
}
if(this.values.key && (this.values.pitch || this.values.pitch === 0)) {
this.update();
if (this.values.key && (this.values.pitch || this.values.pitch === 0)) {
this.update();
}
return this;
};
protected updateValue<T>(key: string, value: T | T[] | null): this {
if (value == null) return this;
this.values[key] = value;
return this;
}
public note = (
value: number | string | null,
...kwargs: number[] | string[]
) => {
if (typeof value === "string") {
const parsedNote = noteNameToMidi(value);
return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity));
} else if (typeof value == null || value == undefined) {
return new SkipEvent();
} else {
return this.updateValue("note", [value, ...kwargs].flat(Infinity));
}
};
public chord = (value: number | string, ...kwargs: number[]) => {
if (typeof value === "string") {
const chord = parseChord(value);
return this.updateValue("note", chord);
} else {
const chord = [value, ...kwargs].flat(Infinity);
return this.updateValue("note", chord);
}
};
public invert = (howMany: number = 0) => {
if (this.values.note) {
let notes = [...this.values.note];
notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12;
}
return this.updateValue("note", notes);
} else {
return this;
}
};
freq = (value: number | number[], ...kwargs: number[]): this => {
/*
* This function is used to set the frequency of the Event.
* @param value - The frequency value
* @returns The Event
*/
if(kwargs.length > 0) {
value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
* This function is used to set the frequency of the Event.
* @param value - The frequency value
* @returns The Event
*/
if (kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
}
this.values["freq"] = value;
if(Array.isArray(value)) {
if (Array.isArray(value)) {
this.values["note"] = [];
this.values["bend"] = [];
for(const v of value) {
for (const v of value) {
const midiNote = freqToMidi(v);
if (midiNote % 1 !== 0) {
this.values["note"].push(Math.floor(midiNote));
@ -317,7 +425,7 @@ export abstract class AudibleEvent extends AbstractEvent {
this.values["note"].push(midiNote);
}
}
if(this.values.bend.length === 0) delete this.values.bend;
if (this.values.bend.length === 0) delete this.values.bend;
} else {
const midiNote = freqToMidi(value);
if (midiNote % 1 !== 0) {

View File

@ -1,8 +1,12 @@
import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection";
import { noteFromPc, chord as parseChord } from "zifferjs";
import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic";
import { noteFromPc } from "zifferjs";
import {
filterObject,
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
} from "../Utils/Generic";
export type MidiParams = {
note: number;
@ -11,27 +15,20 @@ 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;
@ -40,7 +37,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,10 +45,12 @@ export class MidiEvent extends AudibleEvent {
};
port = (value: number | string | number[] | string[]): this => {
if(typeof value === "string"){
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);
} else if (Array.isArray(value)) {
this.values["port"] = value.map((v) =>
typeof v === "string" ? this.midiConnection.getMidiOutputIndex(v) : v,
);
}
return this;
};
@ -86,25 +85,32 @@ export class MidiEvent extends AudibleEvent {
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 filteredValues = filterObject(this.values, [
"key",
"pitch",
"parsedScale",
"octave",
]);
const events = objectWithArraysToArrayOfObjects(filteredValues, [
"parsedScale",
]);
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
(event.key as number) || "C4",
(event.pitch as number) || 0,
(event.parsedScale as number[]) || event.scale || "MAJOR",
(event.octave as number) || 0,
);
event.note = note;
if(bend) event.bend = bend;
if (bend) event.bend = bend;
});
const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams;
this.values.note = newArrays.note;
if(newArrays.bend) this.values.bend = newArrays.bend;
if (newArrays.bend) this.values.bend = newArrays.bend;
};
out = (): void => {
@ -114,9 +120,7 @@ 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;
@ -124,22 +128,23 @@ export class MidiEvent extends AudibleEvent {
const port = params.port
? event.midiConnection.getMidiOutputIndex(params.port)
: event.midiConnection.getCurrentMidiPortIndex() || 0;
event.midiConnection.sendMidiNote(
note,
channel,
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);
play(this, p);
});
};
}

View File

@ -11,10 +11,7 @@ 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,21 +1,18 @@
import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents";
import { sendToServer, type OSCMessage } from "../IO/OSC";
import {
filterObject,
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
} from "../Utils/Generic";
import {
chord as parseChord,
midiToFreq,
noteFromPc,
noteNameToMidi,
} from "zifferjs";
import { midiToFreq, noteFromPc } from "zifferjs";
import {
superdough,
// @ts-ignore
} from "superdough";
// import { Sound } from "zifferjs/src/types";
export type SoundParams = {
dur: number | number[];
@ -35,7 +32,7 @@ export class SoundEvent extends AudibleEvent {
nudge: number;
sound: any;
private methodMap = {
private static methodMap = {
volume: ["volume", "vol"],
zrand: ["zrand", "zr"],
curve: ["curve"],
@ -46,6 +43,8 @@ export class SoundEvent extends AudibleEvent {
pitchJumpTime: ["pitchJumpTime", "pjt"],
lfo: ["lfo"],
znoise: ["znoise"],
address: ["address", "add"],
port: ["port"],
noise: ["noise"],
zmod: ["zmod"],
zcrush: ["zcrush"],
@ -67,17 +66,23 @@ export class SoundEvent extends AudibleEvent {
phaserDepth: ["phaserDepth", "phasdepth"],
phaserSweep: ["phaserSweep", "phassweep"],
phaserCenter: ["phaserCenter", "phascenter"],
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;
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;
},
fmad: (a: number, d: number) => {
this.updateValue("fmattack", a);
this.updateValue("fmdecay", d);
return this;
fmad: function (self: SoundEvent, a: number, d: number) {
self.updateValue("fmattack", a);
self.updateValue("fmdecay", d);
return self;
},
ftype: ["ftype"],
fanchor: ["fanchor"],
@ -85,147 +90,185 @@ export class SoundEvent extends AudibleEvent {
decay: ["decay", "dec"],
sustain: ["sustain", "sus"],
release: ["release", "rel"],
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;
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;
},
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;
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;
},
lpenv: ["lpenv", "lpe"],
lpattack: ["lpattack", "lpa"],
lpdecay: ["lpdecay", "lpd"],
lpsustain: ["lpsustain", "lps"],
lprelease: ["lprelease", "lpr"],
cutoff: (value: number, resonance?: number) => {
this.updateValue("cutoff", value);
cutoff: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("cutoff", value);
if (resonance) {
this.updateValue("resonance", resonance);
self.updateValue("resonance", resonance);
}
return this;
return self;
},
lpf: (value: number, resonance?: number) => {
this.updateValue("cutoff", value);
lpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("cutoff", value);
if (resonance) {
this.updateValue("resonance", resonance);
self.updateValue("resonance", resonance);
}
return this;
return self;
},
resonance: (value: number) => {
resonance: function (self: SoundEvent, value: number) {
if (value >= 0 && value <= 1) {
this.updateValue("resonance", 50 * value);
self.updateValue("resonance", 50 * value);
}
return this;
return self;
},
lpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
this.updateValue("lpenv", depth);
this.updateValue("lpattack", a);
this.updateValue("lpdecay", d);
this.updateValue("lpsustain", s);
this.updateValue("lprelease", r);
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;
},
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;
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;
},
hpenv: ["hpenv", "hpe"],
hpattack: ["hpattack", "hpa"],
hpdecay: ["hpdecay", "hpd"],
hpsustain: ["hpsustain", "hpsus"],
hprelease: ["hprelease", "hpr"],
hcutoff: (value: number, resonance?: number) => {
this.updateValue("hcutoff", value);
hcutoff: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("hcutoff", value);
if (resonance) {
this.updateValue("hresonance", resonance);
self.updateValue("hresonance", resonance);
}
return this;
return self;
},
hpf: (value: number, resonance?: number) => {
this.updateValue("hcutoff", value);
hpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("hcutoff", value);
if (resonance) {
this.updateValue("hresonance", resonance);
self.updateValue("hresonance", resonance);
}
return this;
return self;
},
hpq: (value: number) => {
this.updateValue("hresonance", value);
return this;
hpq: function (self: SoundEvent, value: number) {
self.updateValue("hresonance", value);
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;
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;
},
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;
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;
},
bpenv: ["bpenv", "bpe"],
bpattack: ["bpattack", "bpa"],
bpdecay: ["bpdecay", "bpd"],
bpsustain: ["bpsustain", "bps"],
bprelease: ["bprelease", "bpr"],
bandf: (value: number, resonance?: number) => {
this.updateValue("bandf", value);
bandf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("bandf", value);
if (resonance) {
this.updateValue("bandq", resonance);
self.updateValue("bandq", resonance);
}
return this;
return self;
},
bpf: (value: number, resonance?: number) => {
this.updateValue("bandf", value);
bpf: function (self: SoundEvent, value: number, resonance?: number) {
self.updateValue("bandf", value);
if (resonance) {
this.updateValue("bandq", resonance);
self.updateValue("bandq", resonance);
}
return this;
return self;
},
bandq: ["bandq", "bpq"],
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;
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;
},
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;
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;
},
vib: ["vib"],
vibmod: ["vibmod"],
fm: (value: number | string) => {
fm: function (self: SoundEvent, value: number | string) {
if (typeof value === "number") {
this.values["fmi"] = value;
self.values["fmi"] = value;
} else {
let values = value.split(":");
this.values["fmi"] = parseFloat(values[0]);
if (values.length > 1) this.values["fmh"] = parseFloat(values[1]);
self.values["fmi"] = parseFloat(values[0]);
if (values.length > 1) self.values["fmh"] = parseFloat(values[1]);
}
return this;
return self;
},
loop: ["loop"],
loopBegin: ["loopBegin", "loopb"],
@ -233,13 +276,13 @@ export class SoundEvent extends AudibleEvent {
begin: ["begin"],
end: ["end"],
gain: ["gain"],
dbgain: (value: number) => {
this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return this;
dbgain: 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;
db: function (self: SoundEvent, value: number) {
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return self;
},
velocity: ["velocity", "vel"],
pan: ["pan"],
@ -260,109 +303,72 @@ export class SoundEvent extends AudibleEvent {
roomlp: ["roomlp", "rlp"],
roomdim: ["roomdim", "rdim"],
sound: ["s", "sound"],
size: (value: number) => {
this.updateValue("roomsize", value);
return this;
size: function (self: SoundEvent, value: number) {
self.updateValue("roomsize", value);
return self;
},
sz: (value: number) => {
this.updateValue("roomsize", value);
return this;
sz: function (self: SoundEvent, value: number) {
self.updateValue("roomsize", value);
return self;
},
comp: ["compressor", "cmp"],
ratio: (value: number) => {
this.updateValue("compressorRatio", value);
return this;
ratio: function (self: SoundEvent, value: number) {
self.updateValue("compressorRatio", value);
return self;
},
knee: (value: number) => {
this.updateValue("compressorKnee", value);
return this;
knee: function (self: SoundEvent, value: number) {
self.updateValue("compressorKnee", value);
return self;
},
compAttack: (value: number) => {
this.updateValue("compressorAttack", value);
return this;
compAttack: function (self: SoundEvent, value: number) {
self.updateValue("compressorAttack", value);
return self;
},
compRelease: (value: number) => {
this.updateValue("compressorRelease", value);
return this;
compRelease: function (self: SoundEvent, value: number) {
self.updateValue("compressorRelease", value);
return self;
},
stretch: (beat: number) => {
this.updateValue("unit", "c");
this.updateValue("speed", 1 / beat);
this.updateValue("cut", beat);
return this;
stretch: function (self: SoundEvent, beat: number) {
self.updateValue("unit", "c");
self.updateValue("speed", 1 / beat);
self.updateValue("cut", beat);
return self;
},
};
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(this.methodMap)) {
if (Symbol.iterator in Object(keys)) {
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
// Using arrow function to maintain 'this' context
this[key] = (value: number) => this.updateValue(keys[0], value);
}
} else {
// @ts-ignore
this[methodName] = keys;
this[methodName] = (...args) => keys(this, ...args);
}
}
// for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) {
// if (typeof keys === "object" && Symbol.iterator in Object(keys)) {
// for (const key of keys as string[]) {
// // @ts-ignore
// this[key] = (value: number) => this.updateValue(this, keys[0], value);
// }
// } else {
// // @ts-ignore
// this[methodName] = keys;
// }
// }
this.values = this.processSound(sound);
}
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;
}
// ================================================================================
// AbstactEvent overrides
// ================================================================================
@ -392,7 +398,7 @@ export class SoundEvent extends AudibleEvent {
(event.key as number) || "C4",
(event.pitch as number) || 0,
(event.parsedScale as number[]) || event.scale || "MAJOR",
(event.octave as number) || 0
(event.octave as number) || 0,
);
event.note = note;
event.freq = midiToFreq(note);
@ -404,53 +410,47 @@ export class SoundEvent extends AudibleEvent {
this.values.freq = newArrays.freq;
};
public chord = (value: string) => {
const chord = parseChord(value);
return this.updateValue("note", chord);
};
public invert = (howMany: number = 0) => {
if (this.values.chord) {
let notes = this.values.chord.map(
(obj: { [key: string]: number }) => obj.note
);
notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12;
}
const chord = notes.map((note: number) => {
return { note: note, freq: midiToFreq(note) };
});
return this.updateValue("chord", chord);
} else {
return this;
}
};
public note = (value: number | string | null) => {
if (typeof value === "string") {
return this.updateValue("note", noteNameToMidi(value));
} else if (typeof value == null || value == undefined) {
return this.updateValue("note", 0).updateValue("gain", 0);
} else {
return this.updateValue("note", value);
}
};
out = (orbit?: number | number[]): void => {
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; }
// const correction = Math.max(this.nudge - this.app.clock.deviation, 0);
superdough(filteredEvent, this.nudge, filteredEvent.dur);
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);
}
};
}

View File

@ -5,13 +5,13 @@ import { SkipEvent } from "./SkipEvent";
import { SoundEvent, SoundParams } from "./SoundEvent";
import { MidiEvent, MidiParams } from "./MidiEvent";
import { RestEvent } from "./RestEvent";
import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic";
import { GeneratorIteratorType, GeneratorType, arrayOfObjectsToObjectWithArrays } from "../Utils/Generic";
import { TonnetzSpaces } from "zifferjs/src/tonnetz";
export type InputOptions = { [key: string]: string | number };
export class Player extends AbstractEvent {
input: string|number;
input: string | number;
ziffers: Ziffers;
initCallTime: number = 0;
startCallTime: number = 0;
@ -26,10 +26,10 @@ export class Player extends AbstractEvent {
skipIndex = 0;
constructor(
input: string|number|Generator<number>,
input: string | number | Generator<number>,
options: InputOptions,
public app: Editor,
zid: string = ""
zid: string = "",
) {
super(app);
this.options = options;
@ -38,10 +38,12 @@ export class Player extends AbstractEvent {
this.ziffers = new Ziffers(input, options);
} else if (typeof input === "number") {
this.input = input;
this.ziffers = Ziffers.fromNumber(input,options);
} else {
this.ziffers = Ziffers.fromGenerator(input,options);
this.ziffers = Ziffers.fromNumber(input, options);
} else if (input.constructor === GeneratorType || input.constructor === GeneratorIteratorType){
this.ziffers = Ziffers.fromGenerator(input, options);
this.input = this.ziffers.input;
} else {
throw new Error("Invalid input");
}
this.zid = zid;
}
@ -155,26 +157,30 @@ export class Player extends AbstractEvent {
return areWeThereYet;
};
sound(name?: string) {
sound(name?: string | string[] | SoundParams | SoundParams[]) {
if (this.areWeThereYet()) {
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) {
const obj = event.getExisting(
let obj = event.getExisting(
"freq",
"note",
"pitch",
"key",
"scale",
"octave",
"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).sound(name || "sine");
return new SoundEvent(obj, this.app);
} else if (event instanceof Chord) {
const pitches = event.pitches.map((p) => {
return p.getExisting(
@ -184,14 +190,17 @@ export class Player extends AbstractEvent {
"key",
"scale",
"octave",
"parsedScale"
"parsedScale",
);
}) as SoundParams[];
const add = { dur: noteLengthInSeconds } as SoundParams;
if (name) add.s = name;
let add = { dur: noteLengthInSeconds} as SoundParams;
if(name) add = {...add, ...this.processSound(name)};
else add.s = "sine";
let sound = arrayOfObjectsToObjectWithArrays(
pitches,
add
add,
) as SoundParams;
return new SoundEvent(sound, this.app);
} else if (event instanceof ZRest) {
@ -212,7 +221,7 @@ export class Player extends AbstractEvent {
"key",
"scale",
"octave",
"parsedScale"
"parsedScale",
) as MidiParams;
if (event instanceof Pitch) {
if (event.soundIndex) obj.channel = event.soundIndex as number;
@ -245,6 +254,12 @@ 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;
@ -282,6 +297,12 @@ 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);

View File

@ -17,11 +17,11 @@ Controlling the volume is probably the most important concept you need to know a
| <ic>dbgain</ic> | db | Attenuation in dB from <ic>-inf</ic> to <ic>+10</ic> (acts as a sound mixer fader).|
${makeExample(
"Velocity manipulated by a counter",
`
"Velocity manipulated by a counter",
`
beat(.5)::snd('cp').vel($(1)%10 / 10).out()`,
true
)}
true,
)}
## Amplitude Enveloppe
@ -37,8 +37,8 @@ beat(.5)::snd('cp').vel($(1)%10 / 10).out()`,
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",
`
"Simple synthesizer",
`
let smooth = (sound) => {
return sound.cutoff(r(100,500))
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
@ -50,15 +50,15 @@ beat(.25)::smooth(sound('sawtooth')
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:
${makeExample(
"Replacing .adsr by .ad",
`
"Replacing .adsr by .ad",
`
let smooth = (sound) => {
return sound.cutoff(r(100,500))
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
@ -70,9 +70,8 @@ beat(.25)::smooth(sound('sawtooth')
beat(.25)::smooth(sound('sawtooth')
.note([50,57,55,60].add(12).beat(1.5))).out();
`,
true
)};
`}
true,
)};
`;
};

View File

@ -17,13 +17,13 @@ Use the <ic>sound(name: string)</ic> function to play a sound. You can also writ
Whatever you choose, the syntax stays the same. See the following example:
${makeExample(
"Playing sounds is easy",
`
"Playing sounds is easy",
`
beat(1) && sound('bd').out()
beat(0.5) && sound('hh').out()
`,
true
)}
true,
)}
These commands, in plain english, can be translated to:
@ -33,13 +33,13 @@ These commands, in plain english, can be translated to:
Let's make this example a bit more complex:
${makeExample(
"Adding some effects",
`
"Adding some effects",
`
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,12 +53,14 @@ 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!
@ -67,16 +69,16 @@ 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))
.freq([50,52].beat())
.size(0.9).room(0.9)
.velocity(0.25)
.pan(usine()).release(2).out()`,
true
)}
true,
)}
## Picking a specific sound
@ -100,12 +102,12 @@ If you choose the sound <ic>kick</ic>, you are asking for the first sample in th
The <ic>.n(number)</ic> method can be used to pick a sample from the currently selected sample folder. For instance, the following script will play a random sample from the _kick_ folder:
${makeExample(
"Picking a sample",
`
"Picking a sample",
`
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:
@ -114,18 +116,18 @@ 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:
${makeExample(
"Picking a sample... with the mouse!",
`
"Picking a sample... with the mouse!",
`
// Move your mouse to change the sample being used!
beat(.25) && sound('ST09').n(Math.floor(mouseX())).out()`,
true
)}
true,
)}
The <ic>.n</ic> method is also used for synthesizers but it behaves differently. When using a synthesizer, this method can help you determine the number of harmonics in your waveform. See the **Synthesizers** section to learn more about this.
@ -147,31 +149,34 @@ 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
Learning to create complex chains is very important when using **Topos**. It can take some time to learn all the possible parameters. Don't worry, it's actually rather easy to learn.
${makeExample(
"Complex chain",
`
"Complex chain",
`
beat(0.25) && sound('fhh')
.sometimes(s=>s.speed([2, 0.5].pick()))
.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

@ -18,14 +18,13 @@ Three additional effects that are easy enough to understand. These effects are d
${makeExample(
"Crunch... crunch... crunch!",
`
"Crunch... crunch... crunch!",
`
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

@ -24,12 +24,12 @@ For that reason, it is often a good idea to set fixed reverb values per orbit. D
| <ic>roomdim</ic> | | Reverb lowpass frequency at -60db (in hertz) |
${makeExample(
"Clapping in the cavern",
`
"Clapping in the cavern",
`
beat(2)::snd('cp').room(0.5).size(4).out()
`,
true
)};
true,
)};
## Delay
@ -42,10 +42,13 @@ A good sounding delay unit that can go into feedback territory. Use it without m
| <ic>delayfeedback</ic> | delayfb | Delay feedback (between <ic>0</ic> and <ic>1</ic>) |
${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
@ -56,13 +59,17 @@ beat(1)::snd('kick').out()`, true)}
| <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
@ -76,11 +83,12 @@ Three additional effects that are easy enough to understand. These effects are d
${makeExample(
"Crunch... crunch... crunch!",
`
"Crunch... crunch... crunch!",
`
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

@ -28,8 +28,8 @@ The sampler is a rather complex beast. There is a lot you can do by manipulating
Let's apply some of these methods naïvely. We will then break everything using simpler examples.
${makeExample(
"Complex sampling duties",
`
"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))
@ -38,45 +38,61 @@ beat(.5)::snd('pad').begin(0.2)
.room(0.8).size(0.5)
.clip(1).out()
`,
true
)};
true,
)};
## Playback speed / pitching samples
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
@ -84,26 +100,30 @@ 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
The <ic>stretch</ic> parameter can help you to stretch long samples like amen breaks:
${makeExample(
"Playing an amen break",
`
"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,
)};
true,
)};
## Cutting samples
@ -111,34 +131,45 @@ Sometimes, you will find it necessary to cut a sample. It can be because the sam
Know about the <ic>begin</ic> and <ic>end</ic> parameters. They are not related to the sampler itself, but to the length of the event you are playing. Let's cut the granular example:
${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!
@ -31,8 +31,8 @@ The code you enter in any of the scripts is evaluated in strict mode. This tells
There are some techniques to keep code short and tidy. Don't try to write the shortest possible code! Use shortcuts when it makes sense. Take a look at the following examples:
${makeExample(
"Shortening your if conditions",
`
"Shortening your if conditions",
`
// The && symbol (overriden by :: in Topos) is very often used for conditions!
beat(.75) :: snd('linnhats').n([1,4,5].beat()).out()
beat(1) :: snd('bd').out()
@ -42,42 +42,42 @@ beat(1) :: snd('bd').out()
//// beat(1) :: snd('bd').out()
`,
true
)}
true,
)}
${makeExample(
"More complex conditions using ?",
`
"More complex conditions using ?",
`
// The ? symbol can be used to write a if/true/false condition
beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
// (true) ? log('very true') : log('very false')
`,
false
)}
false,
)}
${makeExample(
"Using not and other short symbols",
`
"Using not and other short symbols",
`
// The ! symbol can be used to reverse a condition
beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
!beat(2) :: beat(0.5) :: snd('clap').out()
`,
false
)}
false,
)}
# About crashes and bugs
Things will crash! It's part of the show! You will learn progressively to avoid mistakes and to write safer code. Do not hesitate to kill the page or to stop the transport if you feel overwhelmed by an algorithm blowing up. There is no safeguard to stop you from doing most things. This is to ensure that you have all the available possible room to write bespoke code and experiment with your ideas through code.
${makeExample(
"This example will crash! Who cares?",
`
"This example will crash! Who cares?",
`
// This is crashing. See? No harm!
qjldfqsdklqsjdlkqjsdlqkjdlksjd
`,
true
)}
true,
)}
`;
};

View File

@ -18,44 +18,44 @@ 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!
${makeExample(
"Calling scripts to form a musical piece",
`
"Calling scripts to form a musical piece",
`
beat(1) :: script(1) // Calling local script n°1
flip(4) :: beat(.5) :: script(2) // Calling script n°2
`,
true
)}
true,
)}
${makeExample(
"Script execution can become musical too!",
`
"Script execution can become musical too!",
`
// Use algorithms to pick a script.
beat(1) :: script([1, 3, 5].pick())
flip(4) :: beat([.5, .25].beat(16)) :: script(
[5, 6, 7, 8].beat())
`,
false
)}
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,10 +44,10 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|----------|-------|------------------------------------------------------------|
|Evaluate|${key_shortcut("Ctrl + Enter")}| Evaluate the current script |
|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|
### Special
@ -55,22 +55,23 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
| 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(
"Claping twice as fast with fill",
`
"Claping twice as fast with fill",
`
beat(fill() ? 1/4 : 1/2)::sound('cp').out()
`,
true
)}
true,
)}
`;
};

View File

@ -16,8 +16,8 @@ You can get the current position of the mouse on the screen by using the followi
- <ic>mouseY()</ic>: the vertical position of the mouse on the screen (as a floating point number).
${makeExample(
"Vibrato controlled by mouse",
`
"Vibrato controlled by mouse",
`
beat(.25) :: sound('sine')
.note([0,4,5,10,11,15,16]
.palindrome()
@ -27,8 +27,8 @@ beat(.25) :: sound('sine')
.pan(r(0, 1))
.room(0.35).size(4).out()
`,
true
)}
true,
)}
<br>
@ -39,15 +39,15 @@ Current mouse position can also be used to generate notes:
${makeExample(
"Using the mouse to output a note!",
`
"Using the mouse to output a note!",
`
beat(.25) :: sound('sine')
.lpf(7000)
.delay(0.5).delayt(1/6).delayfb(0.2)
.note(noteX())
.room(0.35).size(4).out()`,
true
)}
true,
)}
## Mouse and Arrays
@ -58,14 +58,14 @@ You can use the mouse to explore the valuesq contained in an Array:
${makeExample(
"Taking values out of an Array with the mouse",
`
"Taking values out of an Array with the mouse",
`
log([1,2,3,4].mouseX())
log([4,5,6,7].mouseY())
`,
true
)}
true,
)}

View File

@ -8,34 +8,34 @@ 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
)}
"Welcome! Eval to get started",
examples[Math.floor(Math.random() * examples.length)],
true,
)}
# What is Topos?
Topos is an _algorithmic_ sequencer. Topos is also a _live coding_ environment. To sum it up, think: "_making music in real time through code_". Code used as an expressive medium for musical improvisation! Topos uses small algorithms to represent musical sequences and processes.
${makeExample(
"Small algorithms for direct musical expression",
`
"Small algorithms for direct musical expression",
`
rhythm(.5, 4, 8) :: sound('drum').out()
rhythm(.25, [5, 7].beat(2), 8) :: sound(['hc', 'fikea', 'hat'].pick(1))
.lpf([500, 4000+usine(1/2)*2000]).pan(r(0, 1)).ad(0, [1, .5])
.db(-ir(1,8)).speed([1,[0.5, 2].pick()]).room(0.5).size(3).o(4).out()
beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare')
.n(0).speed([1, 0.5]).o(4).out()`,
false
)}
false,
)}
${makeExample(
"Computer music should be immediate and intuitive",
`
"Computer music should be immediate and intuitive",
`
let chord_prog = [0, 0, 5].bar() // Chord progression
beat(.25)::snd('sine')
.note(chord_prog + [60, 64, 67, 71].mouseX()
@ -47,26 +47,31 @@ beat(.25)::snd('sine')
.delay(0.5).delayt(0.25).delayfb(0.7) // Delay
.room(0.5).size(8) // Reverb
.out()`,
false
)}
false,
)}
${makeExample(
"Making the web less dreadful, one beep at at time",
`
"Making the web less dreadful, one beep at at time",
`
beat(.5) :: sound('sid').n($(2))
.room(1).speed([1,2].pick()).out()
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).
## 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

@ -9,11 +9,12 @@ export const chaining = (application: Editor): string => {
You might have noticed that **Topos** is using chains a lot. Chains are a very common pattern when programming, especially when you deal with objets that can be composed from many changing properties. Method chaining is used by many objects but mostly by <ic>sound()</ic> and <ic>midi()</ic>. It looks like this:
${makeExample(
"Method chaining",
`
"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!
@ -22,21 +23,22 @@ Method chains become fun if you add just a little bit of complexity to them. You
You can use the <ic>register(...args)</ic> function to... register a chain that you would like to re-use later on.
${makeExample(
"Re-creating a classic Tidal function",
`
"Re-creating a classic Tidal function",
`
// Playing with extreme panning and playback rate
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:
${makeExample(
"Re-creating a classic Tidal function",
`
"Re-creating a classic Tidal function",
`
// Registering a specific synth architecture
register('sub', (n,x=4,y=80)=>n.ad(0, .25)
.fmi(x).pan([0, 1])
@ -48,8 +50,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,
)}
## Conditional chaining
@ -59,17 +62,17 @@ There are cases when you don't always want to apply one or many elements that ar
All functions from the sound object can be used to modify the event, for example:
${makeExample(
"Modifying sound events with probabilities",
`
"Modifying sound events with probabilities",
`
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",
`
"Chance to play a random note",
`
rhythm(.5, 3, 8) && sound('pluck').note(38).out()
beat(.5) && sound('pluck').note(60)
.often(s => s.note(57))
@ -77,8 +80,8 @@ 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:
@ -105,14 +108,14 @@ There is a growing collection of probability and chance methods you can use:
The conditional chaining also applies to MIDI. Values can also be incremented using <ic>+=</ic> notation.
${makeExample(
"Modifying midi events with probabilities",
`beat(.5) && midi(60).channel(1)
"Modifying midi events with probabilities",
`beat(.5) && midi(60).channel(1)
.odds(1/4, n => n.channel(2))
.often(n => n.note+=4)
.sometimes(s => s.velocity(irand(50,100)))
.out()`,
true
)};
true,
)};
## Ziffers
@ -125,8 +128,8 @@ Ziffers patterns can be chained to <ic>sound()</ic> and <ic>midi()</ic> as well.
* <ic>midi()</ic> - for outputting pattern as MIDI (See **MIDI**)
${makeExample(
"Ziffer player using a sound chain and probabilities!",
`
"Ziffer player using a sound chain and probabilities!",
`
z1('s 0 5 7 0 3 7 0 2 7 0 1 7 0 1 6 5 4 3 2')
.octave([0, 1].beat(2) - 1)
.scale('pentatonic').sound('pluck')
@ -134,7 +137,7 @@ z1('s 0 5 7 0 3 7 0 2 7 0 1 7 0 1 6 5 4 3 2')
.odds(1/2, n => n.speed(0.5))
.room(0.5).size(0.5).out()
`,
true
)};
true,
)};
`;
};

View File

@ -25,8 +25,8 @@ There are some basic controls over the playback of each sample. This allows you
| <ic>pan</ic> | | Stereo position of the audio playback (<ic>0</ic> = left, <ic>1</ic> = right)|
${makeExample(
"Complex sampling duties",
`
"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))
@ -35,17 +35,17 @@ beat(.5)::snd('pad').begin(0.2)
.room(0.8).size(0.5)
.clip(1).out()
`,
true
)};
true,
)};
${makeExample(
"Playing an amen break",
`
"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,
)};
true,
)};
## Filters
@ -63,15 +63,15 @@ There are three basic filters: a _lowpass_, _highpass_ and _bandpass_ filters wi
| <ic>vowel</ic> | | Formant filter with (vocal quality) |
${makeExample(
"Filter sweep using a low frequency oscillator",
`
"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
)};
true,
)};
## Compression

View File

@ -15,20 +15,20 @@ You can control scripts programatically. This is the core concept of Topos after
- <ic>copy_script(from: number, to: number)</ic>: copies a local script denoted by its number to another local script. **This is a destructive operation!**
${makeExample(
"Calling a script! The most important feature!",
`
"Calling a script! The most important feature!",
`
beat(1) :: script(1)
`,
true
)}
true,
)}
${makeExample(
"Calling mutliple scripts at the same time.",
`
"Calling mutliple scripts at the same time.",
`
beat(1) :: script(1, 3, 5)
`,
false
)}
false,
)}
## Math functions
@ -42,24 +42,24 @@ beat(1) :: script(1, 3, 5)
- <ic>delay(ms: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds.
${makeExample(
"Phased woodblocks",
`
"Phased woodblocks",
`
// Some very low-budget version of phase music
beat(.5) :: delay(usine(.125) * 80, () => sound('east').out())
beat(.5) :: delay(50, () => sound('east').out())
`,
true
)}
true,
)}
- <ic>delayr(ms: number, nb: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds, repeated a given number of times.
${makeExample(
"Another woodblock texture",
`
"Another woodblock texture",
`
beat(1) :: delayr(50, 4, () => sound('east').speed([0.5,.25].beat()).out())
flip(2) :: beat(2) :: delayr(150, 4, () => sound('east').speed([0.5,.25].beat() * 4).out())
`,
true
)};
true,
)};
`;
};

View File

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

View File

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

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

View File

@ -14,23 +14,23 @@ Now you know how to play some basic rhythms but in any case, you are stuck in a
- **Use the nine local scripts as containers** for sections of your composition. When you start playing with **Topos**, it's easy to forget that there are multiple scripts you can play with. Each script can store a different section or part from your composition. Here is a simple example:
${makeExample(
"Eight bars per section",
`
"Eight bars per section",
`
// 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>:
${makeExample(
"N beats per section",
`
"N beats per section",
`
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_.
@ -40,42 +40,42 @@ script([1,2,3,4].dur(8, 2, 16, 4))
- <ic>ratio: number = 50</ic>: this argument is ratio expressed in %. It determines how much of the period should be true or false. A ratio of <ic>75</ic> means that 75% of the period will be true. A ratio of <ic>25</ic> means that 25% of the period will be true.
${makeExample(
"Two beats of silence, two beats of playing",
`
"Two beats of silence, two beats of playing",
`
flip(4) :: beat(1) :: snd('kick').out()
`,
true
)}
true,
)}
${makeExample(
"Clapping on the edge",
`
"Clapping on the edge",
`
flip(2.5, 10) :: beat(.25) :: snd('cp').out()
flip(2.5, 75) :: beat(.25) :: snd('click')
.speed(2).end(0.2).out()
flip(2.5) :: beat(.5) :: snd('bd').out()
beat(.25) :: sound('hat').end(0.1).cutoff(1200).pan(usine(1/4)).out()
`,
false
)}
false,
)}
${makeExample(
"Good old true and false",
`
"Good old true and false",
`
if (flip(4, 75)) {
beat(1) :: snd('kick').out()
} else {
beat(.5) :: snd('snare').out()
}
`,
true
)}
true,
)}
<ic>flip</ic> is extremely powerful and is used internally for a lot of other Topos functions. You can also use it to think about **longer durations** spanning over multiple bars. Here is a silly composition that is using <ic>flip</ic> to generate a 4 bars long pattern.
${makeExample(
"Clunky algorithmic rap music",
`
"Clunky algorithmic rap music",
`
// Rap God VS Lil Wild -- Adel Faure
if (flip(8)) {
// Playing this part for two bars
@ -93,24 +93,24 @@ 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:
${makeExample(
"flip is great for parameter variation",
`
"flip is great for parameter variation",
`
beat(.5)::snd(flip(2) ? 'kick' : 'hat').out()
`,
true
)}
true,
)}
- <ic>flipbar(n: number = 1)</ic>: this method works just like <ic>flip</ic> but counts in bars instead of beats. It allows you to think about even larger time cycles. You can also pair it with regular <ic>flip</ic> for writing complex and long-spanning algorithmic beats.
${makeExample(
"Thinking music over bars",
`
"Thinking music over bars",
`
let roomy = (n) => n.room(1).size(1).cutoff(500 + usaw(1/8) * 5000);
function a() {
beat(1) && roomy(sound('kick')).out()
@ -122,24 +122,24 @@ function b() {
flipbar(2) && a()
flipbar(3) && b()
`,
true
)}
true,
)}
${makeExample(
"Alternating over four bars",
`
"Alternating over four bars",
`
flipbar(2)
? beat(.5) && snd(['kick', 'hh'].beat(1)).out()
: beat(.5) && snd(['east', 'east:2'].beat(1)).out()
`,
false
)};
false,
)};
- <ic>onbar(bars: number | number[], n: number)</ic>: The second argument, <ic>n</ic>, is used to divide the time in a period of <ic>n</ic> consecutive bars. The first argument should be a bar number or a list of bar numbers to play on. For example, <ic>onbar([1, 4], 5)</ic> will return <ic>true</ic> on bar <ic>1</ic> and <ic>4</ic> but return <ic>false</ic> the rest of the time. You can easily divide time that way.
${makeExample(
"Using onbar for filler drums",
`
"Using onbar for filler drums",
`
tempo(150);
// Only play on the third and fourth bar of the cycle.
onbar([3,4], 4)::beat(.25)::snd('hh').out();
@ -155,8 +155,8 @@ if (onbar([1,2], 4)) {
rhythm(.5, 1, 7) :: snd('jvbass').n(2).out();
rhythm(.5, 2, 7) :: snd('snare').n(3).out();
}`,
true
)}
true,
)}
`;
};

View File

@ -15,29 +15,29 @@ 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.
${makeExample(
"Listing MIDI outputs",
`
"Listing MIDI outputs",
`
midi_outputs()
`,
true
)}
true,
)}
- <ic>midi_output(output_name: string)</ic>: enter your desired output to connect to it.
${makeExample(
"Changing MIDI output",
`
"Changing MIDI output",
`
midi_output("MIDI Rocket-Trumpet")
`,
true
)}
true,
)}
That's it! You are now ready to play with MIDI.
@ -48,69 +48,69 @@ The most basic MIDI event is the note. MIDI notes traditionally take three param
- <ic>midi(note: number|object)</ic>: send a MIDI Note. This function is quite bizarre. It can be written and used in many different ways. You can pass form one up to three arguments in different forms.
${makeExample(
"MIDI note using one parameter: note",
`
"MIDI note using one parameter: note",
`
// Configure your MIDI first!
// => midi_output("MIDI Bus 1")
rhythm(.5, 5, 8) :: midi(50).out()
`,
true
)}
true,
)}
${makeExample(
"MIDI note using three parameters: note, velocity, channel",
`
"MIDI note using three parameters: note, velocity, channel",
`
// MIDI Note 50, Velocity 50 + LFO, Channel 0
rhythm(.5, 5, 8) :: midi(50, 50 + usine(.5) * 20, 0).out()
`,
false
)}
false,
)}
${makeExample(
"MIDI note by passing an object",
`
"MIDI note by passing an object",
`
// 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:
${makeExample(
"Playing some piano",
`
"Playing some piano",
`
tempo(80) // Setting a default BPM
beat(.5) && midi(36 + [0,12].beat()).sustain(0.02).out()
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
- <ic>control_change({control: number, value: number, channel: number})</ic>: send a MIDI Control Change. This function takes a single object argument to specify the control message (_e.g._ <ic>control_change({control: 1, value: 127, channel: 1})</ic>).
${makeExample(
"Imagine that I am tweaking an hardware synthesizer!",
`
"Imagine that I am tweaking an hardware synthesizer!",
`
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>).
${makeExample(
"Crashing old synthesizers: a hobby",
`
"Crashing old synthesizers: a hobby",
`
program_change([1,2,3,4,5,6,7,8].pick(), 1)
`,
true
)}
true,
)}
## System Exclusive Messages
@ -119,44 +119,44 @@ program_change([1,2,3,4,5,6,7,8].pick(), 1)
${makeExample(
"Nobody can say that we don't support Sysex messages!",
`
"Nobody can say that we don't support Sysex messages!",
`
sysex(0x90, 0x40, 0x7f)
`,
true
)}
true,
)}
## Clock
- <ic>midi_clock()</ic>: send a MIDI Clock message. This function is used to synchronize Topos with other MIDI devices or DAWs.
${makeExample(
"Tic, tac, tic, tac...",
`
"Tic, tac, tic, tac...",
`
beat(.25) && midi_clock() // Sending clock to MIDI device from the global buffer
`,
true
)}
true,
)}
## Using midi with ziffers
Ziffers offers some shorthands for defining channels within the patterns. See Ziffers for more information.
${makeExample(
"Using midi with ziffers",
`
"Using midi with ziffers",
`
z1('0 2 e 5 2 q 4 2').midi().port(2).channel(4).out()
`,
true
)}
true,
)}
${makeExample(
"Setting the channel within the pattern",
`
"Setting the channel within the pattern",
`
z1('(0 2 e 5 2):0 (4 2):1').midi().out()
`,
true
)}
true,
)}
`;
};

View File

@ -14,6 +14,21 @@ Topos is an experimental web based algorithmic sequencer programmed by **BuboBub
Topos is a free and open-source software distributed under [GPL-3.0](https://github.com/Bubobubobubobubo/Topos/blob/main/LICENSE) licence. We welcome all contributions and ideas. You can find the source code on [GitHub](https://github.com/Bubobubobubobubo/topos). You can also join us on [Discord](https://discord.gg/dnUTPbu6bN) to discuss about the project and live coding in general.
## 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,7 +7,7 @@ export const bonus = (application: Editor): string => {
return `
# Bonus features
Some features are here "just for fun" or "just because I can". They are not very interesting per se but are still available nonetheless. They mostly gravitate towards manipulating visuals or patterning other multimedia formats.
Some features have been included as a bonus. These features are often about patterning over things that are not directly related to sound: pictures, video, etc.
## Hydra Visual Live Coding
@ -15,43 +15,62 @@ Some features are here "just for fun" or "just because I can". They are not very
<warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning>
</div>
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows the metaphor of analog synthesizer patching to allow its user to create complex live visuals from a web browser window. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance. Topos provides a simple way to integrate Hydra into a live coding session and to blend it with regular Topos code.
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows an analog synthesizer patching metaphor to encourage live coding complex shaders. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance.
${makeExample(
"Hydra integration",
`beat(4) :: app.hydra.osc(3, 0.5, 2).out()`,
true
)}
"Hydra integration",
`beat(4) :: hydra.osc(3, 0.5, 2).out()`,
true,
)}
You may feel like it's doing nothing! Press ${key_shortcut(
"Ctrl+D"
)} to close the documentation. **Boom, all shiny!**
Close the documentation to see the effect: ${key_shortcut(
"Ctrl+D",
)}! **Boom, all shiny!**
Be careful not to call <ic>app.hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>mod()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches.
Be careful not to call <ic>hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>beat()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches.
Stopping **Hydra** is simple:
${makeExample(
"Stopping Hydra",
`
"Stopping Hydra",
`
beat(4) :: stop_hydra() // this one
beat(4) :: app.hydra.hush() // or this one
beat(4) :: hydra.hush() // or this one
`,
true
)}
true,
)}
I won't teach you how to play with Hydra. You can find some great resources on the [Hydra website](https://hydra.ojack.xyz/):
### Changing the resolution
You can change Hydra resolution using this simple method:
${makeExample(
"Changing Hydra resolution",
`hydra.setResolution(1024, 768)`,
true,
)}
### Documentation
I won't teach Hydra. You can find some great resources directly on the [Hydra website](https://hydra.ojack.xyz/):
- [Hydra interactive documentation](https://hydra.ojack.xyz/docs/)
- [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:
${makeExample(
"Playing many gifs",
`
"Playing many gifs",
`
beat(0.25)::gif({
url:v('gif')[$(1)%6], // Any URL will do!
opacity: r(0.5, 1), // Opacity (0-1)
@ -62,8 +81,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,11 +5,23 @@ 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 it on and off. The oscilloscope is off by default.
You can turn on the oscilloscope to generate interesting visuals or to inspect audio. Use the <ic>scope()</ic> function to turn on/off the oscilloscope and to configure it. The oscilloscope is off by default.
You need to manually feed the scope with the sounds you want to inspect:
${makeExample(
"Oscilloscope configuration",
`
"Feeding a sine to the oscilloscope",
`
beat(1)::sound('sine').freq(200).ad(0, .2).scope().out()
`,
true,
)}
Here is a layout of the scope configuration options:
${makeExample(
"Oscilloscope configuration",
`
scope({
enabled: true, // off by default
color: "#fdba74", // any valid CSS color or "random"
@ -23,12 +35,12 @@ scope({
refresh: 1 // refresh rate (in pulses)
})
`,
true
)}
true,
)}
${makeExample(
"Demo with multiple scope mode",
`
"Demo with multiple scope mode",
`
rhythm(.5, [4,5].dur(4*3, 4*1), 8)::sound('fhardkick').out()
beat(0.25)::sound('square').freq([
[250, 250/2, 250/4].pick(),
@ -44,8 +56,8 @@ 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 :)

75
src/documentation/osc.ts Normal file
View File

@ -0,0 +1,75 @@
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

@ -4,30 +4,30 @@ import { makeExampleFactory } from "../Documentation";
export const patterns = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Patterns
# Array patterns
**Topos** is using arrays as a way to make dynamic patterns of data (rhythms, melodies, etc).
It means that the following:
${makeExample(
"Boring kick",
`
"Boring kick",
`
beat(1)::sound('kick').out()
`,
true
)}
true,
)}
can be turned into something more interesting like this easily:
${makeExample(
"Less boring kick",
`
"Less boring kick",
`
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,
)}
**Topos** comes with a lot of array methods to deal with musical patterns of increasing complexity. Some knowledge of patterns and how to use them will help you to break out of simple loops and repeating structures. The most basic JavaScript data structure is the [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array). Topos is extending it with custom methods to describe patterns that evolve over time. These methods can often be chained to compose more complex expressions: <ic>[1, 2, 3].repeatOdd(5).palindrome().beat()</ic>.
@ -37,18 +37,18 @@ beat([1, 0.5, 0.25].dur(0.75, 0.25, 1) / c)::sound(['kick', 'fsoftsnare'].beat(0
- <ic>beat(division: number)</ic>: this method will return the next value in the list every _n_ pulses. By default, <ic>1</ic> equals to one beat but integer and floating point number values are supported as well. This method is extremely powerful and can be used for many different purposes. Check out the examples.
${makeExample(
"Light drumming",
`
"Light drumming",
`
// Every bar, use a different rhythm
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",
`
"Using beat to create arpeggios",
`
// Arpeggio using pulse divisions
beat([.5, .25].beat(0.5)) :: sound('sine')
.lpf(100+usine(1/4)*400).lpad(2, 0, .25)
@ -62,25 +62,25 @@ beat([.5, .25].beat(0.5)) :: sound('sine')
.delayfb(0.5)
.out()
`,
false
)}
false,
)}
${makeExample(
"Cool ambiance",
`
"Cool ambiance",
`
beat(.5) :: snd(['kick', 'hat'].beat(0.5)).out()
beat([2,4].beat(2)) :: snd('shaker').delay(.5).delayfb(.75).delayt(0.125).out()
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.
${makeExample(
"A simple drumbeat in no time!",
`
"A simple drumbeat in no time!",
`
beat(1)::sound(['kick', 'hat', 'snare', 'hat'].beat()).out()
beat([1/4, 1/2].dur(1.5, 0.5))::sound(['jvbass', 'fikea'].bar())
.ad(0, .25).room(0.5).size(2).resonance(0.15).lpf(
@ -88,12 +88,12 @@ beat([1/4, 1/2].dur(1.5, 0.5))::sound(['jvbass', 'fikea'].bar())
* [1, 2].bar())
.out()
`,
true
)}
true,
)}
${makeExample(
"Using beat and bar in the same example",
`
"Using beat and bar in the same example",
`
beat(2)::snd('snare').out()
beat([1, 0.5].beat()) :: sound(['bass3'].bar())
.freq(100).n([12, 14].bar())
@ -101,14 +101,14 @@ 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.
${makeExample(
"Holding a value for n beats",
`
"Holding a value for n beats",
`
// The second note is kept for twice as long
beat(0.5)::sound('notes').n([1,2].dur(1, 2))
.room(0.5).size(8).delay(0.125).delayt(1/8)
@ -117,8 +117,8 @@ 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
@ -126,63 +126,63 @@ beat(1)::sound(['kick', 'fsnare'].dur(3, 1))
- <ic>pitch()</ic>: convert a list of integers to pitch classes
${makeExample(
"Converting a list of integers to pitch classes using key and scale",
`
"Converting a list of integers to pitch classes using key and scale",
`
beat(0.25) :: snd('sine')
.pitch([0,1,2,3,4,6,7,8].beat(0.125))
.key(["F4","F3"].beat(2.0))
.scale("minor").ad(0, .25).out()
`,
true
)}
true,
)}
- <ic>scale(scale: string, base note: number)</ic>: Map each element of the list to the closest note of the slected scale. [0, 2, 3, 5 ].scale("major", 50) returns [50, 52, <ic>54</ic>, 55]. You can use western scale names like (Major, Minor, Minor pentatonic ...) or [zeitler](https://ianring.com/musictheory/scales/traditions/zeitler) scale names. Alternatively you can also use the integers as used by Ian Ring in his [study of scales](https://ianring.com/musictheory/scales/).
${makeExample(
"Mapping the note array to the E3 major scale",
`
"Mapping the note array to the E3 major scale",
`
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>.
${makeExample(
"Extrapolate a 3-elements Mixolydian scale from 2 notes",
`
"Extrapolate a 3-elements Mixolydian scale from 2 notes",
`
beat(1) :: snd('gtr')
.note([0, 5].scaleArp("mixolydian", 3).beat() + 50)
.out()
`,
true
)}
true,
)}
## Iteration using the mouse
- <ic>mouseX()</ic> / <ic>mouseY()</ic>: divides the screen in <ic>n</ic> zones and returns the value corresponding to the mouse position on screen.</ic>
${makeExample(
"Controlling an arpeggio (octave and note) with mouse",
`
"Controlling an arpeggio (octave and note) with mouse",
`
beat(0.25)::sound('wt_piano')
.note([0,2,3,4,5,7,8,9,11,12].scale(
'minor', 30 + [0,12,24].mouseY()).mouseX())
.room(0.5).size(4).lpad(-2, .2).lpf(500, 0.3)
.ad(0, .2).out()
`,
true
)}
true,
)}
## Simple data operations
- <ic>palindrome()</ic>: Concatenates a list with the same list in reverse.
${makeExample(
"Palindrome filter sweep",
`
"Palindrome filter sweep",
`
beat([1,.5,.25].beat()) :: snd('wt_stereo')
.speed([1, 0.5, 0.25])
.pan(r(0, 1)).freq([100,200,300].beat(0.25))
@ -191,15 +191,15 @@ 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.
- <ic>rand(index: number)</ic>: shorter alias for the same method.
${makeExample(
"Sipping some gasoline at the robot bar",
`
"Sipping some gasoline at the robot bar",
`
// rand, random and pick are doing the same thing!
beat(1)::snd('fhardkick').shape(0.5)
.ad(0, .1).lpf(500).db(-12).out()
@ -210,69 +210,69 @@ 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.
${makeExample(
"Picking values in lists",
`
"Picking values in lists",
`
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.
${makeExample(
"Amen break suffering from data loss",
`
"Amen break suffering from data loss",
`
// 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.
- <ic>repeatEven(amount: number)</ic>: repeat every pair element of the list _n_ times.
- <ic>repeatOdd(amount: number)</ic>: repeat every odd element of the list _n_ times.
${makeExample(
"Repeating samples a given number of times",
`
"Repeating samples a given number of times",
`
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>.
${makeExample(
"Don't you know how to count up to 5?",
`
"Don't you know how to count up to 5?",
`
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!
${makeExample(
"Shuffling a list for extra randomness",
`
"Shuffling a list for extra randomness",
`
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.
${makeExample(
"To make things more complex... here you go",
`
"To make things more complex... here you go",
`
beat(.25) :: snd('sine').fmi([1.99, 2])
.ad(0, .125).lpf(500+r(1,400))
.lpad(usine()*8, 0, .125)
@ -283,21 +283,21 @@ beat(.25) :: snd('sine').fmi([1.99, 2])
.beat(.25)) // while the index changes
.out()
`,
true
)}
true,
)}
## Filtering
- <ic>unique()</ic>: filter a list to remove repeated values.
${makeExample(
"Demonstrative filtering. Final list is [100, 200]",
`
"Demonstrative filtering. Final list is [100, 200]",
`
// 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

@ -0,0 +1,62 @@
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.5).delay(0.125)
.delayfb(0.25).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,
)}
* **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,
)}
${makeExample(
"Random numbers",
`
z1('q 0 (2,4) 4 (5,9)').sound('sine')
.scale("Bebop minor")
.out()
`,
true,
)}
* **Variables:** <ic>A=(0 2 3 4)</ic> Assign a list to a variable
`;
};

View File

@ -0,0 +1,296 @@
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,
)}
## 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()
`,
)}
## 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(
"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,
)}
## 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

@ -0,0 +1,157 @@
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 |
| p | 8/3 | 2.6666 | Triplet maxima | Triplet longa |
| d.. | 7/2 | 3.5 | Double dotted long note | Double dotted breve |
| d. | 3/3 | 3.0 | Double whole note | Double breve |
| d | 2/1 | 2.0 | Double whole note | Breve |
| c | 4/3 | 1.3333 | Triplet long | Triplet breve |
| w.. | 7/4 | 1.75 | Double dotted whole note | Double dotted breve |
| w. | 3/2 | 1.5 | Dotted whole note | Dotted breve |
| w | 1/1 | 1.0 | Whole note | Semibreve |
| y | 2/3 | 0.6666 | Triplet half | Triplet semibreve |
| h.. | 7/8 | 0.875 | Double dotted half note | Double dotted minim |
| h. | 3/4 | 0.75 | Dotted half note | Dotted minim |
| h | 1/2 | 0.5 | Half note  | Minim |
| n | 1/3 | 0.3333 | Triplet whole | 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 |
| a | 1/6 | 0.1666 | Triplet quarter | Triplet crochet  |
| e.. | 7/32 | 0.2187 | Double dotted eighth note | Double dotted quaver |
| e. | 3/16 | 0.1875 | Dotted eighth note | Dotted quaver |
| e | 1/8 | 0.125 | 8th note | Quaver |
| f | 1/12 | 0.0833 | Triplet 8th | Triplet quaver |
| s.. | 7/64 | 0.1093 | Double dotted sixteenth note | Double dotted semiquaver |
| s. | 3/32 | 0.0937 | Dotted sixteenth note | Dotted semiquaver |
| s | 1/16 | 0.0625 | 16th note | Semiquaver |
| x | 1/24 | 0.0416 | Triplet 16th | Triplet semiquaver |
| t.. | 7/128 | 0.0546 | Double dotted thirty-second note | Double dotted demisemiquaver |
| t. | 3/64 | 0.0468 | Dotted thirty-second note | Dotted demisemiquaver |
| t | 1/32 | 0.0312 | 32th note | Demisemiquaver |
| g | 1/48 | 0.0208 | Triplet 32th | Triplet demi-semiquaver |
| u.. | 7/256 | 0.0273 | Double dotted sixty-fourth note | Double dotted hemidemisemiquaver |
| u. | 3/128 | 0.0234 | Dotted sixty-fourth note | Dotted hemidemisemiquaver |
| u | 1/64 | 0.0156 | 64th note | Hemidemisemiquaver |
| j | 1/96 | 0.0104 | Triplet 64th | Triplet hemidemisemiquaver |
| o.. | 7/512 | 0.0136 | Double dotted 128th note | Double dotted semihemidemisemiquaver |
| o. | 3/256 | 0.0117 | Dotted 128th note | Dotted semihemidemisemiquaver |
| o | 1/128 | 0.0078 | 128th note | Semihemidemisemiquaver |
| k | 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

@ -0,0 +1,101 @@
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 [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,
)}
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

@ -0,0 +1,20 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_tonnetz = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Tonnetz
* TBD
${makeExample(
"Triad transformations",
`
z1('i').tonnetz("p l r").sound('wt_stereo')
.adsr(0, .1, 0, 0).out()`,
true,
)}
`;
};

View File

@ -13,12 +13,12 @@ There are some simple functions to play with probabilities.
- <ic>irand(min: number, max:number)</ic>: returns a random integer between <ic>min</ic> and <ic>max</ic>. Shorthands <ic>ir()</ic> or <ic>rI()</ic>.
${makeExample(
"Bleep bloop, what were you expecting?",
`
"Bleep bloop, what were you expecting?",
`
rhythm(0.125, 10, 16) :: sound('sid').n(4).note(50 + irand(50, 62) % 8).out()
`,
true
)}
true,
)}
- <ic>prob(p: number)</ic>: return <ic>true</ic> _p_% of time, <ic>false</ic> in other cases.
@ -26,14 +26,14 @@ rhythm(0.125, 10, 16) :: sound('sid').n(4).note(50 + irand(50, 62) % 8).out()
${makeExample(
"The Teletype experience!",
`
"The Teletype experience!",
`
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.
@ -58,28 +58,28 @@ By default chance operators will be evaluated 48 times within a beat. You can ch
Examples:
${makeExample(
"Using chance operators",
`
"Using chance operators",
`
rarely() :: sound('hh').out(); // Rarely 48 times is still a lot
rarely(4) :: sound('bd').out(); // Rarely in 4 beats is bit less
rarely(8) :: sound('east').out(); // Rarely in 8 beats is even less
`,
true
)}
true,
)}
${makeExample(
"Using chance with other operators",
`
"Using chance with other operators",
`
frequently() :: beat(1) :: sound('kick').out();
often() :: beat(0.5) :: sound('hh').out();
sometimes() :: onbeat(1,3) :: sound('snare').out();
`,
false
)}
false,
)}
${makeExample(
"Using chance with chaining",
`
"Using chance with chaining",
`
beat(0.5) && sound("bd")
.freq(100)
.sometimes(s=>s.crush(2.5))
@ -91,8 +91,7 @@ ${makeExample(
.almostNever(n=>n.freq(400))
.out()
`,
false
)}
`
}
false,
)}
`;
};

View File

@ -12,36 +12,36 @@ Topos is exposing the <ic>samples</ic> function that you can use to load your ow
Samples are loaded on-the-fly from the web. Topos is a web application living in the browser. It is running in a sandboxed environment. Thus, it cannot have access to the files stored on your local system. Loading samples requires building a _map_ of the audio files, where a name is associated to a specific file:
${makeExample(
"Loading samples from a map",
`samples({
"Loading samples from a map",
`samples({
bd: ['bd/BT0AADA.wav','bd/BT0AAD0.wav'],
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:
${makeExample(
"Playing with the loaded samples",
`rhythm(.5, 5, 8)::sound('bd').n(ir(1,2)).end(1).out()
"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:
${makeExample(
"This is how Topos is loading its own samples",
`
"This is how Topos is loading its own samples",
`
// Visit the concerned repos and search for 'strudel.json'
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!
@ -50,16 +50,17 @@ To learn more about the audio sample loading mechanism, please refer to [this pa
You can load samples coming from [Freesound](https://freesound.org/) using the [Shabda](https://shabda.ndre.gr/) API. To do so, study the following example:
${makeExample(
"Loading samples from shabda",
`
"Loading samples from shabda",
`
// Prepend the sample you want with 'shabda:'
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

@ -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,7 +1,10 @@
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);
@ -44,13 +47,11 @@ 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);
@ -63,9 +64,13 @@ On this page, you will find an exhaustive list of all the samples currently load
A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with <ic>wt_</ic> will be looped. Look at this demo:
${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.
@ -79,9 +84,12 @@ ${samples_to_markdown(application, "Waveforms")}
A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the <ic>.bank()</ic> parameter like so:
${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:
@ -111,10 +119,12 @@ ${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.
@ -130,5 +140,13 @@ Many live coders are expecting to find the Tidal sample library wherever they go
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
${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-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
${samples_to_markdown(application, "Juliette")}
</div>
`;
};

View File

@ -13,42 +13,42 @@ Topos comes by default with a forever-increasing number of synthesis capabilitie
The <ic>sound</ic> function can take the name of a synthesizer or waveform as first argument. This has for effect to turn the sampler we all know and love into a synthesizer. <ic>sine</ic>, <ic>sawtooth</ic>,<ic>triangle</ic>, <ic>square</ic> are the names used to select classic oscillator waveforms. Note that you can also make use of filters and envelopes to shape the sound to your liking.
${makeExample(
"Listening to the different waveforms from the sweetest to the harshest",
`
"Listening to the different waveforms from the sweetest to the harshest",
`
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",
`
"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:
- <ic>freq(hz: number)</ic>: sets the frequency of the oscillator.
- <ic>note(note: number|string)</ic>: sets the MIDI note of the oscillator (MIDI note converted to hertz).
${makeExample(
"Selecting a pitch",
`
"Selecting a pitch",
`
beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out()
`,
true
)}
true,
)}
${makeExample(
"Selecting a note",
`
beat(.5) && snd('triangle').note([60,"F4"].pick()).out()
`,
true
true,
)}
Chords can also played using different parameters:
@ -56,44 +56,44 @@ Chords can also played using different parameters:
- <ic>chord(string||number[]|...number)</ic>: parses and sets notes for the chord
${makeExample(
"Playing a named chord",
`
"Playing a named chord",
`
beat(1) && snd('triangle').chord(["C","Em7","Fmaj7","Emin"].beat(2)).adsr(0,.2).out()
`,
true
)}
true,
)}
${makeExample(
"Playing a chord from a list of notes and doing inversions",
`
"Playing a chord from a list of notes and doing inversions",
`
beat(.5) && snd('triangle').chord(60,64,67,72).invert([1,-3,4,-5].pick()).adsr(0,.2).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",
`
"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
)}
true,
)}
## Noise
A certain amount of brown noise can be added by using the <ic>.noise</ic> key:
${makeExample(
"Different vibrato settings",
`
"Different vibrato settings",
`
tempo(140);
beat(1) :: sound('triangle')
.freq(400).release(0.2)
@ -101,8 +101,8 @@ beat(1) :: sound('triangle')
.vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2))
.out()`,
true
)}
true,
)}
## Controlling the amplitude
@ -112,16 +112,16 @@ Controlling the amplitude and duration of the sound can be done using various te
- <ic>velocity(velocity: number)</ic>: sets the velocity of the oscillator (velocity is a multiple of gain).
${makeExample(
"Setting the gain",
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true
)}
"Setting the gain",
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true,
)}
${makeExample(
"Setting the velocity",
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true
)}
"Setting the velocity",
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
true,
)}
<div class="mt-4 mb-4 lg:grid lg:grid-cols-4 lg:gap-4">
<img class="col-span-1 lg:ml-12 bg-gray-100 rounded-lg px-2 py-2", src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ADSR_Envelope_Graph.svg/1280px-ADSR_Envelope_Graph.svg.png" width="400" />
@ -134,45 +134,45 @@ ${makeExample(
- <ic>release(release: number)</ic> / <ic>rel(rel: number)</ic>: sets the release time of the envelope.
${makeExample(
"Using decay and sustain to set the ADSR envelope",
`
"Using decay and sustain to set the ADSR envelope",
`
beat(0.5) :: sound('wt_piano')
.cutoff(1000 + usine() * 4000)
.freq(100).decay(.2)
.sustain([0.1,0.5].beat(4))
.out()`,
true
)}
true,
)}
This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The <ic>adsr()</ic> combines all the parameters together. It is a shortcut for setting the ADSR envelope:
- <ic>adsr(attack: number, decay: number, sustain: number, release: number)</ic>: sets the ADSR envelope.
${makeExample(
"Replacing the previous example with the adsr() method",
`
"Replacing the previous example with the adsr() method",
`
beat(0.5) :: sound('wt_piano')
.cutoff(1000 + usine() * 4000)
.freq(100)
.adsr(0, .2, [0.1,0.5].beat(4), 0)
.out()
`,
true
)}
true,
)}
- <ic>ad(attack: number, decay: number)</ic>: sets the attack and decay phases, setting sustain and release to <ic>0</ic>.
${makeExample(
"Two segment envelope",
`
"Two segment envelope",
`
beat(0.5) :: sound('wt_piano')
.cutoff(1000 + usine() * 4000)
.freq(100)
.ad(0, .2)
.out()
`,
true
)}
true,
)}
## Substractive synthesis using filters
@ -183,10 +183,10 @@ The most basic synthesis technique used since the 1970s is called substractive s
- **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
)}
"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:
@ -199,10 +199,10 @@ These filters all come with their own set of parameters. Note that we are descri
| <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
)}
"Filtering a bass",
`beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`,
true,
)}
### Highpass filter
@ -212,10 +212,10 @@ ${makeExample(
| <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
)}
"Filtering a noise source",
`beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`,
true,
)}
### Bandpass filter
@ -225,10 +225,10 @@ ${makeExample(
| <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
)}
"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**.
@ -239,38 +239,38 @@ You can also use the <ic>ftype</ic> method to change the filter type (order). Th
- <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
)}
"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",
`
"Simple synthesizer voice with filter",
`
beat(.5) && snd('sawtooth')
.cutoff([2000,500].pick() + usine(.5) * 4000)
.resonance(0.2).freq([100,150].pick())
.out()
`,
true
)}
true,
)}
${makeExample(
"Blessed by the square wave",
`
"Blessed by the square wave",
`
beat(4) :: [100,101].forEach((freq) => sound('square').freq(freq).sustain(0.1).out())
beat(.5) :: [100,101].forEach((freq) => sound('square').freq(freq*2).sustain(0.01).out())
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(
"Ghost carillon (move your mouse!)",
`
"Ghost carillon (move your mouse!)",
`
beat(1/8)::sound('sine')
.velocity(rand(0.0, 1.0))
.delay(0.75).delayt(.5)
@ -279,8 +279,8 @@ beat(1/8)::sound('sine')
.freq(mouseX())
.gain(0.25)
.out()`,
false
)}
false,
)}
## Filter envelopes
@ -299,12 +299,12 @@ The examples we have studied so far are static. They filter the sound around a f
${makeExample(
"Filtering a sawtooth wave dynamically",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
"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
)}
true,
)}
### Highpass envelope
@ -319,12 +319,12 @@ ${makeExample(
${makeExample(
"Let's use another filter using the same example",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
"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
)}
true,
)}
### Bandpass envelope
@ -339,14 +339,14 @@ ${makeExample(
${makeExample(
"And the bandpass filter, just for fun",
`beat(.5) :: sound('sawtooth').note([48,60].beat())
"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
)}
true,
)}
## Wavetable synthesis
@ -354,8 +354,8 @@ ${makeExample(
Topos can also do wavetable synthesis. Wavetable synthesis allows you to use any sound file as a source to build an oscillator. By default, Topos comes with more than 1000 waveforms thanks to the awesome [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) pack made by Kristoffer Ekstrand. Any sample name that contains <ic>wt_</ic> as a prefix will be interpreted by the sampler as a wavetable and thus as an oscillator. See for yourself:
${makeExample(
"Acidity test",
`
"Acidity test",
`
beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0]
.pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0)
.cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0)
@ -363,15 +363,15 @@ 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,
)}
Let's explore the galaxy of possible waveforms. It can be hard to explore them all, there is a **lot** of them:
${makeExample(
"Let's explore some wavetables",
`
"Let's explore some wavetables",
`
// Exploring a vast galaxy of waveforms
let collection = [
'wt_sinharm', 'wt_linear', 'wt_bw_sawrounded',
@ -381,8 +381,8 @@ 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...
@ -397,8 +397,8 @@ Another really useful technique to know about is FM synthesis, FM standing for _
There is also an additional parameter, <ic>fm</ic> that combines <ic>fmi</ic> and <ic>fmh</ic> using strings: <ic>fm('2:4')</ic>. Think of it as a static shortcut for getting some timbres more quickly.
${makeExample(
"80s nostalgia",
`
"80s nostalgia",
`
beat([.5, 1].beat(8)) && snd('triangle').adsr(0.02, 0.5, 0.5, 0.25)
.fmi(2).fmh(1.5).note([60,55, 60, 63].beat() - 12)
.pan(noise()).out()
@ -407,23 +407,23 @@ 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(
"Giving some love to ugly inharmonic sounds",
`
"Giving some love to ugly inharmonic sounds",
`
beat([.5, .25].bar()) :: sound('sine').fm('2.2183:3.18293').sustain(0.05).out()
beat([4].bar()) :: sound('sine').fm('5.2183:4.5').sustain(0.05).out()
beat(.5) :: sound('sine')
.fmh([1, 1.75].beat())
.fmi($(1) % 30).orbit(2).room(0.5).out()`,
true
)}
true,
)}
${makeExample(
"Peace and serenity through FM synthesis",
`
"Peace and serenity through FM synthesis",
`
beat(0.25) :: sound('sine')
.note([60, 67, 70, 72, 77].beat() - [0,12].bar())
.attack(0.2).release(0.5).gain(0.25)
@ -432,8 +432,8 @@ 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.
@ -444,8 +444,8 @@ There is also a more advanced set of parameters you can use to control the envel
- <ic>fmrelease</ic> / <ic>fmrel</ic>: release time of the modulator envelope.
${makeExample(
"FM Synthesis with envelope control",
`
"FM Synthesis with envelope control",
`
beat(.5) :: sound('sine')
.note([50,53,55,57].beat(.5) - 12)
.fmi(0.5 + usine(.25) * 1.5)
@ -453,8 +453,8 @@ beat(.5) :: sound('sine')
.fmwave('triangle')
.fmsus(0).fmdec(0.2).out()
`,
true
)}
true,
)}
## ZzFX
@ -463,15 +463,15 @@ beat(.5) :: sound('sine')
ZZfX can be triggered by picking a default ZZfX waveform in the following list: <ic>z_sine</ic>, <ic>z_triangle</ic>, <ic>z_sawtooth</ic>, <ic>z_tan</ic>, <ic>z_noise</ic>.
${makeExample(
"Picking a waveform",
`
"Picking a waveform",
`
beat(.5) :: sound(['z_sine', 'z_triangle', 'z_sawtooth', 'z_tan', 'z_noise'].beat()).out()
`,
true
)}
true,
)}
${makeExample(
"Minimalist chiptune",
`
"Minimalist chiptune",
`
beat(.5) :: sound('z_triangle')
.note([60, 67, 72, 63, 65, 70].beat(.5))
.zrand(0).curve([1,2,3,4].beat(1))
@ -481,8 +481,8 @@ 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:
@ -508,8 +508,8 @@ It comes with a set of parameters that can be used to tweak the sound. Don't und
|<ic>duration</ic>|| Total sound duration (overrides envelope) |
${makeExample(
"Chaotic Noise source",
`
"Chaotic Noise source",
`
beat(.25) :: sound('z_tan')
.note(40).noise(rand(0.0, 1.0))
.pitchJump(84).pitchJumpTime(rand(0.0, 1.0))
@ -519,21 +519,21 @@ beat(.25) :: sound('z_tan')
.sustain(0).decay([0.2, 0.1].pick())
.out()
`,
true
)}
true,
)}
${makeExample(
"What is happening to me?",
`
"What is happening to me?",
`
beat(1) :: snd('zzfx').zzfx([
[4.77,,25,,.15,.2,3,.21,,2.4,,,,,,,.23,.35],
[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",
`
"Les voitures dans le futur",
`
beat(1) :: sound(['z_triangle', 'z_sine'].pick())
.note([60,63,72,75].pick()).tremolo(16)
.zmod([0, 1/2, 1/8].div(2).pick())
@ -541,18 +541,18 @@ 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:
${makeExample(
"Designing a sound on the ZzFX website",
`
"Designing a sound on the ZzFX website",
`
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
@ -567,35 +567,35 @@ Topos can also speak using the [Web Speech API](https://developer.mozilla.org/en
- <ic>volume(number)</ic>: speaking volume, from <ic>0.0</ic> to <ic>1.0</ic>.
${makeExample(
"Hello world!",
`
"Hello world!",
`
beat(4) :: speak("Hello world!")
`,
true
)}
true,
)}
${makeExample(
"Let's hear people talking about Topos",
`
"Let's hear people talking about Topos",
`
beat(2) :: speak("Topos!","fr",irand(0,5))
`,
true
)}
true,
)}
You can also use speech by chaining methods to a string:
${makeExample(
"Foobaba is the real deal",
`
"Foobaba is the real deal",
`
onbeat(4) :: "Foobaba".voice(irand(0,10)).speak()
`,
true
)}
true,
)}
${makeExample(
"Building string and chaining",
`
"Building string and chaining",
`
const subject = ["coder","user","loser"].pick()
const verb = ["is", "was", "isnt"].pick()
const object = ["happy","sad","tired"].pick()
@ -603,12 +603,12 @@ ${makeExample(
beat(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).speak()
`,
true
)}
true,
)}
${makeExample(
"Live coded poetry with array and string chaining",
`
"Live coded poetry with array and string chaining",
`
tempo(70)
const croissant = [
@ -623,7 +623,7 @@ ${makeExample(
.rate(rand(.4,.6))
.speak();
`,
true
)}
true,
)}
`;
};

View File

@ -16,17 +16,17 @@ Time as a cycle. A cycle can be quite long (a few bars) or very short (a few pul
- <ic>offset</ic>: offset (in beats) to apply. An offset of <ic>0.5</ic> will return true against the beat.
${makeExample(
"Using different mod values",
`
"Using different mod values",
`
// This code is alternating between different mod values
beat([1,1/2,1/4,1/8].beat(2)) :: sound('hat').n(0).out()
`,
true
)}
true,
)}
${makeExample(
"Some sort of ringtone",
`
"Some sort of ringtone",
`
// Blip generator :)
let blip = (freq) => {
return sound('wt_piano')
@ -41,16 +41,16 @@ 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(
"Beat can match multiple values",
`
"Beat can match multiple values",
`
beat([.5, 1.25])::sound('hat').out()
`,
false
)}
false,
)}
- <ic>pulse(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ pulses. A pulse is the tiniest possible rhythmic value.
- <ic>number</ic>: if <ic>number = 1</ic>, the function will return <ic>true</ic> every pulse. Lists can be used too.
@ -58,21 +58,21 @@ beat([.5, 1.25])::sound('hat').out()
${makeExample(
"Intriguing rhythms",
`
"Intriguing rhythms",
`
pulse([24, 16])::sound('hat').ad(0, .02).out()
pulse([48, [36,24].dur(4, 1)])::sound('fhardkick').ad(0, .1).out()
`,
true
)}
true,
)}
${makeExample(
"pulse is the OG rhythmic function in Topos",
`
"pulse is the OG rhythmic function in Topos",
`
pulse([48, 24, 16].beat(4)) :: sound('linnhats').out()
beat(1)::snd(['bd', '808oh'].beat(1)).out()
`,
false
)}
false,
)}
- <ic>bar(n: number | number[] = 1, offset: number = 1)</ic>: return true every _n_ bars.
@ -80,37 +80,37 @@ beat(1)::snd(['bd', '808oh'].beat(1)).out()
- <ic>offset</ic>: offset (in bars) to apply.
${makeExample(
"Four beats per bar: proof",
`
"Four beats per bar: proof",
`
bar(1)::sound('kick').out()
beat(1)::sound('hat').speed(2).out()
`,
true
)}
true,
)}
${makeExample(
"Offsetting beat and bar",
`
"Offsetting beat and bar",
`
bar(1)::sound('kick').out()
beat(1)::sound('hat').speed(2).out()
beat(1, 0.5)::sound('hat').speed(4).out()
bar(1, 0.5)::sound('sn').out()
`,
false
)}
false,
)}
- <ic>onbeat(...n: number[])</ic>: The <ic>onbeat</ic> function allows you to lock on to a specific beat from the clock to execute code. It can accept multiple arguments. It's usage is very straightforward and not hard to understand. You can pass either integers or floating point numbers. By default, topos is using a <ic>4/4</ic> bar meaning that you can target any of these beats (or in-between) with this function.
${makeExample(
"Some simple yet detailed rhythms",
`
"Some simple yet detailed rhythms",
`
onbeat(1,2,3,4)::snd('kick').out() // Bassdrum on each beat
onbeat(2,4)::snd('snare').n([8,4].beat(4)).out() // Snare on acccentuated beats
onbeat(1.5,2.5,3.5, 3.75)::snd('hat').gain(r(0.9,1.1)).out() // Cool high-hats
`,
true
)}
true,
)}
## XOX Style sequencers
@ -119,32 +119,32 @@ onbeat(1.5,2.5,3.5, 3.75)::snd('hat').gain(r(0.9,1.1)).out() // Cool high-hats
- <ic>duration: number</ic>: an optional duration (in beats) like <ic>1</ic> or </ic>4</ic>. It can be patterned.
${makeExample(
"Sequence built using a classic XOX sequencer style",
`
"Sequence built using a classic XOX sequencer style",
`
seq('xoxo')::sound('fhardkick').out()
seq('ooxo')::sound('fsoftsnare').out()
seq('xoxo', 0.25)::sound('fhh').out()
`,
true
)}
true,
)}
${makeExample(
"Another sequence using more complex parameters",
`
"Another sequence using more complex parameters",
`
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.
- <ic>expr: string</ic>: any string composed of <ic>x</ic> or <ic>o</ic> like so: <ic>"xooxoxxoxoo"</ic>.
- <ic>duration: number</ic>: an optional duration (in beats) like <ic>1</ic> or </ic>4</ic>. It can be patterned.
${makeExample(
"Long structured patterns",
`
"Long structured patterns",
`
function simplePat() {
log('Simple pattern playing!')
seq('xoxooxxoo', [0.5, 0.25].dur(2, 1))::sound('fhardkick').out()
@ -159,8 +159,8 @@ function complexPat() {
}
fullseq('xooxooxx', 4) ? simplePat() : complexPat()
`,
true
)}
true,
)}
@ -171,8 +171,8 @@ We included a bunch of popular rhythm generators in Topos such as the euclidian
- <ic>rhythm(divisor: number, pulses: number, length: number, rotate: number): boolean</ic>: generates <ic>true</ic> or <ic>false</ic> values from an euclidian rhythm sequence. This is another version of <ic>euclid</ic> that does not take an iterator.
${makeExample(
"rhythm is a beginner friendly rhythmic function!",
`
"rhythm is a beginner friendly rhythmic function!",
`
rhythm(.5, 4, 8)::sound('sine')
.fmi(2)
.room(0.5).size(8)
@ -181,38 +181,38 @@ 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,
)}
- <ic>oneuclid(pulses: number, length: number, rotate: number): boolean</ic>: generates <ic>true</ic> or <ic>false</ic> values from an euclidian rhythm sequence. This is another version of <ic>euclid</ic> that does not take an iterator.
${makeExample(
"Using oneuclid to create a rhythm without iterators",
`
"Using oneuclid to create a rhythm without iterators",
`
// Change speed using bpm
bpm(250)
oneuclid(5, 9) :: snd('kick').out()
oneuclid(7,16) :: snd('east').end(0.5).n(irand(3,5)).out()
`,
true
)}
true,
)}
- <ic>bin(iterator: number, n: number): boolean</ic>: a binary rhythm generator. It transforms the given number into its binary representation (_e.g_ <ic>34</ic> becomes <ic>100010</ic>). It then returns a boolean value based on the iterator in order to generate a rhythm.
- <ic>binrhythm(divisor: number, n: number): boolean: boolean</ic>: iterator-less version of the binary rhythm generator.
${makeExample(
"Change the integers for a surprise rhythm!",
`
"Change the integers for a surprise rhythm!",
`
bpm(135);
beat(.5) && bin($(1), 12) && snd('kick').n([4,9].beat(1.5)).out()
beat(.5) && bin($(2), 34) && snd('snare').n([3,5].beat(1)).out()
`,
true
)}
true,
)}
${makeExample(
"binrhythm for fast cool binary rhythms!",
`
"binrhythm for fast cool binary rhythms!",
`
let a = 0;
a = beat(4) ? irand(1,20) : a;
binrhythm(.5, 6) && snd(['kick', 'snare'].beat(0.5)).n(11).out()
@ -221,34 +221,34 @@ binrhythm([.5, .25].beat(1), 30) && snd('wt_granular').n(a)
.adsr(0, r(.1, .4), 0, 0).freq([50, 60, 72].beat(4))
.room(1).size(1).out()
`,
true
)}
true,
)}
${makeExample(
"Submarine jungle music",
`
"Submarine jungle music",
`
bpm(145);
beat(.5) && bin($(1), 911) && snd('ST69').n([2,3,4].beat())
.delay(0.125).delayt(0.25).end(0.25).speed(1/3)
.room(1).size(1).out()
beat(.5) && sound('amencutup').n(irand(2,7)).shape(0.3).out()
`,
false
)}
false,
)}
If you don't find it spicy enough, you can add some more probabilities to your rhythms by taking advantage of the probability functions. See the functions documentation page to learn more about them.
${makeExample(
"Probablistic drums in one line!",
`
"Probablistic drums in one line!",
`
prob(60)::beat(.5) && euclid($(1), 5, 8) && snd('kick').out()
prob(60)::beat(.5) && euclid($(2), 3, 8) && snd('mash')
.n([1,2,3].beat(1))
.pan(usine(1/4)).out()
prob(80)::beat(.5) && sound(['hh', 'hat'].pick()).out()
`,
true
)}
true,
)}

View File

@ -22,12 +22,12 @@ export const linear_time = (app: Editor): string => {
There is a tiny widget at the bottom right of the screen showing you the current BPM and the status of the transport. You can turn it on or off in the settings menu.
${makeExample(
"Printing the transport",
`
"Printing the transport",
`
log(\`\$\{cbar()}\, \$\{cbeat()\}, \$\{cpulse()\}\`)
`,
true
)}
true,
)}
### BPM and PPQN
@ -64,8 +64,8 @@ These values are **extremely useful** to craft more complex syntax or to write m
You can use time primitives as conditionals. The following example will play a pattern A for 2 bars and a pattern B for 2 bars:
${makeExample(
"Manual mode: using time primitives!",
`
"Manual mode: using time primitives!",
`
// Manual time condition
if((cbar() % 4) > 1) {
beat(2) && sound('kick').out()
@ -83,8 +83,8 @@ 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
@ -94,8 +94,8 @@ Time generally flows from the past to the future. However, you can manipulate it
${makeExample(
"Time is now super elastic!",
`
"Time is now super elastic!",
`
// Obscure Shenanigans - Bubobubobubo
beat([1/4,1/8,1/16].beat(8)):: sound('sine')
.freq([100,50].beat(16) + 50 * ($(1)%10))
@ -108,14 +108,14 @@ flip(3) :: beat([.25,.5].beat(.5)) :: sound('dr')
// Jumping back and forth in time
beat(.25) :: warp([12, 48, 24, 1, 120, 30].pick())
`,
true
)}
true,
)}
- <ic>beat_warp(beat: number)</ic>: this function jumps to the _n_ beat of the clock. The first beat is <ic>1</ic>.
${makeExample(
"Jumping back and forth with beats",
`
"Jumping back and forth with beats",
`
// Resonance bliss - Bubobubobubo
beat(.25)::snd('arpy')
.note(30 + [0,3,7,10].beat())
@ -130,40 +130,40 @@ 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
- <ic>onbeat(...n: number[])</ic>: The <ic>onbeat</ic> function allows you to lock on to a specific beat from the clock to execute code. It can accept multiple arguments. It's usage is very straightforward and not hard to understand. You can pass either integers or floating point numbers. By default, topos is using a <ic>4/4</ic> bar meaning that you can target any of these beats (or in-between) with this function.
${makeExample(
"Some simple yet detailed rhythms",
`
"Some simple yet detailed rhythms",
`
onbeat(1,2,3,4)::snd('kick').out() // Bassdrum on each beat
onbeat(2,4)::snd('snare').n([8,4].beat(4)).out() // Snare on acccentuated beats
onbeat(1.5,2.5,3.5, 3.75)::snd('hat').gain(r(0.9,1.1)).out() // Cool high-hats
`,
true
)}
true,
)}
${makeExample(
"Let's do something more complex",
`
"Let's do something more complex",
`
onbeat(0.5, 2, 3, 3.75)::snd('kick').n(2).out()
onbeat(2, [1.5, 3, 4].pick(), 4)::snd('snare').n(8).out()
beat([.25, 1/8].beat(1.5))::snd('hat').n(2)
.gain(rand(0.4, 0.7)).end(0.05)
.pan(usine()).out()
`,
false
)}
false,
)}
- <ic>oncount(beats: number[], meter: number)</ic>: This function is similar to <ic>onbeat</ic> but it allows you to specify a custom number of beats as the last argument.
${makeExample(
"Using oncount to create more variation in the rhythm",
`
"Using oncount to create more variation in the rhythm",
`
z1('1/16 (0 2 3 4)+(0 2 4 6)').scale('pentatonic').sound('sawtooth')
.cutoff([400,500,1000,2000].beat(1))
.lpadsr(2, 0, .2, 0, 0)
@ -171,20 +171,20 @@ z1('1/16 (0 2 3 4)+(0 2 4 6)').scale('pentatonic').sound('sawtooth')
onbeat(1,1.5,2,3,4) :: sound('bd').gain(2.0).out()
oncount([1,3,5.5,7,7.5,8],8) :: sound('hh').gain(irand(1.0,4.0)).out()
`,
true
)}
true,
)}
${makeExample(
"Using oncount to create rhythms with a custom meter",
`
"Using oncount to create rhythms with a custom meter",
`
bpm(200)
oncount([1, 5, 9, 13],16) :: sound('808bd').n(4).shape(0.5).gain(1.0).out()
oncount([5, 6, 13],16) :: sound('shaker').room(0.25).gain(0.9).out()
oncount([2, 3, 3.5, 6, 7, 10, 15],16) :: sound('hh').n(8).gain(0.8).out()
oncount([1, 4, 5, 8, 9, 10, 11, 12, 13, 14, 15, 16],16) :: sound('hh').out()
`,
true
)}
true,
)}

View File

@ -18,21 +18,21 @@ By default, each script is independant from each other. Scripts live in their ow
**Note:** since this example is running in the documentation, we cannot take advantage of the multiple scripts paradigm. Try to send a variable from the global file to the local file n°6.
${makeExample(
"Setting a global variable",
`
"Setting a global variable",
`
v('my_cool_variable', 2)
`,
true
)}
true,
)}
${makeExample(
"Getting that variable back and printing!",
`
"Getting that variable back and printing!",
`
// Note that we just use one argument
log(v('my_cool_variable'))
`,
false
)}
false,
)}
## Counter and iterators
@ -51,32 +51,32 @@ You will often need to use iterators and/or counters to index over data structur
**Note:** Counters also come with a secret syntax. They can be called with the **$** symbol!
${makeExample(
"Iterating over a list of samples using a counter",
`
"Iterating over a list of samples using a counter",
`
rhythm(.25, 6, 8) :: sound('dr').n($(1)).end(.25).out()
`,
true
)}
true,
)}
${makeExample(
"Using a more complex counter",
`
"Using a more complex counter",
`
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n($(1, 20, 5)).end(.25).out()
`,
false
)}
false,
)}
${makeExample(
"Calling the drunk mechanism",
`
"Calling the drunk mechanism",
`
// Limit is 20, step is 5
rhythm(.25, 6, 8) :: sound('dr').n(drunk()).end(.25).out()
`,
false
)}
false,
)}
`
}
`;
};

View File

@ -1,550 +0,0 @@
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,112 +2,111 @@ 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 {
z(): Player;
z0(): Player;
z1(): Player;
z2(): Player;
z3(): Player;
z4(): Player;
z5(): Player;
z6(): Player;
z7(): Player;
z8(): Player;
z9(): Player;
z10(): Player;
z11(): Player;
z12(): Player;
z13(): Player;
z14(): Player;
z15(): Player;
z16(): Player;
midi(): MidiEvent;
sound(name: string): SoundEvent;
}
interface Number {
z(): Player;
z0(): Player;
z1(): Player;
z2(): Player;
z3(): Player;
z4(): Player;
z5(): Player;
z6(): Player;
z7(): Player;
z8(): Player;
z9(): Player;
z10(): Player;
z11(): Player;
z12(): Player;
z13(): Player;
z14(): Player;
z15(): Player;
z16(): Player;
midi(): MidiEvent;
sound(name: string): SoundEvent | SkipEvent;
}
}
export const makeNumberExtensions = (api: UserAPI) => {
Number.prototype.z0 = function (options: { [key: string]: any } = {}) {
return api.z0(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z0 = function (options: {[key: string]: any} = {}) {
return api.z0(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z1 = function (options: { [key: string]: any } = {}) {
return api.z1(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z1 = function (options: {[key: string]: any} = {}) {
return api.z1(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z2 = function (options: { [key: string]: any } = {}) {
return api.z2(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z2 = function (options: {[key: string]: any} = {}) {
return api.z2(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z3 = function (options: { [key: string]: any } = {}) {
return api.z3(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z3 = function (options: {[key: string]: any} = {}) {
return api.z3(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z4 = function (options: { [key: string]: any } = {}) {
return api.z4(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z4 = function (options: {[key: string]: any} = {}) {
return api.z4(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z5 = function (options: { [key: string]: any } = {}) {
return api.z5(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z5 = function (options: {[key: string]: any} = {}) {
return api.z5(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z6 = function (options: { [key: string]: any } = {}) {
return api.z6(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z6 = function (options: {[key: string]: any} = {}) {
return api.z6(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z7 = function (options: { [key: string]: any } = {}) {
return api.z7(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z7 = function (options: {[key: string]: any} = {}) {
return api.z7(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z8 = function (options: { [key: string]: any } = {}) {
return api.z8(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z8 = function (options: {[key: string]: any} = {}) {
return api.z8(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z9 = function (options: { [key: string]: any } = {}) {
return api.z9(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z9 = function (options: {[key: string]: any} = {}) {
return api.z9(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z10 = function (options: { [key: string]: any } = {}) {
return api.z10(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z10 = function (options: {[key: string]: any} = {}) {
return api.z10(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z11 = function (options: { [key: string]: any } = {}) {
return api.z11(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z11 = function (options: {[key: string]: any} = {}) {
return api.z11(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z12 = function (options: { [key: string]: any } = {}) {
return api.z12(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z12 = function (options: {[key: string]: any} = {}) {
return api.z12(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z13 = function (options: { [key: string]: any } = {}) {
return api.z13(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z13 = function (options: {[key: string]: any} = {}) {
return api.z13(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z14 = function (options: { [key: string]: any } = {}) {
return api.z14(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z14 = function (options: {[key: string]: any} = {}) {
return api.z14(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z15 = function (options: { [key: string]: any } = {}) {
return api.z15(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z15 = function (options: {[key: string]: any} = {}) {
return api.z15(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z16 = function (options: { [key: string]: any } = {}) {
return api.z16(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.z16 = function (options: {[key: string]: any} = {}) {
return api.z16(this.valueOf().toString().split("").join(" "), options);
};
Number.prototype.midi = function (...kwargs: any[]) {
return api.midi(this.valueOf(), ...kwargs);
};
Number.prototype.midi = function (...kwargs: any[]) {
return api.midi(this.valueOf(), ...kwargs);
Number.prototype.sound = function (name: string): SoundEvent | SkipEvent {
if (Number.isInteger(this.valueOf())) {
return (api.sound(name) as SoundEvent).note(this.valueOf());
} else {
return (api.sound(name) as SoundEvent).freq(this.valueOf());
}
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

@ -5,226 +5,227 @@ export {};
// Extend String prototype
declare global {
interface String {
speak(): void;
rate(speed: number): string;
pitch(pitch: number): string;
volume(volume: number): string;
voice(voice: number): string;
lang(language: string): string;
options(): SpeechOptions;
z(): Player;
z0(): Player;
z1(): Player;
z2(): Player;
z3(): Player;
z4(): Player;
z5(): Player;
z6(): Player;
z7(): Player;
z8(): Player;
z9(): Player;
z10(): Player;
z11(): Player;
z12(): Player;
z13(): Player;
z14(): Player;
z15(): Player;
z16(): Player;
note(): number;
}
interface String {
speak(): void;
rate(speed: number): string;
pitch(pitch: number): string;
volume(volume: number): string;
voice(voice: number): string;
lang(language: string): string;
options(): SpeechOptions;
z(): Player;
z0(): Player;
z1(): Player;
z2(): Player;
z3(): Player;
z4(): Player;
z5(): Player;
z6(): Player;
z7(): Player;
z8(): Player;
z9(): Player;
z10(): Player;
z11(): Player;
z12(): Player;
z13(): Player;
z14(): Player;
z15(): Player;
z16(): Player;
note(): number;
}
}
const isJsonString = (str: string):boolean => {
return str[0] === '{' && str[str.length - 1] === '}'
}
const isJsonString = (str: string): boolean => {
return str[0] === "{" && str[str.length - 1] === "}";
};
const stringObject = (str: string, params: object) => {
if(isJsonString(str)) {
const obj = JSON.parse(str);
return JSON.stringify({...obj, ...params});
} else {
return JSON.stringify({...params, text: str});
}
}
if (isJsonString(str)) {
const obj = JSON.parse(str);
return JSON.stringify({ ...obj, ...params });
} 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(() => {
// Done
}).catch((e) => {
console.log("Error speaking:", e);
});
};
String.prototype.speak = function () {
const options = JSON.parse(this.valueOf());
new Speaker({ ...options, text: options.text })
.speak()
.then(() => {
// Done
})
.catch((e) => {
console.log("Error speaking:", e);
});
};
String.prototype.rate = function (speed: number) {
return stringObject(this.valueOf(), {rate: speed});
};
String.prototype.rate = function (speed: number) {
return stringObject(this.valueOf(), { rate: speed });
};
String.prototype.pitch = function (pitch: number) {
return stringObject(this.valueOf(), {pitch: pitch});
};
String.prototype.pitch = function (pitch: number) {
return stringObject(this.valueOf(), { pitch: pitch });
};
String.prototype.lang = function (language: string) {
return stringObject(this.valueOf(),{lang: language});
};
String.prototype.lang = function (language: string) {
return stringObject(this.valueOf(), { lang: language });
};
String.prototype.volume = function (volume: number) {
return stringObject(this.valueOf(), {volume: volume});
};
String.prototype.volume = function (volume: number) {
return stringObject(this.valueOf(), { volume: volume });
};
String.prototype.voice = function (voice: number) {
return stringObject(this.valueOf(), {voice: voice});
};
String.prototype.voice = function (voice: number) {
return stringObject(this.valueOf(), { voice: voice });
};
String.prototype.z = function (options: {[key: string]: any} = {}) {
return api.z(this.valueOf(), options);
};
String.prototype.z0 = function (options: {[key: string]: any} = {}) {
return api.z0(this.valueOf(), options);
};
String.prototype.z = function (options: { [key: string]: any } = {}) {
return api.z(this.valueOf(), options);
};
String.prototype.z1 = function (options: {[key: string]: any} = {}) {
return api.z1(this.valueOf(), options);
};
String.prototype.z0 = function (options: { [key: string]: any } = {}) {
return api.z0(this.valueOf(), options);
};
String.prototype.z2 = function (options: {[key: string]: any} = {}) {
return api.z2(this.valueOf(), options);
};
String.prototype.z1 = function (options: { [key: string]: any } = {}) {
return api.z1(this.valueOf(), options);
};
String.prototype.z3 = function (options: {[key: string]: any} = {}) {
return api.z3(this.valueOf(), options);
};
String.prototype.z2 = function (options: { [key: string]: any } = {}) {
return api.z2(this.valueOf(), options);
};
String.prototype.z4 = function (options: {[key: string]: any} = {}) {
return api.z4(this.valueOf(), options);
};
String.prototype.z3 = function (options: { [key: string]: any } = {}) {
return api.z3(this.valueOf(), options);
};
String.prototype.z5 = function (options: {[key: string]: any} = {}) {
return api.z5(this.valueOf(), options);
};
String.prototype.z4 = function (options: { [key: string]: any } = {}) {
return api.z4(this.valueOf(), options);
};
String.prototype.z6 = function (options: {[key: string]: any} = {}) {
return api.z6(this.valueOf(), options);
};
String.prototype.z5 = function (options: { [key: string]: any } = {}) {
return api.z5(this.valueOf(), options);
};
String.prototype.z7 = function (options: {[key: string]: any} = {}) {
return api.z7(this.valueOf(), options);
};
String.prototype.z6 = function (options: { [key: string]: any } = {}) {
return api.z6(this.valueOf(), options);
};
String.prototype.z8 = function (options: {[key: string]: any} = {}) {
return api.z8(this.valueOf(), options);
};
String.prototype.z7 = function (options: { [key: string]: any } = {}) {
return api.z7(this.valueOf(), options);
};
String.prototype.z9 = function (options: {[key: string]: any} = {}) {
return api.z9(this.valueOf(), options);
};
String.prototype.z8 = function (options: { [key: string]: any } = {}) {
return api.z8(this.valueOf(), options);
};
String.prototype.z10 = function (options: {[key: string]: any} = {}) {
return api.z10(this.valueOf(), options);
};
String.prototype.z9 = function (options: { [key: string]: any } = {}) {
return api.z9(this.valueOf(), options);
};
String.prototype.z11 = function (options: {[key: string]: any} = {}) {
return api.z11(this.valueOf(), options);
};
String.prototype.z10 = function (options: { [key: string]: any } = {}) {
return api.z10(this.valueOf(), options);
};
String.prototype.z12 = function (options: {[key: string]: any} = {}) {
return api.z12(this.valueOf(), options);
};
String.prototype.z11 = function (options: { [key: string]: any } = {}) {
return api.z11(this.valueOf(), options);
};
String.prototype.z13 = function (options: {[key: string]: any} = {}) {
return api.z13(this.valueOf(), options);
};
String.prototype.z12 = function (options: { [key: string]: any } = {}) {
return api.z12(this.valueOf(), options);
};
String.prototype.z14 = function (options: {[key: string]: any} = {}) {
return api.z14(this.valueOf(), options);
};
String.prototype.z13 = function (options: { [key: string]: any } = {}) {
return api.z13(this.valueOf(), options);
};
String.prototype.z15 = function (options: {[key: string]: any} = {}) {
return api.z15(this.valueOf(), options);
};
String.prototype.z14 = function (options: { [key: string]: any } = {}) {
return api.z14(this.valueOf(), options);
};
String.prototype.z16 = function (options: {[key: string]: any} = {}) {
return api.z16(this.valueOf(), options);
};
String.prototype.z15 = function (options: { [key: string]: any } = {}) {
return api.z15(this.valueOf(), options);
};
String.prototype.note = function () {
try {
return parseInt(this.valueOf());
} catch (e) {
return noteNameToMidi(this.valueOf());
}
};
}
String.prototype.z16 = function (options: { [key: string]: any } = {}) {
return api.z16(this.valueOf(), options);
};
String.prototype.note = function () {
try {
return parseInt(this.valueOf());
} catch (e) {
return noteNameToMidi(this.valueOf());
}
};
};
type SpeechOptions = {
text?: string;
rate?: number;
pitch?: number;
volume?: number;
voice?: number;
lang?: string;
}
text?: string;
rate?: number;
pitch?: number;
volume?: number;
voice?: number;
lang?: string;
};
let speakerTimeout: number;
export class Speaker {
constructor(
public options: SpeechOptions
) {}
speak = () => {
return new Promise<void>((resolve, reject) => {
if (this.options.text) {
const synth = window.speechSynthesis;
if(synth.speaking) synth.cancel();
constructor(public options: SpeechOptions) {}
const utterance = new SpeechSynthesisUtterance(this.options.text);
utterance.rate = this.options.rate || 1;
utterance.pitch = this.options.pitch || 1;
utterance.volume = this.options.volume || 1;
if (this.options.voice) {
utterance.voice = synth.getVoices()[this.options.voice];
}
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()}`
} else if (this.options.lang.length === 5) {
utterance.lang = this.options.lang;
} else {
// Fallback to en us
utterance.lang = 'en-US';
}
}
speak = () => {
return new Promise<void>((resolve, reject) => {
if (this.options.text) {
const synth = window.speechSynthesis;
if (synth.speaking) synth.cancel();
utterance.onend = () => {
resolve();
};
utterance.onerror = (error) => {
reject(error);
};
if(synth.speaking) {
// Cancel again?
synth.cancel();
// Set timeout
if(speakerTimeout) clearTimeout(speakerTimeout);
// @ts-ignore
speakerTimeout = setTimeout(() => {
synth.speak(utterance);
}, 200);
} else {
synth.speak(utterance);
}
} else {
reject("No text provided");
const utterance = new SpeechSynthesisUtterance(this.options.text);
utterance.rate = this.options.rate || 1;
utterance.pitch = this.options.pitch || 1;
utterance.volume = this.options.volume || 1;
if (this.options.voice) {
utterance.voice = synth.getVoices()[this.options.voice];
}
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()}`;
} else if (this.options.lang.length === 5) {
utterance.lang = this.options.lang;
} else {
// Fallback to en us
utterance.lang = "en-US";
}
}
});
}
utterance.onend = () => {
resolve();
};
utterance.onerror = (error) => {
reject(error);
};
if (synth.speaking) {
// Cancel again?
synth.cancel();
// Set timeout
if (speakerTimeout) clearTimeout(speakerTimeout);
// @ts-ignore
speakerTimeout = setTimeout(() => {
synth.speak(utterance);
}, 200);
} else {
synth.speak(utterance);
}
} else {
reject("No text provided");
}
});
};
}

View File

@ -1,8 +1,10 @@
import { OscilloscopeConfig, runOscilloscope, scriptBlinkers } from "./AudioVisualisation";
import { OscilloscopeConfig, runOscilloscope } from "./Visuals/Oscilloscope";
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 {
initializeSelectedUniverse,
AppSettings,
@ -46,6 +48,7 @@ export class Editor {
// Editor logic
editor_mode: "global" | "local" | "init" | "notes" = "global";
hidden_interface: boolean = false;
fontSize!: Compartment;
withLineNumbers!: Compartment;
vimModeCompartment!: Compartment;
@ -91,11 +94,27 @@ 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
// ================================================================================
@ -189,6 +208,11 @@ export class Editor {
}
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]
@ -216,24 +240,27 @@ 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-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;
@ -245,10 +272,10 @@ export class Editor {
item
.querySelector(".delete-universe")
?.addEventListener("click", () =>
api._deleteUniverseFromInterface(it)
api._deleteUniverseFromInterface(it),
);
return item;
})
}),
);
existing_universes.innerHTML = "";
@ -256,7 +283,13 @@ export class Editor {
};
changeToLocalBuffer(i: number) {
// Updating the CSS accordingly
/**
* Changes the local buffer based on the provided index.
* Updates the CSS accordingly by adding a specific class to the selected tab and removing it from other tabs.
* Updates the local index and updates the editor view.
*
* @param i The index of the tab to change the local buffer to.
*/
const tabs = document.querySelectorAll('[id^="tab-"]');
const tab = tabs[i] as HTMLElement;
tab.classList.add("bg-orange-300");
@ -269,6 +302,11 @@ 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,
@ -329,7 +367,7 @@ export class Editor {
this.view.dispatch({
effects: this.chosenLanguage.reconfigure(
this.editor_mode == "notes" ? [markdown()] : [javascript()]
this.editor_mode == "notes" ? [markdown()] : [javascript()],
),
});
@ -338,8 +376,14 @@ 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") {
@ -386,7 +430,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");
@ -424,36 +468,36 @@ export class Editor {
}
}
/**
* 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 {
/**
* 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.
*/
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);
}
@ -461,7 +505,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];
}
}
@ -469,12 +513,18 @@ 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;
@ -482,13 +532,16 @@ export class Editor {
console.log("Hydra loaded successfully");
this.initializeHydra();
};
script.onerror = function() {
script.onerror = function () {
console.error("Error loading Hydra script");
};
document.head.appendChild(script);
}
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,
@ -496,18 +549,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);
}

View File

@ -22,7 +22,7 @@
::before,
::after {
--tw-content: '';
--tw-content: "";
}
/*
@ -42,9 +42,23 @@ html {
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
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 */
@ -85,7 +99,7 @@ Add the correct text decoration in Chrome, Edge, and Safari.
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
@ -129,7 +143,8 @@ 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 */
@ -224,9 +239,9 @@ select {
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
@ -273,7 +288,7 @@ Correct the cursor style of increment and decrement buttons in Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
[type="search"] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
@ -366,7 +381,8 @@ 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;
@ -434,7 +450,9 @@ video {
display: none;
}
*, ::before, ::after {
*,
::before,
::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
@ -538,7 +556,7 @@ video {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #F0F0F0;
background: #f0f0f0;
}
.hljs,
@ -583,11 +601,11 @@ video {
.hljs-link,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #BC6060;
color: #bc6060;
}
.hljs-literal {
color: #78A960;
color: #78a960;
}
.hljs-built_in,
@ -886,8 +904,8 @@ video {
.appearance-none {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-moz-appearance: none;
appearance: none;
}
.flex-row {
@ -1333,8 +1351,10 @@ 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 {
@ -1350,11 +1370,14 @@ 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;
}
@ -1399,15 +1422,21 @@ 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 {
@ -1431,7 +1460,7 @@ video {
@media (prefers-reduced-motion: no-preference) {
@keyframes pulse {
50% {
opacity: .5;
opacity: 0.5;
}
}

View File

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

View File

@ -46,10 +46,10 @@ export const toposDarkTheme = EditorView.theme(
borderLeftColor: cursor,
},
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
{
backgroundColor: base00,
border: `0.5px solid ${base00}`,
},
{
backgroundColor: base00,
border: `0.5px solid ${base00}`,
},
".cm-panels": {
backgroundColor: darkBackground,
color: base05,
@ -113,7 +113,7 @@ export const toposDarkTheme = EditorView.theme(
},
},
},
{ dark: true }
{ dark: true },
);
/// The highlighting style for code in the Material Dark theme.

File diff suppressed because it is too large Load Diff

BIN
topos_frog.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

View File

@ -15,7 +15,7 @@
/* Linting */
"strict": true,
"forceConsistentCasingInFileNames": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": true,
"noUnusedLocals": true,
"noUnusedParameters": true,

View File

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

161
yarn.lock
View File

@ -1362,6 +1362,90 @@
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"
@ -1885,7 +1969,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.3, debug@^4.3.4:
debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, 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==
@ -2718,6 +2802,11 @@ 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"
@ -2814,6 +2903,11 @@ 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"
@ -2821,6 +2915,11 @@ 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"
@ -2873,6 +2972,18 @@ 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"
@ -3149,6 +3260,26 @@ 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"
@ -3193,6 +3324,11 @@ 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"
@ -3709,6 +3845,11 @@ 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"
@ -3872,6 +4013,11 @@ 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"
@ -3882,10 +4028,15 @@ yaml@^2.1.1:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b"
integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==
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==
zifferjs@^0.0.47:
version "0.0.47"
resolved "https://registry.yarnpkg.com/zifferjs/-/zifferjs-0.0.47.tgz#393cfe235187e80e970b7281e29c9e4813184f07"
integrity sha512-gc5H9QNuPysiB5zqjXkMfempDf08ydA+gVPPm9sQKifmoc7GtjJQ0mU7TNc1BAGPI2ipJcIop1a+r71y5SbQmQ==
zyklus@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/zyklus/-/zyklus-0.1.4.tgz#229b2966fd1126ef72c6004697269118762bdcd5"
integrity sha512-hbv2cyy4nOI7P8nL8b3ki1jswoLzkUzewPgCLDdDfABryDkV5iO8DAbU25OgO5ShRZHLjXJIylwv5PJQPl3Mpw==
zzfx@^1.2.0:
version "1.2.0"