4747 lines
164 KiB
Rust
4747 lines
164 KiB
Rust
//! Canonical byte codec for the whole [`Score`] graph (item 5 / Agent B's
|
||
//! "whole-score codec").
|
||
//!
|
||
//! The determinism crate fixes the canonical byte form of the *primitives*
|
||
//! (identifiers, hashes, [`CanonicalF64`], [`RationalTime`]); this module
|
||
//! composes those into a total, reversible byte form for every value reachable
|
||
//! from a [`Score`], so the materialized graph itself — not just the Chapter 6
|
||
//! bookkeeping projection — has a canonical serialization
|
||
//! ([`Score::canonical_bytes`] / [`Score::decode_canonical`]).
|
||
//!
|
||
//! The form is deliberately boring and total, mirroring `epiphany-ops`'
|
||
//! [`MaterializedState`](crate) codec and Appendix D §"Canonical serialization
|
||
//! determinism":
|
||
//!
|
||
//! * Integers are little-endian. Booleans are a single `0`/`1` byte.
|
||
//! * Every variable-width *leaf* (an id, a [`RationalTime`], a string) is
|
||
//! `u32`-length-prefixed, so the decoder never guesses a boundary and is
|
||
//! width-agnostic.
|
||
//! * Composites encode their fields in declaration order; the decoder knows the
|
||
//! structure, so fixed-shape composites are not length-prefixed.
|
||
//! * Sequences and maps carry a `u32` count then each element (maps/sets in
|
||
//! their canonical key order). Maps and sets are rebuilt on decode.
|
||
//! * Tagged unions carry a single discriminant byte then the variant payload.
|
||
//! * Strings are length-prefixed UTF-8 and are **not** NFC-folded here: the
|
||
//! graph's [`Score`] equality is byte-exact on its [`String`] fields, so the
|
||
//! codec preserves them exactly (`decode(encode(x)) == x` for every valid
|
||
//! score). Catalog ids are already NFC at construction.
|
||
//!
|
||
//! This is a concrete, reversible canonical form that predates the Binary Format
|
||
//! companion specification; when that lands, reconcile the two (see
|
||
//! `DECISIONS.md`, P11-4). The convention set above is RATIFIED by Pass 11
|
||
//! (item 1.8): core_spec §"Binary Format Companion",
|
||
//! Requirement `req:format:codec-conventions` blesses these conventions as the
|
||
//! companion's inherited baseline, so core/ops/bundle stay mutually consistent
|
||
//! until Agent J formalizes the full companion.
|
||
|
||
use core::num::NonZeroU16;
|
||
use std::collections::{BTreeMap, BTreeSet};
|
||
|
||
use epiphany_determinism::{CanonicalDecode, CanonicalEncode, CanonicalF64, ContentHash};
|
||
|
||
use crate::accidental::{SmuflVersion, SmuflVersionRequirement};
|
||
use crate::event::{
|
||
ArticulationMark, CueEvent, CueRendering, DynamicMark, Event, EventArena, GraceKind,
|
||
GraphicEvent, IndeterminacyHints, IndeterminacyKind, IndeterminateEvent, OrnamentMark,
|
||
PitchedEvent, PlaybackBinding, Rest, StaffPosition, StemConfiguration, TrajectoryDisplay,
|
||
TrajectoryEndpoint, TrajectoryEvent, TrajectoryShape, UnpitchedEvent, UnpitchedMemberId,
|
||
};
|
||
use crate::graph::{
|
||
AleatoricAnchoringDiscipline, AleatoricTimeModel, AnalysisLayer, AnalyticalAnnotation,
|
||
AnnotationAnchor, BarlineAlignmentGroup, BarlineAlignmentMember, Beam, BeamGeometryOverride,
|
||
BeatGroup, BracketKind, Canvas, CanvasLayoutDefaults, CanvasMargins, CanvasSize, ChordSymbol,
|
||
Clef, ClefChange, ClefShape, Comment, CrossCuttingRegistry, CurvatureOverride, CurveDirection,
|
||
DecompositionAttachment, DecompositionSource, EventOrderingDAG, GestureAnchoring,
|
||
GraphicContent, GraphicGesture, GraphicObject, HairpinDirection, Instrument, KeySignature,
|
||
KeySignatureChange, LineStyle, LyricLine, Marker, Measure, MeasureNumberVisibility,
|
||
MetadataEntry, MetadataValue, MeterChange, MetricGrid, MetricTimeModel, NotatedComponent,
|
||
NoteValue, OctaveOffset, PartDefinition, PedalKind, PowerOfTwo, ProportionalTimeModel, Region,
|
||
RegionContent, RegionTimeModel, RepeatKind, RepeatStructure, Score, ScoreMetadata,
|
||
ScoreTuningContext, Slur, SlurKind, SoundConfiguration, SpaceUnit, SpanStyle, Spanner,
|
||
SpannerKind, Staff, StaffBasedContent, StaffBracketKind, StaffExtent, StaffGroup,
|
||
StaffGroupKind, StaffInstance, StaffLineConfiguration, StemDirection, SubBeam,
|
||
TempoMapReference, TextLineDefinition, Tie, TieClass, TimeExtent, TimeSignature,
|
||
TimeSignatureDisplay, Timestamp, TuningContextSettings, Tuplet, TupletRatio, UnpitchedMember,
|
||
ViewDefinition, Voice, VoiceOrigin, Volta,
|
||
};
|
||
use crate::ids::{
|
||
AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId, ChordSymbolId,
|
||
CommentId, EventId, GraphicGestureId, GraphicObjectId, IdentityContext, InstrumentId,
|
||
LyricLineId, MarkerId, MeasureId, OperationId, PartDefinitionId, PitchId, RegionId,
|
||
RepeatStructureId, ReplicaId, SlurId, SpannerId, StaffGroupId, StaffId, StaffInstanceId, TieId,
|
||
TimeSignatureId, TupletId, ViewId, VoiceId,
|
||
};
|
||
use crate::pitch::{
|
||
AccidentalId, AcousticPitch, AcousticRealization, CmnNominal, ForeignFormatId, IdentifiedPitch,
|
||
NominalRegistryId, Pitch, PitchRange, PitchSpaceId, PitchSpacePosition, PitchSpelling,
|
||
PositionRegistryId, ReferencePitch, ScalePosition, SpellingAttachment, SpellingDirective,
|
||
SpellingNominal, SpellingPrecedence, SpellingRenderHints, SpellingRule, SpellingRuleSetId,
|
||
SpellingScope, SpellingSource, SpellingSourceKind, StaffGroupKindRegistryId,
|
||
TieClassRegistryId, TranspositionInterval, TuningReference, TuningSystemId, VoiceSelector,
|
||
};
|
||
use crate::tempo::{Tempo, TempoMap, TempoSegment, TempoShape};
|
||
use crate::time::{
|
||
AnchorOffset, ConcreteDuration, DurationBounds, EventBounds, EventDuration, EventPosition,
|
||
MeasurePosition, MusicalDuration, MusicalPosition, RationalTime, RegionEdge, TimeAnchor,
|
||
TimeBounds, WallClockDuration, WallClockTime,
|
||
};
|
||
use crate::tuning::{TuningOverride, TuningScope};
|
||
|
||
// ===========================================================================
|
||
// Errors and the reader cursor.
|
||
// ===========================================================================
|
||
|
||
/// Why decoding canonical [`Score`] bytes failed.
|
||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||
pub enum ScoreDecodeError {
|
||
/// A field ended before its declared or fixed width.
|
||
UnexpectedEof,
|
||
/// A length prefix or count cannot be represented as `usize` on this target.
|
||
LengthOverflow,
|
||
/// A tagged union carried an unknown discriminant.
|
||
InvalidTag { kind: &'static str, tag: u8 },
|
||
/// A primitive leaf failed its own canonical decoder.
|
||
InvalidValue(&'static str),
|
||
/// A canonical boolean was not `0` or `1`.
|
||
InvalidBoolean(u8),
|
||
/// A canonical text field was not UTF-8.
|
||
InvalidUtf8,
|
||
/// A value failed a type invariant on reconstruction (e.g. a time signature
|
||
/// whose beat groups do not sum to its measure duration).
|
||
Reconstruct(&'static str),
|
||
/// Bytes remained after the complete score was decoded.
|
||
TrailingBytes,
|
||
}
|
||
|
||
impl core::fmt::Display for ScoreDecodeError {
|
||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||
match self {
|
||
Self::UnexpectedEof => f.write_str("unexpected end of score bytes"),
|
||
Self::LengthOverflow => f.write_str("score length does not fit usize"),
|
||
Self::InvalidTag { kind, tag } => write!(f, "invalid {kind} tag {tag}"),
|
||
Self::InvalidValue(kind) => write!(f, "invalid canonical {kind}"),
|
||
Self::InvalidBoolean(v) => write!(f, "invalid canonical boolean {v}"),
|
||
Self::InvalidUtf8 => f.write_str("invalid UTF-8 in canonical text"),
|
||
Self::Reconstruct(kind) => write!(f, "{kind} failed its invariant on decode"),
|
||
Self::TrailingBytes => f.write_str("trailing bytes after score"),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for ScoreDecodeError {}
|
||
|
||
type Result<T> = core::result::Result<T, ScoreDecodeError>;
|
||
|
||
/// A forward-only cursor over canonical bytes.
|
||
pub(crate) struct Reader<'a> {
|
||
bytes: &'a [u8],
|
||
pos: usize,
|
||
}
|
||
|
||
impl<'a> Reader<'a> {
|
||
fn new(bytes: &'a [u8]) -> Self {
|
||
Self { bytes, pos: 0 }
|
||
}
|
||
|
||
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
|
||
let end = self
|
||
.pos
|
||
.checked_add(n)
|
||
.ok_or(ScoreDecodeError::LengthOverflow)?;
|
||
if end > self.bytes.len() {
|
||
return Err(ScoreDecodeError::UnexpectedEof);
|
||
}
|
||
let out = &self.bytes[self.pos..end];
|
||
self.pos = end;
|
||
Ok(out)
|
||
}
|
||
|
||
fn u8(&mut self) -> Result<u8> {
|
||
Ok(self.take(1)?[0])
|
||
}
|
||
|
||
fn u16(&mut self) -> Result<u16> {
|
||
Ok(u16::from_le_bytes(
|
||
self.take(2)?.try_into().expect("2 bytes"),
|
||
))
|
||
}
|
||
|
||
fn u32(&mut self) -> Result<u32> {
|
||
Ok(u32::from_le_bytes(
|
||
self.take(4)?.try_into().expect("4 bytes"),
|
||
))
|
||
}
|
||
|
||
fn u64(&mut self) -> Result<u64> {
|
||
Ok(u64::from_le_bytes(
|
||
self.take(8)?.try_into().expect("8 bytes"),
|
||
))
|
||
}
|
||
|
||
fn count(&mut self) -> Result<usize> {
|
||
let n = usize::try_from(self.u32()?).map_err(|_| ScoreDecodeError::LengthOverflow)?;
|
||
// A count/length prefix can never exceed the bytes remaining, in both of
|
||
// its uses: a length-prefixed leaf needs exactly `n` further bytes
|
||
// (`lp`), and a collection prefix counts elements, each of which is at
|
||
// least one byte in this codec, so `n` elements need at least `n` bytes.
|
||
// (An empty leaf or collection has `n == 0`, which passes.) A larger `n`
|
||
// is corrupt or adversarial: reject it up front rather than looping
|
||
// element-by-element toward EOF — that bounds decode time on hostile
|
||
// input (a garbage `u32` count is otherwise a soft-DoS: e.g. misparsing
|
||
// v1 bytes as v0 reads a huge count and iterates ~remaining times) and
|
||
// caps the `Vec`/set allocation to a real size.
|
||
if n > self.bytes.len() - self.pos {
|
||
return Err(ScoreDecodeError::UnexpectedEof);
|
||
}
|
||
Ok(n)
|
||
}
|
||
|
||
/// A `u32`-length-prefixed byte slice.
|
||
fn lp(&mut self) -> Result<&'a [u8]> {
|
||
let n = self.count()?;
|
||
self.take(n)
|
||
}
|
||
|
||
fn finish(self) -> Result<()> {
|
||
if self.pos == self.bytes.len() {
|
||
Ok(())
|
||
} else {
|
||
Err(ScoreDecodeError::TrailingBytes)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// The codec trait and combinators.
|
||
// ===========================================================================
|
||
|
||
/// A type with a canonical byte form within a [`Score`]. `enc`/`dec` are exact
|
||
/// inverses, kept adjacent per type so they cannot drift.
|
||
pub(crate) trait Codec: Sized {
|
||
fn enc(&self, out: &mut Vec<u8>);
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self>;
|
||
}
|
||
|
||
/// Appends a `u32` little-endian length/count prefix.
|
||
fn put_len(out: &mut Vec<u8>, n: usize) {
|
||
debug_assert!(n <= u32::MAX as usize, "canonical length exceeds u32");
|
||
out.extend_from_slice(&(n as u32).to_le_bytes());
|
||
}
|
||
|
||
/// A length-prefixed leaf: encode the determinism-canonical bytes behind a
|
||
/// `u32` length so the decoder is width-agnostic.
|
||
fn put_leaf<T: CanonicalEncode>(out: &mut Vec<u8>, v: &T) {
|
||
let mut scratch = Vec::new();
|
||
v.encode_canonical(&mut scratch);
|
||
put_len(out, scratch.len());
|
||
out.extend_from_slice(&scratch);
|
||
}
|
||
|
||
fn get_leaf<T: CanonicalDecode>(r: &mut Reader<'_>, name: &'static str) -> Result<T> {
|
||
let slice = r.lp()?;
|
||
T::decode_canonical(slice).map_err(|_| ScoreDecodeError::InvalidValue(name))
|
||
}
|
||
|
||
// --- Primitive impls. -------------------------------------------------------
|
||
|
||
impl Codec for bool {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.push(*self as u8);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(false),
|
||
1 => Ok(true),
|
||
v => Err(ScoreDecodeError::InvalidBoolean(v)),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for u8 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.push(*self);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
r.u8()
|
||
}
|
||
}
|
||
|
||
impl Codec for u16 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.extend_from_slice(&self.to_le_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
r.u16()
|
||
}
|
||
}
|
||
|
||
impl Codec for u32 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.extend_from_slice(&self.to_le_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
r.u32()
|
||
}
|
||
}
|
||
|
||
impl Codec for u64 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.extend_from_slice(&self.to_le_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
r.u64()
|
||
}
|
||
}
|
||
|
||
impl Codec for i8 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.push(*self as u8);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(r.u8()? as i8)
|
||
}
|
||
}
|
||
|
||
impl Codec for i16 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.extend_from_slice(&self.to_le_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(r.u16()? as i16)
|
||
}
|
||
}
|
||
|
||
impl Codec for i32 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.extend_from_slice(&self.to_le_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(r.u32()? as i32)
|
||
}
|
||
}
|
||
|
||
impl Codec for i64 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.extend_from_slice(&self.to_le_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(r.u64()? as i64)
|
||
}
|
||
}
|
||
|
||
impl Codec for u128 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
out.extend_from_slice(&self.to_le_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(u128::from_le_bytes(
|
||
r.take(16)?.try_into().expect("16 bytes"),
|
||
))
|
||
}
|
||
}
|
||
|
||
impl Codec for String {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
put_len(out, self.len());
|
||
out.extend_from_slice(self.as_bytes());
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let bytes = r.lp()?;
|
||
core::str::from_utf8(bytes)
|
||
.map(|s| s.to_owned())
|
||
.map_err(|_| ScoreDecodeError::InvalidUtf8)
|
||
}
|
||
}
|
||
|
||
// --- Generic combinators. ---------------------------------------------------
|
||
|
||
impl<T: Codec> Codec for Option<T> {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
None => out.push(0),
|
||
Some(v) => {
|
||
out.push(1);
|
||
v.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(None),
|
||
1 => Ok(Some(T::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "Option",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl<T: Codec> Codec for Vec<T> {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
put_len(out, self.len());
|
||
for item in self {
|
||
item.enc(out);
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let n = r.count()?;
|
||
let mut v = Vec::with_capacity(n.min(1024));
|
||
for _ in 0..n {
|
||
v.push(T::dec(r)?);
|
||
}
|
||
Ok(v)
|
||
}
|
||
}
|
||
|
||
impl<T: Codec + Ord> Codec for BTreeSet<T> {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
put_len(out, self.len());
|
||
for item in self {
|
||
item.enc(out);
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let n = r.count()?;
|
||
let mut set = BTreeSet::new();
|
||
for _ in 0..n {
|
||
set.insert(T::dec(r)?);
|
||
}
|
||
Ok(set)
|
||
}
|
||
}
|
||
|
||
impl<K: Codec + Ord, V: Codec> Codec for BTreeMap<K, V> {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
put_len(out, self.len());
|
||
for (k, v) in self {
|
||
k.enc(out);
|
||
v.enc(out);
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let n = r.count()?;
|
||
let mut map = BTreeMap::new();
|
||
for _ in 0..n {
|
||
let k = K::dec(r)?;
|
||
let v = V::dec(r)?;
|
||
map.insert(k, v);
|
||
}
|
||
Ok(map)
|
||
}
|
||
}
|
||
|
||
impl<A: Codec, B: Codec> Codec for (A, B) {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.0.enc(out);
|
||
self.1.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let a = A::dec(r)?;
|
||
let b = B::dec(r)?;
|
||
Ok((a, b))
|
||
}
|
||
}
|
||
|
||
// --- Leaf impls (length-prefixed determinism-canonical bytes). --------------
|
||
|
||
/// Implements [`Codec`] for a determinism-canonical leaf by length-prefixing
|
||
/// its canonical bytes; decode runs the leaf's own validating decoder.
|
||
macro_rules! leaf_codec {
|
||
($($ty:ty => $name:literal),* $(,)?) => {
|
||
$(
|
||
impl Codec for $ty {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
put_leaf(out, self);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
get_leaf::<$ty>(r, $name)
|
||
}
|
||
}
|
||
)*
|
||
};
|
||
}
|
||
|
||
leaf_codec! {
|
||
ReplicaId => "ReplicaId",
|
||
OperationId => "OperationId",
|
||
EventId => "EventId",
|
||
PitchId => "PitchId",
|
||
VoiceId => "VoiceId",
|
||
StaffId => "StaffId",
|
||
StaffInstanceId => "StaffInstanceId",
|
||
StaffGroupId => "StaffGroupId",
|
||
RegionId => "RegionId",
|
||
InstrumentId => "InstrumentId",
|
||
PartDefinitionId => "PartDefinitionId",
|
||
MeasureId => "MeasureId",
|
||
BarlineAlignmentGroupId => "BarlineAlignmentGroupId",
|
||
SlurId => "SlurId",
|
||
TieId => "TieId",
|
||
BeamId => "BeamId",
|
||
SpannerId => "SpannerId",
|
||
TupletId => "TupletId",
|
||
MarkerId => "MarkerId",
|
||
AnalyticalAnnotationId => "AnalyticalAnnotationId",
|
||
CommentId => "CommentId",
|
||
RepeatStructureId => "RepeatStructureId",
|
||
LyricLineId => "LyricLineId",
|
||
ChordSymbolId => "ChordSymbolId",
|
||
GraphicObjectId => "GraphicObjectId",
|
||
GraphicGestureId => "GraphicGestureId",
|
||
TimeSignatureId => "TimeSignatureId",
|
||
AnalysisLayerId => "AnalysisLayerId",
|
||
ViewId => "ViewId",
|
||
ContentHash => "ContentHash",
|
||
CanonicalF64 => "CanonicalF64",
|
||
RationalTime => "RationalTime",
|
||
MusicalPosition => "MusicalPosition",
|
||
MusicalDuration => "MusicalDuration",
|
||
WallClockTime => "WallClockTime",
|
||
WallClockDuration => "WallClockDuration",
|
||
}
|
||
|
||
// ===========================================================================
|
||
// Boilerplate macros for the composite types.
|
||
// ===========================================================================
|
||
|
||
/// [`Codec`] for a plain struct whose fields all implement [`Codec`]: encode in
|
||
/// declaration order; decode the same and rebuild the literal.
|
||
macro_rules! struct_codec {
|
||
($ty:ident { $($field:ident),* $(,)? }) => {
|
||
impl Codec for $ty {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
$( self.$field.enc(out); )*
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
$( let $field = Codec::dec(r)?; )*
|
||
Ok($ty { $($field),* })
|
||
}
|
||
}
|
||
|
||
impl crate::textvalue::TextValue for $ty {
|
||
fn project(&self) -> crate::textvalue::Sexp {
|
||
let $ty { $($field),* } = self;
|
||
crate::textvalue::Sexp::List(vec![
|
||
crate::textvalue::Sexp::Symbol(crate::textvalue::kebab(stringify!($ty))),
|
||
$( crate::textvalue::TextValue::project($field), )*
|
||
])
|
||
}
|
||
fn parse(
|
||
s: &crate::textvalue::Sexp,
|
||
) -> core::result::Result<Self, crate::textvalue::TextError> {
|
||
const ARITY: usize = [$(stringify!($field)),*].len();
|
||
let fields = s.expect_struct(&crate::textvalue::kebab(stringify!($ty)), ARITY)?;
|
||
let mut next = fields.iter();
|
||
$( let $field = crate::textvalue::TextValue::parse(
|
||
next.next().expect("arity checked by expect_struct"))?; )*
|
||
Ok($ty { $($field),* })
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/// [`Codec`] for a zero-field unit struct: no bytes. And its [`TextValue`]: the
|
||
/// bare symbol, as a fieldless variant is. It encodes to no bytes and carries no
|
||
/// value in the text either (`req:textproj:value-projection` clause 1).
|
||
///
|
||
/// [`TextValue`]: crate::textvalue::TextValue
|
||
macro_rules! unit_codec {
|
||
($($ty:ident),* $(,)?) => {
|
||
$(
|
||
impl Codec for $ty {
|
||
fn enc(&self, _out: &mut Vec<u8>) {}
|
||
fn dec(_r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok($ty)
|
||
}
|
||
}
|
||
|
||
impl crate::textvalue::TextValue for $ty {
|
||
fn project(&self) -> crate::textvalue::Sexp {
|
||
crate::textvalue_impls::project_unit(stringify!($ty))
|
||
}
|
||
fn parse(
|
||
s: &crate::textvalue::Sexp,
|
||
) -> core::result::Result<Self, crate::textvalue::TextError> {
|
||
crate::textvalue_impls::parse_unit(s, stringify!($ty)).map(|()| $ty)
|
||
}
|
||
}
|
||
)*
|
||
};
|
||
}
|
||
|
||
/// [`Codec`] for a fieldless ("C-like") enum: a single discriminant byte. And its
|
||
/// [`TextValue`]: the variant name as a bare symbol.
|
||
///
|
||
/// Both `match`es are exhaustive over the enum, so a variant added to the type and
|
||
/// not to this invocation **fails to compile** — the guarantee
|
||
/// `operation_kind_tag_vocabulary!` gives the operation decoder, here for every
|
||
/// C-like enum in Chapter 5.
|
||
///
|
||
/// [`TextValue`]: crate::textvalue::TextValue
|
||
macro_rules! cstyle_enum_codec {
|
||
($ty:ident { $($tag:literal => $variant:ident),* $(,)? }) => {
|
||
impl Codec for $ty {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
let tag: u8 = match self { $( $ty::$variant => $tag, )* };
|
||
out.push(tag);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
$( $tag => Ok($ty::$variant), )*
|
||
tag => Err(ScoreDecodeError::InvalidTag { kind: stringify!($ty), tag }),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl crate::textvalue::TextValue for $ty {
|
||
fn project(&self) -> crate::textvalue::Sexp {
|
||
let variant = match self { $( $ty::$variant => stringify!($variant), )* };
|
||
crate::textvalue::Sexp::Symbol(crate::textvalue::kebab(variant))
|
||
}
|
||
fn parse(
|
||
s: &crate::textvalue::Sexp,
|
||
) -> core::result::Result<Self, crate::textvalue::TextError> {
|
||
let name = s.as_symbol().ok_or(crate::textvalue::TextError::Expected {
|
||
expected: "symbol",
|
||
found: crate::textvalue_impls::class_of(s),
|
||
})?;
|
||
$(
|
||
if name == crate::textvalue::kebab(stringify!($variant)) {
|
||
return Ok($ty::$variant);
|
||
}
|
||
)*
|
||
Err(crate::textvalue::TextError::UnknownConstructor {
|
||
type_name: stringify!($ty),
|
||
found: name.to_owned(),
|
||
})
|
||
}
|
||
}
|
||
};
|
||
}
|
||
|
||
/// [`Codec`] for a catalog-id newtype over `String` (construct via `new`, read
|
||
/// via `as_str`; the stored form is already NFC).
|
||
macro_rules! catalog_id_codec {
|
||
($($ty:ident),* $(,)?) => {
|
||
$(
|
||
impl Codec for $ty {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.as_str().to_owned().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok($ty::new(String::dec(r)?))
|
||
}
|
||
}
|
||
|
||
// A catalog id is canonical text, so it projects as a quoted string.
|
||
// `new` folds to NFC, so `parse` interns and then *compares*: returning
|
||
// the folded value would accept a non-NFC spelling and silently
|
||
// normalize it (`req:textproj:strict-parse`).
|
||
impl crate::textvalue::TextValue for $ty {
|
||
fn project(&self) -> crate::textvalue::Sexp {
|
||
crate::textvalue::Sexp::Str(self.as_str().to_owned())
|
||
}
|
||
fn parse(
|
||
s: &crate::textvalue::Sexp,
|
||
) -> core::result::Result<Self, crate::textvalue::TextError> {
|
||
crate::textvalue_impls::parse_catalog_id(s, $ty::new, |v| v.as_str())
|
||
}
|
||
}
|
||
)*
|
||
};
|
||
}
|
||
|
||
unit_codec!(
|
||
ArticulationMark,
|
||
DynamicMark,
|
||
OrnamentMark,
|
||
StemConfiguration,
|
||
TrajectoryDisplay,
|
||
PlaybackBinding,
|
||
CueRendering,
|
||
TempoMapReference,
|
||
);
|
||
|
||
catalog_id_codec!(
|
||
PitchSpaceId,
|
||
TuningSystemId,
|
||
AccidentalId,
|
||
NominalRegistryId,
|
||
PositionRegistryId,
|
||
TieClassRegistryId,
|
||
StaffGroupKindRegistryId,
|
||
SpellingRuleSetId,
|
||
ForeignFormatId,
|
||
);
|
||
|
||
// --- Small newtypes / std types needing reconstruction. ---------------------
|
||
|
||
impl Codec for StaffPosition {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.0.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(StaffPosition(i16::dec(r)?))
|
||
}
|
||
}
|
||
|
||
impl Codec for UnpitchedMemberId {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.0.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(UnpitchedMemberId(u32::dec(r)?))
|
||
}
|
||
}
|
||
|
||
impl Codec for PowerOfTwo {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.get().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
PowerOfTwo::new(u16::dec(r)?).ok_or(ScoreDecodeError::Reconstruct("PowerOfTwo"))
|
||
}
|
||
}
|
||
|
||
impl Codec for NonZeroU16 {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.get().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
NonZeroU16::new(u16::dec(r)?).ok_or(ScoreDecodeError::Reconstruct("NonZeroU16"))
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// time.rs
|
||
// ===========================================================================
|
||
|
||
cstyle_enum_codec!(MeasurePosition { 0 => Start, 1 => End });
|
||
cstyle_enum_codec!(RegionEdge { 0 => Start, 1 => End });
|
||
|
||
impl Codec for AnchorOffset {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
AnchorOffset::Musical(d) => {
|
||
out.push(0);
|
||
d.enc(out);
|
||
}
|
||
AnchorOffset::WallClock(d) => {
|
||
out.push(1);
|
||
d.enc(out);
|
||
}
|
||
AnchorOffset::Zero => out.push(2),
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(AnchorOffset::Musical(Codec::dec(r)?)),
|
||
1 => Ok(AnchorOffset::WallClock(Codec::dec(r)?)),
|
||
2 => Ok(AnchorOffset::Zero),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "AnchorOffset",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for TimeAnchor {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TimeAnchor::Event { id, offset } => {
|
||
out.push(0);
|
||
id.enc(out);
|
||
offset.enc(out);
|
||
}
|
||
TimeAnchor::Measure {
|
||
id,
|
||
position,
|
||
offset,
|
||
} => {
|
||
out.push(1);
|
||
id.enc(out);
|
||
position.enc(out);
|
||
offset.enc(out);
|
||
}
|
||
TimeAnchor::Region { id, edge, offset } => {
|
||
out.push(2);
|
||
id.enc(out);
|
||
edge.enc(out);
|
||
offset.enc(out);
|
||
}
|
||
TimeAnchor::WallClock { time } => {
|
||
out.push(3);
|
||
time.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TimeAnchor::Event {
|
||
id: Codec::dec(r)?,
|
||
offset: Codec::dec(r)?,
|
||
}),
|
||
1 => Ok(TimeAnchor::Measure {
|
||
id: Codec::dec(r)?,
|
||
position: Codec::dec(r)?,
|
||
offset: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(TimeAnchor::Region {
|
||
id: Codec::dec(r)?,
|
||
edge: Codec::dec(r)?,
|
||
offset: Codec::dec(r)?,
|
||
}),
|
||
3 => Ok(TimeAnchor::WallClock {
|
||
time: Codec::dec(r)?,
|
||
}),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TimeAnchor",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for EventPosition {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
EventPosition::Musical(p) => {
|
||
out.push(0);
|
||
p.enc(out);
|
||
}
|
||
EventPosition::WallClock(t) => {
|
||
out.push(1);
|
||
t.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(EventPosition::Musical(Codec::dec(r)?)),
|
||
1 => Ok(EventPosition::WallClock(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "EventPosition",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for ConcreteDuration {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
ConcreteDuration::Musical(d) => {
|
||
out.push(0);
|
||
d.enc(out);
|
||
}
|
||
ConcreteDuration::WallClock(d) => {
|
||
out.push(1);
|
||
d.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(ConcreteDuration::Musical(Codec::dec(r)?)),
|
||
1 => Ok(ConcreteDuration::WallClock(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "ConcreteDuration",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(DurationBounds { lower, upper });
|
||
|
||
impl Codec for EventDuration {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
EventDuration::Musical(d) => {
|
||
out.push(0);
|
||
d.enc(out);
|
||
}
|
||
EventDuration::WallClock(d) => {
|
||
out.push(1);
|
||
d.enc(out);
|
||
}
|
||
EventDuration::Indeterminate(b) => {
|
||
out.push(2);
|
||
b.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(EventDuration::Musical(Codec::dec(r)?)),
|
||
1 => Ok(EventDuration::WallClock(Codec::dec(r)?)),
|
||
2 => Ok(EventDuration::Indeterminate(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "EventDuration",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for TimeBounds {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TimeBounds::MusicalRange { min, max } => {
|
||
out.push(0);
|
||
min.enc(out);
|
||
max.enc(out);
|
||
}
|
||
TimeBounds::WallClockRange { min, max } => {
|
||
out.push(1);
|
||
min.enc(out);
|
||
max.enc(out);
|
||
}
|
||
TimeBounds::Unbounded => out.push(2),
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TimeBounds::MusicalRange {
|
||
min: Codec::dec(r)?,
|
||
max: Codec::dec(r)?,
|
||
}),
|
||
1 => Ok(TimeBounds::WallClockRange {
|
||
min: Codec::dec(r)?,
|
||
max: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(TimeBounds::Unbounded),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TimeBounds",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(EventBounds { start, end });
|
||
|
||
// ===========================================================================
|
||
// pitch.rs
|
||
// ===========================================================================
|
||
|
||
cstyle_enum_codec!(CmnNominal {
|
||
0 => C, 1 => D, 2 => E, 3 => F, 4 => G, 5 => A, 6 => B,
|
||
});
|
||
|
||
cstyle_enum_codec!(SpellingSourceKind {
|
||
0 => UserChosen, 1 => Imported, 2 => Propagated, 3 => Inferred, 4 => Analytical,
|
||
});
|
||
|
||
impl Codec for PitchSpacePosition {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
PitchSpacePosition::Cmn {
|
||
nominal,
|
||
alteration,
|
||
octave,
|
||
} => {
|
||
out.push(0);
|
||
nominal.enc(out);
|
||
alteration.enc(out);
|
||
octave.enc(out);
|
||
}
|
||
PitchSpacePosition::Integer { space_size, index } => {
|
||
out.push(1);
|
||
space_size.enc(out);
|
||
index.enc(out);
|
||
}
|
||
PitchSpacePosition::JiVector { components } => {
|
||
out.push(2);
|
||
components.enc(out);
|
||
}
|
||
PitchSpacePosition::Registered(id) => {
|
||
out.push(3);
|
||
id.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(PitchSpacePosition::Cmn {
|
||
nominal: Codec::dec(r)?,
|
||
alteration: Codec::dec(r)?,
|
||
octave: Codec::dec(r)?,
|
||
}),
|
||
1 => Ok(PitchSpacePosition::Integer {
|
||
space_size: Codec::dec(r)?,
|
||
index: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(PitchSpacePosition::JiVector {
|
||
components: Codec::dec(r)?,
|
||
}),
|
||
3 => Ok(PitchSpacePosition::Registered(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "PitchSpacePosition",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(ScalePosition { space, position });
|
||
|
||
impl Codec for TuningReference {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TuningReference::Inherit => out.push(0),
|
||
TuningReference::Explicit(id) => {
|
||
out.push(1);
|
||
id.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TuningReference::Inherit),
|
||
1 => Ok(TuningReference::Explicit(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TuningReference",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for AcousticRealization {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
AcousticRealization::Implicit => out.push(0),
|
||
AcousticRealization::CentsOffset(c) => {
|
||
out.push(1);
|
||
c.enc(out);
|
||
}
|
||
AcousticRealization::AbsoluteHz(c) => {
|
||
out.push(2);
|
||
c.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(AcousticRealization::Implicit),
|
||
1 => Ok(AcousticRealization::CentsOffset(Codec::dec(r)?)),
|
||
2 => Ok(AcousticRealization::AbsoluteHz(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "AcousticRealization",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for ReferencePitch {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.position.enc(out);
|
||
let hz = CanonicalF64::new(self.frequency_hz()).expect("valid reference pitch is finite");
|
||
hz.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let position = PitchSpacePosition::dec(r)?;
|
||
let hz = CanonicalF64::dec(r)?;
|
||
ReferencePitch::new(position, hz.get())
|
||
.ok_or(ScoreDecodeError::Reconstruct("ReferencePitch"))
|
||
}
|
||
}
|
||
|
||
struct_codec!(AcousticPitch {
|
||
tuning,
|
||
realization
|
||
});
|
||
// Schema major 1: `Instrument.range` embeds this (appended after `name`).
|
||
struct_codec!(PitchRange { lowest, highest });
|
||
struct_codec!(Pitch {
|
||
scale_position,
|
||
acoustic
|
||
});
|
||
struct_codec!(IdentifiedPitch { id, pitch });
|
||
|
||
impl Codec for SpellingNominal {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
SpellingNominal::Cmn(n) => {
|
||
out.push(0);
|
||
n.enc(out);
|
||
}
|
||
SpellingNominal::Integer(i) => {
|
||
out.push(1);
|
||
i.enc(out);
|
||
}
|
||
SpellingNominal::Registered(id) => {
|
||
out.push(2);
|
||
id.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(SpellingNominal::Cmn(Codec::dec(r)?)),
|
||
1 => Ok(SpellingNominal::Integer(Codec::dec(r)?)),
|
||
2 => Ok(SpellingNominal::Registered(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "SpellingNominal",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(SpellingRenderHints {
|
||
parenthesized,
|
||
cautionary,
|
||
editorial,
|
||
small_print
|
||
});
|
||
struct_codec!(PitchSpelling {
|
||
nominal,
|
||
accidentals,
|
||
octave,
|
||
render_hints
|
||
});
|
||
|
||
impl Codec for SpellingSource {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
SpellingSource::UserChosen => out.push(0),
|
||
SpellingSource::Inferred => out.push(1),
|
||
SpellingSource::Imported { format } => {
|
||
out.push(2);
|
||
format.enc(out);
|
||
}
|
||
SpellingSource::Propagated { from } => {
|
||
out.push(3);
|
||
from.enc(out);
|
||
}
|
||
SpellingSource::Analytical => out.push(4),
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(SpellingSource::UserChosen),
|
||
1 => Ok(SpellingSource::Inferred),
|
||
2 => Ok(SpellingSource::Imported {
|
||
format: Codec::dec(r)?,
|
||
}),
|
||
3 => Ok(SpellingSource::Propagated {
|
||
from: Codec::dec(r)?,
|
||
}),
|
||
4 => Ok(SpellingSource::Analytical),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "SpellingSource",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for SpellingPrecedence {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.order_ref().to_vec().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let order: Vec<SpellingSourceKind> = Codec::dec(r)?;
|
||
SpellingPrecedence::new(order).ok_or(ScoreDecodeError::Reconstruct("SpellingPrecedence"))
|
||
}
|
||
}
|
||
|
||
impl Codec for VoiceSelector {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
VoiceSelector::All => out.push(0),
|
||
VoiceSelector::Voices(v) => {
|
||
out.push(1);
|
||
v.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(VoiceSelector::All),
|
||
1 => Ok(VoiceSelector::Voices(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "VoiceSelector",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(SpellingRule { rule_set });
|
||
|
||
impl Codec for SpellingScope {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
SpellingScope::Pitch(id) => {
|
||
out.push(0);
|
||
id.enc(out);
|
||
}
|
||
SpellingScope::Range { start, end, voices } => {
|
||
out.push(1);
|
||
start.enc(out);
|
||
end.enc(out);
|
||
voices.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(SpellingScope::Pitch(Codec::dec(r)?)),
|
||
1 => Ok(SpellingScope::Range {
|
||
start: Codec::dec(r)?,
|
||
end: Codec::dec(r)?,
|
||
voices: Codec::dec(r)?,
|
||
}),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "SpellingScope",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for SpellingDirective {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
SpellingDirective::Explicit(s) => {
|
||
out.push(0);
|
||
s.enc(out);
|
||
}
|
||
SpellingDirective::Rule(rule) => {
|
||
out.push(1);
|
||
rule.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(SpellingDirective::Explicit(Codec::dec(r)?)),
|
||
1 => Ok(SpellingDirective::Rule(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "SpellingDirective",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(SpellingAttachment {
|
||
scope,
|
||
directive,
|
||
source,
|
||
priority,
|
||
layer
|
||
});
|
||
|
||
// ===========================================================================
|
||
// event.rs
|
||
// ===========================================================================
|
||
|
||
impl Codec for GraceKind {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
GraceKind::Acciaccatura => out.push(0),
|
||
GraceKind::Appoggiatura => out.push(1),
|
||
GraceKind::Unmeasured => out.push(2),
|
||
GraceKind::MeasuredFraction(d) => {
|
||
out.push(3);
|
||
d.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(GraceKind::Acciaccatura),
|
||
1 => Ok(GraceKind::Appoggiatura),
|
||
2 => Ok(GraceKind::Unmeasured),
|
||
3 => Ok(GraceKind::MeasuredFraction(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "GraceKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for IndeterminacyKind {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
IndeterminacyKind::Pitch => out.push(0),
|
||
IndeterminacyKind::Duration => out.push(1),
|
||
IndeterminacyKind::Choice => out.push(2),
|
||
IndeterminacyKind::Compound(v) => {
|
||
out.push(3);
|
||
v.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(IndeterminacyKind::Pitch),
|
||
1 => Ok(IndeterminacyKind::Duration),
|
||
2 => Ok(IndeterminacyKind::Choice),
|
||
3 => Ok(IndeterminacyKind::Compound(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "IndeterminacyKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(IndeterminacyHints {
|
||
duration_bounds,
|
||
alternatives,
|
||
textual_instruction
|
||
});
|
||
|
||
impl Codec for TrajectoryEndpoint {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TrajectoryEndpoint::EventPitch(id) => {
|
||
out.push(0);
|
||
id.enc(out);
|
||
}
|
||
TrajectoryEndpoint::ExplicitPitch(p) => {
|
||
out.push(1);
|
||
p.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TrajectoryEndpoint::EventPitch(Codec::dec(r)?)),
|
||
1 => Ok(TrajectoryEndpoint::ExplicitPitch(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TrajectoryEndpoint",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for TrajectoryShape {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TrajectoryShape::Linear => out.push(0),
|
||
TrajectoryShape::Exponential => out.push(1),
|
||
TrajectoryShape::Curve => out.push(2),
|
||
TrajectoryShape::Stepwise(v) => {
|
||
out.push(3);
|
||
v.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TrajectoryShape::Linear),
|
||
1 => Ok(TrajectoryShape::Exponential),
|
||
2 => Ok(TrajectoryShape::Curve),
|
||
3 => Ok(TrajectoryShape::Stepwise(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TrajectoryShape",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(PitchedEvent {
|
||
id,
|
||
voice,
|
||
position,
|
||
duration,
|
||
pitches,
|
||
articulations,
|
||
dynamic,
|
||
ornaments,
|
||
stem,
|
||
grace
|
||
});
|
||
struct_codec!(UnpitchedEvent {
|
||
id,
|
||
voice,
|
||
position,
|
||
duration,
|
||
staff_position,
|
||
instrument_member,
|
||
articulations,
|
||
dynamic,
|
||
stem,
|
||
grace
|
||
});
|
||
struct_codec!(Rest {
|
||
id,
|
||
voice,
|
||
position,
|
||
duration,
|
||
vertical_position,
|
||
visible
|
||
});
|
||
struct_codec!(IndeterminateEvent {
|
||
id,
|
||
voice,
|
||
position,
|
||
duration,
|
||
indeterminacy,
|
||
hints
|
||
});
|
||
struct_codec!(TrajectoryEvent {
|
||
id,
|
||
voice,
|
||
position,
|
||
duration,
|
||
start,
|
||
end,
|
||
shape,
|
||
display
|
||
});
|
||
struct_codec!(GraphicEvent {
|
||
id,
|
||
voice,
|
||
position,
|
||
duration,
|
||
graphics,
|
||
playback_bindings
|
||
});
|
||
struct_codec!(CueEvent {
|
||
id,
|
||
voice,
|
||
position,
|
||
duration,
|
||
source,
|
||
rendering
|
||
});
|
||
|
||
impl Codec for Event {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
Event::Pitched(e) => {
|
||
out.push(0);
|
||
e.enc(out);
|
||
}
|
||
Event::Unpitched(e) => {
|
||
out.push(1);
|
||
e.enc(out);
|
||
}
|
||
Event::Rest(e) => {
|
||
out.push(2);
|
||
e.enc(out);
|
||
}
|
||
Event::Indeterminate(e) => {
|
||
out.push(3);
|
||
e.enc(out);
|
||
}
|
||
Event::Trajectory(e) => {
|
||
out.push(4);
|
||
e.enc(out);
|
||
}
|
||
Event::Graphic(e) => {
|
||
out.push(5);
|
||
e.enc(out);
|
||
}
|
||
Event::Cue(e) => {
|
||
out.push(6);
|
||
e.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(Event::Pitched(Codec::dec(r)?)),
|
||
1 => Ok(Event::Unpitched(Codec::dec(r)?)),
|
||
2 => Ok(Event::Rest(Codec::dec(r)?)),
|
||
3 => Ok(Event::Indeterminate(Codec::dec(r)?)),
|
||
4 => Ok(Event::Trajectory(Codec::dec(r)?)),
|
||
5 => Ok(Event::Graphic(Codec::dec(r)?)),
|
||
6 => Ok(Event::Cue(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag { kind: "Event", tag }),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for EventArena {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
// Canonical (ascending EventId) order; identity travels inside each event.
|
||
let events: Vec<&Event> = self.iter_canonical().collect();
|
||
put_len(out, events.len());
|
||
for event in events {
|
||
event.enc(out);
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let n = r.count()?;
|
||
let mut arena = EventArena::new();
|
||
for _ in 0..n {
|
||
let event = Event::dec(r)?;
|
||
arena
|
||
.insert(event)
|
||
.map_err(|_| ScoreDecodeError::Reconstruct("EventArena"))?;
|
||
}
|
||
Ok(arena)
|
||
}
|
||
}
|
||
|
||
// ===========================================================================
|
||
// tempo.rs
|
||
// ===========================================================================
|
||
|
||
cstyle_enum_codec!(TempoShape {
|
||
0 => Constant, 1 => Linear, 2 => Exponential, 3 => Curve,
|
||
});
|
||
|
||
impl Codec for Tempo {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
let bpm = CanonicalF64::new(self.bpm()).expect("valid tempo bpm is finite");
|
||
bpm.enc(out);
|
||
self.beat_unit().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let bpm = CanonicalF64::dec(r)?;
|
||
let beat_unit = MusicalDuration::dec(r)?;
|
||
Tempo::new(bpm.get(), beat_unit).ok_or(ScoreDecodeError::Reconstruct("Tempo"))
|
||
}
|
||
}
|
||
|
||
struct_codec!(TempoSegment {
|
||
start,
|
||
end,
|
||
start_tempo,
|
||
end_tempo,
|
||
shape
|
||
});
|
||
struct_codec!(TempoMap { initial, segments });
|
||
|
||
// ===========================================================================
|
||
// graph.rs
|
||
// ===========================================================================
|
||
|
||
cstyle_enum_codec!(StemDirection { 0 => Up, 1 => Down });
|
||
cstyle_enum_codec!(MeasureNumberVisibility {
|
||
0 => Auto, 1 => Always, 2 => Never,
|
||
});
|
||
cstyle_enum_codec!(AleatoricAnchoringDiscipline {
|
||
0 => Musical, 1 => WallClock, 2 => EitherPerEvent, 3 => FreelyMixed,
|
||
});
|
||
cstyle_enum_codec!(NoteValue {
|
||
0 => Whole, 1 => Half, 2 => Quarter, 3 => Eighth,
|
||
4 => Sixteenth, 5 => ThirtySecond, 6 => SixtyFourth,
|
||
});
|
||
|
||
// --- Schema-major-2 leaf types (Binary Format §Schema Major 2). -------------
|
||
// Every layout below is the ratified v2 wire form: tag-only enums are one
|
||
// discriminant byte; `SpaceUnit` rides its `CanonicalF64` leaf (framed 12);
|
||
// `Timestamp` is one bare i64 LE; fixed-width primitives inside structs are
|
||
// bare; identifiers keep the leaf framing.
|
||
|
||
impl Codec for Timestamp {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.0.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(Timestamp(i64::dec(r)?))
|
||
}
|
||
}
|
||
|
||
impl Codec for SpaceUnit {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.0.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(SpaceUnit(Codec::dec(r)?))
|
||
}
|
||
}
|
||
|
||
impl Codec for SoundConfiguration {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.0.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(SoundConfiguration(Codec::dec(r)?))
|
||
}
|
||
}
|
||
|
||
impl Codec for OctaveOffset {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.0.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
Ok(OctaveOffset(i8::dec(r)?))
|
||
}
|
||
}
|
||
|
||
cstyle_enum_codec!(LineStyle { 0 => Solid, 1 => Dashed, 2 => Dotted });
|
||
cstyle_enum_codec!(SlurKind {
|
||
0 => Legato,
|
||
1 => Phrase,
|
||
2 => Articulation,
|
||
3 => Editorial,
|
||
});
|
||
cstyle_enum_codec!(CurveDirection { 0 => Above, 1 => Below });
|
||
cstyle_enum_codec!(HairpinDirection {
|
||
0 => Crescendo,
|
||
1 => Diminuendo,
|
||
});
|
||
cstyle_enum_codec!(PedalKind {
|
||
0 => Sustain,
|
||
1 => Sostenuto,
|
||
2 => UnaCorda,
|
||
});
|
||
cstyle_enum_codec!(BracketKind { 0 => Square });
|
||
cstyle_enum_codec!(StaffBracketKind { 0 => Brace, 1 => Bracket });
|
||
|
||
impl Codec for SpannerKind {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
SpannerKind::Generic => out.push(0),
|
||
SpannerKind::Hairpin(d) => {
|
||
out.push(1);
|
||
d.enc(out);
|
||
}
|
||
SpannerKind::OctaveLine(o) => {
|
||
out.push(2);
|
||
o.enc(out);
|
||
}
|
||
SpannerKind::PedalLine(p) => {
|
||
out.push(3);
|
||
p.enc(out);
|
||
}
|
||
SpannerKind::TrillExtension => out.push(4),
|
||
SpannerKind::Glissando => out.push(5),
|
||
SpannerKind::Portamento => out.push(6),
|
||
SpannerKind::TextLine(t) => {
|
||
out.push(7);
|
||
t.enc(out);
|
||
}
|
||
SpannerKind::Bracket(b) => {
|
||
out.push(8);
|
||
b.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(SpannerKind::Generic),
|
||
1 => Ok(SpannerKind::Hairpin(Codec::dec(r)?)),
|
||
2 => Ok(SpannerKind::OctaveLine(Codec::dec(r)?)),
|
||
3 => Ok(SpannerKind::PedalLine(Codec::dec(r)?)),
|
||
4 => Ok(SpannerKind::TrillExtension),
|
||
5 => Ok(SpannerKind::Glissando),
|
||
6 => Ok(SpannerKind::Portamento),
|
||
7 => Ok(SpannerKind::TextLine(Codec::dec(r)?)),
|
||
8 => Ok(SpannerKind::Bracket(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "SpannerKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for RepeatKind {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
RepeatKind::SimpleRepeat { count } => {
|
||
out.push(0);
|
||
count.enc(out);
|
||
}
|
||
RepeatKind::DaCapo { end_target } => {
|
||
out.push(1);
|
||
end_target.enc(out);
|
||
}
|
||
RepeatKind::DalSegno { segno, end_target } => {
|
||
out.push(2);
|
||
segno.enc(out);
|
||
end_target.enc(out);
|
||
}
|
||
RepeatKind::Volta => out.push(3),
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(RepeatKind::SimpleRepeat {
|
||
count: Codec::dec(r)?,
|
||
}),
|
||
1 => Ok(RepeatKind::DaCapo {
|
||
end_target: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(RepeatKind::DalSegno {
|
||
segno: Codec::dec(r)?,
|
||
end_target: Codec::dec(r)?,
|
||
}),
|
||
3 => Ok(RepeatKind::Volta),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "RepeatKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for MetadataValue {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
MetadataValue::Text(s) => {
|
||
out.push(0);
|
||
s.enc(out);
|
||
}
|
||
MetadataValue::Integer(i) => {
|
||
out.push(1);
|
||
i.enc(out);
|
||
}
|
||
MetadataValue::Flag(b) => {
|
||
out.push(2);
|
||
b.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(MetadataValue::Text(Codec::dec(r)?)),
|
||
1 => Ok(MetadataValue::Integer(Codec::dec(r)?)),
|
||
2 => Ok(MetadataValue::Flag(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "MetadataValue",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(CurvatureOverride { direction, height });
|
||
struct_codec!(SpanStyle { line, thickness });
|
||
struct_codec!(SubBeam { level, events });
|
||
struct_codec!(BeamGeometryOverride { slope, offset });
|
||
struct_codec!(TextLineDefinition { text });
|
||
struct_codec!(Volta {
|
||
endings,
|
||
start,
|
||
end
|
||
});
|
||
struct_codec!(MetadataEntry { key, value });
|
||
struct_codec!(TranspositionInterval {
|
||
diatonic_steps,
|
||
chromatic_steps
|
||
});
|
||
struct_codec!(UnpitchedMember {
|
||
member,
|
||
name,
|
||
staff_position
|
||
});
|
||
|
||
// Schema major 2: `ScoreMetadata` appended six fields after the major-0/1
|
||
// order. The frozen prior layout (`title`, `composer`, `copyright`) is read
|
||
// by `dec_metadata_v1`.
|
||
struct_codec!(ScoreMetadata {
|
||
title,
|
||
composer,
|
||
copyright,
|
||
subtitle,
|
||
lyricist,
|
||
arranger,
|
||
creation_timestamp,
|
||
modification_timestamp,
|
||
additional
|
||
});
|
||
// Schema major 1: `Instrument` gained `range` (appended after `name`); the
|
||
// frozen major-0 layout (`id`, `name`) is read by `dec_instruments_v0`.
|
||
// Schema major 2 appended six more fields after `range`; the frozen major-1
|
||
// layout (`id`, `name`, `range`) is read by `dec_instruments_v1`.
|
||
struct_codec!(Instrument {
|
||
id,
|
||
name,
|
||
range,
|
||
abbreviation,
|
||
sound_config,
|
||
transposition,
|
||
default_clef,
|
||
default_staff_lines,
|
||
unpitched_members
|
||
});
|
||
// Schema major 2: three fields appended after `line_count`; the frozen prior
|
||
// layout (`line_count` only) is read by `dec_staff_lines_v1`.
|
||
struct_codec!(StaffLineConfiguration {
|
||
line_count,
|
||
line_spacing,
|
||
line_style,
|
||
bracket
|
||
});
|
||
struct_codec!(GraphicObject { id });
|
||
struct_codec!(GraphicContent { objects });
|
||
struct_codec!(TimeExtent { start, end });
|
||
struct_codec!(StaffExtent { staves });
|
||
// Schema major 2: `default_clef` appended last; the frozen prior layout is
|
||
// read by `dec_staff_v1`.
|
||
struct_codec!(Staff {
|
||
id,
|
||
name,
|
||
abbreviation,
|
||
instrument,
|
||
default_staff_lines,
|
||
group,
|
||
default_clef
|
||
});
|
||
struct_codec!(PartDefinition { id, name, staves });
|
||
struct_codec!(AnalysisLayer { id, name });
|
||
struct_codec!(ViewDefinition {
|
||
id,
|
||
name,
|
||
active_layers
|
||
});
|
||
struct_codec!(MeterChange {
|
||
anchor,
|
||
time_signature
|
||
});
|
||
struct_codec!(MetricGrid { meter_sequence });
|
||
struct_codec!(ProportionalTimeModel { duration });
|
||
struct_codec!(MetricTimeModel { meters, tempo });
|
||
cstyle_enum_codec!(ClefShape {
|
||
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 {
|
||
id,
|
||
start,
|
||
time_signature,
|
||
explicit_number,
|
||
number_visibility
|
||
});
|
||
struct_codec!(BeatGroup {
|
||
duration,
|
||
subdivision,
|
||
accent
|
||
});
|
||
// ===========================================================================
|
||
// accidental.rs / tuning.rs — the schema-major-3 leaf layouts staged onto the
|
||
// wire this tranche (Push 4b tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`).
|
||
// Only these four types gain a `Codec`; `accidental_extensions` and its whole
|
||
// subtree stay in-memory-only (no `Codec` exists, or may exist, for them —
|
||
// `accidental.rs`'s own module doc). Hand-written rather than
|
||
// `struct_codec!`/`cstyle_enum_codec!`: `struct_codec!` also generates a
|
||
// `TextValue` impl, and these four types have none (text projection is a
|
||
// separate surface this tranche does not touch — see the staging-boundary
|
||
// tests below and `textvalue_graph.rs`), so the macro would either fail to
|
||
// compile (no `TextValue` for `u16`/`TuningScope`) or silently open a new
|
||
// text-projection surface neither asked for nor specified. Every layout here
|
||
// is now FROZEN (`req:binfmt:frozen-layout`) — get the field order right.
|
||
impl Codec for SmuflVersion {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.major.enc(out);
|
||
self.minor_centi.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let major = u16::dec(r)?;
|
||
let minor_centi = u16::dec(r)?;
|
||
Ok(SmuflVersion { major, minor_centi })
|
||
}
|
||
}
|
||
|
||
impl Codec for SmuflVersionRequirement {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.minimum.enc(out);
|
||
self.authored_against.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let minimum = SmuflVersion::dec(r)?;
|
||
let authored_against = SmuflVersion::dec(r)?;
|
||
Ok(SmuflVersionRequirement {
|
||
minimum,
|
||
authored_against,
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Codec for TuningScope {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TuningScope::Voice(id) => {
|
||
out.push(0);
|
||
id.enc(out);
|
||
}
|
||
TuningScope::Staff(id) => {
|
||
out.push(1);
|
||
id.enc(out);
|
||
}
|
||
TuningScope::Region(id) => {
|
||
out.push(2);
|
||
id.enc(out);
|
||
}
|
||
TuningScope::Range { start, end, voices } => {
|
||
out.push(3);
|
||
start.enc(out);
|
||
end.enc(out);
|
||
voices.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TuningScope::Voice(Codec::dec(r)?)),
|
||
1 => Ok(TuningScope::Staff(Codec::dec(r)?)),
|
||
2 => Ok(TuningScope::Region(Codec::dec(r)?)),
|
||
3 => Ok(TuningScope::Range {
|
||
start: Codec::dec(r)?,
|
||
end: Codec::dec(r)?,
|
||
voices: Codec::dec(r)?,
|
||
}),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TuningScope",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for TuningOverride {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.scope.enc(out);
|
||
self.pitch_space.enc(out);
|
||
self.tuning_system.enc(out);
|
||
self.reference.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let scope = Codec::dec(r)?;
|
||
let pitch_space = Codec::dec(r)?;
|
||
let tuning_system = Codec::dec(r)?;
|
||
let reference = Codec::dec(r)?;
|
||
Ok(TuningOverride {
|
||
scope,
|
||
pitch_space,
|
||
tuning_system,
|
||
reference,
|
||
})
|
||
}
|
||
}
|
||
|
||
// `ScoreTuningContext` is schema major 3 (Push 4b tranche 3b-i,
|
||
// `spec/CONTRACT_PUSH4B_3BI_WIRE.md`): `smufl` and `overrides` join the
|
||
// major-0 prefix (`default_pitch_space`, `default_tuning_system`,
|
||
// `reference`) on the wire, in that order — append-after-existing, per the
|
||
// frozen-layout rule. `accidental_extensions` stays in-memory-only (staged to
|
||
// a later major, appended after `overrides` when it lands); `struct_codec!`
|
||
// still cannot express that one holdout, so this impl stays hand-written.
|
||
// `dec` default-fills only `accidental_extensions: Vec::new()`.
|
||
//
|
||
// The pre-v3 form (3 fields; majors 0/1/2 all share it) is frozen separately
|
||
// as `enc_tuning_context_v2`/`dec_tuning_context_v2` below, consumed by the
|
||
// frozen `decode_v0_score`/`decode_v1_score`/`decode_v2_score` (and their
|
||
// byte-exact-inverse `encode_v*_score` siblings) so those frozen forms never
|
||
// drift when this live impl changes. A round-trip test just below
|
||
// (`score_tuning_context_smufl_and_overrides_reach_the_wire_accidental_extensions_do_not`)
|
||
// proves `smufl`/`overrides` now survive `enc`→`dec` while
|
||
// `accidental_extensions` still does not. The text projection now draws the
|
||
// staging line in the same place, and its proofs are the direct analogues:
|
||
// `textvalue_graph.rs`'s `score_tuning_context_round_trips_and_overrides_project`
|
||
// and `score_tuning_context_projects_smufl_and_overrides_but_not_accidental_extensions`.
|
||
// (3b-i left the text surface untouched, deferring it; the parity pass that
|
||
// followed moved `smufl`/`overrides` there for the reason stated in
|
||
// `TextValue for ScoreTuningContext`'s doc — the text projection is the same
|
||
// canonical surface this codec is.)
|
||
impl Codec for ScoreTuningContext {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.default_pitch_space.enc(out);
|
||
self.default_tuning_system.enc(out);
|
||
self.reference.enc(out);
|
||
self.smufl.enc(out);
|
||
self.overrides.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let default_pitch_space = Codec::dec(r)?;
|
||
let default_tuning_system = Codec::dec(r)?;
|
||
let reference = Codec::dec(r)?;
|
||
let smufl = Codec::dec(r)?;
|
||
let overrides = Codec::dec(r)?;
|
||
Ok(ScoreTuningContext {
|
||
default_pitch_space,
|
||
default_tuning_system,
|
||
reference,
|
||
accidental_extensions: Vec::new(),
|
||
smufl,
|
||
overrides,
|
||
})
|
||
}
|
||
}
|
||
|
||
// `TuningContextSettings` (genesis tranche G2b,
|
||
// `spec/CONTRACT_GENESIS_G2B_TUNING.md` §1): the authored subset of
|
||
// `ScoreTuningContext` that `SetTuningContext` carries — exactly its five
|
||
// wire-bearing fields, in the codec's existing order. This walk is
|
||
// **byte-identical** to `ScoreTuningContext`'s `enc`/`dec` above by
|
||
// construction (same five fields, same order, same per-field codecs) — a
|
||
// type-level narrowing, not a new wire form. The identity is asserted by a
|
||
// round-trip test just below
|
||
// (`tuning_context_settings_canonical_bytes_match_score_tuning_context`).
|
||
impl Codec for TuningContextSettings {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.default_pitch_space.enc(out);
|
||
self.default_tuning_system.enc(out);
|
||
self.reference.enc(out);
|
||
self.smufl.enc(out);
|
||
self.overrides.enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let default_pitch_space = Codec::dec(r)?;
|
||
let default_tuning_system = Codec::dec(r)?;
|
||
let reference = Codec::dec(r)?;
|
||
let smufl = Codec::dec(r)?;
|
||
let overrides = Codec::dec(r)?;
|
||
Ok(TuningContextSettings {
|
||
default_pitch_space,
|
||
default_tuning_system,
|
||
reference,
|
||
smufl,
|
||
overrides,
|
||
})
|
||
}
|
||
}
|
||
|
||
// Schema major 2: the cross-cutting bodies filled (appended fields); the
|
||
// frozen prior layouts are read by the `dec_*_v1` sub-decoders.
|
||
struct_codec!(Slur {
|
||
id,
|
||
start_event,
|
||
end_event,
|
||
kind,
|
||
curvature_override,
|
||
style
|
||
});
|
||
struct_codec!(Beam {
|
||
id,
|
||
events,
|
||
level,
|
||
sub_beams,
|
||
geometry_override
|
||
});
|
||
// TupletRatio is not a plain struct_codec!: it has private fields and a checked
|
||
// constructor that rejects degenerate ratios, so decode must validate too
|
||
// (a malformed bundle cannot inject a degenerate ratio).
|
||
impl Codec for TupletRatio {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.actual().enc(out);
|
||
self.notated().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let actual = Codec::dec(r)?;
|
||
let notated = Codec::dec(r)?;
|
||
TupletRatio::new(actual, notated).ok_or(ScoreDecodeError::Reconstruct(
|
||
"TupletRatio: degenerate ratio",
|
||
))
|
||
}
|
||
}
|
||
struct_codec!(Tuplet {
|
||
id,
|
||
ratio,
|
||
members,
|
||
parent,
|
||
required_total
|
||
});
|
||
struct_codec!(Spanner {
|
||
id,
|
||
start,
|
||
end,
|
||
staves,
|
||
kind,
|
||
style
|
||
});
|
||
struct_codec!(Marker { id, anchor });
|
||
struct_codec!(RepeatStructure {
|
||
id,
|
||
start,
|
||
end,
|
||
kind,
|
||
voltas
|
||
});
|
||
struct_codec!(Comment {
|
||
id,
|
||
anchor,
|
||
resolved
|
||
});
|
||
struct_codec!(LyricLine { id, events });
|
||
struct_codec!(ChordSymbol { id, anchor });
|
||
struct_codec!(AnalyticalAnnotation { id, anchor, layer });
|
||
struct_codec!(GraphicGesture {
|
||
id,
|
||
objects,
|
||
anchoring
|
||
});
|
||
struct_codec!(BarlineAlignmentMember {
|
||
staff_instance,
|
||
measure,
|
||
position
|
||
});
|
||
struct_codec!(BarlineAlignmentGroup { id, members });
|
||
struct_codec!(NotatedComponent {
|
||
base_value,
|
||
dots,
|
||
tuplet,
|
||
tied_to_next
|
||
});
|
||
struct_codec!(Voice {
|
||
id,
|
||
events,
|
||
default_stem_direction,
|
||
is_primary,
|
||
origin
|
||
});
|
||
struct_codec!(StaffInstance {
|
||
id,
|
||
staff,
|
||
voices,
|
||
clef_sequence,
|
||
key_sequence,
|
||
local_metric_grid,
|
||
measures,
|
||
instrument_override,
|
||
staff_lines_override,
|
||
visible
|
||
});
|
||
struct_codec!(StaffBasedContent {
|
||
staff_instances,
|
||
default_metric_grid,
|
||
barline_alignment_groups,
|
||
user_system_breaks,
|
||
user_page_breaks
|
||
});
|
||
// Schema major 1: `Region` gained `permits_spanning_slurs` (appended after
|
||
// `local_tempo_map`). The frozen major-0 layout (the six fields before it) is
|
||
// read by `dec_region_v0`. `Region` is also a `CanonicalValue` embedded in the
|
||
// canonical `CreateRegion` op payload, so this byte change is canonical — the
|
||
// op-block migration for it is a separate change (schema-major track, D2).
|
||
struct_codec!(Region {
|
||
id,
|
||
time_model,
|
||
content,
|
||
time_extent,
|
||
staff_extent,
|
||
local_tempo_map,
|
||
permits_spanning_slurs
|
||
});
|
||
struct_codec!(CanvasSize { width, height });
|
||
struct_codec!(CanvasMargins {
|
||
top,
|
||
right,
|
||
bottom,
|
||
left
|
||
});
|
||
struct_codec!(CanvasLayoutDefaults { page_size, margins });
|
||
// Schema major 1: `Canvas` gained `layout_defaults` (appended after `regions`).
|
||
// The frozen major-0 layout (`regions` only) is read by `decode_v0_score`.
|
||
struct_codec!(Canvas {
|
||
regions,
|
||
layout_defaults
|
||
});
|
||
struct_codec!(CrossCuttingRegistry {
|
||
slurs,
|
||
ties,
|
||
beams,
|
||
tuplets,
|
||
spanners,
|
||
markers,
|
||
repeats,
|
||
analytical,
|
||
comments,
|
||
graphic_gestures,
|
||
lyrics,
|
||
chord_symbols
|
||
});
|
||
struct_codec!(IdentityContext {
|
||
replica_id,
|
||
next_counter
|
||
});
|
||
|
||
impl Codec for EventOrderingDAG {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.edges_ref().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let edges: BTreeMap<EventId, Vec<EventId>> = Codec::dec(r)?;
|
||
EventOrderingDAG::try_new(edges).ok_or(ScoreDecodeError::Reconstruct("EventOrderingDAG"))
|
||
}
|
||
}
|
||
|
||
struct_codec!(AleatoricTimeModel {
|
||
ordering,
|
||
anchoring,
|
||
bounds,
|
||
duration_hint
|
||
});
|
||
|
||
impl Codec for RegionTimeModel {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
RegionTimeModel::Metric(m) => {
|
||
out.push(0);
|
||
m.enc(out);
|
||
}
|
||
RegionTimeModel::Proportional(p) => {
|
||
out.push(1);
|
||
p.enc(out);
|
||
}
|
||
RegionTimeModel::Aleatoric(a) => {
|
||
out.push(2);
|
||
a.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(RegionTimeModel::Metric(Codec::dec(r)?)),
|
||
1 => Ok(RegionTimeModel::Proportional(Codec::dec(r)?)),
|
||
2 => Ok(RegionTimeModel::Aleatoric(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "RegionTimeModel",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for RegionContent {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
RegionContent::StaffBased(s) => {
|
||
out.push(0);
|
||
s.enc(out);
|
||
}
|
||
RegionContent::FreeGraphic(g) => {
|
||
out.push(1);
|
||
g.enc(out);
|
||
}
|
||
RegionContent::Hybrid {
|
||
staves,
|
||
overlay,
|
||
overlay_below_staves,
|
||
} => {
|
||
out.push(2);
|
||
staves.enc(out);
|
||
overlay.enc(out);
|
||
overlay_below_staves.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(RegionContent::StaffBased(Codec::dec(r)?)),
|
||
1 => Ok(RegionContent::FreeGraphic(Codec::dec(r)?)),
|
||
2 => Ok(RegionContent::Hybrid {
|
||
staves: Codec::dec(r)?,
|
||
overlay: Codec::dec(r)?,
|
||
overlay_below_staves: Codec::dec(r)?,
|
||
}),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "RegionContent",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for VoiceOrigin {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
VoiceOrigin::UserDeclared => out.push(0),
|
||
VoiceOrigin::Imported { format } => {
|
||
out.push(1);
|
||
format.enc(out);
|
||
}
|
||
VoiceOrigin::SystemPromoted {
|
||
winning_operation,
|
||
losing_operation,
|
||
original_voice,
|
||
} => {
|
||
out.push(2);
|
||
winning_operation.enc(out);
|
||
losing_operation.enc(out);
|
||
original_voice.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(VoiceOrigin::UserDeclared),
|
||
1 => Ok(VoiceOrigin::Imported {
|
||
format: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(VoiceOrigin::SystemPromoted {
|
||
winning_operation: Codec::dec(r)?,
|
||
losing_operation: Codec::dec(r)?,
|
||
original_voice: Codec::dec(r)?,
|
||
}),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "VoiceOrigin",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for StaffGroupKind {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
StaffGroupKind::GrandStaff => out.push(0),
|
||
StaffGroupKind::Bracket => out.push(1),
|
||
StaffGroupKind::SubBracket => out.push(2),
|
||
StaffGroupKind::Choral => out.push(3),
|
||
StaffGroupKind::Registered(id) => {
|
||
out.push(4);
|
||
id.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(StaffGroupKind::GrandStaff),
|
||
1 => Ok(StaffGroupKind::Bracket),
|
||
2 => Ok(StaffGroupKind::SubBracket),
|
||
3 => Ok(StaffGroupKind::Choral),
|
||
4 => Ok(StaffGroupKind::Registered(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "StaffGroupKind",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(StaffGroup {
|
||
id,
|
||
name,
|
||
kind,
|
||
members
|
||
});
|
||
|
||
impl Codec for TieClass {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TieClass::Standard => out.push(0),
|
||
TieClass::Editorial => out.push(1),
|
||
TieClass::CrossVoice => out.push(2),
|
||
TieClass::LaissezVibrer => out.push(3),
|
||
TieClass::Registered(id) => {
|
||
out.push(4);
|
||
id.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TieClass::Standard),
|
||
1 => Ok(TieClass::Editorial),
|
||
2 => Ok(TieClass::CrossVoice),
|
||
3 => Ok(TieClass::LaissezVibrer),
|
||
4 => Ok(TieClass::Registered(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TieClass",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(Tie {
|
||
id,
|
||
start_event,
|
||
end_event,
|
||
pitch_pairing,
|
||
class,
|
||
style
|
||
});
|
||
|
||
impl Codec for AnnotationAnchor {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
AnnotationAnchor::Event(id) => {
|
||
out.push(0);
|
||
id.enc(out);
|
||
}
|
||
AnnotationAnchor::Range { start, end } => {
|
||
out.push(1);
|
||
start.enc(out);
|
||
end.enc(out);
|
||
}
|
||
AnnotationAnchor::Region(id) => {
|
||
out.push(2);
|
||
id.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(AnnotationAnchor::Event(Codec::dec(r)?)),
|
||
1 => Ok(AnnotationAnchor::Range {
|
||
start: Codec::dec(r)?,
|
||
end: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(AnnotationAnchor::Region(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "AnnotationAnchor",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for GestureAnchoring {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
GestureAnchoring::Events(v) => {
|
||
out.push(0);
|
||
v.enc(out);
|
||
}
|
||
GestureAnchoring::Range { start, end, staves } => {
|
||
out.push(1);
|
||
start.enc(out);
|
||
end.enc(out);
|
||
staves.enc(out);
|
||
}
|
||
GestureAnchoring::Free => out.push(2),
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(GestureAnchoring::Events(Codec::dec(r)?)),
|
||
1 => Ok(GestureAnchoring::Range {
|
||
start: Codec::dec(r)?,
|
||
end: Codec::dec(r)?,
|
||
staves: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(GestureAnchoring::Free),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "GestureAnchoring",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for DecompositionSource {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
DecompositionSource::UserChosen => out.push(0),
|
||
DecompositionSource::Inferred => out.push(1),
|
||
DecompositionSource::Imported { format } => {
|
||
out.push(2);
|
||
format.enc(out);
|
||
}
|
||
DecompositionSource::Propagated { from } => {
|
||
out.push(3);
|
||
from.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(DecompositionSource::UserChosen),
|
||
1 => Ok(DecompositionSource::Inferred),
|
||
2 => Ok(DecompositionSource::Imported {
|
||
format: Codec::dec(r)?,
|
||
}),
|
||
3 => Ok(DecompositionSource::Propagated {
|
||
from: Codec::dec(r)?,
|
||
}),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "DecompositionSource",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
struct_codec!(DecompositionAttachment {
|
||
target,
|
||
components,
|
||
source
|
||
});
|
||
|
||
impl Codec for TimeSignatureDisplay {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
match self {
|
||
TimeSignatureDisplay::Standard {
|
||
numerator,
|
||
denominator,
|
||
} => {
|
||
out.push(0);
|
||
numerator.enc(out);
|
||
denominator.enc(out);
|
||
}
|
||
TimeSignatureDisplay::Compound {
|
||
numerators,
|
||
denominator,
|
||
} => {
|
||
out.push(1);
|
||
numerators.enc(out);
|
||
denominator.enc(out);
|
||
}
|
||
TimeSignatureDisplay::Irrational {
|
||
numerator,
|
||
denominator,
|
||
} => {
|
||
out.push(2);
|
||
numerator.enc(out);
|
||
denominator.enc(out);
|
||
}
|
||
TimeSignatureDisplay::MixedDenominators { components } => {
|
||
out.push(3);
|
||
components.enc(out);
|
||
}
|
||
TimeSignatureDisplay::None => out.push(4),
|
||
TimeSignatureDisplay::Symbolic(v) => {
|
||
out.push(5);
|
||
v.enc(out);
|
||
}
|
||
}
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
match r.u8()? {
|
||
0 => Ok(TimeSignatureDisplay::Standard {
|
||
numerator: Codec::dec(r)?,
|
||
denominator: Codec::dec(r)?,
|
||
}),
|
||
1 => Ok(TimeSignatureDisplay::Compound {
|
||
numerators: Codec::dec(r)?,
|
||
denominator: Codec::dec(r)?,
|
||
}),
|
||
2 => Ok(TimeSignatureDisplay::Irrational {
|
||
numerator: Codec::dec(r)?,
|
||
denominator: Codec::dec(r)?,
|
||
}),
|
||
3 => Ok(TimeSignatureDisplay::MixedDenominators {
|
||
components: Codec::dec(r)?,
|
||
}),
|
||
4 => Ok(TimeSignatureDisplay::None),
|
||
5 => Ok(TimeSignatureDisplay::Symbolic(Codec::dec(r)?)),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "TimeSignatureDisplay",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Codec for TimeSignature {
|
||
fn enc(&self, out: &mut Vec<u8>) {
|
||
self.id.enc(out);
|
||
self.display.enc(out);
|
||
self.measure_duration().enc(out);
|
||
self.beat_groups().to_vec().enc(out);
|
||
}
|
||
fn dec(r: &mut Reader<'_>) -> Result<Self> {
|
||
let id = Codec::dec(r)?;
|
||
let display = TimeSignatureDisplay::dec(r)?;
|
||
let measure_duration = MusicalDuration::dec(r)?;
|
||
let beat_groups: Vec<BeatGroup> = Codec::dec(r)?;
|
||
TimeSignature::new(id, display, measure_duration, beat_groups)
|
||
.ok_or(ScoreDecodeError::Reconstruct("TimeSignature"))
|
||
}
|
||
}
|
||
|
||
struct_codec!(Score {
|
||
metadata,
|
||
canvas,
|
||
instruments,
|
||
staves,
|
||
staff_groups,
|
||
parts,
|
||
cross_cutting,
|
||
time_signatures,
|
||
tuning_context,
|
||
tempo_map,
|
||
events,
|
||
spelling_attachments,
|
||
decomposition_attachments,
|
||
spelling_precedence,
|
||
analysis_layers,
|
||
views,
|
||
identity,
|
||
tombstoned_pitches,
|
||
tombstoned_events
|
||
});
|
||
|
||
// ===========================================================================
|
||
// Public entry points on Score.
|
||
// ===========================================================================
|
||
|
||
impl Score {
|
||
/// The canonical byte serialization of the whole score graph. Two equal
|
||
/// scores produce identical bytes; the bytes round-trip through
|
||
/// [`Score::decode_canonical`].
|
||
pub fn canonical_bytes(&self) -> Vec<u8> {
|
||
let mut out = Vec::new();
|
||
self.enc(&mut out);
|
||
out
|
||
}
|
||
|
||
/// Decodes the exact inverse of [`Score::canonical_bytes`], validating every
|
||
/// tag, length, primitive, and type invariant. Trailing bytes are rejected.
|
||
///
|
||
/// This is the **current (schema major 3)** layout. To decode bytes whose
|
||
/// schema major is not known to be current, use
|
||
/// [`Score::decode_canonical_versioned`].
|
||
///
|
||
/// Decoding is **strictly canonical**: an accepted byte string is its own
|
||
/// canonical form (`req:format:codec-conventions`). Several leaf and
|
||
/// collection decoders are individually *lenient* — they normalize on decode
|
||
/// (an unreduced [`RationalTime`](crate::RationalTime) reduces to lowest
|
||
/// terms; a `BTreeSet`/`BTreeMap` re-sorts and de-duplicates; a
|
||
/// [`CanonicalF64`] / `ReferencePitch` / `Tempo` normalizes via its
|
||
/// constructor) — so decode alone is not injective. This entry point closes
|
||
/// that gap uniformly: it re-encodes the decoded score and rejects any input
|
||
/// that is not already canonical, so two byte strings can never map to one
|
||
/// score (which would break content-addressing). A valid encoder never emits
|
||
/// a non-canonical score, so this only rejects corrupted or adversarial bytes.
|
||
pub fn decode_canonical(bytes: &[u8]) -> Result<Score> {
|
||
let mut r = Reader::new(bytes);
|
||
let score = Score::dec(&mut r)?;
|
||
r.finish()?;
|
||
if score.canonical_bytes() != bytes {
|
||
return Err(ScoreDecodeError::InvalidValue(
|
||
"non-canonical Score encoding",
|
||
));
|
||
}
|
||
Ok(score)
|
||
}
|
||
|
||
/// The **schema-version dispatch seam** (Binary Format companion
|
||
/// §"Schema Major 1" / §"Schema Major 2" / §"Schema Major 3"): decodes a
|
||
/// full-`Score` snapshot whose bytes were written under the given schema
|
||
/// `major`, migrating a lower-major encoding up to the current in-memory
|
||
/// form on read. Major 3 is the current layout
|
||
/// ([`Score::decode_canonical`]); majors 2, 1, and 0 are decoded through
|
||
/// their frozen wire forms (`decode_v2_score`, `decode_v1_score`,
|
||
/// `decode_v0_score`), each a total default-filling migration — the
|
||
/// composed v0→v1→v2→v3 translation happens in the one v0 read.
|
||
///
|
||
/// The caller (the bundle read path) only reaches this after the chunk gate
|
||
/// has admitted the major into its accept-set, so a major outside
|
||
/// `{0, 1, 2, 3}` is a defensive error, not an expected path.
|
||
pub fn decode_canonical_versioned(bytes: &[u8], major: u16) -> Result<Score> {
|
||
match major {
|
||
3 => Score::decode_canonical(bytes),
|
||
2 => decode_v2_score(bytes),
|
||
1 => decode_v1_score(bytes),
|
||
0 => decode_v0_score(bytes),
|
||
_ => Err(ScoreDecodeError::InvalidValue("unsupported schema major")),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Decodes **schema-major-0** `Score` bytes into the current-layout `Score`,
|
||
/// migrating on read.
|
||
///
|
||
/// This is the **frozen v0 wire form, decoded by value** — a hand-written walk
|
||
/// of the 19 `Score` fields in declaration order, using the current [`Codec`]
|
||
/// for every field whose layout is unchanged, the frozen **v1** sub-decoders
|
||
/// (v0 == v1) for the types schema major 2 filled, the frozen **v2**
|
||
/// sub-decoder ([`dec_tuning_context_v2`]) for `tuning_context` (v0 == v1 ==
|
||
/// v2 — schema major 3 is the first bump to touch it, Push 4b tranche 3b-i),
|
||
/// and a frozen v0 sub-decoder for the three that grew a field in schema
|
||
/// major 1:
|
||
///
|
||
/// * `Canvas` (field 2) — v0 was `regions` only; v1 appended `layout_defaults`.
|
||
/// [`dec_canvas_v0`] reads the region vector and default-fills the defaults.
|
||
/// * `Instrument` (inside field 3, `instruments: Vec<Instrument>`) — v0 was
|
||
/// `{ id, name }`; v1 appended `range`. [`dec_instruments_v0`] default-fills
|
||
/// `range: None`.
|
||
/// * `Region` (inside `Canvas.regions`) — v0 was the six fields before
|
||
/// `permits_spanning_slurs`; v1 appended it. [`dec_region_v0`] default-fills
|
||
/// `false`.
|
||
///
|
||
/// A byte-splice sufficed while only `Canvas` (a single top-level field) had
|
||
/// changed, but two of the three grown structs are nested inside `Vec`s, so
|
||
/// there is no single splice point — the walk must reconstruct each element.
|
||
/// The migration is **total and default-filling**: no score context is needed,
|
||
/// and every new field takes its canonical default. It is frozen by value — it
|
||
/// depends only on the v0 field lists above and the current codec for unchanged
|
||
/// fields; the `v0_score_migrates_*` golden tests guard it (they synthesize real
|
||
/// v0 bytes via a mirror v0 encoder and check the migration reconstructs the
|
||
/// original score with the new fields at their defaults). **Must** read
|
||
/// `tuning_context` via [`dec_tuning_context_v2`], never via the live
|
||
/// [`Codec`] — the live codec now reads/writes the 5-field schema-major-3
|
||
/// form, and reading v0/v1/v2 bytes through it would desynchronize the
|
||
/// cursor for every field after `tuning_context`.
|
||
fn decode_v0_score(bytes: &[u8]) -> Result<Score> {
|
||
let mut r = Reader::new(bytes);
|
||
// The 19 Score fields in declaration order (codec.rs `struct_codec!(Score)`).
|
||
// `canvas` (2) and `instruments` (3) carry v0-specific sub-forms; the
|
||
// schema-major-2 fills route `metadata`, `staves`, `cross_cutting`, and
|
||
// (transitively, inside regions) the staff instances through the frozen
|
||
// v1 sub-decoders — v0 == v1 for every type major 2 changed. `tuning_context`
|
||
// routes through the frozen v2 sub-decoder — v0 == v1 == v2 for it, only
|
||
// major 3 changes it.
|
||
let metadata = dec_metadata_v1(&mut r)?;
|
||
let canvas = dec_canvas_v0(&mut r)?;
|
||
let instruments = dec_instruments_v0(&mut r)?;
|
||
let staves = dec_staves_v1(&mut r)?;
|
||
let staff_groups = Codec::dec(&mut r)?;
|
||
let parts = Codec::dec(&mut r)?;
|
||
let cross_cutting = dec_ccr_v1(&mut r)?;
|
||
let time_signatures = Codec::dec(&mut r)?;
|
||
let tuning_context = dec_tuning_context_v2(&mut r)?;
|
||
let tempo_map = Codec::dec(&mut r)?;
|
||
let events = Codec::dec(&mut r)?;
|
||
let spelling_attachments = Codec::dec(&mut r)?;
|
||
let decomposition_attachments = Codec::dec(&mut r)?;
|
||
let spelling_precedence = Codec::dec(&mut r)?;
|
||
let analysis_layers = Codec::dec(&mut r)?;
|
||
let views = Codec::dec(&mut r)?;
|
||
let identity = Codec::dec(&mut r)?;
|
||
let tombstoned_pitches = Codec::dec(&mut r)?;
|
||
let tombstoned_events = Codec::dec(&mut r)?;
|
||
r.finish()?;
|
||
let score = Score {
|
||
metadata,
|
||
canvas,
|
||
instruments,
|
||
staves,
|
||
staff_groups,
|
||
parts,
|
||
cross_cutting,
|
||
time_signatures,
|
||
tuning_context,
|
||
tempo_map,
|
||
events,
|
||
spelling_attachments,
|
||
decomposition_attachments,
|
||
spelling_precedence,
|
||
analysis_layers,
|
||
views,
|
||
identity,
|
||
tombstoned_pitches,
|
||
tombstoned_events,
|
||
};
|
||
// Strictly canonical on the **v0 wire form**, exactly as
|
||
// `Score::decode_canonical` is for v1: the frozen field walk above decodes
|
||
// leniently (the unchanged fields normalize on decode — an unreduced
|
||
// RationalTime, an unsorted set, …), so re-encode the migrated score to the
|
||
// frozen v0 form and reject any input that is not already its canonical v0
|
||
// encoding. This keeps the migration injective over v0 snapshots too (a
|
||
// major-0 chunk is content-addressed just like a major-1 one). A valid v0
|
||
// writer never emitted a non-canonical snapshot.
|
||
if encode_v0_score(&score) != bytes {
|
||
return Err(ScoreDecodeError::InvalidValue(
|
||
"non-canonical v0 Score encoding",
|
||
));
|
||
}
|
||
Ok(score)
|
||
}
|
||
|
||
/// The **frozen schema-major-0** encoding of a score — the byte-exact inverse of
|
||
/// [`decode_v0_score`]'s field walk: `Canvas` without `layout_defaults`,
|
||
/// `Instrument` without `range`, `Region` without `permits_spanning_slurs`,
|
||
/// `tuning_context` through the frozen 3-field [`enc_tuning_context_v2`] (not
|
||
/// the live [`Codec`], which now writes the 5-field schema-major-3 form);
|
||
/// every other field through the current [`Codec`]. Used by `decode_v0_score`
|
||
/// to enforce strict v0 canonicality (and by the migration tests to synthesize
|
||
/// genuine v0 bytes). A migrated score's schema-major-1 fields hold their
|
||
/// defaults, so this simply omits them.
|
||
pub(crate) fn encode_v0_score(s: &Score) -> Vec<u8> {
|
||
fn enc_region_v0(reg: &Region, out: &mut Vec<u8>) {
|
||
reg.id.enc(out);
|
||
reg.time_model.enc(out);
|
||
// Content through the frozen v1 (== v0) sub-form: its staff
|
||
// instances embed the pre-major-2 StaffLineConfiguration.
|
||
enc_content_v1(®.content, out);
|
||
reg.time_extent.enc(out);
|
||
reg.staff_extent.enc(out);
|
||
reg.local_tempo_map.enc(out);
|
||
// v0: no permits_spanning_slurs.
|
||
}
|
||
let mut out = Vec::new();
|
||
enc_metadata_v1(&s.metadata, &mut out);
|
||
// Canvas v0: `regions` only (no `layout_defaults`).
|
||
put_len(&mut out, s.canvas.regions.len());
|
||
for reg in &s.canvas.regions {
|
||
enc_region_v0(reg, &mut out);
|
||
}
|
||
// Instruments v0: `{ id, name }` only (no `range`).
|
||
put_len(&mut out, s.instruments.len());
|
||
for inst in &s.instruments {
|
||
inst.id.enc(&mut out);
|
||
inst.name.enc(&mut out);
|
||
}
|
||
// Fields 4..19: the schema-major-2-changed ones go through the frozen
|
||
// v1 (== v0) sub-encoders; the rest are unchanged since v0.
|
||
enc_staves_v1(&s.staves, &mut out);
|
||
s.staff_groups.enc(&mut out);
|
||
s.parts.enc(&mut out);
|
||
enc_ccr_v1(&s.cross_cutting, &mut out);
|
||
s.time_signatures.enc(&mut out);
|
||
enc_tuning_context_v2(&s.tuning_context, &mut out);
|
||
s.tempo_map.enc(&mut out);
|
||
s.events.enc(&mut out);
|
||
s.spelling_attachments.enc(&mut out);
|
||
s.decomposition_attachments.enc(&mut out);
|
||
s.spelling_precedence.enc(&mut out);
|
||
s.analysis_layers.enc(&mut out);
|
||
s.views.enc(&mut out);
|
||
s.identity.enc(&mut out);
|
||
s.tombstoned_pitches.enc(&mut out);
|
||
s.tombstoned_events.enc(&mut out);
|
||
out
|
||
}
|
||
|
||
/// Frozen v0 decoder for `Canvas`: the v0 layout was **just `regions`** (a
|
||
/// `Vec<Region>` in v0 element form), with no `layout_defaults`. Reads the
|
||
/// region vector via [`dec_region_v0`] and default-fills the new field.
|
||
fn dec_canvas_v0(r: &mut Reader<'_>) -> Result<Canvas> {
|
||
let n = r.count()?;
|
||
let mut regions = Vec::with_capacity(n.min(1024));
|
||
for _ in 0..n {
|
||
regions.push(dec_region_v0(r)?);
|
||
}
|
||
Ok(Canvas {
|
||
regions,
|
||
layout_defaults: CanvasLayoutDefaults::default(),
|
||
})
|
||
}
|
||
|
||
/// Frozen v0 decoder for a `Region` element: the six fields before
|
||
/// `permits_spanning_slurs`, which is default-filled `false`. (The `Vec`
|
||
/// framing is the caller's; this reads one element.)
|
||
fn dec_region_v0(r: &mut Reader<'_>) -> Result<Region> {
|
||
let id = Codec::dec(r)?;
|
||
let time_model = Codec::dec(r)?;
|
||
let content = dec_content_v1(r)?;
|
||
let time_extent = Codec::dec(r)?;
|
||
let staff_extent = Codec::dec(r)?;
|
||
let local_tempo_map = Codec::dec(r)?;
|
||
Ok(Region {
|
||
id,
|
||
time_model,
|
||
content,
|
||
time_extent,
|
||
staff_extent,
|
||
local_tempo_map,
|
||
permits_spanning_slurs: false,
|
||
})
|
||
}
|
||
|
||
/// Frozen v0 decoder for `Score.instruments`: v0 `Instrument` was `{ id, name }`
|
||
/// (no `range`), which is default-filled `None`. Mirrors the `Vec` framing (a
|
||
/// `u32` count then each element).
|
||
fn dec_instruments_v0(r: &mut Reader<'_>) -> Result<Vec<Instrument>> {
|
||
let n = r.count()?;
|
||
let mut instruments = Vec::with_capacity(n.min(1024));
|
||
for _ in 0..n {
|
||
let id = Codec::dec(r)?;
|
||
let name = Codec::dec(r)?;
|
||
instruments.push(Instrument {
|
||
id,
|
||
name,
|
||
range: None,
|
||
abbreviation: None,
|
||
sound_config: SoundConfiguration::default(),
|
||
transposition: None,
|
||
default_clef: Clef::treble(),
|
||
default_staff_lines: StaffLineConfiguration::default(),
|
||
unpitched_members: Vec::new(),
|
||
});
|
||
}
|
||
Ok(instruments)
|
||
}
|
||
|
||
// ===========================================================================
|
||
// Frozen schema-major-1 wire form (Binary Format §Schema Major 2).
|
||
// ===========================================================================
|
||
//
|
||
// Schema major 2 filled nine type bodies (Slur/Tie/Beam/Spanner,
|
||
// RepeatStructure, Staff, StaffLineConfiguration, Instrument, ScoreMetadata).
|
||
// The v1 (= v0, for all of these — major 1 touched none of them) layouts are
|
||
// frozen here as `enc_*_v1`/`dec_*_v1` sub-codecs, shared by
|
||
// [`decode_v1_score`]/[`encode_v1_score`] and the v0 pair (whose walk routes
|
||
// the transitively-changed fields through these). Each `dec_*_v1`
|
||
// default-fills the appended v2 fields per the companion's total migration
|
||
// table. The chain is transitive where an Option/Vec hides the embedding:
|
||
// Region → RegionContent → StaffBasedContent → StaffInstance →
|
||
// `staff_lines_override`.
|
||
|
||
fn enc_vec_v1<T>(items: &[T], out: &mut Vec<u8>, enc: impl Fn(&T, &mut Vec<u8>)) {
|
||
put_len(out, items.len());
|
||
for item in items {
|
||
enc(item, out);
|
||
}
|
||
}
|
||
|
||
fn dec_vec_v1<T>(r: &mut Reader<'_>, dec: impl Fn(&mut Reader<'_>) -> Result<T>) -> Result<Vec<T>> {
|
||
let n = r.count()?;
|
||
let mut items = Vec::with_capacity(n.min(1024));
|
||
for _ in 0..n {
|
||
items.push(dec(r)?);
|
||
}
|
||
Ok(items)
|
||
}
|
||
|
||
fn enc_metadata_v1(m: &ScoreMetadata, out: &mut Vec<u8>) {
|
||
m.title.enc(out);
|
||
m.composer.enc(out);
|
||
m.copyright.enc(out);
|
||
}
|
||
|
||
fn dec_metadata_v1(r: &mut Reader<'_>) -> Result<ScoreMetadata> {
|
||
Ok(ScoreMetadata {
|
||
title: Codec::dec(r)?,
|
||
composer: Codec::dec(r)?,
|
||
copyright: Codec::dec(r)?,
|
||
..Default::default()
|
||
})
|
||
}
|
||
|
||
fn enc_staff_lines_v1(c: &StaffLineConfiguration, out: &mut Vec<u8>) {
|
||
c.line_count.enc(out);
|
||
}
|
||
|
||
fn dec_staff_lines_v1(r: &mut Reader<'_>) -> Result<StaffLineConfiguration> {
|
||
Ok(StaffLineConfiguration {
|
||
line_count: Codec::dec(r)?,
|
||
..Default::default()
|
||
})
|
||
}
|
||
|
||
fn enc_staff_v1(s: &Staff, out: &mut Vec<u8>) {
|
||
s.id.enc(out);
|
||
s.name.enc(out);
|
||
s.abbreviation.enc(out);
|
||
s.instrument.enc(out);
|
||
enc_staff_lines_v1(&s.default_staff_lines, out);
|
||
s.group.enc(out);
|
||
}
|
||
|
||
fn dec_staff_v1(r: &mut Reader<'_>) -> Result<Staff> {
|
||
Ok(Staff {
|
||
id: Codec::dec(r)?,
|
||
name: Codec::dec(r)?,
|
||
abbreviation: Codec::dec(r)?,
|
||
instrument: Codec::dec(r)?,
|
||
default_staff_lines: dec_staff_lines_v1(r)?,
|
||
group: Codec::dec(r)?,
|
||
default_clef: Clef::treble(),
|
||
})
|
||
}
|
||
|
||
fn enc_staves_v1(staves: &[Staff], out: &mut Vec<u8>) {
|
||
enc_vec_v1(staves, out, enc_staff_v1);
|
||
}
|
||
|
||
fn dec_staves_v1(r: &mut Reader<'_>) -> Result<Vec<Staff>> {
|
||
dec_vec_v1(r, dec_staff_v1)
|
||
}
|
||
|
||
fn enc_slur_v1(s: &Slur, out: &mut Vec<u8>) {
|
||
s.id.enc(out);
|
||
s.start_event.enc(out);
|
||
s.end_event.enc(out);
|
||
}
|
||
|
||
fn dec_slur_v1(r: &mut Reader<'_>) -> Result<Slur> {
|
||
Ok(Slur {
|
||
id: Codec::dec(r)?,
|
||
start_event: Codec::dec(r)?,
|
||
end_event: Codec::dec(r)?,
|
||
kind: SlurKind::Legato,
|
||
curvature_override: None,
|
||
style: SpanStyle::default(),
|
||
})
|
||
}
|
||
|
||
fn enc_tie_v1(t: &Tie, out: &mut Vec<u8>) {
|
||
t.id.enc(out);
|
||
t.start_event.enc(out);
|
||
t.end_event.enc(out);
|
||
t.pitch_pairing.enc(out);
|
||
t.class.enc(out);
|
||
}
|
||
|
||
fn dec_tie_v1(r: &mut Reader<'_>) -> Result<Tie> {
|
||
Ok(Tie {
|
||
id: Codec::dec(r)?,
|
||
start_event: Codec::dec(r)?,
|
||
end_event: Codec::dec(r)?,
|
||
pitch_pairing: Codec::dec(r)?,
|
||
class: Codec::dec(r)?,
|
||
style: SpanStyle::default(),
|
||
})
|
||
}
|
||
|
||
fn enc_beam_v1(b: &Beam, out: &mut Vec<u8>) {
|
||
b.id.enc(out);
|
||
b.events.enc(out);
|
||
b.level.enc(out);
|
||
}
|
||
|
||
fn dec_beam_v1(r: &mut Reader<'_>) -> Result<Beam> {
|
||
Ok(Beam {
|
||
id: Codec::dec(r)?,
|
||
events: Codec::dec(r)?,
|
||
level: Codec::dec(r)?,
|
||
sub_beams: Vec::new(),
|
||
geometry_override: None,
|
||
})
|
||
}
|
||
|
||
fn enc_spanner_v1(s: &Spanner, out: &mut Vec<u8>) {
|
||
s.id.enc(out);
|
||
s.start.enc(out);
|
||
s.end.enc(out);
|
||
s.staves.enc(out);
|
||
}
|
||
|
||
fn dec_spanner_v1(r: &mut Reader<'_>) -> Result<Spanner> {
|
||
Ok(Spanner {
|
||
id: Codec::dec(r)?,
|
||
start: Codec::dec(r)?,
|
||
end: Codec::dec(r)?,
|
||
staves: Codec::dec(r)?,
|
||
kind: SpannerKind::Generic,
|
||
style: SpanStyle::default(),
|
||
})
|
||
}
|
||
|
||
fn enc_repeat_v1(rep: &RepeatStructure, out: &mut Vec<u8>) {
|
||
rep.id.enc(out);
|
||
rep.start.enc(out);
|
||
rep.end.enc(out);
|
||
}
|
||
|
||
fn dec_repeat_v1(r: &mut Reader<'_>) -> Result<RepeatStructure> {
|
||
Ok(RepeatStructure {
|
||
id: Codec::dec(r)?,
|
||
start: Codec::dec(r)?,
|
||
end: Codec::dec(r)?,
|
||
kind: RepeatKind::migration_default(),
|
||
voltas: Vec::new(),
|
||
})
|
||
}
|
||
|
||
fn enc_ccr_v1(c: &CrossCuttingRegistry, out: &mut Vec<u8>) {
|
||
enc_vec_v1(&c.slurs, out, enc_slur_v1);
|
||
enc_vec_v1(&c.ties, out, enc_tie_v1);
|
||
enc_vec_v1(&c.beams, out, enc_beam_v1);
|
||
c.tuplets.enc(out);
|
||
enc_vec_v1(&c.spanners, out, enc_spanner_v1);
|
||
c.markers.enc(out);
|
||
enc_vec_v1(&c.repeats, out, enc_repeat_v1);
|
||
c.analytical.enc(out);
|
||
c.comments.enc(out);
|
||
c.graphic_gestures.enc(out);
|
||
c.lyrics.enc(out);
|
||
c.chord_symbols.enc(out);
|
||
}
|
||
|
||
fn dec_ccr_v1(r: &mut Reader<'_>) -> Result<CrossCuttingRegistry> {
|
||
Ok(CrossCuttingRegistry {
|
||
slurs: dec_vec_v1(r, dec_slur_v1)?,
|
||
ties: dec_vec_v1(r, dec_tie_v1)?,
|
||
beams: dec_vec_v1(r, dec_beam_v1)?,
|
||
tuplets: Codec::dec(r)?,
|
||
spanners: dec_vec_v1(r, dec_spanner_v1)?,
|
||
markers: Codec::dec(r)?,
|
||
repeats: dec_vec_v1(r, dec_repeat_v1)?,
|
||
analytical: Codec::dec(r)?,
|
||
comments: Codec::dec(r)?,
|
||
graphic_gestures: Codec::dec(r)?,
|
||
lyrics: Codec::dec(r)?,
|
||
chord_symbols: Codec::dec(r)?,
|
||
})
|
||
}
|
||
|
||
fn enc_staff_instance_v1(si: &StaffInstance, out: &mut Vec<u8>) {
|
||
si.id.enc(out);
|
||
si.staff.enc(out);
|
||
si.voices.enc(out);
|
||
si.clef_sequence.enc(out);
|
||
si.key_sequence.enc(out);
|
||
si.local_metric_grid.enc(out);
|
||
si.measures.enc(out);
|
||
si.instrument_override.enc(out);
|
||
match &si.staff_lines_override {
|
||
None => out.push(0),
|
||
Some(c) => {
|
||
out.push(1);
|
||
enc_staff_lines_v1(c, out);
|
||
}
|
||
}
|
||
si.visible.enc(out);
|
||
}
|
||
|
||
fn dec_staff_instance_v1(r: &mut Reader<'_>) -> Result<StaffInstance> {
|
||
Ok(StaffInstance {
|
||
id: Codec::dec(r)?,
|
||
staff: Codec::dec(r)?,
|
||
voices: Codec::dec(r)?,
|
||
clef_sequence: Codec::dec(r)?,
|
||
key_sequence: Codec::dec(r)?,
|
||
local_metric_grid: Codec::dec(r)?,
|
||
measures: Codec::dec(r)?,
|
||
instrument_override: Codec::dec(r)?,
|
||
staff_lines_override: match r.u8()? {
|
||
0 => None,
|
||
1 => Some(dec_staff_lines_v1(r)?),
|
||
tag => {
|
||
return Err(ScoreDecodeError::InvalidTag {
|
||
kind: "Option",
|
||
tag,
|
||
})
|
||
}
|
||
},
|
||
visible: Codec::dec(r)?,
|
||
})
|
||
}
|
||
|
||
fn enc_sbc_v1(sbc: &StaffBasedContent, out: &mut Vec<u8>) {
|
||
enc_vec_v1(&sbc.staff_instances, out, enc_staff_instance_v1);
|
||
sbc.default_metric_grid.enc(out);
|
||
sbc.barline_alignment_groups.enc(out);
|
||
sbc.user_system_breaks.enc(out);
|
||
sbc.user_page_breaks.enc(out);
|
||
}
|
||
|
||
fn dec_sbc_v1(r: &mut Reader<'_>) -> Result<StaffBasedContent> {
|
||
Ok(StaffBasedContent {
|
||
staff_instances: dec_vec_v1(r, dec_staff_instance_v1)?,
|
||
default_metric_grid: Codec::dec(r)?,
|
||
barline_alignment_groups: Codec::dec(r)?,
|
||
user_system_breaks: Codec::dec(r)?,
|
||
user_page_breaks: Codec::dec(r)?,
|
||
})
|
||
}
|
||
|
||
fn enc_content_v1(content: &RegionContent, out: &mut Vec<u8>) {
|
||
match content {
|
||
RegionContent::StaffBased(s) => {
|
||
out.push(0);
|
||
enc_sbc_v1(s, out);
|
||
}
|
||
RegionContent::FreeGraphic(g) => {
|
||
out.push(1);
|
||
g.enc(out);
|
||
}
|
||
RegionContent::Hybrid {
|
||
staves,
|
||
overlay,
|
||
overlay_below_staves,
|
||
} => {
|
||
out.push(2);
|
||
enc_sbc_v1(staves, out);
|
||
overlay.enc(out);
|
||
overlay_below_staves.enc(out);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dec_content_v1(r: &mut Reader<'_>) -> Result<RegionContent> {
|
||
match r.u8()? {
|
||
0 => Ok(RegionContent::StaffBased(dec_sbc_v1(r)?)),
|
||
1 => Ok(RegionContent::FreeGraphic(Codec::dec(r)?)),
|
||
2 => Ok(RegionContent::Hybrid {
|
||
staves: dec_sbc_v1(r)?,
|
||
overlay: Codec::dec(r)?,
|
||
overlay_below_staves: Codec::dec(r)?,
|
||
}),
|
||
tag => Err(ScoreDecodeError::InvalidTag {
|
||
kind: "RegionContent",
|
||
tag,
|
||
}),
|
||
}
|
||
}
|
||
|
||
fn enc_region_v1(reg: &Region, out: &mut Vec<u8>) {
|
||
reg.id.enc(out);
|
||
reg.time_model.enc(out);
|
||
enc_content_v1(®.content, out);
|
||
reg.time_extent.enc(out);
|
||
reg.staff_extent.enc(out);
|
||
reg.local_tempo_map.enc(out);
|
||
reg.permits_spanning_slurs.enc(out);
|
||
}
|
||
|
||
fn dec_region_v1(r: &mut Reader<'_>) -> Result<Region> {
|
||
Ok(Region {
|
||
id: Codec::dec(r)?,
|
||
time_model: Codec::dec(r)?,
|
||
content: dec_content_v1(r)?,
|
||
time_extent: Codec::dec(r)?,
|
||
staff_extent: Codec::dec(r)?,
|
||
local_tempo_map: Codec::dec(r)?,
|
||
permits_spanning_slurs: Codec::dec(r)?,
|
||
})
|
||
}
|
||
|
||
fn enc_canvas_v1(c: &Canvas, out: &mut Vec<u8>) {
|
||
enc_vec_v1(&c.regions, out, enc_region_v1);
|
||
c.layout_defaults.enc(out);
|
||
}
|
||
|
||
fn dec_canvas_v1(r: &mut Reader<'_>) -> Result<Canvas> {
|
||
Ok(Canvas {
|
||
regions: dec_vec_v1(r, dec_region_v1)?,
|
||
layout_defaults: Codec::dec(r)?,
|
||
})
|
||
}
|
||
|
||
fn enc_instruments_v1(instruments: &[Instrument], out: &mut Vec<u8>) {
|
||
put_len(out, instruments.len());
|
||
for inst in instruments {
|
||
inst.id.enc(out);
|
||
inst.name.enc(out);
|
||
inst.range.enc(out);
|
||
}
|
||
}
|
||
|
||
fn dec_instruments_v1(r: &mut Reader<'_>) -> Result<Vec<Instrument>> {
|
||
let n = r.count()?;
|
||
let mut instruments = Vec::with_capacity(n.min(1024));
|
||
for _ in 0..n {
|
||
instruments.push(Instrument {
|
||
id: Codec::dec(r)?,
|
||
name: Codec::dec(r)?,
|
||
range: Codec::dec(r)?,
|
||
abbreviation: None,
|
||
sound_config: SoundConfiguration::default(),
|
||
transposition: None,
|
||
default_clef: Clef::treble(),
|
||
default_staff_lines: StaffLineConfiguration::default(),
|
||
unpitched_members: Vec::new(),
|
||
});
|
||
}
|
||
Ok(instruments)
|
||
}
|
||
|
||
/// The **frozen schema-major-1** encoding of a score — the byte-exact inverse
|
||
/// of [`decode_v1_score`]'s field walk (`tuning_context` through the frozen
|
||
/// [`enc_tuning_context_v2`], not the live [`Codec`] — see
|
||
/// [`encode_v0_score`]'s note). Used by `decode_v1_score` to enforce strict v1
|
||
/// canonicality, and by migration tests and the decode fuzzer to synthesize
|
||
/// genuine v1 bytes. A migrated score's schema-major-2 fields hold their
|
||
/// defaults, so this simply omits them.
|
||
pub(crate) fn encode_v1_score(s: &Score) -> Vec<u8> {
|
||
let mut out = Vec::new();
|
||
enc_metadata_v1(&s.metadata, &mut out);
|
||
enc_canvas_v1(&s.canvas, &mut out);
|
||
enc_instruments_v1(&s.instruments, &mut out);
|
||
enc_staves_v1(&s.staves, &mut out);
|
||
s.staff_groups.enc(&mut out);
|
||
s.parts.enc(&mut out);
|
||
enc_ccr_v1(&s.cross_cutting, &mut out);
|
||
s.time_signatures.enc(&mut out);
|
||
enc_tuning_context_v2(&s.tuning_context, &mut out);
|
||
s.tempo_map.enc(&mut out);
|
||
s.events.enc(&mut out);
|
||
s.spelling_attachments.enc(&mut out);
|
||
s.decomposition_attachments.enc(&mut out);
|
||
s.spelling_precedence.enc(&mut out);
|
||
s.analysis_layers.enc(&mut out);
|
||
s.views.enc(&mut out);
|
||
s.identity.enc(&mut out);
|
||
s.tombstoned_pitches.enc(&mut out);
|
||
s.tombstoned_events.enc(&mut out);
|
||
out
|
||
}
|
||
|
||
/// Decodes **schema-major-1** `Score` bytes into the current-layout `Score`,
|
||
/// migrating on read (total, default-filling — Binary Format §Schema
|
||
/// Major 2's migration table). Strictly canonical on the v1 wire form, like
|
||
/// its v0 sibling: re-encodes through [`encode_v1_score`] and rejects any
|
||
/// input that is not already its canonical v1 encoding.
|
||
fn decode_v1_score(bytes: &[u8]) -> Result<Score> {
|
||
let mut r = Reader::new(bytes);
|
||
let metadata = dec_metadata_v1(&mut r)?;
|
||
let canvas = dec_canvas_v1(&mut r)?;
|
||
let instruments = dec_instruments_v1(&mut r)?;
|
||
let staves = dec_staves_v1(&mut r)?;
|
||
let staff_groups = Codec::dec(&mut r)?;
|
||
let parts = Codec::dec(&mut r)?;
|
||
let cross_cutting = dec_ccr_v1(&mut r)?;
|
||
let time_signatures = Codec::dec(&mut r)?;
|
||
let tuning_context = dec_tuning_context_v2(&mut r)?;
|
||
let tempo_map = Codec::dec(&mut r)?;
|
||
let events = Codec::dec(&mut r)?;
|
||
let spelling_attachments = Codec::dec(&mut r)?;
|
||
let decomposition_attachments = Codec::dec(&mut r)?;
|
||
let spelling_precedence = Codec::dec(&mut r)?;
|
||
let analysis_layers = Codec::dec(&mut r)?;
|
||
let views = Codec::dec(&mut r)?;
|
||
let identity = Codec::dec(&mut r)?;
|
||
let tombstoned_pitches = Codec::dec(&mut r)?;
|
||
let tombstoned_events = Codec::dec(&mut r)?;
|
||
r.finish()?;
|
||
let score = Score {
|
||
metadata,
|
||
canvas,
|
||
instruments,
|
||
staves,
|
||
staff_groups,
|
||
parts,
|
||
cross_cutting,
|
||
time_signatures,
|
||
tuning_context,
|
||
tempo_map,
|
||
events,
|
||
spelling_attachments,
|
||
decomposition_attachments,
|
||
spelling_precedence,
|
||
analysis_layers,
|
||
views,
|
||
identity,
|
||
tombstoned_pitches,
|
||
tombstoned_events,
|
||
};
|
||
if encode_v1_score(&score) != bytes {
|
||
return Err(ScoreDecodeError::InvalidValue(
|
||
"non-canonical v1 Score encoding",
|
||
));
|
||
}
|
||
Ok(score)
|
||
}
|
||
|
||
/// The **frozen schema-major-2** encoding of [`ScoreTuningContext`] — the
|
||
/// 3-field pre-major-3 form (`default_pitch_space`, `default_tuning_system`,
|
||
/// `reference`) that majors 0, 1, and 2 all share (schema major 3, Push 4b
|
||
/// tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`, is the first bump to
|
||
/// change this type — it appends `smufl` and `overrides`). Used by
|
||
/// `encode_v0_score`, `encode_v1_score`, and [`encode_v2_score`] — every
|
||
/// frozen pre-major-3 score form — so none of them silently drift onto the
|
||
/// live (now 5-field) [`Codec`].
|
||
fn enc_tuning_context_v2(ctx: &ScoreTuningContext, out: &mut Vec<u8>) {
|
||
ctx.default_pitch_space.enc(out);
|
||
ctx.default_tuning_system.enc(out);
|
||
ctx.reference.enc(out);
|
||
}
|
||
|
||
/// The exact inverse of [`enc_tuning_context_v2`]: reads the 3-field
|
||
/// pre-major-3 form and default-fills the fields major 3 (and earlier
|
||
/// tranches) added — `accidental_extensions: Vec::new()`,
|
||
/// `smufl: SmuflVersionRequirement::default()`, `overrides: Vec::new()`.
|
||
/// Used by `decode_v0_score`, `decode_v1_score`, and [`decode_v2_score`].
|
||
fn dec_tuning_context_v2(r: &mut Reader<'_>) -> Result<ScoreTuningContext> {
|
||
let default_pitch_space = Codec::dec(r)?;
|
||
let default_tuning_system = Codec::dec(r)?;
|
||
let reference = Codec::dec(r)?;
|
||
Ok(ScoreTuningContext {
|
||
default_pitch_space,
|
||
default_tuning_system,
|
||
reference,
|
||
accidental_extensions: Vec::new(),
|
||
smufl: SmuflVersionRequirement::default(),
|
||
overrides: Vec::new(),
|
||
})
|
||
}
|
||
|
||
/// The **frozen schema-major-2** encoding of a score — the byte-exact inverse
|
||
/// of [`decode_v2_score`]'s field walk. Schema major 3 (Push 4b tranche 3b-i)
|
||
/// changes only `tuning_context` (Section "the frozen major-3 wire layout");
|
||
/// every other `Score` field is byte-identical to the live [`Codec`], so this
|
||
/// walk uses the live codec directly for all 18 other fields and
|
||
/// [`enc_tuning_context_v2`] for `tuning_context`. Used by [`decode_v2_score`]
|
||
/// to enforce strict v2 canonicality, and by migration tests to synthesize
|
||
/// genuine v2 bytes.
|
||
pub(crate) fn encode_v2_score(s: &Score) -> Vec<u8> {
|
||
let mut out = Vec::new();
|
||
s.metadata.enc(&mut out);
|
||
s.canvas.enc(&mut out);
|
||
s.instruments.enc(&mut out);
|
||
s.staves.enc(&mut out);
|
||
s.staff_groups.enc(&mut out);
|
||
s.parts.enc(&mut out);
|
||
s.cross_cutting.enc(&mut out);
|
||
s.time_signatures.enc(&mut out);
|
||
enc_tuning_context_v2(&s.tuning_context, &mut out);
|
||
s.tempo_map.enc(&mut out);
|
||
s.events.enc(&mut out);
|
||
s.spelling_attachments.enc(&mut out);
|
||
s.decomposition_attachments.enc(&mut out);
|
||
s.spelling_precedence.enc(&mut out);
|
||
s.analysis_layers.enc(&mut out);
|
||
s.views.enc(&mut out);
|
||
s.identity.enc(&mut out);
|
||
s.tombstoned_pitches.enc(&mut out);
|
||
s.tombstoned_events.enc(&mut out);
|
||
out
|
||
}
|
||
|
||
/// Decodes **schema-major-2** `Score` bytes into the current-layout `Score`,
|
||
/// migrating on read (total, default-filling — Binary Format §"Schema Major
|
||
/// 3"'s migration table: `tuning_context` default-fills `smufl` and
|
||
/// `overrides`; every other field is unchanged since major 2).
|
||
/// Strictly canonical on the v2 wire form, like its v0/v1 siblings: re-encodes
|
||
/// through [`encode_v2_score`] and rejects any input that is not already its
|
||
/// canonical v2 encoding.
|
||
fn decode_v2_score(bytes: &[u8]) -> Result<Score> {
|
||
let mut r = Reader::new(bytes);
|
||
let metadata = Codec::dec(&mut r)?;
|
||
let canvas = Codec::dec(&mut r)?;
|
||
let instruments = Codec::dec(&mut r)?;
|
||
let staves = Codec::dec(&mut r)?;
|
||
let staff_groups = Codec::dec(&mut r)?;
|
||
let parts = Codec::dec(&mut r)?;
|
||
let cross_cutting = Codec::dec(&mut r)?;
|
||
let time_signatures = Codec::dec(&mut r)?;
|
||
let tuning_context = dec_tuning_context_v2(&mut r)?;
|
||
let tempo_map = Codec::dec(&mut r)?;
|
||
let events = Codec::dec(&mut r)?;
|
||
let spelling_attachments = Codec::dec(&mut r)?;
|
||
let decomposition_attachments = Codec::dec(&mut r)?;
|
||
let spelling_precedence = Codec::dec(&mut r)?;
|
||
let analysis_layers = Codec::dec(&mut r)?;
|
||
let views = Codec::dec(&mut r)?;
|
||
let identity = Codec::dec(&mut r)?;
|
||
let tombstoned_pitches = Codec::dec(&mut r)?;
|
||
let tombstoned_events = Codec::dec(&mut r)?;
|
||
r.finish()?;
|
||
let score = Score {
|
||
metadata,
|
||
canvas,
|
||
instruments,
|
||
staves,
|
||
staff_groups,
|
||
parts,
|
||
cross_cutting,
|
||
time_signatures,
|
||
tuning_context,
|
||
tempo_map,
|
||
events,
|
||
spelling_attachments,
|
||
decomposition_attachments,
|
||
spelling_precedence,
|
||
analysis_layers,
|
||
views,
|
||
identity,
|
||
tombstoned_pitches,
|
||
tombstoned_events,
|
||
};
|
||
if encode_v2_score(&score) != bytes {
|
||
return Err(ScoreDecodeError::InvalidValue(
|
||
"non-canonical v2 Score encoding",
|
||
));
|
||
}
|
||
Ok(score)
|
||
}
|
||
|
||
// ===========================================================================
|
||
// Public per-value canonical codec (the K↔J seam).
|
||
// ===========================================================================
|
||
|
||
/// A public, whole-buffer canonical byte form for an *individual* value
|
||
/// reachable from a [`Score`] — the per-type analogue of
|
||
/// [`Score::canonical_bytes`].
|
||
///
|
||
/// ## Why this exists
|
||
///
|
||
/// The internal `Codec` trait (and its `Reader`) are crate-private: they are
|
||
/// the composition machinery for the whole-score codec, and exposing them would
|
||
/// leak the cursor and the combinator surface. But Track B's Operation Catalog
|
||
/// (Agent K) needs *value-typed* operation payloads — an `InsertEvent` that
|
||
/// carries the real [`Event`], a `RespellPitch` that carries the real
|
||
/// [`PitchSpelling`] — and those payloads must serialize canonically so an
|
||
/// operation envelope stays hashable across an implementation boundary.
|
||
///
|
||
/// `CanonicalValue` is that agreed surface: a thin, public, per-type
|
||
/// `canonical_bytes`/`decode_canonical` pair that **delegates to the existing,
|
||
/// ratified `Codec` byte layout** (Pass 11 item 1.8,
|
||
/// `req:format:codec-conventions`). It introduces *no new byte layout* — the
|
||
/// bytes are byte-for-byte the same ones the whole-score codec already emits for
|
||
/// these values, merely made reachable for an individual value. The Binary
|
||
/// Format companion (Agent J) documents this surface as normative; until then it
|
||
/// inherits core's conventions exactly, so core/ops stay consistent. See
|
||
/// `DECISIONS.md` (P11-4 / the K↔J seam).
|
||
///
|
||
/// Implemented for the value types operation payloads embed, and — since
|
||
/// `spec/CONTRACT_CORE_DECODE_VECTORS.md` — for the value types the
|
||
/// cross-implementation decode-vector corpus (`crate::vectors`) pins: the
|
||
/// representative layouts of the Binary Format companion's
|
||
/// §"Representative Complete Layouts" and the schema-major-3 tuning-context
|
||
/// leaves. Not for every `Codec` type, to keep the public surface intentional;
|
||
/// the intent has simply widened to a second reason.
|
||
pub trait CanonicalValue: Sized {
|
||
/// The canonical bytes of this single value, using the same layout the
|
||
/// whole-score codec uses for it.
|
||
fn canonical_bytes(&self) -> Vec<u8>;
|
||
|
||
/// The exact inverse of [`CanonicalValue::canonical_bytes`]. Validates every
|
||
/// tag, length, primitive, and invariant; rejects trailing bytes.
|
||
fn decode_canonical(bytes: &[u8]) -> core::result::Result<Self, ScoreDecodeError>;
|
||
}
|
||
|
||
/// Implements [`CanonicalValue`] for value types that already have a [`Codec`],
|
||
/// by delegating to it. No new byte layout is introduced.
|
||
macro_rules! canonical_value {
|
||
($($ty:ty),* $(,)?) => {
|
||
$(
|
||
impl CanonicalValue for $ty {
|
||
fn canonical_bytes(&self) -> Vec<u8> {
|
||
let mut out = Vec::new();
|
||
Codec::enc(self, &mut out);
|
||
out
|
||
}
|
||
fn decode_canonical(bytes: &[u8]) -> Result<Self> {
|
||
let mut r = Reader::new(bytes);
|
||
let v = <$ty as Codec>::dec(&mut r)?;
|
||
r.finish()?;
|
||
// Strictly canonical, exactly as `Score::decode_canonical`:
|
||
// reject a non-canonical encoding so per-value decode is
|
||
// injective (these bytes are content-addressed inside
|
||
// operation payloads).
|
||
if v.canonical_bytes() != bytes {
|
||
return Err(ScoreDecodeError::InvalidValue(
|
||
"non-canonical value encoding",
|
||
));
|
||
}
|
||
Ok(v)
|
||
}
|
||
}
|
||
)*
|
||
};
|
||
}
|
||
|
||
canonical_value! {
|
||
Event,
|
||
Rest,
|
||
Pitch,
|
||
IdentifiedPitch,
|
||
PitchSpelling,
|
||
Tie,
|
||
Slur,
|
||
Beam,
|
||
Spanner,
|
||
RegionTimeModel,
|
||
TimeAnchor,
|
||
// Structural containers (M2c) — value-typed create payloads embed these.
|
||
Region,
|
||
StaffInstance,
|
||
Voice,
|
||
// Pre-pass annotation leaves — give DerivedAnnotations a canonical byte
|
||
// fingerprint (a normative surface, vs. its Debug form).
|
||
DecompositionAttachment,
|
||
SpellingSourceKind,
|
||
// Score settings (M2d) — value-typed set-* payloads embed these.
|
||
ScoreMetadata,
|
||
MetricGrid,
|
||
// Phase-3 ops tranche — value-typed create/set payloads embed these.
|
||
// `TimeSignature::dec` re-validates the beat-group sum through
|
||
// `TimeSignature::new` (the "reject at construction *and* at decode"
|
||
// discipline the operation catalog's §"Meter and Tempo Overwrites"
|
||
// requires), so a malformed value never round-trips.
|
||
Staff,
|
||
TimeSignature,
|
||
TempoSegment,
|
||
StaffLineConfiguration,
|
||
// Genesis tranche G1 (`CONTRACT_GENESIS_G1_INSTRUMENT.md`) — CreateInstrument
|
||
// embeds the full value, mirroring CreateStaff's `Staff`. `Instrument`
|
||
// already has a `Codec` (`struct_codec!`, `:1756`) and already ships inside
|
||
// `Score`; this makes that existing layout reachable per-value, same as
|
||
// every other entry here.
|
||
Instrument,
|
||
// Repeat authoring (schema-major-2 revision) — CreateRepeatStructure
|
||
// embeds the full value.
|
||
RepeatStructure,
|
||
// Decode-vector corpus (`spec/CONTRACT_CORE_DECODE_VECTORS.md`): the fifth
|
||
// representative layout (`RationalTime` — the other four, `Pitch`,
|
||
// `TimeAnchor`, `Event`, `Slur`, are already above), plus the
|
||
// schema-major-3 tuning-context container and its four leaf types, frozen
|
||
// by Push 4b tranche 3b-i. None of these gain a byte layout here — every
|
||
// one already has a `Codec` impl the whole-score codec uses; this macro
|
||
// only makes that existing layout reachable per-value.
|
||
RationalTime,
|
||
ScoreTuningContext,
|
||
TuningOverride,
|
||
TuningScope,
|
||
SmuflVersionRequirement,
|
||
SmuflVersion,
|
||
// Genesis tranche G2a (`CONTRACT_GENESIS_G2A_SETTINGS.md`) —
|
||
// SetCanvasLayoutDefaults and SetSpellingPrecedence embed the full value,
|
||
// mirroring SetMetadata's `ScoreMetadata`. Both already have a `Codec`
|
||
// (`struct_codec!` for `CanvasLayoutDefaults` at `:2115`; the hand-written
|
||
// `impl Codec for SpellingPrecedence` at `:1144`) and already ship inside
|
||
// `Score`; this makes that existing layout reachable per-value, same as
|
||
// every other entry here. Neither carries a schema-major-1+ layout of its
|
||
// own — see the contract's "Why these two" table.
|
||
CanvasLayoutDefaults,
|
||
SpellingPrecedence,
|
||
// Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` §1) —
|
||
// SetTuningContext embeds this authored subset, not the full
|
||
// `ScoreTuningContext`. Its `Codec` above is byte-identical to
|
||
// `ScoreTuningContext`'s five-field walk; this makes that layout
|
||
// reachable per-value, exactly as `ScoreTuningContext` above is.
|
||
TuningContextSettings,
|
||
// Genesis tranche G3a (`CONTRACT_GENESIS_G3A_ENTITIES.md` pin 3) — the
|
||
// four root-level mints (`CreateStaffGroup`, `CreatePartDefinition`,
|
||
// `CreateAnalysisLayer`, `CreateView`) embed the full value, mirroring
|
||
// `CreateStaff`'s `Staff` / `CreateInstrument`'s `Instrument`. All four
|
||
// already have a `Codec` (`struct_codec!` at `:1790`–`:1796` and `:2329`)
|
||
// and already ship inside `Score`; this makes those existing layouts
|
||
// reachable per-value, same as every other entry here. No new byte
|
||
// layout, and no `textvalue_graph.rs` work — `struct_codec!` already
|
||
// generated each type's `TextValue` impl alongside its `Codec`.
|
||
StaffGroup,
|
||
PartDefinition,
|
||
AnalysisLayer,
|
||
ViewDefinition,
|
||
// Genesis tranche G3b (`CONTRACT_GENESIS_G3B_MEASURE.md` pin 3) —
|
||
// CreateMeasure embeds the full value, mirroring CreateStaffInstance's
|
||
// `StaffInstance`. `Measure` already has a `Codec` (`struct_codec!` at
|
||
// `:1825`) and already ships inside `Score`; this makes that existing
|
||
// layout reachable per-value, same as every other entry here. No new wire
|
||
// layout, and no `textvalue_graph.rs` work — `struct_codec!` already
|
||
// generated `Measure`'s `TextValue` impl alongside its `Codec`.
|
||
Measure,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod value_codec_tests {
|
||
use super::*;
|
||
use crate::generators::{valid_score, valid_score_rich};
|
||
|
||
/// Every `CanonicalValue` round-trips, and its per-value bytes are exactly
|
||
/// the bytes the whole-score codec embeds for that value (no new layout).
|
||
fn assert_value_round_trips<T>(v: &T)
|
||
where
|
||
T: CanonicalValue + Codec + PartialEq + core::fmt::Debug,
|
||
{
|
||
let bytes = v.canonical_bytes();
|
||
// Per-value bytes equal what the internal Codec emits (the embedded form).
|
||
let mut embedded = Vec::new();
|
||
Codec::enc(v, &mut embedded);
|
||
assert_eq!(bytes, embedded, "CanonicalValue diverged from Codec layout");
|
||
let decoded = T::decode_canonical(&bytes).expect("value decodes");
|
||
assert_eq!(&decoded, v, "value round-trip changed the value");
|
||
assert_eq!(
|
||
decoded.canonical_bytes(),
|
||
bytes,
|
||
"value re-encode not byte-identical"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn value_types_round_trip_over_generator_corpus() {
|
||
for seed in 0..64u64 {
|
||
let s = valid_score_rich(seed.wrapping_mul(0x0100_0193).wrapping_add(7));
|
||
for region in &s.canvas.regions {
|
||
assert_value_round_trips(®ion.time_model);
|
||
assert_value_round_trips(region);
|
||
for instance in region.staff_instances() {
|
||
assert_value_round_trips(instance);
|
||
for voice in &instance.voices {
|
||
assert_value_round_trips(voice);
|
||
}
|
||
}
|
||
}
|
||
for tie in &s.cross_cutting.ties {
|
||
assert_value_round_trips(tie);
|
||
}
|
||
for slur in &s.cross_cutting.slurs {
|
||
assert_value_round_trips(slur);
|
||
}
|
||
for beam in &s.cross_cutting.beams {
|
||
assert_value_round_trips(beam);
|
||
}
|
||
for spanner in &s.cross_cutting.spanners {
|
||
assert_value_round_trips(spanner);
|
||
}
|
||
}
|
||
for seed in 0..200u64 {
|
||
let s = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1));
|
||
for ev in s.events.iter() {
|
||
assert_value_round_trips(ev);
|
||
let mut ips = Vec::new();
|
||
ev.collect_identified_pitches(&mut ips);
|
||
for ip in ips {
|
||
assert_value_round_trips(ip);
|
||
assert_value_round_trips(&ip.pitch);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::event::{IndeterminateEvent, Rest, UnpitchedEvent};
|
||
use crate::generators::{valid_score, valid_score_rich};
|
||
use crate::ids::EventId;
|
||
use crate::time::{EventDuration, EventPosition};
|
||
|
||
/// encode → decode → equal, and re-encode byte-identical.
|
||
fn assert_round_trips(score: &Score) {
|
||
let bytes = score.canonical_bytes();
|
||
let decoded = Score::decode_canonical(&bytes).expect("score decodes");
|
||
assert_eq!(&decoded, score, "round-trip changed the score");
|
||
assert_eq!(
|
||
decoded.canonical_bytes(),
|
||
bytes,
|
||
"re-encode not byte-identical"
|
||
);
|
||
}
|
||
|
||
#[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]
|
||
fn score_tuning_context_smufl_and_overrides_reach_the_wire_accidental_extensions_do_not() {
|
||
// Schema major 3 (Push 4b tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`)
|
||
// moves `smufl` and `overrides` onto the wire — the direct
|
||
// **inversion** of the pre-major-3 proof this test replaces (formerly
|
||
// two tests: `score_tuning_context_overrides_do_not_reach_the_wire`,
|
||
// Push 4b tranche 2; and
|
||
// `score_tuning_context_accidental_extensions_smufl_and_overrides_do_not_reach_the_wire`,
|
||
// tranche 3a). `accidental_extensions` stays in memory only (staged to
|
||
// a later major, appended after `overrides` when it lands), so this
|
||
// is now a **staging-boundary** test: it proves the line falls
|
||
// exactly between `overrides` and `accidental_extensions`, not that
|
||
// nothing reaches the wire.
|
||
use crate::accidental::{PitchSpaceModification, SmuflVersion, SmuflVersionRequirement};
|
||
use crate::graph::ScoreTuningContext;
|
||
use crate::ids::{ReplicaId, VoiceId};
|
||
use crate::pitch::TuningSystemId;
|
||
use crate::tuning::{TuningOverride, TuningScope};
|
||
|
||
let bare = ScoreTuningContext::default();
|
||
let mut loaded = ScoreTuningContext::default();
|
||
loaded
|
||
.accidental_extensions
|
||
.push(crate::accidental::fixture_extensions(
|
||
"heji",
|
||
PitchSpaceModification::CmnChromatic(1),
|
||
));
|
||
loaded.smufl = SmuflVersionRequirement {
|
||
minimum: SmuflVersion::from_decimal(1, "12").unwrap(),
|
||
authored_against: SmuflVersion::from_decimal(1, "18").unwrap(),
|
||
};
|
||
loaded.overrides.push(TuningOverride {
|
||
scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 1)),
|
||
pitch_space: None,
|
||
tuning_system: Some(TuningSystemId::new("tet-19")),
|
||
reference: None,
|
||
});
|
||
// Sanity: the fixture actually differs in memory in all three
|
||
// fields, so neither assertion below is vacuous.
|
||
assert_ne!(loaded, bare);
|
||
assert!(!loaded.accidental_extensions.is_empty());
|
||
assert_ne!(loaded.smufl, SmuflVersionRequirement::default());
|
||
assert!(!loaded.overrides.is_empty());
|
||
|
||
let mut bytes_loaded = Vec::new();
|
||
loaded.enc(&mut bytes_loaded);
|
||
let mut bytes_bare = Vec::new();
|
||
bare.enc(&mut bytes_bare);
|
||
// `smufl` and `overrides` differ in memory, so — unlike before major
|
||
// 3 — the two encodings must now differ too.
|
||
assert_ne!(
|
||
bytes_loaded, bytes_bare,
|
||
"smufl and overrides must reach canonical bytes as of schema major 3"
|
||
);
|
||
|
||
let decoded = ScoreTuningContext::dec(&mut Reader::new(&bytes_loaded)).expect("decodes");
|
||
// smufl and overrides round-trip exactly...
|
||
assert_eq!(decoded.smufl, loaded.smufl);
|
||
assert_eq!(decoded.overrides, loaded.overrides);
|
||
// ...but accidental_extensions is still dropped: never on the wire.
|
||
assert!(decoded.accidental_extensions.is_empty());
|
||
assert_eq!(
|
||
decoded,
|
||
ScoreTuningContext {
|
||
accidental_extensions: Vec::new(),
|
||
..loaded.clone()
|
||
}
|
||
);
|
||
}
|
||
|
||
/// Pins the **exact frozen bytes** of the schema-major-3
|
||
/// `ScoreTuningContext` wire form (Push 4b tranche 3b-i).
|
||
///
|
||
/// Why a byte literal and not just a round-trip: the cross-implementation
|
||
/// decode corpus (`spec/vectors/decode_vectors.txt`,
|
||
/// `req:binfmt:decode-vectors`) covers only the **operation** and
|
||
/// **bundle** surfaces — there is no `epiphany-core` vectors module, so
|
||
/// the core score wire has no vector pinning it. Every other test here
|
||
/// round-trips `enc` against `dec`, which stays green under a
|
||
/// *self-consistent* reordering of the fields: swapping `smufl` and
|
||
/// `overrides` in both halves passes the entire workspace suite. That
|
||
/// would silently move a layout `req:binfmt:frozen-layout` freezes
|
||
/// permanently, and a second implementation would disagree. This golden
|
||
/// is what makes such a swap fail.
|
||
///
|
||
/// If this test fails, the wire format changed. That is a schema-major
|
||
/// event — do not "fix" it by re-copying the bytes.
|
||
#[test]
|
||
fn schema_major_3_tuning_context_wire_bytes_are_frozen() {
|
||
use crate::accidental::{PitchSpaceModification, SmuflVersion, SmuflVersionRequirement};
|
||
use crate::graph::ScoreTuningContext;
|
||
use crate::ids::{ReplicaId, VoiceId};
|
||
use crate::pitch::TuningSystemId;
|
||
use crate::tuning::{TuningOverride, TuningScope};
|
||
|
||
fn hex(bytes: &[u8]) -> String {
|
||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||
}
|
||
|
||
// --- the default context: what every score with untouched tuning
|
||
// encodes to, so this literal is the one that appears in practice.
|
||
//
|
||
// 06000000 636d6e2d3132 default_pitch_space = "cmn-12"
|
||
// 06000000 7465742d3132 default_tuning_system = "tet-12"
|
||
// 00050004080000000000000000807b40 reference (frozen since major 0)
|
||
// 01002800 smufl.minimum = {major 1, minor_centi 40} = 1.4
|
||
// 01002800 smufl.authored_against = 1.4
|
||
// 00000000 overrides = u32 count 0
|
||
//
|
||
// Note the S12 normalization is visible on the wire: 1.4 stores
|
||
// `minor_centi` 40 (0x28), NOT a literal minor 4 — that is what makes
|
||
// 1.12 < 1.3 order correctly.
|
||
let mut bare = Vec::new();
|
||
ScoreTuningContext::default().enc(&mut bare);
|
||
assert_eq!(
|
||
hex(&bare),
|
||
"06000000636d6e2d3132060000007465742d3132000500040800000000000000 0080 7b40010028000100280000000000"
|
||
.replace(' ', ""),
|
||
"the default major-3 tuning-context wire form moved"
|
||
);
|
||
assert_eq!(bare.len(), 48);
|
||
|
||
// --- a fully loaded context: non-default smufl, one override, and a
|
||
// populated `accidental_extensions` that MUST contribute zero bytes
|
||
// (staged to a later major).
|
||
//
|
||
// ...prefix and reference as above...
|
||
// 01000c00 smufl.minimum = {1, 12} = 1.12
|
||
// 01001200 smufl.authored_against = {1, 18} = 1.18
|
||
// 01000000 overrides = u32 count 1
|
||
// <34 bytes> the TuningOverride body: TuningScope::Voice
|
||
// (discriminant 0) + VoiceId, then the three Options
|
||
// (pitch_space None, tuning_system Some("tet-19"),
|
||
// reference None).
|
||
let mut loaded = ScoreTuningContext {
|
||
smufl: SmuflVersionRequirement {
|
||
minimum: SmuflVersion::from_decimal(1, "12").unwrap(),
|
||
authored_against: SmuflVersion::from_decimal(1, "18").unwrap(),
|
||
},
|
||
..ScoreTuningContext::default()
|
||
};
|
||
loaded.overrides.push(TuningOverride {
|
||
scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 7)),
|
||
pitch_space: None,
|
||
tuning_system: Some(TuningSystemId::new("tet-19")),
|
||
reference: None,
|
||
});
|
||
loaded
|
||
.accidental_extensions
|
||
.push(crate::accidental::fixture_extensions(
|
||
"heji",
|
||
PitchSpaceModification::CmnChromatic(1),
|
||
));
|
||
assert!(!loaded.accidental_extensions.is_empty());
|
||
|
||
let mut got = Vec::new();
|
||
loaded.enc(&mut got);
|
||
assert_eq!(
|
||
hex(&got),
|
||
"06000000636d6e2d3132060000007465742d31320005000408000000000000000080 7b4001000c000100120001000000001000000000000000000000010000000000000007 0001060000007465742d313900"
|
||
.replace(' ', ""),
|
||
"the loaded major-3 tuning-context wire form moved"
|
||
);
|
||
assert_eq!(got.len(), 82);
|
||
|
||
// The golden is bidirectional: the frozen bytes decode back to the
|
||
// value, with `accidental_extensions` dropped (it never encoded).
|
||
let back = ScoreTuningContext::dec(&mut Reader::new(&got)).expect("golden decodes");
|
||
assert_eq!(back.smufl, loaded.smufl);
|
||
assert_eq!(back.overrides, loaded.overrides);
|
||
assert!(back.accidental_extensions.is_empty());
|
||
}
|
||
|
||
/// Genesis tranche G3b (`CONTRACT_GENESIS_G3B_MEASURE.md` pin 4, M12):
|
||
/// pins the exact frozen bytes of `Measure`'s `struct_codec!` wire form
|
||
/// (`id, start, time_signature, explicit_number, number_visibility`).
|
||
///
|
||
/// The five field values are chosen so their encodings are MUTUALLY
|
||
/// DISTINCT in length (contract §3's `valuegen` trap, from G3a's
|
||
/// `analysis_layer` mistake where a same-width field pair made a
|
||
/// field-swap byte-invisible): `id` is a 20-byte id-leaf, `start` (a
|
||
/// `WallClock` anchor) is 13 bytes, `time_signature` (`Some`) is a
|
||
/// 21-byte id-leaf, `explicit_number` (`Some`) is a bare 5-byte `u32`,
|
||
/// and `number_visibility` is a 1-byte C-style enum tag.
|
||
///
|
||
/// **Mutation:** reorder `struct_codec!(Measure { ... })`'s field list in
|
||
/// this file; must fail — a `struct_codec!` field reorder changes encode
|
||
/// and decode symmetrically and stays green under a round-trip-only
|
||
/// assertion, which is why this pins literal bytes instead.
|
||
#[test]
|
||
fn genesis_g3b_measure_wire_bytes_are_frozen() {
|
||
use crate::graph::{Measure, MeasureNumberVisibility};
|
||
use crate::ids::{MeasureId, ReplicaId, TimeSignatureId};
|
||
use crate::time::{TimeAnchor, WallClockTime};
|
||
|
||
fn hex(bytes: &[u8]) -> String {
|
||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||
}
|
||
|
||
let measure = Measure {
|
||
id: MeasureId::new(ReplicaId(1), 2),
|
||
start: TimeAnchor::WallClock {
|
||
time: WallClockTime(3),
|
||
},
|
||
time_signature: Some(TimeSignatureId::new(ReplicaId(1), 4)),
|
||
explicit_number: Some(5),
|
||
number_visibility: MeasureNumberVisibility::Always,
|
||
};
|
||
let mut bytes = Vec::new();
|
||
measure.enc(&mut bytes);
|
||
let expected = "100000000000000000000001000000000000000203080000000300000000000000011000000000000000000000010000000000000004010500000001";
|
||
assert_eq!(hex(&bytes), expected, "the Measure wire form moved");
|
||
assert!(!bytes.is_empty());
|
||
|
||
let decoded = Measure::dec(&mut Reader::new(&bytes)).expect("golden decodes");
|
||
assert_eq!(decoded, measure);
|
||
}
|
||
|
||
/// Genesis tranche G3b (M14): the strict per-value re-encode comparison
|
||
/// in `canonical_value!` is the ONLY thing standing between an
|
||
/// unreduced `RationalTime` spelling and acceptance.
|
||
///
|
||
/// `Measure.start` carries an `Event`-anchored `Musical` offset whose
|
||
/// `RationalTime` is spelled UNREDUCED, `2/4`. `RationalTime::dec` ends in
|
||
/// `BigRational::new`, which reduces on construction, so the decoded
|
||
/// value is `1/2`; re-encoding it then emits `1/2`'s magnitudes, which
|
||
/// differ from the fed-in `2/4` bytes. A structurally-decodable but
|
||
/// NONCANONICAL encoding — exactly what the strict per-value check
|
||
/// (decode → `finish()` → re-encode → reject on mismatch) exists to
|
||
/// reject.
|
||
///
|
||
/// **Mutation:** in `canonical_value!`, remove the
|
||
/// `v.canonical_bytes() != bytes` check (return `Ok(v)` unconditionally);
|
||
/// must fail. NOT "remove `Measure` from `canonical_value!`" — that does
|
||
/// not compile: the ops-crate envelope decode arm calls `value::<Measure>`,
|
||
/// bound by `T: CanonicalValue`.
|
||
#[test]
|
||
fn genesis_g3b_measure_start_rejects_an_unreduced_rational_time() {
|
||
use crate::graph::{Measure, MeasureNumberVisibility};
|
||
use crate::ids::{EventId, MeasureId, ReplicaId};
|
||
use crate::time::{AnchorOffset, MusicalDuration, RationalTime, TimeAnchor};
|
||
|
||
// The canonical (reduced) 1/2 value this rung's fixture decodes to.
|
||
let reduced = Measure {
|
||
id: MeasureId::new(ReplicaId(1), 2),
|
||
start: TimeAnchor::Event {
|
||
id: EventId::new(ReplicaId(1), 3),
|
||
offset: AnchorOffset::Musical(MusicalDuration(
|
||
RationalTime::new(1, 2).expect("1/2 is representable"),
|
||
)),
|
||
},
|
||
time_signature: None,
|
||
explicit_number: None,
|
||
number_visibility: MeasureNumberVisibility::Auto,
|
||
};
|
||
let mut good_bytes = Vec::new();
|
||
reduced.enc(&mut good_bytes);
|
||
|
||
// The RationalTime wire pattern for 1/2 (Plus sign, 1-byte numerator
|
||
// magnitude `01`, 1-byte denominator magnitude `02`) and its
|
||
// UNREDUCED 2/4 counterpart — same byte length (11), so substituting
|
||
// one for the other does not disturb any surrounding length prefix.
|
||
let pattern_1_2: [u8; 11] = [1, 1, 0, 0, 0, 0x01, 1, 0, 0, 0, 0x02];
|
||
let pattern_2_4: [u8; 11] = [1, 1, 0, 0, 0, 0x02, 1, 0, 0, 0, 0x04];
|
||
|
||
let occurrences = good_bytes
|
||
.windows(pattern_1_2.len())
|
||
.filter(|w| *w == pattern_1_2)
|
||
.count();
|
||
assert_eq!(
|
||
occurrences, 1,
|
||
"the 1/2 RationalTime pattern must appear exactly once, to substitute unambiguously"
|
||
);
|
||
let at = good_bytes
|
||
.windows(pattern_1_2.len())
|
||
.position(|w| w == pattern_1_2)
|
||
.unwrap();
|
||
let mut bad_bytes = good_bytes.clone();
|
||
bad_bytes[at..at + pattern_2_4.len()].copy_from_slice(&pattern_2_4);
|
||
assert_eq!(bad_bytes.len(), good_bytes.len());
|
||
|
||
// The reduced (canonical) bytes round-trip.
|
||
assert_eq!(
|
||
Measure::decode_canonical(&good_bytes).as_ref(),
|
||
Ok(&reduced)
|
||
);
|
||
// The unreduced (2/4) bytes are structurally decodable (RationalTime
|
||
// itself never rejects an unreduced input — it normalizes), but the
|
||
// outer strict re-encode comparison MUST reject them.
|
||
assert!(
|
||
Measure::decode_canonical(&bad_bytes).is_err(),
|
||
"an unreduced RationalTime spelling must be rejected by the strict re-encode check"
|
||
);
|
||
}
|
||
|
||
/// Genesis tranche G2b (`CONTRACT_GENESIS_G2B_TUNING.md` §1, pin/touch-row
|
||
/// 3): `TuningContextSettings`'s canonical encoding is **byte-identical**
|
||
/// to `ScoreTuningContext`'s existing five-field walk — a type-level
|
||
/// narrowing, not a new wire form. Proven directly against non-default
|
||
/// values in all five fields, not just the default.
|
||
///
|
||
/// **Mutation:** reorder two fields in `TuningContextSettings::enc` (e.g.
|
||
/// swap `smufl` and `overrides`) without making the matching change in
|
||
/// `ScoreTuningContext::enc`; must fail.
|
||
#[test]
|
||
fn tuning_context_settings_canonical_bytes_match_score_tuning_context() {
|
||
use crate::accidental::{SmuflVersion, SmuflVersionRequirement};
|
||
use crate::graph::{ScoreTuningContext, TuningContextSettings};
|
||
use crate::ids::{ReplicaId, VoiceId};
|
||
use crate::pitch::TuningSystemId;
|
||
use crate::tuning::{TuningOverride, TuningScope};
|
||
|
||
let full = ScoreTuningContext {
|
||
smufl: SmuflVersionRequirement {
|
||
minimum: SmuflVersion::from_decimal(1, "12").unwrap(),
|
||
authored_against: SmuflVersion::from_decimal(1, "18").unwrap(),
|
||
},
|
||
overrides: vec![TuningOverride {
|
||
scope: TuningScope::Voice(VoiceId::new(ReplicaId(1), 7)),
|
||
pitch_space: None,
|
||
tuning_system: Some(TuningSystemId::new("tet-19")),
|
||
reference: None,
|
||
}],
|
||
..ScoreTuningContext::default()
|
||
};
|
||
let settings = TuningContextSettings {
|
||
default_pitch_space: full.default_pitch_space.clone(),
|
||
default_tuning_system: full.default_tuning_system.clone(),
|
||
reference: full.reference.clone(),
|
||
smufl: full.smufl,
|
||
overrides: full.overrides.clone(),
|
||
};
|
||
|
||
let mut full_bytes = Vec::new();
|
||
full.enc(&mut full_bytes);
|
||
let mut settings_bytes = Vec::new();
|
||
settings.enc(&mut settings_bytes);
|
||
assert_eq!(
|
||
settings_bytes, full_bytes,
|
||
"TuningContextSettings must encode byte-identically to ScoreTuningContext's five-field walk"
|
||
);
|
||
|
||
let decoded =
|
||
TuningContextSettings::dec(&mut Reader::new(&settings_bytes)).expect("decodes");
|
||
assert_eq!(decoded.default_pitch_space, settings.default_pitch_space);
|
||
assert_eq!(
|
||
decoded.default_tuning_system,
|
||
settings.default_tuning_system
|
||
);
|
||
assert_eq!(decoded.reference, settings.reference);
|
||
assert_eq!(decoded.smufl, settings.smufl);
|
||
assert_eq!(decoded.overrides, settings.overrides);
|
||
}
|
||
|
||
#[test]
|
||
fn generator_scores_round_trip() {
|
||
for seed in 0..200u64 {
|
||
assert_round_trips(&valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1)));
|
||
}
|
||
for seed in 0..64u64 {
|
||
assert_round_trips(&valid_score_rich(
|
||
seed.wrapping_mul(0x0100_0193).wrapping_add(7),
|
||
));
|
||
}
|
||
}
|
||
|
||
/// A C-in-`cmn-12` pitch at the given octave, for a non-default
|
||
/// [`PitchRange`] (the generators never populate `Instrument.range`).
|
||
fn cmn_c(octave: i8) -> Pitch {
|
||
Pitch {
|
||
scale_position: ScalePosition {
|
||
space: PitchSpaceId::new("cmn-12"),
|
||
position: PitchSpacePosition::Cmn {
|
||
nominal: CmnNominal::C,
|
||
alteration: 0,
|
||
octave,
|
||
},
|
||
},
|
||
acoustic: AcousticPitch {
|
||
tuning: crate::pitch::TuningReference::Inherit,
|
||
realization: AcousticRealization::Implicit,
|
||
},
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn v1_round_trips_non_default_values_for_every_new_field() {
|
||
// The three schema-major-1 fields must survive a v1 round-trip at
|
||
// *non-default* values — not just vacuously when they equal the default
|
||
// (which is all the generators produce). This is what makes them real
|
||
// wire content rather than always-default padding.
|
||
let mut score = valid_score(5);
|
||
assert!(!score.instruments.is_empty() && !score.canvas.regions.is_empty());
|
||
|
||
let ss = |v: f64| CanonicalF64::new(v).expect("finite");
|
||
let custom = CanvasLayoutDefaults {
|
||
// US Letter (216 × 279.4 mm) in staff spaces, distinct margins.
|
||
page_size: CanvasSize {
|
||
width: ss(216.0),
|
||
height: ss(279.4),
|
||
},
|
||
margins: CanvasMargins {
|
||
top: ss(9.0),
|
||
right: ss(10.0),
|
||
bottom: ss(11.0),
|
||
left: ss(12.0),
|
||
},
|
||
};
|
||
assert_ne!(custom, CanvasLayoutDefaults::default());
|
||
score.canvas.layout_defaults = custom;
|
||
score.canvas.regions[0].permits_spanning_slurs = true;
|
||
score.instruments[0].range = Some(PitchRange {
|
||
lowest: cmn_c(2),
|
||
highest: cmn_c(6),
|
||
});
|
||
|
||
let bytes = score.canonical_bytes();
|
||
assert_eq!(Score::decode_canonical(&bytes).unwrap(), score);
|
||
assert_eq!(Score::decode_canonical_versioned(&bytes, 3).unwrap(), score);
|
||
// The non-default major-1 values are representable at the frozen v1
|
||
// wire form too: the v1 encoding migrates back to the same score
|
||
// (its major-2 and major-3 fields are all defaults here).
|
||
let v1 = encode_v1_score(&score);
|
||
assert_eq!(Score::decode_canonical_versioned(&v1, 1).unwrap(), score);
|
||
}
|
||
|
||
#[test]
|
||
fn v0_score_migrates_default_filling_all_three_new_fields() {
|
||
// Schema major 1 grew three fields — Canvas.layout_defaults,
|
||
// Instrument.range, Region.permits_spanning_slurs — so a major-0 score
|
||
// has none of them, and migrate-on-read must reconstruct each with its
|
||
// canonical default. We synthesize *genuine* v0 bytes via a mirror v0
|
||
// encoder (the byte-exact inverse of decode_v0_score) and check the
|
||
// versioned decoder rebuilds the original score, whose three fields ARE
|
||
// the defaults.
|
||
let ld_len = {
|
||
let mut b = Vec::new();
|
||
CanvasLayoutDefaults::default().enc(&mut b);
|
||
b.len()
|
||
};
|
||
for seed in 0..64u64 {
|
||
let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1));
|
||
// Precondition: the generator produces the v0-equivalent defaults, so
|
||
// the original IS what a v0→v1 migration should reconstruct.
|
||
assert_eq!(
|
||
score.canvas.layout_defaults,
|
||
CanvasLayoutDefaults::default()
|
||
);
|
||
assert!(score.instruments.iter().all(|i| i.range.is_none()));
|
||
assert!(score
|
||
.canvas
|
||
.regions
|
||
.iter()
|
||
.all(|r| !r.permits_spanning_slurs));
|
||
|
||
let current = score.canonical_bytes();
|
||
let v1 = encode_v1_score(&score);
|
||
let v0 = encode_v0_score(&score);
|
||
// Size anchor (independent of the v0 encoder's field order): v0 is
|
||
// exactly v1 minus the appended default bytes — layout_defaults once,
|
||
// plus one byte each for every instrument's range=None (Option tag)
|
||
// and every region's permits_spanning_slurs=false (bool).
|
||
let expected_removed = ld_len + score.instruments.len() + score.canvas.regions.len();
|
||
assert_eq!(
|
||
v1.len() - v0.len(),
|
||
expected_removed,
|
||
"v0 omits exactly the three new fields' default bytes"
|
||
);
|
||
|
||
// Major 0: the shorter v0 bytes migrate up, default-filling all three
|
||
// (and, composed, every schema-major-2 default, and — composed
|
||
// further — the schema-major-3 `smufl`/`overrides` defaults).
|
||
let migrated = Score::decode_canonical_versioned(&v0, 0).unwrap();
|
||
assert_eq!(migrated, score);
|
||
// The migrated score re-encodes to the production (major-3) bytes.
|
||
assert_eq!(migrated.canonical_bytes(), current);
|
||
// Major 1: the frozen v1 bytes migrate to the same score.
|
||
assert_eq!(Score::decode_canonical_versioned(&v1, 1).unwrap(), score);
|
||
// Major 3: the current bytes decode unchanged.
|
||
assert_eq!(
|
||
Score::decode_canonical_versioned(¤t, 3).unwrap(),
|
||
score
|
||
);
|
||
}
|
||
// A major outside {0, 1, 2, 3} is a defensive decode error (the gate
|
||
// rejects it upstream in practice).
|
||
assert!(Score::decode_canonical_versioned(&valid_score(1).canonical_bytes(), 4).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn v1_score_migrates_default_filling_the_major_2_fields() {
|
||
// Schema major 2 filled nine type bodies. A major-1 score carries none
|
||
// of the appended fields; migrate-on-read reconstructs each at its
|
||
// canonical default (Binary Format §Schema Major 2 migration table).
|
||
// Genuine v1 bytes come from the mirror v1 encoder; the size anchor
|
||
// pins that v1 omits exactly the appended default bytes, so the frozen
|
||
// encoder cannot silently drift from the pre-major-2 wire form.
|
||
for seed in 0..64u64 {
|
||
let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(3));
|
||
let current = score.canonical_bytes();
|
||
let v1 = encode_v1_score(&score);
|
||
|
||
let c = &score.cross_cutting;
|
||
let override_sites: usize = score
|
||
.canvas
|
||
.regions
|
||
.iter()
|
||
.flat_map(|r| r.content.staff_instances())
|
||
.filter(|si| si.staff_lines_override.is_some())
|
||
.count();
|
||
// Appended default bytes per component: slur 4 (kind 1 +
|
||
// curvature presence 1 + style line 1 + thickness presence 1);
|
||
// tie 2 (style); beam 5 (sub_beams count 4 + geometry presence 1);
|
||
// spanner 3 (kind tag 1 + style 2); repeat 9 (SimpleRepeat tag 1 +
|
||
// count 4 + voltas count 4); staff 17 (clef 3 + config appends
|
||
// 14 = spacing 12 + style 1 + bracket 1); instrument 28
|
||
// (abbreviation 1 + sound count 4 + transposition 1 + clef 3 +
|
||
// full config 15 + members count 4); an overridden staff-instance
|
||
// config 14; metadata 23 (three presence bytes + two i64
|
||
// timestamps + additional count 4); plus a flat 12
|
||
// (schema-major-3 `tuning_context.smufl`/`overrides`: `smufl` is
|
||
// two bare `SmuflVersion`s = 4 x u16 = 8, `overrides` is an empty
|
||
// Vec = a bare u32 count = 4 — present at every score regardless
|
||
// of content, since `v1` is frozen at the pre-major-3 3-field
|
||
// `tuning_context` while `current` is now major 3).
|
||
let expected_removed = c.slurs.len() * 4
|
||
+ c.ties.len() * 2
|
||
+ c.beams.len() * 5
|
||
+ c.spanners.len() * 3
|
||
+ c.repeats.len() * 9
|
||
+ score.staves.len() * 17
|
||
+ score.instruments.len() * 28
|
||
+ override_sites * 14
|
||
+ 23
|
||
+ 12;
|
||
assert_eq!(
|
||
current.len() - v1.len(),
|
||
expected_removed,
|
||
"v1 omits exactly the appended major-2 default bytes (plus the flat \
|
||
major-3 tuning-context default bytes, present regardless of content)"
|
||
);
|
||
|
||
let migrated = Score::decode_canonical_versioned(&v1, 1).unwrap();
|
||
assert_eq!(migrated, score);
|
||
assert_eq!(migrated.canonical_bytes(), current);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn v2_score_migrates_default_filling_smufl_and_overrides() {
|
||
// Schema major 3 (Push 4b tranche 3b-i, `spec/CONTRACT_PUSH4B_3BI_WIRE.md`)
|
||
// appends `tuning_context.smufl` and `.overrides`; a major-2 score
|
||
// carries neither, so migrate-on-read must reconstruct both at their
|
||
// canonical defaults. `accidental_extensions` is unaffected — it was
|
||
// never on the wire, at any major, so there is nothing for a v2->v3
|
||
// migration to default-fill for it. Genuine v2 bytes come from the
|
||
// mirror v2 encoder; the size anchor pins that v2 omits exactly the
|
||
// flat 12 default bytes (`smufl` 8 + `overrides` count 4), the same
|
||
// constant every score pays regardless of its other content, since
|
||
// the generator leaves `tuning_context` at its default.
|
||
for seed in 0..64u64 {
|
||
let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(5));
|
||
// Precondition: the generator leaves tuning_context's smufl/overrides
|
||
// at their defaults, so a v2->v3 migration reconstructing the
|
||
// default IS reconstructing the original.
|
||
assert_eq!(
|
||
score.tuning_context.smufl,
|
||
crate::accidental::SmuflVersionRequirement::default()
|
||
);
|
||
assert!(score.tuning_context.overrides.is_empty());
|
||
|
||
let current = score.canonical_bytes();
|
||
let v2 = encode_v2_score(&score);
|
||
assert_eq!(
|
||
current.len() - v2.len(),
|
||
12,
|
||
"v2 omits exactly the flat smufl(8)+overrides-count(4) default bytes"
|
||
);
|
||
|
||
let migrated = Score::decode_canonical_versioned(&v2, 2).unwrap();
|
||
assert_eq!(migrated, score);
|
||
assert_eq!(migrated.canonical_bytes(), current);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn current_major_round_trips_non_default_values_for_every_major_2_field() {
|
||
// One score carrying a non-default value for EVERY schema-major-2
|
||
// field (and every payload-carrying SpannerKind variant), so the live
|
||
// codec arms are exercised as real wire content, not always-default
|
||
// padding. Byte round-trip only — the standalone cross-cutting values
|
||
// reference fixture events for hygiene but invariants are not the
|
||
// subject here.
|
||
use crate::ids::{BeamId, RepeatStructureId, SlurId, SpannerId, TieId};
|
||
let mut score = valid_score(11);
|
||
let events: Vec<crate::ids::EventId> = score
|
||
.voices()
|
||
.flat_map(|(_, _, v)| v.events.clone())
|
||
.collect();
|
||
assert!(events.len() >= 2);
|
||
let ss = |v: f64| SpaceUnit(CanonicalF64::new(v).expect("finite"));
|
||
let anchor = |n: i64| crate::time::TimeAnchor::WallClock {
|
||
time: crate::time::WallClockTime(n),
|
||
};
|
||
|
||
score.cross_cutting.slurs.push(Slur {
|
||
id: SlurId::new(ReplicaId(9), 1),
|
||
start_event: events[0],
|
||
end_event: events[1],
|
||
kind: SlurKind::Phrase,
|
||
curvature_override: Some(CurvatureOverride {
|
||
direction: Some(CurveDirection::Below),
|
||
height: Some(ss(2.5)),
|
||
}),
|
||
style: SpanStyle {
|
||
line: LineStyle::Dashed,
|
||
thickness: Some(ss(0.25)),
|
||
},
|
||
});
|
||
score.cross_cutting.ties.push(Tie {
|
||
id: TieId::new(ReplicaId(9), 2),
|
||
start_event: events[0],
|
||
end_event: events[1],
|
||
pitch_pairing: None,
|
||
class: TieClass::Editorial,
|
||
style: SpanStyle {
|
||
line: LineStyle::Dotted,
|
||
thickness: None,
|
||
},
|
||
});
|
||
score.cross_cutting.beams.push(Beam {
|
||
id: BeamId::new(ReplicaId(9), 3),
|
||
events: events.clone(),
|
||
level: 1,
|
||
sub_beams: vec![SubBeam {
|
||
level: 2,
|
||
events: vec![events[0]],
|
||
}],
|
||
geometry_override: Some(BeamGeometryOverride {
|
||
slope: Some(CanonicalF64::new(0.5).expect("finite")),
|
||
offset: Some(ss(-1.0)),
|
||
}),
|
||
});
|
||
for (n, kind) in [
|
||
SpannerKind::Hairpin(HairpinDirection::Crescendo),
|
||
SpannerKind::OctaveLine(OctaveOffset(-1)),
|
||
SpannerKind::PedalLine(PedalKind::Sostenuto),
|
||
SpannerKind::TextLine(TextLineDefinition {
|
||
text: String::from("rit."),
|
||
}),
|
||
SpannerKind::Bracket(BracketKind::Square),
|
||
SpannerKind::TrillExtension,
|
||
]
|
||
.into_iter()
|
||
.enumerate()
|
||
{
|
||
score.cross_cutting.spanners.push(Spanner {
|
||
id: SpannerId::new(ReplicaId(9), 10 + n as u64),
|
||
start: anchor(0),
|
||
end: anchor(1000),
|
||
staves: score.staves.iter().map(|st| st.id).collect(),
|
||
kind,
|
||
style: SpanStyle {
|
||
line: LineStyle::Dashed,
|
||
thickness: Some(ss(0.1)),
|
||
},
|
||
});
|
||
}
|
||
score.cross_cutting.repeats.push(RepeatStructure {
|
||
id: RepeatStructureId::new(ReplicaId(9), 20),
|
||
start: anchor(0),
|
||
end: anchor(500),
|
||
kind: RepeatKind::DalSegno {
|
||
segno: anchor(10),
|
||
end_target: anchor(400),
|
||
},
|
||
voltas: vec![Volta {
|
||
endings: vec![1, 2],
|
||
start: anchor(300),
|
||
end: anchor(500),
|
||
}],
|
||
});
|
||
// Every RepeatKind wire arm as real content: DaCapo (tag 1) and the
|
||
// payload-less Volta (tag 3); SimpleRepeat is the migration default
|
||
// exercised everywhere else.
|
||
score.cross_cutting.repeats.push(RepeatStructure {
|
||
id: RepeatStructureId::new(ReplicaId(9), 21),
|
||
start: anchor(500),
|
||
end: anchor(700),
|
||
kind: RepeatKind::DaCapo {
|
||
end_target: anchor(600),
|
||
},
|
||
voltas: vec![],
|
||
});
|
||
score.cross_cutting.repeats.push(RepeatStructure {
|
||
id: RepeatStructureId::new(ReplicaId(9), 22),
|
||
start: anchor(700),
|
||
end: anchor(900),
|
||
kind: RepeatKind::Volta,
|
||
voltas: vec![Volta {
|
||
endings: vec![3],
|
||
start: anchor(800),
|
||
end: anchor(900),
|
||
}],
|
||
});
|
||
|
||
score.staves[0].default_clef = Clef::bass();
|
||
score.staves[0].default_staff_lines = StaffLineConfiguration {
|
||
line_count: 1,
|
||
line_spacing: ss(0.6),
|
||
line_style: LineStyle::Dotted,
|
||
bracket: Some(StaffBracketKind::Brace),
|
||
};
|
||
let si = score.canvas.regions[0]
|
||
.content
|
||
.staff_instances_mut()
|
||
.expect("staff-based region")
|
||
.first_mut()
|
||
.expect("an instance");
|
||
si.staff_lines_override = Some(StaffLineConfiguration {
|
||
line_count: 4,
|
||
line_spacing: ss(0.8),
|
||
line_style: LineStyle::Dashed,
|
||
bracket: Some(StaffBracketKind::Bracket),
|
||
});
|
||
|
||
score.instruments[0].abbreviation = Some(String::from("Vln."));
|
||
score.instruments[0].sound_config = SoundConfiguration(vec![1, 2, 3]);
|
||
score.instruments[0].transposition = Some(TranspositionInterval {
|
||
diatonic_steps: -1,
|
||
chromatic_steps: -2,
|
||
});
|
||
score.instruments[0].default_clef = Clef::alto();
|
||
score.instruments[0].default_staff_lines = StaffLineConfiguration {
|
||
line_count: 5,
|
||
line_spacing: ss(1.2),
|
||
line_style: LineStyle::Solid,
|
||
bracket: None,
|
||
};
|
||
score.instruments[0].unpitched_members = vec![UnpitchedMember {
|
||
member: UnpitchedMemberId(3),
|
||
name: String::from("snare"),
|
||
staff_position: StaffPosition(2),
|
||
}];
|
||
|
||
score.metadata.subtitle = Some(String::from("a subtitle"));
|
||
score.metadata.lyricist = Some(String::from("a lyricist"));
|
||
score.metadata.arranger = Some(String::from("an arranger"));
|
||
score.metadata.creation_timestamp = Timestamp(1_700_000_000_000_000_000);
|
||
score.metadata.modification_timestamp = Timestamp(1_700_000_100_000_000_000);
|
||
score.metadata.additional = vec![
|
||
MetadataEntry {
|
||
key: String::from("opus"),
|
||
value: MetadataValue::Integer(27),
|
||
},
|
||
MetadataEntry {
|
||
key: String::from("dedication"),
|
||
value: MetadataValue::Text(String::from("f\u{fc}r Elise")),
|
||
},
|
||
MetadataEntry {
|
||
key: String::from("urtext"),
|
||
value: MetadataValue::Flag(true),
|
||
},
|
||
];
|
||
|
||
let bytes = score.canonical_bytes();
|
||
assert_eq!(Score::decode_canonical(&bytes).unwrap(), score);
|
||
assert_eq!(Score::decode_canonical_versioned(&bytes, 3).unwrap(), score);
|
||
// The per-value seam carries the filled bodies too.
|
||
let slur = &score.cross_cutting.slurs[0];
|
||
assert_eq!(
|
||
Slur::decode_canonical(&slur.canonical_bytes()).unwrap(),
|
||
*slur
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn v0_regions_inside_canvas_decode_after_region_grew() {
|
||
// The nested-Vec case the struct decoder exists for: multiple v0 Region
|
||
// elements inside Canvas.regions must each decode and default-fill
|
||
// permits_spanning_slurs. A wrong per-element size would desync the Vec
|
||
// after the first element (every later region would misparse), so a
|
||
// multi-region canvas is the discriminating fixture — valid_score_rich
|
||
// carries three regions.
|
||
let score = valid_score_rich(9);
|
||
assert!(
|
||
score.canvas.regions.len() >= 2,
|
||
"need a multi-region canvas to exercise the Vec walk"
|
||
);
|
||
let v0 = encode_v0_score(&score);
|
||
let migrated = Score::decode_canonical_versioned(&v0, 0).unwrap();
|
||
assert_eq!(migrated, score);
|
||
assert_eq!(migrated.canvas.regions.len(), score.canvas.regions.len());
|
||
assert!(migrated
|
||
.canvas
|
||
.regions
|
||
.iter()
|
||
.all(|r| !r.permits_spanning_slurs));
|
||
}
|
||
|
||
#[test]
|
||
fn v0_decode_is_strictly_canonical_over_the_v0_wire_form() {
|
||
// The major-0 migration is strict, like the major-1 decoder: a canonical
|
||
// v0 encoding is accepted and re-encodes to itself in the frozen v0 form
|
||
// (`decode_v0_score` compares against `encode_v0_score`), while trailing
|
||
// or truncated bytes are rejected. This closes the injectivity gap for
|
||
// major-0 snapshots (content-addressed like major-1 ones).
|
||
for seed in 0..32u64 {
|
||
let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1));
|
||
let v0 = encode_v0_score(&score);
|
||
// Canonical v0 is accepted and re-encodes to the same v0 bytes.
|
||
let migrated = Score::decode_canonical_versioned(&v0, 0).unwrap();
|
||
assert_eq!(encode_v0_score(&migrated), v0);
|
||
// Trailing garbage is rejected.
|
||
let mut trailed = v0.clone();
|
||
trailed.push(0);
|
||
assert!(Score::decode_canonical_versioned(&trailed, 0).is_err());
|
||
// A truncation is rejected.
|
||
if !v0.is_empty() {
|
||
assert!(Score::decode_canonical_versioned(&v0[..v0.len() - 1], 0).is_err());
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn distinct_scores_serialize_differently() {
|
||
assert_ne!(
|
||
valid_score(1).canonical_bytes(),
|
||
valid_score(2).canonical_bytes(),
|
||
"distinct scores must produce distinct canonical bytes"
|
||
);
|
||
// The rich generator differs from the simple one, too.
|
||
assert_ne!(
|
||
valid_score(3).canonical_bytes(),
|
||
valid_score_rich(3).canonical_bytes()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn degenerate_tuplet_ratio_is_rejected_on_decode() {
|
||
// Guards the TupletRatio::dec re-validation (Pass 11 item 3.5): a
|
||
// hand-crafted byte stream must not be able to inject a degenerate ratio
|
||
// that TupletRatio::new would reject at construction. Without the
|
||
// `.ok_or(Reconstruct)` in dec, these would decode into an
|
||
// unconstructible-by-API value.
|
||
let decode = |actual: u32, notated: u32| {
|
||
let mut bytes = Vec::new();
|
||
actual.enc(&mut bytes);
|
||
notated.enc(&mut bytes);
|
||
TupletRatio::dec(&mut Reader::new(&bytes))
|
||
};
|
||
for (a, n) in [(0u32, 0u32), (2, 0), (0, 2), (4, 4)] {
|
||
assert!(
|
||
matches!(decode(a, n), Err(ScoreDecodeError::Reconstruct(_))),
|
||
"degenerate ratio {a}:{n} must be rejected on decode"
|
||
);
|
||
}
|
||
// A well-formed ratio still decodes.
|
||
let ok = decode(3, 2).expect("non-degenerate ratio decodes");
|
||
assert_eq!((ok.actual(), ok.notated()), (3, 2));
|
||
}
|
||
|
||
#[test]
|
||
fn exotic_event_and_pitch_variants_round_trip() {
|
||
// Round-trip is structural, so this need not satisfy graph invariants —
|
||
// it exists to exercise every event/pitch variant the generators omit.
|
||
let mut score = valid_score(0xE0E0);
|
||
let voice = score.canvas.regions[0].staff_instances()[0].voices[0].id;
|
||
let replica = score.identity.replica_id;
|
||
let mut next = 9_000u64;
|
||
let mut mint = || {
|
||
let id = EventId::new(replica, next);
|
||
next += 1;
|
||
id
|
||
};
|
||
let pos = EventPosition::Musical(crate::time::MusicalPosition(
|
||
crate::time::RationalTime::from_int(40),
|
||
));
|
||
let dur = EventDuration::Musical(crate::time::MusicalDuration(
|
||
crate::time::RationalTime::from_int(1),
|
||
));
|
||
|
||
score
|
||
.events
|
||
.insert(Event::Rest(Rest {
|
||
id: mint(),
|
||
voice,
|
||
position: pos.clone(),
|
||
duration: dur.clone(),
|
||
vertical_position: Some(crate::event::StaffPosition(-3)),
|
||
visible: false,
|
||
}))
|
||
.expect("rest inserts");
|
||
score
|
||
.events
|
||
.insert(Event::Unpitched(UnpitchedEvent {
|
||
id: mint(),
|
||
voice,
|
||
position: pos.clone(),
|
||
duration: dur.clone(),
|
||
staff_position: crate::event::StaffPosition(2),
|
||
instrument_member: crate::event::UnpitchedMemberId(7),
|
||
articulations: vec![crate::event::ArticulationMark],
|
||
dynamic: Some(crate::event::DynamicMark),
|
||
stem: crate::event::StemConfiguration,
|
||
grace: Some(GraceKind::MeasuredFraction(crate::time::MusicalDuration(
|
||
crate::time::RationalTime::new(1, 8).unwrap(),
|
||
))),
|
||
}))
|
||
.expect("unpitched inserts");
|
||
score
|
||
.events
|
||
.insert(Event::Indeterminate(IndeterminateEvent {
|
||
id: mint(),
|
||
voice,
|
||
position: pos,
|
||
duration: EventDuration::Indeterminate(crate::time::DurationBounds {
|
||
lower: Some(crate::time::ConcreteDuration::Musical(
|
||
crate::time::MusicalDuration(crate::time::RationalTime::from_int(1)),
|
||
)),
|
||
upper: None,
|
||
}),
|
||
indeterminacy: IndeterminacyKind::Compound(vec![
|
||
IndeterminacyKind::Pitch,
|
||
IndeterminacyKind::Duration,
|
||
]),
|
||
hints: IndeterminacyHints {
|
||
duration_bounds: None,
|
||
alternatives: vec![EventId::new(replica, 1)],
|
||
textual_instruction: Some("ad lib.".to_owned()),
|
||
},
|
||
}))
|
||
.expect("indeterminate inserts");
|
||
|
||
// A spelling attachment exercising the pitch-spelling tree.
|
||
score.spelling_attachments.push(SpellingAttachment {
|
||
scope: SpellingScope::Pitch(crate::ids::PitchId::new(replica, 1)),
|
||
directive: SpellingDirective::Explicit(PitchSpelling {
|
||
nominal: SpellingNominal::Cmn(CmnNominal::F),
|
||
accidentals: vec![AccidentalId::new("sharp")],
|
||
octave: 4,
|
||
render_hints: SpellingRenderHints {
|
||
parenthesized: true,
|
||
cautionary: false,
|
||
editorial: true,
|
||
small_print: false,
|
||
},
|
||
}),
|
||
source: SpellingSource::Imported {
|
||
format: ForeignFormatId::new("musicxml"),
|
||
},
|
||
priority: -2,
|
||
layer: None,
|
||
});
|
||
|
||
// Exotic metadata strings (preserved byte-exactly).
|
||
score.metadata.title = Some("Étude café".to_owned());
|
||
score.metadata.composer = Some("composer".to_owned());
|
||
|
||
assert_round_trips(&score);
|
||
}
|
||
|
||
#[test]
|
||
fn trailing_and_truncated_bytes_are_rejected() {
|
||
let bytes = valid_score(7).canonical_bytes();
|
||
|
||
let mut trailing = bytes.clone();
|
||
trailing.push(0);
|
||
assert_eq!(
|
||
Score::decode_canonical(&trailing),
|
||
Err(ScoreDecodeError::TrailingBytes)
|
||
);
|
||
|
||
let truncated = &bytes[..bytes.len() - 1];
|
||
assert!(
|
||
Score::decode_canonical(truncated).is_err(),
|
||
"a truncated score must be rejected"
|
||
);
|
||
|
||
// An empty buffer is not a score.
|
||
assert!(Score::decode_canonical(&[]).is_err());
|
||
}
|
||
}
|