DMF soundtrack module

master
Ashley N. 2023-08-12 17:08:10 -04:00
parent 9799226e80
commit 81326d32df
4 changed files with 205 additions and 26 deletions

View File

@ -9,4 +9,5 @@ edition = "2018"
[dependencies] [dependencies]
image = "^0.23" image = "^0.23"
colour = "^0.5" colour = "^0.5"
clap = "^2.33" clap = "^2.33"
flate2 = "1.0.26"

View File

@ -3,16 +3,17 @@ extern crate clap;
#[macro_use] #[macro_use]
extern crate colour; extern crate colour;
use clap::{App, Arg, SubCommand};
mod reskit; mod reskit;
use std::error::Error;
use clap::{App, Arg, SubCommand};
use reskit::soundtrack::DmfModule;
use reskit::utility; use reskit::utility;
use reskit::tileset; use reskit::tileset;
fn main() { fn run() -> Result<(), Box<dyn Error>> {
let matches = App::new( "reskit" ) let matches = App::new( "reskit" )
.version( "0.0.1a" ) .version( "0.0.2a" )
.author( "(c) 2021 Ashley N. <ne0ndrag0n@ne0ndrag0n.com>" ) .author( "(c) 2021-2023 Ashley N. <ne0ndrag0n@ne0ndrag0n.com>" )
.about( "Sega Megadrive resource kit and format converter" ) .about( "Sega Megadrive resource kit and format converter" )
.subcommand( .subcommand(
SubCommand::with_name( "tileset" ) SubCommand::with_name( "tileset" )
@ -23,7 +24,7 @@ fn main() {
Arg::with_name( "FORMAT" ) Arg::with_name( "FORMAT" )
.short( "f" ) .short( "f" )
.long( "format" ) .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" ) .default_value( "bin" )
.takes_value( true ) .takes_value( true )
) )
@ -31,31 +32,63 @@ fn main() {
Arg::with_name( "TILEORDER" ) Arg::with_name( "TILEORDER" )
.short( "to" ) .short( "to" )
.long( "tile-order" ) .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" ) .default_value( "tile" )
.takes_value( true ) .takes_value( true )
) )
) )
.subcommand(
SubCommand::with_name( "soundtrack" )
.about( "Generate an on-console soundtrack from a chiptune tracker module" )
.arg_from_usage( "-i, --input=<FILE> 'Specify input module'" )
.arg_from_usage( "-o, --output=<FILE> '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_matches();
// Get arguments for tileset
if let Some( matches ) = matches.subcommand_matches( "tileset" ) { if let Some( matches ) = matches.subcommand_matches( "tileset" ) {
// Get input and output filenames let input_filename = matches.value_of( "input" ).ok_or( "expected: input filename (-i,--input)" )?;
if let Some( input_filename ) = matches.value_of( "input" ) { let output_filename = matches.value_of( "output" ).ok_or( "expected: output_filename (-o,--output)" )?;
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)" );
}
}
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 ) );
}
}

View File

@ -1,2 +1,3 @@
pub mod soundtrack;
pub mod tileset; pub mod tileset;
pub mod utility; pub mod utility;

144
src/reskit/soundtrack.rs Normal file
View File

@ -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<String, Box<dyn Error>> {
let took = bytes.take( take );
let took: Vec<u8> = took.cloned().collect();
let took = from_utf8( &took )?.to_string();
Ok( took )
}
fn get_u8( bytes: &mut Iter<'_, u8> ) -> Result<u8, Box<dyn Error>> {
let took = bytes.take( 1 ).cloned().next().ok_or( "invalid file: expected 1 byte" )?;
Ok( took )
}
fn get_u32( bytes: &mut Iter<'_, u8> ) -> Result<u32, Box<dyn Error>> {
let took = bytes.take( 4 );
let took: Vec<u8> = 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<DmfModule, Box<dyn Error>> {
let mut file = File::open( path )?;
let mut compressed: Vec<u8> = Vec::new();
file.read_to_end( &mut compressed )?;
let mut inflator = ZlibDecoder::new( &compressed[..] );
let mut bytes: Vec<u8> = 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<Vec<u8>, Box<dyn Error>> {
// TODO !!
todo!()
}
}