Fix inconsistent EOL

This commit is contained in:
2025-01-08 22:27:37 +01:00
parent 57671ac79d
commit 1f7141c517
184 changed files with 13686 additions and 13685 deletions

View File

@@ -1,34 +1,34 @@
BUILD_PROFILE ?= debug
CARGO_CMD ?= build
WINDOWS_TARGET ?= x86_64-pc-windows-gnu
UNIX_TARGET ?= x86_64-unknown-linux-gnu
WINDOWS_ARTIFACT = ./target/$(WINDOWS_TARGET)/$(BUILD_PROFILE)/$(ARTIFACT).exe
UNIX_ARTIFACT = ./target/$(UNIX_TARGET)/$(BUILD_PROFILE)/$(ARTIFACT)
define cp_artifact
$(if $(findstring build,$(CARGO_CMD)),
@mkdir -p $(dir $(1))
@cp $(1) $(2)
)
endef
define cargo_windows_default
$(if $(findstring upgrade,$(CARGO_CMD)),
cargo $(CARGO_CMD) --$(BUILD_PROFILE),
cargo $(CARGO_CMD) --$(BUILD_PROFILE) --target=$(WINDOWS_TARGET)
$(call cp_artifact,$(WINDOWS_ARTIFACT), ../../../bin/$(ARTIFACT).exe)
)
endef
define cargo_unix_default
$(if $(findstring upgrade,$(CARGO_CMD)),
cargo $(CARGO_CMD) --$(BUILD_PROFILE),
cargo $(CARGO_CMD) --$(BUILD_PROFILE) --target=$(UNIX_TARGET)
$(call cp_artifact,$(UNIX_ARTIFACT), ../../../bin/$(ARTIFACT))
)
endef
BUILD_PROFILE ?= debug
CARGO_CMD ?= build
WINDOWS_TARGET ?= x86_64-pc-windows-gnu
UNIX_TARGET ?= x86_64-unknown-linux-gnu
WINDOWS_ARTIFACT = ./target/$(WINDOWS_TARGET)/$(BUILD_PROFILE)/$(ARTIFACT).exe
UNIX_ARTIFACT = ./target/$(UNIX_TARGET)/$(BUILD_PROFILE)/$(ARTIFACT)
define cp_artifact
$(if $(findstring build,$(CARGO_CMD)),
@mkdir -p $(dir $(1))
@cp $(1) $(2)
)
endef
define cargo_windows_default
$(if $(findstring upgrade,$(CARGO_CMD)),
cargo $(CARGO_CMD) --$(BUILD_PROFILE),
cargo $(CARGO_CMD) --$(BUILD_PROFILE) --target=$(WINDOWS_TARGET)
$(call cp_artifact,$(WINDOWS_ARTIFACT), ../../../bin/$(ARTIFACT).exe)
)
endef
define cargo_unix_default
$(if $(findstring upgrade,$(CARGO_CMD)),
cargo $(CARGO_CMD) --$(BUILD_PROFILE),
cargo $(CARGO_CMD) --$(BUILD_PROFILE) --target=$(UNIX_TARGET)
$(call cp_artifact,$(UNIX_ARTIFACT), ../../../bin/$(ARTIFACT))
)
endef
# Windows build requires "rustup target add x86_64-pc-windows-gnu" and "sudo apt-get install mingw-w64"

View File

@@ -1,9 +1,9 @@
SUPPORT_PROJECTS = cdtypes tool_helper
PROJECTS = cpp_out psxfileconv mkoverlay psxcdgen_ex psxreadmap wslpath
.PHONY: $(PROJECTS)
$(foreach var,$(PROJECTS),$(var)):
$(MAKE) -C ./$@ $(MAKECMDGOALS)
all-windows: $(PROJECTS)
SUPPORT_PROJECTS = cdtypes tool_helper
PROJECTS = cpp_out psxfileconv mkoverlay psxcdgen_ex psxreadmap wslpath
.PHONY: $(PROJECTS)
$(foreach var,$(PROJECTS),$(var)):
$(MAKE) -C ./$@ $(MAKECMDGOALS)
all-windows: $(PROJECTS)
all: $(PROJECTS)

View File

@@ -1,20 +1,20 @@
<ISO_Project>
<Description>
<Publisher>Jaby Wuff</Publisher>
<!--<License>../Tests/ISO_Planschbecken.xml</License>-->
</Description>
<Track>
<File name="Miau.txt" padded_size="4096">../Tests/Test.mk</File>
<File name="Miau2.txt">../Tests/Test.mk</File>
<File name="SYSTEM.CNF">../Tests/Test.mk</File>
<File name="SCES_003.90">../Tests/Test.mk</File>
<Audiofile>../Tests/ISO_Planschbecken.xml</Audiofile>
<Directory name="Wuff">
<File name="Miau.txt">../Tests/ISO_Planschbecken.xml</File>
<Directory name="Sub" hidden="true">
<File name="SubM.txt">../Tests/ISO_Planschbecken.xml</File>
</Directory>
</Directory>
<Overlay name="Main.ovl" lba_source="../../../../JabyAdventure/application/src/MainState/LBAs.hpp">../../../../JabyAdventure/application/bin/PSX-release/Overlay.main_area</Overlay>
</Track>
<ISO_Project>
<Description>
<Publisher>Jaby Wuff</Publisher>
<!--<License>../Tests/ISO_Planschbecken.xml</License>-->
</Description>
<Track>
<File name="Miau.txt" padded_size="4096">../Tests/Test.mk</File>
<File name="Miau2.txt">../Tests/Test.mk</File>
<File name="SYSTEM.CNF">../Tests/Test.mk</File>
<File name="SCES_003.90">../Tests/Test.mk</File>
<Audiofile>../Tests/ISO_Planschbecken.xml</Audiofile>
<Directory name="Wuff">
<File name="Miau.txt">../Tests/ISO_Planschbecken.xml</File>
<Directory name="Sub" hidden="true">
<File name="SubM.txt">../Tests/ISO_Planschbecken.xml</File>
</Directory>
</Directory>
<Overlay name="Main.ovl" lba_source="../../../../JabyAdventure/application/src/MainState/LBAs.hpp">../../../../JabyAdventure/application/bin/PSX-release/Overlay.main_area</Overlay>
</Track>
</ISO_Project>

View File

