@devaloop/devalang 0.0.1-alpha.8 → 0.0.1-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -1
- package/README.md +4 -8
- package/docs/CHANGELOG.md +27 -0
- package/examples/condition.deva +8 -12
- package/examples/group.deva +3 -3
- package/examples/index.deva +10 -8
- package/examples/loop.deva +10 -8
- package/examples/synth.deva +14 -0
- package/examples/variables.deva +1 -1
- package/out-tsc/bin/devalang.exe +0 -0
- package/out-tsc/scripts/version/fetch.js +1 -5
- package/package.json +1 -1
- package/project-version.json +3 -3
- package/rust/core/audio/engine.rs +89 -12
- package/rust/core/audio/interpreter/arrow_call.rs +129 -0
- package/rust/core/audio/interpreter/call.rs +29 -7
- package/rust/core/audio/interpreter/condition.rs +5 -1
- package/rust/core/audio/interpreter/driver.rs +41 -29
- package/rust/core/audio/interpreter/loop_.rs +11 -3
- package/rust/core/audio/interpreter/mod.rs +1 -0
- package/rust/core/audio/interpreter/spawn.rs +43 -42
- package/rust/core/audio/interpreter/trigger.rs +1 -1
- package/rust/core/audio/renderer.rs +15 -18
- package/rust/core/lexer/handler/arrow.rs +31 -0
- package/rust/core/lexer/handler/driver.rs +12 -1
- package/rust/core/lexer/handler/identifier.rs +1 -0
- package/rust/core/lexer/handler/mod.rs +1 -0
- package/rust/core/lexer/mod.rs +24 -3
- package/rust/core/lexer/token.rs +4 -0
- package/rust/core/parser/driver.rs +23 -4
- package/rust/core/parser/handler/arrow_call.rs +126 -0
- package/rust/core/parser/handler/identifier/call.rs +41 -0
- package/rust/core/parser/handler/identifier/group.rs +75 -0
- package/rust/core/parser/handler/identifier/let_.rs +133 -0
- package/rust/core/parser/handler/identifier/mod.rs +51 -0
- package/rust/core/parser/handler/identifier/sleep.rs +33 -0
- package/rust/core/parser/handler/identifier/spawn.rs +41 -0
- package/rust/core/parser/handler/identifier/synth.rs +65 -0
- package/rust/core/parser/handler/loop_.rs +24 -18
- package/rust/core/parser/handler/mod.rs +2 -1
- package/rust/core/parser/statement.rs +8 -0
- package/rust/core/preprocessor/loader.rs +57 -43
- package/rust/core/preprocessor/module.rs +3 -6
- package/rust/core/preprocessor/processor.rs +13 -4
- package/rust/core/preprocessor/resolver/call.rs +99 -29
- package/rust/core/preprocessor/resolver/condition.rs +38 -12
- package/rust/core/preprocessor/resolver/driver.rs +74 -29
- package/rust/core/preprocessor/resolver/group.rs +24 -81
- package/rust/core/preprocessor/resolver/let_.rs +31 -0
- package/rust/core/preprocessor/resolver/loop_.rs +62 -116
- package/rust/core/preprocessor/resolver/mod.rs +5 -1
- package/rust/core/preprocessor/resolver/spawn.rs +41 -36
- package/rust/core/preprocessor/resolver/synth.rs +50 -0
- package/rust/core/preprocessor/resolver/trigger.rs +51 -50
- package/rust/core/preprocessor/resolver/value.rs +78 -0
- package/rust/core/utils/path.rs +17 -32
- package/rust/core/utils/validation.rs +30 -28
- package/typescript/scripts/version/fetch.ts +1 -6
- package/rust/core/parser/handler/identifier.rs +0 -262
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "devalang"
|
|
3
|
-
version = "0.0.1-alpha.
|
|
3
|
+
version = "0.0.1-alpha.9"
|
|
4
4
|
authors = ["Devaloop <contact@devaloop.com>"]
|
|
5
5
|
description = "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text."
|
|
6
6
|
license = "MIT"
|
package/README.md
CHANGED
|
@@ -27,12 +27,12 @@ Compose loops, control samples, render and play audio — all in clean, readable
|
|
|
27
27
|
|
|
28
28
|
From studio sketches to live sets, Devalang gives you rhythmic control — with the elegance of code.
|
|
29
29
|
|
|
30
|
-
> 🚧 **v0.0.1-alpha.
|
|
30
|
+
> 🚧 **v0.0.1-alpha.9 Notice** 🚧
|
|
31
31
|
>
|
|
32
32
|
> NEW: Devalang VSCode extension is now available !
|
|
33
33
|
> [Get it here](https://marketplace.visualstudio.com/items?itemName=devaloop.devalang-vscode).
|
|
34
34
|
>
|
|
35
|
-
>
|
|
35
|
+
> Includes synthesis, playback, and rendering features, but is still in early development.
|
|
36
36
|
>
|
|
37
37
|
> Currently, Devalang CLI is only available for **Windows**.
|
|
38
38
|
> Linux and macOS binaries will be added in future releases via cross-platform builds.
|
|
@@ -181,24 +181,20 @@ bpm globalBpm
|
|
|
181
181
|
bank globalBank
|
|
182
182
|
# Will declare a custom instrument bank using the globalBank variable
|
|
183
183
|
|
|
184
|
-
# Loops
|
|
185
|
-
|
|
186
184
|
loop 5:
|
|
187
185
|
.customKick kickDuration {reverb=50, drive=25}
|
|
188
186
|
# Will play 5 times a custom sample for 500ms with reverb and overdrive effects
|
|
189
187
|
|
|
190
|
-
# Groups
|
|
191
|
-
|
|
192
188
|
group myGroup:
|
|
193
189
|
.customKick kickDuration {reverb=50, drive=25}
|
|
194
190
|
# Will play the same sample in a group, allowing for more complex patterns
|
|
195
191
|
|
|
196
|
-
# Will be executed line by line (sequentially)
|
|
197
192
|
call myGroup
|
|
193
|
+
# Will be executed line by line (sequentially)
|
|
198
194
|
|
|
195
|
+
# spawn myGroup
|
|
199
196
|
# Will be executed in parallel (concurrently)
|
|
200
197
|
# ⚠️ Note: `spawn` runs the entire group in parallel, but the group’s internal logic remains sequential unless it uses `spawn` internally.
|
|
201
|
-
# spawn myGroup
|
|
202
198
|
```
|
|
203
199
|
|
|
204
200
|
> 🧠 Note: `call` and `spawn` only work with `group` blocks. They do not apply to individual samples or other statements.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## Version 0.0.1-alpha.9 (2025-07-14)
|
|
8
|
+
|
|
9
|
+
### ✨ Syntax
|
|
10
|
+
|
|
11
|
+
- Added support for `synth` directives to define synthesizer sounds directly in code
|
|
12
|
+
→ Example:
|
|
13
|
+
|
|
14
|
+
```deva
|
|
15
|
+
let mySynth = synth sine
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### 🧠 Core Engine
|
|
19
|
+
|
|
20
|
+
- ✅ **Major refactor** of the resolution layer:
|
|
21
|
+
|
|
22
|
+
- `condition`, `loop`, `group`, `trigger`, `call`, `spawn`, and `driver` were fully rewritten
|
|
23
|
+
- Ensures **correct variable scoping**, **cross-module references**, and **imported symbol resolution**
|
|
24
|
+
|
|
25
|
+
- ✅ Audio interpreter updated:
|
|
26
|
+
- `call` and `spawn` now execute correctly in parallel, preserving **temporal alignment** across groups
|
|
27
|
+
|
|
28
|
+
### 🧩 Language Features
|
|
29
|
+
|
|
30
|
+
- Improved `@import` behavior:
|
|
31
|
+
- Modules can now be imported using **relative paths only**
|
|
32
|
+
- No need for absolute or root-based imports
|
|
33
|
+
|
|
7
34
|
## Version 0.0.1-alpha.8 (2025-07-12)
|
|
8
35
|
|
|
9
36
|
### Syntax
|
package/examples/condition.deva
CHANGED
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
# This file demonstrates the use of conditional blocks in Devalang.
|
|
2
2
|
|
|
3
|
-
@import {
|
|
3
|
+
@import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
|
|
4
|
+
@import { myGroup } from "./group.deva"
|
|
4
5
|
|
|
5
|
-
@load "./
|
|
6
|
-
@load "./
|
|
6
|
+
@load "./samples/kick-808.wav" as kickCustom
|
|
7
|
+
@load "./samples/hat-808.wav" as hatCustom
|
|
7
8
|
|
|
8
9
|
group conditionBlock:
|
|
9
10
|
if tempo > 120:
|
|
10
11
|
# Will be executed if the condition is true
|
|
11
|
-
.kickCustom
|
|
12
|
+
.kickCustom auto
|
|
12
13
|
else if tempo > 155:
|
|
13
14
|
# Will be executed if the condition is false
|
|
14
|
-
.hatCustom
|
|
15
|
+
.hatCustom auto
|
|
15
16
|
else:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
# This will call myGroup sequentially (in the main thread)
|
|
19
|
-
call myGroup
|
|
20
|
-
|
|
21
|
-
# This will spawn a group in parallel to the main thread (kick + hat simultaneously)
|
|
22
|
-
# spawn myGroup
|
|
17
|
+
.kickCustom auto
|
|
18
|
+
.hatCustom auto
|
|
23
19
|
|
|
24
20
|
@export { conditionBlock }
|
package/examples/group.deva
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# This file demonstrates the use of grouping in Devalang.
|
|
2
2
|
|
|
3
|
-
@import { duration, default_bank, params, loopCount, tempo } from "./
|
|
3
|
+
@import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
|
|
4
4
|
|
|
5
|
-
@load "./
|
|
6
|
-
@load "./
|
|
5
|
+
@load "./samples/kick-808.wav" as kickCustom
|
|
6
|
+
@load "./samples/hat-808.wav" as hatCustom
|
|
7
7
|
|
|
8
8
|
group myGroup:
|
|
9
9
|
.kickCustom duration params
|
package/examples/index.deva
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# This file demonstrates the use of main features in Devalang.
|
|
2
2
|
|
|
3
|
-
@import { duration, default_bank, params, loopCount, tempo } from "./
|
|
4
|
-
@import {
|
|
5
|
-
@import {
|
|
3
|
+
@import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
|
|
4
|
+
@import { myLead } from "./synth.deva"
|
|
5
|
+
@import { myLoop } from "./loop.deva"
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
@load "./
|
|
9
|
-
@load "./examples/samples/hat-808.wav" as hatCustom
|
|
7
|
+
@load "./samples/kick-808.wav" as kickCustom
|
|
8
|
+
@load "./samples/hat-808.wav" as hatCustom
|
|
10
9
|
|
|
11
10
|
bpm tempo
|
|
12
11
|
|
|
13
12
|
bank default_bank
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
group myTrack:
|
|
15
|
+
spawn myLoop
|
|
16
|
+
spawn myLead
|
|
17
|
+
|
|
18
|
+
call myTrack
|
package/examples/loop.deva
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# This file demonstrates the use of a loop in Devalang.
|
|
2
2
|
|
|
3
|
-
@import { duration, default_bank, params, loopCount, tempo } from "./
|
|
3
|
+
@import { duration, default_bank, params, loopCount, tempo } from "./variables.deva"
|
|
4
4
|
|
|
5
|
-
@load "./
|
|
6
|
-
@load "./
|
|
5
|
+
@load "./samples/kick-808.wav" as kickCustom
|
|
6
|
+
@load "./samples/hat-808.wav" as hatCustom
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
group myLoop:
|
|
9
|
+
loop loopCount:
|
|
10
|
+
.kickCustom duration params
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
# Uncomment the next line (.hat) while executing "play" command
|
|
13
|
+
# with `--repeat` option to see magic happen !
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
.hatCustom duration params
|
|
15
16
|
|
|
17
|
+
@export { myLoop }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# This file defines a simple synth lead using Devalang's syntax.
|
|
2
|
+
|
|
3
|
+
group myLead:
|
|
4
|
+
let mySynth = synth sine
|
|
5
|
+
|
|
6
|
+
mySynth -> note(C4, { duration: 400 })
|
|
7
|
+
mySynth -> note(G4, { duration: 400 })
|
|
8
|
+
mySynth -> note(E4, { duration: 600 })
|
|
9
|
+
mySynth -> note(A4, { duration: 400 })
|
|
10
|
+
mySynth -> note(F4, { duration: 800 })
|
|
11
|
+
mySynth -> note(D4, { duration: 400 })
|
|
12
|
+
mySynth -> note(B3, { duration: 600 })
|
|
13
|
+
|
|
14
|
+
@export { myLead }
|
package/examples/variables.deva
CHANGED
package/out-tsc/bin/devalang.exe
CHANGED
|
Binary file
|
|
@@ -16,19 +16,15 @@ exports.fetchVersion = void 0;
|
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
17
|
const child_process_1 = require("child_process");
|
|
18
18
|
const fetchVersion = (projectVersionPath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
19
|
-
// Lire le fichier
|
|
20
19
|
const data = JSON.parse(fs_1.default.readFileSync(projectVersionPath, "utf-8"));
|
|
21
|
-
// Incrémenter le numéro de build
|
|
22
20
|
data.build = (data.build || 0) + 1;
|
|
23
|
-
// Récupérer le dernier hash git
|
|
24
21
|
try {
|
|
25
22
|
const commit = (0, child_process_1.execSync)("git rev-parse HEAD").toString().trim();
|
|
26
23
|
data.lastCommit = commit;
|
|
27
24
|
}
|
|
28
25
|
catch (err) {
|
|
29
|
-
console.warn("⚠️
|
|
26
|
+
console.warn("⚠️ Unable to fetch git commit hash. Ensure you are in a git repository.");
|
|
30
27
|
}
|
|
31
|
-
// Écrire la mise à jour
|
|
32
28
|
fs_1.default.writeFileSync(projectVersionPath, JSON.stringify(data, null, 2));
|
|
33
29
|
});
|
|
34
30
|
exports.fetchVersion = fetchVersion;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devaloop/devalang",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.1-alpha.
|
|
4
|
+
"version": "0.0.1-alpha.9",
|
|
5
5
|
"description": "Write music like code. Devalang is a domain-specific language (DSL) for sound designers and music hackers. Compose, automate, and control sound — in plain text.",
|
|
6
6
|
"main": "out-tsc/index.js",
|
|
7
7
|
"bin": {
|
package/project-version.json
CHANGED
|
@@ -2,7 +2,10 @@ use std::{ collections::HashMap, fs::File, io::BufReader };
|
|
|
2
2
|
use hound::{ SampleFormat, WavSpec, WavWriter };
|
|
3
3
|
use rodio::{ Decoder, Source };
|
|
4
4
|
|
|
5
|
-
use crate::core::{
|
|
5
|
+
use crate::core::{
|
|
6
|
+
store::variable::VariableTable,
|
|
7
|
+
utils::path::{ normalize_path, resolve_relative_path },
|
|
8
|
+
};
|
|
6
9
|
|
|
7
10
|
const SAMPLE_RATE: u32 = 44100;
|
|
8
11
|
const CHANNELS: u16 = 2;
|
|
@@ -12,14 +15,16 @@ pub struct AudioEngine {
|
|
|
12
15
|
pub volume: f32,
|
|
13
16
|
pub variables: VariableTable,
|
|
14
17
|
pub buffer: Vec<i16>,
|
|
18
|
+
pub module_name: String,
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
impl AudioEngine {
|
|
18
|
-
pub fn new() -> Self {
|
|
22
|
+
pub fn new(module_name: String) -> Self {
|
|
19
23
|
AudioEngine {
|
|
20
24
|
volume: 1.0,
|
|
21
25
|
buffer: vec![],
|
|
22
26
|
variables: VariableTable::new(),
|
|
27
|
+
module_name,
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
|
|
@@ -32,14 +37,28 @@ impl AudioEngine {
|
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
pub fn
|
|
36
|
-
|
|
40
|
+
pub fn merge_with(&mut self, other: AudioEngine) {
|
|
41
|
+
if other.buffer.iter().all(|&s| s == 0) {
|
|
42
|
+
eprintln!("⚠️ Skipping merge: other buffer is silent");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
37
45
|
|
|
38
|
-
if
|
|
39
|
-
|
|
46
|
+
if self.buffer.iter().all(|&s| s == 0) {
|
|
47
|
+
self.buffer = other.buffer;
|
|
48
|
+
self.variables.variables.extend(other.variables.variables);
|
|
49
|
+
return;
|
|
40
50
|
}
|
|
41
51
|
|
|
42
|
-
self.
|
|
52
|
+
self.mix(&other);
|
|
53
|
+
self.variables.variables.extend(other.variables.variables);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub fn set_duration(&mut self, duration_secs: f32) {
|
|
57
|
+
let total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
|
|
58
|
+
|
|
59
|
+
if self.buffer.len() < total_samples {
|
|
60
|
+
self.buffer.resize(total_samples, 0);
|
|
61
|
+
}
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
pub fn set_variables(&mut self, variables: VariableTable) {
|
|
@@ -72,18 +91,76 @@ impl AudioEngine {
|
|
|
72
91
|
Ok(())
|
|
73
92
|
}
|
|
74
93
|
|
|
75
|
-
pub fn
|
|
94
|
+
pub fn insert_note(
|
|
95
|
+
&mut self,
|
|
96
|
+
waveform: String,
|
|
97
|
+
freq: f32,
|
|
98
|
+
amp: f32,
|
|
99
|
+
start_time_ms: f32,
|
|
100
|
+
duration_ms: f32
|
|
101
|
+
) {
|
|
102
|
+
let sample_rate = SAMPLE_RATE as f32;
|
|
103
|
+
let channels = CHANNELS as usize;
|
|
104
|
+
|
|
105
|
+
let total_samples = ((duration_ms / 1000.0) * sample_rate) as usize;
|
|
106
|
+
let start_sample = ((start_time_ms / 1000.0) * sample_rate) as usize;
|
|
107
|
+
let amplitude = (i16::MAX as f32) * amp.clamp(0.0, 1.0);
|
|
108
|
+
|
|
109
|
+
let mut samples = Vec::with_capacity(total_samples);
|
|
110
|
+
let fade_len = (sample_rate * 0.01) as usize; // 10 ms fade
|
|
111
|
+
|
|
112
|
+
for i in 0..total_samples {
|
|
113
|
+
let t = ((start_sample + i) as f32) / sample_rate;
|
|
114
|
+
let phase = 2.0 * std::f32::consts::PI * freq * t;
|
|
115
|
+
|
|
116
|
+
let mut value = match waveform.as_str() {
|
|
117
|
+
"sine" => phase.sin(),
|
|
118
|
+
"square" => if phase.sin() >= 0.0 { 1.0 } else { -1.0 }
|
|
119
|
+
"saw" => 2.0 * (freq * t - (freq * t + 0.5).floor()),
|
|
120
|
+
"triangle" => (2.0 * (2.0 * (freq * t).fract() - 1.0)).abs() * 2.0 - 1.0,
|
|
121
|
+
_ => 0.0,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Fade in/out
|
|
125
|
+
if i < fade_len {
|
|
126
|
+
value *= (i as f32) / (fade_len as f32);
|
|
127
|
+
} else if i >= total_samples - fade_len {
|
|
128
|
+
value *= ((total_samples - i) as f32) / (fade_len as f32);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
samples.push((value * amplitude) as i16);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Convert to stereo
|
|
135
|
+
let stereo_samples: Vec<i16> = samples
|
|
136
|
+
.iter()
|
|
137
|
+
.flat_map(|s| vec![*s, *s])
|
|
138
|
+
.collect();
|
|
139
|
+
|
|
140
|
+
let offset = start_sample * channels;
|
|
141
|
+
let required_len = offset + stereo_samples.len();
|
|
142
|
+
|
|
143
|
+
if self.buffer.len() < required_len {
|
|
144
|
+
self.buffer.resize(required_len, 0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (i, sample) in stereo_samples.iter().enumerate() {
|
|
148
|
+
if *sample != 0 {
|
|
149
|
+
}
|
|
150
|
+
self.buffer[offset + i] = self.buffer[offset + i].saturating_add(*sample);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pub fn insert_sample(
|
|
76
155
|
&mut self,
|
|
77
156
|
filepath: &str,
|
|
78
157
|
time_secs: f32,
|
|
79
158
|
dur_sec: f32,
|
|
80
159
|
effects: Option<HashMap<String, f32>>
|
|
81
160
|
) {
|
|
82
|
-
let
|
|
161
|
+
let resolved = resolve_relative_path(&self.module_name.clone(), filepath);
|
|
83
162
|
|
|
84
|
-
let file = BufReader::new(
|
|
85
|
-
File::open(normalized_filepath).expect("Failed to open audio file")
|
|
86
|
-
);
|
|
163
|
+
let file = BufReader::new(File::open(resolved).expect("Failed to open audio file"));
|
|
87
164
|
let decoder = Decoder::new(file).expect("Failed to decode audio file");
|
|
88
165
|
|
|
89
166
|
// Mono or stereo reading possible here, we will duplicate in L/R
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
use crate::core::{
|
|
2
|
+
audio::engine::AudioEngine,
|
|
3
|
+
parser::statement::{ Statement, StatementKind },
|
|
4
|
+
shared::value::Value,
|
|
5
|
+
store::variable::VariableTable,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
|
|
10
|
+
pub fn interprete_call_arrow_statement(
|
|
11
|
+
stmt: &Statement,
|
|
12
|
+
audio_engine: &mut AudioEngine,
|
|
13
|
+
variable_table: &VariableTable,
|
|
14
|
+
base_bpm: f32,
|
|
15
|
+
base_duration: f32,
|
|
16
|
+
max_end_time: &mut f32,
|
|
17
|
+
mut cursor_time: Option<&mut f32>,
|
|
18
|
+
update_cursor: bool
|
|
19
|
+
) -> (f32, f32) {
|
|
20
|
+
let cursor_copy = cursor_time
|
|
21
|
+
.as_ref()
|
|
22
|
+
.map(|c| **c)
|
|
23
|
+
.unwrap_or(0.0);
|
|
24
|
+
|
|
25
|
+
if let StatementKind::ArrowCall { target, method, args } = &stmt.kind {
|
|
26
|
+
let Some(Value::Map(synth_map)) = variable_table.get(target) else {
|
|
27
|
+
println!("❌ Synth '{}' not found in variable table", target);
|
|
28
|
+
return (*max_end_time, cursor_copy);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let Some(Value::String(entity)) = synth_map.get("entity") else {
|
|
32
|
+
println!("❌ Missing 'entity' key in synth '{}'.", target);
|
|
33
|
+
return (*max_end_time, cursor_copy);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
if entity != "synth" {
|
|
37
|
+
println!("❌ '{}' is not a synth, entity is '{}'.", target, entity);
|
|
38
|
+
return (*max_end_time, cursor_copy);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let Some(Value::Map(value_map)) = synth_map.get("value") else {
|
|
42
|
+
println!("❌ Missing 'value' map in synth '{}'.", target);
|
|
43
|
+
return (*max_end_time, cursor_copy);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let Some(Value::String(waveform)) = value_map.get("waveform") else {
|
|
47
|
+
println!("❌ Missing or invalid 'waveform' in synth '{}'.", target);
|
|
48
|
+
return (*max_end_time, cursor_copy);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
let Some(Value::Map(params)) = value_map.get("parameters") else {
|
|
52
|
+
println!("❌ Missing or invalid 'parameters' in synth '{}'.", target);
|
|
53
|
+
return (*max_end_time, cursor_copy);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let freq = extract_f32(params, "freq").unwrap_or(440.0);
|
|
57
|
+
let amp = extract_f32(params, "amp").unwrap_or(1.0);
|
|
58
|
+
|
|
59
|
+
if method == "note" {
|
|
60
|
+
let Some(Value::Identifier(note_name)) = args.get(0) else {
|
|
61
|
+
println!("❌ Invalid or missing argument for 'note' method on '{}'.", target);
|
|
62
|
+
return (*max_end_time, cursor_copy);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let mut final_note_params = HashMap::new();
|
|
66
|
+
if let Some(Value::Map(note_params)) = args.get(1) {
|
|
67
|
+
for (key, value) in note_params {
|
|
68
|
+
final_note_params.insert(key.clone(), value.clone());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let duration_ms = extract_f32(&final_note_params, "duration").unwrap_or(base_duration);
|
|
73
|
+
let duration_secs = duration_ms / 1000.0;
|
|
74
|
+
|
|
75
|
+
let final_freq = note_to_freq(note_name);
|
|
76
|
+
let start_time = cursor_copy;
|
|
77
|
+
let end_time = start_time + duration_secs;
|
|
78
|
+
|
|
79
|
+
audio_engine.insert_note(
|
|
80
|
+
waveform.clone(),
|
|
81
|
+
final_freq,
|
|
82
|
+
amp,
|
|
83
|
+
start_time * 1000.0,
|
|
84
|
+
duration_ms
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
*max_end_time = (*max_end_time).max(end_time);
|
|
88
|
+
|
|
89
|
+
if update_cursor {
|
|
90
|
+
if let Some(c) = cursor_time.as_mut() {
|
|
91
|
+
**c = end_time;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (*max_end_time, end_time);
|
|
96
|
+
} else {
|
|
97
|
+
println!("❌ Unknown method '{}' on synth '{}'.", method, target);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
(*max_end_time, cursor_copy)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn extract_f32(map: &HashMap<String, Value>, key: &str) -> Option<f32> {
|
|
105
|
+
map.get(key).and_then(|v| {
|
|
106
|
+
match v {
|
|
107
|
+
Value::Number(n) => Some(*n),
|
|
108
|
+
_ => None,
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fn note_to_freq(note: &str) -> f32 {
|
|
114
|
+
let notes = vec!["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
115
|
+
|
|
116
|
+
if note.len() < 2 || note.len() > 3 {
|
|
117
|
+
return 440.0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let (name, octave_str) = note.split_at(note.len() - 1);
|
|
121
|
+
let semitone = notes
|
|
122
|
+
.iter()
|
|
123
|
+
.position(|&n| n == name)
|
|
124
|
+
.unwrap_or(9) as i32;
|
|
125
|
+
let octave = octave_str.parse::<i32>().unwrap_or(4);
|
|
126
|
+
let midi_note = (octave + 1) * 12 + semitone;
|
|
127
|
+
|
|
128
|
+
440.0 * (2.0_f32).powf(((midi_note as f32) - 69.0) / 12.0)
|
|
129
|
+
}
|
|
@@ -14,8 +14,30 @@ pub fn interprete_call_statement(
|
|
|
14
14
|
max_end_time: f32,
|
|
15
15
|
cursor_time: f32
|
|
16
16
|
) -> (AudioEngine, f32, f32) {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
match &stmt.value {
|
|
18
|
+
Value::String(identifier) | Value::Identifier(identifier) => {
|
|
19
|
+
if let Some(Value::Map(map)) = variable_table.clone().get(identifier) {
|
|
20
|
+
if let Some(Value::Block(block)) = map.get("body") {
|
|
21
|
+
let (eng, _, end_time) = execute_audio_block(
|
|
22
|
+
audio_engine,
|
|
23
|
+
variable_table,
|
|
24
|
+
block.clone(),
|
|
25
|
+
base_bpm,
|
|
26
|
+
base_duration,
|
|
27
|
+
max_end_time,
|
|
28
|
+
cursor_time
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (eng, max_end_time.max(end_time), end_time);
|
|
32
|
+
} else {
|
|
33
|
+
eprintln!("❌ Group '{}' has no 'body' block", identifier);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
eprintln!("❌ Group '{}' not found or not a map", identifier);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Value::Map(map) => {
|
|
19
41
|
if let Some(Value::Block(block)) = map.get("body") {
|
|
20
42
|
let (eng, _, end_time) = execute_audio_block(
|
|
21
43
|
audio_engine,
|
|
@@ -29,13 +51,13 @@ pub fn interprete_call_statement(
|
|
|
29
51
|
|
|
30
52
|
return (eng, max_end_time.max(end_time), end_time);
|
|
31
53
|
} else {
|
|
32
|
-
eprintln!("❌ 'body'
|
|
54
|
+
eprintln!("❌ Call map has no 'body' block");
|
|
33
55
|
}
|
|
34
|
-
} else {
|
|
35
|
-
eprintln!("❌ Call target '{}' not found or invalid", identifier);
|
|
36
56
|
}
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
|
|
58
|
+
other => {
|
|
59
|
+
eprintln!("❌ Invalid call statement: expected identifier or map, found {:?}", other);
|
|
60
|
+
}
|
|
39
61
|
}
|
|
40
62
|
|
|
41
63
|
(audio_engine, max_end_time, cursor_time)
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
use crate::core::{
|
|
2
|
-
audio::{
|
|
2
|
+
audio::{
|
|
3
|
+
engine::AudioEngine,
|
|
4
|
+
evaluator::evaluate_condition_string,
|
|
5
|
+
interpreter::driver::execute_audio_block,
|
|
6
|
+
},
|
|
3
7
|
parser::statement::Statement,
|
|
4
8
|
shared::value::Value,
|
|
5
9
|
store::variable::VariableTable,
|