@devaloop/devalang 0.0.1-alpha.11 → 0.0.1-alpha.12

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 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,54 +1,53 @@
1
- [package]
2
- name = "devalang"
3
- version = "0.0.1-alpha.11"
4
- authors = ["Devaloop <contact@devaloop.com>"]
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
- license = "MIT"
7
- repository = "https://github.com/devaloop-labs/devalang"
8
- keywords = ["music", "dsl", "audio", "cli"]
9
- categories = ["command-line-utilities", "development-tools", "parser-implementations"]
10
- readme = "README.md"
11
- homepage = "https://devalang.com"
12
- documentation = "https://docs.devalang.com/"
13
- license-file = "LICENSE"
14
- edition = "2024"
15
-
16
- [[bin]]
17
- name = "devalang"
18
- path = "rust/main.rs"
19
- required-features = ["cli"]
20
-
21
- [lib]
22
- path = "rust/lib.rs"
23
- crate-type = ["cdylib"]
24
-
25
- [profile.release]
26
- opt-level = "s"
27
-
28
- [features]
29
- default = ["cli"]
30
- cli = ["crossterm", "indicatif", "inquire"]
31
-
32
- [dependencies]
33
- clap = { version = "4.5", features = ["derive"] }
34
- serde = { version = "1.0", features = ["derive"] }
35
- serde_json = "1.0"
36
- rodio = "0.17"
37
- hound = "3.4.0"
38
- toml = "0.8"
39
- notify = "6.1"
40
- fs_extra = "1.3"
41
- include_dir = "0.7"
42
- wasm-bindgen = "0.2"
43
- serde-wasm-bindgen = "0.4"
44
- nom_locate = "4.0.0"
45
- chrono = "0.4"
46
- crossterm = { version = "0.27", optional = true }
47
- indicatif = { version = "0.17", optional = true }
48
- inquire = { version = "0.7.5", optional = true }
49
- js-sys = "0.3"
50
- reqwest = { version = "0.12.22", features = ["json"] }
51
- flate2 = "1.0"
52
- tar = "0.4"
53
- tokio = { version = "1", features = ["full"] }
54
- zip = "4.3.0"
1
+ [package]
2
+ name = "devalang"
3
+ version = "0.0.1-alpha.12"
4
+ authors = ["Devaloop <contact@devaloop.com>"]
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
+ license = "MIT"
7
+ repository = "https://github.com/devaloop-labs/devalang"
8
+ keywords = ["music", "dsl", "audio", "cli"]
9
+ categories = ["command-line-utilities", "development-tools", "parser-implementations"]
10
+ readme = "README.md"
11
+ homepage = "https://devalang.com"
12
+ documentation = "https://docs.devalang.com/"
13
+ license-file = "LICENSE"
14
+ edition = "2024"
15
+
16
+ [[bin]]
17
+ name = "devalang"
18
+ path = "rust/main.rs"
19
+ required-features = ["cli"]
20
+
21
+ [lib]
22
+ path = "rust/lib.rs"
23
+ crate-type = ["cdylib"]
24
+
25
+ [profile.release]
26
+ opt-level = "s"
27
+
28
+ [features]
29
+ default = ["cli"]
30
+ cli = ["crossterm", "indicatif", "inquire", "zip", "reqwest", "flate2", "tokio"]
31
+
32
+ [dependencies]
33
+ clap = { version = "4.5", features = ["derive"] }
34
+ serde = { version = "1.0", features = ["derive"] }
35
+ serde_json = "1.0"
36
+ rodio = "0.17"
37
+ hound = "3.4.0"
38
+ toml = "0.8"
39
+ notify = "6.1"
40
+ fs_extra = "1.3"
41
+ include_dir = "0.7"
42
+ wasm-bindgen = "0.2"
43
+ serde-wasm-bindgen = "0.4"
44
+ nom_locate = "4.0.0"
45
+ chrono = "0.4"
46
+ crossterm = { version = "0.27", optional = true }
47
+ indicatif = { version = "0.17", optional = true }
48
+ inquire = { version = "0.7.5", optional = true }
49
+ js-sys = "0.3"
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 }
package/docs/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@
4
4
 
5
5
  # Changelog
6
6
 
7
+ ## Version 0.0.1-alpha.12 (2025-07-21)
8
+
9
+ ### 🧩 Language Features
10
+
11
+ - Implemented `trigger` effects to apply effects to triggers, allowing for more dynamic sound manipulation.
12
+ - Example: `.myTrigger auto { reverb: 1.0, pitch: 1.5, gain: 0.8 }`
13
+
14
+ ### 🧠 Core Engine
15
+
16
+ - Moved `utils::installer` to `installer::utils` to better organize the project structure.
17
+ - Set CLI dependencies as optional in `Cargo.toml` to allow for a cleaner build without CLI features.
18
+ - Patched `@load` relative path resolution to ensure correct loading of external resources.
19
+ - Patched `trigger` statement that was not correctly parsed when using namespaced banks of sounds.
20
+
21
+ ### 🧩 Web Assembly
22
+
23
+ - Patched `lib.rs` dependencies to ensure compatibility with the latest Rust and WASM standards.
24
+
7
25
  ## Version 0.0.1-alpha.11 (2025-07-20)
8
26
 
9
27
  ### 📖 Documentation
@@ -7,10 +7,12 @@
7
7
  @load "./samples/kick-808.wav" as kickCustom
8
8
  @load "./samples/hat-808.wav" as hatCustom
9
9
 
10
- bpm tempo
10
+ # bpm tempo
11
11
 
12
- group myTrack:
13
- spawn myLoop
14
- spawn myLead
12
+ # group myTrack:
13
+ # spawn myLoop
14
+ # spawn myLead
15
15
 
16
- call myTrack
16
+ # call myTrack
17
+
18
+ .kickCustom duration { gain: 1, pitch: 1.25 }
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.11",
4
+ "version": "0.0.1-alpha.12",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.0.1-alpha.11",
2
+ "version": "0.0.1-alpha.12",
3
3
  "channel": "alpha",
4
- "lastCommit": "f643b05f7c5d97c5d22cb255c3641cfc9e385ea2",
5
- "build": 10
4
+ "lastCommit": "e16a21167693d0265195bf4256b3f8ec0ed1e55f",
5
+ "build": 11
6
6
  }
@@ -3,6 +3,7 @@ use hound::{ SampleFormat, WavSpec, WavWriter };
3
3
  use rodio::{ Decoder, Source };
4
4
 
5
5
  use crate::core::{
6
+ shared::value::Value,
6
7
  store::variable::VariableTable,
7
8
  utils::path::{ normalize_path, resolve_relative_path },
8
9
  };
