complete CLI for input & output to a different format (MP3 lame is freaky)
This commit is contained in:
parent
2cf1ec385f
commit
e4dddb4aba
6 changed files with 700 additions and 71 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
*.lock
|
*.lock
|
||||||
bmp/out.*
|
bmp/out.*
|
||||||
fonts
|
fonts
|
||||||
|
test.*
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bingus"
|
name = "bingus"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
|
@ -16,7 +16,6 @@ rayon = { version = "1", optional = true }
|
||||||
infer = "0"
|
infer = "0"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
derive-new = "0"
|
derive-new = "0"
|
||||||
strum = { version = "0.27", features = ["derive"] }
|
|
||||||
derive_wrapper = "0.1"
|
derive_wrapper = "0.1"
|
||||||
symphonia = { version = "0.5", features = ["all"], optional = true }
|
symphonia = { version = "0.5", features = ["all"], optional = true }
|
||||||
printpdf = { version = "0.8.2", features = [
|
printpdf = { version = "0.8.2", features = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, convert::Infallible};
|
||||||
|
|
||||||
use super::sample::Sample;
|
use super::sample::Sample;
|
||||||
use cfg_if::cfg_if;
|
use cfg_if::cfg_if;
|
||||||
|
@ -17,6 +17,12 @@ pub struct RawSamples<T>(Vec<T>)
|
||||||
where
|
where
|
||||||
T: Sample;
|
T: Sample;
|
||||||
|
|
||||||
|
impl<T: Sample> RawSamples<T> {
|
||||||
|
pub fn into_inner(self) -> Vec<T> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> IntoDataBytes for RawSamples<T>
|
impl<T> IntoDataBytes for RawSamples<T>
|
||||||
where
|
where
|
||||||
T: Sample + ToBytes,
|
T: Sample + ToBytes,
|
||||||
|
@ -34,7 +40,7 @@ where
|
||||||
T: Sample + FromBytes + ToBytes + Zero,
|
T: Sample + FromBytes + ToBytes + Zero,
|
||||||
<T as FromBytes>::Bytes: Sized + for<'a> TryFrom<&'a [u8]>,
|
<T as FromBytes>::Bytes: Sized + for<'a> TryFrom<&'a [u8]>,
|
||||||
{
|
{
|
||||||
type Error = ();
|
type Error = Infallible;
|
||||||
type Format = ();
|
type Format = ();
|
||||||
fn try_from_data_bytes(
|
fn try_from_data_bytes(
|
||||||
bytes: crate::Bytes,
|
bytes: crate::Bytes,
|
||||||
|
|
|
@ -17,4 +17,7 @@ strum = { version = "*", features = ["derive"] }
|
||||||
derive-new = "*"
|
derive-new = "*"
|
||||||
clap = { version = "4.5.40", features = ["derive"] }
|
clap = { version = "4.5.40", features = ["derive"] }
|
||||||
derive_wrapper = "0.1.7"
|
derive_wrapper = "0.1.7"
|
||||||
# unwrap-infallible = "0.1"
|
unwrap-infallible = "0.1"
|
||||||
|
mp3lame-encoder = { version = "0.2", features = ["std"] }
|
||||||
|
hound = "3.5.1"
|
||||||
|
flacenc = "0.4.0"
|
||||||
|
|
213
bong/src/cli.rs
213
bong/src/cli.rs
|
@ -1,56 +1,131 @@
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, stdin, stdout},
|
convert::Infallible,
|
||||||
ops::Not,
|
fs::File,
|
||||||
|
io::{self, stdin, stdout, Read},
|
||||||
|
path::PathBuf,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use bingus::doc::DocumentType;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use derive_new::new;
|
use derive_new::new;
|
||||||
use derive_wrapper::AsRef;
|
use strum::{AsRefStr, Display, EnumString};
|
||||||
use strum::{AsRefStr, EnumIter, EnumString};
|
|
||||||
|
|
||||||
#[derive(new, Parser)]
|
#[derive(new, Parser)]
|
||||||
#[clap(version, author, about)]
|
#[clap(version, author, about)]
|
||||||
/// CLI from the Bent project
|
/// CLI from the Bent project
|
||||||
pub(super) struct Cli {
|
pub(crate) struct Cli {
|
||||||
/// Input format (leave empty for auto mode)
|
|
||||||
#[arg(short, long)]
|
|
||||||
pub(super) input_format: Option<MatcherType>,
|
|
||||||
/// Input file or standard input ("-").
|
/// Input file or standard input ("-").
|
||||||
pub(super) input: FileOrStd<Stdin>,
|
pub(crate) input: FileOrStd<Stdin>,
|
||||||
// /// List of commands (process or bend to another format)
|
// /// List of commands (process or bend to another format)
|
||||||
// pub(super) commands: Vec<Command>,
|
// pub(crate) commands: Vec<Command>,
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub(crate) output_format: OutputFormat,
|
||||||
/// Onput file or standard output ("-").
|
/// Onput file or standard output ("-").
|
||||||
pub(super) output: FileOrStd<Stdout>,
|
pub(crate) output: FileOrStd<Stdout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(AsRef)]
|
#[derive(Clone, Copy, Parser)]
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
struct File(std::fs::File);
|
pub struct Dimensions {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
impl Clone for File {
|
#[derive(Clone, Copy, Parser)]
|
||||||
fn clone(&self) -> Self {
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
File(
|
pub struct ShivaFormat {
|
||||||
self.0
|
doc_input_format: DocumentType,
|
||||||
.try_clone()
|
doc_output_format: DocumentType,
|
||||||
.expect("can't clone this file's handle :("),
|
}
|
||||||
)
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
pub struct Mp3Format {
|
||||||
|
#[arg(default_value = "44100")]
|
||||||
|
pub sample_rate: u32,
|
||||||
|
#[arg(default_value = "128")]
|
||||||
|
pub bitrate: u16,
|
||||||
|
#[arg(default_value = "5")]
|
||||||
|
pub quality: u8,
|
||||||
|
#[arg(default_value_t)]
|
||||||
|
pub sample_format: Mp3SampleFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, EnumString, Default, Display)]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
pub enum Mp3SampleFormat {
|
||||||
|
U16,
|
||||||
|
I16,
|
||||||
|
I32,
|
||||||
|
#[default]
|
||||||
|
F32,
|
||||||
|
F64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
pub struct WavFormat {
|
||||||
|
#[arg(default_value = "48000")]
|
||||||
|
pub sample_rate: u32,
|
||||||
|
#[arg(default_value_t)]
|
||||||
|
pub sample_format: WavSampleFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, EnumString, Display, Default)]
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
pub enum WavSampleFormat {
|
||||||
|
I8,
|
||||||
|
I16,
|
||||||
|
I32,
|
||||||
|
#[default]
|
||||||
|
F32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Parser)]
|
||||||
|
pub struct FlacFormat {
|
||||||
|
#[arg(default_value = "48000")]
|
||||||
|
pub sample_rate: usize,
|
||||||
|
#[arg(value_parser = flac_bps_value_parser, default_value = "16")]
|
||||||
|
pub bps: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flac_bps_value_parser(input: &str) -> anyhow::Result<usize> {
|
||||||
|
input.parse().context("not a number").and_then(|n| match n {
|
||||||
|
8 | 16 | 24 => Ok(n),
|
||||||
|
_ => Err(anyhow!("should be 8, 16 or 24")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<WavSampleFormat> for hound::SampleFormat {
|
||||||
|
fn from(value: WavSampleFormat) -> Self {
|
||||||
|
match value {
|
||||||
|
WavSampleFormat::F32 => hound::SampleFormat::Float,
|
||||||
|
_ => hound::SampleFormat::Int,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for File {
|
impl From<Dimensions> for bingus::img::Dimensions {
|
||||||
type Err = io::Error;
|
fn from(value: Dimensions) -> Self {
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
Self {
|
||||||
std::fs::File::open(s).map(File)
|
width: value.width,
|
||||||
|
height: value.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShivaFormat> for bingus::doc::ShivaFormat {
|
||||||
|
fn from(value: ShivaFormat) -> Self {
|
||||||
|
Self::new(value.doc_input_format, value.doc_output_format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
struct Stdin;
|
pub(crate) struct Stdin;
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
struct Stdout;
|
pub(crate) struct Stdout;
|
||||||
|
|
||||||
trait New {
|
pub(crate) trait New {
|
||||||
type Real;
|
type Real;
|
||||||
fn new() -> Self::Real;
|
fn new() -> Self::Real;
|
||||||
}
|
}
|
||||||
|
@ -70,61 +145,69 @@ impl New for Stdout {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Clone + Default> FromStr for FileOrStd<S> {
|
impl<S: Clone + Default> FromStr for FileOrStd<S> {
|
||||||
type Err = io::Error;
|
type Err = Infallible;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
s.is_empty()
|
if s == "-" {
|
||||||
.not()
|
Ok(FileOrStd::Std(Default::default()))
|
||||||
.then_some(s.parse().map(FileOrStd::File))
|
} else {
|
||||||
.unwrap_or(Ok(FileOrStd::Std(Default::default())))
|
s.parse().map(FileOrStd::File)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[cfg_attr(debug_assertions, derive(Debug))]
|
#[cfg_attr(debug_assertions, derive(Debug))]
|
||||||
pub(super) enum FileOrStd<S: Clone> {
|
pub(crate) enum FileOrStd<S: Clone> {
|
||||||
File(File),
|
File(PathBuf),
|
||||||
Std(S),
|
Std(S),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) enum OutputFormat {}
|
impl<'a, S> FileOrStd<S>
|
||||||
|
where
|
||||||
|
S: Clone + New,
|
||||||
|
<S as New>::Real: Read + 'a,
|
||||||
|
{
|
||||||
|
pub(crate) fn reader(self) -> Result<Box<dyn Read + 'a>, io::Error> {
|
||||||
|
Ok(match self {
|
||||||
|
FileOrStd::File(path) => Box::new(File::open(path)?),
|
||||||
|
FileOrStd::Std(_) => Box::new(S::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Parser)]
|
||||||
|
pub(crate) enum OutputFormat {
|
||||||
|
Bytes,
|
||||||
|
Pdf,
|
||||||
|
ShivaDocument(ShivaFormat),
|
||||||
|
Font,
|
||||||
|
Mp3(Mp3Format),
|
||||||
|
Wav(WavFormat),
|
||||||
|
Flac(FlacFormat),
|
||||||
|
Text,
|
||||||
|
ImageLuma8(Dimensions),
|
||||||
|
ImageLumaA8(Dimensions),
|
||||||
|
ImageRgb8(Dimensions),
|
||||||
|
ImageRgba8(Dimensions),
|
||||||
|
ImageLuma16(Dimensions),
|
||||||
|
ImageLumaA16(Dimensions),
|
||||||
|
ImageRgb16(Dimensions),
|
||||||
|
ImageRgba16(Dimensions),
|
||||||
|
ImageRgb32F(Dimensions),
|
||||||
|
ImageRgba32F(Dimensions),
|
||||||
|
}
|
||||||
|
|
||||||
// #[derive(Clone)]
|
// #[derive(Clone)]
|
||||||
// pub(super) enum Command {
|
// pub(crate) enum Command {
|
||||||
// Into(Bendable),
|
// Into(Bendable),
|
||||||
// Process(Process),
|
// Process(Process),
|
||||||
// }
|
// }
|
||||||
|
|
||||||
#[derive(EnumString, AsRefStr, Clone)]
|
#[derive(EnumString, AsRefStr, Clone)]
|
||||||
pub(super) enum Bendable {
|
pub(crate) enum Bendable {
|
||||||
#[strum(serialize = "bin", serialize = "binary")]
|
#[strum(serialize = "bin", serialize = "binary")]
|
||||||
Binary,
|
Binary,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
// #[derive(Clone)]
|
||||||
pub(super) enum Process {}
|
// pub(crate) enum Process {}
|
||||||
|
|
||||||
#[derive(EnumString, EnumIter, Clone, Copy)]
|
|
||||||
enum MatcherType {
|
|
||||||
Audio,
|
|
||||||
Doc,
|
|
||||||
Font,
|
|
||||||
Image,
|
|
||||||
Pdf,
|
|
||||||
Text,
|
|
||||||
Raw,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<MatcherType> for bingus::MatcherType {
|
|
||||||
fn from(value: MatcherType) -> Self {
|
|
||||||
use bingus::MatcherType::*;
|
|
||||||
match value {
|
|
||||||
MatcherType::Audio => Audio,
|
|
||||||
MatcherType::Doc => Doc,
|
|
||||||
MatcherType::Font => Font,
|
|
||||||
MatcherType::Image => Image,
|
|
||||||
MatcherType::Pdf => Archive,
|
|
||||||
MatcherType::Text => Text,
|
|
||||||
MatcherType::Raw => Custom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
539
bong/src/main.rs
539
bong/src/main.rs
|
@ -1,9 +1,546 @@
|
||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
fs::File,
|
||||||
|
io::{Cursor, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail, Context};
|
||||||
|
use bingus::{
|
||||||
|
doc::{printpdf::PdfDocument, ShivaDocument},
|
||||||
|
dynamic::{self, DynamicBendable},
|
||||||
|
fnt::Font,
|
||||||
|
img::{
|
||||||
|
GrayAlphaImage, GrayImage, Image, Luma, LumaA, Rgb, Rgb32FImage, RgbImage, Rgba,
|
||||||
|
Rgba32FImage, RgbaImage,
|
||||||
|
},
|
||||||
|
snd::{conv::IntoSample, sample::i24, RawSamples},
|
||||||
|
txt::Text,
|
||||||
|
Bendable, Bytes, FromDataBytes, IntoDataBytes, TryFromDataBytes,
|
||||||
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
|
use flacenc::{component::BitRepr, error::Verify};
|
||||||
|
use hound::{WavSpec, WavWriter};
|
||||||
|
use mp3lame_encoder::{Builder as Mp3EncoderBuilder, DualPcm, FlushGap};
|
||||||
|
use unwrap_infallible::UnwrapInfallible;
|
||||||
|
|
||||||
|
use crate::cli::{FlacFormat, Mp3Format, New, Stdout, WavFormat};
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
Cli::parse();
|
let Cli {
|
||||||
|
input,
|
||||||
|
output_format,
|
||||||
|
output,
|
||||||
|
} = Cli::parse();
|
||||||
|
let input_bytes = {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
input
|
||||||
|
.reader()
|
||||||
|
.and_then(|mut r| r.read_to_end(&mut buf))
|
||||||
|
.context("reading input")?;
|
||||||
|
buf
|
||||||
|
};
|
||||||
|
let mut bytes = match dynamic::open(&mut Cursor::new(input_bytes.clone()))
|
||||||
|
.context("guessing media type / loading media")?
|
||||||
|
.or(String::from_utf8(input_bytes.clone())
|
||||||
|
.ok()
|
||||||
|
.map(Cow::Owned)
|
||||||
|
.map(DynamicBendable::Text))
|
||||||
|
.unwrap_or(DynamicBendable::Binary(input_bytes))
|
||||||
|
{
|
||||||
|
dynamic::DynamicBendable::Image(dynamic_image) => {
|
||||||
|
Bytes::bend_from(dynamic_image, (), Default::default())
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Binary(b) => Bytes::bend_from(b, (), Default::default()),
|
||||||
|
dynamic::DynamicBendable::Sound(audio) => Bytes::bend_from(audio, (), Default::default()),
|
||||||
|
dynamic::DynamicBendable::Text(cow) => Bytes::bend_from(cow, (), Default::default()),
|
||||||
|
dynamic::DynamicBendable::Doc(shiva_document) => {
|
||||||
|
Bytes::bend_from(shiva_document, (), Default::default())
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Archive(pdf_document) => {
|
||||||
|
Bytes::bend_from(pdf_document, (), Default::default())
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Meta => {
|
||||||
|
unimplemented!("this is not supposed to happen...")
|
||||||
|
}
|
||||||
|
dynamic::DynamicBendable::Font(font) => Bytes::bend_from(font, (), Default::default()),
|
||||||
|
}
|
||||||
|
.context("bending input into bytes")?;
|
||||||
|
match output {
|
||||||
|
cli::FileOrStd::File(file) => {
|
||||||
|
match output_format {
|
||||||
|
cli::OutputFormat::Bytes => {
|
||||||
|
File::bend_from(bytes, Box::new(file), Default::default())
|
||||||
|
.context("writing to file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Pdf => {
|
||||||
|
File::bend_from(
|
||||||
|
PdfDocument::bend_from(bytes, (), Default::default())
|
||||||
|
.map_err(|s| anyhow!("{}", s))
|
||||||
|
.context("bending into PDF document")?
|
||||||
|
.into_data_bytes(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing PDF to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ShivaDocument(shiva_format) => {
|
||||||
|
File::bend_from(
|
||||||
|
ShivaDocument::bend_from(bytes, shiva_format.into(), Default::default())
|
||||||
|
.context("bending into a Shiva Document")?,
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing Shiva Document to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Font => {
|
||||||
|
File::bend_from(
|
||||||
|
Font::bend_from(bytes, (), Default::default())
|
||||||
|
.context("bending to font")?
|
||||||
|
.into_data_bytes(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing font to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Mp3(mp3_format) => {
|
||||||
|
eprintln!("warning: Rust MP3 lame encoder is freaky!");
|
||||||
|
let Mp3Format {
|
||||||
|
bitrate,
|
||||||
|
quality,
|
||||||
|
sample_rate,
|
||||||
|
sample_format,
|
||||||
|
} = mp3_format;
|
||||||
|
let buff = {
|
||||||
|
use mp3lame_encoder::{Bitrate::*, Quality::*};
|
||||||
|
let mut encoder = Mp3EncoderBuilder::new()
|
||||||
|
.context("Failed to create MP3 encoder builder")?;
|
||||||
|
encoder
|
||||||
|
.set_num_channels(1)
|
||||||
|
.context("Failed to set MP3 encoder channels")?;
|
||||||
|
encoder
|
||||||
|
.set_sample_rate(sample_rate)
|
||||||
|
.context("Failed to set MP3 encoder sample rate")?;
|
||||||
|
encoder
|
||||||
|
.set_brate(match bitrate {
|
||||||
|
8 => Kbps8,
|
||||||
|
16 => Kbps16,
|
||||||
|
24 => Kbps24,
|
||||||
|
32 => Kbps32,
|
||||||
|
40 => Kbps40,
|
||||||
|
48 => Kbps48,
|
||||||
|
64 => Kbps64,
|
||||||
|
80 => Kbps80,
|
||||||
|
96 => Kbps96,
|
||||||
|
112 => Kbps112,
|
||||||
|
128 => Kbps128,
|
||||||
|
160 => Kbps160,
|
||||||
|
192 => Kbps192,
|
||||||
|
224 => Kbps224,
|
||||||
|
256 => Kbps256,
|
||||||
|
320 => Kbps320,
|
||||||
|
_ => bail!("invalid MP3 bitrate"),
|
||||||
|
})
|
||||||
|
.context("Failed to set MP3 encoder bitrate")?;
|
||||||
|
encoder
|
||||||
|
.set_quality(match quality {
|
||||||
|
0 => Best,
|
||||||
|
1 => SecondBest,
|
||||||
|
2 => NearBest,
|
||||||
|
3 => VeryNice,
|
||||||
|
4 => Nice,
|
||||||
|
5 => Good,
|
||||||
|
6 => Decent,
|
||||||
|
7 => Ok,
|
||||||
|
8 => SecondWorst,
|
||||||
|
9 => Worst,
|
||||||
|
_ => bail!("invalid MP3 quality"),
|
||||||
|
})
|
||||||
|
.context("Failed to set MP3 encoder quality")?;
|
||||||
|
|
||||||
|
let mut encoder = encoder
|
||||||
|
.build()
|
||||||
|
.context("Failed to initialize MP3 encoder")?;
|
||||||
|
|
||||||
|
let (encoded_size, mut output) = {
|
||||||
|
match sample_format {
|
||||||
|
cli::Mp3SampleFormat::U16 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<u16>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::I16 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<i16>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::I32 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<i32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::F32 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<f32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
cli::Mp3SampleFormat::F64 => {
|
||||||
|
let (left, right): (Vec<_>, Vec<_>) =
|
||||||
|
RawSamples::<f64>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap()
|
||||||
|
.into_inner()
|
||||||
|
.chunks_exact(2)
|
||||||
|
.map(|c| (c[0], c[1]))
|
||||||
|
.unzip();
|
||||||
|
let input = DualPcm {
|
||||||
|
left: &left,
|
||||||
|
right: &right,
|
||||||
|
};
|
||||||
|
let mut output = Vec::with_capacity(
|
||||||
|
mp3lame_encoder::max_required_buffer_size(
|
||||||
|
left.len() + right.len(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
(
|
||||||
|
encoder
|
||||||
|
.encode(input, output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 encoding")?,
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
unsafe {
|
||||||
|
output.set_len(output.len().wrapping_add(encoded_size));
|
||||||
|
}
|
||||||
|
let encoded_size =
|
||||||
|
encoder
|
||||||
|
.flush::<FlushGap>(output.spare_capacity_mut())
|
||||||
|
.context("Failed MP3 flushing (don't know what that means)")?;
|
||||||
|
unsafe {
|
||||||
|
output.set_len(output.len().wrapping_add(encoded_size));
|
||||||
|
}
|
||||||
|
output
|
||||||
|
};
|
||||||
|
File::bend_from(buff, Box::new(file), Default::default())
|
||||||
|
.context("writing MP3 to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Wav(wav_format) => {
|
||||||
|
let WavFormat {
|
||||||
|
sample_rate,
|
||||||
|
sample_format,
|
||||||
|
} = wav_format;
|
||||||
|
match sample_format {
|
||||||
|
cli::WavSampleFormat::I8 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<i8>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: i8::BITS as u16,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
cli::WavSampleFormat::I16 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<i16>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: i16::BITS as u16,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
cli::WavSampleFormat::I32 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<i32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: i32::BITS as u16,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
cli::WavSampleFormat::F32 => {
|
||||||
|
let samples =
|
||||||
|
RawSamples::<f32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let mut buff =
|
||||||
|
Cursor::new(Vec::with_capacity(samples.as_ref().len() * 8));
|
||||||
|
{
|
||||||
|
let mut writer = WavWriter::new(
|
||||||
|
&mut buff,
|
||||||
|
WavSpec {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate,
|
||||||
|
bits_per_sample: 32,
|
||||||
|
sample_format: sample_format.into(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.context("Failed to create WAV writer")?;
|
||||||
|
for sample in samples.as_ref() {
|
||||||
|
writer.write_sample(*sample)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
buff.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing WAV data to output file")?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Flac(flac_format) => {
|
||||||
|
let FlacFormat { sample_rate, bps } = flac_format;
|
||||||
|
let config = flacenc::config::Encoder::default()
|
||||||
|
.into_verified()
|
||||||
|
.expect("Config data error.");
|
||||||
|
let samples = RawSamples::<i32>::bend_from(bytes, (), Default::default())
|
||||||
|
.unwrap_infallible();
|
||||||
|
let source = flacenc::source::MemSource::from_samples(
|
||||||
|
samples.as_ref().iter().copied()
|
||||||
|
.map(|sample| match bps {
|
||||||
|
8 => IntoSample::<i8>::into_sample(sample) as i32,
|
||||||
|
16 => IntoSample::<i16>::into_sample(sample) as i32,
|
||||||
|
24 => IntoSample::<i24>::into_sample(sample).inner(),
|
||||||
|
_=> unimplemented!("sorry, the current implementation for the flac encoder doesn't support any other bitrate than 8, 16 or 24.")
|
||||||
|
})
|
||||||
|
.collect::<Vec<i32>>()
|
||||||
|
.as_slice(),
|
||||||
|
1,
|
||||||
|
bps,
|
||||||
|
sample_rate,
|
||||||
|
);
|
||||||
|
let flac_stream =
|
||||||
|
flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
|
||||||
|
.expect("Encode failed.");
|
||||||
|
|
||||||
|
// `Stream` imlpements `BitRepr` so you can obtain the encoded stream via
|
||||||
|
// `ByteSink` struct that implements `BitSink`.
|
||||||
|
let mut sink = flacenc::bitsink::ByteSink::new();
|
||||||
|
flac_stream
|
||||||
|
.write(&mut sink)
|
||||||
|
.context("Failed to write samples to FLAC byte sink")?;
|
||||||
|
File::try_from_data_bytes(
|
||||||
|
sink.into_inner(),
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing FLAC data to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::Text => {
|
||||||
|
File::bend_from(
|
||||||
|
Text::try_from_data_bytes(bytes, (), Default::default())
|
||||||
|
.context("invalid UTF-8")?,
|
||||||
|
Box::new(file),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.context("writing text to output file")?;
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLuma8(dimensions) => {
|
||||||
|
GrayImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLumaA8(dimensions) => {
|
||||||
|
GrayAlphaImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgb8(dimensions) => {
|
||||||
|
RgbImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgba8(dimensions) => {
|
||||||
|
RgbaImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLuma16(dimensions) => {
|
||||||
|
Image::<Luma<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageLumaA16(dimensions) => {
|
||||||
|
Image::<LumaA<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgb16(dimensions) => {
|
||||||
|
Image::<Rgb<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgba16(dimensions) => {
|
||||||
|
Image::<Rgba<u16>, Vec<u16>>::from_data_bytes(
|
||||||
|
bytes,
|
||||||
|
dimensions.into(),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgb32F(dimensions) => {
|
||||||
|
Rgb32FImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
cli::OutputFormat::ImageRgba32F(dimensions) => {
|
||||||
|
Rgba32FImage::from_data_bytes(bytes, dimensions.into(), Default::default())
|
||||||
|
.save(file)
|
||||||
|
.context("writing image data to output file")?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
cli::FileOrStd::Std(_) => Stdout::new()
|
||||||
|
.write_all(&mut bytes)
|
||||||
|
.context("writing to stdout")?,
|
||||||
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue