1use super::mini::parse_mini;
6use crate::ast::program::*;
7
8pub fn parse_line(line: &str) -> Result<SourceLine, String> {
10 let trimmed = line.trim();
11
12 if trimmed.is_empty() {
14 return Ok(SourceLine::Blank);
15 }
16
17 if trimmed.starts_with("--") {
19 return Ok(SourceLine::Comment(trimmed.to_string()));
20 }
21
22 if let Some(rest) = strip_keyword(trimmed, "bpm") {
24 return parse_bpm(rest);
25 }
26 if let Some(rest) = strip_keyword(trimmed, "sig") {
27 return parse_sig(rest);
28 }
29 if let Some(rest) = strip_keyword(trimmed, "phrase") {
30 return parse_phrase(rest);
31 }
32 if let Some(rest) = strip_keyword(trimmed, "scale") {
33 return parse_scale(rest);
34 }
35 if let Some(rest) = strip_keyword(trimmed, "load") {
36 return parse_load(rest);
37 }
38 if let Some(rest) = strip_keyword(trimmed, "include") {
39 return parse_include(rest);
40 }
41 if let Some(rest) = strip_keyword(trimmed, "arrange") {
42 return parse_arrange(rest);
43 }
44 if let Some(rest) = strip_keyword(trimmed, "tail") {
45 return parse_tail(rest);
46 }
47 if let Some(rest) = strip_keyword(trimmed, "seed") {
48 return parse_seed(rest);
49 }
50 if let Some(rest) = strip_keyword(trimmed, "meta") {
51 return parse_meta(rest);
52 }
53 if let Some(rest) = strip_keyword(trimmed, "use") {
56 return parse_include(rest);
57 }
58
59 if trimmed == "group" || strip_keyword(trimmed, "group").is_some() {
61 return parse_group_header(trimmed, false);
62 }
63 if trimmed == "section" || strip_keyword(trimmed, "section").is_some() {
65 return parse_section_header(trimmed, false);
66 }
67 if let Some(rest) = trimmed.strip_prefix('}') {
71 return parse_group_footer(rest);
72 }
73
74 if let Some(rest) = trimmed.strip_prefix(';') {
76 let rest = rest.trim_start();
77 if rest == "group" || strip_keyword(rest, "group").is_some() {
78 return parse_group_header(rest, true);
79 }
80 if rest == "section" || strip_keyword(rest, "section").is_some() {
81 return parse_section_header(rest, true);
82 }
83 return parse_pattern_line(rest, true);
84 }
85
86 parse_pattern_line(trimmed, false)
88}
89
90fn parse_group_header(line: &str, muted: bool) -> Result<SourceLine, String> {
92 let rest = strip_keyword(line, "group")
93 .ok_or_else(|| "group: expected 'group <name> {'".to_string())?;
94 let Some(name) = rest.strip_suffix('{') else {
95 return Err(
96 "group: the header must end with '{' (filters go on the closing '}')".to_string(),
97 );
98 };
99 let name = name.trim();
100 validate_identifier(name).map_err(|error| format!("group: {error}"))?;
101 Ok(SourceLine::GroupStart {
102 muted,
103 name: name.to_string(),
104 })
105}
106
107fn parse_section_header(line: &str, muted: bool) -> Result<SourceLine, String> {
109 let rest = strip_keyword(line, "section")
110 .ok_or_else(|| "section: expected 'section <name> <cycles> {'".to_string())?;
111 let Some(body) = rest.strip_suffix('{') else {
112 return Err("section: the header must end with '{'".to_string());
113 };
114 let mut parts = body.split_whitespace();
115 let name = parts
116 .next()
117 .ok_or_else(|| "section: expected a name before the length".to_string())?;
118 validate_identifier(name).map_err(|error| format!("section: {error}"))?;
119 let Some(cycles) = parts.next() else {
120 return Err(format!(
121 "section: '{name}' needs a length in cycles — 'section {name} 16 {{'"
122 ));
123 };
124 let cycles: u32 = cycles
125 .parse()
126 .map_err(|_| format!("section: expected a length in cycles, got '{cycles}'"))?;
127 if cycles == 0 {
128 return Err("section: a section lasts at least one cycle".to_string());
129 }
130 if let Some(extra) = parts.next() {
131 return Err(format!(
132 "section: unexpected '{extra}' after the length (filters go on the member lines)"
133 ));
134 }
135 Ok(SourceLine::SectionStart {
136 muted,
137 name: name.to_string(),
138 cycles,
139 })
140}
141
142fn parse_group_footer(rest: &str) -> Result<SourceLine, String> {
144 let rest = rest.trim();
145 if rest.is_empty() {
146 return Ok(SourceLine::GroupEnd(Vec::new()));
147 }
148 let Some(pipeline) = rest.strip_prefix('|') else {
149 return Err(format!(
150 "group: expected '| transform' after '}}', got '{rest}'"
151 ));
152 };
153 Ok(SourceLine::GroupEnd(parse_transforms(pipeline.trim())?))
154}
155
156fn strip_keyword<'a>(input: &'a str, keyword: &str) -> Option<&'a str> {
158 if let Some(rest) = input.strip_prefix(keyword)
159 && rest.starts_with(char::is_whitespace)
160 {
161 Some(rest.trim_start())
162 } else {
163 None
164 }
165}
166
167fn parse_arrange(input: &str) -> Result<SourceLine, String> {
171 let mut items = Vec::new();
172 for token in input.split_whitespace() {
173 let (name, repeat) = match token.split_once('*') {
174 Some((name, count)) => {
175 let repeat: u32 = count.parse().map_err(|_| {
176 format!("arrange: expected a repeat count after '*', got '{count}'")
177 })?;
178 if repeat == 0 {
179 return Err(format!(
180 "arrange: '{name}*0' plays nothing — drop it from the line instead"
181 ));
182 }
183 (name, repeat)
184 }
185 None => (token, 1),
186 };
187 validate_identifier(name).map_err(|error| format!("arrange: {error}"))?;
188 items.push(ArrangeItem {
189 section: name.to_string(),
190 repeat,
191 });
192 }
193 if items.is_empty() {
194 return Err("arrange: expected at least one section name".to_string());
195 }
196 Ok(SourceLine::Arrange(items))
197}
198
199fn parse_tail(input: &str) -> Result<SourceLine, String> {
201 let raw = input.trim();
202 let seconds: f64 = raw
203 .parse()
204 .map_err(|_| format!("tail: expected a number of seconds, got '{raw}'"))?;
205 if !seconds.is_finite() || seconds < 0.0 {
206 return Err(format!("tail: expected zero or more seconds, got '{raw}'"));
207 }
208 Ok(SourceLine::Tail(seconds))
209}
210
211fn parse_meta(input: &str) -> Result<SourceLine, String> {
217 let input = input.trim();
218 let (key, rest) = match input.split_once(char::is_whitespace) {
219 Some(split) => split,
220 None => return Err(format!("meta: expected a value after '{input}'")),
221 };
222 validate_identifier(key).map_err(|error| format!("meta: {error}"))?;
223 let rest = rest.trim();
224 let value = rest
225 .strip_prefix('"')
226 .and_then(|rest| rest.strip_suffix('"'))
227 .ok_or_else(|| format!("meta: the value must be double-quoted, got '{rest}'"))?;
228 if value.contains('"') {
229 return Err("meta: the value cannot contain a double quote".to_string());
230 }
231 Ok(SourceLine::Meta(
232 key.to_ascii_lowercase(),
233 value.to_string(),
234 ))
235}
236
237fn parse_seed(input: &str) -> Result<SourceLine, String> {
239 let raw = input.trim();
240 let seed: u64 = raw
241 .parse()
242 .map_err(|_| format!("seed: expected a non-negative integer, got '{raw}'"))?;
243 Ok(SourceLine::Seed(seed))
244}
245
246fn parse_phrase(input: &str) -> Result<SourceLine, String> {
251 let cycles: u32 = input.trim().parse().map_err(|_| {
252 format!(
253 "phrase: expected a number of cycles, got '{}'",
254 input.trim()
255 )
256 })?;
257 if cycles == 0 {
258 return Err("phrase: a phrase is at least one cycle".to_string());
259 }
260 Ok(SourceLine::Phrase(cycles))
261}
262
263fn parse_bpm(rest: &str) -> Result<SourceLine, String> {
264 let val: u32 = rest
265 .trim()
266 .parse()
267 .map_err(|_| format!("invalid bpm value: '{}'", rest.trim()))?;
268 if !(20..=999).contains(&val) {
269 return Err(format!("bpm must be between 20 and 999, got {}", val));
270 }
271 Ok(SourceLine::Bpm(val))
272}
273
274fn parse_sig(rest: &str) -> Result<SourceLine, String> {
275 let parts: Vec<&str> = rest.trim().split('/').collect();
276 if parts.len() != 2 {
277 return Err(format!(
278 "expected time signature N/D, got '{}'",
279 rest.trim()
280 ));
281 }
282 let num: u8 = parts[0]
283 .trim()
284 .parse()
285 .map_err(|_| format!("invalid numerator: '{}'", parts[0].trim()))?;
286 let den: u8 = parts[1]
287 .trim()
288 .parse()
289 .map_err(|_| format!("invalid denominator: '{}'", parts[1].trim()))?;
290 if num == 0 {
291 return Err("time signature numerator must be > 0".to_string());
292 }
293 if den == 0 {
294 return Err("time signature denominator must be > 0".to_string());
295 }
296 Ok(SourceLine::Sig(num, den))
297}
298
299fn parse_scale(rest: &str) -> Result<SourceLine, String> {
300 let mut tokens = rest.split_whitespace();
301 let root_str = tokens
302 .next()
303 .ok_or_else(|| "expected scale root note".to_string())?;
304 let mode_str = tokens
305 .next()
306 .ok_or_else(|| "expected scale mode".to_string())?;
307
308 let root = parse_pitch_root(root_str)?;
309 let mode = parse_scale_mode(mode_str)?;
310 Ok(SourceLine::Scale(root, mode))
311}
312
313fn parse_load(rest: &str) -> Result<SourceLine, String> {
314 let trimmed = rest.trim();
315 if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
316 let path = &trimmed[1..trimmed.len() - 1];
317 Ok(SourceLine::Load(path.to_string()))
318 } else {
319 Err(format!(
320 "expected quoted path after load, got '{}'",
321 trimmed
322 ))
323 }
324}
325
326fn parse_include(rest: &str) -> Result<SourceLine, String> {
327 let name = rest.trim();
328 validate_identifier(name)?;
329 Ok(SourceLine::Include(name.to_string()))
330}
331
332fn parse_pattern_line(input: &str, muted: bool) -> Result<SourceLine, String> {
335 let mut tokens = SplitKeepQuotes::new(input);
336
337 let name = tokens
338 .next()
339 .ok_or_else(|| "expected pattern name".to_string())?;
340 validate_identifier(name)?;
341
342 let instrument = tokens
343 .next()
344 .ok_or_else(|| "expected instrument name".to_string())?;
345 validate_identifier(instrument)?;
346
347 let notation_str = tokens
348 .next()
349 .ok_or_else(|| "expected quoted mini-notation".to_string())?;
350
351 if !notation_str.starts_with('"') || !notation_str.ends_with('"') || notation_str.len() < 2 {
352 return Err(format!(
353 "expected double-quoted mini-notation, got '{}'",
354 notation_str
355 ));
356 }
357 let inner = ¬ation_str[1..notation_str.len() - 1];
358 let notation = parse_mini(inner)?;
359
360 let remainder: String = tokens.collect::<Vec<&str>>().join(" ");
365 let (span, rest) = split_span(remainder.trim())?;
366 let transforms = parse_transforms(rest.trim())?;
367
368 Ok(SourceLine::Pattern(PatternDef {
369 group: None,
370 section: None,
371 span,
372 muted,
373 name: name.to_string(),
374 instrument: instrument.to_string(),
375 notation,
376 transforms,
377 }))
378}
379
380fn split_span(input: &str) -> Result<(Option<Span>, &str), String> {
383 let Some(rest) = input.strip_prefix('@') else {
384 return Ok((None, input));
385 };
386 let rest = rest.trim_start();
387 let (body, tail) = match rest.find('|') {
389 Some(index) => rest.split_at(index),
390 None => (rest, ""),
391 };
392 Ok((Some(parse_span(body.trim())?), tail))
393}
394
395fn parse_span(body: &str) -> Result<Span, String> {
397 if body.is_empty() {
398 return Err("span: expected a cycle after '@' — '@ 8' or '@ 3..8'".to_string());
399 }
400 let cycle = |raw: &str| -> Result<u32, String> {
401 raw.parse::<u32>()
402 .map_err(|_| format!("span: expected a cycle number, got '{raw}'"))
403 };
404 let Some((from, to)) = body.split_once("..") else {
405 return Ok(Span::at(cycle(body)?));
407 };
408 let (from, to) = (from.trim(), to.trim());
409 if to.contains("..") {
410 return Err(format!("span: '{body}' has more than two ends"));
411 }
412 if from.is_empty() && to.is_empty() {
413 return Err(
414 "span: '@ ..' names no cycles — drop it to sound for the whole section".to_string(),
415 );
416 }
417 Ok(Span {
418 from: if from.is_empty() {
419 None
420 } else {
421 Some(cycle(from)?)
422 },
423 to: if to.is_empty() {
424 None
425 } else {
426 Some(cycle(to)?)
427 },
428 })
429}
430
431fn validate_identifier(s: &str) -> Result<(), String> {
432 if s.is_empty() {
433 return Err("identifier cannot be empty".to_string());
434 }
435 let first = s.chars().next().unwrap();
436 if !first.is_ascii_alphabetic() && first != '_' {
437 return Err(format!(
438 "identifier must start with a letter or underscore, got '{}'",
439 s
440 ));
441 }
442 if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
443 return Err(format!("invalid identifier: '{}'", s));
444 }
445 Ok(())
446}
447
448fn parse_transforms(input: &str) -> Result<Vec<Transform>, String> {
451 if input.is_empty() {
452 return Ok(Vec::new());
453 }
454
455 let mut transforms = Vec::new();
456 let segments: Vec<&str> = input.split('|').collect();
458 for seg in segments {
459 let seg = seg.trim();
460 if seg.is_empty() {
461 continue;
462 }
463 transforms.push(parse_single_transform(seg)?);
464 }
465 Ok(transforms)
466}
467
468fn parse_single_transform(input: &str) -> Result<Transform, String> {
469 let mut parts = input.split_whitespace();
470 let keyword = parts
471 .next()
472 .ok_or_else(|| "expected transform keyword".to_string())?;
473 let transform = parse_transform_keyword(keyword, &mut parts)?;
474 if let Some(extra) = parts.next() {
478 return Err(format!("{}: unexpected argument '{}'", keyword, extra));
479 }
480 Ok(transform)
481}
482
483fn parse_transform_keyword(
484 keyword: &str,
485 parts: &mut std::str::SplitWhitespace,
486) -> Result<Transform, String> {
487 match keyword {
488 "rev" => Ok(Transform::Rev),
489 "fast" => Ok(Transform::Fast(parse_transform_ramp(parts, "fast")?)),
490 "slow" => Ok(Transform::Slow(parse_transform_ramp(parts, "slow")?)),
491 "ramp" => {
492 let text = parts
493 .next()
494 .ok_or_else(|| "ramp: expected a number of cycles".to_string())?;
495 let cycles: u32 = text
496 .parse()
497 .map_err(|_| format!("ramp: invalid number of cycles '{}'", text))?;
498 if cycles == 0 {
499 return Err("ramp: a span needs at least one cycle".to_string());
500 }
501 let curve = match parts.next() {
504 None => RampCurve::Linear,
505 Some("lin") => RampCurve::Linear,
506 Some("exp") => RampCurve::Exponential,
507 Some("osc") => RampCurve::Oscillate,
508 Some(other) => {
509 return Err(format!(
510 "ramp: '{other}' is not a curve; use 'lin' (the default), 'exp' or 'osc'"
511 ));
512 }
513 };
514 Ok(Transform::RampSpan { cycles, curve })
515 }
516 "every" => {
517 let n_str = parts
518 .next()
519 .ok_or_else(|| "every: expected cycle count".to_string())?;
520 let n: u32 = n_str
521 .parse()
522 .map_err(|_| format!("every: invalid number '{}'", n_str))?;
523 let rest: String = parts.by_ref().collect::<Vec<&str>>().join(" ");
524 let inner = parse_single_transform(rest.trim())?;
525 Ok(Transform::Every(n, Box::new(inner)))
526 }
527 "arp" => {
528 let mode_str = parts
529 .next()
530 .ok_or_else(|| "arp: expected mode (up/down/updown/random)".to_string())?;
531 let mode = match mode_str {
532 "up" => ArpMode::Up,
533 "down" => ArpMode::Down,
534 "updown" => ArpMode::UpDown,
535 "random" => ArpMode::Random,
536 other => return Err(format!("arp: unknown mode '{}'", other)),
537 };
538 Ok(Transform::Arp(mode))
539 }
540 "scale" => {
541 let root_str = parts
542 .next()
543 .ok_or_else(|| "scale: expected root note".to_string())?;
544 let mode_str = parts
545 .next()
546 .ok_or_else(|| "scale: expected mode".to_string())?;
547 let root = parse_pitch_root(root_str)?;
548 let mode = parse_scale_mode(mode_str)?;
549 Ok(Transform::Scale(root, mode))
550 }
551 "oct" => {
552 let text = parts
553 .next()
554 .ok_or_else(|| "oct: expected offset".to_string())?;
555 let parse = |part: &str| {
556 part.parse::<i32>()
557 .map_err(|_| format!("oct: invalid offset '{}'", part))
558 };
559 Ok(Transform::Oct(ramp_from_text(text, "oct", parse)?))
560 }
561 "vel" => {
562 let vel = parse_transform_ramp(parts, "vel")?;
563 validate_within("vel", &vel, 0.0, 1.0)?;
564 Ok(Transform::Vel(vel))
565 }
566 "gain" => {
567 let gain = parse_transform_ramp(parts, "gain")?;
568 validate_within("gain", &gain, 0.0, 2.0)?;
569 Ok(Transform::Gain(gain))
570 }
571 "pan" => {
572 let first = parts
573 .next()
574 .ok_or_else(|| "pan: expected a position or a waveform".to_string())?;
575 if first.starts_with(|c: char| c.is_ascii_digit() || c == '-' || c == '+' || c == '.') {
579 let position = ramp_from_text(first, "pan", |part| {
580 part.parse::<f64>()
581 .map_err(|_| format!("pan: invalid position '{}'", part))
582 })?;
583 validate_within("pan", &position, -1.0, 1.0)?;
584 Ok(Transform::Pan(position))
585 } else {
586 Ok(Transform::AutoPan(parse_pan_sweep(first, parts)?))
587 }
588 }
589 "lpf" => {
590 let cutoff = parse_transform_ramp(parts, "lpf")?;
591 validate_at_least("lpf", &cutoff, 0.0)?;
592 Ok(Transform::Lpf(cutoff))
593 }
594 "hpf" => {
595 let cutoff = parse_transform_ramp(parts, "hpf")?;
596 validate_at_least("hpf", &cutoff, 0.0)?;
597 Ok(Transform::Hpf(cutoff))
598 }
599 "delay" => {
600 let time = parse_transform_ramp(parts, "delay time")?;
601 validate_at_least("delay time", &time, 0.0)?;
602 let feedback = parse_transform_ramp(parts, "delay feedback")?;
603 validate_within("delay feedback", &feedback, 0.0, 0.99)?;
604 let mix = match parts.next() {
605 Some(text) => {
606 let mix = ramp_from_text(text, "delay mix", |part| {
607 part.parse::<f64>()
608 .map_err(|_| format!("delay mix: invalid number '{}'", part))
609 })?;
610 validate_within("delay mix", &mix, 0.0, 1.0)?;
611 Some(mix)
612 }
613 None => None,
614 };
615 Ok(Transform::Delay(time, feedback, mix))
616 }
617 "reverb" => {
618 let amount = parse_transform_ramp(parts, "reverb")?;
619 validate_within("reverb", &amount, 0.0, 1.0)?;
620 Ok(Transform::Reverb(amount))
621 }
622 "fx" => {
624 let filter = parts
625 .next()
626 .ok_or_else(|| "fx: expected a filter name".to_string())?;
627 Ok(Transform::Fx(parse_fx_call(filter, parts)?))
628 }
629 keyword if FX_ALIASES.contains(&keyword) => {
633 Ok(Transform::Fx(parse_fx_call(keyword, parts)?))
634 }
635 other => Err(format!("unknown transform: '{}'", other)),
636 }
637}
638
639pub const FX_ALIASES: &[&str] = &[
647 "trem", "bpf", "rbpf", "avg", "clip", "comp", "limit", "lpf", "hpf", "delay", "reverb", "pan",
648 "gain",
649];
650
651pub(crate) fn parse_fx_line(line: &str) -> Result<FxCall, String> {
653 let mut parts = line.split_whitespace();
654 let keyword = parts
655 .next()
656 .ok_or_else(|| "expected a filter name".to_string())?;
657 match keyword {
658 "fx" => {
659 let filter = parts
660 .next()
661 .ok_or_else(|| "fx: expected a filter name".to_string())?;
662 parse_fx_call(filter, &mut parts)
663 }
664 alias if FX_ALIASES.contains(&alias) => parse_fx_call(alias, &mut parts),
665 other => Err(format!(
666 "'{other}' is not a filter line; use 'fx <filter> …' or a short alias"
667 )),
668 }
669}
670
671fn parse_fx_call(filter: &str, parts: &mut std::str::SplitWhitespace) -> Result<FxCall, String> {
672 if filter.is_empty() {
673 return Err("fx: expected a filter name".to_string());
674 }
675 let mut args = Vec::new();
676 let mut seen_named = false;
677 for token in parts.by_ref() {
678 match token.split_once('=') {
679 Some((name, value)) => {
680 if name.is_empty() {
681 return Err(format!("{filter}: '{token}' is missing a parameter name"));
682 }
683 seen_named = true;
684 args.push(FxArg::Named(
685 name.to_string(),
686 parse_fx_value(filter, name, value)?,
687 ));
688 }
689 None => {
690 if seen_named {
693 return Err(format!(
694 "{filter}: '{token}' comes after a named argument; give it a name too"
695 ));
696 }
697 args.push(FxArg::Positional(parse_fx_value(
698 filter, "argument", token,
699 )?));
700 }
701 }
702 }
703 Ok(FxCall {
704 filter: filter.to_string(),
705 args,
706 })
707}
708
709fn parse_fx_value(filter: &str, label: &str, text: &str) -> Result<FxValue, String> {
710 let lowered = text.to_ascii_lowercase();
711 let (number, hertz) = match lowered.strip_suffix("hz") {
712 Some(number) => (number, true),
713 None => (lowered.as_str(), false),
714 };
715 let name = format!("{filter} {label}");
716 let ramp = ramp_from_text(number, &name, |part| {
717 part.parse::<f64>().map_err(|_| {
718 if hertz {
719 format!("{name}: invalid frequency '{text}'")
720 } else {
721 format!("{name}: invalid number '{text}'")
722 }
723 })
724 })?;
725 Ok(if hertz {
728 FxValue::Hertz(ramp)
729 } else {
730 FxValue::Plain(ramp)
731 })
732}
733
734fn parse_pan_sweep(wave: &str, parts: &mut std::str::SplitWhitespace) -> Result<PanSweep, String> {
735 let wave = parse_lfo_wave(wave)?;
736 let rate_text = parts
737 .next()
738 .ok_or_else(|| "pan: expected a rate after the waveform".to_string())?;
739 let rate = parse_lfo_rate(rate_text)?;
740 let depth = match parts.next() {
741 Some(text) => Some(
742 text.parse::<f64>()
743 .map_err(|_| format!("pan depth: invalid number '{}'", text))?,
744 ),
745 None => None,
746 };
747 Ok(PanSweep { wave, rate, depth })
748}
749
750fn parse_lfo_wave(text: &str) -> Result<LfoWave, String> {
751 match text {
752 "sine" | "sin" => Ok(LfoWave::Sine),
753 "tri" | "triangle" => Ok(LfoWave::Triangle),
754 "sq" | "square" => Ok(LfoWave::Square),
755 "saw" => Ok(LfoWave::Saw),
756 "rand" | "random" => Ok(LfoWave::Random),
757 other => Err(format!(
758 "pan: '{}' is not a position or a waveform (sine, tri, sq, saw, rand)",
759 other
760 )),
761 }
762}
763
764fn parse_lfo_rate(text: &str) -> Result<LfoRate, String> {
766 let lowered = text.to_ascii_lowercase();
767 match lowered.strip_suffix("hz") {
768 Some(number) => number
769 .parse::<f64>()
770 .map(LfoRate::Hertz)
771 .map_err(|_| format!("pan rate: invalid frequency '{}'", text)),
772 None => text
773 .parse::<f64>()
774 .map(LfoRate::Cycles)
775 .map_err(|_| format!("pan rate: invalid number of cycles '{}'", text)),
776 }
777}
778
779fn parse_transform_ramp(
781 parts: &mut std::str::SplitWhitespace,
782 name: &str,
783) -> Result<Ramp<f64>, String> {
784 let text = parts
785 .next()
786 .ok_or_else(|| format!("{}: expected number", name))?;
787 ramp_from_text(text, name, |part| {
788 part.parse::<f64>()
789 .map_err(|_| format!("{}: invalid number '{}'", name, part))
790 })
791}
792
793pub(crate) fn ramp_from_text<T: Copy>(
799 text: &str,
800 name: &str,
801 parse: impl Fn(&str) -> Result<T, String>,
802) -> Result<Ramp<T>, String> {
803 ramp_from_text_dyn(text, name, &parse)
804}
805
806fn ramp_from_text_dyn<T: Copy>(
809 text: &str,
810 name: &str,
811 parse: &dyn Fn(&str) -> Result<T, String>,
812) -> Result<Ramp<T>, String> {
813 if let Some(inner) = text.strip_prefix("r(") {
814 let Some(inner) = inner.strip_suffix(')') else {
815 return Err(format!(
816 "{name}: '{text}' does not close its parenthesis — r(...) is written \
817 without spaces, e.g. r(300,9000,4,exp)"
818 ));
819 };
820 return timed_ramp_from_args(inner, name, &parse);
821 }
822 let sweeps = text.contains("..");
823 let steps = text.contains('>');
824 if sweeps && steps {
825 return Err(format!(
826 "{name}: '{text}' mixes '..' and '>'. Use '..' to sweep smoothly, or '>' to hold each value in turn."
827 ));
828 }
829 if sweeps {
830 let mut ends = text.split("..");
831 let from = parse(ends.next().unwrap_or_default())?;
832 let to = parse(ends.next().unwrap_or_default())?;
833 if ends.next().is_some() {
834 return Err(format!(
835 "{name}: '{text}' has more than two ends. Use '>' for a sequence of held values."
836 ));
837 }
838 return Ok(Ramp::Sweep { from, to });
839 }
840 let mut stages = text.split('>');
841 let first = parse(stages.next().unwrap_or_default())?;
842 let rest = stages.map(parse).collect::<Result<Vec<T>, String>>()?;
843 Ok(Ramp::steps(first, rest))
844}
845
846fn timed_ramp_from_args<T: Copy>(
850 inner: &str,
851 name: &str,
852 parse: &dyn Fn(&str) -> Result<T, String>,
853) -> Result<Ramp<T>, String> {
854 let args: Vec<&str> = inner.split(',').map(str::trim).collect();
855 if args.iter().any(|arg| arg.is_empty()) {
856 return Err(format!("{name}: r(...) has an empty argument"));
857 }
858
859 let (ramp, rest) = if args[0].contains("..") || args[0].contains('>') {
862 (ramp_from_text_dyn(args[0], name, parse)?, &args[1..])
863 } else if args.len() >= 2 {
864 let from = parse(args[0])?;
865 let to = parse(args[1])?;
866 (Ramp::Sweep { from, to }, &args[2..])
867 } else {
868 return Err(format!(
869 "{name}: r(...) needs a travel — r(from,to), r(a..b) or r(a>b>c)"
870 ));
871 };
872 if !ramp.travels() {
873 return Err(format!("{name}: r(...) needs a travel, not a single value"));
874 }
875
876 let mut rest = rest.iter();
877 let Some(span_text) = rest.next() else {
878 return Ok(ramp);
880 };
881 let span_divisions: f64 = span_text.parse().map_err(|_| {
882 format!("{name}: r(...) span '{span_text}' is not a number of time divisions")
883 })?;
884 if !span_divisions.is_finite() || span_divisions <= 0.0 {
885 return Err(format!(
886 "{name}: r(...) span must be a positive number of time divisions"
887 ));
888 }
889 let curve = match rest.next().copied() {
890 None => RampCurve::Linear,
891 Some("lin") => RampCurve::Linear,
892 Some("exp") => RampCurve::Exponential,
893 Some("osc") => RampCurve::Oscillate,
894 Some(other) => {
895 return Err(format!(
896 "{name}: r(...) style '{other}' is not a curve; use 'lin' (the default), 'exp' or 'osc'"
897 ));
898 }
899 };
900 if let Some(extra) = rest.next() {
901 return Err(format!(
902 "{name}: r(...) has an unexpected argument '{extra}'"
903 ));
904 }
905 Ok(Ramp::Timed {
906 ramp: Box::new(ramp),
907 span_divisions,
908 curve,
909 })
910}
911
912fn validate_within(name: &str, ramp: &Ramp<f64>, low: f64, high: f64) -> Result<(), String> {
915 for value in ramp.values() {
916 if !value.is_finite() || !(low..=high).contains(&value) {
917 return Err(format!(
920 "{name}: {value} is outside the supported range {low} to {high}"
921 ));
922 }
923 }
924 Ok(())
925}
926
927fn validate_at_least(name: &str, ramp: &Ramp<f64>, low: f64) -> Result<(), String> {
930 for value in ramp.values() {
931 if !value.is_finite() || value < low {
932 return Err(format!("{name}: {value} is below the minimum of {low}"));
933 }
934 }
935 Ok(())
936}
937
938fn parse_pitch_root(s: &str) -> Result<PitchRoot, String> {
941 let mut chars = s.chars();
942 let letter_ch = chars.next().ok_or("expected note letter")?;
943 let letter = match letter_ch {
944 'C' | 'c' => NoteLetter::C,
945 'D' | 'd' => NoteLetter::D,
946 'E' | 'e' => NoteLetter::E,
947 'F' | 'f' => NoteLetter::F,
948 'G' | 'g' => NoteLetter::G,
949 'A' | 'a' => NoteLetter::A,
950 'B' | 'b' => NoteLetter::B,
951 other => return Err(format!("invalid note letter: '{}'", other)),
952 };
953 let rest: String = chars.collect();
954 let accidental = match rest.as_str() {
955 "#" => Accidental::Sharp,
956 "##" => Accidental::DoubleSharp,
957 "b" => Accidental::Flat,
958 "bb" => Accidental::DoubleFlat,
959 "" => Accidental::Natural,
960 other => return Err(format!("invalid accidental: '{}'", other)),
961 };
962 Ok(PitchRoot {
963 name: letter,
964 accidental,
965 })
966}
967
968fn parse_scale_mode(s: &str) -> Result<ScaleMode, String> {
969 match s {
970 "major" => Ok(ScaleMode::Major),
971 "minor" => Ok(ScaleMode::Minor),
972 "dorian" => Ok(ScaleMode::Dorian),
973 "phrygian" => Ok(ScaleMode::Phrygian),
974 "lydian" => Ok(ScaleMode::Lydian),
975 "mixolydian" => Ok(ScaleMode::Mixolydian),
976 "aeolian" => Ok(ScaleMode::Aeolian),
977 "locrian" => Ok(ScaleMode::Locrian),
978 "chromatic" => Ok(ScaleMode::Chromatic),
979 "pentatonic" => Ok(ScaleMode::Pentatonic),
980 "blues" => Ok(ScaleMode::Blues),
981 other => Err(format!("unknown scale mode: '{}'", other)),
982 }
983}
984
985struct SplitKeepQuotes<'a> {
987 rest: &'a str,
988}
989
990impl<'a> SplitKeepQuotes<'a> {
991 fn new(input: &'a str) -> Self {
992 Self { rest: input }
993 }
994}
995
996impl<'a> Iterator for SplitKeepQuotes<'a> {
997 type Item = &'a str;
998
999 fn next(&mut self) -> Option<Self::Item> {
1000 self.rest = self.rest.trim_start();
1001 if self.rest.is_empty() {
1002 return None;
1003 }
1004 if self.rest.starts_with('"') {
1005 if let Some(end) = self.rest[1..].find('"') {
1007 let token = &self.rest[..end + 2]; self.rest = &self.rest[end + 2..];
1009 Some(token)
1010 } else {
1011 let token = self.rest;
1013 self.rest = "";
1014 Some(token)
1015 }
1016 } else {
1017 let end = self
1019 .rest
1020 .find(char::is_whitespace)
1021 .unwrap_or(self.rest.len());
1022 let token = &self.rest[..end];
1023 self.rest = &self.rest[end..];
1024 Some(token)
1025 }
1026 }
1027}
1028
1029#[cfg(test)]
1034mod tests {
1035 use super::*;
1036 use crate::ast::*;
1037
1038 #[test]
1041 fn test_blank_line() {
1042 assert_eq!(parse_line("").unwrap(), SourceLine::Blank);
1043 assert_eq!(parse_line(" ").unwrap(), SourceLine::Blank);
1044 }
1045
1046 #[test]
1047 fn test_comment() {
1048 let result = parse_line("-- hello world").unwrap();
1049 assert_eq!(result, SourceLine::Comment("-- hello world".to_string()));
1050 }
1051
1052 #[test]
1055 fn test_bpm() {
1056 assert_eq!(parse_line("bpm 120").unwrap(), SourceLine::Bpm(120));
1057 assert_eq!(parse_line("bpm 60").unwrap(), SourceLine::Bpm(60));
1058 }
1059
1060 #[test]
1061 fn test_bpm_invalid() {
1062 assert!(parse_line("bpm abc").is_err());
1063 assert!(parse_line("bpm 10").is_err()); assert!(parse_line("bpm 1000").is_err()); }
1066
1067 #[test]
1068 fn test_sig() {
1069 assert_eq!(parse_line("sig 4/4").unwrap(), SourceLine::Sig(4, 4));
1070 assert_eq!(parse_line("sig 3/4").unwrap(), SourceLine::Sig(3, 4));
1071 assert_eq!(parse_line("sig 7/8").unwrap(), SourceLine::Sig(7, 8));
1072 }
1073
1074 #[test]
1075 fn test_sig_invalid() {
1076 assert!(parse_line("sig 0/4").is_err()); assert!(parse_line("sig 4/0").is_err()); assert!(parse_line("sig abc").is_err());
1079 }
1080
1081 #[test]
1082 fn test_sig_accepts_non_binary_denominator() {
1083 assert_eq!(parse_line("sig 4/3").unwrap(), SourceLine::Sig(4, 3));
1084 assert_eq!(parse_line("sig 5/6").unwrap(), SourceLine::Sig(5, 6));
1085 }
1086
1087 #[test]
1088 fn test_explicit_instrument_include() {
1089 assert_eq!(
1090 parse_line("include hihat").unwrap(),
1091 SourceLine::Include("hihat".into())
1092 );
1093 assert_eq!(
1094 parse_line("use hihat").unwrap(),
1095 SourceLine::Include("hihat".into())
1096 );
1097 }
1098
1099 #[test]
1100 fn test_scale() {
1101 let result = parse_line("scale C minor").unwrap();
1102 assert_eq!(
1103 result,
1104 SourceLine::Scale(
1105 PitchRoot {
1106 name: NoteLetter::C,
1107 accidental: Accidental::Natural,
1108 },
1109 ScaleMode::Minor
1110 )
1111 );
1112 }
1113
1114 #[test]
1115 fn test_scale_with_accidental() {
1116 let result = parse_line("scale Eb dorian").unwrap();
1117 assert_eq!(
1118 result,
1119 SourceLine::Scale(
1120 PitchRoot {
1121 name: NoteLetter::E,
1122 accidental: Accidental::Flat,
1123 },
1124 ScaleMode::Dorian
1125 )
1126 );
1127 }
1128
1129 #[test]
1130 fn test_load() {
1131 let result = parse_line("load \"pads.rt\"").unwrap();
1132 assert_eq!(result, SourceLine::Load("pads.rt".to_string()));
1133 }
1134
1135 #[test]
1136 fn test_load_unquoted() {
1137 assert!(parse_line("load pads.rt").is_err());
1138 }
1139
1140 #[test]
1143 fn test_simple_pattern() {
1144 let result = parse_line("kick drums \"x ~ x ~\"").unwrap();
1145 if let SourceLine::Pattern(p) = result {
1146 assert_eq!(p.name, "kick");
1147 assert_eq!(p.instrument, "drums");
1148 assert!(!p.muted);
1149 assert!(p.transforms.is_empty());
1150 assert_eq!(p.notation.sequence.steps.len(), 4);
1151 } else {
1152 panic!("expected pattern");
1153 }
1154 }
1155
1156 #[test]
1157 fn test_muted_pattern() {
1158 let result = parse_line("; bass sine \"c2 ~ eb2 ~\"").unwrap();
1159 if let SourceLine::Pattern(p) = result {
1160 assert!(p.muted);
1161 assert_eq!(p.name, "bass");
1162 assert_eq!(p.instrument, "sine");
1163 } else {
1164 panic!("expected muted pattern");
1165 }
1166 }
1167
1168 #[test]
1169 fn test_pattern_with_transforms() {
1170 let result = parse_line("lead saw \"c4 eb4 g4 bb4\" | rev | slow 2").unwrap();
1171 if let SourceLine::Pattern(p) = result {
1172 assert_eq!(p.name, "lead");
1173 assert_eq!(p.transforms.len(), 2);
1174 assert_eq!(p.transforms[0], Transform::Rev);
1175 assert_eq!(p.transforms[1], Transform::Slow(Ramp::fixed(2.0)));
1176 } else {
1177 panic!("expected pattern");
1178 }
1179 }
1180
1181 #[test]
1182 fn test_pattern_with_every_transform() {
1183 let result = parse_line("hats hihat \"x*8\" | every 4 rev").unwrap();
1184 if let SourceLine::Pattern(p) = result {
1185 assert_eq!(
1186 p.transforms[0],
1187 Transform::Every(4, Box::new(Transform::Rev))
1188 );
1189 } else {
1190 panic!("expected pattern");
1191 }
1192 }
1193
1194 #[test]
1195 fn test_pattern_with_arp() {
1196 let result = parse_line("arp piano \"[c3,e3,g3]\" | arp up").unwrap();
1197 if let SourceLine::Pattern(p) = result {
1198 assert_eq!(p.transforms[0], Transform::Arp(ArpMode::Up));
1199 } else {
1200 panic!("expected pattern");
1201 }
1202 }
1203
1204 #[test]
1205 fn test_pattern_with_scale_transform() {
1206 let result = parse_line("mel saw \"0 2 4 6\" | scale C minor").unwrap();
1207 if let SourceLine::Pattern(p) = result {
1208 if let Transform::Scale(root, mode) = &p.transforms[0] {
1209 assert_eq!(root.name, NoteLetter::C);
1210 assert_eq!(*mode, ScaleMode::Minor);
1211 } else {
1212 panic!("expected scale transform");
1213 }
1214 } else {
1215 panic!("expected pattern");
1216 }
1217 }
1218
1219 #[test]
1220 fn test_pattern_with_effects() {
1221 let result = parse_line(
1222 "pad pad \"[c3,eb3,g3]\" | gain 0.5 | lpf 800 | delay 0.25 0.4 | reverb 0.3",
1223 )
1224 .unwrap();
1225 if let SourceLine::Pattern(p) = result {
1226 assert_eq!(p.transforms.len(), 4);
1227 assert_eq!(p.transforms[0], Transform::Gain(Ramp::fixed(0.5)));
1228 assert_eq!(p.transforms[1], Transform::Lpf(Ramp::fixed(800.0)));
1229 assert_eq!(
1230 p.transforms[2],
1231 Transform::Delay(Ramp::fixed(0.25), Ramp::fixed(0.4), None)
1232 );
1233 assert_eq!(p.transforms[3], Transform::Reverb(Ramp::fixed(0.3)));
1234 } else {
1235 panic!("expected pattern");
1236 }
1237 }
1238
1239 #[test]
1240 fn test_pan_and_vel_transforms() {
1241 let result =
1242 parse_line("lead saw \"c4 e4\" | pan -1.0 | vel 0.42 | gain 1.4 | delay 0.2 0.3 0.5")
1243 .unwrap();
1244 if let SourceLine::Pattern(p) = result {
1245 assert_eq!(p.transforms[0], Transform::Pan(Ramp::fixed(-1.0)));
1246 assert_eq!(p.transforms[1], Transform::Vel(Ramp::fixed(0.42)));
1247 assert_eq!(p.transforms[2], Transform::Gain(Ramp::fixed(1.4)));
1248 assert_eq!(
1249 p.transforms[3],
1250 Transform::Delay(Ramp::fixed(0.2), Ramp::fixed(0.3), Some(Ramp::fixed(0.5)))
1251 );
1252 } else {
1253 panic!("expected pattern");
1254 }
1255 }
1256
1257 #[test]
1258 fn test_pan_sweep_variants() {
1259 let sweep = |source: &str| match parse_line(source).unwrap() {
1260 SourceLine::Pattern(p) => match p.transforms[0] {
1261 Transform::AutoPan(sweep) => sweep,
1262 ref other => panic!("expected a swept pan, got {other:?}"),
1263 },
1264 other => panic!("expected a pattern, got {other:?}"),
1265 };
1266
1267 let basic = sweep("hat hihat \"x*8\" | pan sine 4");
1268 assert_eq!(basic.wave, LfoWave::Sine);
1269 assert_eq!(basic.rate, LfoRate::Cycles(4.0));
1270 assert_eq!(basic.depth, None);
1271
1272 let deep = sweep("hat hihat \"x*8\" | pan sq 1 0.6");
1273 assert_eq!(deep.wave, LfoWave::Square);
1274 assert_eq!(deep.rate, LfoRate::Cycles(1.0));
1275 assert_eq!(deep.depth, Some(0.6));
1276
1277 let absolute = sweep("hat hihat \"x*8\" | pan tri 0.5hz");
1279 assert_eq!(absolute.wave, LfoWave::Triangle);
1280 assert_eq!(absolute.rate, LfoRate::Hertz(0.5));
1281
1282 assert_eq!(sweep("p pad \"0\" | pan saw 2").wave, LfoWave::Saw);
1283 assert_eq!(sweep("p pad \"0\" | pan rand 1").wave, LfoWave::Random);
1284 assert_eq!(
1286 sweep("p pad \"0\" | pan triangle 2HZ").rate,
1287 LfoRate::Hertz(2.0)
1288 );
1289 }
1290
1291 #[test]
1292 fn test_fixed_pan_still_parses_as_a_position() {
1293 let result = parse_line("kick kick \"x ~\" | pan -0.3").unwrap();
1294 if let SourceLine::Pattern(p) = result {
1295 assert_eq!(p.transforms[0], Transform::Pan(Ramp::fixed(-0.3)));
1296 } else {
1297 panic!("expected pattern");
1298 }
1299 }
1300
1301 #[test]
1302 fn test_pan_sweep_errors_are_specific() {
1303 let message = |source: &str| parse_line(source).unwrap_err();
1304 assert!(
1305 message("p pad \"0\" | pan wobble 4").contains("not a position or a waveform"),
1306 "got: {}",
1307 message("p pad \"0\" | pan wobble 4")
1308 );
1309 assert!(message("p pad \"0\" | pan sine").contains("expected a rate"));
1310 assert!(message("p pad \"0\" | pan sine fast").contains("invalid number of cycles"));
1311 assert!(message("p pad \"0\" | pan sine 4 loud").contains("invalid number"));
1312 assert!(message("p pad \"0\" | pan sine 4hz 0.5 extra").contains("unexpected argument"));
1313 }
1314
1315 fn fx_call(source: &str) -> FxCall {
1316 match parse_line(source).unwrap() {
1317 SourceLine::Pattern(p) => match p.transforms[0].clone() {
1318 Transform::Fx(call) => call,
1319 other => panic!("expected a filter call, got {other:?}"),
1320 },
1321 other => panic!("expected a pattern, got {other:?}"),
1322 }
1323 }
1324
1325 #[test]
1326 fn test_fx_call_positional_and_named_arguments() {
1327 let call = fx_call("p pad \"0\" | fx Compressor 0.3 8");
1328 assert_eq!(call.filter, "Compressor");
1329 assert_eq!(
1330 call.args,
1331 vec![
1332 FxArg::Positional(FxValue::Plain(Ramp::fixed(0.3))),
1333 FxArg::Positional(FxValue::Plain(Ramp::fixed(8.0))),
1334 ]
1335 );
1336
1337 let call = fx_call("p pad \"0\" | fx Limiter threshold=0.8 release=0.3");
1338 assert_eq!(
1339 call.args,
1340 vec![
1341 FxArg::Named("threshold".into(), FxValue::Plain(Ramp::fixed(0.8))),
1342 FxArg::Named("release".into(), FxValue::Plain(Ramp::fixed(0.3))),
1343 ]
1344 );
1345
1346 let call = fx_call("p pad \"0\" | fx Compressor 0.3 ratio=8");
1348 assert_eq!(
1349 call.args,
1350 vec![
1351 FxArg::Positional(FxValue::Plain(Ramp::fixed(0.3))),
1352 FxArg::Named("ratio".into(), FxValue::Plain(Ramp::fixed(8.0))),
1353 ]
1354 );
1355
1356 assert!(fx_call("p pad \"0\" | fx Limiter").args.is_empty());
1358 }
1359
1360 #[test]
1361 fn test_fx_aliases_parse_as_filter_calls() {
1362 for (source, filter) in [
1363 ("p pad \"0\" | trem 4 0.6", "trem"),
1364 ("p pad \"0\" | bpf 200 1200", "bpf"),
1365 ("p pad \"0\" | rbpf 900 12", "rbpf"),
1366 ("p pad \"0\" | avg 5", "avg"),
1367 ("p pad \"0\" | clip 0.4", "clip"),
1368 ("p pad \"0\" | comp 0.3 8", "comp"),
1369 ("p pad \"0\" | limit 0.9", "limit"),
1370 ] {
1371 assert_eq!(fx_call(source).filter, filter);
1372 }
1373 }
1374
1375 #[test]
1376 fn test_fx_hz_suffix_is_kept_distinct_from_a_bare_number() {
1377 let call = fx_call("p pad \"0\" | trem 0.5hz");
1378 assert_eq!(
1379 call.args,
1380 vec![FxArg::Positional(FxValue::Hertz(Ramp::fixed(0.5)))]
1381 );
1382 let call = fx_call("p pad \"0\" | trem 4");
1383 assert_eq!(
1384 call.args,
1385 vec![FxArg::Positional(FxValue::Plain(Ramp::fixed(4.0)))]
1386 );
1387 let call = fx_call("p pad \"0\" | fx Tremolo frequency=2HZ");
1389 assert_eq!(
1390 call.args,
1391 vec![FxArg::Named(
1392 "frequency".into(),
1393 FxValue::Hertz(Ramp::fixed(2.0))
1394 )]
1395 );
1396 }
1397
1398 #[test]
1399 fn test_fx_argument_errors_are_specific() {
1400 let message = |source: &str| parse_line(source).unwrap_err();
1401 assert!(message("p pad \"0\" | fx").contains("expected a filter name"));
1402 assert!(message("p pad \"0\" | trem loud").contains("invalid number"));
1403 assert!(message("p pad \"0\" | trem =4").contains("missing a parameter name"));
1404 assert!(
1405 message("p pad \"0\" | fx Compressor ratio=8 0.3").contains("comes after a named"),
1406 "got: {}",
1407 message("p pad \"0\" | fx Compressor ratio=8 0.3")
1408 );
1409 assert!(message("p pad \"0\" | revv").contains("unknown transform"));
1412 }
1413
1414 #[test]
1415 fn test_ramp_span_and_ranged_transforms() {
1416 let result = parse_line("sn snare \"x(4..16,4)\" | vel 0.4..1.0 | ramp 16").unwrap();
1417 if let SourceLine::Pattern(p) = result {
1418 assert_eq!(
1419 p.transforms[0],
1420 Transform::Vel(Ramp::Sweep { from: 0.4, to: 1.0 })
1421 );
1422 assert_eq!(
1423 p.transforms[1],
1424 Transform::RampSpan {
1425 cycles: 16,
1426 curve: RampCurve::Linear
1427 }
1428 );
1429 } else {
1430 panic!("expected pattern");
1431 }
1432
1433 let result = parse_line("p pad \"0\" | oct 0..-2 | fast 1..4 | slow 2..1").unwrap();
1434 if let SourceLine::Pattern(p) = result {
1435 assert_eq!(
1436 p.transforms[0],
1437 Transform::Oct(Ramp::Sweep { from: 0, to: -2 })
1438 );
1439 assert_eq!(
1440 p.transforms[1],
1441 Transform::Fast(Ramp::Sweep { from: 1.0, to: 4.0 })
1442 );
1443 assert_eq!(
1444 p.transforms[2],
1445 Transform::Slow(Ramp::Sweep { from: 2.0, to: 1.0 })
1446 );
1447 } else {
1448 panic!("expected pattern");
1449 }
1450 }
1451
1452 #[test]
1453 fn test_step_chains_in_transform_arguments() {
1454 let result = parse_line("sn snare \"x(2>4>8>16,4)\" | vel 0.3>0.6>1.0 | ramp 16").unwrap();
1455 if let SourceLine::Pattern(p) = result {
1456 assert_eq!(
1457 p.transforms[0],
1458 Transform::Vel(Ramp::Steps {
1459 first: 0.3,
1460 rest: vec![0.6, 1.0]
1461 })
1462 );
1463 assert_eq!(
1464 p.transforms[1],
1465 Transform::RampSpan {
1466 cycles: 16,
1467 curve: RampCurve::Linear
1468 }
1469 );
1470 } else {
1471 panic!("expected pattern");
1472 }
1473
1474 let result = parse_line("p pad \"0\" | oct 0>-1>-2").unwrap();
1476 if let SourceLine::Pattern(p) = result {
1477 assert_eq!(
1478 p.transforms[0],
1479 Transform::Oct(Ramp::Steps {
1480 first: 0,
1481 rest: vec![-1, -2]
1482 })
1483 );
1484 } else {
1485 panic!("expected pattern");
1486 }
1487 }
1488
1489 #[test]
1490 fn test_mixing_the_two_travel_spellings_is_rejected() {
1491 let error = parse_line("p pad \"0\" | vel 0.2..0.5>1.0").unwrap_err();
1492 assert!(error.contains("mixes"), "{error}");
1493 assert!(error.contains("sweep") && error.contains("hold"), "{error}");
1495
1496 let error = parse_line("p pad \"0\" | vel 0.2..0.5..1.0").unwrap_err();
1497 assert!(error.contains("more than two ends"), "{error}");
1498 }
1499
1500 fn transforms(source: &str) -> Vec<Transform> {
1501 match parse_line(source).unwrap() {
1502 SourceLine::Pattern(p) => p.transforms,
1503 other => panic!("expected a pattern, got {other:?}"),
1504 }
1505 }
1506
1507 #[test]
1508 fn test_audio_transforms_take_ranges() {
1509 assert_eq!(
1511 transforms("p pad \"0\" | lpf 300..9000 | ramp 16"),
1512 vec![
1513 Transform::Lpf(Ramp::Sweep {
1514 from: 300.0,
1515 to: 9000.0
1516 }),
1517 Transform::RampSpan {
1518 cycles: 16,
1519 curve: RampCurve::Linear
1520 },
1521 ]
1522 );
1523
1524 assert_eq!(
1525 transforms("p pad \"0\" | hpf 40>200>800"),
1526 vec![Transform::Hpf(Ramp::Steps {
1527 first: 40.0,
1528 rest: vec![200.0, 800.0]
1529 })]
1530 );
1531 assert_eq!(
1532 transforms("p pad \"0\" | gain 0.2..1.0"),
1533 vec![Transform::Gain(Ramp::Sweep { from: 0.2, to: 1.0 })]
1534 );
1535 assert_eq!(
1536 transforms("p pad \"0\" | reverb 0.0..0.6"),
1537 vec![Transform::Reverb(Ramp::Sweep { from: 0.0, to: 0.6 })]
1538 );
1539 assert_eq!(
1541 transforms("p pad \"0\" | pan -0.8..0.8"),
1542 vec![Transform::Pan(Ramp::Sweep {
1543 from: -0.8,
1544 to: 0.8
1545 })]
1546 );
1547 assert_eq!(
1549 transforms("p pad \"0\" | delay 0.1..0.4 0.2>0.6 0.3..0.9"),
1550 vec![Transform::Delay(
1551 Ramp::Sweep { from: 0.1, to: 0.4 },
1552 Ramp::Steps {
1553 first: 0.2,
1554 rest: vec![0.6]
1555 },
1556 Some(Ramp::Sweep { from: 0.3, to: 0.9 }),
1557 )]
1558 );
1559 }
1560
1561 #[test]
1562 fn test_fx_arguments_take_ranges() {
1563 let call = fx_call("p pad \"0\" | trem 2..8hz 0.7");
1565 assert_eq!(
1566 call.args,
1567 vec![
1568 FxArg::Positional(FxValue::Hertz(Ramp::Sweep { from: 2.0, to: 8.0 })),
1569 FxArg::Positional(FxValue::Plain(Ramp::fixed(0.7))),
1570 ]
1571 );
1572 let FxArg::Positional(ref rate) = call.args[0] else {
1573 panic!("expected a positional argument");
1574 };
1575 assert!(rate.travels(), "a ranged fx argument needs a ramp span");
1576 assert!(!matches!(&call.args[1], FxArg::Positional(v) if v.travels()));
1577
1578 let call = fx_call("p pad \"0\" | fx Tremolo depth=0.2>0.9");
1579 assert_eq!(
1580 call.args,
1581 vec![FxArg::Named(
1582 "depth".into(),
1583 FxValue::Plain(Ramp::Steps {
1584 first: 0.2,
1585 rest: vec![0.9]
1586 })
1587 )]
1588 );
1589 }
1590
1591 #[test]
1592 fn test_travels_reports_on_every_ranged_argument() {
1593 let fixed = transforms("p pad \"0\" | lpf 900");
1594 assert_eq!(fixed, vec![Transform::Lpf(Ramp::fixed(900.0))]);
1595 let Transform::Lpf(ref cutoff) = fixed[0] else {
1596 panic!("expected lpf");
1597 };
1598 assert!(!cutoff.travels(), "a plain value must not ask for a ramp");
1599
1600 let swept = transforms("p pad \"0\" | lpf 300..9000");
1601 let Transform::Lpf(ref cutoff) = swept[0] else {
1602 panic!("expected lpf");
1603 };
1604 assert!(cutoff.travels());
1605 assert_eq!(cutoff.start(), 300.0);
1606 assert_eq!(cutoff.values(), vec![300.0, 9000.0]);
1607 }
1608
1609 #[test]
1610 fn test_every_value_of_an_audio_range_is_validated() {
1611 let error = parse_line("p pad \"0\" | reverb 0.5..1.4").unwrap_err();
1613 assert!(error.contains("1.4"), "{error}");
1614 assert!(error.contains("0 to 1"), "{error}");
1615
1616 assert!(
1617 parse_line("p pad \"0\" | gain 0.5>1.0>2.5")
1618 .unwrap_err()
1619 .contains("2.5")
1620 );
1621 assert!(
1622 parse_line("p pad \"0\" | pan -0.5..1.5")
1623 .unwrap_err()
1624 .contains("1.5")
1625 );
1626 assert!(
1627 parse_line("p pad \"0\" | vel 0.4..1.2")
1628 .unwrap_err()
1629 .contains("1.2")
1630 );
1631 assert!(
1632 parse_line("p pad \"0\" | delay 0.2 0.4..1.0")
1633 .unwrap_err()
1634 .contains("delay feedback")
1635 );
1636 assert!(
1637 parse_line("p pad \"0\" | lpf 900..-20")
1638 .unwrap_err()
1639 .contains("below the minimum")
1640 );
1641 assert!(parse_line("p pad \"0\" | trem 0..900").is_ok());
1644 }
1645
1646 #[test]
1647 fn test_ramp_span_must_be_at_least_one_cycle() {
1648 assert!(
1649 parse_line("p pad \"0\" | ramp 0")
1650 .unwrap_err()
1651 .contains("at least one cycle")
1652 );
1653 assert!(
1654 parse_line("p pad \"0\" | ramp")
1655 .unwrap_err()
1656 .contains("expected a number")
1657 );
1658 }
1659
1660 #[test]
1661 fn test_ramp_span_carries_a_curve() {
1662 assert_eq!(
1664 transforms("p pad \"0\" | lpf 300..9000 | ramp 16"),
1665 transforms("p pad \"0\" | lpf 300..9000 | ramp 16 lin")
1666 );
1667 assert_eq!(
1668 transforms("p pad \"0\" | ramp 8 exp"),
1669 vec![Transform::RampSpan {
1670 cycles: 8,
1671 curve: RampCurve::Exponential
1672 }]
1673 );
1674 assert_eq!(RampCurve::default(), RampCurve::Linear);
1675 }
1676
1677 #[test]
1678 fn test_r_form_without_a_span_is_plain_sugar() {
1679 assert_eq!(
1680 transforms("p pad \"0\" | vel r(0.4,1.0)"),
1681 transforms("p pad \"0\" | vel 0.4..1.0")
1682 );
1683 assert_eq!(
1684 transforms("p pad \"0\" | vel r(0.4>0.6>1.0)"),
1685 transforms("p pad \"0\" | vel 0.4>0.6>1.0")
1686 );
1687 }
1688
1689 #[test]
1690 fn test_r_form_with_a_span_carries_its_own_window() {
1691 let parsed = transforms("p pad \"0\" | vel r(0.4,1.0,4,osc)");
1692 assert_eq!(
1693 parsed,
1694 vec![Transform::Vel(Ramp::Timed {
1695 ramp: Box::new(Ramp::Sweep { from: 0.4, to: 1.0 }),
1696 span_divisions: 4.0,
1697 curve: RampCurve::Oscillate,
1698 })]
1699 );
1700 assert_eq!(
1702 transforms("p pad \"0\" | vel r(0.4,1.0,0.5)"),
1703 vec![Transform::Vel(Ramp::Timed {
1704 ramp: Box::new(Ramp::Sweep { from: 0.4, to: 1.0 }),
1705 span_divisions: 0.5,
1706 curve: RampCurve::Linear,
1707 })]
1708 );
1709 }
1710
1711 #[test]
1712 fn test_r_form_still_range_checks_its_travel() {
1713 let error = parse_line("p pad \"0\" | vel r(0.4,2.0,4)").unwrap_err();
1714 assert!(error.contains("outside the supported range"), "{error}");
1715 }
1716
1717 #[test]
1718 fn test_r_form_errors_are_specific() {
1719 let error = parse_line("p pad \"0\" | vel r(0.4, 1.0, 4)").unwrap_err();
1720 assert!(error.contains("without spaces"), "{error}");
1721 let error = parse_line("p pad \"0\" | vel r(0.5)").unwrap_err();
1722 assert!(error.contains("needs a travel"), "{error}");
1723 let error = parse_line("p pad \"0\" | vel r(0.4,1.0,4,sine)").unwrap_err();
1724 assert!(error.contains("'sine' is not a curve"), "{error}");
1725 let error = parse_line("p pad \"0\" | vel r(0.4,1.0,exp)").unwrap_err();
1726 assert!(error.contains("time divisions"), "{error}");
1727 }
1728
1729 #[test]
1730 fn test_osc_ramp_curve_parses() {
1731 assert_eq!(
1732 transforms("p pad \"0\" | ramp 8 osc"),
1733 vec![Transform::RampSpan {
1734 cycles: 8,
1735 curve: RampCurve::Oscillate
1736 }]
1737 );
1738 }
1739
1740 #[test]
1741 fn test_ramp_curve_errors_are_specific() {
1742 let error = parse_line("p pad \"0\" | ramp 8 sine").unwrap_err();
1743 assert!(error.contains("'sine' is not a curve"), "{error}");
1744 assert!(
1745 error.contains("lin") && error.contains("exp") && error.contains("osc"),
1746 "{error}"
1747 );
1748 assert!(
1750 parse_line("p pad \"0\" | ramp 8 exp lin")
1751 .unwrap_err()
1752 .contains("unexpected argument")
1753 );
1754 }
1755
1756 #[test]
1757 fn test_trailing_transform_argument_is_rejected() {
1758 let error = parse_line("lead saw \"c4\" | pan -1.0 0.5").unwrap_err();
1759 assert!(error.contains("unexpected argument"), "got: {error}");
1760 }
1761
1762 #[test]
1763 fn test_every_still_accepts_a_nested_transform() {
1764 let result = parse_line("lead saw \"c4\" | every 4 every 2 rev").unwrap();
1765 if let SourceLine::Pattern(p) = result {
1766 assert_eq!(
1767 p.transforms[0],
1768 Transform::Every(4, Box::new(Transform::Every(2, Box::new(Transform::Rev))))
1769 );
1770 } else {
1771 panic!("expected pattern");
1772 }
1773 }
1774
1775 #[test]
1778 fn test_unknown_keyword_is_error() {
1779 assert!(parse_line("foobar").is_err());
1781 }
1782
1783 #[test]
1784 fn test_pattern_missing_quotes() {
1785 assert!(parse_line("kick drums x ~ x ~").is_err());
1786 }
1787
1788 #[test]
1791 fn test_split_keep_quotes() {
1792 let input = r#"kick drums "x ~ x ~" | rev"#;
1793 let tokens: Vec<&str> = SplitKeepQuotes::new(input).collect();
1794 assert_eq!(tokens, vec!["kick", "drums", "\"x ~ x ~\"", "|", "rev"]);
1795 }
1796}