Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@ GitHub are its section here (`tools/release.sh` copies them), so add to

## Unreleased

- **Separate STOP and PLAY.** Pressing STOP again while stopped returns
to the loop's start, or to the song's start with the loop off.

- **Musical typing.** Turn on KEYS in the transport bar to play the
selected track from the computer keyboard. PIANO is one chromatic row.
GRID is four rows in fourths like a Push, in key when a scale is set.
CHORD plays in-key chords on the bottom row under a melody on the rows
above it. With KEYS on, REC records what you play into the open clip
or a new one, layering pass after pass on a loop (docs/40).

- **Octaves and scale in the piano roll.** A pressed key lights the same
note faintly in every octave on the keys and the grid. In DRAW mode a
ghost shows the note a click would add, with outlines in its other
octaves. A picked scale now marks its keys with a stripe (solid on the
root), greys the keys outside it, and tints its rows in the track's
color.

- **New Project no longer crashes while a reverb rings.** Starting a new
project, or opening one, while the old project's master effects were
still sounding could crash Slab.
Expand Down
100 changes: 100 additions & 0 deletions docs/40-musical-typing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# 40 — Musical typing, octave echoes and the scale

The computer keyboard plays the selected track and records into clips.
The piano roll shows a pressed note in every octave and marks the key's
scale on the keys and the grid.

Source: `src/ui/musical_keys.zig` (layouts, chords, the recording take),
`src/ui/clip_editor.zig` (echoes, ghost, scale marks), `recordKeys` in
`src/main.zig`, the KEYS tile in `src/ui/transport_bar.zig`.

## KEYS

