DMF soundtrack module
parent
9799226e80
commit
81326d32df
|
@ -10,3 +10,4 @@ edition = "2018"
|
||||||
image = "^0.23"
|
image = "^0.23"
|
||||||
colour = "^0.5"
|
colour = "^0.5"
|
||||||
clap = "^2.33"
|
clap = "^2.33"
|
||||||
|
flate2 = "1.0.26"
|
67
src/main.rs
67
src/main.rs
|
@ -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(
|
tileset::generate(
|
||||||
input_filename,
|
input_filename,
|
||||||
output_filename,
|
output_filename,
|
||||||
matches.value_of( "FORMAT" ).unwrap(),
|
matches.value_of( "FORMAT" ).unwrap(),
|
||||||
matches.value_of( "TILEORDER" ).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 {
|
} else {
|
||||||
utility::print_error( "expected: output_filename (-o,--output)" );
|
Err( "no tool provided (try --help)" )?
|
||||||
}
|
|
||||||
} else {
|
|
||||||
utility::print_error( "expected: input filename (-i,--input)" );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
utility::print_error( "no plugin provided (try --help)" );
|
fn main() {
|
||||||
|
if let Err( error ) = run() {
|
||||||
|
utility::print_error( &format!( "{}", error ) );
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
|
pub mod soundtrack;
|
||||||
pub mod tileset;
|
pub mod tileset;
|
||||||
pub mod utility;
|
pub mod utility;
|
|
@ -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!()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue