Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 37 additions & 11 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -930,8 +930,10 @@ fn chunks_for_segments(
let title = format!("{} (phrase {phrase})", inputs.title);
let mut meta = build_chunk_meta(&sub, path, Some(track), id, title, inputs, None);
let last = end.saturating_sub(1);
meta.source.bar_range =
Some((u32::try_from(start).unwrap_or(0), u32::try_from(last).unwrap_or(0)));
meta.source.bar_range = Some((
u32::try_from(start).unwrap_or(0),
u32::try_from(last).unwrap_or(0),
));
meta
})
.collect()
Expand Down Expand Up @@ -1580,16 +1582,28 @@ mod tests {
);

// `techniques` is auto-filled from the notation…
assert!(meta.techniques.contains(&"hammer_on".to_owned()), "{:?}", meta.techniques);
assert!(
meta.techniques.contains(&"hammer_on".to_owned()),
"{:?}",
meta.techniques
);
assert!(
meta.techniques.contains(&"pinch_harmonic".to_owned()),
"{:?}",
meta.techniques
);
// …and the curator's hand-picked tag survives alongside the derived ones.
assert!(meta.tags.contains(&SwancoreTag::Intro), "{:?}", meta.tags);
assert!(meta.tags.contains(&SwancoreTag::HammerOn), "{:?}", meta.tags);
assert!(meta.tags.contains(&SwancoreTag::ArtificialHarmonic), "{:?}", meta.tags);
assert!(
meta.tags.contains(&SwancoreTag::HammerOn),
"{:?}",
meta.tags
);
assert!(
meta.tags.contains(&SwancoreTag::ArtificialHarmonic),
"{:?}",
meta.tags
);
}

