8 Commits

Author SHA1 Message Date
fbf32d7946 Added check for negative deviation 2023-11-25 09:10:58 +02:00
69c24f6a15 Changed clock to use performance.now() 2023-11-24 22:22:17 +02:00
f3ddb39ab6 Reintroduce nudge 2023-11-20 11:12:16 +01:00
8b744e3d90 Delete TransportNode and TransportProcessor 2023-11-20 11:04:15 +01:00
424adeebc0 Cleaning Clock file 2023-11-20 11:03:46 +01:00
ed8bc21713 work in progress 2023-11-20 02:29:47 +01:00
9b7f980027 working on clock 2023-11-20 01:42:44 +01:00
05382bab6e tiny correction 2023-11-19 23:43:18 +01:00
128 changed files with 4927 additions and 18867 deletions

View File

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

View File

@ -47,6 +47,9 @@ jobs:
with: with:
path: "main" path: "main"
- name: Copy favicon folder
run: cp -r main/favicon ./dist/
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v3
with: with:

View File

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

View File

@ -1,9 +1,9 @@
# Topos: A Web-Based Algorithmic Sequencer # Topos: A Web-Based Algorithmic Sequencer
<p align="center"> | <p align="center"> |
<a href="https://discord.gg/aPgV7mSFZh">Discord</a> | <a href="https://discord.gg/aPgV7mSFZh">Discord</a> |
<a href="https://raphaelforment.fr/">BuboBubo</a> | <a href="https://raphaelforment.fr/">BuboBubo</a> | 
<a href="https://github.com/amiika">Amiika</a> | <a href="about:blank">Amiika</a> |
<a href="https://toplap.org/">About Live Coding</a> | <a href="https://toplap.org/">About Live Coding</a> |
<br><br> <br><br>
<h2 align="center"><b>Contributors</b></h2> <h2 align="center"><b>Contributors</b></h2>
@ -12,90 +12,57 @@
<img src="https://contrib.rocks/image?repo=bubobubobubobubo/Topos" /> <img src="https://contrib.rocks/image?repo=bubobubobubobubo/Topos" />
</a> </a>
</p> </p>
<p align="center">
<a href='https://ko-fi.com/I2I2RSBHF' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi3.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
</p>
</p> </p>
--- Topos is a web-based live coding environment. It lives [here](https://topos.live). Documentation is directly embedded in the application itself. Topos is an emulation and extension of the [Monome Teletype](https://monome.org/docs/teletype/) that gradually evolved into something a bit more personal.
Topos is a web based live coding environment designed to be installation-free, independant and fun. Topos is loosely based on the [Monome Teletype](https://monome.org/docs/teletype/). The application follows the same operating principle, but adapts it to the rich multimedia context offered by web browsers. Topos is capable of many things: ![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/img/topos_gif.gif)
- it is a generative/algorithmic 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/),
oscilloscopes, frequency visualizers and image/canvas sequencing capabilities
- it can be used to sequence other MIDI and OSC devices (the latter using a **NodeJS** script)
- it is made to be used without the need of installing anything, always ready at
[https://topos.live](https://topos.live)
---
![Screenshot](https://github.com/Bubobubobubobubo/Topos/blob/main/src/assets/topos_gif.gif)
## Disclaimer ## Disclaimer
**Topos** is still a young and experimental 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. Note that most features are rather experimental and that we don't really have any classical training in web development. **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.
## Local Installation (for devs and contributors) ## Installation (for devs and contributors)
To run the application, you will need to install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/en/). Then, clone the repository and run: To run the application, you will need to install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/en/). Then, clone the repository and run:
- `yarn install` - `yarn install`
- `yarn run dev` - `yarn run dev`
You are good to go. The application will update itself automatically with every change to the codebase. To test the production version of the applicationn, you will need to install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/en/). Then, clone the repository and run: To build the application for production, you will need to install [Node.js](https://nodejs.org/en/) and [Yarn](https://yarnpkg.com/en/). Then, clone the repository and run:
- `yarn run build` - `yarn run build`
- `yarn run start` - `yarn run start`
If the build passes, you can be sure that it will also pass our **CI** pipeline that deploys the application to [https://topos.live](https://topos.live). Always run a build before committing to check for compiler errors. The automatic deployment on the `main` branch will not accept compiler errors! Always run a build before committing to check for compiler errors. The automatic deployment on the `main` branch will not accept compiler errors!
## Tauri version To build a standalone browser application using [Tauri](https://tauri.app/), you will need to have [Node.js](https://nodejs.org/en/), [Yarn](https://yarnpkg.com/en/) and [Rust](https://www.rust-lang.org/) installed. Then, clone the repository and run:
Topos can also be compiled as a standalone application using [Tauri](https://tauri.app/). You will need [Node.js](https://nodejs.org/en/), [Yarn](https://yarnpkg.com/en/) and [Rust](https://www.rust-lang.org/) to be installed on your computer. Then, clone the repository and run:
- `yarn tauri build` - `yarn tauri build`
- `yarn tauri dev` - `yarn tauri dev`
The `tauri` version has never been fleshed out. It's a template for later developments if Topos ever wants to escape from the web :) The `tauri` version is only here to quickstart future developments but nothing has been done yet.
## Docker ## Docker
To run the **Docker** version, run the following command: ### Run the application
`docker run -p 8001:80 yassinsiouda/topos:latest`
`docker run -p 8001:80 bubobubobubo/topos:latest`
### Build and run the prod image ### Build and run the prod image
`docker compose --profile prod up` `docker compose --profile prod up`
### Build and run the dev image ### Build and run the dev image
First you need to map `node_modules` to your local machine for your IDE IntelliSense to work properly : **First installation**
First you need to map node_modules to your local machine for your ide intellisense to work properly
```bash ```bash
docker compose --profile dev up -d docker compose --profile dev up -d
docker cp topos-dev:/app/node_modules . docker cp topos-dev:/app/node_modules .
docker compose --profile dev down docker compose --profile dev down
``` ```
then run the following command: **Then**
```bash ```bash
docker compose --profile dev up docker compose --profile dev up
``` ```
Note that a Docker version of Topos is automatically generated everytime a commit is done on the `main` branch.
## Credits
- Felix Roos for the [SuperDough](https://www.npmjs.com/package/superdough) audio engine.
- Frank Force for the [ZzFX](https://github.com/KilledByAPixel/ZzFX) synthesizer.
- Kristoffer Ekstrand for the [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) waveforms.
- Ryan Kirkbride for some of the audio samples in the [Dough-Fox](https://github.com/Bubobubobubobubo/Dough-Fox) sample pack, taken from [here](https://github.com/Qirky/FoxDot/tree/master/FoxDot/snd).
- Adel Faure for the [JGS](https://adelfaure.net/https://adelfaure.net/) font.
- Raphaël Bastide for the [Steps Mono](https://github.com/raphaelbastide/steps-mono/) font.
Many thanks to all the contributors and folks who tried the software already :)

View File

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

View File

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

View File

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

View File

@ -1,332 +0,0 @@
{
"name": "topos-server",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "topos-server",
"version": "0.0.1",
"license": "GPL-3.0-or-later",
"dependencies": {
"osc": "^2.4.4",
"ws": "^8.17.1"
}
},
"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.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

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

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

19
favicon/site.webmanifest Normal file
View File

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

View File

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

BIN
img/screnshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 KiB

View File

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

24
manifest.webmanifest Normal file
View File

@ -0,0 +1,24 @@
{
"name": "Topos",
"short_name": "Topos",
"description": "Live coding environment",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "./favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "./favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}

View File

@ -14,7 +14,7 @@
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.4.5", "vite": "^4.4.5",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^0.17.4" "vite-plugin-pwa": "^0.16.7"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.1.9", "@codemirror/lang-javascript": "^6.1.9",
@ -30,22 +30,19 @@
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"fflate": "^0.8.0", "fflate": "^0.8.0",
"highlight.js": "^11.9.0",
"jisg": "^0.9.7", "jisg": "^0.9.7",
"lru-cache": "^10.0.1", "lru-cache": "^10.0.1",
"marked": "^7.0.3", "marked": "^7.0.3",
"osc": "^2.4.4",
"postcss": "^8.4.27", "postcss": "^8.4.27",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"showdown-highlight": "^3.1.0", "showdown-highlight": "^3.1.0",
"superdough": "^0.9.12", "superdough": "^0.9.11",
"tailwind-highlightjs": "^2.0.1", "tailwind-highlightjs": "^2.0.1",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"tone": "^14.8.49", "tone": "^14.8.49",
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"vite-plugin-markdown": "^2.1.0", "vite-plugin-markdown": "^2.1.0",
"zifferjs": "^0.0.62", "zifferjs": "^0.0.39",
"zyklus": "^0.1.4",
"zzfx": "^1.2.0" "zzfx": "^1.2.0"
} }
} }

View File

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

View File

@ -1,46 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="48.000000pt" height="48.000000pt" viewBox="0 0 48.000000 48.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,48.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M163 470 c-13 -5 -23 -12 -23 -15 0 -3 46 -5 102 -5 80 0 99 3 90 12
-15 15 -139 21 -169 8z"/>
<path d="M91 391 c-16 -16 -19 -32 -18 -84 1 -53 -2 -69 -18 -87 -15 -16 -20
-38 -22 -83 0 -33 2 -56 5 -50 9 13 44 13 39 -1 -2 -6 -18 -11 -34 -11 -39 0
-50 -10 -33 -30 8 -10 30 -15 60 -15 26 0 64 -7 83 -15 43 -18 125 -20 164 -3
15 6 57 14 93 17 73 7 87 24 51 60 -12 12 -21 17 -21 13 0 -4 5 -13 12 -20 8
-8 8 -12 1 -12 -18 0 -25 34 -9 45 10 7 12 16 6 25 -5 8 -7 24 -4 35 3 13 -2
28 -15 39 -12 11 -21 27 -21 36 0 9 -5 21 -11 27 -8 8 -8 17 0 31 17 32 13 56
-12 80 -31 29 -63 28 -91 -3 -16 -17 -34 -25 -56 -25 -22 0 -40 8 -56 25 -28
30 -67 32 -93 6z m89 -16 c19 -23 5 -29 -24 -10 -16 10 -33 14 -47 10 -14 -5
-19 -4 -15 4 10 16 72 13 86 -4z m211 -7 c12 -22 11 -22 -8 -5 -25 21 -41 22
-65 0 -22 -19 -36 -10 -18 12 20 24 77 19 91 -7z m-264 -30 c-3 -7 -5 -2 -5
12 0 14 2 19 5 13 2 -7 2 -19 0 -25z m229 -5 c-11 -11 -19 6 -11 24 8 17 8 17
12 0 3 -10 2 -21 -1 -24z m-80 -8 c4 -8 10 -12 15 -9 5 3 9 0 9 -6 0 -14 60
-40 77 -33 7 3 13 -2 13 -11 0 -13 -6 -15 -27 -9 -36 9 -210 9 -245 0 -22 -6
-28 -4 -28 9 0 8 6 14 13 11 6 -2 25 2 40 10 15 8 32 11 37 8 6 -4 7 1 3 11
-4 12 -3 15 5 10 7 -4 15 -1 18 8 8 21 63 21 70 1z m83 -91 c10 -9 -37 -33
-77 -39 -55 -8 -117 2 -155 26 l-30 18 54 4 c56 4 201 -2 208 -9z m-286 -30
c-15 -7 21 -44 42 -44 8 0 15 -4 15 -10 0 -16 -33 -11 -59 9 -25 19 -24 52 1
50 9 0 10 -2 1 -5z m355 -21 c2 -14 -4 -23 -17 -28 -12 -3 -21 -13 -21 -21 0
-19 -16 -18 -24 1 -5 15 -31 16 -53 2 -8 -5 -13 -4 -13 2 0 6 5 12 10 12 6 1
15 3 20 4 6 1 18 3 28 4 25 1 66 37 52 44 -6 3 -5 4 2 3 7 -1 14 -12 16 -23z
m-255 -37 c4 -10 1 -13 -9 -9 -7 3 -14 9 -14 14 0 14 17 10 23 -5z m42 -6 c3
-5 1 -10 -4 -10 -6 0 -11 5 -11 10 0 6 2 10 4 10 3 0 8 -4 11 -10z m35 0 c0
-5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m-170
-10 c0 -5 -5 -10 -11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z
m215 -39 c-6 -5 -25 10 -25 20 0 5 6 4 14 -3 8 -7 12 -15 11 -17z m45 8 c0
-14 -16 -11 -29 5 -10 12 -8 13 8 9 12 -3 21 -9 21 -14z m-220 1 c0 -5 -5 -10
-11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z m40 0 c0 -5 -4 -10
-10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m215 0 c3 -5 2
-10 -4 -10 -5 0 -13 5 -16 10 -3 6 -2 10 4 10 5 0 13 -4 16 -10z m43 -10 c7
-11 10 -20 6 -20 -7 0 -34 27 -34 34 0 13 16 5 28 -14z m-160 -17 c-10 -2 -26
-2 -35 0 -10 3 -2 5 17 5 19 0 27 -2 18 -5z m99 1 c-3 -3 -12 -4 -19 -1 -8 3
-5 6 6 6 11 1 17 -2 13 -5z m40 0 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17
-2 13 -5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

View File

@ -1,37 +0,0 @@
{
"name": "Topos",
"short_name": "Topos",
"icons": [
{
"src": "favicon/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "favicon/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"display": "standalone",
"start_url": "/",
"scope": "/",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"description": "Topos is a web based live coding platform",
"screenshots": [
{
"src": "favicon/screenshot_miniature.png",
"sizes": "640x320",
"type": "image/gif",
"form_factor": "wide",
"label": "Topos application"
},
{
"src": "favicon/topos_code.png",
"sizes": "1280x768",
"type": "image/gif",
"label": "Topos code"
}
]
}

1005
src/API.ts

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,7 +1,8 @@
// @ts-ignore // @ts-ignore
import { TransportNode } from "./TransportNode";
import TransportProcessor from "./TransportProcessor?worker&url";
import { Editor } from "./main"; import { Editor } from "./main";
import { tryEvaluate } from "./Evaluator";
const zeroPad = (num: number, places: number) =>
String(num).padStart(places, "0");
export interface TimePosition { export interface TimePosition {
/** /**
@ -23,7 +24,6 @@ export class Clock {
* *
* @param app - The main application instance * @param app - The main application instance
* @param ctx - The current AudioContext used by app * @param ctx - The current AudioContext used by app
* @param transportNode - The TransportNode helper
* @param bpm - The current beats per minute value * @param bpm - The current beats per minute value
* @param time_signature - The time signature * @param time_signature - The time signature
* @param time_position - The current time position * @param time_position - The current time position
@ -33,56 +33,108 @@ export class Clock {
* @param lastPauseTime - The last time the clock was paused * @param lastPauseTime - The last time the clock was paused
* @param lastPlayPressTime - The last time the clock was started * @param lastPlayPressTime - The last time the clock was started
* @param totalPauseTime - The total time the clock has been paused / stopped * @param totalPauseTime - The total time the clock has been paused / stopped
* @param _nudge - The current nudge value
*/ */
lastPulseAt: number;
ctx: AudioContext; afterEvaluation: number;
logicalTime: number;
transportNode: TransportNode | null;
private _bpm: number; private _bpm: number;
time_signature: number[]; time_signature: number[];
time_position: TimePosition; time_position: TimePosition;
private _ppqn: number; private _ppqn: number;
tick: number; tick: number;
running: boolean; running: boolean;
lastPauseTime: number; private timerWorker: Worker | null = null;
lastPlayPressTime: number; _nudge: number;
totalPauseTime: number;
constructor( timeviewer: HTMLElement;
public app: Editor,
ctx: AudioContext, constructor(public app: Editor) {
) { this.timeviewer = document.getElementById("timeviewer")!;
this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.time_signature = [4, 4]; this.time_signature = [4, 4];
this.logicalTime = 0; this.lastPulseAt = 0;
this.afterEvaluation = 0;
this.tick = 0; this.tick = 0;
this._bpm = 120; this._bpm = 120;
this._ppqn = 48; this._ppqn = 48;
this.transportNode = null; this._nudge = 0;
this.ctx = ctx; this.running = false;
this.running = true;
this.lastPauseTime = 0;
this.lastPlayPressTime = 0;
this.totalPauseTime = 0;
ctx.audioWorklet
.addModule(TransportProcessor)
.then((e) => {
this.transportNode = new TransportNode(ctx, {}, this.app);
this.transportNode.connect(ctx.destination);
return e;
})
.catch((e) => {
console.log("Error loading TransportProcessor.js:", e);
});
} }
private initializeWorker(): void {
/**
* 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.
*
* @returns void
*/
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) {
this.lastPulseAt = performance.now();
const futureTimeStamp = this.convertTicksToTimeposition(this.tick);
this.time_position = futureTimeStamp;
if (this.app.settings.send_clock) {
this.app.api.MidiConnection.sendMidiClock();
}
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);
}
this.afterEvaluation = performance.now();
console.log("DEVIATION", this.deviation);
this.tick++;
}
};
convertTicksToTimeposition(ticks: number): TimePosition { convertTicksToTimeposition(ticks: number): TimePosition {
/** /**
* Converts ticks to a TimePosition object. * This function converts a number of ticks to a time position.
* @param ticks The number of ticks to convert. * @param ticks - number of ticks
* @returns The TimePosition object representing the converted ticks. * @returns time position
*/ */
const beatsPerBar = this.app.clock.time_signature[0]; const beatsPerBar = this.app.clock.time_signature[0];
const ppqnPosition = ticks % this.app.clock.ppqn; const ppqnPosition = ticks % this.app.clock.ppqn;
const beatNumber = Math.floor(ticks / this.app.clock.ppqn); const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
@ -116,6 +168,8 @@ export class Clock {
get beats_per_bar(): number { get beats_per_bar(): number {
/** /**
* Returns the number of beats per bar. * Returns the number of beats per bar.
*
* @returns number of beats per bar
*/ */
return this.time_signature[0]; return this.time_signature[0];
} }
@ -141,58 +195,121 @@ export class Clock {
get pulse_duration(): number { get pulse_duration(): number {
/** /**
* Returns the duration of a pulse in seconds. * Returns the duration of a pulse in seconds.
*
* @returns duration of a pulse in seconds
*/ */
return 60 / this.bpm / this.ppqn; return 60 / this._bpm / this.ppqn;
} }
public pulse_duration_at_bpm(bpm: number = this.bpm): number { public pulse_duration_at_bpm(bpm: number = this.bpm): number {
/** /**
* Returns the duration of a pulse in seconds at a specific bpm. * Returns the duration of a pulse in seconds at a specific bpm.
*
* @param bpm - beats per minute
* @returns duration of a pulse in seconds
*/ */
return 60 / bpm / this.ppqn; return 60 / bpm / this.ppqn;
} }
get bpm(): number { get bpm(): number {
/**
* Returns the current BPM.
*
* @returns current BPM
*/
return this._bpm; return this._bpm;
} }
set nudge(nudge: number) { set nudge(nudge: number) {
this.transportNode?.setNudge(nudge); /**
* Sets the nudge.
*
* @param nudge - nudge in seconds
* @returns void
*/
this._nudge = nudge;
}
get nudge(): number {
/**
* Returns the current nudge.
*
* @returns current nudge
*/
return this._nudge;
} }
set bpm(bpm: number) { set bpm(bpm: number) {
/**
* Sets the BPM.
*
* @param bpm - beats per minute
* @returns void
*/
if (bpm > 0 && this._bpm !== bpm) { if (bpm > 0 && this._bpm !== bpm) {
this.transportNode?.setBPM(bpm);
this._bpm = bpm; this._bpm = bpm;
this.logicalTime = this.realTime;
// Restart the worker with the new BPM if the clock is running
if (this.running) {
this.restartWorker();
}
} }
} }
private restartWorker(): void {
/**
* Restarts the worker responsible for sending clock pulses.
*
* @returns void
*/
if (this.timerWorker) {
this.timerWorker.terminate();
}
this.initializeWorker();
this.setWorkerInterval();
}
get ppqn(): number { get ppqn(): number {
/**
* Returns the current PPQN.
*
* @returns current PPQN
*/
return this._ppqn; return this._ppqn;
} }
get realTime(): number { get realTime(): number {
return this.app.audioContext.currentTime - this.totalPauseTime; /**
* 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.lastPulseAt;
} }
get deviation(): number { get deviation(): number {
return Math.abs(this.logicalTime - this.realTime); /**
* Returns the deviation between the logical time and the real time.
*
* @returns deviation between the logical time and the real time
*/
if(this.afterEvaluation<this.lastPulseAt) return 0;
return (this.afterEvaluation - this.lastPulseAt) / 1000;
} }
set ppqn(ppqn: number) { set ppqn(ppqn: number) {
/**
* Sets the PPQN.
*
* @param ppqn - pulses per quarter note
* @returns void
*/
if (ppqn > 0 && this._ppqn !== ppqn) { if (ppqn > 0 && this._ppqn !== ppqn) {
this._ppqn = ppqn; this._ppqn = ppqn;
this.transportNode?.setPPQN(ppqn);
this.logicalTime = this.realTime;
} }
} }
public incrementTick(bpm: number) {
this.tick++;
this.logicalTime += this.pulse_duration_at_bpm(bpm);
}
public nextTickFrom(time: number, nudge: number): number { public nextTickFrom(time: number, nudge: number): number {
/** /**
* Compute the time remaining before the next clock tick. * Compute the time remaining before the next clock tick.
@ -210,50 +327,68 @@ export class Clock {
public convertPulseToSecond(n: number): number { public convertPulseToSecond(n: number): number {
/** /**
* Converts a pulse to a second. * Converts a number of pulses to a number of seconds.
*
* @param n - number of pulses
* @returns number of seconds
*/ */
return n * this.pulse_duration; return n * this.pulse_duration;
} }
public start(): void { public start(): void {
/** /**
* Starts the TransportNode (starts the clock). * This function starts the worker.
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
* @returns void
*/ */
this.app.audioContext.resume(); if (this.running) {
return;
}
this.running = true; this.running = true;
this.app.api.MidiConnection.sendStartMessage(); this.app.api.MidiConnection.sendStartMessage();
this.lastPlayPressTime = this.app.audioContext.currentTime; this.lastPulseAt = 0;
this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime; this.afterEvaluation = 0;
this.transportNode?.start();
if (!this.timerWorker) {
this.initializeWorker();
}
this.setWorkerInterval();
} }
public pause(): void { public pause(): void {
/** /**
* Pauses the TransportNode (pauses the clock). * Pauses the Transport worker.
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
* @returns void
*/ */
this.running = false; this.running = false;
this.transportNode?.pause();
this.app.api.MidiConnection.sendStopMessage(); this.app.api.MidiConnection.sendStopMessage();
this.lastPauseTime = this.app.audioContext.currentTime; if (this.timerWorker) {
this.logicalTime = this.realTime; this.timerWorker.terminate();
this.timerWorker = null;
}
} }
public stop(): void { public stop(): void {
/** /**
* Stops the TransportNode (stops the clock). * Stops the Transport worker and resets the tick to 0. The time position
* is also reset to 0. The clock is stopped by terminating the worker
* responsible for sending clock pulses.
* *
* @remark also sends a MIDI message if a port is declared * @remark also sends a MIDI message if a port is declared
* @returns void
*/ */
this.running = false; this.running = false;
this.tick = 0; this.tick = 0;
this.lastPauseTime = this.app.audioContext.currentTime;
this.logicalTime = this.realTime;
this.time_position = { bar: 0, beat: 0, pulse: 0 }; this.time_position = { bar: 0, beat: 0, pulse: 0 };
this.app.api.MidiConnection.sendStopMessage(); this.app.api.MidiConnection.sendStopMessage();
this.transportNode?.stop(); if (this.timerWorker) {
this.timerWorker.terminate();
this.timerWorker = null;
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
// import { tutorial_universe } from "./universes/tutorial"; // import { tutorial_universe } from "./universes/tutorial";
import { gzipSync, decompressSync, strFromU8 } from "fflate"; import { gzipSync, decompressSync, strFromU8 } from "fflate";
// import { examples } from "./examples/excerpts"; import { examples } from "./examples/excerpts";
import { type Editor } from "./main"; import { type Editor } from "./main";
import { uniqueNamesGenerator, colors, animals } from "unique-names-generator"; import { uniqueNamesGenerator, colors, animals } from "unique-names-generator";
import { tryEvaluate } from "./Evaluator"; import { tryEvaluate } from "./Evaluator";
@ -63,7 +63,7 @@ export interface Settings {
selected_universe: string; selected_universe: string;
line_numbers: boolean; line_numbers: boolean;
time_position: boolean; time_position: boolean;
// load_demo_songs: boolean; load_demo_songs: boolean;
tips: boolean; tips: boolean;
completions: boolean; completions: boolean;
send_clock: boolean; send_clock: boolean;
@ -136,7 +136,7 @@ export class AppSettings {
*/ */
public vimMode: boolean = false; public vimMode: boolean = false;
public theme: string = "Everblush"; public theme: string = "toposTheme";
public font: string = "IBM Plex Mono"; public font: string = "IBM Plex Mono";
public font_size: number = 24; public font_size: number = 24;
public universes: Universes; public universes: Universes;
@ -150,11 +150,11 @@ export class AppSettings {
public midi_clock_input: string | undefined = undefined; public midi_clock_input: string | undefined = undefined;
public default_midi_input: string | undefined = undefined; public default_midi_input: string | undefined = undefined;
public midi_clock_ppqn: number = 24; public midi_clock_ppqn: number = 24;
// public load_demo_songs: boolean = true; public load_demo_songs: boolean = true;
constructor() { constructor() {
const settingsFromStorage = JSON.parse( const settingsFromStorage = JSON.parse(
localStorage.getItem("topos") || "{}", localStorage.getItem("topos") || "{}"
); );
if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) { if (settingsFromStorage && Object.keys(settingsFromStorage).length !== 0) {
@ -174,7 +174,7 @@ export class AppSettings {
this.midi_clock_input = settingsFromStorage.midi_clock_input; this.midi_clock_input = settingsFromStorage.midi_clock_input;
this.midi_clock_ppqn = settingsFromStorage.midi_clock_ppqn || 24; this.midi_clock_ppqn = settingsFromStorage.midi_clock_ppqn || 24;
this.default_midi_input = settingsFromStorage.default_midi_input; this.default_midi_input = settingsFromStorage.default_midi_input;
// this.load_demo_songs = settingsFromStorage.load_demo_songs; this.load_demo_songs = settingsFromStorage.load_demo_songs;
} else { } else {
this.universes = template_universes; this.universes = template_universes;
} }
@ -204,13 +204,13 @@ export class AppSettings {
midi_clock_input: this.midi_clock_input, midi_clock_input: this.midi_clock_input,
midi_clock_ppqn: this.midi_clock_ppqn, midi_clock_ppqn: this.midi_clock_ppqn,
default_midi_input: this.default_midi_input, default_midi_input: this.default_midi_input,
// load_demo_songs: this.load_demo_songs, load_demo_songs: this.load_demo_songs,
}; };
} }
saveApplicationToLocalStorage( saveApplicationToLocalStorage(
universes: Universes, universes: Universes,
settings: Settings, settings: Settings
): void { ): void {
/** /**
* Main method to store the application to local storage. * Main method to store the application to local storage.
@ -232,7 +232,7 @@ export class AppSettings {
this.midi_clock_input = settings.midi_clock_input; this.midi_clock_input = settings.midi_clock_input;
this.midi_clock_ppqn = settings.midi_clock_ppqn; this.midi_clock_ppqn = settings.midi_clock_ppqn;
this.default_midi_input = settings.default_midi_input; this.default_midi_input = settings.default_midi_input;
// this.load_demo_songs = settings.load_demo_songs; this.load_demo_songs = settings.load_demo_songs;
localStorage.setItem("topos", JSON.stringify(this.data)); localStorage.setItem("topos", JSON.stringify(this.data));
} }
} }
@ -245,26 +245,25 @@ export const initializeSelectedUniverse = (app: Editor): void => {
* @param app - The main application * @param app - The main application
* @returns void * @returns void
*/ */
// if (app.settings.load_demo_songs) { if (app.settings.load_demo_songs) {
// let random_example = examples[Math.floor(Math.random() * examples.length)]; let random_example = examples[Math.floor(Math.random() * examples.length)];
// app.selected_universe = "Demo"; app.selected_universe = "Demo";
// app.universes[app.selected_universe] = structuredClone(template_universe);
// app.universes[app.selected_universe].global.committed = random_example;
// app.universes[app.selected_universe].global.candidate = random_example;
// } else {
try {
app.selected_universe = app.settings.selected_universe;
if (app.universes[app.selected_universe] === undefined)
app.universes[app.selected_universe] =
structuredClone(template_universe);
} catch (error) {
app.settings.selected_universe = "Welcome";
app.selected_universe = app.settings.selected_universe;
app.universes[app.selected_universe] = structuredClone(template_universe); app.universes[app.selected_universe] = structuredClone(template_universe);
app.universes[app.selected_universe].global.committed = random_example;
app.universes[app.selected_universe].global.candidate = random_example;
} else {
try {
app.selected_universe = app.settings.selected_universe;
if (app.universes[app.selected_universe] === undefined)
app.universes[app.selected_universe] =
structuredClone(template_universe);
} catch (error) {
app.settings.selected_universe = "Welcome";
app.selected_universe = app.settings.selected_universe;
app.universes[app.selected_universe] = structuredClone(template_universe);
}
} }
( (app.interface.universe_viewer as HTMLInputElement).placeholder! = `${app.selected_universe}`;
app.interface.universe_viewer as HTMLInputElement
).placeholder! = `${app.selected_universe}`;
}; };
export const emptyUrl = () => { export const emptyUrl = () => {
@ -272,11 +271,6 @@ export const emptyUrl = () => {
}; };
export const share = async (app: Editor) => { export const share = async (app: Editor) => {
/**
* Shares the current state of the app by generating a URL with encoded data and copying it to the clipboard.
* @param app - The Editor instance representing the app.
* @returns A Promise that resolves to void.
*/
async function bufferToBase64(buffer: Uint8Array) { async function bufferToBase64(buffer: Uint8Array) {
const base64url: string = await new Promise((r) => { const base64url: string = await new Promise((r) => {
const reader = new FileReader(); const reader = new FileReader();
@ -327,20 +321,8 @@ export const loadUniverserFromUrl = (app: Editor): void => {
export const loadUniverse = ( export const loadUniverse = (
app: Editor, app: Editor,
universeName: string, universeName: string,
universe: Universe = template_universe, universe: Universe = template_universe
): void => { ): void => {
/**
* Loads a universe into the application.
* If the universe does not exist, a fresh clone of the template universe is created and added to the application.
* The references to the selected universe are updated in the application settings.
* The editor view is updated to reflect the selected universe.
* The initialization script for the selected universe is evaluated.
*
* @param app - The Editor application instance.
* @param universeName - The name of the universe to load.
* @param universe - The template universe to clone if the specified universe does not exist.
*/
let selectedUniverse = universeName.trim(); let selectedUniverse = universeName.trim();
if (app.universes[selectedUniverse] === undefined) { if (app.universes[selectedUniverse] === undefined) {
// Pushing a freshly cloned template universe to: // Pushing a freshly cloned template universe to:
@ -352,9 +334,7 @@ export const loadUniverse = (
// Updating references to the currently selected universe // Updating references to the currently selected universe
app.settings.selected_universe = selectedUniverse; app.settings.selected_universe = selectedUniverse;
app.selected_universe = selectedUniverse; app.selected_universe = selectedUniverse;
( (app.interface.universe_viewer as HTMLInputElement).placeholder! = `${selectedUniverse}`;
app.interface.universe_viewer as HTMLInputElement
).placeholder! = `${selectedUniverse}`;
// Updating the editor View to reflect the selected universe // Updating the editor View to reflect the selected universe
app.updateEditorView(); app.updateEditorView();
// Evaluating the initialisation script for the selected universe // Evaluating the initialisation script for the selected universe
@ -362,11 +342,7 @@ export const loadUniverse = (
}; };
export const openUniverseModal = (): void => { export const openUniverseModal = (): void => {
/** // If the modal is hidden, unhide it and hide the editor
* Opens the universe modal.
* If the modal is hidden, it unhides it and hides the editor.
* If the modal is already visible, it closes the modal.
*/
if ( if (
document.getElementById("modal-buffers")!.classList.contains("invisible") document.getElementById("modal-buffers")!.classList.contains("invisible")
) { ) {
@ -379,9 +355,6 @@ export const openUniverseModal = (): void => {
}; };
export const closeUniverseModal = (): void => { export const closeUniverseModal = (): void => {
/**
* Closes the universe modal and performs necessary actions.
*/
// @ts-ignore // @ts-ignore
document.getElementById("buffer-search")!.value = ""; document.getElementById("buffer-search")!.value = "";
document.getElementById("editor")!.classList.remove("invisible"); document.getElementById("editor")!.classList.remove("invisible");
@ -389,9 +362,6 @@ export const closeUniverseModal = (): void => {
}; };
export const openSettingsModal = (): void => { export const openSettingsModal = (): void => {
/**
* Opens the settings modal.
*/
if ( if (
document.getElementById("modal-settings")!.classList.contains("invisible") document.getElementById("modal-settings")!.classList.contains("invisible")
) { ) {
@ -403,9 +373,6 @@ export const openSettingsModal = (): void => {
}; };
export const closeSettingsModal = (): void => { export const closeSettingsModal = (): void => {
/**
* Closes the settings modal and performs necessary actions.
*/
document.getElementById("editor")!.classList.remove("invisible"); document.getElementById("editor")!.classList.remove("invisible");
document.getElementById("modal-settings")!.classList.add("invisible"); document.getElementById("modal-settings")!.classList.add("invisible");
}; };

View File

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

View File

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

View File

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

View File

@ -1,10 +1,8 @@
import { EditorView } from "codemirror"; import { EditorView } from "codemirror";
import { vim } from "@replit/codemirror-vim"; import { vim } from "@replit/codemirror-vim";
import { type Editor } from "./main"; import { type Editor } from "./main";
import colors from "./colors.json";
import { import {
documentation_factory, documentation_factory,
documentation_pages,
hideDocumentation, hideDocumentation,
showDocumentation, showDocumentation,
updateDocumentationContent, updateDocumentationContent,
@ -24,11 +22,18 @@ import { tryEvaluate } from "./Evaluator";
import { inlineHoveringTips } from "./documentation/inlineHelp"; import { inlineHoveringTips } from "./documentation/inlineHelp";
import { lineNumbers } from "@codemirror/view"; import { lineNumbers } from "@codemirror/view";
import { jsCompletions } from "./EditorSetup"; import { jsCompletions } from "./EditorSetup";
import { createDocumentationStyle } from "./DomElements";
import { saveState } from "./WindowBehavior"; import { saveState } from "./WindowBehavior";
import { registerSamplesFromDB, samplesDBConfig, uploadSamplesToDB } from "./IO/SampleLoading";
export const installInterfaceLogic = (app: Editor) => { export const installInterfaceLogic = (app: Editor) => {
// Initialize style // Initialize style
const documentationStyle = createDocumentationStyle(app);
const bindings = Object.keys(documentationStyle).map((key) => ({
type: "output",
regex: new RegExp(`<${key}([^>]*)>`, "g"),
//@ts-ignore
replace: (match, p1) => `<${key} class="${documentationStyle[key]}" ${p1}>`,
}));
(app.interface.line_numbers_checkbox as HTMLInputElement).checked = (app.interface.line_numbers_checkbox as HTMLInputElement).checked =
app.settings.line_numbers; app.settings.line_numbers;
@ -44,19 +49,17 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.midi_channels_scripts; app.settings.midi_channels_scripts;
(app.interface.midi_clock_ppqn as HTMLInputElement).value = (app.interface.midi_clock_ppqn as HTMLInputElement).value =
app.settings.midi_clock_ppqn.toString(); app.settings.midi_clock_ppqn.toString();
// (app.interface.load_demo_songs as HTMLInputElement).checked = (app.interface.load_demo_songs as HTMLInputElement).checked =
// app.settings.load_demo_songs; app.settings.load_demo_songs;
const tabs = document.querySelectorAll('[id^="tab-"]'); const tabs = document.querySelectorAll('[id^="tab-"]');
// Iterate over the tabs with an index // Iterate over the tabs with an index
for (let i = 0; i < tabs.length; i++) { for (let i = 0; i < tabs.length; i++) {
tabs[i].addEventListener("click", (event) => { tabs[i].addEventListener("click", (event) => {
// Updating the CSS accordingly // Updating the CSS accordingly
tabs[i].classList.add("bg-foreground"); tabs[i].classList.add("bg-orange-300");
tabs[i].classList.add("text-selection_foreground");
for (let j = 0; j < tabs.length; j++) { for (let j = 0; j < tabs.length; j++) {
if (j != i) tabs[j].classList.remove("bg-foreground"); if (j != i) tabs[j].classList.remove("bg-orange-300");
if (j != i) tabs[j].classList.remove("text-selection_foreground");
} }
app.currentFile().candidate = app.view.state.doc.toString(); app.currentFile().candidate = app.view.state.doc.toString();
@ -115,59 +118,35 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.universe_viewer.addEventListener("keydown", (event: any) => { app.interface.universe_viewer.addEventListener("keydown", (event: any) => {
if (event.key === "Enter") { if (event.key === "Enter") {
let content = ( let content = (app.interface.universe_viewer as HTMLInputElement).value.trim();
app.interface.universe_viewer as HTMLInputElement
).value.trim();
if (content.length > 2 && content.length < 40) { if (content.length > 2 && content.length < 40) {
if (content !== app.selected_universe) { if (content !== app.selected_universe) {
Object.defineProperty( Object.defineProperty(app.universes, content,
app.universes,
content,
// @ts-ignore // @ts-ignore
Object.getOwnPropertyDescriptor( Object.getOwnPropertyDescriptor(app.universes, app.selected_universe));
app.universes,
app.selected_universe,
),
);
delete app.universes[app.selected_universe]; delete app.universes[app.selected_universe];
} }
app.selected_universe = content; app.selected_universe = content;
loadUniverse(app, app.selected_universe); loadUniverse(app, app.selected_universe);
(app.interface.universe_viewer as HTMLInputElement).placeholder = (app.interface.universe_viewer as HTMLInputElement).placeholder = content;
content; (app.interface.universe_viewer as HTMLInputElement).value = '';
(app.interface.universe_viewer as HTMLInputElement).value = "";
} }
} }
}); });
app.interface.audio_nudge_range.addEventListener("input", () => { app.interface.audio_nudge_range.addEventListener("input", () => {
// TODO: rebuild this app.clock.nudge = parseInt(
// app.clock.nudge = parseInt( (app.interface.audio_nudge_range as HTMLInputElement).value
// (app.interface.audio_nudge_range as HTMLInputElement).value, );
// );
}); });
app.interface.dough_nudge_range.addEventListener("input", () => { app.interface.dough_nudge_range.addEventListener("input", () => {
app.dough_nudge = parseInt( app.dough_nudge = parseInt(
(app.interface.dough_nudge_range as HTMLInputElement).value, (app.interface.dough_nudge_range as HTMLInputElement).value
); );
}); });
app.interface.upload_samples_button.addEventListener("input", async (event) => {
let fileInput = event.target as HTMLInputElement;
if (!fileInput.files?.length) {
return;
}
app.interface.sample_indicator.innerText = "Loading...";
app.interface.sample_indicator.classList.add("animate-pulse");
await uploadSamplesToDB(samplesDBConfig, fileInput.files).then(() => {
registerSamplesFromDB(samplesDBConfig, () => {
app.interface.sample_indicator.innerText = "Import samples";
app.interface.sample_indicator.classList.remove("animate-pulse");
});
});
});
app.interface.upload_universe_button.addEventListener("click", () => { app.interface.upload_universe_button.addEventListener("click", () => {
const fileInput = document.createElement("input"); const fileInput = document.createElement("input");
fileInput.type = "file"; fileInput.type = "file";
@ -248,16 +227,16 @@ export const installInterfaceLogic = (app: Editor) => {
}); });
app.interface.local_button.addEventListener("click", () => app.interface.local_button.addEventListener("click", () =>
app.changeModeFromInterface("local"), app.changeModeFromInterface("local")
); );
app.interface.global_button.addEventListener("click", () => app.interface.global_button.addEventListener("click", () =>
app.changeModeFromInterface("global"), app.changeModeFromInterface("global")
); );
app.interface.init_button.addEventListener("click", () => app.interface.init_button.addEventListener("click", () =>
app.changeModeFromInterface("init"), app.changeModeFromInterface("init")
); );
app.interface.note_button.addEventListener("click", () => app.interface.note_button.addEventListener("click", () =>
app.changeModeFromInterface("notes"), app.changeModeFromInterface("notes")
); );
app.interface.font_family_selector.addEventListener("change", () => { app.interface.font_family_selector.addEventListener("change", () => {
@ -276,7 +255,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px", fontSize: app.settings.font_size + "px",
}, },
".cm-gutters": { fontSize: app.settings.font_size + "px" }, ".cm-gutters": { fontSize: app.settings.font_size + "px" },
}), })
), ),
}); });
}); });
@ -296,58 +275,26 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px", fontSize: app.settings.font_size + "px",
}, },
".cm-gutters": { fontSize: app.settings.font_size + "px" }, ".cm-gutters": { fontSize: app.settings.font_size + "px" },
}), })
), ),
}); });
}); });
app.interface.theme_selector.addEventListener("change", () => {
app.settings.theme = (app.interface.theme_selector as HTMLSelectElement).value;
app.readTheme(app.settings.theme);
// @ts-ignore
let selected_theme = colors[app.settings.theme as string];
let theme_preview = "";
for (const [key, _] of Object.entries(selected_theme)) {
theme_preview += `<p class="inline text-${key} bg-${key}">█</div>`;
}
app.interface.theme_previewer.innerHTML = theme_preview;
});
app.interface.settings_button.addEventListener("click", () => { app.interface.settings_button.addEventListener("click", () => {
// Populate the font selector // Populate the font selector
const fontFamilySelect = document.getElementById( const fontFamilySelect = document.getElementById(
"font-family", "font-family"
) as HTMLSelectElement | null; ) as HTMLSelectElement | null;
if (fontFamilySelect) { if (fontFamilySelect) {
fontFamilySelect.value = app.settings.font; fontFamilySelect.value = app.settings.font;
} }
app.interface.theme_selector.innerHTML = "";
let all_themes = Object.keys(colors);
all_themes.sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
app.interface.theme_selector.innerHTML = all_themes.map((color) => {
return `<option value="${color}">${color}</option>`
}).join("");
// Set the selected theme in the selector to app.settings.theme
// @ts-ignore
app.interface.theme_selector.value = app.settings.theme;
// @ts-ignore
let selected_theme = colors[app.settings.theme as string];
let theme_preview = "<div class='ml-6'>";
for (const [key, _] of Object.entries(selected_theme)) {
theme_preview += `<p class="inline text-${key} bg-${key}">█</p>`;
}
theme_preview += "</div>";
app.interface.theme_previewer.innerHTML = theme_preview;
// Populate the font family selector // Populate the font family selector
const doughNudgeRange = app.interface.dough_nudge_range as HTMLInputElement; const doughNudgeRange = app.interface.dough_nudge_range as HTMLInputElement;
doughNudgeRange.value = app.dough_nudge.toString(); doughNudgeRange.value = app.dough_nudge.toString();
// @ts-ignore // @ts-ignore
const doughNumber = document.getElementById( const doughNumber = document.getElementById(
"doughnumber", "doughnumber"
) as HTMLInputElement; ) as HTMLInputElement;
doughNumber.value = app.dough_nudge.toString(); doughNumber.value = app.dough_nudge.toString();
if (app.settings.font_size === null) { if (app.settings.font_size === null) {
@ -373,8 +320,8 @@ export const installInterfaceLogic = (app: Editor) => {
midiChannelsScripts.checked = app.settings.midi_channels_scripts; midiChannelsScripts.checked = app.settings.midi_channels_scripts;
const midiClockPpqn = app.interface.midi_clock_ppqn as HTMLInputElement; const midiClockPpqn = app.interface.midi_clock_ppqn as HTMLInputElement;
midiClockPpqn.value = app.settings.midi_clock_ppqn.toString(); midiClockPpqn.value = app.settings.midi_clock_ppqn.toString();
// const loadDemoSongs = app.interface.load_demo_songs as HTMLInputElement; const loadDemoSongs = app.interface.load_demo_songs as HTMLInputElement;
// loadDemoSongs.checked = app.settings.load_demo_songs; loadDemoSongs.checked = app.settings.load_demo_songs;
const vimModeCheckbox = app.interface.vim_mode_checkbox as HTMLInputElement; const vimModeCheckbox = app.interface.vim_mode_checkbox as HTMLInputElement;
vimModeCheckbox.checked = app.settings.vimMode; vimModeCheckbox.checked = app.settings.vimMode;
@ -403,7 +350,7 @@ export const installInterfaceLogic = (app: Editor) => {
fontSize: app.settings.font_size + "px", fontSize: app.settings.font_size + "px",
}, },
".cm-gutters": { fontSize: app.settings.font_size + "px" }, ".cm-gutters": { fontSize: app.settings.font_size + "px" },
}), })
), ),
}); });
}); });
@ -461,7 +408,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.tips = checked; app.settings.tips = checked;
app.view.dispatch({ app.view.dispatch({
effects: app.hoveringCompartment.reconfigure( effects: app.hoveringCompartment.reconfigure(
checked ? inlineHoveringTips : [], checked ? inlineHoveringTips : []
), ),
}); });
}); });
@ -474,7 +421,7 @@ export const installInterfaceLogic = (app: Editor) => {
app.settings.completions = checked; app.settings.completions = checked;
app.view.dispatch({ app.view.dispatch({
effects: app.completionsCompartment.reconfigure( effects: app.completionsCompartment.reconfigure(
checked ? jsCompletions : [], checked ? jsCompletions : []
), ),
}); });
}); });
@ -497,17 +444,17 @@ export const installInterfaceLogic = (app: Editor) => {
app.interface.midi_clock_ppqn.addEventListener("change", () => { app.interface.midi_clock_ppqn.addEventListener("change", () => {
let value = parseInt( let value = parseInt(
(app.interface.midi_clock_ppqn as HTMLInputElement).value, (app.interface.midi_clock_ppqn as HTMLInputElement).value
); );
app.settings.midi_clock_ppqn = value; app.settings.midi_clock_ppqn = value;
}); });
// app.interface.load_demo_songs.addEventListener("change", () => { app.interface.load_demo_songs.addEventListener("change", () => {
// let checked = (app.interface.load_demo_songs as HTMLInputElement).checked let checked = (app.interface.load_demo_songs as HTMLInputElement).checked
// ? true ? true
// : false; : false;
// app.settings.load_demo_songs = checked; app.settings.load_demo_songs = checked;
// }); });
app.interface.universe_creator.addEventListener("submit", (event) => { app.interface.universe_creator.addEventListener("submit", (event) => {
event.preventDefault(); event.preventDefault();
@ -530,29 +477,51 @@ export const installInterfaceLogic = (app: Editor) => {
tryEvaluate(app, app.universes[app.selected_universe.toString()].init); tryEvaluate(app, app.universes[app.selected_universe.toString()].init);
documentation_pages.forEach((e) => { [
"introduction",
"sampler",
"amplitude",
"audio_basics",
"reverb_delay",
"interface",
"interaction",
"code",
"time",
"linear",
"cyclic",
"longform",
// "sound",
"synths",
"chaining",
"patterns",
"ziffers",
"midi",
"functions",
"lfos",
"probabilities",
"variables",
"synchronisation",
"mouse",
"shortcuts",
"about",
"bonus",
"oscilloscope",
"sample_list",
"loading_samples",
].forEach((e) => {
let name = `docs_` + e; let name = `docs_` + e;
document.getElementById(name)!.addEventListener("click", async () => {
// Check if the element exists if (name !== "docs_samples") {
let element = document.getElementById(name);
if (element) {
element.addEventListener("click", async () => {
// Clear query params & set id as hash paremeter for uri
window.history.replaceState({}, "", window.location.pathname);
window.location.hash = e;
app.docs = documentation_factory(app);
app.currentDocumentationPane = e; app.currentDocumentationPane = e;
if (name !== "docs_sample_list") { updateDocumentationContent(app, bindings);
updateDocumentationContent(app, app.bindings); } else {
} else { console.log("Loading samples!");
console.log("Loading samples!"); await loadSamples().then(() => {
await loadSamples().then(() => { app.docs = documentation_factory(app);
updateDocumentationContent(app, app.bindings); app.currentDocumentationPane = e;
}); updateDocumentationContent(app, bindings);
} });
}); }
} else { });
console.log("Could not find element " + name);
}
}); });
}; };

View File

@ -26,31 +26,6 @@ export const registerOnKeyDown = (app: Editor) => {
event.preventDefault(); event.preventDefault();
} }
if (event.ctrlKey && event.key === "m") {
event.preventDefault();
let topbar = document.getElementById("topbar");
let sidebar = document.getElementById("sidebar");
console.log("oui ok");
if (app.hidden_interface) {
// Sidebar
sidebar?.classList.remove("flex");
sidebar?.classList.remove("flex-col");
sidebar?.classList.add("hidden");
// Topbar
topbar?.classList.add("hidden");
topbar?.classList.remove("flex");
} else {
// Sidebar
sidebar?.classList.remove("hidden");
sidebar?.classList.add("flex");
sidebar?.classList.add("flex-col");
// Topbar
topbar?.classList.remove("hidden");
topbar?.classList.add("flex");
}
app.hidden_interface = !app.hidden_interface;
}
if (event.ctrlKey && event.key === "s") { if (event.ctrlKey && event.key === "s") {
event.preventDefault(); event.preventDefault();
app.setButtonHighlighting("stop", true); app.setButtonHighlighting("stop", true);
@ -105,18 +80,6 @@ export const registerOnKeyDown = (app: Editor) => {
if (event.key === "Enter" && event.shiftKey && event.ctrlKey) { if (event.key === "Enter" && event.shiftKey && event.ctrlKey) {
event.preventDefault(); event.preventDefault();
app.currentFile().candidate = app.view.state.doc.toString(); app.currentFile().candidate = app.view.state.doc.toString();
app.api.onceEvaluator = true;
app.api.forceEvaluator = true;
tryEvaluate(app, app.currentFile());
app.flashBackground("#404040", 200);
}
// Force eval with clearing cache
if (event.ctrlKey && event.shiftKey && (event.key === "Backspace" || event.key === "Delete")) {
event.preventDefault();
app.api.clearPatternCache();
app.currentFile().candidate = app.view.state.doc.toString();
app.api.forceEvaluator = true;
tryEvaluate(app, app.currentFile()); tryEvaluate(app, app.currentFile());
app.flashBackground("#404040", 200); app.flashBackground("#404040", 200);
} }

View File

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

View File

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

View File

@ -1,88 +1,66 @@
export function objectWithArraysToArrayOfObjects( /*
input: Record<string, any>, * Transforms object with arrays into array of objects
arraysToArrays: string[], *
): Record<string, any>[] { * @param {Record<string, any>} input - Object with arrays
/* * @param {string[]} ignoredKeys - Keys to ignore
* Transforms object with arrays into array of objects * @returns {Record<string, any>[]} Array of objects
* *
* @param {Record<string, any>} input - Object with arrays */
* @param {string[]} ignoredKeys - Keys to ignore export function objectWithArraysToArrayOfObjects(input: Record<string, any>, arraysToArrays: string[]): Record<string, any>[] {
* @returns {Record<string, any>[]} Array of objects 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])) {
const inputCopy = { ...input }; input[k] = [input[k]];
arraysToArrays.forEach((k) => {
if (Array.isArray(inputCopy[k]) && !Array.isArray(inputCopy[k][0])) {
inputCopy[k] = [inputCopy[k]];
}
});
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].push(mergedObj[key]); });
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);
}
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] = [];
}
(acc[key as keyof T] as unknown[]).push(obj[key]);
}); });
return acc; return acc;
}, }, {} as Record<keyof T, any[]>);
{} as Record<string, any>, }
);
}
export function maybeAtomic<T>(value: T): T | T[] { /*
/*
* Returns first value of array if array of length 1, otherwise returns value
* @param {any} value - Value to check
* @returns {any} Value or array
*/
return Array.isArray(value) && value.length === 1 ? value[0] : value;
}
export function filterObject(
obj: Record<string, any>,
filter: string[],
): Record<string, any> {
/*
* Filter certain keys from object * Filter certain keys from object
* *
* @param {Record<string, any>} obj - Object to filter * @param {Record<string, any>} obj - Object to filter
@ -90,21 +68,6 @@ export function filterObject(
* @returns {object} Filtered object * @returns {object} Filtered object
* *
*/ */
return Object.fromEntries( export function filterObject(obj: Record<string, any>, filter: string[]): Record<string, any> {
Object.entries(obj).filter(([key]) => filter.includes(key)), return Object.fromEntries(Object.entries(obj).filter(([key]) => filter.includes(key)));
); }
}
export const maybeToNumber = (something: any): number | any => {
// If something is BigInt
if (typeof something === "bigint") {
return Number(something);
} else {
return something;
}
}
export const GeneratorType = (function*(){yield undefined;}).constructor;
export const GeneratorIteratorType = (function*(){yield undefined;}).prototype.constructor;
export const isGenerator = (v:any) => Object.prototype.toString.call(v) === '[object Generator]';
export const isGeneratorFunction = (v:any) => Object.prototype.toString.call(v) === '[object GeneratorFunction]';

