Additional work generating esf files
parent
d86efb29f2
commit
4001f8d3cc
|
@ -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"
|
|
@ -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 {
|
||||||
|
|
|
@ -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 )
|
||||||
|
}
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>> {
|
||||||
|
|
Loading…
Reference in New Issue