Additional work generating esf files
parent
d86efb29f2
commit
4001f8d3cc
|
@ -15,3 +15,4 @@ samplerate = "0.2.4"
|
|||
hound = "3.5.0"
|
||||
pitch_shift = "1.0.0"
|
||||
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::tileset;
|
||||
|
||||
use crate::reskit::utility::print_good;
|
||||
|
||||
fn run() -> Result<(), Box<dyn Error>> {
|
||||
let matches = App::new( "reskit" )
|
||||
.version( "0.0.2a" )
|
||||
|
@ -100,7 +102,11 @@ fn run() -> Result<(), Box<dyn Error>> {
|
|||
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 {
|
||||
|
|
|
@ -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<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 {
|
||||
32 * octave + 2 * semitone + 1
|
||||
}
|
||||
|
@ -89,7 +120,7 @@ fn get_note( note: &Note, channel: u8 ) -> Result<u8, Box<dyn Error>> {
|
|||
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<u8, Box<dyn Error>> {
|
|||
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<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();
|
||||
|
||||
// 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<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 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<Sample>
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ChannelState {
|
||||
pub current_instrument: Option<u16>,
|
||||
pub current_volume: Option<u8>
|
||||
}
|
||||
|
||||
impl DmfModule {
|
||||
|
||||
pub fn from_file( path: &str ) -> Result<DmfModule, Box<dyn Error>> {
|
||||
|
@ -532,14 +527,14 @@ impl DmfModule {
|
|||
Some( volume as u8 )
|
||||
};
|
||||
|
||||
let mut effects: Vec<Effect> = Vec::new();
|
||||
let mut effects: LinkedHashSet<Effect> = 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<u8> = 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<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() {
|
||||
// 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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use std::{collections::HashMap, error::Error};
|
||||
|
||||
use linked_hash_set::LinkedHashSet;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PsgEnvelope< DataFormat > {
|
||||
pub envelope: Vec<DataFormat>,
|
||||
|
@ -31,7 +33,7 @@ pub struct Sample {
|
|||
pub data: Vec<i16>
|
||||
}
|
||||
|
||||
#[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<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 {
|
||||
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<Note>,
|
||||
pub volume: Option<u8>,
|
||||
pub effects: Vec<Effect>,
|
||||
pub effects: LinkedHashSet<Effect>,
|
||||
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 {
|
||||
|
||||
pub fn get_octave( &self ) -> Result<u8, Box<dyn Error>> {
|
||||
|
|
Loading…
Reference in New Issue