@devaloop/devalang 0.0.1-alpha.3 β 0.0.1-alpha.4
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 +1 -1
- package/Cargo.toml +3 -3
- package/README.md +42 -25
- package/docs/CHANGELOG.md +17 -0
- package/docs/COMMANDS.md +31 -0
- package/docs/CONFIG.md +6 -4
- package/docs/ROADMAP.md +1 -1
- package/docs/TODO.md +4 -4
- package/examples/index.deva +7 -1
- package/examples/samples/hat-808.wav +0 -0
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +41 -41
- package/project-version.json +6 -6
- package/rust/audio/engine.rs +130 -0
- package/rust/audio/interpreter.rs +143 -0
- package/rust/audio/loader.rs +46 -0
- package/rust/audio/mod.rs +5 -0
- package/rust/audio/player.rs +54 -0
- package/rust/audio/render.rs +57 -0
- package/rust/cli/build.rs +17 -6
- package/rust/cli/mod.rs +29 -0
- package/rust/cli/play.rs +191 -0
- package/rust/config/mod.rs +3 -2
- package/rust/core/builder/mod.rs +48 -0
- package/rust/core/debugger/lexer.rs +20 -5
- package/rust/core/debugger/preprocessor.rs +9 -5
- package/rust/core/preprocessor/loader.rs +26 -15
- package/rust/core/preprocessor/resolver/bank.rs +46 -0
- package/rust/core/preprocessor/resolver/loop_.rs +143 -0
- package/rust/core/preprocessor/resolver/mod.rs +152 -0
- package/rust/core/preprocessor/resolver/tempo.rs +49 -0
- package/rust/core/preprocessor/resolver/trigger.rs +114 -0
- package/rust/main.rs +6 -0
- package/rust/utils/watcher.rs +10 -2
- package/rust/core/preprocessor/resolver.rs +0 -372
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.4"
|
|
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"
|
|
@@ -8,8 +8,8 @@ repository = "https://github.com/devaloop-labs/devalang"
|
|
|
8
8
|
keywords = ["music", "dsl", "audio", "cli"]
|
|
9
9
|
categories = ["command-line-utilities", "audio", "development-tools"]
|
|
10
10
|
readme = "README.md"
|
|
11
|
-
homepage = "https://
|
|
12
|
-
documentation = "https://docs.
|
|
11
|
+
homepage = "https://devalang.com"
|
|
12
|
+
documentation = "https://docs.devalang.com/"
|
|
13
13
|
edition = "2024"
|
|
14
14
|
|
|
15
15
|
[[bin]]
|
package/README.md
CHANGED
|
@@ -11,43 +11,42 @@
|
|
|
11
11
|

|
|
12
12
|

|
|
13
13
|
|
|
14
|
-