View File

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

View File

@ -1,593 +0,0 @@
export type ShapeObject = {
x: number;
y: number;
x1: number;
y1: number;
x2: number;
y2: number;
radius: number;
width: number;
height: number;
fillStyle: string;
secondary: string;
strokeStyle: string;
rotation: number;
points: number;
outerRadius: number;
eyeSize: number;
happiness: number;
slices: number;
gap: number;
font: string;
fontSize: number;
text: string;
filter: string;
url: string;
curve: number;
curves: number;
stroke: string;
eaten: number;
hole: number;
};
export const drawBackground = (
canvas: HTMLCanvasElement,
color: string | number,
...gb: number[]
): void => {
/**
* Set background color of the canvas.
* @param color - The color to set. String or 3 numbers representing RGB values.
*/
const ctx = canvas.getContext("2d")!;
if (typeof color === "number") color = `rgb(${color},${gb[0]},${gb[1]})`;
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
export const createLinearGradient = (
canvas: HTMLCanvasElement,
x1: number,
y1: number,
x2: number,
y2: number,
...stops: (number | string)[]
): CanvasGradient => {
const ctx = canvas.getContext("2d")!;
const gradient = ctx.createLinearGradient(x1, y1, x2, y2);
// Parse pairs of values from stops
for (let i = 0; i < stops.length; i += 2) {
let color = stops[i + 1];
if (typeof color === "number")
color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`;
gradient.addColorStop(stops[i] as number, color);
}
return gradient;
};
export const createRadialGradient = (
canvas: HTMLCanvasElement,
x1: number,
y1: number,
r1: number,
x2: number,
y2: number,
r2: number,
...stops: (number | string)[]
) => {
/**
* Set radial gradient on the canvas.
* @param x1 - The x-coordinate of the start circle
* @param y1 - The y-coordinate of the start circle
* @param r1 - The radius of the start circle
* @param x2 - The x-coordinate of the end circle
* @param y2 - The y-coordinate of the end circle
* @param r2 - The radius of the end circle
* @param stops - The stops to set. Pairs of numbers representing the position and color of the stop.
*/
const ctx = canvas.getContext("2d")!;
const gradient = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2);
for (let i = 0; i < stops.length; i += 2) {
let color = stops[i + 1];
if (typeof color === "number")
color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`;
gradient.addColorStop(stops[i] as number, color);
}
return gradient;
};
export const createConicGradient = (
canvas: HTMLCanvasElement,
x: number,
y: number,
angle: number,
...stops: (number | string)[]
) => {
/**
* Set conic gradient on the canvas.
* @param x - The x-coordinate of the center of the gradient
* @param y - The y-coordinate of the center of the gradient
* @param angle - The angle of the gradient, in radians
* @param stops - The stops to set. Pairs of numbers representing the position and color of the stop.
*/
const ctx = canvas.getContext("2d")!;
const gradient = ctx.createConicGradient(x, y, angle);
for (let i = 0; i < stops.length; i += 2) {
let color = stops[i + 1];
if (typeof color === "number")
color = `rgb(${color},${stops[i + 2]},${stops[i + 3]})`;
gradient.addColorStop(stops[i] as number, color);
}
return gradient;
};
export const drawGradientImage = (
canvas: HTMLCanvasElement,
time: number = 666
) => {
/* TODO: This works but is really resource heavy. Should do method for requestAnimationFrame? */
const context = canvas.getContext("2d")!;
const { width, height } = context.canvas;
const imageData = context.getImageData(0, 0, width, height);
for (let p = 0; p < imageData.data.length; p += 4) {
const i = p / 4;
const x = i % width;
const y = (i / width) >>> 0;
const red = 64 + (128 * x) / width + 64 * Math.sin(time / 1000);
const green = 64 + (128 * y) / height + 64 * Math.cos(time / 1000);
const blue = 128;
imageData.data[p + 0] = red;
imageData.data[p + 1] = green;
imageData.data[p + 2] = blue;
imageData.data[p + 3] = 255;
}
context.putImageData(imageData, 0, 0);
return true;
};
export const drawBalloid = (
canvas: HTMLCanvasElement,
curves: number,
radius: number,
curve: number,
fillStyle: string,
secondary: string,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
// Draw the shape using quadratic Bézier curves
ctx.beginPath();
ctx.fillStyle = fillStyle;
if (curves === 0) {
// Draw a circle if curves = 0
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
} else if (curves === 1) {
// Draw a single curve (ellipse) if curves = 1
ctx.ellipse(x, y, radius * 0.8, radius * curve * 0.7, 0, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
} else if (curves === 2) {
// Draw a shape with two symmetric curves starting from the top and meeting at the bottom
ctx.moveTo(x, y - radius);
// First curve
ctx.quadraticCurveTo(x + radius * curve, y, x, y + radius);
// Second symmetric curve
ctx.quadraticCurveTo(x - radius * curve, y, x, y - radius);
ctx.closePath();
ctx.fill();
} else {
// Draw the curved shape with the specified number of curves
ctx.moveTo(x, y - radius);
let points = [];
for (let i = 0; i < curves; i++) {
const startAngle = (i / curves) * 2 * Math.PI;
const endAngle = startAngle + (2 * Math.PI) / curves;
const controlX =
x + radius * curve * Math.cos(startAngle + Math.PI / curves);
const controlY =
y + radius * curve * Math.sin(startAngle + Math.PI / curves);
points.push([
x + radius * Math.cos(startAngle),
y + radius * Math.sin(startAngle),
]);
ctx.moveTo(
x + radius * Math.cos(startAngle),
y + radius * Math.sin(startAngle)
);
ctx.quadraticCurveTo(
controlX,
controlY,
x + radius * Math.cos(endAngle),
y + radius * Math.sin(endAngle)
);
}
ctx.closePath();
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.fillStyle = secondary;
// Form the shape from points with straight lines and fill it
ctx.moveTo(points[0][0], points[0][1]);
for (let point of points) ctx.lineTo(point[0], point[1]);
// Close and fill
ctx.closePath();
ctx.fill();
}
};
export const drawEquilateral = (
canvas: HTMLCanvasElement,
radius: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, -radius);
ctx.lineTo(radius, radius);
ctx.lineTo(-radius, radius);
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.restore();
};
export const drawTriangular = (
canvas: HTMLCanvasElement,
width: number,
height: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, -height);
ctx.lineTo(width, height);
ctx.lineTo(-width, height);
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.restore();
};
export const drawBall = (
canvas: HTMLCanvasElement,
radius: number,
fillStyle: string,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.closePath();
};
export const drawDonut = (
canvas: HTMLCanvasElement,
slices: number,
eaten: number,
radius: number,
hole: number,
fillStyle: string,
secondary: string,
stroke: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
if (slices < 2) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = slices < 1 ? secondary : fillStyle;
ctx.fill();
ctx.beginPath();
ctx.arc(0, 0, hole, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = secondary;
ctx.fill();
ctx.restore();
}
// Draw slices as arcs
const totalSlices = slices;
const sliceAngle = (2 * Math.PI) / totalSlices;
for (let i = 0; i < totalSlices; i++) {
const startAngle = i * sliceAngle;
const endAngle = (i + 1) * sliceAngle;
// Calculate the position of the outer arc
const outerStartX = hole * Math.cos(startAngle);
const outerStartY = hole * Math.sin(startAngle);
ctx.beginPath();
ctx.moveTo(outerStartX, outerStartY);
ctx.arc(0, 0, radius, startAngle, endAngle);
ctx.arc(0, 0, hole, endAngle, startAngle, true);
ctx.closePath();
// Fill and stroke the slices with the specified fill style
if (i < slices - eaten) {
// Regular slices are white
ctx.fillStyle = fillStyle;
} else {
// Missing slices are black
ctx.fillStyle = secondary;
}
ctx.lineWidth = 2;
ctx.fill();
ctx.strokeStyle = stroke;
ctx.stroke();
}
ctx.restore();
};
export const drawPie = (
canvas: HTMLCanvasElement,
slices: number,
eaten: number,
radius: number,
fillStyle: string,
secondary: string,
stroke: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
if (slices < 2) {
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.fillStyle = slices < 1 ? secondary : fillStyle;
ctx.fill();
ctx.restore();
}
// Draw slices as arcs
const totalSlices = slices;
const sliceAngle = (2 * Math.PI) / totalSlices;
for (let i = 0; i < totalSlices; i++) {
const startAngle = i * sliceAngle;
const endAngle = (i + 1) * sliceAngle;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, radius, startAngle, endAngle);
ctx.lineTo(0, 0); // Connect to center
ctx.closePath();
// Fill and stroke the slices with the specified fill style
if (i < slices - eaten) {
// Regular slices are white
ctx.fillStyle = fillStyle;
} else {
// Missing slices are black
ctx.fillStyle = secondary;
}
ctx.lineWidth = 2;
ctx.fill();
ctx.strokeStyle = stroke;
ctx.stroke();
}
ctx.restore();
};
export const drawStar = (
canvas: HTMLCanvasElement,
points: number,
radius: number,
fillStyle: string,
rotation: number,
outerRadius: number,
x: number,
y: number
): void => {
if (points < 1) return drawBall(canvas, radius, fillStyle, x, y);
if (points == 1) return drawEquilateral(canvas, radius, fillStyle, 0, x, y);
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, -radius);
for (let i = 0; i < points; i++) {
ctx.rotate(Math.PI / points);
ctx.lineTo(0, -(radius * outerRadius));
ctx.rotate(Math.PI / points);
ctx.lineTo(0, -radius);
}
ctx.closePath();
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.restore();
};
export const drawStroke = (
canvas: HTMLCanvasElement,
width: number,
strokeStyle: string,
rotation: number = 0,
x1: number,
y1: number,
x2: number,
y2: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x1, y1);
ctx.rotate((rotation * Math.PI) / 180);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(x2 - x1, y2 - y1);
ctx.lineWidth = width;
ctx.strokeStyle = strokeStyle;
ctx.stroke();
ctx.restore();
};
export const drawBox = (
canvas: HTMLCanvasElement,
width: number,
height: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.fillStyle = fillStyle;
ctx.fillRect(0, 0, width, height);
ctx.restore();
};
export const drawSmiley = (
canvas: HTMLCanvasElement,
happiness: number,
radius: number,
eyeSize: number,
fillStyle: string,
rotation: number,
x: number,
y: number
): void => {
const ctx = canvas.getContext("2d")!;
// Map the rotation value to an angle within the range of -PI to PI
const rotationAngle = (rotation / 100) * Math.PI;
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotationAngle);
// Draw face
ctx.beginPath();
ctx.arc(0, 0, radius, 0, 2 * Math.PI);
ctx.fillStyle = fillStyle;
ctx.fill();
ctx.lineWidth = radius / 20;
ctx.strokeStyle = "black";
ctx.stroke();
// Draw eyes
const eyeY = -radius / 5;
const eyeXOffset = radius / 2.5;
const eyeRadiusX = radius / 8;
const eyeRadiusY = (eyeSize * radius) / 10;
ctx.beginPath();
ctx.ellipse(-eyeXOffset, eyeY, eyeRadiusX, eyeRadiusY, 0, 0, 2 * Math.PI);
ctx.fillStyle = "black";
ctx.fill();
ctx.beginPath();
ctx.ellipse(eyeXOffset, eyeY, eyeRadiusX, eyeRadiusY, 0, 0, 2 * Math.PI);
ctx.fillStyle = "black";
ctx.fill();
// Draw mouth with happiness number -1.0 to 1.0. 0.0 Should be a straight line.
const mouthY = radius / 2;
const mouthLength = radius * 0.9;
const smileFactor = 0.25; // Adjust for the smile curvature
let controlPointX = 0;
let controlPointY = 0;
if (happiness >= 0) {
controlPointY = mouthY + (happiness * smileFactor * radius) / 2;
} else {
controlPointY = mouthY + (happiness * smileFactor * radius) / 2;
}
ctx.beginPath();
ctx.moveTo(-mouthLength / 2, mouthY);
ctx.quadraticCurveTo(controlPointX, controlPointY, mouthLength / 2, mouthY);
ctx.lineWidth = 10;
ctx.strokeStyle = "black";
ctx.stroke();
ctx.restore();
};
export const drawText = (
canvas: HTMLCanvasElement,
text: string,
fontSize: number,
rotation: number,
font: string,
x: number,
y: number,
fillStyle: string,
filter: string
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.filter = filter;
ctx.font = `${fontSize}px ${font}`;
ctx.fillStyle = fillStyle;
ctx.fillText(text, 0, 0);
ctx.restore();
};
export const drawImage = (
canvas: HTMLCanvasElement,
url: string,
width: number,
height: number,
rotation: number,
x: number,
y: number,
filter: string = "none"
): void => {
const ctx = canvas.getContext("2d")!;
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation * Math.PI) / 180);
ctx.filter = filter;
const image = new Image();
image.src = url;
ctx.drawImage(image, -width / 2, -height / 2, width, height);
ctx.restore();
};

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,46 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="48.000000pt" height="48.000000pt" viewBox="0 0 48.000000 48.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,48.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M163 470 c-13 -5 -23 -12 -23 -15 0 -3 46 -5 102 -5 80 0 99 3 90 12
-15 15 -139 21 -169 8z"/>
<path d="M91 391 c-16 -16 -19 -32 -18 -84 1 -53 -2 -69 -18 -87 -15 -16 -20
-38 -22 -83 0 -33 2 -56 5 -50 9 13 44 13 39 -1 -2 -6 -18 -11 -34 -11 -39 0
-50 -10 -33 -30 8 -10 30 -15 60 -15 26 0 64 -7 83 -15 43 -18 125 -20 164 -3
15 6 57 14 93 17 73 7 87 24 51 60 -12 12 -21 17 -21 13 0 -4 5 -13 12 -20 8
-8 8 -12 1 -12 -18 0 -25 34 -9 45 10 7 12 16 6 25 -5 8 -7 24 -4 35 3 13 -2
28 -15 39 -12 11 -21 27 -21 36 0 9 -5 21 -11 27 -8 8 -8 17 0 31 17 32 13 56
-12 80 -31 29 -63 28 -91 -3 -16 -17 -34 -25 -56 -25 -22 0 -40 8 -56 25 -28
30 -67 32 -93 6z m89 -16 c19 -23 5 -29 -24 -10 -16 10 -33 14 -47 10 -14 -5
-19 -4 -15 4 10 16 72 13 86 -4z m211 -7 c12 -22 11 -22 -8 -5 -25 21 -41 22
-65 0 -22 -19 -36 -10 -18 12 20 24 77 19 91 -7z m-264 -30 c-3 -7 -5 -2 -5
12 0 14 2 19 5 13 2 -7 2 -19 0 -25z m229 -5 c-11 -11 -19 6 -11 24 8 17 8 17
12 0 3 -10 2 -21 -1 -24z m-80 -8 c4 -8 10 -12 15 -9 5 3 9 0 9 -6 0 -14 60
-40 77 -33 7 3 13 -2 13 -11 0 -13 -6 -15 -27 -9 -36 9 -210 9 -245 0 -22 -6
-28 -4 -28 9 0 8 6 14 13 11 6 -2 25 2 40 10 15 8 32 11 37 8 6 -4 7 1 3 11
-4 12 -3 15 5 10 7 -4 15 -1 18 8 8 21 63 21 70 1z m83 -91 c10 -9 -37 -33
-77 -39 -55 -8 -117 2 -155 26 l-30 18 54 4 c56 4 201 -2 208 -9z m-286 -30
c-15 -7 21 -44 42 -44 8 0 15 -4 15 -10 0 -16 -33 -11 -59 9 -25 19 -24 52 1
50 9 0 10 -2 1 -5z m355 -21 c2 -14 -4 -23 -17 -28 -12 -3 -21 -13 -21 -21 0
-19 -16 -18 -24 1 -5 15 -31 16 -53 2 -8 -5 -13 -4 -13 2 0 6 5 12 10 12 6 1
15 3 20 4 6 1 18 3 28 4 25 1 66 37 52 44 -6 3 -5 4 2 3 7 -1 14 -12 16 -23z
m-255 -37 c4 -10 1 -13 -9 -9 -7 3 -14 9 -14 14 0 14 17 10 23 -5z m42 -6 c3
-5 1 -10 -4 -10 -6 0 -11 5 -11 10 0 6 2 10 4 10 3 0 8 -4 11 -10z m35 0 c0
-5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m-170
-10 c0 -5 -5 -10 -11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z
m215 -39 c-6 -5 -25 10 -25 20 0 5 6 4 14 -3 8 -7 12 -15 11 -17z m45 8 c0
-14 -16 -11 -29 5 -10 12 -8 13 8 9 12 -3 21 -9 21 -14z m-220 1 c0 -5 -5 -10
-11 -10 -5 0 -7 5 -4 10 3 6 8 10 11 10 2 0 4 -4 4 -10z m40 0 c0 -5 -4 -10
-10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0 10 -4 10 -10z m215 0 c3 -5 2
-10 -4 -10 -5 0 -13 5 -16 10 -3 6 -2 10 4 10 5 0 13 -4 16 -10z m43 -10 c7
-11 10 -20 6 -20 -7 0 -34 27 -34 34 0 13 16 5 28 -14z m-160 -17 c-10 -2 -26
-2 -35 0 -10 3 -2 5 17 5 19 0 27 -2 18 -5z m99 1 c-3 -3 -12 -4 -19 -1 -8 3
-5 6 6 6 11 1 17 -2 13 -5z m40 0 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17
-2 13 -5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

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

