use rivet_core::*; fn demo() -> Session { Session::demo() } #[test] fn templates_build_the_story_from_one_motif() { let s = demo(); let roles: Vec<_> = s.blocks.iter().map(|b| b.role).collect(); assert_eq!( roles, [Role::Statement, Role::Answer, Role::Departure, Role::Return] ); assert_eq!(s.total_bars(), 8); assert!(s.blocks.iter().all(|b| b.options.len() == 3)); let phrase = s.compose().unwrap(); assert_eq!(phrase.length, 8 * BAR); assert_eq!( &phrase.notes[..demo_motif().notes.len()], demo_motif().notes.as_slice(), "the story opens with the motif" ); let mut s = demo(); let id = s.motifs[0].id; s.apply_template(Template::Breathe, id).unwrap(); assert_eq!(s.blocks[1].role, Role::Rest); assert_eq!(s.blocks[1].bars, 1); assert_eq!(s.total_bars(), 8); } #[test] fn blocks_have_free_lengths_up_to_the_storyboard_limit() { let mut s = demo(); let motif = s.motifs[0].id; s.insert_block(4, motif, Role::Departure, 5).unwrap(); s.insert_block(5, motif, Role::Rest, 1).unwrap(); assert_eq!(s.total_bars(), 14); assert_eq!(s.compose().unwrap().length, 14 * BAR); assert!(s.insert_block(0, motif, Role::Answer, 9).is_err()); while s.total_bars() + 8 <= MAX_BARS { s.insert_block(0, motif, Role::Answer, 8).unwrap(); } assert!(s.insert_block(0, motif, Role::Answer, 8).is_err()); s.validate().unwrap(); } #[test] fn joins_shape_the_seam_without_crossing_it() { let mut s = demo(); let first = s.blocks[1].phrase().unwrap().notes[0]; let bar = s.settings.meter.bar(); for join in Join::ALL { s.blocks[0].join = join; let phrase = s.compose().unwrap(); let tail: Vec<_> = phrase .notes .iter() .filter(|n| n.start < 2 * bar) .copied() .collect(); assert!(tail.iter().all(|n| n.end() <= 2 * bar), "{join:?}"); let last = tail.last().unwrap(); match join { Join::Cut => assert_eq!(tail, s.blocks[0].phrase().unwrap().notes), Join::Breath => assert!(last.end() <= 2 * bar - s.settings.meter.beat() / 2), Join::Approach => { assert_ne!(last.pitch, first.pitch); assert!(last.pitch.abs_diff(first.pitch) <= 2); } Join::Pickup => { let pickup: Vec<_> = tail .iter() .filter(|n| n.start >= 2 * bar - s.settings.meter.beat()) .collect(); assert_eq!(pickup.len(), 2); let direction = (pickup[1].pitch as i16 - pickup[0].pitch as i16).signum(); let into = (first.pitch as i16 - pickup[1].pitch as i16).signum(); assert_eq!(direction, into, "the pickup moves toward the next block"); } } } } #[test] fn kept_blocks_survive_new_settings_and_other_blocks_follow_them() { let mut s = demo(); s.blocks[1].choice = 2; s.blocks[1].kept = true; let kept = s.blocks[1].phrase().unwrap().clone(); let free = s.blocks[2].options.clone(); s.settings.seed += 7; s.realize_all(); assert_eq!(s.blocks[1].phrase().unwrap(), &kept); assert_ne!(s.blocks[2].options, free); s.another_take(1); assert!(!s.blocks[1].kept); assert_eq!(Session::from_json(&s.to_json().unwrap()).unwrap(), s); } #[test] fn compound_and_odd_meters_compose_whole_bars() { for meter in [ Meter { beats: 3, unit: 4 }, Meter { beats: 6, unit: 8 }, Meter { beats: 7, unit: 8 }, ] { let mut s = Session::default(); s.settings.meter = meter; let motif = Phrase { length: meter.bar(), notes: (0..3) .map(|i| Note { start: i * meter.beat() / 2, duration: meter.beat() / 2, pitch: 60 + 2 * i as u8, velocity: 90, ..Default::default() }) .collect(), }; let id = s.add_motif("Waltz", motif).unwrap(); s.apply_template(Template::Build, id).unwrap(); let phrase = s.compose().unwrap(); assert_eq!(phrase.length, s.total_bars() * meter.bar()); s.validate().unwrap(); } } #[test] fn removing_a_motif_removes_its_blocks_and_limits_hold() { let mut s = demo(); let id = s.motifs[0].id; let other = s.add_motif("Second", demo_motif()).unwrap(); s.insert_block(4, other, Role::Answer, 2).unwrap(); s.remove_motif(id); assert_eq!(s.blocks.len(), 1); assert_eq!(s.blocks[0].motif, other); let long = Phrase { length: 9 * BAR, ..demo_motif() }; assert!(s.add_motif("Too long", long).is_err()); for i in s.motifs.len()..MAX_MOTIFS { s.add_motif(&format!("M{i}"), demo_motif()).unwrap(); } assert!(s.add_motif("One more", demo_motif()).is_err()); } #[test] fn version_three_sessions_become_a_storyboard_without_losing_notes() { let mut source = demo_motif(); source.notes[2].locked = true; for (bars, blocks) in [(8, 4), (4, 2)] { let old = serde_json::json!({ "version": 3, "source": source, "settings": {"bars": bars, "root": 2, "scale": "Dorian", "variation": 40, "preserve_rhythm": false, "preserve_contour": true, "task": "Answer", "ending": "Open", "low": 50, "high": 80, "seed": 9}, "candidates": [], "selected": 0, "accepted": null, "previous_accepted": null, "tempo": 128.0, "follow_host": true, "pins": [], "looping": true, "gestures": [{"id": 1, "name": "Hook", "phrase": demo_motif(), "performance": PerformanceSettings::default()}], "branches": [], "active_branch": null, "next_id": 2 }); let s = Session::from_json(&serde_json::to_vec(&old).unwrap()).unwrap(); assert_eq!(s.version, STATE_VERSION); assert_eq!(s.motifs[0].phrase, source); assert_eq!(s.motifs[1].name, "Hook"); assert_eq!(s.blocks.len(), blocks); assert_eq!(s.blocks.last().unwrap().role, Role::Return); assert_eq!(s.settings.root, 2); assert_eq!(s.settings.scale, Scale::Dorian); assert_eq!(s.settings.ending, Ending::Open); assert_eq!(s.tempo, 128.); assert!(s.follow_host); assert!(s.compose().is_ok()); } let v1 = serde_json::json!({"version": 1, "source": demo_motif(), "settings": {"bars": 8}, "candidates": [], "selected": 0, "accepted": null, "previous_accepted": null, "tempo": 140.0, "follow_host": false}); let s = Session::from_json(&serde_json::to_vec(&v1).unwrap()).unwrap(); assert!(!s.looping, "version-1 sessions kept one-shot audition"); assert!(s.harmony.spans.is_empty()); let mut future = serde_json::to_value(Session::default()).unwrap(); future["version"] = 99.into(); assert!(Session::from_json(&serde_json::to_vec(&future).unwrap()).is_err()); }