|
|
15
15
|
|
|
16
16
|
## πΌ Devalang, by **Devaloop Labs**
|
|
17
17
|
|
|
18
18
|
πΆ Compose music with code β simple, structured, sonic.
|
|
19
19
|
|
|
20
20
|
Devalang is a tiny domain-specific language (DSL) for music makers, sound designers, and audio hackers.
|
|
21
|
-
Compose loops, control samples, and
|
|
21
|
+
Compose loops, control samples, render and play audio β all in clean, readable text.
|
|
22
22
|
|
|
23
23
|
π¦ Whether you're building a track, shaping textures, or performing live, Devalang helps you think in rhythms. Itβs designed to be simple, expressive, and fast β because your ideas shouldnβt wait.
|
|
24
24
|
|
|
25
25
|
From studio sketches to live sets, Devalang gives you rhythmic control β with the elegance of code.
|
|
26
26
|
|
|
27
|
-
> π§ **v0.0.1-alpha.
|
|
27
|
+
> π§ **v0.0.1-alpha.4 Notice** π§
|
|
28
28
|
>
|
|
29
|
-
>
|
|
30
|
-
>
|
|
31
|
-
> You can parse code, generate the AST, and validate syntax β all essential building blocks for the upcoming audio engine.
|
|
32
|
-
>
|
|
33
|
-
> Custom instruments can be defined with `@load`, allowing any sound sample to be triggered with the same syntax.
|
|
29
|
+
> **Audio Engine** is now integrated, enabling audio playback and rendering capabilities.
|
|
34
30
|
>
|
|
35
31
|
> Currently, Devalang CLI is only available for **Windows**.
|
|
36
32
|
> Linux and macOS binaries will be added in future releases via cross-platform builds.
|
|
37
33
|
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## π Quick Access
|
|
37
|
+
|
|
38
|
+
- [π Documentation](./docs/)
|
|
39
|
+
- [π‘ Examples](./examples/)
|
|
40
|
+
- [π Project Website](https://devalang.com)
|
|
41
|
+
|
|
38
42
|
## π Features
|
|
39
43
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
- π Looping system with fixed repetitions (`loop 4:`)
|
|
47
|
-
- π§ͺ Instruction calls with parameters (e.g. `.kick auto {reverb:10, decay:20}`) for testing pattern syntax
|
|
48
|
-
- π `let` assignments for storing reusable values
|
|
49
|
-
- π `@load` assignment to load a sample (.mp3, .wav) to use it as a value
|
|
50
|
-
- π οΈ CLI tools for syntax checking (`check`), AST output (`build`)
|
|
44
|
+
- π΅ **Audio Engine**: Integrated audio playback and rendering
|
|
45
|
+
- π§© **Module system** for importing and exporting variables between files
|
|
46
|
+
- π **Structured AST** generation for debugging and future compilation
|
|
47
|
+
- π’ **Basic data types**: strings, numbers, booleans, maps, arrays
|
|
48
|
+
- ποΈ **Watch mode** for `build`, `check` and `play` commands
|
|
49
|
+
- π **Project templates** for quick setup
|
|
51
50
|
|
|
52
51
|
## π Installation
|
|
53
52
|
|
|
@@ -79,7 +78,7 @@ npx @devaloop/devalang <command>
|
|
|
79
78
|
> cargo install --path .
|
|
80
79
|
```
|
|
81
80
|
|
|
82
|
-
|
|
81
|
+
Development usage (you can customize arguments in package.json)
|
|
83
82
|
|
|
84
83
|
```bash
|
|
85
84
|
# For syntax checking test
|
|
@@ -90,6 +89,12 @@ npm run rust:dev:build
|
|
|
90
89
|
|
|
91
90
|
## β Usage
|
|
92
91
|
|
|
92
|
+
NOTE: Commands are available via `devalang` or `npx @devaloop/devalang`.
|
|
93
|
+
|
|
94
|
+
NOTE: Arguments can be passed to commands using `--<argument>` syntax. You can also use a configuration file to set default values for various settings, making it easier to manage your Devalang project.
|
|
95
|
+
|
|
96
|
+
NOTE: Some commands require a mandatory `--entry` argument to specify the input folder, and a `--output` argument to specify the output folder. If not specified, they default to `./src` and `./output` respectively.
|
|
97
|
+
|
|
93
98
|
For more examples, see [docs/COMMANDS.md](./docs/COMMANDS.md)
|
|
94
99
|
|
|
95
100
|
### Initialize a new project
|
|
@@ -109,13 +114,25 @@ devalang init --name <project-name> --template <template-name>
|
|
|
109
114
|
### Checking syntax only
|
|
110
115
|
|
|
111
116
|
```bash
|
|
112
|
-
devalang check --
|
|
117
|
+
devalang check --watch
|
|
113
118
|
```
|
|
114
119
|
|
|
115
120
|
### Building output files
|
|
116
121
|
|
|
117
122
|
```bash
|
|
118
|
-
devalang build --
|
|
123
|
+
devalang build --watch
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Playing audio files (once by file change)
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
devalang play --watch
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Playing audio files (continuous playback, even without file changes)
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
devalang play --repeat
|
|
119
136
|
```
|
|
120
137
|
|
|
121
138
|
## βοΈ Configuration
|
|
@@ -135,6 +152,8 @@ For more examples, see [docs/SYNTAX.md](./docs/SYNTAX.md)
|
|
|
135
152
|
|
|
136
153
|
@import { globalBpm, globalBank, kickDuration } from "global.deva"
|
|
137
154
|
|
|
155
|
+
@load "./examples/samples/kick-808.wav" as customKick
|
|
156
|
+
|
|
138
157
|
bpm globalBpm
|
|
139
158
|
# Will declare the tempo at the globalBpm variable beats per minute
|
|
140
159
|
|
|
@@ -142,7 +161,7 @@ bank globalBank
|
|
|
142
161
|
# Will declare a custom instrument bank using the globalBank variable
|
|
143
162
|
|
|
144
163
|
loop 5:
|
|
145
|
-
.
|
|
164
|
+
.customKick kickDuration {reverb=50, drive=25}
|
|
146
165
|
# Will play 5 times a kick for the duration of the kickDuration variable with reverb and drive effects
|
|
147
166
|
```
|
|
148
167
|
|
|
@@ -158,7 +177,6 @@ let kickDuration = 500
|
|
|
158
177
|
|
|
159
178
|
## π§― Known issues
|
|
160
179
|
|
|
161
|
-
- No support yet for Audio Engine
|
|
162
180
|
- No support yet for `if`, `else`, `else if` statements
|
|
163
181
|
- No support yet for `@group`, `@pattern`, `@function` statements
|
|
164
182
|
- No support yet for cross-platform builds (Linux, macOS)
|
|
@@ -167,7 +185,6 @@ let kickDuration = 500
|
|
|
167
185
|
|
|
168
186
|
For more info, see [docs/ROADMAP.md](./docs/ROADMAP.md)
|
|
169
187
|
|
|
170
|
-
- β³ Audio engine integration (priority for alpha.4)
|
|
171
188
|
- β³ Other statements (e.g `if`, `@group`, ...)
|
|
172
189
|
- β³ Cross-platform support (Linux, macOS)
|
|
173
190
|
- β³ More built-in instruments (e.g. snare, hi-hat, etc.)
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@
|
|
|
4
4
|
|
|
5
5
|
# Changelog
|
|
6
6
|
|
|
7
|
+
## Version 0.0.1-alpha.4 (2025-07-03)
|
|
8
|
+
|
|
9
|
+
### Audio Engine
|
|
10
|
+
|
|
11
|
+
- Integrated Audio Engine to handle audio playback and rendering.
|
|
12
|
+
- Implemented Audio Player to play audio files.
|
|
13
|
+
- Added support for audio playback with the `play` command.
|
|
14
|
+
|
|
15
|
+
### Commands
|
|
16
|
+
|
|
17
|
+
- Implemented `play` command to play Devalang files.
|
|
18
|
+
- Added `--watch` option to watch for changes in files and automatically rebuild and play them. (once)
|
|
19
|
+
- Added `--repeat` option to repeat the playback of the audio file. (infinite)
|
|
20
|
+
|
|
21
|
+
Note : You cannot use `--watch` and `--repeat` options together. Use `--repeat` instead.
|
|
22
|
+
|
|
23
|
+
|
|
7
24
|
## Version 0.0.1-alpha.3 (2025-07-01)
|
|
8
25
|
|
|
9
26
|
- /!\ Major refactor of the project structure and module system /!\
|
package/docs/COMMANDS.md
CHANGED
|
@@ -52,3 +52,34 @@ Available arguments :
|
|
|
52
52
|
- `--entry`: The input folder (default to `./src`)
|
|
53
53
|
- `--output`: The output folder (default to `./output`)
|
|
54
54
|
- `--watch`: Whether to watch for changes and rebuild (default to `false`)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Playing
|
|
58
|
+
|
|
59
|
+
Playing .deva file(s) without audio playback (once)
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
devalang play --entry ./examples --output ./output
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Playing .deva file(s) with audio playback (once by file change)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
devalang play --entry ./examples --output ./output --watch
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Playing .deva file(s) with audio playback (infinite loop)
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
devalang play --entry ./examples --output ./output --repeat
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Note : You cannot use `--watch` and `--repeat` options together. Use `--repeat` instead.
|
|
78
|
+
|
|
79
|
+
Available arguments :
|
|
80
|
+
|
|
81
|
+
- `--no-config`: Whether to ignore the configuration file (default to `false`)
|
|
82
|
+
- `--entry`: The input folder (default to `./src`)
|
|
83
|
+
- `--output`: The output folder (default to `./output`)
|
|
84
|
+
- `--watch`: Whether to watch for changes and rebuild + play (default to `false`)
|
|
85
|
+
- `--repeat`: Whether to repeat the playback of the audio file (default to `false`)
|
package/docs/CONFIG.md
CHANGED
|
@@ -18,11 +18,13 @@ The configuration file is a TOML (Tom's Obvious, Minimal Language) file that con
|
|
|
18
18
|
[defaults]
|
|
19
19
|
entry = "./src"
|
|
20
20
|
output = "./output"
|
|
21
|
-
watch =
|
|
21
|
+
watch = false
|
|
22
|
+
repeat = true
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
### Available Settings
|
|
25
26
|
|
|
26
|
-
- `entry`: (String) The entry point for your Devalang project
|
|
27
|
-
- `output`: (String) The output directory for generated files
|
|
28
|
-
- `watch`: (Boolean) Whether to watch for changes in files and automatically rebuild or check them
|
|
27
|
+
- `entry`: (String) The entry point for your Devalang project (default to `./src`)
|
|
28
|
+
- `output`: (String) The output directory for generated files (default to `./output`)
|
|
29
|
+
- `watch`: (Boolean) Whether to watch for changes in files and automatically rebuild or check them (default to `false`)
|
|
30
|
+
- `repeat`: (Boolean) Whether to repeat the playback of audio files (default to `false`)
|
package/docs/ROADMAP.md
CHANGED
|
@@ -8,6 +8,7 @@ Devalang is a work in progress. Hereβs what weβre planning next:
|
|
|
8
8
|
|
|
9
9
|
### Stable
|
|
10
10
|
|
|
11
|
+
- β
**Audio engine**: Integrate the audio engine for sound playback.
|
|
11
12
|
- β
**Basic syntax**: Implement the core syntax for Devalang, including data types and basic statements.
|
|
12
13
|
- β
**Watch mode**: Add a watch mode to automatically rebuild on file changes.
|
|
13
14
|
- β
**Module system**: Add support for importing and exporting variables between files using `@import` and `@export`.
|
|
@@ -22,7 +23,6 @@ Devalang is a work in progress. Hereβs what weβre planning next:
|
|
|
22
23
|
|
|
23
24
|
### Upcoming
|
|
24
25
|
|
|
25
|
-
- β³ **Audio engine**: Integrate the audio engine for sound playback.
|
|
26
26
|
- β³ **VSCode extension**: Create a VSCode extension for syntax highlighting and code completion.
|
|
27
27
|
- β³ **WASM support**: Compile Devalang to WebAssembly for use in web applications.
|
|
28
28
|
- β³ **Other statements**: Implement `if`, `else`, and other control structures.
|
package/docs/TODO.md
CHANGED
|
@@ -24,13 +24,13 @@ This is a list of tasks and features to be implemented in Devalang. Note that th
|
|
|
24
24
|
- [ ] Implement debug mode
|
|
25
25
|
- [ ] Implement compilation mode
|
|
26
26
|
- [ ] Implement compression mode
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
27
|
+
- [x] Play
|
|
28
|
+
- [x] Implement Audio Engine
|
|
29
|
+
- [x] Implement loop mode
|
|
30
30
|
|
|
31
31
|
## Core components
|
|
32
32
|
|
|
33
|
-
- [
|
|
33
|
+
- [x] Audio Engine
|
|
34
34
|
- [x] Lexer
|
|
35
35
|
- [x] Parser
|
|
36
36
|
- [x] Preprocessor
|
package/examples/index.deva
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
@import { duration, default_bank, params, loopCount, tempo } from "./examples/exported.deva"
|
|
2
2
|
|
|
3
3
|
@load "./examples/samples/kick-808.wav" as sample
|
|
4
|
+
@load "./examples/samples/hat-808.wav" as hat
|
|
4
5
|
|
|
5
6
|
bpm tempo
|
|
6
7
|
|
|
7
8
|
bank default_bank
|
|
8
9
|
|
|
9
10
|
loop loopCount:
|
|
10
|
-
.sample duration params
|
|
11
|
+
.sample duration params
|
|
12
|
+
|
|
13
|
+
# Uncomment the next line (.hat) while executing "play" command
|
|
14
|
+
# with `--repeat` option to see magic happen !
|
|
15
|
+
|
|
16
|
+
# .hat duration params
|
|
Binary file
|
package/out-tsc/bin/devalang.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@devaloop/devalang",
|
|
3
|
-
"private": false,
|
|
4
|
-
"version": "0.0.1-alpha.
|
|
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
|
-
"main": "out-tsc/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"devalang": "./out-tsc/bin/index.js"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"prepublish": "cargo build --release && npm run script:postbuild",
|
|
12
|
-
"rust:dev:build": "cargo run build --entry examples --output output",
|
|
13
|
-
"rust:dev:check": "cargo run check --entry examples --output output",
|
|
14
|
-
"script:postbuild": "tsc && node out-tsc/scripts/postbuild.js",
|
|
15
|
-
"script:version:bump": "tsc && node out-tsc/scripts/version/index.js"
|
|
16
|
-
},
|
|
17
|
-
"homepage": "https://
|
|
18
|
-
"keywords": [
|
|
19
|
-
"devalang",
|
|
20
|
-
"music",
|
|
21
|
-
"sound",
|
|
22
|
-
"domain-specific language",
|
|
23
|
-
"dsl",
|
|
24
|
-
"programming language",
|
|
25
|
-
"sound design",
|
|
26
|
-
"music hacking",
|
|
27
|
-
"audio",
|
|
28
|
-
"synthesis",
|
|
29
|
-
"scripting",
|
|
30
|
-
"sound synthesis",
|
|
31
|
-
"music programming"
|
|
32
|
-
],
|
|
33
|
-
"author": "Devaloop",
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"repository": {
|
|
36
|
-
"type": "git",
|
|
37
|
-
"url": "https://github.com/devaloop-labs/devalang.git"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"@types/node": "^24.0.3"
|
|
41
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@devaloop/devalang",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.0.1-alpha.4",
|
|
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
|
+
"main": "out-tsc/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"devalang": "./out-tsc/bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepublish": "cargo build --release && npm run script:postbuild",
|
|
12
|
+
"rust:dev:build": "cargo run build --entry examples --output output",
|
|
13
|
+
"rust:dev:check": "cargo run check --entry examples --output output",
|
|
14
|
+
"script:postbuild": "tsc && node out-tsc/scripts/postbuild.js",
|
|
15
|
+
"script:version:bump": "tsc && node out-tsc/scripts/version/index.js"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://devalang.com",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"devalang",
|
|
20
|
+
"music",
|
|
21
|
+
"sound",
|
|
22
|
+
"domain-specific language",
|
|
23
|
+
"dsl",
|
|
24
|
+
"programming language",
|
|
25
|
+
"sound design",
|
|
26
|
+
"music hacking",
|
|
27
|
+
"audio",
|
|
28
|
+
"synthesis",
|
|
29
|
+
"scripting",
|
|
30
|
+
"sound synthesis",
|
|
31
|
+
"music programming"
|
|
32
|
+
],
|
|
33
|
+
"author": "Devaloop",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/devaloop-labs/devalang.git"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@types/node": "^24.0.3"
|
|
41
|
+
}
|
|
42
42
|
}
|
package/project-version.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "0.0.1-alpha.
|
|
3
|
-
"channel": "alpha",
|
|
4
|
-
"lastCommit": "
|
|
5
|
-
"build":
|
|
6
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"version": "0.0.1-alpha.4",
|
|
3
|
+
"channel": "alpha",
|
|
4
|
+
"lastCommit": "ca5336b3cd3f2189971e9e99a93a66042faff007",
|
|
5
|
+
"build": 3
|
|
6
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
use std::{ collections::HashMap, fs::File, io::BufReader };
|
|
2
|
+
use hound::{ SampleFormat, WavSpec, WavWriter };
|
|
3
|
+
use rodio::{ Decoder, Source };
|
|
4
|
+
|
|
5
|
+
use crate::core::{
|
|
6
|
+
parser::statement::Statement,
|
|
7
|
+
store::variable::VariableTable,
|
|
8
|
+
utils::path::normalize_path,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const SAMPLE_RATE: u32 = 44100;
|
|
12
|
+
const CHANNELS: u16 = 2;
|
|
13
|
+
|
|
14
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
15
|
+
pub struct AudioEngine {
|
|
16
|
+
pub volume: f32,
|
|
17
|
+
pub variables: VariableTable,
|
|
18
|
+
pub buffer: Vec<i16>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl AudioEngine {
|
|
22
|
+
pub fn new() -> Self {
|
|
23
|
+
AudioEngine {
|
|
24
|
+
volume: 1.0,
|
|
25
|
+
buffer: vec![],
|
|
26
|
+
variables: VariableTable::new(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn mix(&mut self, other: &AudioEngine) {
|
|
31
|
+
let max_len = self.buffer.len().max(other.buffer.len());
|
|
32
|
+
self.buffer.resize(max_len, 0);
|
|
33
|
+
|
|
34
|
+
for (i, &sample) in other.buffer.iter().enumerate() {
|
|
35
|
+
self.buffer[i] = self.buffer[i].saturating_add(sample);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn set_duration(&mut self, duration_secs: f32) {
|
|
40
|
+
let mut total_samples = (duration_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
|
|
41
|
+
|
|
42
|
+
if total_samples % (CHANNELS as usize) != 0 {
|
|
43
|
+
total_samples += 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
self.buffer.resize(total_samples, 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn set_variables(&mut self, variables: VariableTable) {
|
|
50
|
+
self.variables = variables;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn generate_wav_file(&mut self, output_dir: &String) -> Result<(), String> {
|
|
54
|
+
if self.buffer.len() % (CHANNELS as usize) != 0 {
|
|
55
|
+
self.buffer.push(0);
|
|
56
|
+
println!("Completed buffer to respect stereo format.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let spec = WavSpec {
|
|
60
|
+
channels: CHANNELS,
|
|
61
|
+
sample_rate: SAMPLE_RATE,
|
|
62
|
+
bits_per_sample: 16,
|
|
63
|
+
sample_format: SampleFormat::Int,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let mut writer = WavWriter::create(output_dir, spec).map_err(|e|
|
|
67
|
+
format!("Error creating WAV file: {}", e)
|
|
68
|
+
)?;
|
|
69
|
+
|
|
70
|
+
for sample in &self.buffer {
|
|
71
|
+
writer.write_sample(*sample).map_err(|e| format!("Error writing sample: {:?}", e))?;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
writer.finalize().map_err(|e| format!("Error finalizing WAV: {:?}", e))?;
|
|
75
|
+
|
|
76
|
+
Ok(())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub fn insert(
|
|
80
|
+
&mut self,
|
|
81
|
+
filepath: &str,
|
|
82
|
+
time_secs: f32,
|
|
83
|
+
dur_sec: f32,
|
|
84
|
+
effects: Option<HashMap<String, f32>>
|
|
85
|
+
) {
|
|
86
|
+
let normalized_filepath = normalize_path(filepath);
|
|
87
|
+
|
|
88
|
+
let file = BufReader::new(
|
|
89
|
+
File::open(normalized_filepath).expect("Failed to open audio file")
|
|
90
|
+
);
|
|
91
|
+
let decoder = Decoder::new(file).expect("Failed to decode audio file");
|
|
92
|
+
|
|
93
|
+
// Mono or stereo reading possible here, we will duplicate in L/R
|
|
94
|
+
let max_mono_samples = (dur_sec * (SAMPLE_RATE as f32)) as usize;
|
|
95
|
+
let samples: Vec<i16> = decoder.convert_samples().take(max_mono_samples).collect();
|
|
96
|
+
|
|
97
|
+
if samples.is_empty() {
|
|
98
|
+
eprintln!("No samples found in the audio file: {}", filepath);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// TODO Apply effects here if needed
|
|
103
|
+
let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
|
|
104
|
+
let required_len = offset + samples.len() * (CHANNELS as usize);
|
|
105
|
+
let padded_required_len = if required_len % 2 == 1 {
|
|
106
|
+
required_len + 1
|
|
107
|
+
} else {
|
|
108
|
+
required_len
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
self.buffer.resize(padded_required_len, 0);
|
|
112
|
+
self.pad_samples(&samples, time_secs);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn pad_samples(&mut self, samples: &[i16], time_secs: f32) {
|
|
116
|
+
let offset = (time_secs * (SAMPLE_RATE as f32) * (CHANNELS as f32)) as usize;
|
|
117
|
+
|
|
118
|
+
for (i, &sample) in samples.iter().enumerate() {
|
|
119
|
+
let adjusted_sample = ((sample as f32) * self.volume).round() as i16;
|
|
120
|
+
|
|
121
|
+
let left_pos = offset + i * 2;
|
|
122
|
+
let right_pos = left_pos + 1;
|
|
123
|
+
|
|
124
|
+
if right_pos < self.buffer.len() {
|
|
125
|
+
self.buffer[left_pos] = self.buffer[left_pos].saturating_add(adjusted_sample); // gauche
|
|
126
|
+
self.buffer[right_pos] = self.buffer[right_pos].saturating_add(adjusted_sample); // droite
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
audio::{ engine::AudioEngine, loader::load_trigger },
|
|
3
|
+
core::{
|
|
4
|
+
parser::statement::{ Statement, StatementKind },
|
|
5
|
+
shared::value::Value,
|
|
6
|
+
store::variable::VariableTable,
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
pub fn interprete_statements(
|
|
11
|
+
statements: &Vec<Statement>,
|
|
12
|
+
audio_engine: AudioEngine,
|
|
13
|
+
entry: String,
|
|
14
|
+
output: String
|
|
15
|
+
) -> (AudioEngine, f32, f32) {
|
|
16
|
+
let mut base_bpm = 120.0;
|
|
17
|
+
let mut base_duration = 60.0 / base_bpm;
|
|
18
|
+
|
|
19
|
+
let variable_table = audio_engine.variables.clone();
|
|
20
|
+
|
|
21
|
+
let (updated_audio_engine, base_bpm, max_end_time) = execute_audio_statements(
|
|
22
|
+
audio_engine.clone(),
|
|
23
|
+
variable_table.clone(),
|
|
24
|
+
statements.clone(),
|
|
25
|
+
base_bpm.clone(),
|
|
26
|
+
base_duration.clone(),
|
|
27
|
+
0.0,
|
|
28
|
+
0.0
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
(updated_audio_engine, base_bpm, max_end_time)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pub fn execute_audio_statements(
|
|
35
|
+
mut audio_engine: AudioEngine,
|
|
36
|
+
mut variable_table: VariableTable,
|
|
37
|
+
mut statements: Vec<Statement>,
|
|
38
|
+
mut base_bpm: f32,
|
|
39
|
+
mut base_duration: f32,
|
|
40
|
+
mut max_end_time: f32,
|
|
41
|
+
mut cursor_time: f32
|
|
42
|
+
) -> (AudioEngine, f32, f32) {
|
|
43
|
+
for stmt in statements {
|
|
44
|
+
match &stmt.kind {
|
|
45
|
+
StatementKind::Load { source, alias } => {
|
|
46
|
+
variable_table.set(alias.to_string(), Value::String(source.clone()));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
StatementKind::Let { name } => {
|
|
50
|
+
variable_table.set(name.to_string(), stmt.value.clone());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
StatementKind::Tempo => {
|
|
54
|
+
if let Value::Number(bpm_) = &stmt.value {
|
|
55
|
+
base_bpm = *bpm_ as f32;
|
|
56
|
+
base_duration = 60.0 / base_bpm;
|
|
57
|
+
} else {
|
|
58
|
+
eprintln!("β Invalid tempo value: {:?}", stmt.value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
StatementKind::Trigger { entity, duration } => {
|
|
63
|
+
if let Some(trigger_val) = variable_table.get(entity) {
|
|
64
|
+
let (src, duration_secs) = load_trigger(
|
|
65
|
+
trigger_val,
|
|
66
|
+
duration,
|
|
67
|
+
base_duration,
|
|
68
|
+
variable_table.clone()
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
audio_engine.insert(&src, cursor_time, duration_secs, None);
|
|
72
|
+
|
|
73
|
+
cursor_time += duration_secs;
|
|
74
|
+
|
|
75
|
+
if cursor_time > max_end_time {
|
|
76
|
+
max_end_time = cursor_time;
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
eprintln!("β Unknown trigger entity: {}", entity);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
StatementKind::Loop => {
|
|
84
|
+
if let Value::Map(loop_value) = &stmt.value {
|
|
85
|
+
let iterator = loop_value.get("iterator");
|
|
86
|
+
let body = loop_value.get("body");
|
|
87
|
+
|
|
88
|
+
let loop_count = if let Some(Value::Number(n)) = iterator {
|
|
89
|
+
*n as usize
|
|
90
|
+
} else {
|
|
91
|
+
eprintln!("β Loop iterator must be a number: {:?}", iterator);
|
|
92
|
+
continue;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
let loop_body = if let Some(Value::Block(body)) = body {
|
|
96
|
+
body.clone()
|
|
97
|
+
} else {
|
|
98
|
+
eprintln!("β Loop body must be a block: {:?}", body);
|
|
99
|
+
continue;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for _ in 0..loop_count {
|
|
103
|
+
let (loop_engine, _, loop_end_time) = execute_audio_statements(
|
|
104
|
+
audio_engine.clone(),
|
|
105
|
+
variable_table.clone(),
|
|
106
|
+
loop_body.clone(),
|
|
107
|
+
base_bpm,
|
|
108
|
+
base_duration,
|
|
109
|
+
max_end_time,
|
|
110
|
+
cursor_time
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
audio_engine = loop_engine;
|
|
114
|
+
|
|
115
|
+
// Update time and max_end_time after each loop iteration
|
|
116
|
+
cursor_time = loop_end_time;
|
|
117
|
+
if loop_end_time > max_end_time {
|
|
118
|
+
max_end_time = loop_end_time;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
StatementKind::Bank => {}
|
|
125
|
+
|
|
126
|
+
StatementKind::Import { names, source } => {}
|
|
127
|
+
|
|
128
|
+
StatementKind::Export { names, source } => {}
|
|
129
|
+
|
|
130
|
+
StatementKind::Unknown => {
|
|
131
|
+
// Ignore unknown statements
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_ => {
|
|
135
|
+
eprintln!("Unsupported statement kind: {:?}", stmt);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
audio_engine.set_variables(variable_table);
|
|
141
|
+
|
|
142
|
+
(audio_engine, base_bpm, max_end_time)
|
|
143
|
+
}
|