Feat: internal recording / overdubbing
This commit is contained in:
@@ -140,6 +140,11 @@ pub enum Op {
|
|||||||
MidiStart,
|
MidiStart,
|
||||||
MidiStop,
|
MidiStop,
|
||||||
MidiContinue,
|
MidiContinue,
|
||||||
|
// Recording
|
||||||
|
Rec,
|
||||||
|
Overdub,
|
||||||
|
Orec,
|
||||||
|
Odub,
|
||||||
// Bracket syntax (mark/count for auto-counting)
|
// Bracket syntax (mark/count for auto-counting)
|
||||||
Mark,
|
Mark,
|
||||||
Count(Option<SourceSpan>),
|
Count(Option<SourceSpan>),
|
||||||
|
|||||||
@@ -1610,6 +1610,24 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
drain_select_run(count, resolved_idx, stack, outputs, cmd)?;
|
drain_select_run(count, resolved_idx, stack, outputs, cmd)?;
|
||||||
}
|
}
|
||||||
|
Op::Rec => {
|
||||||
|
let name = pop(stack)?;
|
||||||
|
outputs.push(format!("/doux/rec/sound/{}", name.as_str()?));
|
||||||
|
}
|
||||||
|
Op::Overdub => {
|
||||||
|
let name = pop(stack)?;
|
||||||
|
outputs.push(format!("/doux/rec/sound/{}/overdub/1", name.as_str()?));
|
||||||
|
}
|
||||||
|
Op::Orec => {
|
||||||
|
let orbit = pop(stack)?.as_int()?;
|
||||||
|
let name = pop(stack)?;
|
||||||
|
outputs.push(format!("/doux/rec/sound/{}/orbit/{}", name.as_str()?, orbit));
|
||||||
|
}
|
||||||
|
Op::Odub => {
|
||||||
|
let orbit = pop(stack)?.as_int()?;
|
||||||
|
let name = pop(stack)?;
|
||||||
|
outputs.push(format!("/doux/rec/sound/{}/overdub/1/orbit/{}", name.as_str()?, orbit));
|
||||||
|
}
|
||||||
Op::Forget => {
|
Op::Forget => {
|
||||||
let name = pop(stack)?;
|
let name = pop(stack)?;
|
||||||
self.dict.lock().remove(name.as_str()?);
|
self.dict.lock().remove(name.as_str()?);
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
|
|||||||
"mstart" => Op::MidiStart,
|
"mstart" => Op::MidiStart,
|
||||||
"mstop" => Op::MidiStop,
|
"mstop" => Op::MidiStop,
|
||||||
"mcont" => Op::MidiContinue,
|
"mcont" => Op::MidiContinue,
|
||||||
|
"rec" => Op::Rec,
|
||||||
|
"overdub" | "dub" => Op::Overdub,
|
||||||
|
"orec" => Op::Orec,
|
||||||
|
"odub" => Op::Odub,
|
||||||
"forget" => Op::Forget,
|
"forget" => Op::Forget,
|
||||||
"index" => Op::Index(None),
|
"index" => Op::Index(None),
|
||||||
"key!" => Op::SetKey,
|
"key!" => Op::SetKey,
|
||||||
|
|||||||
@@ -63,6 +63,47 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
compile: Simple,
|
compile: Simple,
|
||||||
varargs: false,
|
varargs: false,
|
||||||
},
|
},
|
||||||
|
// Recording
|
||||||
|
Word {
|
||||||
|
name: "rec",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Sound",
|
||||||
|
stack: "(name --)",
|
||||||
|
desc: "Toggle recording audio output to named sample",
|
||||||
|
example: "\"loop1\" rec",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "overdub",
|
||||||
|
aliases: &["dub"],
|
||||||
|
category: "Sound",
|
||||||
|
stack: "(name --)",
|
||||||
|
desc: "Toggle overdub recording onto existing named sample",
|
||||||
|
example: "\"loop1\" overdub",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "orec",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Sound",
|
||||||
|
stack: "(name orbit --)",
|
||||||
|
desc: "Toggle recording a single orbit into named sample",
|
||||||
|
example: "\"drums\" 0 orec",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
|
Word {
|
||||||
|
name: "odub",
|
||||||
|
aliases: &[],
|
||||||
|
category: "Sound",
|
||||||
|
stack: "(name orbit --)",
|
||||||
|
desc: "Toggle overdub recording a single orbit onto named sample",
|
||||||
|
example: "\"drums\" 0 odub",
|
||||||
|
compile: Simple,
|
||||||
|
varargs: false,
|
||||||
|
},
|
||||||
// Sample
|
// Sample
|
||||||
Word {
|
Word {
|
||||||
name: "bank",
|
name: "bank",
|
||||||
|
|||||||
122
docs/tutorials/recording.md
Normal file
122
docs/tutorials/recording.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Recording
|
||||||
|
|
||||||
|
Live recording captures the master output into a sample. One word to start, the same word to stop. The result is a first-class sample you can play, slice, and layer.
|
||||||
|
|
||||||
|
## rec
|
||||||
|
|
||||||
|
`rec` takes a name from the stack and toggles recording. Everything the engine outputs goes into a buffer until you call `rec` again:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
"drums" rec ;; start recording
|
||||||
|
```
|
||||||
|
|
||||||
|
Play something -- a pattern, a live input, anything that makes sound. When you're done:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
"drums" rec ;; stop recording, register sample
|
||||||
|
```
|
||||||
|
|
||||||
|
The recording is now available as a sample:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
drums s .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Playback
|
||||||
|
|
||||||
|
Recorded samples are ordinary samples. Everything you can do with a loaded sample works here:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
drums s 0.5 speed . ;; half speed
|
||||||
|
drums s 0.25 begin 0.5 end . ;; slice the middle quarter
|
||||||
|
drums s 800 lpf 0.3 verb . ;; filter and reverb
|
||||||
|
drums s -1 speed . ;; reverse
|
||||||
|
```
|
||||||
|
|
||||||
|
## Overdub
|
||||||
|
|
||||||
|
`overdub` (or `dub`) layers new audio on top of an existing recording. It wraps at the buffer boundary, so the loop length stays fixed:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
"drums" overdub ;; start layering onto drums
|
||||||
|
```
|
||||||
|
|
||||||
|
Play new material over the existing content. Stop with the same call:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
"drums" overdub ;; stop, register updated sample
|
||||||
|
```
|
||||||
|
|
||||||
|
If the target name doesn't exist yet, overdub falls back to a fresh recording.
|
||||||
|
|
||||||
|
## Building Layers
|
||||||
|
|
||||||
|
Record a foundation, then overdub to build up:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
;; 1. record a kick pattern
|
||||||
|
"loop" rec
|
||||||
|
;; ... play kick pattern ...
|
||||||
|
"loop" rec
|
||||||
|
|
||||||
|
;; 2. overdub hats
|
||||||
|
"loop" dub
|
||||||
|
;; ... play hat pattern ...
|
||||||
|
"loop" dub
|
||||||
|
|
||||||
|
;; 3. overdub a melody
|
||||||
|
"loop" dub
|
||||||
|
;; ... play melody ...
|
||||||
|
"loop" dub
|
||||||
|
|
||||||
|
;; 4. play the result
|
||||||
|
loop s .
|
||||||
|
```
|
||||||
|
|
||||||
|
Each overdub pass adds to what's already there. The buffer wraps, so longer passes layer cyclically over the original length.
|
||||||
|
|
||||||
|
## Slicing a Recording
|
||||||
|
|
||||||
|
Once you have a recording, carve it up:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
loop s 0.0 begin 0.25 end . ;; first quarter
|
||||||
|
loop s 0.25 begin 0.5 end . ;; second quarter
|
||||||
|
loop s 0.5 begin 0.75 end . ;; third quarter
|
||||||
|
loop s 0.75 begin 1.0 end . ;; last quarter
|
||||||
|
```
|
||||||
|
|
||||||
|
Combine with randomness for variation:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
loop s
|
||||||
|
0.0 0.25 0.5 0.75 4 choose begin
|
||||||
|
0.5 speed
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Orbit Recording
|
||||||
|
|
||||||
|
`orec` records a single orbit instead of the master output. Useful for capturing one layer (e.g. drums on orbit 0) without bleeding other orbits in:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
"drums" 0 orec ;; start recording orbit 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Play patterns routed to orbit 0. When done:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
"drums" 0 orec ;; stop, register sample
|
||||||
|
```
|
||||||
|
|
||||||
|
`odub` overdubs onto a single orbit:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
"drums" 0 odub ;; overdub orbit 0 onto "drums"
|
||||||
|
```
|
||||||
|
|
||||||
|
`rec` and `overdub` still record the master output as before.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
Recordings have a 60-second maximum. Recording is native only -- not available in the WASM build.
|
||||||
@@ -119,6 +119,10 @@ pub const DOCS: &[DocEntry] = &[
|
|||||||
"Using Variables",
|
"Using Variables",
|
||||||
include_str!("../../docs/tutorials/variables.md"),
|
include_str!("../../docs/tutorials/variables.md"),
|
||||||
),
|
),
|
||||||
|
Topic(
|
||||||
|
"Recording",
|
||||||
|
include_str!("../../docs/tutorials/recording.md"),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn topic_count() -> usize {
|
pub fn topic_count() -> usize {
|
||||||
|
|||||||
@@ -227,6 +227,24 @@ fn noall_clears_across_evaluations() {
|
|||||||
assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]);
|
assert!(!outputs[0].contains("lpf"), "lpf should be cleared: {}", outputs[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rec() {
|
||||||
|
let outputs = expect_outputs(r#""loop1" rec"#, 1);
|
||||||
|
assert_eq!(outputs[0], "/rec/rec/sound/loop1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overdub() {
|
||||||
|
let outputs = expect_outputs(r#""loop1" overdub"#, 1);
|
||||||
|
assert_eq!(outputs[0], "/rec/rec/sound/loop1/overdub/1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overdub_alias_dub() {
|
||||||
|
let outputs = expect_outputs(r#""loop1" dub"#, 1);
|
||||||
|
assert_eq!(outputs[0], "/rec/rec/sound/loop1/overdub/1");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn all_replaces_previous_global() {
|
fn all_replaces_previous_global() {
|
||||||
let f = forth();
|
let f = forth();
|
||||||
|
|||||||
Reference in New Issue
Block a user