agb_xm_core/
lib.rs

1use std::collections::HashMap;
2
3use agb_fixnum::Num;
4use agb_tracker_interop::{Jump, PatternEffect, RetriggerVolumeChange, Waveform};
5
6use xmrs::prelude::*;
7
8pub fn parse_module(module: &Module) -> agb_tracker_interop::Track {
9    let instruments = &module.instrument;
10    let mut instruments_map = HashMap::new();
11
12    struct SampleData {
13        data: Vec<u8>,
14        should_loop: bool,
15        fine_tune: f64,
16        relative_note: i8,
17        restart_point: u32,
18        volume: Num<i16, 8>,
19        envelope_id: Option<usize>,
20        fadeout: Num<i32, 8>,
21    }
22
23    let mut samples = vec![];
24    let mut envelopes: Vec<EnvelopeData> = vec![];
25    let mut existing_envelopes: HashMap<EnvelopeData, usize> = Default::default();
26
27    for (instrument_index, instrument) in instruments.iter().enumerate() {
28        let InstrumentType::Default(ref instrument) = instrument.instr_type else {
29            continue;
30        };
31
32        let envelope = &instrument.volume_envelope;
33        let envelope_id = if envelope.enabled {
34            let envelope = EnvelopeData::new(
35                envelope,
36                instrument,
37                module.frequency_type,
38                module.default_bpm as u32,
39            );
40            let id = existing_envelopes
41                .entry(envelope)
42                .or_insert_with_key(|envelope| {
43                    envelopes.push(envelope.clone());
44                    envelopes.len() - 1
45                });
46
47            Some(*id)
48        } else {
49            None
50        };
51
52        for (sample_index, sample) in instrument.sample.iter().enumerate() {
53            let should_loop = !matches!(sample.flags, LoopType::No);
54            let fine_tune = sample.finetune as f64 * 128.0;
55            let relative_note = sample.relative_note;
56            let restart_point = sample.loop_start;
57            let sample_len = if sample.loop_length > 0 {
58                (sample.loop_length + sample.loop_start) as usize
59            } else {
60                usize::MAX
61            };
62
63            let volume = Num::from_f32(sample.volume);
64
65            let sample = match &sample.data {
66                SampleDataType::Mono8(depth8) => depth8
67                    .iter()
68                    .map(|value| *value as u8)
69                    .take(sample_len)
70                    .collect::<Vec<_>>(),
71                SampleDataType::Mono16(depth16) => depth16
72                    .iter()
73                    .map(|sample| (sample >> 8) as i8 as u8)
74                    .take(sample_len)
75                    .collect::<Vec<_>>(),
76                _ => panic!("Stereo samples not supported"),
77            };
78
79            let fadeout = Num::from_f32(instrument.volume_fadeout);
80
81            instruments_map.insert((instrument_index, sample_index), samples.len());
82            samples.push(SampleData {
83                data: sample,
84                should_loop,
85                fine_tune,
86                relative_note,
87                restart_point,
88                volume,
89                envelope_id,
90                fadeout,
91            });
92        }
93    }
94
95    let mut patterns = vec![];
96    let mut pattern_data = vec![];
97
98    for pattern in &module.pattern {
99        let start_pos = pattern_data.len();
100        let mut effect_parameters: [u8; 255] = [0; u8::MAX as usize];
101        let mut tone_portamento_directions = vec![0; module.get_num_channels()];
102        let mut note_and_sample = vec![None; module.get_num_channels()];
103        let mut previous_retriggers: Vec<Option<(RetriggerVolumeChange, u8)>> =
104            vec![None; module.get_num_channels()];
105
106        for row in pattern.iter() {
107            // the combined jump for each row
108            let mut jump = None;
109
110            for (i, slot) in row.iter().enumerate() {
111                let channel_number = i % module.get_num_channels();
112
113                let sample = if slot.instrument == 0 {
114                    0
115                } else {
116                    let instrument_index = (slot.instrument - 1) as usize;
117
118                    if let Some(InstrumentType::Default(instrument)) = module
119                        .instrument
120                        .get(instrument_index)
121                        .map(|instrument| &instrument.instr_type)
122                    {
123                        let sample_slot = *instrument
124                            .sample_for_note
125                            .get(slot.note as usize)
126                            .unwrap_or(&0) as usize;
127                        instruments_map
128                            .get(&(instrument_index, sample_slot))
129                            .map(|sample_idx| sample_idx + 1)
130                            .unwrap_or(0)
131                    } else {
132                        0
133                    }
134                };
135
136                let mut effect1 = PatternEffect::None;
137
138                let previous_note_and_sample = note_and_sample[channel_number];
139                let maybe_note_and_sample = if matches!(slot.note, Note::KeyOff) {
140                    effect1 = PatternEffect::Stop;
141
142                    &note_and_sample[channel_number]
143                } else if !matches!(slot.note, Note::None) {
144                    if sample != 0 {
145                        note_and_sample[channel_number] = Some((slot.note, &samples[sample - 1]));
146                    } else if let Some((note, _)) = &mut note_and_sample[channel_number] {
147                        *note = slot.note;
148                    }
149
150                    &note_and_sample[channel_number]
151                } else {
152                    &note_and_sample[channel_number]
153                };
154
155                if matches!(effect1, PatternEffect::None) {
156                    effect1 = match slot.volume {
157                        0x10..=0x50 => PatternEffect::Volume(
158                            (Num::new((slot.volume - 0x10) as i16) / 64)
159                                * maybe_note_and_sample
160                                    .map(|note_and_sample| note_and_sample.1.volume)
161                                    .unwrap_or(1.into()),
162                        ),
163                        0x60..=0x6F => PatternEffect::VolumeSlide(
164                            -Num::new((slot.volume - 0x60) as i16) / 64,
165                            false,
166                        ),
167                        0x70..=0x7F => PatternEffect::VolumeSlide(
168                            Num::new((slot.volume - 0x70) as i16) / 64,
169                            false,
170                        ),
171                        0x80..=0x8F => PatternEffect::FineVolumeSlide(
172                            -Num::new((slot.volume - 0x80) as i16) / 128,
173                        ),
174                        0x90..=0x9F => PatternEffect::FineVolumeSlide(
175                            Num::new((slot.volume - 0x90) as i16) / 128,
176                        ),
177                        0xC0..=0xCF => PatternEffect::Panning(
178                            Num::new(slot.volume as i16 - (0xC0 + (0xCF - 0xC0) / 2)) / 8,
179                        ),
180                        _ => PatternEffect::None,
181                    };
182                }
183
184                let effect_parameter = if slot.effect_parameter != 0 {
185                    effect_parameters[slot.effect_type as usize] = slot.effect_parameter;
186                    slot.effect_parameter
187                } else {
188                    effect_parameters[slot.effect_type as usize]
189                };
190
191                let effect2 = match slot.effect_type {
192                    0x0 => {
193                        if slot.effect_parameter == 0 {
194                            PatternEffect::None
195                        } else if let Some((note, sample)) = maybe_note_and_sample {
196                            let first_arpeggio = slot.effect_parameter >> 4;
197                            let second_arpeggio = slot.effect_parameter & 0xF;
198
199                            let first_arpeggio_speed = note_to_speed(
200                                *note,
201                                sample.fine_tune,
202                                sample.relative_note + first_arpeggio as i8,
203                                module.frequency_type,
204                            );
205                            let second_arpeggio_speed = note_to_speed(
206                                *note,
207                                sample.fine_tune,
208                                sample.relative_note + second_arpeggio as i8,
209                                module.frequency_type,
210                            );
211
212                            PatternEffect::Arpeggio(
213                                first_arpeggio_speed
214                                    .try_change_base()
215                                    .expect("Arpeggio size too large"),
216                                second_arpeggio_speed
217                                    .try_change_base()
218                                    .expect("Arpeggio size too large"),
219                            )
220                        } else {
221                            PatternEffect::None
222                        }
223                    }
224                    0x1 => {
225                        let c4_speed: Num<u32, 12> =
226                            note_to_speed(Note::C4, 0.0, 0, module.frequency_type).change_base();
227                        let speed: Num<u32, 12> = note_to_speed(
228                            Note::C4,
229                            effect_parameter as f64 * 8.0,
230                            0,
231                            module.frequency_type,
232                        )
233                        .change_base();
234
235                        let portamento_amount = speed / c4_speed;
236
237                        PatternEffect::Portamento(portamento_amount.try_change_base().unwrap())
238                    }
239                    0x2 => {
240                        let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
241                        let speed = note_to_speed(
242                            Note::C4,
243                            effect_parameter as f64 * 8.0,
244                            0,
245                            module.frequency_type,
246                        );
247
248                        let portamento_amount = c4_speed / speed;
249
250                        PatternEffect::Portamento(portamento_amount.try_change_base().unwrap())
251                    }
252                    0x3 => {
253                        if let (Some((note, sample)), Some((prev_note, _))) =
254                            (maybe_note_and_sample, previous_note_and_sample)
255                        {
256                            let target_speed = note_to_speed(
257                                *note,
258                                sample.fine_tune,
259                                sample.relative_note,
260                                module.frequency_type,
261                            );
262
263                            let direction = match (prev_note as usize).cmp(&(*note as usize)) {
264                                std::cmp::Ordering::Less => 1,
265                                std::cmp::Ordering::Equal => {
266                                    tone_portamento_directions[channel_number]
267                                }
268                                std::cmp::Ordering::Greater => -1,
269                            };
270
271                            tone_portamento_directions[channel_number] = direction;
272
273                            let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
274                            let speed = note_to_speed(
275                                Note::C4,
276                                effect_parameter as f64 * 8.0,
277                                0,
278                                module.frequency_type,
279                            );
280
281                            let portamento_amount = if direction > 0 {
282                                speed / c4_speed
283                            } else {
284                                c4_speed / speed
285                            };
286
287                            PatternEffect::TonePortamento(
288                                portamento_amount.try_change_base().unwrap(),
289                                target_speed.try_change_base().unwrap(),
290                            )
291                        } else {
292                            PatternEffect::None
293                        }
294                    }
295                    0x4 => {
296                        let vibrato_speed = effect_parameter >> 4;
297                        let depth = effect_parameter & 0xF;
298
299                        let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
300                        let speed =
301                            note_to_speed(Note::C4, depth as f64 * 16.0, 0, module.frequency_type);
302
303                        let amount = speed / c4_speed - 1;
304
305                        PatternEffect::Vibrato(
306                            Waveform::Sine,
307                            amount.try_change_base().unwrap(),
308                            vibrato_speed,
309                        )
310                    }
311                    0x8 => {
312                        PatternEffect::Panning(Num::new(slot.effect_parameter as i16 - 128) / 128)
313                    }
314                    0x5 | 0x6 | 0xA => {
315                        let first = effect_parameter >> 4;
316                        let second = effect_parameter & 0xF;
317
318                        if first == 0 {
319                            PatternEffect::VolumeSlide(
320                                -Num::new(second as i16) / 64,
321                                slot.effect_type == 0x6,
322                            )
323                        } else {
324                            PatternEffect::VolumeSlide(
325                                Num::new(first as i16) / 64,
326                                slot.effect_type == 0x6,
327                            )
328                        }
329                    }
330                    0x9 => PatternEffect::SampleOffset(effect_parameter as u16 * 256),
331                    0xB => {
332                        let pattern_idx = slot.effect_parameter;
333
334                        jump = Some((
335                            channel_number,
336                            Jump::Position {
337                                pattern: pattern_idx,
338                            },
339                        ));
340
341                        PatternEffect::None
342                    }
343                    0xC => {
344                        if let Some((_, sample)) = maybe_note_and_sample {
345                            PatternEffect::Volume(
346                                (Num::new(slot.effect_parameter as i16) / 64) * sample.volume,
347                            )
348                        } else {
349                            PatternEffect::None
350                        }
351                    }
352                    0xD => {
353                        // NOTE: this field is generally interpreted as decimal.
354                        let first = slot.effect_parameter >> 4;
355                        let second = slot.effect_parameter & 0xF;
356                        let row_idx = first * 10 + second;
357
358                        let pattern_break = Jump::PatternBreak { row: row_idx };
359
360                        // if to the *right* of 0xD effect, make combined
361                        if let Some((idx, Jump::Position { pattern })) = jump {
362                            jump = Some((
363                                idx,
364                                Jump::Combined {
365                                    pattern,
366                                    row: row_idx,
367                                },
368                            ))
369                        } else {
370                            jump = Some((channel_number, pattern_break));
371                        }
372
373                        PatternEffect::None
374                    }
375                    0xE => match slot.effect_parameter >> 4 {
376                        0x1 => {
377                            let c4_speed: Num<u32, 12> =
378                                note_to_speed(Note::C4, 0.0, 0, module.frequency_type)
379                                    .change_base();
380                            let speed: Num<u32, 12> = note_to_speed(
381                                Note::C4,
382                                effect_parameter as f64 * 8.0,
383                                0,
384                                module.frequency_type,
385                            )
386                            .change_base();
387
388                            let portamento_amount = speed / c4_speed;
389
390                            PatternEffect::FinePortamento(
391                                portamento_amount.try_change_base().unwrap(),
392                            )
393                        }
394                        0x2 => {
395                            let c4_speed = note_to_speed(Note::C4, 0.0, 0, module.frequency_type);
396                            let speed = note_to_speed(
397                                Note::C4,
398                                effect_parameter as f64 * 8.0,
399                                0,
400                                module.frequency_type,
401                            );
402
403                            let portamento_amount = c4_speed / speed;
404
405                            PatternEffect::FinePortamento(
406                                portamento_amount.try_change_base().unwrap(),
407                            )
408                        }
409
410                        0x8 => PatternEffect::Panning(
411                            Num::new(((slot.effect_parameter & 0xf) as i16) - 8) / 8,
412                        ),
413                        0x9 => {
414                            let retrigger_amount = slot.effect_parameter & 0xf;
415                            let modified_amount = if retrigger_amount == 0 {
416                                if let Some((_, previous_retrigger)) =
417                                    previous_retriggers[channel_number]
418                                {
419                                    previous_retrigger
420                                } else {
421                                    1
422                                }
423                            } else {
424                                previous_retriggers[channel_number] =
425                                    Some((RetriggerVolumeChange::NoChange, retrigger_amount));
426                                retrigger_amount
427                            };
428
429                            PatternEffect::Retrigger(
430                                RetriggerVolumeChange::NoChange,
431                                modified_amount,
432                            )
433                        }
434                        0xA => PatternEffect::FineVolumeSlide(
435                            Num::new((slot.effect_parameter & 0xf) as i16) / 128,
436                        ),
437                        0xB => PatternEffect::FineVolumeSlide(
438                            -Num::new((slot.effect_parameter & 0xf) as i16) / 128,
439                        ),
440                        0xC => PatternEffect::NoteCut((slot.effect_parameter & 0xf).into()),
441                        0xD => PatternEffect::NoteDelay((slot.effect_parameter & 0xf).into()),
442                        u => {
443                            eprintln!("Unsupported extended effect E{u:X}y");
444                            PatternEffect::None
445                        }
446                    },
447                    0xF => match slot.effect_parameter {
448                        0 => PatternEffect::SetTicksPerStep(u32::MAX),
449                        1..=0x20 => PatternEffect::SetTicksPerStep(slot.effect_parameter as u32),
450                        0x21.. => PatternEffect::SetFramesPerTick(bpm_to_frames_per_tick(
451                            slot.effect_parameter as u32,
452                        )),
453                    },
454                    // G
455                    0x10 => PatternEffect::SetGlobalVolume(
456                        Num::new(slot.effect_parameter as i32) / 0x40,
457                    ),
458                    // H
459                    0x11 => {
460                        let first = effect_parameter >> 4;
461                        let second = effect_parameter & 0xF;
462
463                        if first == 0 {
464                            PatternEffect::GlobalVolumeSlide(-Num::new(second as i32) / 0x40)
465                        } else {
466                            PatternEffect::GlobalVolumeSlide(Num::new(first as i32) / 0x40)
467                        }
468                    }
469                    // R
470                    0x1B => {
471                        let first = effect_parameter >> 4;
472                        let second = effect_parameter & 0xF;
473
474                        let previous_retrigger = &mut previous_retriggers[channel_number];
475                        let volume_type = match first {
476                            0 => previous_retrigger
477                                .map(|retrigger| retrigger.0)
478                                .unwrap_or(RetriggerVolumeChange::NoChange),
479                            1 => RetriggerVolumeChange::DecreaseByOne,
480                            8 => RetriggerVolumeChange::NoChange,
481                            _ => {
482                                eprintln!("Unsupported retrigger effect volume {first}");
483                                RetriggerVolumeChange::NoChange
484                            }
485                        };
486
487                        let ticks_between_retriggers = if second == 0 {
488                            if let Some((_, previous_retrigger)) = previous_retrigger {
489                                *previous_retrigger
490                            } else {
491                                1
492                            }
493                        } else {
494                            second
495                        };
496
497                        *previous_retrigger = Some((volume_type, ticks_between_retriggers));
498
499                        PatternEffect::Retrigger(volume_type, ticks_between_retriggers)
500                    }
501                    e => {
502                        let effect_char = char::from_digit(e as u32, 36)
503                            .unwrap_or('?')
504                            .to_ascii_uppercase();
505                        eprintln!("Unsupported effect {effect_char}xy");
506
507                        PatternEffect::None
508                    }
509                };
510
511                if sample == 0
512                    || matches!(effect2, PatternEffect::TonePortamento(_, _))
513                    || matches!(effect1, PatternEffect::Stop)
514                {
515                    pattern_data.push(agb_tracker_interop::PatternSlot {
516                        speed: 0.into(),
517                        sample: 0,
518                        effect1,
519                        effect2,
520                    });
521                } else {
522                    let sample_played = &samples[sample - 1];
523
524                    let speed = note_to_speed(
525                        slot.note,
526                        sample_played.fine_tune,
527                        sample_played.relative_note,
528                        module.frequency_type,
529                    );
530
531                    pattern_data.push(agb_tracker_interop::PatternSlot {
532                        speed: speed.try_change_base().unwrap(),
533                        sample: sample as u16,
534                        effect1,
535                        effect2,
536                    });
537                }
538            }
539            // At the last channel, evaluate the combined jump,
540            // and place at the first jump effect channel index
541            if let Some((jump_channel, jump)) = jump.take() {
542                let jump_effect = PatternEffect::Jump(jump);
543                let pattern_data_idx =
544                    pattern_data.len() - module.get_num_channels() + jump_channel;
545                if let Some(data) = pattern_data.get_mut(pattern_data_idx) {
546                    data.effect2 = jump_effect;
547                }
548            }
549        }
550
551        patterns.push(agb_tracker_interop::Pattern {
552            length: pattern.len(),
553            start_position: start_pos,
554        });
555    }
556
557    let samples: Vec<_> = samples
558        .iter()
559        .map(|sample| agb_tracker_interop::Sample {
560            data: sample.data.clone().into(),
561            should_loop: sample.should_loop,
562            restart_point: sample.restart_point,
563            volume: sample.volume,
564            volume_envelope: sample.envelope_id,
565            fadeout: sample.fadeout,
566        })
567        .collect();
568
569    let patterns_to_play = module.pattern_order.clone();
570
571    let envelopes = envelopes
572        .iter()
573        .map(|envelope| agb_tracker_interop::Envelope {
574            amount: envelope.amounts.clone().into(),
575            sustain: envelope.sustain,
576            loop_start: envelope.loop_start,
577            loop_end: envelope.loop_end,
578
579            vib_amount: envelope.vib_amount.try_change_base().unwrap(),
580            vib_waveform: envelope.vib_waveform,
581            vib_speed: envelope.vib_speed,
582        })
583        .collect::<Vec<_>>();
584
585    let frames_per_tick = bpm_to_frames_per_tick(module.default_bpm as u32);
586    let ticks_per_step = module.default_tempo;
587
588    agb_tracker_interop::Track {
589        samples: samples.into(),
590        pattern_data: pattern_data.into(),
591        patterns: patterns.into(),
592        num_channels: module.get_num_channels(),
593        patterns_to_play: patterns_to_play.into(),
594        envelopes: envelopes.into(),
595
596        frames_per_tick,
597        ticks_per_step: ticks_per_step.into(),
598        repeat: module.restart_position,
599    }
600}
601
602fn bpm_to_frames_per_tick(bpm: u32) -> Num<u32, 8> {
603    // Number 150 here deduced experimentally
604    Num::<u32, 8>::new(150) / bpm
605}
606
607fn note_to_speed(
608    note: Note,
609    fine_tune: f64,
610    relative_note: i8,
611    frequency_type: FrequencyType,
612) -> Num<u32, 12> {
613    let frequency = match frequency_type {
614        FrequencyType::LinearFrequencies => {
615            note_to_frequency_linear(note, fine_tune, relative_note)
616        }
617        FrequencyType::AmigaFrequencies => note_to_frequency_amiga(note, fine_tune, relative_note),
618    };
619
620    let gba_audio_frequency = 32768f64;
621
622    let speed = frequency / gba_audio_frequency;
623    Num::from_f64(speed)
624}
625
626fn note_to_frequency_linear(note: Note, fine_tune: f64, relative_note: i8) -> f64 {
627    let real_note = (note as usize as f64) + (relative_note as f64) - 1.0; // notes are 1 indexed but below is 0 indexed
628    let period = 10.0 * 12.0 * 16.0 * 4.0 - real_note * 16.0 * 4.0 - fine_tune / 2.0;
629    8363.0 * 2.0f64.powf((6.0 * 12.0 * 16.0 * 4.0 - period) / (12.0 * 16.0 * 4.0))
630}
631
632fn note_to_frequency_amiga(note: Note, fine_tune: f64, relative_note: i8) -> f64 {
633    let note = (note as usize)
634        .checked_add_signed(relative_note as isize)
635        .expect("Note gone negative");
636    let pos = ((note % 12) * 8 + (fine_tune / 16.0) as usize).min(AMIGA_FREQUENCIES.len() - 2);
637    let frac = (fine_tune / 16.0) - (fine_tune / 16.0).floor();
638
639    let period = ((AMIGA_FREQUENCIES[pos] as f64 * (1.0 - frac))
640        + AMIGA_FREQUENCIES[pos + 1] as f64 * frac)
641        * 32.0 // docs say 16 here, but for some reason I need 32 :/
642        / (1 << ((note as i64) / 12)) as f64;
643
644    8363.0 * 1712.0 / period
645}
646
647static AMIGA_FREQUENCIES: &[u32] = &[
648    907, 900, 894, 887, 881, 875, 868, 862, 856, 850, 844, 838, 832, 826, 820, 814, 808, 802, 796,
649    791, 785, 779, 774, 768, 762, 757, 752, 746, 741, 736, 730, 725, 720, 715, 709, 704, 699, 694,
650    689, 684, 678, 675, 670, 665, 660, 655, 651, 646, 640, 636, 632, 628, 623, 619, 614, 610, 604,
651    601, 597, 592, 588, 584, 580, 575, 570, 567, 563, 559, 555, 551, 547, 543, 538, 535, 532, 528,
652    524, 520, 516, 513, 508, 505, 502, 498, 494, 491, 487, 484, 480, 477, 474, 470, 467, 463, 460,
653    457,
654];
655
656#[derive(PartialEq, Eq, Hash, Clone, Debug)]
657struct EnvelopeData {
658    amounts: Vec<Num<i16, 8>>,
659    sustain: Option<usize>,
660    loop_start: Option<usize>,
661    loop_end: Option<usize>,
662
663    vib_waveform: Waveform,
664    vib_speed: u8,
665    vib_amount: Num<i32, 12>,
666}
667
668impl EnvelopeData {
669    fn new(
670        e: &xmrs::envelope::Envelope,
671        instrument: &xmrs::instr_default::InstrDefault,
672        frequency_type: FrequencyType,
673        bpm: u32,
674    ) -> Self {
675        let mut amounts = vec![];
676
677        for frame in 0..=(Self::envelope_frame_to_gba_frame(e.point.last().unwrap().frame, bpm)) {
678            let xm_frame = Self::gba_frame_to_envelope_frame(frame, bpm);
679            let index = e
680                .point
681                .iter()
682                .rposition(|point| point.frame < xm_frame)
683                .unwrap_or(0);
684
685            let first_point = &e.point[index];
686            let second_point = &e.point[index + 1];
687
688            let amount = EnvelopePoint::lerp(first_point, second_point, xm_frame);
689            let amount = Num::from_f32(amount);
690
691            amounts.push(amount);
692        }
693
694        let sustain = if e.sustain_enabled {
695            Some(Self::envelope_frame_to_gba_frame(
696                e.point[e.sustain_point].frame,
697                bpm,
698            ))
699        } else {
700            None
701        };
702        let (loop_start, loop_end) = if e.loop_enabled {
703            (
704                Some(Self::envelope_frame_to_gba_frame(
705                    e.point[e.loop_start_point].frame,
706                    bpm,
707                )),
708                Some(Self::envelope_frame_to_gba_frame(
709                    e.point[e.loop_end_point].frame,
710                    bpm,
711                )),
712            )
713        } else {
714            (None, None)
715        };
716
717        let vib_waveform = match instrument.vibrato.waveform {
718            xmrs::instr_vibrato::Waveform::Sine => Waveform::Sine,
719            xmrs::instr_vibrato::Waveform::Square => Waveform::Square,
720            xmrs::instr_vibrato::Waveform::RampUp => Waveform::Saw,
721            xmrs::instr_vibrato::Waveform::RampDown => Waveform::Saw,
722        };
723
724        let vib_speed = (instrument.vibrato.speed * 64.0) as u8;
725        let vib_depth = instrument.vibrato.depth * 8.0;
726
727        let c4_speed = note_to_speed(Note::C4, 0.0, 0, frequency_type);
728        let mut vib_amount =
729            (note_to_speed(Note::C4, vib_depth.into(), 0, frequency_type) / c4_speed - 1)
730                .try_change_base()
731                .unwrap();
732
733        if matches!(
734            instrument.vibrato.waveform,
735            xmrs::instr_vibrato::Waveform::RampDown
736        ) {
737            vib_amount = -vib_amount;
738        }
739
740        EnvelopeData {
741            amounts,
742            sustain,
743            loop_start,
744            loop_end,
745
746            vib_waveform,
747            vib_speed,
748            vib_amount,
749        }
750    }
751
752    fn envelope_frame_to_gba_frame(envelope_frame: usize, bpm: u32) -> usize {
753        // FT2 manual says number of ticks / second = BPM * 0.4
754        // somehow this works as a good approximation :/
755        (envelope_frame as u32 * 250 / bpm) as usize
756    }
757
758    fn gba_frame_to_envelope_frame(gba_frame: usize, bpm: u32) -> usize {
759        (gba_frame as u32 * bpm / 250) as usize
760    }
761}