mirror of
https://github.com/G2-Games/minidisc-cli.git
synced 2025-04-20 04:02:53 -05:00
315 lines
8.5 KiB
Rust
315 lines
8.5 KiB
Rust
use crate::netmd::mappings::{ALLOWED_HW_KANA, MAPPINGS_DE, MAPPINGS_HW, MAPPINGS_JP, MAPPINGS_RU};
|
|
use byteorder::{LittleEndian, WriteBytesExt};
|
|
use diacritics;
|
|
use encoding_rs::SHIFT_JIS;
|
|
use regex::Regex;
|
|
use std::{error::Error, io::Write, time::Duration, vec::IntoIter};
|
|
use unicode_normalization::UnicodeNormalization;
|
|
|
|
extern crate kana;
|
|
use kana::*;
|
|
|
|
use super::{
|
|
interface::DiscFormat,
|
|
mappings::{HW_TO_FW_RANGE_MAP, MULTI_BYTE_CHARS},
|
|
};
|
|
|
|
/// Sleep for a specified [Duration] on any platform
|
|
pub async fn cross_sleep(duration: Duration) {
|
|
#[cfg(not(target_family = "wasm"))]
|
|
std::thread::sleep(duration);
|
|
|
|
#[cfg(target_family = "wasm")]
|
|
gloo::timers::future::TimeoutFuture::new(duration.as_millis() as u32).await;
|
|
}
|
|
|
|
pub fn bcd_to_int(mut bcd: i32) -> i32 {
|
|
let mut value = 0;
|
|
let mut nybble = 0;
|
|
|
|
while bcd != 0 {
|
|
let nybble_value = bcd & 0xf;
|
|
bcd >>= 4;
|
|
value += nybble_value * i32::pow(10, nybble);
|
|
nybble += 1;
|
|
}
|
|
|
|
value
|
|
}
|
|
|
|
pub fn int_to_bcd(mut value: i32) -> i32 {
|
|
let mut bcd = 0;
|
|
let mut shift = 0;
|
|
|
|
while value > 0 {
|
|
let digit = value % 10;
|
|
bcd |= digit << shift;
|
|
shift += 4;
|
|
value /= 10;
|
|
}
|
|
|
|
bcd
|
|
}
|
|
|
|
pub fn half_width_to_full_width_range(range: &str) -> String {
|
|
range
|
|
.chars()
|
|
.map(|char| HW_TO_FW_RANGE_MAP.get(&char).unwrap())
|
|
.collect()
|
|
}
|
|
|
|
pub fn get_bytes<const S: usize>(iterator: &mut IntoIter<u8>) -> Result<[u8; S], Box<dyn Error>> {
|
|
let byte_vec: Vec<u8> = iterator.take(S).collect();
|
|
let bytes: [u8; S] = byte_vec.try_into().unwrap();
|
|
|
|
Ok(bytes)
|
|
}
|
|
|
|
pub fn length_after_encoding_to_sjis(string: &str) -> usize {
|
|
let new_string = SHIFT_JIS.encode(string);
|
|
|
|
new_string.0.len()
|
|
}
|
|
|
|
pub fn validate_sjis(sjis_string: Vec<u8>) -> bool {
|
|
let (_, _, had_errors) = SHIFT_JIS.decode(&sjis_string);
|
|
|
|
had_errors
|
|
}
|
|
|
|
/// Ensure string contains only hardware allowed characters
|
|
fn check(string: String) -> Option<String> {
|
|
if MAPPINGS_HW.contains_key(string.as_str()) {
|
|
return Some(MAPPINGS_HW.get(string.as_str()).unwrap().to_string());
|
|
}
|
|
let mut ch = string.chars();
|
|
if (ch.next().unwrap() as u32) < 0x7f || ALLOWED_HW_KANA.contains(&string.as_str()) {
|
|
return Some(string);
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn half_width_title_length(title: &str) -> usize {
|
|
let multibyte_len = title
|
|
.chars()
|
|
.map(|c| (*MULTI_BYTE_CHARS.get(&c).unwrap_or(&0) as usize))
|
|
.reduce(|a, b| a + b)
|
|
.unwrap_or_default();
|
|
|
|
title.len() + multibyte_len
|
|
}
|
|
|
|
pub fn sanitize_half_width_title(title: &str) -> String {
|
|
let mut string_title = wide2ascii(title);
|
|
string_title = nowidespace(&string_title);
|
|
string_title = hira2kata(&string_title);
|
|
string_title = combine(&string_title);
|
|
|
|
let new_title: String = string_title
|
|
.chars()
|
|
.map(|c| {
|
|
check(c.to_string()).unwrap_or(
|
|
check(diacritics::remove_diacritics(&c.to_string())).unwrap_or(" ".to_string()),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
new_title
|
|
}
|
|
|
|
// TODO: This function is bad, probably should do the string sanitization in the frontend
|
|
pub fn sanitize_full_width_title(title: &str) -> String {
|
|
let new_title: String = title
|
|
.chars()
|
|
.map(|c| c.to_string())
|
|
.map(|character| {
|
|
match MAPPINGS_JP.get(character.to_string().as_str()) {
|
|
Some(string) => string,
|
|
None => character.as_str(),
|
|
}
|
|
.to_string()
|
|
})
|
|
.map(|character| {
|
|
match MAPPINGS_RU.get(character.as_str()) {
|
|
Some(string) => string,
|
|
None => character.as_str(),
|
|
}
|
|
.to_string()
|
|
})
|
|
.map(|character| {
|
|
match MAPPINGS_DE.get(character.as_str()) {
|
|
Some(string) => string,
|
|
None => character.as_str(),
|
|
}
|
|
.to_string()
|
|
})
|
|
.collect::<String>();
|
|
|
|
new_title
|
|
}
|
|
|
|
/// Convert a UTF-8 string to Shift-JIS for use on the player
|
|
pub fn to_sjis(sjis_str: &str) -> Vec<u8> {
|
|
let sjis_string = SHIFT_JIS.encode(sjis_str).0;
|
|
|
|
if validate_sjis(sjis_string.clone().into()) {
|
|
return agressive_sanitize_title(sjis_str).into();
|
|
}
|
|
|
|
sjis_string.into()
|
|
}
|
|
|
|
pub fn agressive_sanitize_title(title: &str) -> String {
|
|
let re = Regex::new(r"[^\x00-\x7F]").unwrap();
|
|
re.replace_all(
|
|
&diacritics::remove_diacritics(title)
|
|
.nfd()
|
|
.collect::<String>(),
|
|
"",
|
|
)
|
|
.into()
|
|
}
|
|
|
|
pub struct AeaOptions<'a> {
|
|
pub name: &'a str,
|
|
pub channels: u32,
|
|
pub sound_groups: u32,
|
|
pub group_start: u32,
|
|
pub encrypted: u32,
|
|
pub flags: &'a [u8],
|
|
}
|
|
|
|
impl<'a> Default for AeaOptions<'a> {
|
|
fn default() -> Self {
|
|
Self {
|
|
name: "",
|
|
channels: 2,
|
|
sound_groups: 1,
|
|
group_start: 0,
|
|
encrypted: 0,
|
|
flags: &[0, 0, 0, 0, 0, 0, 0, 0],
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn create_aea_header(options: AeaOptions) -> Vec<u8> {
|
|
let encoded_name = options.name.as_bytes();
|
|
|
|
let mut header: Vec<u8> = Vec::new();
|
|
|
|
header.write_u32::<LittleEndian>(2048).unwrap();
|
|
header.write_all(encoded_name).unwrap();
|
|
header
|
|
.write_all(&vec![0; 256 - encoded_name.len()])
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.sound_groups)
|
|
.unwrap();
|
|
header.write_all(&[options.channels as u8, 0]).unwrap();
|
|
|
|
// Write the flags
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[0] as u32)
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[1] as u32)
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[2] as u32)
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[3] as u32)
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[4] as u32)
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[5] as u32)
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[6] as u32)
|
|
.unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.flags[7] as u32)
|
|
.unwrap();
|
|
|
|
header.write_u32::<LittleEndian>(0).unwrap();
|
|
|
|
header.write_u32::<LittleEndian>(options.encrypted).unwrap();
|
|
header
|
|
.write_u32::<LittleEndian>(options.group_start)
|
|
.unwrap();
|
|
|
|
// return the header
|
|
header
|
|
}
|
|
|
|
pub fn create_wav_header(format: DiscFormat, bytes: u32) -> Vec<u8> {
|
|
let mut header: Vec<u8> = Vec::new();
|
|
|
|
let (joint_stereo, bytes_per_frame) = match format {
|
|
DiscFormat::LP4 => (192, 0),
|
|
DiscFormat::LP2 => (96, 1),
|
|
_ => unreachable!("Cannot create WAV header for disc type {:?}", format),
|
|
};
|
|
|
|
let bytes_per_second = (bytes_per_frame * 44100) / 512;
|
|
|
|
header.write_all(r"RIFF".as_bytes()).unwrap();
|
|
header.write_u32::<LittleEndian>(bytes + 60).unwrap();
|
|
header.write_all(r"WAVEfmt".as_bytes()).unwrap();
|
|
header.write_u32::<LittleEndian>(32).unwrap();
|
|
header.write_u16::<LittleEndian>(0x270).unwrap(); // ATRAC3
|
|
header.write_u16::<LittleEndian>(2).unwrap(); // Stereo
|
|
header.write_u32::<LittleEndian>(44100).unwrap();
|
|
header.write_u32::<LittleEndian>(bytes_per_second).unwrap();
|
|
header
|
|
.write_u16::<LittleEndian>(bytes_per_frame as u16 * 2)
|
|
.unwrap();
|
|
|
|
header.write_all(&[0, 0]).unwrap();
|
|
|
|
header.write_u16::<LittleEndian>(14).unwrap();
|
|
header.write_u16::<LittleEndian>(1).unwrap();
|
|
header.write_u32::<LittleEndian>(bytes_per_frame).unwrap();
|
|
header.write_u16::<LittleEndian>(joint_stereo).unwrap();
|
|
header.write_u16::<LittleEndian>(joint_stereo).unwrap();
|
|
|
|
header.write_u16::<LittleEndian>(1).unwrap();
|
|
header.write_u16::<LittleEndian>(0).unwrap();
|
|
|
|
header.write_all(r"data".as_bytes()).unwrap();
|
|
|
|
header.write_u32::<LittleEndian>(bytes).unwrap();
|
|
|
|
header
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct RawTime {
|
|
pub hours: u64,
|
|
pub minutes: u64,
|
|
pub seconds: u64,
|
|
pub frames: u64,
|
|
}
|
|
|
|
impl From<RawTime> for Duration {
|
|
fn from(val: RawTime) -> Self {
|
|
val.as_duration()
|
|
}
|
|
}
|
|
|
|
impl RawTime {
|
|
pub fn as_duration(&self) -> Duration {
|
|
std::time::Duration::from_micros(
|
|
(self.hours * 3600000000)
|
|
+ (self.minutes * 60000000)
|
|
+ (self.seconds * 1000000)
|
|
+ (self.frames * 11600),
|
|
)
|
|
}
|
|
|
|
pub fn as_frames(&self) -> u64 {
|
|
((self.hours * 60 + self.minutes) * 60 + self.seconds) * 512 + self.frames
|
|
}
|
|
}
|