Refactor into concept of 'actionable effects'

master
Ashley N. 2023-08-26 14:52:36 -04:00
parent e45972316e
commit d7735901e2
1 changed files with 120 additions and 80 deletions

View File

@ -166,66 +166,6 @@ fn get_delay( delay: u8 ) -> Result<EchoEvent, Box<dyn Error>> {
Ok( events ) 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. * 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. * While doing so, update the state of the channel for future event generation.
@ -442,36 +382,136 @@ pub fn compact_delays( events: Vec<EchoEvent> ) -> Result<Vec<EchoEvent>, Box<dy
Ok( new_events ) Ok( new_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 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![]
}
)
}
/**
* Generate a note cut effect for this channel and modify channel state to reflect it
*/
fn get_note_cut( channel: &mut Channel, note_cut_effect: &Effect, tick: u8 ) -> Result<EchoEvent, Box<dyn Error>> {
let after_ticks = match note_cut_effect {
Effect::NoteCut { after_ticks } => *after_ticks,
_ => return Err( "internal error: provided effect is not the note cut effect" )?
};
if tick >= after_ticks {
channel.active_note = None;
return Ok( vec![ ESF_NOTE_OFF | channel.id ] );
}
// Nothing to generate this tick
Ok( vec![] )
}
/**
* Get effects that require a specific action when computing the event flushes to Echo.
*/
fn get_actionable_effects( channels: &mut [Channel] ) -> Result<Vec<(usize, Effect)>, Box<dyn Error>> {
// 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.
let mut actionable_effects: Vec<(usize, Effect)> = Vec::new();
for channel_id in 0..channels.len() {
let channel = &channels[ channel_id ];
for effect in &channel.active_effects {
let is_actionable = match effect {
Effect::PortamentoUp { speed: _ } | Effect::PortamentoDown { speed: _ } => true,
Effect::NoteCut { after_ticks: _ } => true,
_ => false // effect is not an actionable effect
};
if is_actionable {
actionable_effects.push( ( channel_id, *effect ) );
}
}
}
Ok( actionable_effects )
}
/** /**
* Calculate delays generated before and after effects applied across all channels. If there are no effects for this row, * Calculate delays generated before and after effects applied across all channels. If there are no effects for this row,
* an empty vec will be returned. This means you should just apply ticks_to_wait as the delay for that row. If there * an empty vec will be returned. This means you should just apply ticks_to_wait as the delay for that row. If there
* -are- effects generated by this function, you should not apply ticks_to_wait, since this function will spend out * -are- effects generated by this function, you should not apply ticks_to_wait, since this function will spend out
* the tick budget for that row instead. * the tick budget for that row instead.
*/ */
fn get_delays_for_effects( channels: &mut [Channel], ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> { fn get_actionable_effect_sequence( channels: &mut [Channel], ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
let mut events: Vec<EchoEvent> = Vec::new(); let mut events: Vec<EchoEvent> = Vec::new();
// All portamento effects deploy per tick, not per row. So we need to aggregate all portamentos across all let actionable_effects = get_actionable_effects( channels )?;
// channels for this row, then flush them once per `ticks_to_wait` for this row. if !actionable_effects.is_empty() {
let mut active_portamentos: Vec<(usize, Effect)> = Vec::new(); for tick in 0..ticks_to_wait {
for channel_id in 0..channels.len() { for ( channel_id, actionable_effect ) in &actionable_effects {
let channel = &channels[ channel_id ]; let event_for_tick = match actionable_effect {
Effect::PortamentoUp { speed: _ } | Effect::PortamentoDown { speed: _ } => Some( get_portamento( &mut channels[ *channel_id ], &actionable_effect )? ),
Effect::NoteCut { after_ticks: _ } => Some( get_note_cut( &mut channels[ *channel_id ], &actionable_effect, tick )? ),
_ => None
};
for effect in &channel.active_effects { if let Some( event_for_tick ) = event_for_tick {
if matches!( effect, Effect::PortamentoDown { speed: _ } ) || if !event_for_tick.is_empty() {
matches!( effect, Effect::PortamentoUp { speed: _ } ) { events.push( event_for_tick );
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 )? ); events.push( get_delay( 1 )? );
} }
} }
@ -485,7 +525,7 @@ fn get_delays_for_effects( channels: &mut [Channel], ticks_to_wait: u8 ) -> Resu
fn get_delays( events: Vec<EchoEvent>, channels: &mut [Channel], ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> { fn get_delays( events: Vec<EchoEvent>, channels: &mut [Channel], ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
let mut events: Vec<EchoEvent> = events; let mut events: Vec<EchoEvent> = events;
let applied_effects = get_delays_for_effects( channels, ticks_to_wait )?; let applied_effects = get_actionable_effect_sequence( channels, ticks_to_wait )?;
if applied_effects.is_empty() { if applied_effects.is_empty() {
// Push the amount of ticks to wait for this row. ticks_to_wait is speed_a or speed_b, times base_speed. // 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 )? ); events.push( get_delay( ticks_to_wait )? );