@devaloop/devalang 0.0.1-alpha.11 → 0.0.1-alpha.13
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/.devalang +8 -8
- package/Cargo.toml +8 -8
- package/README.md +1 -14
- package/docs/CHANGELOG.md +44 -0
- package/docs/TODO.md +1 -1
- package/examples/index.deva +10 -11
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +2 -1
- package/project-version.json +3 -3
- package/rust/cli/build.rs +25 -2
- package/rust/cli/check.rs +26 -3
- package/rust/cli/play.rs +1 -1
- package/rust/core/audio/engine.rs +207 -41
- package/rust/core/audio/interpreter/call.rs +72 -47
- package/rust/core/audio/interpreter/condition.rs +14 -12
- package/rust/core/audio/interpreter/driver.rs +84 -127
- package/rust/core/audio/interpreter/function.rs +21 -0
- package/rust/core/audio/interpreter/load.rs +1 -1
- package/rust/core/audio/interpreter/loop_.rs +24 -18
- package/rust/core/audio/interpreter/mod.rs +2 -1
- package/rust/core/audio/interpreter/sleep.rs +0 -6
- package/rust/core/audio/interpreter/spawn.rs +78 -60
- package/rust/core/audio/interpreter/trigger.rs +169 -61
- package/rust/core/audio/loader/trigger.rs +37 -4
- package/rust/core/audio/player.rs +20 -10
- package/rust/core/audio/renderer.rs +24 -25
- package/rust/core/debugger/mod.rs +2 -0
- package/rust/core/debugger/module.rs +47 -0
- package/rust/core/debugger/store.rs +25 -11
- package/rust/core/error/mod.rs +6 -0
- package/rust/core/lexer/handler/driver.rs +23 -1
- package/rust/core/lexer/handler/identifier.rs +1 -0
- package/rust/core/lexer/handler/mod.rs +1 -0
- package/rust/core/lexer/handler/parenthesis.rs +41 -0
- package/rust/core/lexer/token.rs +3 -0
- package/rust/core/parser/driver.rs +31 -3
- package/rust/core/parser/handler/dot.rs +65 -129
- package/rust/core/parser/handler/identifier/call.rs +69 -22
- package/rust/core/parser/handler/identifier/function.rs +92 -0
- package/rust/core/parser/handler/identifier/let_.rs +13 -19
- package/rust/core/parser/handler/identifier/mod.rs +1 -0
- package/rust/core/parser/handler/identifier/spawn.rs +74 -27
- package/rust/core/parser/statement.rs +16 -4
- package/rust/core/preprocessor/loader.rs +45 -29
- package/rust/core/preprocessor/module.rs +3 -1
- package/rust/core/preprocessor/processor.rs +26 -1
- package/rust/core/preprocessor/resolver/call.rs +61 -84
- package/rust/core/preprocessor/resolver/condition.rs +11 -6
- package/rust/core/preprocessor/resolver/driver.rs +52 -6
- package/rust/core/preprocessor/resolver/function.rs +78 -0
- package/rust/core/preprocessor/resolver/group.rs +43 -13
- package/rust/core/preprocessor/resolver/let_.rs +7 -10
- package/rust/core/preprocessor/resolver/mod.rs +2 -1
- package/rust/core/preprocessor/resolver/spawn.rs +64 -30
- package/rust/core/preprocessor/resolver/trigger.rs +7 -3
- package/rust/core/preprocessor/resolver/value.rs +10 -1
- package/rust/core/shared/value.rs +4 -1
- package/rust/core/store/function.rs +34 -0
- package/rust/core/store/global.rs +9 -10
- package/rust/core/store/mod.rs +2 -1
- package/rust/core/store/variable.rs +6 -0
- package/rust/installer/bank.rs +1 -1
- package/rust/installer/mod.rs +2 -1
- package/rust/lib.rs +10 -8
- package/rust/utils/mod.rs +44 -1
- /package/rust/{utils/installer.rs → installer/utils.rs} +0 -0
package/.devalang
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
[defaults]
|
|
2
|
-
entry = "./examples"
|
|
3
|
-
output = "./output"
|
|
4
|
-
watch = false
|
|
5
|
-
|
|
6
|
-
[[banks]]
|
|
7
|
-
path = "devalang://bank/808"
|
|
8
|
-
version = "0.0.1"
|
|
1
|
+
[defaults]
|
|
2
|
+
entry = "./examples"
|
|
3
|
+
output = "./output"
|
|
4
|
+
watch = false
|
|
5
|
+
|
|
6
|
+
[[banks]]
|
|
7
|
+
path = "devalang://bank/808"
|
|
8
|
+
version = "0.0.1"
|
|
9
9
|
author = "devaloop"
|
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.13"
|
|
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"
|
|
@@ -20,14 +20,14 @@ required-features = ["cli"]
|
|
|
20
20
|
|
|
21
21
|
[lib]
|
|
22
22
|
path = "rust/lib.rs"
|
|
23
|
-
crate-type = ["cdylib"]
|
|
23
|
+
crate-type = ["cdylib", "rlib"]
|
|
24
24
|
|
|
25
25
|
[profile.release]
|
|
26
26
|
opt-level = "s"
|
|
27
27
|
|
|
28
28
|
[features]
|
|
29
29
|
default = ["cli"]
|
|
30
|
-
cli = ["crossterm", "indicatif", "inquire"]
|
|
30
|
+
cli = ["crossterm", "indicatif", "inquire", "zip", "reqwest", "flate2", "tokio"]
|
|
31
31
|
|
|
32
32
|
[dependencies]
|
|
33
33
|
clap = { version = "4.5", features = ["derive"] }
|
|
@@ -47,8 +47,8 @@ crossterm = { version = "0.27", optional = true }
|
|
|
47
47
|
indicatif = { version = "0.17", optional = true }
|
|
48
48
|
inquire = { version = "0.7.5", optional = true }
|
|
49
49
|
js-sys = "0.3"
|
|
50
|
-
reqwest = { version = "0.12.22", features = ["json"] }
|
|
51
|
-
flate2 = "1.0"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
reqwest = { version = "0.12.22", optional = true, features = ["json"] }
|
|
51
|
+
flate2 = { version = "1.0", optional = true }
|
|
52
|
+
tokio = { version = "1", features = ["full"], optional = true }
|
|
53
|
+
zip = { version = "4.3.0", optional = true }
|
|
54
|
+
rayon = "1.10.0"
|
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ 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.13 Notice** 🚧
|
|
31
31
|
>
|
|
32
32
|
> NEW: Devalang is available in your browser at [playground.devalang.com](https://playground.devalang.com) !
|
|
33
33
|
>
|
|
@@ -119,22 +119,9 @@ devalang play --repeat
|
|
|
119
119
|
|
|
120
120
|
### Please refer to the [online documentation](https://docs.devalang.com) for detailed information on syntax, features, and usage examples.
|
|
121
121
|
|
|
122
|
-
## 📜 Changelog Highlights
|
|
123
|
-
|
|
124
|
-
For a complete list of changes, see [docs/CHANGELOG.md](./docs/CHANGELOG.md)
|
|
125
|
-
|
|
126
|
-
- Implemented beat durations in `triggers` and `arrow_calls` statements
|
|
127
|
-
- Implemented `bank` resolver to resolve banks of sounds in the code
|
|
128
|
-
- Support for namespaced banks of sounds (e.g. `.808.myTrigger`)
|
|
129
|
-
- Implemented multiple commands for `bank` management
|
|
130
|
-
- `bank list`, `bank available`, `bank info <bank_name>`, `bank remove <bank_name>`, `bank update`, `bank update <bank_name>`
|
|
131
|
-
- Implemented `install` command to install banks of sounds
|
|
132
|
-
- `install bank <bank_name>`
|
|
133
|
-
|
|
134
122
|
## 🧯 Known issues
|
|
135
123
|
|
|
136
124
|
- No smart modules yet, all groups, variables, and samples must be explicitly imported where used
|
|
137
|
-
- No support yet for `pattern`, `function`, ... statements
|
|
138
125
|
- No support yet for cross-platform builds (Linux, macOS)
|
|
139
126
|
|
|
140
127
|
## 🧪 Roadmap Highlights
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,50 @@
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## Version 0.0.1-alpha.13 (2025-07-26)
|
|
8
|
+
|
|
9
|
+
### 🧩 Language Features
|
|
10
|
+
|
|
11
|
+
- Added support for `fn` directive to define functions in Devalang.
|
|
12
|
+
- Example: `fn myFunction(param1, param2):`
|
|
13
|
+
|
|
14
|
+
### 🧠 Core Engine
|
|
15
|
+
|
|
16
|
+
- Patched `trigger`, `call`, and `spawn`, `renderer` to handle correct cursor time in the audio interpreter.
|
|
17
|
+
- Refactored audio engine and interpreter to handle correct timing and execution while using `loop`, `call`, and `spawn` statements.
|
|
18
|
+
- Refactored `trigger` effects to apply more effects to triggers.
|
|
19
|
+
- Example: `.myTrigger auto { reverb: 0.25, pitch: 0.75, gain: 0.8 }`
|
|
20
|
+
- Refactored `preprocessor` to handle correct namespaced banks of sounds and triggers.
|
|
21
|
+
- Refactored `collect_errors_recursively` to provide detailed error reporting across nested statements.
|
|
22
|
+
- Optimized the `renderer` to handle silent buffers and improve performance.
|
|
23
|
+
|
|
24
|
+
### 🛠️ Utilities
|
|
25
|
+
|
|
26
|
+
- Added the `extract_loop_body_statements` utility for better loop handling.
|
|
27
|
+
- Improved logging for module variables and functions.
|
|
28
|
+
|
|
29
|
+
### 🧩 Web Assembly
|
|
30
|
+
|
|
31
|
+
- Patched `lib.rs` dependencies to ensure compatibility with the latest Rust and WASM standards.
|
|
32
|
+
|
|
33
|
+
## Version 0.0.1-alpha.12 (2025-07-21)
|
|
34
|
+
|
|
35
|
+
### 🧩 Language Features
|
|
36
|
+
|
|
37
|
+
- Implemented `trigger` effects to apply effects to triggers, allowing for more dynamic sound manipulation.
|
|
38
|
+
- Example: `.myTrigger auto { reverb: 1.0, pitch: 1.5, gain: 0.8 }`
|
|
39
|
+
|
|
40
|
+
### 🧠 Core Engine
|
|
41
|
+
|
|
42
|
+
- Moved `utils::installer` to `installer::utils` to better organize the project structure.
|
|
43
|
+
- Set CLI dependencies as optional in `Cargo.toml` to allow for a cleaner build without CLI features.
|
|
44
|
+
- Patched `@load` relative path resolution to ensure correct loading of external resources.
|
|
45
|
+
- Patched `trigger` statement that was not correctly parsed when using namespaced banks of sounds.
|
|
46
|
+
|
|
47
|
+
### 🧩 Web Assembly
|
|
48
|
+
|
|
49
|
+
- Patched `lib.rs` dependencies to ensure compatibility with the latest Rust and WASM standards.
|
|
50
|
+
|
|
7
51
|
## Version 0.0.1-alpha.11 (2025-07-20)
|
|
8
52
|
|
|
9
53
|
### 📖 Documentation
|
package/docs/TODO.md
CHANGED
package/examples/index.deva
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
# This file demonstrates the use of main features in Devalang.
|
|
2
|
+
bpm 135
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
@import { myLead } from "./synth.deva"
|
|
5
|
-
@import { myLoop } from "./loop.deva"
|
|
4
|
+
bank 808
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
let entityTest1 = .808.kick auto
|
|
7
|
+
let entityTest2 = .808.clap auto
|
|
8
|
+
let entityTest3 = .808.snare auto
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
fn myFirstGroup(entity1, entity2, entity3):
|
|
11
|
+
.entity1
|
|
12
|
+
.entity2
|
|
13
|
+
.entity3
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
spawn myLoop
|
|
14
|
-
spawn myLead
|
|
15
|
-
|
|
16
|
-
call myTrack
|
|
15
|
+
call myFirstGroup(entityTest1, entityTest2, entityTest3)
|
package/out-tsc/bin/devalang.exe
CHANGED
|
Binary file
|
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.13",
|
|
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": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"prepublish": "cargo build --release && npm run script:postbuild",
|
|
12
12
|
"rust:dev:build": "cargo run build --entry examples --output output",
|
|
13
13
|
"rust:dev:check": "cargo run check --entry examples --output output",
|
|
14
|
+
"rust:dev:play": "cargo run play --entry examples --output output --repeat",
|
|
14
15
|
"rust:wasm:web": "wasm-pack build --target=web --no-default-features",
|
|
15
16
|
"rust:wasm:node": "wasm-pack build --target=nodejs --no-default-features",
|
|
16
17
|
"script:postbuild": "tsc && node out-tsc/scripts/postbuild.js",
|
package/project-version.json
CHANGED
package/rust/cli/build.rs
CHANGED
|
@@ -4,8 +4,9 @@ use crate::{
|
|
|
4
4
|
builder::Builder,
|
|
5
5
|
debugger::{
|
|
6
6
|
lexer::write_lexer_log_file,
|
|
7
|
+
module::{ write_module_function_log_file, write_module_variable_log_file },
|
|
7
8
|
preprocessor::write_preprocessor_log_file,
|
|
8
|
-
store::
|
|
9
|
+
store::{ write_function_log_file, write_variables_log_file },
|
|
9
10
|
},
|
|
10
11
|
preprocessor::loader::ModuleLoader,
|
|
11
12
|
store::global::GlobalStore,
|
|
@@ -111,13 +112,35 @@ fn begin_build(entry: String, output: String) {
|
|
|
111
112
|
let (modules_tokens, modules_statements) = module_loader.load_all_modules(&mut global_store);
|
|
112
113
|
|
|
113
114
|
// SECTION Write logs
|
|
115
|
+
for (module_path, module) in global_store.modules.clone() {
|
|
116
|
+
write_module_variable_log_file(
|
|
117
|
+
&normalized_output_dir,
|
|
118
|
+
&module_path,
|
|
119
|
+
&module.variable_table
|
|
120
|
+
);
|
|
121
|
+
write_module_function_log_file(
|
|
122
|
+
&normalized_output_dir,
|
|
123
|
+
&module_path,
|
|
124
|
+
&module.function_table
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
|
|
115
129
|
write_preprocessor_log_file(
|
|
116
130
|
&normalized_output_dir,
|
|
117
131
|
"resolved_statements.log",
|
|
118
132
|
modules_statements.clone()
|
|
119
133
|
);
|
|
120
|
-
|
|
134
|
+
write_variables_log_file(
|
|
135
|
+
&normalized_output_dir,
|
|
136
|
+
"global_variables.log",
|
|
137
|
+
global_store.variables.clone()
|
|
138
|
+
);
|
|
139
|
+
write_function_log_file(
|
|
140
|
+
&normalized_output_dir,
|
|
141
|
+
"global_functions.log",
|
|
142
|
+
global_store.functions.clone()
|
|
143
|
+
);
|
|
121
144
|
|
|
122
145
|
// SECTION Building AST and Audio
|
|
123
146
|
let builder = Builder::new();
|
package/rust/cli/check.rs
CHANGED
|
@@ -5,7 +5,12 @@ use crate::{
|
|
|
5
5
|
store::global::GlobalStore,
|
|
6
6
|
utils::path::{ find_entry_file, normalize_path },
|
|
7
7
|
},
|
|
8
|
-
utils::{
|
|
8
|
+
utils::{
|
|
9
|
+
collect_errors_recursively,
|
|
10
|
+
logger::{ LogLevel, Logger },
|
|
11
|
+
spinner::with_spinner,
|
|
12
|
+
watcher::watch_directory,
|
|
13
|
+
},
|
|
9
14
|
};
|
|
10
15
|
use std::{ thread, time::Duration };
|
|
11
16
|
|
|
@@ -104,7 +109,26 @@ fn begin_check(entry: String, output: String) {
|
|
|
104
109
|
// NOTE: We don't use modules in the check command, but we still need to load them
|
|
105
110
|
let modules = module_loader.load_all_modules(&mut global_store);
|
|
106
111
|
|
|
107
|
-
//
|
|
112
|
+
// Debugging: Log loaded modules and errors
|
|
113
|
+
let logger = Logger::new();
|
|
114
|
+
logger.log_message(LogLevel::Info, "Loaded modules:");
|
|
115
|
+
for (module_name, _) in &modules.0 {
|
|
116
|
+
logger.log_message(LogLevel::Info, &format!("- {}", module_name));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let mut all_errors = Vec::new();
|
|
120
|
+
for (_, statements) in &modules.1 {
|
|
121
|
+
all_errors.extend(collect_errors_recursively(statements));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if !all_errors.is_empty() {
|
|
125
|
+
logger.log_message(LogLevel::Error, "Errors detected during check:");
|
|
126
|
+
for error in all_errors {
|
|
127
|
+
logger.log_message(LogLevel::Error, &format!("- {}", error.message));
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
logger.log_message(LogLevel::Success, "No errors detected.");
|
|
131
|
+
}
|
|
108
132
|
|
|
109
133
|
let success_message = format!(
|
|
110
134
|
"Check completed successfully in {:.2?}. Output files written to: '{}'",
|
|
@@ -112,6 +136,5 @@ fn begin_check(entry: String, output: String) {
|
|
|
112
136
|
normalized_output_dir
|
|
113
137
|
);
|
|
114
138
|
|
|
115
|
-
let logger = Logger::new();
|
|
116
139
|
logger.log_message(LogLevel::Success, &success_message);
|
|
117
140
|
}
|
package/rust/cli/play.rs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
use std::{ collections::HashMap, fs::File, io::BufReader, path::Path };
|
|
2
2
|
use hound::{ SampleFormat, WavSpec, WavWriter };
|
|
3
3
|
use rodio::{ Decoder, Source };
|
|
4
|
-
|
|
5
4
|
use crate::core::{
|
|
5
|
+
shared::value::Value,
|
|
6
6
|
store::variable::VariableTable,
|
|
7
|
-
utils::path::
|
|
7
|
+
utils::path::normalize_path,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
const SAMPLE_RATE: u32 = 44100;
|
|
@@ -13,7 +13,6 @@ const CHANNELS: u16 = 2;
|
|
|
13
13
|
#[derive(Debug, Clone, PartialEq)]
|
|
14
14
|
pub struct AudioEngine {
|
|
15
15
|
pub volume: f32,
|
|
16
|
-
pub variables: VariableTable,
|
|
17
16
|
pub buffer: Vec<i16>,
|
|
18
17
|
pub module_name: String,
|
|
19
18
|
}
|
|
@@ -23,7 +22,6 @@ impl AudioEngine {
|
|
|
23
22
|
AudioEngine {
|
|
24
23
|
volume: 1.0,
|
|
25
24
|
buffer: vec![],
|
|
26
|
-
variables: VariableTable::new(),
|
|
27
25
|
module_name,
|
|
28
26
|
}
|
|
29
27
|
}
|
|
@@ -56,12 +54,10 @@ impl AudioEngine {
|
|
|
56
54
|
|
|
57
55
|
if self.buffer.iter().all(|&s| s == 0) {
|
|
58
56
|
self.buffer = other.buffer;
|
|
59
|
-
self.variables.variables.extend(other.variables.variables);
|
|
60
57
|
return;
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
self.mix(&other);
|
|
64
|
-
self.variables.variables.extend(other.variables.variables);
|
|
65
61
|
}
|
|
66
62
|
|
|
67
63
|
pub fn set_duration(&mut self, duration_secs: f32) {
|
|
@@ -72,10 +68,6 @@ impl AudioEngine {
|
|
|
72
68
|
}
|
|
73
69
|
}
|
|
74
70
|
|
|
75
|
-
pub fn set_variables(&mut self, variables: VariableTable) {
|
|
76
|
-
self.variables = variables;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
71
|
pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
|
|
80
72
|
if self.buffer.len() % (CHANNELS as usize) != 0 {
|
|
81
73
|
self.buffer.push(0);
|
|
@@ -167,75 +159,249 @@ impl AudioEngine {
|
|
|
167
159
|
filepath: &str,
|
|
168
160
|
time_secs: f32,
|
|
169
161
|
dur_sec: f32,
|
|
170
|
-
effects: Option<HashMap<String,
|
|
162
|
+
effects: Option<HashMap<String, Value>>,
|
|
163
|
+
variable_table: &VariableTable
|
|
171
164
|
) {
|
|
172
165
|
if filepath.is_empty() {
|
|
173
166
|
eprintln!("❌ Empty file path provided for audio sample.");
|
|
174
167
|
return;
|
|
175
168
|
}
|
|
176
169
|
|
|
170
|
+
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
171
|
+
let module_root = Path::new(&self.module_name);
|
|
177
172
|
let mut resolved_path = String::new();
|
|
178
173
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
174
|
+
// Get the variable path from the variable table
|
|
175
|
+
let mut var_path = filepath.to_string();
|
|
176
|
+
if let Some(Value::String(variable_path)) = variable_table.variables.get(filepath) {
|
|
177
|
+
var_path = variable_path.clone();
|
|
178
|
+
} else if let Some(Value::Sample(sample_path)) = variable_table.variables.get(filepath) {
|
|
179
|
+
var_path = sample_path.clone();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// If it's a namespace
|
|
183
|
+
if var_path.contains(".") {
|
|
184
|
+
let parts: Vec<&str> = var_path.trim_start_matches('.').split('.').collect();
|
|
185
|
+
if parts.len() == 2 {
|
|
186
|
+
let bank_name = parts[0];
|
|
187
|
+
let entity_name = parts[1];
|
|
188
|
+
|
|
189
|
+
// Verifies if the bank is declared
|
|
190
|
+
if !variable_table.variables.contains_key(bank_name) {
|
|
191
|
+
eprintln!(
|
|
192
|
+
"❌ Bank '{}' not declared. Please declare it first using : 'bank {}'",
|
|
193
|
+
bank_name,
|
|
194
|
+
bank_name
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
186
198
|
|
|
187
|
-
if object_type.contains("bank") {
|
|
188
199
|
resolved_path = root
|
|
189
200
|
.join(".deva")
|
|
190
201
|
.join("bank")
|
|
191
|
-
.join(
|
|
192
|
-
.join(format!("{}.wav",
|
|
193
|
-
.
|
|
194
|
-
.unwrap_or("")
|
|
202
|
+
.join(bank_name)
|
|
203
|
+
.join(format!("{}.wav", entity_name))
|
|
204
|
+
.to_string_lossy()
|
|
195
205
|
.to_string();
|
|
196
206
|
} else {
|
|
197
|
-
eprintln!("❌
|
|
207
|
+
eprintln!("❌ Invalid namespace format: {}", var_path);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
} else if var_path.starts_with("devalang://") {
|
|
211
|
+
let path_after_protocol = var_path.replace("devalang://", "");
|
|
212
|
+
let parts: Vec<&str> = path_after_protocol.split('/').collect();
|
|
213
|
+
|
|
214
|
+
if parts.len() < 3 {
|
|
215
|
+
eprintln!(
|
|
216
|
+
"❌ Invalid devalang:// path format. Expected devalang://<type>/<bank>/<entity>"
|
|
217
|
+
);
|
|
198
218
|
return;
|
|
199
219
|
}
|
|
220
|
+
|
|
221
|
+
let obj_type = parts[0];
|
|
222
|
+
let bank_name = parts[1];
|
|
223
|
+
let entity_name = parts[2];
|
|
224
|
+
|
|
225
|
+
resolved_path = root
|
|
226
|
+
.join(".deva")
|
|
227
|
+
.join(obj_type)
|
|
228
|
+
.join(bank_name)
|
|
229
|
+
.join(format!("{}.wav", entity_name))
|
|
230
|
+
.to_string_lossy()
|
|
231
|
+
.to_string();
|
|
232
|
+
} else {
|
|
233
|
+
// Else, resolve as a relative path
|
|
234
|
+
let entry_dir = module_root.parent().unwrap_or(root);
|
|
235
|
+
let absolute_path = root.join(entry_dir).join(&var_path);
|
|
236
|
+
|
|
237
|
+
resolved_path = normalize_path(absolute_path.to_string_lossy().to_string());
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Verify if the file exists
|
|
241
|
+
if !Path::new(&resolved_path).exists() {
|
|
242
|
+
eprintln!("❌ Audio file not found at: {}", resolved_path);
|
|
243
|
+
return;
|
|
200
244
|
}
|
|
201
245
|
|
|
202
|
-
let file =
|
|
203
|
-
|
|
246
|
+
let file = match File::open(&resolved_path) {
|
|
247
|
+
Ok(f) => BufReader::new(f),
|
|
248
|
+
Err(e) => {
|
|
249
|
+
eprintln!("❌ Failed to open audio file {}: {}", resolved_path, e);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
let decoder = match Decoder::new(file) {
|
|
255
|
+
Ok(d) => d,
|
|
256
|
+
Err(e) => {
|
|
257
|
+
eprintln!("❌ Failed to decode audio file {}: {}", resolved_path, e);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
204
261
|
|
|
205
|
-
// Mono or stereo reading possible here, we will duplicate in L/R
|
|
206
262
|
let max_mono_samples = (dur_sec * (SAMPLE_RATE as f32)) as usize;
|
|
207
263
|
let samples: Vec<i16> = decoder.convert_samples().take(max_mono_samples).collect();
|
|
208
264
|
|
|
209
265
|
if samples.is_empty() {
|
|
210
|
-
eprintln!("No samples
|
|
266
|
+
eprintln!("❌ No samples read from {}", resolved_path);
|
|
211
267
|
return;
|
|
212
268
|
}
|
|
213
269
|
|
|
214
|
-
//
|
|
270
|
+
// Calculate buffer offset and size
|
|
215
271
|
let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
|
|
216
272
|
let required_len = offset + samples.len() * (CHANNELS as usize);
|
|
217
|
-
|
|
218
|
-
required_len
|
|
219
|
-
}
|
|
220
|
-
required_len
|
|
221
|
-
};
|
|
273
|
+
if self.buffer.len() < required_len {
|
|
274
|
+
self.buffer.resize(required_len, 0);
|
|
275
|
+
}
|
|
222
276
|
|
|
223
|
-
|
|
224
|
-
|
|
277
|
+
// Apply effects and mix
|
|
278
|
+
if let Some(effects_map) = effects {
|
|
279
|
+
self.pad_samples(&samples, time_secs, Some(effects_map));
|
|
280
|
+
} else {
|
|
281
|
+
self.pad_samples(&samples, time_secs, None);
|
|
282
|
+
}
|
|
225
283
|
}
|
|
226
284
|
|
|
227
|
-
fn pad_samples(
|
|
285
|
+
fn pad_samples(
|
|
286
|
+
&mut self,
|
|
287
|
+
samples: &[i16],
|
|
288
|
+
time_secs: f32,
|
|
289
|
+
effects_map: Option<HashMap<String, Value>>
|
|
290
|
+
) {
|
|
228
291
|
let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
|
|
292
|
+
let total_samples = samples.len();
|
|
293
|
+
|
|
294
|
+
// Default values
|
|
295
|
+
let mut gain = 1.0;
|
|
296
|
+
let mut pan = 0.0;
|
|
297
|
+
let mut fade_in = 0.0;
|
|
298
|
+
let mut fade_out = 0.0;
|
|
299
|
+
let mut pitch = 1.0;
|
|
300
|
+
let mut drive = 0.0;
|
|
301
|
+
let mut reverb = 0.0;
|
|
302
|
+
let mut delay = 0.0; // delay time in seconds
|
|
303
|
+
let delay_feedback = 0.35; // default feedback
|
|
304
|
+
|
|
305
|
+
if let Some(map) = &effects_map {
|
|
306
|
+
for (key, val) in map {
|
|
307
|
+
match (key.as_str(), val) {
|
|
308
|
+
("gain", Value::Number(v)) => {
|
|
309
|
+
gain = *v;
|
|
310
|
+
}
|
|
311
|
+
("pan", Value::Number(v)) => {
|
|
312
|
+
pan = *v;
|
|
313
|
+
}
|
|
314
|
+
("fadeIn", Value::Number(v)) => {
|
|
315
|
+
fade_in = *v;
|
|
316
|
+
}
|
|
317
|
+
("fadeOut", Value::Number(v)) => {
|
|
318
|
+
fade_out = *v;
|
|
319
|
+
}
|
|
320
|
+
("pitch", Value::Number(v)) => {
|
|
321
|
+
pitch = *v;
|
|
322
|
+
}
|
|
323
|
+
("drive", Value::Number(v)) => {
|
|
324
|
+
drive = *v;
|
|
325
|
+
}
|
|
326
|
+
("reverb", Value::Number(v)) => {
|
|
327
|
+
reverb = *v;
|
|
328
|
+
}
|
|
329
|
+
("delay", Value::Number(v)) => {
|
|
330
|
+
delay = *v;
|
|
331
|
+
}
|
|
332
|
+
_ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
|
|
338
|
+
let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
|
|
339
|
+
|
|
340
|
+
let delay_samples = if delay > 0.0 { (delay * (SAMPLE_RATE as f32)) as usize } else { 0 };
|
|
341
|
+
let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
|
|
342
|
+
|
|
343
|
+
for i in 0..total_samples {
|
|
344
|
+
// PITCH FIRST
|
|
345
|
+
let pitch_index = if pitch != 1.0 { ((i as f32) / pitch) as usize } else { i };
|
|
346
|
+
|
|
347
|
+
let mut adjusted = if pitch_index < total_samples {
|
|
348
|
+
samples[pitch_index] as f32
|
|
349
|
+
} else {
|
|
350
|
+
0.0
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// GAIN
|
|
354
|
+
adjusted *= gain;
|
|
355
|
+
|
|
356
|
+
// FADE IN/OUT
|
|
357
|
+
if fade_in_samples > 0 && i < fade_in_samples {
|
|
358
|
+
adjusted *= (i as f32) / (fade_in_samples as f32);
|
|
359
|
+
}
|
|
360
|
+
if fade_out_samples > 0 && i >= total_samples.saturating_sub(fade_out_samples) {
|
|
361
|
+
adjusted *= ((total_samples - i) as f32) / (fade_out_samples as f32);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// DRIVE (soft)
|
|
365
|
+
if drive > 0.0 {
|
|
366
|
+
let normalized = adjusted / (i16::MAX as f32);
|
|
367
|
+
let pre_gain = (10f32).powf(drive / 20.0); // dB mapping
|
|
368
|
+
let driven = (normalized * pre_gain).tanh();
|
|
369
|
+
adjusted = driven * (i16::MAX as f32);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// DELAY
|
|
373
|
+
if delay_samples > 0 && i >= delay_samples {
|
|
374
|
+
let echo = delay_buffer[i - delay_samples] * delay_feedback;
|
|
375
|
+
adjusted += echo;
|
|
376
|
+
}
|
|
377
|
+
if delay_samples > 0 {
|
|
378
|
+
delay_buffer[i] = adjusted;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// REVERB
|
|
382
|
+
if reverb > 0.0 {
|
|
383
|
+
let reverb_delay = (0.03 * (SAMPLE_RATE as f32)) as usize;
|
|
384
|
+
if i >= reverb_delay {
|
|
385
|
+
adjusted += (self.buffer[offset + i - reverb_delay] as f32) * reverb;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// CLAMP
|
|
390
|
+
let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
|
391
|
+
|
|
392
|
+
// PAN
|
|
393
|
+
let left_gain = 1.0 - pan.max(0.0); // Pan > 0 => reduce left
|
|
394
|
+
let right_gain = 1.0 + pan.min(0.0); // Pan < 0 => reduce right
|
|
229
395
|
|
|
230
|
-
|
|
231
|
-
let
|
|
396
|
+
let left = ((adjusted_sample as f32) * left_gain) as i16;
|
|
397
|
+
let right = ((adjusted_sample as f32) * right_gain) as i16;
|
|
232
398
|
|
|
233
399
|
let left_pos = offset + i * 2;
|
|
234
400
|
let right_pos = left_pos + 1;
|
|
235
401
|
|
|
236
402
|
if right_pos < self.buffer.len() {
|
|
237
|
-
self.buffer[left_pos] = self.buffer[left_pos].saturating_add(
|
|
238
|
-
self.buffer[right_pos] = self.buffer[right_pos].saturating_add(
|
|
403
|
+
self.buffer[left_pos] = self.buffer[left_pos].saturating_add(left);
|
|
404
|
+
self.buffer[right_pos] = self.buffer[right_pos].saturating_add(right);
|
|
239
405
|
}
|
|
240
406
|
}
|
|
241
407
|
}
|