DMF soundtrack module
parent
9799226e80
commit
81326d32df
|
@ -10,3 +10,4 @@ edition = "2018"
|
|||
image = "^0.23"
|
||||
colour = "^0.5"
|
||||
clap = "^2.33"
|
||||
flate2 = "1.0.26"
|
83
src/main.rs
83
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<dyn Error>> {
|
||||
let matches = App::new( "reskit" )
|
||||
.version( "0.0.1a" )
|
||||
.author( "(c) 2021 Ashley N. <ne0ndrag0n@ne0ndrag0n.com>" )
|
||||
.version( "0.0.2a" )
|
||||
.author( "(c) 2021-2023 Ashley N. <ne0ndrag0n@ne0ndrag0n.com>" )
|
||||
.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=<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 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 ) );
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod soundtrack;
|
||||
pub mod tileset;
|
||||
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