From 4001f8d3cc1dc86c3538f0f6ef399b4f3bd9d07c Mon Sep 17 00:00:00 2001 From: ashley Date: Wed, 23 Aug 2023 23:51:41 -0400 Subject: [PATCH] Additional work generating esf files --- Cargo.toml | 3 +- src/main.rs | 8 +- src/reskit/soundtrack/engines/echo.rs | 268 +++++++++++++++++++++++++- src/reskit/soundtrack/formats/dmf.rs | 42 ++-- src/reskit/soundtrack/types.rs | 41 +++- 5 files changed, 329 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 637394e..5fad463 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ flate2 = "1.0.26" samplerate = "0.2.4" hound = "3.5.0" pitch_shift = "1.0.0" -uuid = { version = "1.4.1", features = [ "v4" ] } \ No newline at end of file +uuid = { version = "1.4.1", features = [ "v4" ] } +linked_hash_set = "0.1.4" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 7325b6e..ecfb5e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,8 @@ use reskit::soundtrack::formats::dmf::DmfModule; use reskit::utility; use reskit::tileset; +use crate::reskit::utility::print_good; + fn run() -> Result<(), Box> { let matches = App::new( "reskit" ) .version( "0.0.2a" ) @@ -100,7 +102,11 @@ fn run() -> Result<(), Box> { 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( () ) } else { diff --git a/src/reskit/soundtrack/engines/echo.rs b/src/reskit/soundtrack/engines/echo.rs index e3c07aa..b396307 100644 --- a/src/reskit/soundtrack/engines/echo.rs +++ b/src/reskit/soundtrack/engines/echo.rs @@ -1,5 +1,7 @@ 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 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; +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_SHARP: u8 = 1; const ESF_SEMITONE_D: u8 = 2; @@ -77,6 +82,32 @@ fn get_semitone( note: &Note ) -> Result> { } ) } +/** + 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> { + 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 { 32 * octave + 2 * semitone + 1 } @@ -89,7 +120,7 @@ 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" )? + 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> { 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" )? + 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, Box> { +/** + * Generate an echo delay event. "delay" is the amount to wait, in 1/60 of a second ticks. + */ +fn get_delay( delay: u8 ) -> Result> { + 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> { + 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, Box> { let mut events: Vec = Vec::new(); + // Key on or key off note if let Some( note ) = &row.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 { - 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 { // 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 { // 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 ); + if &row.instrument_index != &channel.active_instrument { + events.push( vec![ ESF_SET_INSTRUMENT | channel.id, *instrument as u8 ] ); + channel.active_instrument = Some( *instrument ); } } + 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 ) -> Result, Box> { + let mut events: Vec = 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, Box> { + let mut events: Vec = 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 ) } \ No newline at end of file diff --git a/src/reskit/soundtrack/formats/dmf.rs b/src/reskit/soundtrack/formats/dmf.rs index 35285d9..a38f736 100644 --- a/src/reskit/soundtrack/formats/dmf.rs +++ b/src/reskit/soundtrack/formats/dmf.rs @@ -1,8 +1,9 @@ use std::{error::Error, fs::File, io::Read, convert::TryInto, collections::HashMap, cmp::{min, max}}; use flate2::read::ZlibDecoder; +use linked_hash_set::LinkedHashSet; 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, 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_SUPPORTED_VERSION: u8 = 27; @@ -79,12 +80,6 @@ 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> { @@ -532,14 +527,14 @@ impl DmfModule { Some( volume as u8 ) }; - let mut effects: Vec = Vec::new(); + let mut effects: LinkedHashSet = LinkedHashSet::new(); for _ in 0..num_effects { let effect_code = get_i16( bytes.by_ref() )?; let effect_value = get_i16( bytes.by_ref() )?; let effect_value: Option = if effect_value == -1 { None } else { Some( effect_value as u8 ) }; if effect_code != -1 { - effects.push( + effects.insert( match effect_code as u8 { DMF_EFFECT_ARPEGGIO => Effect::Arpeggio { 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) // 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() + 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 delay: usize = 0; for row_number in 0..self.rows_per_pattern { - let mut events_this_row: Vec = Vec::new(); + 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 ] ); + } - for channel in 0..self.channel_patterns.len() { - // TODO !! + columns + }, + 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 ); } } diff --git a/src/reskit/soundtrack/types.rs b/src/reskit/soundtrack/types.rs index 7a832b5..4034e12 100644 --- a/src/reskit/soundtrack/types.rs +++ b/src/reskit/soundtrack/types.rs @@ -1,5 +1,7 @@ use std::{collections::HashMap, error::Error}; +use linked_hash_set::LinkedHashSet; + #[derive(Debug)] pub struct PsgEnvelope< DataFormat > { pub envelope: Vec, @@ -31,7 +33,7 @@ pub struct Sample { pub data: Vec } -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum Note { NoteOff, CSharp( u8 ), @@ -48,19 +50,34 @@ pub enum Note { 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, + pub active_note: Option, + pub current_volume: Option, + pub active_effects: LinkedHashSet +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] pub enum DcsgChannelMode { Ch3Frequency, FixedFrequency } -#[derive(Debug)] +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] pub enum NoiseType { PeriodicNoise, WhiteNoise } -#[derive(Debug)] +#[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] pub enum Effect { Arpeggio { first_shift: u8, second_shift: u8 }, PortamentoUp { speed: u8 }, @@ -79,10 +96,24 @@ pub enum Effect { pub struct PatternRow { pub note: Option, pub volume: Option, - pub effects: Vec, + pub effects: LinkedHashSet, pub instrument_index: Option } +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 { pub fn get_octave( &self ) -> Result> {