use std::fs::File;
use std::io::{BufWriter, Seek, SeekFrom, Write};
use std::path::Path;
use cpal::{Error, ErrorKind, I24, U24};
use flacenc::bitsink::ByteSink;
use flacenc::component::{BitRepr, Stream};
use flacenc::config;
use flacenc::error::{Verified, Verify};
use flacenc::source::{Context, Fill, FrameBuf};
use crate::import::{Decoded, Samples};
const MAX_CHANNELS: u16 = 8;
const CHUNK_FRAMES: usize = 4096;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FlacSummary {
pub frames: u64,
pub sample_rate: u32,
pub channels: u16,
pub bits_per_sample: u8,
pub bytes: u64,
}
impl FlacSummary {
pub fn duration(&self) -> f64 {
self.frames as f64 / f64::from(self.sample_rate.max(1))
}
}
pub struct FlacWriter<W: Write + Seek = File> {
label: String,
file: BufWriter<W>,
start: u64,
settle: fn(&mut W) -> std::io::Result<()>,
config: Verified<config::Encoder>,
stream: Stream,
buffer: (FrameBuf, Context),
pending: Vec<i32>,
block_size: usize,
channels: usize,
sample_rate: u32,
bits_per_sample: u8,
header_len: usize,
frame_sizes: Option<(usize, usize)>,
}
impl FlacWriter<File> {
pub fn create(
path: impl AsRef<Path>,
sample_rate: u32,
channels: u16,
bits_per_sample: u8,
) -> Result<Self, Error> {
let path = path.as_ref();
let parts = Parts::new(sample_rate, channels, bits_per_sample)?;
let label = path.display().to_string();
let file = File::create(path).map_err(|error| io_error(&label, "create", error))?;
FlacWriter::assemble(file, label, |file| file.sync_all(), parts)
}
}
impl<W: Write + Seek> FlacWriter<W> {
pub fn new(
writer: W,
sample_rate: u32,
channels: u16,
bits_per_sample: u8,
) -> Result<Self, Error> {
let parts = Parts::new(sample_rate, channels, bits_per_sample)?;
FlacWriter::assemble(writer, "the output".to_string(), |_| Ok(()), parts)
}
fn assemble(
mut writer: W,
label: String,
settle: fn(&mut W) -> std::io::Result<()>,
parts: Parts,
) -> Result<Self, Error> {
let Parts {
config,
stream,
buffer,
header,
block_size,
channels,
sample_rate,
bits_per_sample,
} = parts;
let start = writer
.stream_position()
.map_err(|error| io_error(&label, "find the position in", error))?;
let mut file = BufWriter::new(writer);
file.write_all(&header)
.map_err(|error| io_error(&label, "write", error))?;
Ok(FlacWriter {
label,
file,
start,
settle,
config,
stream,
buffer,
pending: Vec::with_capacity(block_size * channels),
block_size,
channels,
sample_rate,
bits_per_sample,
header_len: header.len(),
frame_sizes: None,
})
}
pub fn write(&mut self, interleaved: &[i32]) -> Result<(), Error> {
if interleaved.len() % self.channels != 0 {
return Err(invalid(format!(
"{} samples is not a whole number of {}-channel frames",
interleaved.len(),
self.channels
)));
}
self.pending.extend_from_slice(interleaved);
let whole_block = self.block_size * self.channels;
while self.pending.len() >= whole_block {
let block: Vec<i32> = self.pending.drain(..whole_block).collect();
self.encode(&block)?;
}
Ok(())
}
pub fn finish(self) -> Result<FlacSummary, Error> {
self.finish_into_inner().map(|(summary, _)| summary)
}
pub fn finish_into_inner(mut self) -> Result<(FlacSummary, W), Error> {
if !self.pending.is_empty() {
let rest = std::mem::take(&mut self.pending);
self.encode(&rest)?;
}
let context = &self.buffer.1;
let frames = context.total_samples() as u64;
let info = self.stream.stream_info_mut();
info.set_block_sizes(self.block_size, self.block_size)
.map_err(|error| encoder_error("the block sizes", error))?;
if let Some((smallest, largest)) = self.frame_sizes {
info.set_frame_sizes(smallest, largest)
.map_err(|error| encoder_error("the frame sizes", error))?;
}
info.set_md5_digest(&context.md5_digest());
info.set_total_samples(context.total_samples());
let header = header_bytes(&self.stream)?;
if header.len() != self.header_len {
return Err(Error::with_message(
ErrorKind::Other,
format!(
"the finished FLAC header is {} bytes where the placeholder was {}",
header.len(),
self.header_len
),
));
}
let label = self.label.clone();
let failed = |error| io_error(&label, "write", error);
self.file.flush().map_err(failed)?;
let file = self.file.get_mut();
let end = file.stream_position().map_err(failed)?;
file.seek(SeekFrom::Start(self.start)).map_err(failed)?;
file.write_all(&header).map_err(failed)?;
file.seek(SeekFrom::Start(end)).map_err(failed)?;
(self.settle)(file).map_err(failed)?;
let summary = FlacSummary {
frames,
sample_rate: self.sample_rate,
channels: self.channels as u16,
bits_per_sample: self.bits_per_sample,
bytes: end - self.start,
};
let writer = self
.file
.into_inner()
.map_err(|error| io_error(&label, "write", error.into_error()))?;
Ok((summary, writer))
}
fn encode(&mut self, interleaved: &[i32]) -> Result<(), Error> {
self.buffer
.fill_interleaved(interleaved)
.map_err(|error| encoder_error("a block of samples", error))?;
let number = self.buffer.1.current_frame_number().unwrap_or(0);
let frame = flacenc::encode_fixed_size_frame(
&self.config,
&self.buffer.0,
number,
self.stream.stream_info(),
)
.map_err(|error| {
invalid(format!(
"encoding FLAC frame {number}: {error} — are the samples within {} bits?",
self.bits_per_sample
))
})?;
let mut sink = ByteSink::new();
frame
.write(&mut sink)
.map_err(|error| encoder_error("a frame", error))?;
let bytes = sink.as_slice();
self.file
.write_all(bytes)
.map_err(|error| io_error(&self.label, "write", error))?;
self.frame_sizes = Some(match self.frame_sizes {
None => (bytes.len(), bytes.len()),
Some((smallest, largest)) => (smallest.min(bytes.len()), largest.max(bytes.len())),
});
Ok(())
}
}
struct Parts {
config: Verified<config::Encoder>,
stream: Stream,
buffer: (FrameBuf, Context),
header: Vec<u8>,
block_size: usize,
channels: usize,
sample_rate: u32,
bits_per_sample: u8,
}
impl Parts {
fn new(sample_rate: u32, channels: u16, bits_per_sample: u8) -> Result<Self, Error> {
if !(1..=MAX_CHANNELS).contains(&channels) {
return Err(invalid(format!(
"FLAC carries 1 to {MAX_CHANNELS} channels, not {channels}"
)));
}
if !(8..=24).contains(&bits_per_sample) {
return Err(invalid(format!(
"the FLAC encoder writes 8 to 24 bits a sample, not {bits_per_sample}"
)));
}
let config = config::Encoder::default();
let block_size = config.block_size;
let config = config
.into_verified()
.map_err(|(_, error)| encoder_error("the encoder configuration", error))?;
let stream = Stream::new(
sample_rate as usize,
usize::from(channels),
usize::from(bits_per_sample),
)
.map_err(|error| invalid(format!("a {sample_rate} Hz FLAC stream: {error}")))?;
let header = header_bytes(&stream)?;
let buffer = (
FrameBuf::with_size(usize::from(channels), block_size)
.map_err(|error| encoder_error("the frame buffer", error))?,
Context::new(usize::from(bits_per_sample), usize::from(channels)),
);
Ok(Parts {
config,
stream,
buffer,
header,
block_size,
channels: usize::from(channels),
sample_rate,
bits_per_sample,
})
}
}
pub fn write_flac(path: impl AsRef<Path>, decoded: &Decoded) -> Result<FlacSummary, Error> {
let bits = bits_for(&decoded.samples);
let writer = FlacWriter::create(path, decoded.sample_rate, decoded.channels, bits)?;
write_decoded(writer, decoded)
}
pub fn write_flac_into(
writer: impl Write + Seek,
decoded: &Decoded,
) -> Result<FlacSummary, Error> {
let bits = bits_for(&decoded.samples);
let writer = FlacWriter::new(writer, decoded.sample_rate, decoded.channels, bits)?;
write_decoded(writer, decoded)
}
fn write_decoded<W: Write + Seek>(
mut writer: FlacWriter<W>,
decoded: &Decoded,
) -> Result<FlacSummary, Error> {
let chunk = CHUNK_FRAMES * usize::from(decoded.channels.max(1));
macro_rules! write_as {
($samples:expr, $convert:expr) => {
for block in $samples.chunks(chunk) {
let block: Vec<i32> = block.iter().map($convert).collect();
writer.write(&block)?;
}
};
}
match &decoded.samples {
Samples::U8(samples) => write_as!(samples, |&sample| i32::from(sample) - 128),
Samples::I8(samples) => write_as!(samples, |&sample| i32::from(sample)),
Samples::U16(samples) => write_as!(samples, |&sample| i32::from(sample) - 32_768),
Samples::I16(samples) => write_as!(samples, |&sample| i32::from(sample)),
Samples::U24(samples) => write_as!(samples, |&sample: &U24| sample.inner() - (1 << 23)),
Samples::I24(samples) => write_as!(samples, |&sample: &I24| sample.inner()),
Samples::U32(samples) => write_as!(samples, |&sample| ((sample ^ 0x8000_0000) as i32) >> 8),
Samples::I32(samples) => write_as!(samples, |&sample| sample >> 8),
Samples::F32(samples) => write_as!(samples, |&sample| float_to_24(f64::from(sample))),
Samples::F64(samples) => write_as!(samples, |&sample| float_to_24(sample)),
}
writer.finish()
}
#[cfg(feature = "import")]
pub fn to_flac(input: impl AsRef<Path>, output: impl AsRef<Path>) -> Result<FlacSummary, Error> {
let output = output.as_ref();
convert(input.as_ref(), |rate, channels, bits| {
FlacWriter::create(output, rate, channels, bits)
})
}
#[cfg(feature = "import")]
pub fn to_flac_into(
input: impl AsRef<Path>,
output: impl Write + Seek,
) -> Result<FlacSummary, Error> {
convert(input.as_ref(), |rate, channels, bits| {
FlacWriter::new(output, rate, channels, bits)
})
}
#[cfg(feature = "import")]
fn convert<W: Write + Seek>(
input: &Path,
open: impl FnOnce(u32, u16, u8) -> Result<FlacWriter<W>, Error>,
) -> Result<FlacSummary, Error> {
use cpal::SampleFormat;
use crate::import;
let encoding = import::find_type(input)?;
let probe = import::stream_as::<f32>(input, encoding)?;
let narrow = matches!(
probe.source_format(),
SampleFormat::U8 | SampleFormat::I8 | SampleFormat::U16 | SampleFormat::I16
);
drop(probe);
if narrow {
let stream = import::stream_as::<i16>(input, encoding)?;
pump(stream, open, 16, i32::from)
} else {
let stream = import::stream_as::<I24>(input, encoding)?;
pump(stream, open, 24, |sample: I24| sample.inner())
}
}
#[cfg(feature = "import")]
fn pump<S: crate::output::SampleType, W: Write + Seek>(
mut stream: Box<dyn crate::import::AudioStream<S>>,
open: impl FnOnce(u32, u16, u8) -> Result<FlacWriter<W>, Error>,
bits: u8,
convert: impl Fn(S) -> i32,
) -> Result<FlacSummary, Error> {
let mut block = vec![S::SILENCE; CHUNK_FRAMES * usize::from(stream.channels().max(1))];
let first = stream.read(&mut block)?;
let (channels, rate) = (stream.channels(), stream.sample_rate());
let mut writer = open(rate, channels, bits)?;
let mut samples = Vec::with_capacity(block.len());
let mut read = first;
loop {
if read == 0 {
break;
}
samples.clear();
samples.extend(block[..read].iter().map(|sample| convert(*sample)));
writer.write(&samples)?;
read = stream.read(&mut block)?;
}
writer.finish()
}
fn bits_for(samples: &Samples) -> u8 {
match samples {
Samples::U8(_) | Samples::I8(_) => 8,
Samples::U16(_) | Samples::I16(_) => 16,
_ => 24,
}
}
fn float_to_24(sample: f64) -> i32 {
const FULL: f64 = 8_388_607.0;
(sample.clamp(-1.0, 1.0) * FULL).round() as i32
}
fn header_bytes(stream: &Stream) -> Result<Vec<u8>, Error> {
let mut sink = ByteSink::new();
stream
.write(&mut sink)
.map_err(|error| encoder_error("the FLAC header", error))?;
Ok(sink.into_inner())
}
fn invalid(message: String) -> Error {
Error::with_message(ErrorKind::InvalidInput, message)
}
fn encoder_error(what: &str, error: impl std::fmt::Display) -> Error {
Error::with_message(ErrorKind::Other, format!("FLAC encoder, {what}: {error}"))
}
fn io_error(what: &str, doing: &str, error: std::io::Error) -> Error {
let kind = match error.kind() {
std::io::ErrorKind::PermissionDenied => ErrorKind::PermissionDenied,
_ => ErrorKind::Other,
};
Error::with_message(kind, format!("could not {doing} {what}: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn floats_become_24_bit_integers_clipped_at_full_scale() {
assert_eq!(float_to_24(0.0), 0);
assert_eq!(float_to_24(1.0), 8_388_607);
assert_eq!(float_to_24(-1.0), -8_388_607);
assert_eq!(float_to_24(1.5), 8_388_607, "an overshoot clips, it does not wrap");
}
#[test]
fn impossible_streams_are_refused_before_a_file_is_made() {
let directory = std::env::temp_dir();
for (channels, bits) in [(0, 16), (9, 16), (2, 7), (2, 32)] {
let path = directory.join(format!("atome-refused-{channels}-{bits}.flac"));
assert!(FlacWriter::create(&path, 48_000, channels, bits).is_err());
assert!(!path.exists(), "a refused writer left {} behind", path.display());
}
}
#[test]
fn a_part_frame_is_refused() {
let path = std::env::temp_dir().join(format!("atome-part-frame-{}.flac", std::process::id()));
let mut writer = FlacWriter::create(&path, 48_000, 2, 16).unwrap();
assert!(writer.write(&[1, 2, 3]).is_err());
drop(writer);
std::fs::remove_file(path).ok();
}
}