@devaloop/devalang 0.0.1-alpha.2 → 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 +46 -46
- package/README.md +48 -30
- package/docs/CHANGELOG.md +28 -6
- package/docs/COMMANDS.md +31 -0
- package/docs/CONFIG.md +6 -4
- package/docs/ROADMAP.md +5 -1
- package/docs/TODO.md +10 -35
- package/examples/exported.deva +1 -1
- package/examples/index.deva +8 -1
- package/examples/samples/hat-808.wav +0 -0
- package/out-tsc/bin/devalang.exe +0 -0
- package/package.json +41 -42
- package/project-version.json +5 -5
- 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 -1
- package/rust/audio/player.rs +54 -0
- package/rust/audio/render.rs +57 -0
- package/rust/cli/build.rs +73 -45
- package/rust/cli/check.rs +47 -111
- package/rust/cli/init.rs +1 -1
- package/rust/cli/mod.rs +203 -2
- package/rust/cli/play.rs +191 -0
- package/rust/{utils/config.rs → config/loader.rs} +3 -2
- package/rust/config/mod.rs +16 -0
- package/rust/core/builder/mod.rs +69 -27
- package/rust/core/debugger/lexer.rs +27 -0
- package/rust/core/debugger/mod.rs +12 -49
- package/rust/core/debugger/preprocessor.rs +27 -0
- package/rust/core/error/mod.rs +60 -0
- package/rust/core/lexer/{at.rs → handler/at.rs} +1 -1
- package/rust/core/lexer/{brace.rs → handler/brace.rs} +1 -1
- package/rust/core/lexer/{colon.rs → handler/colon.rs} +1 -1
- package/rust/core/lexer/{comment.rs → handler/comment.rs} +3 -3
- package/rust/core/lexer/{dot.rs → handler/dot.rs} +1 -1
- package/rust/core/lexer/{equal.rs → handler/equal.rs} +1 -1
- package/rust/core/lexer/{identifier.rs → handler/identifier.rs} +1 -1
- package/rust/core/lexer/{indent.rs → handler/indent.rs} +10 -5
- package/rust/core/lexer/handler/mod.rs +238 -0
- package/rust/core/lexer/{newline.rs → handler/newline.rs} +6 -10
- package/rust/core/lexer/{number.rs → handler/number.rs} +1 -1
- package/rust/core/lexer/handler/string.rs +66 -0
- package/rust/core/lexer/mod.rs +25 -14
- package/rust/core/lexer/token.rs +55 -0
- package/rust/core/mod.rs +5 -2
- package/rust/core/parser/handler/at.rs +166 -0
- package/rust/core/parser/handler/bank.rs +38 -0
- package/rust/core/parser/handler/dot.rs +112 -0
- package/rust/core/parser/handler/identifier.rs +134 -0
- package/rust/core/parser/handler/loop_.rs +55 -0
- package/rust/core/parser/handler/mod.rs +6 -0
- package/rust/core/parser/handler/tempo.rs +47 -0
- package/rust/core/parser/mod.rs +204 -166
- package/rust/core/parser/statement.rs +91 -0
- package/rust/core/preprocessor/loader.rs +116 -0
- package/rust/core/preprocessor/mod.rs +2 -24
- package/rust/core/preprocessor/module.rs +37 -56
- package/rust/core/preprocessor/processor.rs +41 -0
- package/rust/core/preprocessor/resolver/bank.rs +38 -51
- package/rust/core/preprocessor/resolver/loop_.rs +126 -65
- package/rust/core/preprocessor/resolver/mod.rs +119 -80
- package/rust/core/preprocessor/resolver/tempo.rs +40 -61
- package/rust/core/preprocessor/resolver/trigger.rs +93 -155
- package/rust/core/shared/duration.rs +8 -0
- package/rust/core/shared/mod.rs +2 -0
- package/rust/core/shared/value.rs +18 -0
- package/rust/core/store/export.rs +28 -0
- package/rust/core/store/global.rs +39 -0
- package/rust/core/store/import.rs +28 -0
- package/rust/core/store/mod.rs +4 -0
- package/rust/core/store/variable.rs +28 -0
- package/rust/core/utils/mod.rs +2 -0
- package/rust/core/utils/validation.rs +35 -0
- package/rust/lib.rs +0 -1
- package/rust/main.rs +22 -18
- package/rust/utils/logger.rs +69 -34
- package/rust/utils/mod.rs +3 -5
- package/rust/utils/watcher.rs +10 -2
- package/templates/minimal/.devalang +1 -1
- package/templates/welcome/.devalang +1 -1
- package/rust/core/lexer/bracket.rs +0 -41
- package/rust/core/lexer/driver.rs +0 -286
- package/rust/core/lexer/quote.rs +0 -61
- package/rust/core/parser/at.rs +0 -142
- package/rust/core/parser/bank.rs +0 -42
- package/rust/core/parser/dot.rs +0 -137
- package/rust/core/parser/identifer.rs +0 -91
- package/rust/core/parser/loop_.rs +0 -62
- package/rust/core/parser/tempo.rs +0 -42
- package/rust/core/parser/variable.rs +0 -129
- package/rust/core/preprocessor/dependencies.rs +0 -54
- package/rust/core/preprocessor/resolver/at.rs +0 -24
- package/rust/core/types/cli.rs +0 -182
- package/rust/core/types/config.rs +0 -15
- package/rust/core/types/mod.rs +0 -8
- package/rust/core/types/module.rs +0 -41
- package/rust/core/types/parser.rs +0 -73
- package/rust/core/types/statement.rs +0 -105
- package/rust/core/types/store.rs +0 -116
- package/rust/core/types/token.rs +0 -83
- package/rust/core/types/variable.rs +0 -32
- package/rust/runner/executer.rs +0 -44
- package/rust/runner/mod.rs +0 -1
- /package/rust/{utils → core/utils}/path.rs +0 -0
- /package/rust/utils/{loader.rs → spinner.rs} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
use crate::core::{ shared::{ duration::Duration, value::Value }, store::variable::VariableTable };
|
|
2
|
+
|
|
3
|
+
pub fn load_trigger(
|
|
4
|
+
trigger: &Value,
|
|
5
|
+
duration: &Duration,
|
|
6
|
+
base_duration: f32,
|
|
7
|
+
variable_table: VariableTable
|
|
8
|
+
) -> (String, f32) {
|
|
9
|
+
let mut trigger_path = String::new();
|
|
10
|
+
let mut duration_as_secs = 0.0;
|
|
11
|
+
|
|
12
|
+
match trigger {
|
|
13
|
+
Value::String(src) => {
|
|
14
|
+
trigger_path = src.to_string();
|
|
15
|
+
}
|
|
16
|
+
_ => {
|
|
17
|
+
eprintln!("❌ Invalid trigger type. Expected a text variable.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
match duration {
|
|
22
|
+
Duration::Identifier(duration_identifier) => {
|
|
23
|
+
if duration_identifier == "auto" {
|
|
24
|
+
duration_as_secs = base_duration;
|
|
25
|
+
} else if let Some(Value::Number(num)) = variable_table.get(duration_identifier) {
|
|
26
|
+
duration_as_secs = *num;
|
|
27
|
+
} else {
|
|
28
|
+
eprintln!("❌ Invalid duration identifier: {}", duration_identifier);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Duration::Number(num) => {
|
|
33
|
+
duration_as_secs = *num;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Duration::Auto => {
|
|
37
|
+
duration_as_secs = base_duration;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_ => {
|
|
41
|
+
eprintln!("❌ Invalid duration type. Expected an identifier.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
(trigger_path, duration_as_secs)
|
|
46
|
+
}
|
package/rust/audio/mod.rs
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
use rodio::{ Decoder, OutputStream, OutputStreamHandle, Sink, Source };
|
|
2
|
+
use std::{ fs::File, io::BufReader };
|
|
3
|
+
|
|
4
|
+
pub struct AudioPlayer {
|
|
5
|
+
_stream: OutputStream,
|
|
6
|
+
handle: OutputStreamHandle,
|
|
7
|
+
sink: Sink,
|
|
8
|
+
last_path: Option<String>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl AudioPlayer {
|
|
12
|
+
pub fn new() -> Self {
|
|
13
|
+
let (stream, handle) = OutputStream::try_default().unwrap();
|
|
14
|
+
let sink = Sink::try_new(&handle).unwrap();
|
|
15
|
+
|
|
16
|
+
Self {
|
|
17
|
+
_stream: stream,
|
|
18
|
+
handle,
|
|
19
|
+
sink,
|
|
20
|
+
last_path: None,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn load_source(&self, path: &str) -> impl Source<Item = f32> + Send + 'static {
|
|
25
|
+
let file = File::open(path).unwrap();
|
|
26
|
+
let reader = BufReader::new(file);
|
|
27
|
+
|
|
28
|
+
Decoder::new(reader).unwrap().convert_samples()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub fn play_file_once(&mut self, path: &str) {
|
|
32
|
+
self.sink.stop();
|
|
33
|
+
self.sink = Sink::try_new(&self.handle).unwrap();
|
|
34
|
+
|
|
35
|
+
self.sink.set_volume(1.0);
|
|
36
|
+
|
|
37
|
+
let source = self.load_source(path);
|
|
38
|
+
|
|
39
|
+
self.sink.append(source);
|
|
40
|
+
self.last_path = Some(path.to_string());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn replay_last(&mut self) {
|
|
44
|
+
if let Some(path) = self.last_path.clone() {
|
|
45
|
+
self.play_file_once(&path);
|
|
46
|
+
} else {
|
|
47
|
+
eprintln!("⚠️ No previous audio to replay.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn wait_until_end(&self) {
|
|
52
|
+
self.sink.sleep_until_end();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
|
|
3
|
+
use crate::{
|
|
4
|
+
audio::{ engine::AudioEngine, interpreter::interprete_statements },
|
|
5
|
+
core::{
|
|
6
|
+
parser::statement::Statement,
|
|
7
|
+
store::global::GlobalStore,
|
|
8
|
+
},
|
|
9
|
+
utils::logger::{ LogLevel, Logger },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn render_audio_with_modules(
|
|
13
|
+
modules: HashMap<String, Vec<Statement>>,
|
|
14
|
+
output_dir: &str,
|
|
15
|
+
global_store: &mut GlobalStore
|
|
16
|
+
) -> HashMap<String, AudioEngine> {
|
|
17
|
+
let mut result = HashMap::new();
|
|
18
|
+
|
|
19
|
+
for (module_name, statements) in modules {
|
|
20
|
+
let mut global_max_end_time = 0.0;
|
|
21
|
+
let mut audio_engine = AudioEngine::new();
|
|
22
|
+
|
|
23
|
+
// Apply the module's variable table if it exists
|
|
24
|
+
if let Some(module) = global_store.get_module(&module_name) {
|
|
25
|
+
audio_engine.set_variables(module.variable_table.clone());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Interpret the statements to fill the audio buffer
|
|
29
|
+
let (mut audio_engine, module_base_bpm, module_max_end_time) = interprete_statements(
|
|
30
|
+
&statements,
|
|
31
|
+
audio_engine,
|
|
32
|
+
module_name.clone(),
|
|
33
|
+
output_dir.to_string()
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Calculate the module's maximum duration
|
|
37
|
+
global_max_end_time = module_max_end_time.max(global_max_end_time);
|
|
38
|
+
audio_engine.set_duration(global_max_end_time);
|
|
39
|
+
|
|
40
|
+
// Check if the buffer contains at least one non-zero sample
|
|
41
|
+
if audio_engine.buffer.iter().all(|&s| s == 0) {
|
|
42
|
+
let logger = Logger::new();
|
|
43
|
+
|
|
44
|
+
logger.log_message(
|
|
45
|
+
LogLevel::Warning,
|
|
46
|
+
format!("Module '{}' ignored: silent buffer (no non-zero samples)", module_name).as_str()
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Insert only if the module produces sound
|
|
53
|
+
result.insert(module_name, audio_engine);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
result
|
|
57
|
+
}
|
package/rust/cli/build.rs
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
use std::{ thread, time::Duration };
|
|
2
|
-
|
|
3
1
|
use crate::{
|
|
2
|
+
audio::render::render_audio_with_modules,
|
|
3
|
+
config::Config,
|
|
4
4
|
core::{
|
|
5
|
-
builder::
|
|
6
|
-
debugger::
|
|
7
|
-
preprocessor::
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
runner::executer::execute_statements,
|
|
11
|
-
utils::{
|
|
12
|
-
loader::with_spinner,
|
|
13
|
-
logger::log_message,
|
|
14
|
-
path::{ find_entry_file, normalize_path },
|
|
15
|
-
watcher::watch_directory,
|
|
5
|
+
builder::Builder,
|
|
6
|
+
debugger::{ lexer::write_lexer_log_file, preprocessor::write_preprocessor_log_file },
|
|
7
|
+
preprocessor::loader::ModuleLoader,
|
|
8
|
+
store::global::GlobalStore,
|
|
9
|
+
utils::path::{ find_entry_file, normalize_path },
|
|
16
10
|
},
|
|
11
|
+
utils::{ logger::{ LogLevel, Logger }, spinner::with_spinner, watcher::watch_directory },
|
|
17
12
|
};
|
|
13
|
+
use std::{ thread, time::Duration };
|
|
18
14
|
|
|
19
15
|
pub fn handle_build_command(
|
|
20
|
-
config: Option<
|
|
16
|
+
config: Option<Config>,
|
|
21
17
|
entry: Option<String>,
|
|
22
18
|
output: Option<String>,
|
|
23
19
|
watch: bool
|
|
@@ -49,26 +45,51 @@ pub fn handle_build_command(
|
|
|
49
45
|
.unwrap_or(false)
|
|
50
46
|
};
|
|
51
47
|
|
|
48
|
+
let logger = Logger::new();
|
|
49
|
+
|
|
50
|
+
if fetched_entry.is_empty() {
|
|
51
|
+
logger.log_message(
|
|
52
|
+
LogLevel::Error,
|
|
53
|
+
"Entry path is not specified. Please provide a valid entry path."
|
|
54
|
+
);
|
|
55
|
+
std::process::exit(1);
|
|
56
|
+
}
|
|
57
|
+
if fetched_output.is_empty() {
|
|
58
|
+
logger.log_message(
|
|
59
|
+
LogLevel::Error,
|
|
60
|
+
"Output directory is not specified. Please provide a valid output directory."
|
|
61
|
+
);
|
|
62
|
+
std::process::exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
let entry_file = find_entry_file(&fetched_entry).unwrap_or_else(|| {
|
|
53
|
-
|
|
66
|
+
logger.log_message(
|
|
67
|
+
LogLevel::Error,
|
|
68
|
+
&format!("❌ index.deva not found in directory: {}", fetched_entry)
|
|
69
|
+
);
|
|
54
70
|
std::process::exit(1);
|
|
55
71
|
});
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
// SECTION Begin build
|
|
74
|
+
if fetched_watch {
|
|
75
|
+
begin_build(entry_file.clone(), fetched_output.clone());
|
|
59
76
|
|
|
60
|
-
|
|
77
|
+
logger.log_message(
|
|
78
|
+
LogLevel::Watcher,
|
|
79
|
+
&format!("Watching for changes in '{}'...", fetched_entry)
|
|
80
|
+
);
|
|
61
81
|
|
|
62
82
|
watch_directory(entry_file.clone(), move || {
|
|
63
|
-
log_message("
|
|
64
|
-
|
|
83
|
+
logger.log_message(LogLevel::Watcher, "Detected changes, re-building...");
|
|
84
|
+
|
|
85
|
+
begin_build(entry_file.clone(), fetched_output.clone());
|
|
65
86
|
}).unwrap();
|
|
66
87
|
} else {
|
|
67
|
-
begin_build(entry_file.clone(), fetched_output.clone()
|
|
88
|
+
begin_build(entry_file.clone(), fetched_output.clone());
|
|
68
89
|
}
|
|
69
90
|
}
|
|
70
91
|
|
|
71
|
-
fn begin_build(entry: String, output: String
|
|
92
|
+
fn begin_build(entry: String, output: String) {
|
|
72
93
|
let spinner = with_spinner("Building...", || {
|
|
73
94
|
thread::sleep(Duration::from_millis(800));
|
|
74
95
|
});
|
|
@@ -78,27 +99,34 @@ fn begin_build(entry: String, output: String, watch: bool) {
|
|
|
78
99
|
let normalized_entry_file = normalize_path(&entry);
|
|
79
100
|
let normalized_output_dir = normalize_path(&output);
|
|
80
101
|
|
|
81
|
-
let global_store =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
let mut global_store = GlobalStore::new();
|
|
103
|
+
let module_loader = ModuleLoader::new(&normalized_entry_file, &normalized_output_dir);
|
|
104
|
+
|
|
105
|
+
// SECTION Load
|
|
106
|
+
// NOTE: We use modules in the build command, so we need to load them
|
|
107
|
+
let (modules_tokens, modules_statements) = module_loader.load_all(&mut global_store);
|
|
108
|
+
|
|
109
|
+
// SECTION Write logs
|
|
110
|
+
write_lexer_log_file(&normalized_output_dir, "lexer_tokens.log", modules_tokens.clone());
|
|
111
|
+
write_preprocessor_log_file(
|
|
112
|
+
&normalized_output_dir,
|
|
113
|
+
"resolved_statements.log",
|
|
114
|
+
modules_statements.clone()
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// SECTION Building AST and Audio
|
|
118
|
+
let builder = Builder::new();
|
|
119
|
+
builder.build_ast(&modules_statements);
|
|
120
|
+
builder.build_audio(&modules_statements, &normalized_output_dir, &mut global_store);
|
|
121
|
+
|
|
122
|
+
// SECTION Logging
|
|
123
|
+
let logger = Logger::new();
|
|
124
|
+
|
|
125
|
+
let success_message = format!(
|
|
126
|
+
"Build completed successfully in {:.2?}. Output files written to: '{}'",
|
|
127
|
+
duration.elapsed(),
|
|
128
|
+
normalized_output_dir
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
logger.log_message(LogLevel::Success, &success_message);
|
|
104
132
|
}
|