Refactor into concept of 'actionable effects'
parent
e45972316e
commit
d7735901e2
|
@ -166,66 +166,6 @@ fn get_delay( delay: u8 ) -> Result<EchoEvent, Box<dyn Error>> {
|
|||
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.
|
||||
|
@ -442,34 +382,134 @@ pub fn compact_delays( events: Vec<EchoEvent> ) -> Result<Vec<EchoEvent>, Box<dy
|
|||
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,
|
||||
* 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
|
||||
* 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();
|
||||
|
||||
// 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 active_portamentos: Vec<(usize, Effect)> = Vec::new();
|
||||
for channel_id in 0..channels.len() {
|
||||
let channel = &channels[ channel_id ];
|
||||
let actionable_effects = get_actionable_effects( channels )?;
|
||||
if !actionable_effects.is_empty() {
|
||||
for tick in 0..ticks_to_wait {
|
||||
for ( channel_id, actionable_effect ) in &actionable_effects {
|
||||
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 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 );
|
||||
if let Some( event_for_tick ) = event_for_tick {
|
||||
if !event_for_tick.is_empty() {
|
||||
events.push( event_for_tick );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>> {
|
||||
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() {
|
||||
// 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 )? );
|
||||
|
|
Loading…
Reference in New Issue