diff --git a/Cargo.toml b/Cargo.toml index d2e7889..89c88e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,5 @@ samplerate = "0.2.4" hound = "3.5.0" pitch_shift = "1.0.0" linked_hash_set = "0.1.4" +linked-hash-map = "0.5.6" convert_case = "0.6.0" \ No newline at end of file diff --git a/src/reskit/cli/evaluator.rs b/src/reskit/cli/evaluator.rs index 25ca473..8582152 100644 --- a/src/reskit/cli/evaluator.rs +++ b/src/reskit/cli/evaluator.rs @@ -1,6 +1,6 @@ -use std::{error::Error, fs::File, io::Write}; +use std::{error::Error, fs::File, io::Write, path::Path}; use clap::Parser; -use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::EchoFormat}, utility::print_good}; +use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::print_good}; use super::settings::{Args, Tools, TileOutputFormat, TileOrder}; pub fn run_command() -> Result<(), Box> { @@ -20,22 +20,43 @@ pub fn run_command() -> Result<(), Box> { TileOrder::Sprite => "sprite" } ), - Tools::Soundtrack { input_file, output_file, input_format: _, output_format: _, source_file_format: _, artifact_output_directory, soundtrack_label, instrument_list_label } => { - let result: Vec = input_file.iter().map( | filename | Ok( DmfModule::from_file( &filename )? ) ).collect::, Box>>()?; - // very temporary! - let result: &DmfModule = result.first().ok_or( "must provide input_file" )?; + Tools::Soundtrack { input_file, output_directory, input_format: _, output_format: _, source_file_format: _, artifact_output_directory } => { + let output_directory = if output_directory.ends_with( "/" ) { output_directory.to_string() } else { format!( "{}/", output_directory ) }; + let artifact_output_directory = if artifact_output_directory.ends_with( "/" ) { artifact_output_directory.to_string() } else { format!( "{}/", artifact_output_directory ) }; - for ( filename, data ) in result.get_artifacts( 0, &soundtrack_label, &output_file, &artifact_output_directory, &instrument_list_label )? { - println!( "Creating artifact file {}{}", artifact_output_directory, filename ); - let mut file = File::create( format!( "{}{}", artifact_output_directory, filename ) )?; - file.write_all( &data )? + let modules: Vec = input_file.iter().map( | filename | Ok( DmfModule::from_file( &filename )? ) ).collect::, Box>>()?; + + let mut combined_asset_list = DmfModule::get_combined_assets_list( artifact_output_directory.clone(), &modules )?; + if combined_asset_list.instruments.len() > 255 { + return Err( "combined instrument set exceeds 255 instruments; try converting fewer modules or modules with fewer instruments" )?; + } + if combined_asset_list.samples.len() > 255 { + return Err( "combined sample set exceeds 255 samples; try converting fewer modules or modules with fewer samples" )?; } - println!( "Creating ESF output file {}", output_file ); - let mut file = File::create( &output_file )?; - file.write_all( &result.get_esf()? )?; + // Output the ESF sequences + for i in 0..modules.len() { + let module = &modules[ i ]; - print_good( &format!( "successfully compiled soundtrack for {}", output_file ) ); + let path = Path::new( &module.path ); + let original_filename_stem = path.file_stem().ok_or( "internal error: unable to parse filename" )?.to_string_lossy(); + let output_file = format!( "{}{}.esf", output_directory, original_filename_stem ); + + combined_asset_list.sequences.push( output_file.clone() ); + + println!( "Creating ESF output file {}", output_file ); + let mut file = File::create( &output_file )?; + file.write_all( &module.get_esf( &combined_asset_list )? )?; + + print_good( &format!( "successfully compiled soundtrack for {}", output_file ) ); + } + + // Output the shared artifacts used by all ESF sequences + println!( "Writing sequence artifacts..." ); + let shared_artifacts = combined_asset_list.to_bytes()?; + println!( "Writing sequence artifact {}", format!( "{}music.asm", artifact_output_directory ) ); + let mut file = File::create( format!( "{}music.asm", artifact_output_directory ) )?; + file.write_all( &shared_artifacts )?; } }; diff --git a/src/reskit/cli/settings.rs b/src/reskit/cli/settings.rs index 88a9be9..9a88113 100644 --- a/src/reskit/cli/settings.rs +++ b/src/reskit/cli/settings.rs @@ -64,13 +64,13 @@ pub enum Tools { #[command(name = "soundtrack")] #[command(about = "Generate a console-compatible soundtrack from a sequence file.")] Soundtrack { - /// Input filename + /// Input filename(s) #[arg(short, long)] input_file: Vec, - /// Output filename (if multiple input_files are provided, used as output parent directory) - #[arg(short, long)] - output_file: String, + /// Output directory + #[arg(short, long, default_value_t=String::from("./"))] + output_directory: String, /// Input sequence file format (the kind of tracker used to compose the track) #[arg(long, value_enum, default_value_t=SequenceFormat::Dmf)] @@ -86,14 +86,6 @@ pub enum Tools { /// Directory to output artifacts (instruments and samples) #[arg(long, default_value_t=String::from( "./" ))] - artifact_output_directory: String, - - /// Identifier used for the soundtrack in the generated source file - #[arg(long, default_value_t=String::from( "MuzPlaceholder1" ))] - soundtrack_label: String, - - /// Identifier used for the soundtrack's artifacts in the generated source file - #[arg(long, default_value_t=String::from( "MuzInstrumentList" ))] - instrument_list_label: String + artifact_output_directory: String } } \ No newline at end of file diff --git a/src/reskit/soundtrack/engines/echo/dmf.rs b/src/reskit/soundtrack/engines/echo/dmf.rs index 2c9dda3..67f0243 100644 --- a/src/reskit/soundtrack/engines/echo/dmf.rs +++ b/src/reskit/soundtrack/engines/echo/dmf.rs @@ -1,295 +1,359 @@ -use std::{collections::HashMap, cmp::{max, min}, error::Error}; -use convert_case::{Case, Casing}; +use std::{cmp::{max, min}, error::Error, fs::File, io::Write}; use samplerate::convert; -use crate::reskit::{soundtrack::{formats::dmf::{DmfModule, ECHO_EWF_SAMPLE_RATE}, types::{InstrumentType, SampleFormat, Channel, PatternRow, Effect}}, utility::{print_warning, Ring}}; -use super::engine::{EchoFormat, ESF_FM_1, ESF_FM_2, ESF_FM_3, ESF_FM_4, ESF_FM_5, ESF_FM_6, ESF_PSG_1, ESF_PSG_2, ESF_PSG_3, ESF_PSG_4, EchoEvent, get_events_for_row, compact_delays, ESF_SET_LOOP, ESF_GO_TO_LOOP, ESF_STOP}; +use crate::reskit::{soundtrack::{formats::dmf::{DmfModule, ECHO_EWF_SAMPLE_RATE}, types::{InstrumentType, SampleFormat, Channel, PatternRow, Effect, CombinedAssets, Instrument, Sample}}, utility::{print_warning, Ring, symbol_to_pascal}}; +use super::engine::{EchoFormat, ESF_FM_1, ESF_FM_2, ESF_FM_3, ESF_FM_4, ESF_FM_5, ESF_FM_6, ESF_PSG_1, ESF_PSG_2, ESF_PSG_3, ESF_PSG_4, EchoEvent, get_events_for_row, compact_delays, ESF_SET_LOOP, ESF_GO_TO_LOOP, ESF_STOP, EchoArtifact}; -impl EchoFormat for DmfModule { +impl EchoArtifact for Instrument { - fn get_artifacts( &self, file_index: u32, soundtrack_label: &str, soundtrack_path: &str, artifact_path: &str, instr_list_label: &str ) -> Result>, Box> { - let mut files: HashMap> = HashMap::new(); - // This next list preserves order of filenames to generate the echo .asm file - let mut instrument_filenames: Vec = Vec::new(); + fn to_bytes( &self ) -> Result, Box> { + let mut data: Vec = Vec::new(); - let mut index = 0; - for instrument in &self.instruments { - match &instrument.instrument_type { - InstrumentType::Fm2612( settings ) => { - // Create feedback + algorithm byte - let alg_fb: u8 = ( settings.fb << 3 ) | settings.alg; + match &self.instrument_type { + InstrumentType::Fm2612( settings ) => { + // Create feedback + algorithm byte + let alg_fb: u8 = ( settings.fb << 3 ) | settings.alg; - // The operators are laid out as FmOperator objects in order 1 -> 3 -> 2 -> 4 - // EIF instrument layout below - let mut mult_dt: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; - let mut tl: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; - let mut ar_rs: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; - let mut dr_am: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; - let mut sr: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; - let mut rr_sl: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; - let mut ssg_eg: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; + // The operators are laid out as FmOperator objects in order 1 -> 3 -> 2 -> 4 + // EIF instrument layout below + let mut mult_dt: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; + let mut tl: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; + let mut ar_rs: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; + let mut dr_am: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; + let mut sr: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; + let mut rr_sl: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; + let mut ssg_eg: [u8; 4] = [ 0x00, 0x00, 0x00, 0x00 ]; - // Likewise, "operators" is laid out 1 -> 3 -> 2 -> 4 - for i in 0..4 { - let dt: u8 = match settings.operators[ i ].dt { - 0 => 0b000, - 1 => 0b001, - 2 => 0b010, - 3 => 0b011, - -1 => 0b101, - -2 => 0b110, - -3 => 0b111, - _ => return Err( "invalid file: invalid value given for dt" )? - }; + // Likewise, "operators" is laid out 1 -> 3 -> 2 -> 4 + for i in 0..4 { + let dt: u8 = match settings.operators[ i ].dt { + 0 => 0b000, + 1 => 0b001, + 2 => 0b010, + 3 => 0b011, + -1 => 0b101, + -2 => 0b110, + -3 => 0b111, + _ => return Err( "invalid file: invalid value given for dt" )? + }; - // https://plutiedev.com/ym2612-registers - // https://github.com/sikthehedgehog/Echo/blob/master/doc/eif.txt - mult_dt[ i ] = ( dt << 4 ) | settings.operators[ i ].mult; - tl[ i ] = settings.operators[ i ].tl; - ar_rs[ i ] = ( settings.operators[ i ].rs << 6 ) | settings.operators[ i ].ar; - dr_am[ i ] = ( settings.operators[ i ].am << 7 ) | settings.operators[ i ].dr; - sr[ i ] = settings.operators[ i ].d2r; // "Sometimes also called 'second decay rate' (D2R)." - rr_sl[ i ] = ( settings.operators[ i ].sl << 4 ) | settings.operators[ i ].rr; + // https://plutiedev.com/ym2612-registers + // https://github.com/sikthehedgehog/Echo/blob/master/doc/eif.txt + mult_dt[ i ] = ( dt << 4 ) | settings.operators[ i ].mult; + tl[ i ] = settings.operators[ i ].tl; + ar_rs[ i ] = ( settings.operators[ i ].rs << 6 ) | settings.operators[ i ].ar; + dr_am[ i ] = ( settings.operators[ i ].am << 7 ) | settings.operators[ i ].dr; + sr[ i ] = settings.operators[ i ].d2r; // "Sometimes also called 'second decay rate' (D2R)." + rr_sl[ i ] = ( settings.operators[ i ].sl << 4 ) | settings.operators[ i ].rr; - if settings.operators[ i ].ssg_mode > 0 { - print_warning( &format!( "SSG-EG mode set on instrument {}, operator {}. this operator may not work on clone hardware or certain emulators", instrument.name, i ) ); - } - - ssg_eg[ i ] = settings.operators[ i ].ssg_mode; + if settings.operators[ i ].ssg_mode > 0 { + print_warning( &format!( "SSG-EG mode set on instrument {}, operator {}. this operator may not work on clone hardware or certain emulators", self.name, i ) ); } - let mut eif: Vec = Vec::new(); - eif.push( alg_fb ); - eif.extend( mult_dt ); - eif.extend( tl ); - eif.extend( ar_rs ); - eif.extend( dr_am ); - eif.extend( sr ); - eif.extend( rr_sl ); - eif.extend( ssg_eg ); - - let filename = format!( "file{}_ins{}_{}.eif", file_index, index, instrument.name ); - instrument_filenames.push( filename.clone() ); - files.insert( filename, eif ); - }, - InstrumentType::PsgDcsg( settings ) => { - // Echo uses volume and arpeggio envelopes - // Noise envelopes are usable but cannot be articulated in an instrument. - // For use of the noise envelope, use ESF $0Bnn/$3Bnn and $3Ann in the stream. - - // comments using example: 20 vol values, 6 arp values, both repeat from the beginning - - // generate the initial portion (always). this is just an expansion - // of the envelopes, without loops, into two Vec's. - // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 - // 01 02 03 04 05 06 -- -- -- -- -- -- -- -- -- -- -- -- -- -- - let mut volume_envelope: Vec = settings.volume.envelope.iter().map( | long | *long as u8 ).collect(); - let mut arpeggio_envelope: Vec = settings.arpeggio.envelope.iter().map( | long | *long as i8 ).collect(); - - // Validations (this makes my life easier when dmf format changes again) - if volume_envelope.is_empty() { - // Push one max volume if there is no volume defined in this instrument - volume_envelope.push( 0x0F ); - } - if arpeggio_envelope.is_empty() { - // Push one byte of no arpeggio shift if no arpeggio is defined in this instrument - arpeggio_envelope.push( 0x00 ); - } - - // now take a slice of the repeatable portion of the smaller array, - // and use it to expand the smaller array to the same size as the - // larger array. if there is no repeatable portion, take a one-element - // slice of the last element in the array. - // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 - // 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 - let mut volume_repeat_pattern: Ring = Ring::create( - if let Some( loop_at ) = settings.volume.loop_at { - Vec::from( &volume_envelope[ loop_at.. ] ) - } else { - vec![ volume_envelope[ volume_envelope.len() - 1 ] ] - }, - None - ); - let mut arpeggio_repeat_pattern: Ring = Ring::create( - if let Some( loop_at ) = settings.arpeggio.loop_at { - Vec::from( &arpeggio_envelope[ loop_at.. ] ) - } else { - vec![ arpeggio_envelope[ arpeggio_envelope.len() - 1 ] ] - }, - None - ); - - if volume_envelope.len() < arpeggio_envelope.len() { - let difference = arpeggio_envelope.len() - volume_envelope.len(); - for _ in 0..difference { - volume_envelope.push( - volume_repeat_pattern.next().expect( "fatal: internal error (volume_repeat_pattern)" ) - ); - } - } else if arpeggio_envelope.len() < volume_envelope.len() { - let difference = volume_envelope.len() - arpeggio_envelope.len(); - for _ in 0..difference { - arpeggio_envelope.push( - arpeggio_repeat_pattern.next().expect( "fatal: internal error (arpeggio_repeat_pattern)" ) - ); - } - } - - let interval_size = max( volume_repeat_pattern.len(), arpeggio_repeat_pattern.len() ); - - // now, you generate the repeating portion using the two repeat_pattern iterators. - // each segment at a time is generated by the size of the larger array. - // generate it until the first repeat pattern is regenerated. - // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 .. (volume repeats from beginning) - // 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 .. (arpeggio repeats up from where it left off) - - // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 .. - // 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 .. - - // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 .. - // 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 .. - let mut volume_repeat: Vec = Vec::new(); - let mut arpeggio_repeat: Vec = Vec::new(); - - let mut initial_volume_repeat: Vec = Vec::new(); - let mut initial_arpeggio_repeat: Vec = Vec::new(); - for _ in 0..interval_size { - initial_volume_repeat.push( volume_repeat_pattern.next().expect( "fatal: internal error (volume_repeat_pattern)" ) ); - initial_arpeggio_repeat.push( arpeggio_repeat_pattern.next().expect( "fatal: internal error (arpeggio_repeat_pattern)" ) ); - } - - volume_repeat.extend( &initial_volume_repeat ); - arpeggio_repeat.extend( &initial_arpeggio_repeat ); - - loop { - // Guard against OOM - if volume_repeat.len() > 65535 { - return Err( "eef error: psg instrument repeat period too long (try modifying repeating envelopes to have evenly-divisible parameters)" )? - } - - let mut volume_repeat_period: Vec = Vec::new(); - let mut arpeggio_repeat_period: Vec = Vec::new(); - for _ in 0..interval_size { - volume_repeat_period.push( volume_repeat_pattern.next().expect( "fatal: internal error (volume_repeat_pattern)" ) ); - arpeggio_repeat_period.push( arpeggio_repeat_pattern.next().expect( "fatal: internal error (arpeggio_repeat_pattern)" ) ); - } - - // End loop once the pattern begins repeating - if initial_volume_repeat == volume_repeat_period && initial_arpeggio_repeat == arpeggio_repeat_period { - break; - } else { - // If it doesn't repeat, append and generate another period - volume_repeat.append( &mut volume_repeat_period ); - arpeggio_repeat.append( &mut arpeggio_repeat_period ); - } - } - - // Now generate the Echo EEF envelope from volume_envelope/volume_repeat + arpeggio_envelope/arpeggio_repeat - let mut envelope: Vec = vec![]; - for i in 0..volume_envelope.len() { - envelope.push( get_eef_volume( volume_envelope[ i ] )? | get_eef_shift( arpeggio_envelope[ i ] )? ); - } - - // Echo begin loop command - envelope.push( 0xFE ); - - for i in 0..volume_repeat.len() { - envelope.push( get_eef_volume( volume_repeat[ i ] )? | get_eef_shift( arpeggio_repeat[ i ] )? ); - } - - // Echo end loop command - envelope.push( 0xFF ); - - let filename = format!( "file{}_ins{}_{}.eef", file_index, index, instrument.name ); - instrument_filenames.push( filename.clone() ); - files.insert( filename, envelope ); + ssg_eg[ i ] = settings.operators[ i ].ssg_mode; } - } - index += 1; + data.push( alg_fb ); + data.extend( mult_dt ); + data.extend( tl ); + data.extend( ar_rs ); + data.extend( dr_am ); + data.extend( sr ); + data.extend( rr_sl ); + data.extend( ssg_eg ); + } + InstrumentType::PsgDcsg( settings ) => { + // Echo uses volume and arpeggio envelopes + // Noise envelopes are usable but cannot be articulated in an instrument. + // For use of the noise envelope, use ESF $0Bnn/$3Bnn and $3Ann in the stream. + + // comments using example: 20 vol values, 6 arp values, both repeat from the beginning + + // generate the initial portion (always). this is just an expansion + // of the envelopes, without loops, into two Vec's. + // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 + // 01 02 03 04 05 06 -- -- -- -- -- -- -- -- -- -- -- -- -- -- + let mut volume_envelope: Vec = settings.volume.envelope.iter().map( | long | *long as u8 ).collect(); + let mut arpeggio_envelope: Vec = settings.arpeggio.envelope.iter().map( | long | *long as i8 ).collect(); + + // Validations (this makes my life easier when dmf format changes again) + if volume_envelope.is_empty() { + // Push one max volume if there is no volume defined in this instrument + volume_envelope.push( 0x0F ); + } + if arpeggio_envelope.is_empty() { + // Push one byte of no arpeggio shift if no arpeggio is defined in this instrument + arpeggio_envelope.push( 0x00 ); + } + + // now take a slice of the repeatable portion of the smaller array, + // and use it to expand the smaller array to the same size as the + // larger array. if there is no repeatable portion, take a one-element + // slice of the last element in the array. + // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 + // 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 + let mut volume_repeat_pattern: Ring = Ring::create( + if let Some( loop_at ) = settings.volume.loop_at { + Vec::from( &volume_envelope[ loop_at.. ] ) + } else { + vec![ volume_envelope[ volume_envelope.len() - 1 ] ] + }, + None + ); + let mut arpeggio_repeat_pattern: Ring = Ring::create( + if let Some( loop_at ) = settings.arpeggio.loop_at { + Vec::from( &arpeggio_envelope[ loop_at.. ] ) + } else { + vec![ arpeggio_envelope[ arpeggio_envelope.len() - 1 ] ] + }, + None + ); + + if volume_envelope.len() < arpeggio_envelope.len() { + let difference = arpeggio_envelope.len() - volume_envelope.len(); + for _ in 0..difference { + volume_envelope.push( + volume_repeat_pattern.next().expect( "fatal: internal error (volume_repeat_pattern)" ) + ); + } + } else if arpeggio_envelope.len() < volume_envelope.len() { + let difference = volume_envelope.len() - arpeggio_envelope.len(); + for _ in 0..difference { + arpeggio_envelope.push( + arpeggio_repeat_pattern.next().expect( "fatal: internal error (arpeggio_repeat_pattern)" ) + ); + } + } + + let interval_size = max( volume_repeat_pattern.len(), arpeggio_repeat_pattern.len() ); + + // now, you generate the repeating portion using the two repeat_pattern iterators. + // each segment at a time is generated by the size of the larger array. + // generate it until the first repeat pattern is regenerated. + // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 .. (volume repeats from beginning) + // 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 .. (arpeggio repeats up from where it left off) + + // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 .. + // 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 .. + + // 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 .. + // 01 02 03 04 05 06 01 02 03 04 05 06 01 02 03 04 05 06 01 02 .. + let mut volume_repeat: Vec = Vec::new(); + let mut arpeggio_repeat: Vec = Vec::new(); + + let mut initial_volume_repeat: Vec = Vec::new(); + let mut initial_arpeggio_repeat: Vec = Vec::new(); + for _ in 0..interval_size { + initial_volume_repeat.push( volume_repeat_pattern.next().expect( "fatal: internal error (volume_repeat_pattern)" ) ); + initial_arpeggio_repeat.push( arpeggio_repeat_pattern.next().expect( "fatal: internal error (arpeggio_repeat_pattern)" ) ); + } + + volume_repeat.extend( &initial_volume_repeat ); + arpeggio_repeat.extend( &initial_arpeggio_repeat ); + + loop { + // Guard against OOM + if volume_repeat.len() > 65535 { + return Err( "eef error: psg instrument repeat period too long (try modifying repeating envelopes to have evenly-divisible parameters)" )? + } + + let mut volume_repeat_period: Vec = Vec::new(); + let mut arpeggio_repeat_period: Vec = Vec::new(); + for _ in 0..interval_size { + volume_repeat_period.push( volume_repeat_pattern.next().expect( "fatal: internal error (volume_repeat_pattern)" ) ); + arpeggio_repeat_period.push( arpeggio_repeat_pattern.next().expect( "fatal: internal error (arpeggio_repeat_pattern)" ) ); + } + + // End loop once the pattern begins repeating + if initial_volume_repeat == volume_repeat_period && initial_arpeggio_repeat == arpeggio_repeat_period { + break; + } else { + // If it doesn't repeat, append and generate another period + volume_repeat.append( &mut volume_repeat_period ); + arpeggio_repeat.append( &mut arpeggio_repeat_period ); + } + } + + // Now generate the Echo EEF envelope from volume_envelope/volume_repeat + arpeggio_envelope/arpeggio_repeat + let mut envelope: Vec = vec![]; + for i in 0..volume_envelope.len() { + envelope.push( get_eef_volume( volume_envelope[ i ] )? | get_eef_shift( arpeggio_envelope[ i ] )? ); + } + + // Echo begin loop command + envelope.push( 0xFE ); + + for i in 0..volume_repeat.len() { + envelope.push( get_eef_volume( volume_repeat[ i ] )? | get_eef_shift( arpeggio_repeat[ i ] )? ); + } + + // Echo end loop command + envelope.push( 0xFF ); + + data.extend( envelope.into_iter() ); + } } - index = 0; - for sample in &self.samples { - // Amplify data in original sample - let data: Vec = sample.data - .iter() - .map( | pcm_sample | { - let big_pcm_sample: i32 = min( ( *pcm_sample as i32 ) + sample.amp as i32, i16::MAX as i32 ); + Ok( data ) + } - big_pcm_sample as i16 - } ) - .collect(); +} - // Convert to f32 for various library operations - let data: Vec = data - .into_iter() - .map( | pcm_sample | { - if let SampleFormat::Bits8 = sample.bitrate { - ( pcm_sample as u8 ) as f32 / 255.0 - } else { - pcm_sample as f32 / 32767.0 - } - } ) - .collect(); +impl EchoArtifact for Sample { - // TODO: Adjust pitch using crate pitch_shift - if sample.pitch > 0 { - print_warning( "pitch shift not yet implemented for pcm samples. your drums/voice samples/etc may sound a bit funny" ); - } + fn to_bytes( &self ) -> Result, Box> { + // Amplify data in original sample + let data: Vec = self.data + .iter() + .map( | pcm_sample | { + let big_pcm_sample: i32 = min( ( *pcm_sample as i32 ) + self.amp as i32, i16::MAX as i32 ); - // Use libsamplerate to resample from whatever it is now to 10650Hz, the sample rate - // required by Echo sound engine EWF samples. - let data: Vec = convert( sample.rate, ECHO_EWF_SAMPLE_RATE, 1, samplerate::ConverterType::SincBestQuality, &data )?; + big_pcm_sample as i16 + } ) + .collect(); - // Convert from f32 back to i16, then downconvert to u8 - let mut data: Vec = data - .into_iter() - .map( | pcm_sample | { - let pcm_sample = ( ( pcm_sample + 1.0 ) * 128.0 ) as u8; + // Convert to f32 for various library operations + let data: Vec = data + .into_iter() + .map( | pcm_sample | { + if let SampleFormat::Bits8 = self.bitrate { + ( pcm_sample as u8 ) as f32 / 255.0 + } else { + pcm_sample as f32 / 32767.0 + } + } ) + .collect(); - // Do not end stream prematurely (EWF format uses 0xFF to end stream) - if pcm_sample == 0xFF { - 0xFE - } else { - pcm_sample - } - } ) - .collect(); - - // Terminate stream - data.push( 0xFF ); - - let filename = format!( "file{}_sample{}_{}.ewf", file_index, index, sample.name ); - instrument_filenames.push( filename.clone() ); - files.insert( filename, data ); - - index += 1; + // TODO: Adjust pitch using crate pitch_shift + if self.pitch > 0 { + print_warning( "pitch shift not yet implemented for pcm samples. your drums/voice samples/etc may sound a bit funny" ); } + // Use libsamplerate to resample from whatever it is now to 10650Hz, the sample rate + // required by Echo sound engine EWF samples. + let data: Vec = convert( self.rate, ECHO_EWF_SAMPLE_RATE, 1, samplerate::ConverterType::SincBestQuality, &data )?; + + // Convert from f32 back to i16, then downconvert to u8 + let mut data: Vec = data + .into_iter() + .map( | pcm_sample | { + let pcm_sample = ( ( pcm_sample + 1.0 ) * 128.0 ) as u8; + + // Do not end stream prematurely (EWF format uses 0xFF to end stream) + if pcm_sample == 0xFF { + 0xFE + } else { + pcm_sample + } + } ) + .collect(); + + // Terminate stream + data.push( 0xFF ); + + Ok( data ) + } + +} + +impl EchoArtifact for CombinedAssets { + + fn to_bytes( &self ) -> Result, Box> { // Write Echo ASM file that includes all the instruments - let mut echo_asm: String = format!( "; Echo instrument definitions file\n; Generated by reskit v{}\n\n", env!( "CARGO_PKG_VERSION" ) ); + let mut echo_asm: String = format!( "; Echo (esf) instruments and sequences source file\n; Generated by reskit v{}\n\n", env!( "CARGO_PKG_VERSION" ) ); + let mut index = 0; - echo_asm += &format!( "{}:\n", soundtrack_label ); - echo_asm += &format!( "\tincbin '{}'\n\n", soundtrack_path ); + for soundtrack_path in &self.sequences { + echo_asm += &format!( "{}:\n", symbol_to_pascal( soundtrack_path ) ); + echo_asm += &format!( "\tincbin '{}'\n\n", soundtrack_path ); + } - echo_asm += &format!( "{}:\n", instr_list_label ); - for filename in &instrument_filenames { - echo_asm += &format!( "\tEcho_ListEntry Instr_{}\n", filename.replace( ".", "_" ).replace( "-", "_" ).to_case( Case::Pascal ) ); + echo_asm += &format!( "Instr_List:\n" ); + + let instrument_paths: Vec = self.instruments.iter() + .map( | instrument | + format!( + "Instrument{}_{}.{}", + if instrument.name.is_empty() { + format!( "_NoName" ) + } else { + format!( "_{}", &instrument.name ) + }, + { + let given = index; + index += 1; + given + }, + match &instrument.instrument_type { + InstrumentType::Fm2612(_) => "eif", + InstrumentType::PsgDcsg(_) => "eef" + } + ) + ) + .collect(); + + let sample_paths: Vec = self.samples.iter() + .map( | sample | + format!( + "Sample{}_{}.ewf", + if sample.name.is_empty() { + format!( "_NoName" ) + } else { + format!( "_{}", &sample.name ) + }, + { + let given = index; + index += 1; + given + } + ) + ) + .collect(); + + for instrument_path in &instrument_paths { + echo_asm += &format!( "\tEcho_ListEntry Instr_{}\n", symbol_to_pascal( instrument_path ) ); + } + for sample_path in &sample_paths { + echo_asm += &format!( "\tEcho_ListEntry Sample_{}\n", symbol_to_pascal( sample_path ) ); } echo_asm += &format!( "\tEcho_ListEnd\n\n" ); - for filename in &instrument_filenames { - echo_asm += &format!( "Instr_{}:\n", filename.replace( ".", "_" ).replace( "-", "_" ).to_case( Case::Pascal ) ); - echo_asm += &format!( "\tincbin '{}{}'\n\n", artifact_path, filename ); + for i in 0..self.instruments.len() { + let instrument = &self.instruments[ i ]; + let instrument_path = &instrument_paths[ i ]; + let instrument_total_path = format!( "{}{}", self.output_prefix, instrument_path ); + + echo_asm += &format!( "Instr_{}:\n", symbol_to_pascal( instrument_path ) ); + echo_asm += &format!( "\tincbin '{}'\n\n", instrument_total_path ); + + println!( "Writing sequence artifact {}", instrument_total_path ); + let instrument_data = instrument.to_bytes()?; + let mut file = File::create( instrument_total_path )?; + file.write_all( &instrument_data )?; } - files.insert( format!( "music.asm" ), echo_asm.into_bytes() ); + for i in 0..self.samples.len() { + let sample = &self.samples[ i ]; + let sample_path = &sample_paths[ i ]; + let sample_total_path = format!( "{}{}", self.output_prefix, sample_path ); - Ok( files ) + echo_asm += &format!( "Sample_{}:\n", symbol_to_pascal( sample_path ) ); + echo_asm += &format!( "\tincbin '{}'\n\n", sample_total_path ); + + println!( "Writing sequence artifact {}", sample_total_path ); + let sample_data = sample.to_bytes()?; + let mut file = File::create( sample_total_path )?; + file.write_all( &sample_data )?; + } + + Ok( echo_asm.into_bytes() ) } - fn get_esf( &self ) -> Result, Box> { +} + +impl EchoFormat for DmfModule { + + fn get_esf( &self, combined_assets: &CombinedAssets ) -> Result, Box> { let mut esf: Vec = Vec::new(); // Write deflemask rows one at a time. @@ -327,13 +391,14 @@ impl EchoFormat for DmfModule { let events_this_row: Vec = get_events_for_row( &mut channels, &self.instruments, + &self.samples, + &combined_assets, columns.clone(), if row_number % 2 == 0 { self.speed_a * self.time_base } else { self.speed_b * self.time_base - }, - self.instruments.len() as u8 + } )?; // Is this the pattern matrix row that we repeat to? diff --git a/src/reskit/soundtrack/engines/echo/engine.rs b/src/reskit/soundtrack/engines/echo/engine.rs index d467c7b..192881e 100644 --- a/src/reskit/soundtrack/engines/echo/engine.rs +++ b/src/reskit/soundtrack/engines/echo/engine.rs @@ -1,6 +1,7 @@ use std::{collections::{HashMap, HashSet}, error::Error, cmp::{min, max}}; +use linked_hash_map::LinkedHashMap; use linked_hash_set::LinkedHashSet; -use crate::reskit::{soundtrack::types::{PatternRow, Note, Channel, OctaveFrequency, Effect, Instrument, NoiseType, DcsgChannelMode, InstrumentType}, utility::print_warning}; +use crate::reskit::{soundtrack::types::{PatternRow, Note, Channel, OctaveFrequency, Effect, Instrument, NoiseType, DcsgChannelMode, InstrumentType, CombinedAssets, Sample}, utility::print_warning}; // https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt const ESF_NOTE_ON: u8 = 0x00; @@ -59,9 +60,13 @@ const ESF_SEMITONE_A: u8 = 9; const ESF_SEMITONE_A_SHARP: u8 = 10; const ESF_SEMITONE_B: u8 = 11; -struct InstrumentSet { +struct InstrumentSet<'a> { fm_ids: HashSet, psg_ids: HashSet, + module_set: &'a Vec, + total_set: &'a Vec, + module_samples: &'a Vec, + total_samples: &'a Vec, default_psg_id: Option } @@ -69,9 +74,13 @@ pub type EchoEvent = Vec; pub trait EchoFormat { - fn get_artifacts( &self, file_index: u32, soundtrack_label: &str, soundtrack_path: &str, artifact_path: &str, instrument_list_label: &str ) -> Result>, Box>; + fn get_esf( &self, combined_assets: &CombinedAssets ) -> Result, Box>; - fn get_esf( &self ) -> Result, Box>; +} + +pub trait EchoArtifact { + + fn to_bytes( &self ) -> Result, Box>; } @@ -280,7 +289,7 @@ fn get_delay( delay: u8 ) -> Result> { * For a specific row and channel, get the events this row and channel contribute to the stream. * While doing so, update the state of the channel for future event generation. */ -fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: &PatternRow, pcm_offset: u8, instrument_set: &InstrumentSet ) -> Result, Box> { +fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: &PatternRow, asset_set: &InstrumentSet ) -> Result, Box> { let mut events: Vec = Vec::new(); // Adjust volume @@ -295,20 +304,25 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: if let Some( instrument ) = &row.instrument_index { // Validate the instrument is applicable to the current channel let correct_type = match channels[ active_channel ].id { - ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => instrument_set.fm_ids.contains( instrument ), + ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => asset_set.fm_ids.contains( instrument ), ESF_FM_6_PCM => { print_warning( "attempted to set instrument on FM6 when it is in pcm mode. this is valid, but it won't do anything until the next time it is set back to fm mode." ); - instrument_set.fm_ids.contains( instrument ) + asset_set.fm_ids.contains( instrument ) }, - ESF_PSG_1..=ESF_PSG_4 => instrument_set.psg_ids.contains( instrument ), + ESF_PSG_1..=ESF_PSG_4 => asset_set.psg_ids.contains( instrument ), invalid_channel => return Err( format!( "internal error: get_events_for_channel: invalid channel {:#04X}", invalid_channel ) )? }; + // Hop one layer of indirection from `instrument` by getting it from instrument_set.module_set, + // then finding the index of that instrument in instrument_set.total_set. + let original_instrument = &asset_set.module_set[ *instrument as usize ]; + let instrument = asset_set.total_set.iter().position( | instrument | instrument == original_instrument ).ok_or( "internal error: could not locate instrument in combined instrument set" )?; + // Do not set the same instrument if it was already set before - if &row.instrument_index != &channels[ active_channel ].active_instrument { + if &channels[ active_channel ].active_instrument != &Some( instrument as u16 ) { if correct_type { - events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, *instrument as u8 ] ); - channels[ active_channel ].active_instrument = Some( *instrument ); + events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] ); + channels[ active_channel ].active_instrument = Some( instrument as u16 ); } else { print_warning( "attempted to set an fm instrument on a psg channel, or vice versa. the instrument was not set - your soundtrack may sound different than expected." ); // Now if current channel is PSG1 through PSG4, it's gonna need an instrument set on it. @@ -316,7 +330,7 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: print_warning( "this is a psg channel, seting the default psg instrument so your note can still play..." ); if row.note.is_some() { // Set "__reskit_default_psg_instrument" - let instrument = instrument_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?; + let instrument = asset_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?; events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] ); channels[ active_channel ].active_instrument = Some( instrument ); @@ -330,7 +344,7 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: if channels[ active_channel ].active_instrument.is_none() && ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channels[ active_channel ].id ) { if row.note.is_some() { // Set "__reskit_default_psg_instrument" - let instrument = instrument_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?; + let instrument = asset_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?; events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] ); channels[ active_channel ].active_instrument = Some( instrument ); @@ -368,7 +382,17 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: ); if channels[ active_channel ].id == ESF_FM_6_PCM { - events.push( vec![ ESF_NOTE_ON | channels[ active_channel ].id, get_pcm_index( note )? + pcm_offset ] ); + let module_pcm_index = get_pcm_index( note )?; + if asset_set.module_samples.get( module_pcm_index as usize ).is_none() { + print_warning( "FM6 is in PCM mode but you specified a note that is not linked to a sample. this has no effect, skipping..." ); + } else { + let sample = &asset_set.module_samples[ module_pcm_index as usize ]; + let global_sample_index = asset_set.total_samples.iter().position( | global_sample | global_sample == sample ).ok_or( "internal error: could not locate sample in combined sample set" )?; + + // Remember, instruments will always be listed first in the output source file, followed by pointers to samples + // So the minimum sample offset is the number of instruments + events.push( vec![ ESF_NOTE_ON | channels[ active_channel ].id, global_sample_index as u8 + asset_set.total_set.len() as u8 ] ); + } } else if channels[ active_channel ].id == ESF_PSG_4 { // Find the single (and it should only ever be a single) Effect::DcsgNoiseMode attached to the channel let dcsg_noise_mode = channels[ active_channel ].active_effects.iter().find( | effect | matches!( effect, Effect::DcsgNoiseMode { mode: _, noise_type: _ } ) ); @@ -805,13 +829,13 @@ fn get_delays( events: Vec, channels: &mut [Channel], ticks_to_wait: * For an entire row across all channels, generate the events that apply to the row as a whole. This usually means applying * the waits so that the row can be flushed to Echo and played - ESF ticks are until the nearest wait or stop event. */ -pub fn get_events_for_row( channels: &mut [Channel], instruments: &Vec, subrows: Vec<&PatternRow>, ticks_to_wait: u8, pcm_offset: u8 ) -> Result, Box> { +pub fn get_events_for_row( channels: &mut [Channel], module_instruments: &Vec, module_samples: &Vec, asset_set: &CombinedAssets, subrows: Vec<&PatternRow>, ticks_to_wait: u8 ) -> Result, Box> { let mut events: Vec = Vec::new(); let mut index = 0; // Find the index of the default psg instrument, if it exists - let default_psg_id: Option = instruments.iter().find_map( | item | { + let default_psg_id: Option = module_instruments.iter().find_map( | item | { if item.name == "__reskit_default_psg_instrument" { Some( index ) } else { @@ -822,7 +846,7 @@ pub fn get_events_for_row( channels: &mut [Channel], instruments: &Vec ) -> Result> { + let mut instruments: Vec = Vec::new(); + let mut samples: Vec = Vec::new(); + + for module in modules { + for instrument in &module.instruments { + if !instruments.contains( instrument ) { + instruments.push( instrument.clone() ); + } + } + + for sample in &module.samples { + if !samples.contains( sample ) { + samples.push( sample.clone() ); + } + } + } + + Ok( CombinedAssets { output_prefix, sequences: Vec::new(), instruments, samples } ) + } + pub fn from_file( path: &str ) -> Result> { let mut file = File::open( path )?; let mut compressed: Vec = Vec::new(); diff --git a/src/reskit/soundtrack/types.rs b/src/reskit/soundtrack/types.rs index 1194cbb..3ae7881 100644 --- a/src/reskit/soundtrack/types.rs +++ b/src/reskit/soundtrack/types.rs @@ -1,15 +1,14 @@ use std::{collections::HashMap, error::Error}; - use linked_hash_set::LinkedHashSet; -#[derive(Debug, PartialEq)] -pub struct PsgEnvelope< DataFormat: PartialEq > { +#[derive(Clone, Debug, PartialEq)] +pub struct PsgEnvelope< DataFormat: PartialEq + Clone > { pub envelope: Vec, pub loop_at: Option, pub settings: HashMap<&'static str, bool> } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct PsgSettings { pub volume: PsgEnvelope< u32 >, pub arpeggio: PsgEnvelope< i32 >, @@ -17,7 +16,7 @@ pub struct PsgSettings { pub wavetable: PsgEnvelope< u32 > } -#[derive(Debug, Default, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Fm2612Operator { pub am: u8, pub ar: u8, @@ -33,7 +32,7 @@ pub struct Fm2612Operator { pub ssg_mode: u8 } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub struct Fm2612Settings { pub alg: u8, pub fb: u8, @@ -42,25 +41,32 @@ pub struct Fm2612Settings { pub operators: [Fm2612Operator; 4] } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum InstrumentType { Fm2612( Fm2612Settings ), PsgDcsg( PsgSettings ) } -#[derive(Debug)] +#[derive(Clone, PartialEq, Debug)] pub struct Instrument { pub name: String, pub instrument_type: InstrumentType } -#[derive(Debug)] +pub struct CombinedAssets { + pub output_prefix: String, + pub sequences: Vec, + pub instruments: Vec, + pub samples: Vec +} + +#[derive(Clone, PartialEq, Debug)] pub enum SampleFormat { Bits8, Bits16 } -#[derive(Debug)] +#[derive(Clone, PartialEq, Debug)] pub struct Sample { pub name: String, pub rate: u32, @@ -173,7 +179,7 @@ impl Note { } ) } - pub fn get_freq( &self ) -> Option { + pub fn _get_freq( &self ) -> Option { match self { Note::NoteOff => None, Note::CSharp( octave ) => match octave { diff --git a/src/reskit/utility.rs b/src/reskit/utility.rs index 6cfe0b5..12eccf3 100644 --- a/src/reskit/utility.rs +++ b/src/reskit/utility.rs @@ -1,5 +1,7 @@ use std::{slice::Iter, error::Error, str::from_utf8, convert::TryInto}; +use convert_case::{Casing, Case}; + pub struct Ring< Type: Copy >{ data: Vec< Type >, position: usize @@ -37,6 +39,15 @@ impl< Type: Copy > Iterator for Ring< Type > { } +pub fn symbol_to_pascal( filename: &str ) -> String { + filename + .replace( "/", "_" ) + .replace( "\\", "_" ) + .replace( ".", "_" ) + .replace( "-", "_" ) + .to_case( Case::Pascal ) +} + pub fn print_error( error_msg: &str ) { red!( "fatal: " ); println!( "{}", error_msg ); }