From 8e6e0d9fb868d8efff2cf91f4dc8a26544dce93b Mon Sep 17 00:00:00 2001 From: ashley Date: Tue, 22 Aug 2023 01:40:06 -0400 Subject: [PATCH] Echo ESF events generation --- src/reskit/soundtrack/engines/echo.rs | 118 ++++++++++++++++++++++++++ src/reskit/soundtrack/formats/dmf.rs | 65 +++++++++----- src/reskit/soundtrack/types.rs | 57 ++++++++++--- 3 files changed, 207 insertions(+), 33 deletions(-) diff --git a/src/reskit/soundtrack/engines/echo.rs b/src/reskit/soundtrack/engines/echo.rs index d806168..e3c07aa 100644 --- a/src/reskit/soundtrack/engines/echo.rs +++ b/src/reskit/soundtrack/engines/echo.rs @@ -1,4 +1,52 @@ use std::{collections::HashMap, error::Error}; +use crate::reskit::soundtrack::{types::{PatternRow, Note}, formats::dmf::ChannelState}; + +// https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt +const ESF_NOTE_ON: u8 = 0x00; +const ESF_NOTE_OFF: u8 = 0x10; +const ESF_SET_VOLUME: u8 = 0x20; +const ESF_SET_FREQUENCY: u8 = 0x30; +const ESF_SET_INSTRUMENT: u8 = 0x40; +const ESF_DELAY_SHORT: u8 = 0xD0; +const ESF_SFX_LOCK: u8 = 0xE0; +const ESF_SET_FM_PARAMS: u8 = 0xF0; +const ESF_SET_FM_DIRECT_0: u8 = 0xF8; +const ESF_SET_FM_DIRECT_1: u8 = 0xF9; +const ESF_SET_FLAGS: u8 = 0xFA; +const ESF_CLEAR_FLAGS: u8 = 0xFB; +const ESF_GOTO: u8 = 0xFC; +const ESF_LOOP_SET: u8 = 0xFD; +const ESF_DELAY_LONG: u8 = 0xFE; +const ESF_STOP: u8 = 0xFF; + +pub const ESF_FM_1: u8 = 0x00; +pub const ESF_FM_2: u8 = 0x01; +pub const ESF_FM_3: u8 = 0x02; +pub const ESF_FM_4: u8 = 0x04; +pub const ESF_FM_5: u8 = 0x05; +pub const ESF_FM_6: u8 = 0x06; + +pub const ESF_PSG_1: u8 = 0x08; +pub const ESF_PSG_2: u8 = 0x09; +pub const ESF_PSG_3: u8 = 0x0A; +pub const ESF_PSG_4: u8 = 0x0B; + +pub const ESF_FM_6_PCM: u8 = 0x0C; + +const ESF_SEMITONE_C: u8 = 0; +const ESF_SEMITONE_C_SHARP: u8 = 1; +const ESF_SEMITONE_D: u8 = 2; +const ESF_SEMITONE_D_SHARP: u8 = 3; +const ESF_SEMITONE_E: u8 = 4; +const ESF_SEMITONE_F: u8 = 5; +const ESF_SEMITONE_F_SHARP: u8 = 6; +const ESF_SEMITONE_G: u8 = 7; +const ESF_SEMITONE_G_SHARP: u8 = 8; +const ESF_SEMITONE_A: u8 = 9; +const ESF_SEMITONE_A_SHARP: u8 = 10; +const ESF_SEMITONE_B: u8 = 11; + +pub type EchoEvent = Vec; pub trait EchoFormat { @@ -8,4 +56,74 @@ pub trait EchoFormat { fn get_ewfs( &self ) -> Result>, Box>; + fn get_esf( &self ) -> Result, Box>; +} + +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, + Note::CSharp(_) => ESF_SEMITONE_C_SHARP, + Note::D(_) => ESF_SEMITONE_D, + Note::DSharp(_) => ESF_SEMITONE_D_SHARP, + Note::E(_) => ESF_SEMITONE_E, + Note::F(_) => ESF_SEMITONE_F, + Note::FSharp(_) => ESF_SEMITONE_F_SHARP, + Note::G(_) => ESF_SEMITONE_G, + Note::GSharp(_) => ESF_SEMITONE_G_SHARP, + Note::A(_) => ESF_SEMITONE_A, + Note::ASharp(_) => ESF_SEMITONE_A_SHARP, + Note::B(_) => ESF_SEMITONE_B + } ) +} + +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 { + 24 * octave + 2 * semitone +} + +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 )? ), + _ => return Err( "internal error: invalid channel" )? + } ) +} + +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, + _ => return Err( "internal error: invalid channel" )? + } ) +} + +pub fn get_events( row: &PatternRow, channel: u8, channel_state: &mut ChannelState ) -> Result, Box> { + let mut events: Vec = Vec::new(); + + if let Some( note ) = &row.note { + if let Note::NoteOff = note { + events.push( vec![ ESF_NOTE_OFF | channel ] ); + } else { + events.push( vec![ ESF_NOTE_ON | channel, get_note( note, channel )? ] ); + } + } + + if let Some( volume ) = &row.volume { + // https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt#L206-L207 + events.push( vec![ ESF_SET_VOLUME | channel, get_volume( channel, *volume )? ] ); + } + + if let Some( instrument ) = &row.instrument_index { + // Do not set the same instrument if it was already set before + if &row.instrument_index != &channel_state.current_instrument { + events.push( vec![ ESF_SET_INSTRUMENT | channel, *instrument as u8 ] ); + channel_state.current_instrument = Some( *instrument ); + } + } + + Ok( events ) } \ No newline at end of file diff --git a/src/reskit/soundtrack/formats/dmf.rs b/src/reskit/soundtrack/formats/dmf.rs index cd74511..719c162 100644 --- a/src/reskit/soundtrack/formats/dmf.rs +++ b/src/reskit/soundtrack/formats/dmf.rs @@ -2,7 +2,7 @@ use std::{error::Error, fs::File, io::Read, convert::TryInto, collections::HashM use flate2::read::ZlibDecoder; use samplerate::convert; use uuid::Uuid; -use crate::reskit::{utility::{get_string, get_u8, skip, get_u32, get_i8, get_i32, get_u16, get_i16, Ring, print_warning}, soundtrack::{types::{SampleFormat, PsgEnvelope, Note, Sample, PsgSettings}, engines::echo::EchoFormat}}; +use crate::reskit::{utility::{get_string, get_u8, skip, get_u32, get_i8, get_i32, get_u16, get_i16, Ring, print_warning}, soundtrack::{types::{SampleFormat, PsgEnvelope, Note, Sample, PsgSettings, PatternRow, Effect}, engines::echo::{EchoFormat, EchoEvent}}}; const DMF_MAGIC_NUMBER: &'static str = ".DelekDefleMask."; const DMF_SUPPORTED_VERSION: u8 = 27; @@ -54,21 +54,6 @@ pub struct Instrument { instrument_type: InstrumentType } - -#[derive(Debug)] -pub struct Effect { - effect_code: i16, - effect_value: Option -} - -#[derive(Debug)] -pub struct PatternRow { - note: Option, - volume: Option, - effects: Vec, - instrument_index: Option -} - pub struct DmfModule { platform: u8, version: u8, @@ -83,6 +68,12 @@ pub struct DmfModule { samples: Vec } +#[derive(Default, Debug)] +pub struct ChannelState { + pub current_instrument: Option, + pub current_volume: Option +} + impl DmfModule { pub fn from_file( path: &str ) -> Result> { @@ -110,7 +101,7 @@ impl DmfModule { 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)" ), + DMF_MD_ENHANCED_CH3 => return Err( "Extended Ch. 3 mode not yet supported (coming soon!) - if your sequence does not use Extended Ch. 3 mode, try re-exporting as standard mode." )?, _ => return Err( "invalid file: invalid console format" )? }; @@ -498,7 +489,7 @@ impl DmfModule { for _ in 0..patterns_count { for row_id in 0..rows_per_pattern { let note = get_u16( bytes.by_ref() )?; - let octave = get_u16( bytes.by_ref() )?; + let octave = get_u16( bytes.by_ref() )? as u8; let note: Option = if note == 0 && octave == 0 { None @@ -524,10 +515,10 @@ impl DmfModule { }; let volume = get_i16( bytes.by_ref() )?; - let volume: Option = if volume == -1 { + let volume: Option = if volume == -1 { None } else { - Some( volume ) + Some( volume as u8 ) }; let mut effects: Vec = Vec::new(); @@ -866,6 +857,40 @@ impl EchoFormat for DmfModule { Ok( ewfs ) } + 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 channel_settings: [ChannelState; 10] = [ + ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default(), + ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default() + ]; + + // Iterate for each row, for each channel + // Recall items are stored as self.channel_patterns[ channel ][ row_number ] + let mut delay: usize = 0; + for row_number in 0..self.rows_per_pattern { + let mut events_this_row: Vec = Vec::new(); + + for channel in 0..self.channel_patterns.len() { + // TODO !! + } + } + + Ok( esf ) + } + } fn get_eef_volume( dmf_volume: u8 ) -> Result> { diff --git a/src/reskit/soundtrack/types.rs b/src/reskit/soundtrack/types.rs index 289715c..b5601f1 100644 --- a/src/reskit/soundtrack/types.rs +++ b/src/reskit/soundtrack/types.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, error::Error}; #[derive(Debug)] pub struct PsgEnvelope< DataFormat > { @@ -34,23 +34,54 @@ pub struct Sample { #[derive(Debug)] pub enum Note { NoteOff, - CSharp( u16 ), - D( u16 ), - DSharp( u16 ), - E( u16 ), - F( u16 ), - FSharp( u16 ), - G( u16 ), - GSharp( u16 ), - A( u16 ), - ASharp( u16 ), - B( u16 ), - C( u16 ) + CSharp( u8 ), + D( u8 ), + DSharp( u8 ), + E( u8 ), + F( u8 ), + FSharp( u8 ), + G( u8 ), + GSharp( u8 ), + A( u8 ), + ASharp( u8 ), + B( u8 ), + C( u8 ) } +#[derive(Debug)] +pub struct Effect { + pub effect_code: i16, + pub effect_value: Option +} + +#[derive(Debug)] +pub struct PatternRow { + pub note: Option, + pub volume: Option, + pub effects: Vec, + pub instrument_index: Option +} impl Note { + pub fn get_octave( &self ) -> Result> { + Ok( match self { + Note::NoteOff => return Err( "internal error: attempted to get octave for Note::NoteOff" )?, + Note::CSharp( octave ) => *octave, + Note::D( octave ) => *octave, + Note::DSharp( octave ) => *octave, + Note::E( octave ) => *octave, + Note::F( octave ) => *octave, + Note::FSharp( octave ) => *octave, + Note::G( octave ) => *octave, + Note::GSharp( octave ) => *octave, + Note::A( octave ) => *octave, + Note::ASharp( octave ) => *octave, + Note::B( octave ) => *octave, + Note::C( octave ) => *octave + } ) + } + pub fn get_freq( &self ) -> Option { match self { Note::NoteOff => None,