The transport bar's **KEYS** latch turns musical typing on. Beside it:
the layout (PIANO, GRID, CHORD), the octave of the lowest C (C4 = MIDI
60, the piano roll's naming), the velocity (drag; double-click 100) and,
in CHORD, the chord type.

Notes go to the track of the clip open in the piano roll, or else the
selected track (not a bus or the master), whether or not the clip
editor is open. They start at the KEYS velocity.

While KEYS is on, its keys belong to it: the panes' unmodified
shortcuts on those keys stand down (D, Q, H, S, Z, E, M, 0, `[` `]`, …).
⌘ combinations, Space, Tab, Enter, Esc, Backspace and the arrows keep
working. A text field, a menu, a dialog or the browser's search takes the
keyboard back, and so does holding ⌘.

| keys | PIANO | GRID | CHORD |
|---|---|---|---|
| notes | `A W S E D F T G Y H U J K O L P ; '` = C up to F | four rows, see below | see below |
| octave | `Z` `X`, or `-` `=` | `-` `=` | `-` `=` |
| velocity | `C` `V`, or `[` `]` | `[` `]` | `[` `]` |

Velocity steps through 1, 20, 40, 60, 80, 100, 127.

### GRID

The four letter and number rows are pads, bottom row (`Z`…`/`) lowest,
like a Push in fourths. Each row has ten keys.

- **Scale off:** chromatic, a semitone per key, a fourth (five
semitones) per row up.
- **Scale on:** in key from the root, a scale step per key, three steps
per row up. Every key is in the scale.

### CHORD

- **`Z` row:** diatonic chords on degrees 1–10 of the scale, an octave
under the melody. In C major `Z` is C, `X` Dm, `C` Em, `V` F, `B` G …
- **`A` row:** the melody in key, from the root.
- **`Q` row:** the same melody an octave up.
- **`1`–`6`:** chord type: TRIAD, 7TH, SUS2, SUS4, 9TH, POWER. Chords are
stacked in scale steps, so every chord stays in key.

Hold a chord with the left hand and play the melody over it. With the
scale off, CHORD uses the major scale on the picker's root.

## Recording

With KEYS on, **REC** starts the transport and records what you play.
If a track is armed, the audio recorder runs too.

- **Where notes go:** into the clip open in the piano roll when it's on
the played track and the playhead is inside it (overdub). Otherwise a
new "Take" clip starts at the bar where the first note fell, and the
editor opens it.
- **Clip length:** the clip grows by whole bars of the meter to hold
what's played.
- **While you play:** held notes draw growing in the piano roll, outlined
in red. They're written when you let go.
- **Loops:** when the transport loop wraps, held notes close at the loop
end and start again at the loop start, so a loop builds up pass after
pass.
- **Undo:** each pass is one undo step.
- **Stopping:** REC again, or stopping the transport, ends the pass. Keys
already down when a pass starts aren't recorded.

Notes keep the beat the frame saw them at (no input-latency
compensation). Quantize (Q) afterwards if you want them on the grid.

## Octave echoes and the ghost note

- **Octave echoes:** keys pressed on the keyboard column or the computer
keyboard, and a note being drawn, light their own key and grid row. The
same note in every other octave lights faintly on both, in the track's
color.
- **Ghost note:** in DRAW mode, or with ⌘ held, the note a click would
add shows under the pointer, snapped to the grid and the scale. Its
octaves show as outlines. Drawing a note shows the same outlines.
- **Drum lanes:** a drum lane map has no octaves, so it shows neither.

## The scale

With a key and scale picked in the piano roll head:

- **Grid:** in-key rows are tinted faintly toward the track color, the
root's rows a step more. Out-of-key rows drop slightly below the plain
glass.
- **Keys:** in-key keys get a track-color stripe on their left edge,
solid on the root. Out-of-key white keys go grey.
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ status and the source before assuming a feature is absent or complete.
40. [39-jp8.md](39-jp8.md) — Juniper-8, the Roland Jupiter-8 from its
voice board: expo cross mod, VCO-2 sync, the IR3109's 24/12 dB taps,
one shared LFO
41. [40-musical-typing.md](40-musical-typing.md) — the computer keyboard
as PIANO, GRID (Push-style fourths) or CHORD plus melody, recording
into clips, octave echoes, ghost notes and scale marks in the piano roll

## Terminology crib sheet

Expand Down
20 changes: 13 additions & 7 deletions src/engine.zig
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const AUD_EV = AUD_Q + 128;
const AUD_TAIL_S = 3;

/// Hold or release a note on a track, from an editor.
pub const AudEvent = struct { track: u8, pitch: u8, on: bool };
pub const AudEvent = struct { track: u8, pitch: u8, on: bool, velocity: u8 = 102 };
/// Expression events for a bent note go out every this many samples while
/// it sounds (docs/22 §Note expression): 0.67 ms at 48 kHz.
pub const EXPR_STEP: u32 = 32;
Expand Down Expand Up @@ -509,10 +509,16 @@ pub const Engine = struct {
/// Start (`on`) or release a held note on a track, playing or not
/// (docs/31 §Note editing). UI thread; dropped when the ring is full.
pub fn holdNote(self: *Engine, track_idx: usize, pitch: u8, on: bool) void {
self.holdNoteVel(track_idx, pitch, on, 102);
}

/// `holdNote` at a MIDI velocity (1..127): the computer keyboard's
/// notes play at its VEL (docs/40).
pub fn holdNoteVel(self: *Engine, track_idx: usize, pitch: u8, on: bool, velocity: u8) void {
if (track_idx > std.math.maxInt(u8) or pitch > 127) return;
const w = self.aud_w.load(.monotonic);
if (w -% self.aud_r.load(.acquire) >= AUD_Q) return;
self.aud_q[w % AUD_Q] = .{ .track = @intCast(track_idx), .pitch = pitch, .on = on };
self.aud_q[w % AUD_Q] = .{ .track = @intCast(track_idx), .pitch = pitch, .on = on, .velocity = velocity };
self.aud_w.store(w +% 1, .release);
}

Expand All @@ -531,21 +537,21 @@ pub const Engine = struct {
const was = self.aud_held[word] & bit != 0;
if (e.on == was) continue;
if (e.on) self.aud_held[word] |= bit else self.aud_held[word] &= ~bit;
self.pushHeld(e.track, e.pitch, e.on);
self.pushHeld(e.track, e.pitch, e.on, e.velocity);
}
self.aud_r.store(r, .release);
if (self.aud_held[0] | self.aud_held[1] != 0 or self.aud_n > 0) self.aud_tail = AUD_TAIL_S * self.transport.sample_rate;
}

fn pushHeld(self: *Engine, track: u8, pitch: u8, on: bool) void {
fn pushHeld(self: *Engine, track: u8, pitch: u8, on: bool, velocity: u8) void {
if (self.aud_n >= AUD_EV) return;
self.aud_ev[self.aud_n] = .{
.sample_offset = 0,
.kind = if (on) .note_on else .note_off,
.channel = 0,
.note_id = -1,
.pitch = @floatFromInt(pitch),
.velocity = if (on) 0.8 else 0,
.velocity = if (on) @as(f32, @floatFromInt(@min(velocity, 127))) / 127.0 else 0,
};
self.aud_ev_track[self.aud_n] = track;
self.aud_n += 1;
Expand All @@ -557,7 +563,7 @@ pub const Engine = struct {
for (0..128) |pi| {
const bit = @as(u64, 1) << @intCast(pi % 64);
if (self.aud_held[pi / 64] & bit == 0) continue;
self.pushHeld(t, @intCast(pi), false);
self.pushHeld(t, @intCast(pi), false, 0);
}
self.aud_held = .{ 0, 0 };
}
Expand Down Expand Up @@ -741,7 +747,7 @@ pub const Engine = struct {
for (self.tracks) |*t| t.clearSounding();
// Whatever the editor still holds sounds again from here.
if (self.aud_track) |at| for (0..128) |pi| {
if (self.aud_held[pi / 64] & (@as(u64, 1) << @intCast(pi % 64)) != 0) self.pushHeld(at, @intCast(pi), true);
if (self.aud_held[pi / 64] & (@as(u64, 1) << @intCast(pi % 64)) != 0) self.pushHeld(at, @intCast(pi), true, 102);
};
self.was_playing = false;
self.ringing = true;
Expand Down
Loading
Loading