Multi-file instrument combining and export (untested)
parent
62790d4c9c
commit
2d9fcb8e34
|
@ -15,4 +15,5 @@ samplerate = "0.2.4"
|
||||||
hound = "3.5.0"
|
hound = "3.5.0"
|
||||||
pitch_shift = "1.0.0"
|
pitch_shift = "1.0.0"
|
||||||
linked_hash_set = "0.1.4"
|
linked_hash_set = "0.1.4"
|
||||||
|
linked-hash-map = "0.5.6"
|
||||||
convert_case = "0.6.0"
|
convert_case = "0.6.0"
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{error::Error, fs::File, io::Write};
|
use std::{error::Error, fs::File, io::Write, path::Path};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::EchoFormat}, utility::print_good};
|
use crate::reskit::{tileset, soundtrack::{formats::dmf::DmfModule, engines::echo::engine::{EchoFormat, EchoArtifact}}, utility::print_good};
|
||||||
use super::settings::{Args, Tools, TileOutputFormat, TileOrder};
|
use super::settings::{Args, Tools, TileOutputFormat, TileOrder};
|
||||||
|
|
||||||
pub fn run_command() -> Result<(), Box<dyn Error>> {
|
pub fn run_command() -> Result<(), Box<dyn Error>> {
|
||||||
|
@ -20,23 +20,44 @@ pub fn run_command() -> Result<(), Box<dyn Error>> {
|
||||||
TileOrder::Sprite => "sprite"
|
TileOrder::Sprite => "sprite"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
Tools::Soundtrack { input_file, output_file, input_format: _, output_format: _, source_file_format: _, artifact_output_directory, soundtrack_label, instrument_list_label } => {
|
Tools::Soundtrack { input_file, output_directory, input_format: _, output_format: _, source_file_format: _, artifact_output_directory } => {
|
||||||
let result: Vec<DmfModule> = input_file.iter().map( | filename | Ok( DmfModule::from_file( &filename )? ) ).collect::<Result<Vec<DmfModule>, Box<dyn Error>>>()?;
|
let output_directory = if output_directory.ends_with( "/" ) { output_directory.to_string() } else { format!( "{}/", output_directory ) };
|
||||||
// very temporary!
|
let artifact_output_directory = if artifact_output_directory.ends_with( "/" ) { artifact_output_directory.to_string() } else { format!( "{}/", artifact_output_directory ) };
|
||||||
let result: &DmfModule = result.first().ok_or( "must provide input_file" )?;
|
|
||||||
|
|
||||||
for ( filename, data ) in result.get_artifacts( 0, &soundtrack_label, &output_file, &artifact_output_directory, &instrument_list_label )? {
|
let modules: Vec<DmfModule> = input_file.iter().map( | filename | Ok( DmfModule::from_file( &filename )? ) ).collect::<Result<Vec<DmfModule>, Box<dyn Error>>>()?;
|
||||||
println!( "Creating artifact file {}{}", artifact_output_directory, filename );
|
|
||||||
let mut file = File::create( format!( "{}{}", artifact_output_directory, filename ) )?;
|
let mut combined_asset_list = DmfModule::get_combined_assets_list( artifact_output_directory.clone(), &modules )?;
|
||||||
file.write_all( &data )?
|
if combined_asset_list.instruments.len() > 255 {
|
||||||
|
return Err( "combined instrument set exceeds 255 instruments; try converting fewer modules or modules with fewer instruments" )?;
|
||||||
}
|
}
|
||||||
|
if combined_asset_list.samples.len() > 255 {
|
||||||
|
return Err( "combined sample set exceeds 255 samples; try converting fewer modules or modules with fewer samples" )?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the ESF sequences
|
||||||
|
for i in 0..modules.len() {
|
||||||
|
let module = &modules[ i ];
|
||||||
|
|
||||||
|
let path = Path::new( &module.path );
|
||||||
|
let original_filename_stem = path.file_stem().ok_or( "internal error: unable to parse filename" )?.to_string_lossy();
|
||||||
|
let output_file = format!( "{}{}.esf", output_directory, original_filename_stem );
|
||||||
|
|
||||||
|
combined_asset_list.sequences.push( output_file.clone() );
|
||||||
|
|
||||||
println!( "Creating ESF output file {}", output_file );
|
println!( "Creating ESF output file {}", output_file );
|
||||||
let mut file = File::create( &output_file )?;
|
let mut file = File::create( &output_file )?;
|
||||||
file.write_all( &result.get_esf()? )?;
|
file.write_all( &module.get_esf( &combined_asset_list )? )?;
|
||||||
|
|
||||||
print_good( &format!( "successfully compiled soundtrack for {}", output_file ) );
|
print_good( &format!( "successfully compiled soundtrack for {}", output_file ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Output the shared artifacts used by all ESF sequences
|
||||||
|
println!( "Writing sequence artifacts..." );
|
||||||
|
let shared_artifacts = combined_asset_list.to_bytes()?;
|
||||||
|
println!( "Writing sequence artifact {}", format!( "{}music.asm", artifact_output_directory ) );
|
||||||
|
let mut file = File::create( format!( "{}music.asm", artifact_output_directory ) )?;
|
||||||
|
file.write_all( &shared_artifacts )?;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok( () )
|
Ok( () )
|
||||||
|
|
|
@ -64,13 +64,13 @@ pub enum Tools {
|
||||||
#[command(name = "soundtrack")]
|
#[command(name = "soundtrack")]
|
||||||
#[command(about = "Generate a console-compatible soundtrack from a sequence file.")]
|
#[command(about = "Generate a console-compatible soundtrack from a sequence file.")]
|
||||||
Soundtrack {
|
Soundtrack {
|
||||||
/// Input filename
|
/// Input filename(s)
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
input_file: Vec<String>,
|
input_file: Vec<String>,
|
||||||
|
|
||||||
/// Output filename (if multiple input_files are provided, used as output parent directory)
|
/// Output directory
|
||||||
#[arg(short, long)]
|
#[arg(short, long, default_value_t=String::from("./"))]
|
||||||
output_file: String,
|
output_directory: String,
|
||||||
|
|
||||||
/// Input sequence file format (the kind of tracker used to compose the track)
|
/// Input sequence file format (the kind of tracker used to compose the track)
|
||||||
#[arg(long, value_enum, default_value_t=SequenceFormat::Dmf)]
|
#[arg(long, value_enum, default_value_t=SequenceFormat::Dmf)]
|
||||||
|
@ -86,14 +86,6 @@ pub enum Tools {
|
||||||
|
|
||||||
/// Directory to output artifacts (instruments and samples)
|
/// Directory to output artifacts (instruments and samples)
|
||||||
#[arg(long, default_value_t=String::from( "./" ))]
|
#[arg(long, default_value_t=String::from( "./" ))]
|
||||||
artifact_output_directory: String,
|
artifact_output_directory: String
|
||||||
|
|
||||||
/// Identifier used for the soundtrack in the generated source file
|
|
||||||
#[arg(long, default_value_t=String::from( "MuzPlaceholder1" ))]
|
|
||||||
soundtrack_label: String,
|
|
||||||
|
|
||||||
/// Identifier used for the soundtrack's artifacts in the generated source file
|
|
||||||
#[arg(long, default_value_t=String::from( "MuzInstrumentList" ))]
|
|
||||||
instrument_list_label: String
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,14 @@
|
||||||
use std::{collections::HashMap, cmp::{max, min}, error::Error};
|
use std::{cmp::{max, min}, error::Error, fs::File, io::Write};
|
||||||
use convert_case::{Case, Casing};
|
|
||||||
use samplerate::convert;
|
use samplerate::convert;
|
||||||
use crate::reskit::{soundtrack::{formats::dmf::{DmfModule, ECHO_EWF_SAMPLE_RATE}, types::{InstrumentType, SampleFormat, Channel, PatternRow, Effect}}, utility::{print_warning, Ring}};
|
use crate::reskit::{soundtrack::{formats::dmf::{DmfModule, ECHO_EWF_SAMPLE_RATE}, types::{InstrumentType, SampleFormat, Channel, PatternRow, Effect, CombinedAssets, Instrument, Sample}}, utility::{print_warning, Ring, symbol_to_pascal}};
|
||||||
use super::engine::{EchoFormat, ESF_FM_1, ESF_FM_2, ESF_FM_3, ESF_FM_4, ESF_FM_5, ESF_FM_6, ESF_PSG_1, ESF_PSG_2, ESF_PSG_3, ESF_PSG_4, EchoEvent, get_events_for_row, compact_delays, ESF_SET_LOOP, ESF_GO_TO_LOOP, ESF_STOP};
|
use super::engine::{EchoFormat, ESF_FM_1, ESF_FM_2, ESF_FM_3, ESF_FM_4, ESF_FM_5, ESF_FM_6, ESF_PSG_1, ESF_PSG_2, ESF_PSG_3, ESF_PSG_4, EchoEvent, get_events_for_row, compact_delays, ESF_SET_LOOP, ESF_GO_TO_LOOP, ESF_STOP, EchoArtifact};
|
||||||
|
|
||||||
impl EchoFormat for DmfModule {
|
impl EchoArtifact for Instrument {
|
||||||
|
|
||||||
fn get_artifacts( &self, file_index: u32, soundtrack_label: &str, soundtrack_path: &str, artifact_path: &str, instr_list_label: &str ) -> Result<HashMap<String, Vec<u8>>, Box<dyn Error>> {
|
fn to_bytes( &self ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
let mut files: HashMap<String, Vec<u8>> = HashMap::new();
|
let mut data: Vec<u8> = Vec::new();
|
||||||
// This next list preserves order of filenames to generate the echo .asm file
|
|
||||||
let mut instrument_filenames: Vec<String> = Vec::new();
|
|
||||||
|
|
||||||
let mut index = 0;
|
match &self.instrument_type {
|
||||||
for instrument in &self.instruments {
|
|
||||||
match &instrument.instrument_type {
|
|
||||||
InstrumentType::Fm2612( settings ) => {
|
InstrumentType::Fm2612( settings ) => {
|
||||||
// Create feedback + algorithm byte
|
// Create feedback + algorithm byte
|
||||||
let alg_fb: u8 = ( settings.fb << 3 ) | settings.alg;
|
let alg_fb: u8 = ( settings.fb << 3 ) | settings.alg;
|
||||||
|
@ -51,26 +46,21 @@ impl EchoFormat for DmfModule {
|
||||||
rr_sl[ i ] = ( settings.operators[ i ].sl << 4 ) | settings.operators[ i ].rr;
|
rr_sl[ i ] = ( settings.operators[ i ].sl << 4 ) | settings.operators[ i ].rr;
|
||||||
|
|
||||||
if settings.operators[ i ].ssg_mode > 0 {
|
if settings.operators[ i ].ssg_mode > 0 {
|
||||||
print_warning( &format!( "SSG-EG mode set on instrument {}, operator {}. this operator may not work on clone hardware or certain emulators", instrument.name, i ) );
|
print_warning( &format!( "SSG-EG mode set on instrument {}, operator {}. this operator may not work on clone hardware or certain emulators", self.name, i ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
ssg_eg[ i ] = settings.operators[ i ].ssg_mode;
|
ssg_eg[ i ] = settings.operators[ i ].ssg_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut eif: Vec<u8> = Vec::new();
|
data.push( alg_fb );
|
||||||
eif.push( alg_fb );
|
data.extend( mult_dt );
|
||||||
eif.extend( mult_dt );
|
data.extend( tl );
|
||||||
eif.extend( tl );
|
data.extend( ar_rs );
|
||||||
eif.extend( ar_rs );
|
data.extend( dr_am );
|
||||||
eif.extend( dr_am );
|
data.extend( sr );
|
||||||
eif.extend( sr );
|
data.extend( rr_sl );
|
||||||
eif.extend( rr_sl );
|
data.extend( ssg_eg );
|
||||||
eif.extend( ssg_eg );
|
}
|
||||||
|
|
||||||
let filename = format!( "file{}_ins{}_{}.eif", file_index, index, instrument.name );
|
|
||||||
instrument_filenames.push( filename.clone() );
|
|
||||||
files.insert( filename, eif );
|
|
||||||
},
|
|
||||||
InstrumentType::PsgDcsg( settings ) => {
|
InstrumentType::PsgDcsg( settings ) => {
|
||||||
// Echo uses volume and arpeggio envelopes
|
// Echo uses volume and arpeggio envelopes
|
||||||
// Noise envelopes are usable but cannot be articulated in an instrument.
|
// Noise envelopes are usable but cannot be articulated in an instrument.
|
||||||
|
@ -199,22 +189,23 @@ impl EchoFormat for DmfModule {
|
||||||
// Echo end loop command
|
// Echo end loop command
|
||||||
envelope.push( 0xFF );
|
envelope.push( 0xFF );
|
||||||
|
|
||||||
let filename = format!( "file{}_ins{}_{}.eef", file_index, index, instrument.name );
|
data.extend( envelope.into_iter() );
|
||||||
instrument_filenames.push( filename.clone() );
|
|
||||||
files.insert( filename, envelope );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index += 1;
|
Ok( data )
|
||||||
}
|
}
|
||||||
|
|
||||||
index = 0;
|
}
|
||||||
for sample in &self.samples {
|
|
||||||
|
impl EchoArtifact for Sample {
|
||||||
|
|
||||||
|
fn to_bytes( &self ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
// Amplify data in original sample
|
// Amplify data in original sample
|
||||||
let data: Vec<i16> = sample.data
|
let data: Vec<i16> = self.data
|
||||||
.iter()
|
.iter()
|
||||||
.map( | pcm_sample | {
|
.map( | pcm_sample | {
|
||||||
let big_pcm_sample: i32 = min( ( *pcm_sample as i32 ) + sample.amp as i32, i16::MAX as i32 );
|
let big_pcm_sample: i32 = min( ( *pcm_sample as i32 ) + self.amp as i32, i16::MAX as i32 );
|
||||||
|
|
||||||
big_pcm_sample as i16
|
big_pcm_sample as i16
|
||||||
} )
|
} )
|
||||||
|
@ -224,7 +215,7 @@ impl EchoFormat for DmfModule {
|
||||||
let data: Vec<f32> = data
|
let data: Vec<f32> = data
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map( | pcm_sample | {
|
.map( | pcm_sample | {
|
||||||
if let SampleFormat::Bits8 = sample.bitrate {
|
if let SampleFormat::Bits8 = self.bitrate {
|
||||||
( pcm_sample as u8 ) as f32 / 255.0
|
( pcm_sample as u8 ) as f32 / 255.0
|
||||||
} else {
|
} else {
|
||||||
pcm_sample as f32 / 32767.0
|
pcm_sample as f32 / 32767.0
|
||||||
|
@ -233,13 +224,13 @@ impl EchoFormat for DmfModule {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// TODO: Adjust pitch using crate pitch_shift
|
// TODO: Adjust pitch using crate pitch_shift
|
||||||
if sample.pitch > 0 {
|
if self.pitch > 0 {
|
||||||
print_warning( "pitch shift not yet implemented for pcm samples. your drums/voice samples/etc may sound a bit funny" );
|
print_warning( "pitch shift not yet implemented for pcm samples. your drums/voice samples/etc may sound a bit funny" );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use libsamplerate to resample from whatever it is now to 10650Hz, the sample rate
|
// Use libsamplerate to resample from whatever it is now to 10650Hz, the sample rate
|
||||||
// required by Echo sound engine EWF samples.
|
// required by Echo sound engine EWF samples.
|
||||||
let data: Vec<f32> = convert( sample.rate, ECHO_EWF_SAMPLE_RATE, 1, samplerate::ConverterType::SincBestQuality, &data )?;
|
let data: Vec<f32> = convert( self.rate, ECHO_EWF_SAMPLE_RATE, 1, samplerate::ConverterType::SincBestQuality, &data )?;
|
||||||
|
|
||||||
// Convert from f32 back to i16, then downconvert to u8
|
// Convert from f32 back to i16, then downconvert to u8
|
||||||
let mut data: Vec<u8> = data
|
let mut data: Vec<u8> = data
|
||||||
|
@ -259,37 +250,110 @@ impl EchoFormat for DmfModule {
|
||||||
// Terminate stream
|
// Terminate stream
|
||||||
data.push( 0xFF );
|
data.push( 0xFF );
|
||||||
|
|
||||||
let filename = format!( "file{}_sample{}_{}.ewf", file_index, index, sample.name );
|
Ok( data )
|
||||||
instrument_filenames.push( filename.clone() );
|
|
||||||
files.insert( filename, data );
|
|
||||||
|
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EchoArtifact for CombinedAssets {
|
||||||
|
|
||||||
|
fn to_bytes( &self ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
// Write Echo ASM file that includes all the instruments
|
// Write Echo ASM file that includes all the instruments
|
||||||
let mut echo_asm: String = format!( "; Echo instrument definitions file\n; Generated by reskit v{}\n\n", env!( "CARGO_PKG_VERSION" ) );
|
let mut echo_asm: String = format!( "; Echo (esf) instruments and sequences source file\n; Generated by reskit v{}\n\n", env!( "CARGO_PKG_VERSION" ) );
|
||||||
|
let mut index = 0;
|
||||||
|
|
||||||
echo_asm += &format!( "{}:\n", soundtrack_label );
|
for soundtrack_path in &self.sequences {
|
||||||
|
echo_asm += &format!( "{}:\n", symbol_to_pascal( soundtrack_path ) );
|
||||||
echo_asm += &format!( "\tincbin '{}'\n\n", soundtrack_path );
|
echo_asm += &format!( "\tincbin '{}'\n\n", soundtrack_path );
|
||||||
|
}
|
||||||
|
|
||||||
echo_asm += &format!( "{}:\n", instr_list_label );
|
echo_asm += &format!( "Instr_List:\n" );
|
||||||
for filename in &instrument_filenames {
|
|
||||||
echo_asm += &format!( "\tEcho_ListEntry Instr_{}\n", filename.replace( ".", "_" ).replace( "-", "_" ).to_case( Case::Pascal ) );
|
let instrument_paths: Vec<String> = self.instruments.iter()
|
||||||
|
.map( | instrument |
|
||||||
|
format!(
|
||||||
|
"Instrument{}_{}.{}",
|
||||||
|
if instrument.name.is_empty() {
|
||||||
|
format!( "_NoName" )
|
||||||
|
} else {
|
||||||
|
format!( "_{}", &instrument.name )
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let given = index;
|
||||||
|
index += 1;
|
||||||
|
given
|
||||||
|
},
|
||||||
|
match &instrument.instrument_type {
|
||||||
|
InstrumentType::Fm2612(_) => "eif",
|
||||||
|
InstrumentType::PsgDcsg(_) => "eef"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let sample_paths: Vec<String> = self.samples.iter()
|
||||||
|
.map( | sample |
|
||||||
|
format!(
|
||||||
|
"Sample{}_{}.ewf",
|
||||||
|
if sample.name.is_empty() {
|
||||||
|
format!( "_NoName" )
|
||||||
|
} else {
|
||||||
|
format!( "_{}", &sample.name )
|
||||||
|
},
|
||||||
|
{
|
||||||
|
let given = index;
|
||||||
|
index += 1;
|
||||||
|
given
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for instrument_path in &instrument_paths {
|
||||||
|
echo_asm += &format!( "\tEcho_ListEntry Instr_{}\n", symbol_to_pascal( instrument_path ) );
|
||||||
|
}
|
||||||
|
for sample_path in &sample_paths {
|
||||||
|
echo_asm += &format!( "\tEcho_ListEntry Sample_{}\n", symbol_to_pascal( sample_path ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
echo_asm += &format!( "\tEcho_ListEnd\n\n" );
|
echo_asm += &format!( "\tEcho_ListEnd\n\n" );
|
||||||
|
|
||||||
for filename in &instrument_filenames {
|
for i in 0..self.instruments.len() {
|
||||||
echo_asm += &format!( "Instr_{}:\n", filename.replace( ".", "_" ).replace( "-", "_" ).to_case( Case::Pascal ) );
|
let instrument = &self.instruments[ i ];
|
||||||
echo_asm += &format!( "\tincbin '{}{}'\n\n", artifact_path, filename );
|
let instrument_path = &instrument_paths[ i ];
|
||||||
|
let instrument_total_path = format!( "{}{}", self.output_prefix, instrument_path );
|
||||||
|
|
||||||
|
echo_asm += &format!( "Instr_{}:\n", symbol_to_pascal( instrument_path ) );
|
||||||
|
echo_asm += &format!( "\tincbin '{}'\n\n", instrument_total_path );
|
||||||
|
|
||||||
|
println!( "Writing sequence artifact {}", instrument_total_path );
|
||||||
|
let instrument_data = instrument.to_bytes()?;
|
||||||
|
let mut file = File::create( instrument_total_path )?;
|
||||||
|
file.write_all( &instrument_data )?;
|
||||||
}
|
}
|
||||||
|
|
||||||
files.insert( format!( "music.asm" ), echo_asm.into_bytes() );
|
for i in 0..self.samples.len() {
|
||||||
|
let sample = &self.samples[ i ];
|
||||||
|
let sample_path = &sample_paths[ i ];
|
||||||
|
let sample_total_path = format!( "{}{}", self.output_prefix, sample_path );
|
||||||
|
|
||||||
Ok( files )
|
echo_asm += &format!( "Sample_{}:\n", symbol_to_pascal( sample_path ) );
|
||||||
|
echo_asm += &format!( "\tincbin '{}'\n\n", sample_total_path );
|
||||||
|
|
||||||
|
println!( "Writing sequence artifact {}", sample_total_path );
|
||||||
|
let sample_data = sample.to_bytes()?;
|
||||||
|
let mut file = File::create( sample_total_path )?;
|
||||||
|
file.write_all( &sample_data )?;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_esf( &self ) -> Result<Vec<u8>, Box<dyn Error>> {
|
Ok( echo_asm.into_bytes() )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EchoFormat for DmfModule {
|
||||||
|
|
||||||
|
fn get_esf( &self, combined_assets: &CombinedAssets ) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
let mut esf: Vec<u8> = Vec::new();
|
let mut esf: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
// Write deflemask rows one at a time.
|
// Write deflemask rows one at a time.
|
||||||
|
@ -327,13 +391,14 @@ impl EchoFormat for DmfModule {
|
||||||
let events_this_row: Vec<EchoEvent> = get_events_for_row(
|
let events_this_row: Vec<EchoEvent> = get_events_for_row(
|
||||||
&mut channels,
|
&mut channels,
|
||||||
&self.instruments,
|
&self.instruments,
|
||||||
|
&self.samples,
|
||||||
|
&combined_assets,
|
||||||
columns.clone(),
|
columns.clone(),
|
||||||
if row_number % 2 == 0 {
|
if row_number % 2 == 0 {
|
||||||
self.speed_a * self.time_base
|
self.speed_a * self.time_base
|
||||||
} else {
|
} else {
|
||||||
self.speed_b * self.time_base
|
self.speed_b * self.time_base
|
||||||
},
|
}
|
||||||
self.instruments.len() as u8
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Is this the pattern matrix row that we repeat to?
|
// Is this the pattern matrix row that we repeat to?
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{collections::{HashMap, HashSet}, error::Error, cmp::{min, max}};
|
use std::{collections::{HashMap, HashSet}, error::Error, cmp::{min, max}};
|
||||||
|
use linked_hash_map::LinkedHashMap;
|
||||||
use linked_hash_set::LinkedHashSet;
|
use linked_hash_set::LinkedHashSet;
|
||||||
use crate::reskit::{soundtrack::types::{PatternRow, Note, Channel, OctaveFrequency, Effect, Instrument, NoiseType, DcsgChannelMode, InstrumentType}, utility::print_warning};
|
use crate::reskit::{soundtrack::types::{PatternRow, Note, Channel, OctaveFrequency, Effect, Instrument, NoiseType, DcsgChannelMode, InstrumentType, CombinedAssets, Sample}, 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;
|
||||||
|
@ -59,9 +60,13 @@ const ESF_SEMITONE_A: u8 = 9;
|
||||||
const ESF_SEMITONE_A_SHARP: u8 = 10;
|
const ESF_SEMITONE_A_SHARP: u8 = 10;
|
||||||
const ESF_SEMITONE_B: u8 = 11;
|
const ESF_SEMITONE_B: u8 = 11;
|
||||||
|
|
||||||
struct InstrumentSet {
|
struct InstrumentSet<'a> {
|
||||||
fm_ids: HashSet<u16>,
|
fm_ids: HashSet<u16>,
|
||||||
psg_ids: HashSet<u16>,
|
psg_ids: HashSet<u16>,
|
||||||
|
module_set: &'a Vec<Instrument>,
|
||||||
|
total_set: &'a Vec<Instrument>,
|
||||||
|
module_samples: &'a Vec<Sample>,
|
||||||
|
total_samples: &'a Vec<Sample>,
|
||||||
default_psg_id: Option<u16>
|
default_psg_id: Option<u16>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,9 +74,13 @@ pub type EchoEvent = Vec<u8>;
|
||||||
|
|
||||||
pub trait EchoFormat {
|
pub trait EchoFormat {
|
||||||
|
|
||||||
fn get_artifacts( &self, file_index: u32, soundtrack_label: &str, soundtrack_path: &str, artifact_path: &str, instrument_list_label: &str ) -> Result<HashMap<String, Vec<u8>>, Box<dyn Error>>;
|
fn get_esf( &self, combined_assets: &CombinedAssets ) -> Result<Vec<u8>, Box<dyn Error>>;
|
||||||
|
|
||||||
fn get_esf( &self ) -> Result<Vec<u8>, Box<dyn Error>>;
|
}
|
||||||
|
|
||||||
|
pub trait EchoArtifact {
|
||||||
|
|
||||||
|
fn to_bytes( &self ) -> Result<Vec<u8>, Box<dyn Error>>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,7 +289,7 @@ fn get_delay( delay: u8 ) -> Result<EchoEvent, Box<dyn Error>> {
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: &PatternRow, pcm_offset: u8, instrument_set: &InstrumentSet ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
|
fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row: &PatternRow, asset_set: &InstrumentSet ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
|
||||||
let mut events: Vec<EchoEvent> = Vec::new();
|
let mut events: Vec<EchoEvent> = Vec::new();
|
||||||
|
|
||||||
// Adjust volume
|
// Adjust volume
|
||||||
|
@ -295,20 +304,25 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row:
|
||||||
if let Some( instrument ) = &row.instrument_index {
|
if let Some( instrument ) = &row.instrument_index {
|
||||||
// Validate the instrument is applicable to the current channel
|
// Validate the instrument is applicable to the current channel
|
||||||
let correct_type = match channels[ active_channel ].id {
|
let correct_type = match channels[ active_channel ].id {
|
||||||
ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => instrument_set.fm_ids.contains( instrument ),
|
ESF_FM_1..=ESF_FM_3 | ESF_FM_4..=ESF_FM_6 => asset_set.fm_ids.contains( instrument ),
|
||||||
ESF_FM_6_PCM => {
|
ESF_FM_6_PCM => {
|
||||||
print_warning( "attempted to set instrument on FM6 when it is in pcm mode. this is valid, but it won't do anything until the next time it is set back to fm mode." );
|
print_warning( "attempted to set instrument on FM6 when it is in pcm mode. this is valid, but it won't do anything until the next time it is set back to fm mode." );
|
||||||
instrument_set.fm_ids.contains( instrument )
|
asset_set.fm_ids.contains( instrument )
|
||||||
},
|
},
|
||||||
ESF_PSG_1..=ESF_PSG_4 => instrument_set.psg_ids.contains( instrument ),
|
ESF_PSG_1..=ESF_PSG_4 => asset_set.psg_ids.contains( instrument ),
|
||||||
invalid_channel => return Err( format!( "internal error: get_events_for_channel: invalid channel {:#04X}", invalid_channel ) )?
|
invalid_channel => return Err( format!( "internal error: get_events_for_channel: invalid channel {:#04X}", invalid_channel ) )?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Hop one layer of indirection from `instrument` by getting it from instrument_set.module_set,
|
||||||
|
// then finding the index of that instrument in instrument_set.total_set.
|
||||||
|
let original_instrument = &asset_set.module_set[ *instrument as usize ];
|
||||||
|
let instrument = asset_set.total_set.iter().position( | instrument | instrument == original_instrument ).ok_or( "internal error: could not locate instrument in combined instrument set" )?;
|
||||||
|
|
||||||
// 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 != &channels[ active_channel ].active_instrument {
|
if &channels[ active_channel ].active_instrument != &Some( instrument as u16 ) {
|
||||||
if correct_type {
|
if correct_type {
|
||||||
events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, *instrument as u8 ] );
|
events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
|
||||||
channels[ active_channel ].active_instrument = Some( *instrument );
|
channels[ active_channel ].active_instrument = Some( instrument as u16 );
|
||||||
} else {
|
} else {
|
||||||
print_warning( "attempted to set an fm instrument on a psg channel, or vice versa. the instrument was not set - your soundtrack may sound different than expected." );
|
print_warning( "attempted to set an fm instrument on a psg channel, or vice versa. the instrument was not set - your soundtrack may sound different than expected." );
|
||||||
// Now if current channel is PSG1 through PSG4, it's gonna need an instrument set on it.
|
// Now if current channel is PSG1 through PSG4, it's gonna need an instrument set on it.
|
||||||
|
@ -316,7 +330,7 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row:
|
||||||
print_warning( "this is a psg channel, seting the default psg instrument so your note can still play..." );
|
print_warning( "this is a psg channel, seting the default psg instrument so your note can still play..." );
|
||||||
if row.note.is_some() {
|
if row.note.is_some() {
|
||||||
// Set "__reskit_default_psg_instrument"
|
// Set "__reskit_default_psg_instrument"
|
||||||
let instrument = instrument_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?;
|
let instrument = asset_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?;
|
||||||
|
|
||||||
events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
|
events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
|
||||||
channels[ active_channel ].active_instrument = Some( instrument );
|
channels[ active_channel ].active_instrument = Some( instrument );
|
||||||
|
@ -330,7 +344,7 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row:
|
||||||
if channels[ active_channel ].active_instrument.is_none() && ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channels[ active_channel ].id ) {
|
if channels[ active_channel ].active_instrument.is_none() && ( ESF_PSG_1..=ESF_PSG_4 ).contains( &channels[ active_channel ].id ) {
|
||||||
if row.note.is_some() {
|
if row.note.is_some() {
|
||||||
// Set "__reskit_default_psg_instrument"
|
// Set "__reskit_default_psg_instrument"
|
||||||
let instrument = instrument_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?;
|
let instrument = asset_set.default_psg_id.ok_or( "internal error: no default psg instrument to apply" )?;
|
||||||
|
|
||||||
events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
|
events.push( vec![ ESF_SET_INSTRUMENT | channels[ active_channel ].id, instrument as u8 ] );
|
||||||
channels[ active_channel ].active_instrument = Some( instrument );
|
channels[ active_channel ].active_instrument = Some( instrument );
|
||||||
|
@ -368,7 +382,17 @@ fn get_events_for_channel( channels: &mut [Channel], active_channel: usize, row:
|
||||||
);
|
);
|
||||||
|
|
||||||
if channels[ active_channel ].id == ESF_FM_6_PCM {
|
if channels[ active_channel ].id == ESF_FM_6_PCM {
|
||||||
events.push( vec![ ESF_NOTE_ON | channels[ active_channel ].id, get_pcm_index( note )? + pcm_offset ] );
|
let module_pcm_index = get_pcm_index( note )?;
|
||||||
|
if asset_set.module_samples.get( module_pcm_index as usize ).is_none() {
|
||||||
|
print_warning( "FM6 is in PCM mode but you specified a note that is not linked to a sample. this has no effect, skipping..." );
|
||||||
|
} else {
|
||||||
|
let sample = &asset_set.module_samples[ module_pcm_index as usize ];
|
||||||
|
let global_sample_index = asset_set.total_samples.iter().position( | global_sample | global_sample == sample ).ok_or( "internal error: could not locate sample in combined sample set" )?;
|
||||||
|
|
||||||
|
// Remember, instruments will always be listed first in the output source file, followed by pointers to samples
|
||||||
|
// So the minimum sample offset is the number of instruments
|
||||||
|
events.push( vec![ ESF_NOTE_ON | channels[ active_channel ].id, global_sample_index as u8 + asset_set.total_set.len() as u8 ] );
|
||||||
|
}
|
||||||
} else if channels[ active_channel ].id == ESF_PSG_4 {
|
} else if channels[ active_channel ].id == ESF_PSG_4 {
|
||||||
// Find the single (and it should only ever be a single) Effect::DcsgNoiseMode attached to the channel
|
// Find the single (and it should only ever be a single) Effect::DcsgNoiseMode attached to the channel
|
||||||
let dcsg_noise_mode = channels[ active_channel ].active_effects.iter().find( | effect | matches!( effect, Effect::DcsgNoiseMode { mode: _, noise_type: _ } ) );
|
let dcsg_noise_mode = channels[ active_channel ].active_effects.iter().find( | effect | matches!( effect, Effect::DcsgNoiseMode { mode: _, noise_type: _ } ) );
|
||||||
|
@ -805,13 +829,13 @@ fn get_delays( events: Vec<EchoEvent>, channels: &mut [Channel], ticks_to_wait:
|
||||||
* For an entire row across all channels, generate the events that apply to the row as a whole. This usually means applying
|
* 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.
|
* 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], instruments: &Vec<Instrument>, subrows: Vec<&PatternRow>, ticks_to_wait: u8, pcm_offset: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
|
pub fn get_events_for_row( channels: &mut [Channel], module_instruments: &Vec<Instrument>, module_samples: &Vec<Sample>, asset_set: &CombinedAssets, subrows: Vec<&PatternRow>, ticks_to_wait: u8 ) -> Result<Vec<EchoEvent>, Box<dyn Error>> {
|
||||||
let mut events: Vec<EchoEvent> = Vec::new();
|
let mut events: Vec<EchoEvent> = Vec::new();
|
||||||
|
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
|
||||||
// Find the index of the default psg instrument, if it exists
|
// Find the index of the default psg instrument, if it exists
|
||||||
let default_psg_id: Option<u16> = instruments.iter().find_map( | item | {
|
let default_psg_id: Option<u16> = module_instruments.iter().find_map( | item | {
|
||||||
if item.name == "__reskit_default_psg_instrument" {
|
if item.name == "__reskit_default_psg_instrument" {
|
||||||
Some( index )
|
Some( index )
|
||||||
} else {
|
} else {
|
||||||
|
@ -822,7 +846,7 @@ pub fn get_events_for_row( channels: &mut [Channel], instruments: &Vec<Instrumen
|
||||||
|
|
||||||
// Generate InstrumentSet which is used to validate when instruments are set on channels
|
// Generate InstrumentSet which is used to validate when instruments are set on channels
|
||||||
index = 0;
|
index = 0;
|
||||||
let fm_ids = instruments.iter()
|
let fm_ids = module_instruments.iter()
|
||||||
.filter_map( | instrument | if matches!( instrument.instrument_type, InstrumentType::Fm2612( _ ) ) {
|
.filter_map( | instrument | if matches!( instrument.instrument_type, InstrumentType::Fm2612( _ ) ) {
|
||||||
let result = index;
|
let result = index;
|
||||||
index += 1;
|
index += 1;
|
||||||
|
@ -834,7 +858,7 @@ pub fn get_events_for_row( channels: &mut [Channel], instruments: &Vec<Instrumen
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
index = 0;
|
index = 0;
|
||||||
let psg_ids = instruments.iter()
|
let psg_ids = module_instruments.iter()
|
||||||
.filter_map( | instrument | if matches!( instrument.instrument_type, InstrumentType::PsgDcsg( _ ) ) {
|
.filter_map( | instrument | if matches!( instrument.instrument_type, InstrumentType::PsgDcsg( _ ) ) {
|
||||||
let result = index;
|
let result = index;
|
||||||
index += 1;
|
index += 1;
|
||||||
|
@ -848,6 +872,10 @@ pub fn get_events_for_row( channels: &mut [Channel], instruments: &Vec<Instrumen
|
||||||
let instrument_set = InstrumentSet {
|
let instrument_set = InstrumentSet {
|
||||||
fm_ids,
|
fm_ids,
|
||||||
psg_ids,
|
psg_ids,
|
||||||
|
module_set: module_instruments,
|
||||||
|
total_set: &asset_set.instruments,
|
||||||
|
module_samples,
|
||||||
|
total_samples: &asset_set.samples,
|
||||||
default_psg_id
|
default_psg_id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -857,7 +885,7 @@ pub fn get_events_for_row( channels: &mut [Channel], instruments: &Vec<Instrumen
|
||||||
events.extend( apply_effects_to_channel( &mut channels[ i ], &subrows[ i ].effects )? );
|
events.extend( apply_effects_to_channel( &mut channels[ i ], &subrows[ i ].effects )? );
|
||||||
|
|
||||||
// Get the ESF events for this channel's part of the row
|
// Get the ESF events for this channel's part of the row
|
||||||
events.extend( get_events_for_channel( channels, i, subrows[ i ], pcm_offset, &instrument_set )? );
|
events.extend( get_events_for_channel( channels, i, subrows[ i ], &instrument_set )? );
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok( get_delays( events, channels, ticks_to_wait )? )
|
Ok( get_delays( events, channels, ticks_to_wait )? )
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{error::Error, fs::File, io::Read, convert::TryInto, collections::HashMap};
|
use std::{error::Error, fs::File, io::Read, convert::TryInto, collections::HashMap};
|
||||||
use flate2::read::ZlibDecoder;
|
use flate2::read::ZlibDecoder;
|
||||||
use linked_hash_set::LinkedHashSet;
|
use linked_hash_set::LinkedHashSet;
|
||||||
use crate::reskit::{utility::{get_string, get_u8, skip, get_u32, get_i8, get_i32, get_u16, get_i16, print_info}, soundtrack::types::{SampleFormat, PsgEnvelope, Note, Sample, PsgSettings, PatternRow, Effect, DcsgChannelMode, NoiseType, Instrument, InstrumentType, Fm2612Operator, Fm2612Settings, Channel}};
|
use crate::reskit::{utility::{get_string, get_u8, skip, get_u32, get_i8, get_i32, get_u16, get_i16, print_info}, soundtrack::types::{SampleFormat, PsgEnvelope, Note, Sample, PsgSettings, PatternRow, Effect, DcsgChannelMode, NoiseType, Instrument, InstrumentType, Fm2612Operator, Fm2612Settings, Channel, CombinedAssets}};
|
||||||
|
|
||||||
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;
|
||||||
|
@ -28,7 +28,7 @@ pub enum FrameMode {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DmfModule {
|
pub struct DmfModule {
|
||||||
path: String,
|
pub path: String,
|
||||||
platform: u8,
|
platform: u8,
|
||||||
version: u8,
|
version: u8,
|
||||||
pub time_base: u8,
|
pub time_base: u8,
|
||||||
|
@ -45,6 +45,32 @@ pub struct DmfModule {
|
||||||
|
|
||||||
impl DmfModule {
|
impl DmfModule {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the combined instrument list across a list of Deflemask modules. When assigning instruments to tracks,
|
||||||
|
* there will be a two step process. First, get the index into the module's list of instruments. Then, find that
|
||||||
|
* instrument in the combined instrument list (by simple equality, even) and use _that_ index instead.
|
||||||
|
*/
|
||||||
|
pub fn get_combined_assets_list( output_prefix: String, modules: &Vec<DmfModule> ) -> Result<CombinedAssets, Box<dyn Error>> {
|
||||||
|
let mut instruments: Vec<Instrument> = Vec::new();
|
||||||
|
let mut samples: Vec<Sample> = Vec::new();
|
||||||
|
|
||||||
|
for module in modules {
|
||||||
|
for instrument in &module.instruments {
|
||||||
|
if !instruments.contains( instrument ) {
|
||||||
|
instruments.push( instrument.clone() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for sample in &module.samples {
|
||||||
|
if !samples.contains( sample ) {
|
||||||
|
samples.push( sample.clone() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok( CombinedAssets { output_prefix, sequences: Vec::new(), instruments, samples } )
|
||||||
|
}
|
||||||
|
|
||||||
pub fn from_file( path: &str ) -> Result<DmfModule, Box<dyn Error>> {
|
pub fn from_file( path: &str ) -> Result<DmfModule, Box<dyn Error>> {
|
||||||
let mut file = File::open( path )?;
|
let mut file = File::open( path )?;
|
||||||
let mut compressed: Vec<u8> = Vec::new();
|
let mut compressed: Vec<u8> = Vec::new();
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use std::{collections::HashMap, error::Error};
|
use std::{collections::HashMap, error::Error};
|
||||||
|
|
||||||
use linked_hash_set::LinkedHashSet;
|
use linked_hash_set::LinkedHashSet;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct PsgEnvelope< DataFormat: PartialEq > {
|
pub struct PsgEnvelope< DataFormat: PartialEq + Clone > {
|
||||||
pub envelope: Vec<DataFormat>,
|
pub envelope: Vec<DataFormat>,
|
||||||
pub loop_at: Option<usize>,
|
pub loop_at: Option<usize>,
|
||||||
pub settings: HashMap<&'static str, bool>
|
pub settings: HashMap<&'static str, bool>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct PsgSettings {
|
pub struct PsgSettings {
|
||||||
pub volume: PsgEnvelope< u32 >,
|
pub volume: PsgEnvelope< u32 >,
|
||||||
pub arpeggio: PsgEnvelope< i32 >,
|
pub arpeggio: PsgEnvelope< i32 >,
|
||||||
|
@ -17,7 +16,7 @@ pub struct PsgSettings {
|
||||||
pub wavetable: PsgEnvelope< u32 >
|
pub wavetable: PsgEnvelope< u32 >
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct Fm2612Operator {
|
pub struct Fm2612Operator {
|
||||||
pub am: u8,
|
pub am: u8,
|
||||||
pub ar: u8,
|
pub ar: u8,
|
||||||
|
@ -33,7 +32,7 @@ pub struct Fm2612Operator {
|
||||||
pub ssg_mode: u8
|
pub ssg_mode: u8
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Fm2612Settings {
|
pub struct Fm2612Settings {
|
||||||
pub alg: u8,
|
pub alg: u8,
|
||||||
pub fb: u8,
|
pub fb: u8,
|
||||||
|
@ -42,25 +41,32 @@ pub struct Fm2612Settings {
|
||||||
pub operators: [Fm2612Operator; 4]
|
pub operators: [Fm2612Operator; 4]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum InstrumentType {
|
pub enum InstrumentType {
|
||||||
Fm2612( Fm2612Settings ),
|
Fm2612( Fm2612Settings ),
|
||||||
PsgDcsg( PsgSettings )
|
PsgDcsg( PsgSettings )
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct Instrument {
|
pub struct Instrument {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub instrument_type: InstrumentType
|
pub instrument_type: InstrumentType
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct CombinedAssets {
|
||||||
|
pub output_prefix: String,
|
||||||
|
pub sequences: Vec<String>,
|
||||||
|
pub instruments: Vec<Instrument>,
|
||||||
|
pub samples: Vec<Sample>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum SampleFormat {
|
pub enum SampleFormat {
|
||||||
Bits8,
|
Bits8,
|
||||||
Bits16
|
Bits16
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct Sample {
|
pub struct Sample {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub rate: u32,
|
pub rate: u32,
|
||||||
|
@ -173,7 +179,7 @@ impl Note {
|
||||||
} )
|
} )
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_freq( &self ) -> Option<f32> {
|
pub fn _get_freq( &self ) -> Option<f32> {
|
||||||
match self {
|
match self {
|
||||||
Note::NoteOff => None,
|
Note::NoteOff => None,
|
||||||
Note::CSharp( octave ) => match octave {
|
Note::CSharp( octave ) => match octave {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use std::{slice::Iter, error::Error, str::from_utf8, convert::TryInto};
|
use std::{slice::Iter, error::Error, str::from_utf8, convert::TryInto};
|
||||||
|
|
||||||
|
use convert_case::{Casing, Case};
|
||||||
|
|
||||||
pub struct Ring< Type: Copy >{
|
pub struct Ring< Type: Copy >{
|
||||||
data: Vec< Type >,
|
data: Vec< Type >,
|
||||||
position: usize
|
position: usize
|
||||||
|
@ -37,6 +39,15 @@ impl< Type: Copy > Iterator for Ring< Type > {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn symbol_to_pascal( filename: &str ) -> String {
|
||||||
|
filename
|
||||||
|
.replace( "/", "_" )
|
||||||
|
.replace( "\\", "_" )
|
||||||
|
.replace( ".", "_" )
|
||||||
|
.replace( "-", "_" )
|
||||||
|
.to_case( Case::Pascal )
|
||||||
|
}
|
||||||
|
|
||||||
pub fn print_error( error_msg: &str ) {
|
pub fn print_error( error_msg: &str ) {
|
||||||
red!( "fatal: " ); println!( "{}", error_msg );
|
red!( "fatal: " ); println!( "{}", error_msg );
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue