1use acorde_core::PlaybackEvent;
9use serde::Serialize;
10
11pub const PLAYBACK_CONTRACT_VERSION: u16 = 1;
12pub const PROVIDER_CONTRACT_VERSION: u16 = 1;
14pub const MAX_ASSET_BYTES: usize = 64 * 1024 * 1024;
15pub const MAX_PRESETS: usize = 16_384;
16pub const MAX_MATERIALIZED_ZONES: usize = 65_536;
18pub const MAX_ZONE_DIAGNOSTICS: usize = 4_096;
20pub const MAX_POLYPHONY: usize = 256;
21pub const MAX_DECODED_FRAMES: usize = 16_000_000;
22
23pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
48pub enum SampleCompression {
49 Pcm16,
50 Vorbis,
51}
52
53#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
79#[serde(rename_all = "snake_case")]
80pub enum StereoChannel {
81 Left,
82 Right,
83}
84
85#[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 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#[derive(Debug, Clone, PartialEq, Serialize)]
114pub struct SampleRegion {
115 pub sample_id: u64,
116 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 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
145pub 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(®ion.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
181pub 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
190pub 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#[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(®ion)?;
223 Ok(Self {
224 bank,
225 program,
226 region,
227 channel_layout: SampleChannelLayout::Mono,
228 })
229 }
230
231 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(®ion)?;
239 Ok(Self {
240 bank,
241 program,
242 region,
243 channel_layout,
244 })
245 }
246
247 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 pub fn resolved_metadata(&self) -> ResolvedPresetZoneMetadata {
258 ResolvedPresetZoneMetadata::from_zone(self)
259 }
260}
261
262#[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 pub channel_layout: SampleChannelLayout,
285 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 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(®ion).map(|()| region)
344 }
345
346 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
355pub 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
379pub 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
409pub 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
422pub 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
440pub 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
470pub 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#[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#[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 pub fn new(region: SampleRegion, sample: DecodedSample) -> Result<Self, Error> {
502 validate_sample_region(®ion)?;
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 pub fn from_full_sample(
523 region: SampleRegion,
524 full_sample: &DecodedSample,
525 ) -> Result<Self, Error> {
526 validate_sample_region(®ion)?;
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 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
561pub trait SampleDecoder {
566 type Error: std::error::Error + Send + Sync + 'static;
567
568 fn decode(&self, region: &SampleRegion) -> Result<DecodedSample, Self::Error>;
569}
570
571pub 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
581pub 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
594pub 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 pub fn frame_count(&self) -> usize {
633 self.pcm_i16.len() / usize::from(self.channels)
634 }
635}
636
637pub fn decode_sample_region(
643 data: &[u8],
644 region: SampleRegion,
645 channels: u8,
646) -> Result<DecodedSampleRegion, Error> {
647 validate_sample_region(®ion)?;
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
666pub fn decode_materialized_sample_region(
673 data: &[u8],
674 region: SampleRegion,
675 channels: u8,
676) -> Result<DecodedSampleRegion, Error> {
677 validate_sample_region(®ion)?;
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
688pub 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#[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#[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#[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#[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
877fn 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
891pub 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
953pub 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 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 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#[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#[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#[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 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#[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
1282pub 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
1333pub 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 (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 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 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 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
2185pub 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#[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#[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(®ions, 60, 100).map(|r| r.sample_id),
2480 Some(10)
2481 );
2482 assert_eq!(
2483 select_sample_region(®ions, 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), ®ion(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}