@devaloop/devalang 0.0.1-beta.1 → 0.0.1-beta.3
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 +9 -10
- package/Cargo.toml +84 -80
- package/README.md +10 -7
- package/docs/CHANGELOG.md +83 -0
- package/docs/ROADMAP.md +6 -2
- package/docs/TODO.md +3 -14
- package/examples/bus.deva +10 -0
- package/examples/chain.deva +19 -0
- package/examples/effect.deva +2 -0
- package/examples/filter.deva +11 -0
- package/examples/lfo.deva +9 -0
- package/examples/plugin.deva +10 -10
- package/examples/routing.deva +23 -0
- package/examples/synth.deva +11 -1
- package/examples/synth_types.deva +17 -0
- package/out-tsc/bin/project-version.json +6 -0
- package/out-tsc/core/functions/index.d.ts +5 -0
- package/out-tsc/core/functions/index.js +11 -0
- package/out-tsc/pkg/devalang_core.d.ts +2 -0
- package/out-tsc/pkg/devalang_core.js +17 -2
- package/out-tsc/pkg/devalang_core_bg.wasm.d.ts +1 -0
- package/out-tsc/scripts/version/copy-to-binary.d.ts +1 -0
- package/out-tsc/scripts/version/copy-to-binary.js +79 -0
- package/package.json +23 -10
- package/project-version.json +3 -3
- package/rust/bindings/Cargo.toml +9 -0
- package/rust/bindings/src/lib.rs +86 -0
- package/rust/cli/addon/commands.rs +35 -0
- package/rust/cli/addon/download.rs +234 -0
- package/rust/cli/addon/install.rs +33 -0
- package/rust/cli/addon/list.rs +224 -0
- package/rust/cli/addon/metadata.rs +124 -0
- package/rust/cli/addon/mod.rs +8 -0
- package/rust/cli/addon/remove.rs +271 -0
- package/rust/cli/addon/update.rs +305 -0
- package/rust/cli/{install/addon.rs → addon/utils.rs} +34 -43
- package/rust/cli/build/commands.rs +153 -103
- package/rust/cli/build/mod.rs +2 -2
- package/rust/cli/build/process.rs +165 -146
- package/rust/cli/check/mod.rs +208 -208
- package/rust/cli/discover/commands.rs +53 -31
- package/rust/cli/discover/config.rs +2 -4
- package/rust/cli/discover/install.rs +139 -28
- package/rust/cli/discover/metadata.rs +3 -3
- package/rust/cli/login/commands.rs +124 -124
- package/rust/cli/me/commands.rs +52 -0
- package/rust/cli/me/mod.rs +1 -0
- package/rust/cli/mod.rs +2 -2
- package/rust/cli/parser.rs +76 -70
- package/rust/cli/play/commands.rs +375 -324
- package/rust/cli/play/mod.rs +5 -5
- package/rust/cli/play/process.rs +159 -150
- package/rust/cli/play/realtime.rs +91 -91
- package/rust/cli/telemetry/commands.rs +22 -22
- package/rust/cli/telemetry/event_creator.rs +80 -80
- package/rust/cli/telemetry/mod.rs +3 -3
- package/rust/cli/telemetry/send.rs +51 -51
- package/rust/cli/template/commands.rs +69 -69
- package/rust/config/driver.rs +112 -103
- package/rust/config/mod.rs +3 -3
- package/rust/config/ops.rs +26 -26
- package/rust/config/settings.rs +101 -101
- package/rust/core/audio/engine/driver.rs +237 -0
- package/rust/core/audio/engine/export.rs +169 -0
- package/rust/core/audio/engine/helpers.rs +178 -170
- package/rust/core/audio/engine/mod.rs +56 -7
- package/rust/core/audio/engine/notes/dsp.rs +88 -0
- package/rust/core/audio/engine/notes/mod.rs +53 -0
- package/rust/core/audio/engine/notes/params.rs +294 -0
- package/rust/core/audio/engine/sample/insert.rs +300 -0
- package/rust/core/audio/engine/sample/mod.rs +40 -0
- package/rust/core/audio/engine/sample/padding.rs +170 -0
- package/rust/core/audio/evaluator/condition.rs +61 -0
- package/rust/core/audio/evaluator/mod.rs +9 -0
- package/rust/core/audio/{evaluator.rs → evaluator/numeric.rs} +152 -310
- package/rust/core/audio/evaluator/rhs.rs +16 -0
- package/rust/core/audio/evaluator/string_expr.rs +94 -0
- package/rust/core/audio/interpreter/driver.rs +574 -542
- package/rust/core/audio/interpreter/mod.rs +2 -14
- package/rust/core/audio/interpreter/statements/arrow_call/interprete.rs +179 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/chord.rs +398 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/effects.rs +323 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/mod.rs +3 -0
- package/rust/core/audio/interpreter/statements/arrow_call/methods/note.rs +371 -0
- package/rust/core/audio/interpreter/statements/arrow_call/mod.rs +3 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/arp.rs +192 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/mod.rs +24 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/pad.rs +116 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/pluck.rs +97 -0
- package/rust/core/audio/interpreter/statements/arrow_call/types/sub.rs +100 -0
- package/rust/core/audio/interpreter/{automate.rs → statements/automate.rs} +2 -4
- package/rust/core/audio/interpreter/{call.rs → statements/call.rs} +36 -5
- package/rust/core/audio/interpreter/{condition.rs → statements/condition.rs} +72 -71
- package/rust/core/audio/interpreter/{function.rs → statements/function.rs} +24 -26
- package/rust/core/audio/interpreter/{let_.rs → statements/let_.rs} +36 -38
- package/rust/core/audio/interpreter/{load.rs → statements/load.rs} +17 -19
- package/rust/core/audio/interpreter/{loop_.rs → statements/loop_.rs} +115 -114
- package/rust/core/audio/interpreter/statements/mod.rs +12 -0
- package/rust/core/audio/interpreter/{sleep.rs → statements/sleep.rs} +28 -28
- package/rust/core/audio/interpreter/{spawn.rs → statements/spawn.rs} +54 -4
- package/rust/core/audio/interpreter/{tempo.rs → statements/tempo.rs} +40 -40
- package/rust/core/audio/interpreter/{trigger.rs → statements/trigger.rs} +242 -239
- package/rust/core/audio/loader/trigger.rs +98 -97
- package/rust/core/audio/mod.rs +6 -7
- package/rust/core/audio/special/easing.rs +189 -189
- package/rust/core/audio/special/env.rs +45 -45
- package/rust/core/audio/special/math.rs +134 -134
- package/rust/core/audio/special/modulator.rs +143 -143
- package/rust/core/builder/mod.rs +129 -86
- package/rust/core/debugger/{module.rs → logs.rs} +52 -55
- package/rust/core/debugger/mod.rs +30 -30
- package/rust/core/debugger/store.rs +38 -40
- package/rust/core/error/mod.rs +269 -269
- package/rust/core/lexer/driver.rs +2 -4
- package/rust/core/mod.rs +9 -10
- package/rust/core/parser/driver/block.rs +111 -0
- package/rust/core/parser/driver/cursor.rs +82 -0
- package/rust/core/parser/driver/driver_impl.rs +159 -0
- package/rust/core/parser/driver/mod.rs +6 -0
- package/rust/core/parser/driver/parse_array.rs +120 -0
- package/rust/core/parser/driver/parse_map.rs +247 -0
- package/rust/core/parser/driver/parser.rs +160 -0
- package/rust/core/parser/handler/arrow_call.rs +90 -15
- package/rust/core/parser/handler/at.rs +279 -279
- package/rust/core/parser/handler/bank.rs +104 -104
- package/rust/core/parser/handler/condition.rs +83 -83
- package/rust/core/parser/handler/dot.rs +148 -148
- package/rust/core/parser/handler/identifier/automate.rs +254 -254
- package/rust/core/parser/handler/identifier/call.rs +91 -91
- package/rust/core/parser/handler/identifier/emit.rs +70 -70
- package/rust/core/parser/handler/identifier/function.rs +113 -113
- package/rust/core/parser/handler/identifier/group.rs +89 -89
- package/rust/core/parser/handler/identifier/let_.rs +173 -173
- package/rust/core/parser/handler/identifier/mod.rs +55 -55
- package/rust/core/parser/handler/identifier/on.rs +107 -107
- package/rust/core/parser/handler/identifier/print.rs +49 -49
- package/rust/core/parser/handler/identifier/sleep.rs +96 -43
- package/rust/core/parser/handler/identifier/spawn.rs +91 -91
- package/rust/core/parser/handler/identifier/synth.rs +39 -3
- package/rust/core/parser/handler/loop_.rs +194 -194
- package/rust/core/parser/handler/pattern.rs +25 -2
- package/rust/core/parser/handler/tempo.rs +105 -57
- package/rust/core/parser/statement.rs +10 -11
- package/rust/core/plugin/loader.rs +137 -137
- package/rust/core/plugin/runner/mod.rs +11 -0
- package/rust/core/plugin/{runner.rs → runner/non_wasm.rs} +206 -72
- package/rust/core/plugin/runner/wasm32.rs +44 -0
- package/rust/core/preprocessor/loader/inject.rs +313 -0
- package/rust/core/preprocessor/loader/loader_helpers.rs +110 -0
- package/rust/core/preprocessor/loader/mod.rs +235 -0
- package/rust/core/preprocessor/module.rs +55 -60
- package/rust/core/preprocessor/{processor.rs → processor/handlers.rs} +107 -114
- package/rust/core/preprocessor/processor/mod.rs +1 -0
- package/rust/core/preprocessor/resolver/function.rs +69 -69
- package/rust/core/preprocessor/resolver/group.rs +122 -94
- package/rust/core/preprocessor/resolver/pattern.rs +14 -2
- package/rust/core/store/global.rs +57 -61
- package/rust/core/store/mod.rs +1 -5
- package/rust/lib.rs +323 -308
- package/rust/macros/Cargo.toml +14 -0
- package/rust/macros/src/lib.rs +52 -0
- package/rust/main.rs +336 -143
- package/rust/types/Cargo.toml +1 -1
- package/rust/types/src/addons.rs +57 -55
- package/rust/types/src/config.rs +82 -74
- package/rust/types/src/lib.rs +15 -12
- package/rust/types/src/plugin.rs +20 -0
- package/rust/types/src/store.rs +139 -0
- package/rust/types/src/telemetry.rs +85 -85
- package/rust/utils/Cargo.toml +5 -2
- package/rust/utils/src/file.rs +477 -94
- package/rust/utils/src/first_usage.rs +97 -97
- package/rust/utils/src/lib.rs +9 -9
- package/rust/utils/src/logger.rs +200 -200
- package/rust/utils/src/path.rs +158 -88
- package/rust/utils/src/signature.rs +41 -41
- package/rust/utils/src/spinner.rs +20 -20
- package/rust/utils/src/version.rs +58 -27
- package/rust/utils/src/watcher.rs +46 -46
- package/rust/web/api.rs +5 -5
- package/rust/web/auth.rs +5 -0
- package/rust/web/cdn.rs +34 -34
- package/rust/web/forge.rs +5 -0
- package/rust/web/mod.rs +2 -0
- package/tests/integration.rs +21 -21
- package/typescript/core/functions/index.ts +11 -0
- package/typescript/pkg/devalang_core.ts +20 -4
- package/typescript/scripts/version/copy-to-binary.ts +82 -0
- package/rust/cli/bank/api.rs +0 -122
- package/rust/cli/bank/commands.rs +0 -275
- package/rust/cli/bank/mod.rs +0 -29
- package/rust/cli/install/bank.rs +0 -53
- package/rust/cli/install/commands.rs +0 -35
- package/rust/cli/install/mod.rs +0 -4
- package/rust/cli/install/plugin.rs +0 -61
- package/rust/core/audio/engine/sample.rs +0 -366
- package/rust/core/audio/engine/synth.rs +0 -325
- package/rust/core/audio/interpreter/arrow_call.rs +0 -311
- package/rust/core/audio/renderer.rs +0 -54
- package/rust/core/parser/driver.rs +0 -584
- package/rust/core/preprocessor/loader.rs +0 -637
- package/rust/core/store/export.rs +0 -28
- package/rust/core/store/function.rs +0 -40
- package/rust/core/store/import.rs +0 -28
- package/rust/core/store/variable.rs +0 -51
- package/rust/core/utils/mod.rs +0 -1
- package/rust/core/utils/path.rs +0 -37
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const argv = process.argv.slice(2);
|
|
9
|
+
let sourceArg;
|
|
10
|
+
let binaryArg;
|
|
11
|
+
let outDirArg;
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const a = argv[i];
|
|
14
|
+
if (a === "--source") {
|
|
15
|
+
sourceArg = argv[++i];
|
|
16
|
+
}
|
|
17
|
+
else if (a === "--binary") {
|
|
18
|
+
binaryArg = argv[++i];
|
|
19
|
+
}
|
|
20
|
+
else if (a === "--out-dir") {
|
|
21
|
+
outDirArg = argv[++i];
|
|
22
|
+
}
|
|
23
|
+
else if (a === "--help" || a === "-h") {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.error(`Unknown arg: ${a}`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Default source: attempt to locate project-version.json at repo root
|
|
32
|
+
const defaultSource = path_1.default.resolve(__dirname, "..", "..", "..", "project-version.json");
|
|
33
|
+
const sourcePath = sourceArg ? path_1.default.resolve(sourceArg) : defaultSource;
|
|
34
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
35
|
+
console.error(`Source project-version.json not found at '${sourcePath}'. Please provide --source or ensure file exists.`);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
let destDir = null;
|
|
39
|
+
if (binaryArg) {
|
|
40
|
+
const binPath = path_1.default.resolve(binaryArg);
|
|
41
|
+
// If it's an existing file, use its directory, otherwise assume user passed target path and use its parent
|
|
42
|
+
if (fs_1.default.existsSync(binPath) && fs_1.default.statSync(binPath).isFile()) {
|
|
43
|
+
destDir = path_1.default.dirname(binPath);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// If binPath looks like a file path (has extension) use parent, else treat as dir
|
|
47
|
+
const ext = path_1.default.extname(binPath);
|
|
48
|
+
if (ext) {
|
|
49
|
+
destDir = path_1.default.dirname(binPath);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
destDir = binPath;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (outDirArg) {
|
|
57
|
+
destDir = path_1.default.resolve(outDirArg);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Default: try to copy next to the running node current working dir (useful when packaging)
|
|
61
|
+
destDir = process.cwd();
|
|
62
|
+
}
|
|
63
|
+
if (!destDir) {
|
|
64
|
+
console.error("Could not resolve destination directory");
|
|
65
|
+
process.exit(3);
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
if (!fs_1.default.existsSync(destDir)) {
|
|
69
|
+
fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
const destPath = path_1.default.join(destDir, "project-version.json");
|
|
72
|
+
fs_1.default.copyFileSync(sourcePath, destPath);
|
|
73
|
+
console.log(`project-version.json copied to '${destPath}'`);
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error("Failed to copy project-version.json:", err);
|
|
78
|
+
process.exit(4);
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devaloop/devalang",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.1-beta.
|
|
4
|
+
"version": "0.0.1-beta.3",
|
|
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
|
+
"longDescription": "Devalang is a compact, performant DSL and toolchain for composing, automating and rendering sound. It provides a CLI, WASM bindings, a TypeScript API, and plugins for editors. Ideal for live-coding, sample-based production, algorithmic composition, and fast prototyping.",
|
|
6
7
|
"main": "out-tsc/index.js",
|
|
7
8
|
"bin": {
|
|
8
9
|
"devalang": "./out-tsc/bin/index.js"
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
"rust:prepack": "cargo build --release",
|
|
19
20
|
"scripts:postbuild": "tsc && node out-tsc/scripts/postbuild.js",
|
|
20
21
|
"scripts:version:bump": "tsc && node out-tsc/scripts/version/index.js",
|
|
22
|
+
"scripts:copy-version": "tsc && node out-tsc/scripts/version/copy-to-binary.js --out-dir ./out-tsc/bin",
|
|
21
23
|
"scripts:build": "tsc",
|
|
22
24
|
"types:build": "tsc --emitDeclarationOnly",
|
|
23
25
|
"types:wasm": "tsc && node out-tsc/scripts/copy-wasm-dts.js",
|
|
@@ -25,25 +27,36 @@
|
|
|
25
27
|
"test:rust": "cargo test",
|
|
26
28
|
"test:ts": "mocha -r ts-node/register tests/typescript/**/*.spec.ts --exit",
|
|
27
29
|
"test": "npm run test:ts",
|
|
28
|
-
"postinstall": "node out-tsc/scripts/postinstall.js"
|
|
30
|
+
"postinstall": "node out-tsc/scripts/postinstall.js",
|
|
31
|
+
"beforepublish": "npm run types:all && npm run scripts:copy-version"
|
|
29
32
|
},
|
|
30
33
|
"homepage": "https://devalang.com",
|
|
31
34
|
"keywords": [
|
|
32
35
|
"devalang",
|
|
33
36
|
"music",
|
|
34
37
|
"sound",
|
|
35
|
-
"domain-specific language",
|
|
36
38
|
"dsl",
|
|
37
|
-
"
|
|
38
|
-
"sound design",
|
|
39
|
-
"music hacking",
|
|
39
|
+
"domain-specific-language",
|
|
40
40
|
"audio",
|
|
41
|
+
"music-programming",
|
|
41
42
|
"synthesis",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
43
|
+
"midi",
|
|
44
|
+
"wasm",
|
|
45
|
+
"vscode",
|
|
46
|
+
"live-coding",
|
|
47
|
+
"sound-design",
|
|
48
|
+
"samples",
|
|
49
|
+
"cli"
|
|
45
50
|
],
|
|
46
|
-
"author":
|
|
51
|
+
"author": {
|
|
52
|
+
"name": "Devaloop",
|
|
53
|
+
"email": "contact@devaloop.com",
|
|
54
|
+
"url": "https://devaloop.com"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/devaloop-labs/devalang/issues",
|
|
58
|
+
"email": "contact@devaloop.com"
|
|
59
|
+
},
|
|
47
60
|
"license": "MIT",
|
|
48
61
|
"repository": {
|
|
49
62
|
"type": "git",
|
package/project-version.json
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//! Minimal, focused bindings helpers for writing custom render functions in
|
|
2
|
+
//! plugin crates.
|
|
3
|
+
//!
|
|
4
|
+
//! The core runtime already handles registration and FFI bridging. This crate
|
|
5
|
+
//! only exports the types and function-signature aliases that plugin authors
|
|
6
|
+
//! should implement in safe Rust. The host will convert raw buffers/pointers
|
|
7
|
+
//! into these safe types before calling the plugin function.
|
|
8
|
+
//!
|
|
9
|
+
//! Guiding principle: plugin authors write purely safe Rust functions with
|
|
10
|
+
//! clear parameters (slices and small structs). No registration helpers or
|
|
11
|
+
//! global registries are provided here.
|
|
12
|
+
|
|
13
|
+
/// Lightweight representation of a musical note.
|
|
14
|
+
#[derive(Debug, Clone, Copy)]
|
|
15
|
+
pub struct Note {
|
|
16
|
+
/// MIDI pitch 0..127
|
|
17
|
+
pub pitch: u8,
|
|
18
|
+
/// Velocity 0..127
|
|
19
|
+
pub velocity: u8,
|
|
20
|
+
/// Note duration in milliseconds (approximate)
|
|
21
|
+
pub duration_ms: u32,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Default for Note {
|
|
25
|
+
fn default() -> Self {
|
|
26
|
+
Self { pitch: 60, velocity: 100, duration_ms: 500 }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Parameters describing the output buffer and audio context.
|
|
31
|
+
#[derive(Debug, Clone, Copy)]
|
|
32
|
+
pub struct BufferParams {
|
|
33
|
+
/// Sample rate in Hz
|
|
34
|
+
pub sample_rate: u32,
|
|
35
|
+
/// Number of channels (1 = mono, 2 = stereo, ...)
|
|
36
|
+
pub channels: u32,
|
|
37
|
+
/// Number of frames (samples per channel) available in the buffer
|
|
38
|
+
pub frames: u32,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Helper to compute expected buffer length (frames * channels).
|
|
42
|
+
impl BufferParams {
|
|
43
|
+
/// Returns the expected length of the interleaved buffer (frames * channels)
|
|
44
|
+
/// as a host-friendly `usize`. The host is responsible for converting
|
|
45
|
+
/// its platform-sized integers into these explicit-width fields.
|
|
46
|
+
pub fn buffer_len(&self) -> usize {
|
|
47
|
+
(self.frames as usize).saturating_mul(self.channels as usize)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Quick validation helper that plugins can call to ensure the provided
|
|
51
|
+
/// output slice matches the declared `params` length.
|
|
52
|
+
pub fn validate_buffer(out: &mut [f32], params: BufferParams) -> Result<(), &'static str> {
|
|
53
|
+
let expected = params.buffer_len();
|
|
54
|
+
if out.len() < expected { Err("output buffer too small") } else { Ok(()) }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl Default for BufferParams {
|
|
59
|
+
fn default() -> Self {
|
|
60
|
+
Self { sample_rate: 44100, channels: 1, frames: 0 }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Common render function signature plugin authors should implement.
|
|
65
|
+
///
|
|
66
|
+
/// - `out` is a mutable slice of f32 samples (interleaved if channels > 1).
|
|
67
|
+
/// - `params` provides sample rate / channels / frames info.
|
|
68
|
+
/// - `note` is the Note descriptor.
|
|
69
|
+
/// - `freq`/`amp` give additional voice parameters the host may pass.
|
|
70
|
+
pub type RenderFn = fn(out: &mut [f32], params: BufferParams, note: Note, freq: f32, amp: f32);
|
|
71
|
+
|
|
72
|
+
/// A more complete signature used by synth-style plugins that also need
|
|
73
|
+
/// access to extra controls (e.g. voice index or time offset).
|
|
74
|
+
pub type RenderFnExt = fn(
|
|
75
|
+
out: &mut [f32],
|
|
76
|
+
params: BufferParams,
|
|
77
|
+
note: Note,
|
|
78
|
+
freq: f32,
|
|
79
|
+
amp: f32,
|
|
80
|
+
voice_index: u32,
|
|
81
|
+
time_ms: u64,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/// Optional signature for control parameter setters. The host can call these
|
|
85
|
+
/// safely by converting raw values into primitives.
|
|
86
|
+
pub type SetParamFn = fn(param_name: &str, value: f32);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
use crate::cli::addon::{
|
|
2
|
+
install::install_addon, list::list_addons, remove::remove_addon, update::update_addon,
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
pub async fn handle_install_addon_command(name: String, no_clear_tmp: bool) -> Result<(), String> {
|
|
6
|
+
if let Err(e) = install_addon(name, no_clear_tmp).await {
|
|
7
|
+
return Err(format!("Failed to install addon: {}", e));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
Ok(())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub async fn handle_list_addon_command() -> Result<(), String> {
|
|
14
|
+
if let Err(e) = list_addons().await {
|
|
15
|
+
return Err(format!("Failed to list addons: {}", e));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Ok(())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub async fn handle_remove_addon_command(name: String) -> Result<(), String> {
|
|
22
|
+
if let Err(e) = remove_addon(name).await {
|
|
23
|
+
return Err(format!("Failed to remove addon: {}", e));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Ok(())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub async fn handle_update_addon_command(name: String) -> Result<(), String> {
|
|
30
|
+
if let Err(e) = update_addon(name).await {
|
|
31
|
+
return Err(format!("Failed to update addon: {}", e));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Ok(())
|
|
35
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
cli::addon::{metadata::AddonToDownloadMetadata, utils::ask_api_for_signed_url},
|
|
3
|
+
config::ops::load_config,
|
|
4
|
+
web::cdn::download_from_cdn,
|
|
5
|
+
};
|
|
6
|
+
use devalang_core::config::driver::{ProjectConfig, ProjectConfigExt};
|
|
7
|
+
use devalang_types::{AddonType, ProjectConfigBankEntry, ProjectConfigPluginEntry};
|
|
8
|
+
use devalang_utils::{
|
|
9
|
+
file::extract_zip_safely,
|
|
10
|
+
logger::{LogLevel, Logger},
|
|
11
|
+
spinner::start_spinner,
|
|
12
|
+
};
|
|
13
|
+
use std::fs;
|
|
14
|
+
|
|
15
|
+
pub async fn download_addon(
|
|
16
|
+
slug: &str,
|
|
17
|
+
addon_metadata: &AddonToDownloadMetadata,
|
|
18
|
+
) -> Result<(), String> {
|
|
19
|
+
let logger = Logger::new();
|
|
20
|
+
let deva_dir = devalang_utils::path::ensure_deva_dir()?;
|
|
21
|
+
|
|
22
|
+
let target_dir = match addon_metadata.addon_type {
|
|
23
|
+
AddonType::Bank => deva_dir.join("banks"),
|
|
24
|
+
AddonType::Plugin => deva_dir.join("plugins"),
|
|
25
|
+
AddonType::Preset => deva_dir.join("presets"),
|
|
26
|
+
AddonType::Template => deva_dir.join("templates"),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if !target_dir.exists() {
|
|
30
|
+
fs::create_dir_all(&target_dir).map_err(|e| {
|
|
31
|
+
format!(
|
|
32
|
+
"Failed to create target dir '{}': {}",
|
|
33
|
+
target_dir.display(),
|
|
34
|
+
e
|
|
35
|
+
)
|
|
36
|
+
})?;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let user_provided_publisher = slug.contains('.');
|
|
40
|
+
let display_name = if user_provided_publisher {
|
|
41
|
+
format!("{}.{}", addon_metadata.publisher, addon_metadata.name)
|
|
42
|
+
} else {
|
|
43
|
+
addon_metadata.name.clone()
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
let archive_path = {
|
|
47
|
+
let tmp_root = deva_dir.join("tmp");
|
|
48
|
+
if !tmp_root.exists() {
|
|
49
|
+
fs::create_dir_all(&tmp_root)
|
|
50
|
+
.map_err(|e| format!("Failed to create tmp dir '{}': {}", tmp_root.display(), e))?;
|
|
51
|
+
}
|
|
52
|
+
tmp_root.join(&display_name).with_extension("tar.gz")
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if let Some(parent) = archive_path.parent() {
|
|
56
|
+
if !parent.exists() {
|
|
57
|
+
fs::create_dir_all(parent)
|
|
58
|
+
.map_err(|e| format!("Failed to prepare tmp dir '{}': {}", parent.display(), e))?;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let extract_path = target_dir
|
|
63
|
+
.join(&addon_metadata.publisher)
|
|
64
|
+
.join(&addon_metadata.name);
|
|
65
|
+
|
|
66
|
+
let signed_url = {
|
|
67
|
+
let spinner =
|
|
68
|
+
start_spinner(format!("Requesting download link for {}", display_name).as_str());
|
|
69
|
+
let request = if user_provided_publisher {
|
|
70
|
+
ask_api_for_signed_url(
|
|
71
|
+
addon_metadata.addon_type.clone(),
|
|
72
|
+
addon_metadata.publisher.clone(),
|
|
73
|
+
&addon_metadata.name,
|
|
74
|
+
)
|
|
75
|
+
} else {
|
|
76
|
+
ask_api_for_signed_url(
|
|
77
|
+
addon_metadata.addon_type.clone(),
|
|
78
|
+
String::new(),
|
|
79
|
+
&addon_metadata.name,
|
|
80
|
+
)
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
match request.await {
|
|
84
|
+
Ok(url) => url,
|
|
85
|
+
Err(err) => {
|
|
86
|
+
let message = format!("Failed to obtain download link: {}", err);
|
|
87
|
+
println!("{}", message);
|
|
88
|
+
return Err(message);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
let config_path = devalang_utils::path::get_devalang_config_path()?;
|
|
94
|
+
let mut config = load_config(Some(&config_path))
|
|
95
|
+
.ok_or_else(|| format!("Failed to load config from '{}'", config_path.display()))?;
|
|
96
|
+
|
|
97
|
+
if extract_path.exists() {
|
|
98
|
+
logger.log_message(
|
|
99
|
+
LogLevel::Info,
|
|
100
|
+
format!(
|
|
101
|
+
"Addon '{}' already present at {}",
|
|
102
|
+
display_name.as_str(),
|
|
103
|
+
extract_path.display()
|
|
104
|
+
)
|
|
105
|
+
.as_str(),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
if ensure_config_entry(&mut config, addon_metadata) {
|
|
109
|
+
if let Err(err) = config.write_config(&config) {
|
|
110
|
+
logger.log_message(
|
|
111
|
+
LogLevel::Error,
|
|
112
|
+
format!("Failed to write config: {}", err).as_str(),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if matches!(
|
|
118
|
+
addon_metadata.addon_type,
|
|
119
|
+
AddonType::Preset | AddonType::Template
|
|
120
|
+
) {
|
|
121
|
+
logger.log_message(
|
|
122
|
+
LogLevel::Info,
|
|
123
|
+
"Presets and templates are not tracked in project config yet.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Ok(());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let download_spinner = start_spinner("Downloading archive...");
|
|
131
|
+
match download_from_cdn(&signed_url, &archive_path).await {
|
|
132
|
+
Ok(_) => println!("Downloaded archive to {}", archive_path.display()),
|
|
133
|
+
Err(err) => {
|
|
134
|
+
let message = format!("Failed to download archive: {}", err);
|
|
135
|
+
println!("{}", message);
|
|
136
|
+
return Err(message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let extract_spinner = start_spinner("Extracting archive");
|
|
141
|
+
match extract_zip_safely(&archive_path, &extract_path) {
|
|
142
|
+
Ok(_) => println!("Installed at {}", extract_path.display()),
|
|
143
|
+
Err(err) => {
|
|
144
|
+
println!("Failed to extract archive: {}", err);
|
|
145
|
+
return Err(err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let mut config_updated = false;
|
|
150
|
+
if ensure_config_entry(&mut config, addon_metadata) {
|
|
151
|
+
match config.write_config(&config) {
|
|
152
|
+
Ok(_) => {
|
|
153
|
+
config_updated = true;
|
|
154
|
+
}
|
|
155
|
+
Err(err) => {
|
|
156
|
+
logger.log_message(
|
|
157
|
+
LogLevel::Error,
|
|
158
|
+
format!("Failed to write config: {}", err).as_str(),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
logger.log_message(
|
|
165
|
+
LogLevel::Info,
|
|
166
|
+
format!(
|
|
167
|
+
"Addon '{}' installed at {}",
|
|
168
|
+
display_name,
|
|
169
|
+
extract_path.display()
|
|
170
|
+
)
|
|
171
|
+
.as_str(),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if config_updated {
|
|
175
|
+
logger.log_message(LogLevel::Info, "Project config updated");
|
|
176
|
+
} else if matches!(
|
|
177
|
+
addon_metadata.addon_type,
|
|
178
|
+
AddonType::Preset | AddonType::Template
|
|
179
|
+
) {
|
|
180
|
+
logger.log_message(
|
|
181
|
+
LogLevel::Info,
|
|
182
|
+
"Presets and templates are not tracked in project config yet.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Cleanup temporary files used during download/install
|
|
187
|
+
if let Err(err) = devalang_utils::file::clear_tmp_folder() {
|
|
188
|
+
logger.log_message(
|
|
189
|
+
LogLevel::Warning,
|
|
190
|
+
format!("Failed to clear tmp folder: {}", err).as_str(),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
Ok(())
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fn ensure_config_entry(
|
|
198
|
+
config: &mut ProjectConfig,
|
|
199
|
+
addon_metadata: &AddonToDownloadMetadata,
|
|
200
|
+
) -> bool {
|
|
201
|
+
match addon_metadata.addon_type {
|
|
202
|
+
AddonType::Bank => {
|
|
203
|
+
let dependency_path = format!(
|
|
204
|
+
"devalang://bank/{}/{}",
|
|
205
|
+
addon_metadata.publisher, addon_metadata.name
|
|
206
|
+
);
|
|
207
|
+
let banks = config.banks.get_or_insert_with(Vec::new);
|
|
208
|
+
if banks.iter().any(|entry| entry.path == dependency_path) {
|
|
209
|
+
false
|
|
210
|
+
} else {
|
|
211
|
+
banks.push(ProjectConfigBankEntry {
|
|
212
|
+
path: dependency_path,
|
|
213
|
+
});
|
|
214
|
+
true
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
AddonType::Plugin => {
|
|
218
|
+
let dependency_path = format!(
|
|
219
|
+
"devalang://plugin/{}/{}",
|
|
220
|
+
addon_metadata.publisher, addon_metadata.name
|
|
221
|
+
);
|
|
222
|
+
let plugins = config.plugins.get_or_insert_with(Vec::new);
|
|
223
|
+
if plugins.iter().any(|entry| entry.path == dependency_path) {
|
|
224
|
+
false
|
|
225
|
+
} else {
|
|
226
|
+
plugins.push(ProjectConfigPluginEntry {
|
|
227
|
+
path: dependency_path,
|
|
228
|
+
});
|
|
229
|
+
true
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
AddonType::Preset | AddonType::Template => false,
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
use devalang_utils::logger::Logger;
|
|
2
|
+
|
|
3
|
+
use crate::cli::addon::{download::download_addon, metadata::get_addon_from_api};
|
|
4
|
+
|
|
5
|
+
pub async fn install_addon(slug: String, no_clear_tmp: bool) -> Result<(), String> {
|
|
6
|
+
let addon_metadata = get_addon_from_api(&slug).await?;
|
|
7
|
+
|
|
8
|
+
if let Err(e) = download_addon(&slug, &addon_metadata).await {
|
|
9
|
+
eprintln!("Failed to download addon '{}': {}", slug, e);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let logger = Logger::new();
|
|
13
|
+
logger.log_message(
|
|
14
|
+
devalang_utils::logger::LogLevel::Success,
|
|
15
|
+
&format!(
|
|
16
|
+
"Successfully installed addon '{}.{}' ({})",
|
|
17
|
+
addon_metadata.publisher,
|
|
18
|
+
addon_metadata.name,
|
|
19
|
+
match addon_metadata.addon_type {
|
|
20
|
+
devalang_types::AddonType::Bank => "bank",
|
|
21
|
+
devalang_types::AddonType::Plugin => "plugin",
|
|
22
|
+
devalang_types::AddonType::Preset => "preset",
|
|
23
|
+
devalang_types::AddonType::Template => "template",
|
|
24
|
+
}
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if !no_clear_tmp {
|
|
29
|
+
let _ = devalang_utils::file::clear_tmp_folder();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Ok(())
|
|
33
|
+
}
|