Compare commits
2 Commits
4772b02f77
...
08029ec604
| Author | SHA1 | Date | |
|---|---|---|---|
| 08029ec604 | |||
| 4f9b1f39f9 |
@@ -799,10 +799,7 @@ impl ThemeColors {
|
||||
key: peach,
|
||||
text: overlay1,
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: text,
|
||||
fg: base,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: text, fg: base },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(215, 205, 245),
|
||||
selected_fg: text,
|
||||
@@ -1638,10 +1635,7 @@ impl ThemeColors {
|
||||
key: orange,
|
||||
text: fg4,
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: fg,
|
||||
fg: bg0,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg0 },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(80, 65, 50),
|
||||
selected_fg: fg,
|
||||
@@ -1916,10 +1910,7 @@ impl ThemeColors {
|
||||
key: orange,
|
||||
text: comment,
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: fg,
|
||||
fg: bg,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(80, 60, 75),
|
||||
selected_fg: fg,
|
||||
@@ -2196,10 +2187,7 @@ impl ThemeColors {
|
||||
key: orange,
|
||||
text: fg_muted,
|
||||
},
|
||||
view_badge: ViewBadgeColors {
|
||||
bg: fg,
|
||||
fg: bg,
|
||||
},
|
||||
view_badge: ViewBadgeColors { bg: fg, fg: bg },
|
||||
nav: NavColors {
|
||||
selected_bg: Color::Rgb(40, 45, 55),
|
||||
selected_fg: fg,
|
||||
@@ -2324,16 +2312,36 @@ impl ThemeColors {
|
||||
|
||||
pub mod ui {
|
||||
use super::*;
|
||||
pub fn bg() -> Color { get().ui.bg }
|
||||
pub fn bg_rgb() -> (u8, u8, u8) { get().ui.bg_rgb }
|
||||
pub fn text_primary() -> Color { get().ui.text_primary }
|
||||
pub fn text_muted() -> Color { get().ui.text_muted }
|
||||
pub fn text_dim() -> Color { get().ui.text_dim }
|
||||
pub fn border() -> Color { get().ui.border }
|
||||
pub fn header() -> Color { get().ui.header }
|
||||
pub fn unfocused() -> Color { get().ui.unfocused }
|
||||
pub fn accent() -> Color { get().ui.accent }
|
||||
pub fn surface() -> Color { get().ui.surface }
|
||||
pub fn bg() -> Color {
|
||||
get().ui.bg
|
||||
}
|
||||
pub fn bg_rgb() -> (u8, u8, u8) {
|
||||
get().ui.bg_rgb
|
||||
}
|
||||
pub fn text_primary() -> Color {
|
||||
get().ui.text_primary
|
||||
}
|
||||
pub fn text_muted() -> Color {
|
||||
get().ui.text_muted
|
||||
}
|
||||
pub fn text_dim() -> Color {
|
||||
get().ui.text_dim
|
||||
}
|
||||
pub fn border() -> Color {
|
||||
get().ui.border
|
||||
}
|
||||
pub fn header() -> Color {
|
||||
get().ui.header
|
||||
}
|
||||
pub fn unfocused() -> Color {
|
||||
get().ui.unfocused
|
||||
}
|
||||
pub fn accent() -> Color {
|
||||
get().ui.accent
|
||||
}
|
||||
pub fn surface() -> Color {
|
||||
get().ui.surface
|
||||
}
|
||||
|
||||
// Constants for backward compatibility
|
||||
pub const BG: Color = Color::Rgb(30, 30, 46);
|
||||
@@ -2385,10 +2393,18 @@ pub mod tile {
|
||||
pub const ACTIVE_SELECTED_BG: Color = Color::Rgb(70, 60, 80);
|
||||
pub const ACTIVE_IN_RANGE_BG: Color = Color::Rgb(55, 55, 70);
|
||||
pub const LINK_BRIGHT: [(u8, u8, u8); 5] = [
|
||||
(203, 166, 247), (245, 194, 231), (250, 179, 135), (137, 220, 235), (166, 227, 161),
|
||||
(203, 166, 247),
|
||||
(245, 194, 231),
|
||||
(250, 179, 135),
|
||||
(137, 220, 235),
|
||||
(166, 227, 161),
|
||||
];
|
||||
pub const LINK_DIM: [(u8, u8, u8); 5] = [
|
||||
(70, 55, 85), (85, 65, 80), (85, 60, 45), (45, 75, 80), (55, 80, 55),
|
||||
(70, 55, 85),
|
||||
(85, 65, 80),
|
||||
(85, 60, 45),
|
||||
(45, 75, 80),
|
||||
(55, 80, 55),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2619,7 +2635,11 @@ pub mod meter {
|
||||
|
||||
pub mod sparkle {
|
||||
pub const COLORS: &[(u8, u8, u8)] = &[
|
||||
(200, 220, 255), (250, 179, 135), (166, 227, 161), (245, 194, 231), (203, 166, 247),
|
||||
(200, 220, 255),
|
||||
(250, 179, 135),
|
||||
(166, 227, 161),
|
||||
(245, 194, 231),
|
||||
(203, 166, 247),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# About
|
||||
|
||||
Cagire is an experimental step sequencer built by BuboBubo (Raphaël Maurice Forment). It is a free and open-source project licensed under the AGPL-3.0 License. Cagire has been developed as a side project. I wanted to learn more about using Forth and needed a playground for experimentating with this audio engine! You are free to contribute to the project by making direct contributions to the codebase or by providing feedback and suggestions.
|
||||
|
||||
## Credits
|
||||
|
||||
- **Doux** (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.
|
||||
- **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits.
|
||||
* _Author_: Oliver Rockstedt [info@sourcebox.de](info@sourcebox.de).
|
||||
* _Original author_: Emilie Gillet [emilie.o.gillet@gmail.com](emilie.o.gillet@gmail.com).
|
||||
|
||||
## About live coding
|
||||
|
||||
Live coding is a technique where a programmer writes code in real-time in front of an audience. It is a way to experiment with code, to share things and thoughts openly, to express yourself through code. It can be technical, poetical, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media. Learn more about live coding on [https://toplap.org](https://toplap.org) or [https://livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: it is an activity that is intrinsically rewarding, and the act of doing it is its own reward. There are no errors, only fun to be found by playing music.
|
||||
|
||||
## About the tool
|
||||
|
||||
I do not want to pair it with a DAW, I do not want to make it fit with other commercial software. I'm not interested in VSTs or other proprietary workstations. Please, try to think of Cagire as an alternative to other tools.
|
||||
112
docs/about_forth.md
Normal file
112
docs/about_forth.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# About Forth
|
||||
|
||||
Forth is a stack-based programming language created by Charles H. Moore in the early 1970s. It was designed for simplicity, directness, and interactive exploration. Forth has been used for many years to do scientific work and program embedded systems: controlling telescopes, running on devices used in space missions, etc. Forth quickly evolved into multiple implementations. None of them really reached an incredible popularity. Nonetheless, the ideas behind Forth continue to garner the interest of many different people in very different (often unrelated) fields. Nowadays, Forth languages are sometimes used by hackers and artists for their peculiarities. A Forth implementation is often simple, direct and beautiful. Forth is an elegant and minimal language to learn. It is easy to understand, to extend and to apply to a specific task. The Forth we use in Cagire is specialized in making live music.
|
||||
|
||||
## Why Forth?
|
||||
|
||||
Most programming languages nowadays use a complex syntax made of `variables`, `expressions` and `statements` like `x = 3 + 4`. Forth works differently. It is way more simple than that, has almost no syntax, and performs computations in a quite unique way. You push values onto a `stack` and apply `words` that transform them:
|
||||
|
||||
```forth
|
||||
3 4 +
|
||||
```
|
||||
|
||||
This program leaves the number `7` on the stack. There are no variables, no parentheses, no syntax to remember. You just end up with words and numbers separated by spaces. For live coding music, this directness is quite exciting. All you do is thinking in terms of transformations and adding things to the stack: take a note, shift it up, add reverb, play it.
|
||||
|
||||
## The Stack
|
||||
|
||||
The stack is where values live. When you type a number, it goes on the stack. When you type a word, it usually takes values off and puts new ones back.
|
||||
|
||||
```forth
|
||||
3 ( stack: 3 )
|
||||
4 ( stack: 3 4 )
|
||||
+ ( stack: 7 )
|
||||
```
|
||||
|
||||
The stack is `last-in, first-out`. The most recent value is always on top. This means that its often better to read Forth programs from the end to the beginning: from right to left, from the bottom to the top.
|
||||
|
||||
## Words
|
||||
|
||||
Everything in Forth is either a `number` or a `word`. Words are like functions but conceptually simpler. They have no arguments or return values in the traditional sense. They just manipulate the stack directly.
|
||||
|
||||
```forth
|
||||
dup ( duplicate the top value )
|
||||
drop ( discard the top value )
|
||||
swap ( swap the top two values )
|
||||
```
|
||||
|
||||
Words compose naturally on the stack. To double a number:
|
||||
|
||||
```forth
|
||||
3 dup + ( 3 3 +)
|
||||
```
|
||||
|
||||
There are a lot of words in a Forth, and thus, Cagire has a `Dictionary` embedded directly into the application. You can also create your own words. They will work just like the already existing words. There are good reasons to create new words on-the-fly:
|
||||
|
||||
- To make synth definitions.
|
||||
- To abstract _some piece of code_ that you use frequently.
|
||||
- To share data and processes between different steps.
|
||||
|
||||
## Values
|
||||
|
||||
Four types of values can live on the stack:
|
||||
|
||||
- **Integers**: `42`, `-7`, `0`
|
||||
- **Floats**: `0.5`, `3.14`, `-1.0`
|
||||
- **Strings**: `"kick"`, `"hello"`
|
||||
- **Quotations**: `{ dup + }` (code as data)
|
||||
|
||||
Quotations are special. They let you pass code around as a value. This is how conditionals and loops work. Think nothing of it for now, you will learn more about how to use it later on.
|
||||
|
||||
## Stack Notation
|
||||
|
||||
Documentation uses a notation to show what words do:
|
||||
|
||||
```
|
||||
( before -- after )
|
||||
```
|
||||
|
||||
For example, `+` has the signature `( a b -- sum )`. It takes two values and leaves one.
|
||||
|
||||
## The Command Register
|
||||
|
||||
Traditional Forth programs print text to a terminal. Cagire's Forth builds sound commands instead. This happens through an invisible accumulator called the command register.
|
||||
|
||||
The command register has two parts:
|
||||
- A **sound name** (what instrument to play)
|
||||
- A list of **parameters** (how to play it)
|
||||
|
||||
Three types of words interact with it:
|
||||
|
||||
```forth
|
||||
kick sound ;; sets the sound name
|
||||
0.5 gain ;; adds a parameter
|
||||
. ;; emits the command and clears the register
|
||||
```
|
||||
|
||||
The word `sound` (or its shorthand `s`) sets what sound to play. Parameter words like `gain`, `freq`, `decay`, or `verb` add key-value pairs to the register. Nothing happens until you emit with `.` (dot). At that moment, the register is packaged into a command and sent to the audio engine.
|
||||
|
||||
This design lets you build sounds incrementally:
|
||||
|
||||
```forth
|
||||
"sine" sound
|
||||
c4 note
|
||||
0.5 gain
|
||||
0.3 decay
|
||||
0.4 verb
|
||||
.
|
||||
```
|
||||
|
||||
Each line adds something to the register. The final `.` triggers the sound. You can also write it all on one line:
|
||||
|
||||
```forth
|
||||
"sine" s c4 note 0.5 gain 0.3 decay 0.4 verb .
|
||||
```
|
||||
|
||||
The order of parameters does not matter. You can even emit multiple times in a single step: If you need to discard the register without emitting, use `clear`:
|
||||
|
||||
```forth
|
||||
"kick" s 0.5 gain clear ;; nothing plays, register is emptied
|
||||
"hat" s . ;; only the hat plays
|
||||
```
|
||||
|
||||
This is useful when conditionals might cancel a sound before it emits.
|
||||
84
docs/arithmetic.md
Normal file
84
docs/arithmetic.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Arithmetic
|
||||
|
||||
Basic math operations. All arithmetic words pop their operands and push the result.
|
||||
|
||||
## Basic Operations
|
||||
|
||||
```
|
||||
3 4 + ( 7 )
|
||||
10 3 - ( 7 )
|
||||
3 4 * ( 12 )
|
||||
10 3 / ( 3.333... )
|
||||
10 3 mod ( 1 )
|
||||
```
|
||||
|
||||
Division always produces a float. Use `floor` if you need an integer result.
|
||||
|
||||
## Negative Numbers
|
||||
|
||||
```
|
||||
5 neg ( -5 )
|
||||
-3 abs ( 3 )
|
||||
```
|
||||
|
||||
## Rounding
|
||||
|
||||
```
|
||||
3.7 floor ( 3 )
|
||||
3.2 ceil ( 4 )
|
||||
3.5 round ( 4 )
|
||||
```
|
||||
|
||||
## Min and Max
|
||||
|
||||
```
|
||||
3 7 min ( 3 )
|
||||
3 7 max ( 7 )
|
||||
```
|
||||
|
||||
## Power and Root
|
||||
|
||||
```
|
||||
2 3 pow ( 8 )
|
||||
9 sqrt ( 3 )
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Calculate a frequency ratio:
|
||||
|
||||
```
|
||||
440 2 12 / pow * ( 440 * 2^(1/12) ≈ 466.16 )
|
||||
```
|
||||
|
||||
Clamp a value between 0 and 1:
|
||||
|
||||
```
|
||||
1.5 0 max 1 min ( 1 )
|
||||
-0.5 0 max 1 min ( 0 )
|
||||
```
|
||||
|
||||
Scale a 0-1 range to 200-800:
|
||||
|
||||
```
|
||||
0.5 600 * 200 + ( 500 )
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `+` | (a b -- sum) | Add |
|
||||
| `-` | (a b -- diff) | Subtract |
|
||||
| `*` | (a b -- prod) | Multiply |
|
||||
| `/` | (a b -- quot) | Divide |
|
||||
| `mod` | (a b -- rem) | Modulo |
|
||||
| `neg` | (a -- -a) | Negate |
|
||||
| `abs` | (a -- \|a\|) | Absolute value |
|
||||
| `floor` | (a -- n) | Round down |
|
||||
| `ceil` | (a -- n) | Round up |
|
||||
| `round` | (a -- n) | Round to nearest |
|
||||
| `min` | (a b -- min) | Minimum |
|
||||
| `max` | (a b -- max) | Maximum |
|
||||
| `pow` | (a b -- a^b) | Power |
|
||||
| `sqrt` | (a -- √a) | Square root |
|
||||
51
docs/banks_patterns.md
Normal file
51
docs/banks_patterns.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Banks & Patterns
|
||||
|
||||
Cagire organizes all your patterns and data following a strict hierarchy:
|
||||
|
||||
- **Projects** contain **Banks**.
|
||||
- **Banks** contain **Patterns**.
|
||||
- **Patterns** contain **Steps**.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
Project
|
||||
└── 32 Banks
|
||||
└── 32 Patterns (per bank)
|
||||
└── 128 Steps (per pattern)
|
||||
```
|
||||
|
||||
A single project gives you 32 banks, each holding 32 patterns. You get 1024 patterns in each project, ~131.000 steps.
|
||||
|
||||
## Patterns
|
||||
|
||||
Each pattern is an independent sequence of steps with its own properties:
|
||||
|
||||
| Property | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| Length | Steps before the pattern loops (`1`-`128`) | `16` |
|
||||
| Speed | Playback rate (`1/8x` to `8x`) | `1x` |
|
||||
| Quantization | When the pattern launches | `Bar` |
|
||||
| Sync Mode | Reset or Phase-Lock on re-trigger | `Reset` |
|
||||
|
||||
Press `e` in the patterns view to edit these settings.
|
||||
|
||||
## Patterns View
|
||||
|
||||
Access the patterns view with `Ctrl+Up` from the sequencer. The view shows all banks and patterns in a grid. Indicators show pattern state:
|
||||
|
||||
- `>` Currently playing
|
||||
- `+` Staged to play
|
||||
- `-` Staged to stop
|
||||
|
||||
### Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Arrows` | Navigate banks and patterns |
|
||||
| `Enter` | Select and return to sequencer |
|
||||
| `Space` | Toggle pattern playback |
|
||||
| `e` | Edit pattern properties |
|
||||
| `r` | Rename bank or pattern |
|
||||
| `c` / `v` | Copy / Paste |
|
||||
| `Delete` | Reset to empty pattern |
|
||||
2
docs/chaining.md
Normal file
2
docs/chaining.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Chaining
|
||||
|
||||
2
docs/chords.md
Normal file
2
docs/chords.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Chords
|
||||
|
||||
41
docs/comparison.md
Normal file
41
docs/comparison.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Comparison
|
||||
|
||||
Compare values and produce boolean results (0 or 1).
|
||||
|
||||
## Equality
|
||||
|
||||
```forth
|
||||
3 3 = ( 1 - equal )
|
||||
3 4 = ( 0 - not equal )
|
||||
3 4 != ( 1 - not equal )
|
||||
3 4 <> ( 1 - not equal, alternative )
|
||||
```
|
||||
|
||||
## Ordering
|
||||
|
||||
```forth
|
||||
2 3 lt ( 1 - less than )
|
||||
3 2 gt ( 1 - greater than )
|
||||
3 3 <= ( 1 - less or equal )
|
||||
3 3 >= ( 1 - greater or equal )
|
||||
```
|
||||
|
||||
## With Conditionals
|
||||
|
||||
```forth
|
||||
step 4 lt { "kick" s . } ? ( kick on first 4 steps )
|
||||
|
||||
beat 8 >= { 0.5 gain } ? ( quieter after beat 8 )
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `=` | (a b -- bool) | Equal |
|
||||
| `!=` | (a b -- bool) | Not equal |
|
||||
| `<>` | (a b -- bool) | Not equal (alias) |
|
||||
| `lt` | (a b -- bool) | Less than |
|
||||
| `gt` | (a b -- bool) | Greater than |
|
||||
| `<=` | (a b -- bool) | Less or equal |
|
||||
| `>=` | (a b -- bool) | Greater or equal |
|
||||
2
docs/conditionals.md
Normal file
2
docs/conditionals.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Conditionals
|
||||
|
||||
2
docs/context.md
Normal file
2
docs/context.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Context
|
||||
|
||||
2
docs/cycles.md
Normal file
2
docs/cycles.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Cycles
|
||||
|
||||
2
docs/definitions.md
Normal file
2
docs/definitions.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Custom Words
|
||||
|
||||
51
docs/delay_reverb.md
Normal file
51
docs/delay_reverb.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Delay & Reverb
|
||||
|
||||
Add space and depth to your sounds with time-based effects.
|
||||
|
||||
## Delay
|
||||
|
||||
```forth
|
||||
"snare" s 0.3 delay . ( delay mix )
|
||||
"snare" s 0.25 delaytime . ( delay time in seconds )
|
||||
"snare" s 0.5 delayfeedback . ( feedback amount )
|
||||
"snare" s 1 delaytype . ( delay type )
|
||||
```
|
||||
|
||||
## Reverb
|
||||
|
||||
```forth
|
||||
"pad" s 0.3 verb . ( reverb mix )
|
||||
"pad" s 2 verbdecay . ( decay time )
|
||||
"pad" s 0.5 verbdamp . ( high frequency damping )
|
||||
"pad" s 0.02 verbpredelay . ( predelay time )
|
||||
"pad" s 0.7 verbdiff . ( diffusion )
|
||||
"pad" s 1 size . ( room size )
|
||||
```
|
||||
|
||||
## Combined Example
|
||||
|
||||
```forth
|
||||
"keys" s
|
||||
c4 note
|
||||
0.2 delay
|
||||
0.375 delaytime
|
||||
0.4 delayfeedback
|
||||
0.25 verb
|
||||
1.5 verbdecay
|
||||
.
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `delay` | (f --) | Set delay mix |
|
||||
| `delaytime` | (f --) | Set delay time |
|
||||
| `delayfeedback` | (f --) | Set delay feedback |
|
||||
| `delaytype` | (n --) | Set delay type |
|
||||
| `verb` | (f --) | Set reverb mix |
|
||||
| `verbdecay` | (f --) | Set reverb decay |
|
||||
| `verbdamp` | (f --) | Set reverb damping |
|
||||
| `verbpredelay` | (f --) | Set reverb predelay |
|
||||
| `verbdiff` | (f --) | Set reverb diffusion |
|
||||
| `size` | (f --) | Set reverb size |
|
||||
43
docs/dictionary.md
Normal file
43
docs/dictionary.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# The Dictionary
|
||||
|
||||
Cagire includes a built-in dictionary of all the internal Forth words. Press `Ctrl+Up` to reach the **Dict** view.
|
||||
|
||||
## Using the Dictionary
|
||||
|
||||
The dictionary shows every available word organized by category:
|
||||
|
||||
- **Stack**: Manipulation words like `dup`, `swap`, `drop`
|
||||
- **Arithmetic**: Math operations
|
||||
- **Sound**: Sound sources and emission
|
||||
- **Filter**, **Envelope**, **Effects**: Sound shaping
|
||||
- **Context**: Sequencer state like `step`, `beat`, `tempo`
|
||||
- And many more...
|
||||
|
||||
## Navigation
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Tab` | Switch between categories and words |
|
||||
| `↑/↓` or `j/k` | Navigate items |
|
||||
| `PgUp/PgDn` | Page through lists |
|
||||
| `/` or `Ctrl+F` | Search |
|
||||
| `Esc` | Clear search |
|
||||
|
||||
## Word Information
|
||||
|
||||
Each word entry shows:
|
||||
|
||||
- **Name** and aliases
|
||||
- **Stack effect**: `( before -- after )`
|
||||
- **Description**: What the word does
|
||||
- **Example**: How to use it
|
||||
|
||||
## Search
|
||||
|
||||
Press `/` to search across all words. The search matches word names, aliases, and descriptions. Press `Esc` to clear and return to browsing.
|
||||
|
||||
## Tips
|
||||
|
||||
- Use the dictionary while writing scripts to check stack effects
|
||||
- Categories group related words together
|
||||
- Some words have shorter aliases (e.g., `sound` → `s`)
|
||||
62
docs/editing.md
Normal file
62
docs/editing.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Editing a Step
|
||||
|
||||
Each step in Cagire contains a Forth script. When the sequencer reaches that step, it runs the script to produce sound. This is where you write your music. Press `Enter` when hovering over any step to open the `code editor`. The editor appears as a modal overlay with the step number in the title bar. If the step is a linked step (shown with an arrow like `→05`), pressing `Enter` navigates to the source step instead.
|
||||
|
||||
## Writing Scripts
|
||||
|
||||
Scripts are written in Forth. Type words and numbers separated by spaces. The simplest script that makes sound is:
|
||||
|
||||
```forth
|
||||
sine sound .
|
||||
```
|
||||
|
||||
Add parameters before words to modify them:
|
||||
|
||||
```forth
|
||||
c4 note 0.75 decay sine sound .
|
||||
```
|
||||
|
||||
Writing long lines is not recommended because it can become quite unmanageable. Instead, break them into multiple lines for clarity:
|
||||
|
||||
```forth
|
||||
c4 note
|
||||
0.75 decay
|
||||
sine sound
|
||||
0.4 verb
|
||||
.
|
||||
```
|
||||
|
||||
## Saving
|
||||
|
||||
- `Esc` - Save and close the editor
|
||||
- `Ctrl+E` - Save without closing
|
||||
|
||||
When you save, the script is compiled and sent to the sequencer. If there's an error, a message appears briefly at the bottom of the screen. You will also receive visual feedback in the form of a flashing window when saving / evaluating a script.
|
||||
|
||||
## Completion
|
||||
|
||||
As you type, the editor suggests matching Forth words. The completion list shows all built-in words that start with your current input. Press `Tab` to insert the selected suggestion, or `Esc` to dismiss the list. Use arrow keys to navigate between suggestions.
|
||||
|
||||
Completion helps you discover words without memorizing them all. Type a few letters and browse what's available. For example, typing `ver` will suggest `verb` (reverb), typing `fil` will show filter-related words.
|
||||
|
||||
## Debugging
|
||||
|
||||
Press `Ctrl+S` to toggle the stack display. This shows the stack state evaluated up to the cursor line, useful for understanding how values flow through your script. Press `Ctrl+R` to execute the script immediately without waiting for the sequencer to reach the step.
|
||||
|
||||
## Keybindings
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Esc` | Save and close |
|
||||
| `Ctrl+E` | Save without closing |
|
||||
| `Ctrl+R` | Execute script once |
|
||||
| `Ctrl+S` | Toggle stack display |
|
||||
| `Ctrl+F` | Search |
|
||||
| `Ctrl+N` | Find next |
|
||||
| `Ctrl+P` | Find previous |
|
||||
| `Ctrl+A` | Select all |
|
||||
| `Ctrl+C` | Copy |
|
||||
| `Ctrl+X` | Cut |
|
||||
| `Ctrl+V` | Paste |
|
||||
| `Shift+Arrows` | Extend selection |
|
||||
| `Tab` | Accept completion |
|
||||
2
docs/effects.md
Normal file
2
docs/effects.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Effects
|
||||
|
||||
48
docs/emitting.md
Normal file
48
docs/emitting.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Emitting Sounds
|
||||
|
||||
The core of Cagire is emitting sounds. Every step script builds up sound commands and emits them to the audio engine.
|
||||
|
||||
## The Sound Register
|
||||
|
||||
Before emitting, you must select a sound source using `sound` (or its alias `s`):
|
||||
|
||||
```forth
|
||||
"kick" sound
|
||||
"kick" s ( same thing, shorter )
|
||||
```
|
||||
|
||||
This sets the current sound register. All subsequent parameter words modify this sound until you emit or clear it.
|
||||
|
||||
## Emitting
|
||||
|
||||
The `.` word emits the current sound:
|
||||
|
||||
```forth
|
||||
"kick" s . ( emit one kick )
|
||||
"kick" s . . . ( emit three kicks )
|
||||
```
|
||||
|
||||
Use `.!` to emit multiple times:
|
||||
|
||||
```forth
|
||||
"kick" s 4 .! ( emit four kicks )
|
||||
```
|
||||
|
||||
## Clearing
|
||||
|
||||
The `clear` word resets the sound register and all parameters:
|
||||
|
||||
```forth
|
||||
"kick" s 0.5 gain . clear "hat" s .
|
||||
```
|
||||
|
||||
This is useful when you want to emit different sounds with independent parameters in the same step.
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `sound` / `s` | (name --) | Set sound source |
|
||||
| `.` | (--) | Emit current sound |
|
||||
| `.!` | (n --) | Emit current sound n times |
|
||||
| `clear` | (--) | Clear sound register and params |
|
||||
2
docs/envelopes.md
Normal file
2
docs/envelopes.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Envelopes
|
||||
|
||||
65
docs/eq_stereo.md
Normal file
65
docs/eq_stereo.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# EQ & Stereo
|
||||
|
||||
Shape the frequency balance and stereo image of your sounds.
|
||||
|
||||
## Three-Band EQ
|
||||
|
||||
```forth
|
||||
"mix" s
|
||||
3 eqlo ( boost low shelf by 3dB )
|
||||
-2 eqmid ( cut mids by 2dB )
|
||||
1 eqhi ( boost high shelf by 1dB )
|
||||
.
|
||||
```
|
||||
|
||||
## Tilt EQ
|
||||
|
||||
A simple one-knob EQ that tilts the frequency balance:
|
||||
|
||||
```forth
|
||||
"bright" s 0.5 tilt . ( brighter )
|
||||
"dark" s -0.5 tilt . ( darker )
|
||||
```
|
||||
|
||||
Range: -1 (dark) to 1 (bright)
|
||||
|
||||
## Gain and Pan
|
||||
|
||||
```forth
|
||||
"kick" s 0.8 gain . ( volume 0-1 )
|
||||
"hat" s 0.5 pan . ( pan right )
|
||||
"hat" s -0.5 pan . ( pan left )
|
||||
"snare" s 1.2 postgain . ( post-processing gain )
|
||||
"lead" s 100 velocity . ( velocity/dynamics )
|
||||
```
|
||||
|
||||
## Stereo Width
|
||||
|
||||
```forth
|
||||
"pad" s 0 width . ( mono )
|
||||
"pad" s 1 width . ( normal stereo )
|
||||
"pad" s 2 width . ( extra wide )
|
||||
```
|
||||
|
||||
## Haas Effect
|
||||
|
||||
Create spatial positioning with subtle delay between channels:
|
||||
|
||||
```forth
|
||||
"vocal" s 8 haas . ( 8ms delay for spatial effect )
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `eqlo` | (f --) | Set low shelf gain (dB) |
|
||||
| `eqmid` | (f --) | Set mid peak gain (dB) |
|
||||
| `eqhi` | (f --) | Set high shelf gain (dB) |
|
||||
| `tilt` | (f --) | Set tilt EQ (-1 dark, 1 bright) |
|
||||
| `gain` | (f --) | Set volume (0-1) |
|
||||
| `postgain` | (f --) | Set post gain |
|
||||
| `velocity` | (f --) | Set velocity |
|
||||
| `pan` | (f --) | Set pan (-1 to 1) |
|
||||
| `width` | (f --) | Set stereo width |
|
||||
| `haas` | (f --) | Set Haas delay (ms) |
|
||||
2
docs/filters.md
Normal file
2
docs/filters.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Filters
|
||||
|
||||
65
docs/generators.md
Normal file
65
docs/generators.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Generators
|
||||
|
||||
Create sequences of values on the stack.
|
||||
|
||||
## Arithmetic Range
|
||||
|
||||
The `..` operator pushes an arithmetic sequence:
|
||||
|
||||
```forth
|
||||
1 4 .. ( pushes 1 2 3 4 )
|
||||
60 67 .. ( pushes 60 61 62 63 64 65 66 67 )
|
||||
```
|
||||
|
||||
## Geometric Range
|
||||
|
||||
The `geom..` operator creates a geometric sequence:
|
||||
|
||||
```forth
|
||||
1 2 4 geom.. ( pushes 1 2 4 8 )
|
||||
100 0.5 4 geom.. ( pushes 100 50 25 12.5 )
|
||||
```
|
||||
|
||||
Stack: `(start ratio count -- values...)`
|
||||
|
||||
## Generate
|
||||
|
||||
The `gen` word executes a quotation multiple times:
|
||||
|
||||
```forth
|
||||
{ 1 6 rand } 4 gen ( 4 random values from 1-6 )
|
||||
{ coin } 8 gen ( 8 random 0/1 values )
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Random melody:
|
||||
|
||||
```forth
|
||||
"pluck" s
|
||||
{ 48 72 rand } 4 gen 4 choose note
|
||||
.
|
||||
```
|
||||
|
||||
Chord from range:
|
||||
|
||||
```forth
|
||||
"pad" s
|
||||
60 67 .. 8 choose note ( random note in octave )
|
||||
.
|
||||
```
|
||||
|
||||
Euclidean-style rhythm values:
|
||||
|
||||
```forth
|
||||
0 1 0 0 1 0 1 0 ( manual )
|
||||
{ coin } 8 gen ( random 8-step pattern )
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `..` | (start end -- values...) | Arithmetic sequence |
|
||||
| `geom..` | (start ratio count -- values...) | Geometric sequence |
|
||||
| `gen` | (quot n -- results...) | Execute quotation n times |
|
||||
52
docs/grid.md
Normal file
52
docs/grid.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# The Sequencer Grid
|
||||
|
||||
The sequencer grid is the main view of Cagire. This is the one you see when you open the application. On this view, you can see the step sequencer grid and edit each step using the `code editor`. At the top, you can optionally display an oscilloscope and a spectrum analyzer.
|
||||
|
||||
## Navigation
|
||||
|
||||
Use arrow keys to move between steps. The grid wraps around at pattern boundaries. You can move in any direction.
|
||||
|
||||
## Preview
|
||||
|
||||
Press `P` to enter preview mode. In preview mode, a view-only code editor opens so that you can see the script of the currently playing step. While in preview mode, you can still move around the grid. Press `Esc` to exit preview mode.
|
||||
|
||||
## Selection
|
||||
|
||||
Hold `Shift` while pressing arrow keys to select multiple steps. Press `Esc` to clear the selection.
|
||||
|
||||
## Editing Steps
|
||||
|
||||
- `Enter` - Open the script editor.
|
||||
- `t` - Toggle step active/inactive.
|
||||
- `r` - Rename a step.
|
||||
- `Del` - Delete selected steps.
|
||||
|
||||
## Copy & Paste
|
||||
|
||||
- `Ctrl+C` - Copy selected steps.
|
||||
- `Ctrl+V` - Paste as copies.
|
||||
- `Ctrl+B` - Paste as linked steps.
|
||||
- `Ctrl+D` - Duplicate selection.
|
||||
- `Ctrl+H` - Harden links (convert to copies).
|
||||
|
||||
`Linked steps` share the same script as their source. When you edit the source, all linked steps update automatically. This is an extremely important and powerful feature. It allows you to create complex patterns with minimal effort. `Ctrl+H` is your best friend to manage linked steps and convert them to real steps.
|
||||
|
||||
## Pattern Controls
|
||||
|
||||
- `<` / `>` - Decrease/increase pattern length
|
||||
- `[` / `]` - Decrease/increase pattern speed
|
||||
- `L` - Set length directly
|
||||
- `S` - Set speed directly
|
||||
|
||||
## Playback
|
||||
|
||||
- `Space` - Toggle play/stop
|
||||
- `+` / `-` - Adjust tempo
|
||||
- `T` - Set tempo directly
|
||||
- `Ctrl+R` - Run current step once (preview)
|
||||
|
||||
## Visual Indicators
|
||||
|
||||
- **Highlighted cell** - Currently playing step
|
||||
- **Colored backgrounds** - Linked steps share colors by source
|
||||
- **Arrow prefix** (`→05`) - Step is linked to step 05
|
||||
58
docs/how_it_works.md
Normal file
58
docs/how_it_works.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# How Does It Work?
|
||||
|
||||
Cagire is a step sequencer where each step contains a **Forth script** instead of the typical note data. When the sequencer reaches a step, it runs the script. A script _can do whatever it is programed to do_, such as producing sound commands sent to an internal audio engine. Everything else is similar to a step sequencer: you can `toggle` / `untoggle`, `copy` / `paste` any step or group of steps, etc. You are completely free to define what your scripts will do. It can be as simple as playing a note, or as complex as triggering random audio samples with complex effects. Scripts can also share code and data with each other.
|
||||
|
||||
## Project / session organization
|
||||
|
||||
Cagire can run multiple patterns concurrently. Each pattern contains a given number of steps. Every session / project is organized hierarchically:
|
||||
|
||||
- **32 Banks**
|
||||
- **32 Patterns** per bank
|
||||
- **128 Steps** per pattern
|
||||
|
||||
That's over 130,000 possible steps per project. Most of my sessions use 15-20 at best.
|
||||
|
||||
## What does a script look like?
|
||||
|
||||
Forth is a stack-based programming language. It is very minimalistic and emphasizes simplicity and readability. Using Forth doesn't feel like programming at all. It feels more like juggling with words and numbers or writing bad computer poetry. There is pretty much no syntax to learn, just a few rules to follow. Forth is ancient, powerful, flexible, and... super fun to live code with! Here is a minimal program that will play a middle C note using a sine wave:
|
||||
|
||||
```forth
|
||||
c4 note sine sound .
|
||||
```
|
||||
|
||||
Read the program backwards and you will understand what it does instantly:
|
||||
|
||||
- `.`: we want to play a sound.
|
||||
- `sine sound`: the sound is a sinewave.
|
||||
- `c4 note`: the pitch is C4 (middle-C).
|
||||
|
||||
Scripts can be simple one-liners or complex programs with conditionals, loops, and randomness. They tend to look like an accumulation of words and numbers. Use space and line returns to your advantage. The Forth language can be learned... on the spot. You just need to understand the following basic rules:
|
||||
|
||||
- there are `words` and `numbers`.
|
||||
- they are delimited by spaces.
|
||||
- everything piles up on the `stack`.
|
||||
|
||||
Obviously you will need to understand what the **stack** is, but it will take you five minutes. That's it. See the **Forth** section for details.
|
||||
|
||||
## The Audio Engine
|
||||
|
||||
Cagire includes a complete synthesis engine. No external software is required to play music. It comes with a large number of sound sources and sound shaping tools: oscillators, sample players, effects, filters, and more. The audio engine is quite capable and versatile, and can accomodate a vast array of genres / styles. Here are a few examples :
|
||||
|
||||
```forth
|
||||
;; sawtooth wave with lowpass filter, chorus and reverb
|
||||
saw sound 1200 lpf 0.2 chorus 0.8 verb .
|
||||
```
|
||||
|
||||
```forth
|
||||
;; pure sine wave with vibrato and bit crushing
|
||||
0.5 vibmod 4 vib sine sound 8 crush 0.8 gain .
|
||||
```
|
||||
|
||||
```forth
|
||||
;; very loud and pitched-down kick drum using an audio sample
|
||||
kkick sound 1.5 distort 0.9 postgain 0.8 speed .
|
||||
```
|
||||
|
||||
## Timing & Synchronization
|
||||
|
||||
Cagire uses **Ableton Link** to manage timing and synchronization. This means that all devices using the same protocol can be synchronized to the same tempo. Most commercial softwares support this protocol. The playback speed is defined as a BPM (beats per minute) value. Patterns can run at different speeds relative to the master tempo. Most of the durations in Cagire are defined in terms of beats.
|
||||
@@ -32,6 +32,10 @@
|
||||
- **Ctrl+C**: Copy step script
|
||||
- **Ctrl+V**: Paste step script
|
||||
|
||||
### Execution
|
||||
|
||||
- **Ctrl+R**: Run current step's script immediately (one-shot)
|
||||
|
||||
### Tempo
|
||||
|
||||
- **+ / =**: Increase tempo
|
||||
@@ -41,6 +45,7 @@
|
||||
|
||||
- **Tab / Esc**: Return to sequencer focus
|
||||
- **Ctrl+E**: Compile current step script
|
||||
- **Ctrl+R**: Run script in editor immediately (one-shot)
|
||||
|
||||
## Audio Page
|
||||
|
||||
|
||||
47
docs/ladder_filters.md
Normal file
47
docs/ladder_filters.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Ladder Filters
|
||||
|
||||
Ladder filters provide a classic analog-style filter sound with stronger resonance character than the standard SVF filters.
|
||||
|
||||
## Ladder Lowpass
|
||||
|
||||
```forth
|
||||
"saw" s 2000 llpf . ( ladder lowpass frequency )
|
||||
"saw" s 0.7 llpq . ( ladder lowpass resonance )
|
||||
```
|
||||
|
||||
## Ladder Highpass
|
||||
|
||||
```forth
|
||||
"noise" s 500 lhpf . ( ladder highpass frequency )
|
||||
"noise" s 0.5 lhpq . ( ladder highpass resonance )
|
||||
```
|
||||
|
||||
## Ladder Bandpass
|
||||
|
||||
```forth
|
||||
"pad" s 1000 lbpf . ( ladder bandpass frequency )
|
||||
"pad" s 0.6 lbpq . ( ladder bandpass resonance )
|
||||
```
|
||||
|
||||
## Comparison with SVF
|
||||
|
||||
Ladder filters have a different resonance character:
|
||||
- More aggressive self-oscillation at high resonance
|
||||
- Classic "squelchy" acid sound
|
||||
- 24dB/octave slope
|
||||
|
||||
Standard SVF filters (`lpf`, `hpf`, `bpf`) have:
|
||||
- Cleaner resonance
|
||||
- Full envelope control (attack, decay, sustain, release)
|
||||
- 12dB/octave slope
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `llpf` | (f --) | Set ladder lowpass frequency |
|
||||
| `llpq` | (f --) | Set ladder lowpass resonance |
|
||||
| `lhpf` | (f --) | Set ladder highpass frequency |
|
||||
| `lhpq` | (f --) | Set ladder highpass resonance |
|
||||
| `lbpf` | (f --) | Set ladder bandpass frequency |
|
||||
| `lbpq` | (f --) | Set ladder bandpass resonance |
|
||||
74
docs/lfo.md
Normal file
74
docs/lfo.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# LFO & Ramps
|
||||
|
||||
Generate time-varying values synchronized to the beat for modulation.
|
||||
|
||||
## Ramp
|
||||
|
||||
The `ramp` word creates a sawtooth wave from 0 to 1:
|
||||
|
||||
```forth
|
||||
0.25 1.0 ramp ( one cycle per 4 beats, linear )
|
||||
1.0 2.0 ramp ( one cycle per beat, exponential curve )
|
||||
```
|
||||
|
||||
Stack: `(freq curve -- val)`
|
||||
|
||||
## Shortcut Ramps
|
||||
|
||||
```forth
|
||||
0.5 linramp ( linear ramp, curve=1 )
|
||||
0.25 expramp ( exponential ramp, curve=3 )
|
||||
2.0 logramp ( logarithmic ramp, curve=0.3 )
|
||||
```
|
||||
|
||||
## Triangle Wave
|
||||
|
||||
```forth
|
||||
0.5 tri ( triangle wave 0→1→0 )
|
||||
```
|
||||
|
||||
## Perlin Noise
|
||||
|
||||
Smooth random modulation:
|
||||
|
||||
```forth
|
||||
0.25 perlin ( smooth noise at 0.25x beat rate )
|
||||
```
|
||||
|
||||
## Range Scaling
|
||||
|
||||
Scale a 0-1 value to any range:
|
||||
|
||||
```forth
|
||||
0.5 200 800 range ( 0.5 becomes 500 )
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Modulate filter cutoff:
|
||||
|
||||
```forth
|
||||
"saw" s
|
||||
0.25 1.0 ramp 500 2000 range lpf
|
||||
.
|
||||
```
|
||||
|
||||
Random panning:
|
||||
|
||||
```forth
|
||||
"hat" s
|
||||
0.5 perlin -1 1 range pan
|
||||
.
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `ramp` | (freq curve -- val) | Ramp 0-1: fract(freq*beat)^curve |
|
||||
| `linramp` | (freq -- val) | Linear ramp (curve=1) |
|
||||
| `expramp` | (freq -- val) | Exponential ramp (curve=3) |
|
||||
| `logramp` | (freq -- val) | Logarithmic ramp (curve=0.3) |
|
||||
| `tri` | (freq -- val) | Triangle wave 0→1→0 |
|
||||
| `perlin` | (freq -- val) | Perlin noise 0-1 |
|
||||
| `range` | (val min max -- scaled) | Scale 0-1 to min-max |
|
||||
2
docs/link.md
Normal file
2
docs/link.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Ableton Link
|
||||
|
||||
58
docs/lofi.md
Normal file
58
docs/lofi.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Lo-fi Effects
|
||||
|
||||
Add grit, warmth, and character with bit crushing, folding, and distortion.
|
||||
|
||||
## Bit Crush
|
||||
|
||||
Reduce bit depth for digital artifacts:
|
||||
|
||||
```forth
|
||||
"drum" s 8 crush . ( 8-bit crush )
|
||||
"drum" s 4 crush . ( heavy 4-bit crush )
|
||||
```
|
||||
|
||||
## Wave Folding
|
||||
|
||||
Fold the waveform back on itself for complex harmonics:
|
||||
|
||||
```forth
|
||||
"sine" s 2 fold . ( moderate folding )
|
||||
"sine" s 4 fold . ( aggressive folding )
|
||||
```
|
||||
|
||||
## Wave Wrap
|
||||
|
||||
Wrap the waveform for a different flavor of distortion:
|
||||
|
||||
```forth
|
||||
"bass" s 0.5 wrap .
|
||||
```
|
||||
|
||||
## Distortion
|
||||
|
||||
Classic overdrive/distortion:
|
||||
|
||||
```forth
|
||||
"guitar" s 0.5 distort . ( distortion amount )
|
||||
"guitar" s 0.8 distortvol . ( output volume )
|
||||
```
|
||||
|
||||
## Combining Effects
|
||||
|
||||
```forth
|
||||
"drum" s
|
||||
12 crush
|
||||
1.5 fold
|
||||
0.3 distort
|
||||
.
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `crush` | (f --) | Set bit crush depth |
|
||||
| `fold` | (f --) | Set wave fold amount |
|
||||
| `wrap` | (f --) | Set wave wrap amount |
|
||||
| `distort` | (f --) | Set distortion amount |
|
||||
| `distortvol` | (f --) | Set distortion output volume |
|
||||
89
docs/logic.md
Normal file
89
docs/logic.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Logic
|
||||
|
||||
Boolean operations and conditional execution.
|
||||
|
||||
## Boolean Operators
|
||||
|
||||
```forth
|
||||
1 1 and ( 1 )
|
||||
0 1 or ( 1 )
|
||||
1 not ( 0 )
|
||||
1 0 xor ( 1 )
|
||||
1 1 nand ( 0 )
|
||||
0 0 nor ( 1 )
|
||||
```
|
||||
|
||||
## Conditional Execution
|
||||
|
||||
Execute a quotation if condition is true:
|
||||
|
||||
```forth
|
||||
{ "kick" s . } coin ? ( 50% chance )
|
||||
{ 0.5 gain } step 0 = ? ( only on step 0 )
|
||||
```
|
||||
|
||||
Execute if false:
|
||||
|
||||
```forth
|
||||
{ "snare" s . } coin !? ( if NOT coin )
|
||||
```
|
||||
|
||||
## If-Else
|
||||
|
||||
```forth
|
||||
{ "kick" s } { "snare" s } coin ifelse .
|
||||
```
|
||||
|
||||
Stack: `(true-quot false-quot bool --)`
|
||||
|
||||
## Pick
|
||||
|
||||
Choose from multiple quotations:
|
||||
|
||||
```forth
|
||||
{ 60 } { 64 } { 67 } step 3 mod pick note
|
||||
```
|
||||
|
||||
Stack: `(quot1 quot2 ... quotN n -- result)`
|
||||
|
||||
## Apply
|
||||
|
||||
Execute a quotation unconditionally:
|
||||
|
||||
```forth
|
||||
{ 2 * } apply ( doubles top of stack )
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Conditional parameter:
|
||||
|
||||
```forth
|
||||
"kick" s
|
||||
{ 0.8 } { 0.4 } iter 2 mod = ifelse gain
|
||||
.
|
||||
```
|
||||
|
||||
Multi-way branching:
|
||||
|
||||
```forth
|
||||
"synth" s
|
||||
{ c4 } { e4 } { g4 } { c5 } step 4 mod pick note
|
||||
.
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `and` | (a b -- bool) | Logical and |
|
||||
| `or` | (a b -- bool) | Logical or |
|
||||
| `not` | (a -- bool) | Logical not |
|
||||
| `xor` | (a b -- bool) | Exclusive or |
|
||||
| `nand` | (a b -- bool) | Not and |
|
||||
| `nor` | (a b -- bool) | Not or |
|
||||
| `?` | (quot bool --) | Execute if true |
|
||||
| `!?` | (quot bool --) | Execute if false |
|
||||
| `ifelse` | (t-quot f-quot bool --) | If-else |
|
||||
| `pick` | (..quots n --) | Execute nth quotation |
|
||||
| `apply` | (quot --) | Execute unconditionally |
|
||||
61
docs/mod_fx.md
Normal file
61
docs/mod_fx.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Modulation Effects
|
||||
|
||||
Phaser, flanger, and chorus effects for movement and width.
|
||||
|
||||
## Phaser
|
||||
|
||||
```forth
|
||||
"pad" s
|
||||
1 phaser ( rate in Hz )
|
||||
0.5 phaserdepth ( depth )
|
||||
0.5 phasersweep ( sweep range )
|
||||
1000 phasercenter ( center frequency )
|
||||
.
|
||||
```
|
||||
|
||||
## Flanger
|
||||
|
||||
```forth
|
||||
"guitar" s
|
||||
0.5 flanger ( rate )
|
||||
0.5 flangerdepth ( depth )
|
||||
0.5 flangerfeedback ( feedback for metallic sound )
|
||||
.
|
||||
```
|
||||
|
||||
## Chorus
|
||||
|
||||
```forth
|
||||
"keys" s
|
||||
1 chorus ( rate )
|
||||
0.5 chorusdepth ( depth )
|
||||
0.02 chorusdelay ( base delay time )
|
||||
.
|
||||
```
|
||||
|
||||
## Combining Effects
|
||||
|
||||
```forth
|
||||
"pad" s
|
||||
c4 note
|
||||
0.3 phaserdepth
|
||||
0.5 phaser
|
||||
0.2 chorus
|
||||
0.3 chorusdepth
|
||||
.
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `phaser` | (f --) | Set phaser rate |
|
||||
| `phaserdepth` | (f --) | Set phaser depth |
|
||||
| `phasersweep` | (f --) | Set phaser sweep |
|
||||
| `phasercenter` | (f --) | Set phaser center frequency |
|
||||
| `flanger` | (f --) | Set flanger rate |
|
||||
| `flangerdepth` | (f --) | Set flanger depth |
|
||||
| `flangerfeedback` | (f --) | Set flanger feedback |
|
||||
| `chorus` | (f --) | Set chorus rate |
|
||||
| `chorusdepth` | (f --) | Set chorus depth |
|
||||
| `chorusdelay` | (f --) | Set chorus delay |
|
||||
2
docs/modulation.md
Normal file
2
docs/modulation.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Modulation
|
||||
|
||||
39
docs/navigation.md
Normal file
39
docs/navigation.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Navigation
|
||||
|
||||
The Cagire application is organized as a grid composed of six views:
|
||||
|
||||
```
|
||||
Dict Patterns Options
|
||||
Help Sequencer Engine
|
||||
```
|
||||
|
||||
- `Dict` (Dictionary): A comprehensive list of all the `Forth` words used in the application.
|
||||
- `Help`: Provides detailed information about the application's features and functionalities.
|
||||
- `Patterns`: Pattern banks and pattern manager. Used to organize a session / project.
|
||||
- `Sequencer`: The main view, where you edit sequences and play music.
|
||||
- `Options`: Configuration settings for the application.
|
||||
- `Engine`: Configuration settings for the internal audio engine.
|
||||
|
||||
## Switching Views
|
||||
|
||||
Use `Ctrl+Arrow` keys to move between views. A minimap appears briefly showing your position in the grid.
|
||||
|
||||
- `Ctrl+Left` / `Ctrl+Right` - move horizontally
|
||||
- `Ctrl+Up` / `Ctrl+Down` - move vertically
|
||||
|
||||
The grid wraps horizontally, so you can cycle through views on the same row.
|
||||
|
||||
## Getting Help
|
||||
|
||||
Press `?` on any view to see its keybindings. This shows all available shortcuts for the current context.
|
||||
|
||||
Press `Esc` to close the keybindings panel.
|
||||
|
||||
## Common Keys
|
||||
|
||||
These work on most views:
|
||||
|
||||
- `Arrow keys` - move or scroll
|
||||
- `Tab` - switch focus between panels
|
||||
- `/` or `Ctrl+f` - search (where available)
|
||||
- `q` - quit (with confirmation)
|
||||
2
docs/notes.md
Normal file
2
docs/notes.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Notes & MIDI
|
||||
|
||||
75
docs/oscillators.md
Normal file
75
docs/oscillators.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Oscillators
|
||||
|
||||
Control synthesis oscillator parameters for synth sounds.
|
||||
|
||||
## Frequency and Pitch
|
||||
|
||||
```forth
|
||||
"sine" s 440 freq . ( set frequency in Hz )
|
||||
"saw" s 60 note . ( set MIDI note )
|
||||
"square" s 0.01 detune . ( slight detune )
|
||||
"lead" s 12 coarse . ( coarse tune in semitones )
|
||||
"bass" s 0.1 glide . ( portamento )
|
||||
```
|
||||
|
||||
## Pulse Width
|
||||
|
||||
For pulse/square oscillators:
|
||||
|
||||
```forth
|
||||
"pulse" s 0.3 pw . ( narrow pulse )
|
||||
"pulse" s 0.5 pw . ( square wave )
|
||||
```
|
||||
|
||||
## Stereo Spread
|
||||
|
||||
```forth
|
||||
"pad" s 0.5 spread . ( stereo spread amount )
|
||||
```
|
||||
|
||||
## Harmonics and Timbre
|
||||
|
||||
For oscillators that support it (like mutable):
|
||||
|
||||
```forth
|
||||
"mutable" s 4 harmonics . ( harmonic content )
|
||||
"mutable" s 0.5 timbre . ( timbre control )
|
||||
"mutable" s 0.3 morph . ( morph parameter )
|
||||
```
|
||||
|
||||
## Multiplier and Warp
|
||||
|
||||
```forth
|
||||
"fm" s 2 mult . ( frequency multiplier )
|
||||
"wt" s 0.5 warp . ( warp amount )
|
||||
"wt" s 1 mirror . ( mirror waveform )
|
||||
```
|
||||
|
||||
## Sub Oscillator
|
||||
|
||||
```forth
|
||||
"bass" s 0.5 sub . ( sub oscillator level )
|
||||
"bass" s 2 suboct . ( sub octave down )
|
||||
"bass" s 1 subwave . ( sub waveform )
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `freq` | (f --) | Set frequency (Hz) |
|
||||
| `note` | (n --) | Set MIDI note |
|
||||
| `detune` | (f --) | Set detune amount |
|
||||
| `coarse` | (f --) | Set coarse tune |
|
||||
| `glide` | (f --) | Set glide/portamento |
|
||||
| `pw` | (f --) | Set pulse width |
|
||||
| `spread` | (f --) | Set stereo spread |
|
||||
| `mult` | (f --) | Set multiplier |
|
||||
| `warp` | (f --) | Set warp amount |
|
||||
| `mirror` | (f --) | Set mirror |
|
||||
| `harmonics` | (f --) | Set harmonics |
|
||||
| `timbre` | (f --) | Set timbre |
|
||||
| `morph` | (f --) | Set morph |
|
||||
| `sub` | (f --) | Set sub oscillator level |
|
||||
| `suboct` | (n --) | Set sub octave |
|
||||
| `subwave` | (n --) | Set sub waveform |
|
||||
2
docs/parameters.md
Normal file
2
docs/parameters.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Parameters
|
||||
|
||||
2
docs/patterns.md
Normal file
2
docs/patterns.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Pattern Management
|
||||
|
||||
44
docs/pitch_envelope.md
Normal file
44
docs/pitch_envelope.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Pitch Envelope
|
||||
|
||||
Control pitch modulation over time with a dedicated ADSR envelope.
|
||||
|
||||
## Envelope Amount
|
||||
|
||||
```forth
|
||||
"bass" s 0.5 penv . ( pitch envelope depth )
|
||||
"bass" s -0.5 penv . ( negative envelope )
|
||||
```
|
||||
|
||||
## Envelope Shape
|
||||
|
||||
```forth
|
||||
"kick" s
|
||||
1.0 penv ( full envelope depth )
|
||||
0.001 patt ( instant attack )
|
||||
0.1 pdec ( fast decay )
|
||||
0 psus ( no sustain )
|
||||
0.1 prel ( short release )
|
||||
.
|
||||
```
|
||||
|
||||
## Classic Kick Drum
|
||||
|
||||
```forth
|
||||
"sine" s
|
||||
40 note
|
||||
2.0 penv
|
||||
0.001 patt
|
||||
0.05 pdec
|
||||
0 psus
|
||||
.
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `penv` | (f --) | Set pitch envelope amount |
|
||||
| `patt` | (f --) | Set pitch attack time |
|
||||
| `pdec` | (f --) | Set pitch decay time |
|
||||
| `psus` | (f --) | Set pitch sustain level |
|
||||
| `prel` | (f --) | Set pitch release time |
|
||||
2
docs/probability.md
Normal file
2
docs/probability.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Probability
|
||||
|
||||
2
docs/randomness.md
Normal file
2
docs/randomness.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Randomness
|
||||
|
||||
76
docs/samples.md
Normal file
76
docs/samples.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Samples
|
||||
|
||||
Control sample playback with timing, looping, and slice parameters.
|
||||
|
||||
## Basic Playback
|
||||
|
||||
```forth
|
||||
"break" s . ( play sample from start )
|
||||
"break" s 0.5 speed . ( half speed )
|
||||
"break" s -1 speed . ( reverse )
|
||||
```
|
||||
|
||||
## Time and Position
|
||||
|
||||
Control where in the sample to start and end:
|
||||
|
||||
```forth
|
||||
"break" s 0.25 begin . ( start at 25% )
|
||||
"break" s 0.5 end . ( end at 50% )
|
||||
"break" s 0.1 time . ( offset start time )
|
||||
```
|
||||
|
||||
## Duration and Gate
|
||||
|
||||
```forth
|
||||
"pad" s 0.5 dur . ( play for 0.5 seconds )
|
||||
"pad" s 0.8 gate . ( gate time as fraction )
|
||||
```
|
||||
|
||||
## Looping
|
||||
|
||||
Fit a sample to a number of beats:
|
||||
|
||||
```forth
|
||||
"break" s 4 loop . ( fit to 4 beats )
|
||||
```
|
||||
|
||||
## Repetition
|
||||
|
||||
```forth
|
||||
"hat" s 4 repeat . ( trigger 4 times )
|
||||
```
|
||||
|
||||
## Voice and Routing
|
||||
|
||||
```forth
|
||||
"kick" s 1 voice . ( assign to voice 1 )
|
||||
"snare" s 0 orbit . ( route to bus 0 )
|
||||
```
|
||||
|
||||
## Sample Selection
|
||||
|
||||
```forth
|
||||
"kick" s 2 n . ( select sample #2 from folder )
|
||||
"kit" s "a" bank . ( use bank suffix )
|
||||
1 cut ( cut group - stops other sounds in same group )
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `time` | (f --) | Set time offset |
|
||||
| `repeat` | (n --) | Set repeat count |
|
||||
| `dur` | (f --) | Set duration |
|
||||
| `gate` | (f --) | Set gate time |
|
||||
| `speed` | (f --) | Set playback speed |
|
||||
| `begin` | (f --) | Set sample start (0-1) |
|
||||
| `end` | (f --) | Set sample end (0-1) |
|
||||
| `loop` | (n --) | Fit sample to n beats |
|
||||
| `voice` | (n --) | Set voice number |
|
||||
| `orbit` | (n --) | Set orbit/bus |
|
||||
| `n` | (n --) | Set sample number |
|
||||
| `bank` | (str --) | Set sample bank suffix |
|
||||
| `cut` | (n --) | Set cut group |
|
||||
| `reset` | (n --) | Reset parameter |
|
||||
2
docs/scales.md
Normal file
2
docs/scales.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Scales
|
||||
|
||||
62
docs/selection.md
Normal file
62
docs/selection.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Selection
|
||||
|
||||
Cycle through values over time for evolving patterns.
|
||||
|
||||
## Step Cycle
|
||||
|
||||
`cycle` cycles through values based on step runs:
|
||||
|
||||
```forth
|
||||
60 64 67 3 cycle note ( cycle through C, E, G )
|
||||
```
|
||||
|
||||
Each time the step runs, it picks the next value.
|
||||
|
||||
## Pattern Cycle
|
||||
|
||||
`pcycle` cycles based on pattern iteration:
|
||||
|
||||
```forth
|
||||
60 64 67 3 pcycle note ( change note each pattern loop )
|
||||
```
|
||||
|
||||
## Emit-Time Cycle
|
||||
|
||||
`tcycle` creates a cycle list resolved at emit time, useful with `.!`:
|
||||
|
||||
```forth
|
||||
60 64 67 3 tcycle note 3 .! ( emit C, E, G in sequence )
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Rotating bass notes:
|
||||
|
||||
```forth
|
||||
"bass" s
|
||||
c3 e3 g3 b3 4 cycle note
|
||||
.
|
||||
```
|
||||
|
||||
Evolving pattern over loops:
|
||||
|
||||
```forth
|
||||
"lead" s
|
||||
0.5 1.0 0.75 0.25 4 pcycle gain
|
||||
.
|
||||
```
|
||||
|
||||
Arpeggiated chord:
|
||||
|
||||
```forth
|
||||
"pluck" s
|
||||
c4 e4 g4 c5 4 tcycle note 4 .!
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `cycle` | (v1..vn n -- val) | Cycle by step runs |
|
||||
| `pcycle` | (v1..vn n -- val) | Cycle by pattern iteration |
|
||||
| `tcycle` | (v1..vn n -- list) | Create cycle list for emit-time |
|
||||
2
docs/sound_basics.md
Normal file
2
docs/sound_basics.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Sound Basics
|
||||
|
||||
118
docs/stack.md
Normal file
118
docs/stack.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# The Stack
|
||||
|
||||
Forth is a stack-based language. Instead of variables and expressions, you push values onto a stack and use words that consume and produce values.
|
||||
|
||||
## How It Works
|
||||
|
||||
The stack is a last-in, first-out (LIFO) structure. Values you type get pushed on top. Words pop values off and push results back.
|
||||
|
||||
```
|
||||
3 4 +
|
||||
```
|
||||
|
||||
Step by step:
|
||||
1. `3` → push 3 onto stack: `[3]`
|
||||
2. `4` → push 4 onto stack: `[3, 4]`
|
||||
3. `+` → pop two values, add them, push result: `[7]`
|
||||
|
||||
## Values
|
||||
|
||||
Three types can live on the stack:
|
||||
|
||||
- **Integers**: `42`, `-7`, `0`
|
||||
- **Floats**: `3.14`, `0.5`, `-1.0`
|
||||
- **Strings**: `"kick"`, `"hello"`
|
||||
|
||||
## Stack Notation
|
||||
|
||||
Documentation uses stack effect notation:
|
||||
|
||||
```
|
||||
( before -- after )
|
||||
```
|
||||
|
||||
For example, `+` has effect `( a b -- sum )` meaning it takes two values and leaves one.
|
||||
|
||||
## Core Words
|
||||
|
||||
### dup
|
||||
|
||||
Duplicate the top value.
|
||||
|
||||
```
|
||||
3 dup ( 3 3 )
|
||||
```
|
||||
|
||||
### drop
|
||||
|
||||
Discard the top value.
|
||||
|
||||
```
|
||||
3 4 drop ( 3 )
|
||||
```
|
||||
|
||||
### swap
|
||||
|
||||
Swap the top two values.
|
||||
|
||||
```
|
||||
3 4 swap ( 4 3 )
|
||||
```
|
||||
|
||||
### over
|
||||
|
||||
Copy the second value to the top.
|
||||
|
||||
```
|
||||
3 4 over ( 3 4 3 )
|
||||
```
|
||||
|
||||
### rot
|
||||
|
||||
Rotate the top three values.
|
||||
|
||||
```
|
||||
1 2 3 rot ( 2 3 1 )
|
||||
```
|
||||
|
||||
### nip
|
||||
|
||||
Drop the second value.
|
||||
|
||||
```
|
||||
3 4 nip ( 4 )
|
||||
```
|
||||
|
||||
### tuck
|
||||
|
||||
Copy top value below second.
|
||||
|
||||
```
|
||||
3 4 tuck ( 4 3 4 )
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Build a chord by duplicating and adding:
|
||||
|
||||
```
|
||||
60 dup 4 + swap 7 + ( 64 67 60 )
|
||||
```
|
||||
|
||||
Use `over` to keep a base value:
|
||||
|
||||
```
|
||||
c4 over M3 swap P5 ( e4 g4 c4 )
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `dup` | (a -- a a) | Duplicate top |
|
||||
| `drop` | (a --) | Discard top |
|
||||
| `swap` | (a b -- b a) | Swap top two |
|
||||
| `over` | (a b -- a b a) | Copy second to top |
|
||||
| `rot` | (a b c -- b c a) | Rotate three |
|
||||
| `nip` | (a b -- b) | Drop second |
|
||||
| `tuck` | (a b -- b a b) | Copy top below second |
|
||||
50
docs/staging.md
Normal file
50
docs/staging.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Stage / Commit
|
||||
|
||||
Cagire requires you to `stage` changes you wish to make to the playback state and then `commit` it. It is way more simple than it seems. For instance, you mark pattern `04` and `05` to start playing, and _then_ you send the order to start the playback (`commit`). The same goes for stopping patterns. You mark which pattern to stop (`stage`) and then you give the order to stop them (`commit`). Why is staging useful? Here are some reasons why this design choice was made:
|
||||
|
||||
- **To apply multiple changes**: Queue several patterns to start/stop, commit them together.
|
||||
- **To get clean timing**: All changes happen on beat/bar boundaries.
|
||||
- **To help with live performance**: Prepare the next section without affecting current playback.
|
||||
|
||||
Staging is an essential feature to understand to be effective when doing live performances:
|
||||
|
||||
1. Open the **Patterns** view (`Ctrl+Up` from sequencer)
|
||||
2. Navigate to a pattern you wish to change/play
|
||||
3. Press `Space` to stage it. The pending change is going to be displayed:
|
||||
- `+` (staged to play)
|
||||
- `-` (staged to stop)
|
||||
4. Repeat for other patterns you want to change
|
||||
5. Press `c` to commit all changes
|
||||
6. Or press `Esc` to cancel
|
||||
|
||||
A pattern might not start immediately depending on the sync mode you have chosen. It might wait for the next beat/bar boundary.
|
||||
|
||||
## Status Indicators
|
||||
|
||||
| Indicator | Meaning |
|
||||
|-----------|---------|
|
||||
| `>` | Currently playing |
|
||||
| `+` | Staged to play |
|
||||
| `-` | Staged to stop |
|
||||
|
||||
A pattern can show both `>` (playing) and `-` (staged to stop).
|
||||
|
||||
## Quantization
|
||||
|
||||
Committed changes don't execute immediately. They wait for a quantization boundary:
|
||||
|
||||
| Setting | Behavior |
|
||||
|---------|----------|
|
||||
| Immediate | Next sequencer tick |
|
||||
| Beat | Next beat |
|
||||
| 1 Bar | Next bar (default) |
|
||||
| 2/4/8 Bars | Next 2, 4, or 8-bar boundary |
|
||||
|
||||
Edit quantization in pattern properties (press `e` on a pattern).
|
||||
|
||||
## Sync Mode
|
||||
|
||||
When a pattern starts, its playback position depends on sync mode:
|
||||
|
||||
- **Reset**: Always start at step 0
|
||||
- **Phase-Lock**: Start at the current beat-aligned position (stays in sync with other patterns)
|
||||
2
docs/tempo.md
Normal file
2
docs/tempo.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Tempo & Speed
|
||||
|
||||
2
docs/timing.md
Normal file
2
docs/timing.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Timing
|
||||
|
||||
2
docs/variables.md
Normal file
2
docs/variables.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Variables
|
||||
|
||||
44
docs/vibrato.md
Normal file
44
docs/vibrato.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Vibrato
|
||||
|
||||
Add pitch vibrato to oscillator sounds.
|
||||
|
||||
## Basic Vibrato
|
||||
|
||||
```forth
|
||||
"lead" s
|
||||
60 note
|
||||
5 vib ( vibrato rate in Hz )
|
||||
0.5 vibmod ( vibrato depth )
|
||||
.
|
||||
```
|
||||
|
||||
## Vibrato Shape
|
||||
|
||||
Control the LFO waveform:
|
||||
|
||||
```forth
|
||||
"pad" s
|
||||
c4 note
|
||||
4 vib
|
||||
0.3 vibmod
|
||||
0 vibshape ( 0=sine, other values for different shapes )
|
||||
.
|
||||
```
|
||||
|
||||
## Subtle vs Expressive
|
||||
|
||||
```forth
|
||||
( subtle vibrato for pads )
|
||||
"pad" s 3 vib 0.1 vibmod .
|
||||
|
||||
( expressive vibrato for leads )
|
||||
"lead" s 6 vib 0.8 vibmod .
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `vib` | (f --) | Set vibrato rate (Hz) |
|
||||
| `vibmod` | (f --) | Set vibrato depth |
|
||||
| `vibshape` | (f --) | Set vibrato LFO shape |
|
||||
55
docs/wavetables.md
Normal file
55
docs/wavetables.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Wavetables
|
||||
|
||||
Control wavetable synthesis parameters for scanning through waveforms.
|
||||
|
||||
## Scan Position
|
||||
|
||||
The `scan` parameter controls which waveform in the wavetable is active:
|
||||
|
||||
```forth
|
||||
"wt" s 0.0 scan . ( first waveform )
|
||||
"wt" s 0.5 scan . ( middle of table )
|
||||
"wt" s 1.0 scan . ( last waveform )
|
||||
```
|
||||
|
||||
## Wavetable Length
|
||||
|
||||
Set the cycle length in samples:
|
||||
|
||||
```forth
|
||||
"wt" s 2048 wtlen . ( standard wavetable size )
|
||||
```
|
||||
|
||||
## Scan Modulation
|
||||
|
||||
Animate the scan position with an LFO:
|
||||
|
||||
```forth
|
||||
"wt" s 0.2 scanlfo . ( LFO rate in Hz )
|
||||
"wt" s 0.4 scandepth . ( LFO depth 0-1 )
|
||||
"wt" s "tri" scanshape . ( LFO shape )
|
||||
```
|
||||
|
||||
Available shapes: `sine`, `tri`, `saw`, `square`, `sh` (sample & hold)
|
||||
|
||||
## Example
|
||||
|
||||
```forth
|
||||
"wavetable" s
|
||||
60 note
|
||||
0.25 scan
|
||||
0.1 scanlfo
|
||||
0.3 scandepth
|
||||
"sine" scanshape
|
||||
.
|
||||
```
|
||||
|
||||
## Words
|
||||
|
||||
| Word | Stack | Description |
|
||||
|------|-------|-------------|
|
||||
| `scan` | (f --) | Set wavetable scan position (0-1) |
|
||||
| `wtlen` | (n --) | Set wavetable cycle length in samples |
|
||||
| `scanlfo` | (f --) | Set scan LFO rate (Hz) |
|
||||
| `scandepth` | (f --) | Set scan LFO depth (0-1) |
|
||||
| `scanshape` | (s --) | Set scan LFO shape |
|
||||
@@ -1,19 +1,31 @@
|
||||
# Welcome to Cagire
|
||||
|
||||
Cagire is a terminal-based step sequencer for live coding music. Each step in a pattern contains a **Forth** script that produces sound and create events. It is made by BuboBubo (Raphaël Maurice Forment): [https://raphaelforment.fr](https://raphaelforment.fr). Cagire is open-source (AGPL-3.0 licensed) and available on GitHub : [https://github.com/BuboBubo/cagire](https://github.com/BuboBubo/cagire). This help view will teach you everything you need to know to start using Cagire and and to live code with it. To use Cagire, you will need to understand two things:
|
||||
Cagire is a terminal-based step sequencer for live coding music. Each step on the sequencer is defined by a **Forth** script that produces sound and create events. The documentation you are currently reading acts both as _tutorial_ and _reference_. It contains everything you need to know to use Cagire effectively. We recommend you to dive in and explore by picking subjects that interest you before slowly learning about everything else. Here are some recommended topics to start with:
|
||||
|
||||
1) How the sequencer works: dealing with steps, patterns and banks.
|
||||
2) How to write a script: how to make sound using code.
|
||||
1) How the sequencer works? Banks, patterns and steps.
|
||||
* the sequencer model, the pattern model, the step sequencer.
|
||||
2) How to write a script? How to make sound using code.
|
||||
* how to write simple scripts that play `musical events`.
|
||||
* how to extend these scripts with `logic` and/or `randomness`.
|
||||
* how define `WORDS`, `variables`, and share data between steps.
|
||||
3) What can I do with the audio engine?
|
||||
* audio sources: samples, oscillators, wavetables, noise generators.
|
||||
* audio effects: filters, delay, reverb, distortion, modulations.
|
||||
4) How far can it go?
|
||||
* how to live code with Cagire.
|
||||
* how fast can I break things?
|
||||
|
||||
## Pages
|
||||
## What is live coding?
|
||||
|
||||
Cagire is organized in several views. Navigate between them using **Ctrl+Left/Right/Up/Down**:
|
||||
Live coding is a technique where a programmer writes code in real-time in front of an audience. It is a way to experiment with code, to share things and thoughts openly, to express yourself through code. It can be technical, poetical, weird, preferably all at once. Live coding can be used to create music, visual art, and other forms of media. Learn more about live coding on [https://toplap.org](https://toplap.org) or [https://livecoding.fr](https://livecoding.fr). Live coding is an autotelic activity: it is an activity that is intrinsically rewarding, and the act of doing it is its own reward. There are no errors, only fun.
|
||||
|
||||
- **Sequencer**: Main view. Edit or preview patterns and scripts. Write Forth scripts.
|
||||
- **Patterns**: Project patterns management. 32 banks of 32 patterns per project. Edit pattern properties (name, length, etc).
|
||||
- **Engine**: Internal audio engine management: device selection, sample loading, performance options and voice / state monitoring.
|
||||
- **Options**: General application settings.
|
||||
- **Dict**: Forth word dictionary, organized by category. Learn about the internal programming language and its features.
|
||||
- **Help**: Documentation. This is the page view you are looking at right now.
|
||||
## About
|
||||
|
||||
Have fun with Cagire! Remember that live coding is all about experimentation and exploration!
|
||||
Cagire is built by BuboBubo (Raphaël Maurice Forment, [https://raphaelforment.fr](https://raphaelforment.fr)). It is a free and open-source project licensed under the `AGPL-3.0 License`. You are free to contribute to the project by making direct contributions to the codebase or by providing feedback and suggestions.
|
||||
|
||||
### Credits
|
||||
|
||||
* **Doux** (audio engine) is a Rust port of Dough, originally written in C by Felix Roos.
|
||||
* **mi-plaits-dsp-rs** is a Rust port of the code used by the Mutable Instruments Plaits.
|
||||
* _Author_: Oliver Rockstedt [info@sourcebox.de](info@sourcebox.de).
|
||||
* _Original author_: Emilie Gillet [emilie.o.gillet@gmail.com](emilie.o.gillet@gmail.com).
|
||||
|
||||
55
src/app.rs
55
src/app.rs
@@ -281,6 +281,45 @@ impl App {
|
||||
self.project_state.mark_dirty(change.bank, change.pattern);
|
||||
}
|
||||
|
||||
pub fn execute_script_oneshot(
|
||||
&self,
|
||||
script: &str,
|
||||
link: &LinkState,
|
||||
audio_tx: &arc_swap::ArcSwap<Sender<crate::engine::AudioCommand>>,
|
||||
) -> Result<(), String> {
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
let step_idx = self.editor_ctx.step;
|
||||
let speed = self
|
||||
.project_state
|
||||
.project
|
||||
.pattern_at(bank, pattern)
|
||||
.speed
|
||||
.multiplier();
|
||||
|
||||
let ctx = StepContext {
|
||||
step: step_idx,
|
||||
beat: link.beat(),
|
||||
bank,
|
||||
pattern,
|
||||
tempo: link.tempo(),
|
||||
phase: link.phase(),
|
||||
slot: 0,
|
||||
runs: 0,
|
||||
iter: 0,
|
||||
speed,
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
};
|
||||
|
||||
let cmds = self.script_engine.evaluate(script, &ctx)?;
|
||||
for cmd in cmds {
|
||||
let _ = audio_tx
|
||||
.load()
|
||||
.send(crate::engine::AudioCommand::Evaluate { cmd, time: None });
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn compile_current_step(&mut self, link: &LinkState) {
|
||||
let step_idx = self.editor_ctx.step;
|
||||
let (bank, pattern) = self.current_bank_pattern();
|
||||
@@ -1117,12 +1156,20 @@ impl App {
|
||||
AppCommand::PageDown => self.page.down(),
|
||||
|
||||
// Help navigation
|
||||
AppCommand::HelpNextTopic => {
|
||||
self.ui.help_topic = (self.ui.help_topic + 1) % help_view::topic_count();
|
||||
AppCommand::HelpToggleFocus => {
|
||||
use crate::state::HelpFocus;
|
||||
self.ui.help_focus = match self.ui.help_focus {
|
||||
HelpFocus::Topics => HelpFocus::Content,
|
||||
HelpFocus::Content => HelpFocus::Topics,
|
||||
};
|
||||
}
|
||||
AppCommand::HelpPrevTopic => {
|
||||
AppCommand::HelpNextTopic(n) => {
|
||||
let count = help_view::topic_count();
|
||||
self.ui.help_topic = (self.ui.help_topic + count - 1) % count;
|
||||
self.ui.help_topic = (self.ui.help_topic + n) % count;
|
||||
}
|
||||
AppCommand::HelpPrevTopic(n) => {
|
||||
let count = help_view::topic_count();
|
||||
self.ui.help_topic = (self.ui.help_topic + count - (n % count)) % count;
|
||||
}
|
||||
AppCommand::HelpScrollDown(n) => {
|
||||
let s = self.ui.help_scroll_mut();
|
||||
|
||||
@@ -138,8 +138,9 @@ pub enum AppCommand {
|
||||
PageDown,
|
||||
|
||||
// Help navigation
|
||||
HelpNextTopic,
|
||||
HelpPrevTopic,
|
||||
HelpToggleFocus,
|
||||
HelpNextTopic(usize),
|
||||
HelpPrevTopic(usize),
|
||||
HelpScrollDown(usize),
|
||||
HelpScrollUp(usize),
|
||||
HelpActivateSearch,
|
||||
|
||||
47
src/input.rs
47
src/input.rs
@@ -508,6 +508,13 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
KeyCode::Char('s') if ctrl => {
|
||||
ctx.app.editor_ctx.show_stack = !ctx.app.editor_ctx.show_stack;
|
||||
}
|
||||
KeyCode::Char('r') if ctrl => {
|
||||
let script = ctx.app.editor_ctx.editor.lines().join("\n");
|
||||
match ctx.app.execute_script_oneshot(&script, ctx.link, ctx.audio_tx) {
|
||||
Ok(()) => ctx.app.ui.flash("Executed", 100, crate::state::FlashKind::Info),
|
||||
Err(e) => ctx.app.ui.flash(&format!("Error: {e}"), 200, crate::state::FlashKind::Error),
|
||||
}
|
||||
}
|
||||
KeyCode::Char('a') if ctrl => {
|
||||
editor.select_all();
|
||||
}
|
||||
@@ -899,6 +906,17 @@ fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool) -> InputR
|
||||
}));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') if ctrl => {
|
||||
let pattern = ctx.app.current_edit_pattern();
|
||||
if let Some(script) = pattern.resolve_script(ctx.app.editor_ctx.step) {
|
||||
if !script.trim().is_empty() {
|
||||
match ctx.app.execute_script_oneshot(script, ctx.link, ctx.audio_tx) {
|
||||
Ok(()) => ctx.app.ui.flash("Executed", 100, crate::state::FlashKind::Info),
|
||||
Err(e) => ctx.app.ui.flash(&format!("Error: {e}"), 200, crate::state::FlashKind::Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Char('r') => {
|
||||
let (bank, pattern, step) = (
|
||||
ctx.app.editor_ctx.bank,
|
||||
@@ -1269,26 +1287,43 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
}
|
||||
|
||||
fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
use crate::state::HelpFocus;
|
||||
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
if ctx.app.ui.help_search_active {
|
||||
match key.code {
|
||||
KeyCode::Esc => ctx.dispatch(AppCommand::HelpClearSearch),
|
||||
KeyCode::Enter => ctx.dispatch(AppCommand::HelpSearchConfirm),
|
||||
KeyCode::Backspace => ctx.dispatch(AppCommand::HelpSearchBackspace),
|
||||
KeyCode::Char(c) => ctx.dispatch(AppCommand::HelpSearchInput(c)),
|
||||
KeyCode::Char(c) if !ctrl => ctx.dispatch(AppCommand::HelpSearchInput(c)),
|
||||
_ => {}
|
||||
}
|
||||
return InputResult::Continue;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('/') => ctx.dispatch(AppCommand::HelpActivateSearch),
|
||||
KeyCode::Char('/') | KeyCode::Char('f') if key.code == KeyCode::Char('/') || ctrl => {
|
||||
ctx.dispatch(AppCommand::HelpActivateSearch);
|
||||
}
|
||||
KeyCode::Esc if !ctx.app.ui.help_search_query.is_empty() => {
|
||||
ctx.dispatch(AppCommand::HelpClearSearch);
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => ctx.dispatch(AppCommand::HelpScrollDown(1)),
|
||||
KeyCode::Char('k') | KeyCode::Up => ctx.dispatch(AppCommand::HelpScrollUp(1)),
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::HelpNextTopic),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::HelpPrevTopic),
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::HelpToggleFocus),
|
||||
KeyCode::Char('j') | KeyCode::Down if ctrl => {
|
||||
ctx.dispatch(AppCommand::HelpNextTopic(5));
|
||||
}
|
||||
KeyCode::Char('k') | KeyCode::Up if ctrl => {
|
||||
ctx.dispatch(AppCommand::HelpPrevTopic(5));
|
||||
}
|
||||
KeyCode::Char('j') | KeyCode::Down => match ctx.app.ui.help_focus {
|
||||
HelpFocus::Topics => ctx.dispatch(AppCommand::HelpNextTopic(1)),
|
||||
HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollDown(1)),
|
||||
},
|
||||
KeyCode::Char('k') | KeyCode::Up => match ctx.app.ui.help_focus {
|
||||
HelpFocus::Topics => ctx.dispatch(AppCommand::HelpPrevTopic(1)),
|
||||
HelpFocus::Content => ctx.dispatch(AppCommand::HelpScrollUp(1)),
|
||||
},
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::HelpScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::HelpScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
|
||||
12
src/page.rs
12
src/page.rs
@@ -26,16 +26,16 @@ impl Page {
|
||||
/// Grid position (col, row) for each page
|
||||
/// Layout:
|
||||
/// col 0 col 1 col 2
|
||||
/// row 0 Options Patterns Help
|
||||
/// row 1 Dict Sequencer Engine
|
||||
/// row 0 Dict Patterns Options
|
||||
/// row 1 Help Sequencer Engine
|
||||
pub const fn grid_pos(self) -> (i8, i8) {
|
||||
match self {
|
||||
Page::Options => (0, 0),
|
||||
Page::Dict => (0, 1),
|
||||
Page::Main => (1, 1),
|
||||
Page::Dict => (0, 0),
|
||||
Page::Help => (0, 1),
|
||||
Page::Patterns => (1, 0),
|
||||
Page::Main => (1, 1),
|
||||
Page::Options => (2, 0),
|
||||
Page::Engine => (2, 1),
|
||||
Page::Help => (2, 0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,4 +23,4 @@ pub use patterns_nav::{PatternsColumn, PatternsNav};
|
||||
pub use playback::{PlaybackState, StagedChange};
|
||||
pub use project::ProjectState;
|
||||
pub use sample_browser::SampleBrowserState;
|
||||
pub use ui::{DictFocus, FlashKind, UiState};
|
||||
pub use ui::{DictFocus, FlashKind, HelpFocus, UiState};
|
||||
|
||||
@@ -19,12 +19,20 @@ pub enum DictFocus {
|
||||
Words,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HelpFocus {
|
||||
#[default]
|
||||
Topics,
|
||||
Content,
|
||||
}
|
||||
|
||||
pub struct UiState {
|
||||
pub sparkles: Sparkles,
|
||||
pub status_message: Option<String>,
|
||||
pub flash_until: Option<Instant>,
|
||||
pub flash_kind: FlashKind,
|
||||
pub modal: Modal,
|
||||
pub help_focus: HelpFocus,
|
||||
pub help_topic: usize,
|
||||
pub help_scrolls: Vec<usize>,
|
||||
pub help_search_active: bool,
|
||||
@@ -52,6 +60,7 @@ impl Default for UiState {
|
||||
flash_until: None,
|
||||
flash_kind: FlashKind::Success,
|
||||
modal: Modal::None,
|
||||
help_focus: HelpFocus::default(),
|
||||
help_topic: 0,
|
||||
help_scrolls: vec![0; crate::views::help_view::topic_count()],
|
||||
help_search_active: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use minimad::{Composite, CompositeStyle, Compound, Line};
|
||||
use minimad::{Composite, CompositeStyle, Compound, Line, TableRow};
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line as RLine, Span};
|
||||
@@ -7,25 +7,123 @@ use ratatui::Frame;
|
||||
use tui_big_text::{BigText, PixelSize};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::state::HelpFocus;
|
||||
use crate::theme;
|
||||
use crate::views::highlight;
|
||||
|
||||
// To add a new help topic: drop a .md file in docs/ and add one line here.
|
||||
const DOCS: &[(&str, &str)] = &[
|
||||
("Welcome", include_str!("../../docs/welcome.md")),
|
||||
("Audio Engine", include_str!("../../docs/audio_engine.md")),
|
||||
("Keybindings", include_str!("../../docs/keybindings.md")),
|
||||
("Sequencer", include_str!("../../docs/sequencer.md")),
|
||||
("About", include_str!("../../docs/about.md")),
|
||||
enum DocEntry {
|
||||
Section(&'static str),
|
||||
Topic(&'static str, &'static str),
|
||||
}
|
||||
|
||||
use DocEntry::{Section, Topic};
|
||||
|
||||
const DOCS: &[DocEntry] = &[
|
||||
// Getting Started
|
||||
Section("Getting Started"),
|
||||
Topic("Welcome", include_str!("../../docs/welcome.md")),
|
||||
Topic("Moving Around", include_str!("../../docs/navigation.md")),
|
||||
Topic(
|
||||
"How Does It Work?",
|
||||
include_str!("../../docs/how_it_works.md"),
|
||||
),
|
||||
Topic(
|
||||
"Banks & Patterns",
|
||||
include_str!("../../docs/banks_patterns.md"),
|
||||
),
|
||||
Topic("Stage / Commit", include_str!("../../docs/staging.md")),
|
||||
Topic("Using the Sequencer", include_str!("../../docs/grid.md")),
|
||||
Topic("Editing a Step", include_str!("../../docs/editing.md")),
|
||||
// Forth fundamentals
|
||||
Section("Forth"),
|
||||
Topic("About Forth", include_str!("../../docs/about_forth.md")),
|
||||
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
|
||||
Topic("The Stack", include_str!("../../docs/stack.md")),
|
||||
Topic("Arithmetic", include_str!("../../docs/arithmetic.md")),
|
||||
Topic("Comparison", include_str!("../../docs/comparison.md")),
|
||||
Topic("Logic", include_str!("../../docs/logic.md")),
|
||||
// Sound generation
|
||||
Section("Sounds"),
|
||||
Topic("Emitting", include_str!("../../docs/emitting.md")),
|
||||
Topic("Samples", include_str!("../../docs/samples.md")),
|
||||
Topic("Oscillators", include_str!("../../docs/oscillators.md")),
|
||||
Topic("Wavetables", include_str!("../../docs/wavetables.md")),
|
||||
// Sound shaping
|
||||
Section("Shaping"),
|
||||
Topic("Envelopes", include_str!("../../docs/envelopes.md")),
|
||||
Topic(
|
||||
"Pitch Envelope",
|
||||
include_str!("../../docs/pitch_envelope.md"),
|
||||
),
|
||||
Topic("Filters", include_str!("../../docs/filters.md")),
|
||||
Topic(
|
||||
"Ladder Filters",
|
||||
include_str!("../../docs/ladder_filters.md"),
|
||||
),
|
||||
// Movement and modulation
|
||||
Section("Movement"),
|
||||
Topic("LFO & Ramps", include_str!("../../docs/lfo.md")),
|
||||
Topic("Modulation", include_str!("../../docs/modulation.md")),
|
||||
Topic("Vibrato", include_str!("../../docs/vibrato.md")),
|
||||
// Effects
|
||||
Section("Effects"),
|
||||
Topic("Delay & Reverb", include_str!("../../docs/delay_reverb.md")),
|
||||
Topic("Mod FX", include_str!("../../docs/mod_fx.md")),
|
||||
Topic("EQ & Stereo", include_str!("../../docs/eq_stereo.md")),
|
||||
Topic("Lo-fi", include_str!("../../docs/lofi.md")),
|
||||
// Variation and randomness
|
||||
Section("Variation"),
|
||||
Topic("Randomness", include_str!("../../docs/randomness.md")),
|
||||
Topic("Probability", include_str!("../../docs/probability.md")),
|
||||
Topic("Selection", include_str!("../../docs/selection.md")),
|
||||
// Timing
|
||||
Section("Timing"),
|
||||
Topic("Context", include_str!("../../docs/context.md")),
|
||||
Topic("Cycles", include_str!("../../docs/cycles.md")),
|
||||
Topic("Timing", include_str!("../../docs/timing.md")),
|
||||
Topic("Patterns", include_str!("../../docs/patterns.md")),
|
||||
Topic("Chaining", include_str!("../../docs/chaining.md")),
|
||||
// Music theory
|
||||
Section("Music"),
|
||||
Topic("Notes", include_str!("../../docs/notes.md")),
|
||||
Topic("Scales", include_str!("../../docs/scales.md")),
|
||||
Topic("Chords", include_str!("../../docs/chords.md")),
|
||||
Topic("Generators", include_str!("../../docs/generators.md")),
|
||||
// Advanced
|
||||
Section("Advanced"),
|
||||
Topic("Variables", include_str!("../../docs/variables.md")),
|
||||
Topic("Conditionals", include_str!("../../docs/conditionals.md")),
|
||||
Topic("Custom Words", include_str!("../../docs/definitions.md")),
|
||||
Topic("Ableton Link", include_str!("../../docs/link.md")),
|
||||
// Reference
|
||||
Section("Reference"),
|
||||
Topic("Audio Engine", include_str!("../../docs/audio_engine.md")),
|
||||
Topic("Keybindings", include_str!("../../docs/keybindings.md")),
|
||||
Topic("Sequencer", include_str!("../../docs/sequencer.md")),
|
||||
// Archive - old files to sort
|
||||
Section("Archive"),
|
||||
Topic("Sound Basics", include_str!("../../docs/sound_basics.md")),
|
||||
Topic("Parameters", include_str!("../../docs/parameters.md")),
|
||||
Topic("Tempo & Speed", include_str!("../../docs/tempo.md")),
|
||||
Topic("Effects (old)", include_str!("../../docs/effects.md")),
|
||||
];
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
DOCS.len()
|
||||
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
|
||||
}
|
||||
|
||||
fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
|
||||
DOCS.iter()
|
||||
.filter_map(|e| match e {
|
||||
Topic(name, content) => Some((*name, *content)),
|
||||
Section(_) => None,
|
||||
})
|
||||
.nth(index)
|
||||
}
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [topics_area, content_area] =
|
||||
Layout::horizontal([Constraint::Length(18), Constraint::Fill(1)]).areas(area);
|
||||
Layout::horizontal([Constraint::Length(24), Constraint::Fill(1)]).areas(area);
|
||||
|
||||
render_topics(frame, app, topics_area);
|
||||
render_content(frame, app, content_area);
|
||||
@@ -33,25 +131,77 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
|
||||
fn render_topics(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
let total_items = DOCS.len();
|
||||
|
||||
// Find the visual index of the selected topic (including sections)
|
||||
let selected_visual_idx = {
|
||||
let mut visual = 0;
|
||||
let mut topic_count = 0;
|
||||
for entry in DOCS.iter() {
|
||||
if let Topic(_, _) = entry {
|
||||
if topic_count == app.ui.help_topic {
|
||||
break;
|
||||
}
|
||||
topic_count += 1;
|
||||
}
|
||||
visual += 1;
|
||||
}
|
||||
visual
|
||||
};
|
||||
|
||||
// Calculate scroll to keep selection visible (centered when possible)
|
||||
let scroll = if selected_visual_idx < visible_height / 2 {
|
||||
0
|
||||
} else if selected_visual_idx > total_items.saturating_sub(visible_height / 2) {
|
||||
total_items.saturating_sub(visible_height)
|
||||
} else {
|
||||
selected_visual_idx.saturating_sub(visible_height / 2)
|
||||
};
|
||||
|
||||
// Count topics before the scroll offset to track topic_idx correctly
|
||||
let mut topic_idx = DOCS
|
||||
.iter()
|
||||
.take(scroll)
|
||||
.filter(|e| matches!(e, Topic(_, _)))
|
||||
.count();
|
||||
|
||||
let items: Vec<ListItem> = DOCS
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (name, _))| {
|
||||
let selected = i == app.ui.help_topic;
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.map(|entry| match entry {
|
||||
Section(name) => {
|
||||
let style = Style::new().fg(theme.ui.text_dim);
|
||||
ListItem::new(format!("─ {name} ─")).style(style)
|
||||
}
|
||||
Topic(name, _) => {
|
||||
let selected = topic_idx == app.ui.help_topic;
|
||||
let style = if selected {
|
||||
Style::new().fg(theme.dict.category_selected).add_modifier(Modifier::BOLD)
|
||||
Style::new()
|
||||
.fg(theme.dict.category_selected)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(theme.ui.text_primary)
|
||||
};
|
||||
let prefix = if selected { "> " } else { " " };
|
||||
topic_idx += 1;
|
||||
ListItem::new(format!("{prefix}{name}")).style(style)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let focused = app.ui.help_focus == HelpFocus::Topics;
|
||||
let border_color = if focused {
|
||||
theme.dict.border_focused
|
||||
} else {
|
||||
theme.dict.border_normal
|
||||
};
|
||||
let list = List::new(items).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(theme.dict.border_focused))
|
||||
.border_style(Style::new().fg(border_color))
|
||||
.title("Topics"),
|
||||
);
|
||||
frame.render_widget(list, area);
|
||||
@@ -62,7 +212,9 @@ const BIG_TITLE_HEIGHT: u16 = 6;
|
||||
|
||||
fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let (_, md) = DOCS[app.ui.help_topic];
|
||||
let Some((_, md)) = get_topic(app.ui.help_topic) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_welcome = app.ui.help_topic == WELCOME_TOPIC;
|
||||
let md_area = if is_welcome {
|
||||
@@ -95,24 +247,6 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let lines = parse_markdown(md);
|
||||
|
||||
let has_search_bar = app.ui.help_search_active || has_query;
|
||||
let search_bar_height: u16 = u16::from(has_search_bar);
|
||||
let visible_height = md_area.height.saturating_sub(6 + search_bar_height) as usize;
|
||||
let max_scroll = lines.len().saturating_sub(visible_height);
|
||||
let scroll = app.ui.help_scroll().min(max_scroll);
|
||||
|
||||
let visible: Vec<RLine> = lines
|
||||
.into_iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.map(|line| {
|
||||
if has_query {
|
||||
highlight_line(line, &query_lower)
|
||||
} else {
|
||||
line
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let content_area = if has_search_bar {
|
||||
let [content, search] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).areas(md_area);
|
||||
@@ -122,17 +256,55 @@ fn render_content(frame: &mut Frame, app: &App, area: Rect) {
|
||||
md_area
|
||||
};
|
||||
|
||||
let para = Paragraph::new(visible)
|
||||
// Calculate dimensions: 2 borders + 4 padding (2 left + 2 right)
|
||||
let content_width = content_area.width.saturating_sub(6) as usize;
|
||||
// 2 borders + 4 padding (2 top + 2 bottom)
|
||||
let visible_height = content_area.height.saturating_sub(6) as usize;
|
||||
|
||||
// Calculate total wrapped line count for accurate max_scroll
|
||||
let total_wrapped: usize = lines
|
||||
.iter()
|
||||
.map(|l| wrapped_line_count(l, content_width))
|
||||
.sum();
|
||||
let max_scroll = total_wrapped.saturating_sub(visible_height);
|
||||
let scroll = app.ui.help_scroll().min(max_scroll);
|
||||
|
||||
let lines: Vec<RLine> = if has_query {
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|line| highlight_line(line, &query_lower))
|
||||
.collect()
|
||||
} else {
|
||||
lines
|
||||
};
|
||||
|
||||
let focused = app.ui.help_focus == HelpFocus::Content;
|
||||
let border_color = if focused {
|
||||
theme.dict.border_focused
|
||||
} else {
|
||||
theme.dict.border_normal
|
||||
};
|
||||
let para = Paragraph::new(lines)
|
||||
.scroll((scroll as u16, 0))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(theme.ui.border))
|
||||
.border_style(Style::new().fg(border_color))
|
||||
.padding(Padding::new(2, 2, 2, 2)),
|
||||
)
|
||||
.wrap(Wrap { trim: false });
|
||||
frame.render_widget(para, content_area);
|
||||
}
|
||||
|
||||
fn wrapped_line_count(line: &RLine, width: usize) -> usize {
|
||||
let char_count: usize = line.spans.iter().map(|s| s.content.chars().count()).sum();
|
||||
if char_count == 0 || width == 0 {
|
||||
1
|
||||
} else {
|
||||
(char_count + width - 1) / width
|
||||
}
|
||||
}
|
||||
|
||||
fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let style = if app.ui.help_search_active {
|
||||
@@ -156,7 +328,9 @@ fn highlight_line<'a>(line: RLine<'a>, query: &str) -> RLine<'a> {
|
||||
}
|
||||
let content = span.content.to_string();
|
||||
let base_style = span.style;
|
||||
let hl_style = base_style.bg(theme.search.match_bg).fg(theme.search.match_fg);
|
||||
let hl_style = base_style
|
||||
.bg(theme.search.match_bg)
|
||||
.fg(theme.search.match_fg);
|
||||
let mut start = 0;
|
||||
let lower_bytes = lower.as_bytes();
|
||||
let query_bytes = query.as_bytes();
|
||||
@@ -185,7 +359,14 @@ fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
||||
/// Find first line matching query across all topics. Returns (topic_index, line_index).
|
||||
pub fn find_match(query: &str) -> Option<(usize, usize)> {
|
||||
let query = query.to_lowercase();
|
||||
for (topic_idx, (_, content)) in DOCS.iter().enumerate() {
|
||||
for (topic_idx, (_, content)) in DOCS
|
||||
.iter()
|
||||
.filter_map(|e| match e {
|
||||
Topic(name, content) => Some((*name, *content)),
|
||||
Section(_) => None,
|
||||
})
|
||||
.enumerate()
|
||||
{
|
||||
for (line_idx, line) in content.lines().enumerate() {
|
||||
if line.to_lowercase().contains(&query) {
|
||||
return Some((topic_idx, line_idx));
|
||||
@@ -200,9 +381,11 @@ fn code_border_style() -> Style {
|
||||
Style::new().fg(theme.markdown.code_border)
|
||||
}
|
||||
|
||||
fn preprocess_underscores(md: &str) -> String {
|
||||
fn preprocess_markdown(md: &str) -> String {
|
||||
let mut out = String::with_capacity(md.len());
|
||||
for line in md.lines() {
|
||||
// Convert dash list markers to asterisks (minimad only recognizes *)
|
||||
let line = convert_dash_lists(line);
|
||||
let mut result = String::with_capacity(line.len());
|
||||
let mut chars = line.char_indices().peekable();
|
||||
let bytes = line.as_bytes();
|
||||
@@ -243,18 +426,44 @@ fn preprocess_underscores(md: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn convert_dash_lists(line: &str) -> String {
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.starts_with("- ") {
|
||||
let indent = line.len() - trimmed.len();
|
||||
format!("{}* {}", " ".repeat(indent), &trimmed[2..])
|
||||
} else {
|
||||
line.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
|
||||
let processed = preprocess_underscores(md);
|
||||
let processed = preprocess_markdown(md);
|
||||
let text = minimad::Text::from(processed.as_str());
|
||||
let mut lines = Vec::new();
|
||||
|
||||
let mut code_line_nr: usize = 0;
|
||||
let mut table_buffer: Vec<TableRow> = Vec::new();
|
||||
|
||||
let flush_table = |buf: &mut Vec<TableRow>, out: &mut Vec<RLine<'static>>| {
|
||||
if buf.is_empty() {
|
||||
return;
|
||||
}
|
||||
let col_widths = compute_column_widths(buf);
|
||||
for (row_idx, row) in buf.drain(..).enumerate() {
|
||||
out.push(render_table_row(row, row_idx, &col_widths));
|
||||
}
|
||||
};
|
||||
|
||||
for line in text.lines {
|
||||
match line {
|
||||
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
|
||||
flush_table(&mut table_buffer, &mut lines);
|
||||
code_line_nr += 1;
|
||||
let raw: String = composite.compounds.iter().map(|c: &minimad::Compound| c.src).collect();
|
||||
let raw: String = composite
|
||||
.compounds
|
||||
.iter()
|
||||
.map(|c: &minimad::Compound| c.src)
|
||||
.collect();
|
||||
let mut spans = vec![
|
||||
Span::styled(format!(" {code_line_nr:>2} "), code_border_style()),
|
||||
Span::styled("│ ", code_border_style()),
|
||||
@@ -267,41 +476,125 @@ fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
|
||||
lines.push(RLine::from(spans));
|
||||
}
|
||||
Line::Normal(composite) => {
|
||||
flush_table(&mut table_buffer, &mut lines);
|
||||
code_line_nr = 0;
|
||||
lines.push(composite_to_line(composite));
|
||||
}
|
||||
Line::TableRow(row) => {
|
||||
code_line_nr = 0;
|
||||
table_buffer.push(row);
|
||||
}
|
||||
Line::TableRule(_) => {
|
||||
// Skip the separator line (---|---|---)
|
||||
}
|
||||
_ => {
|
||||
flush_table(&mut table_buffer, &mut lines);
|
||||
code_line_nr = 0;
|
||||
lines.push(RLine::from(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
flush_table(&mut table_buffer, &mut lines);
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn cell_text_width(cell: &Composite) -> usize {
|
||||
cell.compounds.iter().map(|c| c.src.chars().count()).sum()
|
||||
}
|
||||
|
||||
fn compute_column_widths(rows: &[TableRow]) -> Vec<usize> {
|
||||
let mut widths: Vec<usize> = Vec::new();
|
||||
for row in rows {
|
||||
for (i, cell) in row.cells.iter().enumerate() {
|
||||
let w = cell_text_width(cell);
|
||||
if i >= widths.len() {
|
||||
widths.push(w);
|
||||
} else if w > widths[i] {
|
||||
widths[i] = w;
|
||||
}
|
||||
}
|
||||
}
|
||||
widths
|
||||
}
|
||||
|
||||
fn render_table_row(row: TableRow, row_idx: usize, col_widths: &[usize]) -> RLine<'static> {
|
||||
let theme = theme::get();
|
||||
let is_header = row_idx == 0;
|
||||
let bg = if is_header {
|
||||
theme.ui.surface
|
||||
} else if row_idx % 2 == 0 {
|
||||
theme.table.row_even
|
||||
} else {
|
||||
theme.table.row_odd
|
||||
};
|
||||
|
||||
let base_style = if is_header {
|
||||
Style::new()
|
||||
.fg(theme.markdown.text)
|
||||
.bg(bg)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(theme.markdown.text).bg(bg)
|
||||
};
|
||||
|
||||
let sep_style = Style::new().fg(theme.markdown.code_border).bg(bg);
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
|
||||
for (i, cell) in row.cells.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
spans.push(Span::styled(" │ ", sep_style));
|
||||
}
|
||||
let target_width = col_widths.get(i).copied().unwrap_or(0);
|
||||
let cell_width = cell
|
||||
.compounds
|
||||
.iter()
|
||||
.map(|c| c.src.chars().count())
|
||||
.sum::<usize>();
|
||||
|
||||
for compound in cell.compounds {
|
||||
compound_to_spans(compound, base_style, &mut spans);
|
||||
}
|
||||
|
||||
let padding = target_width.saturating_sub(cell_width);
|
||||
if padding > 0 {
|
||||
spans.push(Span::styled(" ".repeat(padding), base_style));
|
||||
}
|
||||
}
|
||||
|
||||
RLine::from(spans)
|
||||
}
|
||||
|
||||
fn composite_to_line(composite: Composite) -> RLine<'static> {
|
||||
let theme = theme::get();
|
||||
let base_style = match composite.style {
|
||||
CompositeStyle::Header(1) => Style::new()
|
||||
.fg(theme.markdown.h1)
|
||||
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED),
|
||||
CompositeStyle::Header(2) => Style::new().fg(theme.markdown.h2).add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::Header(_) => Style::new().fg(theme.markdown.h3).add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::Header(2) => Style::new()
|
||||
.fg(theme.markdown.h2)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::Header(_) => Style::new()
|
||||
.fg(theme.markdown.h3)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
CompositeStyle::ListItem(_) => Style::new().fg(theme.markdown.list),
|
||||
CompositeStyle::Quote => Style::new().fg(theme.markdown.quote),
|
||||
CompositeStyle::Code => Style::new().fg(theme.markdown.code),
|
||||
CompositeStyle::Paragraph => Style::new().fg(theme.markdown.text),
|
||||
};
|
||||
|
||||
let prefix = match composite.style {
|
||||
CompositeStyle::ListItem(_) => " • ",
|
||||
CompositeStyle::Quote => " │ ",
|
||||
_ => "",
|
||||
let prefix: String = match composite.style {
|
||||
CompositeStyle::ListItem(depth) => {
|
||||
let indent = " ".repeat(depth as usize);
|
||||
format!("{indent}• ")
|
||||
}
|
||||
CompositeStyle::Quote => " │ ".to_string(),
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
let mut spans: Vec<Span<'static>> = Vec::new();
|
||||
if !prefix.is_empty() {
|
||||
spans.push(Span::styled(prefix.to_string(), base_style));
|
||||
spans.push(Span::styled(prefix, base_style));
|
||||
}
|
||||
|
||||
for compound in composite.compounds {
|
||||
|
||||
@@ -34,6 +34,8 @@ pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str
|
||||
bindings.push(("s", "Save", "Save project"));
|
||||
bindings.push(("l", "Load", "Load project"));
|
||||
bindings.push(("f", "Fill", "Toggle fill mode (hold)"));
|
||||
bindings.push(("r", "Rename", "Rename current step"));
|
||||
bindings.push(("Ctrl+R", "Run", "Run step script immediately"));
|
||||
}
|
||||
Page::Patterns => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
|
||||
Reference in New Issue
Block a user