Compare commits
2 Commits
superdough
...
superdirt
| Author | SHA1 | Date | |
|---|---|---|---|
| d74561953e | |||
| 37aac1341a |
3
.github/workflows/deploy.yml
vendored
@ -47,6 +47,9 @@ jobs:
|
||||
with:
|
||||
path: "main"
|
||||
|
||||
- name: Copy favicon folder
|
||||
run: cp -r main/favicon ./dist/
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
|
||||
53
README.md
@ -19,53 +19,52 @@
|
||||
|
||||
---
|
||||
|
||||
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:
|
||||
Topos is a web based live coding environment. Topos is capable of many things:
|
||||
|
||||
- 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 a music sequencer made for improvisation and composition alike
|
||||
- it is a synthesizer capable of additive, substractive, FM and wavetable
|
||||
synthesis, backed up by a [powerful web based audio engine](https://www.npmjs.com/package/superdough)
|
||||
- it can also generate video thanks to [Hydra](https://hydra.ojack.xyz/) and
|
||||
custom oscilloscopes, frequency visualizers and image sequencing capabilities
|
||||
- it can be used to sequence other MIDI devices (and soon.. OSC!)
|
||||
- it is made to be used without the need of installing anything, always ready at
|
||||
[https://topos.live](https://topos.live)
|
||||
- Topos is also an emulation and personal extension of the [Monome Teletype](https://monome.org/docs/teletype/)
|
||||
|
||||
---
|
||||
|
||||

|
||||

|
||||
|
||||
## 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 still a young project developed by two hobbyists :) Contributions are welcome! We wish to be as inclusive and welcoming as possible to your ideas and suggestions! The software is working quite well and we are continuously striving to improve it.
|
||||
|
||||
## 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:
|
||||
|
||||
- `yarn install`
|
||||
- `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 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
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
- `yarn tauri build`
|
||||
- `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
|
||||
|
||||
To run the **Docker** version, run the following command:
|
||||
### Run the application
|
||||
|
||||
`docker run -p 8001:80 bubobubobubo/topos:latest`
|
||||
`docker run -p 8001:80 yassinsiouda/topos:latest`
|
||||
|
||||
### Build and run the prod image
|
||||
|
||||
@ -73,7 +72,8 @@ To run the **Docker** version, run the following command:
|
||||
|
||||
### 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
|
||||
docker compose --profile dev up -d
|
||||
@ -81,21 +81,8 @@ docker cp topos-dev:/app/node_modules .
|
||||
docker compose --profile dev down
|
||||
```
|
||||
|
||||
then run the following command:
|
||||
**Then**
|
||||
|
||||
```bash
|
||||
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 :)
|
||||
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 603 B After Width: | Height: | Size: 603 B |
19
favicon/site.webmanifest
Normal 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"
|
||||
}
|
||||
BIN
img/screnshot.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
447
index.html
@ -1,24 +1,25 @@
|
||||
<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Topos</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Topos is a live coding environment inspired by the Monome Teletype.">
|
||||
<title>Topos</title>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon/favicon.ico" sizes="48x48" ><!-- REVISED (Aug 11, 2023)! -->
|
||||
<link rel="icon" href="/favicon/favicon.svg" sizes="any" type="image/svg+xml"><!-- REVISED (Aug 11, 2023)! -->
|
||||
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png"/>
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<link rel="stylesheet" href='/fonts/index.css' >
|
||||
<link rel="stylesheet" href="/src/output.css" />
|
||||
<link rel="stylesheet" href='/fonts/index.css' >
|
||||
<script src="https://unpkg.com/hydra-synth"></script>
|
||||
</head>
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-family: "Arial";
|
||||
background-color: #111827;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
@ -27,21 +28,10 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.fluid-transition {
|
||||
.fluid-bg-transition {
|
||||
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 {
|
||||
position: fixed; /* ignore margins */
|
||||
@ -76,89 +66,60 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.bar_button {
|
||||
@apply mx-2 px-2 py-2 flex inline rounded-lg bg-background text-foreground hover:bg-foreground hover:text-background
|
||||
}
|
||||
|
||||
.side_button {
|
||||
@apply px-2 py-2 bg-background text-foreground rounded-lg hover:bg-foreground hover:text-background
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@apply bg-selection_foreground text-sm lg:text-xl border-b py-4 text-foreground
|
||||
}
|
||||
|
||||
.tab_panel {
|
||||
@apply inline-block lg:px-4 px-8 py-1 text-brightwhite
|
||||
}
|
||||
|
||||
.doc_header {
|
||||
@apply pl-2 pr-2 lg:text-xl text-sm py-1 my-1 rounded-lg text-white hover:text-brightwhite hover:bg-brightblack
|
||||
}
|
||||
|
||||
.doc_subheader {
|
||||
@apply pl-2 pr-2 lg:text-xl text-sm ml-6 py-1 my-1 rounded-lg text-white hover:text-brightwhite hover:bg-brightblack
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<body id="all" class="z-0 bg-neutral-800 overflow-y-hidden">
|
||||
|
||||
<body id="all" class="z-0 overflow-y-hidden bg-black">
|
||||
<!-- The header is hidden on smaller devices -->
|
||||
<header class="py-0 block">
|
||||
<div id="topbar" class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center bg-background">
|
||||
<a class="flex title-font font-medium items-center mb-0">
|
||||
<img id="topos-logo" src="topos_frog.svg" class="w-12 h-12 text-selection_foreground p-2 rounded-full bg-foreground" alt="Topos Frog Logo"/>
|
||||
<input id="universe-viewer" class="hidden transparent xl:block ml-4 text-2xl bg-background text-brightwhite placeholder-brightwhite" id="renamer" type="text" placeholder="Topos">
|
||||
<header class="py-0 block text-white bg-neutral-900">
|
||||
<div id="topbar" class="mx-auto flex flex-wrap pl-2 py-1 flex-row items-center">
|
||||
<a class="flex title-font font-medium items-center text-black mb-0">
|
||||
<img id="topos-logo" src="topos_frog.svg" class="w-12 h-12 text-black p-2 bg-white rounded-full" alt="Topos Frog Logo" />
|
||||
<input id="universe-viewer" class="hidden bg-transparent xl:block ml-4 text-2xl text-white placeholder-white" id="renamer" type="text" placeholder="Topos">
|
||||
|
||||
</a>
|
||||
<nav class="py-2 flex flex-wrap items-center text-base absolute right-0">
|
||||
<!-- Play Button -->
|
||||
<a title="Play button (Ctrl+P)" id="play-button-1" class="bar_button">
|
||||
<a title="Play button (Ctrl+P)" id="play-button-1" class="flex flex-row mr-2 hover:bg-gray-800 px-2 py-2 rounded-lg">
|
||||
<svg id="play-icon" class="w-7 h-7" fill="currentColor" viewBox="0 0 14 16">
|
||||
<path d="M0 .984v14.032a1 1 0 0 0 1.506.845l12.006-7.016a.974.974 0 0 0 0-1.69L1.506.139A1 1 0 0 0 0 .984Z"/>
|
||||
</svg>
|
||||
<svg id="pause-icon" class="hidden w-7 h-7" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg id="pause-icon" class="hidden w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9 13a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0v6Zm4 0a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0v6Z"/>
|
||||
</svg>
|
||||
<p id="play-label" class="hidden lg:block text-xl pl-2 inline-block">Play</p>
|
||||
<p id="play-label" class="hidden lg:block text-xl pl-2 text-white inline-block">Play</p>
|
||||
</a>
|
||||
|
||||
<!-- Stop button -->
|
||||
<a title="Stop button (Ctrl+R)" id="stop-button-1" class="bar_button">
|
||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<a title="Stop button (Ctrl+R)" id="stop-button-1" class="flex flex-row mr-2 hover:bg-gray-800 px-2 py-2 rounded-lg">
|
||||
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Z"/>
|
||||
<rect x="6.5" y="6.5" width="7" height="7" fill="selection_background" rx="1" ry="1"/>
|
||||
<rect x="6.5" y="6.5" width="7" height="7" fill="black" rx="1" ry="1"/>
|
||||
</svg>
|
||||
<p class="hidden lg:block text-xl pl-2 inline-block">Stop</p>
|
||||
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Stop</p>
|
||||
</a>
|
||||
|
||||
<!-- Eval button -->
|
||||
<a title="Eval button (Ctrl+Enter)" id="eval-button-1" class="bar_button">
|
||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
||||
<a title="Eval button (Ctrl+Enter)" id="eval-button-1" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
|
||||
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 1v5h-5M2 19v-5h5m10-4a8 8 0 0 1-14.947 3.97M1 10a8 8 0 0 1 14.947-3.97"/>
|
||||
</svg>
|
||||
<p class="hidden lg:block text-xl pl-2 inline-block">Eval</p>
|
||||
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Eval</p>
|
||||
</a>
|
||||
|
||||
<a title="Clear button" id="clear-button-1" class="bar_button">
|
||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
||||
<a title="Clear button" id="clear-button-1" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
|
||||
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
||||
<path d="M17 4h-4V2a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v2H1a1 1 0 0 0 0 2h1v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V6h1a1 1 0 1 0 0-2ZM7 2h4v2H7V2Zm1 14a1 1 0 1 1-2 0V8a1 1 0 0 1 2 0v8Zm4 0a1 1 0 0 1-2 0V8a1 1 0 0 1 2 0v8Z"/>
|
||||
</svg>
|
||||
<p class="hidden lg:block text-xl pl-2 inline-block">Clear</p>
|
||||
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Clear</p>
|
||||
</a>
|
||||
|
||||
<a title="Share button" id="share-button" class="bar_button">
|
||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
|
||||
<a title="Share button" id="share-button" class="flex flex-row mr-2 hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
|
||||
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 19 19">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.013 7.962a3.519 3.519 0 0 0-4.975 0l-3.554 3.554a3.518 3.518 0 0 0 4.975 4.975l.461-.46m-.461-4.515a3.518 3.518 0 0 0 4.975 0l3.553-3.554a3.518 3.518 0 0 0-4.974-4.975L10.3 3.7"/>
|
||||
</svg>
|
||||
<p class="hidden lg:block text-xl pl-2 inline-block">Share</p>
|
||||
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Share</p>
|
||||
</a>
|
||||
|
||||
<a title="Open Documentation (Ctrl+D)" id="doc-button-1" class="bar_button">
|
||||
<svg class="w-7 h-7 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<a title="Open Documentation (Ctrl+D)" id="doc-button-1" class="flex flex-row hover:text-gray-900 hover:bg-gray-800 px-2 py-2 rounded-lg">
|
||||
<svg class="w-7 h-7 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
||||
</svg>
|
||||
<p class="hidden lg:block text-xl pl-2 inline-block">Docs</p>
|
||||
<p class="hidden lg:block text-xl pl-2 text-white inline-block">Docs</p>
|
||||
</a>
|
||||
|
||||
</nav>
|
||||
@ -167,135 +128,125 @@
|
||||
</header>
|
||||
|
||||
<div id="documentation" class="hidden">
|
||||
<div id="documentation-page" class="flex flex-row transparent">
|
||||
<aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-background">
|
||||
<nav class="text-xl sm:text-sm overflow-y-scroll mb-24 bg-background">
|
||||
<details class="" open>
|
||||
<div id="documentation-page" class="flex flex-row bg-transparent">
|
||||
<aside class="w-1/8 flex-shrink-0 h-screen overflow-y-auto p-1 lg:p-6 bg-neutral-900 text-white">
|
||||
<nav class="text-xl sm:text-sm overflow-y-scroll mb-24">
|
||||
<details class="" open=true>
|
||||
<summary class="font-semibold lg:text-xl text-orange-300">Basics</summary>
|
||||
<div class="flex flex-col">
|
||||
<p rel="noopener noreferrer" id="docs_introduction" class="doc_header">Welcome </p>
|
||||
<p rel="noopener noreferrer" id="docs_interface" class="doc_header">Interface</p>
|
||||
<p rel="noopener noreferrer" id="docs_interaction" class="doc_header">Interaction</p>
|
||||
<p rel="noopener noreferrer" id="docs_shortcuts" class="doc_header">Keyboard</p>
|
||||
<p rel="noopener noreferrer" id="docs_mouse" class="doc_header">Mouse</p>
|
||||
<p rel="noopener noreferrer" id="docs_code" class="doc_header">Coding</p>
|
||||
<p rel="noopener noreferrer" id="docs_introduction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Welcome </p>
|
||||
<p rel="noopener noreferrer" id="docs_interface" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interface</p>
|
||||
<p rel="noopener noreferrer" id="docs_interaction" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Interaction</p>
|
||||
<p rel="noopener noreferrer" id="docs_shortcuts" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Keyboard</p>
|
||||
<p rel="noopener noreferrer" id="docs_mouse" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Mouse</p>
|
||||
<p rel="noopener noreferrer" id="docs_code" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Coding</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="space-y-2" open>
|
||||
<details class="space-y-2" open=true>
|
||||
<summary class="font-semibold lg:text-xl pb-1 pt-1 text-orange-300">Learning</summary>
|
||||
<div class="flex flex-col">
|
||||
<!-- Time -->
|
||||
<details class="space-y-2">
|
||||
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header">Time</summary>
|
||||
<div class="flex flex-col">
|
||||
<p rel="noopener noreferrer" id="docs_time" class="doc_subheader">Dealing with time</p>
|
||||
<p rel="noopener noreferrer" id="docs_linear" class="doc_subheader">Time & Transport</p>
|
||||
<p rel="noopener noreferrer" id="docs_cyclic" class="doc_subheader">Time & Cycles</p>
|
||||
<p rel="noopener noreferrer" id="docs_longform" class="doc_subheader">Time & Structure</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Audio Engine -->
|
||||
<details class="space-y-2">
|
||||
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header">Audio Engine</summary>
|
||||
<!-- Time -->
|
||||
<details class="space-y-2" open=false>
|
||||
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Time</summary>
|
||||
<div class="flex flex-col">
|
||||
<p rel="noopener noreferrer" id="docs_audio_basics" class="doc_subheader">Playing a sound</p>
|
||||
<p rel="noopener noreferrer" id="docs_amplitude" class="doc_subheader">Amplitude</p>
|
||||
<p rel="noopener noreferrer" id="docs_pitch" class="doc_subheader">Pitch</p>
|
||||
<p rel="noopener noreferrer" id="docs_sampler" class="doc_subheader">Sampler</p>
|
||||
<p rel="noopener noreferrer" id="docs_synths" class="doc_subheader">Synths</p>
|
||||
<p rel="noopener noreferrer" id="docs_filters" class="doc_subheader">Filters</p>
|
||||
<p rel="noopener noreferrer" id="docs_effects" class="doc_subheader">Effects</p>
|
||||
<p rel="noopener noreferrer" id="docs_time" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Dealing with time</p>
|
||||
<p rel="noopener noreferrer" id="docs_linear" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Transport</p>
|
||||
<p rel="noopener noreferrer" id="docs_cyclic" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Cycles</p>
|
||||
<p rel="noopener noreferrer" id="docs_longform" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Time & Structure</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Samples -->
|
||||
<details class="space-y-2">
|
||||
<summary class="ml-2 lg:text-xl pb-1 pt-1 doc_header ">Samples</summary>
|
||||
<details class="space-y-2" open=false>
|
||||
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Audio Engine</summary>
|
||||
<div class="flex flex-col">
|
||||
<p rel="noopener noreferrer" id="docs_sample_list" class="doc_subheader">List of samples</p>
|
||||
<p rel="noopener noreferrer" id="docs_loading_samples" class="doc_subheader">External samples</p>
|
||||
<p rel="noopener noreferrer" id="docs_audio_basics" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Playing a sound</p>
|
||||
<p rel="noopener noreferrer" id="docs_amplitude" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Amplitude</p>
|
||||
<p rel="noopener noreferrer" id="docs_sampler" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Sampler</p>
|
||||
<p rel="noopener noreferrer" id="docs_synths" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synths</p>
|
||||
<p rel="noopener noreferrer" id="docs_reverb_delay" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Effects</p>
|
||||
</div>
|
||||
</details>
|
||||
<p rel="noopener noreferrer" id="docs_midi" class="doc_header">MIDI</p>
|
||||
<p rel="noopener noreferrer" id="docs_osc" class="doc_header">OSC</p>
|
||||
|
||||
<!-- Audio Engine -->
|
||||
<details class="space-y-2" open=false>
|
||||
<summary class="ml-2 lg:text-xl pb-1 pt-1 text-white">Samples</summary>
|
||||
<div class="flex flex-col">
|
||||
<p rel="noopener noreferrer" id="docs_sample_list" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">List of samples</p>
|
||||
<p rel="noopener noreferrer" id="docs_loading_samples" class="ml-8 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Loading Samples</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<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>
|
||||
<p rel="noopener noreferrer" id="docs_osc" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">OSC</p>
|
||||
</div>
|
||||
</details>
|
||||
<details class="space-y-2" open>
|
||||
<details class="space-y-2" open=true>
|
||||
<summary class="font-semibold lg:text-xl pb-1 pt-1 text-orange-300">Patterns</summary>
|
||||
<div class="flex flex-col">
|
||||
|
||||
<p rel="noopener noreferrer" id="docs_patterns" class="pl-2 pr-2 lg:text-xl text-sm hover:neutral-800 py-1 my-1 rounded-lg doc_header">Array patterns</p>
|
||||
<!-- Ziffers -->
|
||||
<details class="space-y-2">
|
||||
<summary class="doc_header">Ziffers</summary>
|
||||
<div class="flex flex-col">
|
||||
<p rel="noopener noreferrer" id="docs_ziffers_basics" class="doc_subheader">Basics</p>
|
||||
<p rel="noopener noreferrer" id="docs_ziffers_scales" class="doc_subheader">Scales</p>
|
||||
<p rel="noopener noreferrer" id="docs_ziffers_rhythm" class="doc_subheader">Rhythm</p>
|
||||
<p rel="noopener noreferrer" id="docs_ziffers_algorithmic" class="doc_subheader">Algorithmic</p>
|
||||
<p rel="noopener noreferrer" id="docs_ziffers_tonnetz" class="doc_subheader">Tonnetz</p>
|
||||
<p rel="noopener noreferrer" id="docs_ziffers_syncing" class="doc_subheader">Syncing</p>
|
||||
</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>
|
||||
|
||||
<p rel="noopener noreferrer" id="docs_variables" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Global Variables</p>
|
||||
<p rel="noopener noreferrer" id="docs_lfos" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Low Freq Oscs.</p>
|
||||
<p rel="noopener noreferrer" id="docs_probabilities" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Probabilities</p>
|
||||
<p rel="noopener noreferrer" id="docs_chaining" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Chaining</p>
|
||||
<p rel="noopener noreferrer" id="docs_functions" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Functions</p>
|
||||
<p rel="noopener noreferrer" id="docs_ziffers" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Ziffers</p>
|
||||
<!--
|
||||
<p rel="noopener noreferrer" id="docs_reference" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Reference</p>
|
||||
-->
|
||||
</div>
|
||||
</details>
|
||||
<details class="space-y-2" open>
|
||||
<summary class="font-semibold lg:text-xl doc_header">More</summary>
|
||||
<details class="space-y-2" open=true>
|
||||
<summary class="font-semibold lg:text-xl text-orange-300">More</summary>
|
||||
<div class="flex flex-col">
|
||||
<a rel="noopener noreferrer" id="docs_synchronisation" class="doc_subheader">Synchronisation</a>
|
||||
<a rel="noopener noreferrer" id="docs_oscilloscope" class="doc_subheader">Oscilloscope</a>
|
||||
<a rel="noopener noreferrer" id="docs_visualization" class="doc_header">Visualization</a>
|
||||
<a rel="noopener noreferrer" id="docs_bonus" class="doc_header">Bonus/Trivia</a>
|
||||
<a rel="noopener noreferrer" id="docs_about" class="doc_header">About Topos</a>
|
||||
<a rel="noopener noreferrer" id="docs_synchronisation" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Synchronisation</a>
|
||||
<a rel="noopener noreferrer" id="docs_oscilloscope" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Oscilloscope</a>
|
||||
<a rel="noopener noreferrer" id="docs_bonus" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">Bonus/Trivia</a>
|
||||
<a rel="noopener noreferrer" id="docs_about" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg">About Topos</a>
|
||||
</div>
|
||||
</details>
|
||||
<details class="" open>
|
||||
<details class="" open=true>
|
||||
<summary class="font-semibold lg:text-xl text-orange-300">Community</summary>
|
||||
<form action="https://github.com/Bubobubobubobubo/topos">
|
||||
<input rel="noopener noreferrer" id="github_link" class="doc_header" type="submit" value="GitHub" />
|
||||
<input rel="noopener noreferrer" id="github_link" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="GitHub" />
|
||||
</form>
|
||||
<form action="https://discord.gg/6T67DqBNNT">
|
||||
<input rel="noopener noreferrer" id="discord_link" class="doc_header" type="submit" value="Discord" />
|
||||
<input rel="noopener noreferrer" id="discord_link" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Discord" />
|
||||
</form>
|
||||
<form action="https://ko-fi.com/raphaelbubo">
|
||||
<input rel="noopener noreferrer" id="support_link" class="doc_header" type="submit" value="Support" />
|
||||
<input rel="noopener noreferrer" id="support_link" class="pl-2 pr-2 lg:text-xl text-sm hover:bg-neutral-800 py-1 my-1 rounded-lg" type="submit" value="Support" />
|
||||
</form>
|
||||
</details>
|
||||
</nav>
|
||||
</aside>
|
||||
<div id="documentation-content" class="w-full flex-grow-1 h-screen overflow-y-scroll lg:px-12 mx-2 my-2 break-words pb-32 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 bg-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<!-- This modal is used for switching between buffers -->
|
||||
<div id="modal-buffers" class="invisible flex justify-center items-center absolute top-0 right-0 bottom-0 left-0">
|
||||
<div id="start-button" class="lg:px-16 px-4 lg:pt-4 lg:pb-4 pt-2 pb-2 rounded-md text-center bg-foreground">
|
||||
<p class="text-semibold lg:text-2xl text-sm pb-4 text-selection_foreground">Known universes</p>
|
||||
<div id="modal-buffers" class="invisible bg-gray-900 bg-opacity-50 flex justify-center items-center absolute top-0 right-0 bottom-0 left-0">
|
||||
<div id="start-button" class="lg:px-16 px-4 lg:pt-4 lg:pb-4 pt-2 pb-2 rounded-md text-center bg-white">
|
||||
<p class="text-semibold lg:text-2xl text-sm pb-4">Known universes</p>
|
||||
<p id="existing-universes" class="text-normal lg:h-auto h-48 overflow-y-auto mb-2"></p>
|
||||
<div id="disclaimer" class="pb-4">
|
||||
<form id="universe-creator">
|
||||
<label for="search" class="mb-2 text-sm font-medium sr-only ">Search</label>
|
||||
<label for="search" class="mb-2 text-sm font-medium text-gray-900 sr-only text-white">Search</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<svg class="w-4 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<svg class="w-4 h-4 text-gray-500 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm border border-neutral-800 outline-0 rounded-lg neutral-800 " placeholder="Buffer..." required>
|
||||
<button id="load-universe-button" class="bg-background text-selection_background absolute right-2.5 bottom-2.5 focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
|
||||
<input name="universe" minlength="2" autocomplete="off" type="text" id="buffer-search" class="block w-full p-4 pl-10 text-sm border border-neutral-800 outline-0 rounded-lg bg-neutral-800 text-white" placeholder="Buffer..." required>
|
||||
<button id="load-universe-button" class="text-black absolute right-2.5 bottom-2.5 bg-white hover:bg-white focus:outline-none font-medium rounded-lg text-sm px-4 py-2">Go</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-2 flex space-x-6 border-t rounded-b border-spacing-y-4">
|
||||
<button id="close-universes-button" data-modal-hide="defaultModal" type="button" class="mt-2 focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center bg-background text-selection_background">Close</button>
|
||||
<div class="mt-2 flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 border-spacing-y-4">
|
||||
<button id="close-universes-button" data-modal-hide="defaultModal" type="button" class="mt-2 hover:bg-neutral-700 bg-neutral-800 text-white focus:ring-4 font-medium rounded-lg text-sm px-5 py-2.5 text-center">Close</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -304,26 +255,29 @@
|
||||
|
||||
<!-- This modal is used for settings -->
|
||||
<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
|
||||
overflow-y-scroll owerflow-x-scroll
|
||||
md:top-0 md:bottom-0 h-screen w-full"
|
||||
>
|
||||
<div class="grid w-full grid-col-3">
|
||||
<div class="white rounded-lg lg:mx-48 mx-0 lg:space-y-8 space-y-4 lg:px-8 bg-foreground">
|
||||
<h1 class="lg:mt-12 mt-6 font-semibold rounded-lg justify-center lg:text-center lg:pl-0 pl-8 mx-4 subtitle">Topos Application Settings</h1>
|
||||
<div class="bg-white rounded-lg lg:mx-48 mx-0 lg:space-y-8 space-y-4 lg:px-8">
|
||||
<h1 class="lg:mt-12 mt-6 font-semibold rounded-lg
|
||||
bg-gray-800 justify-center lg:text-center lg:pl-0 pl-8 text-white mx-4
|
||||
text-sm lg:text-xl border-b border-gray-300 py-4">Topos Application Settings</h1>
|
||||
<div class="flex lg:flex-row flex-col mr-4 ml-4">
|
||||
<!-- Font Size Selection -->
|
||||
<div class="rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 lg:pb-4 pb-2 underline underline-offset-4 text-selection_background">Theme Settings</p>
|
||||
<div class="bg-gray-200 rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 lg:pb-4 pb-2 underline underline-offset-4">Font Settings</p>
|
||||
<div class="mb-6 mx-4 font-semibold">
|
||||
<label for="default-input" class="block mb-2 ml-1 font-normal sd:text-sm text-foreground">Size:</label>
|
||||
<input type="text" id="font-size-input" type="number" class="border
|
||||
text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 focus:border-blue-500">
|
||||
<label for="default-input" class="block mb-2 ml-1 font-normal sd:text-sm">Size:</label>
|
||||
<input type="text" id="font-size-input" type="number" class="bg-gray-50 border border-gray-300 text-gray-900
|
||||
text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700
|
||||
dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
</div>
|
||||
<label for="font" class="block ml-5 mb-2 font-medium sd:text-sm text-foreground">Font:</label>
|
||||
<select id="font-family" class=" ml-4 border mb-2
|
||||
text-sm rounded-lg focus:border-blue-500 block p-2.5">
|
||||
<label for="font" class="block ml-5 mb-2 font-medium sd:text-sm">Font:</label>
|
||||
<select id="font-family" class="bg-gray-50 ml-4 border border-gray-300 mb-2
|
||||
text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
|
||||
<option value="IBM Plex Mono">IBM Plex Mono</option>
|
||||
<option value="Jet Brains">Jet Brains</option>
|
||||
<option value="Courier">Courier</option>
|
||||
@ -335,199 +289,186 @@
|
||||
<option value="Steps Mono">Steps Mono</option>
|
||||
<option value="Steps Mono Thin">Steps Mono Thin</option>
|
||||
</select>
|
||||
<div class="rounded-lg ml-0 lg:w-1/3 w-full pt-2 pb-1 mb-2">
|
||||
<label for="theme" class="block ml-5 mb-2 font-medium sd:text-sm text-foreground">Theme:</label>
|
||||
<select id="theme-selector" class="ml-4 border mb-2
|
||||
text-sm rounded-lg block p-2.5">
|
||||
</select>
|
||||
<div id="theme-previewer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Editor mode selection -->
|
||||
<div class="rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">Editor options</p>
|
||||
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">Editor options</p>
|
||||
<!-- Checkboxes -->
|
||||
<div class="pr-4">
|
||||
<div class="flex items-center mb-4 ml-5">
|
||||
<input id="vim-mode" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-selection_background">Vim Mode</label>
|
||||
<input id="vim-mode" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Vim Mode</label>
|
||||
</div>
|
||||
<div class="flex items-center mb-4 ml-5">
|
||||
<input id="show-line-numbers" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Line Numbers</label>
|
||||
<input id="show-line-numbers" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Line Numbers</label>
|
||||
</div>
|
||||
<div class="flex items-center mb-4 ml-5">
|
||||
<input id="show-time-position" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Time Position</label>
|
||||
<input id="show-time-position" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Time Position</label>
|
||||
</div>
|
||||
<div class="flex items-center mb-4 ml-5">
|
||||
<input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Hovering Tips</label>
|
||||
<input id="show-tips" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Hovering Tips</label>
|
||||
</div>
|
||||
<div class="flex items-center mb-4 ml-5">
|
||||
<input id="show-completions" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Show Completions</label>
|
||||
<input id="show-completions" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Show Completions</label>
|
||||
</div>
|
||||
<div class="flex items-center mb-4 ml-5">
|
||||
<input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Load Demo Song</label>
|
||||
<input id="load-demo-songs" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Load Demo Song</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2 bg-selection_foreground">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">File Management</p>
|
||||
<div class="bg-gray-200 rounded-lg lg:ml-4 lg:w-1/3 w-full pt-2 pb-1 mb-2">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">File Management</p>
|
||||
<div class="flex flex-col space-y-2 pb-2">
|
||||
<button id="download-universes" class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
|
||||
<button id="download-universes" class="bg-gray-800 hover:bg-gray-900 text-white font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
|
||||
<svg class="fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
|
||||
<span class="text-selection_foreground">Download universes</span>
|
||||
<span>Download universes</span>
|
||||
</button>
|
||||
<button id="upload-universes" class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
|
||||
<button id="upload-universes" class="bg-gray-800 hover:bg-gray-900 text-white font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
|
||||
<svg class="rotate-180 fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
|
||||
<span class="text-selection_foreground">Upload universes</span>
|
||||
<span>Upload universes</span>
|
||||
</button>
|
||||
<button id="destroy-universes" class="bg-brightwhite font-bold lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
|
||||
<button id="destroy-universes" class="bg-red-800 hover:bg-red-900 text-white font-bold lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-6 mr-2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
<span class="text-selection_foreground">Destroy universes</span>
|
||||
<span>Destroy universes</span>
|
||||
</button>
|
||||
<!-- Upload audio samples -->
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-2 pt-2 underline underline-offset-4 text-selection_background">Audio samples</p>
|
||||
|
||||
<label class="bg-brightwhite font-bold lg:py-4 lg:px-2 px-1 py-2 rounded-lg inline-flex items-center mx-4 text-selection_background">
|
||||
<svg class="rotate-180 fill-current w-4 h-6 mr-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M13 8V2H7v6H2l8 8 8-8h-5zM0 18h20v2H0v-2z"/></svg>
|
||||
<input id="upload-samples" type="file" class="hidden" accept="file" webkitdirectory directory multiple>
|
||||
<span id="sample-indicator" class="text-selection_foreground">Import samples</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Midi settings -->
|
||||
<div id="midi-settings-container" class="rounded-lg flex lg:flex-row flex-col mx-4 my-4 pt-4 bg-color bg-selection_foreground">
|
||||
<div id="midi-settings-container" class="bg-gray-200 rounded-lg flex lg:flex-row flex-col mx-4 my-4 pt-4">
|
||||
<div class="lg:flex lg:flex-row w-fit">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">MIDI I/O Settings</p>
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">MIDI I/O Settings</p>
|
||||
<div class="flex items-center mb-4 ml-6">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">MIDI Clock: </label>
|
||||
<select id="midi-clock-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">MIDI Clock: </label>
|
||||
<select id="midi-clock-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
|
||||
<option value="-1">Internal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lg:flex block items-center mb-4 ml-6">
|
||||
<label for="default-checkbox" class="ml-2 mr-2 text-sm font-medium text-foreground">Clock PPQN: </label>
|
||||
<select id="midi-clock-ppqn-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Clock PPQN: </label>
|
||||
<select id="midi-clock-ppqn-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
|
||||
<option value="24">24</option>
|
||||
<option value="48">48</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lg:flex block items-center mb-4 ml-6">
|
||||
<input id="send-midi-clock" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 rounded focus:ring-blue-600 focus:ring-2">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Send MIDI Clock</label>
|
||||
<input id="send-midi-clock" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Send MIDI Clock</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:flex block flex-row">
|
||||
<div class="flex items-center mb-4 ml-6">
|
||||
<label for="default-checkbox" class="ml-2 mr-2 text-sm font-medium text-foreground">MIDI input: </label>
|
||||
<select id="default-midi-input" class="w-32 h-8 text-sm font-medium text-black rounded focus:ring-blue-600 focus:ring-2">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">MIDI input: </label>
|
||||
<select id="default-midi-input" class="w-32 h-8 text-sm font-medium text-black bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2">
|
||||
<option value="-1">None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lg:flex block items-center mb-4 ml-6">
|
||||
<input id="midi-channels-scripts" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 rounded focus:ring-blue-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-foreground">Route channels to scripts</label>
|
||||
<input id="midi-channels-scripts" type="checkbox" value="" class="lg:w-8 lg:h-8 h-4 w-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
|
||||
<label for="default-checkbox" class="ml-2 text-sm font-medium text-dark">Route channels to scripts</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio nudge slider -->
|
||||
<div id="midi-settings-container" class="rounded-lg flex flex-col mx-4 my-4 pt-4 pb-2 bg-selection_foreground">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4 text-selection_background">Audio/Event Nudging</p> <div class="flex flex-column pb-2">
|
||||
<p class="pt-0.5 ml-4 text-foreground">Clock:</p>
|
||||
<div id="midi-settings-container" class="bg-gray-200 rounded-lg flex flex-col mx-4 my-4 pt-4 pb-2">
|
||||
<p class="font-bold lg:text-xl text-sm ml-4 pb-4 underline underline-offset-4">Audio/Event Nudging</p>
|
||||
<div class="flex flex-column pb-2">
|
||||
<p class="pt-0.5 ml-4">Clock:</p>
|
||||
<input
|
||||
type="range" id="audio_nudge"
|
||||
name="audiorangeInput"
|
||||
min="-200" max="200"
|
||||
value="0"
|
||||
class="w-full ml-4 text-red"
|
||||
class="w-full ml-4"
|
||||
oninput="nudgenumber.value=audio_nudge.value"
|
||||
>
|
||||
<output
|
||||
name="nudgenumber"
|
||||
id="nudgenumber"
|
||||
for="audiorangeInput"
|
||||
class="rounded-lg ml-2 mr-4 px-4 py-1 text-foreground"
|
||||
class="bg-gray-500 rounded-lg ml-2 mr-4 px-4 py-1 text-white"
|
||||
>0</output>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column">
|
||||
|
||||
<p class="pt-0.5 ml-4 text-foreground">Audio:</p>
|
||||
<p class="pt-0.5 ml-4">Audio:</p>
|
||||
<input
|
||||
type="range" id="dough_nudge"
|
||||
name="doughrangeInput"
|
||||
min="0" max="100"
|
||||
value="0"
|
||||
class="w-full ml-4 text-foreground"
|
||||
class="w-full ml-4"
|
||||
oninput="doughnumber.value=dough_nudge.value"
|
||||
>
|
||||
<output
|
||||
name="doughnumber"
|
||||
id="doughnumber"
|
||||
for="doughrangeInput"
|
||||
class="rounded-lg ml-2 mr-4 px-4 py-1 text-foreground"
|
||||
class="bg-gray-500 rounded-lg ml-2 mr-4 px-4 py-1 text-white"
|
||||
>0</output>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-6 border-t rounded-b mx-4 border-spacing-y-4 pb-36 lg:pb-0">
|
||||
<div class="flex space-x-6 border-t border-gray-200 rounded-b dark:border-gray-600 mx-4 border-spacing-y-4 pb-36 lg:pb-0">
|
||||
<button id="close-settings-button" data-modal-hide="defaultModal" type="button" class="
|
||||
hover:bg-background bg-background mt-4 mb-4 focus:ring-4
|
||||
font-medium rounded-lg text-sm px-5 py-2.5 text-center text-selection_background">OK</button>
|
||||
hover:bg-gray-700 bg-gray-800 mt-4 mb-4 text-white focus:ring-4
|
||||
font-medium rounded-lg text-sm px-5 py-2.5 text-center">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row max-h-fit">
|
||||
|
||||
<!-- This is a lateral bar that will inherit the header buttons if the window is too small. -->
|
||||
<aside id="sidebar" class="
|
||||
flex flex-col items-center w-14
|
||||
h-screen py-2 border-r
|
||||
rtl:border-l max-h-fit
|
||||
rtl:border-r-0 bg-background
|
||||
border-neutral-700 border-none"
|
||||
rtl:border-r-0 bg-neutral-900
|
||||
dark:border-neutral-700 border-none"
|
||||
>
|
||||
<nav class="flex flex-col space-y-6">
|
||||
<a title="Local Scripts (Ctrl + L)" id="local-button" class="side_button">
|
||||
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 18">
|
||||
<a title="Local Scripts (Ctrl + L)" id="local-button" class="pl-2 p-1.5 focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
|
||||
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 18">
|
||||
<path d="M18 5H0v11a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5Zm-7.258-2L9.092.8a2.009 2.009 0 0 0-1.6-.8H2.049a2 2 0 0 0-2 2v1h10.693Z"/>
|
||||
</svg>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a title="Global Script (Ctrl + G)" id="global-button" class="side_button">
|
||||
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 16">
|
||||
<a title="Global Script (Ctrl + G)" id="global-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg hover:bg-gray-800">
|
||||
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 16">
|
||||
<path d="M14.316.051A1 1 0 0 0 13 1v8.473A4.49 4.49 0 0 0 11 9c-2.206 0-4 1.525-4 3.4s1.794 3.4 4 3.4 4-1.526 4-3.4a2.945 2.945 0 0 0-.067-.566c.041-.107.064-.22.067-.334V2.763A2.974 2.974 0 0 1 16 5a1 1 0 0 0 2 0C18 1.322 14.467.1 14.316.051ZM10 3H1a1 1 0 0 1 0-2h9a1 1 0 1 1 0 2Z"/>
|
||||
<path d="M10 7H1a1 1 0 0 1 0-2h9a1 1 0 1 1 0 2Zm-5 4H1a1 1 0 0 1 0-2h4a1 1 0 1 1 0 2Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a title="Initialisation Script (Ctrl + I)" id="init-button" class="side_button">
|
||||
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 14">
|
||||
<a title="Initialisation Script (Ctrl + I)" id="init-button" class="pl-2 p-1.5 focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
|
||||
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1v12m0 0 4-4m-4 4L1 9"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a title="Project notes (Ctrl + N)" id="note-button" class="side_button">
|
||||
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<a title="Project notes (Ctrl + N)" id="note-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
|
||||
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="m13.835 7.578-.005.007-7.137 7.137 2.139 2.138 7.143-7.142-2.14-2.14Zm-10.696 3.59 2.139 2.14 7.138-7.137.007-.005-2.141-2.141-7.143 7.143Zm1.433 4.261L2 12.852.051 18.684a1 1 0 0 0 1.265 1.264L7.147 18l-2.575-2.571Zm14.249-14.25a4.03 4.03 0 0 0-5.693 0L11.7 2.611 17.389 8.3l1.432-1.432a4.029 4.029 0 0 0 0-5.689Z"/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a title="Application Settings" id="settings-button" class="side_button">
|
||||
<svg class="w-8 h-8 " aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<a title="Application Settings" id="settings-button" class="pl-2 p-1.5 text-white focus:outline-nones transition-colors duration-200 rounded-lg text-white hover:bg-gray-800">
|
||||
<svg class="w-8 h-8 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
@ -541,55 +482,55 @@
|
||||
|
||||
<!-- Tabs for local files -->
|
||||
<div class="min-w-screen flex grow flex-col">
|
||||
<ul id="local-script-tabs" class=" flex text-xl font-medium text-center bg-background space-x-1 lg:space-x-8">
|
||||
<ul id="local-script-tabs" class=" flex text-xl font-medium text-center text-white bg-neutral-900 space-x-1 lg:space-x-8">
|
||||
<li class="pl-5">
|
||||
<a title="Local Script 1 (F1)" id="tab-1" class="tab_panel">1</a>
|
||||
<a title="Local Script 1 (F1)" id="tab-1" class="bg-orange-300 inline-block lg:px-4 px-2 py-1 text-white hover:bg-gray-800">1</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 2 (F2)" id="tab-2" class="tab_panel">2</a>
|
||||
<a title="Local Script 2 (F2)" id="tab-2" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">2</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 3 (F3)" id="tab-3" class="tab_panel">3</a>
|
||||
<a title="Local Script 3 (F3)" id="tab-3" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">3</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 4 (F4)" id="tab-4" class="tab_panel">4</a>
|
||||
<a title="Local Script 4 (F4)" id="tab-4" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">4</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 5 (F5)" id="tab-5" class="tab_panel">5</a>
|
||||
<a title="Local Script 5 (F5)" id="tab-5" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">5</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 6 (F6)" id="tab-6" class="tab_panel">6</a>
|
||||
<a title="Local Script 6 (F6)" id="tab-6" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">6</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 7 (F7)" id="tab-7" class="tab_panel">7</a>
|
||||
<a title="Local Script 7 (F7)" id="tab-7" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">7</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 8 (F8)" id="tab-8" class="tab_panel">8</a>
|
||||
<a title="Local Script 8 (F8)" id="tab-8" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">8</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a title="Local Script 9 (F9)" id="tab-9" class="tab_panel">9</a>
|
||||
<a title="Local Script 9 (F9)" id="tab-9" class="inline-block lg:px-4 px-2 py-1 hover:bg-gray-800">9</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
<!-- Here comes the editor itself -->
|
||||
<div id="editor" class="relative flex flex-row h-screen overflow-y-hidden">
|
||||
<canvas id="hydra-bg" class="fullscreencanvas"></canvas>
|
||||
<canvas id="scope" class="fullscreencanvas"></canvas>
|
||||
<canvas id="feedback" class="fullscreencanvas"></canvas>
|
||||
<canvas id="drawings" class="fullscreencanvas"></canvas>
|
||||
<canvas id="hydra-bg" class="hydracanvas"></canvas>
|
||||
</div>
|
||||
<p id="error_line" class="hidden w-screen bg-background font-mono absolute bottom-0 pl-2 py-2">Hello kids</p>
|
||||
<p id="error_line" class="hidden text-red-400 w-screen bg-neutral-900 font-mono absolute bottom-0 pl-2 py-2">Hello kids</p>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
<template id="ui-known-universe-item-template">
|
||||
<!-- A known universe button in "opening" interface -->
|
||||
<li class="py-2 px-4 flex justify-between text-brightwhite hover:bg-selection_background hover:text-selection_foreground">
|
||||
<li class="hover:fill-black hover:bg-white py-2 hover:text-black flex justify-between px-4">
|
||||
<button class="universe-name load-universe" title="Load this universe">Universe Name</button>
|
||||
<button class="delete-universe" title="Delete this universe">🗑</button>
|
||||
</li>
|
||||
</template>
|
||||
</body>
|
||||
<p id="timeviewer" class="rounded-lg px-2 py-2 font-bold cursor-textpointer-events-none select-none text-sm absolute bottom-2 right-2 bg-foreground text-background"></p>
|
||||
<p id="fillviewer" class="invisible rounded-lg px-2 py-2 font-bold cursor-textpointer-events-none select-none text-sm absolute right-2 bottom-12 bg-foreground text-background">/////// Fill ///////</p>
|
||||
<p id="timeviewer" class="rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute bottom-2 right-2"></p>
|
||||
<p id="fillviewer" class="invisible rounded-lg px-2 py-2 font-bold bg-white cursor-textpointer-events-none select-none text-black text-sm absolute right-2 bottom-12">/////// Fill ///////</p>
|
||||
</html>
|
||||
|
||||
24
manifest.webmanifest
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -14,7 +14,7 @@
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.5",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.17.4"
|
||||
"vite-plugin-pwa": "^0.16.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-javascript": "^6.1.9",
|
||||
@ -22,7 +22,6 @@
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@replit/codemirror-vim": "^6.0.14",
|
||||
"@strudel.cycles/webaudio": "^0.8.2",
|
||||
"@strudel/soundfonts": "^0.11.0",
|
||||
"@types/marked": "^5.0.1",
|
||||
"@types/showdown": "^2.0.1",
|
||||
"acorn": "^8.10.0",
|
||||
@ -31,7 +30,6 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"codemirror": "^6.0.1",
|
||||
"fflate": "^0.8.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"jisg": "^0.9.7",
|
||||
"lru-cache": "^10.0.1",
|
||||
"marked": "^7.0.3",
|
||||
@ -39,13 +37,13 @@
|
||||
"postcss": "^8.4.27",
|
||||
"showdown": "^2.1.0",
|
||||
"showdown-highlight": "^3.1.0",
|
||||
"superdough": "^0.10.0",
|
||||
"superdough": "^0.9.11",
|
||||
"tailwind-highlightjs": "^2.0.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tone": "^14.8.49",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"vite-plugin-markdown": "^2.1.0",
|
||||
"zifferjs": "^0.0.62",
|
||||
"zifferjs": "^0.0.44",
|
||||
"zyklus": "^0.1.4",
|
||||
"zzfx": "^1.2.0"
|
||||
}
|
||||
|
||||
@ -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 |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 42 KiB |
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
860
src/API.ts
201
src/Clock.ts
@ -1,7 +1,11 @@
|
||||
// @ts-ignore
|
||||
import { TransportNode } from "./TransportNode";
|
||||
import TransportProcessor from "./TransportProcessor?worker&url";
|
||||
import { Editor } from "./main";
|
||||
import { tryEvaluate } from "./Evaluator";
|
||||
// @ts-ignore
|
||||
import { getAudioContext } from "superdough";
|
||||
// @ts-ignore
|
||||
import "zyklus";
|
||||
const zeroPad = (num: number, places: number) =>
|
||||
String(num).padStart(places, "0");
|
||||
|
||||
export interface TimePosition {
|
||||
/**
|
||||
@ -18,35 +22,29 @@ export interface TimePosition {
|
||||
|
||||
export class Clock {
|
||||
/**
|
||||
* The Clock Class is responsible for keeping track of the current time.
|
||||
* It is also responsible for starting and stopping the Clock TransportNode.
|
||||
*
|
||||
* @param app - The main application instance
|
||||
* @param ctx - The current AudioContext used by app
|
||||
* @param transportNode - The TransportNode helper
|
||||
* @param bpm - The current beats per minute value
|
||||
* @param time_signature - The time signature
|
||||
* @param time_position - The current time position
|
||||
* @param ppqn - The pulses per quarter note
|
||||
* @param tick - The current tick since origin
|
||||
* @param app - main application instance
|
||||
* @param clock - zyklus clock
|
||||
* @param ctx - current AudioContext used by app
|
||||
* @param bpm - current beats per minute value
|
||||
* @param time_signature - time signature
|
||||
* @param time_position - current time position
|
||||
* @param ppqn - pulses per quarter note
|
||||
* @param tick - current tick since origin
|
||||
* @param running - Is the clock running?
|
||||
* @param lastPauseTime - The last time the clock was paused
|
||||
* @param lastPlayPressTime - The last time the clock was started
|
||||
* @param totalPauseTime - The total time the clock has been paused / stopped
|
||||
*/
|
||||
|
||||
private _bpm: number;
|
||||
private _ppqn: number;
|
||||
clock: any;
|
||||
ctx: AudioContext;
|
||||
logicalTime: number;
|
||||
transportNode: TransportNode | null;
|
||||
private _bpm: number;
|
||||
time_signature: number[];
|
||||
time_position: TimePosition;
|
||||
private _ppqn: number;
|
||||
tick: number;
|
||||
running: boolean;
|
||||
lastPauseTime: number;
|
||||
lastPlayPressTime: number;
|
||||
totalPauseTime: number;
|
||||
timeviewer: HTMLElement;
|
||||
deadline: number;
|
||||
|
||||
constructor(
|
||||
public app: Editor,
|
||||
@ -58,31 +56,59 @@ export class Clock {
|
||||
this.tick = 0;
|
||||
this._bpm = 120;
|
||||
this._ppqn = 48;
|
||||
this.transportNode = null;
|
||||
this.ctx = ctx;
|
||||
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);
|
||||
});
|
||||
this.deadline = 0;
|
||||
this.timeviewer = document.getElementById("timeviewer")!;
|
||||
this.clock = getAudioContext().createClock(
|
||||
this.clockCallback,
|
||||
this.pulse_duration,
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
clockCallback = (time: number, duration: number, tick: number) => {
|
||||
/**
|
||||
* Callback function for the zyklus clock. Updates the clock info and sends a
|
||||
* MIDI clock message if the setting is enabled. Also evaluates the global buffer.
|
||||
*
|
||||
* @param time - precise AudioContext time when the tick should happen
|
||||
* @param duration - seconds between each tick
|
||||
* @param tick - count of the current tick
|
||||
*/
|
||||
let deadline = time - getAudioContext().currentTime;
|
||||
this.deadline = deadline;
|
||||
this.tick = tick;
|
||||
if (this.app.clock.running) {
|
||||
if (this.app.settings.send_clock) {
|
||||
this.app.api.MidiConnection.sendMidiClock();
|
||||
}
|
||||
const futureTimeStamp = this.app.clock.convertTicksToTimeposition(
|
||||
this.app.clock.tick,
|
||||
);
|
||||
this.app.clock.time_position = futureTimeStamp;
|
||||
if (futureTimeStamp.pulse % this.app.clock.ppqn == 0) {
|
||||
this.timeviewer.innerHTML = `${zeroPad(futureTimeStamp.bar, 2)}:${
|
||||
futureTimeStamp.beat + 1
|
||||
} / ${this.app.clock.bpm}`;
|
||||
}
|
||||
if (this.app.exampleIsPlaying) {
|
||||
tryEvaluate(this.app, this.app.example_buffer);
|
||||
} else {
|
||||
tryEvaluate(this.app, this.app.global_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Implement TransportNode clock callback and update clock info with it
|
||||
};
|
||||
|
||||
convertTicksToTimeposition(ticks: number): TimePosition {
|
||||
/**
|
||||
* Converts ticks to a TimePosition object.
|
||||
* @param ticks The number of ticks to convert.
|
||||
* @returns The TimePosition object representing the converted ticks.
|
||||
* Converts ticks to a time position.
|
||||
*
|
||||
* @param ticks - ticks to convert
|
||||
* @returns TimePosition
|
||||
*/
|
||||
|
||||
const beatsPerBar = this.app.clock.time_signature[0];
|
||||
const ppqnPosition = ticks % this.app.clock.ppqn;
|
||||
const beatNumber = Math.floor(ticks / this.app.clock.ppqn);
|
||||
@ -93,10 +119,9 @@ export class Clock {
|
||||
|
||||
get ticks_before_new_bar(): number {
|
||||
/**
|
||||
* This function returns the number of ticks separating the current moment
|
||||
* from the beginning of the next bar.
|
||||
* Calculates the number of ticks before the next bar.
|
||||
*
|
||||
* @returns number of ticks until next bar
|
||||
* @returns number - ticks before the next bar
|
||||
*/
|
||||
const ticskMissingFromBeat = this.ppqn - this.time_position.pulse;
|
||||
const beatsMissingFromBar = this.beats_per_bar - this.time_position.beat;
|
||||
@ -105,10 +130,9 @@ export class Clock {
|
||||
|
||||
get next_beat_in_ticks(): number {
|
||||
/**
|
||||
* This function returns the number of ticks separating the current moment
|
||||
* from the beginning of the next beat.
|
||||
* Calculates the number of ticks before the next beat.
|
||||
*
|
||||
* @returns number of ticks until next beat
|
||||
* @returns number - ticks before the next beat
|
||||
*/
|
||||
return this.app.clock.pulses_since_origin + this.time_position.pulse;
|
||||
}
|
||||
@ -116,6 +140,8 @@ export class Clock {
|
||||
get beats_per_bar(): number {
|
||||
/**
|
||||
* Returns the number of beats per bar.
|
||||
*
|
||||
* @returns number - beats per bar
|
||||
*/
|
||||
return this.time_signature[0];
|
||||
}
|
||||
@ -124,7 +150,7 @@ export class Clock {
|
||||
/**
|
||||
* Returns the number of beats since the origin.
|
||||
*
|
||||
* @returns number of beats since origin
|
||||
* @returns number - beats since the origin
|
||||
*/
|
||||
return Math.floor(this.tick / this.ppqn);
|
||||
}
|
||||
@ -133,7 +159,7 @@ export class Clock {
|
||||
/**
|
||||
* Returns the number of pulses since the origin.
|
||||
*
|
||||
* @returns number of pulses since origin
|
||||
* @returns number - pulses since the origin
|
||||
*/
|
||||
return this.tick;
|
||||
}
|
||||
@ -141,119 +167,112 @@ export class Clock {
|
||||
get pulse_duration(): number {
|
||||
/**
|
||||
* Returns the duration of a pulse in seconds.
|
||||
* @returns number - duration of a pulse in seconds
|
||||
*/
|
||||
return 60 / this.bpm / this.ppqn;
|
||||
}
|
||||
|
||||
public pulse_duration_at_bpm(bpm: number = this.bpm): number {
|
||||
/**
|
||||
* Returns the duration of a pulse in seconds at a specific bpm.
|
||||
* Returns the duration of a pulse in seconds at a given bpm.
|
||||
*
|
||||
* @param bpm - bpm to calculate the pulse duration for
|
||||
* @returns number - duration of a pulse in seconds
|
||||
*/
|
||||
return 60 / bpm / this.ppqn;
|
||||
}
|
||||
|
||||
get bpm(): number {
|
||||
/**
|
||||
* Returns the current bpm.
|
||||
* @returns number - current bpm
|
||||
*/
|
||||
return this._bpm;
|
||||
}
|
||||
|
||||
set nudge(nudge: number) {
|
||||
this.transportNode?.setNudge(nudge);
|
||||
get tickDuration(): number {
|
||||
/**
|
||||
* Returns the duration of a tick in seconds.
|
||||
* @returns number - duration of a tick in seconds
|
||||
*/
|
||||
return 1 / this.ppqn;
|
||||
}
|
||||
|
||||
set bpm(bpm: number) {
|
||||
/**
|
||||
* Sets the bpm.
|
||||
* @param bpm - bpm to set
|
||||
*/
|
||||
if (bpm > 0 && this._bpm !== bpm) {
|
||||
this.transportNode?.setBPM(bpm);
|
||||
this._bpm = bpm;
|
||||
this.logicalTime = this.realTime;
|
||||
this.clock.setDuration(() => (this.tickDuration * 60) / this.bpm);
|
||||
}
|
||||
}
|
||||
|
||||
get ppqn(): number {
|
||||
/**
|
||||
* Returns the current ppqn.
|
||||
* @returns number - current ppqn
|
||||
*/
|
||||
return this._ppqn;
|
||||
}
|
||||
|
||||
get realTime(): number {
|
||||
return this.app.audioContext.currentTime - this.totalPauseTime;
|
||||
}
|
||||
|
||||
get deviation(): number {
|
||||
return Math.abs(this.logicalTime - this.realTime);
|
||||
}
|
||||
|
||||
set ppqn(ppqn: number) {
|
||||
/**
|
||||
* Sets the ppqn.
|
||||
* @param ppqn - ppqn to set
|
||||
* @returns number - current ppqn
|
||||
*/
|
||||
if (ppqn > 0 && 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 {
|
||||
/**
|
||||
* Compute the time remaining before the next clock tick.
|
||||
* @param time - audio context currentTime
|
||||
* @param nudge - nudge in the future (in seconds)
|
||||
* @returns remainingTime
|
||||
*/
|
||||
const pulseDuration = this.pulse_duration;
|
||||
const nudgedTime = time + nudge;
|
||||
const nextTickTime = Math.ceil(nudgedTime / pulseDuration) * pulseDuration;
|
||||
const remainingTime = nextTickTime - nudgedTime;
|
||||
|
||||
return remainingTime;
|
||||
}
|
||||
|
||||
public convertPulseToSecond(n: number): number {
|
||||
/**
|
||||
* Converts a pulse to a second.
|
||||
*/
|
||||
return n * this.pulse_duration;
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
/**
|
||||
* Starts the TransportNode (starts the clock).
|
||||
* Start the clock
|
||||
*
|
||||
* @remark also sends a MIDI message if a port is declared
|
||||
*/
|
||||
this.app.audioContext.resume();
|
||||
this.running = true;
|
||||
this.app.api.MidiConnection.sendStartMessage();
|
||||
this.lastPlayPressTime = this.app.audioContext.currentTime;
|
||||
this.totalPauseTime += this.lastPlayPressTime - this.lastPauseTime;
|
||||
this.transportNode?.start();
|
||||
this.clock.start();
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
/**
|
||||
* Pauses the TransportNode (pauses the clock).
|
||||
* Pause the clock.
|
||||
*
|
||||
* @remark also sends a MIDI message if a port is declared
|
||||
*/
|
||||
this.running = false;
|
||||
this.transportNode?.pause();
|
||||
this.app.api.MidiConnection.sendStopMessage();
|
||||
this.lastPauseTime = this.app.audioContext.currentTime;
|
||||
this.logicalTime = this.realTime;
|
||||
this.clock.pause();
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
/**
|
||||
* Stops the TransportNode (stops the clock).
|
||||
* Stops the clock.
|
||||
*
|
||||
* @remark also sends a MIDI message if a port is declared
|
||||
*/
|
||||
this.running = false;
|
||||
this.tick = 0;
|
||||
this.lastPauseTime = this.app.audioContext.currentTime;
|
||||
this.logicalTime = this.realTime;
|
||||
this.time_position = { bar: 0, beat: 0, pulse: 0 };
|
||||
this.app.api.MidiConnection.sendStopMessage();
|
||||
this.transportNode?.stop();
|
||||
this.clock.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { type Editor } from "./main";
|
||||
// Basics
|
||||
import { introduction } from "./documentation/basics/welcome";
|
||||
import { loading_samples } from "./documentation/learning/samples/loading_samples";
|
||||
import { amplitude } from "./documentation/learning/audio_engine/amplitude";
|
||||
import { pitch } from "./documentation/learning/audio_engine/pitch";
|
||||
import { effects } from "./documentation/learning/audio_engine/effects";
|
||||
import { sampler } from "./documentation/learning/audio_engine/sampler";
|
||||
import { sample_banks } from "./documentation/learning/samples/sample_banks";
|
||||
import { audio_basics } from "./documentation/learning/audio_engine/audio_basics";
|
||||
import { sample_list } from "./documentation/learning/samples/sample_list";
|
||||
import { loading_samples } from "./documentation/samples/loading_samples";
|
||||
import { amplitude } from "./documentation/audio_engine/amplitude";
|
||||
import { reverb } from "./documentation/audio_engine/reverb_delay";
|
||||
import { sampler } from "./documentation/audio_engine/sampler";
|
||||
import { sample_banks } from "./documentation/samples/sample_banks";
|
||||
import { audio_basics } from "./documentation/audio_engine/audio_basics";
|
||||
import { sample_list } from "./documentation/samples/sample_list";
|
||||
import { software_interface } from "./documentation/basics/interface";
|
||||
import { shortcuts } from "./documentation/basics/keyboard";
|
||||
import { code } from "./documentation/basics/code";
|
||||
@ -18,38 +17,31 @@ import { oscilloscope } from "./documentation/more/oscilloscope";
|
||||
import { synchronisation } from "./documentation/more/synchronisation";
|
||||
import { about } from "./documentation/more/about";
|
||||
import { bonus } from "./documentation/more/bonus";
|
||||
import { visualization } from "./documentation/more/visualization";
|
||||
import { chaining } from "./documentation/patterns/chaining";
|
||||
import { interaction } from "./documentation/basics/interaction";
|
||||
import { time } from "./documentation/learning/time/time";
|
||||
import { linear_time } from "./documentation/learning/time/linear_time";
|
||||
import { cyclical_time } from "./documentation/learning/time/cyclical_time";
|
||||
import { long_forms } from "./documentation/learning/time/long_forms";
|
||||
import { midi } from "./documentation/learning/midi";
|
||||
import { osc } from "./documentation/learning/osc";
|
||||
import { patterns } from "./documentation/patterns/patterns";
|
||||
import { functions } from "./documentation/patterns/functions";
|
||||
import { generators } from "./documentation/patterns/generators";
|
||||
import { variables } from "./documentation/patterns/variables";
|
||||
import { probabilities } from "./documentation/patterns/probabilities";
|
||||
import { lfos } from "./documentation/patterns/lfos";
|
||||
import { ziffers_basics } from "./documentation/patterns/ziffers/ziffers_basics";
|
||||
import { ziffers_scales } from "./documentation/patterns/ziffers/ziffers_scales";
|
||||
import { ziffers_rhythm } from "./documentation/patterns/ziffers/ziffers_rhythm";
|
||||
import { ziffers_algorithmic } from "./documentation/patterns/ziffers/ziffers_algorithmic";
|
||||
import { ziffers_tonnetz } from "./documentation/patterns/ziffers/ziffers_tonnetz";
|
||||
import { ziffers_syncing } from "./documentation/patterns/ziffers/ziffers_syncing";
|
||||
import { synths } from "./documentation/learning/audio_engine/synths";
|
||||
import { chaining } from "./documentation/chaining";
|
||||
import { interaction } from "./documentation/interaction";
|
||||
import { time } from "./documentation/time/time";
|
||||
import { linear_time } from "./documentation/time/linear_time";
|
||||
import { cyclical_time } from "./documentation/time/cyclical_time";
|
||||
import { long_forms } from "./documentation/long_forms";
|
||||
import { midi } from "./documentation/midi";
|
||||
import { osc } from "./documentation/osc";
|
||||
import { sound } from "./documentation/engine";
|
||||
import { patterns } from "./documentation/patterns";
|
||||
import { functions } from "./documentation/functions";
|
||||
import { variables } from "./documentation/variables";
|
||||
import { probabilities } from "./documentation/probabilities";
|
||||
import { lfos } from "./documentation/lfos";
|
||||
import { ziffers } from "./documentation/ziffers";
|
||||
import { synths } from "./documentation/synths";
|
||||
|
||||
// Setting up the Markdown converter with syntax highlighting
|
||||
import showdown from "showdown";
|
||||
import showdownHighlight from "showdown-highlight";
|
||||
import "highlight.js/styles/atom-one-dark-reasonable.min.css";
|
||||
import { createDocumentationStyle } from "./DomElements";
|
||||
import { filters } from "./documentation/learning/audio_engine/filters";
|
||||
showdown.setFlavor("github");
|
||||
|
||||
export const key_shortcut = (shortcut: string): string => {
|
||||
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-brightwhite bg-brightblack border border-black rounded-lg">${shortcut}</kbd>`;
|
||||
return `<kbd class="lg:px-2 lg:py-1.5 px-1 py-1 lg:text-sm text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg dark:bg-gray-600 dark:text-gray-100 dark:border-gray-500">${shortcut}</kbd>`;
|
||||
};
|
||||
|
||||
export const makeExampleFactory = (application: Editor): Function => {
|
||||
@ -65,58 +57,19 @@ export const makeExampleFactory = (application: Editor): Function => {
|
||||
return `
|
||||
<details ${open ? "open" : ""}>
|
||||
<summary >${description}
|
||||
<button class="ml-4 py-1 align-top text-base px-4 hover:bg-brightgreen bg-green inline-block text-selection_foreground" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
|
||||
<button class="py-1 text-base px-4 hover:brightyellow bg-yellow text-selection_foreground inline-block" onclick="app.api._stopDocExample()">⏸️ Pause</button>
|
||||
<button class="py-1 text-base px-4 hover:bg-brightmagenta bg-magenta text-selection_foreground inline-block" onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
|
||||
<button class="ml-4 py-1 align-top text-base px-4 hover:bg-green-700 bg-emerald-600 inline-block" onclick="app.api._playDocExample(app.api.codeExamples['${codeId}'])">▶️ Play</button>
|
||||
<button class="py-1 text-base px-4 hover:bg-neutral-600 bg-neutral-500 inline-block " onclick="app.api._stopDocExample()">⏸️ Pause</button>
|
||||
<button class="py-1 text-base px-4 hover:bg-neutral-900 bg-neutral-800 inline-block " onclick="navigator.clipboard.writeText(app.api.codeExamples['${codeId}'])">📎 Copy</button>
|
||||
</summary>
|
||||
<pre><code class="hljs language-javascript">${code.trim()}</code></pre>
|
||||
\`\`\`javascript
|
||||
${code}
|
||||
\`\`\`
|
||||
</details>
|
||||
`;
|
||||
};
|
||||
return make_example;
|
||||
};
|
||||
|
||||
export const documentation_pages = [
|
||||
"introduction",
|
||||
"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) => {
|
||||
/**
|
||||
* Creates the documentation for the given application.
|
||||
@ -134,34 +87,26 @@ export const documentation_factory = (application: Editor) => {
|
||||
linear: linear_time(application),
|
||||
cyclic: cyclical_time(application),
|
||||
longform: long_forms(application),
|
||||
sound: sound(application),
|
||||
synths: synths(application),
|
||||
filters: filters(application),
|
||||
chaining: chaining(application),
|
||||
patterns: patterns(application),
|
||||
ziffers_basics: ziffers_basics(application),
|
||||
ziffers_scales: ziffers_scales(application),
|
||||
ziffers_algorithmic: ziffers_algorithmic(application),
|
||||
ziffers_rhythm: ziffers_rhythm(application),
|
||||
ziffers_tonnetz: ziffers_tonnetz(application),
|
||||
ziffers_syncing: ziffers_syncing(application),
|
||||
ziffers: ziffers(application),
|
||||
midi: midi(application),
|
||||
osc: osc(application),
|
||||
lfos: lfos(application),
|
||||
variables: variables(application),
|
||||
probabilities: probabilities(application),
|
||||
functions: functions(application),
|
||||
generators: generators(application),
|
||||
shortcuts: shortcuts(application),
|
||||
amplitude: amplitude(application),
|
||||
pitch: pitch(application),
|
||||
effects: effects(application),
|
||||
reverb_delay: reverb(application),
|
||||
sampler: sampler(application),
|
||||
mouse: mouse(application),
|
||||
oscilloscope: oscilloscope(application),
|
||||
audio_basics: audio_basics(application),
|
||||
synchronisation: synchronisation(application),
|
||||
bonus: bonus(application),
|
||||
visualization: visualization(application),
|
||||
sample_list: sample_list(application),
|
||||
sample_banks: sample_banks(application),
|
||||
loading_samples: loading_samples(application),
|
||||
@ -183,17 +128,13 @@ export const showDocumentation = (app: Editor) => {
|
||||
document.getElementById("documentation")?.classList.remove("hidden");
|
||||
// Load and convert Markdown content from the documentation file
|
||||
let style = createDocumentationStyle(app);
|
||||
|
||||
function update_and_assign(callback: Function) {
|
||||
let bindings = Object.keys(style).map((key) => ({
|
||||
type: "output",
|
||||
regex: new RegExp(`<${key}([^>]*)>`, "g"),
|
||||
//@ts-ignore
|
||||
replace: (match, p1) => `<${key} class="${style[key]}" ${p1}>`,
|
||||
}));
|
||||
callback(bindings)
|
||||
}
|
||||
update_and_assign((e: Object) => updateDocumentationContent(app, e));
|
||||
let bindings = Object.keys(style).map((key) => ({
|
||||
type: "output",
|
||||
regex: new RegExp(`<${key}([^>]*)>`, "g"),
|
||||
//@ts-ignore
|
||||
replace: (match, p1) => `<${key} class="${style[key]}" ${p1}>`,
|
||||
}));
|
||||
updateDocumentationContent(app, bindings);
|
||||
}
|
||||
};
|
||||
|
||||
@ -214,34 +155,15 @@ export const updateDocumentationContent = (app: Editor, bindings: any) => {
|
||||
* @param app - The editor application.
|
||||
* @param bindings - Additional bindings for the showdown converter.
|
||||
*/
|
||||
let loading_message: string = "<h1 class='border-4 py-2 px-2 mx-48 mt-48 text-center text-2xl text-brightwhite'>Loading! <b class='text-red'>Clic to refresh!</b></h1>";
|
||||
const converter = new showdown.Converter({
|
||||
emoji: true,
|
||||
moreStyling: true,
|
||||
backslashEscapesHTMLTags: true,
|
||||
extensions: [showdownHighlight({
|
||||
pre: true,
|
||||
auto_detection: false
|
||||
}), ...bindings],
|
||||
extensions: [showdownHighlight({ auto_detection: true }), ...bindings],
|
||||
});
|
||||
|
||||
if (Object.keys(app.docs).length === 0) {
|
||||
app.docs = documentation_factory(app);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
const converted_markdown = converter.makeHtml(
|
||||
app.docs[app.currentDocumentationPane],
|
||||
);
|
||||
document.getElementById("documentation-content")!.innerHTML =
|
||||
converted_markdown;
|
||||
};
|
||||
|
||||
@ -18,8 +18,6 @@ export const singleElements = {
|
||||
load_universe_button: "load-universe-button",
|
||||
download_universe_button: "download-universes",
|
||||
upload_universe_button: "upload-universes",
|
||||
upload_samples_button: "upload-samples",
|
||||
sample_indicator: "sample-indicator",
|
||||
destroy_universes_button: "destroy-universes",
|
||||
documentation_button: "doc-button-1",
|
||||
eval_button: "eval-button-1",
|
||||
@ -45,8 +43,6 @@ export const singleElements = {
|
||||
midi_clock_checkbox: "send-midi-clock",
|
||||
midi_channels_scripts: "midi-channels-scripts",
|
||||
midi_clock_ppqn: "midi-clock-ppqn-input",
|
||||
theme_selector: "theme-selector",
|
||||
theme_previewer: "theme-previewer",
|
||||
load_demo_songs: "load-demo-songs",
|
||||
normal_mode_button: "normal-mode",
|
||||
vim_mode_button: "vim-mode",
|
||||
@ -56,7 +52,6 @@ export const singleElements = {
|
||||
error_line: "error_line",
|
||||
hydra_canvas: "hydra-bg",
|
||||
feedback: "feedback",
|
||||
drawings: "drawings",
|
||||
scope: "scope",
|
||||
};
|
||||
|
||||
@ -75,30 +70,30 @@ export const createDocumentationStyle = (app: Editor) => {
|
||||
*/
|
||||
|
||||
return {
|
||||
h1: "text-brightwhite lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-4 pt-4 pb-3 px-2",
|
||||
h2: "text-brightwhite lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-2 pt-12 pb-3 px-2",
|
||||
h3: "text-brightwhite lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 border-l-2 border-b-2 lg:mb-4 mb-4 pb-2 px-2 lg:mt-16",
|
||||
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-4 pt-4 pb-3 px-2",
|
||||
h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 border-b-2 pt-12 pb-3 px-2",
|
||||
h3: "text-white lg:text-2xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 border-l-2 border-b-2 lg:mb-4 mb-4 pb-2 px-2 lg:mt-16",
|
||||
ul: "text-underline ml-12",
|
||||
li: "list-disc lg:text-2xl text-base text-white lg:mx-4 mx-2 my-4 my-2 leading-normal",
|
||||
p: "lg:text-2xl text-base text-white lg:mx-6 mx-2 my-4 leading-normal",
|
||||
warning:
|
||||
"animate-pulse lg:text-2xl font-bold text-brightred lg:mx-6 mx-2 my-4 leading-normal",
|
||||
a: "lg:text-2xl text-base text-brightred",
|
||||
"animate-pulse lg:text-2xl font-bold text-rose-600 lg:mx-6 mx-2 my-4 leading-normal",
|
||||
a: "lg:text-2xl text-base text-orange-300",
|
||||
code: `lg:my-4 sm:my-1 text-base lg:text-xl block whitespace-pre overflow-x-hidden`,
|
||||
icode:
|
||||
"lg:my-1 my-1 lg:text-xl sm:text-xs text-brightwhite font-mono bg-brightblack",
|
||||
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-brightwhite font-mono bg-brightblack",
|
||||
blockquote: "text-brightwhite border-l-4 border-white pl-4 my-4 mx-4",
|
||||
"lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
|
||||
ic: "lg:my-1 my-1 lg:text-xl sm:text-xs text-white font-mono bg-neutral-600",
|
||||
blockquote: "text-neutral-200 border-l-4 border-neutral-500 pl-4 my-4 mx-4",
|
||||
details:
|
||||
"lg:mx-20 py-2 px-6 lg:text-2xl text-white border-l-8 box-border bg-selection_foreground",
|
||||
"lg:mx-20 py-2 px-6 lg:text-2xl text-white border-l-8 box-border bg-neutral-900",
|
||||
summary: "font-semibold text-xl",
|
||||
table:
|
||||
"justify-center lg:my-12 my-2 lg:mx-12 mx-2 lg:text-2xl text-base w-full text-left text-white border-collapse",
|
||||
thead:
|
||||
"text-xs text-gray-700 uppercase",
|
||||
"text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400",
|
||||
th: "",
|
||||
td: "",
|
||||
tr: "",
|
||||
box: "border bg-red",
|
||||
box: "border bg-red-500",
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Prec } from "@codemirror/state";
|
||||
import { indentWithTab } from "@codemirror/commands";
|
||||
import { tags as t } from "@lezer/highlight";
|
||||
import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
@ -8,6 +7,8 @@ import {
|
||||
drawSelection,
|
||||
highlightActiveLine,
|
||||
dropCursor,
|
||||
// rectangularSelection,
|
||||
// crosshairCursor,
|
||||
highlightActiveLineGutter,
|
||||
} from "@codemirror/view";
|
||||
import { Extension, EditorState } from "@codemirror/state";
|
||||
@ -17,10 +18,9 @@ import {
|
||||
syntaxHighlighting,
|
||||
indentOnInput,
|
||||
bracketMatching,
|
||||
HighlightStyle,
|
||||
} from "@codemirror/language";
|
||||
import { defaultKeymap, historyKeymap, history } from "@codemirror/commands";
|
||||
import { highlightSelectionMatches } from "@codemirror/search";
|
||||
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
|
||||
import {
|
||||
autocompletion,
|
||||
closeBrackets,
|
||||
@ -30,232 +30,12 @@ import { lintKeymap } from "@codemirror/lint";
|
||||
import { Compartment } from "@codemirror/state";
|
||||
import { Editor } from "./main";
|
||||
import { EditorView } from "codemirror";
|
||||
import { toposTheme } from "./themes/toposTheme";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { inlineHoveringTips } from "./documentation/inlineHelp";
|
||||
import { toposCompletions, soundCompletions } from "./documentation/inlineHelp";
|
||||
import { javascriptLanguage } from "@codemirror/lang-javascript";
|
||||
|
||||
export const getCodeMirrorTheme = (theme: { [key: string]: string }): Extension => {
|
||||
// @ts-ignore
|
||||
const black = theme["black"],
|
||||
red = theme["red"],
|
||||
green = theme["green"],
|
||||
yellow = theme["yellow"],
|
||||
blue = theme["blue"],
|
||||
magenta = theme["magenta"],
|
||||
cyan = theme["cyan"],
|
||||
white = theme["white"],
|
||||
// @ts-ignore
|
||||
brightblack = theme["brightblack"],
|
||||
// @ts-ignore
|
||||
brightred = theme["brightred"],
|
||||
brightgreen = theme["brightgreen"],
|
||||
// @ts-ignore
|
||||
brightyellow = theme["brightyellow"],
|
||||
// @ts-ignore
|
||||
brightblue = theme["brightblue"],
|
||||
// @ts-ignore
|
||||
brightmagenta = theme["brightmagenta"],
|
||||
// @ts-ignore
|
||||
brightcyan = theme["brightcyan"],
|
||||
brightwhite = theme["brightwhite"],
|
||||
background = theme["background"],
|
||||
selection_foreground = theme["selection_foreground"],
|
||||
cursor = theme["cursor"],
|
||||
foreground = theme["foreground"],
|
||||
selection_background = theme["selection_background"];
|
||||
const toposTheme = EditorView.theme({
|
||||
"&": {
|
||||
color: background,
|
||||
backgroundColor: "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({
|
||||
autocomplete: toposCompletions,
|
||||
});
|
||||
@ -279,7 +59,7 @@ export const editorSetup: Extension = (() => [
|
||||
highlightActiveLine(),
|
||||
highlightSelectionMatches(),
|
||||
keymap.of([
|
||||
// ...searchKeymap,
|
||||
...searchKeymap,
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
@ -290,7 +70,6 @@ export const editorSetup: Extension = (() => [
|
||||
export const installEditor = (app: Editor) => {
|
||||
app.vimModeCompartment = new Compartment();
|
||||
app.hoveringCompartment = new Compartment();
|
||||
app.themeCompartment = new Compartment();
|
||||
app.completionsCompartment = new Compartment();
|
||||
app.withLineNumbers = new Compartment();
|
||||
app.chosenLanguage = new Compartment();
|
||||
@ -320,10 +99,7 @@ export const installEditor = (app: Editor) => {
|
||||
app.settings.completions ? [jsCompletions, toposSoundCompletions] : [],
|
||||
),
|
||||
editorSetup,
|
||||
app.themeCompartment.of(
|
||||
getCodeMirrorTheme(app.getColorScheme("Tomorrow Night Burns")),
|
||||
// debug
|
||||
),
|
||||
toposTheme,
|
||||
app.chosenLanguage.of(javascript()),
|
||||
];
|
||||
app.dynamicPlugins = new Compartment();
|
||||
|
||||
@ -81,7 +81,6 @@ export const tryEvaluate = async (
|
||||
);
|
||||
addFunctionToCache(candidateCode, newFunction);
|
||||
} else {
|
||||
application.api.logOnce("Compilation error!");
|
||||
await evaluate(application, code, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +136,7 @@ export class AppSettings {
|
||||
*/
|
||||
|
||||
public vimMode: boolean = false;
|
||||
public theme: string = "Everblush";
|
||||
public theme: string = "toposTheme";
|
||||
public font: string = "IBM Plex Mono";
|
||||
public font_size: number = 24;
|
||||
public universes: Universes;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
};
|
||||
@ -1,10 +1,8 @@
|
||||
import { EditorView } from "codemirror";
|
||||
import { vim } from "@replit/codemirror-vim";
|
||||
import { type Editor } from "./main";
|
||||
import colors from "./colors.json";
|
||||
import {
|
||||
documentation_factory,
|
||||
documentation_pages,
|
||||
hideDocumentation,
|
||||
showDocumentation,
|
||||
updateDocumentationContent,
|
||||
@ -24,11 +22,18 @@ import { tryEvaluate } from "./Evaluator";
|
||||
import { inlineHoveringTips } from "./documentation/inlineHelp";
|
||||
import { lineNumbers } from "@codemirror/view";
|
||||
import { jsCompletions } from "./EditorSetup";
|
||||
import { createDocumentationStyle } from "./DomElements";
|
||||
import { saveState } from "./WindowBehavior";
|
||||
import { registerSamplesFromDB, samplesDBConfig, uploadSamplesToDB } from "./IO/SampleLoading";
|
||||
|
||||
export const installInterfaceLogic = (app: Editor) => {
|
||||
// Initialize style
|
||||
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.settings.line_numbers;
|
||||
@ -52,11 +57,9 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
tabs[i].addEventListener("click", (event) => {
|
||||
// Updating the CSS accordingly
|
||||
tabs[i].classList.add("bg-foreground");
|
||||
tabs[i].classList.add("text-selection_foreground");
|
||||
tabs[i].classList.add("bg-orange-300");
|
||||
for (let j = 0; j < tabs.length; j++) {
|
||||
if (j != i) tabs[j].classList.remove("bg-foreground");
|
||||
if (j != i) tabs[j].classList.remove("text-selection_foreground");
|
||||
if (j != i) tabs[j].classList.remove("bg-orange-300");
|
||||
}
|
||||
app.currentFile().candidate = app.view.state.doc.toString();
|
||||
|
||||
@ -153,21 +156,6 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
);
|
||||
});
|
||||
|
||||
app.interface.upload_samples_button.addEventListener("input", async (event) => {
|
||||
let fileInput = event.target as HTMLInputElement;
|
||||
if (!fileInput.files?.length) {
|
||||
return;
|
||||
}
|
||||
app.interface.sample_indicator.innerText = "Loading...";
|
||||
app.interface.sample_indicator.classList.add("animate-pulse");
|
||||
await uploadSamplesToDB(samplesDBConfig, fileInput.files).then(() => {
|
||||
registerSamplesFromDB(samplesDBConfig, () => {
|
||||
app.interface.sample_indicator.innerText = "Import samples";
|
||||
app.interface.sample_indicator.classList.remove("animate-pulse");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.interface.upload_universe_button.addEventListener("click", () => {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
@ -301,18 +289,6 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.interface.theme_selector.addEventListener("change", () => {
|
||||
app.settings.theme = (app.interface.theme_selector as HTMLSelectElement).value;
|
||||
app.readTheme(app.settings.theme);
|
||||
// @ts-ignore
|
||||
let selected_theme = colors[app.settings.theme as string];
|
||||
let theme_preview = "";
|
||||
for (const [key, _] of Object.entries(selected_theme)) {
|
||||
theme_preview += `<p class="inline text-${key} bg-${key}">█</div>`;
|
||||
}
|
||||
app.interface.theme_previewer.innerHTML = theme_preview;
|
||||
});
|
||||
|
||||
app.interface.settings_button.addEventListener("click", () => {
|
||||
// Populate the font selector
|
||||
const fontFamilySelect = document.getElementById(
|
||||
@ -322,26 +298,6 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
fontFamilySelect.value = app.settings.font;
|
||||
}
|
||||
|
||||
app.interface.theme_selector.innerHTML = "";
|
||||
let all_themes = Object.keys(colors);
|
||||
all_themes.sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
app.interface.theme_selector.innerHTML = all_themes.map((color) => {
|
||||
return `<option value="${color}">${color}</option>`
|
||||
}).join("");
|
||||
// Set the selected theme in the selector to app.settings.theme
|
||||
// @ts-ignore
|
||||
app.interface.theme_selector.value = app.settings.theme;
|
||||
|
||||
// @ts-ignore
|
||||
let selected_theme = colors[app.settings.theme as string];
|
||||
let theme_preview = "<div class='ml-6'>";
|
||||
for (const [key, _] of Object.entries(selected_theme)) {
|
||||
theme_preview += `<p class="inline text-${key} bg-${key}">█</p>`;
|
||||
}
|
||||
theme_preview += "</div>";
|
||||
app.interface.theme_previewer.innerHTML = theme_preview;
|
||||
// Populate the font family selector
|
||||
const doughNudgeRange = app.interface.dough_nudge_range as HTMLInputElement;
|
||||
doughNudgeRange.value = app.dough_nudge.toString();
|
||||
@ -530,29 +486,51 @@ export const installInterfaceLogic = (app: Editor) => {
|
||||
|
||||
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",
|
||||
"synths",
|
||||
"chaining",
|
||||
"patterns",
|
||||
"ziffers",
|
||||
"midi",
|
||||
"osc",
|
||||
"functions",
|
||||
"lfos",
|
||||
"probabilities",
|
||||
"variables",
|
||||
"synchronisation",
|
||||
"mouse",
|
||||
"shortcuts",
|
||||
"about",
|
||||
"bonus",
|
||||
"oscilloscope",
|
||||
"sample_list",
|
||||
"loading_samples",
|
||||
].forEach((e) => {
|
||||
let name = `docs_` + e;
|
||||
|
||||
// Check if the element exists
|
||||
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);
|
||||
document.getElementById(name)!.addEventListener("click", async () => {
|
||||
if (name !== "docs_sample_list") {
|
||||
app.currentDocumentationPane = e;
|
||||
if (name !== "docs_sample_list") {
|
||||
updateDocumentationContent(app, app.bindings);
|
||||
} else {
|
||||
console.log("Loading samples!");
|
||||
await loadSamples().then(() => {
|
||||
updateDocumentationContent(app, app.bindings);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log("Could not find element " + name);
|
||||
}
|
||||
updateDocumentationContent(app, bindings);
|
||||
} else {
|
||||
console.log("Loading samples!");
|
||||
await loadSamples().then(() => {
|
||||
app.docs = documentation_factory(app);
|
||||
app.currentDocumentationPane = e;
|
||||
updateDocumentationContent(app, bindings);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@ -105,18 +105,6 @@ export const registerOnKeyDown = (app: Editor) => {
|
||||
if (event.key === "Enter" && event.shiftKey && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
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());
|
||||
app.flashBackground("#404040", 200);
|
||||
}
|
||||
|
||||
@ -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" });
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -69,15 +69,6 @@ export function arrayOfObjectsToObjectWithArrays<T extends 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[],
|
||||
@ -94,17 +85,3 @@ export function filterObject(
|
||||
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]';
|
||||
@ -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();
|
||||
};
|
||||
@ -44,9 +44,6 @@ export const installWindowBehaviors = (
|
||||
window.addEventListener("resize", () =>
|
||||
handleResize(app.interface.feedback as HTMLCanvasElement),
|
||||
);
|
||||
window.addEventListener("resize", () =>
|
||||
handleResize(app.interface.drawings as HTMLCanvasElement),
|
||||
);
|
||||
window.addEventListener("beforeunload", (event) => {
|
||||
event.preventDefault();
|
||||
saveBeforeExit(app);
|
||||
|
||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 15 KiB |
@ -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 |
|
Before Width: | Height: | Size: 21 KiB |
@ -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 |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 4.9 MiB |
@ -7,9 +7,6 @@ import {
|
||||
safeScale,
|
||||
} from "zifferjs";
|
||||
import { SkipEvent } from "./SkipEvent";
|
||||
import { SoundParams } from "./SoundEvent";
|
||||
import { centsToSemitones, edoToSemitones, ratiosToSemitones } from "zifferjs/src/scale";
|
||||
import { safeMod } from "zifferjs/src/utils";
|
||||
|
||||
export type EventOperation<T> = (instance: T, ...args: any[]) => void;
|
||||
|
||||
@ -211,14 +208,9 @@ export class AbstractEvent {
|
||||
* @param func - The function to be applied to the Event
|
||||
* @returns The transformed Event
|
||||
*/
|
||||
return this.modify(func).update();
|
||||
return this.modify(func);
|
||||
};
|
||||
|
||||
mod = (value: number): AbstractEvent => {
|
||||
this.values.originalPitch = safeMod(this.values.originalPitch, value);
|
||||
return this.update();
|
||||
}
|
||||
|
||||
noteLength = (
|
||||
value: number | number[],
|
||||
...kwargs: number[]
|
||||
@ -230,60 +222,18 @@ export class AbstractEvent {
|
||||
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
this.values["noteLength"] = value;
|
||||
this.values.dur = value.map((v) =>
|
||||
this.app.clock.convertPulseToSecond(v * 4 * this.app.clock.ppqn),
|
||||
);
|
||||
} else {
|
||||
this.values["noteLength"] = value;
|
||||
this.values.dur = this.app.clock.convertPulseToSecond(
|
||||
value * 4 * this.app.clock.ppqn,
|
||||
);
|
||||
}
|
||||
if(this.current) {
|
||||
value = Array.isArray(value) ? value[this.index%value.length] : value;
|
||||
this.current.duration = value;
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
protected processSound = (
|
||||
sound: string | string[] | SoundParams | SoundParams[],
|
||||
): SoundParams => {
|
||||
if (Array.isArray(sound) && typeof sound[0] === "string") {
|
||||
const s: string[] = [];
|
||||
const n: number[] = [];
|
||||
sound.forEach((str) => {
|
||||
const parts = (str as string).split(":");
|
||||
s.push(parts[0]);
|
||||
if (parts[1]) {
|
||||
n.push(parseInt(parts[1]));
|
||||
}
|
||||
});
|
||||
return {
|
||||
s,
|
||||
n: n.length > 0 ? n : undefined,
|
||||
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
|
||||
};
|
||||
} else if (typeof sound === "object") {
|
||||
const validatedObj: SoundParams = {
|
||||
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
|
||||
...(sound as Partial<SoundParams>),
|
||||
};
|
||||
return validatedObj;
|
||||
} else {
|
||||
if (sound.includes(":")) {
|
||||
const vals = sound.split(":");
|
||||
const s = vals[0];
|
||||
const n = parseInt(vals[1]);
|
||||
return {
|
||||
s,
|
||||
n,
|
||||
dur: this.app.clock.convertPulseToSecond(this.app.clock.ppqn),
|
||||
};
|
||||
} else {
|
||||
return { s: sound, dur: 0.5 };
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export abstract class AudibleEvent extends AbstractEvent {
|
||||
@ -301,9 +251,8 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
|
||||
}
|
||||
this.values["pitch"] = value;
|
||||
this.values["originalPitch"] = value;
|
||||
this.defaultPitchKeyScale();
|
||||
return this.update();
|
||||
if (this.values.key && this.values.parsedScale) this.update();
|
||||
return this;
|
||||
};
|
||||
|
||||
pc = this.pitch;
|
||||
@ -317,15 +266,13 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
if (kwargs.length > 0) {
|
||||
value = Array.isArray(value) ? value.concat(kwargs) : [value, ...kwargs];
|
||||
}
|
||||
this.values["paramOctave"] = value;
|
||||
this.values["octave"] = value;
|
||||
if (
|
||||
this.values.key &&
|
||||
(this.values.pitch || this.values.pitch === 0) &&
|
||||
this.values.parsedScale
|
||||
) {
|
||||
return this.update();
|
||||
}
|
||||
|
||||
)
|
||||
this.update();
|
||||
return this;
|
||||
};
|
||||
|
||||
@ -342,19 +289,11 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
if (
|
||||
(this.values.pitch || this.values.pitch === 0) &&
|
||||
this.values.parsedScale
|
||||
) {
|
||||
return this.update();
|
||||
}
|
||||
|
||||
)
|
||||
this.update();
|
||||
return this;
|
||||
};
|
||||
|
||||
defaultPitchKeyScale() {
|
||||
if (!this.values.key) this.values.key = 60;
|
||||
if (!(this.values.pitch || this.values.pitch === 0)) this.values.pitch = 0;
|
||||
if (!this.values.parsedScale) this.values.parsedScale = safeScale("major");
|
||||
}
|
||||
|
||||
scale = (
|
||||
value: string | number | (number | string)[],
|
||||
...kwargs: (string | number)[]
|
||||
@ -372,38 +311,12 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
} else if (Array.isArray(value)) {
|
||||
this.values.parsedScale = value.map((v) => safeScale(v));
|
||||
}
|
||||
this.defaultPitchKeyScale();
|
||||
return this.update();
|
||||
if (this.values.key && (this.values.pitch || this.values.pitch === 0)) {
|
||||
this.update();
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
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;
|
||||
@ -435,7 +348,6 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
};
|
||||
|
||||
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;
|
||||
@ -448,35 +360,6 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 => {
|
||||
/*
|
||||
* This function is used to set the frequency of the Event.
|
||||
@ -512,22 +395,7 @@ export abstract class AudibleEvent extends AbstractEvent {
|
||||
return this;
|
||||
};
|
||||
|
||||
update = (): this => {
|
||||
update = (): void => {
|
||||
// Overwrite in subclasses
|
||||
return this;
|
||||
};
|
||||
|
||||
cue = (functionName: string|Function): this => {
|
||||
this.app.api.cue(functionName);
|
||||
return this;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { AudibleEvent } from "./AbstractEvents";
|
||||
import { type Editor } from "../main";
|
||||
import { MidiConnection } from "../IO/MidiConnection";
|
||||
import { resolvePitchClass } from "zifferjs";
|
||||
import { noteFromPc } from "zifferjs";
|
||||
import {
|
||||
filterObject,
|
||||
arrayOfObjectsToObjectWithArrays,
|
||||
objectWithArraysToArrayOfObjects,
|
||||
maybeAtomic,
|
||||
} from "../Utils/Generic";
|
||||
|
||||
export type MidiParams = {
|
||||
@ -67,7 +66,8 @@ export class MidiEvent extends AudibleEvent {
|
||||
return funcResult;
|
||||
} else {
|
||||
func(this.values);
|
||||
return this.update();
|
||||
this.update();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
@ -83,39 +83,37 @@ export class MidiEvent extends AudibleEvent {
|
||||
return this;
|
||||
};
|
||||
|
||||
update = (): this => {
|
||||
update = (): void => {
|
||||
// Get key, pitch, parsedScale and octave from this.values object
|
||||
const filteredValues = filterObject(this.values, [
|
||||
"key",
|
||||
"pitch",
|
||||
"originalPitch",
|
||||
"parsedScale",
|
||||
"addedOctave"
|
||||
"octave",
|
||||
]);
|
||||
|
||||
const events = objectWithArraysToArrayOfObjects(filteredValues, [
|
||||
"parsedScale",
|
||||
]);
|
||||
|
||||
events.forEach((soundEvent) => {
|
||||
const resolvedPitchClass = resolvePitchClass(
|
||||
(soundEvent.key || "C4"),
|
||||
(soundEvent.originalPitch || soundEvent.pitch || 0),
|
||||
(soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
|
||||
(soundEvent.addedOctave || 0)
|
||||
events.forEach((event) => {
|
||||
const [note, bend] = noteFromPc(
|
||||
(event.key as number) || "C4",
|
||||
(event.pitch as number) || 0,
|
||||
(event.parsedScale as number[]) || event.scale || "MAJOR",
|
||||
(event.octave as number) || 0,
|
||||
);
|
||||
soundEvent.note = resolvedPitchClass.note;
|
||||
soundEvent.pitch = resolvedPitchClass.pitch;
|
||||
soundEvent.octave = resolvedPitchClass.octave;
|
||||
event.note = note;
|
||||
if (bend) event.bend = bend;
|
||||
});
|
||||
|
||||
const newArrays = arrayOfObjectsToObjectWithArrays(events) as MidiParams;
|
||||
|
||||
this.values.note = maybeAtomic(newArrays.note);
|
||||
if (newArrays.bend) this.values.bend = maybeAtomic(newArrays.bend);
|
||||
return this;
|
||||
|
||||
this.values.note = newArrays.note;
|
||||
if (newArrays.bend) this.values.bend = newArrays.bend;
|
||||
};
|
||||
|
||||
out = (outChannel?: number|number[]): void => {
|
||||
out = (): void => {
|
||||
function play(event: MidiEvent, params: MidiParams): void {
|
||||
const channel = params.channel ? params.channel : 0;
|
||||
const velocity = params.velocity ? params.velocity : 100;
|
||||
@ -141,9 +139,6 @@ export class MidiEvent extends AudibleEvent {
|
||||
);
|
||||
}
|
||||
|
||||
this.runChain();
|
||||
if(outChannel) this.channel(outChannel);
|
||||
|
||||
const events = objectWithArraysToArrayOfObjects(this.values, [
|
||||
"parsedScale",
|
||||
]) as MidiParams[];
|
||||
|
||||
@ -5,9 +5,8 @@ import {
|
||||
filterObject,
|
||||
arrayOfObjectsToObjectWithArrays,
|
||||
objectWithArraysToArrayOfObjects,
|
||||
maybeAtomic,
|
||||
} from "../Utils/Generic";
|
||||
import { midiToFreq, resolvePitchClass } from "zifferjs";
|
||||
import { midiToFreq, noteFromPc } from "zifferjs";
|
||||
|
||||
import {
|
||||
superdough,
|
||||
@ -23,13 +22,10 @@ export type SoundParams = {
|
||||
note?: number | number[];
|
||||
freq?: number | number[];
|
||||
pitch?: number | number[];
|
||||
originalPitch?: number | number[];
|
||||
key?: string;
|
||||
scale?: string;
|
||||
parsedScale?: number[];
|
||||
octave?: number | number[];
|
||||
addedOctave?: number | number[];
|
||||
pitchOctave?: number | number[];
|
||||
};
|
||||
|
||||
export class SoundEvent extends AudibleEvent {
|
||||
@ -37,23 +33,21 @@ export class SoundEvent extends AudibleEvent {
|
||||
sound: any;
|
||||
|
||||
private static methodMap = {
|
||||
// SuperDirt related
|
||||
accelerate: ["accelerate", "acc"],
|
||||
legato: ["legato", "leg"],
|
||||
fadeTime: ["fadeTime", "fade"],
|
||||
tremolorate: ["tremolorate", "trem"],
|
||||
tremolodepth: ["tremolodepth", "tremd"],
|
||||
tilt: ["tilt"],
|
||||
plat: ["plat"],
|
||||
leslie: ["leslie"],
|
||||
lrate: ["lrate"],
|
||||
lsize: ["lsize"],
|
||||
volume: ["volume", "vol"],
|
||||
zrand: ["zrand", "zr"],
|
||||
curve: ["curve"],
|
||||
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"],
|
||||
deltaSlide: ["deltaSlide", "dslide"],
|
||||
pitchJump: ["pitchJump", "pj"],
|
||||
@ -62,7 +56,6 @@ export class SoundEvent extends AudibleEvent {
|
||||
znoise: ["znoise"],
|
||||
address: ["address", "add"],
|
||||
port: ["port"],
|
||||
density: ["density"],
|
||||
noise: ["noise"],
|
||||
zmod: ["zmod"],
|
||||
zcrush: ["zcrush"],
|
||||
@ -80,17 +73,11 @@ export class SoundEvent extends AudibleEvent {
|
||||
fmrelease: ["fmrelease", "fmrel"],
|
||||
fmvelocity: ["fmvelocity", "fmvel"],
|
||||
fmwave: ["fmwave", "fmw"],
|
||||
pattack: ["pattack", "patt"],
|
||||
pdecay: ["pdecay", "pdec"],
|
||||
psustain: ["psustain", "psus"],
|
||||
prelease: ["prelease", "prel"],
|
||||
penv: ["penv"],
|
||||
pcurve: ["pcurve"],
|
||||
panchor: ["panchor"],
|
||||
phaser: ["phaser", "phas"],
|
||||
phaserDepth: ["phaserDepth", "phasdepth"],
|
||||
phaserSweep: ["phaserSweep", "phassweep"],
|
||||
phaserCenter: ["phaserCenter", "phascenter"],
|
||||
fmadsr: function(
|
||||
fmadsr: function (
|
||||
self: SoundEvent,
|
||||
a: number,
|
||||
d: number,
|
||||
@ -103,7 +90,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("fmrelease", r);
|
||||
return self;
|
||||
},
|
||||
fmad: function(self: SoundEvent, a: number, d: number) {
|
||||
fmad: function (self: SoundEvent, a: number, d: number) {
|
||||
self.updateValue("fmattack", a);
|
||||
self.updateValue("fmdecay", d);
|
||||
return self;
|
||||
@ -114,7 +101,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
decay: ["decay", "dec"],
|
||||
sustain: ["sustain", "sus"],
|
||||
release: ["release", "rel"],
|
||||
adsr: function(
|
||||
adsr: function (
|
||||
self: SoundEvent,
|
||||
a: number,
|
||||
d: number,
|
||||
@ -127,18 +114,18 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("release", r);
|
||||
return self;
|
||||
},
|
||||
ad: function(self: SoundEvent, a: number, d: number) {
|
||||
ad: function (self: SoundEvent, a: number, d: number) {
|
||||
self.updateValue("attack", a);
|
||||
self.updateValue("decay", d);
|
||||
self.updateValue("sustain", 0.0);
|
||||
self.updateValue("release", 0.0);
|
||||
return self;
|
||||
},
|
||||
scope: function(self: SoundEvent) {
|
||||
scope: function (self: SoundEvent) {
|
||||
self.updateValue("analyze", true);
|
||||
return self;
|
||||
},
|
||||
debug: function(self: SoundEvent, callback?: Function) {
|
||||
debug: function (self: SoundEvent, callback?: Function) {
|
||||
self.updateValue("debug", true);
|
||||
if (callback) {
|
||||
self.updateValue("debugFunction", callback);
|
||||
@ -150,33 +137,27 @@ export class SoundEvent extends AudibleEvent {
|
||||
lpdecay: ["lpdecay", "lpd"],
|
||||
lpsustain: ["lpsustain", "lps"],
|
||||
lprelease: ["lprelease", "lpr"],
|
||||
cutoff: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
cutoff: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("cutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("resonance", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
lpf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
lpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("cutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("resonance", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
resonance: function(self: SoundEvent, value: number) {
|
||||
resonance: function (self: SoundEvent, value: number) {
|
||||
if (value >= 0 && value <= 1) {
|
||||
self.updateValue("resonance", 50 * value);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
lpq: function(self: SoundEvent, value: number) {
|
||||
if (value >= 0 && value <= 1) {
|
||||
self.updateValue("resonance", 50 * value);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
lpadsr: function(
|
||||
lpadsr: function (
|
||||
self: SoundEvent,
|
||||
depth: number,
|
||||
a: number,
|
||||
@ -191,7 +172,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("lprelease", r);
|
||||
return self;
|
||||
},
|
||||
lpad: function(self: SoundEvent, depth: number, a: number, d: number) {
|
||||
lpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||
self.updateValue("lpenv", depth);
|
||||
self.updateValue("lpattack", a);
|
||||
self.updateValue("lpdecay", d);
|
||||
@ -204,25 +185,25 @@ export class SoundEvent extends AudibleEvent {
|
||||
hpdecay: ["hpdecay", "hpd"],
|
||||
hpsustain: ["hpsustain", "hpsus"],
|
||||
hprelease: ["hprelease", "hpr"],
|
||||
hcutoff: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
hcutoff: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("hcutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("hresonance", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
hpf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
hpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("hcutoff", value);
|
||||
if (resonance) {
|
||||
self.updateValue("hresonance", resonance * 50);
|
||||
self.updateValue("hresonance", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
hpq: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("hresonance", value * 50);
|
||||
hpq: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("hresonance", value);
|
||||
return self;
|
||||
},
|
||||
hpadsr: function(
|
||||
hpadsr: function (
|
||||
self: SoundEvent,
|
||||
depth: number,
|
||||
a: number,
|
||||
@ -237,7 +218,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("hprelease", r);
|
||||
return self;
|
||||
},
|
||||
hpad: function(self: SoundEvent, depth: number, a: number, d: number) {
|
||||
hpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||
self.updateValue("hpenv", depth);
|
||||
self.updateValue("hpattack", a);
|
||||
self.updateValue("hpdecay", d);
|
||||
@ -250,25 +231,22 @@ export class SoundEvent extends AudibleEvent {
|
||||
bpdecay: ["bpdecay", "bpd"],
|
||||
bpsustain: ["bpsustain", "bps"],
|
||||
bprelease: ["bprelease", "bpr"],
|
||||
bandf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
bandf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("bandf", value);
|
||||
if (resonance) {
|
||||
self.updateValue("bandq", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
bpf: function(self: SoundEvent, value: number, resonance?: number) {
|
||||
bpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||
self.updateValue("bandf", value);
|
||||
if (resonance) {
|
||||
self.updateValue("bandq", resonance * 50);
|
||||
self.updateValue("bandq", resonance);
|
||||
}
|
||||
return self;
|
||||
},
|
||||
bpq: function(self: SoundEvent, value: number) {
|
||||
self.updateValue("bandq", value * 50);
|
||||
return self;
|
||||
},
|
||||
bpadsr: function(
|
||||
bandq: ["bandq", "bpq"],
|
||||
bpadsr: function (
|
||||
self: SoundEvent,
|
||||
depth: number,
|
||||
a: number,
|
||||
@ -283,7 +261,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
self.updateValue("bprelease", r);
|
||||
return self;
|
||||
},
|
||||
bpad: function(self: SoundEvent, depth: number, a: number, d: number) {
|
||||
bpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||
self.updateValue("bpenv", depth);
|
||||
self.updateValue("bpattack", a);
|
||||
self.updateValue("bpdecay", d);
|
||||
@ -293,7 +271,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
},
|
||||
vib: ["vib"],
|
||||
vibmod: ["vibmod"],
|
||||
fm: function(self: SoundEvent, value: number | string) {
|
||||
fm: function (self: SoundEvent, value: number | string) {
|
||||
if (typeof value === "number") {
|
||||
self.values["fmi"] = value;
|
||||
} else {
|
||||
@ -309,11 +287,11 @@ export class SoundEvent extends AudibleEvent {
|
||||
begin: ["begin"],
|
||||
end: ["end"],
|
||||
gain: ["gain"],
|
||||
dbgain: function(self: SoundEvent, value: number) {
|
||||
dbgain: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
||||
return self;
|
||||
},
|
||||
db: function(self: SoundEvent, value: number) {
|
||||
db: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
||||
return self;
|
||||
},
|
||||
@ -336,32 +314,32 @@ export class SoundEvent extends AudibleEvent {
|
||||
roomlp: ["roomlp", "rlp"],
|
||||
roomdim: ["roomdim", "rdim"],
|
||||
sound: ["s", "sound"],
|
||||
size: function(self: SoundEvent, value: number) {
|
||||
size: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("roomsize", value);
|
||||
return self;
|
||||
},
|
||||
sz: function(self: SoundEvent, value: number) {
|
||||
sz: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("roomsize", value);
|
||||
return self;
|
||||
},
|
||||
comp: ["comp", "compressor", "cmp"],
|
||||
ratio: function(self: SoundEvent, value: number) {
|
||||
comp: ["compressor", "cmp"],
|
||||
ratio: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorRatio", value);
|
||||
return self;
|
||||
},
|
||||
knee: function(self: SoundEvent, value: number) {
|
||||
knee: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorKnee", value);
|
||||
return self;
|
||||
},
|
||||
compAttack: function(self: SoundEvent, value: number) {
|
||||
compAttack: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorAttack", value);
|
||||
return self;
|
||||
},
|
||||
compRelease: function(self: SoundEvent, value: number) {
|
||||
compRelease: function (self: SoundEvent, value: number) {
|
||||
self.updateValue("compressorRelease", value);
|
||||
return self;
|
||||
},
|
||||
stretch: function(self: SoundEvent, beat: number) {
|
||||
stretch: function (self: SoundEvent, beat: number) {
|
||||
self.updateValue("unit", "c");
|
||||
self.updateValue("speed", 1 / beat);
|
||||
self.updateValue("cut", beat);
|
||||
@ -402,6 +380,46 @@ export class SoundEvent extends AudibleEvent {
|
||||
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),
|
||||
};
|
||||
} 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 };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ================================================================================
|
||||
// AbstactEvent overrides
|
||||
// ================================================================================
|
||||
@ -411,48 +429,39 @@ export class SoundEvent extends AudibleEvent {
|
||||
if (funcResult instanceof Object) return funcResult;
|
||||
else {
|
||||
func(this.values);
|
||||
return this.update();
|
||||
this.update();
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
update = (): this => {
|
||||
update = (): void => {
|
||||
const filteredValues = filterObject(this.values, [
|
||||
"key",
|
||||
"pitch",
|
||||
"originalPitch",
|
||||
"parsedScale",
|
||||
"addedOctave",
|
||||
"octave",
|
||||
"paramOctave"
|
||||
]);
|
||||
const events = objectWithArraysToArrayOfObjects(filteredValues, [
|
||||
"parsedScale",
|
||||
]);
|
||||
events.forEach((soundEvent) => {
|
||||
const resolvedPitchClass = resolvePitchClass(
|
||||
(soundEvent.key || "C4"),
|
||||
(soundEvent.originalPitch || soundEvent.pitch || 0),
|
||||
(soundEvent.parsedScale || soundEvent.scale || "MAJOR"),
|
||||
(soundEvent.paramOctave || 0) + (soundEvent.addedOctave || 0)
|
||||
events.forEach((event) => {
|
||||
const [note, _] = noteFromPc(
|
||||
(event.key as number) || "C4",
|
||||
(event.pitch as number) || 0,
|
||||
(event.parsedScale as number[]) || event.scale || "MAJOR",
|
||||
(event.octave as number) || 0,
|
||||
);
|
||||
soundEvent.note = resolvedPitchClass.note;
|
||||
soundEvent.freq = midiToFreq(resolvedPitchClass.note);
|
||||
soundEvent.pitch = resolvedPitchClass.pitch;
|
||||
soundEvent.octave = resolvedPitchClass.octave;
|
||||
event.note = note;
|
||||
event.freq = midiToFreq(note);
|
||||
});
|
||||
|
||||
const newArrays = arrayOfObjectsToObjectWithArrays(events) as SoundParams;
|
||||
|
||||
this.values.note = maybeAtomic(newArrays.note);
|
||||
this.values.freq = maybeAtomic(newArrays.freq);
|
||||
this.values.pitch = maybeAtomic(newArrays.pitch);
|
||||
this.values.octave = maybeAtomic(newArrays.octave);
|
||||
this.values.pitchOctave = maybeAtomic(newArrays.pitchOctave);
|
||||
return this;
|
||||
this.values.note = newArrays.note;
|
||||
this.values.freq = newArrays.freq;
|
||||
};
|
||||
|
||||
out = (orbit?: number | number[]): void => {
|
||||
this.runChain();
|
||||
if (orbit) this.values["orbit"] = orbit;
|
||||
const events = objectWithArraysToArrayOfObjects(this.values, [
|
||||
"parsedScale",
|
||||
@ -467,11 +476,7 @@ export class SoundEvent extends AudibleEvent {
|
||||
if (filteredEvent.freq) {
|
||||
delete filteredEvent.note;
|
||||
}
|
||||
superdough(
|
||||
filteredEvent,
|
||||
this.nudge - this.app.clock.deviation,
|
||||
filteredEvent.dur
|
||||
);
|
||||
superdough(filteredEvent, this.app.clock.deadline, filteredEvent.dur);
|
||||
}
|
||||
};
|
||||
|
||||
@ -495,8 +500,23 @@ export class SoundEvent extends AudibleEvent {
|
||||
address: oscAddress,
|
||||
port: oscPort,
|
||||
args: event,
|
||||
timetag: Math.round(Date.now() + (this.nudge - this.app.clock.deviation)),
|
||||
timetag: Math.round(Date.now() + this.app.clock.deadline),
|
||||
} as OSCMessage);
|
||||
}
|
||||
};
|
||||
|
||||
dirt = (orbit?: number | number[]): void => {
|
||||
if (orbit) this.values["orbit"] = orbit;
|
||||
const events = objectWithArraysToArrayOfObjects(this.values, [
|
||||
"parsedScale",
|
||||
]);
|
||||
for (const event of events) {
|
||||
const filteredEvent = event;
|
||||
if (filteredEvent.freq) { delete filteredEvent.note; }
|
||||
sendToServer({
|
||||
address: "/dirt/play", port: 57120,
|
||||
args: event, timetag: Math.round(Date.now() + this.app.clock.deadline),
|
||||
} as OSCMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,9 +5,8 @@ import { SkipEvent } from "./SkipEvent";
|
||||
import { SoundEvent, SoundParams } from "./SoundEvent";
|
||||
import { MidiEvent, MidiParams } from "./MidiEvent";
|
||||
import { RestEvent } from "./RestEvent";
|
||||
import { arrayOfObjectsToObjectWithArrays, isGenerator } from "../Utils/Generic";
|
||||
import { arrayOfObjectsToObjectWithArrays } from "../Utils/Generic";
|
||||
import { TonnetzSpaces } from "zifferjs/src/tonnetz";
|
||||
import { safeMod } from "zifferjs/src/utils";
|
||||
|
||||
export type InputOptions = { [key: string]: string | number };
|
||||
|
||||
@ -18,7 +17,6 @@ export class Player extends AbstractEvent {
|
||||
startCallTime: number = 0;
|
||||
lastCallTime: number = 0;
|
||||
waitTime = 0;
|
||||
cueName: string|undefined = undefined;
|
||||
played: boolean = false;
|
||||
current!: Pitch | Chord | ZRest;
|
||||
retro: boolean = false;
|
||||
@ -32,7 +30,6 @@ export class Player extends AbstractEvent {
|
||||
options: InputOptions,
|
||||
public app: Editor,
|
||||
zid: string = "",
|
||||
waitTime: number = 0,
|
||||
) {
|
||||
super(app);
|
||||
this.options = options;
|
||||
@ -42,34 +39,13 @@ export class Player extends AbstractEvent {
|
||||
} else if (typeof input === "number") {
|
||||
this.input = input;
|
||||
this.ziffers = Ziffers.fromNumber(input, options);
|
||||
} else if (isGenerator(input)) {
|
||||
} else {
|
||||
this.ziffers = Ziffers.fromGenerator(input, options);
|
||||
this.input = this.ziffers.input;
|
||||
} else {
|
||||
throw new Error("Invalid input");
|
||||
}
|
||||
if(waitTime) this.waitTime = waitTime;
|
||||
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() {
|
||||
this.initCallTime = 0;
|
||||
this.startCallTime = 0;
|
||||
@ -149,13 +125,11 @@ export class Player extends AbstractEvent {
|
||||
|
||||
const patternIsStarting =
|
||||
this.notStarted() &&
|
||||
this.waitTime >= 0 &&
|
||||
this.origin() >= this.waitTime &&
|
||||
(this.pulse() === 0 || this.origin() >= this.nextBeatInTicks());
|
||||
(this.pulse() === 0 || this.origin() >= this.nextBeatInTicks()) &&
|
||||
this.origin() >= this.waitTime;
|
||||
|
||||
const timeToPlayNext =
|
||||
this.current &&
|
||||
this.waitTime >= 0 &&
|
||||
this.pulseToSecond(this.origin()) >=
|
||||
this.pulseToSecond(this.lastCallTime) +
|
||||
this.pulseToSecond(this.current.duration * 4 * this.app.clock.ppqn) &&
|
||||
@ -181,64 +155,40 @@ export class Player extends AbstractEvent {
|
||||
return areWeThereYet;
|
||||
};
|
||||
|
||||
checkCue() {
|
||||
if(this.ziffers.atLast()) {
|
||||
if(this.cueName && this.app.api.cueTimes[this.cueName]) {
|
||||
delete this.app.api.cueTimes[this.cueName];
|
||||
this.cueName = undefined;
|
||||
this.waitTime = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sound(name?: string | string[] | SoundParams | SoundParams[]) {
|
||||
sound(name?: string) {
|
||||
if (this.areWeThereYet()) {
|
||||
this.checkCue();
|
||||
const event = this.next() as Pitch | Chord | ZRest;
|
||||
const noteLengthInSeconds = this.app.clock.convertPulseToSecond(
|
||||
event.duration * 4 * this.app.clock.ppqn,
|
||||
);
|
||||
if (event instanceof Pitch) {
|
||||
let obj = event.getExisting(
|
||||
const obj = event.getExisting(
|
||||
"freq",
|
||||
"note",
|
||||
"pitch",
|
||||
"originalPitch",
|
||||
"key",
|
||||
"scale",
|
||||
"octave",
|
||||
"pitchOctave",
|
||||
"addedOctave",
|
||||
"parsedScale",
|
||||
) as SoundParams;
|
||||
|
||||
if (event.sound) name = event.sound as string;
|
||||
if(name) obj = {...obj, ...this.processSound(name)};
|
||||
else obj.s = "sine";
|
||||
|
||||
if (event.soundIndex) obj.n = event.soundIndex as number;
|
||||
obj.dur = noteLengthInSeconds;
|
||||
return new SoundEvent(obj, this.app);
|
||||
return new SoundEvent(obj, this.app).sound(name || "sine");
|
||||
} else if (event instanceof Chord) {
|
||||
const pitches = event.pitches.map((p) => {
|
||||
return p.getExisting(
|
||||
"freq",
|
||||
"note",
|
||||
"pitch",
|
||||
"originalPitch",
|
||||
"key",
|
||||
"scale",
|
||||
"octave",
|
||||
"pitchOctave",
|
||||
"addedOctave",
|
||||
"parsedScale",
|
||||
);
|
||||
}) as SoundParams[];
|
||||
|
||||
let add = { dur: noteLengthInSeconds} as SoundParams;
|
||||
if(name) add = {...add, ...this.processSound(name)};
|
||||
else add.s = "sine";
|
||||
|
||||
const add = { dur: noteLengthInSeconds } as SoundParams;
|
||||
if (name) add.s = name;
|
||||
let sound = arrayOfObjectsToObjectWithArrays(
|
||||
pitches,
|
||||
add,
|
||||
@ -254,18 +204,14 @@ export class Player extends AbstractEvent {
|
||||
|
||||
midi(value: number | undefined = undefined) {
|
||||
if (this.areWeThereYet()) {
|
||||
this.checkCue();
|
||||
const event = this.next() as Pitch | Chord | ZRest;
|
||||
const obj = event.getExisting(
|
||||
"note",
|
||||
"pitch",
|
||||
"originalPitch",
|
||||
"bend",
|
||||
"key",
|
||||
"scale",
|
||||
"octave",
|
||||
"pitchOctave",
|
||||
"addedOctave",
|
||||
"parsedScale",
|
||||
) as MidiParams;
|
||||
if (event instanceof Pitch) {
|
||||
@ -284,34 +230,11 @@ export class Player extends AbstractEvent {
|
||||
}
|
||||
}
|
||||
|
||||
scale(name: string|number[]) {
|
||||
scale(name: string) {
|
||||
if (this.atTheBeginning()) this.ziffers.scale(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
semitones(values: number|number[], ...rest: number[]) {
|
||||
values = typeof values === "number" ? [values, ...rest] : values;
|
||||
if (this.atTheBeginning()) this.ziffers.semitones(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
cents(values: number|number[], ...rest: number[]) {
|
||||
values = typeof values === "number" ? [values, ...rest] : values;
|
||||
if (this.atTheBeginning()) this.ziffers.cents(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
ratios(values: number|number[], ...rest: number[]) {
|
||||
values = typeof values === "number" ? [values, ...rest] : values;
|
||||
if (this.atTheBeginning()) this.ziffers.ratios(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
edo(value: number, scale: string|number[] = new Array(value).fill(1)) {
|
||||
if (this.atTheBeginning()) this.ziffers.edo(value, scale);
|
||||
return this;
|
||||
}
|
||||
|
||||
key(name: string) {
|
||||
if (this.atTheBeginning()) this.ziffers.key(name);
|
||||
return this;
|
||||
@ -338,92 +261,18 @@ export class Player extends AbstractEvent {
|
||||
return this;
|
||||
}
|
||||
|
||||
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 4, components: number = 1) {
|
||||
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz, repeats, components);
|
||||
octaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
||||
if (this.atTheBeginning()) this.ziffers.octaCycle(tonnetz);
|
||||
return this;
|
||||
}
|
||||
|
||||
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) {
|
||||
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz, repeats, components);
|
||||
hexaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
||||
if (this.atTheBeginning()) this.ziffers.hexaCycle(tonnetz);
|
||||
return this;
|
||||
}
|
||||
|
||||
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5], repeats: number = 3, components: number = 1) {
|
||||
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz, repeats, components);
|
||||
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);
|
||||
enneaCycle(tonnetz: TonnetzSpaces = [3, 4, 5]) {
|
||||
if (this.atTheBeginning()) this.ziffers.enneaCycle(tonnetz);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -439,12 +288,6 @@ export class Player extends AbstractEvent {
|
||||
|
||||
lead = () => this.voiceleading();
|
||||
|
||||
arpeggio(indexes: string|number[], ...rest: number[]) {
|
||||
if(typeof indexes === "number") indexes = [indexes, ...rest];
|
||||
if (this.atTheBeginning()) this.ziffers.arpeggio(indexes);
|
||||
return this;
|
||||
}
|
||||
|
||||
invert = (n: number) => {
|
||||
if (this.atTheBeginning()) {
|
||||
this.ziffers.invert(n);
|
||||
@ -457,71 +300,25 @@ export class Player extends AbstractEvent {
|
||||
return this;
|
||||
}
|
||||
|
||||
rotate(amount: number = 1) {
|
||||
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;
|
||||
}
|
||||
|
||||
wait(value: number | Function) {
|
||||
if (this.atTheBeginning()) {
|
||||
if (typeof value === "function") {
|
||||
const refPat = this.app.api.patternCache.get(value.name) as Player;
|
||||
if (refPat) this.waitTime = refPat.nextEndTime();
|
||||
return this;
|
||||
} else if(typeof value === "number") {
|
||||
this.waitTime =
|
||||
this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn);
|
||||
return this;
|
||||
}
|
||||
this.waitTime =
|
||||
this.origin() + Math.ceil(value * 4 * this.app.clock.ppqn);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
sync(value: string | Function, manualSync: boolean = true) {
|
||||
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;
|
||||
}
|
||||
sync(value: string | Function) {
|
||||
if (this.atTheBeginning() && this.notStarted()) {
|
||||
const origin = this.app.clock.pulses_since_origin;
|
||||
const syncId = typeof value === "function" ? value.name : value;
|
||||
if (origin > 0) {
|
||||
const syncName = typeof value === "function" ? value.name : value;
|
||||
const syncPattern = this.app.api.patternCache.get(syncName) as Player;
|
||||
const syncPattern = this.app.api.patternCache.get(syncId) as Player;
|
||||
if (syncPattern) {
|
||||
const syncPatternDuration = syncPattern.ziffers.duration;
|
||||
const syncPatternStart = syncPattern.startCallTime;
|
||||
@ -533,13 +330,6 @@ export class Player extends AbstractEvent {
|
||||
return this;
|
||||
}
|
||||
|
||||
log(key: string, ...args: string[]) {
|
||||
this.app.api.log(this.ziffers.evaluated.map((p) => {
|
||||
return Object.values(p.getExisting(...[key,...args]));
|
||||
}).join(" "));
|
||||
return this;
|
||||
}
|
||||
|
||||
out = (): void => {
|
||||
// TODO?
|
||||
};
|
||||
|
||||
7181
src/colors.json
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const amplitude = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -33,43 +33,42 @@ beat(.5)::snd('cp').vel($(1)%10 / 10).out()`,
|
||||
| <ic>decay</ic> | dec | Decay value (time to decay to sustain level) |
|
||||
| <ic>sustain</ic> | sus | Sustain value (gain when sound is held) |
|
||||
| <ic>release</ic> | rel | Release value (time for the sound to die off) |
|
||||
| <ic>adsr</ic> | | Shortcut that combines all the parameters together |
|
||||
|
||||
Note that the **sustain** value is not a duration but an amplitude value (how loud). The other values are the time for each stage to take place. Here is a fairly complete example using the <ic>sawtooth</ic> basic waveform.
|
||||
|
||||
${makeExample(
|
||||
"Simple synthesizer",
|
||||
`
|
||||
register("smooth", x => x.cutoff(r(100,500))
|
||||
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
|
||||
.gain(r(0.25, 0.4)).adsr(0, r(.2,.4), r(0,0.5), 0)
|
||||
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125))
|
||||
beat(.25)::sound('sawtooth')
|
||||
.note([50,57,55,60].beat(1))
|
||||
.smooth().out();
|
||||
beat(.25)::sound('sawtooth')
|
||||
.note([50,57,55,60].add(12).beat(1.5))
|
||||
.smooth().out();
|
||||
let smooth = (sound) => {
|
||||
return sound.cutoff(r(100,500))
|
||||
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
|
||||
.gain(r(0.25, 0.4)).adsr(0, r(.2,.4), r(0,0.5), 0)
|
||||
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125)
|
||||
}
|
||||
beat(.25)::smooth(sound('sawtooth')
|
||||
.note([50,57,55,60].beat(1))).out();
|
||||
beat(.25)::smooth(sound('sawtooth')
|
||||
.note([50,57,55,60].add(12).beat(1.5))).out();
|
||||
`,
|
||||
true,
|
||||
)};
|
||||
|
||||
|
||||
Sometimes, using a full ADSR envelope is a bit overkill. There are other simpler controls to manipulate the envelope like the <ic>.ad</ic> method:
|
||||
|
||||
|
||||
${makeExample(
|
||||
"Replacing .adsr by .ad",
|
||||
`
|
||||
register("smooth", x => x.cutoff(r(100,500))
|
||||
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
|
||||
.gain(r(0.25, 0.4)).ad(0, 0.25)
|
||||
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125))
|
||||
beat(.25)::sound('sawtooth')
|
||||
.note([50,57,55,60].beat(1))
|
||||
.smooth().out();
|
||||
beat(.25)::sound('sawtooth')
|
||||
.note([50,57,55,60].add(12).beat(1.5))
|
||||
.smooth().out();
|
||||
let smooth = (sound) => {
|
||||
return sound.cutoff(r(100,500))
|
||||
.lpadsr(usaw(1/8) * 8, 0.05, .125, 0, 0)
|
||||
.gain(r(0.25, 0.4)).ad(0, .25)
|
||||
.room(0.9).size(2).o(2).vib(r(2,8)).vibmod(0.125)
|
||||
}
|
||||
beat(.25)::smooth(sound('sawtooth')
|
||||
.note([50,57,55,60].beat(1))).out();
|
||||
beat(.25)::smooth(sound('sawtooth')
|
||||
.note([50,57,55,60].add(12).beat(1.5))).out();
|
||||
`,
|
||||
true,
|
||||
)};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const audio_basics = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const distortion = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -1,7 +1,7 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const effects = (application: Editor): string => {
|
||||
export const reverb = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
@ -90,34 +90,5 @@ beat(.5)::snd('pad').crush([16, 8, 4].beat(2)).clip(.5).out()
|
||||
`,
|
||||
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 |
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const sampler = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -5,27 +5,27 @@ export const code = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# 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:
|
||||
|
||||
- [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.
|
||||
|
||||
|
||||
- [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?
|
||||
|
||||
|
||||
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 errors and printing:** your code will crash! Don't worry, we do our best to make it crash in the most gracious way possible. Most errors are caught and displayed in the interface. For weirder bugs, open the dev console with ${key_shortcut(
|
||||
"Ctrl + Shift + I",
|
||||
)}. You cannot directly use <ic>console.log('hello, world')</ic> in the interface but you can use <ic>log(message)</ic> to print a one line message. You will have to open the console as well to see your messages being printed there! 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!
|
||||
|
||||
|
||||
# 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:
|
||||
@ -38,8 +38,8 @@ beat(.75) :: snd('linnhats').n([1,4,5].beat()).out()
|
||||
beat(1) :: snd('bd').out()
|
||||
//if (true) && log('very true')
|
||||
// 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,
|
||||
@ -67,7 +67,7 @@ beat(4) ? snd('kick').out() : beat(2) :: snd('snare').out()
|
||||
)}
|
||||
|
||||
# About crashes and bugs
|
||||
|
||||
|
||||
Things will crash! It's part of the show! You will learn progressively to avoid mistakes and to write safer code. Do not hesitate to kill the page or to stop the transport if you feel overwhelmed by an algorithm blowing up. There is no safeguard to stop you from doing most things. This is to ensure that you have all the available possible room to write bespoke code and experiment with your ideas through code.
|
||||
|
||||
${makeExample(
|
||||
|
||||
@ -49,9 +49,6 @@ Topos is made to be controlled entirely with a keyboard. It is recommanded to st
|
||||
|Force Eval|${key_shortcut(
|
||||
"Ctrl + Shift + Enter",
|
||||
)}|Force evaluation of the current script|
|
||||
|Clear cache & Eval|${key_shortcut(
|
||||
"Ctrl + Shift + Backspace (Delete)",
|
||||
)}|Clears cache and forces evaluation to update cached scripts|
|
||||
|
||||
### Special
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
|
||||
export const chaining = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
@ -37,7 +37,7 @@ beat(1)::sound('fhh').juxrev().out()
|
||||
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(
|
||||
"Creating synth presets",
|
||||
"Re-creating a classic Tidal function",
|
||||
`
|
||||
// Registering a specific synth architecture
|
||||
register('sub', (n,x=4,y=80)=>n.ad(0, .25)
|
||||
@ -54,45 +54,6 @@ rhythm(.25, [6, 8].beat(), 12)::sound('sine')
|
||||
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
|
||||
|
||||
@ -140,6 +101,8 @@ 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>always</ic> | Always transforms the Event. | <ic>.always(s => s.note(71))</ic> |
|
||||
|
||||
|
||||
|
||||
### MIDI Chaining
|
||||
|
||||
The conditional chaining also applies to MIDI. Values can also be incremented using <ic>+=</ic> notation.
|
||||
91
src/documentation/engine.ts
Normal 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 |
|
||||
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const functions = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
@ -37,25 +37,6 @@ beat(1) :: script(1, 3, 5)
|
||||
- <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.
|
||||
|
||||
### 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
|
||||
|
||||
- <ic>delay(ms: number, func: Function): void</ic>: Delays the execution of a function by a given number of milliseconds.
|
||||
@ -733,8 +733,8 @@ const completionDatabase: CompletionDatabase = {
|
||||
midi: {
|
||||
name: "midi",
|
||||
category: "midi",
|
||||
description: "Send a MIDI message (note, velocity, channel)",
|
||||
example: "midi(144, 60, 1)",
|
||||
description: "Send a MIDI message",
|
||||
example: "midi(144, 60, 100)",
|
||||
},
|
||||
control_change: {
|
||||
name: "control_change",
|
||||
@ -742,12 +742,6 @@ const completionDatabase: CompletionDatabase = {
|
||||
description: "Send a MIDI control change message",
|
||||
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: {
|
||||
name: "program_change",
|
||||
category: "midi",
|
||||
@ -788,7 +782,7 @@ const completionDatabase: CompletionDatabase = {
|
||||
name: "counter",
|
||||
category: "patterns",
|
||||
description: "Counter/iterator",
|
||||
example: "counter('my_counter', 20, 1)",
|
||||
example: "counter('my_counter_, 20, 1)",
|
||||
},
|
||||
drunk: {
|
||||
name: "drunk",
|
||||
@ -814,17 +808,11 @@ const completionDatabase: CompletionDatabase = {
|
||||
description: "Wraps (or not) of the drunk walk (boolean)",
|
||||
example: "drunk_wrap(true)",
|
||||
},
|
||||
global: {
|
||||
name: "global",
|
||||
v: {
|
||||
name: "v",
|
||||
category: "variable",
|
||||
description: "Global Variable setter or getter",
|
||||
example: "global.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",
|
||||
example: "v('my_var', 10) // Sets global variable 'my_var' to 10",
|
||||
},
|
||||
delete_variable: {
|
||||
name: "delete_variable",
|
||||
@ -910,6 +898,12 @@ const completionDatabase: CompletionDatabase = {
|
||||
description: "Detects if the Alt key is pressed",
|
||||
example: "fill() ? 1 : 0.5",
|
||||
},
|
||||
comp: {
|
||||
name: "comp",
|
||||
category: "synthesis",
|
||||
description: "Compressor threshold (dB)",
|
||||
example: "sound('sine').comp(-4).out()",
|
||||
},
|
||||
ratio: {
|
||||
name: "ratio",
|
||||
category: "synthesis",
|
||||
@ -965,12 +959,12 @@ export const inlineHoveringTips = hoverTooltip(
|
||||
let completion =
|
||||
completionDatabase[text.slice(start - from, end - from)] || {};
|
||||
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>
|
||||
<pre class="-mt-2"><code class="pl-4 text-base">${completion.example}</code></pre></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;
|
||||
return { dom };
|
||||
},
|
||||
@ -990,7 +984,7 @@ export const toposCompletions = (context: CompletionContext) => {
|
||||
info: () => {
|
||||
let div = document.createElement("div");
|
||||
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>
|
||||
<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>
|
||||
`;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
// @ts-ignore
|
||||
export const interaction = (application: Editor): string => {
|
||||
@ -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.
|
||||
|
||||
${makeExample(
|
||||
"Play active notes as chords",
|
||||
`
|
||||
"Play active notes as chords",
|
||||
`
|
||||
beat(1) && active_notes() && sound('sine').chord(active_notes()).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Play active notes as arpeggios",
|
||||
`
|
||||
"Play active notes as arpeggios",
|
||||
`
|
||||
beat(0.25) && active_notes() && sound('juno').note(
|
||||
active_notes().beat(0.5)+[12,24].beat(0.25)
|
||||
).cutoff(300 + usine(1/4) * 2000).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
|
||||
* <ic>sticky_notes(channel?: number)</ic>: returns array of the last pressed keys as an array of MIDI note numbers (0-127). Notes are added and removed from the list with the "Note on"-event. Returns undefined if no keys have been pressed.
|
||||
|
||||
${makeExample(
|
||||
"Play continous arpeggio with sticky notes",
|
||||
`
|
||||
"Play continous arpeggio with sticky notes",
|
||||
`
|
||||
beat(0.25) && sticky_notes() && sound('arp')
|
||||
.note(sticky_notes().palindrome().beat(0.25)).out()
|
||||
`,
|
||||
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.
|
||||
|
||||
${makeExample(
|
||||
"Play last note",
|
||||
`
|
||||
"Play last note",
|
||||
`
|
||||
beat(0.5) && sound('sawtooth').note(last_note())
|
||||
.vib([1, 3, 5].beat(1))
|
||||
.vibmod([1,3,2,4].beat(2)).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
* <ic>buffer()</ic>: return true if there are notes in the buffer.
|
||||
* <ic>buffer_note(channel?: number)</ic>: returns last unread note that has been received. Note is fetched and removed from start of the buffer once this is called. Returns undefined if no notes have been received.
|
||||
|
||||
${makeExample(
|
||||
"Play buffered note",
|
||||
`
|
||||
"Play buffered note",
|
||||
`
|
||||
beat(1) && buffer() && sound('sine').note(buffer_note()).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
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(
|
||||
"Play notes with cc",
|
||||
`
|
||||
beat(0.5) && sound('arp').note(ccin(74)).out()
|
||||
"Play notes with cc",
|
||||
`
|
||||
beat(0.5) && sound('arp').note(last_cc(74)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Control everything with CCs",
|
||||
`
|
||||
"Control everything with CCs",
|
||||
`
|
||||
beat(0.5) :: sound('sine')
|
||||
.freq(ccIn(75)*3)
|
||||
.cutoff(ccIn(76)*2*usine())
|
||||
.freq(last_cc(75)*3)
|
||||
.cutoff(last_cc(76)*2*usine())
|
||||
.sustain(1.0)
|
||||
.out()
|
||||
|
||||
beat(ccIn(74)/127*.5) :: sound('sine')
|
||||
.freq(ccIn(75)*6)
|
||||
.cutoff(ccIn(76)*3*usine())
|
||||
.sustain(ccIn(74)/127*.25)
|
||||
beat(last_cc(74)/127*.5) :: sound('sine')
|
||||
.freq(last_cc(75)*6)
|
||||
.cutoff(last_cc(76)*3*usine())
|
||||
.sustain(last_cc(74)/127*.25)
|
||||
.out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
${makeExample(
|
||||
"Show scale on external keyboard",
|
||||
`show_scale("F","aeolian",0,4)`,
|
||||
true,
|
||||
)}
|
||||
"Show scale on external keyboard",
|
||||
`show_scale("F","aeolian",0,4)`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample("Hide scale", `hide_scale("F","aeolian",0,4)`, true)}
|
||||
|
||||
@ -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,
|
||||
)}
|
||||
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
|
||||
export const pitch = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `# Pitch
|
||||
|
||||
|
||||
## Pitch envelope
|
||||
|
||||
Similar to the amplitude envelope, you can use an envelope to shape the pitch
|
||||
of your sounds (can be samples or synthesizers). This is super useful to create
|
||||
new timbres out of existing sounds.
|
||||
|
||||
| Method | Alias | Description |
|
||||
|---------|-------|-----------------------------------------------|
|
||||
| <ic>pattack</ic> | patt | Attack time |
|
||||
| <ic>pdecay</ic> | pdec | Decay time |
|
||||
| <ic>psustain</ic> | psus | Sustain value |
|
||||
| <ic>prelease</ic> | prel | Release time |
|
||||
| <ic>penv</ic> | | Pitch envelope strength (positive or negative) |
|
||||
| <ic>panchor</ic> | | Envelope anchor range (0 - 1) |
|
||||
|
||||
Resume writing the pitch documentation here.
|
||||
|
||||
`;
|
||||
};
|
||||
59
src/documentation/lfos.ts
Normal 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,
|
||||
)};
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const long_forms = (app: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory, key_shortcut } from "../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory, key_shortcut } from "../Documentation";
|
||||
|
||||
export const midi = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
@ -9,24 +9,6 @@ export const bonus = (application: Editor): string => {
|
||||
|
||||
Some features have been included as a bonus. These features are often about patterning over things that are not directly related to sound: pictures, video, etc.
|
||||
|
||||
## Editor theme configuration
|
||||
|
||||
The editor theme can be changed using the <ic>theme</ic> and <ic>randomTheme</ic> functions. The following example will use a random color scheme for every beat:
|
||||
|
||||
${makeExample(
|
||||
"Random theme on each beat",
|
||||
`
|
||||
beat(1)::randomTheme()
|
||||
`, true)}
|
||||
|
||||
You can also pick a theme using the <ic>theme</ic> function with a string as only argument:
|
||||
|
||||
${makeExample(
|
||||
"Picking a theme",
|
||||
`
|
||||
beat(1)::theme("Batman")
|
||||
`, true)}
|
||||
|
||||
## Hydra Visual Live Coding
|
||||
|
||||
<div class="mx-12 bg-neutral-600 rounded-lg flex flex-col items-center justify-center">
|
||||
|
||||
@ -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>.
|
||||
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const osc = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -1,10 +1,10 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const patterns = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# Array patterns
|
||||
# Patterns
|
||||
|
||||
**Topos** is using arrays as a way to make dynamic patterns of data (rhythms, melodies, etc).
|
||||
It means that the following:
|
||||
@ -120,40 +120,9 @@ beat(1)::sound(['kick', 'fsnare'].dur(3, 1))
|
||||
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
|
||||
|
||||
|
||||
- <ic>pitch()</ic>: convert a list of integers to pitch classes
|
||||
|
||||
${makeExample(
|
||||
@ -167,62 +136,7 @@ beat(0.25) :: snd('sine')
|
||||
true,
|
||||
)}
|
||||
|
||||
- <ic>semitones(number[], ...args?)</ic>: Create scale from semitone intervals.
|
||||
|
||||
${makeExample(
|
||||
"Play pitches from scale created from semitone intervals",
|
||||
`
|
||||
beat(1) :: sound('gtr').pitch([0, 4, 3, 2].beat()).key(64)
|
||||
.semitones(1, 1, 3, 1, 1, 2, 3).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
- <ic>cents(number[], ...args?)</ic>: Create scale from cent intervals.
|
||||
|
||||
${makeExample(
|
||||
"Play pitches from scale created from cent intervals",
|
||||
`
|
||||
rhythm([0.5,0.25].beat(1),14,16) :: sound('pluck')
|
||||
.stretch(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/).
|
||||
- <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",
|
||||
@ -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,
|
||||
)};
|
||||
|
||||
|
||||
`
|
||||
};
|
||||
@ -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,
|
||||
)};
|
||||
|
||||
`;
|
||||
};
|
||||
@ -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
|
||||
|
||||
`;
|
||||
};
|
||||
@ -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,
|
||||
)}
|
||||
|
||||
`;
|
||||
};
|
||||
@ -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,
|
||||
)}
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,101 +0,0 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
|
||||
export const ziffers_scales = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# Scales
|
||||
|
||||
Ziffers supports all the keys and scales. Keys can be defined by using [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation), for example <ic>F3</ic>. Western style (1490 scales) can be with scale names named after greek modes and extended by <a href="https://allthescales.org/intro.php" target="_blank">William Zeitler</a>. You will never really run out of scales to play with using Ziffers. Here is a short list of some possible scales that you can play with:
|
||||
|
||||
| Scale name | Intervals |
|
||||
|------------|------------------------|
|
||||
| Lydian | <ic>2221221</ic> |
|
||||
| Mixolydian | <ic>2212212</ic> |
|
||||
| Aeolian | <ic>2122122</ic> |
|
||||
| Locrian | <ic>1221222</ic> |
|
||||
| Ionian | <ic>2212221</ic> |
|
||||
| Dorian | <ic>2122212</ic> |
|
||||
| Phrygian | <ic>1222122</ic> |
|
||||
| Soryllic | <ic>11122122</ic>|
|
||||
| Modimic | <ic>412122</ic> |
|
||||
| Ionalian | <ic>1312122</ic> |
|
||||
| ... | And it goes on for <a href="https://ianring.com/musictheory/scales/traditions/zeitler" target="_blank">**1490** scales (See full list here)</a>. |
|
||||
|
||||
${makeExample(
|
||||
"What the hell is the Modimic scale?",
|
||||
`
|
||||
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
|
||||
.scale('modimic').fmi(2).fmh(2).room(0.5)
|
||||
.size(0.5).sustain(0.1) .delay(0.5)
|
||||
.delay(0.125).delayfb(0.25).out();
|
||||
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
You can also use more traditional <a href="https://ianring.com/musictheory/scales/traditions/western" target="_blank">western names</a>:
|
||||
|
||||
| Scale name | Intervals |
|
||||
|------------|------------------------|
|
||||
| Major | <ic>2212221</ic> |
|
||||
| Minor | <ic>2122122</ic> |
|
||||
| Minor pentatonic | <ic>32232</ic> |
|
||||
| Harmonic minor | <ic>2122131</ic>|
|
||||
| Harmonic major | <ic>2212131</ic>|
|
||||
| Melodic minor | <ic>2122221</ic>|
|
||||
| Melodic major | <ic>2212122</ic>|
|
||||
| Whole | <ic>222222</ic> |
|
||||
| Blues minor | <ic>321132</ic> |
|
||||
| Blues major | <ic>211323</ic> |
|
||||
|
||||
${makeExample(
|
||||
"Let's fall back to a classic blues minor scale",
|
||||
`
|
||||
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
|
||||
.scale('blues minor').fmi(2).fmh(2).room(0.5)
|
||||
.size(0.5).sustain(0.25).delay(0.25)
|
||||
.delay(0.25).delayfb(0.5).out();
|
||||
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
Microtonal scales can be defined using <a href="https://www.huygens-fokker.org/scala/scl_format.html" target="_blank">Scala format</a> or by extended notation defined by Sevish <a href="https://sevish.com/scaleworkshop/" target="_blank">Scale workshop</a>, for example:
|
||||
|
||||
- **Young:** 106. 198. 306.2 400.1 502. 604. 697.9 806.1 898.1 1004.1 1102. 1200.
|
||||
- **Wendy carlos:** 17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1
|
||||
|
||||
|
||||
${makeExample(
|
||||
"Wendy Carlos, here we go!",
|
||||
`
|
||||
z1("s ^ (0,8) 0 0 _ (0,5) 0 0").sound('sine')
|
||||
.scale('17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1').fmi(2).fmh(2).room(0.5)
|
||||
.size(0.5).sustain(0.15).delay(0.1)
|
||||
.delay(0.25).delayfb(0.5).out();
|
||||
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Werckmeister scale in Scala format",
|
||||
`
|
||||
const werckmeister = "107.82 203.91 311.72 401.955 503.91 605.865 701.955 809.775 900. 1007.82 1103.91 1200."
|
||||
|
||||
z0('s (0,3) ^ 0 3 ^ 0 (3,6) 0 _ (3,5) 0 _ 3 ^ 0 (3,5) ^ 0 6 0 _ 3 0')
|
||||
.key('C3')
|
||||
.scale(werckmeister)
|
||||
.sound('sine')
|
||||
.fmi(1 + usine(0.5) * irand(1,10))
|
||||
.cutoff(100 + usine(.5) * 100)
|
||||
.out()
|
||||
|
||||
onbeat(1,1.5,3) :: sound('bd').cutoff(100 + usine(.25) * 1000).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,112 +0,0 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
|
||||
export const ziffers_syncing = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# Synchronization
|
||||
|
||||
Ziffers patterns can be synced to any event by using **cue**, **sync**, **wait** and **listen** methods.
|
||||
|
||||
## Sync with cue
|
||||
|
||||
The <ic>cue(name: string)</ic> methods can be used to send cue messages for ziffers patterns. The <ic>wait(name: string)</ic> method is used to wait for the cue message to be received before starting the next cycle.
|
||||
|
||||
${makeExample(
|
||||
"Sending cue from event and wait",
|
||||
`
|
||||
beat(4.0) :: sound("bd").cue("foo").out()
|
||||
z1("e 0 3 2 1 2 1").wait("foo").sound("sine").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
The <ic>sync(name: string)</ic> method is used to sync the ziffers pattern to the cue message.
|
||||
|
||||
${makeExample(
|
||||
"Delayed start using individual cue",
|
||||
`
|
||||
register('christmas', n=>n.room(0.25).size(2).speed([0.5, 0.25, 0.125])
|
||||
.delay(0.5).delayt(1/3).delayfb(0.5).bpf(200+usine(1/3)*500).out())
|
||||
onbar(1) :: cue("bar")
|
||||
onbar(2) :: cue('baz')
|
||||
z1("<0.25 0.125> 0 4 2 -2").sync("bar").sound("ST40:25").christmas()
|
||||
z2("<0.25 0.125> 0 6 4 -4").sync("baz").sound("ST40:25").christmas()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
The <ic>listen(name: string)</ic> method can be used to listen for the cue messages and play one event from the pattern for every cue.
|
||||
|
||||
${makeExample(
|
||||
"Delayed start using individual cue",
|
||||
`
|
||||
beat(1.0) :: cue("boom")
|
||||
|
||||
z1("bd <hh ho>").listen("boom")
|
||||
.sound().out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
## Sync with beat
|
||||
|
||||
Patterns can also be synced using beat and setting the note length of events to zero using **z** duration character or <ic>noteLength(number)</ic> method.
|
||||
|
||||
${makeExample(
|
||||
"Syncing with beat",
|
||||
`
|
||||
beat(.5) :: z1("<bd sn:3> hh:5").noteLength(0)
|
||||
.sound().out()
|
||||
|
||||
beat([2.0,0.5,1.5].bar(1)) ::
|
||||
z2("z _ 0 0 <2 1>").sound("bass:5")
|
||||
.dur(0.5).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Automatic sync for ziffers patterns
|
||||
|
||||
Numbered methods **(z0-z16)** are synced automatically to **z0** method if it exsists. Syncing can also be done manually by using either the <ic>wait</ic> method, which will always wait for the current pattern to finish before starting the next cycle, or the <ic>sync</ic> method will only wait for the synced pattern to finish on the first time.
|
||||
|
||||
${makeExample(
|
||||
"Automatic sync to z0",
|
||||
`
|
||||
z0('w 0 8').sound('peri').out()
|
||||
z1('e 0 4 5 9').sound('bell').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Syncing patterns to each other
|
||||
|
||||
Patterns can also be synced together using the <ic>sync(name: Function)</ic> method. This will sync the pattern to the start of the referenced pattern. Copy this example and first run z1 and then z2 at random position.
|
||||
|
||||
${makeExample(
|
||||
"Sync on first run",
|
||||
`
|
||||
z1('w __ 0 5 9 3').sound('bin').out()
|
||||
z2('q __ 4 2 e 6 3 q 6').sync(z1).sound('east').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Sync with wait
|
||||
|
||||
Syncing can also be done using <ic>wait(name: Function)</ic> method. This will wait for the referenced pattern to finish before starting the next cycle.
|
||||
|
||||
${makeExample(
|
||||
"Sync with wait",
|
||||
`
|
||||
z1('w 0 5').sound('pluck').release(0.1).sustain(0.25).out()
|
||||
z2('q 6 3').wait(z1).sound('sine').release(0.16).sustain(0.55).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
@ -1,400 +0,0 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
|
||||
export const ziffers_tonnetz = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# Tonnetz
|
||||
|
||||
The Riemannian Tonnetz is a geometric representation of pitches where we apply mathematical operations to analyze harmonic and melodic relationships in tonal music. Ziffers includes an implementation of live coding Tonnetz developed together with <a href="https://github.com/edelveart/TypeScriptTonnetz" target="_blank">Edgar Delgado Vega</a>. Nevertheless, our implementation allows you to play in different chord complexes and **combine 67 transformations** with **new exploratory notation**. You have at your disposal the sets: traditional PLR, film music, extended PLR* and functions for seventh chords PLRQ, PLRQ*, ST.
|
||||
|
||||
Tonnetz can be visualized as an <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numeric lattice</a> that represents the twelve pitch classes of the chromatic scale. The numeric visualization is a fork of <a href="https://hal.science/hal-03250334/" target="_blank">Web tonnetz</a> by Corentin Guichaou et al. (2021). The lattice can be arranged into multiple pitch spaces which are all supported in Ziffers implementation.
|
||||
|
||||
In addition, we have included common graphs and cycles in Neo-Riemmanian theory: HexaCycles, OctaCycles, Enneacycles, Weitzmann Regions, Boretz Regions, OctaTowers, Cube Dance and Power Towers. You can explore each of these graphs in great generality over different Tonnetz.
|
||||
|
||||
## Explorative notation
|
||||
|
||||
Ziffers implements explorative live coding notation that indexes all of the transformations for triad and seventh chords. For more detailed transformations see Triad and Tetra chapters. Explorative transformations also include cardinal direction transformations (North, South, East, West) as visualized by the <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numerical Tonnetz</a> and correspond to different Neo-Riemannian operations depending on the chord type (Major or Minor).
|
||||
|
||||
Transformations are applied by grouping operations into a **parameter string** which applies the **transformations** to the chord. The parameter string is a **sequence** of transformations **separated by whitespace**, for example <ic>plr rl2 p3lr</ic>. The numbers after the characters defines the **index for the operation**, as there can be multiple operations of the same type.
|
||||
|
||||
Indexed transformations <ic>[plrfsntqNSEW][1-9]*</ic>:
|
||||
|
||||
* p: Parallel
|
||||
* l: Leading-tone exchange
|
||||
* r: Relative
|
||||
* f: Film transformation - Far-fifth
|
||||
* n: Film transformation - Near-fifth (Nebenverdwandt) or PLRQ* transformation
|
||||
* s: Film transformation - Slide
|
||||
* h: Film transformation - Hexatonic Pole
|
||||
* t: Film transformation - Tritone transposition
|
||||
* q: PLR* transformation or PLRQ* transformation
|
||||
* N: North transformation
|
||||
* S: South transformation
|
||||
* E: East transformation
|
||||
* W: West transformation
|
||||
|
||||
### Examples:
|
||||
|
||||
${makeExample(
|
||||
"Explorative transformations with roman chords",
|
||||
`
|
||||
z1('i i7').tonnetz("p1 p2 plr2")
|
||||
.sound('wt_stereo')
|
||||
.adsr(0, .1, 0, 0)
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Arpeggiated explorative transformations",
|
||||
`
|
||||
z1("i7")
|
||||
.tonnetz("p l2 r3 rp4l")
|
||||
.arpeggio("e _ 0 1 s ^ 0 2 1 3 h _ 012 s ^ 2 1")
|
||||
.sound("sine")
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Arpeggios and note lengths with parameters",
|
||||
`
|
||||
z1("024")
|
||||
.tonnetz("p lr rp lrp")
|
||||
.arpeggio(0,2,1,2)
|
||||
.noteLength(1/16,1/8)
|
||||
.sound("sine")
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Explorative transformations with cardinal directions",
|
||||
`
|
||||
z1("1/4 i")
|
||||
.tonnetz("p Np N2p N3p plr N3plr E EE EEE E6 NSE3W2")
|
||||
.sound("sine")
|
||||
.out()
|
||||
`
|
||||
)}
|
||||
|
||||
## Triad transformations
|
||||
|
||||
Triad transformations can be defined explicitly using the <ic>triadTonnetz(transformation: string, tonnetz: number[])</ic> method. This method will only apply specific transformations to triad chords.
|
||||
|
||||
In the table below, we write the transformations types available for triads followed by the **transposition in semitones (+/-)** that we must perform to the **root of the chord**: <ic>0,4,3,7,5,1,8,6</ic>.
|
||||
|
||||
Second, you should know that the numbers next to the names of the transformations represent the **chord types to be exchanged**: <ic>1 = major, 2 = minor, 3 = diminished, 4 = augmented</ic>.
|
||||
|
||||
In fact, the functions <ic>p,l,r</ic> and the so-called [**film music transformations**](https://alpof.wordpress.com/2021/10/09/neo-riemannian-examples-in-music/), could have the numbers <ic>p12, l12, r12, f12 </ic> and so on, since they transform major and minor chords with their respective root transpositions. It must be clarified that the <ic>t6</ic> function is the only one among all that maintains the same type of chord.
|
||||
|
||||
Therefore, you will see that paying attention to the examples will allow you to infer whether you should **raise or lower the root** depending on the **type of chord**.
|
||||
|
||||
| Function | Function type | Root transposition | Example |
|
||||
| :------: | :-------------------------------: | :----------------: | :-----------: |
|
||||
| p | Parallel | 0 | C <-> Cm |
|
||||
| l | Leading-tone | 4 | C <-> Em |
|
||||
| r | Relative | 3 | C <-> Am |
|
||||
| f | Film music: Far-fifth | 7 | C <-> Gm |
|
||||
| n | Film music: Near-fifth | 5 | C <-> Fm |
|
||||
| s | Film music: Slide | 1 | C <-> C#m |
|
||||
| h | Film music: Hexatonic pole | 8 | C <-> G#m |
|
||||
| t6 | Film music: Tritone Transposition | 6 | C <-> F# |
|
||||
| p32 | Parallel | 0 | Cdim <-> Cm |
|
||||
| p41 | Parallel | 0 | Caug <-> C |
|
||||
| lt13 | Leading-tone | 4 | C <-> Edim |
|
||||
| l41 | Leading-tone | 4 | Caug <-> Edim |
|
||||
| l14 | Leading-tone | 4 | C <-> Eaug |
|
||||
| rt23 | Relative | 3 | Cm <-> Adim |
|
||||
| rt42 | Relative | 3 | Caug <-> Am |
|
||||
| q13 | PLR* | 1 | C <-> C#dim |
|
||||
| q42 | PLR* | 1 | Caug <-> C#m |
|
||||
| n42 | PLR* | 5 | Caug <-> Fm |
|
||||
|
||||
* Remark A: We add <ic>t</ic> to <ic>l.13</ic>, <ic>r.23</ic>, <ic>r.42</ic> because we have similar syntax for sevenths transformations <ic>l13</ic>, <ic>r23</ic> and <ic>r42</ic>, although the meaning of the numbers (chord types) is different. See in the next section.
|
||||
* Remark B: For those curious about mathematics, what we have implemented at Ziffers is the group called **PLR\*** [(Cannas, 2018, pp. 93-100)](https://publication-theses.unistra.fr/public/theses_doctorat/2018/CANNAS_Sonia_2018_ED269.pdf).
|
||||
|
||||
### Examples:
|
||||
${makeExample(
|
||||
"Synthetic 'Morton'",
|
||||
`
|
||||
z0('h. 0 q _6 h _4 _3 w _2 _0 h. ^0 q 6 h 4 3 3/4 2 5/4 0 w r')
|
||||
.scale("minor").sound('sawtooth').key("A")
|
||||
.room(0.9).size(9).phaser(0.25).phaserDepth(0.8)
|
||||
.vib(4).vibmod(0.15).out()
|
||||
|
||||
z1('w 904')
|
||||
.scale("chromatic")
|
||||
.tonnetz('o f l l o f l l o')
|
||||
.sound('sine').adsr(0.1, 1, 1, 1.9).out()
|
||||
|
||||
z2('904')
|
||||
.scale("chromatic")
|
||||
.tonnetz('o f l l o f l l o')
|
||||
.arpeggio('s 0 2 1 0 1 2 1 0 2 1 0 1 2 0 1 0')
|
||||
.sound('sine').pan(rand(1, 0)).adsr(0, 0.125, 0.15, 0.25).out()
|
||||
|
||||
z3('e __ 4 s 0 e 1 2 s')
|
||||
.sound('hat').delay(0.5).delayfb(0.35).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Different Tonnetz, Chord Complexes
|
||||
|
||||
At Ziffers we have strived to have fun and inspire you by exploring new sounds that Neo-Riemannian functions can offer you by changing only one parameter: The Tonnetz in which your chords move. By default, the Tonnetz has this form: <ic>[3, 4, 5]</ic>. Let's try an example as it will clarify this idea for us.
|
||||
|
||||
The <ic>Cm</ic> chord has the tone classes: <ic>037</ic>. Notice that the distance between the third of the chord and the root of the chord is <ic>3</ic> <ic>(3-0)</ic>. In turn, the distance of the fifth from the third is <ic>4</ic> semitones <ic>(7-3)</ic>. Finally, the distance left to get from the fifth to the root is <ic>5</ic> <ic>(7+5=0)</ic>. These distances are known as **intervalic structure**. In this regard, the array <ic>[x = 3, y = 4, z = 5]</ic> of a Tonnetz tells us the intervallic structure of the chords to which we apply the Neo-Riemannian functions.
|
||||
|
||||
:warning: To have a geometric intuition of the chords that we are going to describe, we suggest you see the <a href="https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/" target="_blank">numerical Tonnetz.</a>
|
||||
|
||||
In the next three Tonnetz we consider that we go from a minor chord to a major one by inversion (we change <ic>x</ic> and <ic>y</ic>).
|
||||
|
||||
* For the Tonnetz <ic>[3, 4, 5]</ic> we have minor chords <ic>037</ic> and major chords <ic>047</ic>
|
||||
* For a Tonnetz <ic>[2, 3, 7]</ic> we have the "minor" chords <ic>025</ic> and the "major" chords <ic>035</ic>
|
||||
* For a Tonnetz <ic>[1, 4, 7]</ic> we have the "minor" chords <ic>015</ic> and the "major" chords <ic>045</ic>
|
||||
|
||||
Are those all the Tonnetz? In fact, there are <ic>12</ic> spaces that comply with symmetries by **transposition and inversion**:
|
||||
|
||||
<ic>[3, 4, 5], [1, 1, 10], [1, 2, 9], [1, 3, 8], [1, 4, 7], [1, 5, 6], [ 2, 3, 7], [ 2, 5, 5]</ic>
|
||||
|
||||
<ic>[2, 4, 6], [2, 2, 8], [3, 3, 6], [4, 4, 4]</ic>
|
||||
|
||||
What do augmented chords or seventh chords sound like on a Tonnetz <ic>[1,5,6]</ic>? It is up to you to **explore all the transformations in different spaces**.
|
||||
|
||||
What if I want to place another type of Tonnetz that is not on the list? No problem, everyone is invited to the party.
|
||||
|
||||
* Remark C: If you want to know more about the topology and mathematics behind Tonnetz, you can refer to [Bigo (2013)](https://theses.hal.science/tel-01326827).
|
||||
|
||||
## Tetra transformations
|
||||
|
||||
Did you want to experiment with more functions? At Ziffers we have brought you Neo-Riemannian functions for seventh chords. The possibilities of sound exploration increase considerably, even more so if you **include different Tonnetz**.
|
||||
|
||||
Tetra transformations can be applied to seventh chords using the <ic>tetraTonnetz(transformation: string, tonnetz: number[])</ic> method. This method will apply specific transformations to certain type of chords. If the **chord is not the correct type**, the **transformation will not be applied**.
|
||||
|
||||
:warning: If you are here without having read the **triad chords transformations section**, we highly suggest you skip to it. The ideas and notation shown in this section are nothing more than an extension of what was developed above.
|
||||
|
||||
First, here we will deal with **9 interchangeable chord types** to which we will assign a number:
|
||||
|
||||
<ic>1 = 7, 2 = m7, 3 = hdim7, 4 = maj7, 5 = dim7, 6 = minMaj7, 7 = maj7#5, 8 = 7#5, 9 = 7b5</ic>.
|
||||
|
||||
Second, the **transpositions** that carry out the functions <ic>p = 0, l = 4, r = 3, q = 1, n = 5</ic>, raise or lower the **root of the chord** in semitones in equal measure. However, there are two new types of functions whose transpositions are the following: <ic>rr = 6</ic> and <ic>qq = 2</ic>.
|
||||
|
||||
You are ready, these have been all the requirements. Now a couple of examples will be enough for you to know how these functions operate.
|
||||
|
||||
* The <ic>p14</ic> function **does not move the root (0 semitones)** and changes a dominant 7th chord to a major 7th chord. For example: <ic>C7 <-> Cmaj7</ic>.
|
||||
* The <ic>rr39</ic> function **moves the root 6 semitones** and swaps an hdim7 chord for a 7b5 chord. For example: <ic>Cm7b5 <-> F#7b5</ic>.
|
||||
|
||||
So that you can incorporate this new musical machinery into your game, all the possible transformations according to the type of seventh chord are listed below. You already know what each one will do.
|
||||
|
||||
* Remark D: For those curious about the mathematics behind it, we have implemented a group called **PLRQ** and another group called **PLRQ\*** extended [(Cannas, 2018, pp. 71-92)](https://publication-theses.unistra.fr/public/theses_doctorat/2018/CANNAS_Sonia_2018_ED269.pdf).
|
||||
|
||||
| Chord type | P functions | L functions | R functions | Q functions | N functions |
|
||||
| :--------: | :----------------: | :-----------: | :----------------------------: | :---------: | :---------: |
|
||||
| 7 | p12, p14, p18, p19 | l13, l15, l71 | r12, rr19 | q15, qq51 | n51 |
|
||||
| m7 | p12, p23, p26 | l42 | r12, r23, r42 | q62 | |
|
||||
| hdim7 | p23, p35, p39 | l13 | r23, r35, r53, r63, rr35, rr39 | q43, qq38 | |
|
||||
| maj7 | p14, p47, p64 | l42 | r42 | q43 | |
|
||||
| dim7 | p35 | l15 | r35, r53 | q15, qq51 | n51 |
|
||||
| minMaj7 | p26, p64 | | r63, r76, r86 | q62, q76 | |
|
||||
| maj7#5 | p47, p87 | l71 | r76 | q76 | |
|
||||
| dom7#5 | p18, p87, p98 | l89 | r86, rr98 | qq38, qq98 | |
|
||||
| dom7b5 | p19, p39, p98 | l89 | rr19, rr39, rr98 | qq98 | |
|
||||
|
||||
### Examples
|
||||
|
||||
${makeExample(
|
||||
"Transform seventh chord from chromatic scale",
|
||||
`
|
||||
z1("1.0 047{10}")
|
||||
.scale('chromatic')
|
||||
.tetraTonnetz("o p18 q15 l13 n51 p19 q15")
|
||||
.sound("sawtooth")
|
||||
.cutoff(500 + usine(1/8) * 2000)
|
||||
.adsr(.5,0.05,0.25,0.5)
|
||||
.dur(2.0)
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Cyclic methods
|
||||
|
||||
In addition to the traditional tonnetz transformations, Ziffers implements cyclic methods that can be used to cycle through the tonnetz space. Cyclic methods turns individual pitch classes to chords using the tonnetz. The cyclic methods are:
|
||||
|
||||
* <ic>hexaCycle(tonnetz: number[], repeats: number = 3, components: number = 1)</ic>: Cycles through chords via hexatonic cycles
|
||||
* <ic>octaCycle(tonnetz: number[], repeats: number = 4, components: number = 1)</ic>: Cycles through chords via octatonic cycles
|
||||
* <ic>enneaCycle(tonnetz: number[], repeats: number = 3, components: number = 1)</ic>: Cycles through chords via enneatonic cycles
|
||||
|
||||
:warning: By default, the number of graph <ic>components</ic> is set to <ic>1</ic>. Therefore, these methods produce a single hexatonic, octatonic, and enneatonic cycle, respectively. OctaTowers were implemented in the same way, so it generates a single octatonic tower. Try increasing the number of components to obtain different graphs.
|
||||
|
||||
**HexaCycles** are sequences of major and minor triads generated by the <ic>p</ic> and <ic>l</ic> transformations. Let's take the following example starting with a <ic>C</ic> chord: <ic>C -> Cm -> Ab -> Abm -> E -> Em</ic>. You can start on the chord of your choice.
|
||||
|
||||
**OctaCycles** are sequences of major and minor triads generated using <ic>p</ic> and <ic>r</ic> transformations. Starting at <ic>C</ic>, we have the following sequence: <ic>C -> Cm -> Eb -> Ebm -> F# -> F#m -> A -> Am</ic>.
|
||||
|
||||
Unlike HexaCycles and OctaCycles, **EnneaCycles** are four-note chord sequences. Considering the functions implemented for tetrachords in Ziffers, we can interpret these sequences as generated by <ic>p12, p23, and l13</ic> transformations repeatedly: <ic>C7 -> Cm7 -> Cm7b5 -> Ab7 -> Abm7 -> Abm7b5 -> E7 -> Em7 -> Em7b5</ic>.
|
||||
|
||||
### Examples:
|
||||
|
||||
${makeExample(
|
||||
"Arpeggio with ennea cycle",
|
||||
`
|
||||
z1("0 2 -1 3")
|
||||
.enneaCycle()
|
||||
.arpeggio(0,2,1)
|
||||
.scale("modimic")
|
||||
.noteLength(0.15,0.05,0.05,0.25)
|
||||
.sound("sine")
|
||||
.adsr(0.1,0.15,0.25,0.1)
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Variating arpeggios",
|
||||
`
|
||||
z1("s 0 3 2 1")
|
||||
.octaCycle()
|
||||
.arpeggio([0,[0,2],[1,0],[0,1,2]].beat(0.15))
|
||||
.sound("triangle")
|
||||
.adsr(0.1,0.1,0.13,0.15)
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Cycles with vitamins and repetitions
|
||||
|
||||
Finally, cyclic methods in Ziffers can also be vitaminized with doses of different Tonnetz. However, this opens the way to different behavior with cycles.
|
||||
|
||||
We have the Tonnetz <ic>[2, 3, 7]</ic>, so <ic>hexaCycle([2, 3, 7])</ic>. The generated chords we hear are:
|
||||
|
||||
<ic>035 -> 025 -> 902 -> 9{11}2 -> 69{11} -> 68{11} </ic>
|
||||
|
||||
Apparently, everything operates as we expect: six chords and we return to the first. However, here comes the unexpected and perhaps somewhat obscure question:
|
||||
|
||||
* If we look at the graphs of the [numeric Tonnetz](https://numeric-tonnetz-ziffers-6f7c9299bb4e1292f6891b9aceba16d81409236.gitlab.io/), our hexaCycle over <ic>[2, 3, 7]</ic> which starts with the chord <ic>035</ic> goes through all the intermediate chords generated by <ic>p</ic> and <ic>l</ic> functions until reaching <ic>035</ic> again?
|
||||
|
||||
As you can verify it manually, you will see that this is not the case. Upon reaching the <ic>68{11} </ic> chord, the cycle makes a jump of two chords (<ic>368 358</ic>) towards the <ic>035</ic> chord. This does not happen with the cycles in the Tonnetz <ic>[3, 4, 5]</ic>, since all the intermediate chords are played there.
|
||||
|
||||
To play the chords without jumps in our hexaCycle (although the prefix "hexa" would no longer have a precise meaning), we add a number of repetitions.
|
||||
|
||||
${makeExample(
|
||||
"HexaCycles with vitamins",
|
||||
`
|
||||
z1("0")
|
||||
.scale("chromatic")
|
||||
.hexaCycle([2,3,7],4)
|
||||
.sound("sine").out()
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
By default hexaCycles and enneaCycles have <ic>3</ic> repetitions, while octaCycles has <ic>4</ic> repetitions. We have specified a **chromatic scale** although this is the **default scale**. Try changing the **repeats and scales** when playing with different Tonnetz.
|
||||
|
||||
* Remark E: These cycles in Tonnetz <ic>[3, 4, 5]</ic> are implemented based on the work of [Douthett & Steinbach (1998, pp. 245-247, 253-256)](https://www.jstor.org/stable/843877)
|
||||
|
||||
## More traversing methods
|
||||
|
||||
In addition to the cyclical traversing methods, Ziffers implements traversing methods that traverse the Tonnetz in different ways. These methods are:
|
||||
|
||||
* <ic>weitzmannRegions(tonnetz: number[])</ic>: Cycles through chords in a Weitzmann region
|
||||
* <ic>boretzRegions(tonnetz: number[])</ic>: Cycles through chords in a Boretz region
|
||||
* <ic>octaTowers(tonnetz: number[], repeats: number = 3, components: number = 1)</ic>: Cycles through chords using the octaTowers
|
||||
* <ic>cubeDance(tonnetz: number[], repeats: number = 3)</ic>: Cycles through chords in a Cube Dance
|
||||
* <ic>powerTowers(tonnetz: number[], repeats: number = 3)</ic>: Cycles through chords using the Power Towers
|
||||
|
||||
**Weitzmann Regions** is composed only of three-note chords. Following Richard Cohn's **Weitzmann water bug** graph, the region consists of an augmented chord (body), three major chords, and three minor chords (feet). The latter related to the central chord by a minimal parsimonious movement. A cyclic order of **Nebenverdwandt / R** transformations proposed by Carl Weitzmann himself has been chosen.
|
||||
|
||||
**Boretz Regions** is the four-note analogue of the Weitzmann regions. Richard Cohn draws them in **Boretz Spiders**, a graph consisting of 8 feet between 7th and half-diminished 7th chords. The body (prosoma-opisthosoma) is a <ic>dim7</ic> chord, related to the others by a semitonal movement.
|
||||
|
||||
**OctaTowers** generates a graph composed of **12** chords, whose types are <ic>halfdim7, m7 and 7</ic>. A reading from left to right in an ascending diagonal has been chosen. Note that changing the number of components to <ic>3</ic> will obtain the complete graph (**36** chords).
|
||||
|
||||
**Cube Dance** is another graph of **28** chords that is built primarily with HexaCycles (4 hexatonic cycles), except that it adds <ic>augmented</ic> triads as assemblers. As with Power Towers, one possible path has been selected.
|
||||
|
||||
**Power Towers** use **39** four-note chords (<ic>halfdim7, m7 and 7</ic>). As you can notice, it is composed of OctaTowers (3 octatonic towers) assembled by <ic>dim7</ic> type chords. One of the many paths for succession has been chosen.
|
||||
|
||||
As you have noticed, all these graphs usually have many chords, so sometimes it will be convenient to slice up fragments of the cycles. We encourage you to explore these methods and their different parameters. The tonnetz traversing methods can be used in combination with the Ziffers generative methods to sequence, arpeggiate and to randomize the chords in different ways.
|
||||
|
||||
${makeExample(
|
||||
"Cube Dance swing",
|
||||
`
|
||||
z1("0").cubeDance([3,4,5])
|
||||
.sound("sine")
|
||||
.ad(r(0.1,0.5),0.1)
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Selecting subset of chords from the cube dance",
|
||||
`
|
||||
z1("1/2 0")
|
||||
.cubeDance([3,4,5],4)
|
||||
.at(0,8,2,rI(9,14))
|
||||
.sound("triangle")
|
||||
.ad(0.05,0.15)
|
||||
.delay(2)
|
||||
.out()
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Power Towers with pulse",
|
||||
`
|
||||
z1("1/4 2").powerTowers([2,3,7])
|
||||
.between(5,11)
|
||||
.arpeggio("e 0 3 1 2")
|
||||
.sound("sine")
|
||||
.adsr(0.01,0.1,0.1,0.9)
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Between an OctaTower",
|
||||
`
|
||||
z1("s. 0")
|
||||
.octaTower()
|
||||
.between(2,8)
|
||||
.arpeggio(3,2,1,rI(1,5))
|
||||
.sound("sawtooth")
|
||||
.adsr(0.1,0.15,0,0.1)
|
||||
.out()
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Selecting chords from the weitzmann region",
|
||||
`
|
||||
z1("1/8 0")
|
||||
.weitzmannRegions()
|
||||
.at(1,rI(0,7),4,6)
|
||||
.arpeggio(0,2,1,rI(0,2))
|
||||
.sound("sine")
|
||||
.ad(0.15,0.15)
|
||||
.out()
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Boretz Spider",
|
||||
`
|
||||
z1("1/16 0")
|
||||
.boretzRegions([1,4,7])
|
||||
.at(2,rI(3,7),4,6)
|
||||
.arpeggio(1,0,2,rI(1,4))
|
||||
.sound("square")
|
||||
.adsr(0.1,0.1,0.1,0.2)
|
||||
.out()
|
||||
`,
|
||||
true
|
||||
)}
|
||||
|
||||
* Remark F: You can find more details about Weitzmann and Boretz regions in chapters 4 and 7 of Richard Cohn's book [Audacious Euphony: Chromatic Harmony and the Triad's Second Nature (2012)](https://books.google.com.pe/books?id=rZxZCMRiO9EC&pg=PA59&hl=es&source=gbs_toc_r&cad=2#v=onepage&q&f=false).
|
||||
|
||||
`;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const probabilities = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
@ -54,7 +54,6 @@ By default chance operators will be evaluated 48 times within a beat. You can ch
|
||||
- <ic>frequently(beats?: number)</ic>: returns true 90% of the time in given number of beats
|
||||
- <ic>almostAlways(beats?: number)</ic>: returns true 99% of the time in given number of beats
|
||||
- <ic>always(beats?: number)</ic>: returns true. Can be handy when switching between different probabilities
|
||||
- <ic>once()</ic>: returns true once, then false until the code is force evaluated (Shift+Ctrl+Enter)
|
||||
|
||||
Examples:
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const loading_samples = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const sample_banks = (application: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const samples_to_markdown = (
|
||||
application: Editor,
|
||||
@ -32,7 +32,7 @@ export const samples_to_markdown = (
|
||||
|
||||
markdownList += `
|
||||
<button
|
||||
class="hover:bg-foreground inline px-4 py-2 bg-black text-brightwhite hover:text-background text-xl"
|
||||
class="hover:bg-neutral-500 inline px-4 py-2 bg-neutral-700 text-orange-300 text-xl"
|
||||
onclick="app.api._playDocExampleOnce(app.api.codeExamples['${codeId}'])"
|
||||
>
|
||||
${keys[i]}
|
||||
@ -65,80 +65,44 @@ On this page, you will find an exhaustive list of all the samples currently load
|
||||
A very large collection of wavetables for wavetable synthesis. This collection has been released by Kristoffer Ekstrand: [AKWF Waveforms](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/). Every sound sample that starts with <ic>wt_</ic> will be looped. Look at this demo:
|
||||
|
||||
${makeExample(
|
||||
"Wavetable synthesis made easy :)",
|
||||
`
|
||||
"Wavetable synthesis made easy :)",
|
||||
`
|
||||
beat(0.5)::sound('wt_stereo').n([0, 1].pick()).ad(0, .25).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
Pick one folder and spend some time exploring it. There is a lot of different waveforms.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Waveforms")}
|
||||
</div>
|
||||
|
||||
## Drum machines sample pack
|
||||
|
||||
A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best
|
||||
to use the <ic>.bank()</ic> parameter like so:
|
||||
A set of 72 classic drum machines created by **Geikha**: [Geikha Drum Machines](https://github.com/geikha/tidal-drum-machines). To use them efficiently, it is best to use the <ic>.bank()</ic> parameter like so:
|
||||
|
||||
${makeExample(
|
||||
"Using a classic drum machine",
|
||||
`
|
||||
"Using a classic drum machine",
|
||||
`
|
||||
beat(0.5)::sound(['bd', 'cp'].pick()).bank("AkaiLinn").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
Here is the complete list of available machines:
|
||||
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Machines")}
|
||||
</div>
|
||||
|
||||
In practice, using them will lead you to write short two letters long sample names, each one for a different piece of the kit:
|
||||
|
||||
| Kit Piece | Short name |
|
||||
|-----------|----------------|
|
||||
|
|
||||
| **Bass/kick drum** | <ic>bd</ic> |
|
||||
| **Snare drum** | <ic>sd</ic> |
|
||||
| **Rimshot** | <ic>rim</ic> |
|
||||
| **Clap** | <ic>cp</ic> |
|
||||
| **Closed hi-hat** | <ic>hh</ic> |
|
||||
| **Open hi-hat** | <ic>oh</ic> |
|
||||
| **Crash** | <ic>cr</ic> |
|
||||
| **Ride** | <ic>rd</ic> |
|
||||
| **Shakers (and maracas, cabasas, etc)** | <ic>sh</ic> |
|
||||
| **High tom** | <ic>ht</ic> |
|
||||
| **Medium tom** | <ic>mt</ic> |
|
||||
| **Low tom** | <ic>lt</ic> |
|
||||
| **Cowbell** | <ic>cb</ic> |
|
||||
| **Tambourine** | <ic>tb</ic> |
|
||||
| **Other percussions** | <ic>perc</ic> |
|
||||
| **Miscellaneous samples** | <ic>misc</ic> |
|
||||
| **Effects** | <ic>fx</ic> |
|
||||
|
||||
Note that there is also a <ic>drumMachine</ic> function that allows you to play a random drum machine without even typing the name.
|
||||
It takes a single argument, a number, that will pick a machine for you in the list:
|
||||
|
||||
${makeExample(
|
||||
"Using a classic drum machine",
|
||||
`
|
||||
beat(1/2)::sound(['bd', 'cp'].pick()).drumMachine(1).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
## FoxDot sample pack
|
||||
|
||||
The default sample pack used by Ryan Kirkbride's [FoxDot](https://github.com/Qirky/FoxDot). It is a nice curated sample pack that covers all the basic sounds you could want.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "FoxDot")}
|
||||
</div>
|
||||
|
||||
@ -146,7 +110,7 @@ ${samples_to_markdown(application, "FoxDot")}
|
||||
|
||||
This set of audio samples is taken from [this wonderful collection](https://archive.org/details/AmigaSoundtrackerSamplePacksst-xx) of **Ultimate Tracker Amiga samples**. They were initially made by Karsten Obarski. These files were processed: pitched down one octave, gain down 6db. The audio has been processed with [SoX](https://github.com/chirlu/sox). The script used to do so is also included in this repository.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Amiga")}
|
||||
</div>
|
||||
|
||||
@ -155,16 +119,16 @@ ${samples_to_markdown(application, "Amiga")}
|
||||
A collection of many different amen breaks. Use <ic>.stretch()</ic> to play with these:
|
||||
|
||||
${makeExample(
|
||||
"Stretching an amen break",
|
||||
`
|
||||
"Stretching an amen break",
|
||||
`
|
||||
beat(4)::sound('amen1').stretch(4).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
The stretch should be adapted based on the length of each amen break.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Amen")}
|
||||
</div>
|
||||
|
||||
@ -173,7 +137,7 @@ ${samples_to_markdown(application, "Amen")}
|
||||
Many live coders are expecting to find the Tidal sample library wherever they go, so here it is :)
|
||||
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Tidal")}
|
||||
</div>
|
||||
|
||||
@ -181,16 +145,8 @@ ${samples_to_markdown(application, "Tidal")}
|
||||
|
||||
This sample pack is only one folder full of french phonems! It sounds super nice.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-neutral-600 mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "Juliette")}
|
||||
</div>
|
||||
|
||||
## Your samples
|
||||
|
||||
These samples are the one you have loaded for the duration of the session using the <ic>Import Samples</ic> button in the configuration menu.
|
||||
|
||||
<div class="lg:pl-6 lg:pr-6 w-fit rounded-lg bg-background mx-6 mt-2 my-6 px-2 py-2 max-h-96 flex flex-row flex-wrap gap-x-2 gap-y-2 overflow-y-scroll">
|
||||
${samples_to_markdown(application, "user")}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const synths = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
@ -13,36 +13,35 @@ Topos comes by default with a forever-increasing number of synthesis capabilitie
|
||||
The <ic>sound</ic> function can take the name of a synthesizer or waveform as first argument. This has for effect to turn the sampler we all know and love into a synthesizer. <ic>sine</ic>, <ic>sawtooth</ic>,<ic>triangle</ic>, <ic>square</ic> are the names used to select classic oscillator waveforms. Note that you can also make use of filters and envelopes to shape the sound to your liking.
|
||||
|
||||
${makeExample(
|
||||
"Listening to the different waveforms from the sweetest to the harshest",
|
||||
`
|
||||
"Listening to the different waveforms from the sweetest to the harshest",
|
||||
`
|
||||
beat(.5) && snd(['sine', 'triangle', 'sawtooth', 'square'].beat()).freq(100).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
Note that you can also use noise if you do not want to use a periodic oscillator:
|
||||
|
||||
${makeExample(
|
||||
"Listening to the different types of noise",
|
||||
`
|
||||
beat(.5) && snd(['brown', 'pink', 'white', 'crackle'].beat()).adsr(0,.1,0,0).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
The <ic>crackle</ic> type can be controlled using the <ic>density</ic> parameter.
|
||||
${makeExample(
|
||||
"Listening to the different types of noise",
|
||||
`
|
||||
beat(.5) && snd(['brown', 'pink', 'white'].beat()).adsr(0,.1,0,0).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
Two functions are primarily used to control the frequency of the synthesizer:
|
||||
- <ic>freq(hz: number)</ic>: sets the frequency of the oscillator.
|
||||
- <ic>note(note: number|string)</ic>: sets the MIDI note of the oscillator (MIDI note converted to hertz).
|
||||
|
||||
${makeExample(
|
||||
"Selecting a pitch",
|
||||
`
|
||||
"Selecting a pitch",
|
||||
`
|
||||
beat(.5) && snd('triangle').freq([100,200,400].beat(2)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Selecting a note",
|
||||
@ -57,40 +56,72 @@ Chords can also played using different parameters:
|
||||
- <ic>chord(string||number[]|...number)</ic>: parses and sets notes for the chord
|
||||
|
||||
${makeExample(
|
||||
"Playing a named chord",
|
||||
`
|
||||
"Playing a named chord",
|
||||
`
|
||||
beat(1) && snd('triangle').chord(["C","Em7","Fmaj7","Emin"].beat(2)).adsr(0,.2).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Playing a chord from a list of notes and doing inversions",
|
||||
`
|
||||
"Playing a chord from a list of notes and doing inversions",
|
||||
`
|
||||
beat(.5) && snd('triangle').chord(60,64,67,72).invert([1,-3,4,-5].pick()).adsr(0,.2).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
# Controlling amplitude
|
||||
## Vibrato
|
||||
|
||||
You can also add some amount of vibrato to the sound using the <ic>vib</ic> and <ic>vibmod</ic> methods. These can turn any oscillator into something more lively and/or into a sound effect when used with a high amount of modulation.
|
||||
|
||||
${makeExample(
|
||||
"Different vibrato settings",
|
||||
`
|
||||
tempo(140);
|
||||
beat(1) :: sound('triangle')
|
||||
.freq(400).release(0.2)
|
||||
.vib([1/2, 1, 2, 4].beat())
|
||||
.vibmod([1,2,4,8].beat(2))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Noise
|
||||
|
||||
A certain amount of brown noise can be added by using the <ic>.noise</ic> key:
|
||||
|
||||
${makeExample(
|
||||
"Different vibrato settings",
|
||||
`
|
||||
tempo(140);
|
||||
beat(1) :: sound('triangle')
|
||||
.freq(400).release(0.2)
|
||||
.noise([0.2,0.4,0.5].bar())
|
||||
.vib([1/2, 1, 2, 4].beat())
|
||||
.vibmod([1,2,4,8].beat(2))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
## Controlling the amplitude
|
||||
|
||||
Controlling the amplitude and duration of the sound can be done using various techniques. The most important thing to learn is probably how set the amplitude (volume) of your synthesizer:
|
||||
- <ic>gain(gain: number)</ic>: sets the gain of the oscillator.
|
||||
- <ic>velocity(velocity: number)</ic>: sets the velocity of the oscillator (velocity is a multiple of gain).
|
||||
|
||||
${makeExample(
|
||||
"Setting the gain",
|
||||
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
"Setting the gain",
|
||||
`beat(0.25) :: sound('sawtooth').gain([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Setting the velocity",
|
||||
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Envelopes
|
||||
"Setting the velocity",
|
||||
`beat(0.25) :: sound('sawtooth').velocity([0.0, 1/8, 1/4, 1/2, 1].beat(0.5)).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
<div class="mt-4 mb-4 lg:grid lg:grid-cols-4 lg:gap-4">
|
||||
<img class="col-span-1 lg:ml-12 bg-gray-100 rounded-lg px-2 py-2", src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/ADSR_Envelope_Graph.svg/1280px-ADSR_Envelope_Graph.svg.png" width="400" />
|
||||
@ -103,83 +134,143 @@ ${makeExample(
|
||||
- <ic>release(release: number)</ic> / <ic>rel(rel: number)</ic>: sets the release time of the envelope.
|
||||
|
||||
${makeExample(
|
||||
"Using decay and sustain to set the ADSR envelope",
|
||||
`
|
||||
"Using decay and sustain to set the ADSR envelope",
|
||||
`
|
||||
beat(0.5) :: sound('wt_piano')
|
||||
.cutoff(1000 + usine() * 4000)
|
||||
.freq(100).decay(.2)
|
||||
.sustain([0.1,0.5].beat(4))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
This ADSR envelope design is important to know because it is used for other aspects of the synthesis engine such as the filters that we are now going to talk about. But wait, I've kept the best for the end. The <ic>adsr()</ic> combines all the parameters together. It is a shortcut for setting the ADSR envelope:
|
||||
|
||||
- <ic>adsr(attack: number, decay: number, sustain: number, release: number)</ic>: sets the ADSR envelope.
|
||||
|
||||
${makeExample(
|
||||
"Replacing the previous example with the adsr() method",
|
||||
`
|
||||
"Replacing the previous example with the adsr() method",
|
||||
`
|
||||
beat(0.5) :: sound('wt_piano')
|
||||
.cutoff(1000 + usine() * 4000)
|
||||
.freq(100)
|
||||
.adsr(0, .2, [0.1,0.5].beat(4), 0)
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
- <ic>ad(attack: number, decay: number)</ic>: sets the attack and decay phases, setting sustain and release to <ic>0</ic>.
|
||||
|
||||
${makeExample(
|
||||
"Two segment envelope",
|
||||
`
|
||||
"Two segment envelope",
|
||||
`
|
||||
beat(0.5) :: sound('wt_piano')
|
||||
.cutoff(1000 + usine() * 4000)
|
||||
.freq(100)
|
||||
.ad(0, .2)
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
## Substractive synthesis using filters
|
||||
|
||||
The most basic synthesis technique used since the 1970s is called substractive synthesis. This technique is based on the use of rich sound sources (oscillators) as a base to build rich and moving timbres. Because rich sources contain a lot of different harmonics, you might want to filter some of them to obtain the timbre you are looking for. To do so, Topos comes with a set of basic filters that can be used to shape the sound exactly to your liking. There are three filter types by defaut, with more to be added in the future:
|
||||
|
||||
See the Filters page for details on lowpass, highpass and bandpass filters. I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers:
|
||||
- **lowpass filter**: filters the high frequencies, keeping the low frequencies.
|
||||
- **highpass filter**: filtering the low frequencies, keeping the high frequencies.
|
||||
- **bandpass filter**: filters the low and high frequencies around a frequency band, keeping what's in the middle.
|
||||
|
||||
${makeExample(
|
||||
"Filtering the high frequencies of an oscillator",
|
||||
`beat(.5) :: sound('sawtooth').cutoff(50 + usine(1/8) * 2000).out()`,
|
||||
true,
|
||||
)}
|
||||
"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(
|
||||
"Simple synthesizer voice with filter",
|
||||
`
|
||||
"Filtering a bass",
|
||||
`beat(.5) :: sound('jvbass').lpf([250,1000,8000].beat()).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
### Highpass filter
|
||||
|
||||
| Method | Alias | Description |
|
||||
|------------|-----------|---------------------------------|
|
||||
| <ic>hcutoff</ic> | <ic>hpf</ic> | cutoff frequency of the highpass filter |
|
||||
| <ic>hresonance</ic> | <ic>hpq</ic> | resonance of the highpass filter (0-1) |
|
||||
|
||||
${makeExample(
|
||||
"Filtering a noise source",
|
||||
`beat(.5) :: sound('gtr').hpf([250,1000, 2000, 3000, 4000].beat()).end(0.5).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
### Bandpass filter
|
||||
|
||||
| Method | Alias | Description |
|
||||
|------------|-----------|---------------------------------|
|
||||
| <ic>bandf</ic> | <ic>bpf</ic> | cutoff frequency of the bandpass filter |
|
||||
| <ic>bandq</ic> | <ic>bpq</ic> | resonance of the bandpass filter (0-1) |
|
||||
|
||||
${makeExample(
|
||||
"Sweeping the filter on the same guitar sample",
|
||||
`beat(.5) :: sound('gtr').bandf(100 + usine(1/8) * 4000).end(0.5).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
Alternatively, <ic>lpf</ic>, <ic>hpf</ic> and <ic>bpf</ic> can take a second argument, the **resonance**.
|
||||
|
||||
## Filter order (type)
|
||||
|
||||
You can also use the <ic>ftype</ic> method to change the filter type (order). There are two types by default, <ic>12db</ic> for a gentle slope or <ic>24db</ic> for a really steep filtering slope. The <ic>24db</ic> type is particularly useful for substractive synthesis if you are trying to emulate some of the Moog or Prophet sounds:
|
||||
|
||||
- <ic>ftype(type: string)</ic>: sets the filter type (order), either <ic>12db</ic> or <ic>24db</ic>.
|
||||
|
||||
${makeExample(
|
||||
"Filtering a bass",
|
||||
`beat(.5) :: sound('jvbass').ftype(['12db', '24db'].beat(4)).lpf([250,1000,8000].beat()).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
I also encourage you to study these simple examples to get more familiar with the construction of basic substractive synthesizers:
|
||||
|
||||
${makeExample(
|
||||
"Simple synthesizer voice with filter",
|
||||
`
|
||||
beat(.5) && snd('sawtooth')
|
||||
.cutoff([2000,500].pick() + usine(.5) * 4000)
|
||||
.resonance(0.2).freq([100,150].pick())
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Blessed by the square wave",
|
||||
`
|
||||
"Blessed by the square wave",
|
||||
`
|
||||
beat(4) :: [100,101].forEach((freq) => sound('square').freq(freq).sustain(0.1).out())
|
||||
beat(.5) :: [100,101].forEach((freq) => sound('square').freq(freq*2).sustain(0.01).out())
|
||||
beat([.5, .75, 2].beat()) :: [100,101].forEach((freq) => sound('square')
|
||||
.freq(freq*4 + usquare(2) * 200).sustain(0.125).out())
|
||||
beat(.25) :: sound('square').freq(100*[1,2,4,8].beat()).sustain(0.1).out()`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Ghost carillon (move your mouse!)",
|
||||
`
|
||||
"Ghost carillon (move your mouse!)",
|
||||
`
|
||||
beat(1/8)::sound('sine')
|
||||
.velocity(rand(0.0, 1.0))
|
||||
.delay(0.75).delayt(.5)
|
||||
@ -188,33 +279,83 @@ beat(1/8)::sound('sine')
|
||||
.freq(mouseX())
|
||||
.gain(0.25)
|
||||
.out()`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
## Noise
|
||||
## Filter envelopes
|
||||
|
||||
The examples we have studied so far are static. They filter the sound around a fixed cutoff frequency. To make the sound more interesting, you can use the ADSR filter envelopes to shape the filter cutoff frequency over time. You will always find amplitude and filter envelopes on most commercial synthesizers. This is done using the following methods:
|
||||
|
||||
### Lowpass envelope
|
||||
|
||||
| Method | Alias | Description |
|
||||
|------------|-----------|---------------------------------|
|
||||
| <ic>lpenv</ic> | <ic>lpe</ic> | lowpass frequency modulation amount (negative or positive) |
|
||||
| <ic>lpattack</ic> | <ic>lpa</ic> | attack of the lowpass filter |
|
||||
| <ic>lpdecay</ic> | <ic>lpd</ic> | decay of the lowpass filter |
|
||||
| <ic>lpsustain</ic> | <ic>lps</ic> | sustain of the lowpass filter |
|
||||
| <ic>lprelease</ic> | <ic>lpr</ic> | release of the lowpass filter |
|
||||
| <ic>lpadsr</ic> | | (**takes five arguments**) set all the parameters |
|
||||
|
||||
A certain amount of brown noise can be added by using the <ic>.noise</ic> key:
|
||||
|
||||
${makeExample(
|
||||
"Different vibrato settings",
|
||||
`
|
||||
tempo(140);
|
||||
beat(1) :: sound('triangle')
|
||||
.freq(400).release(0.2)
|
||||
.noise([0.2,0.4,0.5].bar())
|
||||
.vib([1/2, 1, 2, 4].beat())
|
||||
.vibmod([1,2,4,8].beat(2))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
"Filtering a sawtooth wave dynamically",
|
||||
`beat(.5) :: sound('sawtooth').note([48,60].beat())
|
||||
.cutoff(5000).lpa([0.05, 0.25, 0.5].beat(2))
|
||||
.lpenv(-8).lpq(10).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
### Highpass envelope
|
||||
|
||||
| Method | Alias | Description |
|
||||
|------------|-----------|---------------------------------|
|
||||
| <ic>hpenv</ic> | <ic>hpe</ic> | highpass frequency modulation amount (negative or positive) |
|
||||
| <ic>hpattack</ic> | <ic>hpa</ic> | attack of the highpass filter |
|
||||
| <ic>hpdecay</ic> | <ic>hpd</ic> | decay of the highpass filter |
|
||||
| <ic>hpsustain</ic> | <ic>hps</ic> | sustain of the highpass filter |
|
||||
| <ic>hprelease</ic> | <ic>hpr</ic> | release of the highpass filter |
|
||||
| <ic>hpadsr</ic> | | (**takes five arguments**) set all the parameters |
|
||||
|
||||
|
||||
${makeExample(
|
||||
"Let's use another filter using the same example",
|
||||
`beat(.5) :: sound('sawtooth').note([48,60].beat())
|
||||
.hcutoff(1000).hpa([0.05, 0.25, 0.5].beat(2))
|
||||
.hpenv(8).hpq(10).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
### Bandpass envelope
|
||||
|
||||
| Method | Alias | Description |
|
||||
|------------|-----------|---------------------------------|
|
||||
| <ic>bpenv</ic> | <ic>bpe</ic> | bandpass frequency modulation amount (negative or positive) |
|
||||
| <ic>bpattack</ic> | <ic>bpa</ic> | attack of the bandpass filter |
|
||||
| <ic>bpdecay</ic> | <ic>bpd</ic> | decay of the bandpass filter |
|
||||
| <ic>bpsustain</ic> | <ic>bps</ic> | sustain of the bandpass filter |
|
||||
| <ic>bprelease</ic> | <ic>bpr</ic> | release of the bandpass filter |
|
||||
| <ic>bpadsr</ic> | | (**takes five arguments**) set all the parameters |
|
||||
|
||||
|
||||
${makeExample(
|
||||
"And the bandpass filter, just for fun",
|
||||
`beat(.5) :: sound('sawtooth').note([48,60].beat())
|
||||
.bandf([500,1000,2000].beat(2))
|
||||
.bpa([0.25, 0.125, 0.5].beat(2) * 4)
|
||||
.bpenv(-4).release(2).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
## Wavetable synthesis
|
||||
|
||||
Topos can also do wavetable synthesis. Wavetable synthesis allows you to use any sound file as a source to build an oscillator. By default, Topos comes with more than 1000 waveforms thanks to the awesome [AKWF](https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/) pack made by Kristoffer Ekstrand. Any sample name that contains <ic>wt_</ic> as a prefix will be interpreted by the sampler as a wavetable and thus as an oscillator. See for yourself:
|
||||
|
||||
${makeExample(
|
||||
"Acidity test",
|
||||
`
|
||||
"Acidity test",
|
||||
`
|
||||
beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0]
|
||||
.pick()).ftype('12db').adsr(0.05/4, 1/16, 0.25/4, 0)
|
||||
.cutoff(1500 + usine(1/8) * 5000).lpadsr(16, 0.2, 0.2, 0.125/2, 0)
|
||||
@ -222,15 +363,15 @@ beat(.25) :: sound('wt_symetric:8').note([50,55,57,60].beat(.25) - [12,0]
|
||||
beat(1) :: sound('kick').n(4).out()
|
||||
beat(2) :: sound('snare').out()
|
||||
beat(.5) :: sound('hh').out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
Let's explore the galaxy of possible waveforms. It can be hard to explore them all, there is a **lot** of them:
|
||||
|
||||
${makeExample(
|
||||
"Let's explore some wavetables",
|
||||
`
|
||||
"Let's explore some wavetables",
|
||||
`
|
||||
// Exploring a vast galaxy of waveforms
|
||||
let collection = [
|
||||
'wt_sinharm', 'wt_linear', 'wt_bw_sawrounded',
|
||||
@ -240,8 +381,8 @@ beat(2) :: v('selec', irand(1, 100))
|
||||
beat(2) :: v('swave', collection.pick())
|
||||
beat(0.5) :: sound(v('swave')).n(v('selec')).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
You can work with them just like with any other waveform. Having so many of them makes them also very useful for generating sound effects, percussive, sounds, etc...
|
||||
|
||||
@ -256,8 +397,8 @@ Another really useful technique to know about is FM synthesis, FM standing for _
|
||||
There is also an additional parameter, <ic>fm</ic> that combines <ic>fmi</ic> and <ic>fmh</ic> using strings: <ic>fm('2:4')</ic>. Think of it as a static shortcut for getting some timbres more quickly.
|
||||
|
||||
${makeExample(
|
||||
"80s nostalgia",
|
||||
`
|
||||
"80s nostalgia",
|
||||
`
|
||||
beat([.5, 1].beat(8)) && snd('triangle').adsr(0.02, 0.5, 0.5, 0.25)
|
||||
.fmi(2).fmh(1.5).note([60,55, 60, 63].beat() - 12)
|
||||
.pan(noise()).out()
|
||||
@ -266,23 +407,23 @@ beat(.25) && snd('triangle').adsr(0.02, 0.1, 0.1, 0.1)
|
||||
.pan(noise()).note([60,55, 60, 63].beat() + [0, 7].pick()).out()
|
||||
beat(2) :: sound('cp').room(1).sz(1).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Giving some love to ugly inharmonic sounds",
|
||||
`
|
||||
"Giving some love to ugly inharmonic sounds",
|
||||
`
|
||||
beat([.5, .25].bar()) :: sound('sine').fm('2.2183:3.18293').sustain(0.05).out()
|
||||
beat([4].bar()) :: sound('sine').fm('5.2183:4.5').sustain(0.05).out()
|
||||
beat(.5) :: sound('sine')
|
||||
.fmh([1, 1.75].beat())
|
||||
.fmi($(1) % 30).orbit(2).room(0.5).out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Peace and serenity through FM synthesis",
|
||||
`
|
||||
"Peace and serenity through FM synthesis",
|
||||
`
|
||||
beat(0.25) :: sound('sine')
|
||||
.note([60, 67, 70, 72, 77].beat() - [0,12].bar())
|
||||
.attack(0.2).release(0.5).gain(0.25)
|
||||
@ -291,8 +432,8 @@ beat(0.25) :: sound('sine')
|
||||
.cutoff(1500).delay(0.5).delayt(0.125)
|
||||
.delayfb(0.8).fmh(Math.floor(usine(.5) * 4))
|
||||
.out()`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
**Note:** you can also set the _modulation index_ and the _harmonic ratio_ with the <ic>fm</ic> argument. You will have to feed both as a string: <ic>fm('2:4')</ic>. If you only feed one number, only the _modulation index_ will be updated.
|
||||
|
||||
@ -303,8 +444,8 @@ There is also a more advanced set of parameters you can use to control the envel
|
||||
- <ic>fmrelease</ic> / <ic>fmrel</ic>: release time of the modulator envelope.
|
||||
|
||||
${makeExample(
|
||||
"FM Synthesis with envelope control",
|
||||
`
|
||||
"FM Synthesis with envelope control",
|
||||
`
|
||||
beat(.5) :: sound('sine')
|
||||
.note([50,53,55,57].beat(.5) - 12)
|
||||
.fmi(0.5 + usine(.25) * 1.5)
|
||||
@ -312,8 +453,8 @@ beat(.5) :: sound('sine')
|
||||
.fmwave('triangle')
|
||||
.fmsus(0).fmdec(0.2).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
## ZzFX
|
||||
|
||||
@ -322,15 +463,15 @@ beat(.5) :: sound('sine')
|
||||
ZZfX can be triggered by picking a default ZZfX waveform in the following list: <ic>z_sine</ic>, <ic>z_triangle</ic>, <ic>z_sawtooth</ic>, <ic>z_tan</ic>, <ic>z_noise</ic>.
|
||||
|
||||
${makeExample(
|
||||
"Picking a waveform",
|
||||
`
|
||||
"Picking a waveform",
|
||||
`
|
||||
beat(.5) :: sound(['z_sine', 'z_triangle', 'z_sawtooth', 'z_tan', 'z_noise'].beat()).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
${makeExample(
|
||||
"Minimalist chiptune",
|
||||
`
|
||||
"Minimalist chiptune",
|
||||
`
|
||||
beat(.5) :: sound('z_triangle')
|
||||
.note([60, 67, 72, 63, 65, 70].beat(.5))
|
||||
.zrand(0).curve([1,2,3,4].beat(1))
|
||||
@ -340,8 +481,8 @@ beat(.5) :: sound('z_triangle')
|
||||
.room(0.5).size(0.9)
|
||||
.pitchJumpTime(0.01).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
It comes with a set of parameters that can be used to tweak the sound. Don't underestimate this synth! It is very powerful for generating anything ranging from chaotic noise sources to lush pads:
|
||||
|
||||
@ -367,8 +508,8 @@ It comes with a set of parameters that can be used to tweak the sound. Don't und
|
||||
|<ic>duration</ic>|| Total sound duration (overrides envelope) |
|
||||
|
||||
${makeExample(
|
||||
"Chaotic Noise source",
|
||||
`
|
||||
"Chaotic Noise source",
|
||||
`
|
||||
beat(.25) :: sound('z_tan')
|
||||
.note(40).noise(rand(0.0, 1.0))
|
||||
.pitchJump(84).pitchJumpTime(rand(0.0, 1.0))
|
||||
@ -378,21 +519,21 @@ beat(.25) :: sound('z_tan')
|
||||
.sustain(0).decay([0.2, 0.1].pick())
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
${makeExample(
|
||||
"What is happening to me?",
|
||||
`
|
||||
"What is happening to me?",
|
||||
`
|
||||
beat(1) :: snd('zzfx').zzfx([
|
||||
[4.77,,25,,.15,.2,3,.21,,2.4,,,,,,,.23,.35],
|
||||
[1.12,,97,.11,.16,.01,4,.77,,,30,.17,,,-1.9,,.01,.67,.2]
|
||||
].beat()).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
${makeExample(
|
||||
"Les voitures dans le futur",
|
||||
`
|
||||
"Les voitures dans le futur",
|
||||
`
|
||||
beat(1) :: sound(['z_triangle', 'z_sine'].pick())
|
||||
.note([60,63,72,75].pick()).tremolo(16)
|
||||
.zmod([0, 1/2, 1/8].div(2).pick())
|
||||
@ -400,25 +541,23 @@ beat(1) :: sound(['z_triangle', 'z_sine'].pick())
|
||||
.room(0.9).size(0.9)
|
||||
.delayt(0.75).delayfb(0.5).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
false,
|
||||
)}
|
||||
|
||||
Note that you can also design sounds [on this website](https://killedbyapixel.github.io/ZzFX/) and copy the generated code in Topos. To do so, please use the <ic>zzfx</ic> method with the generated array:
|
||||
${makeExample(
|
||||
"Designing a sound on the ZzFX website",
|
||||
`
|
||||
"Designing a sound on the ZzFX website",
|
||||
`
|
||||
|
||||
beat(2) :: sound('zzfx').zzfx([3.62,,452,.16,.1,.21,,2.5,,,403,.05,.29,,,,.17,.34,.22,.68]).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
# Speech synthesis
|
||||
|
||||
Topos can also speak using the [Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API). There are two ways to use speech synthesis:
|
||||
|
||||
Speech synthesis API can crash your browser if you use it too much. To avoid crashing the calls should be limited using methods like beat() or run it only once using once().
|
||||
|
||||
- <ic>speak(text: string, lang: string, voice: number, rate: number, pitch: number, volume: number)</ic>
|
||||
- <ic>text</ic>: the text you would like to synthesize (_e.g_ <ic>"Wow, Topos can speak!"</ic>).
|
||||
- <ic>lang</ic>: language code, for example <ic>en</ic> for English, <ic>fr</ic> for French or with the country code for example British English <ic>en-GB</ic>. See supported values from the [list](https://cloud.google.com/speech-to-text/docs/speech-to-text-supported-languages).
|
||||
@ -428,48 +567,48 @@ Speech synthesis API can crash your browser if you use it too much. To avoid cra
|
||||
- <ic>volume(number)</ic>: speaking volume, from <ic>0.0</ic> to <ic>1.0</ic>.
|
||||
|
||||
${makeExample(
|
||||
"Hello world!",
|
||||
`
|
||||
once() && speak("Hello world!")
|
||||
"Hello world!",
|
||||
`
|
||||
beat(4) :: speak("Hello world!")
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Let's hear people talking about Topos",
|
||||
`
|
||||
beat(2) && speak("Topos!","fr",irand(0,5))
|
||||
"Let's hear people talking about Topos",
|
||||
`
|
||||
beat(2) :: speak("Topos!","fr",irand(0,5))
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
You can also use speech by chaining methods to a string:
|
||||
|
||||
${makeExample(
|
||||
"Foobaba is the real deal",
|
||||
`
|
||||
onbeat(4) && "Foobaba".voice(irand(0,10)).speak()
|
||||
"Foobaba is the real deal",
|
||||
`
|
||||
onbeat(4) :: "Foobaba".voice(irand(0,10)).speak()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Building string and chaining",
|
||||
`
|
||||
"Building string and chaining",
|
||||
`
|
||||
const subject = ["coder","user","loser"].pick()
|
||||
const verb = ["is", "was", "isnt"].pick()
|
||||
const object = ["happy","sad","tired"].pick()
|
||||
const sentence = subject+" "+verb+" "+" "+object
|
||||
|
||||
beat(6) && sentence.pitch(0).rate(0).voice([0,2].pick()).speak()
|
||||
beat(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).speak()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Live coded poetry with array and string chaining",
|
||||
`
|
||||
"Live coded poetry with array and string chaining",
|
||||
`
|
||||
tempo(70)
|
||||
|
||||
const croissant = [
|
||||
@ -478,13 +617,13 @@ ${makeExample(
|
||||
"Flamboyant", "Cosmique", "Croissant!"
|
||||
];
|
||||
|
||||
onbeat(4) && croissant.bar()
|
||||
onbeat(4) :: croissant.bar()
|
||||
.lang("fr")
|
||||
.volume(rand(0.2,2.0))
|
||||
.rate(rand(.4,.6))
|
||||
.speak();
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
true,
|
||||
)}
|
||||
`;
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
|
||||
export const cyclical_time = (app: Editor): string => {
|
||||
// @ts-ignore
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import pulses from "./pulses.svg";
|
||||
|
||||
export const linear_time = (app: Editor): string => {
|
||||
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
@ -1,5 +1,5 @@
|
||||
import { makeExampleFactory } from "../../../Documentation";
|
||||
import { type Editor } from "../../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../../main";
|
||||
import times from "./times.svg";
|
||||
|
||||
export const time = (application: Editor): string => {
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
@ -1,5 +1,5 @@
|
||||
import { type Editor } from "../../main";
|
||||
import { makeExampleFactory } from "../../Documentation";
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const variables = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
@ -7,15 +7,20 @@ export const variables = (application: Editor): string => {
|
||||
|
||||
# Variables
|
||||
|
||||
By default, each script is independant from each other. The variables defined in **script 1** are not available in **script 2**, etc. Moreover, they are overriden everytime the file is evaluated. It means that you cannot store any state or share information. However, you can use global variables to make that possible.
|
||||
By default, each script is independant from each other. Scripts live in their own bubble and you cannot get or set variables affecting a script from any other script.
|
||||
|
||||
There is a <ic>global</ic> object that you can use to store and retrieve information. It is a simple key/value store. You can store any type of data in it:
|
||||
**However**, everybody knows that global variables are cool and should be used everywhere. Global variables are an incredibely powerful tool to radically alter a composition in a few lines of code.
|
||||
|
||||
- <ic>variable(a: number | string, b?: any)</ic>: if only one argument is provided, the value of the variable will be returned through its name, denoted by the first argument. If a second argument is used, it will be saved as a global variable under the name of the first argument.
|
||||
- <ic>delete_variable(name: string)</ic>: deletes a global variable from storage.
|
||||
- <ic>clear_variables()</ic>: clear **ALL** variables. **This is a destructive operation**!
|
||||
|
||||
**Note:** since this example is running in the documentation, we cannot take advantage of the multiple scripts paradigm. Try to send a variable from the global file to the local file n°6.
|
||||
|
||||
${makeExample(
|
||||
"Setting a global variable",
|
||||
`
|
||||
// This is script n°3
|
||||
global.my_variable = 2
|
||||
v('my_cool_variable', 2)
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
@ -23,16 +28,15 @@ global.my_variable = 2
|
||||
${makeExample(
|
||||
"Getting that variable back and printing!",
|
||||
`
|
||||
// This is script n°4
|
||||
log(global.my_variable)
|
||||
// Note that we just use one argument
|
||||
log(v('my_cool_variable'))
|
||||
`,
|
||||
true,
|
||||
false,
|
||||
)}
|
||||
|
||||
Now your scripts can share information with each other!
|
||||
|
||||
## Counter and iterators
|
||||
|
||||
|
||||
You will often need to use iterators and/or counters to index over data structures (getting a note from a list of notes, etc...). There are functions ready to be used for this. Each script also comes with its own iterator that you can access using the <ic>i</ic> variable. **Note:** the script iteration count is **not** resetted between sessions. It will continue to increase the more you play, even if you just picked up an old project.
|
||||
|
||||
- <ic>counter(name: number | string, limit?: number, step?: number)</ic>: reads the value of the counter <ic>name</ic>. You can also call this function using the dollar symbol: <ic>$</ic>.
|
||||
554
src/documentation/ziffers.ts
Normal file
@ -0,0 +1,554 @@
|
||||
import { type Editor } from "../main";
|
||||
import { makeExampleFactory } from "../Documentation";
|
||||
|
||||
export const ziffers = (application: Editor): string => {
|
||||
const makeExample = makeExampleFactory(application);
|
||||
return `
|
||||
# Ziffers
|
||||
|
||||
Ziffers is a **musical number based notation** tuned for _live coding_. It is a very powerful and flexible notation for describing musical patterns in very few characters. Number based musical notation has a long history and has been used for centuries as a shorthand technique for music notation. Amiika has written [papers](https://zenodo.org/record/7841945) and other documents describing his system. It is currently implemented for many live coding platforms including [Sardine](https://sardine.raphaelforment.fr) (Raphaël Forment) and [Sonic Pi](https://sonic-pi.net/) (Sam Aaron). Ziffers can be used for:
|
||||
|
||||
- composing melodies using using **classical music notation and concepts**.
|
||||
- exploring **generative / aleatoric / stochastic** melodies and applying them to sounds and synths.
|
||||
- embracing a different mindset and approach to time and **patterning**.
|
||||
|
||||
${makeExample(
|
||||
"Super Fancy Ziffers example",
|
||||
`
|
||||
z1('1/8 024!3 035 024 0124').sound('wt_stereo')
|
||||
.adsr(0, .4, 0.5, .4).gain(0.1)
|
||||
.lpadsr(4, 0, .2, 0, 0)
|
||||
.cutoff(5000 + usine(1/2) * 2000)
|
||||
.n([1,2,4].beat(4)).out()
|
||||
z2('<1/8 1/16> __ 0 <(^) (^ ^)> (0,8)').sound('wt_stereo')
|
||||
.adsr(0, .5, 0.5, .4).gain(0.2)
|
||||
.lpadsr(4, 0, .2, 0, 0).n(14)
|
||||
.cutoff(200 + usine(1/2) * 4000)
|
||||
.n([1,2,4].beat(4)).o(2).room(0.9).out()
|
||||
let osci = 1500 + usine(1/2) * 2000;
|
||||
z3('can can:2').sound().gain(1).cutoff(osci).out()
|
||||
z4('1/4 kick kick snare kick').sound().gain(1).cutoff(osci).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Notation
|
||||
|
||||
The basic Ziffer notation is entirely written in JavaScript strings (_e.g_ <ic>"0 1 2"</ic>). It consists mostly of numbers and letters. The whitespace character is used as a separator. Instead of note names, Ziffer is using numbers to represent musical pitch and letters to represent musical durations. Alternatively, _floating point numbers_ can also be used to represent durations.
|
||||
|
||||
| Syntax | Symbol | Description |
|
||||
|------------ |--------|------------------------|
|
||||
| **Pitches** | <ic>0-9</ic> <ic>{10 11 21}</ic> | Numbers or escaped numbers in curly brackets |
|
||||
| **Duration** | <ic>0.25</ic>, <ic>0.5</ic> | Floating point numbers can also be used as durations |
|
||||
| **Duration** | <ic>1/4</ic>, <ic>1/16</ic> | Fractions can be used as durations |
|
||||
| **Subdivision** | <ic>[1 [2 3]]</ic> | Durations can be subdivided using square brackets |
|
||||
| **Octave** | <ic>^ _</ic> | <ic>^</ic> for octave up and <ic>_</ic> for octave down |
|
||||
| **Accidentals** | <ic># b</ic> | Sharp and flats, just like with regular music notation :smile: |
|
||||
| **Rest** | <ic>r</ic> | Rest / silences |
|
||||
| **Repeat** | <ic>!1-9</ic> | Repeat the item 1 to 9 times |
|
||||
| **Chords** | <ic>[1-9]+ / [iv]+ / [AG]+name</ic> | Multiple pitches grouped together, roman numerals or named chords |
|
||||
| **Samples** | <ic>[a-z0-9_]+</ic> | Samples can be used pitched or unpitched |
|
||||
| **Index/Channel** | <ic>[a-z0-9]+:[0-9]*</ic> | Samples or midi channel can be changed using a colon |
|
||||
|
||||
**Note:** Some features are experimental and some are still unsupported. For full / prior syntax see article about <a href="https://zenodo.org/record/7841945" target="_blank">Ziffers</a>.
|
||||
|
||||
${makeExample(
|
||||
"Pitches from 0 to 9",
|
||||
`
|
||||
z1('0.25 0 1 2 3 4 5 6 7 8 9').sound('wt_stereo')
|
||||
.adsr(0, .1, 0, 0).out()`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Escaped pitches using curly brackets",
|
||||
`z1('_ _ 0 {9 10 11} 4 {12 13 14}')
|
||||
.sound('wt_05').pan(r(0,1))
|
||||
.cutoff(usaw(1/2) * 4000)
|
||||
.room(0.9).size(0.9).out()`,
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Durations using fractions and floating point numbers",
|
||||
`
|
||||
z1('1/8 0 2 4 0 2 4 1/4 0 3 5 0.25 _ 0 7 0 7')
|
||||
.sound('square').delay(0.5).delayt(1/8)
|
||||
.adsr(0, .1, 0, 0).delayfb(0.45).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Disco was invented thanks to Ziffers",
|
||||
`
|
||||
z1('e _ _ 0 ^ 0 _ 0 ^ 0').sound('jvbass').out()
|
||||
beat(1)::snd('bd').out(); beat(2)::snd('sd').out()
|
||||
beat(3) :: snd('cp').room(0.5).size(0.5).orbit(2).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Accidentals and rests for nice melodies",
|
||||
`
|
||||
z1('^ 1/8 0 1 b2 3 4 _ 4 b5 4 3 b2 1 0')
|
||||
.scale('major').sound('triangle')
|
||||
.cutoff(500).lpadsr(5, 0, 1/12, 0, 0)
|
||||
.fmi(0.5).fmh(2).delay(0.5).delayt(1/3)
|
||||
.adsr(0, .1, 0, 0).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Repeat items n-times",
|
||||
`
|
||||
z1('1/8 _ _ 0!4 3!4 4!4 3!4')
|
||||
.scale('major').sound('wt_oboe')
|
||||
.shape(0.2).sustain(0.1).out()
|
||||
z2('1/8 _ 0!4 5!4 4!2 7!2')
|
||||
.scale('major').sound('wt_oboe')
|
||||
.shape(0.2).sustain(0.1).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Subdivided durations",
|
||||
`
|
||||
z1('w [0 [5 [3 7]]] h [0 4]')
|
||||
.scale('major').sound('sine')
|
||||
.fmi(usine(.5)).fmh(2).out()
|
||||
`,
|
||||
false,
|
||||
)}
|
||||
|
||||
## Chords
|
||||
|
||||
Chords can be build by grouping pitches or using roman numeral notation, or by using named chords.
|
||||
|
||||
${makeExample(
|
||||
"Chords from pitches",
|
||||
`
|
||||
z1('1.0 024 045 058 046 014')
|
||||
.sound('sine').adsr(0.5, 1, 0, 0)
|
||||
.room(0.5).size(0.9)
|
||||
.scale("minor").out()
|
||||
`,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Chords from roman numerals",
|
||||
`
|
||||
z1('2/4 i vi ii v')
|
||||
.sound('triangle').adsr(0.2, 0.3, 0, 0)
|
||||
.room(0.5).size(0.9).scale("major").out()
|
||||
`,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Named chords with repeats",
|
||||
`
|
||||
z1('0.25 Bmaj7!2 D7!2 _ Gmaj7!2 Bb7!2 ^ Ebmaj7!2')
|
||||
.sound('square').room(0.5).cutoff(500)
|
||||
.lpadsr(4, 0, .4, 0, 0).size(0.9)
|
||||
.scale("major").out()
|
||||
`,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Transposing chords",
|
||||
`
|
||||
z1('q Amin!2').key(["A2", "E2"].beat(4))
|
||||
.sound('sawtooth').cutoff(500)
|
||||
.lpadsr(2, 0, .5, 0, 0, 0).out()`,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Chord transposition with roman numerals",
|
||||
`
|
||||
z1('i i v%-4 v%-2 vi%-5 vi%-3 iv%-2 iv%-1')
|
||||
.sound('triangle').adsr(1/16, 1/5, 0.1, 0)
|
||||
.delay(0.5).delayt([1/8, 1/4].beat(4))
|
||||
.delayfb(0.5).out()
|
||||
beat(4) :: sound('breaks165').stretch(4).out()
|
||||
`,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Chord transposition with named chords",
|
||||
`
|
||||
z1('1/4 Cmin!3 Fmin!3 Fmin%-1 Fmin%-2 Fmin%-1')
|
||||
.sound("sine").bpf(500 + usine(1/4) * 2000)
|
||||
.out()
|
||||
`,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Programmatic inversions",
|
||||
`
|
||||
z1('1/6 i v 1/3 vi iv').invert([1,-1,-2,0].beat(4))
|
||||
.sound("sawtooth").cutoff(1000)
|
||||
.lpadsr(2, 0, .2, 0, 0).out()
|
||||
`,
|
||||
)}
|
||||
|
||||
## Algorithmic operations
|
||||
|
||||
Ziffers provides shorthands for **many** numeric and algorithimic operations such as evaluating random numbers and creating sequences using list operations:
|
||||
|
||||
* **List operations:** Cartesian operation (_e.g._ <ic>(3 2 1)+(2 5)</ic>) using the <ic>+</ic> operator. All the arithmetic operators are supported.
|
||||
|
||||
${makeExample(
|
||||
"Element-wise operations for melodic generation",
|
||||
`
|
||||
z1("1/8 _ 0 (0 1 3)+(1 2) 0 (2 3 5)-(1 2)").sound('sine')
|
||||
.scale('pentatonic').fmi([0.25,0.5].beat(2)).fmh([2,4].beat(2))
|
||||
.room(0.9).size(0.9).sustain(0.1).delay(0.5).delay(0.125)
|
||||
.delayfb(0.25).out();
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
* **Random numbers:** <ic>(4,6)</ic> Random number between 4 and 6
|
||||
|
||||
${makeExample(
|
||||
"Random numbers, true computer music at last!",
|
||||
`
|
||||
z1("s _ (0,8) 0 0 (0,5) 0 0").sound('sine')
|
||||
.adsr(0, .1, 0, 0).scale('minor')
|
||||
.fmdec(0.25).fmi(2).fmh([0.5, 0.25].beat(2))
|
||||
.room(0.9).size(0.5).sustain(0.1) .delay(0.5)
|
||||
.delay(0.125).delayfb(0.25).out();
|
||||
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Keys and scales
|
||||
|
||||
Ziffers supports all the keys and scales. Keys can be defined by using [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation), for example <ic>F3</ic>. Western style (1490 scales) can be with scale names named after greek modes and extended by [William Zeitler](https://ianring.com/musictheory/scales/traditions/zeitler). You will never really run out of scales to play with using Ziffers. Here is a short list of some possible scales that you can play with:
|
||||
|
||||
| Scale name | Intervals |
|
||||
|------------|------------------------|
|
||||
| Lydian | <ic>2221221</ic> |
|
||||
| Mixolydian | <ic>2212212</ic> |
|
||||
| Aeolian | <ic>2122122</ic> |
|
||||
| Locrian | <ic>1221222</ic> |
|
||||
| Ionian | <ic>2212221</ic> |
|
||||
| Dorian | <ic>2122212</ic> |
|
||||
| Phrygian | <ic>1222122</ic> |
|
||||
| Soryllic | <ic>11122122</ic>|
|
||||
| Modimic | <ic>412122</ic> |
|
||||
| Ionalian | <ic>1312122</ic> |
|
||||
| ... | And it goes on for **1490** scales |
|
||||
|
||||
${makeExample(
|
||||
"What the hell is the Modimic scale?",
|
||||
`
|
||||
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
|
||||
.scale('modimic').fmi(2).fmh(2).room(0.5)
|
||||
.size(0.5).sustain(0.1) .delay(0.5)
|
||||
.delay(0.125).delayfb(0.25).out();
|
||||
beat(.5) :: snd(['kick', 'hat'].beat(.5)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<ic></ic>
|
||||
|
||||
You can also use more traditional <a href="https://ianring.com/musictheory/scales/traditions/western" target="_blank">western names</a>:
|
||||
|
||||
|
||||
| Scale name | Intervals |
|
||||
|------------|------------------------|
|
||||
| Major | <ic>2212221</ic> |
|
||||
| Minor | <ic>2122122</ic> |
|
||||
| Minor pentatonic | <ic>32232</ic> |
|
||||
| Harmonic minor | <ic>2122131</ic>|
|
||||
| Harmonic major | <ic>2212131</ic>|
|
||||
| Melodic minor | <ic>2122221</ic>|
|
||||
| Melodic major | <ic>2212122</ic>|
|
||||
| Whole | <ic>222222</ic> |
|
||||
| Blues minor | <ic>321132</ic> |
|
||||
| Blues major | <ic>211323</ic> |
|
||||
|
||||
|
||||
${makeExample(
|
||||
"Let's fall back to a classic blues minor scale",
|
||||
`
|
||||
z1("s (0,8) 0 0 (0,5) 0 0").sound('sine')
|
||||
.scale('blues minor').fmi(2).fmh(2).room(0.5)
|
||||
.size(0.5).sustain(0.25).delay(0.25)
|
||||
.delay(0.25).delayfb(0.5).out();
|
||||
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
Microtonal scales can be defined using <a href="https://www.huygens-fokker.org/scala/scl_format.html" target="_blank">Scala format</a> or by extended notation defined by Sevish <a href="https://sevish.com/scaleworkshop/" target="_blank">Scale workshop</a>, for example:
|
||||
|
||||
- **Young:** 106. 198. 306.2 400.1 502. 604. 697.9 806.1 898.1 1004.1 1102. 1200.
|
||||
- **Wendy carlos:** 17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1
|
||||
|
||||
|
||||
${makeExample(
|
||||
"Wendy Carlos, here we go!",
|
||||
`
|
||||
z1("s ^ (0,8) 0 0 _ (0,5) 0 0").sound('sine')
|
||||
.scale('17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1').fmi(2).fmh(2).room(0.5)
|
||||
.size(0.5).sustain(0.15).delay(0.1)
|
||||
.delay(0.25).delayfb(0.5).out();
|
||||
beat(1, 1.75) :: snd(['kick', 'hat'].beat(1)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Synchronization
|
||||
|
||||
Ziffers numbered methods **(z0-z16)** can be used to parse and play patterns. Each method is individually cached and can be used to play multiple patterns simultaneously. By default, each Ziffers expression can have a different duration. This system is thus necessary to make everything fit together in a loop-based environment like Topos.
|
||||
|
||||
Numbered methods are synced automatically to **z0** method if it exsists. Syncing can also be done manually by using either the <ic>wait</ic> method, which will always wait for the current pattern to finish before starting the next cycle, or the <ic>sync</ic> method will only wait for the synced pattern to finish on the first time.
|
||||
|
||||
${makeExample(
|
||||
"Automatic sync to z0",
|
||||
`
|
||||
z0('w 0 8').sound('peri').out()
|
||||
z1('e 0 4 5 9').sound('bell').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Sync with wait",
|
||||
`
|
||||
z1('w 0 5').sound('pluck').release(0.1).sustain(0.25).out()
|
||||
z2('q 6 3').wait(z1).sound('sine').release(0.16).sustain(0.55).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Sync on first run",
|
||||
`
|
||||
z1('w __ 0 5 9 3').sound('bin').out()
|
||||
z2('q __ 4 2 e 6 3 q 6').sync(z1).sound('east').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Examples
|
||||
|
||||
- Basic notation
|
||||
|
||||
${makeExample(
|
||||
"Simple method chaining",
|
||||
`
|
||||
z1('0 1 2 3').key('G3')
|
||||
.scale('minor').sound('sine').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"More complex chaining",
|
||||
`
|
||||
z1('0 1 2 3 4').key('G3').scale('minor').sound('sine').often(n => n.pitch+=3).rarely(s => s.delay(0.5)).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Simple options",
|
||||
`
|
||||
z1('0 3 2 4',{key: 'D3', scale: 'minor pentatonic'}).sound('sine').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Duration chars",
|
||||
`
|
||||
z1('q 0 0 4 4 5 5 h4 q 3 3 2 2 1 1 h0').sound('sine').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Fraction durations",
|
||||
`
|
||||
z1('1/4 0 0 4 4 5 5 2/4 4 1/4 3 3 2 2 1 1 2/4 0')
|
||||
.sound('sine').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Decimal durations",
|
||||
`
|
||||
z1('0.25 5 1 2 6 0.125 3 8 0.5 4 1.0 0')
|
||||
.sound('sine').scale("galian").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Rest and octaves",
|
||||
`
|
||||
z1('q 0 ^ e0 r _ 0 _ r 4 ^4 4')
|
||||
.sound('sine').scale("godian").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Rests with durations",
|
||||
`
|
||||
z1('q 0 4 e^r 3 e3 0.5^r h4 1/4^r e 5 r 0.125^r 0')
|
||||
.sound('sine').scale("aeryptian").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
- Scales
|
||||
|
||||
${makeExample(
|
||||
"Microtonal scales",
|
||||
`
|
||||
z1('q 0 3 {10 14} e 8 4 {5 10 12 14 7 0}').sound('sine')
|
||||
.fmi([1,2,4,8].pick())
|
||||
.scale("17/16 9/8 6/5 5/4 4/3 11/8 3/2 13/8 5/3 7/4 15/8 2/1")
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Scala scale from variable",
|
||||
`
|
||||
const werckmeister = "107.82 203.91 311.72 401.955 503.91 605.865 701.955 809.775 900. 1007.82 1103.91 1200."
|
||||
|
||||
z0('s (0,3) ^ 0 3 ^ 0 (3,6) 0 _ (3,5) 0 _ 3 ^ 0 (3,5) ^ 0 6 0 _ 3 0')
|
||||
.key('C3')
|
||||
.scale(werckmeister)
|
||||
.sound('sine')
|
||||
.fmi(1 + usine(0.5) * irand(1,10))
|
||||
.cutoff(100 + usine(.5) * 100)
|
||||
.out()
|
||||
|
||||
onbeat(1,1.5,3) :: sound('bd').cutoff(100 + usine(.25) * 1000).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
- Algorithmic operations
|
||||
|
||||
${makeExample(
|
||||
"Random numbers",
|
||||
`
|
||||
z1('q 0 (2,4) 4 (5,9)').sound('sine')
|
||||
.scale("Bebop minor")
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"List operations",
|
||||
`
|
||||
z1('q (0 3 1 5)+(2 5) e (0 5 2)*(2 3) (0 5 2)>>(2 3) (0 5 2)%(2 3)').sound('sine')
|
||||
.scale("Bebop major")
|
||||
.out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
## Samples
|
||||
|
||||
Samples can be patterned using the sample names or using <c>@</c>-operator for assigning sample to a pitch. Sample index can be changed using the <c>:</c> operator.
|
||||
|
||||
${makeExample(
|
||||
"Sampled drums",
|
||||
`
|
||||
z1('bd [hh hh]').octave(-2).sound('sine').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"More complex pattern",
|
||||
`
|
||||
z1('bd [hh <hh <cp cp:2>>]').octave(-2).sound('sine').out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Pitched samples",
|
||||
`
|
||||
z1('0@sax 3@sax 2@sax 6@sax')
|
||||
.octave(-1).sound()
|
||||
.adsr(0.25,0.125,0.125,0.25).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Pitched samples from list operation",
|
||||
`
|
||||
z1('e (0 3 -1 4)+(-1 0 2 1)@sine')
|
||||
.key('G4')
|
||||
.scale('110 220 320 450')
|
||||
.sound().out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Pitched samples with list notation",
|
||||
`
|
||||
z1('e (0 2 6 3 5 -2)@sax (0 2 6 3 5 -2)@arp')
|
||||
.octave(-1).sound()
|
||||
.adsr(0.25,0.125,0.125,0.25).out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Sample indices",
|
||||
`
|
||||
z1('e 1:2 4:3 6:2')
|
||||
.octave(-1).sound("east").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
${makeExample(
|
||||
"Pitched samples with sample indices",
|
||||
`
|
||||
z1('_e 1@east:2 4@bd:3 6@arp:2 9@baa').sound().out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
|
||||
|
||||
## String prototypes
|
||||
|
||||
You can also use string prototypes as an alternative syntax for creating Ziffers patterns
|
||||
|
||||
${makeExample(
|
||||
"String prototypes",
|
||||
`
|
||||
"q 0 e 5 2 6 2 q 3".z0().sound('sine').out()
|
||||
"q 2 7 8 6".z1().octave(-1).sound('sine').out()
|
||||
"q 2 7 8 6".z2({key: "C2", scale: "aeolian"}).sound('sine').scale("minor").out()
|
||||
`,
|
||||
true,
|
||||
)}
|
||||
|
||||
`;
|
||||
};
|
||||
@ -33,8 +33,6 @@ declare global {
|
||||
gen(min: number, max: number, times: number): number[];
|
||||
sometimes(func: Function): number[];
|
||||
apply(func: Function): number[];
|
||||
counter(name: string | number, limit?: number, step?: number): number[]
|
||||
$(name: string | number, limit?: number, step?: number): number[];
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,31 +398,8 @@ export const makeArrayExtensions = (api: UserAPI) => {
|
||||
return this[Math.floor(api.randomGen() * this.length)];
|
||||
};
|
||||
Array.prototype.rand = Array.prototype.random;
|
||||
|
||||
Array.prototype.counter = function(
|
||||
name: string | number,
|
||||
limit?: number,
|
||||
step?: number) {
|
||||
/**
|
||||
* @param n - Returns next item in array until the end, then returns the last value.
|
||||
*
|
||||
* @returns the shifted array
|
||||
*/
|
||||
const idx = api.counter(name,limit,step);
|
||||
if(limit) {
|
||||
return this[idx % this.length];
|
||||
} else if(idx < this.length) {
|
||||
return this[idx];
|
||||
} else {
|
||||
return this[this.length - 1];
|
||||
}
|
||||
};
|
||||
Array.prototype.$ = Array.prototype.counter;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
Array.prototype.scale = function (
|
||||
scale: string = "major",
|
||||
base_note: number = 0,
|
||||
|
||||
@ -4,7 +4,6 @@ import { Player } from "../classes/ZPlayer";
|
||||
import { SoundEvent } from "../classes/SoundEvent";
|
||||
import { SkipEvent } from "../classes/SkipEvent";
|
||||
|
||||
|
||||
declare global {
|
||||
interface Number {
|
||||
z(): Player;
|
||||
@ -26,132 +25,84 @@ declare global {
|
||||
z15(): Player;
|
||||
z16(): Player;
|
||||
midi(): MidiEvent;
|
||||
sound(name: string): SoundEvent | SkipEvent,
|
||||
linlin(a: number, b: number, c: number, d: number): number,
|
||||
linexp(a: number, b: number, c: number, d: number): number,
|
||||
explin(a: number, b: number, c: number, d: number): number,
|
||||
expexp(a: number, b: number, c: number, d: number): number,
|
||||
lincurve(inMin: number, inMax: number,
|
||||
outMin: number, outMax: number,
|
||||
curve: number): number;
|
||||
sound(name: string): SoundEvent | SkipEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export const makeNumberExtensions = (api: UserAPI) => {
|
||||
|
||||
Number.prototype.linlin = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() < a) return c;
|
||||
if (this.valueOf() > b) return d;
|
||||
return (this.valueOf() - a) / (b - a) * (d - c) + c;
|
||||
};
|
||||
|
||||
Number.prototype.explin = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() <= a) return c;
|
||||
if (this.valueOf() >= b) return d;
|
||||
return (Math.log(this.valueOf() / a)) / (Math.log(b / a)) * (d - c) + c;
|
||||
};
|
||||
|
||||
Number.prototype.expexp = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() <= a) return c;
|
||||
if (this.valueOf() >= b) return d;
|
||||
return Math.pow(d / c, Math.log(this.valueOf() / a) / Math.log(b / a)) * c;
|
||||
};
|
||||
|
||||
Number.prototype.lincurve = function(
|
||||
inMin: number, inMax: number,
|
||||
outMin: number, outMax: number,
|
||||
curve: number) {
|
||||
if (this.valueOf() <= inMin) return outMin;
|
||||
if (this.valueOf() >= inMax) return outMax;
|
||||
if (Math.abs(curve) < 0.001) {
|
||||
return (this.valueOf() - inMin) / (inMax - inMin) * (outMax - outMin) + outMin;
|
||||
};
|
||||
let grow = Math.exp(curve);
|
||||
let a = outMax - outMin / (1.0 - grow);
|
||||
let b = outMin + a;
|
||||
let scaled = (this.valueOf() - inMin) / (inMax - inMin);
|
||||
return b - (a * Math.pow(grow, scaled))
|
||||
}
|
||||
|
||||
Number.prototype.linexp = function(a: number, b: number, c: number, d: number) {
|
||||
if (this.valueOf() <= a) return c;
|
||||
if (this.valueOf() >= b) return d;
|
||||
return Math.pow(d / c, (this.valueOf() - a) / (b - a)) * c;
|
||||
};
|
||||
|
||||
Number.prototype.z0 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z0 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z0(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z1 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z1 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z1(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z2 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z2 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z2(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z3 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z3 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z3(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z4 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z4 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z4(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z5 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z5 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z5(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z6 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z6 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z6(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z7 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z7 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z7(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z8 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z8 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z8(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z9 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z9 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z9(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z10 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z10 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z10(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z11 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z11 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z11(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z12 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z12 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z12(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z13 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z13 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z13(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z14 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z14 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z14(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z15 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z15 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z15(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.z16 = function(options: { [key: string]: any } = {}) {
|
||||
Number.prototype.z16 = function (options: { [key: string]: any } = {}) {
|
||||
return api.z16(this.valueOf().toString().split("").join(" "), options);
|
||||
};
|
||||
|
||||
Number.prototype.midi = function(...kwargs: any[]) {
|
||||
Number.prototype.midi = function (...kwargs: any[]) {
|
||||
return api.midi(this.valueOf(), ...kwargs);
|
||||
};
|
||||
|
||||
Number.prototype.sound = function(name: string): SoundEvent | SkipEvent {
|
||||
Number.prototype.sound = function (name: string): SoundEvent | SkipEvent {
|
||||
if (Number.isInteger(this.valueOf())) {
|
||||
return (api.sound(name) as SoundEvent).note(this.valueOf());
|
||||
} else {
|
||||
|
||||
112
src/main.ts
@ -5,17 +5,16 @@ import { javascript } from "@codemirror/lang-javascript";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { Extension } from "@codemirror/state";
|
||||
import { outputSocket } from "./IO/OSC";
|
||||
import { getCodeMirrorTheme, switchToDebugTheme } from "./EditorSetup";
|
||||
import {
|
||||
initializeSelectedUniverse,
|
||||
AppSettings,
|
||||
Universe,
|
||||
loadUniverserFromUrl,
|
||||
} from "./FileManagement";
|
||||
import { singleElements, buttonGroups, ElementMap, createDocumentationStyle } from "./DomElements";
|
||||
import { singleElements, buttonGroups, ElementMap } from "./DomElements";
|
||||
import { registerFillKeys, registerOnKeyDown } from "./KeyActions";
|
||||
import { installEditor } from "./EditorSetup";
|
||||
import { documentation_factory, documentation_pages, showDocumentation, updateDocumentationContent } from "./Documentation";
|
||||
import { documentation_factory } from "./Documentation";
|
||||
import { EditorView } from "codemirror";
|
||||
import { Clock } from "./Clock";
|
||||
import { loadSamples, UserAPI } from "./API";
|
||||
@ -31,12 +30,12 @@ import { makeStringExtensions } from "./extensions/StringExtensions";
|
||||
import { installInterfaceLogic } from "./InterfaceLogic";
|
||||
import { installWindowBehaviors } from "./WindowBehavior";
|
||||
import { makeNumberExtensions } from "./extensions/NumberExtensions";
|
||||
import colors from "./colors.json";
|
||||
// @ts-ignore
|
||||
const images = import.meta.glob("./assets/*")
|
||||
|
||||
|
||||
import { registerSW } from "virtual:pwa-register";
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
registerSW();
|
||||
}
|
||||
|
||||
export class Editor {
|
||||
// Universes and settings
|
||||
@ -52,7 +51,6 @@ export class Editor {
|
||||
hidden_interface: boolean = false;
|
||||
fontSize!: Compartment;
|
||||
withLineNumbers!: Compartment;
|
||||
themeCompartment!: Compartment;
|
||||
vimModeCompartment!: Compartment;
|
||||
hoveringCompartment!: Compartment;
|
||||
completionsCompartment!: Compartment;
|
||||
@ -70,7 +68,6 @@ export class Editor {
|
||||
public _mouseX: number = 0;
|
||||
public _mouseY: number = 0;
|
||||
show_error: boolean = false;
|
||||
currentThemeName: string = "Everblush";
|
||||
buttonElements: Record<string, HTMLButtonElement[]> = {};
|
||||
interface: ElementMap = {};
|
||||
blinkTimeouts: Record<number, number> = {};
|
||||
@ -86,8 +83,6 @@ export class Editor {
|
||||
mode: "scope",
|
||||
size: 1,
|
||||
};
|
||||
bindings: any[] = [];
|
||||
documentationStyle: any = {};
|
||||
|
||||
// UserAPI
|
||||
api: UserAPI;
|
||||
@ -128,7 +123,6 @@ export class Editor {
|
||||
this.initializeButtonGroups();
|
||||
this.setCanvas(this.interface.feedback as HTMLCanvasElement);
|
||||
this.setCanvas(this.interface.scope as HTMLCanvasElement);
|
||||
this.setCanvas(this.interface.drawings as HTMLCanvasElement);
|
||||
try {
|
||||
this.loadHydraSynthAsync();
|
||||
} catch (error) {
|
||||
@ -211,33 +205,6 @@ export class Editor {
|
||||
|
||||
// Loading universe from URL (if needed)
|
||||
loadUniverserFromUrl(this);
|
||||
|
||||
// Set the color scheme for the application
|
||||
let available_themes = Object.keys(colors);
|
||||
if (this.settings.theme in available_themes) {
|
||||
this.readTheme(this.settings.theme);
|
||||
} else {
|
||||
this.settings.theme = "Everblush";
|
||||
this.readTheme(this.settings.theme);
|
||||
}
|
||||
|
||||
this.documentationStyle = createDocumentationStyle(this);
|
||||
this.bindings = Object.keys(this.documentationStyle).map((key) => ({
|
||||
type: "output",
|
||||
regex: new RegExp(`<${key}([^>]*)>`, "g"),
|
||||
//@ts-ignore
|
||||
replace: (match, p1) => `<${key} class="${this.documentationStyle[key]}" ${p1}>`,
|
||||
}));
|
||||
|
||||
// Get documentation id from hash parameter
|
||||
const document_id = window.location.hash.slice(1);
|
||||
if (document_id && document_id !== "" && documentation_pages.includes(document_id)) {
|
||||
this.currentDocumentationPane = document_id
|
||||
updateDocumentationContent(this, this.bindings);
|
||||
showDocumentation(this);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private getBuffer(type: string): any {
|
||||
@ -293,7 +260,7 @@ export class Editor {
|
||||
|
||||
let list = document.createElement("ul");
|
||||
list.className =
|
||||
"lg:h-80 lg:text-normal text-normal h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-brightwhite bg-background lg:mb-4 border rounded-lg";
|
||||
"lg:h-80 lg:text-normal text-sm h-auto lg:w-80 w-auto lg:pb-2 lg:pt-2 overflow-y-scroll text-white lg:mb-4 border rounded-lg bg-neutral-800";
|
||||
list.append(
|
||||
...Object.keys(this.universes).map((it) => {
|
||||
let item = itemTemplate.content.cloneNode(true) as DocumentFragment;
|
||||
@ -325,9 +292,9 @@ export class Editor {
|
||||
*/
|
||||
const tabs = document.querySelectorAll('[id^="tab-"]');
|
||||
const tab = tabs[i] as HTMLElement;
|
||||
tab.classList.add("bg-foreground");
|
||||
tab.classList.add("bg-orange-300");
|
||||
for (let j = 0; j < tabs.length; j++) {
|
||||
if (j != i) tabs[j].classList.remove("bg-foreground");
|
||||
if (j != i) tabs[j].classList.remove("bg-orange-300");
|
||||
}
|
||||
let tab_id = tab.id.split("-")[1];
|
||||
this.local_index = parseInt(tab_id);
|
||||
@ -350,15 +317,15 @@ export class Editor {
|
||||
let changeColor = (button: HTMLElement) => {
|
||||
interface_buttons.forEach((button) => {
|
||||
let svg = button.children[0] as HTMLElement;
|
||||
if (svg.classList.contains("text-foreground_selection")) {
|
||||
svg.classList.remove("text-foreground_selection");
|
||||
button.classList.remove("text-foreground_selection");
|
||||
if (svg.classList.contains("text-orange-300")) {
|
||||
svg.classList.remove("text-orange-300");
|
||||
button.classList.remove("text-orange-300");
|
||||
}
|
||||
});
|
||||
button.children[0].classList.remove("text-white");
|
||||
button.children[0].classList.add("text-foreground_selection");
|
||||
button.classList.add("text-foreground_selection");
|
||||
button.classList.add("fill-foreground_selection");
|
||||
button.children[0].classList.add("text-orange-300");
|
||||
button.classList.add("text-orange-300");
|
||||
button.classList.add("fill-orange-300");
|
||||
};
|
||||
|
||||
switch (mode) {
|
||||
@ -473,7 +440,7 @@ export class Editor {
|
||||
|
||||
unfocusPlayButtons() {
|
||||
document.querySelectorAll('[id^="play-button-"]').forEach((button) => {
|
||||
button.children[0].classList.remove("fill-foreground_selection");
|
||||
button.children[0].classList.remove("fill-orange-300");
|
||||
button.children[0].classList.remove("animate-pulse");
|
||||
});
|
||||
}
|
||||
@ -565,7 +532,7 @@ export class Editor {
|
||||
console.log("Hydra loaded successfully");
|
||||
this.initializeHydra();
|
||||
};
|
||||
script.onerror = function() {
|
||||
script.onerror = function () {
|
||||
console.error("Error loading Hydra script");
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
@ -582,8 +549,8 @@ export class Editor {
|
||||
enableStreamCapture: false,
|
||||
});
|
||||
this.hydra = this.hydra_backend.synth;
|
||||
this.hydra.setResolution(1280, 768);
|
||||
(globalThis as any).hydra = this.hydra;
|
||||
this.hydra.setResolution(1024, 768);
|
||||
}
|
||||
|
||||
private setCanvas(canvas: HTMLCanvasElement): void {
|
||||
@ -602,49 +569,6 @@ export class Editor {
|
||||
ctx.scale(dpr, dpr);
|
||||
}
|
||||
}
|
||||
|
||||
private updateInterfaceTheme(selected_theme: { [key: string]: string }): void {
|
||||
function hexToRgb(hex: string): { r: number, g: number, b: number } | null {
|
||||
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
};
|
||||
for (const [key, value] of Object.entries(selected_theme)) {
|
||||
let color = hexToRgb(value);
|
||||
if (color) {
|
||||
let colorString = `${color.r} ${color.g} ${color.b}`
|
||||
document.documentElement.style.setProperty("--" + key, colorString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getColorScheme(theme_name: string): { [key: string]: string } {
|
||||
// Check if the theme exists in colors.json
|
||||
let themes: Record<string, { [key: string]: any }> = colors;
|
||||
return themes[theme_name];
|
||||
}
|
||||
|
||||
readTheme(theme_name: string): void {
|
||||
// Check if the theme exists in colors.json
|
||||
if (theme_name == "debug") {
|
||||
switchToDebugTheme(this);
|
||||
return
|
||||
}
|
||||
let themes: Record<string, { [key: string]: any }> = colors;
|
||||
let selected_theme = themes[theme_name];
|
||||
if (selected_theme) {
|
||||
this.currentThemeName = theme_name;
|
||||
this.updateInterfaceTheme(selected_theme);
|
||||
let codeMirrorTheme = getCodeMirrorTheme(selected_theme);
|
||||
// Reconfigure the view with the new theme
|
||||
this.view.dispatch({
|
||||
effects: this.themeCompartment.reconfigure(codeMirrorTheme),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app = new Editor();
|
||||
|
||||
2664
src/style.css
@ -1,29 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--black: 40 42 54;
|
||||
--red: 68 71 90;
|
||||
--green: 248 248 242;
|
||||
--yellow: 98 114 164;
|
||||
--blue: 139 233 253;
|
||||
--magenta: 80 250 123;
|
||||
--cyan: 255 184 108;
|
||||
--white: 255 121 198;
|
||||
--brightblack: 189 147 249;
|
||||
--brightred: 255 85 85;
|
||||
--brightgreen: 241 250 140;
|
||||
--brightyellow: 139 233 253;
|
||||
--brightblue: 80 250 123;
|
||||
--brightmagenta: 255 184 108;
|
||||
--brightcyan: 255 121 198;
|
||||
--brightwhite: 189 147 249;
|
||||
--background: 40 42 54;
|
||||
--selection_foreground: 68 71 90;
|
||||
--cursor: 139 233 253;
|
||||
--foreground: 248 248 242;
|
||||
--selection_background: 189 147 249;
|
||||
}
|
||||
}
|
||||
221
src/themes/toposTheme.ts
Normal file
@ -0,0 +1,221 @@
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { Extension } from "@codemirror/state";
|
||||
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
import { tags as t } from "@lezer/highlight";
|
||||
|
||||
const base00 = "#262626",
|
||||
base01 = "#3B4252",
|
||||
base02 = "#BBBBBB",
|
||||
base03 = "#4C566A",
|
||||
base04 = "#D8DEE9",
|
||||
base05 = "#E5E9F0",
|
||||
base07 = "#8FBCBB",
|
||||
base_red = "#BF616A",
|
||||
base_deeporange = "#D08770",
|
||||
base_pink = "#B48EAD",
|
||||
base_cyan = "#FBCF8B",
|
||||
base_yellow = "#88C0D0",
|
||||
base_orange = "#D08770",
|
||||
base_indigo = "#5E81AC",
|
||||
base_purple = "#B48EAD",
|
||||
base_green = "#A3BE8C",
|
||||
base_lightgreen = "#A3BE8C";
|
||||
|
||||
const invalid = base_red,
|
||||
darkBackground = "#262626",
|
||||
highlightBackground = "#252525",
|
||||
// background = base00,
|
||||
tooltipBackground = base01,
|
||||
cursor = base04;
|
||||
|
||||
/// The editor theme styles for Material Dark.
|
||||
export const toposDarkTheme = EditorView.theme(
|
||||
{
|
||||
"&": {
|
||||
color: base05,
|
||||
// backgroundColor: background,
|
||||
backgroundColor: "transparent",
|
||||
fontSize: "24px",
|
||||
fontFamily: "IBM Plex Mono",
|
||||
},
|
||||
".cm-content": {
|
||||
caretColor: cursor,
|
||||
fontFamily: "IBM Plex Mono",
|
||||
},
|
||||
".cm-cursor, .cm-dropCursor": {
|
||||
borderLeftColor: cursor,
|
||||
},
|
||||
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
|
||||
{
|
||||
backgroundColor: base00,
|
||||
border: `0.5px solid ${base00}`,
|
||||
},
|
||||
".cm-panels": {
|
||||
backgroundColor: darkBackground,
|
||||
color: base05,
|
||||
},
|
||||
".cm-panels.cm-panels-top": { borderBottom: "2px solid black" },
|
||||
".cm-panels.cm-panels-bottom": { borderTop: "2px solid black" },
|
||||
".cm-search.cm-panel": { backgroundColor: "transparent" },
|
||||
".cm-searchMatch": {
|
||||
outline: `1px solid ${base_cyan}`,
|
||||
},
|
||||
".cm-searchMatch.cm-searchMatch-selected": {
|
||||
backgroundColor: highlightBackground,
|
||||
},
|
||||
".cm-activeLine": {
|
||||
// backgroundColor: highlightBackground
|
||||
backgroundColor: "rgb(76,76,106, 0.1)",
|
||||
},
|
||||
".cm-selectionMatch": {
|
||||
backgroundColor: base04,
|
||||
outline: `1px solid ${base_red}`,
|
||||
},
|
||||
|
||||
"&.cm-focused .cm-matchingBracket": {
|
||||
color: base02,
|
||||
// outline: `1px solid ${base02}`,
|
||||
},
|
||||
|
||||
"&.cm-focused .cm-nonmatchingBracket": {
|
||||
color: base_red,
|
||||
},
|
||||
|
||||
".cm-gutters": {
|
||||
//backgroundColor: base00,
|
||||
backgroundColor: "transparent",
|
||||
color: base02,
|
||||
},
|
||||
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: highlightBackground,
|
||||
color: base02,
|
||||
},
|
||||
|
||||
".cm-foldPlaceholder": {
|
||||
border: "none",
|
||||
color: `${base07}`,
|
||||
},
|
||||
|
||||
".cm-tooltip": {
|
||||
border: "none",
|
||||
backgroundColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip .cm-tooltip-arrow:before": {},
|
||||
".cm-tooltip .cm-tooltip-arrow:after": {
|
||||
borderTopColor: tooltipBackground,
|
||||
borderBottomColor: tooltipBackground,
|
||||
},
|
||||
".cm-tooltip-autocomplete": {
|
||||
"& > ul > li[aria-selected]": {
|
||||
backgroundColor: highlightBackground,
|
||||
color: base03,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ dark: true },
|
||||
);
|
||||
|
||||
/// The highlighting style for code in the Material Dark theme.
|
||||
export const toposDarkHighlightStyle = HighlightStyle.define([
|
||||
{ tag: t.keyword, color: base_purple },
|
||||
{
|
||||
tag: [t.name, t.deleted, t.character, t.macroName],
|
||||
color: base_cyan,
|
||||
},
|
||||
{ tag: [t.propertyName], color: base_yellow },
|
||||
{ tag: [t.variableName], color: base05 },
|
||||
{ tag: [t.function(t.variableName)], color: base_cyan },
|
||||
{ tag: [t.labelName], color: base_purple },
|
||||
{
|
||||
tag: [t.color, t.constant(t.name), t.standard(t.name)],
|
||||
color: base_yellow,
|
||||
},
|
||||
{ tag: [t.definition(t.name), t.separator], color: base_pink },
|
||||
{ tag: [t.brace], color: base_purple },
|
||||
{
|
||||
tag: [t.annotation],
|
||||
color: invalid,
|
||||
},
|
||||
{
|
||||
tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
|
||||
color: base_orange,
|
||||
},
|
||||
{
|
||||
tag: [t.typeName, t.className],
|
||||
color: base_orange,
|
||||
},
|
||||
{
|
||||
tag: [t.operator, t.operatorKeyword],
|
||||
color: base_indigo,
|
||||
},
|
||||
{
|
||||
tag: [t.tagName],
|
||||
color: base_deeporange,
|
||||
},
|
||||
{
|
||||
tag: [t.squareBracket],
|
||||
color: base_red,
|
||||
},
|
||||
{
|
||||
tag: [t.angleBracket],
|
||||
color: base02,
|
||||
},
|
||||
{
|
||||
tag: [t.attributeName],
|
||||
color: base05,
|
||||
},
|
||||
{
|
||||
tag: [t.regexp],
|
||||
color: invalid,
|
||||
},
|
||||
{
|
||||
tag: [t.quote],
|
||||
color: base_green,
|
||||
},
|
||||
{ tag: [t.string], color: base_lightgreen },
|
||||
{
|
||||
tag: t.link,
|
||||
color: base_cyan,
|
||||
textDecoration: "underline",
|
||||
textUnderlinePosition: "under",
|
||||
},
|
||||
{
|
||||
tag: [t.url, t.escape, t.special(t.string)],
|
||||
color: base_yellow,
|
||||
},
|
||||
{ tag: [t.meta], color: base03 },
|
||||
{ tag: [t.comment], color: base02, fontStyle: "italic" },
|
||||
{ tag: t.monospace, color: base05 },
|
||||
{ tag: t.strong, fontWeight: "bold", color: base_red },
|
||||
{ tag: t.emphasis, fontStyle: "italic", color: base_lightgreen },
|
||||
{ tag: t.strikethrough, textDecoration: "line-through" },
|
||||
{ tag: t.heading, fontWeight: "bold", color: base_yellow },
|
||||
{ tag: t.heading1, fontWeight: "bold", color: base_yellow },
|
||||
{
|
||||
tag: [t.heading2, t.heading3, t.heading4],
|
||||
fontWeight: "bold",
|
||||
color: base_yellow,
|
||||
},
|
||||
{
|
||||
tag: [t.heading5, t.heading6],
|
||||
color: base_yellow,
|
||||
},
|
||||
{ tag: [t.atom, t.bool, t.special(t.variableName)], color: base_cyan },
|
||||
{
|
||||
tag: [t.processingInstruction, t.inserted],
|
||||
color: base_red,
|
||||
},
|
||||
{
|
||||
tag: [t.contentSeparator],
|
||||
color: base_cyan,
|
||||
},
|
||||
{ tag: t.invalid, color: base02, borderBottom: `1px dotted ${base_red}` },
|
||||
]);
|
||||
|
||||
/// Extension to enable the Material Dark theme (both the editor theme and
|
||||
/// the highlight style).
|
||||
export const toposTheme: Extension = [
|
||||
toposDarkTheme,
|
||||
syntaxHighlighting(toposDarkHighlightStyle),
|
||||
];
|
||||
@ -1,33 +1,21 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.html", "./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
safelist: [
|
||||
{
|
||||
pattern: /hljs+/,
|
||||
},
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
black: "rgb(var(--black) / <alpha-value>)",
|
||||
red: "rgb(var(--red) / <alpha-value>)",
|
||||
green: "rgb(var(--green) / <alpha-value>)",
|
||||
yellow: "rgb(var(--yellow) / <alpha-value>)",
|
||||
blue: "rgb(var(--blue) / <alpha-value>)",
|
||||
magenta: "rgb(var(--magenta) / <alpha-value>)",
|
||||
cyan: "rgb(var(--cyan) / <alpha-value>)",
|
||||
white: "rgb(var(--white) / <alpha-value>)",
|
||||
brightblack: "rgb(var(--brightblack) / <alpha-value>)",
|
||||
brightred: "rgb(var(--brightred) / <alpha-value>)",
|
||||
brightgreen: "rgb(var(--brightgreen) / <alpha-value>)",
|
||||
brightyellow: "rgb(var(--brightyellow) / <alpha-value>)",
|
||||
brightblue: "rgb(var(--brightblue) / <alpha-value>)",
|
||||
brightmagenta: "rgb(var(--brightmagenta) / <alpha-value>)",
|
||||
brightcyan: "rgb(var(--brightcyan) / <alpha-value>)",
|
||||
brightwhite: "rgb(var(--brightwhite) / <alpha-value>)",
|
||||
background: "rgb(var(--background) / <alpha-value>)",
|
||||
selection_foreground: "rgb(var(--selection_foreground) / <alpha-value>)",
|
||||
cursor: "rgb(var(--cursor) / <alpha-value>)",
|
||||
foreground: "rgb(var(--foreground) / <alpha-value>)",
|
||||
selection_background: "rgb(var(--selection_background) / <alpha-value>)",
|
||||
}
|
||||
},
|
||||
extend: {},
|
||||
safelist: [{
|
||||
pattern: /(bg|text|border)-(transparent|color0|color1|color2|color3|color4|color5|color6|color7|color8|color9|color10|color11|color12|color13|color14|color15|background|selection_background|cursor|foreground|selection_background)/,
|
||||
}],
|
||||
extend: {},
|
||||
hljs: {
|
||||
theme: "nord",
|
||||
custom: {
|
||||
general: {
|
||||
comment: "#FEFEFE",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwind-highlightjs")],
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 427 KiB After Width: | Height: | Size: 427 KiB |