support for overriding pixtone samples

This commit is contained in:
Alula 2021-06-21 00:42:10 +02:00
parent 3fe8e132e5
commit 752ecac3ee
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
5 changed files with 237 additions and 97 deletions

View File

@ -7,6 +7,8 @@ use crate::case_insensitive_hashmap;
use crate::common::{BulletFlag, Color, Rect};
use crate::engine_constants::npcs::NPCConsts;
use crate::player::ControlMode;
use crate::sound::pixtone::{Channel, PixToneParameters, Waveform, Envelope};
use crate::sound::SoundManager;
use crate::str;
use crate::text_script::TextScriptEncoding;
@ -1395,7 +1397,7 @@ impl EngineConstants {
inventory_item_count_x: 6,
text_shadow: false,
text_speed_normal: 4,
text_speed_fast: 1
text_speed_fast: 1,
},
title: TitleConsts {
intro_text: "Studio Pixel presents".to_string(),
@ -1468,7 +1470,7 @@ impl EngineConstants {
}
}
pub fn apply_csplus_patches(&mut self) {
pub fn apply_csplus_patches(&mut self, sound_manager: &SoundManager) {
info!("Applying Cave Story+ constants patches...");
self.is_cs_plus = true;
@ -1482,6 +1484,33 @@ impl EngineConstants {
self.font_space_offset = 2.0;
self.soundtracks.insert("Remastered".to_string(), "/base/Ogg11/".to_string());
self.soundtracks.insert("New".to_string(), "/base/Ogg/".to_string());
let typewriter_sample = PixToneParameters {
// fx2 (CS+)
channels: [
Channel {
enabled: true,
length: 2000,
carrier: Waveform { waveform_type: 0, pitch: 92.000000, level: 32, offset: 0 },
frequency: Waveform { waveform_type: 0, pitch: 3.000000, level: 44, offset: 0 },
amplitude: Waveform { waveform_type: 0, pitch: 0.000000, level: 32, offset: 0 },
envelope: Envelope {
initial: 7,
time_a: 2,
value_a: 18,
time_b: 128,
value_b: 0,
time_c: 255,
value_c: 0,
},
},
Channel::disabled(),
Channel::disabled(),
Channel::disabled(),
],
};
sound_manager.set_sample_params(2, typewriter_sample);
}
pub fn apply_csplus_nx_patches(&mut self) {

View File

@ -130,16 +130,17 @@ pub struct SharedGameState {
impl SharedGameState {
pub fn new(ctx: &mut Context) -> GameResult<SharedGameState> {
let mut constants = EngineConstants::defaults();
let sound_manager = SoundManager::new(ctx)?;
let mut base_path = "/";
let settings = Settings::load(ctx)?;
if filesystem::exists(ctx, "/base/Nicalis.bmp") {
info!("Cave Story+ (PC) data files detected.");
constants.apply_csplus_patches();
constants.apply_csplus_patches(&sound_manager);
base_path = "/base/";
} else if filesystem::exists(ctx, "/base/lighting.tbl") {
info!("Cave Story+ (Switch) data files detected.");
constants.apply_csplus_patches();
constants.apply_csplus_patches(&sound_manager);
constants.apply_csplus_nx_patches();
base_path = "/base/";
} else if filesystem::exists(ctx, "/mrmap.bin") {
@ -157,6 +158,32 @@ impl SharedGameState {
texture_set.apply_seasonal_content(season, &settings);
}
for i in 0..0xffu8 {
let path = format!("{}/pxt/fx{:02x}.pxt", base_path, i);
if let Ok(file) = filesystem::open(ctx, path) {
sound_manager.set_sample_params_from_file(i, file)?;
continue;
}
let path = format!("/pxt/fx{:02x}.pxt", i);
if let Ok(file) = filesystem::open(ctx, path) {
sound_manager.set_sample_params_from_file(i, file)?;
continue;
}
let path = format!("{}/PixTone/{:03}.pxt", base_path, i);
if let Ok(file) = filesystem::open(ctx, path) {
sound_manager.set_sample_params_from_file(i, file)?;
continue;
}
let path = format!("/PixTone/{:03}.pxt", i);
if let Ok(file) = filesystem::open(ctx, path) {
sound_manager.set_sample_params_from_file(i, file)?;
continue;
}
}
println!("lookup path: {:#?}", texture_set.paths);
#[cfg(feature = "hooks")]
@ -195,7 +222,7 @@ impl SharedGameState {
texture_set,
#[cfg(feature = "scripting")]
lua: LuaScriptingState::new(),
sound_manager: SoundManager::new(ctx)?,
sound_manager,
settings,
shutdown: false,
})

View File

@ -1,17 +1,18 @@
use std::io;
use std::sync::mpsc;
use std::sync::mpsc::{Receiver, Sender};
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
use std::time::Duration;
use cpal::Sample;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::Sample;
#[cfg(feature = "ogg-playback")]
use lewton::inside_ogg::OggStreamReader;
use num_traits::clamp;
use crate::engine_constants::EngineConstants;
use crate::framework::context::Context;
use crate::framework::error::{GameError, GameResult};
use crate::framework::error::GameError::{AudioError, InvalidValue};
use crate::framework::error::{GameError, GameResult};
use crate::framework::filesystem;
use crate::framework::filesystem::File;
use crate::settings::Settings;
@ -19,15 +20,17 @@ use crate::settings::Settings;
use crate::sound::ogg_playback::{OggPlaybackEngine, SavedOggPlaybackState};
use crate::sound::org_playback::{OrgPlaybackEngine, SavedOrganyaPlaybackState};
use crate::sound::organya::Song;
use crate::sound::pixtone::PixTonePlayback;
use crate::sound::pixtone::{PixToneParameters, PixTonePlayback};
use crate::sound::wave_bank::SoundBank;
use crate::str;
use std::io::{BufReader, BufRead, Lines};
use std::str::FromStr;
#[cfg(feature = "ogg-playback")]
mod ogg_playback;
mod org_playback;
mod organya;
mod pixtone;
pub mod pixtone;
mod pixtone_sfx;
mod stuff;
mod wav;
@ -52,7 +55,8 @@ impl SoundManager {
let (tx, rx): (Sender<PlaybackMessage>, Receiver<PlaybackMessage>) = mpsc::channel();
let host = cpal::default_host();
let device = host.default_output_device().ok_or_else(|| AudioError(str!("Error initializing audio device.")))?;
let device =
host.default_output_device().ok_or_else(|| AudioError(str!("Error initializing audio device.")))?;
let config = device.default_output_config()?;
let bnk = wave_bank::SoundBank::load_from(filesystem::open(ctx, "/builtin/organya-wavetable-doukutsu.bin")?)?;
@ -70,11 +74,17 @@ impl SoundManager {
Ok(SoundManager { tx: tx.clone(), prev_song_id: 0, current_song_id: 0 })
}
pub fn play_sfx(&mut self, id: u8) {
pub fn play_sfx(&self, id: u8) {
let _ = self.tx.send(PlaybackMessage::PlaySample(id));
}
pub fn play_song(&mut self, song_id: usize, constants: &EngineConstants, settings: &Settings, ctx: &mut Context) -> GameResult {
pub fn play_song(
&mut self,
song_id: usize,
constants: &EngineConstants,
settings: &Settings,
ctx: &mut Context,
) -> GameResult {
if self.current_song_id == song_id {
return Ok(());
}
@ -98,16 +108,21 @@ impl SoundManager {
let songs_paths = paths.iter().map(|prefix| {
[
#[cfg(feature = "ogg-playback")]
(SongFormat::OggMultiPart, vec![format!("{}{}_intro.ogg", prefix, song_name), format!("{}{}_loop.ogg", prefix, song_name)]),
#[cfg(feature = "ogg-playback")]
#[cfg(feature = "ogg-playback")]
(
SongFormat::OggMultiPart,
vec![format!("{}{}_intro.ogg", prefix, song_name), format!("{}{}_loop.ogg", prefix, song_name)],
),
#[cfg(feature = "ogg-playback")]
(SongFormat::OggSinglePart, vec![format!("{}{}.ogg", prefix, song_name)]),
(SongFormat::Organya, vec![format!("{}{}.org", prefix, song_name)]),
]
});
for songs in songs_paths {
for (format, paths) in songs.iter().filter(|(_, paths)| paths.iter().all(|path| filesystem::exists(ctx, path))) {
for (format, paths) in
songs.iter().filter(|(_, paths)| paths.iter().all(|path| filesystem::exists(ctx, path)))
{
match format {
SongFormat::Organya => {
// we're sure that there's one element
@ -134,7 +149,9 @@ impl SoundManager {
// we're sure that there's one element
let path = unsafe { paths.get_unchecked(0) };
match filesystem::open(ctx, path).map(|f| OggStreamReader::new(f).map_err(|e| GameError::ResourceLoadError(e.to_string()))) {
match filesystem::open(ctx, path).map(|f| {
OggStreamReader::new(f).map_err(|e| GameError::ResourceLoadError(e.to_string()))
}) {
Ok(Ok(song)) => {
log::info!("Playing single part Ogg BGM: {} {}", song_id, path);
@ -157,16 +174,28 @@ impl SoundManager {
let path_loop = unsafe { paths.get_unchecked(1) };
match (
filesystem::open(ctx, path_intro).map(|f| OggStreamReader::new(f).map_err(|e| GameError::ResourceLoadError(e.to_string()))),
filesystem::open(ctx, path_loop).map(|f| OggStreamReader::new(f).map_err(|e| GameError::ResourceLoadError(e.to_string()))),
filesystem::open(ctx, path_intro).map(|f| {
OggStreamReader::new(f).map_err(|e| GameError::ResourceLoadError(e.to_string()))
}),
filesystem::open(ctx, path_loop).map(|f| {
OggStreamReader::new(f).map_err(|e| GameError::ResourceLoadError(e.to_string()))
}),
) {
(Ok(Ok(song_intro)), Ok(Ok(song_loop))) => {
log::info!("Playing multi part Ogg BGM: {} {} + {}", song_id, path_intro, path_loop);
log::info!(
"Playing multi part Ogg BGM: {} {} + {}",
song_id,
path_intro,
path_loop
);
self.prev_song_id = self.current_song_id;
self.current_song_id = song_id;
self.tx.send(PlaybackMessage::SaveState)?;
self.tx.send(PlaybackMessage::PlayOggSongMultiPart(Box::new(song_intro), Box::new(song_loop)))?;
self.tx.send(PlaybackMessage::PlayOggSongMultiPart(
Box::new(song_intro),
Box::new(song_loop),
))?;
return Ok(());
}
@ -197,7 +226,7 @@ impl SoundManager {
Ok(())
}
pub fn set_speed(&mut self, speed: f32) -> GameResult {
pub fn set_speed(&self, speed: f32) -> GameResult {
if speed <= 0.0 {
return Err(InvalidValue(str!("Speed must be bigger than 0.0!")));
}
@ -209,6 +238,72 @@ impl SoundManager {
pub fn current_song(&self) -> usize {
self.current_song_id
}
pub fn set_sample_params_from_file<R: io::Read>(&self, id: u8, data: R) -> GameResult {
let mut reader = BufReader::new(data).lines();
let mut params = PixToneParameters::empty();
fn next_string<T: FromStr, R: io::Read>(reader: &mut Lines<BufReader<R>>) -> GameResult<T> {
loop {
if let Some(Ok(str)) = reader.next() {
let str = str.trim();
if str == "" || str.starts_with("#") {
continue;
}
let mut splits = str.split(":");
let _ = splits.next();
if let Some(str) = splits.next() {
println!("{}", str);
return str.trim().parse::<T>().map_err(|_| GameError::ParseError("failed to parse the value as specified type.".to_string()))
} else {
break;
}
} else {
break;
}
}
return Err(GameError::ParseError("unexpected end.".to_string()))
};
for channel in params.channels.iter_mut() {
channel.enabled = next_string::<u8, R>(&mut reader)? != 0;
channel.length = next_string::<u32, R>(&mut reader)?;
channel.carrier.waveform_type = next_string::<u8, R>(&mut reader)?;
channel.carrier.pitch = next_string::<f32, R>(&mut reader)?;
channel.carrier.level = next_string::<i32, R>(&mut reader)?;
channel.carrier.offset = next_string::<i32, R>(&mut reader)?;
channel.frequency.waveform_type = next_string::<u8, R>(&mut reader)?;
channel.frequency.pitch = next_string::<f32, R>(&mut reader)?;
channel.frequency.level = next_string::<i32, R>(&mut reader)?;
channel.frequency.offset = next_string::<i32, R>(&mut reader)?;
channel.amplitude.waveform_type = next_string::<u8, R>(&mut reader)?;
channel.amplitude.pitch = next_string::<f32, R>(&mut reader)?;
channel.amplitude.level = next_string::<i32, R>(&mut reader)?;
channel.amplitude.offset = next_string::<i32, R>(&mut reader)?;
channel.envelope.initial = next_string::<i32, R>(&mut reader)?;
channel.envelope.time_a = next_string::<i32, R>(&mut reader)?;
channel.envelope.value_a = next_string::<i32, R>(&mut reader)?;
channel.envelope.time_b = next_string::<i32, R>(&mut reader)?;
channel.envelope.value_b = next_string::<i32, R>(&mut reader)?;
channel.envelope.time_c = next_string::<i32, R>(&mut reader)?;
channel.envelope.value_c = next_string::<i32, R>(&mut reader)?;
}
self.set_sample_params(id, params)
}
pub fn set_sample_params(&self, id: u8, params: PixToneParameters) -> GameResult {
self.tx.send(PlaybackMessage::SetSampleParams(id, params))?;
Ok(())
}
}
enum PlaybackMessage {
@ -222,6 +317,7 @@ enum PlaybackMessage {
SetSpeed(f32),
SaveState,
RestoreState,
SetSampleParams(u8, PixToneParameters),
}
#[derive(PartialEq, Eq)]
@ -239,7 +335,12 @@ enum PlaybackStateType {
Ogg(SavedOggPlaybackState),
}
fn run<T>(rx: Receiver<PlaybackMessage>, bank: SoundBank, device: &cpal::Device, config: &cpal::StreamConfig) -> GameResult
fn run<T>(
rx: Receiver<PlaybackMessage>,
bank: SoundBank,
device: &cpal::Device,
config: &cpal::StreamConfig,
) -> GameResult
where
T: cpal::Sample,
{
@ -257,10 +358,10 @@ where
log::info!("Audio format: {} {}", sample_rate, channels);
org_engine.set_sample_rate(sample_rate as usize);
#[cfg(feature = "ogg-playback")]
{
org_engine.loops = usize::MAX;
ogg_engine.set_sample_rate(sample_rate as usize);
}
{
org_engine.loops = usize::MAX;
ogg_engine.set_sample_rate(sample_rate as usize);
}
let buf_size = sample_rate as usize * 10 / 1000;
let mut bgm_buf = vec![0x8080; buf_size * 2];
@ -388,6 +489,9 @@ where
}
}
}
Ok(PlaybackMessage::SetSampleParams(id, params)) => {
pixtone.set_sample_parameters(id, params);
}
Err(_) => {
break;
}
@ -449,16 +553,25 @@ where
}
if frame.len() >= 2 {
let sample_l =
clamp((((bgm_sample_l ^ 0x8000) as i16) as isize) + (((pxt_sample ^ 0x8000) as i16) as isize), -0x7fff, 0x7fff) as u16 ^ 0x8000;
let sample_r =
clamp((((bgm_sample_r ^ 0x8000) as i16) as isize) + (((pxt_sample ^ 0x8000) as i16) as isize), -0x7fff, 0x7fff) as u16 ^ 0x8000;
let sample_l = clamp(
(((bgm_sample_l ^ 0x8000) as i16) as isize) + (((pxt_sample ^ 0x8000) as i16) as isize),
-0x7fff,
0x7fff,
) as u16
^ 0x8000;
let sample_r = clamp(
(((bgm_sample_r ^ 0x8000) as i16) as isize) + (((pxt_sample ^ 0x8000) as i16) as isize),
-0x7fff,
0x7fff,
) as u16
^ 0x8000;
frame[0] = Sample::from::<u16>(&sample_l);
frame[1] = Sample::from::<u16>(&sample_r);
} else {
let sample = clamp(
((((bgm_sample_l ^ 0x8000) as i16) + ((bgm_sample_r ^ 0x8000) as i16)) / 2) as isize + (((pxt_sample ^ 0x8000) as i16) as isize),
((((bgm_sample_l ^ 0x8000) as i16) + ((bgm_sample_r ^ 0x8000) as i16)) / 2) as isize
+ (((pxt_sample ^ 0x8000) as i16) as isize),
-0x7fff,
0x7fff,
) as u16

View File

@ -4,7 +4,7 @@ use lazy_static::lazy_static;
use num_traits::clamp;
use vec_mut_scan::VecMutScan;
use crate::sound::pixtone_sfx::PIXTONE_TABLE;
use crate::sound::pixtone_sfx::{DEFAULT_PIXTONE_TABLE};
use crate::sound::stuff::cubic_interp;
lazy_static! {
@ -31,17 +31,7 @@ lazy_static! {
};
}
/*#[test]
fn test_waveforms() {
let reference = include_bytes!("pixtone_ref.dat");
for n in 1..(WAVEFORMS.len()) {
for (i, &val) in WAVEFORMS[n].iter().enumerate() {
assert_eq!((val as u8, i, n), (reference[n as usize * 256 + i], i, n));
}
}
}*/
#[derive(Copy, Clone)]
pub struct Waveform {
pub waveform_type: u8,
pub pitch: f32,
@ -55,6 +45,7 @@ impl Waveform {
}
}
#[derive(Copy, Clone)]
pub struct Envelope {
pub initial: i32,
pub time_a: i32,
@ -104,6 +95,7 @@ impl Envelope {
}
}
#[derive(Copy, Clone)]
pub struct Channel {
pub enabled: bool,
pub length: u32,
@ -149,6 +141,7 @@ impl Channel {
}
}
#[derive(Copy, Clone)]
pub struct PixToneParameters {
pub channels: [Channel; 4],
}
@ -204,23 +197,39 @@ pub struct PlaybackState(u8, f32, u32);
pub struct PixTonePlayback {
pub samples: HashMap<u8, Vec<i16>>,
pub playback_state: Vec<PlaybackState>,
pub table: [PixToneParameters; 256],
}
#[allow(unused)]
impl PixTonePlayback {
pub fn new() -> PixTonePlayback {
let mut table = [PixToneParameters::empty(); 256];
for (i, params) in DEFAULT_PIXTONE_TABLE.iter().enumerate() {
table[i] = *params;
}
PixTonePlayback {
samples: HashMap::new(),
playback_state: vec![],
table,
}
}
pub fn create_samples(&mut self) {
for (i, params) in PIXTONE_TABLE.iter().enumerate() {
for (i, params) in self.table.iter().enumerate() {
self.samples.insert(i as u8, params.synth());
}
}
pub fn set_sample_parameters(&mut self, id: u8, params: PixToneParameters) {
self.table[id as usize] = params;
self.samples.insert(id, params.synth());
}
pub fn set_sample_data(&mut self, id: u8, data: Vec<i16>) {
self.samples.insert(id, data);
}
pub fn play_sfx(&mut self, id: u8) {
for state in self.playback_state.iter_mut() {
if state.0 == id && state.2 == 0 {

View File

@ -1,6 +1,6 @@
use crate::sound::pixtone::{Channel, Envelope, PixToneParameters, Waveform};
pub static PIXTONE_TABLE: [PixToneParameters; 160] = [
pub static DEFAULT_PIXTONE_TABLE: [PixToneParameters; 160] = [
PixToneParameters::empty(), // fx0
PixToneParameters { // fx1
channels: [
@ -40,21 +40,21 @@ pub static PIXTONE_TABLE: [PixToneParameters; 160] = [
Channel::disabled(),
],
},
PixToneParameters { // fx2 (CS+)
PixToneParameters { // fx2
channels: [
Channel {
enabled: true,
length: 2000,
length: 4000,
carrier: Waveform {
waveform_type: 0,
pitch: 92.000000,
waveform_type: 1,
pitch: 54.000000,
level: 32,
offset: 0,
},
frequency: Waveform {
waveform_type: 0,
pitch: 3.000000,
level: 44,
waveform_type: 5,
pitch: 0.100000,
level: 33,
offset: 0,
},
amplitude: Waveform {
@ -64,11 +64,11 @@ pub static PIXTONE_TABLE: [PixToneParameters; 160] = [
offset: 0,
},
envelope: Envelope {
initial: 7,
time_a: 2,
value_a: 18,
initial: 53,
time_a: 57,
value_a: 44,
time_b: 128,
value_b: 0,
value_b: 24,
time_c: 255,
value_c: 0,
},
@ -78,44 +78,6 @@ pub static PIXTONE_TABLE: [PixToneParameters; 160] = [
Channel::disabled(),
],
},
// PixToneParameters { // fx2
// channels: [
// Channel {
// enabled: true,
// length: 4000,
// carrier: Waveform {
// waveform_type: 1,
// pitch: 54.000000,
// level: 32,
// offset: 0,
// },
// frequency: Waveform {
// waveform_type: 5,
// pitch: 0.100000,
// level: 33,
// offset: 0,
// },
// amplitude: Waveform {
// waveform_type: 0,
// pitch: 0.000000,
// level: 32,
// offset: 0,
// },
// envelope: Envelope {
// initial: 53,
// time_a: 57,
// value_a: 44,
// time_b: 128,
// value_b: 24,
// time_c: 255,
// value_c: 0,
// },
// },
// Channel::disabled(),
// Channel::disabled(),
// Channel::disabled(),
// ],
// },
PixToneParameters { // fx3
channels: [
Channel {