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