@@ -1,58 +1,58 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"cargo_task": [
"./ all!All",
"cpp_out all!cpp_out",
"mkoverlay all!mkoverlay",
"psxcdgen_ex all!psxcdgen_ex",
"psxcdread all!psxcdread",
"psxfileconv all!psxfileconv",
"psxreadmap all!psxreadmap",
"wslpath all!wslpath",
]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "cargo",
"type": "shell",
"group": {
"kind": "build"
},
"command": "../../scripts/podman_jaby_engine.sh ../../:src/Tools make -C ${input:linux_cargo_task} CARGO_CMD=${input:cargo cmd} BUILD_PROFILE=${input:build cfg}",
"problemMatcher": []
}
],
"inputs": [
{
"id": "linux_cargo_task",
"type": "command",
"command": "shellCommand.execute",
"args": {
"command": "echo ${config:cargo_task} | tr , \\\\n",
"fieldSeparator": "!"
}
},
{
"id": "build cfg",
"type": "pickString",
"options": ["debug", "release", "compatible", "incompatible"],
"default": "release",
"description": "build configuration"
},
{
"id": "cargo cmd",
"type":"pickString",
"options": ["build", "check", "upgrade", "clean", "run", "tree"],
"default": "build",
"description": "cargo command to run"
}
]
}
{
"folders": [
{
"path": "."
}
],
"settings": {
"cargo_task": [
"./ all!All",
"cpp_out all!cpp_out",
"mkoverlay all!mkoverlay",
"psxcdgen_ex all!psxcdgen_ex",
"psxcdread all!psxcdread",
"psxfileconv all!psxfileconv",
"psxreadmap all!psxreadmap",
"wslpath all!wslpath",
]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "cargo",
"type": "shell",
"group": {
"kind": "build"
},
"command": "../../scripts/podman_jaby_engine.sh ../../:src/Tools make -C ${input:linux_cargo_task} CARGO_CMD=${input:cargo cmd} BUILD_PROFILE=${input:build cfg}",
"problemMatcher": []
}
],
"inputs": [
{
"id": "linux_cargo_task",
"type": "command",
"command": "shellCommand.execute",
"args": {
"command": "echo ${config:cargo_task} | tr , \\\\n",
"fieldSeparator": "!"
}
},
{
"id": "build cfg",
"type": "pickString",
"options": ["debug", "release", "compatible", "incompatible"],
"default": "release",
"description": "build configuration"
},
{
"id": "cargo cmd",
"type":"pickString",
"options": ["build", "check", "upgrade", "clean", "run", "tree"],
"default": "build",
"description": "cargo command to run"
}
]
}
}

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = cdtypes
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = cdtypes
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = cpp_out
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = cpp_out
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,52 +1,52 @@
use clap::{Parser};
use cpp_out::{Configuration, Error, FileType};
use std::path::PathBuf;
use tool_helper::exit_with_error;
#[derive(Parser)]
#[clap(about = "Output a file content or stdin to a c++ header/source file", long_about = None)]
struct CommandLine {
#[clap(value_parser)]
input_file: Option<PathBuf>,
#[clap(short='n', long="name")]
data_name: String,
#[clap(short='o')]
output_file: PathBuf,
}
fn configurate(cmd: &mut CommandLine) -> Result<Configuration, Error> {
let file_name = tool_helper::get_file_name_from_path_buf(&cmd.output_file, "output")?;
let extension = tool_helper::os_str_to_string(Error::ok_or_new(cmd.output_file.extension(), ||"File extension required for output".to_owned())?, "extension")?.to_ascii_lowercase();
let file_type = Error::ok_or_new({
match extension.as_ref() {
"h" => Some(FileType::CHeader),
"hpp" => Some(FileType::CPPHeader),
"c" => Some(FileType::CSource),
"cpp" => Some(FileType::CPPSource),
_ => None
}
}, ||format!("{} unkown file extension", extension))?;
Ok(Configuration{file_name, data_name: std::mem::take(&mut cmd.data_name), line_feed: cpp_out::LineFeed::Windows, file_type})
}
fn run_main(mut cmd: CommandLine) -> Result<(), Error> {
let cfg = configurate(&mut cmd)?;
let input = tool_helper::open_input(&cmd.input_file)?;
let output = tool_helper::open_output(&Some(cmd.output_file))?;
return cpp_out::convert(cfg, input, output);
}
fn main() {
match CommandLine::try_parse() {
Ok(cmd) => {
if let Err(error) = run_main(cmd) {
exit_with_error(error)
}
},
Err(error) => println!("{}", error)
}
use clap::{Parser};
use cpp_out::{Configuration, Error, FileType};
use std::path::PathBuf;
use tool_helper::exit_with_error;
#[derive(Parser)]
#[clap(about = "Output a file content or stdin to a c++ header/source file", long_about = None)]
struct CommandLine {
#[clap(value_parser)]
input_file: Option<PathBuf>,
#[clap(short='n', long="name")]
data_name: String,
#[clap(short='o')]
output_file: PathBuf,
}
fn configurate(cmd: &mut CommandLine) -> Result<Configuration, Error> {
let file_name = tool_helper::get_file_name_from_path_buf(&cmd.output_file, "output")?;
let extension = tool_helper::os_str_to_string(Error::ok_or_new(cmd.output_file.extension(), ||"File extension required for output".to_owned())?, "extension")?.to_ascii_lowercase();
let file_type = Error::ok_or_new({
match extension.as_ref() {
"h" => Some(FileType::CHeader),
"hpp" => Some(FileType::CPPHeader),
"c" => Some(FileType::CSource),
"cpp" => Some(FileType::CPPSource),
_ => None
}
}, ||format!("{} unkown file extension", extension))?;
Ok(Configuration{file_name, data_name: std::mem::take(&mut cmd.data_name), line_feed: cpp_out::LineFeed::Windows, file_type})
}
fn run_main(mut cmd: CommandLine) -> Result<(), Error> {
let cfg = configurate(&mut cmd)?;
let input = tool_helper::open_input(&cmd.input_file)?;
let output = tool_helper::open_output(&Some(cmd.output_file))?;
return cpp_out::convert(cfg, input, output);
}
fn main() {
match CommandLine::try_parse() {
Ok(cmd) => {
if let Err(error) = run_main(cmd) {
exit_with_error(error)
}
},
Err(error) => println!("{}", error)
}
}

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = mkoverlay
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = mkoverlay
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,51 +1,51 @@
use clap::Parser;
use mkoverlay::types::OverlayDesc;
use std::path::PathBuf;
use tool_helper::{Error, exit_with_error, format_if_error, open_input, open_output};
#[derive(Parser)]
#[clap(about = "Creates a linker script and makefile part for the JabyEngine toolchain", long_about = None)]
struct CommandLine {
#[clap(long="mk-file", help="Output path for the makefile")]
mk_file_output: Option<PathBuf>,
#[clap(long="ld-script", help="Output path for the linker script file")]
ld_file_output: Option<PathBuf>,
#[clap(value_parser, help="Input JSON for creating the files")]
input: Option<PathBuf>
}
fn parse_input(input: Option<PathBuf>) -> Result<OverlayDesc, Error> {
if let Some(input) = input {
let input = format_if_error!(open_input(&Some(input)), "Opening input file failed with: {error_text}")?;
format_if_error!(mkoverlay::types::json_reader::read_config(input), "Parsing JSON file failed with: {error_text}")
}
else {
Ok(OverlayDesc::new())
}
}
fn run_main(cmd_line: CommandLine) -> Result<(), Error> {
let input = parse_input(cmd_line.input)?;
let mut mk_output = format_if_error!(open_output(&cmd_line.mk_file_output), "Opening file for writing makefile failed with: {error_text}")?;
let mut ld_output = format_if_error!(open_output(&cmd_line.ld_file_output), "Opening file for writing linkerfile failed with: {error_text}")?;
format_if_error!(mkoverlay::creator::makefile::write(&mut mk_output, &input), "Writing makefile failed with: {error_text}")?;
format_if_error!(mkoverlay::creator::ldscript::write(&mut ld_output, &input), "Writing ld script failed with: {error_text}")
}
fn main() {
match CommandLine::try_parse() {
Ok(cmd_line) => {
match run_main(cmd_line) {
Ok(_) => (),
Err(error) => exit_with_error(error)
}
},
Err(error) => {
println!("{}", error)
}
}
use clap::Parser;
use mkoverlay::types::OverlayDesc;
use std::path::PathBuf;
use tool_helper::{Error, exit_with_error, format_if_error, open_input, open_output};
#[derive(Parser)]
#[clap(about = "Creates a linker script and makefile part for the JabyEngine toolchain", long_about = None)]
struct CommandLine {
#[clap(long="mk-file", help="Output path for the makefile")]
mk_file_output: Option<PathBuf>,
#[clap(long="ld-script", help="Output path for the linker script file")]
ld_file_output: Option<PathBuf>,
#[clap(value_parser, help="Input JSON for creating the files")]
input: Option<PathBuf>
}
fn parse_input(input: Option<PathBuf>) -> Result<OverlayDesc, Error> {
if let Some(input) = input {
let input = format_if_error!(open_input(&Some(input)), "Opening input file failed with: {error_text}")?;
format_if_error!(mkoverlay::types::json_reader::read_config(input), "Parsing JSON file failed with: {error_text}")
}
else {
Ok(OverlayDesc::new())
}
}
fn run_main(cmd_line: CommandLine) -> Result<(), Error> {
let input = parse_input(cmd_line.input)?;
let mut mk_output = format_if_error!(open_output(&cmd_line.mk_file_output), "Opening file for writing makefile failed with: {error_text}")?;
let mut ld_output = format_if_error!(open_output(&cmd_line.ld_file_output), "Opening file for writing linkerfile failed with: {error_text}")?;
format_if_error!(mkoverlay::creator::makefile::write(&mut mk_output, &input), "Writing makefile failed with: {error_text}")?;
format_if_error!(mkoverlay::creator::ldscript::write(&mut ld_output, &input), "Writing ld script failed with: {error_text}")
}
fn main() {
match CommandLine::try_parse() {
Ok(cmd_line) => {
match run_main(cmd_line) {
Ok(_) => (),
Err(error) => exit_with_error(error)
}
},
Err(error) => {
println!("{}", error)
}
}
}

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = psxcdgen_ex
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = psxcdgen_ex
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,88 +1,88 @@
use super::types::LicenseFile;
use super::Error;
use std::path::PathBuf;
mod xml;
pub enum LZ4State {
None,
AlreadyCompressed,
Compress,
}
pub struct CDAudioFile {
pub file_path: PathBuf,
pub align: bool,
}
impl CDAudioFile {
pub fn new(file_path: PathBuf, align: bool) -> CDAudioFile {
CDAudioFile{file_path, align}
}
}
pub struct Configuration {
pub publisher: Option<String>,
pub license_file: LicenseFile,
pub backup_license_file: LicenseFile,
pub root: Directory,
pub cd_audio_files: Vec<CDAudioFile>,
pub lead_out_sectors: Option<usize>,
}
impl Configuration {
pub fn new() -> Configuration {
Configuration{publisher: None, license_file: LicenseFile::None, backup_license_file: LicenseFile::None, root: Directory::new("root", false), cd_audio_files: Vec::new(), lead_out_sectors: None}
}
}
pub struct CommonProperties {
pub name: String,
pub is_hidden: bool,
pub lz4_state: LZ4State,
pub padded_size: Option<usize>,
}
pub enum FileKind {
Regular,
Main(PathBuf),
Overlay(PathBuf),
XAAudio(Vec<PathBuf>),
}
pub struct File {
pub common: CommonProperties,
pub path: PathBuf,
pub kind: FileKind
}
pub struct Directory {
pub name: String,
pub is_hidden: bool,
member: Vec<DirMember>,
}
impl Directory {
pub fn new(name: &str, is_hidden: bool) -> Directory {
Directory{name: String::from(name), is_hidden, member: Vec::new()}
}
pub fn add_file(&mut self, file: File) {
self.member.push(DirMember::File(file));
}
pub fn add_dir(&mut self, dir: Directory) {
self.member.push(DirMember::Directory(dir));
}
pub fn into_iter(self) -> std::vec::IntoIter<DirMember> {
self.member.into_iter()
}
}
pub enum DirMember {
File(File),
Directory(Directory),
}
pub fn parse_xml(xml: String) -> Result<Configuration, Error> {
Ok(xml::parse(xml)?)
use super::types::LicenseFile;
use super::Error;
use std::path::PathBuf;
mod xml;
pub enum LZ4State {
None,
AlreadyCompressed,
Compress,
}
pub struct CDAudioFile {
pub file_path: PathBuf,
pub align: bool,
}
impl CDAudioFile {
pub fn new(file_path: PathBuf, align: bool) -> CDAudioFile {
CDAudioFile{file_path, align}
}
}
pub struct Configuration {
pub publisher: Option<String>,
pub license_file: LicenseFile,
pub backup_license_file: LicenseFile,
pub root: Directory,
pub cd_audio_files: Vec<CDAudioFile>,
pub lead_out_sectors: Option<usize>,
}
impl Configuration {
pub fn new() -> Configuration {
Configuration{publisher: None, license_file: LicenseFile::None, backup_license_file: LicenseFile::None, root: Directory::new("root", false), cd_audio_files: Vec::new(), lead_out_sectors: None}
}
}
pub struct CommonProperties {
pub name: String,
pub is_hidden: bool,
pub lz4_state: LZ4State,
pub padded_size: Option<usize>,
}
pub enum FileKind {
Regular,
Main(PathBuf),
Overlay(PathBuf),
XAAudio(Vec<PathBuf>),
}
pub struct File {
pub common: CommonProperties,
pub path: PathBuf,
pub kind: FileKind
}
pub struct Directory {
pub name: String,
pub is_hidden: bool,
member: Vec<DirMember>,
}
impl Directory {
pub fn new(name: &str, is_hidden: bool) -> Directory {
Directory{name: String::from(name), is_hidden, member: Vec::new()}
}
pub fn add_file(&mut self, file: File) {
self.member.push(DirMember::File(file));
}
pub fn add_dir(&mut self, dir: Directory) {
self.member.push(DirMember::Directory(dir));
}
pub fn into_iter(self) -> std::vec::IntoIter<DirMember> {
self.member.into_iter()
}
}
pub enum DirMember {
File(File),
Directory(Directory),
}
pub fn parse_xml(xml: String) -> Result<Configuration, Error> {
Ok(xml::parse(xml)?)
}

View File

@@ -1,315 +1,315 @@
use cdtypes::types::time::Time;
use std::path::PathBuf;
use tool_helper::{format_if_error, path_with_env_from, print_warning, string_with_env_from};
use crate::{config_reader::Directory, types::LicenseFile};
use super::{CDAudioFile, CommonProperties, Configuration, Error, File, FileKind, LZ4State};
mod license_tag {
pub const NAME: &'static str = "License";
pub mod attribute {
pub const ALT_LOGO: &'static str = "alt-logo";
pub const ALT_TEXT: &'static str = "alt-text";
}
}
mod root_tag {
pub const NAME: &'static str = "PSXCD";
pub mod attribute {}
}
mod filesystem_tag {
pub const NAME: &'static str = "Filesystem";
pub mod attribute {
pub const LEAD_OUT:&'static str = "lead-out";
}
}
mod file_and_directory_attribute {
pub const NAME: &'static str = "name";
pub const HIDDEN: &'static str = "hidden";
pub const PADDED_SIZE: &'static str = "padded_size";
pub const LBA_SOURCE: &'static str = "lba_source";
pub const LZ4_STATE: &'static str = "lz4";
}
mod interleaved_file_tag {
pub const NAME: &'static str = "InterleavedFile";
pub mod attribute {}
}
mod cdda_tag {
pub const NAME: &'static str = "AudioTrack";
pub mod attribute {}
}
mod alt_license_tag {
pub const NAME: &'static str = "AltLicense";
pub mod attribute {
pub const TEXT: &'static str = "text";
}
}
pub fn parse(xml: String) -> Result<Configuration, Error> {
let mut config = Configuration::new();
let parser = format_if_error!(roxmltree::Document::parse(xml.as_str()), "Parsing XML file failed with: {error_text}")?;
let children = parser.root().children();
for node in children {
if node.is_element() && node.tag_name().name() == root_tag::NAME {
parse_root(node, &mut config)?;
}
}
Ok(config)
}
fn parse_root(root: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
let mut parsed_track = false;
for node in root.children() {
if node.is_element() {
match node.tag_name().name() {
"Description" => parse_description(node, config),
filesystem_tag::NAME => {
if !parsed_track {
parsed_track = true;
parse_track(node, config)
}
else {
Err(Error::from_text(format!("Found more than 1 filesystem. Multi filesystem discs are not supported")))
}
},
cdda_tag::NAME => parse_cd_audio(node, config),
_ => Ok(())
}?;
}
}
Ok(())
}
fn parse_description(description: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
fn parse_license_path(license: roxmltree::Node) -> (LicenseFile, LicenseFile) {
let required_lic_file = LicenseFile::from_authentic_option(path_from_node(&license, license_tag::NAME).ok());
let alt_logo = {
if let Some(path) = license.attribute(license_tag::attribute::ALT_LOGO) {
Some(path_with_env_from(path))
}
else {
None
}
};
let alt_text = license.attribute(license_tag::attribute::ALT_TEXT);
(required_lic_file, LicenseFile::from_tmd_option(alt_logo, alt_text))
}
fn parse_altlicense_path(license: roxmltree::Node) -> LicenseFile {
LicenseFile::from_tmd_option(path_from_node(&license, alt_license_tag::NAME).ok(), license.attribute(alt_license_tag::attribute::TEXT))
}
for node in description.descendants() {
if node.is_element() {
match node.tag_name().name() {
"Publisher" => config.publisher = Some(String::from(node.text().unwrap_or_default())),
license_tag::NAME => (config.license_file, config.backup_license_file) = parse_license_path(node),
alt_license_tag::NAME => {
if matches!(config.license_file, LicenseFile::None) {
config.license_file = parse_altlicense_path(node)
}
else {
print_warning("Alternate license ignored because a (alternate) license was already provided".to_owned())
}
},
_ => ()
}
}
}
Ok(())
}
fn parse_track(track: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
fn parse_regular_file(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
let common = read_common_properties(&file, is_hidden, None)?;
let path = path_from_node(&file, &common.name)?;
Ok(File{common, path, kind: FileKind::Regular})
}
fn parse_main_file(file: roxmltree::Node) -> Result<File, Error> {
if let Some(lba_path) = file.attribute(file_and_directory_attribute::LBA_SOURCE) {
let common = read_common_properties(&file, false, Some(LZ4State::None))?;
let path = path_from_node(&file, &common.name)?;
Ok(File{common, path, kind: FileKind::Main(PathBuf::from(lba_path))})
}
else {
tool_helper::print_warning(format!("The main file should always contain the \"{}\" attribute, even when just empty", file_and_directory_attribute::LBA_SOURCE));
parse_regular_file(file, false)
}
}
fn parse_overlay_file(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
// v They will be compressed automatically
let common = read_common_properties(&file, is_hidden, Some(LZ4State::AlreadyCompressed))?;
let path = path_from_node(&file, &common.name)?;
let lba_source = {
if let Some(lba_source) = file.attribute(file_and_directory_attribute::LBA_SOURCE) {
lba_source
}
else {
tool_helper::print_warning(format!("Overlays should always contain the \"{}\" attribute, even when just empty", file_and_directory_attribute::LBA_SOURCE));
""
}
};
Ok(File{common, path, kind: FileKind::Overlay(PathBuf::from(lba_source))})
}
fn parse_interleaved(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
// v Never compress interleaved files
let common = read_common_properties(&file, is_hidden, Some(LZ4State::None))?;
let channel = {
let mut channel = Vec::new();
for node in file.children() {
if node.is_element() && node.tag_name().name() == "Channel" {
channel.push(path_from_node(&node, "Channel")?);
}
}
channel
};
Ok(File{common, path: PathBuf::new(), kind: FileKind::XAAudio(channel)})
}
fn parse_file_system(cur_node: roxmltree::Node, root: &mut Directory, mut is_hidden: bool) -> Result<(), Error> {
for node in cur_node.children() {
if node.is_element() {
match node.tag_name().name() {
"File" => root.add_file(parse_regular_file(node, is_hidden)?),
"Main" => root.add_file(parse_main_file(node)?),
"Overlay" => root.add_file(parse_overlay_file(node, is_hidden)?),
interleaved_file_tag::NAME => root.add_file(parse_interleaved(node, is_hidden)?),
"Directory" => {
is_hidden |= parse_boolean_attribute(&node, file_and_directory_attribute::HIDDEN)?;
let mut new_dir = Directory::new(node.attribute(file_and_directory_attribute::NAME).unwrap_or_default(), is_hidden);
parse_file_system(node, &mut new_dir, is_hidden)?;
root.add_dir(new_dir);
},
_ => (),
}
}
}
Ok(())
}
fn get_lead_out_sectors(track: roxmltree::Node) -> Result<Option<usize>, Error> {
const MIN_OPTION_NAME:&'static str = "minutes";
const SECOND_OPTION_NAME:&'static str = "seconds";
const SECTOR_OPTION_NAME:&'static str = "sectors";
fn parse_split<'a>(who: &'a str, str_opt: Option<&'a str>) -> Result<u8, Error> {
match str_opt.unwrap_or("0").parse::<u8>() {
Ok(num) => Ok(num),
Err(error) => Err(Error::from_text(format!("Failed converting \"{}\" option for \"{}\" attribute \"{}\": {}", who, filesystem_tag::NAME, filesystem_tag::attribute::LEAD_OUT, error)))
}
}
//---------------------------------------------------------------------
if let Some(length_str) = track.attribute(filesystem_tag::attribute::LEAD_OUT) {
let mut pieces = length_str.split(':');
let (mins, seconds, sectors) = (
parse_split(MIN_OPTION_NAME, pieces.next())?,
parse_split(SECOND_OPTION_NAME, pieces.next())?,
parse_split(SECTOR_OPTION_NAME, pieces.next())?
);
Ok(Some(Time::from_mss(mins, seconds, sectors).as_sectors()))
}
else {
Ok(None)
}
}
config.lead_out_sectors = get_lead_out_sectors(track)?;
parse_file_system(track, &mut config.root, false)
}
fn parse_cd_audio(cdda: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
config.cd_audio_files.push(CDAudioFile::new(path_from_node(&cdda, "CD Audio file")?, parse_boolean_attribute(&cdda, "align")?));
Ok(())
}
fn read_common_properties(xml: &roxmltree::Node, is_hidden: bool, force_lz4_state: Option<LZ4State>) -> Result<CommonProperties, Error> {
let lz4_state = {
if let Some(forced_lz4_state) = force_lz4_state {
forced_lz4_state
}
else {
if let Some(state_str) = xml.attribute(file_and_directory_attribute::LZ4_STATE) {
match state_str.to_lowercase().as_str() {
"yes" => LZ4State::Compress,
"already" => LZ4State::AlreadyCompressed,
_ => LZ4State::None,
}
}
else {
LZ4State::None
}
}
};
Ok(CommonProperties{
name: string_with_env_from(xml.attribute(file_and_directory_attribute::NAME).unwrap_or_default()),
is_hidden: is_hidden | parse_boolean_attribute(&xml, file_and_directory_attribute::HIDDEN)?,
lz4_state,
padded_size: read_padded_size(&xml)?
})
}
fn read_padded_size(xml: &roxmltree::Node) -> Result<Option<usize>, Error> {
if let Some(padded_attr) = xml.attribute(file_and_directory_attribute::PADDED_SIZE) {
let padded_size = format_if_error!(padded_attr.parse::<usize>(), "Failed reading \"{}\" as padded size: {error_text}", padded_attr)?;
Ok(Some(padded_size))
}
else {
Ok(None)
}
}
fn parse_boolean_attribute(xml: &roxmltree::Node, attribute_name: &str) -> Result<bool, Error> {
if let Some(bool_str) = xml.attribute(attribute_name) {
match bool_str {
"true" | "1" => Ok(true),
"false" | "0" => Ok(false),
_ => {
return Err(Error::from_text(format!("Attribute \"{}\" expects either true,false or 1,0 as valid attributes - found {}", attribute_name, bool_str)));
}
}
}
else {
Ok(false)
}
}
fn path_from_node(node: &roxmltree::Node, name: &str) -> Result<PathBuf, Error> {
if let Some(path) = node.text() {
Ok(path_with_env_from(path.trim()))
}
else {
Err(Error::from_text(format!("No path specified for {}", name)))
}
use cdtypes::types::time::Time;
use std::path::PathBuf;
use tool_helper::{format_if_error, path_with_env_from, print_warning, string_with_env_from};
use crate::{config_reader::Directory, types::LicenseFile};
use super::{CDAudioFile, CommonProperties, Configuration, Error, File, FileKind, LZ4State};
mod license_tag {
pub const NAME: &'static str = "License";
pub mod attribute {
pub const ALT_LOGO: &'static str = "alt-logo";
pub const ALT_TEXT: &'static str = "alt-text";
}
}
mod root_tag {
pub const NAME: &'static str = "PSXCD";
pub mod attribute {}
}
mod filesystem_tag {
pub const NAME: &'static str = "Filesystem";
pub mod attribute {
pub const LEAD_OUT:&'static str = "lead-out";
}
}
mod file_and_directory_attribute {
pub const NAME: &'static str = "name";
pub const HIDDEN: &'static str = "hidden";
pub const PADDED_SIZE: &'static str = "padded_size";
pub const LBA_SOURCE: &'static str = "lba_source";
pub const LZ4_STATE: &'static str = "lz4";
}
mod interleaved_file_tag {
pub const NAME: &'static str = "InterleavedFile";
pub mod attribute {}
}
mod cdda_tag {
pub const NAME: &'static str = "AudioTrack";
pub mod attribute {}
}
mod alt_license_tag {
pub const NAME: &'static str = "AltLicense";
pub mod attribute {
pub const TEXT: &'static str = "text";
}
}
pub fn parse(xml: String) -> Result<Configuration, Error> {
let mut config = Configuration::new();
let parser = format_if_error!(roxmltree::Document::parse(xml.as_str()), "Parsing XML file failed with: {error_text}")?;
let children = parser.root().children();
for node in children {
if node.is_element() && node.tag_name().name() == root_tag::NAME {
parse_root(node, &mut config)?;
}
}
Ok(config)
}
fn parse_root(root: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
let mut parsed_track = false;
for node in root.children() {
if node.is_element() {
match node.tag_name().name() {
"Description" => parse_description(node, config),
filesystem_tag::NAME => {
if !parsed_track {
parsed_track = true;
parse_track(node, config)
}
else {
Err(Error::from_text(format!("Found more than 1 filesystem. Multi filesystem discs are not supported")))
}
},
cdda_tag::NAME => parse_cd_audio(node, config),
_ => Ok(())
}?;
}
}
Ok(())
}
fn parse_description(description: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
fn parse_license_path(license: roxmltree::Node) -> (LicenseFile, LicenseFile) {
let required_lic_file = LicenseFile::from_authentic_option(path_from_node(&license, license_tag::NAME).ok());
let alt_logo = {
if let Some(path) = license.attribute(license_tag::attribute::ALT_LOGO) {
Some(path_with_env_from(path))
}
else {
None
}
};
let alt_text = license.attribute(license_tag::attribute::ALT_TEXT);
(required_lic_file, LicenseFile::from_tmd_option(alt_logo, alt_text))
}
fn parse_altlicense_path(license: roxmltree::Node) -> LicenseFile {
LicenseFile::from_tmd_option(path_from_node(&license, alt_license_tag::NAME).ok(), license.attribute(alt_license_tag::attribute::TEXT))
}
for node in description.descendants() {
if node.is_element() {
match node.tag_name().name() {
"Publisher" => config.publisher = Some(String::from(node.text().unwrap_or_default())),
license_tag::NAME => (config.license_file, config.backup_license_file) = parse_license_path(node),
alt_license_tag::NAME => {
if matches!(config.license_file, LicenseFile::None) {
config.license_file = parse_altlicense_path(node)
}
else {
print_warning("Alternate license ignored because a (alternate) license was already provided".to_owned())
}
},
_ => ()
}
}
}
Ok(())
}
fn parse_track(track: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
fn parse_regular_file(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
let common = read_common_properties(&file, is_hidden, None)?;
let path = path_from_node(&file, &common.name)?;
Ok(File{common, path, kind: FileKind::Regular})
}
fn parse_main_file(file: roxmltree::Node) -> Result<File, Error> {
if let Some(lba_path) = file.attribute(file_and_directory_attribute::LBA_SOURCE) {
let common = read_common_properties(&file, false, Some(LZ4State::None))?;
let path = path_from_node(&file, &common.name)?;
Ok(File{common, path, kind: FileKind::Main(PathBuf::from(lba_path))})
}
else {
tool_helper::print_warning(format!("The main file should always contain the \"{}\" attribute, even when just empty", file_and_directory_attribute::LBA_SOURCE));
parse_regular_file(file, false)
}
}
fn parse_overlay_file(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
// v They will be compressed automatically
let common = read_common_properties(&file, is_hidden, Some(LZ4State::AlreadyCompressed))?;
let path = path_from_node(&file, &common.name)?;
let lba_source = {
if let Some(lba_source) = file.attribute(file_and_directory_attribute::LBA_SOURCE) {
lba_source
}
else {
tool_helper::print_warning(format!("Overlays should always contain the \"{}\" attribute, even when just empty", file_and_directory_attribute::LBA_SOURCE));
""
}
};
Ok(File{common, path, kind: FileKind::Overlay(PathBuf::from(lba_source))})
}
fn parse_interleaved(file: roxmltree::Node, is_hidden: bool) -> Result<File, Error> {
// v Never compress interleaved files
let common = read_common_properties(&file, is_hidden, Some(LZ4State::None))?;
let channel = {
let mut channel = Vec::new();
for node in file.children() {
if node.is_element() && node.tag_name().name() == "Channel" {
channel.push(path_from_node(&node, "Channel")?);
}
}
channel
};
Ok(File{common, path: PathBuf::new(), kind: FileKind::XAAudio(channel)})
}
fn parse_file_system(cur_node: roxmltree::Node, root: &mut Directory, mut is_hidden: bool) -> Result<(), Error> {
for node in cur_node.children() {
if node.is_element() {
match node.tag_name().name() {
"File" => root.add_file(parse_regular_file(node, is_hidden)?),
"Main" => root.add_file(parse_main_file(node)?),
"Overlay" => root.add_file(parse_overlay_file(node, is_hidden)?),
interleaved_file_tag::NAME => root.add_file(parse_interleaved(node, is_hidden)?),
"Directory" => {
is_hidden |= parse_boolean_attribute(&node, file_and_directory_attribute::HIDDEN)?;
let mut new_dir = Directory::new(node.attribute(file_and_directory_attribute::NAME).unwrap_or_default(), is_hidden);
parse_file_system(node, &mut new_dir, is_hidden)?;
root.add_dir(new_dir);
},
_ => (),
}
}
}
Ok(())
}
fn get_lead_out_sectors(track: roxmltree::Node) -> Result<Option<usize>, Error> {
const MIN_OPTION_NAME:&'static str = "minutes";
const SECOND_OPTION_NAME:&'static str = "seconds";
const SECTOR_OPTION_NAME:&'static str = "sectors";
fn parse_split<'a>(who: &'a str, str_opt: Option<&'a str>) -> Result<u8, Error> {
match str_opt.unwrap_or("0").parse::<u8>() {
Ok(num) => Ok(num),
Err(error) => Err(Error::from_text(format!("Failed converting \"{}\" option for \"{}\" attribute \"{}\": {}", who, filesystem_tag::NAME, filesystem_tag::attribute::LEAD_OUT, error)))
}
}
//---------------------------------------------------------------------
if let Some(length_str) = track.attribute(filesystem_tag::attribute::LEAD_OUT) {
let mut pieces = length_str.split(':');
let (mins, seconds, sectors) = (
parse_split(MIN_OPTION_NAME, pieces.next())?,
parse_split(SECOND_OPTION_NAME, pieces.next())?,
parse_split(SECTOR_OPTION_NAME, pieces.next())?
);
Ok(Some(Time::from_mss(mins, seconds, sectors).as_sectors()))
}
else {
Ok(None)
}
}
config.lead_out_sectors = get_lead_out_sectors(track)?;
parse_file_system(track, &mut config.root, false)
}
fn parse_cd_audio(cdda: roxmltree::Node, config: &mut Configuration) -> Result<(), Error> {
config.cd_audio_files.push(CDAudioFile::new(path_from_node(&cdda, "CD Audio file")?, parse_boolean_attribute(&cdda, "align")?));
Ok(())
}
fn read_common_properties(xml: &roxmltree::Node, is_hidden: bool, force_lz4_state: Option<LZ4State>) -> Result<CommonProperties, Error> {
let lz4_state = {
if let Some(forced_lz4_state) = force_lz4_state {
forced_lz4_state
}
else {
if let Some(state_str) = xml.attribute(file_and_directory_attribute::LZ4_STATE) {
match state_str.to_lowercase().as_str() {
"yes" => LZ4State::Compress,
"already" => LZ4State::AlreadyCompressed,
_ => LZ4State::None,
}
}
else {
LZ4State::None
}
}
};
Ok(CommonProperties{
name: string_with_env_from(xml.attribute(file_and_directory_attribute::NAME).unwrap_or_default()),
is_hidden: is_hidden | parse_boolean_attribute(&xml, file_and_directory_attribute::HIDDEN)?,
lz4_state,
padded_size: read_padded_size(&xml)?
})
}
fn read_padded_size(xml: &roxmltree::Node) -> Result<Option<usize>, Error> {
if let Some(padded_attr) = xml.attribute(file_and_directory_attribute::PADDED_SIZE) {
let padded_size = format_if_error!(padded_attr.parse::<usize>(), "Failed reading \"{}\" as padded size: {error_text}", padded_attr)?;
Ok(Some(padded_size))
}
else {
Ok(None)
}
}
fn parse_boolean_attribute(xml: &roxmltree::Node, attribute_name: &str) -> Result<bool, Error> {
if let Some(bool_str) = xml.attribute(attribute_name) {
match bool_str {
"true" | "1" => Ok(true),
"false" | "0" => Ok(false),
_ => {
return Err(Error::from_text(format!("Attribute \"{}\" expects either true,false or 1,0 as valid attributes - found {}", attribute_name, bool_str)));
}
}
}
else {
Ok(false)
}
}
fn path_from_node(node: &roxmltree::Node, name: &str) -> Result<PathBuf, Error> {
if let Some(path) = node.text() {
Ok(path_with_env_from(path.trim()))
}
else {
Err(Error::from_text(format!("No path specified for {}", name)))
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,62 +1,62 @@
use clap::Parser;
use psxcdgen_ex::{file_writer::{ImageType, write_image}, config_reader};
use std::path::PathBuf;
use tool_helper::{Error, exit_with_error, read_file_to_string};
#[derive(Parser)]
#[clap(about = "Creates an ISO image from a description file", long_about = None)]
struct CommandLine {
#[clap(value_enum, value_parser, help="Specifies the disc image type")]
output_type: ImageType,
#[clap(short='o', help="Output path; Some OUTPUT_TYPE might create additional files with different endings")]
output_file: PathBuf,
#[clap(long="list", id="Path to file", help="If set will list the CD content to stdout; With path will output list to file")]
list_content: Option<Option<PathBuf>>,
#[clap(value_parser, help="Input XML path for creating the image")]
input_file: PathBuf,
}
fn run_main(cmd_line: CommandLine) -> Result<(), Error> {
const PKG_MINIMUM_CONTENT_SIZE:usize = 512*1024;
let (mut desc, lba_embedded_files) = psxcdgen_ex::process(config_reader::parse_xml(read_file_to_string(&cmd_line.input_file)?)?)?;
let file_map = desc.create_file_map();
psxcdgen_ex::process_files(file_map, lba_embedded_files)?;
let content_size = desc.get_content_size();
if content_size < PKG_MINIMUM_CONTENT_SIZE {
let missing_size = PKG_MINIMUM_CONTENT_SIZE - content_size;
desc.end_dummy_padding = missing_size;
tool_helper::print_warning(format!("Content size {}b smaller then {}b.\nCD will be padded with {}b to work as a .pkg", content_size, PKG_MINIMUM_CONTENT_SIZE, missing_size));
}
if desc.lead_out_sectors == 0 {
tool_helper::print_warning(format!("Consider adding a 2 second lead-out to your track (Add attribute 'lead-out=\"0:2:0\"' to your Track) for supporting the PS3"));
}
write_image(&desc, cmd_line.output_type, cmd_line.output_file)?;
if let Some(list_content_option) = cmd_line.list_content {
psxcdgen_ex::dump_content(&desc, tool_helper::open_output(&list_content_option)?)
}
else {
Ok(())
}
}
fn main() {
match CommandLine::try_parse() {
Ok(cmd_line) => {
if let Err(error) = run_main(cmd_line) {
exit_with_error(error)
}
},
Err(error) => {
println!("{}", error)
}
}
use clap::Parser;
use psxcdgen_ex::{file_writer::{ImageType, write_image}, config_reader};
use std::path::PathBuf;
use tool_helper::{Error, exit_with_error, read_file_to_string};
#[derive(Parser)]
#[clap(about = "Creates an ISO image from a description file", long_about = None)]
struct CommandLine {
#[clap(value_enum, value_parser, help="Specifies the disc image type")]
output_type: ImageType,
#[clap(short='o', help="Output path; Some OUTPUT_TYPE might create additional files with different endings")]
output_file: PathBuf,
#[clap(long="list", id="Path to file", help="If set will list the CD content to stdout; With path will output list to file")]
list_content: Option<Option<PathBuf>>,
#[clap(value_parser, help="Input XML path for creating the image")]
input_file: PathBuf,
}
fn run_main(cmd_line: CommandLine) -> Result<(), Error> {
const PKG_MINIMUM_CONTENT_SIZE:usize = 512*1024;
let (mut desc, lba_embedded_files) = psxcdgen_ex::process(config_reader::parse_xml(read_file_to_string(&cmd_line.input_file)?)?)?;
let file_map = desc.create_file_map();
psxcdgen_ex::process_files(file_map, lba_embedded_files)?;
let content_size = desc.get_content_size();
if content_size < PKG_MINIMUM_CONTENT_SIZE {
let missing_size = PKG_MINIMUM_CONTENT_SIZE - content_size;
desc.end_dummy_padding = missing_size;
tool_helper::print_warning(format!("Content size {}b smaller then {}b.\nCD will be padded with {}b to work as a .pkg", content_size, PKG_MINIMUM_CONTENT_SIZE, missing_size));
}
if desc.lead_out_sectors == 0 {
tool_helper::print_warning(format!("Consider adding a 2 second lead-out to your track (Add attribute 'lead-out=\"0:2:0\"' to your Track) for supporting the PS3"));
}
write_image(&desc, cmd_line.output_type, cmd_line.output_file)?;
if let Some(list_content_option) = cmd_line.list_content {
psxcdgen_ex::dump_content(&desc, tool_helper::open_output(&list_content_option)?)
}
else {
Ok(())
}
}
fn main() {
match CommandLine::try_parse() {
Ok(cmd_line) => {
if let Err(error) = run_main(cmd_line) {
exit_with_error(error)
}
},
Err(error) => {
println!("{}", error)
}
}
}

View File

@@ -1,7 +1,7 @@
use crate::types::RawData;
pub fn skip_to_lba_area(content: &mut RawData) -> &mut [u8] {
const PSX_HEADER_SIZE:usize = 2048;
&mut content[PSX_HEADER_SIZE..]
use crate::types::RawData;
pub fn skip_to_lba_area(content: &mut RawData) -> &mut [u8] {
const PSX_HEADER_SIZE:usize = 2048;
&mut content[PSX_HEADER_SIZE..]
}

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = psxcdread
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = psxcdread
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = psxfileconv
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = psxfileconv
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,2 +1,2 @@
pub mod xa;
pub mod xa;
pub mod vag;

View File

@@ -1,169 +1,169 @@
pub mod types;
use clap::Args;
use std::{io::Write, str::FromStr};
use tool_helper::{Error, Input};
use types::{LPC, MonoADPCMIterator, VAGADPCM, VAGHeader};
#[derive(Args)]
pub struct Arguments {
#[clap(long, help="Specify the file name to be embedded in the header", default_value = "File name without extensions up to 16 characters")]
name: Option<String>,
#[clap(short='l', help="Set a loop at the specified time", value_name = "<min>:<sec>.<msec>[-<min>:<sec>.<msec>]", value_parser = clap::value_parser!(Loop))]
r#loop: Option<Loop>
}
#[derive(Clone)]
pub struct Loop {
start_time_sec: f64,
end_time_sec: Option<f64>,
}
impl Loop {
const END_DELIMITER: char = '-';
const MIN_DELIMITER: char = ':';
fn parse(str: Option<&str>) -> Result<Option<f64>, String> {
fn print_sec_conversion_error(str: &str, error: &dyn core::fmt::Display) -> String {
format!("Converting specified seconds \"{}\" failed with: {}", str, error)
}
if let Some(str) = str {
let time = {
if let Some(min_delim_idx) = str.find(Self::MIN_DELIMITER) {
let (min, sec) = str.split_at(min_delim_idx);
let min = min.parse::<u64>().map_err(|e| format!("Converting specified minutes \"{}\" failed with: {}", min, e))?;
let sec = sec.trim_start_matches(Self::MIN_DELIMITER).parse::<f64>().map_err(|e| print_sec_conversion_error(sec, &e))?;
(min*60) as f64 + sec
}
else {
str.parse::<f64>().map_err(|e| print_sec_conversion_error(str, &e))?
}
};
Ok(Some(time))
}
else {
Ok(None)
}
}
}
impl FromStr for Loop {
type Err = String;
fn from_str(arg: &str) -> Result<Self, Self::Err> {
let mut args = arg.split(Self::END_DELIMITER);
let start_time_sec = Self::parse(args.next()).map_err(|e| format!("{}", e))?.ok_or_else(|| format!("A start time is required for a loop"))?;
let end_time_sec = Self::parse(args.next()).map_err(|e| format!("{}", e))?;
Ok(Loop{start_time_sec, end_time_sec})
}
}
struct LoopInfo {
start_sample: usize,
end_sample: usize,
}
impl LoopInfo {
fn create(r#loop: &Loop, sample_frequency: u32, vagadpcm_count: usize) -> LoopInfo {
fn calculate_sample(time_sec: f64, vag_frequency: f64) -> usize {
(time_sec*vag_frequency) as usize
}
let sample_frequency = sample_frequency as f64 / VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM as f64;
let start_sample = calculate_sample(r#loop.start_time_sec, sample_frequency);
let end_sample = {
if let Some(end_time_sec) = r#loop.end_time_sec {
let end_sample = calculate_sample(end_time_sec, sample_frequency);
if end_sample > vagadpcm_count {vagadpcm_count} else {end_sample}
}
else {
vagadpcm_count
}
};
LoopInfo{start_sample, end_sample}
}
fn is_loop_start_reached(&self, sample: usize) -> bool {
self.start_sample == sample
}
fn is_loop_end_reached(&self, sample: usize) -> bool {
self.end_sample == sample
}
}
pub fn convert(args: Arguments, output_file_path: &Option<std::path::PathBuf>, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let mut wav_file = hound::WavReader::new(input)?;
let wav_header = wav_file.spec();
validate(&wav_header)?;
let vagadpcm_samples = VAGHeader::expected_vagadpcm_samples(wav_file.len()) + 1;
let sample_info = args.r#loop.map(|v| LoopInfo::create(&v, wav_header.sample_rate, vagadpcm_samples as usize));
let mut lpc = LPC::empty();
tool_helper::raw::write_raw(output, &VAGHeader::create(vagadpcm_samples, wav_header.sample_rate, &get_name_for_file_header(args.name, output_file_path))?)?;
for (sample_id, adpcm_sample) in MonoADPCMIterator::create(wav_file.samples::<i16>()).enumerate() {
let (mut vagadpcm, new_lpc) = VAGADPCM::create(adpcm_sample?, lpc);
if let Some(sample_info) = &sample_info {
if sample_info.is_loop_start_reached(sample_id) {
vagadpcm = vagadpcm.set_loop_start();
}
if sample_info.is_loop_end_reached(sample_id + 1) {
vagadpcm = vagadpcm.set_loop_end_repeat();
}
}
tool_helper::raw::write_raw(output, &vagadpcm)?;
lpc = new_lpc;
}
if sample_info.is_none() {
tool_helper::raw::write_raw(output, &VAGADPCM::end())?;
}
Ok(())
}
fn validate(wav_header: &hound::WavSpec) -> Result<(), Error> {
if wav_header.sample_format != hound::SampleFormat::Int {
return Err(Error::from_str("VAG: Only integer samples are supported as input."));
}
if wav_header.bits_per_sample != 16 {
return Err(Error::from_str("VAG: Only 16bits samples are currently supported as input."));
}
if wav_header.channels != 1 {
return Err(Error::from_str("VAG: Only mono samples are currently supported"));
}
Ok(())
}
fn get_name_for_file_header(name: Option<String>, output_file_path: &Option<std::path::PathBuf>) -> String {
if let Some(name) = name {
return name;
}
if let Some(output_file_path) = output_file_path {
if let Some(file_name) = output_file_path.file_name() {
let mut string = file_name.to_string_lossy().to_string();
if let Some(idx) = string.rfind('.') {
string.replace_range(idx.., "");
}
return string;
}
}
return "<out>".to_owned();
pub mod types;
use clap::Args;
use std::{io::Write, str::FromStr};
use tool_helper::{Error, Input};
use types::{LPC, MonoADPCMIterator, VAGADPCM, VAGHeader};
#[derive(Args)]
pub struct Arguments {
#[clap(long, help="Specify the file name to be embedded in the header", default_value = "File name without extensions up to 16 characters")]
name: Option<String>,
#[clap(short='l', help="Set a loop at the specified time", value_name = "<min>:<sec>.<msec>[-<min>:<sec>.<msec>]", value_parser = clap::value_parser!(Loop))]
r#loop: Option<Loop>
}
#[derive(Clone)]
pub struct Loop {
start_time_sec: f64,
end_time_sec: Option<f64>,
}
impl Loop {
const END_DELIMITER: char = '-';
const MIN_DELIMITER: char = ':';
fn parse(str: Option<&str>) -> Result<Option<f64>, String> {
fn print_sec_conversion_error(str: &str, error: &dyn core::fmt::Display) -> String {
format!("Converting specified seconds \"{}\" failed with: {}", str, error)
}
if let Some(str) = str {
let time = {
if let Some(min_delim_idx) = str.find(Self::MIN_DELIMITER) {
let (min, sec) = str.split_at(min_delim_idx);
let min = min.parse::<u64>().map_err(|e| format!("Converting specified minutes \"{}\" failed with: {}", min, e))?;
let sec = sec.trim_start_matches(Self::MIN_DELIMITER).parse::<f64>().map_err(|e| print_sec_conversion_error(sec, &e))?;
(min*60) as f64 + sec
}
else {
str.parse::<f64>().map_err(|e| print_sec_conversion_error(str, &e))?
}
};
Ok(Some(time))
}
else {
Ok(None)
}
}
}
impl FromStr for Loop {
type Err = String;
fn from_str(arg: &str) -> Result<Self, Self::Err> {
let mut args = arg.split(Self::END_DELIMITER);
let start_time_sec = Self::parse(args.next()).map_err(|e| format!("{}", e))?.ok_or_else(|| format!("A start time is required for a loop"))?;
let end_time_sec = Self::parse(args.next()).map_err(|e| format!("{}", e))?;
Ok(Loop{start_time_sec, end_time_sec})
}
}
struct LoopInfo {
start_sample: usize,
end_sample: usize,
}
impl LoopInfo {
fn create(r#loop: &Loop, sample_frequency: u32, vagadpcm_count: usize) -> LoopInfo {
fn calculate_sample(time_sec: f64, vag_frequency: f64) -> usize {
(time_sec*vag_frequency) as usize
}
let sample_frequency = sample_frequency as f64 / VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM as f64;
let start_sample = calculate_sample(r#loop.start_time_sec, sample_frequency);
let end_sample = {
if let Some(end_time_sec) = r#loop.end_time_sec {
let end_sample = calculate_sample(end_time_sec, sample_frequency);
if end_sample > vagadpcm_count {vagadpcm_count} else {end_sample}
}
else {
vagadpcm_count
}
};
LoopInfo{start_sample, end_sample}
}
fn is_loop_start_reached(&self, sample: usize) -> bool {
self.start_sample == sample
}
fn is_loop_end_reached(&self, sample: usize) -> bool {
self.end_sample == sample
}
}
pub fn convert(args: Arguments, output_file_path: &Option<std::path::PathBuf>, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let mut wav_file = hound::WavReader::new(input)?;
let wav_header = wav_file.spec();
validate(&wav_header)?;
let vagadpcm_samples = VAGHeader::expected_vagadpcm_samples(wav_file.len()) + 1;
let sample_info = args.r#loop.map(|v| LoopInfo::create(&v, wav_header.sample_rate, vagadpcm_samples as usize));
let mut lpc = LPC::empty();
tool_helper::raw::write_raw(output, &VAGHeader::create(vagadpcm_samples, wav_header.sample_rate, &get_name_for_file_header(args.name, output_file_path))?)?;
for (sample_id, adpcm_sample) in MonoADPCMIterator::create(wav_file.samples::<i16>()).enumerate() {
let (mut vagadpcm, new_lpc) = VAGADPCM::create(adpcm_sample?, lpc);
if let Some(sample_info) = &sample_info {
if sample_info.is_loop_start_reached(sample_id) {
vagadpcm = vagadpcm.set_loop_start();
}
if sample_info.is_loop_end_reached(sample_id + 1) {
vagadpcm = vagadpcm.set_loop_end_repeat();
}
}
tool_helper::raw::write_raw(output, &vagadpcm)?;
lpc = new_lpc;
}
if sample_info.is_none() {
tool_helper::raw::write_raw(output, &VAGADPCM::end())?;
}
Ok(())
}
fn validate(wav_header: &hound::WavSpec) -> Result<(), Error> {
if wav_header.sample_format != hound::SampleFormat::Int {
return Err(Error::from_str("VAG: Only integer samples are supported as input."));
}
if wav_header.bits_per_sample != 16 {
return Err(Error::from_str("VAG: Only 16bits samples are currently supported as input."));
}
if wav_header.channels != 1 {
return Err(Error::from_str("VAG: Only mono samples are currently supported"));
}
Ok(())
}
fn get_name_for_file_header(name: Option<String>, output_file_path: &Option<std::path::PathBuf>) -> String {
if let Some(name) = name {
return name;
}
if let Some(output_file_path) = output_file_path {
if let Some(file_name) = output_file_path.file_name() {
let mut string = file_name.to_string_lossy().to_string();
if let Some(idx) = string.rfind('.') {
string.replace_range(idx.., "");
}
return string;
}
}
return "<out>".to_owned();
}

View File

@@ -1,226 +1,226 @@
use bitflags::bitflags;
use tool_helper::{raw::RawConversion, Error};
#[repr(packed)]
#[derive(Clone)]
pub struct VAGHeader {
_id: [u8; 4],
version: u32,
_reserved: u32,
data_size: u32,
sampling_frequency: u32,
_reserved2: [u8; 12],
_name: [u8; 16]
}
impl VAGHeader {
const SIZE: usize = std::mem::size_of::<VAGHeader>();
const ID: [u8; 4] = ['V' as u8, 'A' as u8, 'G' as u8, 'p' as u8];
const VERSION: u32 = 0x02;
const RESERVED: u32 = 0;
const RESERVED2: [u8; 12] = [0; 12];
pub fn expected_vagadpcm_samples(adpcm_samples: u32) -> u32 {
((adpcm_samples as usize + (VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM - 1))/VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM) as u32
}
pub fn create(vagadpcm_samples: u32, sampling_frequency: u32, name: &str) -> Result<VAGHeader, Error> {
let data_size = vagadpcm_samples*VAGADPCM::SIZE as u32;
let name = {
if !name.is_ascii() {
return Err(Error::from_text(format!("File name {} is not ascii", name)));
}
let name_length = if name.len() > 16 {16} else {name.len()};
let mut new_name = [0u8; 16];
new_name[..name_length].copy_from_slice(&name.as_bytes()[..name_length]);
new_name
};
Ok(VAGHeader{
_id: VAGHeader::ID,
version: VAGHeader::VERSION,
_reserved: VAGHeader::RESERVED,
data_size: data_size,
sampling_frequency: sampling_frequency,
_reserved2: VAGHeader::RESERVED2,
_name: name
})
}
}
impl RawConversion<{VAGHeader::SIZE}> for VAGHeader {
fn convert_to_raw(&self) -> [u8; VAGHeader::SIZE] {
unsafe {
let mut vag_header = self.clone();
vag_header.version = self.version.to_be();
vag_header.data_size = self.data_size.to_be();
vag_header.sampling_frequency = self.sampling_frequency.to_be();
let data: [u8; VAGHeader::SIZE] = std::mem::transmute(vag_header);
data
}
}
}
bitflags! {
struct VAGFlagBits : u8 {
const LoopEnd = (1 << 0);
const Repeat = (1 << 1);
const LoopStart = (1 << 2);
}
}
pub struct VAGADPCM {
data: [u32; 4]
}
impl VAGADPCM {
const SIZE: usize = std::mem::size_of::<VAGADPCM>();
pub(super) const ADPCM_SAMPLES_PER_VAGADPCM:usize = 28;
pub fn empty() -> VAGADPCM {
VAGADPCM{data: [0; 4]}
}
fn create_for_filter_shift(filter: u32, shift: u32) -> VAGADPCM {
VAGADPCM{data: [(12 - shift) | filter << 4, 0, 0, 0]}
}
pub fn end() -> VAGADPCM {
VAGADPCM::empty().set_loop_self()
}
pub fn create(samples: ADPCMSampleForVag, lpc_tap: LPC) -> (VAGADPCM, LPC) {
fn cap_value(value: i32, min: i32, max: i32) -> i32 {
if value < min {
min
}
else if value > max {
max
}
else {
value
}
}
let mut best_frame = VAGADPCM::empty();
let mut best_tap = LPC::empty();
let mut best_error = std::u64::MAX as u64;
for (filter_id, filter) in LPC::FILTERS.iter().enumerate() {
for shift in 0..=12 {
let mut this_frame = VAGADPCM::create_for_filter_shift(filter_id as u32, shift);
let this_tap = lpc_tap.clone();
let mut this_error = 0;
for (sample_id, sample) in samples.iter().enumerate() {
let x = *sample as i32;
let p = (this_tap.first*filter.first + this_tap.second*filter.second + 32) >> 6;
let r = x - p;
let q = cap_value((r + (((1 << shift) - (r < 0) as i32) >> 1)) >> shift, -8, 7);
let y = cap_value(p + (q << shift), std::i16::MIN as i32, std::i16::MAX as i32);
let e = y - x;
this_frame.data[(sample_id+4)/8] |= ((q&0xF) << (((sample_id + 4)%8)*4)) as u32;
this_error += (e*e) as u64;
}
if this_error < best_error {
best_tap = this_tap;
best_error = this_error;
best_frame = this_frame;
}
}
}
(best_frame, best_tap)
}
fn set_vag_flags(&mut self, flags: VAGFlagBits) {
let mut first_sample = self.data[0].to_ne_bytes();
first_sample[1] = flags.bits();
self.data[0] = u32::from_ne_bytes(first_sample);
}
pub fn set_loop_start(mut self) -> Self {
self.set_vag_flags(VAGFlagBits::LoopStart);
self
}
pub fn set_loop_end_repeat(mut self) -> Self {
self.set_vag_flags(VAGFlagBits::LoopEnd | VAGFlagBits::Repeat);
self
}
pub fn set_loop_self(mut self) -> Self {
self.set_vag_flags(VAGFlagBits::LoopStart | VAGFlagBits::LoopEnd);
self
}
}
impl RawConversion<{VAGADPCM::SIZE}> for VAGADPCM {
fn convert_to_raw(&self) -> [u8; VAGADPCM::SIZE] {
unsafe {
let data: [u8; VAGADPCM::SIZE] = std::mem::transmute_copy(&self as &VAGADPCM);
data
}
}
}
#[derive(Clone)]
pub struct LPC {
first: i32,
second: i32
}
impl LPC {
const FILTERS: [LPC; 5] = [
LPC{first: 0, second: 0},
LPC{first: 60, second: 0},
LPC{first: 115, second: -52},
LPC{first: 98, second: -55},
LPC{first: 122, second: -60}
];
pub fn empty() -> LPC {
LPC{first: 0, second: 0}
}
}
pub type ADPCMSampleForVag = [i16; VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM];
pub struct MonoADPCMIterator<I: std::iter::Iterator<Item=Result<i16, hound::Error>>> {
iter: I
}
impl<I:std::iter::Iterator<Item=Result<i16, hound::Error>>> MonoADPCMIterator<I>{
pub fn create(iter: I) -> MonoADPCMIterator<I> {
MonoADPCMIterator{iter}
}
}
impl<I:std::iter::Iterator<Item=Result<i16, hound::Error>>> std::iter::Iterator for MonoADPCMIterator<I> {
type Item = Result<[i16; VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM], Error>;
fn next(&mut self) -> Option<Self::Item> {
const STREAM_GONE_ERROR: &'static str = "Reading ADPCM sample failed";
if let Some(next_sample) = self.iter.next() {
let Ok(next_sample) = next_sample else {return Some(Err(Error::from_str(STREAM_GONE_ERROR)));};
let mut sample = [0;VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM];
sample[0] = next_sample;
for idx in 1..VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM {
let Ok(next_sample) = self.iter.next().unwrap_or(Ok(0)) else {return Some(Err(Error::from_str(STREAM_GONE_ERROR)));};
sample[idx] = next_sample;
}
return Some(Ok(sample));
}
None
}
use bitflags::bitflags;
use tool_helper::{raw::RawConversion, Error};
#[repr(packed)]
#[derive(Clone)]
pub struct VAGHeader {
_id: [u8; 4],
version: u32,
_reserved: u32,
data_size: u32,
sampling_frequency: u32,
_reserved2: [u8; 12],
_name: [u8; 16]
}
impl VAGHeader {
const SIZE: usize = std::mem::size_of::<VAGHeader>();
const ID: [u8; 4] = ['V' as u8, 'A' as u8, 'G' as u8, 'p' as u8];
const VERSION: u32 = 0x02;
const RESERVED: u32 = 0;
const RESERVED2: [u8; 12] = [0; 12];
pub fn expected_vagadpcm_samples(adpcm_samples: u32) -> u32 {
((adpcm_samples as usize + (VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM - 1))/VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM) as u32
}
pub fn create(vagadpcm_samples: u32, sampling_frequency: u32, name: &str) -> Result<VAGHeader, Error> {
let data_size = vagadpcm_samples*VAGADPCM::SIZE as u32;
let name = {
if !name.is_ascii() {
return Err(Error::from_text(format!("File name {} is not ascii", name)));
}
let name_length = if name.len() > 16 {16} else {name.len()};
let mut new_name = [0u8; 16];
new_name[..name_length].copy_from_slice(&name.as_bytes()[..name_length]);
new_name
};
Ok(VAGHeader{
_id: VAGHeader::ID,
version: VAGHeader::VERSION,
_reserved: VAGHeader::RESERVED,
data_size: data_size,
sampling_frequency: sampling_frequency,
_reserved2: VAGHeader::RESERVED2,
_name: name
})
}
}
impl RawConversion<{VAGHeader::SIZE}> for VAGHeader {
fn convert_to_raw(&self) -> [u8; VAGHeader::SIZE] {
unsafe {
let mut vag_header = self.clone();
vag_header.version = self.version.to_be();
vag_header.data_size = self.data_size.to_be();
vag_header.sampling_frequency = self.sampling_frequency.to_be();
let data: [u8; VAGHeader::SIZE] = std::mem::transmute(vag_header);
data
}
}
}
bitflags! {
struct VAGFlagBits : u8 {
const LoopEnd = (1 << 0);
const Repeat = (1 << 1);
const LoopStart = (1 << 2);
}
}
pub struct VAGADPCM {
data: [u32; 4]
}
impl VAGADPCM {
const SIZE: usize = std::mem::size_of::<VAGADPCM>();
pub(super) const ADPCM_SAMPLES_PER_VAGADPCM:usize = 28;
pub fn empty() -> VAGADPCM {
VAGADPCM{data: [0; 4]}
}
fn create_for_filter_shift(filter: u32, shift: u32) -> VAGADPCM {
VAGADPCM{data: [(12 - shift) | filter << 4, 0, 0, 0]}
}
pub fn end() -> VAGADPCM {
VAGADPCM::empty().set_loop_self()
}
pub fn create(samples: ADPCMSampleForVag, lpc_tap: LPC) -> (VAGADPCM, LPC) {
fn cap_value(value: i32, min: i32, max: i32) -> i32 {
if value < min {
min
}
else if value > max {
max
}
else {
value
}
}
let mut best_frame = VAGADPCM::empty();
let mut best_tap = LPC::empty();
let mut best_error = std::u64::MAX as u64;
for (filter_id, filter) in LPC::FILTERS.iter().enumerate() {
for shift in 0..=12 {
let mut this_frame = VAGADPCM::create_for_filter_shift(filter_id as u32, shift);
let this_tap = lpc_tap.clone();
let mut this_error = 0;
for (sample_id, sample) in samples.iter().enumerate() {
let x = *sample as i32;
let p = (this_tap.first*filter.first + this_tap.second*filter.second + 32) >> 6;
let r = x - p;
let q = cap_value((r + (((1 << shift) - (r < 0) as i32) >> 1)) >> shift, -8, 7);
let y = cap_value(p + (q << shift), std::i16::MIN as i32, std::i16::MAX as i32);
let e = y - x;
this_frame.data[(sample_id+4)/8] |= ((q&0xF) << (((sample_id + 4)%8)*4)) as u32;
this_error += (e*e) as u64;
}
if this_error < best_error {
best_tap = this_tap;
best_error = this_error;
best_frame = this_frame;
}
}
}
(best_frame, best_tap)
}
fn set_vag_flags(&mut self, flags: VAGFlagBits) {
let mut first_sample = self.data[0].to_ne_bytes();
first_sample[1] = flags.bits();
self.data[0] = u32::from_ne_bytes(first_sample);
}
pub fn set_loop_start(mut self) -> Self {
self.set_vag_flags(VAGFlagBits::LoopStart);
self
}
pub fn set_loop_end_repeat(mut self) -> Self {
self.set_vag_flags(VAGFlagBits::LoopEnd | VAGFlagBits::Repeat);
self
}
pub fn set_loop_self(mut self) -> Self {
self.set_vag_flags(VAGFlagBits::LoopStart | VAGFlagBits::LoopEnd);
self
}
}
impl RawConversion<{VAGADPCM::SIZE}> for VAGADPCM {
fn convert_to_raw(&self) -> [u8; VAGADPCM::SIZE] {
unsafe {
let data: [u8; VAGADPCM::SIZE] = std::mem::transmute_copy(&self as &VAGADPCM);
data
}
}
}
#[derive(Clone)]
pub struct LPC {
first: i32,
second: i32
}
impl LPC {
const FILTERS: [LPC; 5] = [
LPC{first: 0, second: 0},
LPC{first: 60, second: 0},
LPC{first: 115, second: -52},
LPC{first: 98, second: -55},
LPC{first: 122, second: -60}
];
pub fn empty() -> LPC {
LPC{first: 0, second: 0}
}
}
pub type ADPCMSampleForVag = [i16; VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM];
pub struct MonoADPCMIterator<I: std::iter::Iterator<Item=Result<i16, hound::Error>>> {
iter: I
}
impl<I:std::iter::Iterator<Item=Result<i16, hound::Error>>> MonoADPCMIterator<I>{
pub fn create(iter: I) -> MonoADPCMIterator<I> {
MonoADPCMIterator{iter}
}
}
impl<I:std::iter::Iterator<Item=Result<i16, hound::Error>>> std::iter::Iterator for MonoADPCMIterator<I> {
type Item = Result<[i16; VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM], Error>;
fn next(&mut self) -> Option<Self::Item> {
const STREAM_GONE_ERROR: &'static str = "Reading ADPCM sample failed";
if let Some(next_sample) = self.iter.next() {
let Ok(next_sample) = next_sample else {return Some(Err(Error::from_str(STREAM_GONE_ERROR)));};
let mut sample = [0;VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM];
sample[0] = next_sample;
for idx in 1..VAGADPCM::ADPCM_SAMPLES_PER_VAGADPCM {
let Ok(next_sample) = self.iter.next().unwrap_or(Ok(0)) else {return Some(Err(Error::from_str(STREAM_GONE_ERROR)));};
sample[idx] = next_sample;
}
return Some(Ok(sample));
}
None
}
}

View File

@@ -1,39 +1,39 @@
mod raw_audio;
mod xa_audio;
use clap::{Args, ValueEnum};
use std::io::Write;
use tool_helper::{Error, Input};
use xa_audio::{LOW_FREQUENCY, HIGH_FREQUENCY};
#[derive(Args, Clone)]
pub struct Arguments {
#[clap(short='f', long="frequency", value_enum, value_parser, default_value_t=Frequency::High)]
frequency: Frequency,
#[clap(short='b', long="bitdepth", value_enum, value_parser, default_value_t=SampleDepth::Normal)]
sample_depth: SampleDepth,
}
#[derive(Copy, Clone, ValueEnum)]
pub enum Frequency {
High,
Low,
}
#[derive(Copy, Clone, ValueEnum)]
pub enum SampleDepth {
Normal,
High
}
pub fn convert(args: Arguments, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let prepared_xa_audio = raw_audio::load_as_i16_audio(input, frequency_to_value(args.frequency))?;
xa_audio::encode(prepared_xa_audio, output, &args)
}
fn frequency_to_value(requested_freq: Frequency) -> u32 {
match requested_freq {
Frequency::High => HIGH_FREQUENCY,
Frequency::Low => LOW_FREQUENCY,
}
mod raw_audio;
mod xa_audio;
use clap::{Args, ValueEnum};
use std::io::Write;
use tool_helper::{Error, Input};
use xa_audio::{LOW_FREQUENCY, HIGH_FREQUENCY};
#[derive(Args, Clone)]
pub struct Arguments {
#[clap(short='f', long="frequency", value_enum, value_parser, default_value_t=Frequency::High)]
frequency: Frequency,
#[clap(short='b', long="bitdepth", value_enum, value_parser, default_value_t=SampleDepth::Normal)]
sample_depth: SampleDepth,
}
#[derive(Copy, Clone, ValueEnum)]
pub enum Frequency {
High,
Low,
}
#[derive(Copy, Clone, ValueEnum)]
pub enum SampleDepth {
Normal,
High
}
pub fn convert(args: Arguments, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let prepared_xa_audio = raw_audio::load_as_i16_audio(input, frequency_to_value(args.frequency))?;
xa_audio::encode(prepared_xa_audio, output, &args)
}
fn frequency_to_value(requested_freq: Frequency) -> u32 {
match requested_freq {
Frequency::High => HIGH_FREQUENCY,
Frequency::Low => LOW_FREQUENCY,
}
}

View File

@@ -1,35 +1,35 @@
use super::Error;
use symphonia::core::errors::Error as SymError;
use rubato::{ResampleError, ResamplerConstructionError};
fn generic_map_error(action: &str, error_str: String) -> Error {
Error::from_text(format!("symphonia error: {} during {}", error_str, action))
}
pub fn probe(error: SymError) -> Error {
generic_map_error("probing of input", error.to_string())
}
pub fn decoder(error: SymError) -> Error {
generic_map_error("finding codec", error.to_string())
}
pub fn next_packet(error: SymError) -> Error {
generic_map_error("getting next raw packet", error.to_string())
}
pub fn decode(error: SymError) -> Error {
generic_map_error("decoding of raw packet", error.to_string())
}
pub fn resampler_construction(error: ResamplerConstructionError) -> Error {
generic_map_error("creating resampler", error.to_string())
}
pub fn resample(error: ResampleError) -> Error {
generic_map_error("resampling", error.to_string())
}
pub fn find_track() -> Error {
Error::from_str("symphonia error: No audio track located")
use super::Error;
use symphonia::core::errors::Error as SymError;
use rubato::{ResampleError, ResamplerConstructionError};
fn generic_map_error(action: &str, error_str: String) -> Error {
Error::from_text(format!("symphonia error: {} during {}", error_str, action))
}
pub fn probe(error: SymError) -> Error {
generic_map_error("probing of input", error.to_string())
}
pub fn decoder(error: SymError) -> Error {
generic_map_error("finding codec", error.to_string())
}
pub fn next_packet(error: SymError) -> Error {
generic_map_error("getting next raw packet", error.to_string())
}
pub fn decode(error: SymError) -> Error {
generic_map_error("decoding of raw packet", error.to_string())
}
pub fn resampler_construction(error: ResamplerConstructionError) -> Error {
generic_map_error("creating resampler", error.to_string())
}
pub fn resample(error: ResampleError) -> Error {
generic_map_error("resampling", error.to_string())
}
pub fn find_track() -> Error {
Error::from_str("symphonia error: No audio track located")
}

View File

@@ -1,238 +1,238 @@
mod error;
use rubato::{FftFixedInOut, Resampler};
use symphonia::core::{
audio::{AudioBuffer, Layout, SampleBuffer, Signal, SignalSpec},
codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL},
errors::Error as SymError,
formats::{FormatOptions, FormatReader},
io::MediaSourceStream,
meta::MetadataOptions,
probe::Hint
};
use tool_helper::{Error, Input};
#[derive(Copy, Clone, PartialEq)]
pub enum Orality {
Stereo,
Mono,
}
pub struct CDAudioSamples {
samples: Vec::<i16>,
orality: Orality,
}
impl CDAudioSamples {
pub fn new(samples: Vec<i16>, channels: usize) -> Result<CDAudioSamples, Error> {
let orality = match channels {
0 => return Err(Error::from_str("Input file has no audio channels")),
1 => Orality::Mono,
2 => Orality::Stereo,
_ => return Err(Error::from_str("Only Mono and Stereo input are supported")),
};
Ok(CDAudioSamples{samples, orality})
}
pub fn samples(&self) -> &Vec::<i16> {
&self.samples
}
pub fn orality(&self) -> Orality {
self.orality.clone()
}
}
struct InternalAudioSamples {
planar_samples: Vec<Vec<f32>>,
frequency: u32,
}
impl InternalAudioSamples {
pub fn new(planar_samples: Vec<Vec<f32>>, frequency: u32) -> Result<InternalAudioSamples, Error> {
if planar_samples.len() < 1 || planar_samples.len() > 2{
Err(Error::from_str("Audio samples need to be either mono or stereo"))
}
else {
Ok(InternalAudioSamples{planar_samples, frequency})
}
}
pub fn into_audio_buffer(self) -> AudioBuffer<f32> {
let duration = self.sample_len() as u64;
let mut new_audio_buffer = AudioBuffer::new(duration, SignalSpec::new_with_layout(self.frequency, if self.planar_samples.len() == 1 {Layout::Mono} else {Layout::Stereo}));
new_audio_buffer.render_silence(None);
for (channel_idx, channel) in self.planar_samples.into_iter().enumerate() {
let dst_channel = new_audio_buffer.chan_mut(channel_idx);
for (sample_idx, sample) in channel.into_iter().enumerate() {
dst_channel[sample_idx] = sample;
}
}
new_audio_buffer
}
pub fn sample_len(&self) -> usize {
self.planar_samples[0].len()
}
pub fn channels(&self) -> usize {
self.planar_samples.len()
}
pub fn planar_slices(&self) -> Vec<&[f32]> {
let mut planar_slices = Vec::new();
for channel in &self.planar_samples {
planar_slices.push(channel.as_slice());
}
planar_slices
}
}
pub fn load_as_i16_audio(input: Input, target_frequency: u32) -> Result<CDAudioSamples, Error> {
let raw_audio = load_raw_audio(input)?;
let raw_audio = resample(raw_audio, target_frequency)?;
down_sample_interleave(raw_audio)
}
fn load_raw_audio(input: Input) -> Result<InternalAudioSamples, Error> {
let media_stream = MediaSourceStream::new(Box::new(load_to_ram(input)?), Default::default());
let format = symphonia::default::get_probe().format(&Hint::new(), media_stream, &FormatOptions::default(), &MetadataOptions::default()).map_err(error::probe)?.format;
let track = format.tracks().iter().find(|t| t.codec_params.codec != CODEC_TYPE_NULL).ok_or_else(error::find_track)?;
// Create a decoder for the track.
let decoder = symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default()).map_err(error::decoder)?;
let track_id = track.id;
decode(format, decoder, track_id)
}
fn decode(mut format: Box<dyn FormatReader>, mut decoder: Box<dyn Decoder>, track_id: u32) -> Result<InternalAudioSamples, Error> {
let mut samples = Vec::new();
let mut channel_count = 0;
let mut frequency = 0;
let mut read_buffer = None;
loop {
// Get the next packet from the media format.
let packet = match format.next_packet() {
Ok(packet) => packet,
Err(err) => {
if let SymError::IoError(io_err) = &err {
if io_err.kind() == std::io::ErrorKind::UnexpectedEof {
return InternalAudioSamples::new(samples, frequency);
}
}
return Err(error::next_packet(err));
}
};
// Consume any new metadata that has been read since the last packet.
format.metadata().skip_to_latest();
// If the packet does not belong to the selected track, skip over it.
if packet.track_id() != track_id {
continue;
}
// Decode the packet into audio samples.
let packet = decoder.decode(&packet).map_err(error::decode)?;
if read_buffer.is_none() {
let duration = packet.capacity() as u64;
let specs = packet.spec();
channel_count = specs.channels.count();
frequency = specs.rate;
read_buffer = Some(SampleBuffer::<f32>::new(duration, packet.spec().clone()));
for _ in 0..channel_count {
samples.push(Vec::new());
}
}
if let Some(read_buffer) = &mut read_buffer {
read_buffer.copy_planar_ref(packet);
let cur_samples = read_buffer.samples();
let mut cur_samples = cur_samples.chunks(cur_samples.len()/channel_count);
for dst_sample in &mut samples {
dst_sample.extend(cur_samples.next().ok_or_else(|| Error::from_str("Not enough channels in input as expected"))?);
}
}
}
}
fn resample(input: InternalAudioSamples, target_frequency: u32) -> Result<InternalAudioSamples, Error> {
const HIGH_QUALITY_CHUNKS:usize = (1024*10)*100;
fn process_partial(input_option: Option<&[&[f32]]>, resampler: &mut FftFixedInOut<f32>, planar_output: &mut Vec<Vec<f32>>) -> Result<(), Error> {
let new_samples = resampler.process_partial(input_option, None).map_err(error::resample)?;
for (channel, channel_samples) in new_samples.into_iter().enumerate() {
planar_output[channel].extend(channel_samples.iter());
}
Ok(())
}
let chunk_size = HIGH_QUALITY_CHUNKS;
let mut planar_input = input.planar_slices();
let mut resampler = FftFixedInOut::<f32>::new(input.frequency as usize, target_frequency as usize, chunk_size, input.channels()).map_err(error::resampler_construction)?;
let delay = resampler.output_delay();
let mut sample_len = input.sample_len();
let new_sample_len = (sample_len as f64*(target_frequency as f64/input.frequency as f64)) as usize;
let mut planar_output = {
let mut planar_output = Vec::new();
for _ in 0..planar_input.len() {
planar_output.push(Vec::<f32>::new());
}
planar_output
};
loop {
let next_input_frames = resampler.input_frames_next();
if next_input_frames > sample_len {
if sample_len > 0 {
// Still frames left
process_partial(Some(&planar_input), &mut resampler, &mut planar_output)?;
}
break;
}
let new_samples = resampler.process(&planar_input, None).map_err(error::resample)?;
for (channel, slice) in planar_input.iter_mut().enumerate() {
*slice = &slice[next_input_frames..];
planar_output[channel].extend(new_samples[channel].iter());
}
sample_len -= next_input_frames;
}
if planar_output[0].len() < delay + new_sample_len {
// Flush
process_partial(None, &mut resampler, &mut planar_output)?;
}
for channel in &mut planar_output {
let start = delay;
let end = start + new_sample_len;
*channel = channel[start..end].into();
}
InternalAudioSamples::new(planar_output, target_frequency)
}
fn down_sample_interleave(input: InternalAudioSamples) -> Result<CDAudioSamples, Error> {
let channels = input.channels();
let audio_buffer = input.into_audio_buffer();
let mut sample_buffer = SampleBuffer::<i16>::new(audio_buffer.capacity() as u64, audio_buffer.spec().clone());
sample_buffer.copy_interleaved_typed::<f32>(&audio_buffer);
CDAudioSamples::new(sample_buffer.samples().to_vec(), channels)
}
fn load_to_ram(mut input: Input) -> Result<std::io::Cursor<Vec<u8>>, Error> {
let mut buffer = Vec::default();
input.read_to_end(&mut buffer)?;
Ok(std::io::Cursor::new(buffer))
mod error;
use rubato::{FftFixedInOut, Resampler};
use symphonia::core::{
audio::{AudioBuffer, Layout, SampleBuffer, Signal, SignalSpec},
codecs::{Decoder, DecoderOptions, CODEC_TYPE_NULL},
errors::Error as SymError,
formats::{FormatOptions, FormatReader},
io::MediaSourceStream,
meta::MetadataOptions,
probe::Hint
};
use tool_helper::{Error, Input};
#[derive(Copy, Clone, PartialEq)]
pub enum Orality {
Stereo,
Mono,
}
pub struct CDAudioSamples {
samples: Vec::<i16>,
orality: Orality,
}
impl CDAudioSamples {
pub fn new(samples: Vec<i16>, channels: usize) -> Result<CDAudioSamples, Error> {
let orality = match channels {
0 => return Err(Error::from_str("Input file has no audio channels")),
1 => Orality::Mono,
2 => Orality::Stereo,
_ => return Err(Error::from_str("Only Mono and Stereo input are supported")),
};
Ok(CDAudioSamples{samples, orality})
}
pub fn samples(&self) -> &Vec::<i16> {
&self.samples
}
pub fn orality(&self) -> Orality {
self.orality.clone()
}
}
struct InternalAudioSamples {
planar_samples: Vec<Vec<f32>>,
frequency: u32,
}
impl InternalAudioSamples {
pub fn new(planar_samples: Vec<Vec<f32>>, frequency: u32) -> Result<InternalAudioSamples, Error> {
if planar_samples.len() < 1 || planar_samples.len() > 2{
Err(Error::from_str("Audio samples need to be either mono or stereo"))
}
else {
Ok(InternalAudioSamples{planar_samples, frequency})
}
}
pub fn into_audio_buffer(self) -> AudioBuffer<f32> {
let duration = self.sample_len() as u64;
let mut new_audio_buffer = AudioBuffer::new(duration, SignalSpec::new_with_layout(self.frequency, if self.planar_samples.len() == 1 {Layout::Mono} else {Layout::Stereo}));
new_audio_buffer.render_silence(None);
for (channel_idx, channel) in self.planar_samples.into_iter().enumerate() {
let dst_channel = new_audio_buffer.chan_mut(channel_idx);
for (sample_idx, sample) in channel.into_iter().enumerate() {
dst_channel[sample_idx] = sample;
}
}
new_audio_buffer
}
pub fn sample_len(&self) -> usize {
self.planar_samples[0].len()
}
pub fn channels(&self) -> usize {
self.planar_samples.len()
}
pub fn planar_slices(&self) -> Vec<&[f32]> {
let mut planar_slices = Vec::new();
for channel in &self.planar_samples {
planar_slices.push(channel.as_slice());
}
planar_slices
}
}
pub fn load_as_i16_audio(input: Input, target_frequency: u32) -> Result<CDAudioSamples, Error> {
let raw_audio = load_raw_audio(input)?;
let raw_audio = resample(raw_audio, target_frequency)?;
down_sample_interleave(raw_audio)
}
fn load_raw_audio(input: Input) -> Result<InternalAudioSamples, Error> {
let media_stream = MediaSourceStream::new(Box::new(load_to_ram(input)?), Default::default());
let format = symphonia::default::get_probe().format(&Hint::new(), media_stream, &FormatOptions::default(), &MetadataOptions::default()).map_err(error::probe)?.format;
let track = format.tracks().iter().find(|t| t.codec_params.codec != CODEC_TYPE_NULL).ok_or_else(error::find_track)?;
// Create a decoder for the track.
let decoder = symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default()).map_err(error::decoder)?;
let track_id = track.id;
decode(format, decoder, track_id)
}
fn decode(mut format: Box<dyn FormatReader>, mut decoder: Box<dyn Decoder>, track_id: u32) -> Result<InternalAudioSamples, Error> {
let mut samples = Vec::new();
let mut channel_count = 0;
let mut frequency = 0;
let mut read_buffer = None;
loop {
// Get the next packet from the media format.
let packet = match format.next_packet() {
Ok(packet) => packet,
Err(err) => {
if let SymError::IoError(io_err) = &err {
if io_err.kind() == std::io::ErrorKind::UnexpectedEof {
return InternalAudioSamples::new(samples, frequency);
}
}
return Err(error::next_packet(err));
}
};
// Consume any new metadata that has been read since the last packet.
format.metadata().skip_to_latest();
// If the packet does not belong to the selected track, skip over it.
if packet.track_id() != track_id {
continue;
}
// Decode the packet into audio samples.
let packet = decoder.decode(&packet).map_err(error::decode)?;
if read_buffer.is_none() {
let duration = packet.capacity() as u64;
let specs = packet.spec();
channel_count = specs.channels.count();
frequency = specs.rate;
read_buffer = Some(SampleBuffer::<f32>::new(duration, packet.spec().clone()));
for _ in 0..channel_count {
samples.push(Vec::new());
}
}
if let Some(read_buffer) = &mut read_buffer {
read_buffer.copy_planar_ref(packet);
let cur_samples = read_buffer.samples();
let mut cur_samples = cur_samples.chunks(cur_samples.len()/channel_count);
for dst_sample in &mut samples {
dst_sample.extend(cur_samples.next().ok_or_else(|| Error::from_str("Not enough channels in input as expected"))?);
}
}
}
}
fn resample(input: InternalAudioSamples, target_frequency: u32) -> Result<InternalAudioSamples, Error> {
const HIGH_QUALITY_CHUNKS:usize = (1024*10)*100;
fn process_partial(input_option: Option<&[&[f32]]>, resampler: &mut FftFixedInOut<f32>, planar_output: &mut Vec<Vec<f32>>) -> Result<(), Error> {
let new_samples = resampler.process_partial(input_option, None).map_err(error::resample)?;
for (channel, channel_samples) in new_samples.into_iter().enumerate() {
planar_output[channel].extend(channel_samples.iter());
}
Ok(())
}
let chunk_size = HIGH_QUALITY_CHUNKS;
let mut planar_input = input.planar_slices();
let mut resampler = FftFixedInOut::<f32>::new(input.frequency as usize, target_frequency as usize, chunk_size, input.channels()).map_err(error::resampler_construction)?;
let delay = resampler.output_delay();
let mut sample_len = input.sample_len();
let new_sample_len = (sample_len as f64*(target_frequency as f64/input.frequency as f64)) as usize;
let mut planar_output = {
let mut planar_output = Vec::new();
for _ in 0..planar_input.len() {
planar_output.push(Vec::<f32>::new());
}
planar_output
};
loop {
let next_input_frames = resampler.input_frames_next();
if next_input_frames > sample_len {
if sample_len > 0 {
// Still frames left
process_partial(Some(&planar_input), &mut resampler, &mut planar_output)?;
}
break;
}
let new_samples = resampler.process(&planar_input, None).map_err(error::resample)?;
for (channel, slice) in planar_input.iter_mut().enumerate() {
*slice = &slice[next_input_frames..];
planar_output[channel].extend(new_samples[channel].iter());
}
sample_len -= next_input_frames;
}
if planar_output[0].len() < delay + new_sample_len {
// Flush
process_partial(None, &mut resampler, &mut planar_output)?;
}
for channel in &mut planar_output {
let start = delay;
let end = start + new_sample_len;
*channel = channel[start..end].into();
}
InternalAudioSamples::new(planar_output, target_frequency)
}
fn down_sample_interleave(input: InternalAudioSamples) -> Result<CDAudioSamples, Error> {
let channels = input.channels();
let audio_buffer = input.into_audio_buffer();
let mut sample_buffer = SampleBuffer::<i16>::new(audio_buffer.capacity() as u64, audio_buffer.spec().clone());
sample_buffer.copy_interleaved_typed::<f32>(&audio_buffer);
CDAudioSamples::new(sample_buffer.samples().to_vec(), channels)
}
fn load_to_ram(mut input: Input) -> Result<std::io::Cursor<Vec<u8>>, Error> {
let mut buffer = Vec::default();
input.read_to_end(&mut buffer)?;
Ok(std::io::Cursor::new(buffer))
}

View File

@@ -1,20 +1,20 @@
mod xapcm;
use super::Arguments;
use super::raw_audio::CDAudioSamples;
use cdtypes::types::sector::{Mode2Form2, SECTOR_SIZE};
use std::io::Write;
use tool_helper::Error;
pub const HIGH_FREQUENCY:u32 = 37_800;
pub const LOW_FREQUENCY:u32 = 18_900;
pub fn encode(input: CDAudioSamples, output: &mut dyn Write, arguments: &Arguments) -> Result<(), Error> {
let mut encoder = xapcm::Encoder::new(&input, arguments.frequency, arguments.sample_depth);
while let Some(xa_sector) = encoder.encode_next_xa_sector()? {
let xa_sector = unsafe {std::mem::transmute::<Mode2Form2, [u8; SECTOR_SIZE]>(xa_sector)};
output.write(&xa_sector)?;
}
Ok(())
mod xapcm;
use super::Arguments;
use super::raw_audio::CDAudioSamples;
use cdtypes::types::sector::{Mode2Form2, SECTOR_SIZE};
use std::io::Write;
use tool_helper::Error;
pub const HIGH_FREQUENCY:u32 = 37_800;
pub const LOW_FREQUENCY:u32 = 18_900;
pub fn encode(input: CDAudioSamples, output: &mut dyn Write, arguments: &Arguments) -> Result<(), Error> {
let mut encoder = xapcm::Encoder::new(&input, arguments.frequency, arguments.sample_depth);
while let Some(xa_sector) = encoder.encode_next_xa_sector()? {
let xa_sector = unsafe {std::mem::transmute::<Mode2Form2, [u8; SECTOR_SIZE]>(xa_sector)};
output.write(&xa_sector)?;
}
Ok(())
}

View File

@@ -1,313 +1,313 @@
use crate::audio::xa::{raw_audio::{CDAudioSamples, Orality}, Frequency, SampleDepth};
use cdtypes::types::sector::{Mode2Form2, XAADPCMBitsPerSample, XAADPCMSampleRate, XAADPCMSound};
use tool_helper::Error;
pub struct Encoder<'a> {
left: ChannelState,
right: ChannelState,
source: &'a[i16],
frequency: Frequency,
sample_depth: SampleDepth,
orality: Orality,
samples_per_block: i32,
sample_limit: i32
}
impl<'a> Encoder<'a> {
const BLOCKS_PER_SECTOR:usize = 18;
const XA_ADPCM_FILTER_COUNT: i32 = 4;
const FILTER_K1: [i16; 5] = [0, 60, 115, 98, 122];
const FILTER_K2: [i16; 5] = [0, 0, -52, -55, -60];
pub fn new(cd_sample: &CDAudioSamples, frequency: Frequency, sample_depth: SampleDepth) -> Encoder {
let orality = cd_sample.orality();
let (samples_per_block, sample_limit) = Self::samples_per_block_and_limit(&cd_sample.samples(), sample_depth, orality);
Encoder{left: ChannelState::default(), right: ChannelState::default(), source: &cd_sample.samples(), frequency, sample_depth, orality, samples_per_block, sample_limit}
}
pub fn encode_next_xa_sector(&mut self) -> Result<Option<Mode2Form2>, Error> {
if self.source.is_empty() {
return Ok(None);
}
let mut sector = self.create_new_sector();
let mut dst = &mut sector.data[0..];
for _ in 0..Self::BLOCKS_PER_SECTOR {
if self.source.len() < self.samples_per_block as usize {
self.source = &self.source[self.source.len()..];
break;
}
self.encode_xa(&self.source[0..], self.sample_limit, dst)?;
self.sample_limit -= self.samples_per_block;
self.source = &self.source[self.samples_per_block as usize..];
dst = &mut dst[0x80..];
}
sector.finalize();
Ok(Some(sector))
}
fn create_new_sector(&self) -> Mode2Form2 {
let mut sector = Mode2Form2::new();
let sub_mode = &mut sector.sub_header.sub_mode;
let coding_info = &mut sector.sub_header.coding_info;
sub_mode.set_real_time();
coding_info.set_sound_type(match self.orality {
Orality::Mono => XAADPCMSound::Mono,
Orality::Stereo => XAADPCMSound::Stereo
});
coding_info.set_sample_rate(match self.frequency {
Frequency::Low => XAADPCMSampleRate::Freq18900Hz,
Frequency::High => XAADPCMSampleRate::Freq37800Hz,
});
coding_info.set_bits_per_sample(match self.sample_depth {
SampleDepth::Normal => XAADPCMBitsPerSample::Normal,
SampleDepth::High => XAADPCMBitsPerSample::High,
});
sector
}
fn encode_xa(&mut self, samples: &[i16], sample_limit: i32, data: &mut [u8]) -> Result<(), Error> {
const SHIFT_RANGE_4BPS: i32 = 12;
const SHIFT_RANGE_8BPS: i32 = 8;
let channels = [&mut self.left, &mut self.right];
match self.sample_depth {
SampleDepth::Normal => {
let (modulo, offset) = if self.orality == Orality::Stereo {(2, &STEREO_4BIT)} else {(1, &MONO_4BIT)};
let (first_offset, second_offset) = offset;
for (offset_idx, offset_set) in [first_offset, second_offset].iter().enumerate() {
for (idx, offset) in offset_set.iter().enumerate() {
let byte = Self::encode(channels[idx%modulo], &samples[offset.sample..], sample_limit + offset.sample_limit, offset.pitch, &mut data[offset.data..], offset.data_shift, offset.data_pitch, Self::XA_ADPCM_FILTER_COUNT, SHIFT_RANGE_4BPS)?;
data[idx + (offset_idx*8)] = byte;
data[idx + 4 + (offset_idx*8)] = byte;
}
}
},
SampleDepth::High => {
let (modulo, offset_set) = if self.orality == Orality::Stereo {(2, &STEREO_8BIT)} else {(1, &MONO_8BIT)};
for (idx, offset) in offset_set.iter().enumerate() {
let byte = Self::encode(channels[idx%modulo], &samples[offset.sample..], sample_limit + offset.sample_limit, offset.pitch, &mut data[offset.data..], offset.data_shift, offset.data_pitch, Self::XA_ADPCM_FILTER_COUNT, SHIFT_RANGE_8BPS)?;
data[idx] = byte;
data[idx + 4] = byte;
}
}
}
Ok(())
}
fn encode(channel_state: &mut ChannelState, samples: &[i16], sample_limit: i32, pitch: i32, data: &mut [u8], data_shift: i32, data_pitch: i32, filter_count: i32, shift_range: i32) -> Result<u8, Error> {
let mut best_mse = 1i64 << 50i64;
let mut best_filer = 0;
let mut best_sample_shift = 0;
for filter in 0..filter_count {
let true_min_shift = Self::find_min_shift(channel_state, samples, sample_limit, pitch, filter, shift_range)?;
// Testing has shown that the optimal shift can be off the true minimum shift
// by 1 in *either* direction.
// This is NOT the case when dither is used.
let min_shift = if true_min_shift - 1 < 0 {0} else {true_min_shift - 1};
let max_shift = if true_min_shift + 1 > shift_range {shift_range} else {true_min_shift + 1};
for sample_shift in min_shift..=max_shift {
let mut proposed = channel_state.clone();
Self::attempt_encode(&mut proposed, samples, sample_limit, pitch, data, data_shift, data_pitch, filter, sample_shift, shift_range)?;
if best_mse > proposed.mse {
best_mse = proposed.mse;
best_filer = filter;
best_sample_shift = sample_shift;
}
}
}
Self::attempt_encode(channel_state, samples, sample_limit, pitch, data, data_shift, data_pitch, best_filer, best_sample_shift, shift_range)
}
fn attempt_encode(out_channel_state: &mut ChannelState, samples: &[i16], sample_limit: i32, pitch: i32, data: &mut [u8], data_shift: i32, data_pitch: i32, filter: i32, sample_shift: i32, shift_range: i32) -> Result<u8, Error> {
let sample_mask = (0xFFFF >> shift_range) as u8;
let nondata_mask = (!(sample_mask << data_shift)) as u8;
let min_shift = sample_shift;
let k1 = Self::FILTER_K1[filter as usize] as i32;
let k2 = Self::FILTER_K2[filter as usize] as i32;
let hdr = ((min_shift & 0x0F) | ((filter as i32) << 4)) as u8;
out_channel_state.mse = 0;
for i in 0..28 {
let sample = (if i >= sample_limit {0} else {samples[(i*pitch) as usize] as i32}) + out_channel_state.qerr;
let previous_value = (k1*out_channel_state.prev1 + k2*out_channel_state.prev2 + (1 << 5)) >> 6;
let mut sample_enc = sample - previous_value;
sample_enc <<= min_shift;
sample_enc += 1 << (shift_range - 1);
sample_enc >>= shift_range;
if sample_enc < (std::i16::MIN as i32 >> shift_range) {sample_enc = std::i16::MIN as i32 >> shift_range}
if sample_enc > (std::i16::MAX as i32 >> shift_range) {sample_enc = std::i16::MAX as i32 >> shift_range}
sample_enc &= sample_mask as i32;
let mut sample_dec = (((sample_enc & sample_mask as i32) << shift_range) as i16) as i32;
sample_dec >>= min_shift;
sample_dec += previous_value;
if sample_dec > std::i16::MAX as i32 {sample_dec = std::i16::MAX as i32}
if sample_dec < std::i16::MIN as i32 {sample_dec = std::i16::MIN as i32}
let sample_error = sample_dec - sample;
if sample_error >= (1 << 30) || sample_error <= -(1 << 30) {
return Err(Error::from_text(format!("Sample error exceeds 30bit: {}", sample_error)));
}
data[(i*data_pitch) as usize] = ((data[(i*data_pitch) as usize] & nondata_mask) as i32 | (sample_enc << data_shift)) as u8;
out_channel_state.mse += (sample_error as u64*sample_error as u64) as i64;
out_channel_state.prev2 = out_channel_state.prev1;
out_channel_state.prev1 = sample_dec;
}
Ok(hdr)
}
fn find_min_shift(channel_state: &ChannelState, samples: &[i16], sample_limit: i32, pitch: i32, filter: i32, shift_range: i32) -> Result<i32, Error> {
/*
Assumption made:
There is value in shifting right one step further to allow the nibbles to clip.
However, given a possible shift value, there is no value in shifting one step less.
Having said that, this is not a completely accurate model of the encoder,
so maybe we will need to shift one step less.
*/
let mut prev1 = channel_state.prev1;
let mut prev2 = channel_state.prev2;
let k1 = Self::FILTER_K1[filter as usize] as i32;
let k2 = Self::FILTER_K2[filter as usize] as i32;
let mut right_shift = 0;
let mut s_min = 0;
let mut s_max = 0;
for i in 0..28 {
let raw_sample = if i >= sample_limit {0} else {samples[(i*pitch) as usize]} as i32;
let prev_values = (k1*prev1 + k2*prev2 + (1 << 5)) >> 6;
let sample = raw_sample - prev_values;
if sample < s_min {
s_min = sample;
}
if sample > s_max {
s_max = sample;
}
prev2 = prev1;
prev1 = raw_sample;
}
while right_shift < shift_range && (s_max >> right_shift) > (std::i16::MAX as i32 >> shift_range) {
right_shift += 1;
}
while right_shift < shift_range && (s_min >> right_shift) < (std::i16::MIN as i32 >> shift_range) {
right_shift += 1;
}
let min_shift = shift_range - right_shift;
if 0 <= min_shift && min_shift <= shift_range {
Ok(min_shift)
}
else {
Err(Error::from_text(format!("0 <= {} && {} <= {} was not satisfied with min_shift: {}", min_shift, min_shift, shift_range, min_shift)))
}
}
fn samples_per_block_and_limit(input: &[i16], sample_depth: SampleDepth, orality: Orality) -> (i32, i32) {
let samples_per_block = match sample_depth {
SampleDepth::Normal => 224,
SampleDepth::High => 112,
};
let sample_limit = match orality {
Orality::Stereo => input.len()*2,
Orality::Mono => input.len(),
};
(samples_per_block, sample_limit as i32)
}
}
#[derive(Clone)]
struct ChannelState {
qerr: i32, // quanitisation error
mse: i64, // mean square error
prev1: i32,
prev2: i32
}
impl std::default::Default for ChannelState {
fn default() -> Self {
ChannelState{qerr: 0, mse: 0, prev1: 0, prev2: 0}
}
}
struct EncodingOffsets {
sample: usize,
sample_limit: i32,
pitch: i32,
data: usize,
data_shift: i32,
data_pitch: i32,
}
const STEREO_4BIT: ([EncodingOffsets; 4], [EncodingOffsets; 4]) = (
[
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 2, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 1, sample_limit: 0, pitch: 2, data: 0x10, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 56, sample_limit: -28, pitch: 2, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56 + 1, sample_limit: -28, pitch: 2, data: 0x11, data_shift: 4, data_pitch: 4},
],
[
EncodingOffsets{sample: 56*2, sample_limit: -28*2, pitch: 2, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56*2 + 1, sample_limit: -28*2, pitch: 2, data: 0x12, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 56*3, sample_limit: -28*3, pitch: 2, data: 0x13, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56*3 + 1, sample_limit: -28*3, pitch: 2, data: 0x13, data_shift: 4, data_pitch: 4}
]
);
const MONO_4BIT: ([EncodingOffsets; 4], [EncodingOffsets; 4]) = (
[
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 1, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28, sample_limit: -28, pitch: 1, data: 0x10, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 28*2, sample_limit: -28*2, pitch: 1, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*3, sample_limit: -28*3, pitch: 1, data: 0x11, data_shift: 4, data_pitch: 4},
],
[
EncodingOffsets{sample: 28*4, sample_limit: -28*4, pitch: 1, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*5, sample_limit: -28*5, pitch: 1, data: 0x12, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 28*6, sample_limit: -28*6, pitch: 1, data: 0x13, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*7, sample_limit: -28*7, pitch: 1, data: 0x13, data_shift: 4, data_pitch: 4}
]
);
const STEREO_8BIT: [EncodingOffsets;4] = [
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 2, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 1, sample_limit: 0, pitch: 2, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56, sample_limit: -28, pitch: 2, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56 + 1, sample_limit: -28, pitch: 2, data: 0x13, data_shift: 0, data_pitch: 4},
];
const MONO_8BIT: [EncodingOffsets;4] = [
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 1, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28, sample_limit: -28, pitch: 1, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*2, sample_limit: -28*2, pitch: 1, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*3, sample_limit: -28*3, pitch: 1, data: 0x13, data_shift: 0, data_pitch: 4},
use crate::audio::xa::{raw_audio::{CDAudioSamples, Orality}, Frequency, SampleDepth};
use cdtypes::types::sector::{Mode2Form2, XAADPCMBitsPerSample, XAADPCMSampleRate, XAADPCMSound};
use tool_helper::Error;
pub struct Encoder<'a> {
left: ChannelState,
right: ChannelState,
source: &'a[i16],
frequency: Frequency,
sample_depth: SampleDepth,
orality: Orality,
samples_per_block: i32,
sample_limit: i32
}
impl<'a> Encoder<'a> {
const BLOCKS_PER_SECTOR:usize = 18;
const XA_ADPCM_FILTER_COUNT: i32 = 4;
const FILTER_K1: [i16; 5] = [0, 60, 115, 98, 122];
const FILTER_K2: [i16; 5] = [0, 0, -52, -55, -60];
pub fn new(cd_sample: &CDAudioSamples, frequency: Frequency, sample_depth: SampleDepth) -> Encoder {
let orality = cd_sample.orality();
let (samples_per_block, sample_limit) = Self::samples_per_block_and_limit(&cd_sample.samples(), sample_depth, orality);
Encoder{left: ChannelState::default(), right: ChannelState::default(), source: &cd_sample.samples(), frequency, sample_depth, orality, samples_per_block, sample_limit}
}
pub fn encode_next_xa_sector(&mut self) -> Result<Option<Mode2Form2>, Error> {
if self.source.is_empty() {
return Ok(None);
}
let mut sector = self.create_new_sector();
let mut dst = &mut sector.data[0..];
for _ in 0..Self::BLOCKS_PER_SECTOR {
if self.source.len() < self.samples_per_block as usize {
self.source = &self.source[self.source.len()..];
break;
}
self.encode_xa(&self.source[0..], self.sample_limit, dst)?;
self.sample_limit -= self.samples_per_block;
self.source = &self.source[self.samples_per_block as usize..];
dst = &mut dst[0x80..];
}
sector.finalize();
Ok(Some(sector))
}
fn create_new_sector(&self) -> Mode2Form2 {
let mut sector = Mode2Form2::new();
let sub_mode = &mut sector.sub_header.sub_mode;
let coding_info = &mut sector.sub_header.coding_info;
sub_mode.set_real_time();
coding_info.set_sound_type(match self.orality {
Orality::Mono => XAADPCMSound::Mono,
Orality::Stereo => XAADPCMSound::Stereo
});
coding_info.set_sample_rate(match self.frequency {
Frequency::Low => XAADPCMSampleRate::Freq18900Hz,
Frequency::High => XAADPCMSampleRate::Freq37800Hz,
});
coding_info.set_bits_per_sample(match self.sample_depth {
SampleDepth::Normal => XAADPCMBitsPerSample::Normal,
SampleDepth::High => XAADPCMBitsPerSample::High,
});
sector
}
fn encode_xa(&mut self, samples: &[i16], sample_limit: i32, data: &mut [u8]) -> Result<(), Error> {
const SHIFT_RANGE_4BPS: i32 = 12;
const SHIFT_RANGE_8BPS: i32 = 8;
let channels = [&mut self.left, &mut self.right];
match self.sample_depth {
SampleDepth::Normal => {
let (modulo, offset) = if self.orality == Orality::Stereo {(2, &STEREO_4BIT)} else {(1, &MONO_4BIT)};
let (first_offset, second_offset) = offset;
for (offset_idx, offset_set) in [first_offset, second_offset].iter().enumerate() {
for (idx, offset) in offset_set.iter().enumerate() {
let byte = Self::encode(channels[idx%modulo], &samples[offset.sample..], sample_limit + offset.sample_limit, offset.pitch, &mut data[offset.data..], offset.data_shift, offset.data_pitch, Self::XA_ADPCM_FILTER_COUNT, SHIFT_RANGE_4BPS)?;
data[idx + (offset_idx*8)] = byte;
data[idx + 4 + (offset_idx*8)] = byte;
}
}
},
SampleDepth::High => {
let (modulo, offset_set) = if self.orality == Orality::Stereo {(2, &STEREO_8BIT)} else {(1, &MONO_8BIT)};
for (idx, offset) in offset_set.iter().enumerate() {
let byte = Self::encode(channels[idx%modulo], &samples[offset.sample..], sample_limit + offset.sample_limit, offset.pitch, &mut data[offset.data..], offset.data_shift, offset.data_pitch, Self::XA_ADPCM_FILTER_COUNT, SHIFT_RANGE_8BPS)?;
data[idx] = byte;
data[idx + 4] = byte;
}
}
}
Ok(())
}
fn encode(channel_state: &mut ChannelState, samples: &[i16], sample_limit: i32, pitch: i32, data: &mut [u8], data_shift: i32, data_pitch: i32, filter_count: i32, shift_range: i32) -> Result<u8, Error> {
let mut best_mse = 1i64 << 50i64;
let mut best_filer = 0;
let mut best_sample_shift = 0;
for filter in 0..filter_count {
let true_min_shift = Self::find_min_shift(channel_state, samples, sample_limit, pitch, filter, shift_range)?;
// Testing has shown that the optimal shift can be off the true minimum shift
// by 1 in *either* direction.
// This is NOT the case when dither is used.
let min_shift = if true_min_shift - 1 < 0 {0} else {true_min_shift - 1};
let max_shift = if true_min_shift + 1 > shift_range {shift_range} else {true_min_shift + 1};
for sample_shift in min_shift..=max_shift {
let mut proposed = channel_state.clone();
Self::attempt_encode(&mut proposed, samples, sample_limit, pitch, data, data_shift, data_pitch, filter, sample_shift, shift_range)?;
if best_mse > proposed.mse {
best_mse = proposed.mse;
best_filer = filter;
best_sample_shift = sample_shift;
}
}
}
Self::attempt_encode(channel_state, samples, sample_limit, pitch, data, data_shift, data_pitch, best_filer, best_sample_shift, shift_range)
}
fn attempt_encode(out_channel_state: &mut ChannelState, samples: &[i16], sample_limit: i32, pitch: i32, data: &mut [u8], data_shift: i32, data_pitch: i32, filter: i32, sample_shift: i32, shift_range: i32) -> Result<u8, Error> {
let sample_mask = (0xFFFF >> shift_range) as u8;
let nondata_mask = (!(sample_mask << data_shift)) as u8;
let min_shift = sample_shift;
let k1 = Self::FILTER_K1[filter as usize] as i32;
let k2 = Self::FILTER_K2[filter as usize] as i32;
let hdr = ((min_shift & 0x0F) | ((filter as i32) << 4)) as u8;
out_channel_state.mse = 0;
for i in 0..28 {
let sample = (if i >= sample_limit {0} else {samples[(i*pitch) as usize] as i32}) + out_channel_state.qerr;
let previous_value = (k1*out_channel_state.prev1 + k2*out_channel_state.prev2 + (1 << 5)) >> 6;
let mut sample_enc = sample - previous_value;
sample_enc <<= min_shift;
sample_enc += 1 << (shift_range - 1);
sample_enc >>= shift_range;
if sample_enc < (std::i16::MIN as i32 >> shift_range) {sample_enc = std::i16::MIN as i32 >> shift_range}
if sample_enc > (std::i16::MAX as i32 >> shift_range) {sample_enc = std::i16::MAX as i32 >> shift_range}
sample_enc &= sample_mask as i32;
let mut sample_dec = (((sample_enc & sample_mask as i32) << shift_range) as i16) as i32;
sample_dec >>= min_shift;
sample_dec += previous_value;
if sample_dec > std::i16::MAX as i32 {sample_dec = std::i16::MAX as i32}
if sample_dec < std::i16::MIN as i32 {sample_dec = std::i16::MIN as i32}
let sample_error = sample_dec - sample;
if sample_error >= (1 << 30) || sample_error <= -(1 << 30) {
return Err(Error::from_text(format!("Sample error exceeds 30bit: {}", sample_error)));
}
data[(i*data_pitch) as usize] = ((data[(i*data_pitch) as usize] & nondata_mask) as i32 | (sample_enc << data_shift)) as u8;
out_channel_state.mse += (sample_error as u64*sample_error as u64) as i64;
out_channel_state.prev2 = out_channel_state.prev1;
out_channel_state.prev1 = sample_dec;
}
Ok(hdr)
}
fn find_min_shift(channel_state: &ChannelState, samples: &[i16], sample_limit: i32, pitch: i32, filter: i32, shift_range: i32) -> Result<i32, Error> {
/*
Assumption made:
There is value in shifting right one step further to allow the nibbles to clip.
However, given a possible shift value, there is no value in shifting one step less.
Having said that, this is not a completely accurate model of the encoder,
so maybe we will need to shift one step less.
*/
let mut prev1 = channel_state.prev1;
let mut prev2 = channel_state.prev2;
let k1 = Self::FILTER_K1[filter as usize] as i32;
let k2 = Self::FILTER_K2[filter as usize] as i32;
let mut right_shift = 0;
let mut s_min = 0;
let mut s_max = 0;
for i in 0..28 {
let raw_sample = if i >= sample_limit {0} else {samples[(i*pitch) as usize]} as i32;
let prev_values = (k1*prev1 + k2*prev2 + (1 << 5)) >> 6;
let sample = raw_sample - prev_values;
if sample < s_min {
s_min = sample;
}
if sample > s_max {
s_max = sample;
}
prev2 = prev1;
prev1 = raw_sample;
}
while right_shift < shift_range && (s_max >> right_shift) > (std::i16::MAX as i32 >> shift_range) {
right_shift += 1;
}
while right_shift < shift_range && (s_min >> right_shift) < (std::i16::MIN as i32 >> shift_range) {
right_shift += 1;
}
let min_shift = shift_range - right_shift;
if 0 <= min_shift && min_shift <= shift_range {
Ok(min_shift)
}
else {
Err(Error::from_text(format!("0 <= {} && {} <= {} was not satisfied with min_shift: {}", min_shift, min_shift, shift_range, min_shift)))
}
}
fn samples_per_block_and_limit(input: &[i16], sample_depth: SampleDepth, orality: Orality) -> (i32, i32) {
let samples_per_block = match sample_depth {
SampleDepth::Normal => 224,
SampleDepth::High => 112,
};
let sample_limit = match orality {
Orality::Stereo => input.len()*2,
Orality::Mono => input.len(),
};
(samples_per_block, sample_limit as i32)
}
}
#[derive(Clone)]
struct ChannelState {
qerr: i32, // quanitisation error
mse: i64, // mean square error
prev1: i32,
prev2: i32
}
impl std::default::Default for ChannelState {
fn default() -> Self {
ChannelState{qerr: 0, mse: 0, prev1: 0, prev2: 0}
}
}
struct EncodingOffsets {
sample: usize,
sample_limit: i32,
pitch: i32,
data: usize,
data_shift: i32,
data_pitch: i32,
}
const STEREO_4BIT: ([EncodingOffsets; 4], [EncodingOffsets; 4]) = (
[
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 2, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 1, sample_limit: 0, pitch: 2, data: 0x10, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 56, sample_limit: -28, pitch: 2, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56 + 1, sample_limit: -28, pitch: 2, data: 0x11, data_shift: 4, data_pitch: 4},
],
[
EncodingOffsets{sample: 56*2, sample_limit: -28*2, pitch: 2, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56*2 + 1, sample_limit: -28*2, pitch: 2, data: 0x12, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 56*3, sample_limit: -28*3, pitch: 2, data: 0x13, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56*3 + 1, sample_limit: -28*3, pitch: 2, data: 0x13, data_shift: 4, data_pitch: 4}
]
);
const MONO_4BIT: ([EncodingOffsets; 4], [EncodingOffsets; 4]) = (
[
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 1, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28, sample_limit: -28, pitch: 1, data: 0x10, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 28*2, sample_limit: -28*2, pitch: 1, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*3, sample_limit: -28*3, pitch: 1, data: 0x11, data_shift: 4, data_pitch: 4},
],
[
EncodingOffsets{sample: 28*4, sample_limit: -28*4, pitch: 1, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*5, sample_limit: -28*5, pitch: 1, data: 0x12, data_shift: 4, data_pitch: 4},
EncodingOffsets{sample: 28*6, sample_limit: -28*6, pitch: 1, data: 0x13, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*7, sample_limit: -28*7, pitch: 1, data: 0x13, data_shift: 4, data_pitch: 4}
]
);
const STEREO_8BIT: [EncodingOffsets;4] = [
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 2, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 1, sample_limit: 0, pitch: 2, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56, sample_limit: -28, pitch: 2, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 56 + 1, sample_limit: -28, pitch: 2, data: 0x13, data_shift: 0, data_pitch: 4},
];
const MONO_8BIT: [EncodingOffsets;4] = [
EncodingOffsets{sample: 0, sample_limit: 0, pitch: 1, data: 0x10, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28, sample_limit: -28, pitch: 1, data: 0x11, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*2, sample_limit: -28*2, pitch: 1, data: 0x12, data_shift: 0, data_pitch: 4},
EncodingOffsets{sample: 28*3, sample_limit: -28*3, pitch: 1, data: 0x13, data_shift: 0, data_pitch: 4},
];

View File

@@ -1,69 +1,69 @@
use clap::{Args, ValueEnum};
use std::str::FromStr;
#[derive(Args)]
pub struct Arguments {
#[clap(value_enum, value_parser)]
pub color_depth: ColorType,
#[clap(value_enum, value_parser, default_value_t=ClutAlignment::None)]
pub clut_align: ClutAlignment,
#[clap(long="semi-trans", default_value_t=false)]
pub semi_transparent: bool,
#[clap(long="color-trans", default_value_t=false)]
pub transparent_palette: bool
}
#[derive(Clone)]
pub struct Point {
pub x: u16,
pub y: u16,
}
impl Point {
pub const POINT_VALUE_NAME:&'static str = "{x,y}";
}
impl std::default::Default for Point {
fn default() -> Self {
Point{x: 0, y: 0}
}
}
impl ToString for Point {
fn to_string(&self) -> std::string::String {
"{0,0}".to_owned()
}
}
impl FromStr for Point {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let values:Vec<&str> = s.split(&['{', ',', '}']).filter_map(|value| if value.is_empty() {None} else {Some(value)}).collect();
if values.len() != 2 {
return Err(format!("Two values expected for Point but found {}", values.len()));
}
let x = values[0].parse().map_err(|e| format!("Failed converting 'x' for Point: {e}"))?;
let y = values[1].parse().map_err(|e| format!("Failed converting 'y' for Point: {e}"))?;
Ok(Point{x, y})
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum ColorType{
Clut4,
Clut8,
Full16,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum ClutAlignment {
None,
Linear,
Block
use clap::{Args, ValueEnum};
use std::str::FromStr;
#[derive(Args)]
pub struct Arguments {
#[clap(value_enum, value_parser)]
pub color_depth: ColorType,
#[clap(value_enum, value_parser, default_value_t=ClutAlignment::None)]
pub clut_align: ClutAlignment,
#[clap(long="semi-trans", default_value_t=false)]
pub semi_transparent: bool,
#[clap(long="color-trans", default_value_t=false)]
pub transparent_palette: bool
}
#[derive(Clone)]
pub struct Point {
pub x: u16,
pub y: u16,
}
impl Point {
pub const POINT_VALUE_NAME:&'static str = "{x,y}";
}
impl std::default::Default for Point {
fn default() -> Self {
Point{x: 0, y: 0}
}
}
impl ToString for Point {
fn to_string(&self) -> std::string::String {
"{0,0}".to_owned()
}
}
impl FromStr for Point {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let values:Vec<&str> = s.split(&['{', ',', '}']).filter_map(|value| if value.is_empty() {None} else {Some(value)}).collect();
if values.len() != 2 {
return Err(format!("Two values expected for Point but found {}", values.len()));
}
let x = values[0].parse().map_err(|e| format!("Failed converting 'x' for Point: {e}"))?;
let y = values[1].parse().map_err(|e| format!("Failed converting 'y' for Point: {e}"))?;
Ok(Point{x, y})
}
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum ColorType{
Clut4,
Clut8,
Full16,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub enum ClutAlignment {
None,
Linear,
Block
}

View File

@@ -1,150 +1,150 @@
pub mod args;
pub mod color_clut;
pub mod color_full16;
pub mod reduced_tim;
pub mod tim;
pub mod types;
use args::{ColorType, ClutAlignment, Point};
use color_clut::{IndexedImage, OutputType};
use color_full16::{RgbaImage, RgbImage};
use types::{Color as PSXColor, HeaderEncoder, PSXImageConverter, Rect};
use image::{DynamicImage, io::Reader as ImageReader};
use std::io::{Cursor, Write};
use tool_helper::{Error, Input};
fn modify_palette(mut image: IndexedImage, clut_align: ClutAlignment, semi_transparent: bool, transparent_palette: bool) -> IndexedImage {
if semi_transparent {
for color in image.palette.iter_mut() {
*color = PSXColor::semi_transparent(color.get_red(), color.get_green(), color.get_blue());
}
}
if transparent_palette {
if clut_align == ClutAlignment::Block {
for color in image.palette.iter_mut().step_by(16) {
*color = PSXColor::transparent();
}
}
else {
if let Some(first_color) = image.palette.get_mut(0) {
*first_color = PSXColor::transparent();
}
}
}
image
}
fn encode<T: PSXImageConverter>(header_conv: &mut dyn HeaderEncoder, image: T, tex_pos: Point, clut_pos: Point, color_depth: ColorType, clut_align: ClutAlignment, output: &mut dyn Write) -> Result<(), Error> {
let (width, height) = {
fn return_error(clut_type: u32, div: u32, width: u16, height: u16) -> Result<(u16, u16), Error> {
return Err(Error::from_callback(|| {format!("CLUT {} images require a width divideable by {} (found width: {}/{}={}, height: {})", clut_type, div, width, div, (width as f32/div as f32), height)}));
}
let width = image.width();
let height = image.height();
match color_depth {
ColorType::Clut4 => {
if width & 3 == 0 {
Ok((width/4, height))
}
else {
return_error(4, 4, width, height)
}
},
ColorType::Clut8 => {
if width & 1 == 0 {
Ok((width/2, height))
}
else {
return_error(8, 2, width, height)
}
},
ColorType::Full16 => {
Ok((width, height))
}
}
}?;
let palette = image.get_palette();
let (pal_width, pal_height) = {
if let Some(palette) = &palette {
let pal_length_adjusted = {
let pal_length = palette.len();
if pal_length <= 16 {
16u16
}
else {
256u16
}
};
match clut_align {
ClutAlignment::None |
ClutAlignment::Linear => (pal_length_adjusted, 1u16),
ClutAlignment::Block => (16u16, pal_length_adjusted/16u16),
}
}
else {
(0u16, 0u16)
}
};
header_conv.encode_settings(color_depth, Rect::new(tex_pos.x, tex_pos.y, width, height), Rect::new(clut_pos.x, clut_pos.y, pal_width, pal_height))?;
header_conv.write_header(output)?;
header_conv.write_clut_header(output)?;
if let Some(palette) = palette {
let mut color_count = pal_width*pal_height;
for color in palette {
tool_helper::raw::write_raw(output, color)?;
color_count -= 1;
}
while color_count > 0 {
tool_helper::raw::write_raw(output, &PSXColor::black())?;
color_count -= 1;
}
}
header_conv.write_pixel_header(output)?;
for color in image {
tool_helper::raw::write_raw(output, &color)?;
}
Ok(())
}
fn convert_full16(header_conv: &mut dyn HeaderEncoder, input: Input, output: &mut dyn Write, tex_pos: Point) -> Result<(), Error> {
match ImageReader::new(Cursor::new(tool_helper::input_to_vec(input)?)).with_guessed_format()?.decode() {
Ok(image) => {
match image {
DynamicImage::ImageRgb8(image) => encode(header_conv, RgbImage::new(image), tex_pos, Point::default(), ColorType::Full16, ClutAlignment::None, output),
DynamicImage::ImageRgba8(image) => encode(header_conv, RgbaImage::new(image), tex_pos, Point::default(), ColorType::Full16, ClutAlignment::None, output),
_ => Err(Error::from_str("Only RGB and RGBA images are supported for 16bit encoding"))
}
},
Err(error) => Err(Error::from_error(error))
}
}
fn convert_palette_based(header_conv: &mut dyn HeaderEncoder, input: Input, output: &mut dyn Write, tex_pos: Point, clut_pos: Point, color_type: ColorType, clut_align: ClutAlignment, semi_transparent: bool, transparent_palette: bool) -> Result<(), Error> {
match png::Decoder::new(input).read_info() {
Ok(reader) => {
let output_type = {
match color_type {
ColorType::Clut4 => OutputType::FourBit,
ColorType::Clut8 => OutputType::EightBit,
_ => return Err(Error::from_str("ColorType not supported"))
}
};
encode(header_conv, modify_palette(IndexedImage::new(reader, output_type)?, clut_align, semi_transparent, transparent_palette), tex_pos, clut_pos, color_type, clut_align, output)
},
Err(error) => Err(Error::from_error(error))
}
pub mod args;
pub mod color_clut;
pub mod color_full16;
pub mod reduced_tim;
pub mod tim;
pub mod types;
use args::{ColorType, ClutAlignment, Point};
use color_clut::{IndexedImage, OutputType};
use color_full16::{RgbaImage, RgbImage};
use types::{Color as PSXColor, HeaderEncoder, PSXImageConverter, Rect};
use image::{DynamicImage, io::Reader as ImageReader};
use std::io::{Cursor, Write};
use tool_helper::{Error, Input};
fn modify_palette(mut image: IndexedImage, clut_align: ClutAlignment, semi_transparent: bool, transparent_palette: bool) -> IndexedImage {
if semi_transparent {
for color in image.palette.iter_mut() {
*color = PSXColor::semi_transparent(color.get_red(), color.get_green(), color.get_blue());
}
}
if transparent_palette {
if clut_align == ClutAlignment::Block {
for color in image.palette.iter_mut().step_by(16) {
*color = PSXColor::transparent();
}
}
else {
if let Some(first_color) = image.palette.get_mut(0) {
*first_color = PSXColor::transparent();
}
}
}
image
}
fn encode<T: PSXImageConverter>(header_conv: &mut dyn HeaderEncoder, image: T, tex_pos: Point, clut_pos: Point, color_depth: ColorType, clut_align: ClutAlignment, output: &mut dyn Write) -> Result<(), Error> {
let (width, height) = {
fn return_error(clut_type: u32, div: u32, width: u16, height: u16) -> Result<(u16, u16), Error> {
return Err(Error::from_callback(|| {format!("CLUT {} images require a width divideable by {} (found width: {}/{}={}, height: {})", clut_type, div, width, div, (width as f32/div as f32), height)}));
}
let width = image.width();
let height = image.height();
match color_depth {
ColorType::Clut4 => {
if width & 3 == 0 {
Ok((width/4, height))
}
else {
return_error(4, 4, width, height)
}
},
ColorType::Clut8 => {
if width & 1 == 0 {
Ok((width/2, height))
}
else {
return_error(8, 2, width, height)
}
},
ColorType::Full16 => {
Ok((width, height))
}
}
}?;
let palette = image.get_palette();
let (pal_width, pal_height) = {
if let Some(palette) = &palette {
let pal_length_adjusted = {
let pal_length = palette.len();
if pal_length <= 16 {
16u16
}
else {
256u16
}
};
match clut_align {
ClutAlignment::None |
ClutAlignment::Linear => (pal_length_adjusted, 1u16),
ClutAlignment::Block => (16u16, pal_length_adjusted/16u16),
}
}
else {
(0u16, 0u16)
}
};
header_conv.encode_settings(color_depth, Rect::new(tex_pos.x, tex_pos.y, width, height), Rect::new(clut_pos.x, clut_pos.y, pal_width, pal_height))?;
header_conv.write_header(output)?;
header_conv.write_clut_header(output)?;
if let Some(palette) = palette {
let mut color_count = pal_width*pal_height;
for color in palette {
tool_helper::raw::write_raw(output, color)?;
color_count -= 1;
}
while color_count > 0 {
tool_helper::raw::write_raw(output, &PSXColor::black())?;
color_count -= 1;
}
}
header_conv.write_pixel_header(output)?;
for color in image {
tool_helper::raw::write_raw(output, &color)?;
}
Ok(())
}
fn convert_full16(header_conv: &mut dyn HeaderEncoder, input: Input, output: &mut dyn Write, tex_pos: Point) -> Result<(), Error> {
match ImageReader::new(Cursor::new(tool_helper::input_to_vec(input)?)).with_guessed_format()?.decode() {
Ok(image) => {
match image {
DynamicImage::ImageRgb8(image) => encode(header_conv, RgbImage::new(image), tex_pos, Point::default(), ColorType::Full16, ClutAlignment::None, output),
DynamicImage::ImageRgba8(image) => encode(header_conv, RgbaImage::new(image), tex_pos, Point::default(), ColorType::Full16, ClutAlignment::None, output),
_ => Err(Error::from_str("Only RGB and RGBA images are supported for 16bit encoding"))
}
},
Err(error) => Err(Error::from_error(error))
}
}
fn convert_palette_based(header_conv: &mut dyn HeaderEncoder, input: Input, output: &mut dyn Write, tex_pos: Point, clut_pos: Point, color_type: ColorType, clut_align: ClutAlignment, semi_transparent: bool, transparent_palette: bool) -> Result<(), Error> {
match png::Decoder::new(input).read_info() {
Ok(reader) => {
let output_type = {
match color_type {
ColorType::Clut4 => OutputType::FourBit,
ColorType::Clut8 => OutputType::EightBit,
_ => return Err(Error::from_str("ColorType not supported"))
}
};
encode(header_conv, modify_palette(IndexedImage::new(reader, output_type)?, clut_align, semi_transparent, transparent_palette), tex_pos, clut_pos, color_type, clut_align, output)
},
Err(error) => Err(Error::from_error(error))
}
}

View File

@@ -1,16 +1,16 @@
pub mod types;
use super::args::{ColorType, Point};
use std::io::Write;
use types::Header;
use tool_helper::{Error, Input};
pub type Arguments = super::args::Arguments;
pub fn convert(args: Arguments, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let mut header_conv = Header::default();
match args.color_depth {
ColorType::Full16 => super::convert_full16(&mut header_conv, input, output, Point::default()),
_ => super::convert_palette_based(&mut header_conv, input, output, Point::default(), Point::default(), args.color_depth, args.clut_align, args.semi_transparent, args.transparent_palette),
}
pub mod types;
use super::args::{ColorType, Point};
use std::io::Write;
use types::Header;
use tool_helper::{Error, Input};
pub type Arguments = super::args::Arguments;
pub fn convert(args: Arguments, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let mut header_conv = Header::default();
match args.color_depth {
ColorType::Full16 => super::convert_full16(&mut header_conv, input, output, Point::default()),
_ => super::convert_palette_based(&mut header_conv, input, output, Point::default(), Point::default(), args.color_depth, args.clut_align, args.semi_transparent, args.transparent_palette),
}
}

View File

@@ -1,63 +1,63 @@
use super::super::{args::ColorType, types::{HeaderEncoder, set_member_value, Rect}};
use std::io::Write;
use tool_helper::{bits::BitRange, raw::RawConversion, Error};
#[repr(packed(1))]
pub struct Header {
value: u32
}
impl Header {
const TEX_WIDTH_BIT_RANGE: BitRange = BitRange::from_to(0, 8);
const TEX_HEIGHT_BIT_RANGE: BitRange = BitRange::from_to(9, 16);
const CLUT_WIDTH_BIT_RANGE: BitRange = BitRange::from_to(17, 22);
const CLUT_HEIGHT_BIT_RANGE: BitRange = BitRange::from_to(23, 31);
}
impl Default for Header {
fn default() -> Self {
Header{value: 0}
}
}
impl HeaderEncoder for Header {
fn encode_settings(&mut self, _color_type: ColorType, tex_rect: Rect, clut_rect: Rect) -> Result<(), Error> {
let clut_width = clut_rect.width;
let clut_height = clut_rect.height;
let tex_width = tex_rect.width;
let tex_height = tex_rect.height;
if tex_width & 1 == 1 || tex_height & 1 == 1 {
Err(Error::from_text(format!("Image size (width: {}, height: {}) needs to be even", tex_width, tex_height)))
}
else {
let value = set_member_value!(set_member_value!(set_member_value!(set_member_value!(0,
tex_width, 1, u32),
tex_height, 1, u32),
clut_width, 4, u32),
clut_height, 0, u32);
self.value = value;
Ok(())
}
}
fn write_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
Ok(output.write(&self.convert_to_raw())?)
}
fn write_clut_header(&self, _output: &mut dyn Write) -> Result<usize, Error> {
Ok(0)
}
fn write_pixel_header(&self, _output: &mut dyn Write) -> Result<usize, Error> {
Ok(0)
}
}
impl RawConversion<4> for Header {
fn convert_to_raw(&self) -> [u8; 4] {
self.value.to_le_bytes()
}
use super::super::{args::ColorType, types::{HeaderEncoder, set_member_value, Rect}};
use std::io::Write;
use tool_helper::{bits::BitRange, raw::RawConversion, Error};
#[repr(packed(1))]
pub struct Header {
value: u32
}
impl Header {
const TEX_WIDTH_BIT_RANGE: BitRange = BitRange::from_to(0, 8);
const TEX_HEIGHT_BIT_RANGE: BitRange = BitRange::from_to(9, 16);
const CLUT_WIDTH_BIT_RANGE: BitRange = BitRange::from_to(17, 22);
const CLUT_HEIGHT_BIT_RANGE: BitRange = BitRange::from_to(23, 31);
}
impl Default for Header {
fn default() -> Self {
Header{value: 0}
}
}
impl HeaderEncoder for Header {
fn encode_settings(&mut self, _color_type: ColorType, tex_rect: Rect, clut_rect: Rect) -> Result<(), Error> {
let clut_width = clut_rect.width;
let clut_height = clut_rect.height;
let tex_width = tex_rect.width;
let tex_height = tex_rect.height;
if tex_width & 1 == 1 || tex_height & 1 == 1 {
Err(Error::from_text(format!("Image size (width: {}, height: {}) needs to be even", tex_width, tex_height)))
}
else {
let value = set_member_value!(set_member_value!(set_member_value!(set_member_value!(0,
tex_width, 1, u32),
tex_height, 1, u32),
clut_width, 4, u32),
clut_height, 0, u32);
self.value = value;
Ok(())
}
}
fn write_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
Ok(output.write(&self.convert_to_raw())?)
}
fn write_clut_header(&self, _output: &mut dyn Write) -> Result<usize, Error> {
Ok(0)
}
fn write_pixel_header(&self, _output: &mut dyn Write) -> Result<usize, Error> {
Ok(0)
}
}
impl RawConversion<4> for Header {
fn convert_to_raw(&self) -> [u8; 4] {
self.value.to_le_bytes()
}
}

View File

@@ -1,29 +1,29 @@
pub mod types;
use super::args::{ColorType, Point};
use clap::Args;
use std::io::Write;
use types::Header;
use tool_helper::{Error, Input};
#[derive(Args)]
pub struct Arguments {
#[clap(flatten)]
global: super::args::Arguments,
#[clap(long, value_parser, default_value_t, value_name = Point::POINT_VALUE_NAME)]
clut_pos: Point,
#[clap(long, value_parser, default_value_t, value_name = Point::POINT_VALUE_NAME)]
tex_pos: Point,
}
pub fn convert(args: Arguments, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let global_args = args.global;
let mut header_conv = Header::default();
match global_args.color_depth {
ColorType::Full16 => super::convert_full16(&mut header_conv, input, output, args.tex_pos),
_ => super::convert_palette_based(&mut header_conv, input, output, args.tex_pos, args.clut_pos, global_args.color_depth, global_args.clut_align, global_args.semi_transparent, global_args.transparent_palette),
}
pub mod types;
use super::args::{ColorType, Point};
use clap::Args;
use std::io::Write;
use types::Header;
use tool_helper::{Error, Input};
#[derive(Args)]
pub struct Arguments {
#[clap(flatten)]
global: super::args::Arguments,
#[clap(long, value_parser, default_value_t, value_name = Point::POINT_VALUE_NAME)]
clut_pos: Point,
#[clap(long, value_parser, default_value_t, value_name = Point::POINT_VALUE_NAME)]
tex_pos: Point,
}
pub fn convert(args: Arguments, input: Input, output: &mut dyn Write) -> Result<(), Error> {
let global_args = args.global;
let mut header_conv = Header::default();
match global_args.color_depth {
ColorType::Full16 => super::convert_full16(&mut header_conv, input, output, args.tex_pos),
_ => super::convert_palette_based(&mut header_conv, input, output, args.tex_pos, args.clut_pos, global_args.color_depth, global_args.clut_align, global_args.semi_transparent, global_args.transparent_palette),
}
}

View File

@@ -1,100 +1,100 @@
use super::super::{args::ColorType, types::{HeaderEncoder, Rect}};
use std::io::Write;
use tool_helper::{bits::{Bit, BitRange}, raw::RawConversion, Error};
pub struct Header {
flag: u32,
clut_block: DataBlock,
pixel_block: DataBlock,
}
impl Header {
const ID_VALUE: u32 = BitRange::from_to(0, 7).as_value(0x10) as u32;
const ID_VERSION_VALUE:u32 = BitRange::from_to(8, 15).as_value(0x0) as u32;
const FLAG_PMODE_BIT_RANGE: BitRange = BitRange::from_to(0, 2);
const FLAG_CF_BIT: Bit = Bit::at(3);
const ID:u32 = Self::ID_VALUE | Self::ID_VERSION_VALUE;
}
impl Default for Header {
fn default() -> Self {
Header{flag: 0, clut_block: DataBlock::default(), pixel_block: DataBlock::default()}
}
}
impl HeaderEncoder for Header {
fn encode_settings(&mut self, color_type: ColorType, tex_rect: Rect, clut_rect: Rect) -> Result<(), Error> {
self.flag = match color_type {
ColorType::Clut4 => (Self::FLAG_PMODE_BIT_RANGE.as_value(0x0) | Self::FLAG_CF_BIT.as_value(true)) as u32,
ColorType::Clut8 => (Self::FLAG_PMODE_BIT_RANGE.as_value(0x1) | Self::FLAG_CF_BIT.as_value(true)) as u32,
ColorType::Full16 => (Self::FLAG_PMODE_BIT_RANGE.as_value(0x2) | Self::FLAG_CF_BIT.as_value(false)) as u32,
};
self.clut_block = DataBlock::new(clut_rect);
self.pixel_block = DataBlock::new(tex_rect);
Ok(())
}
fn write_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
let bytes = output.write(&Header::ID.to_le_bytes())?;
Ok(bytes + output.write(&self.flag.to_le_bytes())?)
}
fn write_clut_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
if self.clut_block.w > 0 {
Ok(output.write(&self.clut_block.convert_to_raw())?)
}
else {
Ok(0)
}
}
fn write_pixel_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
Ok(output.write(&self.pixel_block.convert_to_raw())?)
}
}
pub struct DataBlock {
bytes: u32,
x: u16,
y: u16,
w: u16,
h: u16,
}
impl DataBlock {
const RAW_HEADER_SIZE: usize = (4*std::mem::size_of::<u16>()) + std::mem::size_of::<u32>();
pub fn new(rect: Rect) -> DataBlock {
let x = rect.x;
let y = rect.y;
let w = rect.width;
let h = rect.height;
let bytes = ((w as usize*h as usize*std::mem::size_of::<u16>()) + Self::RAW_HEADER_SIZE) as u32;
DataBlock{bytes, x, y, w, h}
}
}
impl std::default::Default for DataBlock {
fn default() -> Self {
DataBlock{bytes: 0, x: 0, y: 0, w: 0, h: 0}
}
}
impl RawConversion<{Self::RAW_HEADER_SIZE}> for DataBlock {
fn convert_to_raw(&self) -> [u8; Self::RAW_HEADER_SIZE] {
let mut raw = [0u8; Self::RAW_HEADER_SIZE];
raw[ 0..4].copy_from_slice(&self.bytes.to_le_bytes());
raw[ 4..6].copy_from_slice(&self.x.to_le_bytes());
raw[ 6..8].copy_from_slice(&self.y.to_le_bytes());
raw[ 8..10].copy_from_slice(&self.w.to_le_bytes());
raw[10..12].copy_from_slice(&self.h.to_le_bytes());
raw
}
use super::super::{args::ColorType, types::{HeaderEncoder, Rect}};
use std::io::Write;
use tool_helper::{bits::{Bit, BitRange}, raw::RawConversion, Error};
pub struct Header {
flag: u32,
clut_block: DataBlock,
pixel_block: DataBlock,
}
impl Header {
const ID_VALUE: u32 = BitRange::from_to(0, 7).as_value(0x10) as u32;
const ID_VERSION_VALUE:u32 = BitRange::from_to(8, 15).as_value(0x0) as u32;
const FLAG_PMODE_BIT_RANGE: BitRange = BitRange::from_to(0, 2);
const FLAG_CF_BIT: Bit = Bit::at(3);
const ID:u32 = Self::ID_VALUE | Self::ID_VERSION_VALUE;
}
impl Default for Header {
fn default() -> Self {
Header{flag: 0, clut_block: DataBlock::default(), pixel_block: DataBlock::default()}
}
}
impl HeaderEncoder for Header {
fn encode_settings(&mut self, color_type: ColorType, tex_rect: Rect, clut_rect: Rect) -> Result<(), Error> {
self.flag = match color_type {
ColorType::Clut4 => (Self::FLAG_PMODE_BIT_RANGE.as_value(0x0) | Self::FLAG_CF_BIT.as_value(true)) as u32,
ColorType::Clut8 => (Self::FLAG_PMODE_BIT_RANGE.as_value(0x1) | Self::FLAG_CF_BIT.as_value(true)) as u32,
ColorType::Full16 => (Self::FLAG_PMODE_BIT_RANGE.as_value(0x2) | Self::FLAG_CF_BIT.as_value(false)) as u32,
};
self.clut_block = DataBlock::new(clut_rect);
self.pixel_block = DataBlock::new(tex_rect);
Ok(())
}
fn write_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
let bytes = output.write(&Header::ID.to_le_bytes())?;
Ok(bytes + output.write(&self.flag.to_le_bytes())?)
}
fn write_clut_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
if self.clut_block.w > 0 {
Ok(output.write(&self.clut_block.convert_to_raw())?)
}
else {
Ok(0)
}
}
fn write_pixel_header(&self, output: &mut dyn Write) -> Result<usize, Error> {
Ok(output.write(&self.pixel_block.convert_to_raw())?)
}
}
pub struct DataBlock {
bytes: u32,
x: u16,
y: u16,
w: u16,
h: u16,
}
impl DataBlock {
const RAW_HEADER_SIZE: usize = (4*std::mem::size_of::<u16>()) + std::mem::size_of::<u32>();
pub fn new(rect: Rect) -> DataBlock {
let x = rect.x;
let y = rect.y;
let w = rect.width;
let h = rect.height;
let bytes = ((w as usize*h as usize*std::mem::size_of::<u16>()) + Self::RAW_HEADER_SIZE) as u32;
DataBlock{bytes, x, y, w, h}
}
}
impl std::default::Default for DataBlock {
fn default() -> Self {
DataBlock{bytes: 0, x: 0, y: 0, w: 0, h: 0}
}
}
impl RawConversion<{Self::RAW_HEADER_SIZE}> for DataBlock {
fn convert_to_raw(&self) -> [u8; Self::RAW_HEADER_SIZE] {
let mut raw = [0u8; Self::RAW_HEADER_SIZE];
raw[ 0..4].copy_from_slice(&self.bytes.to_le_bytes());
raw[ 4..6].copy_from_slice(&self.x.to_le_bytes());
raw[ 6..8].copy_from_slice(&self.y.to_le_bytes());
raw[ 8..10].copy_from_slice(&self.w.to_le_bytes());
raw[10..12].copy_from_slice(&self.h.to_le_bytes());
raw
}
}

View File

@@ -1,3 +1,3 @@
pub mod audio;
pub mod images;
pub mod audio;
pub mod images;
pub mod nothing;

View File

@@ -1,7 +1,7 @@
use std::io::Write;
use tool_helper::{Error, Input};
pub fn copy(input: &mut Input, output: &mut dyn Write) -> Result<(), Error> {
std::io::copy(input, output)?;
Ok(())
use std::io::Write;
use tool_helper::{Error, Input};
pub fn copy(input: &mut Input, output: &mut dyn Write) -> Result<(), Error> {
std::io::copy(input, output)?;
Ok(())
}

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = psxreadmap
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = psxreadmap
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,117 +1,117 @@
use clap::Parser;
use crossterm::{event::{self, Event as CEvent, KeyboardEnhancementFlags, KeyEventKind, PushKeyboardEnhancementFlags}, execute, terminal};
use psxreadmap::{ConsoleUI, Event, EventReceiver, Terminal, load_memory_map, UIState};
use std::{io::{self, Stdout}, path::PathBuf, sync::mpsc, thread, time::{Duration, Instant}};
use tool_helper::{Error, exit_with_error, open_output};
use ratatui::backend::CrosstermBackend;
pub const RUN_DUMP_TOOL_IN_WSL:bool = cfg!(windows);
#[derive(Parser)]
#[clap(about = "Opens and scans an ELF file to print extended memory information", long_about = None)]
struct CommandLine {
#[clap(value_parser, help="Input ELF file for scannning")]
input: PathBuf,
#[clap(long="wsl", help="Run \"objdump\" in WSL", default_value_t=RUN_DUMP_TOOL_IN_WSL)]
use_wsl: bool,
#[clap(long="raw", default_value_t=false)]
raw_dump: bool,
#[clap(short='o', help="Output a memory map file with running the tool")]
output: Option<PathBuf>
}
pub fn main() {
match CommandLine::try_parse() {
Ok(cmd_line) => {
match run_main(cmd_line) {
Ok(_) => (),
Err(error) => exit_with_error(error)
}
},
Err(error) => {
println!("{})", error)
}
}
}
fn run_main(cmd: CommandLine) -> Result<(), Error> {
if cmd.raw_dump {
psxreadmap::get_tool_output(cmd.use_wsl, cmd.input, open_output(&cmd.output)?)?;
return Ok(());
}
let memory_map = load_memory_map(cmd.use_wsl, cmd.input)?; dump_memory_map(cmd.output, &memory_map)?;
let rx = start_event_loop();
let terminal = setup_console()?;
let mut console_ui = ConsoleUI::new(rx, terminal);
console_ui.update_data(memory_map)?;
loop {
match console_ui.update()? {
UIState::Alive => (),
UIState::Render => {console_ui.render()?;}
UIState::Terminated => break
}
}
console_ui.close()
}
fn dump_memory_map(output: Option<PathBuf>, memory_map: &readmap::types::MemoryMap) -> Result<(), Error> {
if let Some(output) = output {
let output = tool_helper::open_output(&Some(output))?;
readmap::dump::write(output, &memory_map)?;
}
Ok(())
}
fn start_event_loop() -> EventReceiver {
// Set up a mpsc (multiproducer, single consumer) channel to communicate between the input handler and the rendering loop.
let (tx, rx) = mpsc::channel();
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
tx.send(Event::ForceRender).expect("Send ForceRender command!");
loop {
let timeout = tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).expect("Event poll working") {
if let CEvent::Key(key) = event::read().expect("Can read key input") {
if key.kind == KeyEventKind::Release {
tx.send(Event::Input(key)).expect("Can send KeyInput");
}
}
}
if last_tick.elapsed() >= tick_rate {
if let Ok(_) = tx.send(Event::Tick) {
last_tick = Instant::now();
}
}
}
});
rx
}
fn setup_console() -> Result<Terminal, Error> {
fn open_stdout() -> Result<Stdout, Error> {
let mut stdout = io::stdout();
if cfg!(unix) {
execute!(stdout, PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::REPORT_EVENT_TYPES))?;
}
Ok(stdout)
}
terminal::enable_raw_mode()?;
// Setup Crossterm for the Terminal
let stdout = open_stdout()?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
Ok(terminal)
use clap::Parser;
use crossterm::{event::{self, Event as CEvent, KeyboardEnhancementFlags, KeyEventKind, PushKeyboardEnhancementFlags}, execute, terminal};
use psxreadmap::{ConsoleUI, Event, EventReceiver, Terminal, load_memory_map, UIState};
use std::{io::{self, Stdout}, path::PathBuf, sync::mpsc, thread, time::{Duration, Instant}};
use tool_helper::{Error, exit_with_error, open_output};
use ratatui::backend::CrosstermBackend;
pub const RUN_DUMP_TOOL_IN_WSL:bool = cfg!(windows);
#[derive(Parser)]
#[clap(about = "Opens and scans an ELF file to print extended memory information", long_about = None)]
struct CommandLine {
#[clap(value_parser, help="Input ELF file for scannning")]
input: PathBuf,
#[clap(long="wsl", help="Run \"objdump\" in WSL", default_value_t=RUN_DUMP_TOOL_IN_WSL)]
use_wsl: bool,
#[clap(long="raw", default_value_t=false)]
raw_dump: bool,
#[clap(short='o', help="Output a memory map file with running the tool")]
output: Option<PathBuf>
}
pub fn main() {
match CommandLine::try_parse() {
Ok(cmd_line) => {
match run_main(cmd_line) {
Ok(_) => (),
Err(error) => exit_with_error(error)
}
},
Err(error) => {
println!("{})", error)
}
}
}
fn run_main(cmd: CommandLine) -> Result<(), Error> {
if cmd.raw_dump {
psxreadmap::get_tool_output(cmd.use_wsl, cmd.input, open_output(&cmd.output)?)?;
return Ok(());
}
let memory_map = load_memory_map(cmd.use_wsl, cmd.input)?; dump_memory_map(cmd.output, &memory_map)?;
let rx = start_event_loop();
let terminal = setup_console()?;
let mut console_ui = ConsoleUI::new(rx, terminal);
console_ui.update_data(memory_map)?;
loop {
match console_ui.update()? {
UIState::Alive => (),
UIState::Render => {console_ui.render()?;}
UIState::Terminated => break
}
}
console_ui.close()
}
fn dump_memory_map(output: Option<PathBuf>, memory_map: &readmap::types::MemoryMap) -> Result<(), Error> {
if let Some(output) = output {
let output = tool_helper::open_output(&Some(output))?;
readmap::dump::write(output, &memory_map)?;
}
Ok(())
}
fn start_event_loop() -> EventReceiver {
// Set up a mpsc (multiproducer, single consumer) channel to communicate between the input handler and the rendering loop.
let (tx, rx) = mpsc::channel();
let tick_rate = Duration::from_millis(200);
thread::spawn(move || {
let mut last_tick = Instant::now();
tx.send(Event::ForceRender).expect("Send ForceRender command!");
loop {
let timeout = tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_else(|| Duration::from_secs(0));
if event::poll(timeout).expect("Event poll working") {
if let CEvent::Key(key) = event::read().expect("Can read key input") {
if key.kind == KeyEventKind::Release {
tx.send(Event::Input(key)).expect("Can send KeyInput");
}
}
}
if last_tick.elapsed() >= tick_rate {
if let Ok(_) = tx.send(Event::Tick) {
last_tick = Instant::now();
}
}
}
});
rx
}
fn setup_console() -> Result<Terminal, Error> {
fn open_stdout() -> Result<Stdout, Error> {
let mut stdout = io::stdout();
if cfg!(unix) {
execute!(stdout, PushKeyboardEnhancementFlags(KeyboardEnhancementFlags::REPORT_EVENT_TYPES))?;
}
Ok(stdout)
}
terminal::enable_raw_mode()?;
// Setup Crossterm for the Terminal
let stdout = open_stdout()?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
Ok(terminal)
}

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = tool_helper
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = tool_helper
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)

