Agent I-0: real clef + key-signature data in the core graph
Prerequisite for the visible-slice engraving milestones (I-1+): give the
score graph the clef/key data the engrave pipeline needs for clef-relative
staff positioning and key-signature rendering, replacing the Chapter-7
TimeAnchor-only placeholders.
epiphany-core:
- ClefShape { G, F, C, Percussion } — each family's reference pitch (G4 / F3 /
middle C4) pins the staff-position mapping.
- Clef { shape, line, octave_shift } — the SMuFL family, the staff line its
reference pitch sits on (1 = bottom line), and an octave transposition; with
treble/bass/alto/tenor constructors and a treble Default. Generalizes to any
C-clef line and octave-transposing clefs.
- KeySignature — circle-of-fifths position, validated -7..=7: a private field
behind KeySignature::new (Option) + fifths(), and a custom Codec that rejects
an out-of-range count on decode (Reconstruct, as PowerOfTwo / Tempo do).
- ClefChange now carries clef: Clef; KeySignatureChange carries key: KeySignature.
- Codec (cstyle_enum_codec / struct_codec / the validating KeySignature impl),
lib.rs re-exports, a round-trip test over every shape x line x octave and every
fifths value, and an out-of-range decode-rejection regression test.
Zero blast radius: nothing constructed these with data (clef/key sequences are
always empty) and invariants only read .anchor, so existing Score bytes, hashes,
and goldens are unchanged. Fixtures get populated and the engrave pipeline reads
the data in I-1.
Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green;
conformance_suite scale 1 passes. Stages only epiphany-core; the pre-existing
Agent-I working tree (engrave/layout-ir/render-svg + .gitignore) stays unstaged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dfdbc625b1
commit
47a581a4de
|
|
@ -48,15 +48,16 @@ use crate::event::{
|
||||||
use crate::graph::{
|
use crate::graph::{
|
||||||
AleatoricAnchoringDiscipline, AleatoricTimeModel, AnalysisLayer, AnalyticalAnnotation,
|
AleatoricAnchoringDiscipline, AleatoricTimeModel, AnalysisLayer, AnalyticalAnnotation,
|
||||||
AnnotationAnchor, BarlineAlignmentGroup, BarlineAlignmentMember, Beam, BeatGroup, Canvas,
|
AnnotationAnchor, BarlineAlignmentGroup, BarlineAlignmentMember, Beam, BeatGroup, Canvas,
|
||||||
ChordSymbol, ClefChange, Comment, CrossCuttingRegistry, DecompositionAttachment,
|
ChordSymbol, Clef, ClefChange, ClefShape, Comment, CrossCuttingRegistry,
|
||||||
DecompositionSource, EventOrderingDAG, GestureAnchoring, GraphicContent, GraphicGesture,
|
DecompositionAttachment, DecompositionSource, EventOrderingDAG, GestureAnchoring,
|
||||||
GraphicObject, Instrument, KeySignatureChange, LyricLine, Marker, Measure,
|
GraphicContent, GraphicGesture, GraphicObject, Instrument, KeySignature, KeySignatureChange,
|
||||||
MeasureNumberVisibility, MeterChange, MetricGrid, MetricTimeModel, NotatedComponent, NoteValue,
|
LyricLine, Marker, Measure, MeasureNumberVisibility, MeterChange, MetricGrid, MetricTimeModel,
|
||||||
PartDefinition, PowerOfTwo, ProportionalTimeModel, Region, RegionContent, RegionTimeModel,
|
NotatedComponent, NoteValue, PartDefinition, PowerOfTwo, ProportionalTimeModel, Region,
|
||||||
RepeatStructure, Score, ScoreMetadata, ScoreTuningContext, Slur, Spanner, Staff,
|
RegionContent, RegionTimeModel, RepeatStructure, Score, ScoreMetadata, ScoreTuningContext,
|
||||||
StaffBasedContent, StaffExtent, StaffGroup, StaffGroupKind, StaffInstance,
|
Slur, Spanner, Staff, StaffBasedContent, StaffExtent, StaffGroup, StaffGroupKind,
|
||||||
StaffLineConfiguration, StemDirection, TempoMapReference, Tie, TieClass, TimeExtent,
|
StaffInstance, StaffLineConfiguration, StemDirection, TempoMapReference, Tie, TieClass,
|
||||||
TimeSignature, TimeSignatureDisplay, Tuplet, TupletRatio, ViewDefinition, Voice, VoiceOrigin,
|
TimeExtent, TimeSignature, TimeSignatureDisplay, Tuplet, TupletRatio, ViewDefinition, Voice,
|
||||||
|
VoiceOrigin,
|
||||||
};
|
};
|
||||||
use crate::ids::{
|
use crate::ids::{
|
||||||
AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId, ChordSymbolId,
|
AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId, ChordSymbolId,
|
||||||
|
|
@ -1453,8 +1454,27 @@ struct_codec!(MeterChange {
|
||||||
struct_codec!(MetricGrid { meter_sequence });
|
struct_codec!(MetricGrid { meter_sequence });
|
||||||
struct_codec!(ProportionalTimeModel { duration });
|
struct_codec!(ProportionalTimeModel { duration });
|
||||||
struct_codec!(MetricTimeModel { meters, tempo });
|
struct_codec!(MetricTimeModel { meters, tempo });
|
||||||
struct_codec!(ClefChange { anchor });
|
cstyle_enum_codec!(ClefShape {
|
||||||
struct_codec!(KeySignatureChange { anchor });
|
0 => G,
|
||||||
|
1 => F,
|
||||||
|
2 => C,
|
||||||
|
3 => Percussion,
|
||||||
|
});
|
||||||
|
struct_codec!(Clef {
|
||||||
|
shape,
|
||||||
|
line,
|
||||||
|
octave_shift
|
||||||
|
});
|
||||||
|
impl Codec for KeySignature {
|
||||||
|
fn enc(&self, out: &mut Vec<u8>) {
|
||||||
|
self.fifths().enc(out);
|
||||||
|
}
|
||||||
|
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||||||
|
KeySignature::new(i8::dec(r)?).ok_or(ScoreDecodeError::Reconstruct("KeySignature"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
struct_codec!(ClefChange { anchor, clef });
|
||||||
|
struct_codec!(KeySignatureChange { anchor, key });
|
||||||
struct_codec!(Measure {
|
struct_codec!(Measure {
|
||||||
id,
|
id,
|
||||||
start,
|
start,
|
||||||
|
|
@ -2193,6 +2213,72 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clef_and_key_signature_codecs_round_trip() {
|
||||||
|
use crate::graph::{Clef, ClefChange, ClefShape, KeySignature, KeySignatureChange};
|
||||||
|
use crate::time::{TimeAnchor, WallClockTime};
|
||||||
|
|
||||||
|
fn rt<T: Codec + PartialEq + core::fmt::Debug>(v: &T) {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
v.enc(&mut out);
|
||||||
|
let mut r = Reader::new(&out);
|
||||||
|
let decoded = T::dec(&mut r).expect("decodes");
|
||||||
|
assert_eq!(&decoded, v, "round-trip changed the value");
|
||||||
|
}
|
||||||
|
|
||||||
|
for shape in [
|
||||||
|
ClefShape::G,
|
||||||
|
ClefShape::F,
|
||||||
|
ClefShape::C,
|
||||||
|
ClefShape::Percussion,
|
||||||
|
] {
|
||||||
|
rt(&shape);
|
||||||
|
for line in [-3i8, 1, 2, 3, 4, 5] {
|
||||||
|
for octave_shift in [-1i8, 0, 1] {
|
||||||
|
rt(&Clef {
|
||||||
|
shape,
|
||||||
|
line,
|
||||||
|
octave_shift,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for shape_clef in [Clef::treble(), Clef::bass(), Clef::alto(), Clef::tenor()] {
|
||||||
|
rt(&shape_clef);
|
||||||
|
}
|
||||||
|
for fifths in -7i8..=7 {
|
||||||
|
rt(&KeySignature::new(fifths).expect("fifths is in range"));
|
||||||
|
}
|
||||||
|
let anchor = TimeAnchor::WallClock {
|
||||||
|
time: WallClockTime(0),
|
||||||
|
};
|
||||||
|
rt(&ClefChange {
|
||||||
|
anchor: anchor.clone(),
|
||||||
|
clef: Clef::bass(),
|
||||||
|
});
|
||||||
|
rt(&KeySignatureChange {
|
||||||
|
anchor,
|
||||||
|
key: KeySignature::new(-3).expect("fifths is in range"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_signature_rejects_out_of_range_fifths_on_decode() {
|
||||||
|
let decode = |fifths: i8| {
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
fifths.enc(&mut bytes);
|
||||||
|
KeySignature::dec(&mut Reader::new(&bytes))
|
||||||
|
};
|
||||||
|
assert!(decode(-7).is_ok());
|
||||||
|
assert!(decode(7).is_ok());
|
||||||
|
for fifths in [-8i8, 8] {
|
||||||
|
assert!(
|
||||||
|
matches!(decode(fifths), Err(ScoreDecodeError::Reconstruct(_))),
|
||||||
|
"out-of-range fifth count {fifths} must be rejected"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn generator_scores_round_trip() {
|
fn generator_scores_round_trip() {
|
||||||
for seed in 0..200u64 {
|
for seed in 0..200u64 {
|
||||||
|
|
|
||||||
|
|
@ -51,17 +51,116 @@ impl Default for StaffLineConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A clef placed at a point in a staff instance. Placeholder (Chapter 7).
|
/// The SMuFL clef family a [`Clef`] draws from. The reference pitch each family
|
||||||
|
/// fixes (G4 / F3 / middle C4) is what pins the staff-position mapping.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub enum ClefShape {
|
||||||
|
/// G clef (treble family) — reference pitch G4.
|
||||||
|
G,
|
||||||
|
/// F clef (bass family) — reference pitch F3.
|
||||||
|
F,
|
||||||
|
/// C clef (alto / tenor family) — reference pitch middle C (C4).
|
||||||
|
C,
|
||||||
|
/// Unpitched percussion clef — no diatonic reference.
|
||||||
|
Percussion,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clef: the SMuFL [`ClefShape`], the staff line its reference pitch sits on
|
||||||
|
/// (`1` = the bottom line of the staff, counting up), and an octave
|
||||||
|
/// transposition (`-1` for treble-8vb, `+1` for treble-8va, …). The shape's
|
||||||
|
/// reference pitch on `line` fixes where every pitch under this clef lands on
|
||||||
|
/// the staff.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
|
pub struct Clef {
|
||||||
|
pub shape: ClefShape,
|
||||||
|
pub line: i8,
|
||||||
|
pub octave_shift: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clef {
|
||||||
|
/// Treble clef — G clef on line 2.
|
||||||
|
pub const fn treble() -> Self {
|
||||||
|
Clef {
|
||||||
|
shape: ClefShape::G,
|
||||||
|
line: 2,
|
||||||
|
octave_shift: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Bass clef — F clef on line 4.
|
||||||
|
pub const fn bass() -> Self {
|
||||||
|
Clef {
|
||||||
|
shape: ClefShape::F,
|
||||||
|
line: 4,
|
||||||
|
octave_shift: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Alto clef — C clef on line 3.
|
||||||
|
pub const fn alto() -> Self {
|
||||||
|
Clef {
|
||||||
|
shape: ClefShape::C,
|
||||||
|
line: 3,
|
||||||
|
octave_shift: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Tenor clef — C clef on line 4.
|
||||||
|
pub const fn tenor() -> Self {
|
||||||
|
Clef {
|
||||||
|
shape: ClefShape::C,
|
||||||
|
line: 4,
|
||||||
|
octave_shift: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Clef {
|
||||||
|
fn default() -> Self {
|
||||||
|
Clef::treble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A key signature as a position on the circle of fifths: `fifths` sharps when
|
||||||
|
/// positive (`1` = G major, one sharp), flats when negative (`-1` = F major,
|
||||||
|
/// one flat), and `0` for C major / A minor. The accidental set the renderer
|
||||||
|
/// draws is derived from this count.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, Default)]
|
||||||
|
pub struct KeySignature {
|
||||||
|
fifths: i8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeySignature {
|
||||||
|
/// Lowest key-signature fifth count in conventional CMN notation: seven flats.
|
||||||
|
pub const MIN_FIFTHS: i8 = -7;
|
||||||
|
/// Highest key-signature fifth count in conventional CMN notation: seven sharps.
|
||||||
|
pub const MAX_FIFTHS: i8 = 7;
|
||||||
|
|
||||||
|
/// Builds a key signature, rejecting values outside the conventional
|
||||||
|
/// `-7..=7` circle-of-fifths range.
|
||||||
|
pub const fn new(fifths: i8) -> Option<Self> {
|
||||||
|
if fifths >= Self::MIN_FIFTHS && fifths <= Self::MAX_FIFTHS {
|
||||||
|
Some(KeySignature { fifths })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The circle-of-fifths position (`-7..=7`).
|
||||||
|
pub const fn fifths(self) -> i8 {
|
||||||
|
self.fifths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A clef placed at a point in a staff instance (Chapter 7).
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct ClefChange {
|
pub struct ClefChange {
|
||||||
pub anchor: TimeAnchor,
|
pub anchor: TimeAnchor,
|
||||||
|
pub clef: Clef,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A key-signature change at a point in a staff instance. Placeholder
|
/// A key-signature change at a point in a staff instance (Chapter 7).
|
||||||
/// (Chapter 7).
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub struct KeySignatureChange {
|
pub struct KeySignatureChange {
|
||||||
pub anchor: TimeAnchor,
|
pub anchor: TimeAnchor,
|
||||||
|
pub key: KeySignature,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether and how a measure number is shown. Placeholder (Chapter 7).
|
/// Whether and how a measure number is shown. Placeholder (Chapter 7).
|
||||||
|
|
|
||||||
|
|
@ -99,9 +99,9 @@ pub use event::{
|
||||||
pub use graph::{
|
pub use graph::{
|
||||||
derive_promoted_voice_id, AleatoricAnchoringDiscipline, AleatoricTimeModel, AnalysisLayer,
|
derive_promoted_voice_id, AleatoricAnchoringDiscipline, AleatoricTimeModel, AnalysisLayer,
|
||||||
AnalyticalAnnotation, AnnotationAnchor, BarlineAlignmentGroup, BarlineAlignmentMember, Beam,
|
AnalyticalAnnotation, AnnotationAnchor, BarlineAlignmentGroup, BarlineAlignmentMember, Beam,
|
||||||
BeatGroup, Canvas, ChordSymbol, ClefChange, Comment, CoordinateDiscipline,
|
BeatGroup, Canvas, ChordSymbol, Clef, ClefChange, ClefShape, Comment, CoordinateDiscipline,
|
||||||
CrossCuttingRegistry, DecompositionAttachment, DecompositionSource, EventOrderingDAG,
|
CrossCuttingRegistry, DecompositionAttachment, DecompositionSource, EventOrderingDAG,
|
||||||
GestureAnchoring, GraphicContent, GraphicGesture, GraphicObject, Instrument,
|
GestureAnchoring, GraphicContent, GraphicGesture, GraphicObject, Instrument, KeySignature,
|
||||||
KeySignatureChange, LyricLine, Marker, Measure, MeasureNumberVisibility, MeterChange,
|
KeySignatureChange, LyricLine, Marker, Measure, MeasureNumberVisibility, MeterChange,
|
||||||
MetricGrid, MetricTimeModel, NotatedComponent, NoteValue, PartDefinition, PowerOfTwo,
|
MetricGrid, MetricTimeModel, NotatedComponent, NoteValue, PartDefinition, PowerOfTwo,
|
||||||
ProportionalTimeModel, Region, RegionContent, RegionTimeModel, RepeatStructure, Score,
|
ProportionalTimeModel, Region, RegionContent, RegionTimeModel, RepeatStructure, Score,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue