Skip to main content

flexaudio_encode/
writer.rs

1//! FLAC ストリーミング書き出し ([`FlacWriter`])。
2//!
3//! flacenc の主入口 `encode_with_fixed_block_size` は全サンプルを [`Source`] から
4//! 一括で読む設計で、長時間録音では全データをメモリに抱えることになる。ここでは
5//! フレーム単位の入口 [`flacenc::encode_fixed_size_frame`] を使い、ブロック
6//! (4096 サンプル/ch)が溜まるたびにエンコードしてファイルへ流す。
7//!
8//! STREAMINFO ヘッダは作成時に仮の値(総サンプル数 0・MD5 ゼロ)で書いておき、
9//! [`FlacWriter::finalize`] で先頭へシークして確定値に書き換える。STREAMINFO は
10//! 固定長 34 バイトなのでヘッダ全長は常に 42 バイトで変わらず、上書きで安全に
11//! 差し替えられる。
12//!
13//! [`Source`]: flacenc::source::Source
14
15use std::fs::File;
16use std::io::{BufWriter, Seek, Write};
17use std::path::Path;
18
19use flacenc::bitsink::ByteSink;
20use flacenc::component::{BitRepr, Stream, StreamInfo};
21use flacenc::config::Encoder as EncoderConfig;
22use flacenc::error::{Verified, Verify};
23use flacenc::source::{Context, Fill, FrameBuf};
24
25use crate::error::{EncodeError, Result};
26
27/// 1 フレームあたりのブロックサイズ(サンプル/チャンネル)。flacenc の既定値と同じ。
28const BLOCK_SIZE: usize = 4096;
29
30/// 量子化ビット深度。現状 16bit 固定。
31/// flacenc 自体は 24bit まで対応しているので、必要になれば [`quantize_i16`] の
32/// スケールとここを広げれば 24bit FLAC も書ける(現時点では余地のみ)。
33const BITS_PER_SAMPLE: usize = 16;
34
35/// 対応するサンプルレート上限 (Hz)。FLAC 形式自体は 655,350 Hz まで表せるが、
36/// flacenc の検証が 96 kHz までに制限しているのでそれに合わせる。
37const MAX_SAMPLE_RATE: u32 = 96_000;
38
39/// FLAC ヘッダ全長: "fLaC" マジック 4B + メタデータブロックヘッダ 4B + STREAMINFO 34B。
40const HEADER_LEN: usize = 42;
41
42/// f32 サンプル 1 個を 16bit 整数へ量子化する(ディザなしの単純量子化)。
43///
44/// スケールは 32768(負側フルスケール = -1.0 基準)。+1.0 ちょうどは 16bit に
45/// 収まらないので 32767 へクランプする。範囲外の入力もクランプで飽和させる。
46/// NaN は `as` キャストの飽和セマンティクスで 0 になる。
47#[inline]
48fn quantize_i16(x: f32) -> i32 {
49    (x * 32768.0).round().clamp(-32768.0, 32767.0) as i32
50}
51
52/// flacenc 系エラーを [`EncodeError::Encoder`] に写すヘルパ。
53fn enc_err(e: impl std::fmt::Display) -> EncodeError {
54    EncodeError::Encoder(e.to_string())
55}
56
57/// 録音チャンクを逐次 FLAC ファイルへ書き出すライター。
58///
59/// interleaved `f32`(flexaudio の `AudioChunk.data` と同じ形)を
60/// [`write_chunk`](FlacWriter::write_chunk) に流し、終わったら
61/// [`finalize`](FlacWriter::finalize) でヘッダを確定する。エンコードは呼び出し
62/// スレッド上で同期的に行う。
63///
64/// finalize を呼ばずに drop した場合もベストエフォートで閉じる(端数の書き出しと
65/// ヘッダ確定を試み、エラーは握りつぶす)。確実に完結させたいときは finalize を
66/// 呼ぶこと。
67pub struct FlacWriter {
68    file: BufWriter<File>,
69    config: Verified<EncoderConfig>,
70    /// 確定値を積算するストリーム情報。finalize でヘッダに書き戻す。
71    stream_info: StreamInfo,
72    /// エンコード入力用のブロックバッファ(flacenc がチャンネル分離して保持)。
73    frame_buf: FrameBuf,
74    /// MD5 と総サンプル数の積算(エンコードとは独立した flacenc の仕組み)。
75    context: Context,
76    /// フレームのビット列化に使い回すシンク。
77    sink: ByteSink,
78    /// ブロックサイズに満たない量子化済みサンプルの持ち越し。
79    pending: Vec<i32>,
80    channels: usize,
81    /// 次に書くフレーム番号(固定ブロックサイズなのでフレーム単位の連番)。
82    frame_number: usize,
83    /// finalize 済みなら Drop で何もしない。
84    finalized: bool,
85}
86
87impl FlacWriter {
88    /// `path` に 16bit FLAC ファイルを新規作成する(既存ファイルは上書き)。
89    ///
90    /// 対応範囲は `channels` 1..=2、`sample_rate` 1..=96,000 Hz。範囲外は
91    /// [`EncodeError::Unsupported`](ファイルは作られない)。
92    pub fn create<P: AsRef<Path>>(path: P, sample_rate: u32, channels: u16) -> Result<FlacWriter> {
93        if !(1..=2).contains(&channels) {
94            return Err(EncodeError::Unsupported(format!(
95                "channels must be 1 or 2, got {channels}"
96            )));
97        }
98        if !(1..=MAX_SAMPLE_RATE).contains(&sample_rate) {
99            return Err(EncodeError::Unsupported(format!(
100                "sample rate must be 1..={MAX_SAMPLE_RATE} Hz, got {sample_rate}"
101            )));
102        }
103
104        let config = EncoderConfig::default()
105            .into_verified()
106            .map_err(|(_, e)| enc_err(e))?;
107        let mut stream_info =
108            StreamInfo::new(sample_rate as usize, channels as usize, BITS_PER_SAMPLE)
109                .map_err(enc_err)?;
110        // 固定ブロックサイズのストリームとして宣言する(flacenc の一括入口と同じ流儀)。
111        stream_info
112            .set_block_sizes(BLOCK_SIZE, BLOCK_SIZE)
113            .map_err(enc_err)?;
114        let frame_buf = FrameBuf::with_size(channels as usize, BLOCK_SIZE).map_err(enc_err)?;
115        let context = Context::new(BITS_PER_SAMPLE, channels as usize);
116
117        let mut writer = FlacWriter {
118            file: BufWriter::new(File::create(path)?),
119            config,
120            stream_info,
121            frame_buf,
122            context,
123            sink: ByteSink::new(),
124            pending: Vec::new(),
125            channels: channels as usize,
126            frame_number: 0,
127            finalized: false,
128        };
129        // 仮ヘッダ。finalize で同じ長さのまま確定値に上書きする。
130        let header = writer.header_bytes()?;
131        writer.file.write_all(&header)?;
132        Ok(writer)
133    }
134
135    /// interleaved `f32` サンプルを追記する。
136    ///
137    /// 長さは `channels` の倍数であること(flexaudio の `AudioChunk.data` は
138    /// そのまま渡せる)。倍数でなければ [`EncodeError::Unsupported`] を返し、
139    /// 何も書かない。ブロックに満たない端数は内部に持ち越し、次の呼び出しか
140    /// finalize で書かれる。
141    pub fn write_chunk(&mut self, interleaved: &[f32]) -> Result<()> {
142        if interleaved.len() % self.channels != 0 {
143            return Err(EncodeError::Unsupported(format!(
144                "chunk length {} is not a multiple of channels {}",
145                interleaved.len(),
146                self.channels
147            )));
148        }
149        self.pending.reserve(interleaved.len());
150        self.pending
151            .extend(interleaved.iter().map(|&x| quantize_i16(x)));
152        self.drain_full_blocks()
153    }
154
155    /// 端数フレームの書き出しとヘッダ確定を行い、ファイルを閉じる。
156    ///
157    /// self を消費するので、以後の [`write_chunk`](FlacWriter::write_chunk) は型で
158    /// 不可能。Drop でも同じ処理をベストエフォートで行うが、書き込みエラーを
159    /// 検知できるのはこちらだけなので finalize 推奨。
160    pub fn finalize(mut self) -> Result<()> {
161        let result = self.finish_inner();
162        // Drop での二重実行を防ぐ(失敗していても再試行はしない)。
163        self.finalized = true;
164        result
165    }
166
167    /// pending から満杯ブロックをすべてエンコードして書き出す。
168    fn drain_full_blocks(&mut self) -> Result<()> {
169        let block_len = BLOCK_SIZE * self.channels;
170        // self.pending を借用したまま &mut self の encode_block は呼べないので、
171        // 一旦取り出して処理し、戻すときに消費済み分を落とす。
172        let pending = std::mem::take(&mut self.pending);
173        let mut consumed = 0;
174        let mut result = Ok(());
175        while consumed + block_len <= pending.len() {
176            if let Err(e) = self.encode_block(&pending[consumed..consumed + block_len]) {
177                result = Err(e);
178                break;
179            }
180            consumed += block_len;
181        }
182        self.pending = pending;
183        self.pending.drain(..consumed);
184        result
185    }
186
187    /// 1 ブロック分(最終ブロックのみ端数可)の量子化済みサンプルを 1 フレームに
188    /// エンコードしてファイルへ書く。
189    fn encode_block(&mut self, block: &[i32]) -> Result<()> {
190        self.frame_buf.fill_interleaved(block).map_err(enc_err)?;
191        // MD5 と総サンプル数はここで積算される。
192        self.context.fill_interleaved(block).map_err(enc_err)?;
193
194        let frame = flacenc::encode_fixed_size_frame(
195            &self.config,
196            &self.frame_buf,
197            self.frame_number,
198            &self.stream_info,
199        )
200        .map_err(enc_err)?;
201        // min/max フレームサイズ統計を積む(finalize でヘッダに反映される)。
202        self.stream_info.update_frame_info(&frame);
203
204        self.sink.clear();
205        frame.write(&mut self.sink).map_err(enc_err)?;
206        // FLAC フレームはバイト境界で終わるので as_slice で欠けなく取れる。
207        self.file.write_all(self.sink.as_slice())?;
208        self.frame_number += 1;
209        Ok(())
210    }
211
212    /// finalize の実体。Drop からも呼ばれる。
213    fn finish_inner(&mut self) -> Result<()> {
214        // 端数を最終フレームとして吐く(FLAC は最終フレームだけ短くてよい)。
215        if !self.pending.is_empty() {
216            let tail = std::mem::take(&mut self.pending);
217            self.encode_block(&tail)?;
218        }
219
220        // STREAMINFO を確定させる。
221        // - min/max ブロックサイズ: RFC 9639 では最終(端数)ブロックを数えないが、
222        //   update_frame_info は数えてしまい、端数が 16 サンプル未満だと仕様違反の
223        //   ヘッダになる(claxon などが拒否する)。固定ブロックサイズのストリーム
224        //   なので宣言値に戻す。
225        // - 総サンプル数: update_frame_info も積算するが、Context の値を正とする。
226        self.stream_info
227            .set_block_sizes(BLOCK_SIZE, BLOCK_SIZE)
228            .map_err(enc_err)?;
229        self.stream_info.set_md5_digest(&self.context.md5_digest());
230        self.stream_info
231            .set_total_samples(self.context.total_samples());
232
233        // 先頭の仮ヘッダを同じ長さの確定ヘッダで上書きする。
234        let header = self.header_bytes()?;
235        self.file.rewind()?;
236        self.file.write_all(&header)?;
237        self.file.flush()?;
238        Ok(())
239    }
240
241    /// 現時点の StreamInfo から FLAC ヘッダ("fLaC" + STREAMINFO ブロック)を作る。
242    ///
243    /// 常に [`HEADER_LEN`] バイト。作成時と finalize 時で長さが変わらないことに
244    /// 依存して、finalize では先頭を上書きする。
245    fn header_bytes(&self) -> Result<Vec<u8>> {
246        let mut info = self.stream_info.clone();
247        if self.frame_number == 0 {
248            // フレームが 1 つも無い間は min/max フレームサイズが flacenc の番兵値
249            // (u32::MAX / 0) のままなので、FLAC の「0 = 不明」に倒してから書く。
250            info.set_frame_sizes(0, 0).map_err(enc_err)?;
251        }
252        // フレームを持たない Stream の直列化 = マジック + STREAMINFO ブロックのみ。
253        let stream = Stream::with_stream_info(info);
254        let mut sink = ByteSink::new();
255        stream.write(&mut sink).map_err(enc_err)?;
256        let bytes = sink.into_inner();
257        debug_assert_eq!(bytes.len(), HEADER_LEN);
258        Ok(bytes)
259    }
260}
261
262impl Drop for FlacWriter {
263    fn drop(&mut self) {
264        if !self.finalized {
265            // ベストエフォート。エラーは握りつぶす(検知したいなら finalize を呼ぶ)。
266            let _ = self.finish_inner();
267        }
268    }
269}
270
271// flacenc 側の型(Verified 等)が Debug を持たないので derive できず、手書きする。
272impl std::fmt::Debug for FlacWriter {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        f.debug_struct("FlacWriter")
275            .field("sample_rate", &self.stream_info.sample_rate())
276            .field("channels", &self.channels)
277            .field("frame_number", &self.frame_number)
278            .field("pending_samples", &self.pending.len())
279            .field("finalized", &self.finalized)
280            .finish_non_exhaustive()
281    }
282}
283
284#[cfg(test)]
285mod tests {
286    use super::quantize_i16;
287
288    #[test]
289    fn quantize_reference_points() {
290        assert_eq!(quantize_i16(0.0), 0);
291        assert_eq!(quantize_i16(0.25), 8192);
292        assert_eq!(quantize_i16(-1.0), -32768);
293        // 正側フルスケールは 16bit に収まらないのでクランプ。
294        assert_eq!(quantize_i16(1.0), 32767);
295        // 範囲外は飽和。
296        assert_eq!(quantize_i16(2.0), 32767);
297        assert_eq!(quantize_i16(-2.0), -32768);
298        // 非有限値。
299        assert_eq!(quantize_i16(f32::NAN), 0);
300        assert_eq!(quantize_i16(f32::INFINITY), 32767);
301        assert_eq!(quantize_i16(f32::NEG_INFINITY), -32768);
302        // 丸めは最近接(round half away from zero)。1.5/32768 は 2 進で正確。
303        assert_eq!(quantize_i16(1.5 / 32768.0), 2);
304        assert_eq!(quantize_i16(-1.5 / 32768.0), -2);
305    }
306}