@@ -167,7 +168,7 @@ impl AudioEngine {
167
168
  filepath: &str,
168
169
  time_secs: f32,
169
170
  dur_sec: f32,
170
- effects: Option<HashMap<String, f32>>
171
+ effects: Option<HashMap<String, Value>>
171
172
  ) {
172
173
  if filepath.is_empty() {
173
174
  eprintln!("❌ Empty file path provided for audio sample.");
@@ -197,9 +198,26 @@ impl AudioEngine {
197
198
  eprintln!("❌ Unsupported devalang:// object type: {}", object_type);
198
199
  return;
199
200
  }
201
+ } else {
202
+ let module_path = &self.module_name;
203
+ let root = Path::new(module_path).parent();
204
+
205
+ if let Some(root_path) = root {
206
+ resolved_path = root_path.join(filepath).to_str().unwrap_or("").to_string();
207
+ } else {
208
+ eprintln!("❌ Could not resolve root path for module: {}", module_path);
209
+ return;
210
+ }
211
+ }
212
+
213
+ if !Path::new(&resolved_path).exists() {
214
+ eprintln!("❌ Audio file not found at: {}", resolved_path);
215
+ return;
200
216
  }
201
217
 
202
- let file = BufReader::new(File::open(resolved_path).expect("Failed to open audio file"));
218
+ let file = BufReader::new(
219
+ File::open(&resolved_path).expect(&format!("Failed to open audio file {}", filepath))
220
+ );
203
221
  let decoder = Decoder::new(file).expect("Failed to decode audio file");
204
222
 
205
223
  // Mono or stereo reading possible here, we will duplicate in L/R
@@ -211,7 +229,7 @@ impl AudioEngine {
211
229
  return;
212
230
  }
213
231
 
214
- // TODO Apply effects here if needed
232
+ // Pad the buffer to ensure it can accommodate the new samples
215
233
  let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
216
234
  let required_len = offset + samples.len() * (CHANNELS as usize);
217
235
  let padded_required_len = if required_len % 2 == 1 {
@@ -221,21 +239,116 @@ impl AudioEngine {
221
239
  };
222
240
 
223
241
  self.buffer.resize(padded_required_len, 0);
224
- self.pad_samples(&samples, time_secs);
242
+
243
+ // Apply effects
244
+ if let Some(effects_map) = effects {
245
+ self.pad_samples(&samples, time_secs, Some(effects_map));
246
+ } else {
247
+ self.pad_samples(&samples, time_secs, None);
248
+ }
225
249
  }
226
250
 
227
- fn pad_samples(&mut self, samples: &[i16], time_secs: f32) {
251
+ fn pad_samples(
252
+ &mut self,
253
+ samples: &[i16],
254
+ time_secs: f32,
255
+ effects_map: Option<HashMap<String, Value>>
256
+ ) {
228
257
  let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
258
+ let total_samples = samples.len();
259
+
260
+ // Default values
261
+ let mut gain = 1.0;
262
+ let mut pan = 0.0;
263
+ let mut fade_in = 0.0;
264
+ let mut fade_out = 0.0;
265
+ let mut pitch = 1.0;
266
+ let mut drive = 0.0;
267
+ let mut reverb = 0.0;
268
+
269
+ if let Some(map) = &effects_map {
270
+ for (key, val) in map {
271
+ match (key.as_str(), val) {
272
+ ("gain", Value::Number(v)) => {
273
+ gain = *v;
274
+ }
275
+ ("pan", Value::Number(v)) => {
276
+ pan = *v;
277
+ }
278
+ ("fadeIn", Value::Number(v)) => {
279
+ fade_in = *v;
280
+ }
281
+ ("fadeOut", Value::Number(v)) => {
282
+ fade_out = *v;
283
+ }
284
+ ("pitch", Value::Number(v)) => {
285
+ pitch = *v;
286
+ }
287
+ ("drive", Value::Number(v)) => {
288
+ // Drive effect can be implemented here if needed
289
+ drive = *v;
290
+ }
291
+ ("reverb", Value::Number(v)) => {
292
+ reverb = *v;
293
+ }
294
+ _ => eprintln!("⚠️ Unknown or invalid effect '{}'", key),
295
+ }
296
+ }
297
+ }
298
+
299
+ let fade_in_samples = (fade_in * (SAMPLE_RATE as f32)) as usize;
300
+ let fade_out_samples = (fade_out * (SAMPLE_RATE as f32)) as usize;
229
301
 
230
302
  for (i, &sample) in samples.iter().enumerate() {
231
- let adjusted_sample = ((sample as f32) * self.volume).round() as i16;
303
+ // Gain
304
+ let mut adjusted = (sample as f32) * gain;
305
+
306
+ // Fade in
307
+ if fade_in_samples > 0 && i < fade_in_samples {
308
+ adjusted *= (i as f32) / (fade_in_samples as f32);
309
+ }
310
+
311
+ // Fade out
312
+ if fade_out_samples > 0 && i >= total_samples.saturating_sub(fade_out_samples) {
313
+ adjusted *= ((total_samples - i) as f32) / (fade_out_samples as f32);
314
+ }
315
+
316
+ // Pitch adjustment
317
+ if pitch != 1.0 {
318
+ let pitch_adjusted_index = ((i as f32) / pitch) as usize;
319
+ if pitch_adjusted_index < total_samples {
320
+ adjusted = (samples[pitch_adjusted_index] as f32) * gain;
321
+ } else {
322
+ adjusted = 0.0; // Out of bounds, set to zero
323
+ }
324
+ }
325
+
326
+ // Drive effect
327
+ if drive > 0.0 {
328
+ adjusted = adjusted.tanh() * (1.0 + drive);
329
+ }
330
+
331
+ // Reverb effect
332
+ if reverb > 0.0 {
333
+ let reverb_delay = (reverb * (SAMPLE_RATE as f32)) as usize;
334
+ if i >= reverb_delay {
335
+ adjusted += self.buffer[offset + i - reverb_delay] as f32 * 0.5; // Simple feedback
336
+ }
337
+ }
338
+
339
+ // Clamp
340
+ let adjusted_sample = adjusted.round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
341
+
342
+ // Pan (L/R split)
343
+ let left = ((adjusted_sample as f32) * (1.0 - pan.clamp(0.0, 1.0))) as i16;
344
+ let right = ((adjusted_sample as f32) * (1.0 + pan.clamp(-1.0, 0.0)).abs()) as i16;
232
345
 
233
346
  let left_pos = offset + i * 2;
234
347
  let right_pos = left_pos + 1;
235
348
 
236
349
  if right_pos < self.buffer.len() {
237
- self.buffer[left_pos] = self.buffer[left_pos].saturating_add(adjusted_sample); // gauche
238
- self.buffer[right_pos] = self.buffer[right_pos].saturating_add(adjusted_sample); // droite
350
+ self.buffer[left_pos] = self.buffer[left_pos].saturating_add(left);
351
+ self.buffer[right_pos] = self.buffer[right_pos].saturating_add(right);
239
352
  }
240
353
  }
241
354
  }
@@ -1,3 +1,5 @@
1
+ use std::collections::HashMap;
2
+
1
3
  use crate::core::{
2
4
  audio::{ engine::AudioEngine, loader::trigger::load_trigger },
3
5
  parser::statement::{ Statement, StatementKind },
@@ -67,8 +69,13 @@ pub fn interprete_trigger_statement(
67
69
  variable_table.clone()
68
70
  );
69
71
 
72
+ if let Some(effects) = extract_effects(stmt.value.clone()) {
73
+ audio_engine.insert_sample(&src, cursor_time, duration_final, Some(effects));
74
+ } else {
75
+ audio_engine.insert_sample(&src, cursor_time, duration_final, None);
76
+ }
77
+
70
78
  let mut updated_engine = audio_engine.clone();
71
- updated_engine.insert_sample(&src, cursor_time, duration_final, None);
72
79
 
73
80
  let new_cursor_time = cursor_time + duration_final;
74
81
  let new_max_end_time = new_cursor_time.max(max_end_time);
@@ -100,3 +107,17 @@ fn resolve_namespaced_variable<'a>(path: &str, variables: &'a VariableTable) ->
100
107
 
101
108
  current
102
109
  }
110
+
111
+ fn extract_effects(value: Value) -> Option<HashMap<String, Value>> {
112
+ if let Value::Map(map) = value.clone() {
113
+ let mut effects = HashMap::new();
114
+
115
+ for (key, val) in map {
116
+ effects.insert(key.clone(), val);
117
+ }
118
+
119
+ Some(effects)
120
+ } else {
121
+ None
122
+ }
123
+ }
@@ -206,9 +206,35 @@ impl Parser {
206
206
  Value::String(token.lexeme.clone())
207
207
  }
208
208
  TokenKind::Number => {
209
- self.advance();
210
- Value::Number(token.lexeme.parse().unwrap_or(0.0))
209
+ let mut number_str = token.lexeme.clone();
210
+ self.advance(); // consume the first number
211
+
212
+ if let Some(dot_token) = self.peek_clone() {
213
+ if dot_token.kind == TokenKind::Dot {
214
+ self.advance(); // consume the dot
215
+
216
+ if let Some(decimal_token) = self.peek_clone() {
217
+ if decimal_token.kind == TokenKind::Number {
218
+ self.advance(); // consume the number after the dot
219
+ number_str.push('.');
220
+ number_str.push_str(&decimal_token.lexeme);
221
+ } else {
222
+ println!(
223
+ "Expected number after dot, got {:?}",
224
+ decimal_token
225
+ );
226
+ return Some(Value::Null);
227
+ }
228
+ } else {
229
+ println!("Expected number after dot, but reached EOF");
230
+ return Some(Value::Null);
231
+ }
232
+ }
233
+ }
234
+
235
+ Value::Number(number_str.parse::<f32>().unwrap_or(0.0))
211
236
  }
237
+
212
238
  TokenKind::Identifier => {
213
239
  self.advance();
214
240
  Value::Identifier(token.lexeme.clone())
@@ -15,15 +15,10 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
15
15
  // Parse namespaced identifier: .808.kick.snare
16
16
  let mut parts = Vec::new();
17
17
 
18
- loop {
19
- let Some(token) = parser.peek_clone() else {
20
- break;
21
- };
22
-
18
+ while let Some(token) = parser.peek_clone() {
23
19
  match token.kind {
24
- // Stop if we encounter a likely duration keyword
25
20
  TokenKind::Number => {
26
- // If there's a slash after the number, it's probably a fraction (1/4)
21
+ // Stop if it's part of a duration
27
22
  if let Some(TokenKind::Slash) = parser.peek_nth_kind(1) {
28
23
  break;
29
24
  }
@@ -33,7 +28,11 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
33
28
  }
34
29
 
35
30
  TokenKind::Identifier => {
36
- // Stop if it's the duration keyword "auto"
31
+ // Stop parsing entity name if next token is ':' or if already have one ident and current might be a param
32
+ if parts.len() >= 1 {
33
+ break; // we've already got the entity
34
+ }
35
+
37
36
  if token.lexeme == "auto" {
38
37
  break;
39
38
  }
@@ -43,7 +42,7 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
43
42
  }
44
43
 
45
44
  TokenKind::Dot => {
46
- parser.advance(); // consume dot
45
+ parser.advance(); // continue chaining
47
46
  }
48
47
 
49
48
  _ => {
@@ -52,7 +51,7 @@ pub fn parse_dot_token(parser: &mut Parser, _global_store: &mut GlobalStore) ->
52
51
  }
53
52
  }
54
53
 
55
- let entity = parts.join(".");
54
+ let entity = if parts.len() == 1 { parts[0].clone() } else { parts[..=1].join(".") };
56
55
 
57
56
  if entity.is_empty() {
58
57
  return Statement {
@@ -2,7 +2,7 @@ use std::path::{ Path, PathBuf };
2
2
  use crate::{
3
3
  common::cdn::get_cdn_url,
4
4
  config::loader::{ add_bank_to_config, load_config },
5
- utils::installer::{ download_file, extract_archive },
5
+ installer::utils::{ download_file, extract_archive },
6
6
  };
7
7
 
8
8
  pub async fn install_bank(name: &str, target_dir: &Path) -> Result<(), String> {
@@ -1 +1,2 @@
1
- pub mod bank;
1
+ pub mod bank;
2
+ pub mod utils;
package/rust/lib.rs CHANGED
@@ -1,7 +1,6 @@
1
1
  pub mod core;
2
2
  pub mod utils;
3
3
  pub mod config;
4
- pub mod common;
5
4
 
6
5
  use serde::{ Deserialize, Serialize };
7
6
  use wasm_bindgen::prelude::*;
package/rust/utils/mod.rs CHANGED
@@ -3,5 +3,4 @@ pub mod signature;
3
3
  pub mod spinner;
4
4
  pub mod watcher;
5
5
  pub mod file;
6
- pub mod logger;
7
- pub mod installer;
6
+ pub mod logger;
File without changes