diff --git a/src/main.rs b/src/main.rs index f9a7b2a..2f18a48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ use std::error::Error; use std::fs::File; use std::io::Write; use clap::{App, Arg, SubCommand}; -use reskit::soundtrack::engines::echo::EchoFormat; +use reskit::soundtrack::engines::echo::engine::EchoFormat; use reskit::soundtrack::formats::dmf::DmfModule; use reskit::utility; use reskit::tileset; diff --git a/src/reskit/soundtrack/engines/echo/dmf.rs b/src/reskit/soundtrack/engines/echo/dmf.rs new file mode 100644 index 0000000..4e8104a --- /dev/null +++ b/src/reskit/soundtrack/engines/echo/dmf.rs @@ -0,0 +1,360 @@ +use std::{collections::HashMap, cmp::{max, min}, error::Error}; +use convert_case::{Case, Casing}; +use samplerate::convert; +use uuid::Uuid; +use crate::reskit::{soundtrack::{formats::dmf::{DmfModule, ECHO_EWF_SAMPLE_RATE}, types::{InstrumentType, SampleFormat, Channel, PatternRow}}, 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}; + + +impl EchoFormat for DmfModule { + + fn get_artifacts( &self, 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(); + + for instrument in &self.instruments { + match &instrument.instrument_type { + InstrumentType::Fm2612( settings ) => { + instrument_filenames.push( format!( "{}.eif", instrument.name ) ); + + // 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 ]; + + // 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; + + 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; + } + + 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 ); + + files.insert( format!( "{}.eif", if instrument.name.is_empty() { Uuid::new_v4().to_string() } else { instrument.name.to_owned() } ), eif ); + }, + InstrumentType::PsgDcsg( settings ) => { + instrument_filenames.push( format!( "{}.eef", instrument.name ) ); + + // 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(); + + // 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![ 0x00 ]; + 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 ); + + files.insert( format!( "{}.eef", if instrument.name.is_empty() { Uuid::new_v4().to_string() } else { instrument.name.to_owned() } ), envelope ); + } + } + } + + for sample in &self.samples { + instrument_filenames.push( format!( "{}.ewf", sample.name ) ); + + // 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 ); + + 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(); + + // 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" ); + } + + // 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 )?; + + // 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 ); + + files.insert( format!( "{}.ewf", if sample.name.is_empty() { Uuid::new_v4().to_string() } else { sample.name.to_owned() } ), data ); + } + + // 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" ) ); + + echo_asm += &format!( "{}:\n", soundtrack_label ); + 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( ".", "_" ).to_case( Case::Pascal ) ); + } + + echo_asm += &format!( "\tEcho_ListEnd\n\n" ); + + for filename in &instrument_filenames { + echo_asm += &format!( "Instr_{}:\n", filename.replace( ".", "_" ).to_case( Case::Pascal ) ); + echo_asm += &format!( "\tincbin '{}{}'\n\n", artifact_path, filename ); + } + + files.insert( format!( "music.asm" ), echo_asm.into_bytes() ); + + Ok( files ) + } + + fn get_esf( &self ) -> Result, Box> { + let mut esf: Vec = Vec::new(); + + // Write deflemask rows one at a time. + // Final pass will combine delays into single delay events + // to save space on the cartridge. + + // ESF ticks are not the same as Deflemask ticks! + // A series of events fit into a single ESF tick, which ends + // upon the next delay event or stop event. + // Use instrument changes wisely, as these are expensive register writes. + // PCM playback stalls the stream pipeline as well so use wisely. + + // FM1 -> SN4 channel states (used to compute effects and to aid in not duplicating instrument sets) + // Since trackers like to set the same instrument each time a note on is played... + let mut channels: [Channel; 10] = [ + Channel::new( ESF_FM_1 ), Channel::new( ESF_FM_2 ), Channel::new( ESF_FM_3 ), Channel::new( ESF_FM_4 ), Channel::new( ESF_FM_5 ), Channel::new( ESF_FM_6 ), + Channel::new( ESF_PSG_1 ), Channel::new( ESF_PSG_2 ), Channel::new( ESF_PSG_3 ), Channel::new( ESF_PSG_4 ) + ]; + + // Iterate for each row, for each channel + // Recall items are stored as self.channel_patterns[ channel ][ row_number ] + let mut all_events: Vec = Vec::new(); + for row_number in 0..self.rows_per_pattern { + let events_this_row: Vec = get_events_for_row( + &mut channels, + { + let mut columns: Vec<&PatternRow> = Vec::new(); + for channel in 0..10 { + columns.push( &self.channel_patterns[ channel ][ row_number as usize ] ); + } + + columns + }, + if row_number % 2 == 0 { + self.speed_a * self.time_base + } else { + self.speed_b * self.time_base + }, + self.instruments.len() as u8 + )?; + + // Transfer ESF events to the main stream + all_events.extend( events_this_row ); + } + + // Compact sequences of delays to save rom space + let all_events = compact_delays( all_events )?; + for event in all_events { + esf.extend( event ); + } + + esf.push( 0xFF ); // Terminate the stream + + Ok( esf ) + } + +} + +fn get_eef_volume( dmf_volume: u8 ) -> Result> { + Ok( min( 0x0F - dmf_volume, 0x0F ) ) +} + +fn get_eef_shift( dmf_shift: i8 ) -> Result> { + // Clamp to [-12, 12] + let as_i8: i8 = max( -12, min( dmf_shift, 12 ) ); + + Ok( + match as_i8 { + 0 => 0x00, + + 1 => 0x10, -1 => 0x80, + 2 => 0x20, -2 => 0x90, + 3 => 0x30, -3 => 0xA0, + 4 => 0x40, -4 => 0xB0, + 5 | 6 => 0x50, -5 | -6 => 0xC0, + 7 | 8 => 0x60, -7 | -8 => 0xD0, + 9 | 10 | 11 | 12 => 0x70, -9 | -10 | -11 | -12 => 0xE0, + + _ => return Err( "internal error: arpeggio shift out of defined range" )? + } + ) +} \ No newline at end of file diff --git a/src/reskit/soundtrack/engines/echo.rs b/src/reskit/soundtrack/engines/echo/engine.rs similarity index 93% rename from src/reskit/soundtrack/engines/echo.rs rename to src/reskit/soundtrack/engines/echo/engine.rs index c224581..783a6c0 100644 --- a/src/reskit/soundtrack/engines/echo.rs +++ b/src/reskit/soundtrack/engines/echo/engine.rs @@ -1,7 +1,6 @@ use std::{collections::HashMap, error::Error}; use linked_hash_set::LinkedHashSet; - -use crate::reskit::{soundtrack::{types::{PatternRow, Note, Channel, OctaveFrequency, Effect}, formats::dmf::DmfModule}, utility::print_warning}; +use crate::reskit::{soundtrack::types::{PatternRow, Note, Channel, OctaveFrequency, Effect}, utility::print_warning}; // https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt const ESF_NOTE_ON: u8 = 0x00; @@ -61,7 +60,7 @@ pub trait EchoFormat { } -fn get_semitone( note: &Note ) -> Result> { +pub fn get_semitone( note: &Note ) -> Result> { Ok( match note { Note::NoteOff => return Err( "internal error: attempted to get semitone for a Note::NoteOff" )?, Note::C(_) => ESF_SEMITONE_C, @@ -82,7 +81,7 @@ fn get_semitone( note: &Note ) -> Result> { /** * Notes are used to index PCM sample playback. C = 1, etc. */ -fn get_pcm_index( note: &Note ) -> Result> { +pub fn get_pcm_index( note: &Note ) -> Result> { Ok( match note { Note::NoteOff => return Err( "internal error: attempted to get pcm index for a Note::NoteOff" )?, Note::C(_) => 0, @@ -108,7 +107,7 @@ fn get_pcm_index( note: &Note ) -> Result> { D - 722 | F# - 910 | A# - 1146 D# - 765 | G - 964 | B - 1214 */ -fn get_semitone_frequency( semitone: u8 ) -> Result> { +pub fn get_semitone_frequency( semitone: u8 ) -> Result> { Ok( match semitone { ESF_SEMITONE_C => 644, ESF_SEMITONE_C_SHARP => 681, @@ -126,15 +125,15 @@ fn get_semitone_frequency( semitone: u8 ) -> Result> { } ) } -fn get_fm_note_byte( octave: u8, semitone: u8 ) -> u8 { +pub fn get_fm_note_byte( octave: u8, semitone: u8 ) -> u8 { 32 * octave + 2 * semitone + 1 } -fn get_psg_note_byte( octave: u8, semitone: u8 ) -> u8 { +pub fn get_psg_note_byte( octave: u8, semitone: u8 ) -> u8 { 24 * octave + 2 * semitone } -fn get_note( note: &Note, channel: u8 ) -> Result> { +pub fn get_note( note: &Note, channel: u8 ) -> Result> { Ok( match channel { ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => get_fm_note_byte( note.get_octave()?, get_semitone( note )? ), ESF_PSG_1..=ESF_PSG_4 => get_psg_note_byte( note.get_octave()?, get_semitone( note )? ), @@ -142,7 +141,7 @@ fn get_note( note: &Note, channel: u8 ) -> Result> { } ) } -fn get_volume( channel: u8, volume: u8 ) -> Result> { +pub fn get_volume( channel: u8, volume: u8 ) -> Result> { Ok( match channel { ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => 0x7F - volume, ESF_PSG_1..=ESF_PSG_4 => 0x0F - volume, @@ -153,7 +152,7 @@ fn get_volume( channel: u8, volume: u8 ) -> Result> { /** * Generate an echo delay event. "delay" is the amount to wait, in 1/60 of a second ticks. */ -fn get_delay( delay: u8 ) -> Result> { +pub fn get_delay( delay: u8 ) -> Result> { let events: EchoEvent = if delay == 0 { // No delay to generate! Probably should be an error vec![] @@ -170,7 +169,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( channel: &mut Channel, row: &PatternRow, pcm_offset: u8 ) -> Result, Box> { +pub fn get_events_for_channel( channel: &mut Channel, row: &PatternRow, pcm_offset: u8 ) -> Result, Box> { let mut events: Vec = Vec::new(); // Adjust volume @@ -219,7 +218,7 @@ fn get_events_for_channel( channel: &mut Channel, row: &PatternRow, pcm_offset: * Add effects from a given row to a particular channel and perform activities due on insert. This may generate Echo * events that should be applied to the stream. */ -fn apply_effects_to_channel( channel: &mut Channel, effects: &LinkedHashSet ) -> Result, Box> { +pub fn apply_effects_to_channel( channel: &mut Channel, effects: &LinkedHashSet ) -> Result, Box> { let mut events: Vec = Vec::new(); for effect in effects { @@ -332,7 +331,7 @@ fn apply_effects_to_channel( channel: &mut Channel, effects: &LinkedHashSet ) -> Result, Box> { +pub fn sequence_to_compacted_delay( current_sequence: &Vec ) -> Result, Box> { let mut new_events: Vec = Vec::new(); let mut cumulative_delay: u16 = 0; @@ -393,7 +392,7 @@ pub fn compact_delays( events: Vec ) -> Result, Box Result> { +pub fn get_portamento( channel: &mut Channel, portamento_effect: &Effect ) -> Result> { Ok( if channel.id == ESF_FM_6_PCM { // Generate no events if channel is set to pcm @@ -452,7 +451,7 @@ fn get_portamento( channel: &mut Channel, portamento_effect: &Effect ) -> Result /** * Generate a note cut effect for this channel and modify channel state to reflect it */ -fn get_note_cut( channel: &mut Channel, note_cut_effect: &Effect, tick: u8 ) -> Result> { +pub fn get_note_cut( channel: &mut Channel, note_cut_effect: &Effect, tick: u8 ) -> Result> { let after_ticks = match note_cut_effect { Effect::NoteCut { after_ticks } => *after_ticks, _ => return Err( "internal error: provided effect is not the note cut effect" )? @@ -478,7 +477,7 @@ fn get_note_cut( channel: &mut Channel, note_cut_effect: &Effect, tick: u8 ) -> /** * Get effects that require a specific action when computing the event flushes to Echo. */ -fn get_actionable_effects( channels: &mut [Channel] ) -> Result, Box> { +pub fn get_actionable_effects( channels: &mut [Channel] ) -> Result, Box> { // All portamento effects deploy per tick, not per row. So we need to aggregate all portamentos across all // channels for this row, then flush them once per `ticks_to_wait` for this row. let mut actionable_effects: Vec<(usize, Effect)> = Vec::new(); @@ -507,7 +506,7 @@ fn get_actionable_effects( channels: &mut [Channel] ) -> Result Result, Box> { +pub fn get_actionable_effect_sequence( channels: &mut [Channel], ticks_to_wait: u8 ) -> Result, Box> { let mut events: Vec = Vec::new(); let actionable_effects = get_actionable_effects( channels )?; @@ -537,7 +536,7 @@ fn get_actionable_effect_sequence( channels: &mut [Channel], ticks_to_wait: u8 ) /** * Get the delays due at the end of a row. These delays are what flushes the tick to Echo so that it can play. */ -fn get_delays( events: Vec, channels: &mut [Channel], ticks_to_wait: u8 ) -> Result, Box> { +pub fn get_delays( events: Vec, channels: &mut [Channel], ticks_to_wait: u8 ) -> Result, Box> { let mut events: Vec = events; let applied_effects = get_actionable_effect_sequence( channels, ticks_to_wait )?; diff --git a/src/reskit/soundtrack/engines/echo/mod.rs b/src/reskit/soundtrack/engines/echo/mod.rs new file mode 100644 index 0000000..183f3f7 --- /dev/null +++ b/src/reskit/soundtrack/engines/echo/mod.rs @@ -0,0 +1,2 @@ +pub mod dmf; +pub mod engine; \ No newline at end of file diff --git a/src/reskit/soundtrack/formats/dmf.rs b/src/reskit/soundtrack/formats/dmf.rs index 1c71a6f..6879792 100644 --- a/src/reskit/soundtrack/formats/dmf.rs +++ b/src/reskit/soundtrack/formats/dmf.rs @@ -1,16 +1,13 @@ -use std::{error::Error, fs::File, io::Read, convert::TryInto, collections::HashMap, cmp::{min, max}}; +use std::{error::Error, fs::File, io::Read, convert::TryInto, collections::HashMap}; use flate2::read::ZlibDecoder; use linked_hash_set::LinkedHashSet; -use samplerate::convert; -use uuid::Uuid; -use convert_case::{Case, Casing}; -use crate::reskit::{utility::{get_string, get_u8, skip, get_u32, get_i8, get_i32, get_u16, get_i16, Ring, print_warning, print_info}, soundtrack::{types::{SampleFormat, PsgEnvelope, Note, Sample, PsgSettings, PatternRow, Effect, DcsgChannelMode, NoiseType, Channel, Instrument, InstrumentType, Fm2612Operator, Fm2612Settings}, engines::echo::{EchoFormat, EchoEvent, ESF_FM_1, ESF_FM_2, ESF_FM_3, ESF_FM_4, ESF_FM_5, ESF_PSG_1, ESF_FM_6, ESF_PSG_2, ESF_PSG_3, ESF_PSG_4, get_events_for_row, compact_delays}}}; +use crate::reskit::{utility::{get_string, get_u8, skip, get_u32, get_i8, get_i32, get_u16, get_i16, print_info}, soundtrack::types::{SampleFormat, PsgEnvelope, Note, Sample, PsgSettings, PatternRow, Effect, DcsgChannelMode, NoiseType, Instrument, InstrumentType, Fm2612Operator, Fm2612Settings}}; const DMF_MAGIC_NUMBER: &'static str = ".DelekDefleMask."; const DMF_SUPPORTED_VERSION: u8 = 27; const DMF_MD: u8 = 0x02; const DMF_MD_ENHANCED_CH3: u8 = 0x42; -const ECHO_EWF_SAMPLE_RATE: u32 = 10650; +pub const ECHO_EWF_SAMPLE_RATE: u32 = 10650; const DMF_EFFECT_ARPEGGIO: u8 = 0x00; const DMF_EFFECT_PORTAMENTO_UP: u8 = 0x01; @@ -34,15 +31,15 @@ pub struct DmfModule { path: String, platform: u8, version: u8, - time_base: u8, - speed_a: u8, - speed_b: u8, + pub time_base: u8, + pub speed_a: u8, + pub speed_b: u8, frame_mode: FrameMode, - rows_per_pattern: u32, + pub rows_per_pattern: u32, pattern_matrix: Vec>, - channel_patterns: Vec>, - instruments: Vec, - samples: Vec + pub channel_patterns: Vec>, + pub instruments: Vec, + pub samples: Vec } impl DmfModule { @@ -632,357 +629,4 @@ impl DmfModule { ) } -} - -impl EchoFormat for DmfModule { - - fn get_artifacts( &self, 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(); - - for instrument in &self.instruments { - match &instrument.instrument_type { - InstrumentType::Fm2612( settings ) => { - instrument_filenames.push( format!( "{}.eif", instrument.name ) ); - - // 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 ]; - - // 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; - - 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; - } - - 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 ); - - files.insert( format!( "{}.eif", if instrument.name.is_empty() { Uuid::new_v4().to_string() } else { instrument.name.to_owned() } ), eif ); - }, - InstrumentType::PsgDcsg( settings ) => { - instrument_filenames.push( format!( "{}.eef", instrument.name ) ); - - // 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(); - - // 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![ 0x00 ]; - 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 ); - - files.insert( format!( "{}.eef", if instrument.name.is_empty() { Uuid::new_v4().to_string() } else { instrument.name.to_owned() } ), envelope ); - } - } - } - - for sample in &self.samples { - instrument_filenames.push( format!( "{}.ewf", sample.name ) ); - - // 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 ); - - 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(); - - // 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" ); - } - - // 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 )?; - - // 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 ); - - files.insert( format!( "{}.ewf", if sample.name.is_empty() { Uuid::new_v4().to_string() } else { sample.name.to_owned() } ), data ); - } - - // 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" ) ); - - echo_asm += &format!( "{}:\n", soundtrack_label ); - 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( ".", "_" ).to_case( Case::Pascal ) ); - } - - echo_asm += &format!( "\tEcho_ListEnd\n\n" ); - - for filename in &instrument_filenames { - echo_asm += &format!( "Instr_{}:\n", filename.replace( ".", "_" ).to_case( Case::Pascal ) ); - echo_asm += &format!( "\tincbin '{}{}'\n\n", artifact_path, filename ); - } - - files.insert( format!( "music.asm" ), echo_asm.into_bytes() ); - - Ok( files ) - } - - fn get_esf( &self ) -> Result, Box> { - let mut esf: Vec = Vec::new(); - - // Write deflemask rows one at a time. - // Final pass will combine delays into single delay events - // to save space on the cartridge. - - // ESF ticks are not the same as Deflemask ticks! - // A series of events fit into a single ESF tick, which ends - // upon the next delay event or stop event. - // Use instrument changes wisely, as these are expensive register writes. - // PCM playback stalls the stream pipeline as well so use wisely. - - // FM1 -> SN4 channel states (used to compute effects and to aid in not duplicating instrument sets) - // Since trackers like to set the same instrument each time a note on is played... - let mut channels: [Channel; 10] = [ - Channel::new( ESF_FM_1 ), Channel::new( ESF_FM_2 ), Channel::new( ESF_FM_3 ), Channel::new( ESF_FM_4 ), Channel::new( ESF_FM_5 ), Channel::new( ESF_FM_6 ), - Channel::new( ESF_PSG_1 ), Channel::new( ESF_PSG_2 ), Channel::new( ESF_PSG_3 ), Channel::new( ESF_PSG_4 ) - ]; - - // Iterate for each row, for each channel - // Recall items are stored as self.channel_patterns[ channel ][ row_number ] - let mut all_events: Vec = Vec::new(); - for row_number in 0..self.rows_per_pattern { - let events_this_row: Vec = get_events_for_row( - &mut channels, - { - let mut columns: Vec<&PatternRow> = Vec::new(); - for channel in 0..10 { - columns.push( &self.channel_patterns[ channel ][ row_number as usize ] ); - } - - columns - }, - if row_number % 2 == 0 { - self.speed_a * self.time_base - } else { - self.speed_b * self.time_base - }, - self.instruments.len() as u8 - )?; - - // Transfer ESF events to the main stream - all_events.extend( events_this_row ); - } - - // Compact sequences of delays to save rom space - let all_events = compact_delays( all_events )?; - for event in all_events { - esf.extend( event ); - } - - esf.push( 0xFF ); // Terminate the stream - - Ok( esf ) - } - -} - -fn get_eef_volume( dmf_volume: u8 ) -> Result> { - Ok( min( 0x0F - dmf_volume, 0x0F ) ) -} - -fn get_eef_shift( dmf_shift: i8 ) -> Result> { - // Clamp to [-12, 12] - let as_i8: i8 = max( -12, min( dmf_shift, 12 ) ); - - Ok( - match as_i8 { - 0 => 0x00, - - 1 => 0x10, -1 => 0x80, - 2 => 0x20, -2 => 0x90, - 3 => 0x30, -3 => 0xA0, - 4 => 0x40, -4 => 0xB0, - 5 | 6 => 0x50, -5 | -6 => 0xC0, - 7 | 8 => 0x60, -7 | -8 => 0xD0, - 9 | 10 | 11 | 12 => 0x70, -9 | -10 | -11 | -12 => 0xE0, - - _ => return Err( "internal error: arpeggio shift out of defined range" )? - } - ) } \ No newline at end of file