Echo ESF events generation
parent
8326f10617
commit
8e6e0d9fb8
|
@ -1,4 +1,52 @@
|
||||||
use std::{collections::HashMap, error::Error};
|
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<u8>;
|
||||||
|
|
||||||
pub trait EchoFormat {
|
pub trait EchoFormat {
|
||||||
|
|
||||||
|
@ -8,4 +56,74 @@ pub trait EchoFormat {
|
||||||
|
|
||||||
fn get_ewfs( &self ) -> Result<HashMap<String, Vec<u8>>, Box<dyn Error>>;
|
fn get_ewfs( &self ) -> Result<HashMap<String, Vec<u8>>, Box<dyn Error>>;
|
||||||
|
|
||||||
|
fn get_esf( &self ) -> Result<Vec<u8>, Box<dyn Error>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_semitone( note: &Note ) -> Result<u8, Box<dyn Error>> {
|
||||||
|
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<u8, Box<dyn Error>> {
|
||||||
|
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<u8, Box<dyn Error>> {
|
||||||
|
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<Vec<EchoEvent>, Box<dyn Error>> {
|
||||||
|
let mut events: Vec<EchoEvent> = 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 )
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ use std::{error::Error, fs::File, io::Read, convert::TryInto, collections::HashM
|
||||||
use flate2::read::ZlibDecoder;
|
use flate2::read::ZlibDecoder;
|
||||||
use samplerate::convert;
|
use samplerate::convert;
|
||||||
use uuid::Uuid;
|
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_MAGIC_NUMBER: &'static str = ".DelekDefleMask.";
|
||||||
const DMF_SUPPORTED_VERSION: u8 = 27;
|
const DMF_SUPPORTED_VERSION: u8 = 27;
|
||||||
|
@ -54,21 +54,6 @@ pub struct Instrument {
|
||||||
instrument_type: InstrumentType
|
instrument_type: InstrumentType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Effect {
|
|
||||||
effect_code: i16,
|
|
||||||
effect_value: Option<i16>
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PatternRow {
|
|
||||||
note: Option<Note>,
|
|
||||||
volume: Option<i16>,
|
|
||||||
effects: Vec<Effect>,
|
|
||||||
instrument_index: Option<u16>
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DmfModule {
|
pub struct DmfModule {
|
||||||
platform: u8,
|
platform: u8,
|
||||||
version: u8,
|
version: u8,
|
||||||
|
@ -83,6 +68,12 @@ pub struct DmfModule {
|
||||||
samples: Vec<Sample>
|
samples: Vec<Sample>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct ChannelState {
|
||||||
|
pub current_instrument: Option<u16>,
|
||||||
|
pub current_volume: Option<u8>
|
||||||
|
}
|
||||||
|
|
||||||
impl DmfModule {
|
impl DmfModule {
|
||||||
|
|
||||||
pub fn from_file( path: &str ) -> Result<DmfModule, Box<dyn Error>> {
|
pub fn from_file( path: &str ) -> Result<DmfModule, Box<dyn Error>> {
|
||||||
|
@ -110,7 +101,7 @@ impl DmfModule {
|
||||||
let platform = get_u8( bytes.by_ref() )?;
|
let platform = get_u8( bytes.by_ref() )?;
|
||||||
match platform {
|
match platform {
|
||||||
DMF_MD => println!( "Mode:\tSega Megadrive" ),
|
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" )?
|
_ => return Err( "invalid file: invalid console format" )?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -498,7 +489,7 @@ impl DmfModule {
|
||||||
for _ in 0..patterns_count {
|
for _ in 0..patterns_count {
|
||||||
for row_id in 0..rows_per_pattern {
|
for row_id in 0..rows_per_pattern {
|
||||||
let note = get_u16( bytes.by_ref() )?;
|
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<Note> = if note == 0 && octave == 0 {
|
let note: Option<Note> = if note == 0 && octave == 0 {
|
||||||
None
|
None
|
||||||
|
@ -524,10 +515,10 @@ impl DmfModule {
|
||||||
};
|
};
|
||||||
|
|
||||||
let volume = get_i16( bytes.by_ref() )?;
|
let volume = get_i16( bytes.by_ref() )?;
|
||||||
let volume: Option<i16> = if volume == -1 {
|
let volume: Option<u8> = if volume == -1 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some( volume )
|
Some( volume as u8 )
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut effects: Vec<Effect> = Vec::new();
|
let mut effects: Vec<Effect> = Vec::new();
|
||||||
|
@ -866,6 +857,40 @@ impl EchoFormat for DmfModule {
|
||||||
Ok( ewfs )
|
Ok( ewfs )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_esf( &self ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
|
let mut esf: Vec<u8> = 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<EchoEvent> = Vec::new();
|
||||||
|
|
||||||
|
for channel in 0..self.channel_patterns.len() {
|
||||||
|
// TODO !!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok( esf )
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_eef_volume( dmf_volume: u8 ) -> Result<u8, Box<dyn Error>> {
|
fn get_eef_volume( dmf_volume: u8 ) -> Result<u8, Box<dyn Error>> {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, error::Error};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct PsgEnvelope< DataFormat > {
|
pub struct PsgEnvelope< DataFormat > {
|
||||||
|
@ -34,23 +34,54 @@ pub struct Sample {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Note {
|
pub enum Note {
|
||||||
NoteOff,
|
NoteOff,
|
||||||
CSharp( u16 ),
|
CSharp( u8 ),
|
||||||
D( u16 ),
|
D( u8 ),
|
||||||
DSharp( u16 ),
|
DSharp( u8 ),
|
||||||
E( u16 ),
|
E( u8 ),
|
||||||
F( u16 ),
|
F( u8 ),
|
||||||
FSharp( u16 ),
|
FSharp( u8 ),
|
||||||
G( u16 ),
|
G( u8 ),
|
||||||
GSharp( u16 ),
|
GSharp( u8 ),
|
||||||
A( u16 ),
|
A( u8 ),
|
||||||
ASharp( u16 ),
|
ASharp( u8 ),
|
||||||
B( u16 ),
|
B( u8 ),
|
||||||
C( u16 )
|
C( u8 )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Effect {
|
||||||
|
pub effect_code: i16,
|
||||||
|
pub effect_value: Option<i16>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PatternRow {
|
||||||
|
pub note: Option<Note>,
|
||||||
|
pub volume: Option<u8>,
|
||||||
|
pub effects: Vec<Effect>,
|
||||||
|
pub instrument_index: Option<u16>
|
||||||
|
}
|
||||||
|
|
||||||
impl Note {
|
impl Note {
|
||||||
|
|
||||||
|
pub fn get_octave( &self ) -> Result<u8, Box<dyn Error>> {
|
||||||
|
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<f32> {
|
pub fn get_freq( &self ) -> Option<f32> {
|
||||||
match self {
|
match self {
|
||||||
Note::NoteOff => None,
|
Note::NoteOff => None,
|
||||||
|
|
Loading…
Reference in New Issue