From 81326d32df84a2a8a76b09c932bb2a12ebf7db13 Mon Sep 17 00:00:00 2001 From: ashley Date: Sat, 12 Aug 2023 17:08:10 -0400 Subject: [PATCH] DMF soundtrack module --- Cargo.toml | 3 +- src/main.rs | 83 +++++++++++++++------- src/reskit/mod.rs | 1 + src/reskit/soundtrack.rs | 144 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+), 26 deletions(-) create mode 100644 src/reskit/soundtrack.rs diff --git a/Cargo.toml b/Cargo.toml index 9fdcc42..4b9f3ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ edition = "2018" [dependencies] image = "^0.23" colour = "^0.5" -clap = "^2.33" \ No newline at end of file +clap = "^2.33" +flate2 = "1.0.26" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 570210e..5677db6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,16 +3,17 @@ extern crate clap; #[macro_use] extern crate colour; -use clap::{App, Arg, SubCommand}; - mod reskit; +use std::error::Error; +use clap::{App, Arg, SubCommand}; +use reskit::soundtrack::DmfModule; use reskit::utility; use reskit::tileset; -fn main() { +fn run() -> Result<(), Box> { let matches = App::new( "reskit" ) - .version( "0.0.1a" ) - .author( "(c) 2021 Ashley N. " ) + .version( "0.0.2a" ) + .author( "(c) 2021-2023 Ashley N. " ) .about( "Sega Megadrive resource kit and format converter" ) .subcommand( SubCommand::with_name( "tileset" ) @@ -23,7 +24,7 @@ fn main() { Arg::with_name( "FORMAT" ) .short( "f" ) .long( "format" ) - .help( "Specify output format for tileset (valid FORMAT options: bin, inc)") + .help( "Specify output format for tileset (valid options: bin, inc)") .default_value( "bin" ) .takes_value( true ) ) @@ -31,31 +32,63 @@ fn main() { Arg::with_name( "TILEORDER" ) .short( "to" ) .long( "tile-order" ) - .help( "Specify tile order for tileset (valid TILEORDER options: tile, sprite" ) + .help( "Specify tile order for tileset (valid options: tile, sprite" ) .default_value( "tile" ) .takes_value( true ) ) ) + .subcommand( + SubCommand::with_name( "soundtrack" ) + .about( "Generate an on-console soundtrack from a chiptune tracker module" ) + .arg_from_usage( "-i, --input= 'Specify input module'" ) + .arg_from_usage( "-o, --output= 'Specify output file'") + .arg( + Arg::with_name( "TRACKER_FORMAT" ) + .short( "tf" ) + .long( "tracker-format" ) + .help( "Specify tracker module format (valid options: dmf (DefleMask Tracker))") + .default_value( "dmf" ) + .takes_value( true ) + ) + .arg( + Arg::with_name( "DRIVER_FORMAT" ) + .short( "df" ) + .long( "driver-format" ) + .help( "Specify console sound driver format (valid options: esf (Echo))") + .default_value( "esf" ) + .takes_value( true ) + ) + ) .get_matches(); - // Get arguments for tileset if let Some( matches ) = matches.subcommand_matches( "tileset" ) { - // Get input and output filenames - if let Some( input_filename ) = matches.value_of( "input" ) { - if let Some( output_filename ) = matches.value_of( "output" ) { - return tileset::generate( - input_filename, - output_filename, - matches.value_of( "FORMAT" ).unwrap(), - matches.value_of( "TILEORDER" ).unwrap() - ); - } else { - utility::print_error( "expected: output_filename (-o,--output)" ); - } - } else { - utility::print_error( "expected: input filename (-i,--input)" ); - } - } + let input_filename = matches.value_of( "input" ).ok_or( "expected: input filename (-i,--input)" )?; + let output_filename = matches.value_of( "output" ).ok_or( "expected: output_filename (-o,--output)" )?; - utility::print_error( "no plugin provided (try --help)" ); + tileset::generate( + input_filename, + output_filename, + matches.value_of( "FORMAT" ).unwrap(), + matches.value_of( "TILEORDER" ).unwrap() + ); + + Ok( () ) + } else if let Some( matches ) = matches.subcommand_matches( "soundtrack" ) { + let input_filename = matches.value_of( "input" ).ok_or( "expected: input filename (-i,--input)" )?; + let output_filename = matches.value_of( "output" ).ok_or( "expected: output_filename (-o,--output)" )?; + + let result = DmfModule::from_file( &input_filename )?; + + // TODO !! + + Ok( () ) + } else { + Err( "no tool provided (try --help)" )? + } } + +fn main() { + if let Err( error ) = run() { + utility::print_error( &format!( "{}", error ) ); + } +} \ No newline at end of file diff --git a/src/reskit/mod.rs b/src/reskit/mod.rs index fcabdd3..bcc13ab 100644 --- a/src/reskit/mod.rs +++ b/src/reskit/mod.rs @@ -1,2 +1,3 @@ +pub mod soundtrack; pub mod tileset; pub mod utility; \ No newline at end of file diff --git a/src/reskit/soundtrack.rs b/src/reskit/soundtrack.rs new file mode 100644 index 0000000..1c3f1ad --- /dev/null +++ b/src/reskit/soundtrack.rs @@ -0,0 +1,144 @@ +use std::{error::Error, fs::File, io::Read, str::from_utf8, slice::Iter, convert::TryInto}; +use flate2::read::ZlibDecoder; + +const DMF_MAGIC_NUMBER: &'static str = ".DelekDefleMask."; +const DMF_SUPPORTED_VERSION: u8 = 0x18; +const DMF_MD: u8 = 0x02; +const DMF_MD_ENHANCED_CH3: u8 = 0x42; + +#[derive(Debug)] +pub enum FrameMode { + Ntsc, + Pal, + Custom( u8, u8, u8 ) +} + +pub struct DmfModule { + version: u8, + time_base: u8, + speed_a: u8, + speed_b: u8, + frame_mode: FrameMode, + rows_per_pattern: u32, + patterns: u8 +} + +fn get_string( bytes: &mut Iter<'_, u8>, take: usize ) -> Result> { + let took = bytes.take( take ); + let took: Vec = took.cloned().collect(); + let took = from_utf8( &took )?.to_string(); + + Ok( took ) +} + +fn get_u8( bytes: &mut Iter<'_, u8> ) -> Result> { + let took = bytes.take( 1 ).cloned().next().ok_or( "invalid file: expected 1 byte" )?; + + Ok( took ) +} + +fn get_u32( bytes: &mut Iter<'_, u8> ) -> Result> { + let took = bytes.take( 4 ); + let took: Vec = took.cloned().collect(); + let took = u32::from_le_bytes( took[0..4].try_into()? ); + + Ok( took ) +} + +fn skip( bytes: &mut Iter<'_, u8>, by: usize ) { + for i in 0..by { + bytes.next(); + } +} + +impl DmfModule { + + pub fn from_file( path: &str ) -> Result> { + let mut file = File::open( path )?; + let mut compressed: Vec = Vec::new(); + file.read_to_end( &mut compressed )?; + + let mut inflator = ZlibDecoder::new( &compressed[..] ); + let mut bytes: Vec = Vec::new(); + inflator.read_to_end( &mut bytes )?; + + let mut bytes = bytes.iter(); + + if get_string( bytes.by_ref(), 16 )? != DMF_MAGIC_NUMBER { + return Err( format!( "invalid file: missing DMF header" ) )? + } + + println!( ".DelekDefleMask. DMF file" ); + + let version = get_u8( bytes.by_ref() )?; + if version != DMF_SUPPORTED_VERSION { + return Err( format!( "invalid file: unsupported version: {}", version ) )? + } + + let platform = get_u8( bytes.by_ref() )?; + match platform { + DMF_MD => println!( "Mode:\tSega Megadrive" ), + DMF_MD_ENHANCED_CH3 => println!( "Mode:\t\tSega Megadrive (Ext. Ch. 3 Mode)" ), + _ => return Err( "invalid file: invalid console format" )? + }; + + let song_name_len = get_u8( bytes.by_ref() )?; + println!( "Song name:\t{}", get_string( bytes.by_ref(), song_name_len.into() )? ); + + let song_author_len = get_u8( bytes.by_ref() )?; + println!( "Song author:\t{}", get_string( bytes.by_ref(), song_author_len.into() )? ); + + // Burn 2 bytes, don't care about the highlight patterns + skip( bytes.by_ref(), 2 ); + + // DMF format appears to subtract by 1 from the base time shown in the UI + // Base time is multiplied by speed_a and speed_b so setting it to 1 and + // getting 0 doesn't make any sense. + let time_base = get_u8( bytes.by_ref() )? + 1; + let speed_a = get_u8( bytes.by_ref() )?; + let speed_b = get_u8( bytes.by_ref() )?; + + println!( "Time base:\t{}", time_base ); + println!( "Speed A:\t{}", speed_a ); + println!( "Speed B:\t{}", speed_b ); + + let frame_mode = match get_u8( bytes.by_ref() )? { + 0 => FrameMode::Pal, + 1 => FrameMode::Ntsc, + _ => return Err( "invalid file: invalid frame mode" )? + }; + + let custom_frame_mode = get_u8( bytes.by_ref() )? == 1; + let custom_frame_mode_hz_1 = get_u8( bytes.by_ref() )?; + let custom_frame_mode_hz_2 = get_u8( bytes.by_ref() )?; + let custom_frame_mode_hz_3 = get_u8( bytes.by_ref() )?; + + let frame_mode = if custom_frame_mode { + FrameMode::Custom( custom_frame_mode_hz_1, custom_frame_mode_hz_2, custom_frame_mode_hz_3 ) + } else { + frame_mode + }; + + println!( "Frame mode:\t{:?}", frame_mode ); + + let rows_per_pattern = get_u32( bytes.by_ref() )?; + + println!( "Rows/pattern:\t{}", rows_per_pattern ); + + let patterns = get_u8( bytes.by_ref() )?; + + println!( "Patterns:\t{}", patterns ); + + // TODO !! + + Ok( + DmfModule { version, time_base, speed_a, speed_b, frame_mode, rows_per_pattern, patterns } + ) + } + + pub fn to_esf( self ) -> Result, Box> { + // TODO !! + todo!() + } + +} \ No newline at end of file