├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── convert_to_wav.rs └── streaming_player.rs ├── mod_files ├── 1 step further.mod ├── 19xx.mod ├── BALLI.MOD ├── BOG_WRAITH.MOD ├── BUBBLE_BOBBLE.MOD ├── CHIP_SLAYER!.MOD ├── GSLINGER.MOD ├── JARRE.MOD ├── ballad_ej.mod ├── ballade_pour_adeline.mod ├── chcknbnk.mod ├── cream_of_the_earth.mod ├── overload.mod ├── sarcophaser.mod ├── spacedebris.mod ├── star-rai.mod ├── stardstm.mod ├── switchback.mod └── wasteland.mod ├── reg_expected.json ├── src ├── bin │ └── scan_play.rs ├── lib.rs ├── loader.rs ├── static_tables.rs └── textout.rs └── tests └── regression.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(Windows) streamer example", 9 | "type": "cppvsdbg", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/target/debug/examples/streaming_player.exe", 12 | "args": [], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceFolder}", 15 | "environment": [], 16 | "externalConsole": true, 17 | }, 18 | { 19 | "name": "(Windows) converter example", 20 | "type": "cppvsdbg", 21 | "request": "launch", 22 | "program": "${workspaceFolder}/target/debug/examples/convert_to_wav.exe", 23 | "args": [], 24 | "stopAtEntry": false, 25 | "cwd": "${workspaceFolder}", 26 | "environment": [], 27 | "externalConsole": true, 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug.allowBreakpointsEverywhere": true 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "cargo", 8 | "subcommand": "test", 9 | "problemMatcher": [ 10 | "$rustc" 11 | ], 12 | "group": { 13 | "kind": "test", 14 | "isDefault": true 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mod_player" 3 | version = "0.1.4" 4 | authors = ["Jani Peltonen"] 5 | edition = "2018" 6 | description = "A library for parsing and playing mod music files" 7 | keyword = [ "mod", "audio", "amiga", "player", "music" ] 8 | categories = [ "games", "multimedia::audio" ] 9 | license = "MIT" 10 | repository = "https://github.com/janiorca/mod_player" 11 | readme = "readme.md" 12 | 13 | [dev-dependencies] 14 | cpal = "0.11.0" 15 | hound = "3.4.0" 16 | crc = "1.8.1" 17 | serde = { version = "1.0.90", features = ["derive"] } 18 | serde_json = "1.0.39" 19 | 20 | [dependencies] 21 | 22 | 23 | [rust] 24 | backtrace = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 janiorca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mod_player is a crate that reads and plays back mod audio files. The mod_player decodes the audio one sample pair (left,right) at a time 2 | that can be streamed to an audio device or a file. 3 | 4 | For playback, only two functions are needed; 5 | * read_mod_file to read the file into a Song structure 6 | * next_sample to get the next sample 7 | 8 | To use the library to decode a mod file and save it to disk ( using the hound audio crate for WAV saving ) 9 | 10 | ```rust 11 | use hound; 12 | 13 | fn main() { 14 | let spec = hound::WavSpec { 15 | channels: 2, 16 | sample_rate: 48100, 17 | bits_per_sample: 32, 18 | sample_format: hound::SampleFormat::Float, 19 | }; 20 | 21 | let mut writer = hound::WavWriter::create( "out.wav", spec).unwrap(); 22 | let song = mod_player::read_mod_file("BUBBLE_BOBBLE.MOD"); 23 | let mut player_state : mod_player::PlayerState = mod_player::PlayerState::new( 24 | song.format.num_channels, spec.sample_rate ); 25 | loop { 26 | let ( left, right ) = mod_player::next_sample(&song, &mut player_state); 27 | writer.write_sample( left ); 28 | writer.write_sample( right ); 29 | if player_state.song_has_ended || player_state.has_looped { 30 | break; 31 | } 32 | } 33 | } 34 | ``` -------------------------------------------------------------------------------- /examples/convert_to_wav.rs: -------------------------------------------------------------------------------- 1 | use hound; 2 | 3 | fn main() { 4 | let spec = hound::WavSpec { 5 | channels: 2, 6 | sample_rate: 48100, 7 | bits_per_sample: 32, 8 | sample_format: hound::SampleFormat::Float, 9 | }; 10 | 11 | let mut writer = hound::WavWriter::create("out.wav", spec).unwrap(); 12 | let song = mod_player::read_mod_file("mod_files/CHIP_SLAYER!.MOD"); 13 | mod_player::textout::print_song_info(&song); 14 | let mut player_state: mod_player::PlayerState = 15 | mod_player::PlayerState::new(song.format.num_channels, spec.sample_rate); 16 | loop { 17 | let (left, right) = mod_player::next_sample(&song, &mut player_state); 18 | writer.write_sample(left); 19 | writer.write_sample(right); 20 | if player_state.song_has_ended || player_state.has_looped { 21 | break; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/streaming_player.rs: -------------------------------------------------------------------------------- 1 | extern crate cpal; 2 | use cpal::traits::{DeviceTrait,EventLoopTrait, HostTrait}; 3 | use cpal::{StreamData,UnknownTypeOutputBuffer}; 4 | use std::sync; 5 | use std::sync::mpsc; 6 | use std::thread; 7 | 8 | enum PlayerCommand { 9 | Stop {}, 10 | } 11 | 12 | fn setup_stream(song: sync::Arc) -> mpsc::Sender { 13 | let host = cpal::default_host(); 14 | let device = host.default_output_device().expect("Failed to get default output device"); 15 | 16 | let format = device 17 | .default_output_format() 18 | .expect("Failed to get default output format"); 19 | let fmt = match format.data_type { 20 | cpal::SampleFormat::I16 => "i16", 21 | cpal::SampleFormat::U16 => "u16", 22 | cpal::SampleFormat::F32 => "f32", 23 | }; 24 | println!( 25 | "Sample rate: {} Sample format: {} Channels: {}", 26 | format.sample_rate.0, fmt, format.channels 27 | ); 28 | 29 | let event_loop = host.event_loop(); 30 | let stream_id = event_loop.build_output_stream(&device, &format).unwrap(); 31 | event_loop.play_stream(stream_id.clone()); 32 | 33 | let mut player_state: mod_player::PlayerState = 34 | mod_player::PlayerState::new(song.format.num_channels, format.sample_rate.0); 35 | let mut last_line_pos = 9999; 36 | let (tx, _rx) = mpsc::channel(); 37 | thread::spawn(move || { 38 | event_loop.run(move |_, result| { 39 | if player_state.current_line != last_line_pos { 40 | if player_state.current_line == 0 { 41 | println!(""); 42 | } 43 | print!( 44 | "{:>2}:{:>2} ", 45 | player_state.song_pattern_position, player_state.current_line 46 | ); 47 | mod_player::textout::print_line(player_state.get_song_line(&song)); 48 | last_line_pos = player_state.current_line; 49 | } 50 | let stream_data = match result { 51 | Ok(data) => data, 52 | Err(err) => { 53 | eprintln!("an error occurred on stream {:?}: {}", stream_id, err); 54 | return; 55 | } 56 | _ => return, 57 | }; 58 | match stream_data { 59 | StreamData::Output { 60 | buffer: UnknownTypeOutputBuffer::F32(mut buffer), 61 | } => { 62 | for sample in buffer.chunks_mut(format.channels as usize) { 63 | let (left, right) = mod_player::next_sample(&song, &mut player_state); 64 | sample[0] = left; 65 | sample[1] = right; 66 | } 67 | } 68 | _ => (), 69 | } 70 | }); 71 | }); 72 | tx 73 | } 74 | 75 | fn main() { 76 | let song = sync::Arc::new(mod_player::read_mod_file("mod_files/chcknbnk.mod")); 77 | 78 | mod_player::textout::print_song_info(&song); 79 | let _tx = setup_stream(song.clone()); 80 | loop { 81 | let mut command = String::new(); 82 | std::io::stdin().read_line(&mut command); 83 | return; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /mod_files/1 step further.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/1 step further.mod -------------------------------------------------------------------------------- /mod_files/19xx.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/19xx.mod -------------------------------------------------------------------------------- /mod_files/BALLI.MOD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/BALLI.MOD -------------------------------------------------------------------------------- /mod_files/BOG_WRAITH.MOD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/BOG_WRAITH.MOD -------------------------------------------------------------------------------- /mod_files/BUBBLE_BOBBLE.MOD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/BUBBLE_BOBBLE.MOD -------------------------------------------------------------------------------- /mod_files/CHIP_SLAYER!.MOD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/CHIP_SLAYER!.MOD -------------------------------------------------------------------------------- /mod_files/GSLINGER.MOD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/GSLINGER.MOD -------------------------------------------------------------------------------- /mod_files/JARRE.MOD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/JARRE.MOD -------------------------------------------------------------------------------- /mod_files/ballad_ej.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/ballad_ej.mod -------------------------------------------------------------------------------- /mod_files/ballade_pour_adeline.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/ballade_pour_adeline.mod -------------------------------------------------------------------------------- /mod_files/chcknbnk.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/chcknbnk.mod -------------------------------------------------------------------------------- /mod_files/cream_of_the_earth.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/cream_of_the_earth.mod -------------------------------------------------------------------------------- /mod_files/overload.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/overload.mod -------------------------------------------------------------------------------- /mod_files/sarcophaser.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/sarcophaser.mod -------------------------------------------------------------------------------- /mod_files/spacedebris.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/spacedebris.mod -------------------------------------------------------------------------------- /mod_files/star-rai.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/star-rai.mod -------------------------------------------------------------------------------- /mod_files/stardstm.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/stardstm.mod -------------------------------------------------------------------------------- /mod_files/switchback.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/switchback.mod -------------------------------------------------------------------------------- /mod_files/wasteland.mod: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janiorca/mod_player/7dde5952e4cb6ce4359c8470da17ade7843adaa4/mod_files/wasteland.mod -------------------------------------------------------------------------------- /reg_expected.json: -------------------------------------------------------------------------------- 1 | { 2 | "song_checksums": { 3 | "GSLINGER.MOD": 11044796265083751873, 4 | "cream_of_the_earth.mod": 2531274292700977346, 5 | "ballad_ej.mod": 3088559176277653008, 6 | "overload.mod": 8291155814300728607, 7 | "sarcophaser.mod": 6600761910284789959, 8 | "19xx.mod": 4043859774360714502, 9 | "chcknbnk.mod": 17205351901250905565, 10 | "1 step further.MOD": 1544263850974986975, 11 | "BUBBLE_BOBBLE.MOD": 102582253281752130, 12 | "BOG_WRAITH.mod": 8434637554607953124, 13 | "BALLI.MOD": 17209538200570288517, 14 | "switchback.mod": 7281711966600916815, 15 | "stardstm.MOD": 10178888233402482210, 16 | "wasteland.mod": 15576526782190603867, 17 | "JARRE.mod": 12596620841227597147, 18 | "ballade_pour_adeline.MOD": 8131928364582226168, 19 | "star-rai.mod": 12438005807597964324, 20 | "CHIP_SLAYER!.MOD": 17676714724179657864 21 | } 22 | } -------------------------------------------------------------------------------- /src/bin/scan_play.rs: -------------------------------------------------------------------------------- 1 | /* 2 | Scans for mods in the directory and tries to decode all of them. Used to quickly find problem mod files 3 | */ 4 | use std::env; 5 | use std::path::Path; 6 | 7 | fn find_mods(path: &Path) -> Vec { 8 | let mut mods: Vec = Vec::new(); 9 | for entry in path.read_dir().expect("can't get path") { 10 | let clean_entry = entry.expect("not found"); 11 | let path = clean_entry.path(); 12 | if path.is_dir() { 13 | let mut sub_path_mods = find_mods(path.as_path()); 14 | mods.append(&mut sub_path_mods); 15 | } else { 16 | let path_str = path.to_str().expect("Bad string"); 17 | let path_string = String::from(path_str); 18 | let parts: Vec<&str> = path_string.split('.').collect(); 19 | if parts.len() > 1 { 20 | let extension = parts.last().expect("cant get file extension"); 21 | if extension.eq_ignore_ascii_case("MOD") { 22 | mods.push(path_string); 23 | } 24 | } 25 | } 26 | } 27 | mods 28 | } 29 | 30 | fn main() { 31 | // let path = env::current_dir().expect("failed to get current directory"); 32 | let path = std::path::PathBuf::from("C:/work/mods"); 33 | // let path = std::path::PathBuf::from("C:/work/crate/mod_player/mod_files"); 34 | println!("The current directory is {}", path.display()); 35 | let mods: Vec = find_mods(path.as_path()); 36 | for mod_name in mods { 37 | println!("Processing: {}", mod_name); 38 | let song = mod_player::read_mod_file(&mod_name); 39 | mod_player::textout::print_song_info(&song); 40 | // mod_player::textout:: 41 | let mut f = 0.0; 42 | let mut player_state: mod_player::PlayerState = 43 | mod_player::PlayerState::new(song.format.num_channels, 48100); 44 | println!("Start play loop for: {}", mod_name); 45 | loop { 46 | let (left, right) = mod_player::next_sample(&song, &mut player_state); 47 | f += left + right; 48 | if player_state.song_has_ended || player_state.has_looped { 49 | break; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # mod_player 2 | //! 3 | //! mod_player is a crate that reads and plays back mod audio files. The mod_player decodes the audio one sample pair (left,right) at a time 4 | //! that can be streamed to an audio device or a file. 5 | //! 6 | //! For playback, only two functions are needed; 7 | //! * read_mod_file to read the file into a Song structure 8 | //! * next_sample to get the next sample 9 | //! 10 | //! To use the library to decode a mod file and save it to disk ( using the hound audio crate for WAV saving ) 11 | //! 12 | //! ```rust 13 | //! use hound; 14 | //! 15 | //! fn main() { 16 | //! let spec = hound::WavSpec { 17 | //! channels: 2, 18 | //! sample_rate: 48100, 19 | //! bits_per_sample: 32, 20 | //! sample_format: hound::SampleFormat::Float, 21 | //! }; 22 | //! 23 | //! let mut writer = hound::WavWriter::create( "out.wav", spec).unwrap(); 24 | //! let song = mod_player::read_mod_file("mod_files/BUBBLE_BOBBLE.MOD"); 25 | //! let mut player_state : mod_player::PlayerState = mod_player::PlayerState::new( 26 | //! song.format.num_channels, spec.sample_rate ); 27 | //! loop { 28 | //! let ( left, right ) = mod_player::next_sample(&song, &mut player_state); 29 | //! writer.write_sample( left ); 30 | //! writer.write_sample( right ); 31 | //! if player_state.song_has_ended || player_state.has_looped { 32 | //! break; 33 | //! } 34 | //! } 35 | //! } 36 | //! ``` 37 | 38 | #[cfg(test)] 39 | mod tests { 40 | #[test] 41 | fn it_works() { 42 | assert_eq!(2 + 2, 4); 43 | } 44 | } 45 | 46 | mod loader; 47 | pub use loader::read_mod_file; 48 | pub use loader::read_mod_file_slice; 49 | mod static_tables; 50 | pub mod textout; 51 | 52 | const CLOCK_TICKS_PERS_SECOND: f32 = 3579545.0; // Amiga hw clcok ticks per second 53 | // const CLOCK_TICKS_PERS_SECOND: f32 = 3579545.0; // NTSC 54 | 55 | fn fine_tune_period(period: u32, fine_tune: u32, use_fine_tune_table: bool) -> u32 { 56 | if use_fine_tune_table { 57 | let index: i32 = static_tables::FREQUENCY_TABLE 58 | .binary_search(&period) 59 | .expect("Unexpected period value") as i32; 60 | return static_tables::FINE_TUNE_TABLE[fine_tune as usize][index as usize]; 61 | } else { 62 | return (period as f32 * static_tables::SCALE_FINE_TUNE[fine_tune as usize]) as u32; 63 | } 64 | } 65 | 66 | /// Holds the info and sample data for a sample 67 | pub struct Sample { 68 | name: String, 69 | size: u32, 70 | volume: u8, 71 | fine_tune: u8, 72 | repeat_offset: u32, 73 | repeat_size: u32, 74 | samples: Vec, 75 | } 76 | 77 | impl Sample { 78 | fn new(sample_info: &[u8]) -> Sample { 79 | let sample_name = String::from_utf8_lossy(&sample_info[0..22]); 80 | let sample_size: u32 = ((sample_info[23] as u32) + (sample_info[22] as u32) * 256) * 2; 81 | let fine_tune = sample_info[24]; 82 | let volume = sample_info[25]; 83 | 84 | // the repeat offset appears to be in bytes ... 85 | let mut repeat_offset: u32 = 86 | ((sample_info[27] as u32) + (sample_info[26] as u32) * 256) * 2; 87 | // .. but the size is in word? 88 | let repeat_size: u32 = ((sample_info[29] as u32) + (sample_info[28] as u32) * 256) * 2; 89 | 90 | if sample_size > 0 { 91 | if repeat_offset + repeat_size > sample_size { 92 | repeat_offset -= (repeat_offset + repeat_size) - sample_size; 93 | } 94 | } 95 | 96 | Sample { 97 | name: String::from(sample_name), 98 | size: sample_size, 99 | volume: volume, 100 | fine_tune: fine_tune, 101 | repeat_offset: repeat_offset, 102 | repeat_size: repeat_size, 103 | samples: Vec::new(), 104 | } 105 | } 106 | } 107 | 108 | enum Effect { 109 | /* SetPanningPosition = 8, 110 | */ 111 | None, // 0 112 | Arpeggio { 113 | chord_offset_1: u8, 114 | chord_offset_2: u8, 115 | }, 116 | SlideUp { 117 | speed: u8, 118 | }, // 1 119 | SlideDown { 120 | speed: u8, 121 | }, // 2 122 | TonePortamento { 123 | speed: u8, 124 | }, // 3 125 | Vibrato { 126 | speed: u8, 127 | amplitude: u8, 128 | }, // 4 129 | TonePortamentoVolumeSlide { 130 | volume_change: i8, 131 | }, //5 132 | VibratoVolumeSlide { 133 | volume_change: i8, 134 | }, // 6 135 | Tremolo { 136 | speed: u8, 137 | amplitude: u8, 138 | }, // 7 139 | Pan { 140 | position: u8, 141 | }, // 8 142 | SetSampleOffset { 143 | offset: u8, 144 | }, // 9 145 | VolumeSlide { 146 | volume_change: i8, 147 | }, // 10 148 | PositionJump { 149 | next_pattern: u8, 150 | }, // 11, 151 | SetVolume { 152 | volume: u8, 153 | }, // 12 154 | PatternBreak { 155 | next_pattern_pos: u8, 156 | }, //13 157 | SetSpeed { 158 | speed: u8, 159 | }, // 15 160 | 161 | SetHardwareFilter { 162 | new_state: u8, 163 | }, //E0 164 | FinePortaUp { 165 | period_change: u8, 166 | }, //E1 167 | FinePortaDown { 168 | period_change: u8, 169 | }, //E2 170 | Glissando { 171 | use_smooth_slide: bool, 172 | }, //E2 173 | PatternLoop { 174 | arg: u8, 175 | }, //E6 176 | TremoloWaveform { 177 | wave: u8, 178 | }, // E7 179 | CoarsePan { 180 | pan_pos: u8, 181 | }, //E8 182 | RetriggerSample { 183 | retrigger_delay: u8, 184 | }, //E9 185 | FineVolumeSlideUp { 186 | volume_change: u8, 187 | }, //EA 188 | FineVolumeSlideDown { 189 | volume_change: u8, 190 | }, //EB 191 | CutNote { 192 | delay: u8, 193 | }, //EC 194 | DelayedSample { 195 | delay_ticks: u8, 196 | }, //ED 197 | DelayedLine { 198 | delay_ticks: u8, 199 | }, //EE 200 | InvertLoop { 201 | loop_position: u8, 202 | }, //EF 203 | SetVibratoWave { 204 | wave: u8, 205 | }, 206 | SetFineTune { 207 | fine_tune: u8, 208 | }, 209 | } 210 | 211 | impl Effect { 212 | fn new(effect_number: u8, effect_argument: i8) -> Effect { 213 | match effect_number { 214 | 0 => match effect_argument { 215 | 0 => Effect::None, 216 | _ => Effect::Arpeggio { 217 | chord_offset_1: effect_argument as u8 >> 4, 218 | chord_offset_2: effect_argument as u8 & 0x0f, 219 | }, 220 | // _ => panic!( format!( "unhandled arpeggio effect: {}", effect_number ) ) 221 | }, 222 | 1 => Effect::SlideUp { 223 | speed: effect_argument as u8, 224 | }, // decrease period, increase frequency, higher note 225 | 2 => Effect::SlideDown { 226 | speed: effect_argument as u8, 227 | }, 228 | 3 => Effect::TonePortamento { 229 | speed: effect_argument as u8, 230 | }, 231 | 4 => Effect::Vibrato { 232 | speed: effect_argument as u8 >> 4, 233 | amplitude: effect_argument as u8 & 0x0f, 234 | }, 235 | 5 => { 236 | if (effect_argument as u8 & 0xf0) != 0 { 237 | Effect::TonePortamentoVolumeSlide { 238 | volume_change: effect_argument >> 4, 239 | } 240 | } else { 241 | Effect::TonePortamentoVolumeSlide { 242 | volume_change: -effect_argument, 243 | } 244 | } 245 | } 246 | 6 => { 247 | if (effect_argument as u8 & 0xf0) != 0 { 248 | Effect::VibratoVolumeSlide { 249 | volume_change: effect_argument >> 4, 250 | } 251 | } else { 252 | Effect::VibratoVolumeSlide { 253 | volume_change: -effect_argument, 254 | } 255 | } 256 | } 257 | 7 => Effect::Tremolo { 258 | speed: effect_argument as u8 >> 4, 259 | amplitude: effect_argument as u8 & 0x0f, 260 | }, 261 | 8 => Effect::Pan { 262 | position: effect_argument as u8, 263 | }, 264 | 9 => Effect::SetSampleOffset { 265 | offset: effect_argument as u8, 266 | }, 267 | 10 => { 268 | if (effect_argument as u8 & 0xf0) != 0 { 269 | Effect::VolumeSlide { 270 | volume_change: effect_argument >> 4, 271 | } 272 | } else { 273 | Effect::VolumeSlide { 274 | volume_change: -effect_argument, 275 | } 276 | } 277 | } 278 | 11 => Effect::PositionJump { 279 | next_pattern: effect_argument as u8, 280 | }, 281 | 12 => Effect::SetVolume { 282 | volume: effect_argument as u8, 283 | }, 284 | 13 => Effect::PatternBreak { 285 | next_pattern_pos: (((0xf0 & (effect_argument as u32)) >> 4) * 10 286 | + (effect_argument as u32 & 0x0f)) as u8, 287 | }, 288 | 14 => { 289 | let extended_effect = (effect_argument as u8) >> 4; 290 | let extended_argument = (effect_argument as u8) & 0x0f; 291 | match extended_effect { 292 | 0 => Effect::SetHardwareFilter { 293 | new_state: extended_argument as u8, 294 | }, 295 | 1 => Effect::FinePortaUp { 296 | period_change: extended_argument as u8, 297 | }, 298 | 2 => Effect::FinePortaDown { 299 | period_change: extended_argument as u8, 300 | }, 301 | 3 => Effect::Glissando { 302 | use_smooth_slide: extended_argument != 0, 303 | }, 304 | 4 => Effect::SetVibratoWave { 305 | wave: extended_argument, 306 | }, 307 | 5 => Effect::SetFineTune { 308 | fine_tune: extended_argument, 309 | }, 310 | 6 => Effect::PatternLoop { 311 | arg: extended_argument as u8, 312 | }, 313 | 7 => Effect::TremoloWaveform { 314 | wave: extended_argument as u8, 315 | }, 316 | 8 => Effect::CoarsePan { 317 | pan_pos: extended_argument as u8, 318 | }, 319 | 9 => Effect::RetriggerSample { 320 | retrigger_delay: extended_argument as u8, 321 | }, 322 | 10 => Effect::FineVolumeSlideUp { 323 | volume_change: extended_argument as u8, 324 | }, 325 | 11 => Effect::FineVolumeSlideDown { 326 | volume_change: extended_argument as u8, 327 | }, 328 | 12 => Effect::CutNote { 329 | delay: extended_argument as u8, 330 | }, 331 | 13 => Effect::DelayedSample { 332 | delay_ticks: extended_argument as u8, 333 | }, 334 | 14 => Effect::DelayedLine { 335 | delay_ticks: extended_argument as u8, 336 | }, 337 | 15 => Effect::InvertLoop { 338 | loop_position: extended_argument as u8, 339 | }, 340 | _ => panic!(format!( 341 | "unhandled extended effect number: {}", 342 | extended_effect 343 | )), 344 | } 345 | } 346 | 15 => Effect::SetSpeed { 347 | speed: effect_argument as u8, 348 | }, 349 | _ => panic!(format!("unhandled effect number: {}", effect_number)), 350 | } 351 | } 352 | } 353 | 354 | /// Describes what sound sample to play and an effect (if any) that should be applied. 355 | pub struct Note { 356 | sample_number: u8, 357 | period: u32, 358 | /// how many clock ticks each sample is held for 359 | effect: Effect, 360 | } 361 | 362 | fn change_note(current_period: u32, change: i32) -> u32 { 363 | // find note in frequency table 364 | let mut result = current_period as i32 + change; 365 | if result > 856 { 366 | result = 856; 367 | } 368 | if result < 113 { 369 | result = 113; 370 | } 371 | result as u32 372 | } 373 | 374 | impl Note { 375 | fn new(note_data: &[u8], format_description: &FormatDescription) -> Note { 376 | let mut sample_number = ((note_data[2] & 0xf0) >> 4) + (note_data[0] & 0xf0); 377 | if format_description.num_samples == 15 { 378 | sample_number = sample_number & 0x0f; 379 | } else { 380 | sample_number = sample_number & 0x1f; 381 | } 382 | let period = ((note_data[0] & 0x0f) as u32) * 256 + (note_data[1] as u32); 383 | let effect_argument = note_data[3] as i8; 384 | let effect_number = note_data[2] & 0x0f; 385 | let effect = Effect::new(effect_number, effect_argument); 386 | 387 | Note { 388 | sample_number, 389 | period, 390 | effect, 391 | } 392 | } 393 | } 394 | 395 | pub struct Pattern { 396 | lines: Vec>, // outer vector is the lines (64). Inner vector holds the notes for the line 397 | } 398 | 399 | impl Pattern { 400 | fn new() -> Pattern { 401 | let mut lines: Vec> = Vec::new(); 402 | for _line in 0..64 { 403 | lines.push(Vec::new()); 404 | } 405 | Pattern { lines } 406 | } 407 | } 408 | 409 | /// The features of the song 410 | pub struct FormatDescription { 411 | pub num_channels: u32, 412 | pub num_samples: u32, 413 | /// Is the format description based on a tag. Most mod file have a tag descriptor that makes it possible to identify the file with some 414 | /// certainty. The very earliest files do not have a tag but are assumed to support 4 channels and 15 samples. 415 | pub has_tag: bool, 416 | } 417 | 418 | /// Contains the entire mod song 419 | pub struct Song { 420 | /// The name of the song as specified in the mod file 421 | pub name: String, 422 | /// Features of the song 423 | pub format: FormatDescription, 424 | /// The audio samples used by the song 425 | pub samples: Vec, 426 | /// Patterns contain all the note data 427 | pub patterns: Vec, 428 | /// Specifies the order in whcih the patterns should be played in. The same pattern may played several times in the same song 429 | pub pattern_table: Vec, 430 | /// How many patterns in the pattern table should be used. The pattern table is a fixed length can usually longer than the song 431 | pub num_used_patterns: u32, 432 | /// Which pattern should be played after the last pattern in the pattern_table. Used for infinitely looping repeating songs 433 | pub end_position: u32, 434 | /// Set to true if all the notes are standard notes (i.e. conforming to the standard period table) 435 | pub has_standard_notes: bool, 436 | } 437 | 438 | struct ChannelInfo { 439 | sample_num: u8, // which sample is playing 440 | sample_pos: f32, 441 | period: u32, // 442 | fine_tune: u32, 443 | size: u32, 444 | volume: f32, // max 1.0 445 | volume_change: f32, // max 1.0 446 | note_change: i32, 447 | period_target: u32, // note portamento target 448 | last_porta_speed: i32, // last portamento to note and speed parameters 449 | last_porta_target: u32, // must be tracked separately to porta up and down ( and these may be referred to several lines later) 450 | 451 | base_period: u32, // the untuned period. The same value as the last valid note period value 452 | vibrato_pos: u32, 453 | vibrato_speed: u32, 454 | vibrato_depth: i32, 455 | 456 | tremolo_pos: u32, 457 | tremolo_speed: u32, 458 | tremolo_depth: i32, 459 | 460 | retrigger_delay: u32, 461 | retrigger_counter: u32, 462 | 463 | cut_note_delay: u32, 464 | arpeggio_counter: u32, 465 | arpeggio_offsets: [u32; 2], 466 | } 467 | 468 | impl ChannelInfo { 469 | fn new() -> ChannelInfo { 470 | ChannelInfo { 471 | sample_num: 0, 472 | sample_pos: 0.0, 473 | period: 0, 474 | fine_tune: 0, 475 | size: 0, 476 | volume: 0.0, 477 | volume_change: 0.0, 478 | note_change: 0, 479 | period_target: 0, 480 | last_porta_speed: 0, 481 | last_porta_target: 0, 482 | 483 | base_period: 0, 484 | vibrato_pos: 0, 485 | vibrato_speed: 0, 486 | vibrato_depth: 0, 487 | 488 | tremolo_pos: 0, 489 | tremolo_speed: 0, 490 | tremolo_depth: 0, 491 | 492 | retrigger_delay: 0, 493 | retrigger_counter: 0, 494 | cut_note_delay: 0, 495 | arpeggio_counter: 0, 496 | arpeggio_offsets: [0, 0], 497 | } 498 | } 499 | } 500 | /// Keeps track of all the dynamic state required for playing the song. 501 | pub struct PlayerState { 502 | channels: Vec, 503 | // where in the pattern table are we currently 504 | pub song_pattern_position: u32, 505 | /// current line position in the pattern. Every pattern has 64 lines 506 | pub current_line: u32, 507 | /// set when the song stops playing 508 | pub song_has_ended: bool, 509 | /// set when the song loops. The player does not unset this flag after it has been set. To detect subsequent loops the flag to be manually unset by the client 510 | pub has_looped: bool, 511 | device_sample_rate: u32, 512 | song_speed: u32, // in vblanks 513 | current_vblank: u32, // how many vblanks since last play line 514 | samples_per_vblank: u32, // how many device samples per 'vblank' 515 | clock_ticks_per_device_sample: f32, // how many amiga hardware clock ticks per device sample 516 | current_vblank_sample: u32, // how many device samples have we played for the current 'vblank' 517 | 518 | next_pattern_pos: i32, // on next line if == -1 do nothing else go to next pattern on line next_pattern_pos 519 | next_position: i32, // on next line if == 1 do nothing else go to beginning of the this pattern 520 | delay_line: u32, // how many extra ticks to delay before playing next line 521 | 522 | pattern_loop_position: Option, // set if we have a good position to loop to 523 | pattern_loop: i32, 524 | set_pattern_position: bool, // set to jump 525 | } 526 | 527 | impl PlayerState { 528 | pub fn new(num_channels: u32, device_sample_rate: u32) -> PlayerState { 529 | let mut channels = Vec::new(); 530 | for _channel in 0..num_channels { 531 | channels.push(ChannelInfo::new()) 532 | } 533 | PlayerState { 534 | channels, 535 | song_pattern_position: 0, 536 | current_line: 0, 537 | current_vblank: 0, 538 | current_vblank_sample: 0, 539 | device_sample_rate: device_sample_rate, 540 | song_speed: 6, 541 | samples_per_vblank: device_sample_rate / 50, 542 | clock_ticks_per_device_sample: CLOCK_TICKS_PERS_SECOND / device_sample_rate as f32, 543 | next_pattern_pos: -1, 544 | next_position: -1, 545 | delay_line: 0, 546 | song_has_ended: false, 547 | has_looped: false, 548 | 549 | pattern_loop_position: None, 550 | pattern_loop: 0, 551 | set_pattern_position: false, 552 | } 553 | } 554 | 555 | pub fn get_song_line<'a>(&self, song: &'a Song) -> &'a Vec { 556 | let pattern_idx = song.pattern_table[self.song_pattern_position as usize]; 557 | let pattern = &song.patterns[pattern_idx as usize]; 558 | let line = &pattern.lines[self.current_line as usize]; 559 | line 560 | } 561 | } 562 | 563 | fn play_note(note: &Note, player_state: &mut PlayerState, channel_num: usize, song: &Song) { 564 | let channel = &mut player_state.channels[channel_num]; 565 | 566 | let old_period = channel.period; 567 | let old_vibrato_pos = channel.vibrato_pos; 568 | let old_vibrato_speed = channel.vibrato_speed; 569 | let old_vibrato_depth = channel.vibrato_depth; 570 | let old_tremolo_speed = channel.tremolo_speed; 571 | let old_tremolo_depth = channel.tremolo_depth; 572 | let old_sample_pos = channel.sample_pos; 573 | let old_sample_num = channel.sample_num; 574 | 575 | if note.sample_number > 0 { 576 | // sample number 0, means that the sample keeps playing. The sample indices starts at one, so subtract 1 to get to 0 based index 577 | let current_sample: &Sample = &song.samples[(note.sample_number - 1) as usize]; 578 | channel.volume = current_sample.volume as f32; // Get volume from sample 579 | // channel.size = current_sample.repeat_size + current_sample.repeat_offset; 580 | channel.size = current_sample.size; 581 | channel.sample_num = note.sample_number; 582 | channel.fine_tune = current_sample.fine_tune as u32; 583 | } 584 | 585 | channel.volume_change = 0.0; 586 | channel.note_change = 0; 587 | channel.retrigger_delay = 0; 588 | channel.vibrato_speed = 0; 589 | channel.vibrato_depth = 0; 590 | channel.tremolo_speed = 0; 591 | channel.tremolo_depth = 0; 592 | 593 | channel.arpeggio_counter = 0; 594 | channel.arpeggio_offsets[0] = 0; 595 | channel.arpeggio_offsets[1] = 0; 596 | if note.period != 0 { 597 | channel.period = fine_tune_period(note.period, channel.fine_tune, song.has_standard_notes); 598 | channel.base_period = note.period; 599 | channel.sample_pos = 0.0; 600 | // If a note period was played we need to reset the size to start playing from the start 601 | // ( and redo any sample loops. sample.size changes as the sample repeats ) 602 | if channel.sample_num > 0 { 603 | let current_sample: &Sample = &song.samples[(channel.sample_num - 1) as usize]; 604 | channel.size = current_sample.size; 605 | } 606 | } 607 | 608 | match note.effect { 609 | Effect::SetSpeed { speed } => { 610 | // depending on argument the speed is either sets as VBI counts or Beats Per Minute 611 | if speed <= 31 { 612 | // VBI countsa 613 | player_state.song_speed = speed as u32; 614 | } else { 615 | // BPM changes the timing between ticks ( easiest way to do that is to ) 616 | // default is 125 bpm => 500 => ticks per minute ( by default each tick is 6 vblanks ) = > 3000 vblanks per minute or 50 vblanks per sec 617 | // new BPM * 4 => ticks per minute * 6 / 60 => vblanks per sec = BPM * 0.4 618 | let vblanks_per_sec = speed as f32 * 0.4; 619 | player_state.samples_per_vblank = 620 | (player_state.device_sample_rate as f32 / vblanks_per_sec) as u32 621 | } 622 | } 623 | Effect::Arpeggio { 624 | chord_offset_1, 625 | chord_offset_2, 626 | } => { 627 | channel.arpeggio_offsets[0] = chord_offset_1 as u32; 628 | channel.arpeggio_offsets[1] = chord_offset_2 as u32; 629 | channel.arpeggio_counter = 0; 630 | } 631 | Effect::SlideUp { speed } => { 632 | channel.note_change = -(speed as i32); 633 | } 634 | Effect::SlideDown { speed } => { 635 | channel.note_change = speed as i32; 636 | } 637 | Effect::TonePortamento { speed } => { 638 | // if a new sound was played ( period was so on the note ) that is the new target. otherwise carry on with old target 639 | if note.period != 0 { 640 | channel.period_target = channel.period; // use channel.period which has already been fine-tuned 641 | } else { 642 | if channel.last_porta_target != 0 { 643 | // use the last porta target if it has been set ( some mod tunes set tone porta without history or note) 644 | channel.period_target = channel.last_porta_target; 645 | } else { 646 | // if no note available, use current period ( making this a no-op) 647 | channel.period_target = old_period; 648 | } 649 | } 650 | channel.period = old_period; // reset back to old after we used it 651 | if speed != 0 { 652 | // only change speed if it non-zero. ( zero means to carry on with the effects as before) 653 | channel.note_change = speed as i32; 654 | } else { 655 | channel.note_change = channel.last_porta_speed; 656 | } 657 | // store porta values. Much later portamento effects could still depend on them 658 | channel.last_porta_speed = channel.note_change; 659 | channel.last_porta_target = channel.period_target; 660 | // If the portamento effect happens on the same sample, keep position 661 | if old_sample_num == channel.sample_num { 662 | channel.sample_pos = old_sample_pos; 663 | } 664 | } 665 | Effect::Vibrato { speed, amplitude } => { 666 | if speed == 0 { 667 | channel.vibrato_speed = old_vibrato_speed; 668 | } 669 | if amplitude == 0 { 670 | channel.vibrato_depth = old_vibrato_depth; 671 | } 672 | } 673 | Effect::TonePortamentoVolumeSlide { volume_change } => { 674 | // Continue 675 | channel.volume_change = volume_change as f32; 676 | if note.period != 0 { 677 | channel.period_target = channel.period; 678 | } else { 679 | channel.period_target = channel.last_porta_target; 680 | } 681 | channel.period = old_period; 682 | channel.sample_pos = old_sample_pos; 683 | channel.last_porta_target = channel.period_target; 684 | channel.note_change = channel.last_porta_speed; 685 | } 686 | Effect::VibratoVolumeSlide { volume_change } => { 687 | channel.volume_change = volume_change as f32; 688 | channel.vibrato_pos = old_vibrato_pos as u32; 689 | channel.vibrato_speed = old_vibrato_speed as u32; 690 | channel.vibrato_depth = old_vibrato_depth as i32; 691 | } 692 | Effect::Tremolo { speed, amplitude } => { 693 | if speed == 0 && amplitude == 0 { 694 | channel.tremolo_depth = old_tremolo_depth; 695 | channel.tremolo_speed = old_tremolo_speed; 696 | } else { 697 | channel.tremolo_depth = amplitude as i32; 698 | channel.tremolo_speed = speed as u32; 699 | } 700 | } 701 | Effect::SetSampleOffset { offset } => { 702 | // Ignore, unless we are also playing a new sound 703 | if note.period != 0 && channel.sample_num > 0 { 704 | channel.sample_pos = (offset as f32) * 256.0; 705 | // Does the offset go past the end of the sound 706 | let current_sample: &Sample = &song.samples[(channel.sample_num - 1) as usize]; 707 | if channel.sample_pos as u32 > current_sample.size { 708 | channel.sample_pos = (channel.sample_pos as u32 % current_sample.size) as f32 709 | } 710 | } 711 | } 712 | Effect::VolumeSlide { volume_change } => { 713 | channel.volume_change = volume_change as f32; 714 | } 715 | Effect::SetVolume { volume } => { 716 | channel.volume = volume as f32; 717 | } 718 | Effect::PatternBreak { next_pattern_pos } => { 719 | player_state.next_pattern_pos = next_pattern_pos as i32; 720 | if player_state.next_pattern_pos > 63 { 721 | // only possible to jump to index 63 at most. Anything highrt interpreted as jumping to beginning of next pattern 722 | player_state.next_pattern_pos = 0; 723 | } 724 | } 725 | Effect::PositionJump { next_pattern } => { 726 | if next_pattern as u32 <= player_state.song_pattern_position { 727 | player_state.has_looped = true; 728 | } 729 | player_state.next_position = next_pattern as i32; 730 | } 731 | Effect::FinePortaUp { period_change } => { 732 | channel.period = change_note(channel.period, -(period_change as i32)); 733 | } 734 | Effect::FinePortaDown { period_change } => { 735 | channel.period = change_note(channel.period, period_change as i32); 736 | } 737 | Effect::PatternLoop { arg } => { 738 | if arg == 0 { 739 | // arg 0 marks the loop start position 740 | player_state.pattern_loop_position = Some(player_state.current_line); 741 | } else { 742 | if player_state.pattern_loop == 0 { 743 | player_state.pattern_loop = arg as i32; 744 | } else { 745 | player_state.pattern_loop -= 1; 746 | } 747 | if player_state.pattern_loop > 0 && player_state.pattern_loop_position.is_some() { 748 | player_state.set_pattern_position = true; 749 | } else { 750 | // Double loops ( loops start followed by two or more loops can confuse the player. Once a loop is passed. invalidate the loop marker) 751 | player_state.pattern_loop_position = None; 752 | } 753 | } 754 | } 755 | Effect::TremoloWaveform { wave: _ } => { 756 | // println!("set tremolo wave"); 757 | } 758 | Effect::CoarsePan { pan_pos: _ } => { 759 | // Skip pan for now 760 | } 761 | 762 | Effect::RetriggerSample { retrigger_delay } => { 763 | channel.retrigger_delay = retrigger_delay as u32; 764 | channel.retrigger_counter = 0; 765 | } 766 | Effect::FineVolumeSlideUp { volume_change } => { 767 | channel.volume = channel.volume + volume_change as f32; 768 | if channel.volume > 64.0 { 769 | channel.volume = 64.0; 770 | } 771 | } 772 | Effect::FineVolumeSlideDown { volume_change } => { 773 | channel.volume = channel.volume - volume_change as f32; 774 | if channel.volume < 0.0 { 775 | channel.volume = 0.0; 776 | } 777 | } 778 | Effect::CutNote { delay } => { 779 | channel.cut_note_delay = delay as u32; 780 | } 781 | Effect::SetHardwareFilter { new_state: _ } => { 782 | // not much to do. only works on the a500 783 | } 784 | Effect::DelayedLine { delay_ticks } => { 785 | player_state.delay_line = delay_ticks as u32; 786 | } 787 | Effect::InvertLoop { loop_position: _ } => { 788 | //Ignore for now 789 | } 790 | Effect::None => {} 791 | _ => { 792 | // println!("Unhandled effect"); 793 | } 794 | } 795 | } 796 | 797 | fn play_line(song: &Song, player_state: &mut PlayerState) { 798 | // is a pattern break active 799 | if player_state.next_pattern_pos != -1 { 800 | player_state.song_pattern_position += 1; 801 | player_state.current_line = player_state.next_pattern_pos as u32; 802 | player_state.next_pattern_pos = -1; 803 | } else if player_state.next_position != -1 { 804 | player_state.song_pattern_position = player_state.next_position as u32; 805 | player_state.current_line = 0; 806 | player_state.next_position = -1; 807 | } 808 | 809 | // We could have been place past the end of the song 810 | if player_state.song_pattern_position >= song.num_used_patterns { 811 | if song.end_position < song.num_used_patterns { 812 | player_state.song_pattern_position = song.end_position; 813 | player_state.has_looped = true; 814 | } else { 815 | player_state.song_has_ended = true; 816 | } 817 | } 818 | 819 | let line = player_state.get_song_line(song); 820 | for channel_number in 0..line.len() { 821 | play_note( 822 | &line[channel_number as usize], 823 | player_state, 824 | channel_number, 825 | song, 826 | ); 827 | } 828 | 829 | if player_state.set_pattern_position && player_state.pattern_loop_position.is_some() { 830 | // jump to pattern loop position of the pattern loop was triggered 831 | player_state.set_pattern_position = false; 832 | player_state.current_line = player_state.pattern_loop_position.unwrap(); 833 | } else { 834 | // othwerwise advance to next pattern 835 | player_state.current_line += 1; 836 | if player_state.current_line >= 64 { 837 | player_state.song_pattern_position += 1; 838 | if player_state.song_pattern_position >= song.num_used_patterns { 839 | player_state.song_has_ended = true; 840 | } 841 | player_state.current_line = 0; 842 | } 843 | } 844 | } 845 | 846 | fn update_effects(player_state: &mut PlayerState, song: &Song) { 847 | for channel in &mut player_state.channels { 848 | if channel.sample_num != 0 { 849 | if channel.cut_note_delay > 0 { 850 | channel.cut_note_delay -= 1; 851 | if channel.cut_note_delay == 0 { 852 | channel.cut_note_delay = 0; 853 | // set size of playing sample to zero to indicate nothing is playing 854 | channel.size = 0; 855 | } 856 | } 857 | 858 | if channel.retrigger_delay > 0 { 859 | channel.retrigger_counter += 1; 860 | if channel.retrigger_delay == channel.retrigger_counter { 861 | channel.sample_pos = 0.0; 862 | channel.retrigger_counter = 0; 863 | } 864 | } 865 | channel.volume += channel.volume_change; 866 | if channel.tremolo_depth > 0 { 867 | let base_volume = song.samples[(channel.sample_num - 1) as usize].volume as i32; 868 | let tremolo_size: i32 = (static_tables::VIBRATO_TABLE 869 | [(channel.tremolo_pos & 63) as usize] 870 | * channel.tremolo_depth) 871 | / 64; 872 | let volume = base_volume + tremolo_size; 873 | channel.tremolo_pos += channel.tremolo_speed; 874 | channel.volume = volume as f32; 875 | } 876 | if channel.volume < 0.0 { 877 | channel.volume = 0.0 878 | } 879 | if channel.volume > 64.0 { 880 | channel.volume = 64.0 881 | } 882 | 883 | if channel.arpeggio_offsets[0] != 0 || channel.arpeggio_offsets[1] != 0 { 884 | let new_period: u32; 885 | let index = static_tables::FREQUENCY_TABLE 886 | .binary_search(&channel.base_period) 887 | .expect(&format!( 888 | "Unexpected period value at arpeggio {}, {}:{}", 889 | channel.base_period, 890 | player_state.song_pattern_position, 891 | player_state.current_line 892 | )) as i32; 893 | if channel.arpeggio_counter > 0 { 894 | let mut note_offset = index 895 | - channel.arpeggio_offsets[(channel.arpeggio_counter - 1) as usize] as i32; 896 | if note_offset < 0 { 897 | note_offset = 0; 898 | } 899 | new_period = static_tables::FREQUENCY_TABLE[note_offset as usize]; 900 | } else { 901 | new_period = channel.base_period; 902 | } 903 | channel.period = 904 | fine_tune_period(new_period, channel.fine_tune, song.has_standard_notes); 905 | 906 | channel.arpeggio_counter += 1; 907 | if channel.arpeggio_counter >= 3 { 908 | channel.arpeggio_counter = 0; 909 | } 910 | } 911 | if channel.vibrato_depth > 0 { 912 | let period = fine_tune_period( 913 | channel.base_period, 914 | channel.fine_tune, 915 | song.has_standard_notes, 916 | ); 917 | channel.period = ((period as i32) 918 | + (static_tables::VIBRATO_TABLE[(channel.vibrato_pos & 63) as usize] 919 | * channel.vibrato_depth) 920 | / 32) as u32; 921 | channel.vibrato_pos += channel.vibrato_speed; 922 | } else if channel.note_change != 0 { 923 | // changing note to a target 924 | if channel.period_target != 0 { 925 | if channel.period_target > channel.period { 926 | channel.period = change_note(channel.period, channel.note_change); 927 | if channel.period >= channel.period_target { 928 | channel.period = channel.period_target; 929 | } 930 | } else { 931 | channel.period = change_note(channel.period, -channel.note_change); 932 | if channel.period <= channel.period_target { 933 | channel.period = channel.period_target; 934 | } 935 | } 936 | } else { 937 | // or just moving it 938 | channel.period = change_note(channel.period, channel.note_change); 939 | } 940 | } 941 | } 942 | } 943 | } 944 | 945 | /// Calculates the next sample pair (left, right) to be played from the song. The returned samples have the range [-1, 1] 946 | pub fn next_sample(song: &Song, player_state: &mut PlayerState) -> (f32, f32) { 947 | let mut left = 0.0; 948 | let mut right = 0.0; 949 | 950 | // Have we reached a new vblank 951 | if player_state.current_vblank_sample >= player_state.samples_per_vblank { 952 | player_state.current_vblank_sample = 0; 953 | 954 | update_effects(player_state, song); 955 | 956 | // Is it time to play a new note line either by VBI counting or BPM counting 957 | if player_state.current_vblank >= player_state.song_speed { 958 | if player_state.delay_line > 0 { 959 | player_state.delay_line -= 1; 960 | } else { 961 | player_state.current_vblank = 0; 962 | play_line(song, player_state); 963 | } 964 | } 965 | // apply on every vblank but only after the line has been processed 966 | player_state.current_vblank += 1; 967 | } 968 | player_state.current_vblank_sample += 1; 969 | 970 | for channel_number in 0..player_state.channels.len() { 971 | let channel_info: &mut ChannelInfo = &mut player_state.channels[channel_number]; 972 | if channel_info.size > 2 { 973 | let current_sample: &Sample = &song.samples[(channel_info.sample_num - 1) as usize]; 974 | 975 | // check if we have reached the end of the sample ( do this before getting the sample as some note data can change the 976 | // postions past available data. ) 977 | if channel_info.sample_pos >= channel_info.size as f32 { 978 | let overflow: f32 = channel_info.sample_pos - channel_info.size as f32; 979 | channel_info.sample_pos = current_sample.repeat_offset as f32 + overflow; 980 | channel_info.size = current_sample.repeat_size + current_sample.repeat_offset; 981 | if channel_info.size <= 2 { 982 | continue; 983 | } 984 | } 985 | 986 | // Grab the sample, no filtering 987 | let mut channel_value: f32 = 988 | current_sample.samples[(channel_info.sample_pos as u32) as usize] as f32; // [ -127, 127 ] 989 | 990 | // let left_pos = channel_info.sample_pos as u32; 991 | // let left_weight: f32 = 1.0 - (channel_info.sample_pos - left_pos as f32); 992 | // let mut channel_value: f32 = current_sample.samples[ left_pos as usize] as f32; // [ -127, 127 ] 993 | // if left_pos < (current_sample.size - 1) as u32 { 994 | // let right_value = current_sample.samples[(left_pos + 1) as usize] as f32; 995 | // channel_value = left_weight * channel_value + (1.0 - left_weight) * right_value; 996 | // } 997 | 998 | // max channel vol (64), sample range [ -128,127] scaled to [-1,1] 999 | channel_value *= channel_info.volume / (128.0 * 64.0); 1000 | 1001 | // update position 1002 | channel_info.sample_pos += 1003 | player_state.clock_ticks_per_device_sample / channel_info.period as f32; 1004 | 1005 | let channel_selector = (channel_number as u8) & 0x0003; 1006 | if channel_selector == 0 || channel_selector == 3 { 1007 | left += channel_value; 1008 | } else { 1009 | right += channel_value; 1010 | } 1011 | } 1012 | } 1013 | (left, right) 1014 | } 1015 | -------------------------------------------------------------------------------- /src/loader.rs: -------------------------------------------------------------------------------- 1 | use super::static_tables; 2 | use super::{FormatDescription, Note, Pattern, Sample, Song}; 3 | use std::fs; 4 | 5 | fn is_standard_note_period(period: u32) -> bool { 6 | // treat 0 as standard note because it is not a playable note 7 | if period == 0 { 8 | return true; 9 | } 10 | return match static_tables::FREQUENCY_TABLE.binary_search(&period) { 11 | Ok(_idx) => true, 12 | Err(_idx) => false, 13 | }; 14 | } 15 | 16 | // Go through all the notes to determine if it uses only standard notes 17 | // ( this is a requirement for using table based fine tunes ) 18 | fn has_standard_notes_only(patterns: &Vec, pattern_table: &Vec) -> bool { 19 | for pattern_idx in pattern_table { 20 | if *pattern_idx as usize >= patterns.len() { 21 | continue; 22 | } 23 | let pattern = &patterns[*pattern_idx as usize]; 24 | 25 | for line in &pattern.lines { 26 | for note in line { 27 | if !is_standard_note_period(note.period) { 28 | return false; 29 | } 30 | } 31 | } 32 | } 33 | return true; 34 | } 35 | 36 | /** 37 | * Identify the mod format version based on the tag. If there is not identifiable that it is assumed to be an original mod. 38 | */ 39 | fn get_format(file_data: &[u8]) -> FormatDescription { 40 | let format_tag = String::from_utf8_lossy(&file_data[1080..1084]); 41 | println!("formtat tag: {}", format_tag); 42 | match format_tag.as_ref() { 43 | "M.K." | "FLT4" | "M!K!" | "4CHN" => FormatDescription { 44 | num_channels: 4, 45 | num_samples: 31, 46 | has_tag: true, 47 | }, 48 | "8CHN" => FormatDescription { 49 | num_channels: 8, 50 | num_samples: 31, 51 | has_tag: true, 52 | }, 53 | "6CHN" => FormatDescription { 54 | num_channels: 6, 55 | num_samples: 31, 56 | has_tag: true, 57 | }, 58 | "12CH" => FormatDescription { 59 | num_channels: 12, 60 | num_samples: 31, 61 | has_tag: true, 62 | }, 63 | "CD81" => FormatDescription { 64 | num_channels: 8, 65 | num_samples: 31, 66 | has_tag: true, 67 | }, 68 | "CD61" => { 69 | panic!("unhandled tag cd61"); 70 | } 71 | _ => FormatDescription { 72 | num_channels: 4, 73 | num_samples: 15, 74 | has_tag: false, 75 | }, 76 | } 77 | } 78 | 79 | /// Reads a module music file and returns a song structure ready for playing 80 | /// 81 | /// # Arguments 82 | /// * `file_name` - the mod file on disk 83 | /// 84 | pub fn read_mod_file(file_name: &str) -> Song { 85 | let file_data: Vec = fs::read(file_name).expect(&format!("Cant open file {}", &file_name)); 86 | read_mod_file_slice(&file_data) 87 | } 88 | 89 | /// Reads a module music file (in byte slice form) and returns a song structure ready for playing 90 | /// 91 | /// # Arguments 92 | /// * `file_data` - the slice of bytes to load from 93 | /// 94 | pub fn read_mod_file_slice(file_data: &[u8]) -> Song { 95 | let song_name = String::from_utf8_lossy(&file_data[0..20]); 96 | let format = get_format(file_data); 97 | 98 | let mut samples: Vec = Vec::new(); 99 | let mut offset: usize = 20; 100 | for _sample_num in 0..format.num_samples { 101 | samples.push(Sample::new(&file_data[offset..(offset + 30) as usize])); 102 | offset += 30; 103 | } 104 | 105 | // Figure out where to stop and repeat pos ( with option to repeat in the player ) 106 | let num_used_patterns: u8 = file_data[offset]; 107 | let end_position: u8 = file_data[offset + 1]; 108 | offset += 2; 109 | let pattern_table: Vec = file_data[offset..(offset + 128)].to_vec(); 110 | offset += 128; 111 | 112 | // Skip the tag if one has been identified 113 | if format.has_tag { 114 | offset += 4; 115 | } 116 | 117 | // Work out how the total size of the sample data at tbe back od the file 118 | let mut total_sample_size = 0; 119 | for sample in &mut samples { 120 | total_sample_size = total_sample_size + sample.size; 121 | } 122 | 123 | // The pattern take up all the space that remains after everything else has been accounted for 124 | let total_pattern_size = file_data.len() as u32 - (offset as u32) - total_sample_size; 125 | let single_pattern_size = format.num_channels * 4 * 64; 126 | let mut num_patterns = total_pattern_size / single_pattern_size; 127 | // Find the highest pattern referenced within the used patter references. This is the minimum number of patterns we must load 128 | let slc = &pattern_table[0..(num_used_patterns as usize)]; 129 | let min_pattern_required = *slc.iter().max().unwrap() + 1; 130 | // we must read AT LEAST the max_pattern_required patterns 131 | if (min_pattern_required as u32) > num_patterns { 132 | num_patterns = min_pattern_required as u32; 133 | } 134 | 135 | // Read the patterns 136 | let mut patterns: Vec = Vec::new(); 137 | for _pattern_number in 0..num_patterns { 138 | let mut pattern = Pattern::new(); 139 | for line in 0..64 { 140 | for _channel in 0..format.num_channels { 141 | let note = Note::new(&file_data[offset..(offset + 4)], &format); 142 | pattern.lines[line].push(note); 143 | offset += 4; 144 | } 145 | } 146 | patterns.push(pattern); 147 | } 148 | 149 | // Some mods have weird garbage between the end of the pattern data and the samples 150 | // ( and some weird files do not have enough for both patterns ans samples. Effectively some storage is used for both!!) 151 | // Skip the potential garbage by working out the sample position from the back of the file 152 | offset = (file_data.len() as u32 - total_sample_size) as usize; 153 | 154 | for sample_number in 0..samples.len() { 155 | let length = samples[sample_number].size; 156 | for _idx in 0..length { 157 | samples[sample_number].samples.push(file_data[offset] as i8); 158 | offset += 1; 159 | } 160 | } 161 | 162 | // there are non standard notes, we cant use table based fine tune 163 | let has_standard_notes = has_standard_notes_only(&patterns, &pattern_table); 164 | 165 | Song { 166 | name: String::from(song_name), 167 | format: format, 168 | samples: samples, 169 | patterns: patterns, 170 | pattern_table: pattern_table, 171 | num_used_patterns: num_used_patterns as u32, 172 | end_position: end_position as u32, 173 | has_standard_notes: has_standard_notes, 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/static_tables.rs: -------------------------------------------------------------------------------- 1 | pub static VIBRATO_TABLE: [i32; 64] = [ 2 | 0, 24, 49, 74, 97, 120, 141, 161, 180, 197, 212, 224, 235, 244, 250, 253, 255, 253, 250, 244, 3 | 235, 224, 212, 197, 180, 161, 141, 120, 97, 74, 49, 24, -0, -24, -49, -74, -97, -120, -141, 4 | -161, -180, -197, -212, -224, -235, -244, -250, -253, -255, -253, -250, -244, -235, -224, -212, 5 | -197, -180, -161, -141, -120, -97, -74, -49, -24, 6 | ]; 7 | 8 | #[rustfmt::skip] 9 | pub static FREQUENCY_TABLE: [u32; 60] = [ 10 | // B A# A G# G F# F E D# D C# C 11 | 57, 60, 64, 67, 71, 76, 80, 85, 90, 95, 101, 107, 12 | 113, 120, 127, 135, 143, 151, 160, 170, 180, 190, 202, 214, 13 | 226, 240, 254, 269, 285, 302, 320, 339, 360, 381, 404, 428, 14 | 453, 480, 508, 538, 570, 604, 640, 678, 720, 762, 808, 856, 15 | 907, 961, 1017, 1077, 1141, 1209, 1281, 1357, 1440, 1525, 1616, 1712, 16 | ]; 17 | 18 | #[rustfmt::skip] 19 | pub static FINE_TUNE_TABLE : [ [ u32; 60 ]; 16 ] = [ 20 | [ 56, 60, 63, 67, 71, 75, 80, 85, 90, 95, 101, 107, // 0 21 | 113, 120, 127, 135, 143, 151, 160, 170, 180, 190, 202, 214, 22 | 226, 240, 254, 269, 285, 302, 320, 339, 360, 381, 404, 428, 23 | 453, 480, 508, 538, 570, 604, 640, 678, 720, 762, 808, 856, 24 | 906, 960, 1016, 1076, 1140, 1208, 1280, 1356, 1440, 1524, 1616, 1712 ], 25 | [ 56, 59, 63, 67, 71, 75, 79, 84, 89, 94, 100, 106, // 1 26 | 113, 119, 126, 134, 142, 150, 159, 169, 179, 189, 201, 213, 27 | 225, 239, 253, 268, 284, 300, 318, 337, 357, 379, 401, 425, 28 | 450, 477, 505, 535, 567, 601, 637, 674, 715, 757, 802, 850, 29 | 900, 954, 1010, 1070, 1134, 1202, 1274, 1348, 1430, 1514, 1604, 1700 ], 30 | [ 56, 59, 62, 66, 70, 74, 79, 83, 88, 94, 99, 105, // 2 31 | 112, 118, 125, 133, 141, 149, 158, 167, 177, 188, 199, 211, 32 | 224, 237, 251, 266, 282, 298, 316, 335, 355, 376, 398, 422, 33 | 447, 474, 502, 532, 563, 597, 632, 670, 709, 752, 796, 844, 34 | 894, 948, 1004, 1064, 1126, 1194, 1264, 1340, 1418, 1504, 1592, 1688 ], 35 | [ 55, 59, 62, 66, 70, 74, 78, 83, 88, 93, 99, 104, // 3 36 | 111, 118, 125, 132, 140, 148, 157, 166, 176, 187, 198, 209, 37 | 222, 235, 249, 264, 280, 296, 314, 332, 352, 373, 395, 419, 38 | 444, 470, 498, 528, 559, 592, 628, 665, 704, 746, 791, 838, 39 | 888, 940, 996, 1056, 1118, 1184, 1256, 1330, 1408, 1492, 1582, 1676 ], 40 | [ 55, 58, 62, 65, 69, 73, 78, 82, 87, 92, 98, 104, // 4 41 | 110, 117, 124, 131, 139, 147, 156, 165, 175, 185, 196, 208, 42 | 220, 233, 247, 262, 278, 294, 312, 330, 350, 370, 392, 416, 43 | 441, 467, 495, 524, 555, 588, 623, 660, 699, 741, 785, 832, 44 | 882, 934, 990, 1048, 1110, 1176, 1246, 1320, 1398, 1482, 1570, 1664 ], 45 | [ 54, 58, 61, 65, 69, 73, 77, 82, 87, 92, 97, 103, // 5 46 | 109, 116, 123, 130, 138, 146, 155, 164, 174, 184, 195, 206, 47 | 219, 232, 245, 260, 276, 292, 309, 328, 347, 368, 390, 413, 48 | 437, 463, 491, 520, 551, 584, 619, 655, 694, 736, 779, 826, 49 | 874, 926, 982, 1040, 1102, 1168, 1238, 1310, 1388, 1472, 1558, 1652 ], 50 | [ 54, 57, 61, 64, 68, 72, 77, 81, 86, 91, 96, 102, // 6 51 | 109, 115, 122, 129, 137, 145, 154, 163, 172, 183, 193, 205, 52 | 217, 230, 244, 258, 274, 290, 307, 325, 345, 365, 387, 410, 53 | 434, 460, 487, 516, 547, 580, 614, 651, 689, 730, 774, 820, 54 | 868, 920, 974, 1032, 1094, 1160, 1228, 1302, 1378, 1460, 1548, 1640 ], 55 | [ 54, 57, 60, 64, 68, 72, 76, 80, 85, 90, 96, 102, // 7 56 | 108, 114, 121, 128, 136, 144, 152, 161, 171, 181, 192, 204, 57 | 216, 228, 242, 256, 272, 288, 305, 323, 342, 363, 384, 407, 58 | 431, 457, 484, 513, 543, 575, 610, 646, 684, 725, 768, 814, 59 | 862, 914, 968, 1026, 1086, 1150, 1220, 1292, 1368, 1450, 1536, 1628 ], 60 | [ 60, 63, 67, 71, 75, 80, 85, 90, 95, 101, 107, 113, // -8 61 | 120, 127, 135, 143, 151, 160, 170, 180, 190, 202, 214, 226, 62 | 240, 254, 269, 285, 302, 320, 339, 360, 381, 404, 428, 453, 63 | 480, 508, 538, 570, 604, 640, 678, 720, 762, 808, 856, 907, 64 | 960, 1016, 1076, 1140, 1208, 1280, 1356, 1440, 1524, 1616, 1712, 1814 ], 65 | [ 59, 63, 67, 71, 75, 79, 84, 89, 94, 100, 106, 112, 66 | 119, 126, 134, 142, 150, 159, 169, 179, 189, 200, 212, 225, 67 | 238, 253, 268, 284, 300, 318, 337, 357, 379, 401, 425, 450, 68 | 477, 505, 535, 567, 601, 636, 675, 715, 757, 802, 850, 900, 69 | 954, 1010, 1070, 1134, 1202, 1272, 1350, 1430, 1514, 1604, 1700, 1800 ], 70 | [ 59, 62, 66, 70, 74, 79, 83, 88, 94, 99, 105, 111, 71 | 118, 125, 133, 141, 149, 158, 167, 177, 188, 199, 211, 223, 72 | 237, 251, 266, 282, 298, 316, 335, 355, 376, 398, 422, 447, 73 | 474, 502, 532, 563, 597, 632, 670, 709, 752, 796, 844, 894, 74 | 948, 1004, 1064, 1126, 1194, 1264, 1340, 1418, 1504, 1592, 1688, 1788 ], 75 | [ 59, 62, 66, 70, 74, 78, 83, 88, 93, 99, 104, 111, 76 | 118, 125, 132, 140, 148, 157, 166, 176, 187, 198, 209, 222, 77 | 235, 249, 264, 280, 296, 314, 332, 352, 373, 395, 419, 444, 78 | 470, 498, 528, 559, 592, 628, 665, 704, 746, 791, 838, 887, 79 | 940, 996, 1056, 1118, 1184, 1256, 1330, 1408, 1492, 1582, 1676, 1774 ], 80 | [ 58, 61, 65, 69, 73, 78, 82, 87, 92, 98, 104, 110, 81 | 117, 123, 131, 139, 147, 156, 165, 175, 185, 196, 208, 220, 82 | 233, 247, 262, 278, 294, 312, 330, 350, 370, 392, 416, 441, 83 | 467, 494, 524, 555, 588, 623, 660, 699, 741, 785, 832, 881, 84 | 934, 988, 1048, 1110, 1176, 1246, 1320, 1398, 1482, 1570, 1664, 1762 ], 85 | [ 58, 61, 65, 69, 73, 77, 82, 87, 92, 97, 103, 109, 86 | 116, 123, 130, 138, 146, 155, 164, 174, 184, 195, 206, 219, 87 | 232, 245, 260, 276, 292, 309, 328, 347, 368, 390, 413, 437, 88 | 463, 491, 520, 551, 584, 619, 655, 694, 736, 779, 826, 875, 89 | 926, 982, 1040, 1102, 1168, 1238, 1310, 1388, 1472, 1558, 1652, 1750 ], 90 | [ 57, 61, 64, 68, 72, 77, 81, 86, 91, 96, 102, 108, 91 | 115, 122, 129, 137, 145, 154, 163, 172, 183, 193, 205, 217, 92 | 230, 244, 258, 274, 290, 307, 325, 345, 365, 387, 410, 434, 93 | 460, 487, 516, 547, 580, 614, 651, 689, 730, 774, 820, 868, 94 | 920, 974, 1032, 1094, 1160, 1228, 1302, 1378, 1460, 1548, 1640, 1736 ], 95 | [ 57, 60, 64, 68, 72, 76, 80, 85, 90, 96, 101, 108, 96 | 114, 121, 128, 136, 144, 152, 161, 171, 181, 192, 203, 216, 97 | 228, 242, 256, 272, 288, 305, 323, 342, 363, 384, 407, 431, 98 | 457, 484, 513, 543, 575, 610, 646, 684, 725, 768, 814, 862, 99 | 914, 968, 1026, 1086, 1150, 1220, 1292, 1368, 1450, 1536, 1628, 1724, 100 | ]]; 101 | 102 | pub static SCALE_FINE_TUNE: [f32; 16] = [ 103 | // from 0 to 7 104 | 1.0, 1.0072464, 1.0145453, 1.0218972, 1.0293022, 1.036761, 1.0442737, 1.051841, 105 | // from -8 to -1 106 | 0.9438743, 0.950714, 0.9576033, 0.96454245, 0.9715319, 0.9785721, 0.9856632, 0.9928057, 107 | ]; 108 | -------------------------------------------------------------------------------- /src/textout.rs: -------------------------------------------------------------------------------- 1 | //! # for printing information about the mod song 2 | //! 3 | //! text_out contains utility functions for printing out information about mods. Primarily intended to be used for debugging and understanding the progress of the playback 4 | use super::Sample; 5 | use super::{Effect, Note, Song}; 6 | 7 | static NOTE_FREQUENCY_STRINGS: [(u32, &str); 60] = [ 8 | (57, "B-6"), 9 | (60, "A#6"), 10 | (64, "A-6"), 11 | (67, "G#6"), 12 | (71, "G-6"), 13 | (76, "F#6"), 14 | (80, "F-6"), 15 | (85, "E-6"), 16 | (90, "D#6"), 17 | (95, "D-6"), 18 | (101, "C#6"), 19 | (107, "C-6"), 20 | (113, "B-5"), 21 | (120, "A#5"), 22 | (127, "A-5"), 23 | (135, "G#5"), 24 | (143, "G-5"), 25 | (151, "F#5"), 26 | (160, "F-5"), 27 | (170, "E-5"), 28 | (180, "D#5"), 29 | (190, "D-5"), 30 | (202, "C#5"), 31 | (214, "C-5"), 32 | (226, "B-4"), 33 | (240, "A#4"), 34 | (254, "A-4"), 35 | (269, "G#4"), 36 | (285, "G-4"), 37 | (302, "F#4"), 38 | (320, "F-4"), 39 | (339, "E-4"), 40 | (360, "D#4"), 41 | (381, "D-4"), 42 | (404, "C#4"), 43 | (428, "C-4"), 44 | (453, "B-3"), 45 | (480, "A#3"), 46 | (508, "A-3"), 47 | (538, "G#3"), 48 | (570, "G-3"), 49 | (604, "F#3"), 50 | (640, "F-3"), 51 | (678, "E-3"), 52 | (720, "D#3"), 53 | (762, "D-3"), 54 | (808, "C#3"), 55 | (856, "C-3"), 56 | (907, "B-2"), 57 | (961, "A#2"), 58 | (1017, "A-2"), 59 | (1077, "G#2"), 60 | (1141, "G-2"), 61 | (1209, "F#2"), 62 | (1281, "F-2"), 63 | (1357, "E-3"), 64 | (1440, "D#2"), 65 | (1525, "D-2"), 66 | (1616, "C#2"), 67 | (1712, "C-2"), 68 | ]; 69 | 70 | #[rustfmt::skip] 71 | impl Effect { 72 | fn to_string(&self) -> String { 73 | return match self { 74 | Effect::Arpeggio { chord_offset_1, chord_offset_2 } => format!("Arpgi {:02}{:02}", chord_offset_1, chord_offset_2), 75 | Effect::SlideUp { speed } => format!("SldUp {:>4}", speed), 76 | Effect::SlideDown { speed } => format!( "SldDn {:>4}", speed ), 77 | Effect::TonePortamento { speed } => format!("TonPo {:>4}", speed ), 78 | Effect::Vibrato { speed, amplitude } => format!("Vibra {:02}{:02}",speed,amplitude), 79 | Effect::TonePortamentoVolumeSlide { volume_change } => format!( "TPVos {:>4}", volume_change), 80 | Effect::VibratoVolumeSlide { volume_change } => format!( "ViVoS {:>4}", volume_change), 81 | Effect::Tremolo { speed, amplitude } => format!("Trmlo {:02}{:02}", speed, amplitude), 82 | Effect::Pan { position } => format!("Pan {:>5}.", position ), 83 | Effect::SetSampleOffset { offset } => format!("Offst {:>4}", offset ), 84 | Effect::VolumeSlide { volume_change } => {format!( "VolSl {:>4}", volume_change) } 85 | Effect::PositionJump { next_pattern } => format!( "Jump {:>4}", next_pattern ), 86 | Effect::SetVolume { volume } => format!("Volme {:>4}", volume), 87 | Effect::PatternBreak { next_pattern_pos } => format!( "Break {:>4}", next_pattern_pos), 88 | Effect::SetSpeed { speed } => format!( "Speed {:>4}", speed), 89 | Effect::SetHardwareFilter { new_state } => format!( "StHwF {:>4}", new_state ), 90 | Effect::FinePortaUp { period_change } => format!("FPoUp {:>4}", period_change), 91 | Effect::FinePortaDown { period_change } => format!("FPoDn {:>4}", period_change), 92 | Effect::Glissando { use_smooth_slide } => format!("Glsnd {:>4}", use_smooth_slide), 93 | Effect::PatternLoop { arg } => format!("PtnLp {:>4}", arg), 94 | Effect::TremoloWaveform { wave } => format!( "TrmWv {:>4}", wave ), 95 | Effect::CoarsePan { pan_pos } => format!("CrPan {:>4}", pan_pos ), 96 | Effect::RetriggerSample { retrigger_delay } => format!( "ReTrg {:>4}", retrigger_delay), 97 | Effect::FineVolumeSlideUp { volume_change } => format!("FVSUp {:>4}", volume_change), 98 | Effect::FineVolumeSlideDown { volume_change } => format!("FVSDn {:>4}", volume_change), 99 | Effect::CutNote { delay } => format!( "CutNt {:>4}", delay ), 100 | Effect::DelayedSample { delay_ticks } => format!( "DlySm {:>4}", delay_ticks), 101 | Effect::DelayedLine { delay_ticks } => format!( "DlyLn {:>4}", delay_ticks), 102 | Effect::InvertLoop{ loop_position } => format!( "InvLp {:>4}", loop_position ), 103 | Effect::SetVibratoWave { wave } => format!("VibWv {:>4}", wave ), 104 | Effect::SetFineTune { fine_tune } => format!("FnTne {:>4}", fine_tune), 105 | _ => String::from(".........."), 106 | }; 107 | } 108 | } 109 | 110 | //impl crate::mod_player::Sample{ 111 | //impl super::Sample{ 112 | impl Sample { 113 | fn print(&self) { 114 | println!(" sample Name: {}", self.name); 115 | println!(" sample Size: {}", self.size); 116 | println!( 117 | " sample volume: {}, fine tune {}", 118 | self.volume, self.fine_tune 119 | ); 120 | println!( 121 | " repeat Offset: {}, repeat Size {}", 122 | self.repeat_offset, self.repeat_size 123 | ); 124 | } 125 | } 126 | 127 | fn note_string(period: u32) -> &'static str { 128 | if period == 0 { 129 | return "..."; 130 | } 131 | let ret = NOTE_FREQUENCY_STRINGS.binary_search_by(|val| val.0.cmp(&period)); 132 | return match ret { 133 | Ok(idx) => return NOTE_FREQUENCY_STRINGS[idx].1, 134 | Err(idx) => { 135 | if idx == 0 { 136 | NOTE_FREQUENCY_STRINGS[0].1 137 | } else if idx == NOTE_FREQUENCY_STRINGS.len() { 138 | NOTE_FREQUENCY_STRINGS[NOTE_FREQUENCY_STRINGS.len() - 1].1 139 | } else { 140 | // Pick the one that is is closer to 141 | if period - NOTE_FREQUENCY_STRINGS[idx - 1].0 142 | < NOTE_FREQUENCY_STRINGS[idx].0 - period 143 | { 144 | NOTE_FREQUENCY_STRINGS[idx - 1].1 145 | } else { 146 | NOTE_FREQUENCY_STRINGS[idx].1 147 | } 148 | } 149 | } 150 | }; 151 | } 152 | #[cfg(test)] 153 | mod tests { 154 | use super::*; 155 | 156 | #[test] 157 | fn test_note_string() { 158 | assert_eq!(note_string(57), "B-6", "Match first note"); 159 | assert_eq!(note_string(1712), "C-2", "Match first note"); 160 | assert_eq!(note_string(2000), "C-2", "Values past lowest map to C-2"); 161 | assert_eq!(note_string(12), "B-6", "Values past highest map to C-2"); 162 | assert_eq!(note_string(0), "...", "zero maps to ellipsis"); 163 | assert_eq!(note_string(128), "A-5", "Umatched go to nearest"); 164 | assert_eq!(note_string(133), "G#5", "Umatched go to nearest"); 165 | assert_eq!(note_string(184), "D#5", "Umatched go to nearest"); 166 | assert_eq!(note_string(185), "D-5", "Umatched go to nearest"); 167 | assert_eq!(note_string(185), "D-5", "Umatched go to nearest"); 168 | } 169 | } 170 | 171 | /// Prints out one line of note data 172 | pub fn print_line(line: &Vec) { 173 | for note in line.iter() { 174 | let sample_string; 175 | if note.sample_number == 0 { 176 | sample_string = "..".to_string(); 177 | } else { 178 | sample_string = note.sample_number.to_string(); 179 | } 180 | print!( 181 | "{} {:>2} {} ", 182 | note_string(note.period), 183 | sample_string, 184 | note.effect.to_string() 185 | ); 186 | } 187 | println!(""); 188 | } 189 | 190 | /// Print out general info about the song 191 | pub fn print_song_info(song: &Song) { 192 | println!("Song: {}", song.name); 193 | 194 | println!("Number of channels: {}", song.format.num_channels); 195 | println!("Number of samples: {}", song.format.num_samples); 196 | // for sample in &song.samples { 197 | for sample_num in 0..song.samples.len() { 198 | println!("Sample #{}", sample_num + 1); 199 | song.samples[sample_num].print(); 200 | } 201 | 202 | println!(" num patterns in song: {}", song.patterns.len()); 203 | println!(" end position: {}", song.end_position); 204 | println!(" uses standard note table: {}", song.has_standard_notes); 205 | } 206 | -------------------------------------------------------------------------------- /tests/regression.rs: -------------------------------------------------------------------------------- 1 | use crc::{crc64, Hasher64}; 2 | use mod_player; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json; 5 | use std::collections::HashMap; 6 | use std::io; 7 | use std::time; 8 | use std::{fs, str}; 9 | 10 | #[derive(Serialize, Deserialize)] 11 | struct ExpectedChecksums { 12 | song_checksums: HashMap, 13 | } 14 | 15 | fn get_expected_results() -> io::Result { 16 | let expected_json = fs::read_to_string("reg_expected.json")?; 17 | let result: ExpectedChecksums = serde_json::from_str(&expected_json)?; 18 | Ok(result) 19 | } 20 | 21 | #[test] 22 | // Calculates a checksum for all songs and checks them against expected checksums 23 | // produces a 24 | // Use the following command to run just this test with full output 25 | // cargo test --test regression -- --nocapture 26 | // Use the following to get release timings 27 | // cargo test --test regression --release -- --nocapture 28 | fn reg_test() { 29 | let test_songs = [ 30 | "CHIP_SLAYER!.MOD", 31 | "BUBBLE_BOBBLE.MOD", 32 | "cream_of_the_earth.mod", 33 | "switchback.mod", 34 | "stardstm.MOD", 35 | "overload.mod", 36 | "BOG_WRAITH.mod", 37 | "wasteland.mod", 38 | "1 step further.MOD", 39 | "BALLI.MOD", 40 | "ballade_pour_adeline.MOD", 41 | "sarcophaser.mod", 42 | "chcknbnk.mod", 43 | "GSLINGER.MOD", 44 | "19xx.mod", 45 | "ballad_ej.mod", // rare 12 channel mod 46 | "JARRE.mod", // weird broken mod. incomplete last pattern???? 47 | "star-rai.mod", // 6 channel mod 48 | ]; 49 | let mut song_checksums: HashMap = HashMap::new(); 50 | let expected_results = get_expected_results().ok(); 51 | if expected_results.is_none() { 52 | println!("No expected results read. Will produce actuals output for all and then fail") 53 | } 54 | 55 | for test_song in &test_songs { 56 | let song = mod_player::read_mod_file(&format!("mod_files/{}", test_song)); 57 | let mut player_state: mod_player::PlayerState = 58 | mod_player::PlayerState::new(song.format.num_channels, 48100); 59 | 60 | const SOUND_BUFFER_SIZE: usize = 48100 * 2 * 1000; 61 | let mut sound_data = vec![0.0f32; SOUND_BUFFER_SIZE]; 62 | let before_play = time::Instant::now(); 63 | let mut played_song_length = (SOUND_BUFFER_SIZE as f32) / (2.0f32 * 48100.0f32); 64 | for pos in (0..SOUND_BUFFER_SIZE).step_by(2) { 65 | let (left, right) = mod_player::next_sample(&song, &mut player_state); 66 | sound_data[pos] = left; 67 | sound_data[pos + 1] = right; 68 | if player_state.song_has_ended || player_state.has_looped { 69 | played_song_length = pos as f32 / (2.0f32 * 48100.0f32); 70 | break; 71 | } 72 | } 73 | let after_play = time::Instant::now(); 74 | let play_time = after_play.duration_since(before_play); 75 | println!("time for {} is {} uSecs", test_song, play_time.as_micros()); 76 | println!( 77 | "playspeed: {}", 78 | played_song_length * 1000_000.0 / (play_time.as_micros() as f32) 79 | ); 80 | 81 | let mut digest = crc64::Digest::new(crc64::ECMA); 82 | for pos in 0..SOUND_BUFFER_SIZE { 83 | let sample = sound_data[pos]; 84 | let sample_as_bytes: [u8; 4] = sample.to_bits().to_le_bytes(); 85 | digest.write(&sample_as_bytes); 86 | } 87 | 88 | let value = digest.sum64(); 89 | song_checksums.insert(test_song.to_string(), value); 90 | if let Some(ref expected) = expected_results { 91 | assert_eq!( 92 | expected.song_checksums.get(*test_song), 93 | Some(&value), 94 | "song {} produces the same checksum as before", 95 | test_song 96 | ); 97 | } 98 | } 99 | let actuals_checksums = ExpectedChecksums { song_checksums }; 100 | let serialized = serde_json::to_string_pretty(&actuals_checksums).unwrap(); 101 | fs::write("reg_actual.json", &serialized).expect("Cant write actual results"); 102 | 103 | if expected_results.is_none() { 104 | println!("Calculated following results. Failing because no expected data found"); 105 | println!("{}", serialized); 106 | assert!(false); 107 | } 108 | } 109 | --------------------------------------------------------------------------------