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 )
|
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 )? );
|
||||||
|
|
Loading…
Reference in New Issue