2540 lines
78 KiB
Rust
2540 lines
78 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::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, BeatGroup, Canvas,
|
|
ChordSymbol, Clef, ClefChange, ClefShape, Comment, CrossCuttingRegistry,
|
|
DecompositionAttachment, DecompositionSource, EventOrderingDAG, GestureAnchoring,
|
|
GraphicContent, GraphicGesture, GraphicObject, Instrument, KeySignature, KeySignatureChange,
|
|
LyricLine, Marker, Measure, MeasureNumberVisibility, MeterChange, MetricGrid, MetricTimeModel,
|
|
NotatedComponent, NoteValue, PartDefinition, PowerOfTwo, ProportionalTimeModel, Region,
|
|
RegionContent, RegionTimeModel, RepeatStructure, Score, ScoreMetadata, ScoreTuningContext,
|
|
Slur, Spanner, Staff, StaffBasedContent, StaffExtent, StaffGroup, StaffGroupKind,
|
|
StaffInstance, StaffLineConfiguration, StemDirection, TempoMapReference, Tie, TieClass,
|
|
TimeExtent, TimeSignature, TimeSignatureDisplay, Tuplet, TupletRatio, ViewDefinition, Voice,
|
|
VoiceOrigin,
|
|
};
|
|
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, PitchSpaceId, PitchSpacePosition, PitchSpelling, PositionRegistryId,
|
|
ReferencePitch, ScalePosition, SpellingAttachment, SpellingDirective, SpellingNominal,
|
|
SpellingPrecedence, SpellingRenderHints, SpellingRule, SpellingRuleSetId, SpellingScope,
|
|
SpellingSource, SpellingSourceKind, StaffGroupKindRegistryId, TieClassRegistryId,
|
|
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,
|
|
};
|
|
|
|
// ===========================================================================
|
|
// 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> {
|
|
usize::try_from(self.u32()?).map_err(|_| ScoreDecodeError::LengthOverflow)
|
|
}
|
|
|
|
/// 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 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),* })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// [`Codec`] for a zero-field unit struct: no bytes.
|
|
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)
|
|
}
|
|
}
|
|
)*
|
|
};
|
|
}
|
|
|
|
/// [`Codec`] for a fieldless ("C-like") enum: a single discriminant byte.
|
|
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 }),
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// [`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)?))
|
|
}
|
|
}
|
|
)*
|
|
};
|
|
}
|
|
|
|
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
|
|
});
|
|
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,
|
|
});
|
|
|
|
struct_codec!(ScoreMetadata {
|
|
title,
|
|
composer,
|
|
copyright
|
|
});
|
|
struct_codec!(Instrument { id, name });
|
|
struct_codec!(StaffLineConfiguration { line_count });
|
|
struct_codec!(GraphicObject { id });
|
|
struct_codec!(GraphicContent { objects });
|
|
struct_codec!(TimeExtent { start, end });
|
|
struct_codec!(StaffExtent { staves });
|
|
struct_codec!(Staff {
|
|
id,
|
|
name,
|
|
abbreviation,
|
|
instrument,
|
|
default_staff_lines,
|
|
group
|
|
});
|
|
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
|
|
});
|
|
struct_codec!(ScoreTuningContext {
|
|
default_pitch_space,
|
|
default_tuning_system,
|
|
reference
|
|
});
|
|
struct_codec!(Slur {
|
|
id,
|
|
start_event,
|
|
end_event
|
|
});
|
|
struct_codec!(Beam { id, events, level });
|
|
// 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
|
|
});
|
|
struct_codec!(Marker { id, anchor });
|
|
struct_codec!(RepeatStructure { id, start, end });
|
|
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
|
|
});
|
|
struct_codec!(Region {
|
|
id,
|
|
time_model,
|
|
content,
|
|
time_extent,
|
|
staff_extent,
|
|
local_tempo_map
|
|
});
|
|
struct_codec!(Canvas { regions });
|
|
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
|
|
});
|
|
|
|
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 1)** layout. To decode bytes whose
|
|
/// schema major is not known to be current, use
|
|
/// [`Score::decode_canonical_versioned`].
|
|
pub fn decode_canonical(bytes: &[u8]) -> Result<Score> {
|
|
let mut r = Reader::new(bytes);
|
|
let score = Score::dec(&mut r)?;
|
|
r.finish()?;
|
|
Ok(score)
|
|
}
|
|
|
|
/// The **schema-version dispatch seam** (Binary Format companion
|
|
/// §"Schema Major 1"): decodes a full-`Score` snapshot whose bytes were
|
|
/// written under the given schema `major`, migrating a major-0 encoding up
|
|
/// to the current in-memory form on read. Major 1 is the current layout
|
|
/// ([`Score::decode_canonical`]); major 0 is decoded through the frozen v0
|
|
/// wire form (`decode_v0_score`) and then migrated (`migrate_v0_score`).
|
|
///
|
|
/// At schema major 1's introduction the v0 and v1 layouts are **identical**,
|
|
/// so the v0 path is the identity today — the machinery is a behavioral
|
|
/// no-op. When a later phase grows a field on `Canvas`, `Instrument`, or
|
|
/// `Region`, the frozen pre-field decoder is added here (reading the old
|
|
/// layout) and `migrate_v0_score` default-fills the new field.
|
|
///
|
|
/// 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}`
|
|
/// is a defensive error, not an expected path.
|
|
pub fn decode_canonical_versioned(bytes: &[u8], major: u16) -> Result<Score> {
|
|
match major {
|
|
1 => Score::decode_canonical(bytes),
|
|
0 => {
|
|
let v0 = decode_v0_score(bytes)?;
|
|
Ok(migrate_v0_score(v0))
|
|
}
|
|
_ => Err(ScoreDecodeError::InvalidValue("unsupported schema major")),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Decodes a `Score` from its **frozen schema-major-0** wire bytes.
|
|
///
|
|
/// At major 1's introduction the v0 layout equals the current layout, so this
|
|
/// delegates to [`Score::decode_canonical`]. **Phase C/D freeze this by value:**
|
|
/// when `Canvas`/`Instrument`/`Region` grow a field, this function is replaced
|
|
/// by a copy of the pre-field positional decoder (reading the old layout), and a
|
|
/// golden v0 byte fixture guards it against drift — so the migrate-on-read path
|
|
/// decodes real historical bytes, not the current layout in disguise.
|
|
fn decode_v0_score(bytes: &[u8]) -> Result<Score> {
|
|
Score::decode_canonical(bytes)
|
|
}
|
|
|
|
/// Migrates a `Score` decoded from schema major 0 up to the current in-memory
|
|
/// form. Total and default-filling (no score context needed).
|
|
///
|
|
/// Identity today (v0 layout == v1 layout). **Phase C/D fill the new fields
|
|
/// here:** `Canvas.layout_defaults` = the A4/8 mm default, `Instrument.range`
|
|
/// = `None`, `Region.permits_spanning_slurs` = `false`.
|
|
fn migrate_v0_score(score: Score) -> Score {
|
|
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 only for the value types operation payloads embed, not for every
|
|
/// `Codec` type, to keep the public surface intentional.
|
|
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()?;
|
|
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,
|
|
}
|
|
|
|
#[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 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),
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn versioned_decode_is_identity_across_majors_at_major_1_introduction() {
|
|
// The schema-version dispatch seam (Binary Format §"Schema Major 1"):
|
|
// at major 1's introduction the v0 and v1 layouts are identical, so
|
|
// decoding the same bytes under either major yields the same score as
|
|
// the unversioned decoder. This pins the machinery as a behavioral
|
|
// no-op; a later phase that grows a field flips the major-0 path to a
|
|
// real frozen decode + default-fill, and this test moves with it.
|
|
for seed in 0..64u64 {
|
|
let score = valid_score(seed.wrapping_mul(0x9E37_79B9).wrapping_add(1));
|
|
let bytes = score.canonical_bytes();
|
|
let via_current = Score::decode_canonical(&bytes).expect("decodes");
|
|
let via_v1 = Score::decode_canonical_versioned(&bytes, 1).expect("major 1 decodes");
|
|
let via_v0 = Score::decode_canonical_versioned(&bytes, 0).expect("major 0 decodes");
|
|
assert_eq!(via_current, via_v1, "major 1 == unversioned");
|
|
assert_eq!(
|
|
via_current, via_v0,
|
|
"major 0 (identity migration) == unversioned"
|
|
);
|
|
}
|
|
// A major outside the accept-set is a defensive decode error (the gate
|
|
// rejects it upstream in practice).
|
|
assert!(Score::decode_canonical_versioned(&valid_score(1).canonical_bytes(), 2).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());
|
|
}
|
|
}
|