Additional work generating esf files

master
Ashley N. 2023-08-23 23:51:41 -04:00
parent d86efb29f2
commit 4001f8d3cc
5 changed files with 329 additions and 33 deletions

View File

@ -15,3 +15,4 @@ samplerate = "0.2.4"
hound = "3.5.0" hound = "3.5.0"
pitch_shift = "1.0.0" pitch_shift = "1.0.0"
uuid = { version = "1.4.1", features = [ "v4" ] } uuid = { version = "1.4.1", features = [ "v4" ] }
linked_hash_set = "0.1.4"

View File

@ -13,6 +13,8 @@ use reskit::soundtrack::formats::dmf::DmfModule;
use reskit::utility; use reskit::utility;
use reskit::tileset; use reskit::tileset;
use crate::reskit::utility::print_good;
fn run() -> Result<(), Box<dyn Error>> { fn run() -> Result<(), Box<dyn Error>> {
let matches = App::new( "reskit" ) let matches = App::new( "reskit" )
.version( "0.0.2a" ) .version( "0.0.2a" )
@ -100,7 +102,11 @@ fn run() -> Result<(), Box<dyn Error>> {
file.write_all( &data )? file.write_all( &data )?
} }
// TODO !! println!( "Creating ESF output file {}", output_filename );
let mut file = File::create( output_filename )?;
file.write_all( &result.get_esf()? )?;
print_good( &format!( "successfully compiled soundtrack for {}", output_filename ) );
Ok( () ) Ok( () )
} else { } else {

View File

@ -1,5 +1,7 @@
use std::{collections::HashMap, error::Error}; use std::{collections::HashMap, error::Error};
use crate::reskit::soundtrack::{types::{PatternRow, Note}, formats::dmf::ChannelState}; use linked_hash_set::LinkedHashSet;
use crate::reskit::{soundtrack::types::{PatternRow, Note, Channel, OctaveFrequency, Effect}, utility::print_warning};
// https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt // https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt
const ESF_NOTE_ON: u8 = 0x00; const ESF_NOTE_ON: u8 = 0x00;
@ -33,6 +35,9 @@ pub const ESF_PSG_4: u8 = 0x0B;
pub const ESF_FM_6_PCM: u8 = 0x0C; pub const ESF_FM_6_PCM: u8 = 0x0C;
const ESF_FM_PARAM_LEFT_SPEAKER: u8 = 0x80;
const ESF_FM_PARAM_RIGHT_SPEAKER: u8 = 0x40;
const ESF_SEMITONE_C: u8 = 0; const ESF_SEMITONE_C: u8 = 0;
const ESF_SEMITONE_C_SHARP: u8 = 1; const ESF_SEMITONE_C_SHARP: u8 = 1;
const ESF_SEMITONE_D: u8 = 2; const ESF_SEMITONE_D: u8 = 2;
@ -77,6 +82,32 @@ fn get_semitone( note: &Note ) -> Result<u8, Box<dyn Error>> {
} ) } )
} }
/**
Gets the semitone frequency used by Echo.
https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt#L243
C - 644 | E - 810 | G# - 1021
C# - 681 | F - 858 | A - 1081
D - 722 | F# - 910 | A# - 1146
D# - 765 | G - 964 | B - 1214
*/
fn get_semitone_frequency( semitone: u8 ) -> Result<u16, Box<dyn Error>> {
Ok( match semitone {
ESF_SEMITONE_C => 644,
ESF_SEMITONE_C_SHARP => 681,
ESF_SEMITONE_D => 722,
ESF_SEMITONE_D_SHARP => 765,
ESF_SEMITONE_E => 810,
ESF_SEMITONE_F => 858,
ESF_SEMITONE_F_SHARP => 910,
ESF_SEMITONE_G => 964,
ESF_SEMITONE_G_SHARP => 1021,
ESF_SEMITONE_A => 1081,
ESF_SEMITONE_A_SHARP => 1146,
ESF_SEMITONE_B => 1214,
_ => return Err( "internal error: invalid semitone value provided to get_semitone_frequency" )?
} )
}
fn get_fm_note_byte( octave: u8, semitone: u8 ) -> u8 { fn get_fm_note_byte( octave: u8, semitone: u8 ) -> u8 {
32 * octave + 2 * semitone + 1 32 * octave + 2 * semitone + 1
} }
@ -89,7 +120,7 @@ fn get_note( note: &Note, channel: u8 ) -> Result<u8, Box<dyn Error>> {
Ok( match channel { 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_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 )? ), ESF_PSG_1..=ESF_PSG_4 => get_psg_note_byte( note.get_octave()?, get_semitone( note )? ),
_ => return Err( "internal error: invalid channel" )? invalid_channel => return Err( format!( "internal error: get_note: invalid channel {:#04X}", invalid_channel ) )?
} ) } )
} }
@ -97,33 +128,250 @@ fn get_volume( channel: u8, volume: u8 ) -> Result<u8, Box<dyn Error>> {
Ok( match channel { Ok( match channel {
ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => 0x7F - volume, ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => 0x7F - volume,
ESF_PSG_1..=ESF_PSG_4 => 0x0F - volume, ESF_PSG_1..=ESF_PSG_4 => 0x0F - volume,
_ => return Err( "internal error: invalid channel" )? invalid_channel => return Err( format!( "internal error: get_note: invalid channel {:#04X}", invalid_channel ) )?
} ) } )
} }
pub fn get_events( row: &PatternRow, channel: u8, channel_state: &mut ChannelState ) -> Result<Vec<EchoEvent>, Box<dyn Error>> { /**
* Generate an echo delay event. "delay" is the amount to wait, in 1/60 of a second ticks.
*/
fn get_delay( delay: u8 ) -> Result<EchoEvent, Box<dyn Error>> {
let events: EchoEvent = if delay == 0 {
// No delay to generate! Probably should be an error
vec![]
} else if delay <= 16 {
vec![ ESF_DELAY_SHORT | ( delay - 1 ) ]
} else {
vec![ ESF_DELAY_LONG, delay ]
};
Ok( events )
}
/**
* Generate an Echo frequency shift up event for a given channel, updating active_note to be ready
* for the next event.
*/
fn get_portamento( channel: &mut Channel, portamento_effect: &Effect ) -> Result<EchoEvent, Box<dyn Error>> {
Ok(
if channel.id == ESF_FM_6_PCM {
// Generate no events if channel is set to pcm
print_warning( "attempted to portamento up on FM6 when it is set to pcm. this makes no sense and does nothing" );
vec![]
} else if let Some( octave_frequency ) = channel.active_note {
let ( octave, frequency ) = {
let by_amount: i16 = match portamento_effect {
Effect::PortamentoUp { speed } => *speed as i16,
Effect::PortamentoDown { speed } => -( *speed as i16 ),
_ => return Err( "internal error: provided effect is not a supported portamento effect" )?
};
let new_frequency: i16 = octave_frequency.frequency as i16 + by_amount;
if new_frequency > get_semitone_frequency( ESF_SEMITONE_B )? as i16 {
if octave_frequency.octave == 7 {
// Nowhere else to go up
( 7, get_semitone_frequency( ESF_SEMITONE_B )? )
} else {
// Go up an octave then add the difference to middle C
let difference = new_frequency - get_semitone_frequency( ESF_SEMITONE_B )? as i16;
( octave_frequency.octave + 1, get_semitone_frequency( ESF_SEMITONE_C )? + difference as u16 )
}
} else if new_frequency < get_semitone_frequency( ESF_SEMITONE_C )? as i16 {
if octave_frequency.octave == 0 {
// Nowhere else to go down
( 0, get_semitone_frequency( ESF_SEMITONE_C )? )
} else {
// Go down an octave then subtract the overshoot of C from B
let difference = get_semitone_frequency( ESF_SEMITONE_C )? as i16 - new_frequency;
( octave_frequency.octave - 1, get_semitone_frequency( ESF_SEMITONE_B )? - difference as u16 )
}
} else {
// Move within the same octave
( octave_frequency.octave, new_frequency as u16 )
}
};
// Set the new OctaveFrequency on the channel
channel.active_note = Some( OctaveFrequency { octave, frequency } );
// Generate the note slide ESF events
// YM2612 register format for frequency: https://plutiedev.com/ym2612-registers#reg-A0
vec![
ESF_SET_FREQUENCY | channel.id,
octave << 3 | ( ( ( 0x0700 & frequency ) >> 8 ) as u8 ),
( 0x00FF & frequency ) as u8
]
} else {
// No active note, nothing to generate
vec![]
}
)
}
/**
* 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 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
let mut events: Vec<EchoEvent> = Vec::new(); let mut events: Vec<EchoEvent> = Vec::new();
// Key on or key off note
if let Some( note ) = &row.note { if let Some( note ) = &row.note {
if let Note::NoteOff = note { if let Note::NoteOff = note {
events.push( vec![ ESF_NOTE_OFF | channel ] ); if channel.id == ESF_FM_6_PCM {
// TODO: Completely different scenario required for PCM channel
} else {
channel.active_note = None;
events.push( vec![ ESF_NOTE_OFF | channel.id ] );
}
} else { } else {
events.push( vec![ ESF_NOTE_ON | channel, get_note( note, channel )? ] ); if channel.id == ESF_FM_6_PCM {
// TODO: Completely different scenario required for PCM channel
} else {
channel.active_note = Some(
OctaveFrequency {
octave: note.get_octave()?,
frequency: get_semitone_frequency( get_semitone( note )? )?
}
);
let note = get_note( note, channel.id )?;
events.push( vec![ ESF_NOTE_ON | channel.id, note ] );
}
} }
} }
// Adjust volume
if let Some( volume ) = &row.volume { if let Some( volume ) = &row.volume {
// https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt#L206-L207 // https://github.com/sikthehedgehog/Echo/blob/master/doc/esf.txt#L206-L207
events.push( vec![ ESF_SET_VOLUME | channel, get_volume( channel, *volume )? ] ); let volume = get_volume( channel.id, *volume )?;
channel.current_volume = Some( volume );
events.push( vec![ ESF_SET_VOLUME | channel.id, volume ] );
} }
// Change instrument
if let Some( instrument ) = &row.instrument_index { if let Some( instrument ) = &row.instrument_index {
// Do not set the same instrument if it was already set before // Do not set the same instrument if it was already set before
if &row.instrument_index != &channel_state.current_instrument { if &row.instrument_index != &channel.active_instrument {
events.push( vec![ ESF_SET_INSTRUMENT | channel, *instrument as u8 ] ); events.push( vec![ ESF_SET_INSTRUMENT | channel.id, *instrument as u8 ] );
channel_state.current_instrument = Some( *instrument ); channel.active_instrument = Some( *instrument );
} }
} }
Ok( events ) Ok( events )
} }
/**
* 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<Effect> ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
let mut events: Vec<EchoEvent> = Vec::new();
for effect in effects {
match effect {
Effect::DacEnable { enabled } => {
if !enabled {
if channel.id == ESF_FM_6 || channel.id == ESF_FM_6_PCM {
channel.id = ESF_FM_6;
} else {
print_warning( "Effect::DacEnable can only be applied to FM6. ignoring..." );
}
channel.active_effects.remove( &Effect::DacEnable { enabled: true } );
} else {
if channel.id == ESF_FM_6 || channel.id == ESF_FM_6_PCM {
channel.id = ESF_FM_6_PCM;
} else {
print_warning( "Effect::DacEnable can only be applied to FM6. ignoring..." );
}
channel.active_effects.insert_if_absent( Effect::DacEnable { enabled: true } );
}
},
Effect::SetPanning { left, right } => {
let left = *left;
let right = *right;
if channel.id == ESF_FM_6_PCM || ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channel.id ) {
print_warning( "Effect::SetPanning unsupported on PCM or PSG channel" );
} else {
if !channel.active_effects.contains( &Effect::SetPanning { left, right } ) {
// Remove other active SetPanning effects
channel.active_effects.remove( &Effect::SetPanning { left: true, right: true } );
channel.active_effects.remove( &Effect::SetPanning { left: true, right: false } );
channel.active_effects.remove( &Effect::SetPanning { left: false, right: true } );
channel.active_effects.remove( &Effect::SetPanning { left: false, right: false } );
// Insert new panning commands
let panning: u8 = if left && right {
ESF_FM_PARAM_LEFT_SPEAKER & ESF_FM_PARAM_RIGHT_SPEAKER
} else if left {
ESF_FM_PARAM_LEFT_SPEAKER
} else if right {
ESF_FM_PARAM_RIGHT_SPEAKER
} else {
0x00
};
events.push( vec![ ESF_SET_FM_PARAMS | channel.id, panning ] );
// Insert new active SetPanning effect - will not be doubly added
channel.active_effects.insert( Effect::SetPanning { left, right } );
}
}
},
unsupported_effect => print_warning( &format!( "effect unsupported: {:?}. your soundtrack may sound different than expected.", unsupported_effect ) )
}
}
Ok( events )
}
/**
* 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], subrows: Vec<&PatternRow>, ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
let mut events: Vec<EchoEvent> = Vec::new();
// Get events for each subrow (part of the total row for each channel)
for i in 0..channels.len() {
// Apply effects to channel's current state
events.extend( apply_effects_to_channel( &mut channels[ i ], &subrows[ i ].effects )? );
// Get the ESF events for this channel's part of the row
events.extend( get_events_for_channel( &mut channels[ i ], subrows[ i ] )? );
}
// 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.
// So let's say portamentos are defined on FM1 and FM5 and ticks_to_wait is 3. The generated events are:
// [ freq_shift_fm1, freq_shift_fm5, wait 1 tick, freq_shift_fm1, freq_shift_fm5, wait 1 tick, freq_shift_fm1, freq_shift_fm5, wait 1 tick ]
let mut active_portamentos: Vec<(usize, Effect)> = Vec::new();
for channel_id in 0..channels.len() {
let channel = &channels[ channel_id ];
for effect in &channel.active_effects {
if matches!( effect, Effect::PortamentoDown { speed: _ } ) ||
matches!( effect, Effect::PortamentoUp { speed: _ } ) {
active_portamentos.push( ( channel_id, *effect ) );
}
}
}
if !active_portamentos.is_empty() {
for _tick in 0..ticks_to_wait {
for ( channel_id, portamento ) in &active_portamentos {
let portamento_event: EchoEvent = get_portamento( &mut channels[ *channel_id ], &portamento )?;
events.push( portamento_event );
}
events.push( get_delay( 1 )? );
}
} else {
// Push the amount of ticks to wait for this row. ticks_to_wait is speed_a or speed_b, times base_speed.
events.push( get_delay( ticks_to_wait )? );
}
Ok( events )
}

View File

@ -1,8 +1,9 @@
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, cmp::{min, max}};
use flate2::read::ZlibDecoder; use flate2::read::ZlibDecoder;
use linked_hash_set::LinkedHashSet;
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, PatternRow, Effect, DcsgChannelMode, NoiseType}, engines::echo::{EchoFormat, EchoEvent}}}; 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, DcsgChannelMode, NoiseType, Channel}, 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}}};
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;
@ -79,12 +80,6 @@ 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>> {
@ -532,14 +527,14 @@ impl DmfModule {
Some( volume as u8 ) Some( volume as u8 )
}; };
let mut effects: Vec<Effect> = Vec::new(); let mut effects: LinkedHashSet<Effect> = LinkedHashSet::new();
for _ in 0..num_effects { for _ in 0..num_effects {
let effect_code = get_i16( bytes.by_ref() )?; let effect_code = get_i16( bytes.by_ref() )?;
let effect_value = get_i16( bytes.by_ref() )?; let effect_value = get_i16( bytes.by_ref() )?;
let effect_value: Option<u8> = if effect_value == -1 { None } else { Some( effect_value as u8 ) }; let effect_value: Option<u8> = if effect_value == -1 { None } else { Some( effect_value as u8 ) };
if effect_code != -1 { if effect_code != -1 {
effects.push( effects.insert(
match effect_code as u8 { match effect_code as u8 {
DMF_EFFECT_ARPEGGIO => Effect::Arpeggio { DMF_EFFECT_ARPEGGIO => Effect::Arpeggio {
first_shift: effect_value.ok_or( "invalid file: expected effect value for arpeggio" )? >> 4, first_shift: effect_value.ok_or( "invalid file: expected effect value for arpeggio" )? >> 4,
@ -939,19 +934,34 @@ impl EchoFormat for DmfModule {
// FM1 -> SN4 channel states (used to compute effects and to aid in not duplicating instrument sets) // 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... // Since trackers like to set the same instrument each time a note on is played...
let mut channel_settings: [ChannelState; 10] = [ let mut channels: [Channel; 10] = [
ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default(), 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 ),
ChannelState::default(), ChannelState::default(), ChannelState::default(), ChannelState::default() 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 // Iterate for each row, for each channel
// Recall items are stored as self.channel_patterns[ channel ][ row_number ] // 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 { for row_number in 0..self.rows_per_pattern {
let mut events_this_row: Vec<EchoEvent> = Vec::new(); let events_this_row: Vec<EchoEvent> = 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 ] );
}
for channel in 0..self.channel_patterns.len() { columns
// TODO !! },
if row_number % 2 == 0 {
self.speed_a * self.time_base
} else {
self.speed_b * self.time_base
}
)?;
// Transfer ESF events to the main stream
for event in events_this_row {
esf.extend( event );
} }
} }

View File

@ -1,5 +1,7 @@
use std::{collections::HashMap, error::Error}; use std::{collections::HashMap, error::Error};
use linked_hash_set::LinkedHashSet;
#[derive(Debug)] #[derive(Debug)]
pub struct PsgEnvelope< DataFormat > { pub struct PsgEnvelope< DataFormat > {
pub envelope: Vec<DataFormat>, pub envelope: Vec<DataFormat>,
@ -31,7 +33,7 @@ pub struct Sample {
pub data: Vec<i16> pub data: Vec<i16>
} }
#[derive(Debug)] #[derive(Clone, Copy, Debug)]
pub enum Note { pub enum Note {
NoteOff, NoteOff,
CSharp( u8 ), CSharp( u8 ),
@ -48,19 +50,34 @@ pub enum Note {
C( u8 ) C( u8 )
} }
#[derive(Debug)] #[derive(Clone, Copy, Debug)]
pub struct OctaveFrequency {
pub octave: u8,
pub frequency: u16
}
#[derive(Default, Debug)]
pub struct Channel {
pub id: u8,
pub active_instrument: Option<u16>,
pub active_note: Option<OctaveFrequency>,
pub current_volume: Option<u8>,
pub active_effects: LinkedHashSet<Effect>
}
#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
pub enum DcsgChannelMode { pub enum DcsgChannelMode {
Ch3Frequency, Ch3Frequency,
FixedFrequency FixedFrequency
} }
#[derive(Debug)] #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
pub enum NoiseType { pub enum NoiseType {
PeriodicNoise, PeriodicNoise,
WhiteNoise WhiteNoise
} }
#[derive(Debug)] #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)]
pub enum Effect { pub enum Effect {
Arpeggio { first_shift: u8, second_shift: u8 }, Arpeggio { first_shift: u8, second_shift: u8 },
PortamentoUp { speed: u8 }, PortamentoUp { speed: u8 },
@ -79,10 +96,24 @@ pub enum Effect {
pub struct PatternRow { pub struct PatternRow {
pub note: Option<Note>, pub note: Option<Note>,
pub volume: Option<u8>, pub volume: Option<u8>,
pub effects: Vec<Effect>, pub effects: LinkedHashSet<Effect>,
pub instrument_index: Option<u16> pub instrument_index: Option<u16>
} }
impl Channel {
pub fn new( id: u8 ) -> Channel {
Channel {
id,
active_instrument: None,
active_note: None,
current_volume: None,
active_effects: LinkedHashSet::new()
}
}
}
impl Note { impl Note {
pub fn get_octave( &self ) -> Result<u8, Box<dyn Error>> { pub fn get_octave( &self ) -> Result<u8, Box<dyn Error>> {