Skip to main content

acorde_soundfont/
lib.rs

1//! A bounded, provider-neutral boundary between `acorde` playback events and a
2//! SoundFont renderer.
3//!
4//! This crate validates SF2/SF3 metadata and produces lifecycle actions. It
5//! intentionally does not decode samples or ship a synthesizer; applications
6//! choose a licensed renderer on the other side of this stable boundary.
7
8use acorde_core::PlaybackEvent;
9use serde::Serialize;
10
11pub const PLAYBACK_CONTRACT_VERSION: u16 = 1;
12/// Version of the provider capability/decoder/renderer adapter contract.
13pub const PROVIDER_CONTRACT_VERSION: u16 = 1;
14pub const MAX_ASSET_BYTES: usize = 64 * 1024 * 1024;
15pub const MAX_PRESETS: usize = 16_384;
16/// Maximum number of provider-materialized zones retained in one snapshot.
17pub const MAX_MATERIALIZED_ZONES: usize = 65_536;
18/// Maximum number of non-fatal provider diagnostics retained in one snapshot.
19pub const MAX_ZONE_DIAGNOSTICS: usize = 4_096;
20pub const MAX_POLYPHONY: usize = 256;
21pub const MAX_DECODED_FRAMES: usize = 16_000_000;
22
23/// Generator names understood by the provider-neutral zone contract.
24pub const SUPPORTED_GENERATORS: &[&str] = &[
25    "keyRange",
26    "velRange",
27    "overridingRootKey",
28    "fineTune",
29    "initialAttenuation",
30    "sampleModes",
31    "startAddrsOffset",
32    "endAddrsOffset",
33    "startloopAddrsOffset",
34    "endloopAddrsOffset",
35    "instrument",
36    "sampleID",
37    "releaseVolEnv",
38];
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
41pub enum SoundFontFormat {
42    Sf2,
43    Sf3,
44}
45
46/// Compression handled by a provider. Decoding remains outside this crate.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
48pub enum SampleCompression {
49    Pcm16,
50    Vorbis,
51}
52
53/// Capabilities advertised by a separately licensed SoundFont provider.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct ProviderCapabilities {
56    pub sf2_pcm16: bool,
57    pub sf3_vorbis: bool,
58    pub synthesis: bool,
59}
60
61impl ProviderCapabilities {
62    pub const fn metadata_only() -> Self {
63        Self {
64            sf2_pcm16: false,
65            sf3_vorbis: false,
66            synthesis: false,
67        }
68    }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
72pub struct SampleLoop {
73    pub start_frame: u32,
74    pub end_frame: u32,
75}
76
77/// The channel represented by one member of a linked stereo sample pair.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
79#[serde(rename_all = "snake_case")]
80pub enum StereoChannel {
81    Left,
82    Right,
83}
84
85/// PCM layout needed to decode one materialized SoundFont sample region.
86///
87/// SF2 stores linked stereo samples as two independent mono ranges.  SF3 may
88/// instead store one interleaved Ogg/Vorbis stream.  A host can therefore use
89/// [`ResolvedPresetZoneMetadata::decode_channels`] without reparsing RIFF
90/// tables, while `LinkedStereo` identifies the other mono region to combine.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
92#[serde(rename_all = "snake_case", tag = "kind")]
93pub enum SampleChannelLayout {
94    Mono,
95    LinkedStereo {
96        channel: StereoChannel,
97        linked_sample_id: u64,
98    },
99    InterleavedStereo,
100}
101
102impl SampleChannelLayout {
103    /// Number of PCM channels carried by this region's own source payload.
104    pub const fn decode_channels(self) -> u8 {
105        match self {
106            Self::InterleavedStereo => 2,
107            Self::Mono | Self::LinkedStereo { .. } => 1,
108        }
109    }
110}
111
112/// A provider-neutral SF2/SF3 sample region selected by key and velocity.
113#[derive(Debug, Clone, PartialEq, Serialize)]
114pub struct SampleRegion {
115    pub sample_id: u64,
116    /// Half-open source-frame range in the provider-owned sample payload.
117    pub start_frame: u32,
118    pub end_frame: u32,
119    pub key_min: u8,
120    pub key_max: u8,
121    pub velocity_min: u8,
122    pub velocity_max: u8,
123    pub root_key: u8,
124    pub fine_tune_cents: i16,
125    pub attenuation_db: f32,
126    pub sample_rate: u32,
127    pub compression: SampleCompression,
128    /// Asset-relative loop points. [`DecodedSampleRegion`] normalizes these for cropped PCM.
129    pub loop_points: Option<SampleLoop>,
130    pub attack_secs: f32,
131    pub decay_secs: f32,
132    pub sustain_level: f32,
133    pub release_secs: f32,
134}
135
136impl SampleRegion {
137    pub fn contains(&self, key: u8, velocity: u8) -> bool {
138        key >= self.key_min
139            && key <= self.key_max
140            && velocity >= self.velocity_min
141            && velocity <= self.velocity_max
142    }
143}
144
145/// Validate the bounded generator values needed to turn one zone into a playable voice.
146pub fn validate_sample_region(region: &SampleRegion) -> Result<(), Error> {
147    if region.key_min > region.key_max {
148        return Err(Error::InvalidZone("key range"));
149    }
150    if region.velocity_min == 0 || region.velocity_min > region.velocity_max {
151        return Err(Error::InvalidZone("velocity range"));
152    }
153    if region.start_frame >= region.end_frame
154        || u64::from(region.end_frame) > MAX_DECODED_FRAMES as u64
155    {
156        return Err(Error::InvalidZone("sample frame range"));
157    }
158    if region.sample_rate == 0
159        || !region.attenuation_db.is_finite()
160        || !region.attack_secs.is_finite()
161        || !region.decay_secs.is_finite()
162        || !region.sustain_level.is_finite()
163        || !region.release_secs.is_finite()
164        || region.attack_secs < 0.0
165        || region.decay_secs < 0.0
166        || region.release_secs < 0.0
167        || !(0.0..=1.0).contains(&region.sustain_level)
168    {
169        return Err(Error::InvalidZone("sample/envelope parameters"));
170    }
171    if let Some(loop_points) = region.loop_points
172        && (loop_points.start_frame >= loop_points.end_frame
173            || loop_points.start_frame < region.start_frame
174            || loop_points.end_frame > region.end_frame)
175    {
176        return Err(Error::InvalidZone("loop points"));
177    }
178    Ok(())
179}
180
181/// Reject a parsed SoundFont generator that this boundary cannot represent safely.
182pub fn validate_generator_name(name: &str) -> Result<(), Error> {
183    if SUPPORTED_GENERATORS.contains(&name) {
184        Ok(())
185    } else {
186        Err(Error::UnsupportedGenerator(name.to_string()))
187    }
188}
189
190/// Selects a region deterministically: narrower key/velocity zones win, then sample ID.
191pub fn select_sample_region(
192    regions: &[SampleRegion],
193    key: u8,
194    velocity: u8,
195) -> Option<&SampleRegion> {
196    regions
197        .iter()
198        .filter(|region| region.contains(key, velocity))
199        .min_by_key(|region| {
200            (
201                u16::from(region.key_max.saturating_sub(region.key_min)),
202                u16::from(region.velocity_max.saturating_sub(region.velocity_min)),
203                region.sample_id,
204            )
205        })
206}
207
208/// A provider-neutral mapping from one bank/program preset to a playable sample zone.
209///
210/// SF2 generators or SF3 metadata are interpreted by the provider and materialized here;
211/// consumers can then select zones without reimplementing SoundFont parsing.
212#[derive(Debug, Clone, PartialEq, Serialize)]
213pub struct SoundFontPresetZone {
214    pub bank: u16,
215    pub program: u16,
216    pub region: SampleRegion,
217    channel_layout: SampleChannelLayout,
218}
219
220impl SoundFontPresetZone {
221    pub fn new(bank: u16, program: u16, region: SampleRegion) -> Result<Self, Error> {
222        validate_sample_region(&region)?;
223        Ok(Self {
224            bank,
225            program,
226            region,
227            channel_layout: SampleChannelLayout::Mono,
228        })
229    }
230
231    /// Construct a zone with the source PCM layout resolved by a SoundFont provider.
232    pub fn with_channel_layout(
233        bank: u16,
234        program: u16,
235        region: SampleRegion,
236        channel_layout: SampleChannelLayout,
237    ) -> Result<Self, Error> {
238        validate_sample_region(&region)?;
239        Ok(Self {
240            bank,
241            program,
242            region,
243            channel_layout,
244        })
245    }
246
247    /// Source PCM layout for this region. `new` remains mono for compatibility.
248    pub const fn channel_layout(&self) -> SampleChannelLayout {
249        self.channel_layout
250    }
251
252    pub fn contains(&self, bank: u16, program: u16, key: u8, velocity: u8) -> bool {
253        self.bank == bank && self.program == program && self.region.contains(key, velocity)
254    }
255
256    /// Return an owned snapshot of the resolved zone for downstream hosts.
257    pub fn resolved_metadata(&self) -> ResolvedPresetZoneMetadata {
258        ResolvedPresetZoneMetadata::from_zone(self)
259    }
260}
261
262/// Fully resolved, provider-neutral metadata for one playable preset zone.
263///
264/// Providers interpret SF2 generators or SF3 metadata before constructing a
265/// [`SoundFontPresetZone`]. Downstream hosts can retain this owned snapshot for
266/// UI, diagnostics, cache keys, or audio setup without repeating that parsing.
267#[derive(Debug, Clone, PartialEq, Serialize)]
268pub struct ResolvedPresetZoneMetadata {
269    pub bank: u16,
270    pub program: u16,
271    pub sample_id: u64,
272    pub start_frame: u32,
273    pub end_frame: u32,
274    pub key_min: u8,
275    pub key_max: u8,
276    pub velocity_min: u8,
277    pub velocity_max: u8,
278    pub root_key: u8,
279    pub fine_tune_cents: i16,
280    pub attenuation_db: f32,
281    pub sample_rate: u32,
282    pub compression: SampleCompression,
283    /// Layout of the source payload; this determines [`Self::decode_channels`].
284    pub channel_layout: SampleChannelLayout,
285    /// Number of PCM channels to pass to [`decode_sample_region`].
286    pub decode_channels: u8,
287    pub loop_points: Option<SampleLoop>,
288    pub attack_secs: f32,
289    pub decay_secs: f32,
290    pub sustain_level: f32,
291    pub release_secs: f32,
292}
293
294impl ResolvedPresetZoneMetadata {
295    fn from_zone(zone: &SoundFontPresetZone) -> Self {
296        let region = &zone.region;
297        Self {
298            bank: zone.bank,
299            program: zone.program,
300            sample_id: region.sample_id,
301            start_frame: region.start_frame,
302            end_frame: region.end_frame,
303            key_min: region.key_min,
304            key_max: region.key_max,
305            velocity_min: region.velocity_min,
306            velocity_max: region.velocity_max,
307            root_key: region.root_key,
308            fine_tune_cents: region.fine_tune_cents,
309            attenuation_db: region.attenuation_db,
310            sample_rate: region.sample_rate,
311            compression: region.compression,
312            channel_layout: zone.channel_layout,
313            decode_channels: zone.channel_layout.decode_channels(),
314            loop_points: region.loop_points,
315            attack_secs: region.attack_secs,
316            decay_secs: region.decay_secs,
317            sustain_level: region.sustain_level,
318            release_secs: region.release_secs,
319        }
320    }
321
322    /// Reconstruct the validated provider-neutral region represented by this snapshot.
323    pub fn sample_region(&self) -> Result<SampleRegion, Error> {
324        let region = SampleRegion {
325            sample_id: self.sample_id,
326            start_frame: self.start_frame,
327            end_frame: self.end_frame,
328            key_min: self.key_min,
329            key_max: self.key_max,
330            velocity_min: self.velocity_min,
331            velocity_max: self.velocity_max,
332            root_key: self.root_key,
333            fine_tune_cents: self.fine_tune_cents,
334            attenuation_db: self.attenuation_db,
335            sample_rate: self.sample_rate,
336            compression: self.compression,
337            loop_points: self.loop_points,
338            attack_secs: self.attack_secs,
339            decay_secs: self.decay_secs,
340            sustain_level: self.sustain_level,
341            release_secs: self.release_secs,
342        };
343        validate_sample_region(&region).map(|()| region)
344    }
345
346    /// Decode this snapshot using its materialized channel count.
347    pub fn decode_sample_region(&self, data: &[u8]) -> Result<DecodedSampleRegion, Error> {
348        if self.decode_channels != self.channel_layout.decode_channels() {
349            return Err(Error::InvalidSample);
350        }
351        decode_materialized_sample_region(data, self.sample_region()?, self.decode_channels)
352    }
353}
354
355/// Select a preset zone deterministically for a bank/program/key/velocity tuple.
356pub fn select_preset_zone(
357    zones: &[SoundFontPresetZone],
358    bank: u16,
359    program: u16,
360    key: u8,
361    velocity: u8,
362) -> Option<&SoundFontPresetZone> {
363    zones
364        .iter()
365        .filter(|zone| zone.contains(bank, program, key, velocity))
366        .min_by_key(|zone| {
367            (
368                u16::from(zone.region.key_max.saturating_sub(zone.region.key_min)),
369                u16::from(
370                    zone.region
371                        .velocity_max
372                        .saturating_sub(zone.region.velocity_min),
373                ),
374                zone.region.sample_id,
375            )
376        })
377}
378
379/// Return every playable preset zone in deterministic order for a note request.
380///
381/// SoundFonts may intentionally layer overlapping key/velocity zones. The historical
382/// [`select_preset_zone`] helper remains the single-zone convenience path and returns the first
383/// entry from this ordering.
384pub fn select_preset_zones(
385    zones: &[SoundFontPresetZone],
386    bank: u16,
387    program: u16,
388    key: u8,
389    velocity: u8,
390) -> Vec<&SoundFontPresetZone> {
391    let mut matches: Vec<_> = zones
392        .iter()
393        .filter(|zone| zone.contains(bank, program, key, velocity))
394        .collect();
395    matches.sort_by_key(|zone| {
396        (
397            u16::from(zone.region.key_max.saturating_sub(zone.region.key_min)),
398            u16::from(
399                zone.region
400                    .velocity_max
401                    .saturating_sub(zone.region.velocity_min),
402            ),
403            zone.region.sample_id,
404        )
405    });
406    matches
407}
408
409/// Resolve and return owned metadata for a bank/program/key/velocity request.
410pub fn resolve_preset_zone_metadata(
411    zones: &[SoundFontPresetZone],
412    bank: u16,
413    program: u16,
414    key: u8,
415    velocity: u8,
416) -> Result<ResolvedPresetZoneMetadata, Error> {
417    select_preset_zone(zones, bank, program, key, velocity)
418        .map(SoundFontPresetZone::resolved_metadata)
419        .ok_or(Error::InvalidSample)
420}
421
422/// Select a bank/program/key/velocity zone and build its provider-neutral voice plan.
423///
424/// This is the single mapping entry point intended for Composer and other hosts:
425/// SF2/SF3 providers materialize [`SoundFontPresetZone`] values once, then callers
426/// do not need to duplicate preset or generator selection logic.
427pub fn schedule_preset_note_on(
428    voice_id: u64,
429    event: PlaybackEvent,
430    zones: &[SoundFontPresetZone],
431    bank: u16,
432    program: u16,
433    velocity_exponent: f32,
434) -> Result<SampleAction, Error> {
435    let zone = select_preset_zone(zones, bank, program, event.pitch_midi, event.velocity)
436        .ok_or(Error::InvalidSample)?;
437    schedule_sample_note_on(voice_id, event, &zone.region, velocity_exponent)
438}
439
440/// Build one provider-neutral note-on action for every matching layered preset zone.
441pub fn schedule_preset_note_ons(
442    voice_id: u64,
443    event: PlaybackEvent,
444    zones: &[SoundFontPresetZone],
445    bank: u16,
446    program: u16,
447    velocity_exponent: f32,
448) -> Result<Vec<SampleAction>, Error> {
449    let matches = select_preset_zones(zones, bank, program, event.pitch_midi, event.velocity);
450    if matches.is_empty() {
451        return Err(Error::InvalidSample);
452    }
453    matches
454        .into_iter()
455        .enumerate()
456        .map(|(layer, zone)| {
457            let layer_voice_id = voice_id
458                .checked_add(u64::try_from(layer).map_err(|_| Error::InvalidSample)?)
459                .ok_or(Error::InvalidSample)?;
460            schedule_sample_note_on(
461                layer_voice_id,
462                event.clone(),
463                &zone.region,
464                velocity_exponent,
465            )
466        })
467        .collect()
468}
469
470/// Converts MIDI velocity to a deterministic linear gain with a configurable exponent.
471pub fn velocity_gain(velocity: u8, exponent: f32) -> Result<f32, Error> {
472    if velocity == 0 || !exponent.is_finite() || exponent <= 0.0 {
473        return Err(Error::InvalidConfig);
474    }
475    Ok((f32::from(velocity) / 127.0).powf(exponent))
476}
477
478/// Validated PCM returned by a separately licensed decoder.
479#[derive(Debug, Clone, PartialEq, Eq)]
480pub struct DecodedSample {
481    pub sample_rate: u32,
482    pub channels: u8,
483    pub pcm_i16: Vec<i16>,
484}
485
486/// PCM decoded for one [`SampleRegion`].
487///
488/// `sample` is indexed from zero through `region.end_frame - region.start_frame`; therefore
489/// `loop_points` are buffer-relative as well. The original asset-relative coordinates remain in
490/// `region.loop_points`. This prevents a cropped non-zero source range from being rendered with
491/// loop coordinates from the full SoundFont payload.
492#[derive(Debug, Clone, PartialEq)]
493pub struct DecodedSampleRegion {
494    pub region: SampleRegion,
495    pub sample: DecodedSample,
496    pub loop_points: Option<SampleLoop>,
497}
498
499impl DecodedSampleRegion {
500    /// Construct a playable region from PCM already cropped to the region's half-open frame range.
501    pub fn new(region: SampleRegion, sample: DecodedSample) -> Result<Self, Error> {
502        validate_sample_region(&region)?;
503        if sample.sample_rate != region.sample_rate
504            || sample.frame_count()
505                != usize::try_from(region.end_frame - region.start_frame)
506                    .map_err(|_| Error::InvalidSample)?
507        {
508            return Err(Error::InvalidSample);
509        }
510        let loop_points = region.loop_points.map(|loop_points| SampleLoop {
511            start_frame: loop_points.start_frame - region.start_frame,
512            end_frame: loop_points.end_frame - region.start_frame,
513        });
514        Ok(Self {
515            region,
516            sample,
517            loop_points,
518        })
519    }
520
521    /// Crop full asset-relative decoded PCM into this region's buffer-relative contract.
522    pub fn from_full_sample(
523        region: SampleRegion,
524        full_sample: &DecodedSample,
525    ) -> Result<Self, Error> {
526        validate_sample_region(&region)?;
527        if full_sample.sample_rate != region.sample_rate {
528            return Err(Error::InvalidSample);
529        }
530        let start = usize::try_from(region.start_frame).map_err(|_| Error::InvalidSample)?;
531        let end = usize::try_from(region.end_frame).map_err(|_| Error::InvalidSample)?;
532        if end > full_sample.frame_count() {
533            return Err(Error::Truncated);
534        }
535        let channels = usize::from(full_sample.channels);
536        let samples = full_sample.pcm_i16[start * channels..end * channels].to_vec();
537        Self::new(
538            region,
539            DecodedSample::new(full_sample.sample_rate, full_sample.channels, samples)?,
540        )
541    }
542
543    /// Apply this region's buffer-relative loop coordinates to one scheduled action.
544    pub fn normalize_action(&self, action: &SampleAction) -> Result<SampleAction, Error> {
545        let mut normalized = action.clone();
546        match &mut normalized {
547            SampleAction::Start {
548                sample_id,
549                parameters,
550                ..
551            } if *sample_id == self.region.sample_id => {
552                parameters.loop_points = self.loop_points;
553                Ok(normalized)
554            }
555            SampleAction::Start { .. } => Err(Error::InvalidSample),
556            SampleAction::Stop { .. } => Ok(normalized),
557        }
558    }
559}
560
561/// A separately licensed provider that decodes a selected region into PCM.
562///
563/// Implementations own codec dependencies, asset access, and licensing. This
564/// crate only receives the bounded, validated PCM result.
565pub trait SampleDecoder {
566    type Error: std::error::Error + Send + Sync + 'static;
567
568    fn decode(&self, region: &SampleRegion) -> Result<DecodedSample, Self::Error>;
569}
570
571/// A host/provider-owned renderer for validated PCM and scheduled voice actions.
572///
573/// Audio output, device access, and backend-specific state remain outside the
574/// reusable score library while hosts retain a stable integration point.
575pub trait SampleRenderer {
576    type Error: std::error::Error + Send + Sync + 'static;
577
578    fn render(&mut self, sample: &DecodedSample, action: &SampleAction) -> Result<(), Self::Error>;
579}
580
581/// Complete adapter contract for an optional, separately licensed provider.
582///
583/// The provider owns SF2/SF3 parsing, codec dependencies, licensed assets, and audio output.
584/// A host can inspect [`Self::capabilities`] before selecting a region and receives the
585/// provider's explicit error for unsupported codec, generator, or license conditions.
586pub trait SoundFontProvider {
587    type Error: std::error::Error + Send + Sync + 'static;
588
589    fn capabilities(&self) -> ProviderCapabilities;
590    fn decode(&self, region: &SampleRegion) -> Result<DecodedSample, Self::Error>;
591    fn render(&mut self, sample: &DecodedSample, action: &SampleAction) -> Result<(), Self::Error>;
592}
593
594/// Validate that a region can be handed to the advertised provider capabilities.
595pub fn validate_provider_capabilities(
596    region: &SampleRegion,
597    capabilities: ProviderCapabilities,
598) -> Result<(), Error> {
599    let supported = match region.compression {
600        SampleCompression::Pcm16 => capabilities.sf2_pcm16,
601        SampleCompression::Vorbis => capabilities.sf3_vorbis,
602    };
603    if !supported {
604        return Err(Error::UnsupportedCompression(region.compression));
605    }
606    if !capabilities.synthesis {
607        return Err(Error::UnsupportedProviderCapability("synthesis"));
608    }
609    Ok(())
610}
611
612impl DecodedSample {
613    pub fn new(sample_rate: u32, channels: u8, pcm_i16: Vec<i16>) -> Result<Self, Error> {
614        if sample_rate == 0 || channels == 0 || channels > 2 || pcm_i16.is_empty() {
615            return Err(Error::InvalidSample);
616        }
617        let frames = pcm_i16.len() / usize::from(channels);
618        if frames == 0
619            || frames > MAX_DECODED_FRAMES
620            || !pcm_i16.len().is_multiple_of(usize::from(channels))
621        {
622            return Err(Error::SampleTooLarge(frames));
623        }
624        Ok(Self {
625            sample_rate,
626            channels,
627            pcm_i16,
628        })
629    }
630
631    /// Number of interleaved PCM frames after validation.
632    pub fn frame_count(&self) -> usize {
633        self.pcm_i16.len() / usize::from(self.channels)
634    }
635}
636
637/// Decode one validated SoundFont region into PCM with buffer-relative loop coordinates.
638///
639/// SF2 reads exactly the region's source-frame interval. Feature-enabled SF3 first decodes the
640/// Ogg payload and then applies the same bounded crop. `channels` is supplied by the host or
641/// materialized sample metadata because the provider-neutral region does not own channel count.
642pub fn decode_sample_region(
643    data: &[u8],
644    region: SampleRegion,
645    channels: u8,
646) -> Result<DecodedSampleRegion, Error> {
647    validate_sample_region(&region)?;
648    match region.compression {
649        SampleCompression::Pcm16 => DecodedSampleRegion::new(
650            region.clone(),
651            decode_sf2_pcm16(
652                data,
653                usize::try_from(region.start_frame).map_err(|_| Error::InvalidSample)?,
654                usize::try_from(region.end_frame).map_err(|_| Error::InvalidSample)?,
655                region.sample_rate,
656                channels,
657            )?,
658        ),
659        SampleCompression::Vorbis => {
660            let full_sample = decode_sf3_vorbis(data, region.sample_rate, channels)?;
661            DecodedSampleRegion::from_full_sample(region, &full_sample)
662        }
663    }
664}
665
666/// Decode a region from a [`MaterializedSoundFontAsset`] snapshot.
667///
668/// Unlike the backward-compatible [`decode_sample_region`] convenience helper,
669/// this resolves an SF3 Ogg logical stream by the region's materialized
670/// `sample_id`. Use [`ResolvedPresetZoneMetadata::decode_sample_region`] when
671/// a serialized snapshot is the only provider data available.
672pub fn decode_materialized_sample_region(
673    data: &[u8],
674    region: SampleRegion,
675    channels: u8,
676) -> Result<DecodedSampleRegion, Error> {
677    validate_sample_region(&region)?;
678    match region.compression {
679        SampleCompression::Pcm16 => decode_sample_region(data, region, channels),
680        SampleCompression::Vorbis => {
681            let full_sample =
682                decode_sf3_vorbis_sample(data, region.sample_id, region.sample_rate, channels)?;
683            DecodedSampleRegion::from_full_sample(region, &full_sample)
684        }
685    }
686}
687
688/// Decode an interleaved little-endian PCM16 sample from an SF2 `smpl` chunk.
689///
690/// `start_frame..end_frame` is half-open. The SF2 container and sample data are supplied by
691/// the host, so this dependency-free path does not read files or bundle assets.
692pub fn decode_sf2_pcm16(
693    data: &[u8],
694    start_frame: usize,
695    end_frame: usize,
696    sample_rate: u32,
697    channels: u8,
698) -> Result<DecodedSample, Error> {
699    if sample_rate == 0 || !(1..=2).contains(&channels) || start_frame >= end_frame {
700        return Err(Error::InvalidSample);
701    }
702    let bytes = find_riff_chunk(data, b"smpl").ok_or(Error::InvalidHeader)?;
703    let frame_bytes = usize::from(channels) * 2;
704    let start = start_frame
705        .checked_mul(frame_bytes)
706        .ok_or(Error::SampleTooLarge(start_frame))?;
707    let end = end_frame
708        .checked_mul(frame_bytes)
709        .ok_or(Error::SampleTooLarge(end_frame))?;
710    if end > bytes.len() || start >= end {
711        return Err(Error::Truncated);
712    }
713    let pcm = bytes[start..end]
714        .chunks_exact(2)
715        .map(|pair| i16::from_le_bytes([pair[0], pair[1]]))
716        .collect();
717    DecodedSample::new(sample_rate, channels, pcm)
718}
719
720/// Decode an SF3 Vorbis payload when the separately licensed `sf3-vorbis` feature is enabled.
721#[cfg(feature = "sf3-vorbis")]
722pub fn decode_sf3_vorbis(
723    data: &[u8],
724    sample_rate: u32,
725    channels: u8,
726) -> Result<DecodedSample, Error> {
727    decode_sf3_vorbis_sample(data, 0, sample_rate, channels)
728}
729
730/// Decode the Ogg/Vorbis stream identified by a materialized SF3 sample ID.
731///
732/// SF3 stores one Ogg logical stream per sample header.  This keeps callers from
733/// accidentally decoding sample zero for every materialized region.
734#[cfg(feature = "sf3-vorbis")]
735pub fn decode_sf3_vorbis_sample(
736    data: &[u8],
737    sample_id: u64,
738    sample_rate: u32,
739    channels: u8,
740) -> Result<DecodedSample, Error> {
741    use std::io::Cursor;
742    if sample_rate == 0 || !(1..=2).contains(&channels) {
743        return Err(Error::InvalidSample);
744    }
745    let payload = find_ogg_payload_at(data, sample_id).ok_or(Error::InvalidHeader)?;
746    let mut reader = lewton::inside_ogg::OggStreamReader::new(Cursor::new(payload))
747        .map_err(|_| Error::Decode("invalid SF3 Vorbis stream".to_string()))?;
748    if reader.ident_hdr.audio_channels != channels
749        || reader.ident_hdr.audio_sample_rate != sample_rate
750    {
751        return Err(Error::InvalidSample);
752    }
753    let mut pcm = Vec::new();
754    while let Some(packet) = reader
755        .read_dec_packet_itl()
756        .map_err(|_| Error::Decode("SF3 Vorbis decode failed".to_string()))?
757    {
758        pcm.extend(packet);
759        if pcm.len() / usize::from(channels) > MAX_DECODED_FRAMES {
760            return Err(Error::SampleTooLarge(MAX_DECODED_FRAMES + 1));
761        }
762    }
763    DecodedSample::new(sample_rate, channels, pcm)
764}
765
766/// SF3 decoding is intentionally unavailable unless a licensed Vorbis provider is selected.
767#[cfg(not(feature = "sf3-vorbis"))]
768pub fn decode_sf3_vorbis(
769    _data: &[u8],
770    _sample_rate: u32,
771    _channels: u8,
772) -> Result<DecodedSample, Error> {
773    Err(Error::UnsupportedCompression(SampleCompression::Vorbis))
774}
775
776/// See [`decode_sf3_vorbis_sample`]. This explicit fallback preserves the
777/// feature-gated compression boundary for callers that retain sample IDs.
778#[cfg(not(feature = "sf3-vorbis"))]
779pub fn decode_sf3_vorbis_sample(
780    _data: &[u8],
781    _sample_id: u64,
782    _sample_rate: u32,
783    _channels: u8,
784) -> Result<DecodedSample, Error> {
785    Err(Error::UnsupportedCompression(SampleCompression::Vorbis))
786}
787
788fn find_riff_chunk<'a>(data: &'a [u8], wanted: &[u8; 4]) -> Option<&'a [u8]> {
789    if data.len() < 12 || &data[..4] != b"RIFF" {
790        return None;
791    }
792    find_chunk_in_range(&data[12..], wanted)
793}
794
795fn find_chunk_in_range<'a>(mut data: &'a [u8], wanted: &[u8; 4]) -> Option<&'a [u8]> {
796    while data.len() >= 8 {
797        let id = &data[..4];
798        let len = u32::from_le_bytes(data[4..8].try_into().ok()?) as usize;
799        let end = 8usize.checked_add(len)?;
800        if end > data.len() {
801            return None;
802        }
803        let payload = &data[8..end];
804        if id == wanted {
805            return Some(payload);
806        }
807        if id == b"LIST"
808            && payload.len() >= 4
809            && let Some(found) = find_chunk_in_range(&payload[4..], wanted)
810        {
811            return Some(found);
812        }
813        data = &data[end + (len & 1).min(data.len().saturating_sub(end))..];
814    }
815    None
816}
817
818fn ogg_payload_end(data: &[u8], start: usize) -> Option<usize> {
819    let mut offset = start;
820    let mut serial = None;
821    loop {
822        let header_end = offset.checked_add(27)?;
823        if header_end > data.len() || &data[offset..offset + 4] != b"OggS" {
824            return None;
825        }
826        if data[offset + 4] != 0 {
827            return None;
828        }
829        let page_serial = u32::from_le_bytes(data[offset + 14..offset + 18].try_into().ok()?);
830        let segment_count = usize::from(data[offset + 26]);
831        let lacing_end = header_end.checked_add(segment_count)?;
832        if lacing_end > data.len() {
833            return None;
834        }
835        let payload_len = data[header_end..lacing_end]
836            .iter()
837            .try_fold(0usize, |sum, segment| {
838                sum.checked_add(usize::from(*segment))
839            })?;
840        let page_end = lacing_end.checked_add(payload_len)?;
841        if page_end > data.len() {
842            return None;
843        }
844        if serial.is_none() {
845            serial = Some(page_serial);
846        }
847        if Some(page_serial) != serial {
848            return None;
849        }
850        if data[offset + 5] & 0x04 != 0 {
851            return Some(page_end);
852        }
853        offset = page_end;
854        if &data[offset..].get(..4)? != b"OggS" {
855            return None;
856        }
857    }
858}
859
860fn find_ogg_payload_at(data: &[u8], sample_id: u64) -> Option<&[u8]> {
861    let sample_id = usize::try_from(sample_id).ok()?;
862    let mut search_from = 0;
863    for current_id in 0..=sample_id {
864        let relative_start = data[search_from..]
865            .windows(4)
866            .position(|window| window == b"OggS")?;
867        let start = search_from.checked_add(relative_start)?;
868        let end = ogg_payload_end(data, start)?;
869        if current_id == sample_id {
870            return Some(&data[start..end]);
871        }
872        search_from = end;
873    }
874    None
875}
876
877/// Read the channel count from an SF3 Ogg/Vorbis identification packet without
878/// invoking a codec. It is used only to describe materialized metadata; actual
879/// decoding remains behind the `sf3-vorbis` feature.
880fn sf3_ogg_channels(data: &[u8], sample_id: u64) -> Option<u8> {
881    let payload = find_ogg_payload_at(data, sample_id)?;
882    let header = payload
883        .windows(7)
884        .position(|window| window == b"\x01vorbis")?;
885    payload
886        .get(header + 11)
887        .copied()
888        .filter(|channels| (1..=2).contains(channels))
889}
890
891/// Render one scheduled sample action into deterministic interleaved PCM16 frames.
892pub fn render_sample_action(
893    sample: &DecodedSample,
894    action: &SampleAction,
895    output_rate: u32,
896) -> Result<Vec<i16>, Error> {
897    if output_rate == 0 {
898        return Err(Error::InvalidConfig);
899    }
900    let SampleAction::Start {
901        sample_id,
902        parameters,
903        event,
904        ..
905    } = action
906    else {
907        return Ok(Vec::new());
908    };
909    if *sample_id == 0 || event.duration_secs < 0.0 || !event.duration_secs.is_finite() {
910        return Err(Error::InvalidEvent);
911    }
912    let channels = usize::from(sample.channels);
913    let frames = (event.duration_secs * f64::from(output_rate)).ceil() as usize;
914    let frames = frames.min(MAX_DECODED_FRAMES);
915    let source_frames = sample.pcm_i16.len() / channels;
916    let ratio = f64::from(sample.sample_rate) * f64::from(parameters.playback_rate_ratio)
917        / f64::from(output_rate);
918    if source_frames == 0 || !ratio.is_finite() || ratio <= 0.0 || !parameters.gain.is_finite() {
919        return Err(Error::InvalidSample);
920    }
921    let mut output = Vec::with_capacity(frames * channels);
922    for frame in 0..frames {
923        let mut source = frame as f64 * ratio;
924        if let Some(loop_points) = parameters.loop_points {
925            let loop_start = usize::try_from(loop_points.start_frame).unwrap_or(usize::MAX);
926            let loop_end = usize::try_from(loop_points.end_frame).unwrap_or(usize::MAX);
927            if loop_start < loop_end && loop_end <= source_frames && source >= loop_end as f64 {
928                source = loop_start as f64
929                    + (source - loop_start as f64).rem_euclid((loop_end - loop_start) as f64);
930            }
931        }
932        let source_frame = (source as usize).min(source_frames - 1);
933        let t = frame as f32 / output_rate as f32;
934        let envelope = if parameters.attack_secs > 0.0 && t < parameters.attack_secs {
935            t / parameters.attack_secs
936        } else if parameters.decay_secs > 0.0 && t < parameters.attack_secs + parameters.decay_secs
937        {
938            1.0 - (1.0 - parameters.sustain_level)
939                * ((t - parameters.attack_secs) / parameters.decay_secs)
940        } else {
941            parameters.sustain_level
942        };
943        for channel in 0..channels {
944            let value = f32::from(sample.pcm_i16[source_frame * channels + channel])
945                * parameters.gain
946                * envelope;
947            output.push(value.clamp(-32768.0, 32767.0).round() as i16);
948        }
949    }
950    Ok(output)
951}
952
953/// Render an action against a [`DecodedSampleRegion`] using its normalized loop coordinates.
954pub fn render_sample_region_action(
955    sample_region: &DecodedSampleRegion,
956    action: &SampleAction,
957    output_rate: u32,
958) -> Result<Vec<i16>, Error> {
959    let normalized = sample_region.normalize_action(action)?;
960    render_sample_action(&sample_region.sample, &normalized, output_rate)
961}
962
963#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
964pub struct SoundFontPreset {
965    pub bank: u16,
966    pub program: u16,
967    pub name: String,
968}
969
970#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
971pub struct SoundFontAsset {
972    pub format: SoundFontFormat,
973    /// FNV-1a 64-bit checksum of the bytes; independent of the source path.
974    pub checksum: u64,
975    pub provider_version: String,
976    pub presets: Vec<SoundFontPreset>,
977}
978
979impl SoundFontAsset {
980    pub fn preset(&self, bank: u16, program: u16) -> Result<&SoundFontPreset, Error> {
981        self.presets
982            .iter()
983            .find(|p| p.bank == bank && p.program == program)
984            .ok_or(Error::PresetNotFound { bank, program })
985    }
986
987    /// Attach provider-materialized zones to this loaded asset.
988    ///
989    /// The provider remains responsible for interpreting SF2/SF3 generators and
990    /// sample tables. This method only validates the bounded, path-independent
991    /// hand-off and makes the resulting snapshot available to downstream hosts.
992    pub fn materialize_zones(
993        self,
994        zones: impl IntoIterator<Item = SoundFontPresetZone>,
995        diagnostics: impl IntoIterator<Item = SoundFontZoneDiagnostic>,
996    ) -> Result<MaterializedSoundFontAsset, Error> {
997        MaterializedSoundFontAsset::new(self, zones, diagnostics)
998    }
999}
1000
1001/// Typed, non-fatal information reported while a provider materializes zones.
1002#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1003pub enum SoundFontZoneDiagnostic {
1004    MissingSample {
1005        bank: u16,
1006        program: u16,
1007        sample_id: u64,
1008    },
1009    UnsupportedGenerator {
1010        bank: u16,
1011        program: u16,
1012        generator: String,
1013    },
1014    InvalidZone {
1015        bank: u16,
1016        program: u16,
1017        reason: String,
1018    },
1019    MissingSampleAt {
1020        location: SoundFontZoneLocation,
1021        sample_id: u64,
1022    },
1023    UnsupportedGeneratorAt {
1024        location: SoundFontZoneLocation,
1025        generator: String,
1026    },
1027    InvalidZoneAt {
1028        location: SoundFontZoneLocation,
1029        reason: String,
1030    },
1031    UnsupportedSampleTypeAt {
1032        location: SoundFontZoneLocation,
1033        sample_id: u64,
1034        sample_type: u16,
1035    },
1036    InvalidSampleLinkAt {
1037        location: SoundFontZoneLocation,
1038        sample_id: u64,
1039        linked_sample_id: u64,
1040        reason: String,
1041    },
1042}
1043
1044/// Source coordinates inside an SF2/SF3 preset/instrument table.
1045#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
1046pub struct SoundFontZoneLocation {
1047    pub bank: u16,
1048    pub program: u16,
1049    pub preset_index: usize,
1050    pub preset_bag_index: Option<usize>,
1051    pub instrument_index: Option<usize>,
1052    pub instrument_bag_index: Option<usize>,
1053    pub generator_index: Option<usize>,
1054}
1055
1056/// A loaded SoundFont plus its provider-owned, bounded zone materialization.
1057///
1058/// Keeping this as a wrapper preserves `SoundFontAsset` equality and the
1059/// metadata-only `load` API while still giving hosts one reusable snapshot.
1060#[derive(Debug, Clone, PartialEq)]
1061pub struct MaterializedSoundFontAsset {
1062    pub asset: SoundFontAsset,
1063    zones: Vec<SoundFontPresetZone>,
1064    diagnostics: Vec<SoundFontZoneDiagnostic>,
1065}
1066
1067impl MaterializedSoundFontAsset {
1068    fn new(
1069        asset: SoundFontAsset,
1070        zones: impl IntoIterator<Item = SoundFontPresetZone>,
1071        diagnostics: impl IntoIterator<Item = SoundFontZoneDiagnostic>,
1072    ) -> Result<Self, Error> {
1073        let mut zones: Vec<_> = zones.into_iter().collect();
1074        if zones.len() > MAX_MATERIALIZED_ZONES {
1075            return Err(Error::TooManyZones(zones.len()));
1076        }
1077        zones.sort_by_key(|zone| {
1078            (
1079                zone.bank,
1080                zone.program,
1081                u16::from(zone.region.key_max.saturating_sub(zone.region.key_min)),
1082                u16::from(
1083                    zone.region
1084                        .velocity_max
1085                        .saturating_sub(zone.region.velocity_min),
1086                ),
1087                zone.region.sample_id,
1088            )
1089        });
1090
1091        let mut diagnostics: Vec<_> = diagnostics.into_iter().collect();
1092        if diagnostics.len() > MAX_ZONE_DIAGNOSTICS {
1093            return Err(Error::TooManyZoneDiagnostics(diagnostics.len()));
1094        }
1095        diagnostics.sort_by_key(|diagnostic| diagnostic.sort_key());
1096        Ok(Self {
1097            asset,
1098            zones,
1099            diagnostics,
1100        })
1101    }
1102
1103    pub fn zones(&self) -> &[SoundFontPresetZone] {
1104        &self.zones
1105    }
1106
1107    pub fn diagnostics(&self) -> &[SoundFontZoneDiagnostic] {
1108        &self.diagnostics
1109    }
1110
1111    /// Return an owned snapshot for one bank/program without exposing provider data.
1112    pub fn snapshot_for_preset(
1113        &self,
1114        bank: u16,
1115        program: u16,
1116    ) -> Result<SoundFontZoneSnapshot, Error> {
1117        let preset = self.asset.preset(bank, program)?.clone();
1118        Ok(SoundFontZoneSnapshot {
1119            format: self.asset.format,
1120            checksum: self.asset.checksum,
1121            provider_version: self.asset.provider_version.clone(),
1122            preset,
1123            zones: self
1124                .zones
1125                .iter()
1126                .filter(|zone| zone.bank == bank && zone.program == program)
1127                .map(SoundFontPresetZone::resolved_metadata)
1128                .collect(),
1129            diagnostics: self
1130                .diagnostics
1131                .iter()
1132                .filter(|diagnostic| diagnostic.matches_preset(bank, program))
1133                .cloned()
1134                .collect(),
1135        })
1136    }
1137}
1138
1139/// Owned, deterministic zone metadata suitable for a browser/worker snapshot.
1140#[derive(Debug, Clone, PartialEq, Serialize)]
1141pub struct SoundFontZoneSnapshot {
1142    pub format: SoundFontFormat,
1143    pub checksum: u64,
1144    pub provider_version: String,
1145    pub preset: SoundFontPreset,
1146    pub zones: Vec<ResolvedPresetZoneMetadata>,
1147    pub diagnostics: Vec<SoundFontZoneDiagnostic>,
1148}
1149
1150impl SoundFontZoneDiagnostic {
1151    fn sort_key(&self) -> (u16, u16, u8, String) {
1152        match self {
1153            Self::MissingSample {
1154                bank,
1155                program,
1156                sample_id,
1157            } => (*bank, *program, 0, sample_id.to_string()),
1158            Self::UnsupportedGenerator {
1159                bank,
1160                program,
1161                generator,
1162            } => (*bank, *program, 1, generator.clone()),
1163            Self::InvalidZone {
1164                bank,
1165                program,
1166                reason,
1167            } => (*bank, *program, 2, reason.clone()),
1168            Self::MissingSampleAt {
1169                location,
1170                sample_id,
1171            } => (
1172                location.bank,
1173                location.program,
1174                3,
1175                format!("{location:?}:{sample_id}"),
1176            ),
1177            Self::UnsupportedGeneratorAt {
1178                location,
1179                generator,
1180            } => (
1181                location.bank,
1182                location.program,
1183                4,
1184                format!("{location:?}:{generator}"),
1185            ),
1186            Self::InvalidZoneAt { location, reason } => (
1187                location.bank,
1188                location.program,
1189                5,
1190                format!("{location:?}:{reason}"),
1191            ),
1192            Self::UnsupportedSampleTypeAt {
1193                location,
1194                sample_id,
1195                sample_type,
1196            } => (
1197                location.bank,
1198                location.program,
1199                6,
1200                format!("{location:?}:{sample_id}:{sample_type}"),
1201            ),
1202            Self::InvalidSampleLinkAt {
1203                location,
1204                sample_id,
1205                linked_sample_id,
1206                reason,
1207            } => (
1208                location.bank,
1209                location.program,
1210                7,
1211                format!("{location:?}:{sample_id}:{linked_sample_id}:{reason}"),
1212            ),
1213        }
1214    }
1215
1216    fn matches_preset(&self, bank: u16, program: u16) -> bool {
1217        match self {
1218            Self::MissingSample {
1219                bank: diagnostic_bank,
1220                program: diagnostic_program,
1221                ..
1222            }
1223            | Self::UnsupportedGenerator {
1224                bank: diagnostic_bank,
1225                program: diagnostic_program,
1226                ..
1227            }
1228            | Self::InvalidZone {
1229                bank: diagnostic_bank,
1230                program: diagnostic_program,
1231                ..
1232            } => *diagnostic_bank == bank && *diagnostic_program == program,
1233            Self::MissingSampleAt { location, .. }
1234            | Self::UnsupportedGeneratorAt { location, .. }
1235            | Self::InvalidZoneAt { location, .. }
1236            | Self::UnsupportedSampleTypeAt { location, .. }
1237            | Self::InvalidSampleLinkAt { location, .. } => {
1238                location.bank == bank && location.program == program
1239            }
1240        }
1241    }
1242}
1243
1244#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
1245pub enum Error {
1246    #[error("SoundFont is too large ({0} bytes)")]
1247    TooLarge(usize),
1248    #[error("invalid SoundFont RIFF header")]
1249    InvalidHeader,
1250    #[error("truncated SoundFont chunk")]
1251    Truncated,
1252    #[error("SoundFont preset table is missing or empty")]
1253    NoPresets,
1254    #[error("SoundFont contains too many presets ({0})")]
1255    TooManyPresets(usize),
1256    #[error("SoundFont contains too many materialized zones ({0})")]
1257    TooManyZones(usize),
1258    #[error("SoundFont contains too many zone diagnostics ({0})")]
1259    TooManyZoneDiagnostics(usize),
1260    #[error("SoundFont preset ({bank}, {program}) was not found")]
1261    PresetNotFound { bank: u16, program: u16 },
1262    #[error("invalid playback configuration")]
1263    InvalidConfig,
1264    #[error("playback event has invalid timing or velocity")]
1265    InvalidEvent,
1266    #[error("sample parameters are invalid")]
1267    InvalidSample,
1268    #[error("SoundFont zone is invalid: {0}")]
1269    InvalidZone(&'static str),
1270    #[error("SoundFont generator is unsupported: {0}")]
1271    UnsupportedGenerator(String),
1272    #[error("decoded sample is too large ({0} frames)")]
1273    SampleTooLarge(usize),
1274    #[error("provider does not support sample compression {0:?}")]
1275    UnsupportedCompression(SampleCompression),
1276    #[error("provider does not support capability: {0}")]
1277    UnsupportedProviderCapability(&'static str),
1278    #[error("SoundFont decode failed: {0}")]
1279    Decode(String),
1280}
1281
1282/// Load only the bounded, path-independent metadata needed by a renderer.
1283pub fn load(data: &[u8], provider_version: impl Into<String>) -> Result<SoundFontAsset, Error> {
1284    if data.len() > MAX_ASSET_BYTES {
1285        return Err(Error::TooLarge(data.len()));
1286    }
1287    if data.len() < 12 || &data[0..4] != b"RIFF" || &data[8..12] != b"sfbk" {
1288        return Err(Error::InvalidHeader);
1289    }
1290    let format = if data.windows(4).any(|w| w == b"OggS") {
1291        SoundFontFormat::Sf3
1292    } else {
1293        SoundFontFormat::Sf2
1294    };
1295    let mut presets = Vec::new();
1296    let mut pos = 12;
1297    while pos < data.len() {
1298        if data.len() - pos < 8 {
1299            return Err(Error::Truncated);
1300        }
1301        let id = &data[pos..pos + 4];
1302        let len = u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
1303            as usize;
1304        let start = pos + 8;
1305        let end = start.checked_add(len).ok_or(Error::Truncated)?;
1306        if end > data.len() {
1307            return Err(Error::Truncated);
1308        }
1309        if id == b"phdr" {
1310            parse_presets(&data[start..end], &mut presets)?;
1311        }
1312        if id == b"LIST" && len >= 4 && &data[start..start + 4] == b"pdta" {
1313            parse_pdta(&data[start + 4..end], &mut presets)?;
1314        }
1315        pos = end + (len & 1);
1316        if pos > data.len() {
1317            return Err(Error::Truncated);
1318        }
1319    }
1320    presets.sort_by_key(|p| (p.bank, p.program));
1321    presets.dedup_by_key(|p| (p.bank, p.program));
1322    if presets.is_empty() {
1323        return Err(Error::NoPresets);
1324    }
1325    Ok(SoundFontAsset {
1326        format,
1327        checksum: fnv1a(data),
1328        provider_version: provider_version.into(),
1329        presets,
1330    })
1331}
1332
1333/// Load a SoundFont and materialize the supported preset/instrument generator subset.
1334///
1335/// This remains a metadata operation: sample bytes are not decoded and unsupported
1336/// generators are reported as diagnostics. Providers that need a larger generator
1337/// projection can still construct [`SoundFontPresetZone`] values themselves.
1338pub fn load_materialized(
1339    data: &[u8],
1340    provider_version: impl Into<String>,
1341) -> Result<MaterializedSoundFontAsset, Error> {
1342    let asset = load(data, provider_version)?;
1343    let (zones, diagnostics) = parse_materialized_zones(data, asset.format)?;
1344    asset.materialize_zones(zones, diagnostics)
1345}
1346
1347#[derive(Debug, Clone, Copy)]
1348struct SfPresetHeader {
1349    bank: u16,
1350    program: u16,
1351    bag_index: usize,
1352}
1353
1354#[derive(Debug, Clone, Copy)]
1355struct SfInstrumentHeader {
1356    bag_index: usize,
1357}
1358
1359#[derive(Debug, Clone, Copy)]
1360struct SfBag {
1361    generator_index: usize,
1362}
1363
1364#[derive(Debug, Clone, Copy)]
1365struct SfGenerator {
1366    operator: u16,
1367    amount: i16,
1368}
1369
1370#[derive(Debug, Clone, Copy)]
1371struct SfSampleHeader {
1372    start: u32,
1373    end: u32,
1374    start_loop: u32,
1375    end_loop: u32,
1376    sample_rate: u32,
1377    root_key: u8,
1378    sample_link: u16,
1379    sample_type: u16,
1380}
1381
1382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1383enum SampleLayoutError {
1384    UnsupportedType {
1385        sample_type: u16,
1386    },
1387    InvalidLink {
1388        linked_sample_id: u64,
1389        reason: &'static str,
1390    },
1391}
1392
1393fn sample_channel_layout(
1394    samples: &[SfSampleHeader],
1395    sample_index: usize,
1396    format: SoundFontFormat,
1397) -> Result<SampleChannelLayout, SampleLayoutError> {
1398    let sample = samples
1399        .get(sample_index)
1400        .ok_or(SampleLayoutError::InvalidLink {
1401            linked_sample_id: sample_index as u64,
1402            reason: "sample index",
1403        })?;
1404    let sample_type = match (format, sample.sample_type) {
1405        // SF3 marks compressed variants with bit 0x10 while retaining the
1406        // SF2 mono/right/left low bits.
1407        (SoundFontFormat::Sf3, 0x11) => 1,
1408        (SoundFontFormat::Sf3, 0x12) => 2,
1409        (SoundFontFormat::Sf3, 0x14) => 4,
1410        _ => sample.sample_type,
1411    };
1412    match sample_type {
1413        1 => Ok(SampleChannelLayout::Mono),
1414        2 | 4 => {
1415            // FluidR3Mono and similar SF3 assets retain the compressed
1416            // left/right marker while storing a standalone mono Ogg stream
1417            // with a zero link.  It is not a stereo pair; expose its actual
1418            // one-channel decode contract instead of inventing a partner.
1419            if format == SoundFontFormat::Sf3 && sample.sample_link == 0 {
1420                return Ok(SampleChannelLayout::Mono);
1421            }
1422            let linked_index = usize::from(sample.sample_link);
1423            let Some(linked) = samples.get(linked_index) else {
1424                return Err(SampleLayoutError::InvalidLink {
1425                    linked_sample_id: u64::from(sample.sample_link),
1426                    reason: "linked sample index",
1427                });
1428            };
1429            let expected_type = if sample_type == 2 { 4 } else { 2 };
1430            let linked_type = match (format, linked.sample_type) {
1431                (SoundFontFormat::Sf3, 0x11) => 1,
1432                (SoundFontFormat::Sf3, 0x12) => 2,
1433                (SoundFontFormat::Sf3, 0x14) => 4,
1434                _ => linked.sample_type,
1435            };
1436            if linked_type != expected_type {
1437                return Err(SampleLayoutError::InvalidLink {
1438                    linked_sample_id: u64::from(sample.sample_link),
1439                    reason: "linked sample type",
1440                });
1441            }
1442            if usize::from(linked.sample_link) != sample_index {
1443                return Err(SampleLayoutError::InvalidLink {
1444                    linked_sample_id: u64::from(sample.sample_link),
1445                    reason: "linked sample back-reference",
1446                });
1447            }
1448            if sample.sample_rate != linked.sample_rate
1449                || sample.end.saturating_sub(sample.start)
1450                    != linked.end.saturating_sub(linked.start)
1451                || sample.start_loop.saturating_sub(sample.start)
1452                    != linked.start_loop.saturating_sub(linked.start)
1453                || sample.end_loop.saturating_sub(sample.start)
1454                    != linked.end_loop.saturating_sub(linked.start)
1455            {
1456                return Err(SampleLayoutError::InvalidLink {
1457                    linked_sample_id: u64::from(sample.sample_link),
1458                    reason: "linked sample frame layout",
1459                });
1460            }
1461            Ok(SampleChannelLayout::LinkedStereo {
1462                channel: if sample_type == 4 {
1463                    StereoChannel::Left
1464                } else {
1465                    StereoChannel::Right
1466                },
1467                linked_sample_id: u64::from(sample.sample_link),
1468            })
1469        }
1470        _ => Err(SampleLayoutError::UnsupportedType {
1471            sample_type: sample.sample_type,
1472        }),
1473    }
1474}
1475
1476#[derive(Debug, Default)]
1477struct SfTables {
1478    presets: Vec<SfPresetHeader>,
1479    preset_bags: Vec<SfBag>,
1480    preset_generators: Vec<SfGenerator>,
1481    instruments: Vec<SfInstrumentHeader>,
1482    instrument_bags: Vec<SfBag>,
1483    instrument_generators: Vec<SfGenerator>,
1484    samples: Vec<SfSampleHeader>,
1485}
1486
1487#[derive(Debug, Default)]
1488struct BoundedZoneDiagnostics(Vec<SoundFontZoneDiagnostic>);
1489
1490impl BoundedZoneDiagnostics {
1491    fn push(&mut self, diagnostic: SoundFontZoneDiagnostic) {
1492        if self.0.len() < MAX_ZONE_DIAGNOSTICS {
1493            self.0.push(diagnostic);
1494        }
1495    }
1496
1497    fn into_inner(self) -> Vec<SoundFontZoneDiagnostic> {
1498        self.0
1499    }
1500}
1501
1502fn bag_generators<'a>(
1503    bags: &[SfBag],
1504    generators: &'a [SfGenerator],
1505    bag_index: usize,
1506) -> Option<&'a [SfGenerator]> {
1507    let bag = bags.get(bag_index)?;
1508    let end = bags
1509        .get(bag_index + 1)
1510        .map_or(generators.len(), |next| next.generator_index);
1511    if bag.generator_index <= end && end <= generators.len() {
1512        Some(&generators[bag.generator_index..end])
1513    } else {
1514        None
1515    }
1516}
1517
1518fn parse_materialized_zones(
1519    data: &[u8],
1520    format: SoundFontFormat,
1521) -> Result<(Vec<SoundFontPresetZone>, Vec<SoundFontZoneDiagnostic>), Error> {
1522    let tables = parse_sf_tables(data)?;
1523    let (mut zones, diagnostics) = materialize_tables(&tables, format)?;
1524    if format == SoundFontFormat::Sf3 {
1525        for zone in &mut zones {
1526            if sf3_ogg_channels(data, zone.region.sample_id) == Some(2) {
1527                zone.channel_layout = SampleChannelLayout::InterleavedStereo;
1528            }
1529        }
1530    }
1531    Ok((zones, diagnostics))
1532}
1533
1534fn materialize_tables(
1535    tables: &SfTables,
1536    format: SoundFontFormat,
1537) -> Result<(Vec<SoundFontPresetZone>, Vec<SoundFontZoneDiagnostic>), Error> {
1538    if tables.presets.is_empty() || tables.instruments.is_empty() {
1539        return Ok((Vec::new(), Vec::new()));
1540    }
1541    let mut zones = Vec::new();
1542    let mut diagnostics = BoundedZoneDiagnostics::default();
1543    for (preset_index, preset) in tables.presets.iter().enumerate() {
1544        let preset_end = tables
1545            .presets
1546            .get(preset_index + 1)
1547            .map_or(tables.preset_bags.len(), |next| next.bag_index);
1548        if preset.bag_index >= preset_end || preset_end > tables.preset_bags.len() {
1549            diagnostics.push(SoundFontZoneDiagnostic::InvalidZoneAt {
1550                location: zone_location(preset, preset_index, None, None, None),
1551                reason: "preset bag range".into(),
1552            });
1553            continue;
1554        }
1555        let preset_global_generators = bag_generators(
1556            &tables.preset_bags,
1557            &tables.preset_generators,
1558            preset.bag_index,
1559        )
1560        .filter(|generators| !generators.iter().any(|generator| generator.operator == 41));
1561        for bag_index in preset.bag_index..preset_end {
1562            let bag_next = tables
1563                .preset_bags
1564                .get(bag_index + 1)
1565                .map_or(tables.preset_generators.len(), |bag| bag.generator_index);
1566            let bag = tables.preset_bags[bag_index];
1567            if bag.generator_index > bag_next || bag_next > tables.preset_generators.len() {
1568                diagnostics.push(SoundFontZoneDiagnostic::InvalidZoneAt {
1569                    location: zone_location(preset, preset_index, Some(bag_index), None, None),
1570                    reason: "preset generator range".into(),
1571                });
1572                continue;
1573            }
1574            let preset_generators = &tables.preset_generators[bag.generator_index..bag_next];
1575            let instrument_index = preset_generators
1576                .iter()
1577                .find(|generator| generator.operator == 41)
1578                .map(|generator| generator.amount as usize);
1579            let Some(instrument_index) = instrument_index else {
1580                continue;
1581            };
1582            let Some(instrument) = tables.instruments.get(instrument_index) else {
1583                diagnostics.push(SoundFontZoneDiagnostic::InvalidZoneAt {
1584                    location: zone_location(
1585                        preset,
1586                        preset_index,
1587                        Some(bag_index),
1588                        Some(instrument_index),
1589                        None,
1590                    ),
1591                    reason: "instrument index".into(),
1592                });
1593                continue;
1594            };
1595            let instrument_end = tables
1596                .instruments
1597                .get(instrument_index + 1)
1598                .map_or(tables.instrument_bags.len(), |next| next.bag_index);
1599            if instrument.bag_index >= instrument_end
1600                || instrument_end > tables.instrument_bags.len()
1601            {
1602                diagnostics.push(SoundFontZoneDiagnostic::InvalidZoneAt {
1603                    location: zone_location(
1604                        preset,
1605                        preset_index,
1606                        Some(bag_index),
1607                        Some(instrument_index),
1608                        None,
1609                    ),
1610                    reason: "instrument bag range".into(),
1611                });
1612                continue;
1613            }
1614            let instrument_global_generators = bag_generators(
1615                &tables.instrument_bags,
1616                &tables.instrument_generators,
1617                instrument.bag_index,
1618            )
1619            .filter(|generators| !generators.iter().any(|generator| generator.operator == 53));
1620            for instrument_bag_index in instrument.bag_index..instrument_end {
1621                let instrument_bag = tables.instrument_bags[instrument_bag_index];
1622                let instrument_next = tables
1623                    .instrument_bags
1624                    .get(instrument_bag_index + 1)
1625                    .map_or(tables.instrument_generators.len(), |next| {
1626                        next.generator_index
1627                    });
1628                if instrument_bag.generator_index > instrument_next
1629                    || instrument_next > tables.instrument_generators.len()
1630                {
1631                    diagnostics.push(SoundFontZoneDiagnostic::InvalidZoneAt {
1632                        location: zone_location(
1633                            preset,
1634                            preset_index,
1635                            Some(bag_index),
1636                            Some(instrument_index),
1637                            Some(instrument_bag_index),
1638                        ),
1639                        reason: "instrument generator range".into(),
1640                    });
1641                    continue;
1642                }
1643                let instrument_generators =
1644                    &tables.instrument_generators[instrument_bag.generator_index..instrument_next];
1645                let sample_index = instrument_generators
1646                    .iter()
1647                    .find(|generator| generator.operator == 53)
1648                    .map(|generator| generator.amount as usize);
1649                let Some(sample_index) = sample_index else {
1650                    continue;
1651                };
1652                let Some(sample) = tables.samples.get(sample_index).copied() else {
1653                    diagnostics.push(SoundFontZoneDiagnostic::MissingSampleAt {
1654                        location: zone_location(
1655                            preset,
1656                            preset_index,
1657                            Some(bag_index),
1658                            Some(instrument_index),
1659                            Some(instrument_bag_index),
1660                        ),
1661                        sample_id: sample_index as u64,
1662                    });
1663                    continue;
1664                };
1665                let mut projection = ZoneProjection::default();
1666                let mut supported = true;
1667                for (generator_offset, generator) in preset_global_generators
1668                    .iter()
1669                    .flat_map(|generators| generators.iter())
1670                    .chain(preset_generators)
1671                    .chain(
1672                        instrument_global_generators
1673                            .iter()
1674                            .flat_map(|generators| generators.iter()),
1675                    )
1676                    .chain(instrument_generators)
1677                    .enumerate()
1678                {
1679                    if !projection.apply(*generator) {
1680                        diagnostics.push(SoundFontZoneDiagnostic::UnsupportedGeneratorAt {
1681                            location: SoundFontZoneLocation {
1682                                generator_index: Some(
1683                                    if generator_offset < preset_generators.len() {
1684                                        bag.generator_index + generator_offset
1685                                    } else {
1686                                        instrument_bag.generator_index + generator_offset
1687                                            - preset_generators.len()
1688                                    },
1689                                ),
1690                                ..zone_location(
1691                                    preset,
1692                                    preset_index,
1693                                    Some(bag_index),
1694                                    Some(instrument_index),
1695                                    Some(instrument_bag_index),
1696                                )
1697                            },
1698                            generator: generator_name(generator.operator).into(),
1699                        });
1700                        supported = false;
1701                    }
1702                }
1703                if !supported {
1704                    continue;
1705                }
1706                let location = zone_location(
1707                    preset,
1708                    preset_index,
1709                    Some(bag_index),
1710                    Some(instrument_index),
1711                    Some(instrument_bag_index),
1712                );
1713                let channel_layout =
1714                    match sample_channel_layout(&tables.samples, sample_index, format) {
1715                        Ok(layout) => layout,
1716                        Err(SampleLayoutError::UnsupportedType { sample_type }) => {
1717                            diagnostics.push(SoundFontZoneDiagnostic::UnsupportedSampleTypeAt {
1718                                location,
1719                                sample_id: sample_index as u64,
1720                                sample_type,
1721                            });
1722                            continue;
1723                        }
1724                        Err(SampleLayoutError::InvalidLink {
1725                            linked_sample_id,
1726                            reason,
1727                        }) => {
1728                            diagnostics.push(SoundFontZoneDiagnostic::InvalidSampleLinkAt {
1729                                location,
1730                                sample_id: sample_index as u64,
1731                                linked_sample_id,
1732                                reason: reason.into(),
1733                            });
1734                            continue;
1735                        }
1736                    };
1737                match projection.into_region(sample, sample_index as u64, format) {
1738                    Ok(region) => {
1739                        match SoundFontPresetZone::with_channel_layout(
1740                            preset.bank,
1741                            preset.program,
1742                            region,
1743                            channel_layout,
1744                        ) {
1745                            Ok(zone) => zones.push(zone),
1746                            Err(error) => {
1747                                diagnostics.push(SoundFontZoneDiagnostic::InvalidZoneAt {
1748                                    location: zone_location(
1749                                        preset,
1750                                        preset_index,
1751                                        Some(bag_index),
1752                                        Some(instrument_index),
1753                                        Some(instrument_bag.generator_index),
1754                                    ),
1755                                    reason: error.to_string(),
1756                                })
1757                            }
1758                        }
1759                    }
1760                    Err(reason) => diagnostics.push(SoundFontZoneDiagnostic::InvalidZoneAt {
1761                        location: zone_location(
1762                            preset,
1763                            preset_index,
1764                            Some(bag_index),
1765                            Some(instrument_index),
1766                            Some(instrument_bag.generator_index),
1767                        ),
1768                        reason,
1769                    }),
1770                }
1771            }
1772        }
1773    }
1774    Ok((zones, diagnostics.into_inner()))
1775}
1776
1777fn zone_location(
1778    preset: &SfPresetHeader,
1779    preset_index: usize,
1780    preset_bag_index: Option<usize>,
1781    instrument_index: Option<usize>,
1782    instrument_bag_index: Option<usize>,
1783) -> SoundFontZoneLocation {
1784    SoundFontZoneLocation {
1785        bank: preset.bank,
1786        program: preset.program,
1787        preset_index,
1788        preset_bag_index,
1789        instrument_index,
1790        instrument_bag_index,
1791        generator_index: None,
1792    }
1793}
1794
1795#[derive(Debug)]
1796struct ZoneProjection {
1797    key_min: u8,
1798    key_max: u8,
1799    velocity_min: u8,
1800    velocity_max: u8,
1801    root_key: Option<u8>,
1802    fine_tune_cents: i16,
1803    attenuation_db: f32,
1804    start_offset: i32,
1805    end_offset: i32,
1806    start_loop_offset: i32,
1807    end_loop_offset: i32,
1808    sample_modes: u16,
1809    release_timecents: Option<i16>,
1810}
1811
1812impl Default for ZoneProjection {
1813    fn default() -> Self {
1814        Self {
1815            key_min: 0,
1816            key_max: 127,
1817            velocity_min: 1,
1818            velocity_max: 127,
1819            root_key: None,
1820            fine_tune_cents: 0,
1821            attenuation_db: 0.0,
1822            start_offset: 0,
1823            end_offset: 0,
1824            start_loop_offset: 0,
1825            end_loop_offset: 0,
1826            sample_modes: 0,
1827            release_timecents: None,
1828        }
1829    }
1830}
1831
1832impl ZoneProjection {
1833    /// Apply generators in SoundFont inheritance order: preset global, preset local,
1834    /// instrument global, then instrument local. Range/root/mode values use the last supported
1835    /// value, while additive offsets, attenuation, and fine tune accumulate.
1836    fn apply(&mut self, generator: SfGenerator) -> bool {
1837        match generator.operator {
1838            0 => {
1839                self.start_offset = self
1840                    .start_offset
1841                    .saturating_add(i32::from(generator.amount))
1842            }
1843            1 => self.end_offset = self.end_offset.saturating_add(i32::from(generator.amount)),
1844            2 => {
1845                self.start_loop_offset = self
1846                    .start_loop_offset
1847                    .saturating_add(i32::from(generator.amount))
1848            }
1849            3 => {
1850                self.end_loop_offset = self
1851                    .end_loop_offset
1852                    .saturating_add(i32::from(generator.amount))
1853            }
1854            38 => self.release_timecents = Some(generator.amount),
1855            41 | 53 => {}
1856            43 => {
1857                self.key_min = generator.amount as u16 as u8;
1858                self.key_max = (generator.amount as u16 >> 8) as u8;
1859            }
1860            44 => {
1861                self.velocity_min = generator.amount as u16 as u8;
1862                self.velocity_max = (generator.amount as u16 >> 8) as u8;
1863            }
1864            48 => self.attenuation_db += f32::from(generator.amount) / 10.0,
1865            52 => self.fine_tune_cents = self.fine_tune_cents.saturating_add(generator.amount),
1866            54 => self.sample_modes = generator.amount as u16,
1867            58 => self.root_key = Some(generator.amount as u16 as u8),
1868            _ => return false,
1869        }
1870        true
1871    }
1872
1873    fn into_region(
1874        self,
1875        sample: SfSampleHeader,
1876        sample_id: u64,
1877        format: SoundFontFormat,
1878    ) -> Result<SampleRegion, String> {
1879        let start = offset_frame(sample.start, self.start_offset)?;
1880        let end = offset_frame(sample.end, self.end_offset)?;
1881        if start >= end {
1882            return Err("sample frame range".into());
1883        }
1884        let loop_points = if self.sample_modes & 3 != 0 {
1885            let loop_start = offset_frame(sample.start_loop, self.start_loop_offset)?;
1886            let loop_end = offset_frame(sample.end_loop, self.end_loop_offset)?;
1887            Some(SampleLoop {
1888                start_frame: loop_start,
1889                end_frame: loop_end,
1890            })
1891        } else {
1892            None
1893        };
1894        // SF2 `smpl` is one shared PCM array, while SF3 maps every sample
1895        // header to a distinct Ogg logical stream.  SF3 regions must be
1896        // stream-relative before the decoder crops the selected payload.
1897        let (start, end, loop_points) = if format == SoundFontFormat::Sf3 {
1898            let normalize = |frame: u32| {
1899                frame
1900                    .checked_sub(sample.start)
1901                    .ok_or_else(|| "SF3 sample frame range".to_string())
1902            };
1903            let normalized_loop = loop_points
1904                .map(|points| {
1905                    Ok::<SampleLoop, String>(SampleLoop {
1906                        start_frame: normalize(points.start_frame)?,
1907                        end_frame: normalize(points.end_frame)?,
1908                    })
1909                })
1910                .transpose()?;
1911            (normalize(start)?, normalize(end)?, normalized_loop)
1912        } else {
1913            (start, end, loop_points)
1914        };
1915        Ok(SampleRegion {
1916            sample_id,
1917            start_frame: start,
1918            end_frame: end,
1919            key_min: self.key_min,
1920            key_max: self.key_max,
1921            velocity_min: self.velocity_min.max(1),
1922            velocity_max: self.velocity_max,
1923            root_key: self.root_key.unwrap_or(sample.root_key),
1924            fine_tune_cents: self.fine_tune_cents,
1925            attenuation_db: self.attenuation_db,
1926            sample_rate: sample.sample_rate,
1927            compression: match format {
1928                SoundFontFormat::Sf2 => SampleCompression::Pcm16,
1929                SoundFontFormat::Sf3 => SampleCompression::Vorbis,
1930            },
1931            loop_points,
1932            attack_secs: 0.0,
1933            decay_secs: 0.0,
1934            sustain_level: 1.0,
1935            release_secs: self
1936                .release_timecents
1937                .map_or(0.0, |value| 2.0_f32.powf(f32::from(value) / 1200.0)),
1938        })
1939    }
1940}
1941
1942fn offset_frame(base: u32, offset: i32) -> Result<u32, String> {
1943    let value = i64::from(base) + i64::from(offset);
1944    if value < 0 || value > i64::from(u32::MAX) {
1945        return Err("sample offset overflow".into());
1946    }
1947    Ok(value as u32)
1948}
1949
1950fn generator_name(operator: u16) -> &'static str {
1951    match operator {
1952        0 => "startAddrsOffset",
1953        1 => "endAddrsOffset",
1954        2 => "startloopAddrsOffset",
1955        3 => "endloopAddrsOffset",
1956        38 => "releaseVolEnv",
1957        41 => "instrument",
1958        43 => "keyRange",
1959        44 => "velRange",
1960        48 => "initialAttenuation",
1961        52 => "fineTune",
1962        53 => "sampleID",
1963        54 => "sampleModes",
1964        58 => "overridingRootKey",
1965        _ => "unknown",
1966    }
1967}
1968
1969fn parse_sf_tables(data: &[u8]) -> Result<SfTables, Error> {
1970    let mut tables = SfTables::default();
1971    let mut pos = 12;
1972    while pos < data.len() {
1973        let (id, payload, next) = next_chunk(data, pos)?;
1974        if &id == b"LIST" && payload.len() >= 4 && &payload[..4] == b"pdta" {
1975            parse_pdta_tables(&payload[4..], &mut tables)?;
1976        }
1977        pos = next;
1978    }
1979    Ok(tables)
1980}
1981
1982fn parse_pdta_tables(data: &[u8], tables: &mut SfTables) -> Result<(), Error> {
1983    let mut pos = 0;
1984    while pos < data.len() {
1985        let (id, payload, next) = next_chunk(data, pos)?;
1986        match &id {
1987            b"phdr" => tables.presets = parse_preset_headers(payload)?,
1988            b"pbag" => tables.preset_bags = parse_bags(payload)?,
1989            b"pgen" => tables.preset_generators = parse_generators(payload)?,
1990            b"inst" => tables.instruments = parse_instrument_headers(payload)?,
1991            b"ibag" => tables.instrument_bags = parse_bags(payload)?,
1992            b"igen" => tables.instrument_generators = parse_generators(payload)?,
1993            b"shdr" => tables.samples = parse_sample_headers(payload)?,
1994            _ => {}
1995        }
1996        pos = next;
1997    }
1998    Ok(())
1999}
2000
2001fn next_chunk(data: &[u8], pos: usize) -> Result<([u8; 4], &[u8], usize), Error> {
2002    if data.len().saturating_sub(pos) < 8 {
2003        return Err(Error::Truncated);
2004    }
2005    let id = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
2006    let len =
2007        u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]]) as usize;
2008    let start = pos + 8;
2009    let end = start.checked_add(len).ok_or(Error::Truncated)?;
2010    let next = end.checked_add(len & 1).ok_or(Error::Truncated)?;
2011    if end > data.len() || next > data.len() {
2012        return Err(Error::Truncated);
2013    }
2014    Ok((id, &data[start..end], next))
2015}
2016
2017fn parse_preset_headers(data: &[u8]) -> Result<Vec<SfPresetHeader>, Error> {
2018    const RECORD: usize = 38;
2019    if data.len() < RECORD || !data.len().is_multiple_of(RECORD) {
2020        return Err(Error::Truncated);
2021    }
2022    Ok(data
2023        .chunks_exact(RECORD)
2024        .take(data.len() / RECORD - 1)
2025        .map(|record| SfPresetHeader {
2026            program: u16::from_le_bytes([record[20], record[21]]),
2027            bank: u16::from_le_bytes([record[22], record[23]]),
2028            bag_index: usize::from(u16::from_le_bytes([record[24], record[25]])),
2029        })
2030        .collect())
2031}
2032
2033fn parse_instrument_headers(data: &[u8]) -> Result<Vec<SfInstrumentHeader>, Error> {
2034    const RECORD: usize = 22;
2035    if data.len() < RECORD || !data.len().is_multiple_of(RECORD) {
2036        return Err(Error::Truncated);
2037    }
2038    Ok(data
2039        .chunks_exact(RECORD)
2040        .take(data.len() / RECORD - 1)
2041        .map(|record| SfInstrumentHeader {
2042            bag_index: usize::from(u16::from_le_bytes([record[20], record[21]])),
2043        })
2044        .collect())
2045}
2046
2047fn parse_bags(data: &[u8]) -> Result<Vec<SfBag>, Error> {
2048    if data.len() < 4 || !data.len().is_multiple_of(4) {
2049        return Err(Error::Truncated);
2050    }
2051    Ok(data
2052        .chunks_exact(4)
2053        .map(|record| SfBag {
2054            generator_index: usize::from(u16::from_le_bytes([record[0], record[1]])),
2055        })
2056        .collect())
2057}
2058
2059fn parse_generators(data: &[u8]) -> Result<Vec<SfGenerator>, Error> {
2060    if data.len() < 4 || !data.len().is_multiple_of(4) {
2061        return Err(Error::Truncated);
2062    }
2063    Ok(data
2064        .chunks_exact(4)
2065        .map(|record| SfGenerator {
2066            operator: u16::from_le_bytes([record[0], record[1]]),
2067            amount: i16::from_le_bytes([record[2], record[3]]),
2068        })
2069        .collect())
2070}
2071
2072fn parse_sample_headers(data: &[u8]) -> Result<Vec<SfSampleHeader>, Error> {
2073    const RECORD: usize = 46;
2074    if data.len() < RECORD || !data.len().is_multiple_of(RECORD) {
2075        return Err(Error::Truncated);
2076    }
2077    Ok(data
2078        .chunks_exact(RECORD)
2079        .take(data.len() / RECORD - 1)
2080        .map(|record| SfSampleHeader {
2081            start: u32::from_le_bytes([record[20], record[21], record[22], record[23]]),
2082            end: u32::from_le_bytes([record[24], record[25], record[26], record[27]]),
2083            start_loop: u32::from_le_bytes([record[28], record[29], record[30], record[31]]),
2084            end_loop: u32::from_le_bytes([record[32], record[33], record[34], record[35]]),
2085            sample_rate: u32::from_le_bytes([record[36], record[37], record[38], record[39]]),
2086            root_key: record[40],
2087            sample_link: u16::from_le_bytes([record[42], record[43]]),
2088            sample_type: u16::from_le_bytes([record[44], record[45]]),
2089        })
2090        .collect())
2091}
2092
2093fn parse_pdta(data: &[u8], presets: &mut Vec<SoundFontPreset>) -> Result<(), Error> {
2094    let mut pos = 0;
2095    while pos < data.len() {
2096        if data.len() - pos < 8 {
2097            return Err(Error::Truncated);
2098        }
2099        let len = u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]])
2100            as usize;
2101        let start = pos + 8;
2102        let end = start.checked_add(len).ok_or(Error::Truncated)?;
2103        if end > data.len() {
2104            return Err(Error::Truncated);
2105        }
2106        if &data[pos..pos + 4] == b"phdr" {
2107            parse_presets(&data[start..end], presets)?;
2108        }
2109        pos = end + (len & 1);
2110    }
2111    Ok(())
2112}
2113
2114fn parse_presets(data: &[u8], presets: &mut Vec<SoundFontPreset>) -> Result<(), Error> {
2115    const RECORD: usize = 38;
2116    if data.len() < RECORD || !data.len().is_multiple_of(RECORD) {
2117        return Err(Error::Truncated);
2118    }
2119    let count = data.len() / RECORD;
2120    if count > MAX_PRESETS + 1 {
2121        return Err(Error::TooManyPresets(count));
2122    }
2123    for record in data.chunks_exact(RECORD).take(count.saturating_sub(1)) {
2124        let name_end = record[..20].iter().position(|b| *b == 0).unwrap_or(20);
2125        let name = String::from_utf8_lossy(&record[..name_end])
2126            .trim()
2127            .to_owned();
2128        let program = u16::from_le_bytes([record[20], record[21]]);
2129        let bank = u16::from_le_bytes([record[22], record[23]]);
2130        presets.push(SoundFontPreset {
2131            bank,
2132            program,
2133            name,
2134        });
2135        if presets.len() > MAX_PRESETS {
2136            return Err(Error::TooManyPresets(presets.len()));
2137        }
2138    }
2139    Ok(())
2140}
2141
2142fn fnv1a(data: &[u8]) -> u64 {
2143    data.iter().fold(0xcbf29ce484222325, |hash, byte| {
2144        (hash ^ u64::from(*byte)).wrapping_mul(0x100000001b3)
2145    })
2146}
2147
2148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2149pub enum VoiceStealing {
2150    Oldest,
2151}
2152
2153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2154pub struct PlaybackConfig {
2155    pub max_polyphony: usize,
2156    pub voice_stealing: VoiceStealing,
2157    pub max_cached_samples: usize,
2158}
2159
2160#[derive(Debug, Clone, PartialEq)]
2161pub struct SampleVoiceParameters {
2162    pub playback_rate_ratio: f32,
2163    pub gain: f32,
2164    pub loop_points: Option<SampleLoop>,
2165    pub attack_secs: f32,
2166    pub decay_secs: f32,
2167    pub sustain_level: f32,
2168    pub release_secs: f32,
2169}
2170
2171#[derive(Debug, Clone)]
2172pub enum SampleAction {
2173    Start {
2174        voice_id: u64,
2175        sample_id: u64,
2176        event: Box<PlaybackEvent>,
2177        parameters: SampleVoiceParameters,
2178    },
2179    Stop {
2180        voice_id: u64,
2181        release_secs: f32,
2182    },
2183}
2184
2185/// Build a deterministic provider action for one `PlaybackEvent` and selected region.
2186pub fn schedule_sample_note_on(
2187    voice_id: u64,
2188    event: PlaybackEvent,
2189    region: &SampleRegion,
2190    velocity_exponent: f32,
2191) -> Result<SampleAction, Error> {
2192    validate_sample_region(region)?;
2193    if !region.contains(event.pitch_midi, event.velocity) {
2194        return Err(Error::InvalidSample);
2195    }
2196    let gain = velocity_gain(event.velocity, velocity_exponent)?
2197        * 10.0_f32.powf(-region.attenuation_db / 20.0);
2198    let semitones = f32::from(event.pitch_midi) - f32::from(region.root_key)
2199        + f32::from(region.fine_tune_cents) / 100.0;
2200    let playback_rate_ratio = 2.0_f32.powf(semitones / 12.0);
2201    Ok(SampleAction::Start {
2202        voice_id,
2203        sample_id: region.sample_id,
2204        event: Box::new(event),
2205        parameters: SampleVoiceParameters {
2206            playback_rate_ratio,
2207            gain,
2208            loop_points: region.loop_points,
2209            attack_secs: region.attack_secs,
2210            decay_secs: region.decay_secs,
2211            sustain_level: region.sustain_level,
2212            release_secs: region.release_secs,
2213        },
2214    })
2215}
2216
2217/// Small deterministic FIFO sample cache. The provider owns decoded sample memory.
2218#[derive(Debug, Clone)]
2219pub struct SampleCache {
2220    capacity: usize,
2221    ids: Vec<u64>,
2222}
2223
2224impl SampleCache {
2225    pub fn new(capacity: usize) -> Result<Self, Error> {
2226        if capacity == 0 {
2227            return Err(Error::InvalidConfig);
2228        }
2229        Ok(Self {
2230            capacity,
2231            ids: Vec::new(),
2232        })
2233    }
2234
2235    pub fn touch(&mut self, sample_id: u64) {
2236        self.ids.retain(|id| *id != sample_id);
2237        if self.ids.len() >= self.capacity {
2238            self.ids.remove(0);
2239        }
2240        self.ids.push(sample_id);
2241    }
2242
2243    pub fn contains(&self, sample_id: u64) -> bool {
2244        self.ids.contains(&sample_id)
2245    }
2246
2247    pub fn len(&self) -> usize {
2248        self.ids.len()
2249    }
2250
2251    pub fn is_empty(&self) -> bool {
2252        self.ids.is_empty()
2253    }
2254
2255    pub fn clear(&mut self) {
2256        self.ids.clear();
2257    }
2258}
2259
2260impl Default for PlaybackConfig {
2261    fn default() -> Self {
2262        Self {
2263            max_polyphony: 64,
2264            voice_stealing: VoiceStealing::Oldest,
2265            max_cached_samples: 128,
2266        }
2267    }
2268}
2269
2270#[derive(Debug, Clone)]
2271pub enum Action {
2272    NoteOn {
2273        voice_id: u64,
2274        event: Box<PlaybackEvent>,
2275    },
2276    NoteOff {
2277        voice_id: u64,
2278        release_secs: f64,
2279    },
2280    VoiceStolen {
2281        voice_id: u64,
2282    },
2283}
2284
2285#[derive(Debug, Clone)]
2286struct Voice {
2287    id: u64,
2288    address: Option<String>,
2289    released: bool,
2290}
2291
2292/// State machine for a renderer. The renderer consumes [`Action`] values.
2293/// `PlaybackEvent` remains unchanged and is forwarded intact on note-on.
2294#[derive(Debug, Clone)]
2295pub struct PlaybackBoundary {
2296    config: PlaybackConfig,
2297    voices: Vec<Voice>,
2298    next_id: u64,
2299    sustain: bool,
2300    cached_samples: Vec<u64>,
2301}
2302
2303impl PlaybackBoundary {
2304    pub fn new(config: PlaybackConfig) -> Result<Self, Error> {
2305        if config.max_polyphony == 0
2306            || config.max_polyphony > MAX_POLYPHONY
2307            || config.max_cached_samples == 0
2308        {
2309            return Err(Error::InvalidConfig);
2310        }
2311        Ok(Self {
2312            config,
2313            voices: Vec::new(),
2314            next_id: 1,
2315            sustain: false,
2316            cached_samples: Vec::new(),
2317        })
2318    }
2319
2320    pub fn note_on(&mut self, event: PlaybackEvent) -> Result<Vec<Action>, Error> {
2321        if !event.time_secs.is_finite()
2322            || !event.duration_secs.is_finite()
2323            || event.duration_secs < 0.0
2324            || event.velocity == 0
2325            || event.velocity > 127
2326        {
2327            return Err(Error::InvalidEvent);
2328        }
2329        let mut actions = Vec::new();
2330        if self.voices.len() >= self.config.max_polyphony {
2331            let stolen = self.voices.remove(0);
2332            actions.push(Action::VoiceStolen {
2333                voice_id: stolen.id,
2334            });
2335        }
2336        let id = self.next_id;
2337        self.next_id = self.next_id.wrapping_add(1).max(1);
2338        self.voices.push(Voice {
2339            id,
2340            address: event.address.clone(),
2341            released: false,
2342        });
2343        actions.push(Action::NoteOn {
2344            voice_id: id,
2345            event: Box::new(event),
2346        });
2347        Ok(actions)
2348    }
2349
2350    pub fn note_off(
2351        &mut self,
2352        address: Option<&str>,
2353        release_secs: f64,
2354    ) -> Result<Vec<Action>, Error> {
2355        if !release_secs.is_finite() || release_secs < 0.0 {
2356            return Err(Error::InvalidEvent);
2357        }
2358        let index = self
2359            .voices
2360            .iter()
2361            .position(|v| !v.released && v.address.as_deref() == address);
2362        if let Some(index) = index {
2363            if self.sustain {
2364                self.voices[index].released = true;
2365                return Ok(Vec::new());
2366            }
2367            let voice = self.voices.remove(index);
2368            return Ok(vec![Action::NoteOff {
2369                voice_id: voice.id,
2370                release_secs,
2371            }]);
2372        }
2373        Ok(Vec::new())
2374    }
2375
2376    pub fn set_sustain(&mut self, down: bool, release_secs: f64) -> Result<Vec<Action>, Error> {
2377        if !release_secs.is_finite() || release_secs < 0.0 {
2378            return Err(Error::InvalidEvent);
2379        }
2380        let was_down = self.sustain;
2381        self.sustain = down;
2382        if was_down && !down {
2383            let released = self
2384                .voices
2385                .iter()
2386                .filter(|v| v.released)
2387                .map(|v| v.id)
2388                .collect::<Vec<_>>();
2389            self.voices.retain(|v| !v.released);
2390            return Ok(released
2391                .into_iter()
2392                .map(|voice_id| Action::NoteOff {
2393                    voice_id,
2394                    release_secs,
2395                })
2396                .collect());
2397        }
2398        Ok(Vec::new())
2399    }
2400
2401    pub fn cleanup(&mut self) {
2402        self.voices.clear();
2403        self.cached_samples.clear();
2404    }
2405    pub fn cache_sample(&mut self, sample_id: u64) {
2406        if !self.cached_samples.contains(&sample_id) {
2407            if self.cached_samples.len() >= self.config.max_cached_samples {
2408                self.cached_samples.remove(0);
2409            }
2410            self.cached_samples.push(sample_id);
2411        }
2412    }
2413    pub fn cached_sample_count(&self) -> usize {
2414        self.cached_samples.len()
2415    }
2416    pub fn active_voice_count(&self) -> usize {
2417        self.voices.len()
2418    }
2419}
2420
2421#[cfg(test)]
2422mod tests {
2423    use super::*;
2424    fn event(address: &str, velocity: u8) -> PlaybackEvent {
2425        PlaybackEvent {
2426            address: Some(address.into()),
2427            source: None,
2428            source_voice_number: None,
2429            time_beats: 0.0,
2430            time_secs: 0.0,
2431            pitch_midi: 60,
2432            pitch_midi_cents: 6000,
2433            pitch_bend_curve: Vec::new(),
2434            post_note_pause_beats: 0.0,
2435            articulations: Vec::new(),
2436            chord_symbol: None,
2437            guitar_technique: None,
2438            velocity,
2439            duration_beats: 1.0,
2440            duration_secs: 1.0,
2441            pedal: false,
2442            part_index: 0,
2443            channel: 0,
2444            program: 0,
2445            instrument_id: None,
2446            is_metronome: false,
2447        }
2448    }
2449
2450    fn region(sample_id: u64, key_min: u8, key_max: u8) -> SampleRegion {
2451        SampleRegion {
2452            sample_id,
2453            start_frame: 0,
2454            end_frame: 1_000,
2455            key_min,
2456            key_max,
2457            velocity_min: 1,
2458            velocity_max: 127,
2459            root_key: 60,
2460            fine_tune_cents: 0,
2461            attenuation_db: 0.0,
2462            sample_rate: 44_100,
2463            compression: SampleCompression::Pcm16,
2464            loop_points: Some(SampleLoop {
2465                start_frame: 10,
2466                end_frame: 100,
2467            }),
2468            attack_secs: 0.01,
2469            decay_secs: 0.2,
2470            sustain_level: 0.8,
2471            release_secs: 0.3,
2472        }
2473    }
2474
2475    #[test]
2476    fn region_selection_prefers_narrowest_matching_zone() {
2477        let regions = [region(20, 0, 127), region(10, 60, 60)];
2478        assert_eq!(
2479            select_sample_region(&regions, 60, 100).map(|r| r.sample_id),
2480            Some(10)
2481        );
2482        assert_eq!(
2483            select_sample_region(&regions, 61, 100).map(|r| r.sample_id),
2484            Some(20)
2485        );
2486    }
2487
2488    #[test]
2489    fn preset_zone_selection_preserves_bank_program_and_bounds() {
2490        let zones = vec![
2491            SoundFontPresetZone::new(0, 0, region(20, 0, 127)).expect("wide zone"),
2492            SoundFontPresetZone::new(0, 0, region(10, 60, 60)).expect("narrow zone"),
2493            SoundFontPresetZone::new(1, 0, region(30, 60, 60)).expect("other bank"),
2494        ];
2495        let selected = select_preset_zone(&zones, 0, 0, 60, 100).expect("matching zone");
2496        assert_eq!(selected.region.sample_id, 10);
2497        assert_eq!(selected.region.start_frame, 0);
2498        assert!(select_preset_zone(&zones, 1, 0, 60, 100).is_some());
2499        assert!(select_preset_zone(&zones, 2, 0, 60, 100).is_none());
2500    }
2501
2502    #[test]
2503    fn sample_schedule_contains_tuning_gain_and_envelope() {
2504        let action = schedule_sample_note_on(7, event("n", 127), &region(3, 0, 127), 1.0)
2505            .expect("sample action");
2506        assert!(matches!(action, SampleAction::Start {
2507            voice_id: 7,
2508            sample_id: 3,
2509            parameters: SampleVoiceParameters { gain, playback_rate_ratio, .. },
2510            ..
2511        } if (gain - 1.0).abs() < f32::EPSILON && (playback_rate_ratio - 1.0).abs() < f32::EPSILON));
2512    }
2513
2514    #[test]
2515    fn preset_note_plan_selects_bank_program_and_key_velocity_zone() {
2516        let zones = vec![
2517            SoundFontPresetZone::new(2, 10, region(1, 0, 127)).expect("zone"),
2518            SoundFontPresetZone::new(2, 10, region(2, 60, 60)).expect("narrow zone"),
2519        ];
2520        let action = schedule_preset_note_on(9, event("preset", 100), &zones, 2, 10, 1.0)
2521            .expect("preset plan");
2522        assert!(matches!(action, SampleAction::Start { sample_id: 2, .. }));
2523    }
2524
2525    #[test]
2526    fn layered_preset_selection_and_schedule_are_stable() {
2527        let zones = vec![
2528            SoundFontPresetZone::new(2, 10, region(20, 0, 127)).expect("wide layer"),
2529            SoundFontPresetZone::new(2, 10, region(10, 60, 60)).expect("narrow layer"),
2530        ];
2531        let selected = select_preset_zones(&zones, 2, 10, 60, 100);
2532        assert_eq!(
2533            selected
2534                .iter()
2535                .map(|zone| zone.region.sample_id)
2536                .collect::<Vec<_>>(),
2537            vec![10, 20]
2538        );
2539        let actions = schedule_preset_note_ons(9, event("layered", 100), &zones, 2, 10, 1.0)
2540            .expect("layered plan");
2541        assert_eq!(actions.len(), 2);
2542        assert!(matches!(
2543            actions[0],
2544            SampleAction::Start {
2545                voice_id: 9,
2546                sample_id: 10,
2547                ..
2548            }
2549        ));
2550        assert!(matches!(
2551            actions[1],
2552            SampleAction::Start {
2553                voice_id: 10,
2554                sample_id: 20,
2555                ..
2556            }
2557        ));
2558    }
2559
2560    #[test]
2561    fn materialization_inherits_preset_and_instrument_global_generators() {
2562        let key_range = i16::from_le_bytes([50, 70]);
2563        let mut tables = SfTables {
2564            presets: vec![SfPresetHeader {
2565                bank: 0,
2566                program: 0,
2567                bag_index: 0,
2568            }],
2569            preset_bags: vec![SfBag { generator_index: 0 }, SfBag { generator_index: 1 }],
2570            preset_generators: vec![
2571                SfGenerator {
2572                    operator: 43,
2573                    amount: key_range,
2574                },
2575                SfGenerator {
2576                    operator: 41,
2577                    amount: 0,
2578                },
2579            ],
2580            instruments: vec![SfInstrumentHeader { bag_index: 0 }],
2581            instrument_bags: vec![SfBag { generator_index: 0 }, SfBag { generator_index: 1 }],
2582            instrument_generators: vec![
2583                SfGenerator {
2584                    operator: 52,
2585                    amount: 12,
2586                },
2587                SfGenerator {
2588                    operator: 53,
2589                    amount: 0,
2590                },
2591            ],
2592            samples: vec![SfSampleHeader {
2593                start: 0,
2594                end: 1_000,
2595                start_loop: 10,
2596                end_loop: 100,
2597                sample_rate: 44_100,
2598                root_key: 60,
2599                sample_link: 0,
2600                sample_type: 1,
2601            }],
2602        };
2603        let (zones, diagnostics) =
2604            materialize_tables(&tables, SoundFontFormat::Sf2).expect("tables materialize");
2605        assert!(diagnostics.is_empty());
2606        assert_eq!(zones.len(), 1);
2607        assert_eq!(zones[0].region.key_min, 50);
2608        assert_eq!(zones[0].region.key_max, 70);
2609        assert_eq!(zones[0].region.fine_tune_cents, 12);
2610        tables.samples[0].sample_type = 8;
2611        let (zones, diagnostics) =
2612            materialize_tables(&tables, SoundFontFormat::Sf2).expect("tables materialize");
2613        assert!(zones.is_empty());
2614        assert!(matches!(
2615            diagnostics.as_slice(),
2616            [SoundFontZoneDiagnostic::UnsupportedSampleTypeAt {
2617                sample_id: 0,
2618                sample_type: 8,
2619                ..
2620            }]
2621        ));
2622    }
2623
2624    #[test]
2625    fn sample_channel_layout_resolves_mono_linked_stereo_and_sf3_single_payloads() {
2626        let header = |sample_type, sample_link, start| SfSampleHeader {
2627            start,
2628            end: start + 1_000,
2629            start_loop: start + 10,
2630            end_loop: start + 100,
2631            sample_rate: 44_100,
2632            root_key: 60,
2633            sample_link,
2634            sample_type,
2635        };
2636        let samples = [header(4, 1, 100), header(2, 0, 8_000)];
2637        assert_eq!(
2638            sample_channel_layout(&samples, 0, SoundFontFormat::Sf2),
2639            Ok(SampleChannelLayout::LinkedStereo {
2640                channel: StereoChannel::Left,
2641                linked_sample_id: 1,
2642            })
2643        );
2644        assert_eq!(
2645            sample_channel_layout(&samples, 1, SoundFontFormat::Sf2),
2646            Ok(SampleChannelLayout::LinkedStereo {
2647                channel: StereoChannel::Right,
2648                linked_sample_id: 0,
2649            })
2650        );
2651        assert_eq!(
2652            sample_channel_layout(&[header(0x14, 0, 100)], 0, SoundFontFormat::Sf3),
2653            Ok(SampleChannelLayout::Mono)
2654        );
2655    }
2656
2657    #[test]
2658    fn malformed_sample_layouts_return_typed_errors() {
2659        let header = |sample_type, sample_link| SfSampleHeader {
2660            start: 100,
2661            end: 1_100,
2662            start_loop: 110,
2663            end_loop: 200,
2664            sample_rate: 44_100,
2665            root_key: 60,
2666            sample_link,
2667            sample_type,
2668        };
2669        assert_eq!(
2670            sample_channel_layout(&[header(2, 9)], 0, SoundFontFormat::Sf2),
2671            Err(SampleLayoutError::InvalidLink {
2672                linked_sample_id: 9,
2673                reason: "linked sample index",
2674            })
2675        );
2676        assert_eq!(
2677            sample_channel_layout(&[header(8, 0)], 0, SoundFontFormat::Sf2),
2678            Err(SampleLayoutError::UnsupportedType { sample_type: 8 })
2679        );
2680    }
2681
2682    #[test]
2683    fn resolved_zone_metadata_is_owned_and_round_trips_to_region() {
2684        let zone = SoundFontPresetZone::with_channel_layout(
2685            2,
2686            10,
2687            region(7, 48, 72),
2688            SampleChannelLayout::LinkedStereo {
2689                channel: StereoChannel::Left,
2690                linked_sample_id: 8,
2691            },
2692        )
2693        .expect("zone");
2694        let metadata = zone.resolved_metadata();
2695        assert_eq!(metadata.bank, 2);
2696        assert_eq!(metadata.program, 10);
2697        assert_eq!(metadata.sample_id, 7);
2698        assert_eq!(metadata.key_min, 48);
2699        assert_eq!(metadata.key_max, 72);
2700        assert_eq!(metadata.decode_channels, 1);
2701        assert_eq!(
2702            metadata.channel_layout,
2703            SampleChannelLayout::LinkedStereo {
2704                channel: StereoChannel::Left,
2705                linked_sample_id: 8,
2706            }
2707        );
2708        assert_eq!(
2709            metadata.loop_points,
2710            Some(SampleLoop {
2711                start_frame: 10,
2712                end_frame: 100
2713            })
2714        );
2715        assert_eq!(
2716            metadata.sample_region().expect("validated region"),
2717            zone.region
2718        );
2719    }
2720
2721    #[test]
2722    fn resolved_zone_metadata_uses_the_same_deterministic_selection() {
2723        let zones = vec![
2724            SoundFontPresetZone::new(2, 10, region(20, 0, 127)).expect("wide zone"),
2725            SoundFontPresetZone::new(2, 10, region(10, 60, 60)).expect("narrow zone"),
2726        ];
2727        let metadata =
2728            resolve_preset_zone_metadata(&zones, 2, 10, 60, 100).expect("resolved metadata");
2729        assert_eq!(metadata.sample_id, 10);
2730        assert_eq!(
2731            metadata
2732                .sample_region()
2733                .expect("validated region")
2734                .sample_id,
2735            10
2736        );
2737        assert_eq!(
2738            resolve_preset_zone_metadata(&zones, 3, 10, 60, 100),
2739            Err(Error::InvalidSample)
2740        );
2741    }
2742
2743    #[test]
2744    fn decoded_sample_and_cache_are_bounded() {
2745        assert_eq!(
2746            DecodedSample::new(44_100, 2, vec![]),
2747            Err(Error::InvalidSample)
2748        );
2749        let mut cache = SampleCache::new(2).expect("cache");
2750        cache.touch(1);
2751        cache.touch(2);
2752        cache.touch(3);
2753        assert!(!cache.contains(1));
2754        assert!(cache.contains(2));
2755        assert_eq!(cache.len(), 2);
2756    }
2757
2758    #[test]
2759    fn provider_capabilities_reject_unsupported_paths_explicitly() {
2760        let pcm = region(1, 0, 127);
2761        assert_eq!(
2762            validate_provider_capabilities(&pcm, ProviderCapabilities::metadata_only()),
2763            Err(Error::UnsupportedCompression(SampleCompression::Pcm16))
2764        );
2765
2766        let capabilities = ProviderCapabilities {
2767            sf2_pcm16: true,
2768            sf3_vorbis: false,
2769            synthesis: false,
2770        };
2771        assert_eq!(
2772            validate_provider_capabilities(&pcm, capabilities),
2773            Err(Error::UnsupportedProviderCapability("synthesis"))
2774        );
2775
2776        let mut vorbis = pcm;
2777        vorbis.compression = SampleCompression::Vorbis;
2778        assert_eq!(
2779            validate_provider_capabilities(
2780                &vorbis,
2781                ProviderCapabilities {
2782                    sf2_pcm16: true,
2783                    sf3_vorbis: false,
2784                    synthesis: true,
2785                }
2786            ),
2787            Err(Error::UnsupportedCompression(SampleCompression::Vorbis))
2788        );
2789    }
2790
2791    #[test]
2792    fn malformed_zone_data_is_rejected_with_a_typed_diagnostic() {
2793        let mut invalid = region(1, 60, 59);
2794        assert_eq!(
2795            validate_sample_region(&invalid),
2796            Err(Error::InvalidZone("key range"))
2797        );
2798        invalid = region(1, 0, 127);
2799        invalid.loop_points = Some(SampleLoop {
2800            start_frame: 20,
2801            end_frame: 10,
2802        });
2803        assert_eq!(
2804            validate_sample_region(&invalid),
2805            Err(Error::InvalidZone("loop points"))
2806        );
2807        assert!(validate_generator_name("keyRange").is_ok());
2808        assert_eq!(
2809            validate_generator_name("unsupportedFilterCutoff"),
2810            Err(Error::UnsupportedGenerator(
2811                "unsupportedFilterCutoff".to_string()
2812            ))
2813        );
2814    }
2815
2816    #[test]
2817    fn decodes_sf2_pcm_and_renders_a_deterministic_note() {
2818        let mut sf2 = b"RIFFxxxxsfbk".to_vec();
2819        sf2.extend(b"LIST".as_slice());
2820        sf2.extend(20u32.to_le_bytes());
2821        sf2.extend(b"sdta".as_slice());
2822        sf2.extend(b"smpl".as_slice());
2823        sf2.extend(8u32.to_le_bytes());
2824        for value in [1000i16, -1000, 2000, -2000] {
2825            sf2.extend(value.to_le_bytes());
2826        }
2827        let sample = decode_sf2_pcm16(&sf2, 1, 3, 2, 1).expect("PCM16 sample");
2828        assert_eq!(sample.pcm_i16, vec![-1000, 2000]);
2829        let mut playback_region = region(1, 0, 127);
2830        playback_region.attack_secs = 0.0;
2831        playback_region.sustain_level = 1.0;
2832        let action = schedule_sample_note_on(1, event("render", 127), &playback_region, 1.0)
2833            .expect("sample action");
2834        let rendered = render_sample_action(&sample, &action, 2).expect("rendered sample");
2835        assert_eq!(rendered, vec![-1000, 2000]);
2836    }
2837
2838    #[test]
2839    fn region_decode_normalizes_nonzero_source_loops_before_rendering() {
2840        let mut sf2 = b"RIFFxxxxsfbk".to_vec();
2841        sf2.extend(b"LIST".as_slice());
2842        sf2.extend(24u32.to_le_bytes());
2843        sf2.extend(b"sdta");
2844        sf2.extend(b"smpl");
2845        sf2.extend(12u32.to_le_bytes());
2846        for value in [10i16, 20, 30, 40, 50, 60] {
2847            sf2.extend(value.to_le_bytes());
2848        }
2849        let mut playback_region = region(7, 0, 127);
2850        playback_region.start_frame = 2;
2851        playback_region.end_frame = 6;
2852        playback_region.loop_points = Some(SampleLoop {
2853            start_frame: 3,
2854            end_frame: 5,
2855        });
2856        playback_region.sample_rate = 2;
2857        playback_region.attack_secs = 0.0;
2858        playback_region.sustain_level = 1.0;
2859
2860        let decoded =
2861            decode_sample_region(&sf2, playback_region.clone(), 1).expect("region decode");
2862        assert_eq!(decoded.sample.pcm_i16, vec![30, 40, 50, 60]);
2863        assert_eq!(
2864            decoded.loop_points,
2865            Some(SampleLoop {
2866                start_frame: 1,
2867                end_frame: 3,
2868            })
2869        );
2870
2871        let mut scheduled = event("nonzero-region", 127);
2872        scheduled.duration_secs = 3.0;
2873        let action =
2874            schedule_sample_note_on(1, scheduled, &playback_region, 1.0).expect("sample action");
2875        let rendered = render_sample_region_action(&decoded, &action, 2).expect("region render");
2876        assert_eq!(rendered, vec![30, 40, 50, 40, 50, 40]);
2877    }
2878
2879    #[test]
2880    fn region_loop_must_be_inside_the_asset_frame_range() {
2881        let mut invalid = region(1, 0, 127);
2882        invalid.start_frame = 10;
2883        invalid.end_frame = 20;
2884        invalid.loop_points = Some(SampleLoop {
2885            start_frame: 9,
2886            end_frame: 15,
2887        });
2888        assert_eq!(
2889            validate_sample_region(&invalid),
2890            Err(Error::InvalidZone("loop points"))
2891        );
2892    }
2893
2894    #[cfg(feature = "sf3-vorbis")]
2895    #[test]
2896    fn sf3_region_decode_uses_the_same_buffer_relative_contract() {
2897        let data = include_bytes!("../../../tests/fixtures/synthetic.sf3");
2898        let full = decode_sf3_vorbis(data, 8_000, 2).expect("SF3 fixture");
2899        assert!(full.frame_count() >= 3, "fixture must have three frames");
2900        let mut playback_region = region(8, 0, 127);
2901        playback_region.start_frame = 1;
2902        playback_region.end_frame = 3;
2903        playback_region.sample_rate = 8_000;
2904        playback_region.compression = SampleCompression::Vorbis;
2905        playback_region.loop_points = Some(SampleLoop {
2906            start_frame: 2,
2907            end_frame: 3,
2908        });
2909
2910        let decoded = decode_sample_region(data, playback_region, 2).expect("SF3 region decode");
2911        assert_eq!(decoded.sample.frame_count(), 2);
2912        assert_eq!(
2913            decoded.loop_points,
2914            Some(SampleLoop {
2915                start_frame: 1,
2916                end_frame: 2,
2917            })
2918        );
2919    }
2920
2921    #[test]
2922    fn decodes_real_cc0_sf2_and_renders_non_silent_audio() {
2923        let data = include_bytes!("../../../tests/fixtures/UprightPianoKW-small-20190703.sf2");
2924        let sample = decode_sf2_pcm16(data, 0, 1024, 44_100, 1).expect("CC0 SF2 fixture");
2925        assert_eq!(sample.sample_rate, 44_100);
2926        assert_eq!(sample.channels, 1);
2927        assert!(sample.pcm_i16.iter().any(|value| *value != 0));
2928
2929        let mut playback_region = region(42, 0, 127);
2930        playback_region.attack_secs = 0.0;
2931        playback_region.sustain_level = 1.0;
2932        let action = schedule_sample_note_on(42, event("cc0-sf2", 127), &playback_region, 1.0)
2933            .expect("SF2 sample action");
2934        let rendered = render_sample_action(&sample, &action, 44_100).expect("SF2 render");
2935        assert!(!rendered.is_empty());
2936        assert!(rendered.iter().any(|value| *value != 0));
2937    }
2938
2939    #[cfg(feature = "sf3-vorbis")]
2940    #[test]
2941    fn decodes_permitted_synthetic_sf3_vorbis_fixture() {
2942        assert_eq!(
2943            sf3_ogg_channels(include_bytes!("../../../tests/fixtures/synthetic.sf3"), 0),
2944            Some(2)
2945        );
2946        let sample = decode_sf3_vorbis(
2947            include_bytes!("../../../tests/fixtures/synthetic.sf3"),
2948            8000,
2949            2,
2950        )
2951        .expect("SF3 Vorbis fixture");
2952        assert_eq!(sample.sample_rate, 8000);
2953        assert_eq!(sample.channels, 2);
2954        assert!(!sample.pcm_i16.is_empty());
2955        assert!(sample.pcm_i16.iter().any(|value| *value != 0));
2956    }
2957
2958    #[cfg(feature = "sf3-vorbis")]
2959    #[test]
2960    fn decodes_real_mit_sf3_vorbis_and_renders_non_silent_audio() {
2961        let data = include_bytes!("../../../tests/fixtures/FluidR3Mono_GM.sf3");
2962        let sample = decode_sf3_vorbis(data, 11_025, 1).expect("MIT SF3 fixture");
2963        assert_eq!(sample.sample_rate, 11_025);
2964        assert_eq!(sample.channels, 1);
2965        assert!(sample.pcm_i16.iter().any(|value| *value != 0));
2966
2967        let mut playback_region = region(43, 0, 127);
2968        playback_region.attack_secs = 0.0;
2969        playback_region.sustain_level = 1.0;
2970        let action = schedule_sample_note_on(43, event("mit-sf3", 127), &playback_region, 1.0)
2971            .expect("SF3 sample action");
2972        let rendered = render_sample_action(&sample, &action, 11_025).expect("SF3 render");
2973        assert!(!rendered.is_empty());
2974        assert!(rendered.iter().any(|value| *value != 0));
2975    }
2976    fn sf(ogg: bool) -> Vec<u8> {
2977        let mut p = vec![0u8; 38 * 2];
2978        p[..6].copy_from_slice(b"Piano\0");
2979        p[20..22].copy_from_slice(&0u16.to_le_bytes());
2980        p[22..24].copy_from_slice(&0u16.to_le_bytes());
2981        p[38..42].copy_from_slice(b"EOP\0");
2982        let mut ph = b"phdr".to_vec();
2983        ph.extend((p.len() as u32).to_le_bytes());
2984        ph.extend(p);
2985        let mut list = b"LIST".to_vec();
2986        list.extend(((ph.len() + 4) as u32).to_le_bytes());
2987        list.extend(b"pdta");
2988        list.extend(ph);
2989        let mut out = b"RIFFxxxxsfbk".to_vec();
2990        out.extend(list);
2991        if ogg {
2992            out.extend(b"data");
2993            out.extend(4u32.to_le_bytes());
2994            out.extend(b"OggS");
2995        }
2996        out
2997    }
2998    #[test]
2999    fn loads_sf2_and_preset() {
3000        let asset = load(&sf(false), "test").expect("fixture");
3001        assert_eq!(asset.format, SoundFontFormat::Sf2);
3002        assert_eq!(asset.preset(0, 0).expect("preset").name, "Piano");
3003    }
3004    #[test]
3005    fn loads_sf3_marker() {
3006        assert_eq!(
3007            load(&sf(true), "test").expect("fixture").format,
3008            SoundFontFormat::Sf3
3009        );
3010    }
3011
3012    #[test]
3013    fn materialized_asset_returns_deterministic_bounded_preset_snapshot() {
3014        let asset = load(&sf(false), "provider-1").expect("fixture");
3015        let wide = SoundFontPresetZone::new(0, 0, region(20, 0, 127)).expect("wide zone");
3016        let narrow = SoundFontPresetZone::new(0, 0, region(10, 60, 60)).expect("narrow zone");
3017        let materialized = asset
3018            .materialize_zones(
3019                [wide, narrow],
3020                [SoundFontZoneDiagnostic::MissingSample {
3021                    bank: 0,
3022                    program: 0,
3023                    sample_id: 99,
3024                }],
3025            )
3026            .expect("materialized asset");
3027
3028        let snapshot = materialized.snapshot_for_preset(0, 0).expect("snapshot");
3029        assert_eq!(snapshot.format, SoundFontFormat::Sf2);
3030        assert_eq!(snapshot.provider_version, "provider-1");
3031        assert_eq!(snapshot.zones.len(), 2);
3032        assert_eq!(snapshot.zones[0].sample_id, 10);
3033        assert_eq!(snapshot.zones[1].sample_id, 20);
3034        assert_eq!(snapshot.diagnostics.len(), 1);
3035        assert!(matches!(
3036            snapshot.diagnostics[0],
3037            SoundFontZoneDiagnostic::MissingSample { sample_id: 99, .. }
3038        ));
3039    }
3040
3041    #[test]
3042    fn materialized_asset_rejects_unbounded_zone_input() {
3043        let asset = load(&sf(false), "test").expect("fixture");
3044        let zones = std::iter::repeat_with(|| {
3045            SoundFontPresetZone::new(0, 0, region(1, 0, 127)).expect("zone")
3046        })
3047        .take(MAX_MATERIALIZED_ZONES + 1);
3048        assert_eq!(
3049            asset.materialize_zones(zones, []),
3050            Err(Error::TooManyZones(MAX_MATERIALIZED_ZONES + 1))
3051        );
3052    }
3053
3054    #[test]
3055    fn materialized_sf3_snapshot_preserves_provider_format_and_diagnostics() {
3056        let asset = load(&sf(true), "sf3-provider").expect("fixture");
3057        let mut sf3_region = region(44, 0, 127);
3058        sf3_region.compression = SampleCompression::Vorbis;
3059        let zone = SoundFontPresetZone::new(0, 0, sf3_region).expect("SF3 zone");
3060        let materialized = asset
3061            .materialize_zones(
3062                [zone],
3063                [SoundFontZoneDiagnostic::UnsupportedGenerator {
3064                    bank: 0,
3065                    program: 0,
3066                    generator: "exclusiveClass".into(),
3067                }],
3068            )
3069            .expect("SF3 materialized asset");
3070        let snapshot = materialized.snapshot_for_preset(0, 0).expect("snapshot");
3071        assert_eq!(snapshot.format, SoundFontFormat::Sf3);
3072        assert_eq!(snapshot.provider_version, "sf3-provider");
3073        assert_eq!(snapshot.zones[0].compression, SampleCompression::Vorbis);
3074        assert!(matches!(
3075            snapshot.diagnostics[0],
3076            SoundFontZoneDiagnostic::UnsupportedGenerator { ref generator, .. }
3077                if generator == "exclusiveClass"
3078        ));
3079    }
3080
3081    #[test]
3082    fn load_materialized_reads_real_sf2_generator_tables() {
3083        let materialized = load_materialized(
3084            include_bytes!("../../../tests/fixtures/UprightPianoKW-small-20190703.sf2"),
3085            "fixture-provider",
3086        )
3087        .expect("real SF2 materialization");
3088        assert_eq!(materialized.asset.format, SoundFontFormat::Sf2);
3089        assert!(!materialized.zones().is_empty());
3090        let preset = materialized.zones().first().expect("materialized zone");
3091        let snapshot = materialized
3092            .snapshot_for_preset(preset.bank, preset.program)
3093            .expect("preset snapshot");
3094        assert!(!snapshot.zones.is_empty());
3095        assert_eq!(snapshot.zones[0].compression, SampleCompression::Pcm16);
3096    }
3097
3098    #[test]
3099    fn load_materialized_reads_real_sf3_generator_tables() {
3100        let data = include_bytes!("../../../tests/fixtures/FluidR3Mono_GM.sf3");
3101        let materialized =
3102            load_materialized(data, "fixture-sf3-provider").expect("real SF3 materialization");
3103        assert_eq!(materialized.asset.format, SoundFontFormat::Sf3);
3104        assert!(!materialized.zones().is_empty());
3105        assert!(
3106            materialized
3107                .zones()
3108                .iter()
3109                .all(|zone| zone.region.compression == SampleCompression::Vorbis)
3110        );
3111        let zone = materialized.zones().first().expect("materialized zone");
3112        let snapshot = materialized
3113            .snapshot_for_preset(zone.bank, zone.program)
3114            .expect("snapshot");
3115        let metadata = snapshot.zones.first().expect("materialized zone");
3116        assert!(
3117            metadata.sample_id > 0,
3118            "fixture must exercise a non-first Ogg stream"
3119        );
3120        assert_eq!(metadata.start_frame, 0);
3121        #[cfg(feature = "sf3-vorbis")]
3122        let decoded = metadata
3123            .decode_sample_region(data)
3124            .expect("materialized SF3 decode");
3125        #[cfg(feature = "sf3-vorbis")]
3126        assert_eq!(decoded.sample.channels, metadata.decode_channels);
3127        #[cfg(feature = "sf3-vorbis")]
3128        assert!(!decoded.sample.pcm_i16.is_empty());
3129        #[cfg(not(feature = "sf3-vorbis"))]
3130        assert_eq!(
3131            metadata.decode_sample_region(data),
3132            Err(Error::UnsupportedCompression(SampleCompression::Vorbis))
3133        );
3134    }
3135
3136    #[test]
3137    fn malformed_generator_table_reports_source_location() {
3138        let mut data =
3139            include_bytes!("../../../tests/fixtures/UprightPianoKW-small-20190703.sf2").to_vec();
3140        let pbag_offset = data
3141            .windows(4)
3142            .position(|chunk| chunk == b"pbag")
3143            .expect("pbag chunk")
3144            + 8;
3145        data[pbag_offset..pbag_offset + 2].copy_from_slice(&u16::MAX.to_le_bytes());
3146        let materialized = load_materialized(&data, "malformed-fixture").expect("diagnostics");
3147        let located = materialized
3148            .diagnostics()
3149            .iter()
3150            .find_map(|diagnostic| match diagnostic {
3151                SoundFontZoneDiagnostic::InvalidZoneAt { location, reason }
3152                    if reason == "preset generator range" =>
3153                {
3154                    Some(*location)
3155                }
3156                _ => None,
3157            })
3158            .expect("located malformed-zone diagnostic");
3159        assert_eq!(located.preset_index, 0);
3160        assert!(located.preset_bag_index.is_some());
3161        assert_eq!(located.bank, 0);
3162        assert_eq!(located.program, 0);
3163    }
3164
3165    #[test]
3166    fn lifecycle_preserves_velocity_and_sustain() {
3167        let mut b = PlaybackBoundary::new(PlaybackConfig {
3168            max_polyphony: 1,
3169            ..Default::default()
3170        })
3171        .expect("config");
3172        let actions = b.note_on(event("n", 100)).expect("note");
3173        assert!(
3174            actions.iter().any(
3175                |action| matches!(action, Action::NoteOn { event, .. } if event.velocity == 100)
3176            )
3177        );
3178        assert!(b.set_sustain(true, 0.1).expect("sustain").is_empty());
3179        assert!(b.note_off(Some("n"), 0.2).expect("off").is_empty());
3180        assert_eq!(b.set_sustain(false, 0.3).expect("release").len(), 1);
3181        b.cleanup();
3182        assert_eq!(b.active_voice_count(), 0);
3183    }
3184}