@devaloop/devalang 0.0.1-alpha.17 → 0.0.1-alpha.18
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 +5 -1
- package/Cargo.toml +4 -4
- package/README.md +9 -6
- package/docs/CHANGELOG.md +46 -1
- package/docs/TODO.md +1 -1
- package/examples/index.deva +9 -6
- package/examples/pattern.deva +5 -5
- package/out-tsc/pkg/devalang_core.d.ts +1 -1
- package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +7 -7
- package/package.json +1 -1
- package/project-version.json +3 -3
- package/rust/cli/build/commands.rs +10 -0
- package/rust/cli/install/addon.rs +84 -38
- package/rust/cli/telemetry/event_creator.rs +17 -17
- package/rust/core/audio/engine/helpers.rs +21 -9
- package/rust/core/audio/engine/sample.rs +68 -7
- package/rust/core/audio/engine/synth.rs +19 -4
- package/rust/core/audio/evaluator.rs +64 -26
- package/rust/core/audio/interpreter/arrow_call.rs +21 -16
- package/rust/core/audio/interpreter/call.rs +156 -1
- package/rust/core/audio/interpreter/spawn.rs +145 -1
- package/rust/core/audio/special/math.rs +22 -2
- package/rust/core/lexer/driver.rs +61 -0
- package/rust/core/lexer/handler/identifier.rs +3 -2
- package/rust/core/lexer/mod.rs +1 -62
- package/rust/core/lexer/token.rs +1 -0
- package/rust/core/parser/driver.rs +12 -9
- package/rust/core/parser/handler/dot.rs +3 -2
- package/rust/core/parser/handler/loop_.rs +2 -2
- package/rust/core/parser/handler/mod.rs +1 -0
- package/rust/core/parser/handler/pattern.rs +74 -0
- package/rust/core/preprocessor/loader.rs +87 -127
- package/rust/core/preprocessor/processor.rs +7 -7
- package/rust/core/preprocessor/resolver/call.rs +28 -0
- package/rust/core/preprocessor/resolver/driver.rs +15 -13
- package/rust/core/preprocessor/resolver/mod.rs +1 -0
- package/rust/core/preprocessor/resolver/pattern.rs +75 -0
- package/rust/core/preprocessor/resolver/spawn.rs +27 -0
- package/rust/core/store/variable.rs +15 -1
- package/rust/main.rs +4 -1
- package/rust/types/Cargo.toml +3 -0
- package/rust/types/src/ast.rs +4 -0
- package/rust/utils/Cargo.toml +4 -1
- package/rust/web/api.rs +2 -2
package/.devalang
CHANGED
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.18"
|
|
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"
|
|
@@ -30,7 +30,7 @@ default = ["cli"]
|
|
|
30
30
|
cli = ["crossterm", "indicatif", "inquire", "zip", "reqwest", "flate2", "tokio"]
|
|
31
31
|
|
|
32
32
|
[dependencies]
|
|
33
|
-
devalang_types = { path = "rust/types" }
|
|
33
|
+
devalang_types = { path = "rust/types", version = "0.0.1" }
|
|
34
34
|
clap = { version = "4.5", features = ["derive"] }
|
|
35
35
|
serde = { version = "1.0", features = ["derive"] }
|
|
36
36
|
serde_json = "1.0"
|
|
@@ -68,9 +68,9 @@ getrandom = { version = "0.3", features = ["wasm_js"] }
|
|
|
68
68
|
uuid = { version = "1.18.0", features = ["v4", "js", "rng-getrandom"] }
|
|
69
69
|
# Keep a lightweight linkage to the utils crate for wasm builds without
|
|
70
70
|
# enabling native CLI features.
|
|
71
|
-
devalang_utils = { path = "rust/utils", default-features = false }
|
|
71
|
+
devalang_utils = { path = "rust/utils", default-features = false, version = "0.0.1" }
|
|
72
72
|
|
|
73
73
|
# devalang_utils with CLI and wasmtime are only needed for native targets.
|
|
74
74
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
|
75
|
-
devalang_utils = { path = "rust/utils", features = ["cli"] }
|
|
75
|
+
devalang_utils = { path = "rust/utils", features = ["cli"], version = "0.0.1" }
|
|
76
76
|
wasmtime = "19"
|
package/README.md
CHANGED
|
@@ -84,13 +84,16 @@ Create a new Devalang file `src/index.deva` in the project directory:
|
|
|
84
84
|
# BPM definition
|
|
85
85
|
bpm 125
|
|
86
86
|
|
|
87
|
-
# Bank picking (make sure you installed it)
|
|
87
|
+
# Bank picking (make sure you've installed it)
|
|
88
88
|
bank devaloop.808 as my808Bank
|
|
89
89
|
|
|
90
|
+
# Pattern literal without options
|
|
91
|
+
pattern kickPattern with my808Bank.kick = "x--- x--- x--- x---"
|
|
92
|
+
|
|
90
93
|
group myGroup:
|
|
91
94
|
# Rhythmic (each beat playing a kick)
|
|
92
|
-
on beat:
|
|
93
|
-
|
|
95
|
+
# on beat:
|
|
96
|
+
# .my808Bank.kick 1/4
|
|
94
97
|
|
|
95
98
|
# Synth definition with ADSR
|
|
96
99
|
let myLead = synth sine {
|
|
@@ -133,12 +136,12 @@ group myGroup:
|
|
|
133
136
|
})
|
|
134
137
|
|
|
135
138
|
# Notes with params
|
|
136
|
-
myLead -> note(E4, { duration: 400 })
|
|
137
139
|
myLead -> note(G4, { duration: 600, glide: true, target_freq: 659.25 })
|
|
138
140
|
myLead -> note(B3, { duration: 400, slide: true, target_amp: 0.3 })
|
|
139
141
|
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
+
# Spawning the group & the pattern to play them in parallel
|
|
143
|
+
spawn myGroup
|
|
144
|
+
spawn kickPattern
|
|
142
145
|
```
|
|
143
146
|
|
|
144
147
|
### And the best part ? You can play it directly from the command line:
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,52 @@
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
-
## Version 0.0.1-alpha.
|
|
7
|
+
## Version 0.0.1-alpha.18 (2025-09-02)
|
|
8
|
+
|
|
9
|
+
### ✨ Language Features
|
|
10
|
+
|
|
11
|
+
- New `pattern` statement to define rhythmic patterns with an optional target entity.
|
|
12
|
+
- Example: `pattern kickPattern with my808.kick = "x--- x--- x--- x---"`
|
|
13
|
+
- Patterns can be invoked with `call` or `spawn` just like functions or groups.
|
|
14
|
+
|
|
15
|
+
### 🧠 Core Engine
|
|
16
|
+
|
|
17
|
+
- Pattern playback: schedules steps across one bar (4 beats), computing per-step duration and triggering the target on non-rest characters.
|
|
18
|
+
- ADSR envelope: improved interpolation at segment boundaries to avoid clicks and handle 0/1-sample edge cases.
|
|
19
|
+
- Sample engine: robust stereo-to-mono mixdown with RMS-preserving scaling; applies a tiny automatic fade (~1 ms) when samples start/end abruptly to reduce clicks.
|
|
20
|
+
|
|
21
|
+
### 🧩 Parser & Lexer
|
|
22
|
+
|
|
23
|
+
- Added `Pattern` token and parser handler; supports `pattern <name> [with <bank.trigger>] = "..."`.
|
|
24
|
+
- Introduced a dedicated lexer driver (`rust/core/lexer/driver.rs`) to separate file resolution from tokenization.
|
|
25
|
+
- Map/array parsing now logs structured errors via the shared logger instead of printing to stdout.
|
|
26
|
+
|
|
27
|
+
### 🔁 Preprocessor & Resolution
|
|
28
|
+
|
|
29
|
+
- Pattern resolver stores definitions in the variable table, enabling later `call`/`spawn` usage.
|
|
30
|
+
- Variable lookup now walks parent scopes, fixing missed resolutions for outer-scope identifiers.
|
|
31
|
+
|
|
32
|
+
### 🛠️ CLI & Telemetry
|
|
33
|
+
|
|
34
|
+
- `build`: non-watch mode now executes and surfaces errors correctly.
|
|
35
|
+
- `install`: requires authentication and reports API/JSON errors with clear messages.
|
|
36
|
+
- Telemetry: generates a stable UUID when missing; consistently records CLI version, OS, and args.
|
|
37
|
+
- Ensures the `.deva` directory exists at startup.
|
|
38
|
+
|
|
39
|
+
### 📚 Examples
|
|
40
|
+
|
|
41
|
+
- Added `examples/pattern.deva`; updated `examples/index.deva` to demonstrate `pattern` and `spawn`.
|
|
42
|
+
|
|
43
|
+
### 📦 Packaging
|
|
44
|
+
|
|
45
|
+
- Added crate metadata (description, license, authors) and pinned internal versions for `devalang_types` and `devalang_utils`.
|
|
46
|
+
|
|
47
|
+
### 🐛 Fixes & Stability
|
|
48
|
+
|
|
49
|
+
- Safer `$math` parsing with diagnostics for malformed calls and argument evaluation failures.
|
|
50
|
+
- Minor parser fixes (loop body collection, clearer error messages) and logging cleanups across modules.
|
|
51
|
+
|
|
52
|
+
## Version 0.0.1-alpha.17 (2025-08-30)
|
|
8
53
|
|
|
9
54
|
### ✨ Addons
|
|
10
55
|
|
package/docs/TODO.md
CHANGED
package/examples/index.deva
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
# BPM definition
|
|
4
4
|
bpm 125
|
|
5
5
|
|
|
6
|
-
# Bank picking (make sure you
|
|
6
|
+
# Bank picking (make sure you've installed it)
|
|
7
7
|
bank devaloop.808 as my808Bank
|
|
8
8
|
|
|
9
|
+
# Pattern literal without options
|
|
10
|
+
pattern kickPattern with my808Bank.kick = "x--- x--- x--- x---"
|
|
11
|
+
|
|
9
12
|
group myGroup:
|
|
10
13
|
# Rhythmic (each beat playing a kick)
|
|
11
|
-
on beat:
|
|
12
|
-
|
|
14
|
+
# on beat:
|
|
15
|
+
# .my808Bank.kick 1/4
|
|
13
16
|
|
|
14
17
|
# Synth definition with ADSR
|
|
15
18
|
let myLead = synth sine {
|
|
@@ -52,9 +55,9 @@ group myGroup:
|
|
|
52
55
|
})
|
|
53
56
|
|
|
54
57
|
# Notes with params
|
|
55
|
-
myLead -> note(E4, { duration: 400 })
|
|
56
58
|
myLead -> note(G4, { duration: 600, glide: true, target_freq: 659.25 })
|
|
57
59
|
myLead -> note(B3, { duration: 400, slide: true, target_amp: 0.3 })
|
|
58
60
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
+
# Spawning the group & the pattern to play them in parallel
|
|
62
|
+
spawn myGroup
|
|
63
|
+
spawn kickPattern
|
package/examples/pattern.deva
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# This file demonstrates the usage of patterns in Devaloop
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
bank devaloop.808 as my808
|
|
4
4
|
|
|
5
|
-
#
|
|
6
|
-
|
|
5
|
+
# Pattern literal without options
|
|
6
|
+
pattern kickPattern with my808.kick = "x--- x--- x--- x---"
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
call kickPattern
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
|
+
export function register_playhead_callback(cb: Function): void;
|
|
3
4
|
export function parse(entry_path: string, source: string): any;
|
|
4
5
|
export function debug_render(user_code: string): any;
|
|
5
6
|
export function render_audio(user_code: string): Float32Array;
|
|
6
|
-
export function register_playhead_callback(cb: Function): void;
|
|
7
7
|
export function unregister_playhead_callback(): void;
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
export const memory: WebAssembly.Memory;
|
|
4
|
-
export const parse: (a: number, b: number, c: number, d: number) => [number, number, number];
|
|
5
4
|
export const debug_render: (a: number, b: number) => [number, number, number];
|
|
6
|
-
export const
|
|
5
|
+
export const parse: (a: number, b: number, c: number, d: number) => [number, number, number];
|
|
7
6
|
export const register_playhead_callback: (a: any) => void;
|
|
7
|
+
export const render_audio: (a: number, b: number) => [number, number, number];
|
|
8
8
|
export const unregister_playhead_callback: () => void;
|
|
9
|
-
export const rust_lzma_wasm_shim_malloc: (a: number) => number;
|
|
10
9
|
export const rust_lzma_wasm_shim_calloc: (a: number, b: number) => number;
|
|
11
10
|
export const rust_lzma_wasm_shim_free: (a: number) => void;
|
|
11
|
+
export const rust_lzma_wasm_shim_malloc: (a: number) => number;
|
|
12
|
+
export const rust_lzma_wasm_shim_memchr: (a: number, b: number, c: number) => number;
|
|
12
13
|
export const rust_lzma_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
|
|
13
14
|
export const rust_lzma_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
|
|
14
15
|
export const rust_lzma_wasm_shim_memmove: (a: number, b: number, c: number) => number;
|
|
15
16
|
export const rust_lzma_wasm_shim_memset: (a: number, b: number, c: number) => number;
|
|
16
17
|
export const rust_lzma_wasm_shim_strlen: (a: number) => number;
|
|
17
|
-
export const rust_lzma_wasm_shim_memchr: (a: number, b: number, c: number) => number;
|
|
18
|
-
export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
|
|
19
|
-
export const rust_zstd_wasm_shim_malloc: (a: number) => number;
|
|
20
|
-
export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
|
|
21
18
|
export const rust_zstd_wasm_shim_calloc: (a: number, b: number) => number;
|
|
22
19
|
export const rust_zstd_wasm_shim_free: (a: number) => void;
|
|
20
|
+
export const rust_zstd_wasm_shim_malloc: (a: number) => number;
|
|
21
|
+
export const rust_zstd_wasm_shim_memcmp: (a: number, b: number, c: number) => number;
|
|
23
22
|
export const rust_zstd_wasm_shim_memcpy: (a: number, b: number, c: number) => number;
|
|
24
23
|
export const rust_zstd_wasm_shim_memmove: (a: number, b: number, c: number) => number;
|
|
25
24
|
export const rust_zstd_wasm_shim_memset: (a: number, b: number, c: number) => number;
|
|
25
|
+
export const rust_zstd_wasm_shim_qsort: (a: number, b: number, c: number, d: number) => void;
|
|
26
26
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
27
27
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
28
28
|
export const __wbindgen_exn_store: (a: number) => void;
|
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.18",
|
|
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
|
@@ -91,6 +91,16 @@ pub fn handle_build_command(
|
|
|
91
91
|
);
|
|
92
92
|
})
|
|
93
93
|
.unwrap();
|
|
94
|
+
} else {
|
|
95
|
+
let res = crate::cli::build::process::process_build(
|
|
96
|
+
entry_file,
|
|
97
|
+
fetched_output,
|
|
98
|
+
debug,
|
|
99
|
+
compress,
|
|
100
|
+
);
|
|
101
|
+
if let Err(e) = res {
|
|
102
|
+
return Err(e);
|
|
103
|
+
}
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
Ok(())
|
|
@@ -22,59 +22,105 @@ pub async fn install_addon(
|
|
|
22
22
|
pub async fn ask_api_for_signed_url(addon_type: AddonType, slug: &str) -> Result<String, String> {
|
|
23
23
|
let api_url = get_api_url();
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
use devalang_utils::logger::Logger;
|
|
26
|
+
use devalang_utils::logger::LogLevel;
|
|
27
|
+
|
|
28
|
+
// Require an authenticated user for addon installation: token must be present and non-empty
|
|
29
|
+
let stored_token_opt = get_user_config()
|
|
30
|
+
.and_then(|cfg| {
|
|
31
|
+
let t = cfg.session.clone();
|
|
32
|
+
if t.trim().is_empty() {
|
|
33
|
+
None
|
|
34
|
+
} else {
|
|
35
|
+
Some(t)
|
|
36
|
+
}
|
|
37
|
+
});
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
if stored_token_opt.is_none() {
|
|
40
|
+
let logger = Logger::new();
|
|
41
|
+
let msg = "Authentication required — run `devalang login` to authenticate";
|
|
42
|
+
logger.log_message(LogLevel::Error, msg);
|
|
43
|
+
return Err("Authentication required: run 'devalang login' to authenticate".to_string());
|
|
44
|
+
}
|
|
31
45
|
|
|
32
|
-
let request_url =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
let request_url = if let Some(token) = &stored_token_opt {
|
|
47
|
+
format!(
|
|
48
|
+
"{}/v1/assets/url?type={}&slug={}&token={}",
|
|
49
|
+
api_url,
|
|
50
|
+
match addon_type {
|
|
51
|
+
AddonType::Bank => "bank",
|
|
52
|
+
AddonType::Plugin => "plugin",
|
|
53
|
+
AddonType::Preset => "preset",
|
|
54
|
+
AddonType::Template => "template",
|
|
55
|
+
},
|
|
56
|
+
slug,
|
|
57
|
+
token
|
|
58
|
+
)
|
|
59
|
+
} else {
|
|
60
|
+
format!(
|
|
61
|
+
"{}/v1/assets/url?type={}&slug={}",
|
|
62
|
+
api_url,
|
|
63
|
+
match addon_type {
|
|
64
|
+
AddonType::Bank => "bank",
|
|
65
|
+
AddonType::Plugin => "plugin",
|
|
66
|
+
AddonType::Preset => "preset",
|
|
67
|
+
AddonType::Template => "template",
|
|
68
|
+
},
|
|
69
|
+
slug
|
|
70
|
+
)
|
|
71
|
+
};
|
|
44
72
|
|
|
45
73
|
let mut headers = reqwest::header::HeaderMap::new();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
74
|
+
if let Some(token) = stored_token_opt {
|
|
75
|
+
headers.insert(
|
|
76
|
+
"Authorization",
|
|
77
|
+
format!("Bearer {}", token).parse().unwrap(),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
51
80
|
|
|
52
81
|
let client: reqwest::Client = reqwest::Client::builder()
|
|
53
82
|
.default_headers(headers)
|
|
54
83
|
.build()
|
|
55
84
|
.map_err(|_| "Failed to build HTTP client".to_string())?;
|
|
56
85
|
|
|
57
|
-
let
|
|
86
|
+
let resp = client
|
|
58
87
|
.get(&request_url)
|
|
59
88
|
.send()
|
|
60
89
|
.await
|
|
61
|
-
.map_err(|
|
|
90
|
+
.map_err(|e| format!("Failed to receive response: {}", e))?;
|
|
62
91
|
|
|
63
|
-
let
|
|
64
|
-
|
|
92
|
+
let status = resp.status();
|
|
93
|
+
let body_text = resp
|
|
94
|
+
.text()
|
|
65
95
|
.await
|
|
66
|
-
.map_err(|
|
|
96
|
+
.map_err(|e| format!("Failed to read response body: {}", e))?;
|
|
97
|
+
|
|
98
|
+
// Try to parse JSON; if parsing fails, return body for diagnostics
|
|
99
|
+
let json: serde_json::Value = match serde_json::from_str(&body_text) {
|
|
100
|
+
Ok(v) => v,
|
|
101
|
+
Err(_) => {
|
|
102
|
+
return Err(format!(
|
|
103
|
+
"Invalid JSON response (status {}): {}",
|
|
104
|
+
status, body_text
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
67
108
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.cloned()
|
|
75
|
-
.unwrap_or_default(),
|
|
76
|
-
)
|
|
77
|
-
.map_err(|_| "Failed to parse response body".to_string())?;
|
|
109
|
+
// Extract payload.url safely
|
|
110
|
+
let signed_url_opt = json
|
|
111
|
+
.get("payload")
|
|
112
|
+
.and_then(|p| p.get("url"))
|
|
113
|
+
.and_then(|u| u.as_str())
|
|
114
|
+
.map(|s| s.to_string());
|
|
78
115
|
|
|
79
|
-
|
|
116
|
+
if let Some(signed_url) = signed_url_opt {
|
|
117
|
+
Ok(signed_url)
|
|
118
|
+
} else {
|
|
119
|
+
// Provide detailed diagnostics to help user understand why it's null
|
|
120
|
+
let err_msg = format!(
|
|
121
|
+
"API returned no URL (status {}): {}",
|
|
122
|
+
status, body_text
|
|
123
|
+
);
|
|
124
|
+
Err(err_msg)
|
|
125
|
+
}
|
|
80
126
|
}
|
|
@@ -2,6 +2,7 @@ use crate::config::settings::get_user_config;
|
|
|
2
2
|
use devalang_types::{
|
|
3
3
|
TelemetryErrorLevel as SharedTelemetryErrorLevel, TelemetryEvent as SharedTelemetryEvent,
|
|
4
4
|
};
|
|
5
|
+
use uuid::Uuid;
|
|
5
6
|
|
|
6
7
|
pub type TelemetryEvent = SharedTelemetryEvent;
|
|
7
8
|
pub type TelemetryErrorLevel = SharedTelemetryErrorLevel;
|
|
@@ -57,24 +58,23 @@ impl TelemetryEventCreator {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
pub fn get_base_event(&self) -> TelemetryEvent {
|
|
60
|
-
let
|
|
61
|
+
let uuid = match get_user_config() {
|
|
62
|
+
Some(cfg) if !cfg.telemetry.uuid.is_empty() => cfg.telemetry.uuid.clone(),
|
|
63
|
+
_ => Uuid::new_v4().to_string(),
|
|
64
|
+
};
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
success: true,
|
|
75
|
-
};
|
|
66
|
+
TelemetryEvent {
|
|
67
|
+
uuid,
|
|
68
|
+
cli_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
69
|
+
os: std::env::consts::OS.to_string(),
|
|
70
|
+
command: std::env::args().collect::<Vec<_>>(),
|
|
71
|
+
project_info: None,
|
|
72
|
+
error_level: TelemetryErrorLevel::None,
|
|
73
|
+
error_message: None,
|
|
74
|
+
exit_code: None,
|
|
75
|
+
timestamp: chrono::Utc::now().to_string(),
|
|
76
|
+
duration: 0,
|
|
77
|
+
success: true,
|
|
76
78
|
}
|
|
77
|
-
|
|
78
|
-
default_event
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -105,17 +105,29 @@ pub fn adsr_envelope_value(
|
|
|
105
105
|
release_samples: usize,
|
|
106
106
|
sustain_level: f32,
|
|
107
107
|
) -> f32 {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
let attack_start = 0usize;
|
|
109
|
+
let decay_start = attack_samples;
|
|
110
|
+
let sustain_start = attack_samples + decay_samples;
|
|
111
|
+
let release_start = attack_samples + decay_samples + sustain_samples;
|
|
112
|
+
|
|
113
|
+
if i < attack_start + attack_samples && attack_samples > 0 {
|
|
114
|
+
let k = i - attack_start;
|
|
115
|
+
let denom = if attack_samples > 1 { (attack_samples - 1) as f32 } else { 1.0 };
|
|
116
|
+
(k as f32) / denom
|
|
117
|
+
} else if i < decay_start + decay_samples && decay_samples > 0 {
|
|
118
|
+
let k = i - decay_start;
|
|
119
|
+
let denom = if decay_samples > 1 { (decay_samples - 1) as f32 } else { 1.0 };
|
|
120
|
+
let ratio = (k as f32) / denom;
|
|
121
|
+
1.0 - (1.0 - sustain_level) * ratio
|
|
122
|
+
} else if i < sustain_start + sustain_samples {
|
|
113
123
|
sustain_level
|
|
114
124
|
} else if release_samples > 0 {
|
|
115
|
-
sustain_level
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
// release: interpolate from sustain_level down to 0 inclusive
|
|
126
|
+
let k = i.saturating_sub(release_start);
|
|
127
|
+
let denom = if release_samples > 1 { (release_samples - 1) as f32 } else { 1.0 };
|
|
128
|
+
let ratio = (k as f32) / denom;
|
|
129
|
+
let val = sustain_level * (1.0 - ratio);
|
|
130
|
+
if val < 0.0 { 0.0 } else { val }
|
|
119
131
|
} else {
|
|
120
132
|
0.0
|
|
121
133
|
}
|
|
@@ -153,8 +153,30 @@ impl super::synth::AudioEngine {
|
|
|
153
153
|
}
|
|
154
154
|
};
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
let
|
|
156
|
+
// Read frames from decoder and convert to mono if needed.
|
|
157
|
+
let max_frames = (dur_sec * (SAMPLE_RATE as f32)) as usize;
|
|
158
|
+
let dec_channels = decoder.channels() as usize;
|
|
159
|
+
let max_raw_samples = max_frames.saturating_mul(dec_channels.max(1));
|
|
160
|
+
let raw_samples: Vec<i16> = decoder.convert_samples().take(max_raw_samples).collect();
|
|
161
|
+
|
|
162
|
+
// Convert interleaved channels to mono by averaging channels per frame.
|
|
163
|
+
// Apply a small RMS-preserving scale so mono level is similar to mixed stereo.
|
|
164
|
+
let actual_frames = if dec_channels > 0 { raw_samples.len() / dec_channels } else { 0 };
|
|
165
|
+
let mut samples: Vec<i16> = Vec::with_capacity(actual_frames);
|
|
166
|
+
let rms_scale = (dec_channels as f32).sqrt();
|
|
167
|
+
for frame in 0..actual_frames {
|
|
168
|
+
let mut sum: i32 = 0;
|
|
169
|
+
for ch in 0..dec_channels {
|
|
170
|
+
sum += raw_samples[frame * dec_channels + ch] as i32;
|
|
171
|
+
}
|
|
172
|
+
if dec_channels > 0 {
|
|
173
|
+
let avg = (sum / (dec_channels as i32)) as f32;
|
|
174
|
+
let scaled = (avg * rms_scale).clamp(i16::MIN as f32, i16::MAX as f32) as i16;
|
|
175
|
+
samples.push(scaled);
|
|
176
|
+
} else {
|
|
177
|
+
samples.push(0);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
158
180
|
|
|
159
181
|
if samples.is_empty() {
|
|
160
182
|
eprintln!("❌ No samples read from {}", resolved_path);
|
|
@@ -228,6 +250,37 @@ impl super::synth::AudioEngine {
|
|
|
228
250
|
let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
|
|
229
251
|
let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
|
|
230
252
|
|
|
253
|
+
// If no fade specified, apply a tiny default fade (2 ms) when sample boundaries are non-zero
|
|
254
|
+
let default_boundary_fade_ms = 1.0_f32; // 1 ms
|
|
255
|
+
let default_fade_samples = (default_boundary_fade_ms * (SAMPLE_RATE as f32)) as usize;
|
|
256
|
+
let mut effective_fade_in = fade_in_samples;
|
|
257
|
+
let mut effective_fade_out = fade_out_samples;
|
|
258
|
+
if effective_fade_in == 0 {
|
|
259
|
+
if let Some(&first) = samples.first() {
|
|
260
|
+
if first.abs() > 64 { // increased threshold to detect only strong abrupt starts
|
|
261
|
+
effective_fade_in = default_fade_samples.max(1);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if effective_fade_out == 0 {
|
|
266
|
+
if let Some(&last) = samples.last() {
|
|
267
|
+
if last.abs() > 64 { // increased threshold to detect only strong abrupt ends
|
|
268
|
+
effective_fade_out = default_fade_samples.max(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Ensure fades do not exceed half the sample length to avoid silencing short samples
|
|
274
|
+
if total_samples > 0 {
|
|
275
|
+
let cap = total_samples / 2;
|
|
276
|
+
if effective_fade_in > cap {
|
|
277
|
+
effective_fade_in = cap.max(1);
|
|
278
|
+
}
|
|
279
|
+
if effective_fade_out > cap {
|
|
280
|
+
effective_fade_out = cap.max(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
231
284
|
let delay_samples = if delay > 0.0 {
|
|
232
285
|
(delay * (SAMPLE_RATE as f32)) as usize
|
|
233
286
|
} else {
|
|
@@ -235,7 +288,7 @@ impl super::synth::AudioEngine {
|
|
|
235
288
|
};
|
|
236
289
|
let mut delay_buffer: Vec<f32> = vec![0.0; total_samples + delay_samples];
|
|
237
290
|
|
|
238
|
-
|
|
291
|
+
for i in 0..total_samples {
|
|
239
292
|
let pitch_index = if pitch != 1.0 {
|
|
240
293
|
((i as f32) / pitch) as usize
|
|
241
294
|
} else {
|
|
@@ -250,11 +303,19 @@ impl super::synth::AudioEngine {
|
|
|
250
303
|
|
|
251
304
|
adjusted *= gain;
|
|
252
305
|
|
|
253
|
-
if
|
|
254
|
-
|
|
306
|
+
if effective_fade_in > 0 && i < effective_fade_in {
|
|
307
|
+
if effective_fade_in == 1 {
|
|
308
|
+
adjusted *= 0.0;
|
|
309
|
+
} else {
|
|
310
|
+
adjusted *= (i as f32) / (effective_fade_in as f32);
|
|
311
|
+
}
|
|
255
312
|
}
|
|
256
|
-
if
|
|
257
|
-
|
|
313
|
+
if effective_fade_out > 0 && i >= total_samples.saturating_sub(effective_fade_out) {
|
|
314
|
+
if effective_fade_out == 1 {
|
|
315
|
+
adjusted *= 0.0;
|
|
316
|
+
} else {
|
|
317
|
+
adjusted *= ((total_samples - 1 - i) as f32) / ((effective_fade_out - 1) as f32);
|
|
318
|
+
}
|
|
258
319
|
}
|
|
259
320
|
|
|
260
321
|
if drive > 0.0 {
|
|
@@ -51,6 +51,12 @@ impl AudioEngine {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
pub fn merge_with(&mut self, other: AudioEngine) {
|
|
54
|
+
// If the other buffer is empty, simply return without warning (common for spawns that produced nothing)
|
|
55
|
+
if other.buffer.is_empty() {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If the other buffer is present but contains only zeros, warn and skip merge
|
|
54
60
|
if other.buffer.iter().all(|&s| s == 0) {
|
|
55
61
|
eprintln!("⚠️ Skipping merge: other buffer is silent");
|
|
56
62
|
return;
|
|
@@ -225,10 +231,19 @@ impl AudioEngine {
|
|
|
225
231
|
);
|
|
226
232
|
|
|
227
233
|
// Fade in/out
|
|
228
|
-
if i < fade_len {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
if fade_len > 0 && i < fade_len {
|
|
235
|
+
if fade_len == 1 {
|
|
236
|
+
value *= 0.0;
|
|
237
|
+
} else {
|
|
238
|
+
value *= (i as f32) / (fade_len as f32);
|
|
239
|
+
}
|
|
240
|
+
} else if fade_len > 0 && i >= total_samples.saturating_sub(fade_len) {
|
|
241
|
+
if fade_len == 1 {
|
|
242
|
+
value *= 0.0;
|
|
243
|
+
} else {
|
|
244
|
+
// ensure last sample becomes exactly zero to avoid clicks
|
|
245
|
+
value *= ((total_samples - 1 - i) as f32) / ((fade_len - 1) as f32);
|
|
246
|
+
}
|
|
232
247
|
}
|
|
233
248
|
|
|
234
249
|
value *= envelope;
|