View File

@@ -1,267 +1,267 @@
use colored::*;
use envmnt::{ExpandOptions, ExpansionType};
use std::{boxed::Box, fs::OpenOptions, io::{BufRead, BufReader, BufWriter, Read, Write}, path::PathBuf, str::FromStr};
pub mod bits;
pub mod compress;
pub mod raw;
pub mod vec_helper;
pub type BufferedInputFile = BufReader<std::fs::File>;
pub type Output = Box<dyn Write>;
pub type Input = Box<dyn BufRead>;
#[macro_export]
macro_rules! format_if_error {
($result:expr, $format_text:literal) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text, error_text=error_text)
})
};
($result:expr, $format_text:literal, $($arg:expr)*) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text, $($arg),*, error_text=error_text)
})
};
}
#[macro_export]
macro_rules! format_if_error_drop_cause {
($result:expr, $format_text:literal) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text)
})
};
($result:expr, $format_text:literal, $($arg:expr)*) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text, $($arg),*)
})
};
}
const DEFAULT_ENV_EXPAND_OPTIONS : ExpandOptions = ExpandOptions{expansion_type: Some(ExpansionType::All), default_to_empty: false};
pub struct Error {
pub exit_code: i32,
pub text: String,
}
impl Error {
const DEFAULT_EXITCODE:i32 = -1;
pub fn from_str(str: &str) -> Error {
Self::from_text(str.to_owned())
}
pub fn from_text(text: String) -> Error {
Error{exit_code: Self::DEFAULT_EXITCODE, text}
}
pub fn from_error<T>(error: T) -> Error where T: std::fmt::Display {
Error::from_text(error.to_string())
}
pub fn from_core_error<T>(error: T) -> Error where T: core::fmt::Display {
Error::from_text(error.to_string())
}
pub fn from_callback<F>(callback: F) -> Error where F: Fn() -> String {
Error::from_text(callback())
}
pub fn not_implemented(function: &str) -> Error {
Error::from_text(format!("{} not implemented yet", function))
}
pub fn try_or_new<T, S>(result: std::result::Result<T, S>) -> Result<T, Error> where S: std::fmt::Display {
match result {
Ok(value) => Ok(value),
Err(error) => Err(Error{exit_code: Self::DEFAULT_EXITCODE, text: error.to_string()}),
}
}
pub fn ok_or_new<T, F>(option: Option<T>, error_text: F) -> Result<T, Error> where F: Fn () -> String{
Ok(option.ok_or(Error::from_callback(error_text))?)
}
pub fn print_generic_to_std_err<T: std::fmt::Display>(object: &T) {
eprintln!("{}", format!("ERROR: {}", object).red());
}
pub fn print_to_std_err(&self) {
Self::print_generic_to_std_err(self)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text)
}
}
impl std::convert::From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Error{exit_code: -1, text: error.to_string()}
}
}
impl std::convert::From<cdtypes::Error> for Error {
fn from(error: cdtypes::Error) -> Self {
Error::from_error(error)
}
}
impl std::convert::From<std::convert::Infallible> for Error {
fn from(error: std::convert::Infallible) -> Self {
Error::from_error(error)
}
}
impl std::convert::From<std::sync::mpsc::RecvError> for Error {
fn from(error: std::sync::mpsc::RecvError) -> Self {
Error::from_error(error)
}
}
impl std::convert::From<hound::Error> for Error {
fn from(error: hound::Error) -> Self {
Error::from_error(error)
}
}
pub fn exit_with_error(error: Error) {
error.print_to_std_err();
std::process::exit(error.exit_code);
}
pub fn print_warning(str: String) {
eprintln!("{}", str.yellow());
}
pub fn prefix_if_error<T>(prefix: &str, result: Result<T, Error>) -> Result<T, Error> {
match result {
Ok(value) => Ok(value),
Err(mut error) => {
let mut new_text = String::from(prefix);
new_text.push_str(error.text.as_str());
error.text = new_text;
Err(error)
},
}
}
pub fn callback_if_error<F: Fn(String) -> String, T>(result: Result<T, Error>, callback: F) -> Result<T, Error> {
match result {
Ok(value) => Ok(value),
Err(mut error) => {
error.text = callback(error.text);
Err(error)
}
}
}
pub fn callback_if_any_error<F: Fn(String) -> String, T, E: std::string::ToString>(result: Result<T, E>, callback: F) -> Result<T, Error> {
match result {
Ok(value) => Ok(value),
Err(error) => {
Err(Error::from_text(callback(error.to_string())))
}
}
}
pub fn string_with_env_from(str: &str) -> String {
String::from(envmnt::expand(str, Some(DEFAULT_ENV_EXPAND_OPTIONS)))
}
pub fn path_with_env_from(path: &str) -> PathBuf {
PathBuf::from(envmnt::expand(path, Some(DEFAULT_ENV_EXPAND_OPTIONS)))
}
pub fn open_output_file(output_path: &PathBuf) -> Result<BufWriter<std::fs::File>, Error> {
Ok(std::io::BufWriter::new(std::fs::File::create(win_or_wsl_path(output_path)?)?))
}
pub fn open_output(output_file: &Option<PathBuf>) -> Result<Output, Error> {
match output_file {
Some(output_path) => Ok(Box::new(open_output_file(&output_path)?)),
None => Ok(Box::new(BufWriter::new(std::io::stdout()))),
}
}
pub fn open_input(input_file: &Option<PathBuf>) -> Result<Input, Error> {
match input_file {
Some(input_path) => Ok(Box::new(open_input_file_buffered(&input_path)?)),
None => Ok(Box::new(BufReader::new(std::io::stdin()))),
}
}
pub fn open_input_file_buffered(input_path: &PathBuf) -> Result<BufferedInputFile, Error> {
Ok(BufReader::new(std::fs::File::open(win_or_wsl_path(input_path)?)?))
}
pub fn os_str_to_string(input: &std::ffi::OsStr, name: &str) -> Result<String, Error> {
Ok(Error::ok_or_new(input.to_str(), ||format!("Converting {} to UTF-8 failed", name))?.to_owned())
}
pub fn get_file_name_from_path_buf(input: &PathBuf, name: &str) -> Result<String, Error> {
os_str_to_string(Error::ok_or_new(input.file_name(), ||format!("No {} file name found", name))?, name)
}
pub fn input_to_vec(input: Input) -> Result<Vec<u8>, Error> {
let mut data = Vec::new();
for byte in input.bytes() {
data.push(byte?);
}
Ok(data)
}
pub fn read_file(file_path: &PathBuf) -> Result<Vec<u8>, Error> {
if let Some(ext) = file_path.extension() {
if ext == "subst" {
// File needs substitution!
let file_content = read_file_to_string(file_path)?;
return Ok(string_with_env_from(&file_content).into_bytes());
}
}
match std::fs::read(win_or_wsl_path(file_path)?) {
Ok(data) => Ok(data),
Err(error) => create_file_read_error(file_path, error),
}
}
pub fn read_file_to_string(file_path: &PathBuf) -> Result<String, Error> {
match std::fs::read_to_string(win_or_wsl_path(file_path)?) {
Ok(string) => Ok(string),
Err(error) => create_file_read_error(file_path, error),
}
}
pub fn write_file(file_path: &PathBuf, data: Vec<u8>) -> Result<(), Error> {
let mut file = OpenOptions::new().read(true).write(true).truncate(true).create(true).open(file_path)?;
file.write_all(data.as_slice())?;
Ok(())
}
fn create_file_read_error<T>(file_path: &PathBuf, error: std::io::Error) -> Result<T, Error> {
Err(Error::from_text(format!("Failed reading file {} with error: \"{}\"", file_path.display(), error)))
}
fn win_or_wsl_path(path: &PathBuf) -> Result<PathBuf, Error> {
let path = path.clone();
if path.exists() {
Ok(path)
}
else {
match path.into_os_string().into_string() {
Ok(path) => Ok(PathBuf::from_str(wslpath::force_convert(path).as_str())?),
Err(error) => Err(Error::from_text(format!("Failed converting {:?} to string for win/wsl mapping", error)))
}
}
use colored::*;
use envmnt::{ExpandOptions, ExpansionType};
use std::{boxed::Box, fs::OpenOptions, io::{BufRead, BufReader, BufWriter, Read, Write}, path::PathBuf, str::FromStr};
pub mod bits;
pub mod compress;
pub mod raw;
pub mod vec_helper;
pub type BufferedInputFile = BufReader<std::fs::File>;
pub type Output = Box<dyn Write>;
pub type Input = Box<dyn BufRead>;
#[macro_export]
macro_rules! format_if_error {
($result:expr, $format_text:literal) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text, error_text=error_text)
})
};
($result:expr, $format_text:literal, $($arg:expr)*) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text, $($arg),*, error_text=error_text)
})
};
}
#[macro_export]
macro_rules! format_if_error_drop_cause {
($result:expr, $format_text:literal) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text)
})
};
($result:expr, $format_text:literal, $($arg:expr)*) => {
tool_helper::callback_if_any_error($result, |error_text| {
format!($format_text, $($arg),*)
})
};
}
const DEFAULT_ENV_EXPAND_OPTIONS : ExpandOptions = ExpandOptions{expansion_type: Some(ExpansionType::All), default_to_empty: false};
pub struct Error {
pub exit_code: i32,
pub text: String,
}
impl Error {
const DEFAULT_EXITCODE:i32 = -1;
pub fn from_str(str: &str) -> Error {
Self::from_text(str.to_owned())
}
pub fn from_text(text: String) -> Error {
Error{exit_code: Self::DEFAULT_EXITCODE, text}
}
pub fn from_error<T>(error: T) -> Error where T: std::fmt::Display {
Error::from_text(error.to_string())
}
pub fn from_core_error<T>(error: T) -> Error where T: core::fmt::Display {
Error::from_text(error.to_string())
}
pub fn from_callback<F>(callback: F) -> Error where F: Fn() -> String {
Error::from_text(callback())
}
pub fn not_implemented(function: &str) -> Error {
Error::from_text(format!("{} not implemented yet", function))
}
pub fn try_or_new<T, S>(result: std::result::Result<T, S>) -> Result<T, Error> where S: std::fmt::Display {
match result {
Ok(value) => Ok(value),
Err(error) => Err(Error{exit_code: Self::DEFAULT_EXITCODE, text: error.to_string()}),
}
}
pub fn ok_or_new<T, F>(option: Option<T>, error_text: F) -> Result<T, Error> where F: Fn () -> String{
Ok(option.ok_or(Error::from_callback(error_text))?)
}
pub fn print_generic_to_std_err<T: std::fmt::Display>(object: &T) {
eprintln!("{}", format!("ERROR: {}", object).red());
}
pub fn print_to_std_err(&self) {
Self::print_generic_to_std_err(self)
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.text)
}
}
impl std::convert::From<std::io::Error> for Error {
fn from(error: std::io::Error) -> Self {
Error{exit_code: -1, text: error.to_string()}
}
}
impl std::convert::From<cdtypes::Error> for Error {
fn from(error: cdtypes::Error) -> Self {
Error::from_error(error)
}
}
impl std::convert::From<std::convert::Infallible> for Error {
fn from(error: std::convert::Infallible) -> Self {
Error::from_error(error)
}
}
impl std::convert::From<std::sync::mpsc::RecvError> for Error {
fn from(error: std::sync::mpsc::RecvError) -> Self {
Error::from_error(error)
}
}
impl std::convert::From<hound::Error> for Error {
fn from(error: hound::Error) -> Self {
Error::from_error(error)
}
}
pub fn exit_with_error(error: Error) {
error.print_to_std_err();
std::process::exit(error.exit_code);
}
pub fn print_warning(str: String) {
eprintln!("{}", str.yellow());
}
pub fn prefix_if_error<T>(prefix: &str, result: Result<T, Error>) -> Result<T, Error> {
match result {
Ok(value) => Ok(value),
Err(mut error) => {
let mut new_text = String::from(prefix);
new_text.push_str(error.text.as_str());
error.text = new_text;
Err(error)
},
}
}
pub fn callback_if_error<F: Fn(String) -> String, T>(result: Result<T, Error>, callback: F) -> Result<T, Error> {
match result {
Ok(value) => Ok(value),
Err(mut error) => {
error.text = callback(error.text);
Err(error)
}
}
}
pub fn callback_if_any_error<F: Fn(String) -> String, T, E: std::string::ToString>(result: Result<T, E>, callback: F) -> Result<T, Error> {
match result {
Ok(value) => Ok(value),
Err(error) => {
Err(Error::from_text(callback(error.to_string())))
}
}
}
pub fn string_with_env_from(str: &str) -> String {
String::from(envmnt::expand(str, Some(DEFAULT_ENV_EXPAND_OPTIONS)))
}
pub fn path_with_env_from(path: &str) -> PathBuf {
PathBuf::from(envmnt::expand(path, Some(DEFAULT_ENV_EXPAND_OPTIONS)))
}
pub fn open_output_file(output_path: &PathBuf) -> Result<BufWriter<std::fs::File>, Error> {
Ok(std::io::BufWriter::new(std::fs::File::create(win_or_wsl_path(output_path)?)?))
}
pub fn open_output(output_file: &Option<PathBuf>) -> Result<Output, Error> {
match output_file {
Some(output_path) => Ok(Box::new(open_output_file(&output_path)?)),
None => Ok(Box::new(BufWriter::new(std::io::stdout()))),
}
}
pub fn open_input(input_file: &Option<PathBuf>) -> Result<Input, Error> {
match input_file {
Some(input_path) => Ok(Box::new(open_input_file_buffered(&input_path)?)),
None => Ok(Box::new(BufReader::new(std::io::stdin()))),
}
}
pub fn open_input_file_buffered(input_path: &PathBuf) -> Result<BufferedInputFile, Error> {
Ok(BufReader::new(std::fs::File::open(win_or_wsl_path(input_path)?)?))
}
pub fn os_str_to_string(input: &std::ffi::OsStr, name: &str) -> Result<String, Error> {
Ok(Error::ok_or_new(input.to_str(), ||format!("Converting {} to UTF-8 failed", name))?.to_owned())
}
pub fn get_file_name_from_path_buf(input: &PathBuf, name: &str) -> Result<String, Error> {
os_str_to_string(Error::ok_or_new(input.file_name(), ||format!("No {} file name found", name))?, name)
}
pub fn input_to_vec(input: Input) -> Result<Vec<u8>, Error> {
let mut data = Vec::new();
for byte in input.bytes() {
data.push(byte?);
}
Ok(data)
}
pub fn read_file(file_path: &PathBuf) -> Result<Vec<u8>, Error> {
if let Some(ext) = file_path.extension() {
if ext == "subst" {
// File needs substitution!
let file_content = read_file_to_string(file_path)?;
return Ok(string_with_env_from(&file_content).into_bytes());
}
}
match std::fs::read(win_or_wsl_path(file_path)?) {
Ok(data) => Ok(data),
Err(error) => create_file_read_error(file_path, error),
}
}
pub fn read_file_to_string(file_path: &PathBuf) -> Result<String, Error> {
match std::fs::read_to_string(win_or_wsl_path(file_path)?) {
Ok(string) => Ok(string),
Err(error) => create_file_read_error(file_path, error),
}
}
pub fn write_file(file_path: &PathBuf, data: Vec<u8>) -> Result<(), Error> {
let mut file = OpenOptions::new().read(true).write(true).truncate(true).create(true).open(file_path)?;
file.write_all(data.as_slice())?;
Ok(())
}
fn create_file_read_error<T>(file_path: &PathBuf, error: std::io::Error) -> Result<T, Error> {
Err(Error::from_text(format!("Failed reading file {} with error: \"{}\"", file_path.display(), error)))
}
fn win_or_wsl_path(path: &PathBuf) -> Result<PathBuf, Error> {
let path = path.clone();
if path.exists() {
Ok(path)
}
else {
match path.into_os_string().into_string() {
Ok(path) => Ok(PathBuf::from_str(wslpath::force_convert(path).as_str())?),
Err(error) => Err(Error::from_text(format!("Failed converting {:?} to string for win/wsl mapping", error)))
}
}
}

View File

@@ -1,9 +1,9 @@
use super::Error;
use std::str;
pub fn to_string(output: Vec<u8>) -> Result<String, Error> {
match str::from_utf8(&output) {
Ok(str) => Ok(str.to_owned()),
Err(_) => Err(Error::from_str("Invalid UTF8 sequence"))
}
use super::Error;
use std::str;
pub fn to_string(output: Vec<u8>) -> Result<String, Error> {
match str::from_utf8(&output) {
Ok(str) => Ok(str.to_owned()),
Err(_) => Err(Error::from_str("Invalid UTF8 sequence"))
}
}

View File

@@ -1,13 +1,13 @@
include ../Common.mk
ARTIFACT = wslpath
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
include ../Common.mk
ARTIFACT = wslpath
.PHONY: $(WINDOWS_ARTIFACT) $(UNIX_ARTIFACT)
$(WINDOWS_ARTIFACT):
$(call cargo_windows_default)
$(UNIX_ARTIFACT):
$(call cargo_unix_default)
all-windows: $(WINDOWS_ARTIFACT)
all: $(UNIX_ARTIFACT)