Skip to main content

siglus_scene_vm/audio/
jitan.rs

1//! Siglus voice time-compression (`C_jitan_cnv`) port.
2//!
3//! This intentionally follows the original `util_jitan_cnv.cpp` algorithm
4//! instead of using a generic resampler/WSOLA implementation.  Siglus keeps
5//! the sample rate unchanged, copies a short block, skips a rate-dependent
6//! block, then searches/smooths the splice against the preceding waveform.
7
8use anyhow::{Context, Result, bail, ensure};
9use siglus_assets::vorbis::{Pcm16, pcm16_to_wav_bytes};
10
11const JITAN_RATE_NORMAL: i32 = 100;
12const JITAN_RATE_CONVERTER_MAX: i32 = 400;
13const JITAN_CLAMP_SAMPLE: i32 = 32_760;
14const JITAN_STEREO_SUM_CLAMP: i32 = 30_000;
15
16/// Apply the original Siglus JITAN path to a decoded PCM16 WAV.
17///
18/// `elm_sound_player.cpp` only invokes the converter when the requested rate
19/// differs from 100%.  At JITAN rates, stereo KOE is first converted to mono by
20/// summing L+R and clamping to +/-30000.  The low-level converter itself only
21/// accepts mono 16-bit PCM.
22pub fn convert_koe_wav(wav: Vec<u8>, percent: u16) -> Result<Vec<u8>> {
23    if percent == JITAN_RATE_NORMAL as u16 {
24        return Ok(wav);
25    }
26
27    let pcm = parse_pcm16_wav(&wav)?;
28    let mono = match pcm.channels {
29        1 => pcm.samples,
30        2 => stereo_to_mono_original(&pcm.samples),
31        channels => {
32            bail!(
33                "Siglus JITAN only handles mono/stereo KOE before the mono converter; got {channels} channels"
34            )
35        }
36    };
37
38    let converted = convert_pcm16_original(&mono, pcm.sample_rate, percent as i32)
39        // C_sound::create() zero-initializes the destination vector.  The
40        // original caller ignores C_jitan_cnv::convert()'s false return, so a
41        // voice shorter than 100 ms is played as a same-length zero buffer.
42        .unwrap_or_else(|| vec![0; mono.len()]);
43
44    Ok(pcm16_to_wav_bytes(&Pcm16 {
45        channels: 1,
46        sample_rate: pcm.sample_rate,
47        samples: converted,
48    }))
49}
50
51fn parse_pcm16_wav(wav: &[u8]) -> Result<Pcm16> {
52    ensure!(
53        wav.len() >= 12 && &wav[..4] == b"RIFF" && &wav[8..12] == b"WAVE",
54        "KOE JITAN requires RIFF/WAVE"
55    );
56
57    let mut pos = 12usize;
58    let mut format: Option<(u16, u16, u32, u16)> = None;
59    let mut data: Option<&[u8]> = None;
60    while pos + 8 <= wav.len() {
61        let tag = &wav[pos..pos + 4];
62        let size = u32::from_le_bytes(
63            wav[pos + 4..pos + 8]
64                .try_into()
65                .context("read WAV chunk size")?,
66        ) as usize;
67        pos += 8;
68        let end = pos.checked_add(size).context("WAV chunk size overflow")?;
69        ensure!(end <= wav.len(), "truncated WAV chunk");
70
71        if tag == b"fmt " {
72            ensure!(size >= 16, "short WAV fmt chunk");
73            format = Some((
74                u16::from_le_bytes(wav[pos..pos + 2].try_into()?),
75                u16::from_le_bytes(wav[pos + 2..pos + 4].try_into()?),
76                u32::from_le_bytes(wav[pos + 4..pos + 8].try_into()?),
77                u16::from_le_bytes(wav[pos + 14..pos + 16].try_into()?),
78            ));
79        } else if tag == b"data" && data.is_none() {
80            data = Some(&wav[pos..end]);
81        }
82
83        pos = end.saturating_add(size & 1);
84    }
85
86    let (format_tag, channels, sample_rate, bits) = format.context("missing WAV fmt chunk")?;
87    ensure!(
88        format_tag == 1,
89        "JITAN requires PCM WAV, got format {format_tag}"
90    );
91    ensure!(bits == 16, "JITAN requires 16-bit PCM, got {bits}-bit");
92    ensure!(channels != 0, "JITAN WAV has zero channels");
93    ensure!(sample_rate != 0, "JITAN WAV has zero sample rate");
94    let data = data.context("missing WAV data chunk")?;
95    let frame_bytes = channels as usize * 2;
96    ensure!(
97        data.len() % frame_bytes == 0,
98        "unaligned PCM16 KOE payload: {} bytes / {} channels",
99        data.len(),
100        channels
101    );
102
103    let samples = data
104        .as_chunks::<2>()
105        .0
106        .iter()
107        .map(|sample| i16::from_le_bytes([sample[0], sample[1]]))
108        .collect();
109    Ok(Pcm16 {
110        channels,
111        sample_rate,
112        samples,
113    })
114}
115
116fn stereo_to_mono_original(input: &[i16]) -> Vec<i16> {
117    input
118        .as_chunks::<2>()
119        .0
120        .iter()
121        .map(|pair| {
122            (pair[0] as i32 + pair[1] as i32).clamp(-JITAN_STEREO_SUM_CLAMP, JITAN_STEREO_SUM_CLAMP)
123                as i16
124        })
125        .collect()
126}
127
128/// Safe index-based transcription of `C_jitan_cnv::convert()` and
129/// `convert_func()`.  `None` is the original `false` return.
130fn convert_pcm16_original(input: &[i16], sample_rate: u32, percent: i32) -> Option<Vec<i16>> {
131    if input.is_empty() {
132        return None;
133    }
134
135    let percent = percent.clamp(JITAN_RATE_NORMAL, JITAN_RATE_CONVERTER_MAX);
136    let extra_percent = percent - JITAN_RATE_NORMAL;
137    let rate = usize::try_from(sample_rate).ok()?;
138    let play_block_smp = rate / 80;
139    let before_smp = rate / 110;
140    let after_smp = rate / 110;
141    let delete_block_smp = play_block_smp.saturating_mul(extra_percent as usize) / 100;
142
143    convert_func_original(
144        input,
145        rate,
146        play_block_smp,
147        delete_block_smp,
148        before_smp,
149        after_smp,
150        true,
151        true,
152    )
153}
154
155fn convert_func_original(
156    input: &[i16],
157    rate: usize,
158    play_block_smp: usize,
159    delete_block_smp: usize,
160    before_smp: usize,
161    after_smp: usize,
162    zero_flag: bool,
163    smooth_flag: bool,
164) -> Option<Vec<i16>> {
165    // Original: wave_size < ((rate / 10) << 1) => false for mono PCM16.
166    if input.len() < rate / 10 || play_block_smp == 0 {
167        return None;
168    }
169
170    let work_base_smp = play_block_smp;
171    let jump_smp = delete_block_smp;
172
173    // C++ reserves the original PCM bytes plus one second.  Its helper bounds
174    // still use the original wave_size, so keep that separate from capacity.
175    let dst_bound = input.len();
176    let mut output = vec![0i16; input.len().saturating_add(rate)];
177    let center = before_smp;
178    let mut splice = vec![0i16; before_smp.saturating_add(after_smp)];
179    let mut splice_before_count = 0usize;
180    let mut splice_after_count = 0usize;
181
182    let mut dst = 0usize;
183    let mut src = 0usize;
184    let mut new_sample_count = 0usize;
185    let mut before_copy_smp = 0usize;
186
187    loop {
188        if src >= input.len() {
189            break;
190        }
191
192        if dst != 0 && zero_flag {
193            src = convert_func_16bit_rep(&output, dst - 1, input, src, work_base_smp, dst_bound);
194        }
195        if src >= input.len() {
196            break;
197        }
198
199        let mut copy_smp = if zero_flag {
200            convert_func_16bit_copy_size(input, src, work_base_smp)
201        } else {
202            work_base_smp
203        };
204        copy_smp = copy_smp.min(input.len() - src);
205
206        splice_before_count = before_smp.min(src);
207        if splice_before_count != 0 {
208            let from = src - splice_before_count;
209            let to = center;
210            splice[to - splice_before_count..to].copy_from_slice(&input[from..src]);
211        }
212
213        if output.len() < dst.saturating_add(copy_smp) {
214            output.resize(dst.saturating_add(copy_smp), 0);
215        }
216        if copy_smp != 0 {
217            output[dst..dst + copy_smp].copy_from_slice(&input[src..src + copy_smp]);
218        }
219
220        let mut smooth_len = 0i32;
221        if dst != 0 && smooth_flag {
222            smooth_len =
223                convert_func_smooth(&mut output, dst, before_copy_smp, copy_smp, dst_bound);
224        }
225        before_copy_smp = copy_smp;
226
227        if splice_before_count != 0 || splice_after_count != 0 {
228            convert_func_gousei(
229                &mut output,
230                dst,
231                dst_bound,
232                &mut splice,
233                center,
234                splice_before_count,
235                splice_after_count,
236                smooth_len,
237            );
238        }
239
240        splice_after_count = after_smp.min(input.len().saturating_sub(src + copy_smp));
241        if splice_after_count != 0 {
242            splice[center..center + splice_after_count]
243                .copy_from_slice(&input[src + copy_smp..src + copy_smp + splice_after_count]);
244        }
245
246        dst = dst.saturating_add(copy_smp);
247        src = src.saturating_add(copy_smp);
248        new_sample_count = new_sample_count.saturating_add(copy_smp);
249        src = src.saturating_add(jump_smp);
250    }
251
252    if new_sample_count >= 100 {
253        let start = new_sample_count - 100;
254        for offset in 0..100usize {
255            let multiplier = 100i32 - offset as i32;
256            output[start + offset] = ((output[start + offset] as i32 * multiplier) / 100) as i16;
257        }
258    }
259
260    output.truncate(new_sample_count);
261    Some(output)
262}
263
264fn convert_func_16bit_rep(
265    dst: &[i16],
266    dst_last: usize,
267    src: &[i16],
268    src_index: usize,
269    work_base_smp: usize,
270    dst_bound: usize,
271) -> usize {
272    let limit_smp_cnt1 = work_base_smp >> 2;
273    let limit_smp_cnt2 = work_base_smp >> 2;
274    if limit_smp_cnt1 == 0 || limit_smp_cnt2 == 0 {
275        return src_index;
276    }
277
278    let (vector_flag, target_smp) = if dst_last < dst_bound && dst_last < dst.len() {
279        (
280            convert_func_16bit_vector(dst, dst_last, dst_bound),
281            dst[dst_last] as i32,
282        )
283    } else {
284        (0, 0)
285    };
286
287    if vector_flag != 0 {
288        if let Some(distance) = convert_func_16bit_rep_func(
289            src,
290            src_index,
291            limit_smp_cnt1,
292            limit_smp_cnt2,
293            vector_flag,
294            target_smp,
295            -1,
296        ) {
297            return src_index.saturating_sub(distance);
298        }
299        if let Some(distance) = convert_func_16bit_rep_func(
300            src,
301            src_index,
302            limit_smp_cnt1,
303            limit_smp_cnt2,
304            vector_flag,
305            target_smp,
306            1,
307        ) {
308            return src_index.saturating_add(distance).min(src.len());
309        }
310    }
311
312    if let Some(distance) = convert_func_16bit_rep_func(
313        src,
314        src_index,
315        limit_smp_cnt1,
316        limit_smp_cnt2,
317        0,
318        target_smp,
319        -1,
320    ) {
321        return src_index.saturating_sub(distance);
322    }
323    if let Some(distance) = convert_func_16bit_rep_func(
324        src,
325        src_index,
326        limit_smp_cnt1,
327        limit_smp_cnt2,
328        0,
329        target_smp,
330        1,
331    ) {
332        return src_index.saturating_add(distance).min(src.len());
333    }
334    src_index
335}
336
337fn convert_func_16bit_rep_func(
338    src: &[i16],
339    start: usize,
340    limit_smp_cnt1: usize,
341    limit_smp_cnt2: usize,
342    vector_flag: i32,
343    target_smp: i32,
344    add: isize,
345) -> Option<usize> {
346    let mut index = start as isize;
347    let mut best_count: Option<usize> = None;
348    let mut test_smp_cnt = 0usize;
349    let mut get_flag = false;
350    let mut min_len = 999_999i32;
351
352    loop {
353        if index < 0 || index as usize >= src.len() {
354            break;
355        }
356        let idx = index as usize;
357        let sample = src[idx] as i32;
358
359        if vector_flag == 0 {
360            let distance = (target_smp - sample).abs();
361            if distance < min_len {
362                min_len = distance;
363                best_count = Some(test_smp_cnt);
364            }
365        } else {
366            let candidate_vector = convert_func_16bit_vector(src, idx, src.len());
367            if candidate_vector == vector_flag {
368                if vector_flag == 1 {
369                    if sample <= target_smp {
370                        let distance = target_smp - sample;
371                        if !get_flag || distance < min_len {
372                            min_len = distance;
373                            best_count = Some(test_smp_cnt);
374                            get_flag = true;
375                        }
376                    } else if !get_flag {
377                        let distance = sample - target_smp;
378                        if distance < min_len {
379                            min_len = distance;
380                            best_count = Some(test_smp_cnt);
381                        }
382                    }
383                } else if sample >= target_smp {
384                    let distance = sample - target_smp;
385                    if !get_flag || distance < min_len {
386                        min_len = distance;
387                        best_count = Some(test_smp_cnt);
388                        get_flag = true;
389                    }
390                } else if !get_flag {
391                    let distance = target_smp - sample;
392                    if distance < min_len {
393                        min_len = distance;
394                        best_count = Some(test_smp_cnt);
395                    }
396                }
397            }
398        }
399
400        if best_count.is_some() && min_len <= 100 {
401            break;
402        }
403        test_smp_cnt += 1;
404        index += add;
405        if test_smp_cnt >= limit_smp_cnt1 {
406            break;
407        }
408        if test_smp_cnt >= limit_smp_cnt2
409            && if vector_flag == 0 {
410                best_count.is_some()
411            } else {
412                get_flag
413            }
414        {
415            break;
416        }
417    }
418
419    best_count
420}
421
422fn convert_func_16bit_vector(src: &[i16], index: usize, end: usize) -> i32 {
423    if index >= end || index >= src.len() {
424        return 0;
425    }
426    let base_smp = src[index] as i32;
427    let mut vector_flag = 0i32;
428    let mut wp = index as isize - 1;
429    for _ in 0..10 {
430        if wp < 0 || wp as usize >= end || wp as usize >= src.len() {
431            break;
432        }
433        let sample = src[wp as usize] as i32;
434        if sample < base_smp {
435            vector_flag -= 1;
436            if vector_flag <= -3 {
437                break;
438            }
439        } else if sample > base_smp {
440            vector_flag += 1;
441            if vector_flag >= 3 {
442                break;
443            }
444        }
445        wp -= 1;
446    }
447
448    if vector_flag >= 3 {
449        1
450    } else if vector_flag <= -3 {
451        -1
452    } else {
453        0
454    }
455}
456
457fn convert_func_16bit_copy_size(src: &[i16], src_index: usize, work_base_smp: usize) -> usize {
458    let remaining = src.len().saturating_sub(src_index);
459    if remaining <= work_base_smp {
460        return remaining;
461    }
462
463    let mut copy_smp = work_base_smp;
464    let mut wp = src_index + copy_smp - 1;
465    let mut min_smp = (src[wp] as i32).abs();
466    wp = wp.saturating_sub(1);
467    let mut min_i = 0usize;
468    for i in 0..10usize {
469        if wp < src_index || wp >= src.len() {
470            break;
471        }
472        let test_smp = (src[wp] as i32).abs();
473        if test_smp < min_smp {
474            min_smp = test_smp;
475            min_i = i + 1;
476        }
477        if wp == 0 {
478            break;
479        }
480        wp -= 1;
481    }
482    if min_i != 0 {
483        copy_smp = copy_smp.saturating_sub(min_i);
484    }
485
486    let boundary = src_index + copy_smp - 1;
487    min_smp = (src[boundary] as i32).abs();
488    let sign_negative = src[boundary] < 0;
489
490    if min_smp == 0 {
491        return copy_smp;
492    }
493
494    let limit_smp_cnt = copy_smp >> 2;
495
496    let mut min_smp_a = min_smp;
497    let mut copy_smp_cnt_a = 0usize;
498    let mut test_smp_cnt = 1usize;
499    let mut scan = src_index + copy_smp;
500    loop {
501        if scan >= src.len() {
502            break;
503        }
504        let raw = src[scan];
505        if (raw < 0) != sign_negative {
506            break;
507        }
508        let test_smp = (raw as i32).abs();
509        if test_smp < min_smp_a {
510            copy_smp_cnt_a = test_smp_cnt;
511            min_smp_a = test_smp;
512            if min_smp_a == 0 {
513                break;
514            }
515        }
516        test_smp_cnt += 1;
517        if test_smp_cnt >= limit_smp_cnt {
518            break;
519        }
520        scan += 1;
521    }
522
523    let mut min_smp_b = min_smp;
524    let mut copy_smp_cnt_b = 0usize;
525    test_smp_cnt = 1;
526    let mut scan = src_index as isize + copy_smp as isize - 2;
527    loop {
528        if scan < src_index as isize || scan < 0 || scan as usize >= src.len() {
529            break;
530        }
531        let raw = src[scan as usize];
532        if (raw < 0) != sign_negative {
533            break;
534        }
535        let test_smp = (raw as i32).abs();
536        if test_smp < min_smp_b {
537            copy_smp_cnt_b = test_smp_cnt;
538            min_smp_b = test_smp;
539            if min_smp_b == 0 {
540                break;
541            }
542        }
543        test_smp_cnt += 1;
544        if test_smp_cnt >= limit_smp_cnt {
545            break;
546        }
547        scan -= 1;
548    }
549
550    if min_smp_a <= min_smp_b {
551        copy_smp.saturating_add(copy_smp_cnt_a).min(remaining)
552    } else {
553        copy_smp.saturating_sub(copy_smp_cnt_b)
554    }
555}
556
557fn convert_func_smooth(
558    dst: &mut [i16],
559    boundary: usize,
560    before_smp_cnt: usize,
561    after_smp_cnt: usize,
562    end: usize,
563) -> i32 {
564    if boundary == 0 || boundary >= end || boundary >= dst.len() {
565        return 0;
566    }
567
568    let smooth_len = (dst[boundary] as i32 - dst[boundary - 1] as i32) >> 1;
569
570    if before_smp_cnt != 0 {
571        let mut wp = boundary as isize - 1;
572        for i in (1..=before_smp_cnt).rev() {
573            if wp < 0 || wp as usize >= end || wp as usize >= dst.len() {
574                break;
575            }
576            let correction = ((smooth_len as f64 / before_smp_cnt as f64) * i as f64) as i32;
577            if correction == 0 {
578                break;
579            }
580            let value = (dst[wp as usize] as i32 + correction)
581                .clamp(-JITAN_CLAMP_SAMPLE, JITAN_CLAMP_SAMPLE);
582            dst[wp as usize] = value as i16;
583            wp -= 1;
584        }
585    }
586
587    if after_smp_cnt != 0 {
588        let mut wp = boundary;
589        for i in (1..=after_smp_cnt).rev() {
590            if wp >= end || wp >= dst.len() {
591                break;
592            }
593            let correction = ((smooth_len as f64 / after_smp_cnt as f64) * i as f64) as i32;
594            if correction == 0 {
595                break;
596            }
597            let value =
598                (dst[wp] as i32 - correction).clamp(-JITAN_CLAMP_SAMPLE, JITAN_CLAMP_SAMPLE);
599            dst[wp] = value as i16;
600            wp += 1;
601        }
602    }
603
604    smooth_len
605}
606
607#[allow(clippy::too_many_arguments)]
608fn convert_func_gousei(
609    dst: &mut [i16],
610    boundary: usize,
611    end: usize,
612    splice: &mut [i16],
613    center: usize,
614    mut before_count: usize,
615    mut after_count: usize,
616    smooth_len: i32,
617) {
618    if before_count != 0 {
619        if smooth_len != 0 {
620            let mut dp = center as isize - 1;
621            for _ in 0..before_count {
622                if dp < 0 || dp as usize >= splice.len() {
623                    break;
624                }
625                let value = (splice[dp as usize] as i32 - smooth_len)
626                    .clamp(-JITAN_CLAMP_SAMPLE, JITAN_CLAMP_SAMPLE);
627                splice[dp as usize] = value as i16;
628                dp -= 1;
629            }
630        }
631        before_count = before_count.min(boundary);
632    }
633
634    if after_count != 0 {
635        if smooth_len != 0 {
636            let mut dp = center;
637            for _ in 0..after_count {
638                if dp >= splice.len() {
639                    break;
640                }
641                let value =
642                    (splice[dp] as i32 + smooth_len).clamp(-JITAN_CLAMP_SAMPLE, JITAN_CLAMP_SAMPLE);
643                splice[dp] = value as i16;
644                dp += 1;
645            }
646        }
647        after_count = if boundary >= end {
648            0
649        } else {
650            after_count.min(end - boundary)
651        };
652    }
653
654    if after_count != 0 {
655        for i in 0..after_count {
656            if boundary + i >= dst.len() || center + i >= splice.len() {
657                break;
658            }
659            std::mem::swap(&mut dst[boundary + i], &mut splice[center + i]);
660        }
661    }
662
663    let proc_count = before_count.saturating_add(after_count);
664    if proc_count <= 1 {
665        return;
666    }
667
668    let aaa = (proc_count - 1) as f64;
669    let splice_start = center.saturating_sub(before_count);
670    let dst_start = boundary.saturating_sub(before_count);
671    for i in 0..proc_count {
672        if splice_start + i >= splice.len() || dst_start + i >= dst.len() {
673            break;
674        }
675        let value = ((splice[splice_start + i] as f64 / aaa) * i as f64
676            + (dst[dst_start + i] as f64 / aaa) * ((proc_count - 1 - i) as f64))
677            as i32;
678        dst[dst_start + i] = value.clamp(-JITAN_CLAMP_SAMPLE, JITAN_CLAMP_SAMPLE) as i16;
679    }
680}
681
682#[cfg(test)]
683mod tests {
684    use super::*;
685
686    fn wav(channels: u16, sample_rate: u32, samples: Vec<i16>) -> Vec<u8> {
687        pcm16_to_wav_bytes(&Pcm16 {
688            channels,
689            sample_rate,
690            samples,
691        })
692    }
693
694    #[test]
695    fn normal_rate_is_byte_identical() {
696        let input = wav(2, 8_000, vec![100, -100, 200, -200]);
697        assert_eq!(convert_koe_wav(input.clone(), 100).unwrap(), input);
698    }
699
700    #[test]
701    fn stereo_jitan_uses_original_sum_to_mono_rule() {
702        // 80 ms: C_jitan_cnv rejects it after the stereo->mono conversion.
703        // The original caller ignores that false return and plays the newly
704        // created zero-filled mono C_sound buffer.
705        let frames = 8_000usize * 80 / 1000;
706        let mut samples = Vec::with_capacity(frames * 2);
707        for _ in 0..frames {
708            samples.push(20_000);
709            samples.push(20_000);
710        }
711        let out = convert_koe_wav(wav(2, 8_000, samples), 200).unwrap();
712        let pcm = parse_pcm16_wav(&out).unwrap();
713        assert_eq!(pcm.channels, 1);
714        assert_eq!(pcm.samples.len(), frames);
715        assert!(pcm.samples.iter().all(|&sample| sample == 0));
716    }
717
718    fn fnv1a_pcm16(samples: &[i16]) -> u64 {
719        let mut hash = 0xcbf29ce484222325u64;
720        for sample in samples {
721            for byte in sample.to_le_bytes() {
722                hash ^= byte as u64;
723                hash = hash.wrapping_mul(0x100000001b3);
724            }
725        }
726        hash
727    }
728
729    #[test]
730    fn converter_matches_original_cpp_golden_vectors() {
731        // Golden values produced by the original util_jitan_cnv.cpp with the
732        // same deterministic mono PCM input.  This catches seemingly harmless
733        // changes to splice search, smoothing, integer rounding, or tail fade.
734        let input = (0..8_000usize)
735            .map(|i| (((i * 7_919 + 12_345) % 60_001) as i32 - 30_000) as i16)
736            .collect::<Vec<_>>();
737        for (rate, expected_len, expected_hash) in [
738            (125, 6_526, 0x32ed7b7901ceab1a),
739            (150, 5_754, 0xcb55646dcdd69270),
740            (200, 4_130, 0x7422c527b9b4de3d),
741            (300, 2_599, 0x88033d2b78a75beb),
742        ] {
743            let out = convert_pcm16_original(&input, 8_000, rate).unwrap();
744            assert_eq!(out.len(), expected_len, "rate={rate}");
745            assert_eq!(fnv1a_pcm16(&out), expected_hash, "rate={rate}");
746        }
747    }
748
749    #[test]
750    fn original_converter_shortens_long_voice_and_fades_tail() {
751        let rate = 8_000u32;
752        let frames = rate as usize;
753        let samples = (0..frames)
754            .map(|i| {
755                ((i as f64 * 440.0 * std::f64::consts::TAU / rate as f64).sin() * 12_000.0) as i16
756            })
757            .collect::<Vec<_>>();
758        let out = convert_koe_wav(wav(1, rate, samples), 200).unwrap();
759        let pcm = parse_pcm16_wav(&out).unwrap();
760        assert_eq!(pcm.channels, 1);
761        assert!(pcm.samples.len() < frames);
762        assert!(pcm.samples.len() > frames / 3);
763        assert!(pcm.samples.last().copied().unwrap_or(0).abs() < 500);
764    }
765}