1use std::path::PathBuf;
4
5pub type PulseResult<T> = Result<T, PulseError>;
7
8#[derive(Debug, thiserror::Error)]
10pub enum PulseError {
11 #[error("invalid note name: {value}")]
13 InvalidNoteName {
14 value: String,
16 },
17 #[error("invalid mode: {value}")]
19 InvalidMode {
20 value: String,
22 },
23 #[error("invalid chord kind: {value}")]
25 InvalidChordKind {
26 value: String,
28 },
29 #[error("invalid tempo: {bpm}")]
31 InvalidTempo {
32 bpm: f32,
34 },
35 #[error("invalid duration: {duration}")]
37 InvalidDuration {
38 duration: f32,
40 },
41 #[error("duration count mismatch: {notes} notes and {durations} durations")]
43 DurationCountMismatch {
44 notes: usize,
46 durations: usize,
48 },
49 #[error("invalid chord voice count: {count}")]
51 InvalidChordVoiceCount {
52 count: usize,
54 },
55 #[error("invalid sequence option {option}: {value}")]
57 InvalidSequenceOption {
58 option: String,
60 value: String,
62 },
63 #[error("invalid instrument: {value}")]
65 InvalidInstrument {
66 value: String,
68 },
69 #[error("invalid drum type: {value}")]
71 InvalidDrumType {
72 value: String,
74 },
75 #[error("invalid rhythm pattern: {value}")]
77 InvalidRhythmPattern {
78 value: String,
80 },
81 #[error("invalid midi note: {note}")]
83 InvalidMidiNote {
84 note: i64,
86 },
87 #[error("invalid frequency: {frequency}")]
89 InvalidFrequency {
90 frequency: f32,
92 },
93 #[error("invalid drum grid steps: {steps}")]
95 InvalidDrumGridSteps {
96 steps: usize,
98 },
99 #[error("invalid step duration: {duration}")]
101 InvalidStepDuration {
102 duration: f32,
104 },
105 #[error("invalid drum option {option}: {value}")]
107 InvalidDrumOption {
108 option: String,
110 value: String,
112 },
113 #[error("invalid repeat times: {repeat_times}")]
115 InvalidRepeatTimes {
116 repeat_times: usize,
118 },
119 #[error("midi import failed: {message}")]
121 MidiImportFailed {
122 message: String,
124 },
125 #[error("midi export failed: {message}")]
127 MidiExportFailed {
128 message: String,
130 },
131 #[error("invalid export path: {}", path.display())]
133 InvalidExportPath {
134 path: PathBuf,
136 },
137 #[error("unsupported export format: {format}")]
139 UnsupportedExportFormat {
140 format: String,
142 },
143 #[error("invalid export sample_rate: {sample_rate}")]
145 InvalidExportSampleRate {
146 sample_rate: u32,
148 },
149 #[error("invalid export normalize option {option}: {value}")]
151 InvalidExportNormalizeOption {
152 option: String,
154 value: String,
156 },
157 #[error("invalid export flac_bits_per_sample: {bits_per_sample}")]
159 InvalidExportFlacBitsPerSample {
160 bits_per_sample: u32,
162 },
163 #[error("invalid export midi option {option}: {value}")]
165 InvalidExportMidiOption {
166 option: String,
168 value: String,
170 },
171 #[error("export failed: {message}")]
173 ExportFailed {
174 message: String,
176 },
177 #[error("gpu feature is not enabled")]
179 GpuFeatureNotEnabled,
180 #[error("playback failed: {message}")]
182 PlaybackFailed {
183 message: String,
185 },
186 #[error("invalid playback rate: {rate}")]
188 InvalidPlaybackRate {
189 rate: f32,
191 },
192 #[error("invalid playback volume: {volume}")]
194 InvalidPlaybackVolume {
195 volume: f32,
197 },
198 #[error("invalid playback pan: {pan}")]
200 InvalidPlaybackPan {
201 pan: f32,
203 },
204 #[error("invalid generator: {name}")]
206 InvalidGenerator {
207 name: String,
209 },
210 #[error("missing generator option: {name}")]
212 MissingGeneratorOption {
213 name: String,
215 },
216 #[error("invalid generator option {name}: {value}")]
218 InvalidGeneratorOption {
219 name: String,
221 value: String,
223 },
224 #[error("unsupported generator output: {output}")]
226 UnsupportedGeneratorOutput {
227 output: String,
229 },
230 #[error("invalid effect: {name}")]
232 InvalidEffect {
233 name: String,
235 },
236 #[error("invalid effect preset {effect}: {preset}")]
238 InvalidEffectPreset {
239 effect: String,
241 preset: String,
243 },
244 #[error("invalid effect option {effect}.{option}: {value}")]
246 InvalidEffectOption {
247 effect: String,
249 option: String,
251 value: String,
253 },
254 #[error("invalid effect scope {effect}: {scope}")]
256 InvalidEffectScope {
257 effect: String,
259 scope: String,
261 },
262 #[error("invalid synth: {name}")]
264 InvalidSynth {
265 name: String,
267 },
268 #[error("invalid synth preset {synth}: {preset}")]
270 InvalidSynthPreset {
271 synth: String,
273 preset: String,
275 },
276 #[error("invalid synth option {synth}.{option}: {value}")]
278 InvalidSynthOption {
279 synth: String,
281 option: String,
283 value: String,
285 },
286 #[error("sample load failed: {message}")]
288 SampleLoadFailed {
289 message: String,
291 },
292 #[error("invalid sample option {option}: {value}")]
294 InvalidSampleOption {
295 option: String,
297 value: String,
299 },
300 #[error("invalid midi clip option {option}: {value}")]
302 InvalidMidiClipOption {
303 option: String,
305 value: String,
307 },
308}
309
310impl From<PulseError> for mlua::Error {
311 fn from(value: PulseError) -> Self {
312 mlua::Error::external(value)
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn error_messages_start_lowercase_and_include_value() {
322 let error = PulseError::InvalidMode {
323 value: "superlocrian".to_string(),
324 };
325
326 assert_eq!(error.to_string(), "invalid mode: superlocrian");
327 }
328
329 #[test]
330 fn pulse_error_converts_to_lua_external_error() {
331 let lua_error: mlua::Error = PulseError::InvalidTempo { bpm: 0.0 }.into();
332
333 let rendered = lua_error.to_string();
334 assert!(rendered.contains("invalid tempo: 0"));
335 }
336
337 #[test]
338 fn capability_expansion_errors_render_lowercase_values() {
339 assert_eq!(
340 PulseError::InvalidDrumType {
341 value: "kick_707".to_string()
342 }
343 .to_string(),
344 "invalid drum type: kick_707"
345 );
346 assert_eq!(
347 PulseError::InvalidRhythmPattern {
348 value: "vaporwave_backbeat".to_string()
349 }
350 .to_string(),
351 "invalid rhythm pattern: vaporwave_backbeat"
352 );
353 assert_eq!(
354 PulseError::InvalidMidiNote { note: 200 }.to_string(),
355 "invalid midi note: 200"
356 );
357 assert_eq!(
358 PulseError::InvalidDrumGridSteps { steps: 0 }.to_string(),
359 "invalid drum grid steps: 0"
360 );
361 assert_eq!(
362 PulseError::InvalidStepDuration { duration: 0.0 }.to_string(),
363 "invalid step duration: 0"
364 );
365 assert_eq!(
366 PulseError::InvalidRepeatTimes { repeat_times: 0 }.to_string(),
367 "invalid repeat times: 0"
368 );
369 assert_eq!(
370 PulseError::UnsupportedExportFormat {
371 format: "ogg".to_string()
372 }
373 .to_string(),
374 "unsupported export format: ogg"
375 );
376 assert_eq!(
377 PulseError::GpuFeatureNotEnabled.to_string(),
378 "gpu feature is not enabled"
379 );
380 assert_eq!(
381 PulseError::InvalidExportNormalizeOption {
382 option: "target_db".to_string(),
383 value: "3".to_string()
384 }
385 .to_string(),
386 "invalid export normalize option target_db: 3"
387 );
388 assert_eq!(
389 PulseError::InvalidExportFlacBitsPerSample {
390 bits_per_sample: 12
391 }
392 .to_string(),
393 "invalid export flac_bits_per_sample: 12"
394 );
395 assert_eq!(
396 PulseError::InvalidExportMidiOption {
397 option: "midi_velocity_gain".to_string(),
398 value: "0".to_string()
399 }
400 .to_string(),
401 "invalid export midi option midi_velocity_gain: 0"
402 );
403 assert_eq!(
404 PulseError::PlaybackFailed {
405 message: "device unavailable".to_string()
406 }
407 .to_string(),
408 "playback failed: device unavailable"
409 );
410 assert_eq!(
411 PulseError::InvalidPlaybackRate { rate: 0.0 }.to_string(),
412 "invalid playback rate: 0"
413 );
414 assert_eq!(
415 PulseError::InvalidPlaybackVolume { volume: 1.5 }.to_string(),
416 "invalid playback volume: 1.5"
417 );
418 assert_eq!(
419 PulseError::InvalidPlaybackPan { pan: 2.0 }.to_string(),
420 "invalid playback pan: 2"
421 );
422 assert_eq!(
423 PulseError::InvalidGenerator {
424 name: "vaporwave_sequence".to_string()
425 }
426 .to_string(),
427 "invalid generator: vaporwave_sequence"
428 );
429 assert_eq!(
430 PulseError::MissingGeneratorOption {
431 name: "length".to_string()
432 }
433 .to_string(),
434 "missing generator option: length"
435 );
436 assert_eq!(
437 PulseError::InvalidGeneratorOption {
438 name: "length".to_string(),
439 value: "0".to_string()
440 }
441 .to_string(),
442 "invalid generator option length: 0"
443 );
444 assert_eq!(
445 PulseError::UnsupportedGeneratorOutput {
446 output: "tuple3".to_string()
447 }
448 .to_string(),
449 "unsupported generator output: tuple3"
450 );
451 assert_eq!(
452 PulseError::InvalidEffect {
453 name: "shimmer".to_string()
454 }
455 .to_string(),
456 "invalid effect: shimmer"
457 );
458 assert_eq!(
459 PulseError::InvalidEffectPreset {
460 effect: "delay".to_string(),
461 preset: "cathedral".to_string()
462 }
463 .to_string(),
464 "invalid effect preset delay: cathedral"
465 );
466 assert_eq!(
467 PulseError::InvalidEffectOption {
468 effect: "delay".to_string(),
469 option: "feedback".to_string(),
470 value: "1.2".to_string()
471 }
472 .to_string(),
473 "invalid effect option delay.feedback: 1.2"
474 );
475 assert_eq!(
476 PulseError::InvalidEffectScope {
477 effect: "filter".to_string(),
478 scope: "master".to_string()
479 }
480 .to_string(),
481 "invalid effect scope filter: master"
482 );
483 assert_eq!(
484 PulseError::InvalidSynth {
485 name: "subtractive".to_string()
486 }
487 .to_string(),
488 "invalid synth: subtractive"
489 );
490 assert_eq!(
491 PulseError::InvalidSynthPreset {
492 synth: "fm".to_string(),
493 preset: "glass".to_string()
494 }
495 .to_string(),
496 "invalid synth preset fm: glass"
497 );
498 assert_eq!(
499 PulseError::InvalidSynthOption {
500 synth: "additive".to_string(),
501 option: "harmonics".to_string(),
502 value: "empty".to_string()
503 }
504 .to_string(),
505 "invalid synth option additive.harmonics: empty"
506 );
507 assert_eq!(
508 PulseError::SampleLoadFailed {
509 message: "missing.wav".to_string()
510 }
511 .to_string(),
512 "sample load failed: missing.wav"
513 );
514 assert_eq!(
515 PulseError::InvalidSampleOption {
516 option: "rate".to_string(),
517 value: "0".to_string()
518 }
519 .to_string(),
520 "invalid sample option rate: 0"
521 );
522 }
523}