#[test]
Expand Down Expand Up @@ -1663,7 +1677,12 @@ mod tests {
// Four bars, a note on each downbeat.
let voice = voice_of(
0,
vec![quarter(0, 60), quarter(1920, 62), quarter(3840, 64), quarter(5760, 65)],
vec![
quarter(0, 60),
quarter(1920, 62),
quarter(3840, 64),
quarter(5760, 65),
],
);
let score = Score {
ticks_per_quarter: 480,
Expand All @@ -1673,8 +1692,7 @@ mod tests {
loss: LossReport::new(),
};

let chunks =
phrase_chunks(Path::new("riff.gp5"), &score, &split_inputs()).expect("splits");
let chunks = phrase_chunks(Path::new("riff.gp5"), &score, &split_inputs()).expect("splits");
assert!(!chunks.is_empty(), "at least one phrase chunk");

// Whatever the detector decides, the chunks tile the four bars with
Expand Down Expand Up @@ -1706,8 +1724,13 @@ mod tests {
};

// A sounding [0,2) segment and a silent [2,4) one.
let chunks =
chunks_for_segments(Path::new("riff.gp5"), &score, &split_inputs(), 0, &[0..2, 2..4]);
let chunks = chunks_for_segments(
Path::new("riff.gp5"),
&score,
&split_inputs(),
0,
&[0..2, 2..4],
);
assert_eq!(chunks.len(), 1, "the silent [2,4) segment is dropped");
assert_eq!(
chunks[0].source.bar_range,
Expand All @@ -1728,7 +1751,10 @@ mod tests {
// track, is a rest in this phrase and must be dropped rather than
// re-measured on the later track that happens to have notes there.
let detected = track_of(vec![voice_of(0, vec![quarter(0, 60), quarter(1920, 62)])]);
let other = track_of(vec![voice_of(0, vec![quarter(3840, 48), quarter(5760, 50)])]);
let other = track_of(vec![voice_of(
0,
vec![quarter(3840, 48), quarter(5760, 50)],
)]);
let score = Score {
ticks_per_quarter: 480,
master_bars: split_master_bars(),
Expand Down
5 changes: 4 additions & 1 deletion core/src/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,10 @@ fn norm_bar(track: &Track, bar: &MasterBar) -> NormBar {
.iter()
.filter_map(|voice| {
let notes = voice_notes_in_bar(voice, bar);
(!notes.is_empty()).then_some(NormVoice { id: voice.id, notes })
(!notes.is_empty()).then_some(NormVoice {
id: voice.id,
notes,
})
})
.collect();
// Canonical order independent of `track.voices` import order (ADR-0020).
Expand Down
7 changes: 4 additions & 3 deletions core/src/novelty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,10 @@ pub fn flag_phrase_duplicates(
#[allow(clippy::cast_precision_loss)]
let share = report.longest_match_notes as f64 / report.candidate_notes as f64;
match report.longest_match_reference {
Some(of) if share >= min_quote_share => {
Some(PhraseDuplicate { of, quote_share: share })
}
Some(of) if share >= min_quote_share => Some(PhraseDuplicate {
of,
quote_share: share,
}),
_ => None,
}
}
Expand Down
5 changes: 4 additions & 1 deletion core/src/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,10 @@ mod tests {
fn cuts_snap_to_their_containing_bar() {
// 3840 is bar 2's downbeat; 1920 is bar 1's, 5760 is bar 3's.
assert_eq!(bar_segments(&bars(4), &[3840]), vec![0..2, 2..4]);
assert_eq!(bar_segments(&bars(4), &[1920, 5760]), vec![0..1, 1..3, 3..4]);
assert_eq!(
bar_segments(&bars(4), &[1920, 5760]),
vec![0..1, 1..3, 3..4]
);
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion core/tests/dump_golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ fn sample_score() -> Score {
pitch: pitch(64),
velocity: Velocity::new(80).expect("valid velocity"),
marks: NoteMarks::empty().with(NoteMark::Accent),
position: Some(NotePosition::explicit(FretboardPosition { string: 1, fret: 0 })),
position: Some(NotePosition::explicit(FretboardPosition {
string: 1,
fret: 0,
})),
});
let plain = AtomEvent::Note(AtomNote {
absolute_start: Ticks(480),
Expand Down
5 changes: 4 additions & 1 deletion core/tests/novelty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,5 +439,8 @@ fn flag_phrase_duplicates_compares_the_detected_track_on_both_sides() {
let flags = flag_phrase_duplicates(&phrases, 1, 0.8);
let dup = flags[1].expect("the track-1 repeat is flagged despite a different track 0");
assert_eq!(dup.of, 0);
assert!(dup.quote_share >= 0.99, "the track-1 line is a verbatim repeat");
assert!(
dup.quote_share >= 0.99,
"the track-1 line is a verbatim repeat"
);
}
15 changes: 11 additions & 4 deletions core/tests/slice_extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ fn bar(index: usize, start: u32) -> MasterBar {

/// Four bars, one note on each downbeat (pitch 60 + bar index).
fn four_bar_score() -> Score {
let notes = [note(0, 60), note(BAR, 61), note(2 * BAR, 62), note(3 * BAR, 63)];
let notes = [
note(0, 60),
note(BAR, 61),
note(2 * BAR, 62),
note(3 * BAR, 63),
];
Score {
ticks_per_quarter: 480,
master_bars: vec![bar(0, 0), bar(1, BAR), bar(2, 2 * BAR), bar(3, 3 * BAR)],
Expand All @@ -77,8 +82,7 @@ fn four_bar_score() -> Score {
}

fn onsets(score: &Score) -> Vec<(u32, u8)> {
score.tracks[0]
.voices[0]
score.tracks[0].voices[0]
.event_groups
.iter()
.flat_map(|g| &g.atoms)
Expand Down Expand Up @@ -117,5 +121,8 @@ fn empty_range_yields_no_bars_and_out_of_range_end_clamps() {
// End past the last bar clamps to what exists (all four bars here).
let all = extract_bars(&four_bar_score(), 0..9);
assert_eq!(all.master_bars.len(), 4);
assert_eq!(onsets(&all), vec![(0, 60), (BAR, 61), (2 * BAR, 62), (3 * BAR, 63)]);
assert_eq!(
onsets(&all),
vec![(0, 60), (BAR, 61), (2 * BAR, 62), (3 * BAR, 63)]
);
}
43 changes: 36 additions & 7 deletions core/tests/technique_tags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,27 +83,48 @@ fn maps_spans_and_marks_to_direct_tags_and_a_superset_name_list() {
assert!(d.tags.contains(&SwancoreTag::ArtificialHarmonic));
// Legato and accent have no dedicated SwancoreTag — they must NOT be tagged…
assert!(!d.tags.contains(&SwancoreTag::NaturalHarmonic));
assert_eq!(d.tags.len(), 3, "only the directly-taggable techniques: {:?}", d.tags);
assert_eq!(
d.tags.len(),
3,
"only the directly-taggable techniques: {:?}",
d.tags
);

// …but the free-form name list is the superset and records them anyway.
for name in ["hammer_on", "palm_mute", "legato", "pinch_harmonic", "accent"] {
assert!(d.names.contains(&name.to_owned()), "names missing {name}: {:?}", d.names);
for name in [
"hammer_on",
"palm_mute",
"legato",
"pinch_harmonic",
"accent",
] {
assert!(
d.names.contains(&name.to_owned()),
"names missing {name}: {:?}",
d.names
);
}
}

#[test]
fn let_ring_span_derives_the_let_ring_tag_and_name() {
// let-ring is newly parsed from GP (#75): a LetRing span must auto-derive
// both the dedicated SwancoreTag::LetRing and the "let_ring" name.
let d = derive_techniques(&score_with(&[SpanTechnique::LetRing], NoteMarks::empty()), 0);
let d = derive_techniques(
&score_with(&[SpanTechnique::LetRing], NoteMarks::empty()),
0,
);
assert_eq!(d.tags, vec![SwancoreTag::LetRing]);
assert_eq!(d.names, vec!["let_ring".to_owned()]);
}

#[test]
fn presence_only_so_repeats_dedupe_and_output_is_deterministic() {
// The same technique across groups/notes is "present", counted once.
let score = score_with(&[SpanTechnique::Slide, SpanTechnique::Slide], NoteMarks::empty());
let score = score_with(
&[SpanTechnique::Slide, SpanTechnique::Slide],
NoteMarks::empty(),
);
let a = derive_techniques(&score, 0);
let b = derive_techniques(&score, 0);
assert_eq!(a, b, "pure function of the score (SPEC §6)");
Expand All @@ -125,7 +146,11 @@ fn merge_tags_keeps_chosen_order_and_appends_only_new_derived() {
// HammerOn was already chosen → not duplicated; PalmMute is appended.
assert_eq!(
merge_tags(&chosen, &derived),
vec![SwancoreTag::Intro, SwancoreTag::HammerOn, SwancoreTag::PalmMute]
vec![
SwancoreTag::Intro,
SwancoreTag::HammerOn,
SwancoreTag::PalmMute
]
);
// Idempotent: merging the result again changes nothing.
let once = merge_tags(&chosen, &derived);
Expand Down Expand Up @@ -158,7 +183,11 @@ fn derives_from_all_voices_like_structure_measures() {
}],
});
let d = derive_techniques(&score, 0);
assert!(d.tags.contains(&SwancoreTag::PalmMute), "secondary-voice span: {:?}", d.tags);
assert!(
d.tags.contains(&SwancoreTag::PalmMute),
"secondary-voice span: {:?}",
d.tags
);
assert!(
d.tags.contains(&SwancoreTag::ArtificialHarmonic),
"secondary-voice mark: {:?}",
Expand Down