Before

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

View File

@ -1,20 +1,14 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { import {
freqToMidi, freqToMidi,
chord as parseChord,
noteNameToMidi,
resolvePitchBend, resolvePitchBend,
safeScale, safeScale
} from "zifferjs"; } from "zifferjs";
import { SkipEvent } from "./SkipEvent";
import { SoundParams } from "./SoundEvent";
import { centsToSemitones, edoToSemitones, ratiosToSemitones } from "zifferjs/src/scale";
import { safeMod } from "zifferjs/src/utils";
export type EventOperation<T> = (instance: T, ...args: any[]) => void; export type EventOperation<T> = (instance: T, ...args: any[]) => void;
export interface AbstractEvent { export interface AbstractEvent {
[key: string]: any; [key: string]: any
} }
export class AbstractEvent { export class AbstractEvent {
@ -211,79 +205,25 @@ export class AbstractEvent {
* @param func - The function to be applied to the Event * @param func - The function to be applied to the Event
* @returns The transformed Event * @returns The transformed Event
*/ */
return this.modify(func).update(); return this.modify(func);
}; };
mod = (value: number): AbstractEvent => { noteLength = (value: number | number[], ...kwargs: number[]): AbstractEvent => {
this.values.originalPitch = safeMod(this.values.originalPitch, value);
return this.update();
}
noteLength = (
value: number | number[],
...kwargs: number[]
): AbstractEvent => {
/** /**
* This function is used to set the note length of the Event. * This function is used to set the note length of the Event.
*/ */
if (kwargs.length > 0) { if(kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
} }
if (Array.isArray(value)) { if(Array.isArray(value)) {
this.values.dur = value.map((v) => this.values["noteLength"] = value;
this.app.clock.convertPulseToSecond(v * 4 * this.app.clock.ppqn), this.values.dur = value.map((v) => this.app.clock.convertPulseToSecond(v*4*this.app.clock.ppqn));
);
} else { } else {
this.values.dur = this.app.clock.convertPulseToSecond( this.values["noteLength"] = value;
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; 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 { export abstract class AudibleEvent extends AbstractEvent {
@ -292,205 +232,83 @@ export abstract class AudibleEvent extends AbstractEvent {
} }
pitch = (value: number | number[], ...kwargs: number[]): this => { pitch = (value: number | number[], ...kwargs: number[]): this => {
/* /*
* This function is used to set the pitch of the Event. * This function is used to set the pitch of the Event.
* @param value - The pitch value * @param value - The pitch value
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if(kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
} }
this.values["pitch"] = value; this.values["pitch"] = value;
this.values["originalPitch"] = value; if(this.values.key && this.values.parsedScale) this.update();
this.defaultPitchKeyScale(); return this;
return this.update(); }
};
pc = this.pitch; pc = this.pitch;
octave = (value: number | number[], ...kwargs: number[]): this => { octave = (value: number | number[], ...kwargs: number[]): this => {
/* /*
* This function is used to set the octave of the Event. * This function is used to set the octave of the Event.
* @param value - The octave value * @param value - The octave value
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if(kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
} }
this.values["paramOctave"] = value; this.values["octave"] = value;
if ( if(this.values.key && (this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update();
this.values.key &&
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
) {
return this.update();
}
return this; return this;
}; };
key = (value: string | string[], ...kwargs: string[]): this => { key = (value: string | string[], ...kwargs: string[]): this => {
/* /*
* This function is used to set the key of the Event. * This function is used to set the key of the Event.
* @param value - The key value * @param value - The key value
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if(kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
} }
this.values["key"] = value; this.values["key"] = value;
if ( if((this.values.pitch || this.values.pitch === 0) && this.values.parsedScale) this.update();
(this.values.pitch || this.values.pitch === 0) &&
this.values.parsedScale
) {
return this.update();
}
return this; return this;
}; };
defaultPitchKeyScale() { scale = (value: string | number | (number|string)[], ...kwargs: (string|number)[]): this => {
if (!this.values.key) this.values.key = 60;
if (!(this.values.pitch || this.values.pitch === 0)) this.values.pitch = 0;
if (!this.values.parsedScale) this.values.parsedScale = safeScale("major");
}
scale = (
value: string | number | (number | string)[],
...kwargs: (string | number)[]
): this => {
/* /*
* This function is used to set the scale of the Event. * This function is used to set the scale of the Event.
* @param value - The scale value * @param value - The scale value
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if(kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
} }
if (typeof value === "string" || typeof value === "number") { if (typeof value === "string" || typeof value === "number") {
this.values.parsedScale = safeScale(value) as number[]; this.values.parsedScale = safeScale(value) as number[];
} else if (Array.isArray(value)) { } else if(Array.isArray(value)) {
this.values.parsedScale = value.map((v) => safeScale(v)); this.values.parsedScale = value.map((v) => safeScale(v));
} }
this.defaultPitchKeyScale(); if(this.values.key && (this.values.pitch || this.values.pitch === 0)) {
return this.update(); this.update();
};
semitones(values: number|number[], ...rest: number[]) {
const scaleValues = typeof values === "number" ? [values, ...rest] : values;
this.values.parsedScale = safeScale(scaleValues);
this.defaultPitchKeyScale();
return this.update();
}
steps = this.semitones;
cents(values: number|number[], ...rest: number[]) {
const scaleValues = typeof values === "number" ? [values, ...rest] : values;
this.values.parsedScale = safeScale(centsToSemitones(scaleValues));
this.defaultPitchKeyScale();
return this.update();
}
ratios(values: number|number[], ...rest: number[]) {
const scaleValues = typeof values === "number" ? [values, ...rest] : values;
this.values.parsedScale = safeScale(ratiosToSemitones(scaleValues));
this.defaultPitchKeyScale();
return this.update();
}
edo(value: number, intervals: string|number[] = new Array(value).fill(1)) {
this.values.parsedScale = edoToSemitones(value, intervals);
this.defaultPitchKeyScale();
return this.update();
}
protected updateValue<T>(key: string, value: T | T[] | null): this {
if (value == null) return this;
this.values[key] = value;
return this;
}
public note = (
value: number | string | null,
...kwargs: number[] | string[]
) => {
if (typeof value === "string") {
const parsedNote = noteNameToMidi(value);
return this.updateValue("note", [parsedNote, ...kwargs].flat(Infinity));
} else if (typeof value == null || value == undefined) {
return new SkipEvent();
} else {
return this.updateValue("note", [value, ...kwargs].flat(Infinity));
}
};
public chord = (value: number | string, ...kwargs: number[]) => {
if (typeof value === "string") {
const chord = parseChord(value);
return this.updateValue("note", chord);
} else {
const chord = [value, ...kwargs].flat(Infinity);
return this.updateValue("note", chord);
}
};
public invert = (howMany: number = 0) => {
if(howMany === 0) return this;
if (this.values.note) {
let notes = [...this.values.note];
notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12;
}
return this.updateValue("note", notes);
} else {
return this;
}
};
public log = (key: string|string[], ...args: string[]) => {
/*
* Log values from values using log()
*
* @param key - The key(s) to log
* @returns this and logs the values
*/
if (typeof key === "string") {
if(args && args.length > 0) {
this.app.api.log([key, ...args].map((k) => this.values[k]));
} else {
this.app.api.log(this.values[key]);
}
} else {
this.app.api.log([...key, ...args].map((k) => this.values[k]));
} }
return this; return this;
} };
public draw = (lambda: Function) => {
lambda(this.values, (this.app.interface.drawings as HTMLCanvasElement).getContext("2d"));
return this;
}
public clear = () => {
this.app.api.clear();
return this;
}
freq = (value: number | number[], ...kwargs: number[]): this => { freq = (value: number | number[], ...kwargs: number[]): this => {
/* /*
* This function is used to set the frequency of the Event. * This function is used to set the frequency of the Event.
* @param value - The frequency value * @param value - The frequency value
* @returns The Event * @returns The Event
*/ */
if (kwargs.length > 0) { if(kwargs.length > 0) {
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]; value = (Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs]);
} }
this.values["freq"] = value; this.values["freq"] = value;
if (Array.isArray(value)) { if(Array.isArray(value)) {
this.values["note"] = []; this.values["note"] = [];
this.values["bend"] = []; this.values["bend"] = [];
for (const v of value) { for(const v of value) {
const midiNote = freqToMidi(v); const midiNote = freqToMidi(v);
if (midiNote % 1 !== 0) { if (midiNote % 1 !== 0) {
this.values["note"].push(Math.floor(midiNote)); this.values["note"].push(Math.floor(midiNote));
@ -499,7 +317,7 @@ export abstract class AudibleEvent extends AbstractEvent {
this.values["note"].push(midiNote); 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 { } else {
const midiNote = freqToMidi(value); const midiNote = freqToMidi(value);
if (midiNote % 1 !== 0) { if (midiNote % 1 !== 0) {
@ -512,22 +330,7 @@ export abstract class AudibleEvent extends AbstractEvent {
return this; return this;
}; };
update = (): this => { update = (): void => {
// Overwrite in subclasses // Overwrite in subclasses
return this;
}; };
cue = (functionName: string|Function): this => {
this.app.api.cue(functionName);
return this;
}
runChain = (): this => {
// chainAll is defined using all() in the API
if("chainAll" in this && typeof this.chainAll === "function") {
this.values = this.chainAll().values;
}
return this;
}
} }

View File

@ -1,13 +1,8 @@
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { type Editor } from "../main"; import { type Editor } from "../main";
import { MidiConnection } from "../IO/MidiConnection"; import { MidiConnection } from "../IO/MidiConnection";
import { resolvePitchClass } from "zifferjs"; import { noteFromPc, chord as parseChord } from "zifferjs";
import { import { filterObject, arrayOfObjectsToObjectWithArrays, objectWithArraysToArrayOfObjects } from "../Utils/Generic";
filterObject,
arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects,
maybeAtomic,
} from "../Utils/Generic";
export type MidiParams = { export type MidiParams = {
note: number; note: number;
@ -16,20 +11,27 @@ export type MidiParams = {
port?: number; port?: number;
sustain?: number; sustain?: number;
velocity?: number; velocity?: number;
}; }
export class MidiEvent extends AudibleEvent { export class MidiEvent extends AudibleEvent {
midiConnection: MidiConnection; midiConnection: MidiConnection;
constructor( constructor(input: MidiParams, public app: Editor) {
input: MidiParams,
public app: Editor,
) {
super(app); super(app);
this.values = input; this.values = input;
this.midiConnection = app.api.MidiConnection; this.midiConnection = app.api.MidiConnection;
} }
public chord = (value: string) => {
this.values.note = parseChord(value);
return this;
};
note = (value: number | number[]): this => {
this.values["note"] = value;
return this;
};
sustain = (value: number | number[]): this => { sustain = (value: number | number[]): this => {
this.values["sustain"] = value; this.values["sustain"] = value;
return this; return this;
@ -38,7 +40,7 @@ export class MidiEvent extends AudibleEvent {
velocity = (value: number | number[]): this => { velocity = (value: number | number[]): this => {
this.values["velocity"] = value; this.values["velocity"] = value;
return this; return this;
}; }
channel = (value: number | number[]): this => { channel = (value: number | number[]): this => {
this.values["channel"] = value; this.values["channel"] = value;
@ -46,12 +48,10 @@ export class MidiEvent extends AudibleEvent {
}; };
port = (value: number | string | number[] | string[]): this => { port = (value: number | string | number[] | string[]): this => {
if (typeof value === "string") { if(typeof value === "string"){
this.values["port"] = this.midiConnection.getMidiOutputIndex(value); this.values["port"] = this.midiConnection.getMidiOutputIndex(value);
} else if (Array.isArray(value)) { } else if(Array.isArray(value)){
this.values["port"] = value.map((v) => this.values["port"] = value.map((v) => typeof v === "string" ? this.midiConnection.getMidiOutputIndex(v) : v);
typeof v === "string" ? this.midiConnection.getMidiOutputIndex(v) : v,
);
} }
return this; return this;
}; };
@ -67,7 +67,8 @@ export class MidiEvent extends AudibleEvent {
return funcResult; return funcResult;
} else { } else {
func(this.values); func(this.values);
return this.update(); this.update();
return this;
} }
}; };
@ -83,46 +84,39 @@ export class MidiEvent extends AudibleEvent {
return this; return this;
}; };
update = (): this => { update = (): void => {
const filteredValues = filterObject(this.values, [ // Get key, pitch, parsedScale and octave from this.values object
"key", const filteredValues = filterObject(this.values, ["key", "pitch", "parsedScale", "octave"]);
"pitch",
"originalPitch", const events = objectWithArraysToArrayOfObjects(filteredValues,["parsedScale"]);
"parsedScale",
"addedOctave"
]);
const events = objectWithArraysToArrayOfObjects(filteredValues, [ events.forEach((event) => {
"parsedScale", const [note, bend] = noteFromPc(
]); event.key as number || "C4",
event.pitch as number || 0,
events.forEach((soundEvent) => { event.parsedScale as number[] || event.scale || "MAJOR",
const resolvedPitchClass = resolvePitchClass( event.octave as number || 0
(soundEvent.key || "C4"),
(soundEvent.originalPitch || soundEvent.pitch || 0),
(soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
(soundEvent.addedOctave || 0)
); );
soundEvent.note = resolvedPitchClass.note; event.note = note;
soundEvent.pitch = resolvedPitchClass.pitch; if(bend) event.bend = bend;
soundEvent.octave = resolvedPitchClass.octave;
}); });
const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams; const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams;
this.values.note = maybeAtomic(newArrays.note); this.values.note = newArrays.note;
if (newArrays.bend) this.values.bend = maybeAtomic(newArrays.bend); if(newArrays.bend) this.values.bend = newArrays.bend;
return this;
}; };
out = (outChannel?: number|number[]): void => { out = (): void => {
function play(event: MidiEvent, params: MidiParams): void { function play(event: MidiEvent, params: MidiParams): void {
const channel = params.channel ? params.channel : 0; const channel = params.channel ? params.channel : 0;
const velocity = params.velocity ? params.velocity : 100; const velocity = params.velocity ? params.velocity : 100;
const note = params.note ? params.note : 60; const note = params.note ? params.note : 60;
const sustain = params.sustain 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(); : event.app.clock.pulse_duration * event.app.api.ppqn();
const bend = params.bend ? params.bend : undefined; const bend = params.bend ? params.bend : undefined;
@ -130,26 +124,22 @@ export class MidiEvent extends AudibleEvent {
const port = params.port const port = params.port
? event.midiConnection.getMidiOutputIndex(params.port) ? event.midiConnection.getMidiOutputIndex(params.port)
: event.midiConnection.getCurrentMidiPortIndex() || 0; : event.midiConnection.getCurrentMidiPortIndex() || 0;
event.midiConnection.sendMidiNote( event.midiConnection.sendMidiNote(
note, note,
channel, channel,
velocity, velocity,
sustain, sustain,
port, port,
bend, bend
); );
} }
this.runChain(); const events = objectWithArraysToArrayOfObjects(this.values,["parsedScale"]) as MidiParams[];
if(outChannel) this.channel(outChannel);
const events = objectWithArraysToArrayOfObjects(this.values, [
"parsedScale",
]) as MidiParams[];
events.forEach((p: MidiParams) => { events.forEach((p: MidiParams) => {
play(this, p); play(this,p);
}); });
}; };
} }

View File

@ -11,7 +11,10 @@ export class RestEvent extends AbstractEvent {
return RestEvent.createRestProxy(this.values["noteLength"], this.app); 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); const instance = new RestEvent(length, app);
return new Proxy(instance, { return new Proxy(instance, {
// @ts-ignore // @ts-ignore

View File

@ -1,19 +1,21 @@
import { type Editor } from "../main"; import { type Editor } from "../main";
import { AudibleEvent } from "./AbstractEvents"; import { AudibleEvent } from "./AbstractEvents";
import { sendToServer, type OSCMessage } from "../IO/OSC";
import { import {
filterObject, filterObject,
arrayOfObjectsToObjectWithArrays, arrayOfObjectsToObjectWithArrays,
objectWithArraysToArrayOfObjects, objectWithArraysToArrayOfObjects,
maybeAtomic,
} from "../Utils/Generic"; } from "../Utils/Generic";
import { midiToFreq, resolvePitchClass } from "zifferjs"; import {
chord as parseChord,
midiToFreq,
noteFromPc,
noteNameToMidi,
} from "zifferjs";
import { import {
superdough, superdough,
// @ts-ignore // @ts-ignore
} from "superdough"; } from "superdough";
// import { Sound } from "zifferjs/src/types";
export type SoundParams = { export type SoundParams = {
dur: number | number[]; dur: number | number[];
@ -23,46 +25,27 @@ export type SoundParams = {
note?: number | number[]; note?: number | number[];
freq?: number | number[]; freq?: number | number[];
pitch?: number | number[]; pitch?: number | number[];
originalPitch?: number | number[];
key?: string; key?: string;
scale?: string; scale?: string;
parsedScale?: number[]; parsedScale?: number[];
octave?: number | number[]; octave?: number | number[];
addedOctave?: number | number[];
pitchOctave?: number | number[];
}; };
export class SoundEvent extends AudibleEvent { export class SoundEvent extends AudibleEvent {
nudge: number; nudge: number;
sound: any; sound: any;
private static methodMap = { private methodMap = {
volume: ["volume", "vol"], volume: ["volume", "vol"],
zrand: ["zrand", "zr"], zrand: ["zrand", "zr"],
curve: ["curve"], curve: ["curve"],
bank: ["bank"], bank: ["bank"],
drumMachine: function(self: SoundEvent, a: number) {
let machines = ["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"];
self.updateValue("bank", machines[a % machines.length]);
return self;
},
slide: ["slide", "sld"], slide: ["slide", "sld"],
deltaSlide: ["deltaSlide", "dslide"], deltaSlide: ["deltaSlide", "dslide"],
pitchJump: ["pitchJump", "pj"], pitchJump: ["pitchJump", "pj"],
pitchJumpTime: ["pitchJumpTime", "pjt"], pitchJumpTime: ["pitchJumpTime", "pjt"],
lfo: ["lfo"], lfo: ["lfo"],
znoise: ["znoise"], znoise: ["znoise"],
address: ["address", "add"],
port: ["port"],
density: ["density"],
noise: ["noise"], noise: ["noise"],
zmod: ["zmod"], zmod: ["zmod"],
zcrush: ["zcrush"], zcrush: ["zcrush"],
@ -84,23 +67,17 @@ export class SoundEvent extends AudibleEvent {
phaserDepth: ["phaserDepth", "phasdepth"], phaserDepth: ["phaserDepth", "phasdepth"],
phaserSweep: ["phaserSweep", "phassweep"], phaserSweep: ["phaserSweep", "phassweep"],
phaserCenter: ["phaserCenter", "phascenter"], phaserCenter: ["phaserCenter", "phascenter"],
fmadsr: function( fmadsr: (a: number, d: number, s: number, r: number) => {
self: SoundEvent, this.updateValue("fmattack", a);
a: number, this.updateValue("fmdecay", d);
d: number, this.updateValue("fmsustain", s);
s: number, this.updateValue("fmrelease", r);
r: number, return this;
) {
self.updateValue("fmattack", a);
self.updateValue("fmdecay", d);
self.updateValue("fmsustain", s);
self.updateValue("fmrelease", r);
return self;
}, },
fmad: function(self: SoundEvent, a: number, d: number) { fmad: (a: number, d: number) => {
self.updateValue("fmattack", a); this.updateValue("fmattack", a);
self.updateValue("fmdecay", d); this.updateValue("fmdecay", d);
return self; return this;
}, },
ftype: ["ftype"], ftype: ["ftype"],
fanchor: ["fanchor"], fanchor: ["fanchor"],
@ -108,194 +85,147 @@ export class SoundEvent extends AudibleEvent {
decay: ["decay", "dec"], decay: ["decay", "dec"],
sustain: ["sustain", "sus"], sustain: ["sustain", "sus"],
release: ["release", "rel"], release: ["release", "rel"],
adsr: function( adsr: (a: number, d: number, s: number, r: number) => {
self: SoundEvent, this.updateValue("attack", a);
a: number, this.updateValue("decay", d);
d: number, this.updateValue("sustain", s);
s: number, this.updateValue("release", r);
r: number, return this;
) {
self.updateValue("attack", a);
self.updateValue("decay", d);
self.updateValue("sustain", s);
self.updateValue("release", r);
return self;
}, },
ad: function(self: SoundEvent, a: number, d: number) { ad: (a: number, d: number) => {
self.updateValue("attack", a); this.updateValue("attack", a);
self.updateValue("decay", d); this.updateValue("decay", d);
self.updateValue("sustain", 0.0); this.updateValue("sustain", 0.0);
self.updateValue("release", 0.0); this.updateValue("release", 0.0);
return self; return this;
},
scope: function(self: SoundEvent) {
self.updateValue("analyze", true);
return self;
},
debug: function(self: SoundEvent, callback?: Function) {
self.updateValue("debug", true);
if (callback) {
self.updateValue("debugFunction", callback);
}
return self;
}, },
lpenv: ["lpenv", "lpe"], lpenv: ["lpenv", "lpe"],
lpattack: ["lpattack", "lpa"], lpattack: ["lpattack", "lpa"],
lpdecay: ["lpdecay", "lpd"], lpdecay: ["lpdecay", "lpd"],
lpsustain: ["lpsustain", "lps"], lpsustain: ["lpsustain", "lps"],
lprelease: ["lprelease", "lpr"], lprelease: ["lprelease", "lpr"],
cutoff: function(self: SoundEvent, value: number, resonance?: number) { cutoff: (value: number, resonance?: number) => {
self.updateValue("cutoff", value); this.updateValue("cutoff", value);
if (resonance) { if (resonance) {
self.updateValue("resonance", resonance); this.updateValue("resonance", resonance);
} }
return self; return this;
}, },
lpf: function(self: SoundEvent, value: number, resonance?: number) { lpf: (value: number, resonance?: number) => {
self.updateValue("cutoff", value); this.updateValue("cutoff", value);
if (resonance) { if (resonance) {
self.updateValue("resonance", resonance); this.updateValue("resonance", resonance);
} }
return self; return this;
}, },
resonance: function(self: SoundEvent, value: number) { resonance: (value: number) => {
if (value >= 0 && value <= 1) { if (value >= 0 && value <= 1) {
self.updateValue("resonance", 50 * value); this.updateValue("resonance", 50 * value);
} }
return self; return this;
}, },
lpq: function(self: SoundEvent, value: number) { lpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
if (value >= 0 && value <= 1) { this.updateValue("lpenv", depth);
self.updateValue("resonance", 50 * value); this.updateValue("lpattack", a);
} this.updateValue("lpdecay", d);
return self; this.updateValue("lpsustain", s);
this.updateValue("lprelease", r);
return this;
}, },
lpadsr: function( lpad: (depth: number, a: number, d: number) => {
self: SoundEvent, this.updateValue("lpenv", depth);
depth: number, this.updateValue("lpattack", a);
a: number, this.updateValue("lpdecay", d);
d: number, this.updateValue("lpsustain", 0);
s: number, this.updateValue("lprelease", 0);
r: number, return this;
) {
self.updateValue("lpenv", depth);
self.updateValue("lpattack", a);
self.updateValue("lpdecay", d);
self.updateValue("lpsustain", s);
self.updateValue("lprelease", r);
return self;
},
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"], hpenv: ["hpenv", "hpe"],
hpattack: ["hpattack", "hpa"], hpattack: ["hpattack", "hpa"],
hpdecay: ["hpdecay", "hpd"], hpdecay: ["hpdecay", "hpd"],
hpsustain: ["hpsustain", "hpsus"], hpsustain: ["hpsustain", "hpsus"],
hprelease: ["hprelease", "hpr"], hprelease: ["hprelease", "hpr"],
hcutoff: function(self: SoundEvent, value: number, resonance?: number) { hcutoff: (value: number, resonance?: number) => {
self.updateValue("hcutoff", value); this.updateValue("hcutoff", value);
if (resonance) { if (resonance) {
self.updateValue("hresonance", resonance); this.updateValue("hresonance", resonance);
} }
return self; return this;
}, },
hpf: function(self: SoundEvent, value: number, resonance?: number) { hpf: (value: number, resonance?: number) => {
self.updateValue("hcutoff", value); this.updateValue("hcutoff", value);
if (resonance) { if (resonance) {
self.updateValue("hresonance", resonance * 50); this.updateValue("hresonance", resonance);
} }
return self; return this;
}, },
hpq: function(self: SoundEvent, value: number) { hpq: (value: number) => {
self.updateValue("hresonance", value * 50); this.updateValue("hresonance", value);
return self; return this;
}, },
hpadsr: function( hpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
self: SoundEvent, this.updateValue("hpenv", depth);
depth: number, this.updateValue("hpattack", a);
a: number, this.updateValue("hpdecay", d);
d: number, this.updateValue("hpsustain", s);
s: number, this.updateValue("hprelease", r);
r: number, return this;
) {
self.updateValue("hpenv", depth);
self.updateValue("hpattack", a);
self.updateValue("hpdecay", d);
self.updateValue("hpsustain", s);
self.updateValue("hprelease", r);
return self;
}, },
hpad: function(self: SoundEvent, depth: number, a: number, d: number) { hpad: (depth: number, a: number, d: number) => {
self.updateValue("hpenv", depth); this.updateValue("hpenv", depth);
self.updateValue("hpattack", a); this.updateValue("hpattack", a);
self.updateValue("hpdecay", d); this.updateValue("hpdecay", d);
self.updateValue("hpsustain", 0); this.updateValue("hpsustain", 0);
self.updateValue("hprelease", 0); this.updateValue("hprelease", 0);
return self; return this;
}, },
bpenv: ["bpenv", "bpe"], bpenv: ["bpenv", "bpe"],
bpattack: ["bpattack", "bpa"], bpattack: ["bpattack", "bpa"],
bpdecay: ["bpdecay", "bpd"], bpdecay: ["bpdecay", "bpd"],
bpsustain: ["bpsustain", "bps"], bpsustain: ["bpsustain", "bps"],
bprelease: ["bprelease", "bpr"], bprelease: ["bprelease", "bpr"],
bandf: function(self: SoundEvent, value: number, resonance?: number) { bandf: (value: number, resonance?: number) => {
self.updateValue("bandf", value); this.updateValue("bandf", value);
if (resonance) { if (resonance) {
self.updateValue("bandq", resonance); this.updateValue("bandq", resonance);
} }
return self; return this;
}, },
bpf: function(self: SoundEvent, value: number, resonance?: number) { bpf: (value: number, resonance?: number) => {
self.updateValue("bandf", value); this.updateValue("bandf", value);
if (resonance) { if (resonance) {
self.updateValue("bandq", resonance * 50); this.updateValue("bandq", resonance);
} }
return self; return this;
}, },
bpq: function(self: SoundEvent, value: number) { bandq: ["bandq", "bpq"],
self.updateValue("bandq", value * 50); bpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
return self; this.updateValue("bpenv", depth);
this.updateValue("bpattack", a);
this.updateValue("bpdecay", d);
this.updateValue("bpsustain", s);
this.updateValue("bprelease", r);
return this;
}, },
bpadsr: function( bpad: (depth: number, a: number, d: number) => {
self: SoundEvent, this.updateValue("bpenv", depth);
depth: number, this.updateValue("bpattack", a);
a: number, this.updateValue("bpdecay", d);
d: number, this.updateValue("bpsustain", 0);
s: number, this.updateValue("bprelease", 0);
r: number, return this;
) {
self.updateValue("bpenv", depth);
self.updateValue("bpattack", a);
self.updateValue("bpdecay", d);
self.updateValue("bpsustain", s);
self.updateValue("bprelease", r);
return self;
},
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"], vib: ["vib"],
vibmod: ["vibmod"], vibmod: ["vibmod"],
fm: function(self: SoundEvent, value: number | string) { fm: (value: number | string) => {
if (typeof value === "number") { if (typeof value === "number") {
self.values["fmi"] = value; this.values["fmi"] = value;
} else { } else {
let values = value.split(":"); let values = value.split(":");
self.values["fmi"] = parseFloat(values[0]); this.values["fmi"] = parseFloat(values[0]);
if (values.length > 1) self.values["fmh"] = parseFloat(values[1]); if (values.length > 1) this.values["fmh"] = parseFloat(values[1]);
} }
return self; return this;
}, },
loop: ["loop"], loop: ["loop"],
loopBegin: ["loopBegin", "loopb"], loopBegin: ["loopBegin", "loopb"],
@ -303,13 +233,13 @@ export class SoundEvent extends AudibleEvent {
begin: ["begin"], begin: ["begin"],
end: ["end"], end: ["end"],
gain: ["gain"], gain: ["gain"],
dbgain: function(self: SoundEvent, value: number) { dbgain: (value: number) => {
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return self; return this;
}, },
db: function(self: SoundEvent, value: number) { db: (value: number) => {
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10)); this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
return self; return this;
}, },
velocity: ["velocity", "vel"], velocity: ["velocity", "vel"],
pan: ["pan"], pan: ["pan"],
@ -330,72 +260,109 @@ export class SoundEvent extends AudibleEvent {
roomlp: ["roomlp", "rlp"], roomlp: ["roomlp", "rlp"],
roomdim: ["roomdim", "rdim"], roomdim: ["roomdim", "rdim"],
sound: ["s", "sound"], sound: ["s", "sound"],
size: function(self: SoundEvent, value: number) { size: (value: number) => {
self.updateValue("roomsize", value); this.updateValue("roomsize", value);
return self; return this;
}, },
sz: function(self: SoundEvent, value: number) { sz: (value: number) => {
self.updateValue("roomsize", value); this.updateValue("roomsize", value);
return self; return this;
}, },
comp: ["comp", "compressor", "cmp"], comp: ["compressor", "cmp"],
ratio: function(self: SoundEvent, value: number) { ratio: (value: number) => {
self.updateValue("compressorRatio", value); this.updateValue("compressorRatio", value);
return self; return this;
}, },
knee: function(self: SoundEvent, value: number) { knee: (value: number) => {
self.updateValue("compressorKnee", value); this.updateValue("compressorKnee", value);
return self; return this;
}, },
compAttack: function(self: SoundEvent, value: number) { compAttack: (value: number) => {
self.updateValue("compressorAttack", value); this.updateValue("compressorAttack", value);
return self; return this;
}, },
compRelease: function(self: SoundEvent, value: number) { compRelease: (value: number) => {
self.updateValue("compressorRelease", value); this.updateValue("compressorRelease", value);
return self; return this;
}, },
stretch: function(self: SoundEvent, beat: number) { stretch: (beat: number) => {
self.updateValue("unit", "c"); this.updateValue("unit", "c");
self.updateValue("speed", 1 / beat); this.updateValue("speed", 1 / beat);
self.updateValue("cut", beat); this.updateValue("cut", beat);
return self; return this;
}, },
}; };
constructor( constructor(sound: string | string[] | SoundParams, public app: Editor) {
sound: string | string[] | SoundParams,
public app: Editor,
) {
super(app); super(app);
this.nudge = app.dough_nudge / 100; this.nudge = app.dough_nudge / 100;
for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) { for (const [methodName, keys] of Object.entries(this.methodMap)) {
if (typeof keys === "object" && Symbol.iterator in Object(keys)) { if (Symbol.iterator in Object(keys)) {
for (const key of keys as string[]) { for (const key of keys as string[]) {
// Using arrow function to maintain 'this' context // @ts-ignore
this[key] = (value: number) => this.updateValue(keys[0], value); this[key] = (value: number) => this.updateValue(keys[0], value);
} }
} else { } else {
// @ts-ignore // @ts-ignore
this[methodName] = (...args) => keys(this, ...args); this[methodName] = 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
// this[key] = (value: number) => this.updateValue(this, keys[0], value);
// }
// } else {
// // @ts-ignore
// this[methodName] = keys;
// }
// }
this.values = this.processSound(sound); this.values = this.processSound(sound);
} }
private processSound = (
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 // AbstactEvent overrides
// ================================================================================ // ================================================================================
@ -405,92 +372,84 @@ export class SoundEvent extends AudibleEvent {
if (funcResult instanceof Object) return funcResult; if (funcResult instanceof Object) return funcResult;
else { else {
func(this.values); func(this.values);
return this.update(); this.update();
return this;
} }
}; };
update = (): this => { update = (): void => {
const filteredValues = filterObject(this.values, [ const filteredValues = filterObject(this.values, [
"key", "key",
"pitch", "pitch",
"originalPitch",
"parsedScale", "parsedScale",
"addedOctave",
"octave", "octave",
"paramOctave"
]); ]);
const events = objectWithArraysToArrayOfObjects(filteredValues, [ const events = objectWithArraysToArrayOfObjects(filteredValues, [
"parsedScale", "parsedScale",
]); ]);
events.forEach((soundEvent) => { events.forEach((event) => {
const resolvedPitchClass = resolvePitchClass( const [note, _] = noteFromPc(
(soundEvent.key || "C4"), (event.key as number) || "C4",
(soundEvent.originalPitch || soundEvent.pitch || 0), (event.pitch as number) || 0,
(soundEvent.parsedScale || soundEvent.scale || "MAJOR"), (event.parsedScale as number[]) || event.scale || "MAJOR",
(soundEvent.paramOctave || 0) + (soundEvent.addedOctave || 0) (event.octave as number) || 0
); );
soundEvent.note = resolvedPitchClass.note; event.note = note;
soundEvent.freq = midiToFreq(resolvedPitchClass.note); event.freq = midiToFreq(note);
soundEvent.pitch = resolvedPitchClass.pitch;
soundEvent.octave = resolvedPitchClass.octave;
}); });
const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams; const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams;
this.values.note = maybeAtomic(newArrays.note); this.values.note = newArrays.note;
this.values.freq = maybeAtomic(newArrays.freq); this.values.freq = newArrays.freq;
this.values.pitch = maybeAtomic(newArrays.pitch);
this.values.octave = maybeAtomic(newArrays.octave);
this.values.pitchOctave = maybeAtomic(newArrays.pitchOctave);
return this;
}; };
public chord = (value: string) => {
const chord = parseChord(value);
return this.updateValue("note", chord);
};
public invert = (howMany: number = 0) => {
if (this.values.chord) {
let notes = this.values.chord.map(
(obj: { [key: string]: number }) => obj.note
);
notes = howMany < 0 ? [...notes].reverse() : notes;
for (let i = 0; i < Math.abs(howMany); i++) {
notes[i % notes.length] += howMany <= 0 ? -12 : 12;
}
const chord = notes.map((note: number) => {
return { note: note, freq: midiToFreq(note) };
});
return this.updateValue("chord", chord);
} else {
return this;
}
};
public note = (value: number | string | null) => {
if (typeof value === "string") {
return this.updateValue("note", noteNameToMidi(value));
} else if (typeof value == null || value == undefined) {
return this.updateValue("note", 0).updateValue("gain", 0);
} else {
return this.updateValue("note", value);
}
};
out = (orbit?: number | number[]): void => { out = (orbit?: number | number[]): void => {
this.runChain();
if (orbit) this.values["orbit"] = orbit; if (orbit) this.values["orbit"] = orbit;
const events = objectWithArraysToArrayOfObjects(this.values, [ const events = objectWithArraysToArrayOfObjects(this.values, [
"parsedScale", "parsedScale",
]); ]);
for (const event of events) { for (const event of events) {
// Filter non superdough parameters // Filter non superdough parameters
// TODO: Should filter relevant fields for superdough // TODO: Should filter relevant fields for superdough
// const filteredEvent = filterObject(event, ["analyze","note","dur","freq","s"]); // const filteredEvent = filterObject(event, ["analyze","note","dur","freq","s"]);
const filteredEvent = event; const filteredEvent = event;
// No need for note if there is freq // No need for note if there is freq
if (filteredEvent.freq) { if (filteredEvent.freq) { delete filteredEvent.note; }
delete filteredEvent.note; superdough(filteredEvent, this.nudge - this.app.clock.deviation, filteredEvent.dur);
}
superdough(
filteredEvent,
this.nudge - this.app.clock.deviation,
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.nudge - this.app.clock.deviation)),
} as OSCMessage);
} }
}; };
} }

View File

@ -5,20 +5,18 @@ import { SkipEvent } from "./SkipEvent";
import { SoundEvent, SoundParams } from "./SoundEvent"; import { SoundEvent, SoundParams } from "./SoundEvent";
import { MidiEvent, MidiParams } from "./MidiEvent"; import { MidiEvent, MidiParams } from "./MidiEvent";
import { RestEvent } from "./RestEvent"; import { RestEvent } from "./RestEvent";
import { arrayOfObjectsToObjectWithArrays, isGenerator } from "../Utils/Generic"; import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic";
import { TonnetzSpaces } from "zifferjs/src/tonnetz"; import { TonnetzSpaces } from "zifferjs/src/tonnetz";
import { safeMod } from "zifferjs/src/utils";
export type InputOptions = { [key: string]: string | number }; export type InputOptions = { [key: string]: string | number };
export class Player extends AbstractEvent { export class Player extends AbstractEvent {
input: string | number; input: string|number;
ziffers: Ziffers; ziffers: Ziffers;
initCallTime: number = 0; initCallTime: number = 0;
startCallTime: number = 0; startCallTime: number = 0;
lastCallTime: number = 0; lastCallTime: number = 0;
waitTime = 0; waitTime = 0;
cueName: string|undefined = undefined;
played: boolean = false; played: boolean = false;
current!: Pitch | Chord | ZRest; current!: Pitch | Chord | ZRest;
retro: boolean = false; retro: boolean = false;
@ -28,11 +26,10 @@ export class Player extends AbstractEvent {
skipIndex = 0; skipIndex = 0;
constructor( constructor(
input: string | number | Generator<number>, input: string|number|Generator<number>,
options: InputOptions, options: InputOptions,
public app: Editor, public app: Editor,
zid: string = "", zid: string = ""
waitTime: number = 0,
) { ) {
super(app); super(app);
this.options = options; this.options = options;
@ -41,35 +38,14 @@ export class Player extends AbstractEvent {
this.ziffers = new Ziffers(input, options); this.ziffers = new Ziffers(input, options);
} else if (typeof input === "number") { } else if (typeof input === "number") {
this.input = input; this.input = input;
this.ziffers = Ziffers.fromNumber(input, options); this.ziffers = Ziffers.fromNumber(input,options);
} else if (isGenerator(input)) {
this.ziffers = Ziffers.fromGenerator(input, options);
this.input = this.ziffers.input;
} else { } else {
throw new Error("Invalid input"); this.ziffers = Ziffers.fromGenerator(input,options);
this.input = this.ziffers.input;
} }
if(waitTime) this.waitTime = waitTime;
this.zid = zid; this.zid = zid;
} }
updatePattern(input: string, options: InputOptions): boolean {
const oldIndex = this.ziffers.index;
const newPattern = new Ziffers(input, options);
if(newPattern.values.length > 0) {
this.ziffers = newPattern;
this.ziffers.update();
this.ziffers.index = oldIndex;
this.input = input;
this.options = options;
return true;
}
return false;
}
isValid() {
return this.ziffers.values.length > 0;
}
reset() { reset() {
this.initCallTime = 0; this.initCallTime = 0;
this.startCallTime = 0; this.startCallTime = 0;
@ -149,13 +125,11 @@ export class Player extends AbstractEvent {
const patternIsStarting = const patternIsStarting =
this.notStarted() && this.notStarted() &&
this.waitTime >= 0 && (this.pulse() === 0 || this.origin() >= this.nextBeatInTicks()) &&
this.origin() >= this.waitTime && this.origin() >= this.waitTime;
(this.pulse() === 0 || this.origin() >= this.nextBeatInTicks());
const timeToPlayNext = const timeToPlayNext =
this.current && this.current &&
this.waitTime >= 0 &&
this.pulseToSecond(this.origin()) >= this.pulseToSecond(this.origin()) >=
this.pulseToSecond(this.lastCallTime) + this.pulseToSecond(this.lastCallTime) +
this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) && this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) &&
@ -181,67 +155,43 @@ export class Player extends AbstractEvent {
return areWeThereYet; return areWeThereYet;
}; };
checkCue() { sound(name?: string) {
if(this.ziffers.atLast()) {
if(this.cueName && this.app.api.cueTimes[this.cueName]) {
delete this.app.api.cueTimes[this.cueName];
this.cueName = undefined;
this.waitTime = -1;
}
}
}
sound(name?: string | string[] | SoundParams | SoundParams[]) {
if (this.areWeThereYet()) { if (this.areWeThereYet()) {
this.checkCue();
const event = this.next() as Pitch | Chord | ZRest; const event = this.next() as Pitch | Chord | ZRest;
const noteLengthInSeconds = this.app.clock.convertPulseToSecond( const noteLengthInSeconds = this.app.clock.convertPulseToSecond(
event.duration * 4 * this.app.clock.ppqn, event.duration * 4 * this.app.clock.ppqn
); );
if (event instanceof Pitch) { if (event instanceof Pitch) {
let obj = event.getExisting( const obj = event.getExisting(
"freq", "freq",
"note", "note",
"pitch", "pitch",
"originalPitch",
"key", "key",
"scale", "scale",
"octave", "octave",
"pitchOctave", "parsedScale"
"addedOctave",
"parsedScale",
) as SoundParams; ) as SoundParams;
if (event.sound) name = event.sound as string; 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; if (event.soundIndex) obj.n = event.soundIndex as number;
obj.dur = noteLengthInSeconds; obj.dur = noteLengthInSeconds;
return new SoundEvent(obj, this.app); return new SoundEvent(obj, this.app).sound(name || "sine");
} else if (event instanceof Chord) { } else if (event instanceof Chord) {
const pitches = event.pitches.map((p) => { const pitches = event.pitches.map((p) => {
return p.getExisting( return p.getExisting(
"freq", "freq",
"note", "note",
"pitch", "pitch",
"originalPitch",
"key", "key",
"scale", "scale",
"octave", "octave",
"pitchOctave", "parsedScale"
"addedOctave",
"parsedScale",
); );
}) as SoundParams[]; }) as SoundParams[];
const add = { dur: noteLengthInSeconds } as SoundParams;
let add = { dur: noteLengthInSeconds} as SoundParams; if (name) add.s = name;
if(name) add = {...add, ...this.processSound(name)};
else add.s = "sine";
let sound = arrayOfObjectsToObjectWithArrays( let sound = arrayOfObjectsToObjectWithArrays(
pitches, pitches,
add, add
) as SoundParams; ) as SoundParams;
return new SoundEvent(sound, this.app); return new SoundEvent(sound, this.app);
} else if (event instanceof ZRest) { } else if (event instanceof ZRest) {
@ -254,19 +204,15 @@ export class Player extends AbstractEvent {
midi(value: number | undefined = undefined) { midi(value: number | undefined = undefined) {
if (this.areWeThereYet()) { if (this.areWeThereYet()) {
this.checkCue();
const event = this.next() as Pitch | Chord | ZRest; const event = this.next() as Pitch | Chord | ZRest;
const obj = event.getExisting( const obj = event.getExisting(
"note", "note",
"pitch", "pitch",
"originalPitch",
"bend", "bend",
"key", "key",
"scale", "scale",
"octave", "octave",
"pitchOctave", "parsedScale"
"addedOctave",
"parsedScale",
) as MidiParams; ) as MidiParams;
if (event instanceof Pitch) { if (event instanceof Pitch) {
if (event.soundIndex) obj.channel = event.soundIndex as number; if (event.soundIndex) obj.channel = event.soundIndex as number;
@ -284,34 +230,11 @@ export class Player extends AbstractEvent {
} }
} }
scale(name: string|number[]) { scale(name: string) {
if (this.atTheBeginning()) this.ziffers.scale(name); if (this.atTheBeginning()) this.ziffers.scale(name);
return this; return this;
} }
semitones(values: number|number[], ...rest: number[]) {
values = typeof values === "number" ? [values, ...rest] : values;
if (this.atTheBeginning()) this.ziffers.semitones(values);
return this;
}
cents(values: number|number[], ...rest: number[]) {
values = typeof values === "number" ? [values, ...rest] : values;
if (this.atTheBeginning()) this.ziffers.cents(values);
return this;
}
ratios(values: number|number[], ...rest: number[]) {
values = typeof values === "number" ? [values, ...rest] : values;
if (this.atTheBeginning()) this.ziffers.ratios(values);
return this;
}
edo(value: number, scale: string|number[] = new Array(value).fill(1)) {
if (this.atTheBeginning()) this.ziffers.edo(value, scale);
return this;
}
key(name: string) { key(name: string) {
if (this.atTheBeginning()) this.ziffers.key(name); if (this.atTheBeginning()) this.ziffers.key(name);
return this; return this;
@ -322,12 +245,6 @@ export class Player extends AbstractEvent {
return this; return this;
} }
tonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) {
// @ts-ignore
if (this.atTheBeginning()) this.ziffers.tonnetz(transform, tonnetz);
return this;
}
triadTonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) { triadTonnetz(transform: string, tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.triadTonnetz(transform, tonnetz); if (this.atTheBeginning()) this.ziffers.triadTonnetz(transform, tonnetz);
return this; return this;
@ -338,92 +255,18 @@ export class Player extends AbstractEvent {
return this; return this;
} }
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 4, components: number = 1) { octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz, repeats, components); if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz);
return this; return this;
} }
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) { hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz, repeats, components); if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz);
return this; return this;
} }
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) { enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz, repeats, components); if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz);
return this;
}
cubeDance(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
if (this.atTheBeginning()) this.ziffers.cubeDance(tonnetz, repeats);
return this;
}
powerTowers(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3) {
if (this.atTheBeginning()) this.ziffers.powerTowers(tonnetz, repeats);
return this;
}
powerTower = this.powerTowers;
octaTower(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) {
if (this.atTheBeginning()) this.ziffers.octaTower(tonnetz, repeats, components);
return this;
}
octaTowers = this.octaTower;
boretzRegions(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.boretzRegions(tonnetz);
return this;
}
boretz = this.boretzRegions;
weitzmannRegions(tonnetz: TonnetzSpaces = [3, 4, 5]) {
if (this.atTheBeginning()) this.ziffers.weitzmannRegions(tonnetz);
return this;
}
weitzmann = this.weitzmannRegions;
shuffle() {
if (this.atTheBeginning()) this.ziffers.shuffle();
return this;
}
deal(amount: number = this.ziffers.values.length) {
if (this.atTheBeginning()) this.ziffers.deal(amount);
return this;
}
from(value: number) {
if (this.atTheBeginning()) this.ziffers.from(value);
return this;
}
to(value: number) {
if (this.atTheBeginning()) this.ziffers.to(value);
return this;
}
between(value: number, value2: number) {
if (this.atTheBeginning()) this.ziffers.between(value, value2+1);
return this;
}
at(value: number, ...rest: number[]) {
if (this.atTheBeginning()) this.ziffers.at(value, ...rest);
return this;
}
keep() {
this.ziffers.setRedo(0);
return this;
}
repeat(amount: number) {
this.ziffers.setRedo(amount < 0 ? 0 : amount);
return this;
}
every(amount: number) {
if (this.atTheBeginning()) this.ziffers.every(amount);
return this; return this;
} }
@ -439,12 +282,6 @@ export class Player extends AbstractEvent {
lead = () => this.voiceleading(); 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) => { invert = (n: number) => {
if (this.atTheBeginning()) { if (this.atTheBeginning()) {
this.ziffers.invert(n); this.ziffers.invert(n);
@ -457,71 +294,25 @@ export class Player extends AbstractEvent {
return this; return this;
} }
rotate(amount: number = 1) { wait(value: number | Function) {
if (this.atTheBeginning()) {
this.ziffers.rotate(amount+safeMod(this.ziffers.cycleIndex,this.ziffers.evaluated.length));
}
return this;
}
listen(value: string) {
if(typeof value === "string") {
const cueTime = this.app.api.cueTimes[value];
this.cueName = value;
if(cueTime && this.app.clock.pulses_since_origin <= cueTime) {
this.waitTime = cueTime;
} else {
this.waitTime = -1;
}
return this;
}
}
wait(value: number | string | Function) {
if(typeof value === "string") {
const cueTime = this.app.api.cueTimes[value];
this.cueName = value;
if(cueTime && this.app.clock.pulses_since_origin <= cueTime) {
this.waitTime = cueTime;
} else if(this.atTheBeginning()){
this.waitTime = -1;
}
return this;
}
if (this.atTheBeginning()) { if (this.atTheBeginning()) {
if (typeof value === "function") { if (typeof value === "function") {
const refPat = this.app.api.patternCache.get(value.name) as Player; const refPat = this.app.api.patternCache.get(value.name) as Player;
if (refPat) this.waitTime = refPat.nextEndTime(); if (refPat) this.waitTime = refPat.nextEndTime();
return this; return this;
} else if(typeof value === "number") {
this.waitTime =
this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn);
return this;
} }
this.waitTime =
this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn);
} }
return this; return this;
} }
sync(value: string | Function, manualSync: boolean = true) { sync(value: string | Function) {
if(typeof value === "string" && manualSync) {
if(manualSync) {
const cueTime = this.app.api.cueTimes[value];
if(cueTime) {
this.waitTime = cueTime;
} else {
this.waitTime = -1;
}
}
return this;
}
if (this.atTheBeginning() && this.notStarted()) { if (this.atTheBeginning() && this.notStarted()) {
const origin = this.app.clock.pulses_since_origin; const origin = this.app.clock.pulses_since_origin;
const syncId = typeof value === "function" ? value.name : value;
if (origin > 0) { if (origin > 0) {
const syncName = typeof value === "function" ? value.name : value; const syncPattern = this.app.api.patternCache.get(syncId) as Player;
const syncPattern = this.app.api.patternCache.get(syncName) as Player;
if (syncPattern) { if (syncPattern) {
const syncPatternDuration = syncPattern.ziffers.duration; const syncPatternDuration = syncPattern.ziffers.duration;
const syncPatternStart = syncPattern.startCallTime; const syncPatternStart = syncPattern.startCallTime;
@ -533,13 +324,6 @@ export class Player extends AbstractEvent {
return this; return this;
} }
log(key: string, ...args: string[]) {
this.app.api.log(this.ziffers.evaluated.map((p) => {
return Object.values(p.getExisting(...[key,...args]));
}).join(" "));
return this;
}
out = (): void => { out = (): void => {
// TODO? // TODO?
}; };

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main"; import { type Editor } from "../../main";
import { makeExampleFactory } from "../../../Documentation"; import { makeExampleFactory } from "../../Documentation";
export const distortion = (application: Editor): string => { export const distortion = (application: Editor): string => {
// @ts-ignore // @ts-ignore
@ -18,13 +18,14 @@ Three additional effects that are easy enough to understand. These effects are d
${makeExample( ${makeExample(
"Crunch... crunch... crunch!", "Crunch... crunch... crunch!",
` `
beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me
beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out() beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out()
`, `,
true, true
)}; )};
`}
`;
};

View File

@ -1,7 +1,7 @@
import { type Editor } from "../../../main"; import { type Editor } from "../../main";
import { makeExampleFactory } from "../../../Documentation"; import { makeExampleFactory } from "../../Documentation";
export const effects = (application: Editor): string => { export const reverb = (application: Editor): string => {
// @ts-ignore // @ts-ignore
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
return ` return `
@ -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) | | <ic>roomdim</ic> | | Reverb lowpass frequency at -60db (in hertz) |
${makeExample( ${makeExample(
"Clapping in the cavern", "Clapping in the cavern",
` `
beat(2)::snd('cp').room(0.5).size(4).out() beat(2)::snd('cp').room(0.5).size(4).out()
`, `,
true, true
)}; )};
## Delay ## Delay
@ -42,13 +42,10 @@ A good sounding delay unit that can go into feedback territory. Use it without m
| <ic>delayfeedback</ic> | delayfb | Delay feedback (between <ic>0</ic> and <ic>1</ic>) | | <ic>delayfeedback</ic> | delayfb | Delay feedback (between <ic>0</ic> and <ic>1</ic>) |
${makeExample( ${makeExample(
"Who doesn't like delay?", "Who doesn't like delay?", `
`
beat(2)::snd('cp').delay(0.5).delaytime(0.75).delayfb(0.8).out() beat(2)::snd('cp').delay(0.5).delaytime(0.75).delayfb(0.8).out()
beat(4)::snd('snare').out() beat(4)::snd('snare').out()
beat(1)::snd('kick').out()`, beat(1)::snd('kick').out()`, true)}
true,
)}
## Phaser ## Phaser
@ -59,17 +56,13 @@ beat(1)::snd('kick').out()`,
| <ic>phaserSweep</ic> | <ic>phassweep</ic> | Phaser frequency sweep (in hertz) | | <ic>phaserSweep</ic> | <ic>phassweep</ic> | Phaser frequency sweep (in hertz) |
| <ic>phaserCenter</ic> | <ic>phascenter</ic> | Phaser center frequency (default to 1000) | | <ic>phaserCenter</ic> | <ic>phascenter</ic> | Phaser center frequency (default to 1000) |
${makeExample( ${makeExample("Super cool phaser lick", `
"Super cool phaser lick",
`
rhythm(.5, 7, 8)::sound('wt_stereo') rhythm(.5, 7, 8)::sound('wt_stereo')
.phaser(0.75).phaserSweep(3000) .phaser(0.75).phaserSweep(3000)
.phaserCenter(1500).phaserDepth(1) .phaserCenter(1500).phaserDepth(1)
.note([0, 1, 2, 3, 4, 5, 6].scale('pentatonic', 50).beat(0.25)) .note([0, 1, 2, 3, 4, 5, 6].scale('pentatonic', 50).beat(0.25))
.room(0.5).size(4).out() .room(0.5).size(4).out()
`, `, true)}
true,
)}
## Distorsion, saturation, destruction ## Distorsion, saturation, destruction
@ -83,41 +76,11 @@ Three additional effects that are easy enough to understand. These effects are d
${makeExample( ${makeExample(
"Crunch... crunch... crunch!", "Crunch... crunch... crunch!",
` `
beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me beat(.5)::snd('pad').coarse($(1) % 16).clip(.5).out(); // Comment me
beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out() beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out()
`, `,
true, true
)}; )};
`}
## Vibrato
You can also add some amount of vibrato to the sound using the <ic>vib</ic> and <ic>vibmod</ic> methods. These can turn any oscillator into something more lively and/or into a sound effect when used with a high amount of modulation.
${makeExample(
"Different vibrato settings",
`
tempo(140);
beat(1) :: sound('triangle')
.freq(400).release(0.2)
.vib([1/2, 1, 2, 4].beat())
.vibmod([1,2,4,8].beat(2))
.out()`,
true,
)}
## Compression
This effect is leveraging the basic WebAudio compressor. More information can be found about it on the [DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode?retiredLocale=de#instance_properties) page. This can be come quite complex :)
| Method | Alias | Description |
|------------|-----------|---------------------------------|
| <ic>comp</ic> | cmp | Compressor threshold value (dB) over which compressor operates |
| <ic>ratio</ic> | rt | Compressor ratio: input amount in dB needed for 1dB change in the output |
| <ic>knee</ic> | kn | dB value defining the range over which the signal transitions to compressed section |
| <ic>compAttack</ic> | cmpa | In seconds, time to decrease the gain by 10db |
| <ic>compRelease</ic> | cmpr | In seconds, time to increase the gain by 10db |
`;
};

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main"; import { type Editor } from "../../main";
import { makeExampleFactory } from "../../../Documentation"; import { makeExampleFactory } from "../../Documentation";
export const sampler = (application: Editor): string => { export const sampler = (application: Editor): string => {
// @ts-ignore // @ts-ignore
@ -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. Let's apply some of these methods naïvely. We will then break everything using simpler examples.
${makeExample( ${makeExample(
"Complex sampling duties", "Complex sampling duties",
` `
// Using some of the modifiers described above :) // Using some of the modifiers described above :)
beat(.5)::snd('pad').begin(0.2) beat(.5)::snd('pad').begin(0.2)
.speed([1, 0.9, 0.8].beat(4)) .speed([1, 0.9, 0.8].beat(4))
@ -38,61 +38,45 @@ beat(.5)::snd('pad').begin(0.2)
.room(0.8).size(0.5) .room(0.8).size(0.5)
.clip(1).out() .clip(1).out()
`, `,
true, true
)}; )};
## Playback speed / pitching samples ## Playback speed / pitching samples
Let's play with the <ic>speed</ic> parameter to control the pitch of sample playback: Let's play with the <ic>speed</ic> parameter to control the pitch of sample playback:
${makeExample( ${makeExample("Controlling the playback speed", `
"Controlling the playback speed",
`
beat(0.5)::sound('notes') beat(0.5)::sound('notes')
.speed([1,2,3,4].palindrome().beat(0.5)).out() .speed([1,2,3,4].palindrome().beat(0.5)).out()
`, `, true)}
true,
)}
It also works by using negative values. It reverses the playback: It also works by using negative values. It reverses the playback:
${makeExample( ${makeExample("Playing samples backwards", `
"Playing samples backwards",
`
beat(0.5)::sound('notes') beat(0.5)::sound('notes')
.speed(-[1,2,3,4].palindrome().beat(0.5)).out() .speed(-[1,2,3,4].palindrome().beat(0.5)).out()
`, `, true)}
true,
)}
Of course you can play melodies using samples: Of course you can play melodies using samples:
${makeExample( ${makeExample("Playing melodies using samples", `
"Playing melodies using samples",
`
beat(0.5)::sound('notes') beat(0.5)::sound('notes')
.room(0.5).size(4) .room(0.5).size(4)
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.5)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.5)).out()
`, `, true)}
true,
)}
## Panning ## Panning
To pan samples, use the <ic>.pan</ic> method with a number between <ic>0</ic> and <ic>1</ic>. To pan samples, use the <ic>.pan</ic> method with a number between <ic>0</ic> and <ic>1</ic>.
${makeExample( ${makeExample("Playing melodies using samples", `
"Playing melodies using samples",
`
beat(0.25)::sound('notes') beat(0.25)::sound('notes')
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out()
`, `, true)}
true,
)}
## Looping over a sample ## Looping over a sample
@ -100,30 +84,26 @@ beat(0.25)::sound('notes')
Using <ic>loop</ic> (<ic>1</ic> for looping), <ic>loopBegin</ic> and <ic>loopEnd</ic> (between <ic>0</ic> and <ic>1</ic>), you can loop over the length of a sample. It can be super effective to create granular effects. Using <ic>loop</ic> (<ic>1</ic> for looping), <ic>loopBegin</ic> and <ic>loopEnd</ic> (between <ic>0</ic> and <ic>1</ic>), you can loop over the length of a sample. It can be super effective to create granular effects.
${makeExample( ${makeExample("Granulation using loop", `
"Granulation using loop",
`
beat(0.25)::sound('fikea').loop(1) beat(0.25)::sound('fikea').loop(1)
.lpf(ir(2000, 5000)) .lpf(ir(2000, 5000))
.loopBegin(0).loopEnd(r(0, 1)) .loopBegin(0).loopEnd(r(0, 1))
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out()
`, `, true)}
true,
)}
## Stretching a sample ## Stretching a sample
The <ic>stretch</ic> parameter can help you to stretch long samples like amen breaks: The <ic>stretch</ic> parameter can help you to stretch long samples like amen breaks:
${makeExample( ${makeExample(
"Playing an amen break", "Playing an amen break",
` `
// Note that stretch has the same value as beat // Note that stretch has the same value as beat
beat(4) :: sound('amen1').n(11).stretch(4).out() beat(4) :: sound('amen1').n(11).stretch(4).out()
beat(1) :: sound('kick').shape(0.35).out()`, beat(1) :: sound('kick').shape(0.35).out()`,
true, true,
)}; )};
## Cutting samples ## Cutting samples
@ -131,45 +111,34 @@ Sometimes, you will find it necessary to cut a sample. It can be because the sam
Know about the <ic>begin</ic> and <ic>end</ic> parameters. They are not related to the sampler itself, but to the length of the event you are playing. Let's cut the granular example: 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( ${makeExample("Cutting a sample using end", `
"Cutting a sample using end",
`
beat(0.25)::sound('notes') beat(0.25)::sound('notes')
.end(usine(1/2)/0.5) .end(usine(1/2)/0.5)
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out() .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.25)).out()
`, `, true)}
true,
)}
You can also use <ic>clip</ic> to cut the sample everytime a new sample comes in: You can also use <ic>clip</ic> to cut the sample everytime a new sample comes in:
${makeExample( ${makeExample("Cutting a sample using end", `
"Cutting a sample using end",
`
beat(0.125)::sound('notes') beat(0.125)::sound('notes')
.cut(1) .cut(1)
.room(0.5).size(4).pan(r(0, 1)) .room(0.5).size(4).pan(r(0, 1))
.note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.125) .note([0, 2, 3, 4, 5].scale('minor', 50).beat(0.125)
+ [-12,12].beat()).out() + [-12,12].beat()).out()
`, `, true)}
true,
)}
## Adding vibrato to samples ## Adding vibrato to samples
You can add vibrato to any sample using <ic>vib</ic> and <ic>vibmod</ic>: You can add vibrato to any sample using <ic>vib</ic> and <ic>vibmod</ic>:
${makeExample( ${makeExample("Adding vibrato to a sample", `
"Adding vibrato to a sample",
`
beat(1)::sound('fhang').vib([1, 2, 4].bar()).vibmod([0.5, 2].beat()).out() beat(1)::sound('fhang').vib([1, 2, 4].bar()).vibmod([0.5, 2].beat()).out()
`, `, true)}
true,
)}
`}
`;
};

View File

@ -1,193 +0,0 @@
import { type Editor } from "../../main";
import { makeExampleFactory } from "../../Documentation";
export const atelier = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Atelier (06 mars 2024)
Bonjour tout le monde ! Nous sommes :
- [Rémi Georges](https://remigeorges.fr) : musicien, réalisateur en informatique musicale.
- [Agathe Herrou](https://www.youtube.com/@th4music) : musicienne, chercheuse.
- [Raphaël Forment](https://raphaelforment.fr) : musicien, doctorant.
Nous pratiquons le [live coding](https://livecoding.fr). Nous utilisons notre ordinateur comme un instrument de musique, nous programmons de la musique devant notre public. Nous pouvons faire plein de choses comme :
- créer des instruments de musique, des synthétiseurs, des boîtes à rythme.
- jouer des échantillons, charger des images, des vidéos, créer des animations.
- contrôler d'autres instruments, jouer avec d'autres musiciens.
Topos est un instrument de musique. On peut l'utiliser depuis n'importe quel ordinateur, sans avoir à installer quoi que ce soit. Nous l'avons fabriqué pour que tout le monde puisse jouer facilement de la musique.
## Découverte
<br>
${makeExample(
"Percussions", `
tempo(120) // Changer le tempo
beat(1)::sound('kick').out()
beat(2)::sound('snare').out()
beat(.5)::sound('hh').out()
`, true,)}
<br>
- Qu'est-ce qu'il se passe si je change un nombre ?
- Qu'est-ce qu'il se passe si je change un nom ?
- Essayez par exemple <ic>"sid"</ic> ou <ic>"trump"</ic>.
- Qu'est-ce qu'il se passe si j'enlève <ic>.out()</ic> ?
- Est-il possible de jouer un rythme très rapide ou très lent ?
### Ajout d'une basse
<br>
${makeExample(
"Une basse", `
// Aucun changement dans le code
beat(1)::sound('kick').out()
beat(2)::sound('snare').out()
beat(.5)::sound('hh').out()
// Une nouvelle partie
beat([0.25,0.5].beat(1))::sound("pluck")
.note([40,45].beat(2)).out()
`, true,)}
<br>
- Qu'est-ce que le son <ic>"pluck"</ic> ?
- Que signifie <ic>.note([40,45].beat(2))</ic> ?
- Que se passe-t-il si je change la valeur dans <ic>.beat(2)</ic> ?
- Que se passe-t-il lorsque j'ajoute de nouveaux nombres dans <ic>[40, 45]</ic> ?
### Ajout d'une mélodie
<br>
${makeExample(
"Le morceau complet", `
// Aucun changement dans le code
beat(1)::sound('kick').out()
beat(2)::sound('snare').out()
beat(.5)::sound('hh').out()
beat([0.25,0.5].beat(1))::sound("pluck")
.note([40,45].beat(2)).out()
// Nouvelle partie mélodique
beat([0.25,0.5].beat())::sound("pluck")
.note([0,7,5,8,2,9,0].scale("Major",60).beat(1))
.vib(8).vibmod(1/4)
.delay(0.5).room(1.5).size(0.5)
.out()
`, true,)}
<br>
Ici, on ajoute une nouvelle mélodie mais il s'agit aussi d'un nouvel instrument. C'est pour cela que le code est plus long. Quand on fait du <em>live coding</em>, on code tout en même temps : notes, rythmes, mélodies, sons. C'est beaucoup de choses ! C'est pour cela que le code est court, on essaie de tout taper très vite en jouant !
- Que signifie selon vous <ic>vib</ic>, <ic>delay</ic>, <ic>room</ic> ou <ic>size</ic> ?
- Que se passe-t-il si je change les valeurs dans <ic>vib</ic>, <ic>delay</ic>, <ic>room</ic> ou <ic>size</ic> ?
<br>
**Exercices :**
- Transformer <ic>vib(8)</ic> en <ic>vib([2,4,8].beat(1))</ic>.
- Transformer <ic>"pluck"</ic> en <ic>["pluck", "clap"].beat(1)</ic>.
Vous pouvez aussi utiliser la fonction <ic>rhythm</ic> pour jouer rapidement des rythmes.
${makeExample(
"Rythmes rythmes rythmes", `
rhythm(0.5, 3, 8)::sound('bd').out()
rhythm(0.5, 3, 8)::sound('clap').out()
rhythm(0.5, 6, 8)::sound('hat').out()
rhythm(0.25, 6, 8)::sound('hat')
.vel(0.3).speed(2).out()
rhythm(0.5, 2, 8)::sound('sd').out()
`, true)};
## Créer un instrument
<br>
Nous allons créer un nouvel instrument à partir d'un son de base. Voici un premier son :
${makeExample("Notre son de base", `beat(2)::sound('sine').note(50).ad(0, .5).out()`, true)}
<br>
Ce son est assez ennuyeux. Nous allons ajouter quelques paramètres :
${makeExample("Beaucoup mieux !", `beat(2)::sound('sine').note(50).fmi(2).fmh(2).ad(0, .5).out()`, true)}
<br>
Nous allons aussi ajouter quelques effets intéressants :
${makeExample("Ajout d'un écho", `beat(2)::sound('sine').note(50)
.fmi(2).fmh(2).ad(1/16, 1.5)
.delay(0.5).delayt(0.75).out()`,
true)}
<br>
Nous pouvons utiliser plusieurs techniques pour rendre le son plus dynamique :
- générer des valeurs aléatoires pour les paramètres
- utiliser des générateurs de valeurs (comme <ic>usine</ic>)
- utiliser la souris ou un autre contrôleur pour changer les valeurs en temps réel
${makeExample("Plus dynamique encore", `
beat(2)::sound('sine').note([50,55,57,62,66, 69, 74].mouseX())
.fmi(usine(1/4)).fmh([1,2,0.5].beat())
.ad(1/16, 1.5).delay(0.5).delayt(0.75)
.out()`, true)}
<br>
Un exemple final, le plus complexe jusqu'à présent :
${makeExample("Un instrument de musique complet", `
beat(2)::sound('triangle')
.note([50,55,57,62,66, 69, 74].mouseX())
.fmi(usine(1/4)).fmh([1,2,0.5].beat())
.ad(1/16, 1.5).delay(0.5).delayt(0.75)
.room(0.5).size(8).lpf(usine(1/3)*4000).out()`, true)}
## Compléments
${makeExample("Quelques échantillons", `
ab ade ades2 ades3 ades4 alex alphabet amencutup armora arp arpy auto
baa baa2 bass bass0 bass1 bass2 bass3 bassdm bassfoo battles bd bend
bev bin birds birds3 bleep blip blue bottle breaks125 breaks152
breaks157 breaks165 breath bubble can casio cb cc chin circus clak
click clubkick co coins control cosmicg cp cr crow d db diphone
diphone2 dist dork2 dorkbot dr dr2 dr55 dr_few drum drumtraks e east
electro1 em2 erk f feel feelfx fest fire flick fm foo future gab
gabba gabbaloud gabbalouder glasstap glitch glitch2 gretsch gtr h
hand hardcore hardkick haw hc hh hh27 hit hmm ho hoover house ht if
ifdrums incoming industrial insect invaders jazz jungbass jungle juno
jvbass kicklinn koy kurt latibro led less lighter linnhats lt made
made2 mash mash2 metal miniyeah monsterb moog mouth mp3 msg mt mute
newnotes noise noise2 notes numbers oc off outdoor pad padlong pebbles
perc peri pluck popkick print proc procshort psr rave rave2 ravemono
realclaps reverbkick rm rs sax sd seawolf sequential sf sheffield
short sid sine sitar sn space speakspell speech speechless speedupdown
stab stomp subroc3d sugar sundance tabla tabla2 tablex tacscan tech
techno tink tok toys trump ul ulgab uxay v voodoo wind wobble world
xmas yeah`, true)}
`
};

View File

@ -5,79 +5,79 @@ export const code = (application: Editor): string => {
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
return ` return `
# Code # Code
Topos scripts are using the [JavaScript](https://en.wikipedia.org/wiki/JavaScript) syntax. This is the language used to write pretty much anything in a web browser. JavaScript is easy to learn, and even faster to learn if you are already familiar with other programming languages. Here are some good resources if you want to learn more about it: Topos scripts are using the [JavaScript](https://en.wikipedia.org/wiki/JavaScript) syntax. This is the language used to write pretty much anything in a web browser. JavaScript is easy to learn, and even faster to learn if you are already familiar with other programming languages. Here are some good resources if you want to learn more about it:
- [MDN (Mozilla Web Docs)](https://developer.mozilla.org/): it covers pretty much anything and is considered to be a reliable source to learn how the web currently works. Any web developer knows about it. - [MDN (Mozilla Web Docs)](https://developer.mozilla.org/): it covers pretty much anything and is considered to be a reliable source to learn how the web currently works. Any web developer knows about it.
- [Learn JS in Y Minutes](https://learnxinyminutes.com/docs/javascript/): a good tour of the language. Can be useful as a refresher. - [Learn JS in Y Minutes](https://learnxinyminutes.com/docs/javascript/): a good tour of the language. Can be useful as a refresher.
- [The Modern JavaScript Tutorial](https://javascript.info/): another well known source to learn the language. - [The Modern JavaScript Tutorial](https://javascript.info/): another well known source to learn the language.
**You do not need to have any prior knowledge of programming to use Topos**. **You do not need to have any prior knowledge of programming** to use Topos**.
# How is the code evaluated? # How is the code evaluated?
The code you enter in any of the scripts is evaluated in strict mode. This tells your browser that the code you run can be optimized quite agressively. We need this because by default, **the global script is evaluated 48 times per beat**. It also means that you can crash at the speed of light :smile:. There are some things to keep in mind: The code you enter in any of the scripts is evaluated in strict mode. This tells your browser that the code you run can be optimized quite agressively. We need this because by default, **the global script is evaluated 48 times per beat**. It also means that you can crash at the speed of light :smile:. There are some things to keep in mind:
- **about variables:** the state of your variables is not kept between iterations. If you write <ic>let a = 2</ic> and remove that value from your script, **it will crash**! Variable and state is not preserved between each run of the script. There are other ways to deal with variables and to share variables between scripts! Some variables like **iterators** can keep their state between iterations because they are saved **with the file itself**. There is also **global variables**. - **about variables:** the state of your variables is not kept between iterations. If you write <ic>let a = 2</ic> and remove that value from your script, **it will crash**! Variable and state is not preserved between each run of the script. There are other ways to deal with variables and to share variables between scripts! Some variables like **iterators** can keep their state between iterations because they are saved **with the file itself**. There is also **global variables**.
- **about errors and printing:** your code will crash! Don't worry, we do our best to make it crash in the most gracious way possible. Most errors are caught and displayed in the interface. For weirder bugs, open the dev console with ${key_shortcut( - **about errors and printing:** your code will crash! Don't worry, we do our best to make it crash in the most gracious way possible. Most errors are caught and displayed in the interface. For weirder bugs, open the dev console with ${key_shortcut(
"Ctrl + Shift + I", "Ctrl + Shift + I"
)}. You cannot directly use <ic>console.log('hello, world')</ic> in the interface but you can use <ic>log(message)</ic> to print a one line message. You will have to open the console as well to see your messages being printed there! You can also use <ic>logOnce(message)</ic> to print a message only once (or everytime you press Ctrl+Shift+Backspace). )}. You cannot directly use <ic>console.log('hello, world')</ic> in the interface but you can use <ic>log(message)</ic> to print a one line message. You will have to open the console as well to see your messages being printed there!
- **about new syntax:** sometimes, we had some fun with JavaScript's syntax in order to make it easier/faster to write on stage. <ic>&&</ic> can also be written <ic>::</ic> or <ic>-></ic> because it is faster to type or better for the eyes! - **about new syntax:** sometimes, we had some fun with JavaScript's syntax in order to make it easier/faster to write on stage. <ic>&&</ic> can also be written <ic>::</ic> or <ic>-></ic> because it is faster to type or better for the eyes!
# Common idioms # Common idioms
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: 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( ${makeExample(
"Shortening your if conditions", "Shortening your if conditions",
` `
// The && symbol (overriden by :: in Topos) is very often used for conditions! // The && symbol (overriden by :: in Topos) is very often used for conditions!
beat(.75) :: snd('linnhats').n([1,4,5].beat()).out() beat(.75) :: snd('linnhats').n([1,4,5].beat()).out()
beat(1) :: snd('bd').out() beat(1) :: snd('bd').out()
//if (true) && log('very true') //if (true) && log('very true')
// These two lines are the same: // These two lines are the same:
// beat(1) && snd('bd').out() // beat(1) && snd('bd').out()
//// beat(1) :: snd('bd').out() //// beat(1) :: snd('bd').out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"More complex conditions using ?", "More complex conditions using ?",
` `
// The ? symbol can be used to write a if/true/false condition // The ? symbol can be used to write a if/true/false condition
beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out() beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
// (true) ? log('very true') : log('very false') // (true) ? log('very true') : log('very false')
`, `,
false, false
)} )}
${makeExample( ${makeExample(
"Using not and other short symbols", "Using not and other short symbols",
` `
// The ! symbol can be used to reverse a condition // The ! symbol can be used to reverse a condition
beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out() beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
!beat(2) :: beat(0.5) :: snd('clap').out() !beat(2) :: beat(0.5) :: snd('clap').out()
`, `,
false, false
)} )}
# About crashes and bugs # About crashes and bugs
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. 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( ${makeExample(
"This example will crash! Who cares?", "This example will crash! Who cares?",
` `
// This is crashing. See? No harm! // This is crashing. See? No harm!
qjldfqsdklqsjdlkqjsdlqkjdlksjd 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. Every Topos session is composed of **local**, **global** and **init** scripts. These scripts form a structure called a "_universe_". The scripts can describe whatever you want: songs, sketches, small tools, or whatever. All the scripts are written using the JavaScript programming language. They describe a musical or algorithmic process. You can call them anytime.
- **the global script** (${key_shortcut( - **the global script** (${key_shortcut(
"Ctrl + G", "Ctrl + G"
)}): _Evaluated for every clock pulse_. The central piece, acting as the conductor for all the other scripts. You can also jam directly from the global script to test your ideas before pushing them to a separate script. You can also access that script using the ${key_shortcut( )}): _Evaluated for every clock pulse_. The central piece, acting as the conductor for all the other scripts. You can also jam directly from the global script to test your ideas before pushing them to a separate script. You can also access that script using the ${key_shortcut(
"F10", "F10"
)} key. )} key.
- **the local scripts** (${key_shortcut( - **the local scripts** (${key_shortcut(
"Ctrl + L", "Ctrl + L"
)}): _Evaluated on demand_. Local scripts are used to store anything too complex to sit in the global script. It can be a musical process, a whole section of your composition, a complex controller that you've built for your hardware, etc... You can also switch to one of the local scripts by using the function keys (${key_shortcut( )}): _Evaluated on demand_. Local scripts are used to store anything too complex to sit in the global script. It can be a musical process, a whole section of your composition, a complex controller that you've built for your hardware, etc... You can also switch to one of the local scripts by using the function keys (${key_shortcut(
"F1", "F1"
)} to ${key_shortcut("F9")}). )} to ${key_shortcut("F9")}).
- **the init script** (${key_shortcut( - **the init script** (${key_shortcut(
"Ctrl + I", "Ctrl + I"
)}): _Evaluated on program load_. Used to set up the software the session to the desired state before playing, for example changing bpm or to initialize global variables (See Functions). You can also access that script using the ${key_shortcut( )}): _Evaluated on program load_. Used to set up the software the session to the desired state before playing, for example changing bpm or to initialize global variables (See Functions). You can also access that script using the ${key_shortcut(
"F11", "F11"
)} key. )} key.
- **the note file** (${key_shortcut( - **the note file** (${key_shortcut(
"Ctrl + N", "Ctrl + N"
)}): _Not evaluated_. Used to store your thoughts or commentaries about the session you are currently playing. It is nothing more than a scratchpad really! )}): _Not evaluated_. Used to store your thoughts or commentaries about the session you are currently playing. It is nothing more than a scratchpad really!
${makeExample( ${makeExample(
"Calling scripts to form a musical piece", "Calling scripts to form a musical piece",
` `
beat(1) :: script(1) // Calling local script n°1 beat(1) :: script(1) // Calling local script n°1
flip(4) :: beat(.5) :: script(2) // Calling script n°2 flip(4) :: beat(.5) :: script(2) // Calling script n°2
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Script execution can become musical too!", "Script execution can become musical too!",
` `
// Use algorithms to pick a script. // Use algorithms to pick a script.
beat(1) :: script([1, 3, 5].pick()) beat(1) :: script([1, 3, 5].pick())
flip(4) :: beat([.5, .25].beat(16)) :: script( flip(4) :: beat([.5, .25].beat(16)) :: script(
[5, 6, 7, 8].beat()) [5, 6, 7, 8].beat())
`, `,
false, false
)} )}
### Navigating the interface ### Navigating the interface
@ -81,7 +81,7 @@ There are some useful functions to help you manage your scripts:
A set of files is called a _universe_. You can switch between universes immediately immediately by pressing ${key_shortcut( A set of files is called a _universe_. You can switch between universes immediately immediately by pressing ${key_shortcut(
"Ctrl + B", "Ctrl + B"
)}. You can also create a new universe by entering a name. Load a universe by typing its name. Once a universe is loaded, it is not possible to call any data/code from any other universe. Switching between universes does not stop the transport nor reset the clock. The context switches but time keeps flowing. This can be useful for transitioning between songs / parts. )}. You can also create a new universe by entering a name. Load a universe by typing its name. Once a universe is loaded, it is not possible to call any data/code from any other universe. Switching between universes does not stop the transport nor reset the clock. The context switches but time keeps flowing. This can be useful for transitioning between songs / parts.
There are some useful functions to help you manage your universes: There are some useful functions to help you manage your universes:

View File

@ -14,10 +14,10 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
| Shortcut | Key | Description | | Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|**Start/Pause** transport|${key_shortcut( |**Start/Pause** transport|${key_shortcut(
"Ctrl + P", "Ctrl + P"
)}|Start or pause audio playback| )}|Start or pause audio playback|
|**Stop** the transport |${key_shortcut( |**Stop** the transport |${key_shortcut(
"Ctrl + S", "Ctrl + S"
)}|Stop and rewind audio playback| )}|Stop and rewind audio playback|
### Moving in the interface ### Moving in the interface
@ -26,15 +26,15 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|Universe switch|${key_shortcut("Ctrl + B")}|Switch to a new universe| |Universe switch|${key_shortcut("Ctrl + B")}|Switch to a new universe|
|Global Script|${key_shortcut("Ctrl + G")} or ${key_shortcut( |Global Script|${key_shortcut("Ctrl + G")} or ${key_shortcut(
"F10", "F10"
)}|Switch to global script | )}|Switch to global script |
|Local scripts|${key_shortcut("Ctrl + L")} or ${key_shortcut( |Local scripts|${key_shortcut("Ctrl + L")} or ${key_shortcut(
"F11", "F11"
)}|Switch to local scripts | )}|Switch to local scripts |
|Init script|${key_shortcut("Ctrl + L")}|Switch to init script| |Init script|${key_shortcut("Ctrl + L")}|Switch to init script|
|Note File|${key_shortcut("Ctrl + N")}|Switch to note file| |Note File|${key_shortcut("Ctrl + N")}|Switch to note file|
|Local Script|${key_shortcut("F1")} to ${key_shortcut( |Local Script|${key_shortcut("F1")} to ${key_shortcut(
"F9", "F9"
)}|Switch to a specific local script| )}|Switch to a specific local script|
|Documentation|${key_shortcut("Ctrl + D")}|Open the documentation| |Documentation|${key_shortcut("Ctrl + D")}|Open the documentation|
@ -44,37 +44,33 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|Evaluate|${key_shortcut("Ctrl + Enter")}| Evaluate the current script | |Evaluate|${key_shortcut("Ctrl + Enter")}| Evaluate the current script |
|Local Eval|${key_shortcut("Ctrl + F1")} to ${key_shortcut( |Local Eval|${key_shortcut("Ctrl + F1")} to ${key_shortcut(
"Ctrl + F9", "Ctrl + F9"
)}|Local File Evaluation| )}|Local File Evaluation|
|Force Eval|${key_shortcut( |Force Eval|${key_shortcut(
"Ctrl + Shift + Enter", "Ctrl + Shift + Enter"
)}|Force evaluation of the current script| )}|Force evaluation of the current script|
|Clear cache & Eval|${key_shortcut(
"Ctrl + Shift + Backspace (Delete)",
)}|Clears cache and forces evaluation to update cached scripts|
### Special ### Special
| Shortcut | Key | Description | | Shortcut | Key | Description |
|----------|-------|------------------------------------------------------------| |----------|-------|------------------------------------------------------------|
|Vim Mode|${key_shortcut("Ctrl + V")}| Switch between Vim and Normal Mode| |Vim Mode|${key_shortcut("Ctrl + V")}| Switch between Vim and Normal Mode|
|Maximize|${key_shortcut("Ctrl + M")}| Show/Hide the interface|
# Keyboard Fill # Keyboard Fill
By pressing the ${key_shortcut( By pressing the ${key_shortcut(
"Alt", "Alt"
)} key, you can trigger the <ic>Fill</ic> mode which can either be <ic>true</ic> or <ic>false</ic>. The fill will be set to <ic>true</ic> as long as the key is held. Try pressing ${key_shortcut( )} key, you can trigger the <ic>Fill</ic> mode which can either be <ic>true</ic> or <ic>false</ic>. The fill will be set to <ic>true</ic> as long as the key is held. Try pressing ${key_shortcut(
"Alt", "Alt"
)} when playing this example: )} when playing this example:
${makeExample( ${makeExample(
"Claping twice as fast with fill", "Claping twice as fast with fill",
` `
beat(fill() ? 1/4 : 1/2)::sound('cp').out() 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). - <ic>mouseY()</ic>: the vertical position of the mouse on the screen (as a floating point number).
${makeExample( ${makeExample(
"Vibrato controlled by mouse", "Vibrato controlled by mouse",
` `
beat(.25) :: sound('sine') beat(.25) :: sound('sine')
.note([0,4,5,10,11,15,16] .note([0,4,5,10,11,15,16]
.palindrome() .palindrome()
@ -27,8 +27,8 @@ beat(.25) :: sound('sine')
.pan(r(0, 1)) .pan(r(0, 1))
.room(0.35).size(4).out() .room(0.35).size(4).out()
`, `,
true, true
)} )}
<br> <br>
@ -39,15 +39,15 @@ Current mouse position can also be used to generate notes:
${makeExample( ${makeExample(
"Using the mouse to output a note!", "Using the mouse to output a note!",
` `
beat(.25) :: sound('sine') beat(.25) :: sound('sine')
.lpf(7000) .lpf(7000)
.delay(0.5).delayt(1/6).delayfb(0.2) .delay(0.5).delayt(1/6).delayfb(0.2)
.note(noteX()) .note(noteX())
.room(0.35).size(4).out()`, .room(0.35).size(4).out()`,
true, true
)} )}
## Mouse and Arrays ## Mouse and Arrays
@ -58,14 +58,14 @@ You can use the mouse to explore the valuesq contained in an Array:
${makeExample( ${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([1,2,3,4].mouseX())
log([4,5,6,7].mouseY()) log([4,5,6,7].mouseY())
`, `,
true, true
)} )}

View File

@ -8,13 +8,13 @@ export const introduction = (application: Editor): string => {
# Welcome # Welcome
Welcome to the **Topos** documentation. You can jump here anytime by pressing ${key_shortcut( Welcome to the **Topos** documentation. You can jump here anytime by pressing ${key_shortcut(
"Ctrl + D", "Ctrl + D"
)}. Press again to make the documentation disappear. Contributions are much appreciated! The documentation [lives here](https://github.com/Bubobubobubobubo/topos/tree/main/src/documentation). )}. Press again to make the documentation disappear. Contributions are much appreciated! The documentation [lives here](https://github.com/Bubobubobubobubo/topos/tree/main/src/documentation).
${makeExample( ${makeExample(
"Welcome! Eval to get started", "Welcome! Eval to get started",
examples[Math.floor(Math.random() * examples.length)], examples[Math.floor(Math.random() * examples.length)],
true, true
)} )}
# What is Topos? # What is Topos?
@ -30,7 +30,7 @@ rhythm(.25, [5, 7].beat(2), 8) :: sound(['hc', 'fikea', 'hat'].pick(1))
.db(-ir(1,8)).speed([1,[0.5, 2].pick()]).room(0.5).size(3).o(4).out() .db(-ir(1,8)).speed([1,[0.5, 2].pick()]).room(0.5).size(3).o(4).out()
beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare') beat([2,0.5].dur(13.5, 0.5))::snd('fsoftsnare')
.n(0).speed([1, 0.5]).o(4).out()`, .n(0).speed([1, 0.5]).o(4).out()`,
false, false
)} )}
${makeExample( ${makeExample(
@ -47,7 +47,7 @@ beat(.25)::snd('sine')
.delay(0.5).delayt(0.25).delayfb(0.7) // Delay .delay(0.5).delayt(0.25).delayfb(0.7) // Delay
.room(0.5).size(8) // Reverb .room(0.5).size(8) // Reverb
.out()`, .out()`,
false, false
)} )}
${makeExample( ${makeExample(
@ -58,18 +58,15 @@ beat(.5) :: sound('sid').n($(2))
beat(.25) :: sound('sid').note( beat(.25) :: sound('sid').note(
[34, 36, 41].beat(.25) + [[0,-24].pick(),12].beat()) [34, 36, 41].beat(.25) + [[0,-24].pick(),12].beat())
.room(0.9).size(0.9).n(4).out()`, .room(0.9).size(0.9).n(4).out()`,
false, false
)} )}
Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Teletype is/was an open source hardware module for Eurorack synthesizers. While the Teletype was initially born as an hardware module, Topos aims to be a web-browser based cousin of it! It is a sequencer, a scriptable interface, a companion for algorithmic music-making. Topos wishes to fullfill the same goal as the Teletype, keeping the same spirit alive on the web. It is free, open-source, and made to be shared and used by everyone. Learn more about live coding on [livecoding.fr](https://livecoding.fr). Topos is deeply inspired by the [Monome Teletype](https://monome.org/). The Teletype is/was an open source hardware module for Eurorack synthesizers. While the Teletype was initially born as an hardware module, Topos aims to be a web-browser based cousin of it! It is a sequencer, a scriptable interface, a companion for algorithmic music-making. Topos wishes to fullfill the same goal as the Teletype, keeping the same spirit alive on the web. It is free, open-source, and made to be shared and used by everyone. Learn more about live coding on [livecoding.fr](https://livecoding.fr).
## Alternative documentation source (.pdf) ## Demo Songs
You can also find a .pdf version listing the principal commands and functions [here](https://github.com/Bubobubobubobubo/topos/blob/main/src/documentation/basics/TOPOS_COMMANDS.pdf). This document has been generated by Chris Collis. It recaps the main sections of this documentation and can be a good companion while learning Topos.
## 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>
Reloading the application will get you one random song example to study every time. Press ${key_shortcut(
"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 :).
`; `;
}; };

View File

@ -1,5 +1,5 @@
import { makeExampleFactory } from "../../Documentation"; import { makeExampleFactory } from "../Documentation";
import { type Editor } from "../../main"; import { type Editor } from "../main";
export const chaining = (application: Editor): string => { export const chaining = (application: Editor): string => {
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
@ -9,12 +9,11 @@ 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: 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( ${makeExample(
"Method chaining", "Method chaining",
` `
beat(1)::sound('bd').speed(2).lpf(500).out() beat(1)::sound('bd').speed(2).lpf(500).out()
`, `, true
true, )}
)}
Method chains become fun if you add just a little bit of complexity to them. You can start to add conditions, start to register complex chains to be re-used later on, etc.. We will not remind you how to write basic chains. The whole documentation is full of examples! Let's explore more delicate patterns! Method chains become fun if you add just a little bit of complexity to them. You can start to add conditions, start to register complex chains to be re-used later on, etc.. We will not remind you how to write basic chains. The whole documentation is full of examples! Let's explore more delicate patterns!
@ -23,22 +22,21 @@ 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. You can use the <ic>register(...args)</ic> function to... register a chain that you would like to re-use later on.
${makeExample( ${makeExample(
"Re-creating a classic Tidal function", "Re-creating a classic Tidal function",
` `
// Playing with extreme panning and playback rate // Playing with extreme panning and playback rate
register('juxrev', n=>n.pan([0, 1]).speed([1, -1])) register('juxrev', n=>n.pan([0, 1]).speed([1, -1]))
// Using our new abstraction // Using our new abstraction
beat(1)::sound('fhh').juxrev().out() beat(1)::sound('fhh').juxrev().out()
`, `, true
true, )}
)}
This is an extremely powerful construct. For example, you can use it to create synthesizer presets and reuse them later on. You can also define parameters for your registered functions. For example: This is an extremely powerful construct. For example, you can use it to create synthesizer presets and reuse them later on. You can also define parameters for your registered functions. For example:
${makeExample( ${makeExample(
"Creating synth presets", "Re-creating a classic Tidal function",
` `
// Registering a specific synth architecture // Registering a specific synth architecture
register('sub', (n,x=4,y=80)=>n.ad(0, .25) register('sub', (n,x=4,y=80)=>n.ad(0, .25)
.fmi(x).pan([0, 1]) .fmi(x).pan([0, 1])
@ -50,49 +48,9 @@ register('sub', (n,x=4,y=80)=>n.ad(0, .25)
// Using it with an arpeggio // Using it with an arpeggio
rhythm(.25, [6, 8].beat(), 12)::sound('sine') rhythm(.25, [6, 8].beat(), 12)::sound('sine')
.note([0, 2, 4, 5].scale('minor', 50).beat(0.5)) .note([0, 2, 4, 5].scale('minor', 50).beat(0.5))
.sub(8).out()`, .sub(8).out()`, true
true, )}
)}
## Registering chain for all events
The chain can also be registered automatically for all events. This is useful if you want to add a specific effect to all your events.
${makeExample(
"Registering chain to all events at once",
`
z0("h 9 ^ <7 5 3 1>")
.sound("sine")
.out()
z1("0 4 3 2")
.sound("sine")
.out()
all(x=>x.room(1).delay(rI(0,0.5)))
`,
true,
)}
## Logging values from the chain
You can use the <ic>log()</ic> function to print values from the current event. This can be useful to debug your code. Useful parameters to log could be **note**, **pitch**, **dur**, **octave** etc...
${makeExample(
"Logging values from the chain",
`
beat(1) :: sound("sine").pitch(rI(1,6)).log("note").out()
`,
true,
)}
${makeExample(
"Logging values from ziffers pattern",
`
z1("0 3 2 5").scale("rocritonic").sound("sine").log("pitch","note","key").out()
`,
true,
)}
## Conditional chaining ## Conditional chaining
@ -101,17 +59,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: All functions from the sound object can be used to modify the event, for example:
${makeExample( ${makeExample(
"Modifying sound events with probabilities", "Modifying sound events with probabilities",
` `
beat(.5) && sound('fhh') beat(.5) && sound('fhh')
.odds(1/4, s => s.speed(irand(1,4))) .odds(1/4, s => s.speed(irand(1,4)))
.rarely(s => s.room(0.5).size(8).speed(0.5)) .rarely(s => s.room(0.5).size(8).speed(0.5))
.out()`, .out()`,
true, true
)} )}
${makeExample( ${makeExample(
"Chance to play a random note", "Chance to play a random note",
` `
rhythm(.5, 3, 8) && sound('pluck').note(38).out() rhythm(.5, 3, 8) && sound('pluck').note(38).out()
beat(.5) && sound('pluck').note(60) beat(.5) && sound('pluck').note(60)
.often(s => s.note(57)) .often(s => s.note(57))
@ -119,8 +77,8 @@ beat(.5) && sound('pluck').note(60)
.note(62) .note(62)
.room(0.5).size(3) .room(0.5).size(3)
.out()`, .out()`,
false, false
)} )}
There is a growing collection of probability and chance methods you can use: There is a growing collection of probability and chance methods you can use:
@ -140,19 +98,21 @@ There is a growing collection of probability and chance methods you can use:
| <ic>almostAlways</ic> | With a 98.5% probability. | <ic>.almostAlways(s => s.note(70))</ic> | | <ic>almostAlways</ic> | With a 98.5% probability. | <ic>.almostAlways(s => s.note(70))</ic> |
| <ic>always</ic> | Always transforms the Event. | <ic>.always(s => s.note(71))</ic> | | <ic>always</ic> | Always transforms the Event. | <ic>.always(s => s.note(71))</ic> |
### MIDI Chaining ### MIDI Chaining
The conditional chaining also applies to MIDI. Values can also be incremented using <ic>+=</ic> notation. The conditional chaining also applies to MIDI. Values can also be incremented using <ic>+=</ic> notation.
${makeExample( ${makeExample(
"Modifying midi events with probabilities", "Modifying midi events with probabilities",
`beat(.5) && midi(60).channel(1) `beat(.5) && midi(60).channel(1)
.odds(1/4, n => n.channel(2)) .odds(1/4, n => n.channel(2))
.often(n => n.note+=4) .often(n => n.note+=4)
.sometimes(s => s.velocity(irand(50,100))) .sometimes(s => s.velocity(irand(50,100)))
.out()`, .out()`,
true, true
)}; )};
## Ziffers ## Ziffers
@ -165,8 +125,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**) * <ic>midi()</ic> - for outputting pattern as MIDI (See **MIDI**)
${makeExample( ${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') 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) .octave([0, 1].beat(2) - 1)
.scale('pentatonic').sound('pluck') .scale('pentatonic').sound('pluck')
@ -174,7 +134,7 @@ z1('s 0 5 7 0 3 7 0 2 7 0 1 7 0 1 6 5 4 3 2')
.odds(1/2, n => n.speed(0.5)) .odds(1/2, n => n.speed(0.5))
.room(0.5).size(0.5).out() .room(0.5).size(0.5).out()
`, `,
true, true
)}; )};
`; `;
}; };

View File

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

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main"; import { type Editor } from "../main";
import { makeExampleFactory } from "../../Documentation"; import { makeExampleFactory } from "../Documentation";
export const functions = (application: Editor): string => { export const functions = (application: Editor): string => {
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
@ -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!** - <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( ${makeExample(
"Calling a script! The most important feature!", "Calling a script! The most important feature!",
` `
beat(1) :: script(1) beat(1) :: script(1)
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Calling mutliple scripts at the same time.", "Calling mutliple scripts at the same time.",
` `
beat(1) :: script(1, 3, 5) beat(1) :: script(1, 3, 5)
`, `,
false, false
)} )}
## Math functions ## Math functions
@ -37,48 +37,29 @@ beat(1) :: script(1, 3, 5)
- <ic>mean(...values: number[]): number</ic>: returns the arithmetic mean of a list of numbers. - <ic>mean(...values: number[]): number</ic>: returns the arithmetic mean of a list of numbers.
- <ic>limit(value: number, min: number, max: number): number</ic>: Limits a value between a minimum and a maximum. - <ic>limit(value: number, min: number, max: number): number</ic>: Limits a value between a minimum and a maximum.
### Scaling functions
There are some very useful scaling methods taken from **SuperCollider**. You can call these on any number:
- <ic>.linlin(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale linearly from one range to another.
- <ic>.linexp(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale a linear range to an exponential range.
- <ic>.explin(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale an exponential range to a linear range.
- <ic>.expexp(inMin: number, inMax: number, outMin: number, outMax: number)</ic>: scale an exponential range to another exponential range.
- <ic>.lincurve(inMin: number, inMax: number, outMin: number, outMax: number, curve: number)</ic>: scale a number from one range to another following a specific curve.
- <ic>curve: number</ic>: <ic>0</ic> is linear, <ic>< 0</ic> is concave, negatively curved, <ic>> 0</ic> is convex, positively curved
${makeExample(
"Scaling an LFO",
`usine(1/2).linlin(0, 1, 0, 100)`,
true,
)}
## Delay functions ## Delay functions
- <ic>delay(ms: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds. - <ic>delay(ms: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds.
${makeExample( ${makeExample(
"Phased woodblocks", "Phased woodblocks",
` `
// Some very low-budget version of phase music // Some very low-budget version of phase music
beat(.5) :: delay(usine(.125) * 80, () => sound('east').out()) beat(.5) :: delay(usine(.125) * 80, () => sound('east').out())
beat(.5) :: delay(50, () => sound('east').out()) beat(.5) :: delay(50, () => sound('east').out())
`, `,
true, true
)} )}
- <ic>delayr(ms: number, nb: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds, repeated a given number of times. - <ic>delayr(ms: number, nb: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds, repeated a given number of times.
${makeExample( ${makeExample(
"Another woodblock texture", "Another woodblock texture",
` `
beat(1) :: delayr(50, 4, () => sound('east').speed([0.5,.25].beat()).out()) beat(1) :: delayr(50, 4, () => sound('east').speed([0.5,.25].beat()).out())
flip(2) :: beat(2) :: delayr(150, 4, () => sound('east').speed([0.5,.25].beat() * 4).out()) flip(2) :: beat(2) :: delayr(150, 4, () => sound('east').speed([0.5,.25].beat() * 4).out())
`, `,
true, true
)}; )};
`; `;
}; };

View File

@ -733,8 +733,8 @@ const completionDatabase: CompletionDatabase = {
midi: { midi: {
name: "midi", name: "midi",
category: "midi", category: "midi",
description: "Send a MIDI message (note, velocity, channel)", description: "Send a MIDI message",
example: "midi(144, 60, 1)", example: "midi(144, 60, 100)",
}, },
control_change: { control_change: {
name: "control_change", name: "control_change",
@ -742,12 +742,6 @@ const completionDatabase: CompletionDatabase = {
description: "Send a MIDI control change message", description: "Send a MIDI control change message",
example: "control_change({control: 1, value: 60, channel: 10})", example: "control_change({control: 1, value: 60, channel: 10})",
}, },
cc: {
name: "cc",
category: "midi",
description: "Send a MIDI control change message",
example: "cc({control: 1, value: 60, channel: 10})",
},
program_change: { program_change: {
name: "program_change", name: "program_change",
category: "midi", category: "midi",
@ -788,7 +782,7 @@ const completionDatabase: CompletionDatabase = {
name: "counter", name: "counter",
category: "patterns", category: "patterns",
description: "Counter/iterator", description: "Counter/iterator",
example: "counter('my_counter', 20, 1)", example: "counter('my_counter_, 20, 1)",
}, },
drunk: { drunk: {
name: "drunk", name: "drunk",
@ -814,17 +808,11 @@ const completionDatabase: CompletionDatabase = {
description: "Wraps (or not) of the drunk walk (boolean)", description: "Wraps (or not) of the drunk walk (boolean)",
example: "drunk_wrap(true)", example: "drunk_wrap(true)",
}, },
global: { v: {
name: "global", name: "v",
category: "variable", category: "variable",
description: "Global Variable setter or getter", description: "Global Variable setter or getter",
example: "global.my_var = 10; // Sets global variable 'my_var' to 10", example: "v('my_var', 10) // Sets global variable 'my_var' to 10",
},
g: {
name: "g",
category: "variable",
description: "Global Variable setter or getter",
example: "g.my_var = 10; // Sets global variable 'my_var' to 10",
}, },
delete_variable: { delete_variable: {
name: "delete_variable", name: "delete_variable",
@ -910,6 +898,12 @@ const completionDatabase: CompletionDatabase = {
description: "Detects if the Alt key is pressed", description: "Detects if the Alt key is pressed",
example: "fill() ? 1 : 0.5", example: "fill() ? 1 : 0.5",
}, },
comp: {
name: "comp",
category: "synthesis",
description: "Compressor threshold (dB)",
example: "sound('sine').comp(-4).out()",
},
ratio: { ratio: {
name: "ratio", name: "ratio",
category: "synthesis", category: "synthesis",
@ -965,17 +959,17 @@ export const inlineHoveringTips = hoverTooltip(
let completion = let completion =
completionDatabase[text.slice(start - from, end - from)] || {}; completionDatabase[text.slice(start - from, end - from)] || {};
let divContent = ` let divContent = `
<h1 class="text-brightwhite text-base pb-1">${completion.name} [<em class="text-white">${completion.category}</em>]</h1> <h1 class="text-orange-300 text-base pb-1">${completion.name} [<em class="text-white">${completion.category}</em>]</h1>
<p class="text-base pl-4">${completion.description}</p> <p class="text-base pl-4">${completion.description}</p>
<pre class="-mt-2"><code class="pl-4 text-base">${completion.example}</code></pre></div> <pre class="-mt-2"><code class="pl-4 text-base">${completion.example}</code></pre></div>
`; `;
let dom = document.createElement("div"); let dom = document.createElement("div");
dom.classList.add("px-4", "py-2", "bg-background", "rounded-lg"); dom.classList.add("px-4", "py-2", "bg-neutral-700", "rounded-lg");
dom.innerHTML = divContent; dom.innerHTML = divContent;
return { dom }; return { dom };
}, },
}; };
}, }
); );
export const toposCompletions = (context: CompletionContext) => { export const toposCompletions = (context: CompletionContext) => {
@ -990,7 +984,7 @@ export const toposCompletions = (context: CompletionContext) => {
info: () => { info: () => {
let div = document.createElement("div"); let div = document.createElement("div");
div.innerHTML = ` div.innerHTML = `
<h1 class="text-brightwhite text-base pb-1">${completionDatabase[key].name} [<em class="text-white">${completionDatabase[key].category}</em>]</h1> <h1 class="text-orange-300 text-base pb-1">${completionDatabase[key].name} [<em class="text-white">${completionDatabase[key].category}</em>]</h1>
<p class="text-base pl-4">${completionDatabase[key].description}</p> <p class="text-base pl-4">${completionDatabase[key].description}</p>
<div class="overflow-hidden overflow-scroll rounded px-2 ml-4 mt-2 bg-neutral-800"><code class="text-sm">${completionDatabase[key].example}</code></div> <div class="overflow-hidden overflow-scroll rounded px-2 ml-4 mt-2 bg-neutral-800"><code class="text-sm">${completionDatabase[key].example}</code></div>
`; `;

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main"; import { type Editor } from "../main";
import { makeExampleFactory } from "../../Documentation"; import { makeExampleFactory } from "../Documentation";
// @ts-ignore // @ts-ignore
export const interaction = (application: Editor): string => { export const interaction = (application: Editor): string => {
@ -20,57 +20,57 @@ MIDI input can be enabled in the settings panel. Once you have done that, you ca
* <ic>active_notes(channel?: number)</ic>: returns array of the active notes / pressed keys as an array of MIDI note numbers (0-127). Returns undefined if no notes are active. * <ic>active_notes(channel?: number)</ic>: returns array of the active notes / pressed keys as an array of MIDI note numbers (0-127). Returns undefined if no notes are active.
${makeExample( ${makeExample(
"Play active notes as chords", "Play active notes as chords",
` `
beat(1) && active_notes() && sound('sine').chord(active_notes()).out() beat(1) && active_notes() && sound('sine').chord(active_notes()).out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Play active notes as arpeggios", "Play active notes as arpeggios",
` `
beat(0.25) && active_notes() && sound('juno').note( beat(0.25) && active_notes() && sound('juno').note(
active_notes().beat(0.5)+[12,24].beat(0.25) active_notes().beat(0.5)+[12,24].beat(0.25)
).cutoff(300 + usine(1/4) * 2000).out() ).cutoff(300 + usine(1/4) * 2000).out()
`, `,
false, false
)} )}
* <ic>sticky_notes(channel?: number)</ic>: returns array of the last pressed keys as an array of MIDI note numbers (0-127). Notes are added and removed from the list with the "Note on"-event. Returns undefined if no keys have been pressed. * <ic>sticky_notes(channel?: number)</ic>: returns array of the last pressed keys as an array of MIDI note numbers (0-127). Notes are added and removed from the list with the "Note on"-event. Returns undefined if no keys have been pressed.
${makeExample( ${makeExample(
"Play continous arpeggio with sticky notes", "Play continous arpeggio with sticky notes",
` `
beat(0.25) && sticky_notes() && sound('arp') beat(0.25) && sticky_notes() && sound('arp')
.note(sticky_notes().palindrome().beat(0.25)).out() .note(sticky_notes().palindrome().beat(0.25)).out()
`, `,
true, true
)} )}
* <ic>last_note(channel?: number)</ic>: returns the last note that has been received. Returns 60 if no other notes have been received. * <ic>last_note(channel?: number)</ic>: returns the last note that has been received. Returns 60 if no other notes have been received.
${makeExample( ${makeExample(
"Play last note", "Play last note",
` `
beat(0.5) && sound('sawtooth').note(last_note()) beat(0.5) && sound('sawtooth').note(last_note())
.vib([1, 3, 5].beat(1)) .vib([1, 3, 5].beat(1))
.vibmod([1,3,2,4].beat(2)).out() .vibmod([1,3,2,4].beat(2)).out()
`, `,
false, false
)} )}
* <ic>buffer()</ic>: return true if there are notes in the buffer. * <ic>buffer()</ic>: return true if there are notes in the buffer.
* <ic>buffer_note(channel?: number)</ic>: returns last unread note that has been received. Note is fetched and removed from start of the buffer once this is called. Returns undefined if no notes have been received. * <ic>buffer_note(channel?: number)</ic>: returns last unread note that has been received. Note is fetched and removed from start of the buffer once this is called. Returns undefined if no notes have been received.
${makeExample( ${makeExample(
"Play buffered note", "Play buffered note",
` `
beat(1) && buffer() && sound('sine').note(buffer_note()).out() beat(1) && buffer() && sound('sine').note(buffer_note()).out()
`, `,
false, false
)} )}
@ -80,33 +80,33 @@ ${makeExample(
Midi CC messages can be used to control any value in Topos. MIDI input can be defined in Settings and last received CC message can be used to control any numeric value within Topos. Midi CC messages can be used to control any value in Topos. MIDI input can be defined in Settings and last received CC message can be used to control any numeric value within Topos.
Currently supported methods for CC input are: Currently supported methods for CC input are:
* <ic>ccIn(control: number, channel?: number)</ic>: Returns last received CC value for given control number (and optional channel). By default last CC value is last value from ANY channel or 64 if no CC messages have been received. * <ic>last_cc(control: number, channel?: number)</ic>: Returns last received CC value for given control number (and optional channel). By default last CC value is last value from ANY channel or 64 if no CC messages have been received.
${makeExample( ${makeExample(
"Play notes with cc", "Play notes with cc",
` `
beat(0.5) && sound('arp').note(ccin(74)).out() beat(0.5) && sound('arp').note(last_cc(74)).out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Control everything with CCs", "Control everything with CCs",
` `
beat(0.5) :: sound('sine') beat(0.5) :: sound('sine')
.freq(ccIn(75)*3) .freq(last_cc(75)*3)
.cutoff(ccIn(76)*2*usine()) .cutoff(last_cc(76)*2*usine())
.sustain(1.0) .sustain(1.0)
.out() .out()
beat(ccIn(74)/127*.5) :: sound('sine') beat(last_cc(74)/127*.5) :: sound('sine')
.freq(ccIn(75)*6) .freq(last_cc(75)*6)
.cutoff(ccIn(76)*3*usine()) .cutoff(last_cc(76)*3*usine())
.sustain(ccIn(74)/127*.25) .sustain(last_cc(74)/127*.25)
.out() .out()
`, `,
false, false
)} )}
## Run scripts with MIDI ## Run scripts with MIDI
@ -126,10 +126,10 @@ Topos can output scales to external keyboards lighted keys using the following f
- <ic>show_scale(key: string, scale: string|int, channel?: number, port?: string|number, soundOff?: boolean): void</ic>: sends the scale as midi on messages to specified port and channel to light the keys of external keyboard. If soundOff is true, all sound off message will be sent after every note on message. This can be useful with some keyboards not supporting external channel for lightning or routing for the midi in to suppress the sound from incoming note on messages. - <ic>show_scale(key: string, scale: string|int, channel?: number, port?: string|number, soundOff?: boolean): void</ic>: sends the scale as midi on messages to specified port and channel to light the keys of external keyboard. If soundOff is true, all sound off message will be sent after every note on message. This can be useful with some keyboards not supporting external channel for lightning or routing for the midi in to suppress the sound from incoming note on messages.
${makeExample( ${makeExample(
"Show scale on external keyboard", "Show scale on external keyboard",
`show_scale("F","aeolian",0,4)`, `show_scale("F","aeolian",0,4)`,
true, true
)} )}
${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)} ${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)}

View File

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

View File

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

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

@ -0,0 +1,59 @@
import { type Editor } from "../main";
import { makeExampleFactory } from "../Documentation";
export const lfos = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Low Frequency Oscillators
Low Frequency Oscillators (_LFOs_) are an important piece in any digital audio workstation or synthesizer. Topos implements some basic waveforms you can play with to automatically modulate your paremeters.
- <ic>sine(freq: number = 1, times: number = 1, offset: number= 0): number</ic>: returns a sinusoïdal oscillation between <ic>-1</ic> and <ic>1</ic>.
- <ic>freq</ic> : frequency in hertz.
- <ic>times</ic> : output value multiplier.
- <ic>offset</ic>: linear offset.
- <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
)};
- <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
)}
- <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
)}
- <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
)};
- <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
)};
`
}

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../../main"; import { type Editor } from "../main";
import { makeExampleFactory } from "../../../Documentation"; import { makeExampleFactory } from "../Documentation";
export const long_forms = (app: Editor): string => { export const long_forms = (app: Editor): string => {
// @ts-ignore // @ts-ignore
@ -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: - **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( ${makeExample(
"Eight bars per section", "Eight bars per section",
` `
// Playing each script for 8 bars in succession // Playing each script for 8 bars in succession
script([1,2,3,4].bar(8)) script([1,2,3,4].bar(8))
`, `,
true, true
)} )}
You can also give a specific duration to each section using <ic>.dur</ic>: You can also give a specific duration to each section using <ic>.dur</ic>:
${makeExample( ${makeExample(
"N beats per section", "N beats per section",
` `
script([1,2,3,4].dur(8, 2, 16, 4)) script([1,2,3,4].dur(8, 2, 16, 4))
`, `,
true, true
)} )}
- **Use universes as well**. Transitions between universes are _seamless_, instantaneous. Just switch to different content if you ever hit the limitations of the current _universe_. - **Use universes as well**. Transitions between universes are _seamless_, instantaneous. Just switch to different content if you ever hit the limitations of the current _universe_.
@ -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. - <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( ${makeExample(
"Two beats of silence, two beats of playing", "Two beats of silence, two beats of playing",
` `
flip(4) :: beat(1) :: snd('kick').out() flip(4) :: beat(1) :: snd('kick').out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Clapping on the edge", "Clapping on the edge",
` `
flip(2.5, 10) :: beat(.25) :: snd('cp').out() flip(2.5, 10) :: beat(.25) :: snd('cp').out()
flip(2.5, 75) :: beat(.25) :: snd('click') flip(2.5, 75) :: beat(.25) :: snd('click')
.speed(2).end(0.2).out() .speed(2).end(0.2).out()
flip(2.5) :: beat(.5) :: snd('bd').out() flip(2.5) :: beat(.5) :: snd('bd').out()
beat(.25) :: sound('hat').end(0.1).cutoff(1200).pan(usine(1/4)).out() beat(.25) :: sound('hat').end(0.1).cutoff(1200).pan(usine(1/4)).out()
`, `,
false, false
)} )}
${makeExample( ${makeExample(
"Good old true and false", "Good old true and false",
` `
if (flip(4, 75)) { if (flip(4, 75)) {
beat(1) :: snd('kick').out() beat(1) :: snd('kick').out()
} else { } else {
beat(.5) :: snd('snare').out() beat(.5) :: snd('snare').out()
} }
`, `,
true, true
)} )}
<ic>flip</ic> is extremely powerful and is used internally for a lot of other Topos functions. You can also use it to think about **longer durations** spanning over multiple bars. Here is a silly composition that is using <ic>flip</ic> to generate a 4 bars long pattern. <ic>flip</ic> is extremely powerful and is used internally for a lot of other Topos functions. You can also use it to think about **longer durations** spanning over multiple bars. Here is a silly composition that is using <ic>flip</ic> to generate a 4 bars long pattern.
${makeExample( ${makeExample(
"Clunky algorithmic rap music", "Clunky algorithmic rap music",
` `
// Rap God VS Lil Wild -- Adel Faure // Rap God VS Lil Wild -- Adel Faure
if (flip(8)) { if (flip(8)) {
// Playing this part for two bars // 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() beat(.5)::snd('diphone').end(0.5).n([1,2,3,4].pick()).out()
} }
`, `,
true, true
)} )}
You can use it everywhere to spice things up, including as a method parameter picker: You can use it everywhere to spice things up, including as a method parameter picker:
${makeExample( ${makeExample(
"flip is great for parameter variation", "flip is great for parameter variation",
` `
beat(.5)::snd(flip(2) ? 'kick' : 'hat').out() beat(.5)::snd(flip(2) ? 'kick' : 'hat').out()
`, `,
true, true
)} )}
- <ic>flipbar(n: number = 1)</ic>: this method works just like <ic>flip</ic> but counts in bars instead of beats. It allows you to think about even larger time cycles. You can also pair it with regular <ic>flip</ic> for writing complex and long-spanning algorithmic beats. - <ic>flipbar(n: number = 1)</ic>: this method works just like <ic>flip</ic> but counts in bars instead of beats. It allows you to think about even larger time cycles. You can also pair it with regular <ic>flip</ic> for writing complex and long-spanning algorithmic beats.
${makeExample( ${makeExample(
"Thinking music over bars", "Thinking music over bars",
` `
let roomy = (n) => n.room(1).size(1).cutoff(500 + usaw(1/8) * 5000); let roomy = (n) => n.room(1).size(1).cutoff(500 + usaw(1/8) * 5000);
function a() { function a() {
beat(1) && roomy(sound('kick')).out() beat(1) && roomy(sound('kick')).out()
@ -122,24 +122,24 @@ function b() {
flipbar(2) && a() flipbar(2) && a()
flipbar(3) && b() flipbar(3) && b()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Alternating over four bars", "Alternating over four bars",
` `
flipbar(2) flipbar(2)
? beat(.5) && snd(['kick', 'hh'].beat(1)).out() ? beat(.5) && snd(['kick', 'hh'].beat(1)).out()
: beat(.5) && snd(['east', 'east:2'].beat(1)).out() : beat(.5) && snd(['east', 'east:2'].beat(1)).out()
`, `,
false, false
)}; )};
- <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. - <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( ${makeExample(
"Using onbar for filler drums", "Using onbar for filler drums",
` `
tempo(150); tempo(150);
// Only play on the third and fourth bar of the cycle. // Only play on the third and fourth bar of the cycle.
onbar([3,4], 4)::beat(.25)::snd('hh').out(); 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, 1, 7) :: snd('jvbass').n(2).out();
rhythm(.5, 2, 7) :: snd('snare').n(3).out(); rhythm(.5, 2, 7) :: snd('snare').n(3).out();
}`, }`,
true, true
)} )}
`; `;
}; };

View File

@ -1,5 +1,5 @@
import { type Editor } from "../../main"; import { type Editor } from "../main";
import { makeExampleFactory, key_shortcut } from "../../Documentation"; import { makeExampleFactory, key_shortcut } from "../Documentation";
export const midi = (application: Editor): string => { export const midi = (application: Editor): string => {
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
@ -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:** Your web browser is capable of sending and receiving MIDI information through the [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API). The support for MIDI on browsers is a bit shaky. Please, take some time to configure and test. To our best knowledge, **Chrome** is currently leading on this feature, followed closely by **Firefox**. The other major web browsers are also starting to support this API. **There are two important functions for configuration:**
- <ic>midi_outputs()</ic>: prints the list of available MIDI devices on the screen. You will have to open the web console using ${key_shortcut( - <ic>midi_outputs()</ic>: prints the list of available MIDI devices on the screen. You will have to open the web console using ${key_shortcut(
"Ctrl+Shift+I", "Ctrl+Shift+I"
)} or sometimes ${key_shortcut( )} or sometimes ${key_shortcut(
"F12", "F12"
)}. You can also open it from the menu of your web browser. **Note:** close the docs to see it printed. )}. You can also open it from the menu of your web browser. **Note:** close the docs to see it printed.
${makeExample( ${makeExample(
"Listing MIDI outputs", "Listing MIDI outputs",
` `
midi_outputs() midi_outputs()
`, `,
true, true
)} )}
- <ic>midi_output(output_name: string)</ic>: enter your desired output to connect to it. - <ic>midi_output(output_name: string)</ic>: enter your desired output to connect to it.
${makeExample( ${makeExample(
"Changing MIDI output", "Changing MIDI output",
` `
midi_output("MIDI Rocket-Trumpet") midi_output("MIDI Rocket-Trumpet")
`, `,
true, true
)} )}
That's it! You are now ready to play with MIDI. That's it! You are now ready to play with MIDI.
@ -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. - <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( ${makeExample(
"MIDI note using one parameter: note", "MIDI note using one parameter: note",
` `
// Configure your MIDI first! // Configure your MIDI first!
// => midi_output("MIDI Bus 1") // => midi_output("MIDI Bus 1")
rhythm(.5, 5, 8) :: midi(50).out() rhythm(.5, 5, 8) :: midi(50).out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"MIDI note using three parameters: note, velocity, channel", "MIDI note using three parameters: note, velocity, channel",
` `
// MIDI Note 50, Velocity 50 + LFO, Channel 0 // MIDI Note 50, Velocity 50 + LFO, Channel 0
rhythm(.5, 5, 8) :: midi(50, 50 + usine(.5) * 20, 0).out() rhythm(.5, 5, 8) :: midi(50, 50 + usine(.5) * 20, 0).out()
`, `,
false, false
)} )}
${makeExample( ${makeExample(
"MIDI note by passing an object", "MIDI note by passing an object",
` `
// MIDI Note 50, Velocity 50 + LFO, Channel 0 // MIDI Note 50, Velocity 50 + LFO, Channel 0
rhythm(.5, 5, 8) :: midi({note: 50, velocity: 50 + usine(.5) * 20, channel: 0}).out() rhythm(.5, 5, 8) :: midi({note: 50, velocity: 50 + usine(.5) * 20, channel: 0}).out()
`, `,
false, false
)} )}
We can now have some fun and starting playing a small piano piece: We can now have some fun and starting playing a small piano piece:
${makeExample( ${makeExample(
"Playing some piano", "Playing some piano",
` `
tempo(80) // Setting a default BPM tempo(80) // Setting a default BPM
beat(.5) && midi(36 + [0,12].beat()).sustain(0.02).out() beat(.5) && midi(36 + [0,12].beat()).sustain(0.02).out()
beat(.25) && midi([64, 76].pick()).sustain(0.05).out() beat(.25) && midi([64, 76].pick()).sustain(0.05).out()
beat(.75) && midi([64, 67, 69].beat()).sustain(0.05).out() beat(.75) && midi([64, 67, 69].beat()).sustain(0.05).out()
beat(.25) && midi([64, 67, 69].beat() + 24).sustain(0.05).out() beat(.25) && midi([64, 67, 69].beat() + 24).sustain(0.05).out()
`, `,
true, true
)} )}
## Control and Program Changes ## Control and Program Changes
- <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>). - <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( ${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: [24,25].pick(), value: irand(1,120), channel: 1})
control_change({control: [30,35].pick(), value: irand(1,120) / 2, channel: 1}) control_change({control: [30,35].pick(), value: irand(1,120) / 2, channel: 1})
`, `,
true, true
)} )}
- <ic>program_change(program: number, channel: number)</ic>: send a MIDI Program Change. This function takes two arguments to specify the program and the channel (_e.g._ <ic>program_change(1, 1)</ic>). - <ic>program_change(program: number, channel: number)</ic>: send a MIDI Program Change. This function takes two arguments to specify the program and the channel (_e.g._ <ic>program_change(1, 1)</ic>).
${makeExample( ${makeExample(
"Crashing old synthesizers: a hobby", "Crashing old synthesizers: a hobby",
` `
program_change([1,2,3,4,5,6,7,8].pick(), 1) program_change([1,2,3,4,5,6,7,8].pick(), 1)
`, `,
true, true
)} )}
## System Exclusive Messages ## System Exclusive Messages
@ -119,44 +119,44 @@ program_change([1,2,3,4,5,6,7,8].pick(), 1)
${makeExample( ${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) sysex(0x90, 0x40, 0x7f)
`, `,
true, true
)} )}
## Clock ## Clock
- <ic>midi_clock()</ic>: send a MIDI Clock message. This function is used to synchronize Topos with other MIDI devices or DAWs. - <ic>midi_clock()</ic>: send a MIDI Clock message. This function is used to synchronize Topos with other MIDI devices or DAWs.
${makeExample( ${makeExample(
"Tic, tac, tic, tac...", "Tic, tac, tic, tac...",
` `
beat(.25) && midi_clock() // Sending clock to MIDI device from the global buffer beat(.25) && midi_clock() // Sending clock to MIDI device from the global buffer
`, `,
true, true
)} )}
## Using midi with ziffers ## Using midi with ziffers
Ziffers offers some shorthands for defining channels within the patterns. See Ziffers for more information. Ziffers offers some shorthands for defining channels within the patterns. See Ziffers for more information.
${makeExample( ${makeExample(
"Using midi with ziffers", "Using midi with ziffers",
` `
z1('0 2 e 5 2 q 4 2').midi().port(2).channel(4).out() z1('0 2 e 5 2 q 4 2').midi().port(2).channel(4).out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Setting the channel within the pattern", "Setting the channel within the pattern",
` `
z1('(0 2 e 5 2):0 (4 2):1').midi().out() z1('(0 2 e 5 2):0 (4 2):1').midi().out()
`, `,
true, true
)} )}
`; `;
}; };

View File

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

View File

@ -7,25 +7,7 @@ export const bonus = (application: Editor): string => {
return ` return `
# Bonus features # Bonus features
Some features have been included as a bonus. These features are often about patterning over things that are not directly related to sound: pictures, video, etc. 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.
## Editor theme configuration
The editor theme can be changed using the <ic>theme</ic> and <ic>randomTheme</ic> functions. The following example will use a random color scheme for every beat:
${makeExample(
"Random theme on each beat",
`
beat(1)::randomTheme()
`, true)}
You can also pick a theme using the <ic>theme</ic> function with a string as only argument:
${makeExample(
"Picking a theme",
`
beat(1)::theme("Batman")
`, true)}
## Hydra Visual Live Coding ## Hydra Visual Live Coding
@ -33,62 +15,43 @@ beat(1)::theme("Batman")
<warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning> <warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning>
</div> </div>
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows an analog synthesizer patching metaphor to encourage live coding complex shaders. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance. [Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows the metaphor of analog synthesizer patching to allow its user to create complex live visuals from a web browser window. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance. Topos provides a simple way to integrate Hydra into a live coding session and to blend it with regular Topos code.
${makeExample( ${makeExample(
"Hydra integration", "Hydra integration",
`beat(4) :: hydra.osc(3, 0.5, 2).out()`, `beat(4) :: app.hydra.osc(3, 0.5, 2).out()`,
true, true
)} )}
Close the documentation to see the effect: ${key_shortcut( You may feel like it's doing nothing! Press ${key_shortcut(
"Ctrl+D", "Ctrl+D"
)}! **Boom, all shiny!** )} to close the documentation. **Boom, all shiny!**
Be careful not to call <ic>hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>beat()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches. Be careful not to call <ic>app.hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>mod()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches.
Stopping **Hydra** is simple: Stopping **Hydra** is simple:
${makeExample( ${makeExample(
"Stopping Hydra", "Stopping Hydra",
` `
beat(4) :: stop_hydra() // this one beat(4) :: stop_hydra() // this one
beat(4) :: hydra.hush() // or this one beat(4) :: app.hydra.hush() // or this one
`, `,
true, true
)} )}
I won't teach you how to play with Hydra. You can find some great resources on the [Hydra website](https://hydra.ojack.xyz/):
### Changing the resolution
You can change Hydra resolution using this simple method:
${makeExample(
"Changing Hydra resolution",
`hydra.setResolution(1024, 768)`,
true,
)}
### Documentation
I won't teach Hydra. You can find some great resources directly on the [Hydra website](https://hydra.ojack.xyz/):
- [Hydra interactive documentation](https://hydra.ojack.xyz/docs/) - [Hydra interactive documentation](https://hydra.ojack.xyz/docs/)
- [List of Hydra Functions](https://hydra.ojack.xyz/api/) - [List of Hydra Functions](https://hydra.ojack.xyz/api/)
- [Source code on GitHub](https://github.com/hydra-synth/hydra) - [Source code on GitHub](https://github.com/hydra-synth/hydra)
### The Hydra namespace
In comparison with the basic Hydra editor, please note that you have to prefix all Hydra functions with <ic>hydra.</ic> to avoid conflicts with Topos functions. For example, <ic>osc()</ic> becomes <ic>hydra.osc()</ic>.
${makeExample("Hydra namespace", `hydra.voronoi(20).out()`, true)}
## GIF player ## GIF player
Topos embeds a small <ic>.gif</ic> picture player with a small API. GIFs are automatically fading out after the given duration. Look at the following example: Topos embeds a small <ic>.gif</ic> picture player with a small API. GIFs are automatically fading out after the given duration. Look at the following example:
${makeExample( ${makeExample(
"Playing many gifs", "Playing many gifs",
` `
beat(0.25)::gif({ beat(0.25)::gif({
url:v('gif')[$(1)%6], // Any URL will do! url:v('gif')[$(1)%6], // Any URL will do!
opacity: r(0.5, 1), // Opacity (0-1) opacity: r(0.5, 1), // Opacity (0-1)
@ -99,8 +62,8 @@ beat(0.25)::gif({
rotation: ir(1, 360), // Rotation (in degrees) rotation: ir(1, 360), // Rotation (in degrees)
posX: ir(1,1200), // CSS Horizontal Position posX: ir(1,1200), // CSS Horizontal Position
posY: ir(1, 800), // CSS Vertical Position posY: ir(1, 800), // CSS Vertical Position
`, `, true
true, )}
)}
`; `;
}; };

View File

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

View File

@ -1,227 +0,0 @@
import { type Editor } from "../../main";
import { key_shortcut, makeExampleFactory } from "../../Documentation";
export const visualization = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Vizualisation
While Topos is mainly being developed as a live coding environment for algorithmic music composition, it also includes some features for live code visualizations. This section will introduce you to these features.
## Hydra Visual Live Coding
<div class="mx-12 bg-neutral-600 rounded-lg flex flex-col items-center justify-center">
<warning>⚠️ This feature can generate flashing images that could trigger photosensitivity or epileptic seizures. ⚠️ </warning>
</div>
[Hydra](https://hydra.ojack.xyz/?sketch_id=mahalia_1) is a popular live-codable video synthesizer developed by [Olivia Jack](https://ojack.xyz/) and other contributors. It follows an analog synthesizer patching metaphor to encourage live coding complex shaders. Being very easy to use, extremely powerful and also very rewarding to use, Hydra has become a popular choice for adding visuals into a live code performance.
${makeExample(
"Hydra integration",
`beat(4) :: hydra.osc(3, 0.5, 2).out()`,
false,
)}
Close the documentation to see the effect: ${key_shortcut(
"Ctrl+D",
)}! **Boom, all shiny!**
Be careful not to call <ic>hydra</ic> too often as it can impact performances. You can use any rhythmical function like <ic>beat()</ic> function to limit the number of function calls. You can write any Topos code like <ic>[1,2,3].beat()</ic> to bring some life and movement in your Hydra sketches.
Stopping **Hydra** is simple:
${makeExample(
"Stopping Hydra",
`
beat(4) :: stop_hydra() // this one
beat(4) :: hydra.hush() // or this one
`,
false,
)}
### Changing the resolution
You can change Hydra resolution using this simple method:
${makeExample(
"Changing Hydra resolution",
`hydra.setResolution(1024, 768)`,
false,
)}
### Hydra 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",
`
beat(0.25)::gif({
url:v('gif')[$(1)%6], // Any URL will do!
opacity: r(0.5, 1), // Opacity (0-1)
size:"300px", // CSS size property
center:false, // Centering on the screen?
filter:'none', // CSS Filter
dur: 2, // In beats (Topos unit)
rotation: ir(1, 360), // Rotation (in degrees)
posX: ir(1,1200), // CSS Horizontal Position
posY: ir(1, 800), // CSS Vertical Position
`,
false,
)}
## Canvas live coding
Documentation in progress! Copy the example and run it separately (Showing visualization examples in the documentation not implemented yet).
Canvas live coding is a feature that allows you to draw musical events to the canvas. Canvas can be used to create complex visualizations. The feature is based on the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API" target="_blank">Canvas API</a> and the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D" target="_blank">CanvasRenderingContext2D</a> interface. The feature is still in development and more functions will be added in the future.
In addition to the standard Canvas API, Topos also includes some pre-defined shapes for convenience. See the Shapes section below for more info.
* <ic>draw(f: Function)</ic> - Draws to a canvas with the given function.
${makeExample(
"Drawing to canvas",
`
beat(0.5) && clear() && draw(context => {
context.fillStyle = 'red';
// Begin the path for the heart shape
context.beginPath();
const x = wc();
const y = hc();
context.fillStyle = 'red';
// Begin the path for the heart shape
context.beginPath();
context.moveTo(x + 125, y + 50);
context.bezierCurveTo(x + 75, y, x, y + 75, x + 125, y + 175);
context.bezierCurveTo(x + 250, y + 75, x + 175, y, x + 125, y + 50);
// Fill the heart with red color
context.fill();
})
`,
false,
)}
${makeExample(
"Using draw with events and shapes",
`
beat(0.25) && sound("bass1:5").pitch(rI(1,6)).draw(x => {
donut(x.pitch)
}).out()
`,
false,
)}
${makeExample(
"Using draw with ziffers and shapes",
`
z1("1/8 (0 2 1 4)+(2 1)").sound("sine").ad(0.05,.25).clear()
.draw(x => {
pie({slices:7,eaten:(7-x.pitch-1),fillStyle:"green", rotate: 250})
}).log("pitch").out()
`,
false,
)}
* <ic<image(url, x, y, width, height, rotation)</ic> - Draws an image to a canvas.
${makeExample(
"Image to canvas",
`
beat(0.5) && clear() && image("http://localhost:8000/topos_frog.svg",200,200+epulse()%15)
`,
false,
)}
* <ic>clear()</ic> - Clears the canvas.
* <ic>background(fill: string)</ic> - Sets the background color, image or gradient.
* <ic>w()</ic> - Returns the canvas width.
* <ic>h()</ic> - Returns the canvas height.
* <ic>wc()</ic> - Returns the center of the canvas width.
* <ic>hc()</ic> - Returns the center of the canvas height.
### Text to canvas
Text can be drawn to canvas using the <ic>drawText()</ic> function. The function can take any unicode characters including emojis. The function can also be used to draw random characters from a given unicode range. Different filters can also be applied using the **filter** parameter. See filter in <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter" target="_blank">canvas documentation</a> for more info.
* <ic>drawText(text, fontSize, rotation, font, x, y)</ic> - Draws text to a canvas.
${makeExample(
"Writing to canvas",
`
beat(0.5) && clear() && drawText("Hello world!", 100, 0, "Arial", 100, 100)
`,
false,
)}
* <ic>randomChar(number, min, max)</ic> - Returns a number of random characters from given unicode range.
${makeExample(
"Drawing random characters to canvas",
`
beat(0.5) && clear() && drawText(randomChar(10,1000,2000),30)
`,
false,
)}
* <ic>emoji(size)</ic> - Returns a random emojis as text.
* <ic>animals(size)</ic> - Returns a random animal emojis as text.
* <ic>food(size)</ic> - Returns a random food emojis as text.
${makeExample(
"Drawing food emojis to canvas",
`
beat(0.5) && clear() && drawText({x: 10, y: epulse()%700, text: food(50)})
`,
false,
)}
* <ic>expression(size)</ic> - Returns a random expression emojis as text.
### Shapes
In addition to supporting drawing to canvas directly, Topos also include some pre-defined shapes for convenience. Every shape can be defined by either by inputting one object as parameter or by inputting the parameters separately.
The predefined shapes are:
* <ic>smiley(happiness, radius, eyes, fill, rotate, x, y)</ic>
* <ic>ball(radius,fill,x,y)</ic>
* <ic>box(width, height, fill, rotate)</ic>
* <ic>pointy(width, height, fill, rotate, x, y)</ic>
* <ic>equilateral(radius, fill, rotate, x, y)</ic>
* <ic>star(points, radius, fill rotate, outerRadius, x, y</ic>
* <ic>pie(slices, eaten, radius, fill, secondary, stroke, rotate, x, y</ic>
* <ic>donut(slices, eaten, radius, hole, fill, secondary, stroke, rotate, x, y</ic>
* <ic>balloid(petals, radius, curve, fill, secondary, x, y)</ic>
* <ic>stroke(width, stroke, rotate, x1, y1, x2, y2)</ic>
### Gradients
* <ic>linearGradient(x1, y1, x2, y2, ...stops)</ic> - Creates a <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient">linear gradient</a>.
* <ic>radialGradient(x1, y1, r1, x2, y2, r2, ...stops)</ic> - Creates a <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createRadialGradient">radial gradient</a>.
* <ic>conicGradient(x, y, angle, ...stops)</ic> - Creates a <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createConicGradient">conic gradient</a>.
`;
};

View File

@ -1,33 +1,33 @@
import { type Editor } from "../../main"; import { type Editor } from "../main";
import { makeExampleFactory } from "../../Documentation"; import { makeExampleFactory } from "../Documentation";
export const patterns = (application: Editor): string => { export const patterns = (application: Editor): string => {
const makeExample = makeExampleFactory(application); const makeExample = makeExampleFactory(application);
return ` return `
# Array patterns # Patterns
**Topos** is using arrays as a way to make dynamic patterns of data (rhythms, melodies, etc). **Topos** is using arrays as a way to make dynamic patterns of data (rhythms, melodies, etc).
It means that the following: It means that the following:
${makeExample( ${makeExample(
"Boring kick", "Boring kick",
` `
beat(1)::sound('kick').out() beat(1)::sound('kick').out()
`, `,
true, true
)} )}
can be turned into something more interesting like this easily: can be turned into something more interesting like this easily:
${makeExample( ${makeExample(
"Less boring kick", "Less boring kick",
` `
let c = [1,2].dur(3, 1) let c = [1,2].dur(3, 1)
beat([1, 0.5, 0.25].dur(0.75, 0.25, 1) / c)::sound(['kick', 'fsoftsnare'].beat(0.75)) beat([1, 0.5, 0.25].dur(0.75, 0.25, 1) / c)::sound(['kick', 'fsoftsnare'].beat(0.75))
.ad(0, .25).shape(usine(1/2)*0.5).speed([1, 2, 4].beat(0.5)).out() .ad(0, .25).shape(usine(1/2)*0.5).speed([1, 2, 4].beat(0.5)).out()
`, `,
true, true
)} )}
**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>. **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. - <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( ${makeExample(
"Light drumming", "Light drumming",
` `
// Every bar, use a different rhythm // Every bar, use a different rhythm
beat([1, 0.75].beat(4)) :: sound('cp').out() beat([1, 0.75].beat(4)) :: sound('cp').out()
beat([0.5, 1].beat(4)) :: sound('kick').out() beat([0.5, 1].beat(4)) :: sound('kick').out()
beat(2)::snd('snare').shape(.5).out() beat(2)::snd('snare').shape(.5).out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Using beat to create arpeggios", "Using beat to create arpeggios",
` `
// Arpeggio using pulse divisions // Arpeggio using pulse divisions
beat([.5, .25].beat(0.5)) :: sound('sine') beat([.5, .25].beat(0.5)) :: sound('sine')
.lpf(100+usine(1/4)*400).lpad(2, 0, .25) .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) .delayfb(0.5)
.out() .out()
`, `,
false, false
)} )}
${makeExample( ${makeExample(
"Cool ambiance", "Cool ambiance",
` `
beat(.5) :: snd(['kick', 'hat'].beat(0.5)).out() beat(.5) :: snd(['kick', 'hat'].beat(0.5)).out()
beat([2,4].beat(2)) :: snd('shaker').delay(.5).delayfb(.75).delayt(0.125).out() beat([2,4].beat(2)) :: snd('shaker').delay(.5).delayfb(.75).delayt(0.125).out()
flip(2)::beat(1)::snd('froomy').out() flip(2)::beat(1)::snd('froomy').out()
flip(4)::beat(2)::snd('pad').n(2).shape(.5) flip(4)::beat(2)::snd('pad').n(2).shape(.5)
.orbit(2).room(0.9).size(0.9).release(0.5).out() .orbit(2).room(0.9).size(0.9).release(0.5).out()
`, `,
false, false
)} )}
- <ic>bar(value: number = 1)</ic>: returns the next value every bar (if <ic>value = 1</ic>). Using a larger value will return the next value every <ic>n</ic> bars. - <ic>bar(value: number = 1)</ic>: returns the next value every bar (if <ic>value = 1</ic>). Using a larger value will return the next value every <ic>n</ic> bars.
${makeExample( ${makeExample(
"A simple drumbeat in no time!", "A simple drumbeat in no time!",
` `
beat(1)::sound(['kick', 'hat', 'snare', 'hat'].beat()).out() beat(1)::sound(['kick', 'hat', 'snare', 'hat'].beat()).out()
beat([1/4, 1/2].dur(1.5, 0.5))::sound(['jvbass', 'fikea'].bar()) 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( .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()) * [1, 2].bar())
.out() .out()
`, `,
true, true
)} )}
${makeExample( ${makeExample(
"Using beat and bar in the same example", "Using beat and bar in the same example",
` `
beat(2)::snd('snare').out() beat(2)::snd('snare').out()
beat([1, 0.5].beat()) :: sound(['bass3'].bar()) beat([1, 0.5].beat()) :: sound(['bass3'].bar())
.freq(100).n([12, 14].bar()) .freq(100).n([12, 14].bar())
@ -101,14 +101,14 @@ beat([1, 0.5].beat()) :: sound(['bass3'].bar())
.pan(r(0, 1)) .pan(r(0, 1))
.speed([1,2,3].beat()) .speed([1,2,3].beat())
.out() .out()
`, `
)} )}
- <ic>dur(...list: numbers[])</ic> : keeps the same value for a duration of <ic>n</ic> beats corresponding to the <ic>nth</ic> number of the list you provide. - <ic>dur(...list: numbers[])</ic> : keeps the same value for a duration of <ic>n</ic> beats corresponding to the <ic>nth</ic> number of the list you provide.
${makeExample( ${makeExample(
"Holding a value for n beats", "Holding a value for n beats",
` `
// The second note is kept for twice as long // The second note is kept for twice as long
beat(0.5)::sound('notes').n([1,2].dur(1, 2)) beat(0.5)::sound('notes').n([1,2].dur(1, 2))
.room(0.5).size(8).delay(0.125).delayt(1/8) .room(0.5).size(8).delay(0.125).delayt(1/8)
@ -117,158 +117,72 @@ beat(0.5)::sound('notes').n([1,2].dur(1, 2))
beat(1)::sound(['kick', 'fsnare'].dur(3, 1)) beat(1)::sound(['kick', 'fsnare'].dur(3, 1))
.n([0,3].dur(3, 1)).out() .n([0,3].dur(3, 1)).out()
`, `,
true, true
)} )}
${makeExample(
"Patterning with ternary statements",
`
const dada = flipbar(2) ? [0,[3,5,-1].bar(3),2,3] : [9,8,9,6]
beat(0.5) :: sound('wt_hvoice:3')
.pitch(dada.beat(0.5))
.scale("88.0")
.adsr(0.05, 0.05, 0, 0)
.cutoff(500 + usine(1/8) * 5000)
.room(1.5)
.resonance(0.25)
.out()
beat(1) :: sound('kick').n(4).out()
onbeat([0.5,0.8].beat(1),2) :: sound('snare').out()
onbeat(0.5,0.8,1,1.5,2,2.5,3,4) :: sound('hh').out()
`,
true,
)}
## Iteration using a counter
- <ic>counter(name,limit?,step?)</ic>: return the next value on the list based on counter value. The limit is optional and defaults to the length of the list. The step is optional and defaults to 1. Setting / changing limit will reset the counter.
- <ic>$(name,limit?,step?)</ic>: shorter alias for the counter.
${makeExample(
"Using counter to iterate over a list",
`
beat(0.5) :: sound("bd").gain(line(0,1,0.01).$("ramp")).out()
`,
true,
)}
## Manipulating notes and scales ## Manipulating notes and scales
- <ic>pitch()</ic>: convert a list of integers to pitch classes - <ic>pitch()</ic>: convert a list of integers to pitch classes
${makeExample( ${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') beat(0.25) :: snd('sine')
.pitch([0,1,2,3,4,6,7,8].beat(0.125)) .pitch([0,1,2,3,4,6,7,8].beat(0.125))
.key(["F4","F3"].beat(2.0)) .key(["F4","F3"].beat(2.0))
.scale("minor").ad(0, .25).out() .scale("minor").ad(0, .25).out()
`, `,
true, true
)} )}
- <ic>semitones(number[], ...args?)</ic>: Create scale from semitone intervals. - <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( ${makeExample(
"Play pitches from scale created from semitone intervals", "Mapping the note array to the E3 major scale",
` `
beat(1) :: sound('gtr').pitch([0, 4, 3, 2].beat()).key(64)
.semitones(1, 1, 3, 1, 1, 2, 3).out()
`,
true,
)}
- <ic>cents(number[], ...args?)</ic>: Create scale from cent intervals.
${makeExample(
"Play pitches from scale created from cent intervals",
`
rhythm([0.5,0.25].beat(1),14,16) :: sound('pluck')
.stretch(iR(1,5)).pitch(iR(0,6)).key(57)
.cents(120,270,540,670,785,950,1215).out()
`,
true,
)}
- <ic>ratios(number[], ...args?)</ic>: Create scale from ratios.
${makeExample(
"Play pitches from scale created from ratios",
`
rhythm([0.5,0.25].beat(0.25),5,7) :: sound('east:3')
.pitch([0,1,2,3,4,5,6,7,8,9,10,11].beat(0.25)).key(67)
.ratios(2/11,4/11,6/11,8/11,10/11,11/11).out()
`,
true,
)}
- <ic>edo(number, scale?: string|number[])</ic>: Create scale from equal divisions of the octave. Creates chromatic scale by default.
${makeExample(
"Play pitches from scale created from equal divisions of the octave",
`
z0("e bd bd <bd bd [bd bd] [bd bd bd bd]>").sound().out()
flipbar(1) :: rhythm(.25,14,16) :: sound("ST10:30").stretch(3).gain(0.5)
.pitch([0,10,r(20,40),r(100,200),r(-200,200),r(200,300),200,r(3,666)].beat([1.0,0.5,0.25].bar(6)))
.octave(r(-6,6))
.edo(666,"rocritonic")
.out()
rhythm(2.0,26,32) :: sound("ST20").n([22,5,24,34,31,5,11,19].pick()).stretch(rI(1,6))
.pitch(rI(127,300))
.edo(666)
.out()
`,
true,
)}
- <ic>scale(scale: string, base note: number)</ic>: Map each element of the list to the closest note of the slected scale. [0, 2, 3, 5 ].scale("major", 50) returns [50, 52, <ic>54</ic>, 55]. You can use western scale names like (Major, Minor, Minor pentatonic ...) or [zeitler](https://ianring.com/musictheory/scales/traditions/zeitler) scale names. Alternatively you can also use the integers as used by Ian Ring in his [study of scales](https://ianring.com/musictheory/scales/).
${makeExample(
"Mapping the note array to the E3 major scale",
`
beat(1) :: snd('gtr') beat(1) :: snd('gtr')
.note([0, 5, 2, 1, 7].scale("Major", 52).beat()) .note([0, 5, 2, 1, 7].scale("Major", 52).beat())
.out() .out()
`, `,
true, true
)} )}
- <ic>scaleArp(scale: string, mask: number)</ic>: extrapolate a custom-masked scale from each list elements. [0].scale("major", 3) returns [0,2,4]. <ic>scaleArp</ic> supports the same scales as <ic>scale</ic>. - <ic>scaleArp(scale: string, mask: number)</ic>: extrapolate a custom-masked scale from each list elements. [0].scale("major", 3) returns [0,2,4]. <ic>scaleArp</ic> supports the same scales as <ic>scale</ic>.
${makeExample( ${makeExample(
"Extrapolate a 3-elements Mixolydian scale from 2 notes", "Extrapolate a 3-elements Mixolydian scale from 2 notes",
` `
beat(1) :: snd('gtr') beat(1) :: snd('gtr')
.note([0, 5].scaleArp("mixolydian", 3).beat() + 50) .note([0, 5].scaleArp("mixolydian", 3).beat() + 50)
.out() .out()
`, `,
true, true
)} )}
## Iteration using the mouse ## Iteration using the mouse
- <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> - <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( ${makeExample(
"Controlling an arpeggio (octave and note) with mouse", "Controlling an arpeggio (octave and note) with mouse",
` `
beat(0.25)::sound('wt_piano') beat(0.25)::sound('wt_piano')
.note([0,2,3,4,5,7,8,9,11,12].scale( .note([0,2,3,4,5,7,8,9,11,12].scale(
'minor', 30 + [0,12,24].mouseY()).mouseX()) 'minor', 30 + [0,12,24].mouseY()).mouseX())
.room(0.5).size(4).lpad(-2, .2).lpf(500, 0.3) .room(0.5).size(4).lpad(-2, .2).lpf(500, 0.3)
.ad(0, .2).out() .ad(0, .2).out()
`, `,
true, true
)} )}
## Simple data operations ## Simple data operations
- <ic>palindrome()</ic>: Concatenates a list with the same list in reverse. - <ic>palindrome()</ic>: Concatenates a list with the same list in reverse.
${makeExample( ${makeExample(
"Palindrome filter sweep", "Palindrome filter sweep",
` `
beat([1,.5,.25].beat()) :: snd('wt_stereo') beat([1,.5,.25].beat()) :: snd('wt_stereo')
.speed([1, 0.5, 0.25]) .speed([1, 0.5, 0.25])
.pan(r(0, 1)).freq([100,200,300].beat(0.25)) .pan(r(0, 1)).freq([100,200,300].beat(0.25))
@ -277,15 +191,15 @@ beat([1,.5,.25].beat()) :: snd('wt_stereo')
.lpf([500,1000,2000,4000].palindrome().beat()) .lpf([500,1000,2000,4000].palindrome().beat())
.lpad(4, 0, .25).sustain(0.125).out() .lpad(4, 0, .25).sustain(0.125).out()
`, `,
true, true
)} )}
- <ic>random(index: number)</ic>: pick a random element in the given list. - <ic>random(index: number)</ic>: pick a random element in the given list.
- <ic>rand(index: number)</ic>: shorter alias for the same method. - <ic>rand(index: number)</ic>: shorter alias for the same method.
${makeExample( ${makeExample(
"Sipping some gasoline at the robot bar", "Sipping some gasoline at the robot bar",
` `
// rand, random and pick are doing the same thing! // rand, random and pick are doing the same thing!
beat(1)::snd('fhardkick').shape(0.5) beat(1)::snd('fhardkick').shape(0.5)
.ad(0, .1).lpf(500).db(-12).out() .ad(0, .1).lpf(500).db(-12).out()
@ -296,69 +210,69 @@ beat([.5, 1].rand() / 2) :: snd(
.lpf([5000,3000,2000].pick()) .lpf([5000,3000,2000].pick())
.end(0.5).out() .end(0.5).out()
`, `,
true, true
)} )}
- <ic>pick()</ic>: pick a random element in the list. - <ic>pick()</ic>: pick a random element in the list.
${makeExample( ${makeExample(
"Picking values in lists", "Picking values in lists",
` `
beat(0.25)::sound(['ftabla', 'fwood'].pick()) beat(0.25)::sound(['ftabla', 'fwood'].pick())
.speed([1,2,3,4].pick()).ad(0, .125).n(ir(1,10)) .speed([1,2,3,4].pick()).ad(0, .125).n(ir(1,10))
.room(0.5).size(1).out() .room(0.5).size(1).out()
`, `,
true, true
)} )}
- <ic>degrade(amount: number)</ic>: removes _n_% of the list elements. Lists can be degraded as long as one element remains. The amount of degradation is given as a percentage. - <ic>degrade(amount: number)</ic>: removes _n_% of the list elements. Lists can be degraded as long as one element remains. The amount of degradation is given as a percentage.
${makeExample( ${makeExample(
"Amen break suffering from data loss", "Amen break suffering from data loss",
` `
// Tweak the value to degrade this amen break even more! // Tweak the value to degrade this amen break even more!
beat(.25)::snd('amencutup').n([1,2,3,4,5,6,7,8,9].degrade(20).loop($(1))).out() beat(.25)::snd('amencutup').n([1,2,3,4,5,6,7,8,9].degrade(20).loop($(1))).out()
`, `,
true, true
)} )}
- <ic>repeat(amount: number)</ic>: repeat every list elements _n_ times. - <ic>repeat(amount: number)</ic>: repeat every list elements _n_ times.
- <ic>repeatEven(amount: number)</ic>: repeat every pair element of the list _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. - <ic>repeatOdd(amount: number)</ic>: repeat every odd element of the list _n_ times.
${makeExample( ${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() beat(.25)::sound('amencutup').n([1,2,3,4,5,6,7,8].repeat(4).beat(.25)).out()
`, `,
true, true
)} )}
- <ic>loop(index: number)</ic>: loop takes one argument, the _index_. It allows you to iterate over a list using an iterator such as a counter. This is super useful to control how you are accessing values in a list without relying on a temporal method such as <ic>.beat()</ic> or </ic>.bar()</ic>. - <ic>loop(index: number)</ic>: loop takes one argument, the _index_. It allows you to iterate over a list using an iterator such as a counter. This is super useful to control how you are accessing values in a list without relying on a temporal method such as <ic>.beat()</ic> or </ic>.bar()</ic>.
${makeExample( ${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() beat(1) :: sound('numbers').n([1,2,3,4,5].loop($(3, 10, 2))).out()
`, `,
true, true
)} )}
- <ic>shuffle(): this</ic>: shuffles a list! Simple enough! - <ic>shuffle(): this</ic>: shuffles a list! Simple enough!
${makeExample( ${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() beat(1) :: sound('numbers').n([1,2,3,4,5].shuffle().loop($(1)).out()
`, `,
true, true
)} )}
- <ic>rotate(steps: number)</ic>: rotate a list to the right _n_ times. The last value become the first, rinse and repeat. - <ic>rotate(steps: number)</ic>: rotate a list to the right _n_ times. The last value become the first, rinse and repeat.
${makeExample( ${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]) beat(.25) :: snd('sine').fmi([1.99, 2])
.ad(0, .125).lpf(500+r(1,400)) .ad(0, .125).lpf(500+r(1,400))
.lpad(usine()*8, 0, .125) .lpad(usine()*8, 0, .125)
@ -369,21 +283,21 @@ beat(.25) :: snd('sine').fmi([1.99, 2])
.beat(.25)) // while the index changes .beat(.25)) // while the index changes
.out() .out()
`, `,
true, true
)} )}
## Filtering ## Filtering
- <ic>unique()</ic>: filter a list to remove repeated values. - <ic>unique()</ic>: filter a list to remove repeated values.
${makeExample( ${makeExample(
"Demonstrative filtering. Final list is [100, 200]", "Demonstrative filtering. Final list is [100, 200]",
` `
// Remove unique and 100 will repeat four times! // Remove unique and 100 will repeat four times!
beat(1)::snd('sine').sustain(0.1).freq([100,100,100,100,200].unique().beat()).out() beat(1)::snd('sine').sustain(0.1).freq([100,100,100,100,200].unique().beat()).out()
`, `,
true, true
)} )}
## Simple math operations ## Simple math operations

View File

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

View File

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

View File

@ -1,97 +0,0 @@
import { type Editor } from "../../../main";
import { makeExampleFactory } from "../../../Documentation";
export const ziffers_algorithmic = (application: Editor): string => {
const makeExample = makeExampleFactory(application);
return `
# Algorithmic operations
Ziffers provides shorthands for **many** numeric and algorithimic operations such as evaluating random numbers and creating sequences using list operations:
* **List operations:** Element-wise operation (_e.g._ <ic>(3 2 1)+(2 5)</ic>) using the <ic>+</ic> operator. All the arithmetic operators are supported.
${makeExample(
"Element-wise operations for melodic generation",
`
z1("1/8 _ 0 (0 1 3)+(1 2) 0 (2 3 5)-(1 2)").sound('sine')
.scale('pentatonic').fmi([0.25,0.5].beat(2)).fmh([2,4].beat(2))
.room(0.9).size(0.9).sustain(0.1).delay(0.125)
.delayfb(0.25).out();
`,
true,
)}
${makeExample(
"List operations",
`
z1('q (0 3 1 5)+(2 5) e (0 5 2)*(2 1) (0 5 2)%(2 3)')
.sound('sine')
.room(.5).size(1.0).adsr(.15,.15,.25,.1)
.scale("Bebop major")
.out()
`,
true,
)}
* **Random numbers:** <ic>(4,6)</ic> Random number between 4 and 6
${makeExample(
"Random numbers, true computer music at last!",
`
z1("s _ (0,8) 0 0 (0,5) 0 0").sound('sine')
.adsr(0, .1, 0, 0).scale('minor')
.fmdec(0.25).fmi(2).fmh([0.5, 0.25].beat(2))
.room(0.9).size(0.5).sustain(0.1) .delay(0.5)
.delay(0.125).delayfb(0.25).out();
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
`,
true,
)}
## Variables
* <ic>A=(0 2 3 (1,4))</ic> Assign pre-evaluated list to a variable
* <ic>B~(0 2 3 (1,4))</ic> Assign list with operations to a variable
${makeExample(
"Assign lists to variables",
`
z1("s A=(0 (1,4)) B~(2 (3,8)) A B A B A")
.scale("modimic")
.sound("triangle")
.adsr(0.01,0.15,0.25,0)
.gain(0.5)
.out()
`,
true,
)}
${makeExample(
"Combine variables into lists and do operations",
`
z1("s A=(0 3) B=(3 8) C=(((A+B)+A)*B) D=(C-B) A A+C D")
.sound("sawtooth")
.ad(0.05,1.0)
.gain(0.5)
.out()
`,
true,
)}
## Generative functions
* <ic>at(index: number, ...args?: number[])</ic> Get event(s) at given index
* <ic>repeat(amount: number)</ic> Repeat the generated pattern without re-evaluating random patterns
* <ic>keep()</ic> Keep the generated pattern without re-evaluating random patterns. Same as repeat(0).
* <ic>shuffle()</ic> Shuffle the pattern
* <ic>deal(amount: number): Shuffle the generated pattern and deal given number of elements
* <ic>retrograde()</ic> Reverse the generated pattern
* <ic>invert()</ic> Invert the generated pattern
* <ic>rotate(amount: number)</ic> Rotate the generated pattern by given amount
* <ic>between(start: number, end: number)</ic> Select a range of elements from the generated pattern
* <ic>from(start: number)</ic> Select a range of elements from the start index to the end of the pattern
* <ic>to(end: number)</ic> Select a range of elements from the beginning of the pattern to the end index
* <ic>every(amount: number)</ic> Select every n-th element from the pattern
`;
};

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More