diff --git a/internal/capability/transcode/amrnb/LICENSE b/internal/capability/transcode/amrnb/LICENSE new file mode 100644 index 0000000..a4f8e3c --- /dev/null +++ b/internal/capability/transcode/amrnb/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative + Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and do + not modify the License. You may add Your own attribution notices + within Derivative Works that You distribute, alongside or as an + addendum to the NOTICE text from the Work, provided that such + additional attribution notices cannot be construed as modifying + the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/internal/capability/transcode/amrnb/NOTICE b/internal/capability/transcode/amrnb/NOTICE new file mode 100644 index 0000000..ea1a4ab --- /dev/null +++ b/internal/capability/transcode/amrnb/NOTICE @@ -0,0 +1,15 @@ +goamr-nb +Copyright 2026 The goamr-nb authors + +This product is a pure-Go port of AMR-NB (3GPP TS 26.071, 26.073, 26.090, +26.101, 26.104) derived from the following Apache-2.0 licensed C reference: + + - opencore-amrnb (AMR-NB encoder and decoder) + https://sourceforge.net/projects/opencore-amr/ + Copyright (C) 2009 Martin Storsjo + Copyright (C) The Android Open Source Project + +The fixed-point DSP was translated function-by-function from the above +source, preserving its algorithms and bit-exact semantics. The original +work is licensed under the Apache License, Version 2.0; this port is +distributed under the same license (see LICENSE). diff --git a/internal/capability/transcode/amrnb/VENDORED.md b/internal/capability/transcode/amrnb/VENDORED.md new file mode 100644 index 0000000..dadee3a --- /dev/null +++ b/internal/capability/transcode/amrnb/VENDORED.md @@ -0,0 +1,51 @@ +# vendored: github.com/VoiceBlender/goamr-nb v1.0.0(AMR-NB 编解码器) + +本目录是**第三方源码的 vendored 拷贝**,不是本项目自研代码。合规要务:随源码保留 +了上游的 `LICENSE`(Apache-2.0)与 `NOTICE`(Apache-2.0 第 4 条要求),**不得删除**。 + +## 出处(可复现) + +| 项 | 值 | +|---|---| +| 仓库 | https://github.com/VoiceBlender/goamr-nb | +| 版本 | `v1.0.0`(tag) | +| 上游提交 | `635f7966829a0e5d01030355e36aa6792352c57e` | +| 上游发布时间 | 2026-06-14T17:25:04Z(模块代理 `.info`) | +| 取得方式 | 模块代理 `https://goproxy.cn/github.com/%21voice%21blender/goamr-nb/@v/v1.0.0.zip`(sha256 `663140d606646d62d6660e8ff8930725826680b4373055e6aeeab55f858f6067`) | +| 许可证 | **Apache-2.0**(`LICENSE` + `NOTICE` 均逐字保留) | +| 上游依赖 | 仅 `golang.org/x/sys`(`simd_amd64.go` 用 `x/sys/cpu` 判 AVX2);本仓库 `go.mod` 已有 `golang.org/x/sys v0.47.0`,**未新增任何模块依赖** | + +## 为什么是「拷进仓库」而不是 `require` + +上游是 2026-06 新建、**1 个提交 / 1 个 tag / 2 star** 的仓库。当外部依赖用等于把 +**构建**押在一个随时可能消失的仓库上;而 AMR-NB 是**冻结编解码**(1999 定标), +拷进来基本不需要维护。因此:`go.mod` **不加** require,源码在本目录内自持。 + +## 拷了什么 / 没拷什么 + +- **拷了**:上游全部 57 个非测试 `.go` 文件 + `LICENSE` + `NOTICE`,逐字未改。 +- **没拷**:上游 48 个 `*_test.go`(含 `dsp_diff_test.go` 这类需要外部 C 参考 + harness 的测试)与 `Makefile` / `README.md`。本项目的正确性证据在 + `../codec_amr_nb_test.go`(ffmpeg/libopencore_amrnb 黄金向量),不依赖上游测试。 +- **本地新增**:`vendored_export.go`(导出桥,见其文件头说明)与本文档。 + 除这两个文件外,本目录内容与上游一致。 + +## 已知能力边界(上游自述,实测确认;上游 README 的 `bit-exact` 说法**不覆盖**下列) + +`dsp_decoder.go:13` 的注释写明:**`Cb_gain_average`(增益平滑)与自适应相位色散 +(phase dispersion)尚未移植**;`dsp.go:68` 写明**错误隐藏(bad frame)与 CNG +(SID 舒适噪声)尚未移植**。 + +实测结论(本项目 2026-10-05,fixture 见 `../testdata/`): +坏帧 / SID / no-data 帧**返回 160 样本静音**(不报错、不做隐藏);好帧解码与 +`opencore-amrnb`(ffmpeg `libopencore_amrnb`)**逐字节不一致**,样本级差值的量级见 +`../codec_amr_nb_test.go` 里记录的实测基线(SNR 约 16–21 dB,波形相关系数 ≥0.987, +长度/帧数/时长**完全一致**)。可听、可播、内容正确,但**不是**参考位精确实现。 + +另注:`dsp.go` 顶部的「DSP 目前是 STUBBED」注释已过期(上游未同步)—— +`dsp_decoder.go` 里的解码 DSP 是完整实现,实测能解出真实语音。 + +## 交叉编译 + +纯 Go(`CGO_ENABLED=0` 可构建)。`simd_amd64.s` 是唯一非 Go 源文件, +`simd_noasm.go` 提供 `!amd64` 的纯 Go 兜底 ⇒ `GOOS=linux/darwin` 交叉编译通过。 diff --git a/internal/capability/transcode/amrnb/amrnb.go b/internal/capability/transcode/amrnb/amrnb.go new file mode 100644 index 0000000..f95d50c --- /dev/null +++ b/internal/capability/transcode/amrnb/amrnb.go @@ -0,0 +1,103 @@ +package amrnb + +import "errors" + +// Encoder encodes 8 kHz mono PCM into AMR-NB RFC 4867 RTP payloads. +// +// It satisfies the codec.Encoder interface: Encode takes one 20 ms frame +// (160 int16 samples) and returns a complete RTP payload (CMR + ToC + speech +// bits) in the configured payload format. +type Encoder struct { + mode Mode + octetAligned bool + st encoderState +} + +// EncoderConfig configures a new AMR-NB encoder. +type EncoderConfig struct { + Mode Mode // active speech mode (0..7) + OctetAligned bool // true: octet-aligned framing; false: bandwidth-efficient +} + +// NewEncoder creates an AMR-NB encoder for the given mode and payload format. +func NewEncoder(cfg EncoderConfig) (*Encoder, error) { + if !cfg.Mode.Valid() { + return nil, errors.New("amrnb: invalid encoder mode") + } + e := &Encoder{mode: cfg.Mode, octetAligned: cfg.OctetAligned} + e.st.reset() + return e, nil +} + +// Encode encodes one 20 ms frame (160 samples) into an RTP payload. +func (e *Encoder) Encode(samples []int16) ([]byte, error) { + if len(samples) != FrameSamples { + return nil, errors.New("amrnb: encode expects exactly 160 samples") + } + bits, err := e.st.encodeFrame(e.mode, samples) + if err != nil { + return nil, err + } + f := frame{ft: e.mode.FrameType(), q: true, data: bits} + if e.octetAligned { + return packPayloadOctet(noCMR, []frame{f}), nil + } + return packPayloadBandwidthEfficient(noCMR, []frame{f}), nil +} + +// SetMode changes the active speech mode (e.g. in response to a peer CMR). +func (e *Encoder) SetMode(m Mode) { + if m.Valid() { + e.mode = m + } +} + +// Reset clears all encoder state. +func (e *Encoder) Reset() { e.st.reset() } + +// Decoder decodes AMR-NB RFC 4867 RTP payloads into 8 kHz mono PCM. +type Decoder struct { + octetAligned bool + st decoderState +} + +// DecoderConfig configures a new AMR-NB decoder. +type DecoderConfig struct { + OctetAligned bool // must match the negotiated payload format +} + +// NewDecoder creates an AMR-NB decoder for the given payload format. +func NewDecoder(cfg DecoderConfig) *Decoder { + d := &Decoder{octetAligned: cfg.OctetAligned} + d.st.reset() + return d +} + +// Decode decodes one RTP payload, returning concatenated PCM for all frames it +// carries (one 160-sample frame per speech/SID/no-data ToC entry). +func (d *Decoder) Decode(payload []byte) ([]int16, error) { + var ( + frames []frame + err error + ) + if d.octetAligned { + _, frames, err = unpackPayloadOctet(payload) + } else { + _, frames, err = unpackPayloadBandwidthEfficient(payload) + } + if err != nil { + return nil, err + } + out := make([]int16, 0, len(frames)*FrameSamples) + for _, f := range frames { + pcm, derr := d.st.decodeFrame(f) + if derr != nil { + return nil, derr + } + out = append(out, pcm...) + } + return out, nil +} + +// Reset clears all decoder state. +func (d *Decoder) Reset() { d.st.reset() } diff --git a/internal/capability/transcode/amrnb/basicops.go b/internal/capability/transcode/amrnb/basicops.go new file mode 100644 index 0000000..b29038e --- /dev/null +++ b/internal/capability/transcode/amrnb/basicops.go @@ -0,0 +1,261 @@ +package amrnb + +// Fixed-point basic operators, ported from the ITU-T / ETSI "basicop2" +// reference (the same primitives used by opencore-amrnb). +// +// Word16 maps to int16, Word32 to int32. All operators saturate to the 16- or +// 32-bit range exactly as the reference does; Go has no implicit integer +// saturation, so every operation that can overflow clamps explicitly. The +// reference's global Overflow/Carry flags are not modelled — no AMR-NB code +// path depends on reading them back. + +const ( + maxInt16 = 32767 + minInt16 = -32768 + maxInt32 = 2147483647 + minInt32 = -2147483648 +) + +// satWord16 clamps a 32-bit value to the int16 range. +func satWord16(v int32) int16 { + if v > maxInt16 { + return maxInt16 + } + if v < minInt16 { + return minInt16 + } + return int16(v) +} + +// satWord32 clamps a 64-bit value to the int32 range. +func satWord32(v int64) int32 { + if v > maxInt32 { + return maxInt32 + } + if v < minInt32 { + return minInt32 + } + return int32(v) +} + +// add returns the saturated 16-bit sum a+b. +func add(a, b int16) int16 { return satWord16(int32(a) + int32(b)) } + +// sub returns the saturated 16-bit difference a-b. +func sub(a, b int16) int16 { return satWord16(int32(a) - int32(b)) } + +// abs_s returns the saturated 16-bit absolute value (|-32768| -> 32767). +func abs_s(a int16) int16 { + if a == minInt16 { + return maxInt16 + } + if a < 0 { + return -a + } + return a +} + +// shl returns a shifted left by b bits with saturation; negative b shifts right. +func shl(a, b int16) int16 { + if b < 0 { + return shr(a, -b) + } + result := int32(a) << uint(b) + // Detect overflow by checking the shift is reversible. + if b >= 31 { + if a == 0 { + return 0 + } + if a > 0 { + return maxInt16 + } + return minInt16 + } + return satWord16(result) +} + +// shr returns a shifted right by b bits (arithmetic); negative b shifts left. +func shr(a, b int16) int16 { + if b < 0 { + return shl(a, -b) + } + if b >= 15 { + if a < 0 { + return -1 + } + return 0 + } + return int16(int32(a) >> uint(b)) +} + +// L_shl returns the saturated 32-bit left shift; negative b shifts right. +func L_shl(a int32, b int16) int32 { + if b <= 0 { + if b == 0 { + return a + } + return L_shr(a, -b) + } + if b >= 31 { + if a > 0 { + return maxInt32 + } + if a < 0 { + return minInt32 + } + return 0 + } + // The reference shifts one bit at a time, saturating as soon as the value + // reaches the overflow region. Since |a<= 2^30 -> MAX_32, + // < -2^30 -> MIN_32; otherwise a<= 0x40000000 { + return maxInt32 + } + if last < -0x40000000 { + return minInt32 + } + return a << uint(b) +} + +// L_shr returns the 32-bit arithmetic right shift; negative b shifts left. +func L_shr(a int32, b int16) int32 { + if b < 0 { + return L_shl(a, -b) + } + if b >= 31 { + if a < 0 { + return -1 + } + return 0 + } + return a >> uint(b) +} + +// mult returns satWord16((a*b)>>15). +func mult(a, b int16) int16 { + p := (int32(a) * int32(b)) >> 15 + return satWord16(p) +} + +// L_mult returns satWord32(a*b<<1) — the 32-bit product with the fractional +// left shift. +func L_mult(a, b int16) int32 { + p := int32(a) * int32(b) + if p != 0x40000000 { + return p << 1 + } + return maxInt32 +} + +// L_add returns the saturated 32-bit sum. +func L_add(a, b int32) int32 { return satWord32(int64(a) + int64(b)) } + +// L_sub returns the saturated 32-bit difference. +func L_sub(a, b int32) int32 { return satWord32(int64(a) - int64(b)) } + +// L_mac returns acc + (a*b<<1), saturated. +func L_mac(acc int32, a, b int16) int32 { return L_add(acc, L_mult(a, b)) } + +// L_msu returns acc - (a*b<<1), saturated. +func L_msu(acc int32, a, b int16) int32 { return L_sub(acc, L_mult(a, b)) } + +// negate returns the saturated 16-bit negation. +func negate(a int16) int16 { + if a == minInt16 { + return maxInt16 + } + return -a +} + +// L_negate returns the saturated 32-bit negation. +func L_negate(a int32) int32 { + if a == minInt32 { + return maxInt32 + } + return -a +} + +// extract_h returns the high 16 bits of a 32-bit value. +func extract_h(a int32) int16 { return int16(a >> 16) } + +// extract_l returns the low 16 bits of a 32-bit value. +func extract_l(a int32) int16 { return int16(a & 0xFFFF) } + +// round_ returns the rounded high word: extract_h(L_add(a, 0x8000)). +func round_(a int32) int16 { return extract_h(L_add(a, 0x8000)) } + +// mult_r returns the rounded multiply: satWord16(((a*b)+0x4000)>>15). +func mult_r(a, b int16) int16 { + p := (int32(a)*int32(b) + 0x4000) >> 15 + return satWord16(p) +} + +// L_deposit_h places a 16-bit value in the high half of a 32-bit word. +func L_deposit_h(a int16) int32 { return int32(a) << 16 } + +// L_deposit_l sign-extends a 16-bit value into a 32-bit word. +func L_deposit_l(a int16) int32 { return int32(a) } + +// norm_l returns the number of left shifts needed to normalize a (0 for 0). +func norm_l(a int32) int16 { + if a == 0 { + return 0 + } + if a == -1 { + return 31 + } + if a < 0 { + a = ^a + } + var n int16 + for a < 0x40000000 { + a <<= 1 + n++ + } + return n +} + +// norm_s returns the number of left shifts needed to normalize a 16-bit value. +func norm_s(a int16) int16 { + if a == 0 { + return 0 + } + if a == -1 { + return 15 + } + v := a + if v < 0 { + v = ^v + } + var n int16 + for v < 0x4000 { + v <<= 1 + n++ + } + return n +} + +// div_s returns the fractional division (a/b)<<15 for 0 <= a <= b, b != 0. +func div_s(a, b int16) int16 { + if a == 0 { + return 0 + } + if a == b { + return maxInt16 + } + num := int32(a) + den := int32(b) + var quot int32 + for i := 0; i < 15; i++ { + quot <<= 1 + num <<= 1 + if num >= den { + num -= den + quot++ + } + } + return int16(quot) +} diff --git a/internal/capability/transcode/amrnb/dsp.go b/internal/capability/transcode/amrnb/dsp.go new file mode 100644 index 0000000..92a1a3e --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp.go @@ -0,0 +1,83 @@ +package amrnb + +// This file holds the AMR-NB signal-processing state and the entry points the +// public Encoder/Decoder call per 20 ms frame. The fixed-point DSP itself +// (LP analysis/LSF quantization, open/closed-loop pitch, algebraic codebook +// search, gain quantization, synthesis and post-filtering, DTX) is a large +// bit-exact port of 3GPP TS 26.090 — see opencore-amrnb (Apache-2.0), which +// ships both encoder and decoder. +// +// The structs below define where that ported state lives. The encodeFrame / +// decodeFrame methods are the single integration seam where the per-block DSP +// functions are orchestrated per 20 ms frame. +// +// NOTE: the DSP is currently STUBBED — coder()/decoder() are not yet ported, so +// encodeFrame emits a zero-filled bitstream of the correct size and decodeFrame +// emits silence. The public API, RFC 4867 framing, fixed-point operators and +// SIMD kernels are complete and exercised by the pure-Go tests; the bit-exact +// DSP lands in a later phase (see the plan). + +// encoderState holds the persistent encoder analysis state across frames. +type encoderState struct { + started bool + st coderState +} + +func (s *encoderState) reset() { + s.started = true + s.st.reset() +} + +// encodeFrame analyses one 160-sample frame in the given mode and returns the +// RFC 4867 frame payload bytes (the sorted/packed speech bits, without the +// CMR/ToC header which the caller adds). Mirrors Speech_Encode_Frame: mask the 3 +// LSBs (13-bit input), high-pass + downscale (Pre_Process), run the analysis +// (cod_amr), then serialize (prm2bits + IETF reorder via packFrame). +func (s *encoderState) encodeFrame(mode Mode, samples []int16) ([]byte, error) { + if !s.started { + s.reset() + } + in := make([]int16, cL_FRAME) + for i := 0; i < cL_FRAME && i < len(samples); i++ { + in[i] = samples[i] &^ 7 // delete 3 LSBs (13-bit input) + } + s.st.pre.process(in, cL_FRAME) + + prm, err := s.st.codAmr(int(mode), in) + if err != nil { + return nil, err + } + return packFrame(prm, mode), nil +} + +// decoderState holds the persistent decoder synthesis state across frames. +type decoderState struct { + dsp decoderStateDSP + lastMode Mode + started bool +} + +func (s *decoderState) reset() { + s.dsp.reset() + s.lastMode = Mode1220 + s.started = true +} + +// decodeFrame decodes one parsed AMR-NB frame into 160 PCM samples. Good speech +// frames are fully decoded; bad/SID/no-data frames currently emit silence +// (error concealment and CNG are not yet ported). +func (s *decoderState) decodeFrame(f frame) ([]int16, error) { + if !s.started { + s.reset() + } + synth := make([]int16, cAMR_NB_PCM_FRAME) + if f.ft <= FTMode1220 { // speech + mode := Mode(f.ft) + s.lastMode = mode + if f.q { + params := mimeUnsort(f.data, mode) + s.dsp.decodeSpeechFrame(int(mode), params, synth) + } + } + return synth, nil +} diff --git a/internal/capability/transcode/amrnb/dsp_acelp.go b/internal/capability/transcode/amrnb/dsp_acelp.go new file mode 100644 index 0000000..9e194d7 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_acelp.go @@ -0,0 +1,183 @@ +package amrnb + +// Adaptive-codebook (pitch) reconstruction, ported from opencore-amrnb +// pred_lt.cpp, dec_lag3.cpp and dec_lag6.cpp. predLt interpolates the past +// excitation at a fractional pitch lag to build this subframe's adaptive +// codebook contribution; decLag3/decLag6 turn the received pitch index into an +// integer lag T0 and a fractional part. Bit-exact with the reference. + +const ( + cL_INTER10 = 10 // interpolation FIR half-context (L_INTERPOL-1) + cUP_SAMP_MAX = 6 // fractional pitch upsampling factor +) + +// predLt computes the interpolated past excitation for the current subframe in +// place: exc[pos : pos+Lsubfr] is overwritten using exc[pos-T0 ...] convolved +// with the 1/6-resolution FIR selected by frac. For short lags (T0 < Lsubfr) +// the loop deliberately reads samples it just wrote, matching the reference. +// Mirrors Pred_lt_3or6. +func predLt(exc []int16, pos int, T0, frac, Lsubfr, flag3 int16) { + var coeff [2 * cL_INTER10]int16 + + pX0 := pos - int(T0) + frac = -frac + if flag3 != 0 { + frac <<= 1 // inter_3l[k] = inter_6[2k] + } + if frac < 0 { + frac += cUP_SAMP_MAX + pX0-- + } + f := int(frac) + + k := 0 + ci := 0 + for i := cL_INTER10 >> 1; i > 0; i-- { + coeff[ci] = inter_6_pred_lt[f+k] + coeff[ci+1] = inter_6_pred_lt[(cUP_SAMP_MAX-f)+k] + ci += 2 + k += cUP_SAMP_MAX + coeff[ci] = inter_6_pred_lt[f+k] + coeff[ci+1] = inter_6_pred_lt[(cUP_SAMP_MAX-f)+k] + ci += 2 + k += cUP_SAMP_MAX + } + + pExc := pos + for j := int(Lsubfr) >> 1; j != 0; j-- { + pX0++ + ix2 := pX0 + ix3 := pX0 + pX0++ + + ic := 0 + s1 := int32(0x4000) + s2 := int32(0x4000) + for i := cL_INTER10 >> 1; i > 0; i-- { + s2 += int32(exc[ix3]) * int32(coeff[ic]) + ix3-- + s1 += int32(exc[ix3]) * int32(coeff[ic]) + ic++ + s1 += int32(exc[ix2]) * int32(coeff[ic]) + ix2++ + s2 += int32(exc[ix2]) * int32(coeff[ic]) + ic++ + s2 += int32(exc[ix3]) * int32(coeff[ic]) + ix3-- + s1 += int32(exc[ix3]) * int32(coeff[ic]) + ic++ + s1 += int32(exc[ix2]) * int32(coeff[ic]) + ix2++ + s2 += int32(exc[ix2]) * int32(coeff[ic]) + ic++ + } + exc[pExc] = int16(s1 >> 15) + pExc++ + exc[pExc] = int16(s2 >> 15) + pExc++ + } +} + +// decLag3 decodes the 1/3-resolution pitch lag (used by most modes) from the +// received index into an integer lag T0 and fractional part T0frac. i_subfr==0 +// marks the 1st/3rd subframe (absolute coding); otherwise the lag is coded +// relative to t0_min (or, with flag4, to T0prev). Mirrors Dec_lag3. +func decLag3(index, t0Min, t0Max, iSubfr, T0prev, flag4 int16) (T0, T0frac int16) { + var i, tmpLag int16 + if iSubfr == 0 { // 1st or 3rd subframe + if index < 197 { + tmpLag = index + 2 + tmpLag = mult(tmpLag, 10923) + i = tmpLag + 19 + T0 = i + i <<= 1 + i += T0 + tmpLag = index - i + T0frac = tmpLag + 58 + } else { + T0 = index - 112 + T0frac = 0 + } + } else { // 2nd or 4th subframe + if flag4 == 0 { + i = index + 2 + i = int16((int32(i) * 10923) >> 15) + i -= 1 + T0 = i + t0Min + i = i + (i << 1) + tmpLag = index - 2 + T0frac = tmpLag - i + } else { + tmpLag = T0prev + i = sub(tmpLag, t0Min) + if i > 5 { + tmpLag = t0Min + 5 + } + i = t0Max - tmpLag + if i > 4 { + tmpLag = t0Max - 4 + } + if index < 4 { + i = tmpLag - 5 + T0 = i + index + T0frac = 0 + } else if index < 12 { + i = index - 5 + i = int16((int32(i) * 10923) >> 15) + i-- + T0 = i + tmpLag + i = i + (i << 1) + tmpLag = index - 9 + T0frac = tmpLag - i + } else { + i = index - 12 + i = i + tmpLag + T0 = i + 1 + T0frac = 0 + } + } + } + return T0, T0frac +} + +// decLag6 decodes the 1/6-resolution pitch lag (MR122) from the received index. +// T0in supplies the previous subframe's integer lag for the relative (2nd/4th) +// subframe path. Mirrors Dec_lag6. +func decLag6(index, pitMin, pitMax, iSubfr, T0in int16) (T0, T0frac int16) { + var i, T0min, T0max, k int16 + if iSubfr == 0 { // 1st or 3rd subframe + if index < 463 { + i = index + 5 + i = int16((int32(i) * 5462) >> 15) + i += 17 + T0 = i + i <<= 1 + i += T0 + i <<= 1 + i = index - i + T0frac = i + 105 + } else { + T0 = index - 368 + T0frac = 0 + } + } else { // 2nd or 4th subframe + T0min = T0in - 5 + if T0min < pitMin { + T0min = pitMin + } + T0max = T0min + 9 + if T0max > pitMax { + T0max = pitMax + T0min = T0max - 9 + } + i = index + 5 + i = int16((int32(i) * 5462) >> 15) + i -= 1 + T0 = i + T0min + i = i + (i << 1) + i <<= 1 + k = index - 3 + T0frac = k - i + } + return T0, T0frac +} diff --git a/internal/capability/transcode/amrnb/dsp_agc.go b/internal/capability/transcode/amrnb/dsp_agc.go new file mode 100644 index 0000000..e63b7f0 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_agc.go @@ -0,0 +1,77 @@ +package amrnb + +// Adaptive gain control for the post-filter, ported from opencore-amrnb agc.cpp. +// agc scales the post-filtered signal so its energy tracks the synthesis +// energy, with a per-sample gain smoothed by agc_fac across the subframe. The +// running gain is carried in agcState. Bit-exact with the reference. + +const cAGC_FAC = 29491 // 0.9, the post-filter AGC factor + +// agcState holds the smoothed per-sample gain across subframes. +type agcState struct { + pastGain int16 +} + +// energyOld accumulates the energy of a prescaled signal (used when energyNew +// would overflow). Mirrors energy_old. +func energyOld(in []int16, lTrm int) int32 { + var s int32 + for i := 0; i < lTrm; i++ { + temp := in[i] >> 2 + s = L_mac(s, temp, temp) + } + return s +} + +// energyNew returns the energy of a signal (>>4 scaled), falling back to +// energyOld on saturation. Mirrors energy_new. +func energyNew(in []int16, lTrm int) int32 { + var s int32 + for i := 0; i < lTrm; i++ { + s = L_mac(s, in[i], in[i]) + } + if s != maxInt32 { + return s >> 4 + } + return energyOld(in, lTrm) +} + +// agc scales sigOut in place so its energy approaches sigIn's, applying a gain +// smoothed across the subframe by agcFac and carried in the state. Mirrors agc. +func (st *agcState) agc(sigIn, sigOut []int16, agcFac int16, lTrm int) { + s := energyNew(sigOut, lTrm) + if s == 0 { + st.pastGain = 0 + return + } + exp := norm_l(s) - 1 + gainOut := round_(L_shl(s, exp)) + + s = energyNew(sigIn, lTrm) + var g0 int16 + if s == 0 { + g0 = 0 + } else { + i := norm_l(s) + gainIn := round_(s << i) + exp -= i + + temp := div_s(gainOut, gainIn) + ss := int32(temp) << 7 + ss = L_shr(ss, exp) + ss = invSqrt(ss) + lTemp := ss << 9 + ii := int16((lTemp + 0x8000) >> 16) + temp = 32767 - agcFac + g0 = int16((int32(ii) * int32(temp)) >> 15) + } + + gain := st.pastGain + for i := 0; i < lTrm; i++ { + gain = int16((int32(gain) * int32(agcFac)) >> 15) + gain += g0 + lTemp := (int32(sigOut[i]) * int32(gain)) << 1 + sigOut[i] = int16(lTemp >> 13) + } + st.pastGain = gain +} diff --git a/internal/capability/transcode/amrnb/dsp_az_lsp.go b/internal/capability/transcode/amrnb/dsp_az_lsp.go new file mode 100644 index 0000000..2ae65fd --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_az_lsp.go @@ -0,0 +1,127 @@ +package amrnb + +// A(z) -> LSP conversion, ported from opencore-amrnb az_lsp.cpp (Chebps, Az_lsp). +// Az_lsp finds the line spectral pairs of an order-10 LP filter by locating the +// roots of the symmetric/antisymmetric polynomials F1(z)/F2(z) on the unit +// circle, evaluated via the Chebyshev recursion (Chebps) over the cosine grid. +// This is the encoder-side inverse of lspAz. Bit-exact with the reference. + +const cNC = cM / 2 // 5 + +// chebps evaluates the Chebyshev series of f at x. Mirrors Chebps. +func chebps(x int16, f []int16, n int) int16 { + pf := 1 + lTemp := int32(0x01000000) + t0 := (int32(x) << 10) + (int32(f[pf]) << 14) + pf++ + b1h := int16(t0 >> 16) + b1l := int16((t0 >> 1) - (int32(b1h) << 15)) + + for i := 2; i < n; i++ { + t0 = int32(b1h) * int32(x) + t0 += (int32(b1l) * int32(x)) >> 15 + t0 <<= 2 + t0 -= lTemp + t0 += int32(f[pf]) << 14 + pf++ + lTemp = (int32(b1h) << 16) + (int32(b1l) << 1) + b1h = int16(t0 >> 16) + b1l = int16((t0 >> 1) - (int32(b1h) << 15)) + } + + t0 = int32(b1h) * int32(x) + t0 += (int32(b1l) * int32(x)) >> 15 + t0 <<= 1 + t0 -= lTemp + t0 += int32(f[pf]) << 13 + + if uint32(t0+33554432) < 67108863 { + return int16(t0 >> 10) + } else if t0 > 0x01ffffff { + return maxInt16 + } + return minInt16 +} + +// azLsp converts order-10 LP coefficients a[0:M+1] to line spectral pairs +// lsp[0:M]. oldLsp is reused if fewer than M roots are found. Mirrors Az_lsp. +func azLsp(a, lsp, oldLsp []int16) { + var f1, f2 [cNC + 1]int16 + f1[0] = 1024 + f2[0] = 1024 + for i := 0; i < cNC; i++ { + lt1 := int32(a[i+1]) + lt2 := int32(a[cM-i]) + x := int16((lt1 + lt2) >> 2) + y := int16((lt1 - lt2) >> 2) + x -= f1[i] + f1[i+1] = x + y += f2[i] + f2[i+1] = y + } + + nf := 0 + ip := 0 + coef := f1[:] + + xlow := grid[0] + ylow := chebps(xlow, coef, cNC) + + j := 0 + for nf < cM && j < 60 { + j++ + xhigh := xlow + yhigh := ylow + xlow = grid[j] + ylow = chebps(xlow, coef, cNC) + + if int32(ylow)*int32(yhigh) <= 0 { + for i := 4; i != 0; i-- { + xmid := (xlow >> 1) + (xhigh >> 1) + ymid := chebps(xmid, coef, cNC) + if int32(ylow)*int32(ymid) <= 0 { + yhigh = ymid + xhigh = xmid + } else { + ylow = ymid + xlow = xmid + } + } + + x := xhigh - xlow + yv := yhigh - ylow + var xint int16 + if yv == 0 { + xint = xlow + } else { + sign := yv + yv = abs_s(yv) + exp := norm_s(yv) + yv = shl(yv, exp) + yv = div_s(16383, yv) + yv = int16((int32(x) * int32(yv)) >> uint(19-exp)) + if sign < 0 { + yv = -yv + } + xint = int16(int32(xlow) - ((int32(ylow) * int32(yv)) >> 10)) + } + + lsp[nf] = xint + xlow = xint + nf++ + + if ip == 0 { + ip = 1 + coef = f2[:] + } else { + ip = 0 + coef = f1[:] + } + ylow = chebps(xlow, coef, cNC) + } + } + + if nf < cM { + copy(lsp[:cM], oldLsp[:cM]) + } +} diff --git a/internal/capability/transcode/amrnb/dsp_bits.go b/internal/capability/transcode/amrnb/dsp_bits.go new file mode 100644 index 0000000..7ea45b1 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_bits.go @@ -0,0 +1,50 @@ +package amrnb + +// Decoder-side bit de-serialization, ported from opencore-amrnb bits2prm.cpp +// (Bin2int/Bits2prm) and the IETF->ETS reordering in wmf_to_ets.cpp (used for +// MIME_IETF input). The chain inverts the encoder side: IETF storage bytes -> +// ETS serial bits (unpacked MSB-first then inverse-permuted by reorderBits) -> +// parameter array (each parameter read MSB-first from its bitno width). + +// bin2int reads nBits from bits[0:nBits], MSB first (bits[0] is the most +// significant bit), and returns the assembled value. Mirrors Bin2int. +func bin2int(nBits int, bits []int16) int16 { + var v int16 + for i := 0; i < nBits; i++ { + v = (v << 1) | (bits[i] & 1) + } + return v +} + +// ietfToEts unpacks the RFC 4867 speech payload bytes (IETF storage order, +// MSB-first) and inverse-permutes them via reorderBits[mode] into the ETS +// serial bit array. Mirrors the speech branch of wmf_to_ets. +func ietfToEts(mode Mode, data []byte) []int16 { + nb := modeBits[mode] + ro := reorderBits[mode] + ets := make([]int16, nb) + for i := 0; i < nb; i++ { + bit := (data[i>>3] >> uint(7-(i&7))) & 1 + ets[ro[i]] = int16(bit) + } + return ets +} + +// bits2prm reads the ETS serial bit array back into the mode's parameter array. +// Mirrors Bits2prm. +func bits2prm(mode Mode, ets []int16) []int16 { + bn := bitno[mode] + prm := make([]int16, len(bn)) + pos := 0 + for i, w := range bn { + prm[i] = bin2int(int(w), ets[pos:]) + pos += int(w) + } + return prm +} + +// mimeUnsort decodes the RFC 4867 speech payload bytes for the given mode into +// the decoder parameter array (the inverse of packFrame). +func mimeUnsort(data []byte, mode Mode) []int16 { + return bits2prm(mode, ietfToEts(mode, data)) +} diff --git a/internal/capability/transcode/amrnb/dsp_const.go b/internal/capability/transcode/amrnb/dsp_const.go new file mode 100644 index 0000000..f6c52f1 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_const.go @@ -0,0 +1,54 @@ +package amrnb + +// Core AMR-NB signal-processing constants, from opencore-amrnb cnst.h and +// mode.h. AMR-NB codes 8 kHz speech in 20 ms frames of 160 samples, with an +// order-10 LP filter and four 40-sample subframes. + +const ( + cL_FRAME = 160 // frame size (samples) + cL_FRAME_BY2 = 80 // frame size / 2 + cL_SUBFR = 40 // subframe size + cL_CODE = 40 // codevector length + cNB_SUBFR = 4 // subframes per frame + cL_WINDOW = 240 // LP analysis window size + cL_NEXT = 40 // LP analysis look-ahead + cL_TOTAL = 320 // total speech analysis buffer + cM = 10 // LP filter order + cMP1 = 11 // LP filter order + 1 + cL_INTERPOL = 11 // interpolation filter length (10+1) + cPIT_MIN = 20 // minimum pitch lag + cPIT_MAX = 143 // maximum pitch lag + + cAMR_NB_PCM_FRAME = 160 // PCM samples produced/consumed per frame + cMAX_PRM_SIZE = 57 // max number of serial parameters (MR122) +) + +// Internal coding-mode enum, matching opencore-amrnb mode.h (enum Mode). These +// are the DSP-side mode indices; the public Mode type shares the speech values +// 0..7, with dMRDTX used for SID/comfort-noise handling. +const ( + dMR475 = iota + dMR515 + dMR59 + dMR67 + dMR74 + dMR795 + dMR102 + dMR122 + dMRDTX + dNMODES +) + +// Receive frame types, matching opencore-amrnb enum RXFrameType. The decoder +// driver maps RFC 4867 ToC frame types onto these. +const ( + cRX_SPEECH_GOOD = iota + cRX_SPEECH_DEGRADED + cRX_ONSET + cRX_SPEECH_BAD + cRX_SID_FIRST + cRX_SID_UPDATE + cRX_SID_BAD + cRX_NO_DATA + cRX_N_FRAMETYPES +) diff --git a/internal/capability/transcode/amrnb/dsp_dec_cb.go b/internal/capability/transcode/amrnb/dsp_dec_cb.go new file mode 100644 index 0000000..efe74f9 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_dec_cb.go @@ -0,0 +1,262 @@ +package amrnb + +// Per-mode algebraic (fixed) codebook decoders, ported from opencore-amrnb +// d2_9pf.cpp, d2_11pf.cpp, d3_14pf.cpp, d4_17pf.cpp, d8_31pf.cpp and +// d1035pf.cpp. Each reconstructs the 40-sample innovation vector cod[] from a +// mode's pulse-position and sign indices (GSM-style tracks, Gray-coded for the +// 4/8/10-pulse modes). Pulse amplitudes are ±8191/∓8192 (or ±4096 for MR122), +// matching the reference exactly. + +// pulse amplitude for the ±1.0 codebook entries (MR122 uses ±4096). +const cPOS_CODE = 8191 + +// decode2i40_9bits decodes the 2-pulse codebook for MR475/MR515. Mirrors +// decode_2i40_9bits (d2_9pf.cpp); subNr is the subframe number. +func decode2i40_9bits(subNr, sign, index int16, cod []int16) { + var pos [2]int16 + + j := index & 64 + j >>= 3 + i := index & 7 + k := shl(subNr, 1) + k += j + pos[0] = i*5 + startPos[k] + k++ + + index >>= 3 + i = index & 7 + pos[1] = i*5 + startPos[k] + + for n := 0; n < cL_SUBFR; n++ { + cod[n] = 0 + } + for j := 0; j < 2; j++ { + s := sign & 0x1 + cod[pos[j]] = s*16383 - 8192 + sign >>= 1 + } +} + +// decode2i40_11bits decodes the 2-pulse codebook for MR59. Mirrors +// decode_2i40_11bits (d2_11pf.cpp). +func decode2i40_11bits(sign, index int16, cod []int16) { + var pos [2]int16 + + j := index & 0x1 + index >>= 1 + i := index & 0x7 + pos[0] = i*5 + j*2 + 1 + + index >>= 3 + j = index & 0x3 + index >>= 2 + i = index & 0x7 + if j == 3 { + pos[1] = i*5 + 4 + } else { + pos[1] = i*5 + j + } + + for n := 0; n < cL_SUBFR; n++ { + cod[n] = 0 + } + for j := 0; j < 2; j++ { + s := sign & 1 + cod[pos[j]] = s*16383 - 8192 + sign >>= 1 + } +} + +// decode3i40_14bits decodes the 3-pulse codebook for MR67. Mirrors +// decode_3i40_14bits (d3_14pf.cpp). +func decode3i40_14bits(sign, index int16, cod []int16) { + var pos [3]int16 + + i := index & 0x7 + pos[0] = i * 5 + + index >>= 3 + j := index & 0x1 + index >>= 1 + i = index & 0x7 + pos[1] = i*5 + j*2 + 1 + + index >>= 3 + j = index & 0x1 + index >>= 1 + i = index & 0x7 + pos[2] = i*5 + j*2 + 2 + + for n := 0; n < cL_SUBFR; n++ { + cod[n] = 0 + } + for j := 0; j < 3; j++ { + s := sign & 1 + cod[pos[j]] = s*16383 - 8192 + sign >>= 1 + } +} + +// decode4i40_17bits decodes the 4-pulse Gray-coded codebook for MR74/MR795. +// Mirrors decode_4i40_17bits (d4_17pf.cpp). +func decode4i40_17bits(sign, index int16, cod []int16) { + var pos [4]int16 + + i := index & 0x7 + i = dgray[i] + pos[0] = i * 5 + + index >>= 3 + i = index & 0x7 + i = dgray[i] + pos[1] = i*5 + 1 + + index >>= 3 + i = index & 0x7 + i = dgray[i] + pos[2] = i*5 + 2 + + index >>= 3 + j := index & 0x1 + index >>= 1 + i = index & 0x7 + i = dgray[i] + pos[3] = i*5 + 3 + j + + for n := 0; n < cL_SUBFR; n++ { + cod[n] = 0 + } + for j := 0; j < 4; j++ { + s := sign & 0x1 + cod[pos[j]] = s*16383 - 8192 + sign >>= 1 + } +} + +// decompress10 expands one compressed 10x10x10 track index into three pulse +// positions. Mirrors decompress10 (d8_31pf.cpp). +func decompress10(MSBs, LSBs, index1, index2, index3 int16, posIndx []int16) { + if MSBs > 124 { + MSBs = 124 + } + ia := mult(MSBs, 1311) + ia = int16(int32(MSBs) - (L_mult(ia, 25) >> 1)) + ib := mult(ia, 6554) + ib = ia - int16(L_mult(ib, 5)>>1) + ib = shl(ib, 1) + ic := LSBs - ((LSBs >> 2) << 2) + posIndx[index1] = ib + (ic & 1) + + ib = mult(ia, 6554) + ib = shl(ib, 1) + posIndx[index2] = ib + (ic >> 1) + + ib = LSBs >> 2 + ic = mult(MSBs, 1311) + ic = shl(ic, 1) + posIndx[index3] = add(ib, ic) +} + +// decompressCode expands the eight compressed MR102 pulse indices into linear +// signs and codeword positions. Mirrors decompress_code (d8_31pf.cpp). +func decompressCode(indx, signIndx, posIndx []int16) { + const nbTrack = 4 + for i := 0; i < nbTrack; i++ { + signIndx[i] = indx[i] + } + + MSBs := indx[nbTrack] >> 3 + LSBs := indx[nbTrack] & 0x7 + decompress10(MSBs, LSBs, 0, 4, 1, posIndx) + + MSBs = indx[nbTrack+1] >> 3 + LSBs = indx[nbTrack+1] & 0x7 + decompress10(MSBs, LSBs, 2, 6, 5, posIndx) + + MSBs = indx[nbTrack+2] >> 2 + LSBs = indx[nbTrack+2] & 0x3 + ia := int16(L_mult(MSBs, 25) >> 1) + ia += 12 + MSBs0_24 := ia >> 5 + + ia = mult(MSBs0_24, 6554) + ia &= 1 + ib := mult(MSBs0_24, 6554) + ib = MSBs0_24 - int16(L_mult(ib, 5)>>1) + if ia == 1 { + ib = 4 - ib + } + ib = shl(ib, 1) + ia = LSBs & 0x1 + posIndx[3] = add(ib, ia) + + ia = mult(MSBs0_24, 6554) + ia = shl(ia, 1) + posIndx[7] = ia + (LSBs >> 1) +} + +// dec8i40_31bits decodes the 8-pulse codebook for MR102. Mirrors +// dec_8i40_31bits (d8_31pf.cpp). +func dec8i40_31bits(index, cod []int16) { + const nbTrack = 4 + var linearSigns [nbTrack]int16 + var linearCodewords [8]int16 + + for i := 0; i < cL_CODE; i++ { + cod[i] = 0 + } + decompressCode(index, linearSigns[:], linearCodewords[:]) + + for j := int16(0); j < nbTrack; j++ { + pos1 := (linearCodewords[j] << 2) + j + var sign int16 + if linearSigns[j] == 0 { + sign = cPOS_CODE // +1.0 + } else { + sign = -cPOS_CODE // -1.0 + } + if pos1 < cL_SUBFR { + cod[pos1] = sign + } + pos2 := (linearCodewords[j+4] << 2) + j + if pos2 < pos1 { + sign = negate(sign) + } + if pos2 < cL_SUBFR { + cod[pos2] += sign + } + } +} + +// dec10i40_35bits decodes the 10-pulse Gray-coded codebook for MR122. Mirrors +// dec_10i40_35bits (d1035pf.cpp). +func dec10i40_35bits(index, cod []int16) { + const nbTrack = 5 + for i := 0; i < cL_CODE; i++ { + cod[i] = 0 + } + for j := int16(0); j < nbTrack; j++ { + tmp := index[j] + i := tmp & 7 + i = dgray[i] + i = i * 5 + pos1 := i + j + + var sign int16 + if (tmp>>3)&1 == 0 { + sign = 4096 // +1.0 + } else { + sign = -4096 // -1.0 + } + cod[pos1] = sign + + i = index[j+5] & 7 + i = dgray[i] + i = i * 5 + pos2 := i + j + if pos2 < pos1 { + sign = negate(sign) + } + cod[pos2] += sign + } +} diff --git a/internal/capability/transcode/amrnb/dsp_dec_gain.go b/internal/capability/transcode/amrnb/dsp_dec_gain.go new file mode 100644 index 0000000..13f4d16 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_dec_gain.go @@ -0,0 +1,101 @@ +package amrnb + +// Gain dequantization, ported from opencore-amrnb d_gain_p.cpp, d_gain_c.cpp and +// dec_gain.cpp. These turn the received gain indices into the per-subframe pitch +// gain and fixed-codebook gain, driving the gc_pred energy predictor. dGainPitch +// and dGainCode handle MR795/MR122; decGain handles the joint/scalar-VQ modes +// (MR475 joint, MR515/MR59 low-rate, MR67/MR74/MR102 high-rate). Bit-exact with +// the reference. + +const cMR475_VQ_SIZE = 256 + +// dGainPitch returns the quantized pitch gain (Q14) for the given index. +// Mirrors d_gain_pitch. +func dGainPitch(mode int, index int16) int16 { + gain := qua_gain_pitch[index] + if mode == dMR122 { + gain &^= 3 // clear 2 LSBits + } + return gain +} + +// dGainCode decodes the fixed-codebook gain for MR795/MR122 from the predicted +// gain (gc_pred) and the quantized correction factor, then updates the +// predictor. Mirrors d_gain_code. +func (pred *gcPredState) dGainCode(mode int, index int16, code []int16) (gainCode int16) { + r := pred.gcPred(mode, code) + exp, frac := r.expGcode0, r.fracGcode0 + + index &= 31 // index < 32, avoid buffer overflow + tblTmp := index + (index << 1) + p := qua_gain_code[tblTmp:] + + if mode == dMR122 { + gcode0 := int16(pow2(exp, frac)) + gcode0 = shl(gcode0, 4) + gainCode = shl(mult(gcode0, p[0]), 1) + } else { + gcode0 := int16(pow2(14, frac)) + lTmp := L_mult(p[0], gcode0) + lTmp = L_shr(lTmp, sub(9, exp)) + gainCode = int16(lTmp >> 16) // Q1 + } + + quaEnerMR122 := p[1] + quaEner := p[2] + pred.update(quaEnerMR122, quaEner) + return gainCode +} + +// decGain decodes both the pitch gain and fixed-codebook gain for the +// scalar/joint-VQ modes (all except MR795/MR122), updating the predictor. +// evenSubfr selects the MR475 joint-gain half. Mirrors Dec_gain. +func (pred *gcPredState) decGain(mode int, index int16, code []int16, evenSubfr int16) (gainPit, gainCod int16) { + index = shl(index, 2) + + var gCode, quaEnerMR122, quaEner int16 + + switch { + case mode == dMR102 || mode == dMR74 || mode == dMR67: + p := table_gain_highrates[index:] + gainPit = p[0] + gCode = p[1] + quaEnerMR122 = p[2] + quaEner = p[3] + case mode == dMR475: + index += (1 ^ evenSubfr) << 1 // evenSubfr is 0 or 1 + if index > int16(cMR475_VQ_SIZE*4-2) { + index = int16(cMR475_VQ_SIZE*4 - 2) + } + p := table_gain_MR475[index:] + gainPit = p[0] + gCode = p[1] + // Predictor update values are recomputed (not stored in the 4.75 table): + // qua_ener_MR122 = log2(g), qua_ener = 20*log10(g) + e, f := log2(int32(gCode)) // Log2(x Q12) = log2(x) + 12 + e -= 12 + t1 := shr_r(f, 5) + t2 := shl(e, 10) + quaEnerMR122 = add(t1, t2) + lTmp := mpy32_16(e, f, 24660) // 24660 Q12 ~= 20*log10(2) + lTmp = L_shl(lTmp, 13) + quaEner = round_(lTmp) + default: // MR515, MR59 + p := table_gain_lowrates[index:] + gainPit = p[0] + gCode = p[1] + quaEnerMR122 = p[2] + quaEner = p[3] + } + + r := pred.gcPred(mode, code) + exp, frac := r.expGcode0, r.fracGcode0 + gcode0 := int16(pow2(14, frac)) + + lTmp := L_mult(gCode, gcode0) + lTmp = L_shr(lTmp, 10-exp) + gainCod = int16(lTmp >> 16) + + pred.update(quaEnerMR122, quaEner) + return gainPit, gainCod +} diff --git a/internal/capability/transcode/amrnb/dsp_dec_lsf.go b/internal/capability/transcode/amrnb/dsp_dec_lsf.go new file mode 100644 index 0000000..7a09274 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_dec_lsf.go @@ -0,0 +1,223 @@ +package amrnb + +// LSF dequantization, ported from opencore-amrnb d_plsf.cpp, d_plsf_3.cpp, +// d_plsf_5.cpp and reorder.cpp. These reconstruct the line-spectral +// frequencies from their transmitted codebook indices through a moving-average +// predictor (state carried across frames), enforce a minimum LSF spacing, and +// convert to the cosine (LSP) domain via lsfLsp. d_plsf_3 serves all modes +// except 12.2 kbit/s; d_plsf_5 serves 12.2 kbit/s (two LSP sets per frame). + +const ( + cLSF_GAP = 205 // minimum distance between adjacent LSFs (reorder) + + cALPHA_3 = 29491 // 0.9 (d_plsf_3 bad-frame lean toward mean) + cONE_ALPHA_3 = 3277 // 0.1 + cALPHA_5 = 31128 // ~0.95 (d_plsf_5 bad-frame) + cONE_ALPHA_5 = 1639 // ~0.05 + + cLSP_PRED_FAC_MR122 = 21299 // 0.65 Q15, MR122 LSP prediction factor + + cDICO1_SIZE = 256 + cDICO2_SIZE = 512 + cDICO3_SIZE = 512 + cMR515_3_SIZE = 128 + cMR795_1_SIZE = 512 +) + +// dPlsfState holds the LSF predictor memory across frames. Mirrors D_plsfState. +type dPlsfState struct { + pastRQ [cM]int16 // past quantized prediction error, Q15 + pastLsfQ [cM]int16 // past dequantized LSFs, Q15 +} + +// reset clears the LSF predictor: past residual to zero, past LSFs to the +// 5-table mean. Mirrors D_plsf_reset. +func (st *dPlsfState) reset() { + for i := 0; i < cM; i++ { + st.pastRQ[i] = 0 + } + copy(st.pastLsfQ[:], mean_lsf_5[:cM]) +} + +// reorderLsf enforces a minimum distance minDist between successive LSFs. +// Mirrors Reorder_lsf (reorder.cpp). +func reorderLsf(lsf []int16, minDist, n int16) { + lsfMin := minDist + for i := int16(0); i < n; i++ { + if lsf[i] < lsfMin { + lsf[i] = lsfMin + lsfMin = lsfMin + minDist + } else { + lsfMin = lsf[i] + minDist + } + } +} + +// dPlsf3 decodes the 3-split LSF for the given mode from three indices into +// lsp1q (length cM). bfi selects the bad-frame concealment path. Mirrors +// D_plsf_3. +func (st *dPlsfState) dPlsf3(mode int, bfi int16, indice []int16, lsp1q []int16) { + var lsf1r, lsf1q [cM]int16 + + if bfi != 0 { + for i := 0; i < cM; i++ { + temp := mult(st.pastLsfQ[i], cALPHA_3) + index := mult(mean_lsf_3[i], cONE_ALPHA_3) + lsf1q[i] = add(index, temp) + } + if mode != dMRDTX { + for i := 0; i < cM; i++ { + temp := mult(st.pastRQ[i], pred_fac_3[i]) + temp = add(mean_lsf_3[i], temp) + st.pastRQ[i] = sub(lsf1q[i], temp) + } + } else { + for i := 0; i < cM; i++ { + temp := add(mean_lsf_3[i], st.pastRQ[i]) + st.pastRQ[i] = sub(lsf1q[i], temp) + } + } + } else { + var pCb1, pCb3 []int16 + var indexLimit1, indexLimit3 int16 + pCb2 := dico2_lsf_3 + indexLimit2 := int16((cDICO2_SIZE - 1) * 3) + switch { + case mode == dMR475 || mode == dMR515: + pCb1, pCb3 = dico1_lsf_3, mr515_3_lsf + indexLimit1, indexLimit3 = (cDICO1_SIZE-1)*3, (cMR515_3_SIZE-1)*4 + case mode == dMR795: + pCb1, pCb3 = mr795_1_lsf, dico3_lsf_3 + indexLimit1, indexLimit3 = (cMR795_1_SIZE-1)*3, (cDICO3_SIZE-1)*4 + default: // MR59, MR67, MR74, MR102, MRDTX + pCb1, pCb3 = dico1_lsf_3, dico3_lsf_3 + indexLimit1, indexLimit3 = (cDICO1_SIZE-1)*3, (cDICO3_SIZE-1)*4 + } + + index := indice[0] + temp := index + (index << 1) + if temp > indexLimit1 { + temp = indexLimit1 + } + lsf1r[0] = pCb1[temp] + lsf1r[1] = pCb1[temp+1] + lsf1r[2] = pCb1[temp+2] + + index = indice[1] + if mode == dMR475 || mode == dMR515 { + index <<= 1 // these modes use every second entry + } + temp = index + (index << 1) + if temp > indexLimit2 { + temp = indexLimit2 + } + lsf1r[3] = pCb2[temp] + lsf1r[4] = pCb2[temp+1] + lsf1r[5] = pCb2[temp+2] + + index = indice[2] + temp = index << 2 + if temp > indexLimit3 { + temp = indexLimit3 + } + lsf1r[6] = pCb3[temp] + lsf1r[7] = pCb3[temp+1] + lsf1r[8] = pCb3[temp+2] + lsf1r[9] = pCb3[temp+3] + + if mode != dMRDTX { + for i := 0; i < cM; i++ { + temp = mult(st.pastRQ[i], pred_fac_3[i]) + temp = add(mean_lsf_3[i], temp) + lsf1q[i] = add(lsf1r[i], temp) + st.pastRQ[i] = lsf1r[i] + } + } else { + for i := 0; i < cM; i++ { + temp = add(mean_lsf_3[i], st.pastRQ[i]) + lsf1q[i] = add(lsf1r[i], temp) + st.pastRQ[i] = lsf1r[i] + } + } + } + + reorderLsf(lsf1q[:], cLSF_GAP, cM) + copy(st.pastLsfQ[:], lsf1q[:]) + lsfLsp(lsf1q[:], lsp1q, cM) +} + +// dPlsf5 decodes the 5-split LSF (MR122) from five indices into two LSP sets +// lsp1q and lsp2q (each length cM). Mirrors D_plsf_5. +func (st *dPlsfState) dPlsf5(bfi int16, indice []int16, lsp1q, lsp2q []int16) { + var lsf1r, lsf2r, lsf1q, lsf2q [cM]int16 + + if bfi != 0 { + for i := 0; i < cM; i++ { + temp := int16((int32(st.pastLsfQ[i]) * cALPHA_5) >> 15) + sign := int16((int32(mean_lsf_5[i]) * cONE_ALPHA_5) >> 15) + lsf1q[i] = add(sign, temp) + lsf2q[i] = lsf1q[i] + temp = int16((int32(st.pastRQ[i]) * cLSP_PRED_FAC_MR122) >> 15) + temp = add(mean_lsf_5[i], temp) + st.pastRQ[i] = sub(lsf2q[i], temp) + } + } else { + temp := shl(indice[0], 2) + lsf1r[0] = dico1_lsf_5[temp] + lsf1r[1] = dico1_lsf_5[temp+1] + lsf2r[0] = dico1_lsf_5[temp+2] + lsf2r[1] = dico1_lsf_5[temp+3] + + temp = shl(indice[1], 2) + lsf1r[2] = dico2_lsf_5[temp] + lsf1r[3] = dico2_lsf_5[temp+1] + lsf2r[2] = dico2_lsf_5[temp+2] + lsf2r[3] = dico2_lsf_5[temp+3] + + sign := indice[2] & 1 + var ii int16 + if indice[2] < 0 { + ii = ^(^indice[2] >> 1) + } else { + ii = indice[2] >> 1 + } + temp = shl(ii, 2) + if sign == 0 { + lsf1r[4] = dico3_lsf_5[temp] + lsf1r[5] = dico3_lsf_5[temp+1] + lsf2r[4] = dico3_lsf_5[temp+2] + lsf2r[5] = dico3_lsf_5[temp+3] + } else { + lsf1r[4] = negate(dico3_lsf_5[temp]) + lsf1r[5] = negate(dico3_lsf_5[temp+1]) + lsf2r[4] = negate(dico3_lsf_5[temp+2]) + lsf2r[5] = negate(dico3_lsf_5[temp+3]) + } + + temp = shl(indice[3], 2) + lsf1r[6] = dico4_lsf_5[temp] + lsf1r[7] = dico4_lsf_5[temp+1] + lsf2r[6] = dico4_lsf_5[temp+2] + lsf2r[7] = dico4_lsf_5[temp+3] + + temp = shl(indice[4], 2) + lsf1r[8] = dico5_lsf_5[temp] + lsf1r[9] = dico5_lsf_5[temp+1] + lsf2r[8] = dico5_lsf_5[temp+2] + lsf2r[9] = dico5_lsf_5[temp+3] + + for i := 0; i < cM; i++ { + temp = mult(st.pastRQ[i], cLSP_PRED_FAC_MR122) + temp = add(mean_lsf_5[i], temp) + lsf1q[i] = add(lsf1r[i], temp) + lsf2q[i] = add(lsf2r[i], temp) + st.pastRQ[i] = lsf2r[i] + } + } + + reorderLsf(lsf1q[:], cLSF_GAP, cM) + reorderLsf(lsf2q[:], cLSF_GAP, cM) + copy(st.pastLsfQ[:], lsf2q[:]) + lsfLsp(lsf1q[:], lsp1q, cM) + lsfLsp(lsf2q[:], lsp2q, cM) +} diff --git a/internal/capability/transcode/amrnb/dsp_decoder.go b/internal/capability/transcode/amrnb/dsp_decoder.go new file mode 100644 index 0000000..d3e5ed1 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_decoder.go @@ -0,0 +1,242 @@ +package amrnb + +// AMR-NB decoder frame driver, ported from opencore-amrnb dec_amr.cpp +// (Decoder_amr) and sp_dec.cpp (GSMFrameDecode). It orchestrates the +// already-ported building blocks for one 20 ms frame: LSF dequantization and +// LSP interpolation, then four subframes of pitch-lag decode, adaptive +// codebook, fixed codebook with pitch sharpening, gain decode, excitation +// assembly and LP synthesis; finally the formant post-filter and output +// high-pass. +// +// This implements the active-speech (good-frame) path. Error concealment +// (bad-frame / DTX), background-noise adaptation, the gain-averaging +// (Cb_gain_average) and adaptive phase dispersion are not yet ported, so the +// excitation is synthesized directly from the total excitation (equivalent to +// phase dispersion disabled) and the code gain is used unmixed. + +const ( + cSHARPMAX = 13017 // max pitch sharpening + cExcOff = cPIT_MAX + cL_INTERPOL // 154: excitation history length + cPIT_MIN_MR122 = 18 +) + +// decoderStateDSP holds the persistent decoder synthesis state across frames. +type decoderStateDSP struct { + oldExc [cL_SUBFR + cPIT_MAX + cL_INTERPOL]int16 // excitation w/ history + lspOld [cM]int16 // previous-frame LSPs + memSyn [cM]int16 // synthesis filter memory + sharp int16 // pitch sharpening (prev gain_pit) + oldT0 int16 // previous integer pitch lag + lsf dPlsfState // LSF predictor state + pred gcPredState // gain predictor state + pst postFilterState // formant post-filter state + post postProcessState // output HP filter state +} + +func (st *decoderStateDSP) reset() { + for i := range st.oldExc { + st.oldExc[i] = 0 + } + copy(st.lspOld[:], lsp_init_data[:cM]) + for i := range st.memSyn { + st.memSyn[i] = 0 + } + st.sharp = 0 + st.oldT0 = 40 + st.lsf.reset() + st.pred.reset() + st.pst.reset() + st.post = postProcessState{} +} + +// sharpenCode applies pitch sharpening to the innovation: code[i] += pitSharp · +// code[i-T0] for i in [T0, L_SUBFR). +func sharpenCode(code []int16, T0 int, pitSharp int16) { + for i := T0; i < cL_SUBFR; i++ { + code[i] = add(code[i], mult(code[i-T0], pitSharp)) + } +} + +// decodeAmr decodes one frame's parameters into 160 raw synthesis samples and +// returns the four per-subframe LP coefficient sets for the post-filter. +func (st *decoderStateDSP) decodeAmr(mode int, params []int16, synth []int16) []int16 { + var lspMid, lspNew [cM]int16 + at := make([]int16, cNB_SUBFR*cMP1) + + pi := 0 + if mode == dMR122 { + st.lsf.dPlsf5(0, params[pi:pi+5], lspMid[:], lspNew[:]) + pi += 5 + intLpc1and3(st.lspOld[:], lspMid[:], lspNew[:], at) + } else { + st.lsf.dPlsf3(mode, 0, params[pi:pi+3], lspNew[:]) + pi += 3 + intLpc1to3(st.lspOld[:], lspNew[:], at) + } + copy(st.lspOld[:], lspNew[:]) + + var code [cL_SUBFR]int16 + exc := st.oldExc[cExcOff:] // current-subframe excitation window (40 samples) + + azOff := 0 + subfrNr := -1 + evenSubfr := int16(0) + var indexMR475 int16 // carried across the MR475 subframe pair + + for iSubfr := 0; iSubfr < cL_FRAME; iSubfr += cL_SUBFR { + subfrNr++ + evenSubfr = 1 - evenSubfr + az := at[azOff:] + + pitFlag := int16(iSubfr) + if iSubfr == cL_FRAME_BY2 && mode != dMR475 && mode != dMR515 { + pitFlag = 0 + } + + // ---- pitch lag + adaptive codebook ---- + index := params[pi] + pi++ + var T0, T0frac int16 + if mode != dMR122 { + flag4 := int16(0) + if mode == dMR475 || mode == dMR515 || mode == dMR59 || mode == dMR67 { + flag4 = 1 + } + deltaLow, deltaRange := int16(5), int16(9) + if mode == dMR795 { + deltaLow, deltaRange = 10, 19 + } + t0min := sub(st.oldT0, deltaLow) + if t0min < cPIT_MIN { + t0min = cPIT_MIN + } + t0max := add(t0min, deltaRange) + if t0max > cPIT_MAX { + t0max = cPIT_MAX + t0min = sub(t0max, deltaRange) + } + T0, T0frac = decLag3(index, t0min, t0max, pitFlag, st.oldT0, flag4) + predLt(st.oldExc[:], cExcOff, T0, T0frac, cL_SUBFR, 1) + } else { + T0, T0frac = decLag6(index, cPIT_MIN_MR122, cPIT_MAX, pitFlag, st.oldT0) + predLt(st.oldExc[:], cExcOff, T0, T0frac, cL_SUBFR, 0) + } + + // ---- fixed codebook + pitch sharpening ---- + var gainPit, gainCode int16 + switch mode { + case dMR475, dMR515: + posIdx := params[pi] + pi++ + signs := params[pi] + pi++ + decode2i40_9bits(int16(subfrNr), signs, posIdx, code[:]) + sharpenCode(code[:], int(T0), shl(st.sharp, 1)) + case dMR59: + posIdx := params[pi] + pi++ + signs := params[pi] + pi++ + decode2i40_11bits(signs, posIdx, code[:]) + sharpenCode(code[:], int(T0), shl(st.sharp, 1)) + case dMR67: + posIdx := params[pi] + pi++ + signs := params[pi] + pi++ + decode3i40_14bits(signs, posIdx, code[:]) + sharpenCode(code[:], int(T0), shl(st.sharp, 1)) + case dMR74, dMR795: + posIdx := params[pi] + pi++ + signs := params[pi] + pi++ + decode4i40_17bits(signs, posIdx, code[:]) + sharpenCode(code[:], int(T0), shl(st.sharp, 1)) + case dMR102: + dec8i40_31bits(params[pi:pi+7], code[:]) + pi += 7 + sharpenCode(code[:], int(T0), shl(st.sharp, 1)) + case dMR122: + gpIdx := params[pi] + pi++ + gainPit = dGainPitch(mode, gpIdx) + dec10i40_35bits(params[pi:pi+10], code[:]) + pi += 10 + sharpenCode(code[:], int(T0), shl(gainPit, 1)) + } + + // ---- gain decode ---- + switch mode { + case dMR475: + if evenSubfr != 0 { + indexMR475 = params[pi] + pi++ + } + gainPit, gainCode = st.pred.decGain(mode, indexMR475, code[:], evenSubfr) + case dMR515, dMR59, dMR67, dMR74, dMR102: + idx := params[pi] + pi++ + gainPit, gainCode = st.pred.decGain(mode, idx, code[:], evenSubfr) + case dMR795: + gpIdx := params[pi] + pi++ + gainPit = dGainPitch(mode, gpIdx) + gcIdx := params[pi] + pi++ + gainCode = st.pred.dGainCode(mode, gcIdx, code[:]) + case dMR122: + gcIdx := params[pi] + pi++ + gainCode = st.pred.dGainCode(mode, gcIdx, code[:]) + } + + // store pitch sharpening for next subframe (not in MR475 even subframe) + if mode != dMR475 || evenSubfr == 0 { + st.sharp = gainPit + if st.sharp > cSHARPMAX { + st.sharp = cSHARPMAX + } + } + + // ---- total excitation: gain_pit·adaptive + gain_code·code ---- + var pitchFac, tmpShift int16 + if mode <= dMR102 { + pitchFac = gainPit + tmpShift = 1 + } else { + pitchFac = shr(gainPit, 1) + tmpShift = 2 + } + for i := 0; i < cL_SUBFR; i++ { + L := L_mult(exc[i], pitchFac) + L = L_mac(L, code[i], gainCode) + L = L_shl(L, tmpShift) + exc[i] = round_(L) + } + + // ---- LP synthesis ---- + synFilt(az, exc[:cL_SUBFR], synth[iSubfr:iSubfr+cL_SUBFR], cL_SUBFR, st.memSyn[:], false) + copy(st.memSyn[:], synth[iSubfr+cL_SUBFR-cM:iSubfr+cL_SUBFR]) + + // slide the excitation buffer left by one subframe + copy(st.oldExc[:cExcOff], st.oldExc[cL_SUBFR:cL_SUBFR+cExcOff]) + + st.oldT0 = T0 + azOff += cMP1 + } + + return at +} + +// decodeSpeechFrame decodes one good speech frame end-to-end into 160 PCM +// samples: parameter decode, post-filter, output high-pass, and the 13-bit +// output truncation. Mirrors GSMFrameDecode for the speech path. +func (st *decoderStateDSP) decodeSpeechFrame(mode int, params []int16, synth []int16) { + az := st.decodeAmr(mode, params, synth) + st.pst.postFilter(mode, synth, az) + st.post.process(synth, cL_FRAME) + for i := 0; i < cL_FRAME; i++ { + synth[i] &^= 7 // truncate to 13 bits + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_bits.go b/internal/capability/transcode/amrnb/dsp_enc_bits.go new file mode 100644 index 0000000..d77a2f1 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_bits.go @@ -0,0 +1,50 @@ +package amrnb + +// Encoder-side bit serialization, ported from opencore-amrnb prm2bits.cpp +// (Int2bin/Prm2bits) and the ETS->IETF reordering in ets_to_wmf.cpp +// (ets_to_ietf). The chain is: parameter array -> ETS serial bits (each +// parameter expanded MSB-first to its bitno width) -> IETF storage bytes (the +// ETS bits permuted by reorderBits and packed MSB-first). The leading ToC byte +// is added by the payload layer, not here. + +// int2bin writes the low nBits of value into bits[0:nBits], MSB first +// (bits[0] is the most significant bit). Mirrors Int2bin. +func int2bin(value int16, nBits int, bits []int16) { + for i := nBits - 1; i >= 0; i-- { + bits[i] = value & 1 + value >>= 1 + } +} + +// prm2bits expands the encoder parameter array prm into the ETS serial bit +// array for the given speech mode (length modeBits[mode]). Mirrors Prm2bits. +func prm2bits(mode Mode, prm []int16) []int16 { + bn := bitno[mode] + ets := make([]int16, modeBits[mode]) + pos := 0 + for i, w := range bn { + int2bin(prm[i], int(w), ets[pos:]) + pos += int(w) + } + return ets +} + +// etsToIetf permutes the ETS serial bits into IETF storage order via +// reorderBits[mode] and packs them MSB-first into bytes. Mirrors the speech +// branch of ets_to_ietf (without the ToC byte). +func etsToIetf(mode Mode, ets []int16) []byte { + nb := modeBits[mode] + ro := reorderBits[mode] + out := make([]byte, (nb+7)/8) + for k := 0; k < nb; k++ { + bit := ets[ro[k]] & 1 + out[k>>3] |= byte(bit) << uint(7-(k&7)) + } + return out +} + +// packFrame serializes a mode's encoder parameters into the RFC 4867 speech +// payload bytes (IETF storage order), the inverse of mimeUnsort. +func packFrame(prm []int16, mode Mode) []byte { + return etsToIetf(mode, prm2bits(mode, prm)) +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cb10.go b/internal/capability/transcode/amrnb/dsp_enc_cb10.go new file mode 100644 index 0000000..1e332c2 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cb10.go @@ -0,0 +1,96 @@ +package amrnb + +// 10-pulse algebraic codebook search (35 bits, MR122), ported from +// opencore-amrnb c1035pf.cpp. Reuses search10and8i40 (10 pulses, step 5), then +// builds the codeword (±4096 pulses) and Gray-encodes the per-track position +// indices (q_p). The exact encoder counterpart of dec10i40_35bits. Bit-exact +// with the reference. + +// build10Code assembles the 10-pulse innovation cod, filtered code y, and the +// raw per-track position+sign indices (with the same-track ordering/swap logic +// that the decoder inverts). Mirrors build_code (c1035pf.cpp). +func build10Code(codvec, sign, cod, h, y, indx []int16) { + const L = cL_CODE + for i := 0; i < L; i++ { + cod[i] = 0 + } + for t := 0; t < 5; t++ { + indx[t] = -1 + } + + var sgn [10]int16 + for k := 0; k < 10; k++ { + i := codvec[k] + index := int16((int32(i) * 6554) >> 15) + track := i - (index + (index << 2)) // i - index*5 + + if sign[i] > 0 { + cod[i] += 4096 + sgn[k] = 8192 + } else { + cod[i] -= 4096 + sgn[k] = -8192 + index += 8 + } + + temp := indx[track] + switch { + case temp < 0: + indx[track] = index + case (index^temp)&8 == 0: + if temp <= index { + indx[track+5] = index + } else { + indx[track+5] = temp + indx[track] = index + } + default: + if (temp & 7) <= (index & 7) { + indx[track+5] = temp + indx[track] = index + } else { + indx[track+5] = index + } + } + } + + for i := 0; i < L; i++ { + var s int32 + for k := 0; k < 10; k++ { + ci := int(codvec[k]) + var hv int16 + if i-ci >= 0 { + hv = h[i-ci] + } + s += (int32(hv) * int32(sgn[k])) >> 7 + } + y[i] = int16((s + 0x80) >> 8) + } +} + +// code10i40_35bits runs the full MR122 10-pulse search, writing the innovation +// cod, filtered code y, and the 10-value Gray-coded index. Mirrors +// code_10i40_35bits. +func code10i40_35bits(x, cn, h, cod, y, indx []int16) { + var dn, sign [cL_CODE]int16 + var rr [cL_CODE * cL_CODE]int16 + var ipos, codvec [10]int16 + var posMax [5]int16 + + corHX(h, x, dn[:], 2) + setSign12k2(dn[:], cn, sign[:], posMax[:], 5, ipos[:], 5) + corH(h, sign[:], rr[:]) + search10and8i40(10, 5, 5, dn[:], rr[:], ipos[:], posMax[:], codvec[:]) + build10Code(codvec[:], sign[:], cod, h, y, indx) + + // q_p: Gray-encode each track's position index (keeping the sign bit for the + // first five). + for n := 0; n < 10; n++ { + tmp := indx[n] + if n < 5 { + indx[n] = (tmp & 0x8) | gray[tmp&0x7] + } else { + indx[n] = gray[tmp&0x7] + } + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cb2.go b/internal/capability/transcode/amrnb/dsp_enc_cb2.go new file mode 100644 index 0000000..39457e4 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cb2.go @@ -0,0 +1,150 @@ +package amrnb + +// 2-pulse algebraic codebook search (9 bits, MR475/MR515), ported from +// opencore-amrnb c2_9pf.cpp. code2i40_9bits sharpens the impulse response with +// the pitch gain, builds the correlation gradient (corHX) and the rr criterion +// matrix (corH), exhaustively searches the two pulse positions maximizing +// ps²/energy (search2i40), then assembles the innovation vector, its filtered +// version, and the codebook index (build2Code). The exact encoder counterpart +// of the decoder's decode2i40_9bits. Bit-exact with the reference. + +// per-subframe track table used to encode the pulse track-pair (c2_9pf.cpp). +var trackTable = [20]int16{ + 0, 1, 0, 1, -1, // subframe 1 + 0, -1, 1, 0, 1, // subframe 2 + 0, 1, 0, -1, 1, // subframe 3 + 0, 1, -1, 0, 1, // subframe 4 +} + +// search2i40 finds the two pulse positions (one per track of a track pair) that +// maximize the codebook criterion ps²/energy. Mirrors search_2i40. +func search2i40(subNr int16, dn, rr, codvec []int16) { + const L = cL_CODE + var ipos [2]int16 + psk := int16(-1) + alpk := int16(1) + codvec[0] = 0 + codvec[1] = 1 + + for track1 := int16(0); track1 < 2; track1++ { + i := (subNr << 1) + (track1 << 3) + ipos[0] = startPos[i] + ipos[1] = startPos[i+1] + + for i0 := ipos[0]; i0 < L; i0 += cSTEP { + ps0 := dn[i0] + alp0 := int32(rr[int(i0)*L+int(i0)]) << 14 + + sq := int16(-1) + alp := int16(1) + ix := ipos[1] + + for i1 := ipos[1]; i1 < L; i1 += cSTEP { + ps1 := ps0 + dn[i1] + alp1 := alp0 + (int32(rr[int(i1)*L+int(i1)]) << 14) + alp1 += int32(rr[int(i0)*L+int(i1)]) << 15 + sq1 := int16((int32(ps1) * int32(ps1)) >> 15) + alp16 := int16((alp1 + 0x8000) >> 16) + s := (int32(alp)*int32(sq1))<<1 - (int32(sq)*int32(alp16))<<1 + if s > 0 { + sq = sq1 + alp = alp16 + ix = i1 + } + } + + s := (int32(alpk)*int32(sq))<<1 - (int32(psk)*int32(alp))<<1 + if s > 0 { + psk = sq + alpk = alp + codvec[0] = i0 + codvec[1] = ix + } + } + } +} + +// build2Code assembles the innovation vector cod (two ±8191/∓8192 pulses), its +// filtered version y, the pulse signs, and the codebook index. Mirrors +// build_code (c2_9pf.cpp). +func build2Code(subNr int16, codvec, dnSign, cod, h, y []int16) (indx, sign int16) { + const L = cL_CODE + ptBase := int(subNr) + (int(subNr) << 2) // subNr*5 + for i := 0; i < L; i++ { + cod[i] = 0 + } + + var sgn [2]int16 + rsign := int16(0) + for k := 0; k < 2; k++ { + i := codvec[k] + j := dnSign[i] + index := int16((int32(i) * 6554) >> 15) // pos/5 + track := i - 5*index + first := trackTable[ptBase+int(track)] + if k == 0 { + track = 0 + if first != 0 { + index += 64 + } + } else { + track = 1 + index <<= 3 + } + if j > 0 { + cod[i] = 8191 + sgn[k] = 32767 + rsign += 1 << uint(track) + } else { + cod[i] = -8192 + sgn[k] = -32768 + } + indx += index + } + sign = rsign + + c0, c1 := int(codvec[0]), int(codvec[1]) + for i := 0; i < L; i++ { + var h0, h1 int16 + if i-c0 >= 0 { + h0 = h[i-c0] + } + if i-c1 >= 0 { + h1 = h[i-c1] + } + s := L_mult(h0, sgn[0]) + s = L_mac(s, h1, sgn[1]) + y[i] = round_(s) + } + return indx, sign +} + +// code2i40_9bits runs the full 2-pulse search for one subframe, returning the +// codebook index and pulse signs and writing the innovation code and its +// filtered version y. h is sharpened in place by the pitch gain. Mirrors +// code_2i40_9bits. +func code2i40_9bits(subNr int16, x, h []int16, T0, pitchSharp int16, code, y []int16) (index, sign int16) { + sharp := shl(pitchSharp, 1) + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + h[i] = add(h[i], mult(h[i-T0], sharp)) + } + } + + var dn, dn2, dnSign [cL_CODE]int16 + var rr [cL_CODE * cL_CODE]int16 + var codvec [2]int16 + + corHX(h, x, dn[:], 1) + setSign(dn[:], dnSign[:], dn2[:], 8) + corH(h, dnSign[:], rr[:]) + search2i40(subNr, dn[:], rr[:], codvec[:]) + index, sign = build2Code(subNr, codvec[:], dnSign[:], code, h, y) + + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + code[i] = add(code[i], mult(code[i-T0], sharp)) + } + } + return index, sign +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cb2_11.go b/internal/capability/transcode/amrnb/dsp_enc_cb2_11.go new file mode 100644 index 0000000..d7f4622 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cb2_11.go @@ -0,0 +1,154 @@ +package amrnb + +// 2-pulse algebraic codebook search (11 bits, MR59), ported from opencore-amrnb +// c2_11pf.cpp. Like c2_9pf but with a wider track structure (2×4 track pairs) +// and its own index encoding. Reuses corHX/setSign/corH. The exact encoder +// counterpart of decode2i40_11bits. Bit-exact with the reference. + +var startPos1 = [2]int16{1, 3} +var startPos2 = [4]int16{0, 1, 2, 4} + +// search2i40_11 finds the two pulse positions maximizing ps²/energy across the +// 2×4 track pairs. Mirrors search_2i40 (c2_11pf.cpp). +func search2i40_11(dn, rr, codvec []int16) { + const L = cL_CODE + var ipos [2]int16 + psk := int16(-1) + alpk := int16(1) + codvec[0] = 0 + codvec[1] = 1 + + for track1 := 0; track1 < 2; track1++ { + for track2 := 0; track2 < 4; track2++ { + ipos[0] = startPos1[track1] + ipos[1] = startPos2[track2] + + for i0 := ipos[0]; i0 < L; i0 += cSTEP { + ps0 := dn[i0] + alp0 := int32(rr[int(i0)*L+int(i0)]) << 14 + + sq := int16(-1) + alp := int16(1) + ix := ipos[1] + + for i1 := ipos[1]; i1 < L; i1 += cSTEP { + ps1 := add(ps0, dn[i1]) + alp1 := alp0 + (int32(rr[int(i1)*L+int(i1)]) << 14) + alp1 += int32(rr[int(i0)*L+int(i1)]) << 15 + sq1 := int16((int32(ps1) * int32(ps1)) >> 15) + alp16 := int16((alp1 + 0x8000) >> 16) + s := (int32(alp)*int32(sq1))<<1 - (int32(sq)*int32(alp16))<<1 + if s > 0 { + sq = sq1 + alp = alp16 + ix = i1 + } + } + + s := (int32(alpk)*int32(sq))<<1 - (int32(psk)*int32(alp))<<1 + if s > 0 { + psk = sq + alpk = alp + codvec[0] = i0 + codvec[1] = ix + } + } + } + } +} + +// build2Code11 assembles the innovation, filtered code, signs and the 11-bit +// index. Mirrors build_code (c2_11pf.cpp). +func build2Code11(codvec, dnSign, cod, h, y []int16) (indx, sign int16) { + const L = cL_CODE + for i := 0; i < L; i++ { + cod[i] = 0 + } + + var sgn [2]int16 + rsign := int16(0) + for k := 0; k < 2; k++ { + i := codvec[k] + j := dnSign[i] + index := int16((int32(i) * 6554) >> 15) + tw := (index << 3) + (index << 1) // index*10 + tw >>= 1 // index*5 + track := i - tw // i % 5 + + switch track { + case 0: + track = 1 + index <<= 6 + case 1: + if k == 0 { + track = 0 + index <<= 1 + } else { + track = 1 + index = (index << 6) + 16 + } + case 2: + track = 1 + index = (index << 6) + 32 + case 3: + track = 0 + index = (index << 1) + 1 + case 4: + track = 1 + index = (index << 6) + 48 + } + + if j > 0 { + cod[i] = 8191 + sgn[k] = 32767 + rsign = add(rsign, shl(1, track)) + } else { + cod[i] = -8192 + sgn[k] = -32768 + } + indx = add(indx, index) + } + sign = rsign + + c0, c1 := int(codvec[0]), int(codvec[1]) + for i := 0; i < L; i++ { + var h0, h1 int16 + if i-c0 >= 0 { + h0 = h[i-c0] + } + if i-c1 >= 0 { + h1 = h[i-c1] + } + s := L_mult(h0, sgn[0]) + s = L_mac(s, h1, sgn[1]) + y[i] = round_(s) + } + return indx, sign +} + +// code2i40_11bits runs the full MR59 2-pulse search. Mirrors code_2i40_11bits. +func code2i40_11bits(x, h []int16, T0, pitchSharp int16, code, y []int16) (index, sign int16) { + sharp := shl(pitchSharp, 1) + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + h[i] = add(h[i], mult(h[i-T0], sharp)) + } + } + + var dn, dn2, dnSign [cL_CODE]int16 + var rr [cL_CODE * cL_CODE]int16 + var codvec [2]int16 + + corHX(h, x, dn[:], 1) + setSign(dn[:], dnSign[:], dn2[:], 8) + corH(h, dnSign[:], rr[:]) + search2i40_11(dn[:], rr[:], codvec[:]) + index, sign = build2Code11(codvec[:], dnSign[:], code, h, y) + + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + code[i] = add(code[i], mult(code[i-T0], sharp)) + } + } + return index, sign +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cb3.go b/internal/capability/transcode/amrnb/dsp_enc_cb3.go new file mode 100644 index 0000000..da0326e --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cb3.go @@ -0,0 +1,174 @@ +package amrnb + +// 3-pulse algebraic codebook search (14 bits, MR67), ported from opencore-amrnb +// c3_14pf.cpp. A depth-first search: the first pulse ranges only over the +// pruned candidate positions (dn2), then nested searches place the 2nd and 3rd +// pulses; the track triple is cyclically rotated so each track gets the pruned +// role. The exact encoder counterpart of decode3i40_14bits. Bit-exact with the +// reference. + +// search3i40 places three pulses maximizing the codebook criterion. Mirrors +// search_3i40 (c3_14pf.cpp). +func search3i40(dn, dn2, rr, codvec []int16) { + const L = cL_CODE + var ipos [3]int16 + psk := int16(-1) + alpk := int16(1) + codvec[0], codvec[1], codvec[2] = 0, 1, 2 + + for track1 := int16(1); track1 < 4; track1 += 2 { + for track2 := int16(2); track2 < 5; track2 += 2 { + ipos[0], ipos[1], ipos[2] = 0, track1, track2 + + for rot := 0; rot < 3; rot++ { + for i0 := ipos[0]; i0 < L; i0 += cSTEP { + if dn2[i0] < 0 { + continue + } + ps0 := dn[i0] + alp0 := int32(rr[int(i0)*L+int(i0)]) << 14 + + sq := int16(-1) + alp := int16(1) + ps := int16(0) + ix := ipos[1] + for i1 := ipos[1]; i1 < L; i1 += cSTEP { + ps1 := ps0 + dn[i1] + alp1 := alp0 + (int32(rr[int(i1)*L+int(i1)]) << 14) + alp1 += int32(rr[int(i0)*L+int(i1)]) << 15 + sq1 := int16((int32(ps1) * int32(ps1)) >> 15) + alp16 := int16((alp1 + 0x8000) >> 16) + s := (int32(alp)*int32(sq1))<<1 - (int32(sq)*int32(alp16))<<1 + if s > 0 { + sq = sq1 + ps = ps1 + alp = alp16 + ix = i1 + } + } + i1 := ix + + ps0 = ps + alp0 = int32(alp) << 14 + sq = -1 + alp = 1 + ps = 0 + ix = ipos[2] + for i2 := ipos[2]; i2 < L; i2 += cSTEP { + ps1 := ps0 + dn[i2] + alp1 := alp0 + (int32(rr[int(i2)*L+int(i2)]) << 12) + alp1 += int32(rr[int(i1)*L+int(i2)]) << 13 + alp1 += int32(rr[int(i0)*L+int(i2)]) << 13 + sq1 := int16((int32(ps1) * int32(ps1)) >> 15) + alp16 := int16((alp1 + 0x8000) >> 16) + s := (int32(alp)*int32(sq1))<<1 - (int32(sq)*int32(alp16))<<1 + if s > 0 { + sq = sq1 + ps = ps1 + alp = alp16 + ix = i2 + } + } + i2 := ix + + s := L_msu(L_mult(alpk, sq), psk, alp) + if s > 0 { + psk = sq + alpk = alp + codvec[0], codvec[1], codvec[2] = i0, i1, i2 + } + } + pos := ipos[2] + ipos[2] = ipos[1] + ipos[1] = ipos[0] + ipos[0] = pos + } + } + } +} + +// build3Code assembles the 3-pulse innovation, filtered code, signs and 14-bit +// index. Mirrors build_code (c3_14pf.cpp). +func build3Code(codvec, dnSign, cod, h, y []int16) (indx, sign int16) { + const L = cL_CODE + for i := 0; i < L; i++ { + cod[i] = 0 + } + + var sgn [3]int16 + rsign := int16(0) + for k := 0; k < 3; k++ { + i := codvec[k] + j := dnSign[i] + index := int16((int32(i) * 6554) >> 15) + track := i - index*5 + switch track { + case 1: + index <<= 4 + case 2: + track = 2 + index <<= 8 + case 3: + track = 1 + index = (index << 4) + 8 + case 4: + track = 2 + index = (index << 8) + 128 + } + if j > 0 { + cod[i] = 8191 + sgn[k] = 32767 + track = 1 << uint(track) + rsign += track + } else { + cod[i] = -8192 + sgn[k] = -32768 + } + indx += index + } + sign = rsign + + c0, c1, c2 := int(codvec[0]), int(codvec[1]), int(codvec[2]) + for i := 0; i < L; i++ { + var h0, h1, h2 int16 + if i-c0 >= 0 { + h0 = h[i-c0] + } + if i-c1 >= 0 { + h1 = h[i-c1] + } + if i-c2 >= 0 { + h2 = h[i-c2] + } + s := L_mac(L_mac(L_mult(h0, sgn[0]), h1, sgn[1]), h2, sgn[2]) + y[i] = round_(s) + } + return indx, sign +} + +// code3i40_14bits runs the full MR67 3-pulse search. Mirrors code_3i40_14bits. +func code3i40_14bits(x, h []int16, T0, pitchSharp int16, code, y []int16) (index, sign int16) { + sharp := shl(pitchSharp, 1) + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + h[i] = add(h[i], mult(h[i-T0], sharp)) + } + } + + var dn, dn2, dnSign [cL_CODE]int16 + var rr [cL_CODE * cL_CODE]int16 + var codvec [3]int16 + + corHX(h, x, dn[:], 1) + setSign(dn[:], dnSign[:], dn2[:], 6) + corH(h, dnSign[:], rr[:]) + search3i40(dn[:], dn2[:], rr[:], codvec[:]) + index, sign = build3Code(codvec[:], dnSign[:], code, h, y) + + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + code[i] = add(code[i], mult(code[i-T0], sharp)) + } + } + return index, sign +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cb4.go b/internal/capability/transcode/amrnb/dsp_enc_cb4.go new file mode 100644 index 0000000..9ef3832 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cb4.go @@ -0,0 +1,175 @@ +package amrnb + +// 4-pulse algebraic codebook search (17 bits, MR74/MR795), ported from +// opencore-amrnb c4_17pf.cpp. A depth-first search over four pulses with dn2 +// pruning of the first pulse and cyclic track rotation; the index is Gray-coded +// per position. The exact encoder counterpart of decode4i40_17bits. Bit-exact +// with the reference. + +// search4i40 places four pulses maximizing the codebook criterion. Mirrors +// search_4i40 (c4_17pf.cpp). +func search4i40(dn, dn2, rr, codvec []int16) { + const L = cL_CODE + var ipos [4]int16 + psk := int16(-1) + alpk := int16(1) + codvec[0], codvec[1], codvec[2], codvec[3] = 0, 1, 2, 3 + + for track := int16(3); track < 5; track++ { + ipos[0], ipos[1], ipos[2], ipos[3] = 0, 1, 2, track + + for rot := 0; rot < 4; rot++ { + for i0 := ipos[0]; i0 < L; i0 += cSTEP { + if dn2[i0] < 0 { + continue + } + ps0 := dn[i0] + alp0 := int32(rr[int(i0)*L+int(i0)]) << 14 + + sq := int16(-1) + alp := int16(1) + ps := int16(0) + ix := ipos[1] + for i1 := ipos[1]; i1 < L; i1 += cSTEP { + ps1 := ps0 + dn[i1] + alp1 := alp0 + (int32(rr[int(i1)*L+int(i1)]) << 14) + alp1 += int32(rr[int(i0)*L+int(i1)]) << 15 + sq1 := int16((int32(ps1) * int32(ps1)) >> 15) + alp16 := int16((alp1 + 0x8000) >> 16) + s := (int32(alp)*int32(sq1))<<1 - (int32(sq)*int32(alp16))<<1 + if s > 0 { + sq, ps, alp, ix = sq1, ps1, alp16, i1 + } + } + i1 := ix + + ps0 = ps + alp0 = int32(alp) << 14 + sq, alp, ps, ix = -1, 1, 0, ipos[2] + for i2 := ipos[2]; i2 < L; i2 += cSTEP { + ps1 := ps0 + dn[i2] + alp1 := alp0 + (int32(rr[int(i2)*L+int(i2)]) << 12) + alp1 += int32(rr[int(i1)*L+int(i2)]) << 13 + alp1 += int32(rr[int(i0)*L+int(i2)]) << 13 + sq1 := int16((int32(ps1) * int32(ps1)) >> 15) + alp16 := int16((alp1 + 0x8000) >> 16) + s := (int32(alp)*int32(sq1))<<1 - (int32(sq)*int32(alp16))<<1 + if s > 0 { + sq, ps, alp, ix = sq1, ps1, alp16, i2 + } + } + i2 := ix + + ps0 = ps + alp0 = int32(alp) << 16 + sq, alp, ps, ix = -1, 1, 0, ipos[3] + for i3 := ipos[3]; i3 < L; i3 += cSTEP { + ps1 := ps0 + dn[i3] + alp1 := alp0 + (int32(rr[int(i3)*L+int(i3)]) << 12) + alp1 += int32(rr[int(i2)*L+int(i3)]) << 13 + alp1 += int32(rr[int(i1)*L+int(i3)]) << 13 + alp1 += int32(rr[int(i0)*L+int(i3)]) << 13 + sq1 := int16((int32(ps1) * int32(ps1)) >> 15) + alp16 := int16((alp1 + 0x8000) >> 16) + s := (int32(alp)*int32(sq1))<<1 - (int32(sq)*int32(alp16))<<1 + if s > 0 { + sq, ps, alp, ix = sq1, ps1, alp16, i3 + } + } + + s := (int32(alpk)*int32(sq))<<1 - (int32(psk)*int32(alp))<<1 + if s > 0 { + psk = sq + alpk = alp + codvec[0], codvec[1], codvec[2], codvec[3] = i0, i1, i2, ix + } + } + pos := ipos[3] + ipos[3] = ipos[2] + ipos[2] = ipos[1] + ipos[1] = ipos[0] + ipos[0] = pos + } + } +} + +// build4Code assembles the 4-pulse innovation, filtered code, signs and the +// Gray-coded 17-bit index. Mirrors build_code (c4_17pf.cpp). +func build4Code(codvec, dnSign, cod, h, y []int16) (indx, sign int16) { + const L = cL_CODE + for i := 0; i < L; i++ { + cod[i] = 0 + } + + var sgn [4]int16 + rsign := int16(0) + for k := 0; k < 4; k++ { + i := codvec[k] + j := dnSign[i] + index := int16((int32(i) * 6554) >> 15) + track := i - index*5 + index = gray[index] + switch track { + case 1: + index <<= 3 + case 2: + index <<= 6 + case 3: + index <<= 10 + case 4: + track = 3 + index = (index << 10) + 512 + } + if j > 0 { + cod[i] = 8191 + sgn[k] = 32767 + track = 1 << uint(track) + rsign += track + } else { + cod[i] = -8192 + sgn[k] = -32768 + } + indx += index + } + sign = rsign + + c := [4]int{int(codvec[0]), int(codvec[1]), int(codvec[2]), int(codvec[3])} + for i := 0; i < L; i++ { + var s int32 + for k := 0; k < 4; k++ { + if i-c[k] >= 0 { + s = L_mac(s, h[i-c[k]], sgn[k]) + } + } + y[i] = round_(s) + } + return indx, sign +} + +// code4i40_17bits runs the full MR74/MR795 4-pulse search. Mirrors +// code_4i40_17bits. +func code4i40_17bits(x, h []int16, T0, pitchSharp int16, code, y []int16) (index, sign int16) { + sharp := shl(pitchSharp, 1) + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + h[i] = add(h[i], mult(h[i-T0], sharp)) + } + } + + var dn, dn2, dnSign [cL_CODE]int16 + var rr [cL_CODE * cL_CODE]int16 + var codvec [4]int16 + + corHX(h, x, dn[:], 1) + setSign(dn[:], dnSign[:], dn2[:], 4) + corH(h, dnSign[:], rr[:]) + search4i40(dn[:], dn2[:], rr[:], codvec[:]) + index, sign = build4Code(codvec[:], dnSign[:], code, h, y) + + if T0 < cL_CODE { + for i := T0; i < cL_CODE; i++ { + code[i] = add(code[i], mult(code[i-T0], sharp)) + } + } + return index, sign +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cb8.go b/internal/capability/transcode/amrnb/dsp_enc_cb8.go new file mode 100644 index 0000000..60bede9 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cb8.go @@ -0,0 +1,125 @@ +package amrnb + +// 8-pulse algebraic codebook search (31 bits, MR102), ported from opencore-amrnb +// c8_31pf.cpp. Drives search10and8i40 (8 pulses, step 4), then builds the +// codeword and compresses the eight pulse positions / four track signs into the +// 7-value transmitted index. The exact encoder counterpart of dec8i40_31bits +// (decompressCode). Bit-exact with the reference. + +// build8Code assembles the 8-pulse innovation cod and filtered code y, and the +// linear track signs / pulse positions (with the same-track ordering/swap logic +// that decompressCode inverts). Mirrors build_code (c8_31pf.cpp). +func build8Code(codvec, sign, cod, h, y []int16) (signIndx [4]int16, posIndx [8]int16) { + const L = cL_CODE + for i := 0; i < L; i++ { + cod[i] = 0 + } + for t := 0; t < 4; t++ { + posIndx[t] = -1 + signIndx[t] = -1 + } + + var sgn [8]int16 + for k := 0; k < 8; k++ { + i := codvec[k] + j := sign[i] + posIndex := i >> 2 + track := i & 3 + + var signIndex int16 + if j > 0 { + cod[i] = cod[i] + 8191 + sgn[k] = 32767 + signIndex = 0 + } else { + cod[i] = cod[i] - 8191 + sgn[k] = -32768 + signIndex = 1 + } + + if posIndx[track] < 0 { + posIndx[track] = posIndex + signIndx[track] = signIndex + } else if (signIndex^signIndx[track])&1 == 0 { + if posIndx[track] <= posIndex { + posIndx[track+4] = posIndex + } else { + posIndx[track+4] = posIndx[track] + posIndx[track] = posIndex + signIndx[track] = signIndex + } + } else { + if posIndx[track] <= posIndex { + posIndx[track+4] = posIndx[track] + posIndx[track] = posIndex + signIndx[track] = signIndex + } else { + posIndx[track+4] = posIndex + } + } + } + + for i := 0; i < L; i++ { + var s int32 + for k := 0; k < 8; k++ { + ci := int(codvec[k]) + if i-ci >= 0 { + s = L_mac(s, h[i-ci], sgn[k]) + } + } + y[i] = round_(s) + } + return signIndx, posIndx +} + +// compress10 packs three position indices into one 10-bit value. Mirrors +// compress10 (the inverse of decompress10). +func compress10(A, B, C int16) int16 { + ia := A >> 1 + ib := (B >> 1) * 5 + ic := (C >> 1) * 25 + indx := (ib + ic + ia) << 3 + indx += ((B & 1) << 1) + ((C & 1) << 2) + (A & 1) + return indx +} + +// compressCode packs the four track signs and eight pulse positions into the +// seven transmitted index values. Mirrors compress_code. +func compressCode(signIndx, posIndx, indx []int16) { + for i := 0; i < 4; i++ { + indx[i] = signIndx[i] + } + indx[4] = compress10(posIndx[0], posIndx[4], posIndx[1]) + indx[5] = compress10(posIndx[2], posIndx[6], posIndx[5]) + + ib := (posIndx[7] >> 1) & 1 + ia := posIndx[3] >> 1 + if ib == 1 { + ia = 4 - ia + } + ib = (posIndx[7] >> 1) * 5 + ib += ia + ib <<= 5 + ib += 12 + ic := int16((int32(ib) * 1311) >> 15) + ic <<= 2 + ia = posIndx[3] & 1 + ib = (posIndx[7] & 1) << 1 + indx[6] = ib + ic + ia +} + +// code8i40_31bits runs the full MR102 8-pulse search, writing the innovation +// cod, filtered code y, and the 7-value index. Mirrors code_8i40_31bits. +func code8i40_31bits(x, cn, h, cod, y, indx []int16) { + var dn, sign [cL_CODE]int16 + var rr [cL_CODE * cL_CODE]int16 + var ipos, codvec [8]int16 + var posMax [4]int16 + + corHX2(h, x, dn[:], 2, 4, 4) + setSign12k2(dn[:], cn, sign[:], posMax[:], 4, ipos[:], 4) + corH(h, sign[:], rr[:]) + search10and8i40(8, 4, 4, dn[:], rr[:], ipos[:], posMax[:], codvec[:]) + signIndx, posIndx := build8Code(codvec[:], sign[:], cod, h, y) + compressCode(signIndx[:], posIndx[:], indx) +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cbsearch.go b/internal/capability/transcode/amrnb/dsp_enc_cbsearch.go new file mode 100644 index 0000000..7a9302b --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cbsearch.go @@ -0,0 +1,85 @@ +package amrnb + +// Algebraic-codebook search infrastructure, ported from opencore-amrnb +// cor_h_x.cpp (cor_h_x) and set_sign.cpp (set_sign). corHX computes the +// backward correlation dn[] between the search target and the weighted impulse +// response (the gradient that drives pulse placement); setSign extracts the +// per-position pulse signs and prunes each track to its strongest candidate +// positions. These feed the per-mode pulse searches. Bit-exact with the +// reference. + +const ( + cNB_TRACK = 5 // number of pulse tracks + cSTEP = 5 // codebook track step +) + +// corHX computes dn[i] = normalized 2·sum_j x[i+j]·h[j], the correlation between +// the target x and the impulse response h, scaled so it fits 16 bits (sf is the +// scaling factor: 2 for 12.2k, 1 otherwise). Mirrors cor_h_x. +func corHX(h, x, dn []int16, sf int16) { + // y32[i] = 2·sum_{j=0}^{L-1-i} x[i+j]·h[j]. Zero-padding x past L makes the + // out-of-range taps contribute 0, so the whole set of positions is one L-tap + // FIR over the padded input (firRaw). The per-term <<1 distributes over the + // wrapping sum, so dst[i]<<1 is bit-identical to the per-term shift. + var xpad [2 * cL_CODE]int16 + copy(xpad[:cL_CODE], x[:cL_CODE]) + var dst [cL_CODE]int32 + firRaw(dst[:], xpad[:], h[:cL_CODE]) + + var y32 [cL_CODE]int32 + for i := 0; i < cL_CODE; i++ { + y32[i] = dst[i] << 1 + } + + tot := int32(5) + for k := 0; k < cNB_TRACK; k++ { + var max int32 + for i := k; i < cL_CODE; i += cSTEP { + s := y32[i] + if s < 0 { + s = -s + } + if s > max { + max = s + } + } + tot += max >> 1 + } + + j := norm_l(tot) - sf + for i := 0; i < cL_CODE; i++ { + s := L_shl(y32[i], j) + dn[i] = int16((s + 0x8000) >> 16) + } +} + +// setSign records the sign of each dn[] entry (±32767), takes |dn|, and prunes +// dn2[] to keep only the n strongest positions per track (the rest set to -1). +// Mirrors set_sign. +func setSign(dn, sign, dn2 []int16, n int16) { + for i := 0; i < cL_CODE; i++ { + val := dn[i] + if val >= 0 { + sign[i] = 32767 + } else { + sign[i] = -32767 + val = negate(val) + } + dn[i] = val + dn2[i] = val + } + + for i := 0; i < cNB_TRACK; i++ { + for k := int16(0); k < 8-n; k++ { + min := int16(0x7fff) + pos := i + for j := i; j < cL_CODE; j += cSTEP { + if dn2[j] >= 0 && dn2[j] < min { + min = dn2[j] + pos = j + } + } + dn2[pos] = -1 + } + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_cbsearch2.go b/internal/capability/transcode/amrnb/dsp_enc_cbsearch2.go new file mode 100644 index 0000000..0db43e5 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_cbsearch2.go @@ -0,0 +1,106 @@ +package amrnb + +// Parameterized codebook-search correlation, ported from opencore-amrnb +// cor_h_x2.cpp. corHX2 is the generalized form of corHX used by the multi-pulse +// searches (MR102: 4 tracks, MR122: 5 tracks): dn[i] = normalized 2·Σ x[j]·h[j-i] +// with a configurable track count and step. Bit-exact with the reference. + +const cLOG2_OF_32 = 5 + +// corHX2 computes the normalized target↔impulse correlation dn[] over nbTrack +// tracks with the given step, scaled by sf. Mirrors cor_h_x2. +func corHX2(h, x, dn []int16, sf, nbTrack, step int16) { + // y32[i] = 2·sum_{j=i}^{L-1} x[j]·h[j-i] = 2·sum_{j'=0}^{L-1-i} x[i+j']·h[j']. + // Zero-padding x past L turns this into one L-tap FIR over the padded input + // (firRaw); s<<1 == dst[i]<<1 holds mod 2^32 since <<1 distributes over the + // wrapping sum. + var xpad [2 * cL_CODE]int16 + copy(xpad[:cL_CODE], x[:cL_CODE]) + var dst [cL_CODE]int32 + firRaw(dst[:], xpad[:], h[:cL_CODE]) + + var y32 [cL_CODE]int32 + for i := 0; i < cL_CODE; i++ { + y32[i] = dst[i] << 1 + } + + tot := int32(cLOG2_OF_32) + for k := int16(0); k < nbTrack; k++ { + var max int32 + for i := k; i < cL_CODE; i += step { + if a := L_abs(y32[i]); a > max { + max = a + } + } + tot += max >> 1 + } + + j := norm_l(tot) - sf + for i := 0; i < cL_CODE; i++ { + dn[i] = round_(L_shl(y32[i], j)) + } +} + +// setSign12k2 combines the residual cn and the correlation dn to determine each +// position's pulse sign, the per-track maximum positions (posMax), and a cyclic +// pulse-track ordering (ipos, length 2*nbTrack) starting from the strongest +// track — the prep for the multi-pulse search_10and8i40. dn is updated in place +// (negated where the combined sign is negative). Mirrors set_sign12k2. +func setSign12k2(dn, cn, sign, posMax []int16, nbTrack int16, ipos []int16, step int16) { + const L = cL_CODE + var en [cL_CODE]int16 + + s := int32(256) + t := int32(256) + for i := 0; i < L; i++ { + s = L_mac(s, cn[i], cn[i]) + t += (int32(dn[i]) * int32(dn[i])) << 1 + } + s = invSqrt(s) + kCn := int16(L_shl(s, 5) >> 16) + t = invSqrt(t) + kDn := int16(t >> 11) + + for i := L - 1; i >= 0; i-- { + lTemp := (int32(kCn) * int32(cn[i])) << 1 + val := dn[i] + ss := L_mac(lTemp, kDn, val) + cor := round_(L_shl(ss, 10)) + if cor >= 0 { + sign[i] = 32767 + } else { + sign[i] = -32767 + cor = negate(cor) + dn[i] = negate(val) + } + en[i] = cor + } + + maxOfAll := int16(-1) + var pos int16 + for i := int16(0); i < nbTrack; i++ { + max := int16(-1) + for j := i; j < L; j += step { + if en[j] > max { + max = en[j] + pos = j + } + } + posMax[i] = pos + if max > maxOfAll { + maxOfAll = max + ipos[0] = i + } + } + + pos = ipos[0] + ipos[nbTrack] = pos + for i := int16(1); i < nbTrack; i++ { + pos++ + if pos >= nbTrack { + pos = 0 + } + ipos[i] = pos + ipos[i+nbTrack] = pos + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_clltp.go b/internal/capability/transcode/amrnb/dsp_enc_clltp.go new file mode 100644 index 0000000..1420ce7 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_clltp.go @@ -0,0 +1,53 @@ +package amrnb + +// Closed-loop long-term-prediction (adaptive codebook) step, ported from +// opencore-amrnb cl_ltp.cpp. clLtp runs the fractional pitch search (pitchFr), +// builds the adaptive codebook (predLt), filters it (convolve), computes the +// pitch gain (gPitch) with mode-specific clamping (and MR122 quantization), then +// forms the codebook-search target xn2 = xn - gain·y1 and updates res2. The +// tone-stabilization gain clip (gpc_flag) is not applied (active-speech path). +// Bit-exact with the reference for that path. + +const cGP_CLIP = 15565 // 0.95 Q14 pitch-gain clip + +type clLtpResult struct { + T0, T0frac int16 + gainPit int16 + gpLimit int16 + gCoeff [5]int16 + pitchIndex int16 + gainPitIndex int16 // MR122 only + hasGainPitIndex bool +} + +// clLtp performs the adaptive-codebook analysis for one subframe. exc[excOff] is +// the current subframe; predLt writes it in place. xn2 (codebook target) and +// res2 (residual) are written. Mirrors cl_ltp. +func (st *pitchFrState) clLtp(mode, frameOffset int, Top, h1, exc []int16, excOff int, res2, xn, xn2, yl []int16) clLtpResult { + var r clLtpResult + resu3 := int16(0) + r.T0, r.T0frac, resu3, r.pitchIndex = st.pitchFr(mode, Top, exc, excOff, xn, h1, cL_SUBFR, frameOffset) + + predLt(exc, excOff, r.T0, r.T0frac, cL_SUBFR, resu3) + convolve(exc[excOff:], h1, yl, cL_SUBFR) + + r.gainPit = gPitch(mode, xn, yl, r.gCoeff[:], cL_SUBFR) + r.gpLimit = maxInt16 // tone-stabilization clip disabled + + if mode == dMR475 || mode == dMR515 { + if r.gainPit > 13926 { + r.gainPit = 13926 + } + } else if mode == dMR122 { + var cand, cind [3]int16 + r.gainPitIndex = qGainPitch(dMR122, r.gpLimit, &r.gainPit, cand[:], cind[:]) + r.hasGainPitIndex = true + } + + g := r.gainPit + for i := 0; i < cL_SUBFR; i++ { + xn2[i] = xn[i] - int16((int32(yl[i])*int32(g))>>14) + res2[i] -= int16((int32(exc[excOff+i]) * int32(g)) >> 14) + } + return r +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_clpitch.go b/internal/capability/transcode/amrnb/dsp_enc_clpitch.go new file mode 100644 index 0000000..ea73d16 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_clpitch.go @@ -0,0 +1,247 @@ +package amrnb + +// Closed-loop fractional pitch search, ported from opencore-amrnb pitch_fr.cpp +// (Pitch_fr, Norm_Corr, searchFrac, getRange) and inter_36.cpp (Interpol_3or6). +// pitchFr refines the open-loop lag: it builds the normalized correlation of the +// target with the filtered adaptive excitation over an integer search range, +// picks the best integer lag, then interpolates to a fractional lag, and encodes +// the result with encLag3/encLag6. Bit-exact with the reference. + +const cL_INTER_SRCH = 4 // closed-loop pitch search interpolation half-length + +// mode-dependent search parameters (pitch_fr.cpp mode_dep_parm), indexed by the +// internal speech mode 0..7. +var modeDepParm = [8]struct { + maxFracLag, flag3, firstFrac, lastFrac int16 + deltaIntLow, deltaIntRange, deltaFrcLow int16 + deltaFrcRange, pitMin int16 +}{ + {84, 1, -2, 2, 5, 10, 5, 9, cPIT_MIN}, // MR475 + {84, 1, -2, 2, 5, 10, 5, 9, cPIT_MIN}, // MR515 + {84, 1, -2, 2, 3, 6, 5, 9, cPIT_MIN}, // MR59 + {84, 1, -2, 2, 3, 6, 5, 9, cPIT_MIN}, // MR67 + {84, 1, -2, 2, 3, 6, 5, 9, cPIT_MIN}, // MR74 + {84, 1, -2, 2, 3, 6, 10, 19, cPIT_MIN}, // MR795 + {84, 1, -2, 2, 3, 6, 5, 9, cPIT_MIN}, // MR102 + {94, 0, -3, 3, 3, 6, 5, 9, cPIT_MIN_MR122}, // MR122 +} + +// pitchFrState carries the previous subframe's integer lag across subframes. +type pitchFrState struct { + T0prev int16 +} + +func (st *pitchFrState) reset() { st.T0prev = 0 } + +// getRange clamps a search range [t0min, t0max] of width deltaRange around T0. +// Mirrors getRange. +func getRange(T0, deltaLow, deltaRange, pitmin, pitmax int16) (t0min, t0max int16) { + temp := T0 - deltaLow + if temp < pitmin { + temp = pitmin + } + t0min = temp + temp += deltaRange + if temp > pitmax { + temp = pitmax + t0min = pitmax - deltaRange + } + t0max = temp + return t0min, t0max +} + +// normCorr computes the normalized correlation corr[t-tMin] of the target xn +// with the adaptive excitation at each integer lag t in [tMin, tMax], filtered +// by the impulse response h. exc[excOff] is the current subframe start. Mirrors +// Norm_Corr. +func normCorr(exc []int16, excOff int, xn, h []int16, Lsubfr int, tMin, tMax int16, corr []int16) { + var excf, scaledExcf [cL_SUBFR]int16 + + k := -int(tMin) + convolve(exc[excOff+k:], h, excf[:], Lsubfr) + + var s int32 + for j := 0; j < Lsubfr; j++ { + t := excf[j] + scaledExcf[j] = t >> 2 + s += int32(t) * int32(t) + } + + var sExcf []int16 + var hFac, scaling int16 + if s <= (67108864 >> 1) { + sExcf = excf[:] + hFac = 12 + scaling = 0 + } else { + sExcf = scaledExcf[:] + hFac = 14 + scaling = 2 + } + + xnf := xn[:Lsubfr] // hoist bounds out of the inner correlation loop + sef := sExcf[:Lsubfr] // (both length Lsubfr) + hf := h[:Lsubfr] + for i := tMin; i <= tMax; i++ { + sc := firDot(xnf, sef) + s2 := firDot(sef, sef) + s2 <<= 1 + s2 = invSqrt(s2) + normH := int16(s2 >> 16) + normL := int16((s2 >> 1) - (int32(normH) << 15)) + corrH := int16(sc >> 15) + corrL := int16(sc - (int32(corrH) << 15)) + corr[int(i)-int(tMin)] = int16(mpy_32(corrH, corrL, normH, normL)) + + if i != tMax { + k-- + temp := exc[excOff+k] + for j := Lsubfr - 1; j >= 1; j-- { + sef[j] = int16((int32(temp)*int32(hf[j]))>>uint(hFac)) + sef[j-1] + } + sef[0] = temp >> scaling + } + } +} + +// interpol3or6 interpolates the correlation around corr[off] at fractional +// position frac (1/3 if flag3 else 1/6). Mirrors Interpol_3or6. +func interpol3or6(corr []int16, off int, frac, flag3 int16) int16 { + if flag3 != 0 { + frac <<= 1 + } + if frac < 0 { + frac += cUP_SAMP_MAX + off-- + } + f := int(frac) + s := int32(0x4000) + k := 0 + x1 := off + x2 := off + 1 + for i := cL_INTER_SRCH >> 1; i != 0; i-- { + s += int32(corr[x1]) * int32(inter6Pitch[f+k]) + x1-- + s += int32(corr[x2]) * int32(inter6Pitch[(cUP_SAMP_MAX-f)+k]) + x2++ + k += cUP_SAMP_MAX + s += int32(corr[x1]) * int32(inter6Pitch[f+k]) + x1-- + s += int32(corr[x2]) * int32(inter6Pitch[(cUP_SAMP_MAX-f)+k]) + x2++ + k <<= 1 + } + return int16(s >> 15) +} + +// searchFrac refines the fractional pitch around the integer lag by maximizing +// the interpolated correlation, then normalizes the (lag, frac) pair. Mirrors +// searchFrac. +func searchFrac(lag, frac *int16, lastFrac int16, corr []int16, tMin int, flag3 int16) { + off := int(*lag) - tMin + max := interpol3or6(corr, off, *frac, flag3) + for i := *frac + 1; i <= lastFrac; i++ { + ci := interpol3or6(corr, off, i, flag3) + if ci > max { + max = ci + *frac = i + } + } + if flag3 == 0 { + if *frac == -3 { + *frac = 3 + *lag-- + } + } else { + if *frac == -2 { + *frac = 1 + *lag-- + } + if *frac == 2 { + *frac = -1 + *lag++ + } + } +} + +// pitchFr performs the closed-loop fractional pitch search for one subframe and +// returns the integer lag T0, fractional part frac, the resolution flag resu3 +// (1=1/3, 0=1/6), and the encoded analysis index. Mirrors Pitch_fr. +func (st *pitchFrState) pitchFr(mode int, Top []int16, exc []int16, excOff int, xn, h []int16, Lsubfr, iSubfr int) (T0, frac, resu3, anaIndex int16) { + p := modeDepParm[mode] + maxFracLag := p.maxFracLag + flag3 := p.flag3 + frac = p.firstFrac + lastFrac := p.lastFrac + + deltaSearch := int16(1) + var t0min, t0max int16 + if iSubfr == 0 || iSubfr == cL_FRAME_BY2 { + if (mode != dMR475 && mode != dMR515) || iSubfr != cL_FRAME_BY2 { + deltaSearch = 0 + frameOffset := int16(1) + if iSubfr == 0 { + frameOffset = 0 + } + t0min, t0max = getRange(Top[frameOffset], p.deltaIntLow, p.deltaIntRange, p.pitMin, cPIT_MAX) + } else { + t0min, t0max = getRange(st.T0prev, p.deltaFrcLow, p.deltaFrcRange, p.pitMin, cPIT_MAX) + } + } else { + t0min, t0max = getRange(st.T0prev, p.deltaFrcLow, p.deltaFrcRange, p.pitMin, cPIT_MAX) + } + + tMin := int16(int(t0min) - cL_INTER_SRCH) + tMax := int16(int(t0max) + cL_INTER_SRCH) + corr := make([]int16, int(tMax)-int(tMin)+1) + normCorr(exc, excOff, xn, h, Lsubfr, tMin, tMax, corr) + + max := corr[int(t0min)-int(tMin)] + lag := t0min + for i := t0min + 1; i <= t0max; i++ { + if corr[int(i)-int(tMin)] >= max { + max = corr[int(i)-int(tMin)] + lag = i + } + } + + if deltaSearch == 0 && lag > maxFracLag { + frac = 0 + } else if deltaSearch != 0 && (mode == dMR475 || mode == dMR515 || mode == dMR59 || mode == dMR67) { + tmpLag := st.T0prev + if tmpLag-t0min > 5 { + tmpLag = t0min + 5 + } + if t0max-tmpLag > 4 { + tmpLag = t0max - 4 + } + switch { + case lag == tmpLag || lag == tmpLag-1: + searchFrac(&lag, &frac, lastFrac, corr, int(tMin), flag3) + case lag == tmpLag-2: + frac = 0 + searchFrac(&lag, &frac, lastFrac, corr, int(tMin), flag3) + case lag == tmpLag+1: + lastFrac = 0 + searchFrac(&lag, &frac, lastFrac, corr, int(tMin), flag3) + default: + frac = 0 + } + } else { + searchFrac(&lag, &frac, lastFrac, corr, int(tMin), flag3) + } + + if flag3 != 0 { + flag4 := int16(0) + if mode == dMR475 || mode == dMR515 || mode == dMR59 || mode == dMR67 { + flag4 = 1 + } + anaIndex = encLag3(lag, frac, st.T0prev, t0min, t0max, deltaSearch, flag4) + } else { + anaIndex = encLag6(lag, frac, t0min, deltaSearch) + } + + st.T0prev = lag + resu3 = flag3 + return lag, frac, resu3, anaIndex +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_coder.go b/internal/capability/transcode/amrnb/dsp_enc_coder.go new file mode 100644 index 0000000..43a3b81 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_coder.go @@ -0,0 +1,356 @@ +package amrnb + +// Main encoder driver, ported from opencore-amrnb cod_amr.cpp (cod_amr) and the +// per-frame wrapper sp_enc.cpp (Speech_Encode_Frame). It ties together the LP +// analysis (lpc), LSP quantization (lsp), perceptual weighting + open-loop pitch +// (pre_big/ol_ltp), and the four-subframe analysis-by-synthesis loop +// (subframePreProc -> clLtp -> cbsearch -> gainQuant -> subframePostProc), +// producing the parameter array consumed by prm2bits/packFrame. +// +// Scope: the active-speech path for all eight modes — MR515/MR59/MR67/MR74/MR102 +// via Qua_gain, MR122 via q_gain_code, MR475 via the joint two-subframe VQ +// (mr475Loop), and MR795 via the gain-adapted quantizer. MR102 uses the weighted +// open-loop pitch (pitchOlWgh). DTX/VAD is always-active. All eight modes are +// bit-exact with the opencore-amrnb reference encoder. + +// buffer offsets within oldSpeech (length cL_TOTAL). +const ( + cSpeechOff = cL_TOTAL - cL_FRAME - cL_NEXT // 120: present frame start + cPWindowOff = cL_TOTAL - cL_WINDOW // 80: LPC analysis window + cPWin122Off = cPWindowOff - cL_NEXT // 40: EFR (MR122) window, no lookahead + cNewSpchOff = cL_TOTAL - cL_FRAME // 160: new speech +) + +// coderState holds the persistent encoder analysis state across frames, +// mirroring cod_amrState plus the Pre_Process state. +type coderState struct { + oldSpeech [cL_TOTAL]int16 // [history | present | lookahead] + oldWsp [cPIT_MAX + cL_FRAME]int16 // weighted speech with pitch history + oldExc [cExcOff + cL_FRAME]int16 // excitation with pitch+interp history + + pre preProcessState + lspS lspEncState + pred gcPredState + predUnq gcPredState // "unquantized" gain predictor (MR475 only) + adapt gainAdaptState // gain adaptor (MR795 only) + pit pitchFrState + pitWght pitchOLWghtState // weighted open-loop pitch (MR102 only) + lev levinsonState + + oldLags [5]int16 // closed-loop lag history (MR102 weighted OLP) + olGainFlg [2]int16 // per-half-frame open-loop gain flag (MR102) + prmBuf [64]int16 // reused parameter array (max params = 57, MR122) + + memSyn [cM]int16 + memW0 [cM]int16 + memErr [cM]int16 + memW [cM]int16 // weighting-filter memory for pre_big + sharp int16 + + // MR475 joint-pair quantizer state carried from the even to the odd subframe + sf0ExpGcode0, sf0FracGcode0 int16 + sf0FracCoeff, sf0ExpCoeff [5]int16 + sf0ExpTargetEn, sf0FracTargetEn int16 +} + +func (st *coderState) reset() { + *st = coderState{} + st.lspS.reset() + st.pred.reset() + st.pit.reset() + st.pitWght.reset() + for i := range st.oldLags { + st.oldLags[i] = 40 + } + st.sharp = 0 // SHARPMIN +} + +// lpcAnalysis runs the windowed autocorrelation + Levinson chain, filling the +// per-window LP filters in az: az[3*MP1] (A_new) always, and az[MP1] (A_mid) for +// MR122. Mirrors lpc. +func (st *coderState) lpcAnalysis(mode int, az []int16) { + var rh, rl [cMP1]int16 + var rc [4]int16 + if mode == dMR122 { + autocorr(st.oldSpeech[cPWin122Off:], cM, rh[:], rl[:], window_160_80) + lagWindow(cM, rh[:], rl[:]) + st.lev.levinson(rh[:], rl[:], az[cMP1:], rc[:]) + + autocorr(st.oldSpeech[cPWin122Off:], cM, rh[:], rl[:], window_232_8) + lagWindow(cM, rh[:], rl[:]) + st.lev.levinson(rh[:], rl[:], az[3*cMP1:], rc[:]) + return + } + autocorr(st.oldSpeech[cPWindowOff:], cM, rh[:], rl[:], window_200_40) + lagWindow(cM, rh[:], rl[:]) + st.lev.levinson(rh[:], rl[:], az[3*cMP1:], rc[:]) +} + +// preBig computes the perceptually weighted speech wsp for the two subframes of +// one half-frame (frameOffset 0 or L_FRAME_BY2), updating the weighting-filter +// memory memW. Mirrors pre_big. +func (st *coderState) preBig(mode, frameOffset int, az []int16) { + g1 := gamma1[:] + if mode > dMR795 { // MR102, MR122 + g1 = gamma1_12k2[:] + } + aOffset := 0 + if frameOffset > 0 { + aOffset = cMP1 * 2 + } + fo := frameOffset + for i := 0; i < 2; i++ { + var ap1, ap2 [cMP1]int16 + weightAi(az[aOffset:], g1, ap1[:]) + weightAi(az[aOffset:], gamma2[:], ap2[:]) + residu(ap1[:], st.oldSpeech[:], cSpeechOff+fo, cL_SUBFR, st.oldWsp[cPIT_MAX+fo:]) + w := st.oldWsp[cPIT_MAX+fo : cPIT_MAX+fo+cL_SUBFR] + synFilt(ap2[:], w, w, cL_SUBFR, st.memW[:], true) + aOffset += cMP1 + fo += cL_SUBFR + } +} + +// gainQuant quantizes the subframe gains for the non-MR475 modes, writing the +// quantization index/indices into prm, returning the quantized gains and the +// number of indices written, and advancing the predictor. res/exc are the LP +// residual and adaptive-codebook excitation (used only by MR795). Mirrors the +// non-MR475 branch of gainQuant. +func (st *coderState) gainQuant(mode int, r clLtpResult, xn, xn2, y1, y2, code, res, exc, prm []int16) (gainPit, gainCode int16, nIdx int) { + gp := st.pred.gcPred(mode, code) + var qe122, qe int16 + switch { + case mode == dMR122: + gainPit = r.gainPit // already quantized in cl_ltp + gainCode = gCode(xn2, y2) + var idx int16 + idx, qe122, qe = qGainCode(mode, gp.expGcode0, gp.fracGcode0, &gainCode) + prm[0] = idx + nIdx = 1 + case mode == dMR795: + fc, ec, cgFrac, cgExp := calcFiltEnergies(mode, xn, xn2, y1, y2, r.gCoeff[:]) + gainPit = r.gainPit + var gpIdx, gcIdx int16 + gainCode, qe122, qe, gpIdx, gcIdx = st.mr795GainQuant(res, exc, code, fc[:], ec[:], + gp.expEn, gp.fracEn, gp.expGcode0, gp.fracGcode0, cgFrac, cgExp, r.gpLimit, &gainPit) + prm[0], prm[1] = gpIdx, gcIdx + nIdx = 2 + default: + fc, ec, _, _ := calcFiltEnergies(mode, xn, xn2, y1, y2, r.gCoeff[:]) + var idx int16 + idx, gainPit, gainCode, qe122, qe = quaGain(mode, gp.expGcode0, gp.fracGcode0, fc[:], ec[:], r.gpLimit) + prm[0] = idx + nIdx = 1 + } + st.pred.update(qe122, qe) + return +} + +// codAmr encodes one pre-processed 160-sample frame, returning the parameter +// array (LSF indices followed by the per-subframe pitch/codebook/gain indices, +// in prm2bits order). Mirrors cod_amr for the supported modes. +func (st *coderState) codAmr(mode int, newSpeech []int16) ([]int16, error) { + copy(st.oldSpeech[cNewSpchOff:], newSpeech[:cL_FRAME]) + + az := make([]int16, cNB_SUBFR*cMP1) + azQ := make([]int16, cNB_SUBFR*cMP1) + st.lpcAnalysis(mode, az) + + prm := st.prmBuf[:len(bitno[Mode(mode)])] // reused; caller serializes it before the next frame + ap := st.lspS.lsp(mode, az, azQ, prm) + + st.preBig(mode, 0, az) + st.preBig(mode, cL_FRAME_BY2, az) + var Top [2]int16 + if mode == dMR475 || mode == dMR515 { + Top[0] = pitchOl(mode, st.oldWsp[:], cPIT_MAX, cPIT_MIN, cPIT_MAX, cL_FRAME) + Top[1] = Top[0] + } else { + Top[0] = st.olPitch(mode, 0) + Top[1] = st.olPitch(mode, cL_FRAME_BY2) + } + + synth := make([]int16, cL_FRAME) + if mode == dMR475 { + st.mr475Loop(az, azQ, Top[:], prm, ap, synth) + copy(st.oldExc[:cExcOff], st.oldExc[cL_FRAME:cL_FRAME+cExcOff]) + copy(st.oldWsp[:cPIT_MAX], st.oldWsp[cL_FRAME:cL_FRAME+cPIT_MAX]) + copy(st.oldSpeech[:cL_TOTAL-cL_FRAME], st.oldSpeech[cL_FRAME:cL_TOTAL]) + return prm, nil + } + for subfrNr := 0; subfrNr < cNB_SUBFR; subfrNr++ { + iSubfr := subfrNr * cL_SUBFR + aSub := az[subfrNr*cMP1:] + aqSub := azQ[subfrNr*cMP1:] + excBase := cExcOff + iSubfr + + xn := make([]int16, cL_SUBFR) + res := make([]int16, cL_SUBFR) + errSig := make([]int16, cL_SUBFR) + h1 := make([]int16, cL_SUBFR) + subframePreProc(mode, aSub, aqSub, st.oldSpeech[:], cSpeechOff+iSubfr, + st.memErr[:], st.memW0[:], st.oldExc[excBase:], h1, xn, res, errSig) + + res2 := make([]int16, cL_SUBFR) + copy(res2, res) + + xn2 := make([]int16, cL_SUBFR) + y1 := make([]int16, cL_SUBFR) + r := st.pit.clLtp(mode, iSubfr, Top[:], h1, st.oldExc[:], excBase, res2, xn, xn2, y1) + // MR102: feed the first/last closed-loop lag back into the OLP history + if subfrNr == 0 && st.olGainFlg[0] > 0 { + st.oldLags[1] = r.T0 + } else if subfrNr == 3 && st.olGainFlg[1] > 0 { + st.oldLags[0] = r.T0 + } + prm[ap] = r.pitchIndex + ap++ + if r.hasGainPitIndex { + prm[ap] = r.gainPitIndex + ap++ + } + + code := make([]int16, cL_SUBFR) + y2 := make([]int16, cL_SUBFR) + ncb := cbsearch(xn2, h1, r.T0, st.sharp, r.gainPit, res2, code, y2, mode, subfrNr, prm[ap:]) + ap += ncb + + gainPit, gainCode, ng := st.gainQuant(mode, r, xn, xn2, y1, y2, code, res, st.oldExc[excBase:], prm[ap:]) + ap += ng + + subframePostProc(st.oldSpeech[:], cSpeechOff, mode, iSubfr, gainPit, gainCode, + aqSub, synth, xn, code, y1, y2, st.memSyn[:], st.memErr[:], st.memW0[:], + st.oldExc[:], cExcOff, &st.sharp) + } + + // slide the analysis buffers left by one frame + copy(st.oldExc[:cExcOff], st.oldExc[cL_FRAME:cL_FRAME+cExcOff]) + copy(st.oldWsp[:cPIT_MAX], st.oldWsp[cL_FRAME:cL_FRAME+cPIT_MAX]) + copy(st.oldSpeech[:cL_TOTAL-cL_FRAME], st.oldSpeech[cL_FRAME:cL_TOTAL]) + + return prm, nil +} + +// mr475Loop runs the four-subframe analysis-by-synthesis loop for MR475, whose +// gains are quantized two subframes at a time. Each even subframe is processed +// tentatively (storing its energy terms and an unquantized synthesis), and at +// the following odd subframe the joint VQ chooses one index for the pair; both +// subframes' final syntheses are then redone with the reconstructed gains. +// Mirrors the MR475 branch of cod_amr. +func (st *coderState) mr475Loop(az, azQ, Top, prm []int16, ap int, synth []int16) { + const mode = dMR475 + var memSynSave, memW0Save, memErrSave [cM]int16 + var h1Sf0, xnSf0, y2Sf0, codeSf0 [cL_SUBFR]int16 + var sharpSave, gainPitSf0, gainCodeSf0, T0Sf0, T0fracSf0 int16 + var iSubfrSf0, gainIdxPos int + + for subfrNr := 0; subfrNr < cNB_SUBFR; subfrNr++ { + iSubfr := subfrNr * cL_SUBFR + evenSubfr := (subfrNr & 1) == 0 + aSub := az[subfrNr*cMP1:] + aqSub := azQ[subfrNr*cMP1:] + excBase := cExcOff + iSubfr + + if evenSubfr { + memSynSave = st.memSyn + memW0Save = st.memW0 + memErrSave = st.memErr + sharpSave = st.sharp + } + + // Both subframes of the pair build their target from memW0Save so the + // even subframe's tentative synthesis carries into the odd one. + xn := make([]int16, cL_SUBFR) + res := make([]int16, cL_SUBFR) + errSig := make([]int16, cL_SUBFR) + h1 := make([]int16, cL_SUBFR) + subframePreProc(mode, aSub, aqSub, st.oldSpeech[:], cSpeechOff+iSubfr, + st.memErr[:], memW0Save[:], st.oldExc[excBase:], h1, xn, res, errSig) + if evenSubfr { + copy(h1Sf0[:], h1) + } + res2 := make([]int16, cL_SUBFR) + copy(res2, res) + + xn2 := make([]int16, cL_SUBFR) + y1 := make([]int16, cL_SUBFR) + r := st.pit.clLtp(mode, iSubfr, Top, h1, st.oldExc[:], excBase, res2, xn, xn2, y1) + prm[ap] = r.pitchIndex + ap++ + + code := make([]int16, cL_SUBFR) + y2 := make([]int16, cL_SUBFR) + ap += cbsearch(xn2, h1, r.T0, st.sharp, r.gainPit, res2, code, y2, mode, subfrNr, prm[ap:]) + + if evenSubfr { + gainIdxPos = ap // reserve the joint gain index slot + ap++ + + st.predUnq = st.pred // gc_pred_copy + gpu := st.predUnq.gcPred(mode, code) + st.sf0ExpGcode0, st.sf0FracGcode0 = gpu.expGcode0, gpu.fracGcode0 + fc, ec, cgFrac, cgExp := calcFiltEnergies(mode, xn, xn2, y1, y2, r.gCoeff[:]) + st.sf0FracCoeff, st.sf0ExpCoeff = fc, ec + st.sf0ExpTargetEn, st.sf0FracTargetEn = calcTargetEnergy(xn) + st.predUnq.mr475UpdateUnqPred(st.sf0ExpGcode0, st.sf0FracGcode0, cgExp, cgFrac) + + // tentative synthesis with the unquantized optimum gains + gainCode := shl(cgFrac, cgExp+1) + iSubfrSf0 = iSubfr + copy(xnSf0[:], xn) + copy(y2Sf0[:], y2) + copy(codeSf0[:], code) + T0Sf0, T0fracSf0 = r.T0, r.T0frac + subframePostProc(st.oldSpeech[:], cSpeechOff, mode, iSubfr, r.gainPit, gainCode, + aqSub, synth, xn, code, y1, y2, memSynSave[:], st.memErr[:], memW0Save[:], + st.oldExc[:], cExcOff, &st.sharp) + st.sharp = sharpSave + continue + } + + // odd subframe: joint gain quantization for the pair + gpu := st.predUnq.gcPred(mode, code) + fc, ec, _, _ := calcFiltEnergies(mode, xn, xn2, y1, y2, r.gCoeff[:]) + tEnExp, tEnFrac := calcTargetEnergy(xn) + idx, sf0gp, sf0gc, sf1gp, sf1gc := st.mr475GainQuant(code, gpu.expGcode0, gpu.fracGcode0, + ec[:], fc[:], tEnExp, tEnFrac, r.gpLimit) + prm[gainIdxPos] = idx + gainPitSf0, gainCodeSf0 = sf0gp, sf0gc + + // redo the even subframe's final synthesis with the quantized gains + st.memErr = memErrSave + predLt(st.oldExc[:], cExcOff+iSubfrSf0, T0Sf0, T0fracSf0, cL_SUBFR, 1) + convolve(st.oldExc[cExcOff+iSubfrSf0:], h1Sf0[:], y1, cL_SUBFR) + subframePostProc(st.oldSpeech[:], cSpeechOff, mode, iSubfrSf0, gainPitSf0, gainCodeSf0, + azQ[(subfrNr-1)*cMP1:], synth, xnSf0[:], codeSf0[:], y1, y2Sf0[:], + st.memSyn[:], st.memErr[:], st.memW0[:], st.oldExc[:], cExcOff, &sharpSave) + + // redo the odd subframe's analysis + synthesis with the final memories + subframePreProc(mode, aSub, aqSub, st.oldSpeech[:], cSpeechOff+iSubfr, + st.memErr[:], st.memW0[:], st.oldExc[excBase:], h1, xn, res, errSig) + predLt(st.oldExc[:], excBase, r.T0, r.T0frac, cL_SUBFR, 1) + convolve(st.oldExc[excBase:], h1, y1, cL_SUBFR) + subframePostProc(st.oldSpeech[:], cSpeechOff, mode, iSubfr, sf1gp, sf1gc, + aqSub, synth, xn, code, y1, y2, st.memSyn[:], st.memErr[:], st.memW0[:], + st.oldExc[:], cExcOff, &st.sharp) + } +} + +// olPitch runs the open-loop pitch over the weighted-speech buffer for the +// half-frame at frameOffset. +func (st *coderState) olPitch(mode, frameOffset int) int16 { + idx := 0 + if frameOffset != 0 { + idx = 1 + } + if mode == dMR102 { + // weighted open-loop pitch with median/gain-flag adaptation + return st.pitWght.pitchOlWgh(st.oldWsp[:], cPIT_MAX+frameOffset, cPIT_MIN, cPIT_MAX, + cL_FRAME_BY2, st.oldLags[:], st.olGainFlg[:], idx) + } + pitMin := int16(cPIT_MIN) + if mode == dMR122 { + pitMin = cPIT_MIN_MR122 + } + return pitchOl(mode, st.oldWsp[:], cPIT_MAX+frameOffset, int(pitMin), cPIT_MAX, cL_FRAME_BY2) +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_corh.go b/internal/capability/transcode/amrnb/dsp_enc_corh.go new file mode 100644 index 0000000..511ba43 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_corh.go @@ -0,0 +1,103 @@ +package amrnb + +// Sign-weighted impulse-response autocorrelation matrix, ported from +// opencore-amrnb cor_h.cpp. corH builds rr[i][j] (flattened, row-major, size +// L_CODE*L_CODE): the diagonal holds the truncated energy of the weighted +// impulse response (used to place a pulse at position i), and the off-diagonal +// holds the sign-weighted cross-correlation between candidate pulse positions +// i and j. This matrix drives the per-mode algebraic codebook pulse searches. +// The reference's exact incremental Q15 roundings and pointer walk are +// preserved (bit-exact). + +// corH fills the flattened rr (length cL_CODE*cL_CODE) from the weighted impulse +// response h and the per-position signs. Mirrors cor_h. +func corH(h, sign, rr []int16) { + var h2 [cL_CODE]int16 + + // energy of h, and the scaling of h into h2 + s := int32(1) + for i := 0; i < cL_CODE; i++ { + s += int32(h[i]) * int32(h[i]) + } + s <<= 1 + if s < 0 { // overflow -> halve + for i := 0; i < cL_CODE; i++ { + h2[i] = h[i] >> 1 + } + } else { + s >>= 1 + s = invSqrt(s) + var dec int16 + if s < 0x00ffffff { + dec = int16(((s >> 9) * 32440) >> 15) + } else { + dec = 32440 // 0.99 + } + for i := 0; i < cL_CODE; i++ { + h2[i] = int16((int32(h[i])*int32(dec) + 0x20) >> 6) + } + } + + const L = cL_CODE + + // diagonal: rr[L-1-m][L-1-m] = sum_{k=0..m} h2[k]^2 + s = 0 + p := 0 + rr1 := (L-1)*L + (L - 1) + for i := L >> 1; i != 0; i-- { + t := int32(h2[p]) + p++ + s += t * t + rr[rr1] = int16((s + 0x4000) >> 15) + rr1 -= L + 1 + t = int32(h2[p]) + p++ + s += t * t + rr[rr1] = int16((s + 0x4000) >> 15) + rr1 -= L + 1 + } + + // off-diagonal: sign-weighted cross-correlation at each distance dec + pRrRef1 := (L - 1) * L + for dec := 1; dec < L; dec += 2 { + rr1 := pRrRef1 + (L - 1 - dec) // &rr[L-1][L-1-dec] + rr2 := (L-1-dec)*L + (L - 1) // &rr[L-1-dec][L-1] + rr3 := (L-1-(dec+1))*L + (L - 1) + + var s2 int32 + s = 0 + pSign1 := L - 1 + pSign2 := L - 1 - dec + ph2 := 0 + ph := dec + + for i := L - dec - 1; i != 0; i-- { + s += int32(h2[ph2]) * int32(h2[ph]) + ph++ + s2 += int32(h2[ph2]) * int32(h2[ph]) + ph2++ + tmp1 := int16((s + 0x4000) >> 15) + tmp11 := int16((s2 + 0x4000) >> 15) + tmp2 := int16((int32(sign[pSign1]) * int32(sign[pSign2])) >> 15) + pSign2-- + tmp22 := int16((int32(sign[pSign1]) * int32(sign[pSign2])) >> 15) + pSign1-- + + rr[rr2] = int16((int32(tmp1) * int32(tmp2)) >> 15) + rr[rr1] = rr[rr2] + rr1-- + rr[rr1] = int16((int32(tmp11) * int32(tmp22)) >> 15) + rr[rr3] = rr[rr1] + + rr1 -= L + rr2 -= L + 1 + rr3 -= L + 1 + } + + s += int32(h2[ph2]) * int32(h2[ph]) + tmp1 := int16((s + 0x4000) >> 15) + tmp2 := int16((int32(sign[pSign1]) * int32(sign[pSign2])) >> 15) + rr[rr1] = int16((int32(tmp1) * int32(tmp2)) >> 15) + rr[rr2] = rr[rr1] + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_filt.go b/internal/capability/transcode/amrnb/dsp_enc_filt.go new file mode 100644 index 0000000..36c380e --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_filt.go @@ -0,0 +1,20 @@ +package amrnb + +// Encoder analysis-by-synthesis filter kernels, ported from opencore-amrnb +// convolve.cpp. convolve computes the (truncated) lower-triangular convolution +// y[n] = sum_{i=0..n} x[i]·h[n-i] >> 12, used throughout the closed-loop pitch +// and algebraic codebook searches to filter an excitation by the weighted +// synthesis impulse response. The reference accumulates in non-saturating int32, +// so a straightforward double loop is bit-exact. +func convolve(x, h, y []int16, L int) { + xf := x[:L] // hoist bounds: i<=n> 12) + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_lpc.go b/internal/capability/transcode/amrnb/dsp_enc_lpc.go new file mode 100644 index 0000000..cfc3148 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_lpc.go @@ -0,0 +1,244 @@ +package amrnb + +// Encoder LP analysis chain, ported from opencore-amrnb pre_proc.cpp +// (Pre_Process), autocorr.cpp (Autocorr), lag_wind.cpp (Lag_window) and +// levinson.cpp (Levinson), plus the 32-bit operators they need (Mpy_32, Div_32, +// L_abs). Together these turn 8 kHz PCM into the order-10 LP coefficients A(z): +// input high-pass + scaling, windowed autocorrelation, lag-window bandwidth +// expansion, then the Levinson-Durbin recursion. Bit-exact with the reference. + +// L_abs returns the saturated 32-bit absolute value (|MIN_32| -> MAX_32). +// Mirrors L_abs. +func L_abs(x int32) int32 { + if x == minInt32 { + return maxInt32 + } + if x < 0 { + return -x + } + return x +} + +// mpy_32 multiplies two 32-bit values given in (hi,lo) double-precision form. +// Mirrors the generic Mpy_32. +func mpy_32(hi1, lo1, hi2, lo2 int16) int32 { + L := L_mult(hi1, hi2) + L = L_add(L, ((int32(hi1)*int32(lo2))>>15)<<1) + L = L_add(L, ((int32(lo1)*int32(hi2))>>15)<<1) + return L +} + +// div_32 divides a 32-bit numerator by a 32-bit denominator (given as hi,lo). +// Mirrors Div_32. +func div_32(num int32, denomHi, denomLo int16) int32 { + approx := div_s(0x3fff, denomHi) + result := mpy32_16(denomHi, denomLo, approx) + result = 0x7fffffff - result + hi, lo := L_Extract(result) + result = mpy32_16(hi, lo, approx) + hi, lo = L_Extract(result) + nHi, nLo := L_Extract(num) + result = mpy_32(nHi, nLo, hi, lo) + return L_shl(result, 2) +} + +// preProcessState holds the encoder input high-pass biquad memory. +type preProcessState struct { + x0, x1 int16 + y1hi, y1lo, y2hi, y2lo int16 +} + +// process applies the input high-pass filter (with 0.5 scaling) in place over +// lg samples. Mirrors Pre_Process. +func (st *preProcessState) process(signal []int16, lg int) { + xn2 := st.x1 + xn1 := st.x0 + for i := 0; i < lg; i++ { + L := int32(st.y1hi) * 7807 + L += (int32(st.y1lo) * 7807) >> 15 + L += int32(st.y2hi) * -3733 + st.y2hi = st.y1hi + L += (int32(st.y2lo) * -3733) >> 15 + st.y2lo = st.y1lo + L += int32(xn2) * 1899 + xn2 = xn1 + L += int32(xn1) * -3798 + xn1 = signal[i] + L += int32(xn1) * 1899 + + signal[i] = int16((L + 0x800) >> 12) + st.y1hi = int16(L >> 12) + st.y1lo = int16((L << 3) - (int32(st.y1hi) << 15)) + } + st.x1 = xn2 + st.x0 = xn1 +} + +// autocorr windows x with wind and computes the order-m autocorrelation into +// (rh,rl) double precision, returning the normalization shift applied. Mirrors +// Autocorr. +func autocorr(x []int16, m int, rh, rl, wind []int16) int16 { + var y [cL_WINDOW]int16 + + var sum int32 + overflow := false + b := 0 + for ; b < cL_WINDOW; b++ { + temp := int16((int32(x[b])*int32(wind[b]) + 0x4000) >> 15) + y[b] = temp + sum += (int32(temp) * int32(temp)) << 1 + if sum < 0 { + overflow = true + break + } + } + + overflShft := int16(0) + if overflow { + for k := b + 1; k < cL_WINDOW; k++ { + y[k] = int16((int32(x[k])*int32(wind[k]) + 0x4000) >> 15) + } + for { + overflShft += 4 + sum = 0 + for k := 0; k < cL_WINDOW; k++ { + t := y[k] >> 2 + y[k] = t + sum += (int32(t) * int32(t)) << 1 + } + if sum > 0 { + break + } + } + } + + sum++ // avoid all-zeros + norm := norm_l(sum) + sum <<= uint(norm) + rh[0] = int16(sum >> 16) + rl[0] = int16((sum >> 1) - (int32(rh[0]) << 15)) + + for i := 1; i <= m; i++ { + // sum_{n=i}^{W-1} y[n]·y[n-i] = sum_k y[i+k]·y[k] (wrapping int32) via firDot + s := firDot(y[i:cL_WINDOW], y[:cL_WINDOW-i]) + s <<= uint(norm + 1) + rh[i] = int16(s >> 16) + rl[i] = int16((s >> 1) - (int32(rh[i]) << 15)) + } + + return norm - overflShft +} + +// lagWindow applies the lag-window bandwidth expansion to the autocorrelation +// in place. Mirrors Lag_window. +func lagWindow(m int, rh, rl []int16) { + for i := 1; i <= m; i++ { + x := mpy_32(rh[i], rl[i], lag_h[i-1], lag_l[i-1]) + rh[i] = int16(x >> 16) + rl[i] = int16((x >> 1) - (int32(rh[i]) << 15)) + } +} + +// levinsonState carries the last stable A(z) for the unstable-filter fallback. +type levinsonState struct { + oldA [cMP1]int16 +} + +// levinson runs the Levinson-Durbin recursion on the (rh,rl) autocorrelation, +// producing order-10 LP coefficients A[0:M+1] and the first 4 reflection +// coefficients rc[0:4]. Mirrors Levinson. +func (st *levinsonState) levinson(rh, rl, a, rc []int16) { + var ah, al, anh, anl [cMP1]int16 + + t1 := (int32(rh[1]) << 16) + (int32(rl[1]) << 1) + t2 := L_abs(t1) + t0 := div_32(t2, rh[0], rl[0]) // R[1]/R[0] + if t1 > 0 { + t0 = L_negate(t0) + } + kh, kl := L_Extract(t0) + rc[0] = round_(t0) + + t0 >>= 4 + ah[1] = int16(t0 >> 16) + al[1] = int16((t0 >> 1) - (int32(ah[1]) << 15)) + + t0 = mpy_32(kh, kl, kh, kl) // K*K + t0 = L_abs(t0) + t0 = 0x7fffffff - t0 // 1 - K*K + hi, lo := L_Extract(t0) + t0 = mpy_32(rh[0], rl[0], hi, lo) // Alpha + alpExp := norm_l(t0) + t0 = t0 << uint(alpExp) + alpH := int16(t0 >> 16) + alpL := int16((t0 >> 1) - (int32(alpH) << 15)) + + for i := 2; i <= cM; i++ { + t0 = 0 + for j := 1; j < i; j++ { + t0 += (int32(rh[j]) * int32(al[i-j])) >> 15 + t0 += (int32(rl[j]) * int32(ah[i-j])) >> 15 + t0 += int32(rh[j]) * int32(ah[i-j]) + } + t0 <<= 5 + t1 = (int32(rh[i]) << 16) + (int32(rl[i]) << 1) + t0 += t1 + + t1 = L_abs(t0) + t2 = div_32(t1, alpH, alpL) + if t0 > 0 { + t2 = L_negate(t2) + } + t2 = L_shl(t2, alpExp) + kh = int16(t2 >> 16) + kl = int16((t2 >> 1) - (int32(kh) << 15)) + + if i < 5 { + rc[i-1] = int16((t2 + 0x8000) >> 16) + } + if abs_s(kh) > 32750 { // unstable filter -> reuse last A(z) + copy(a[:cMP1], st.oldA[:]) + for k := 0; k < 4; k++ { + rc[k] = 0 + } + return + } + + for j := 1; j < i; j++ { + t0 = (int32(kh) * int32(al[i-j])) >> 15 + t0 += (int32(kl) * int32(ah[i-j])) >> 15 + t0 += int32(kh) * int32(ah[i-j]) + t0 += (int32(ah[j]) << 15) + int32(al[j]) + anh[j] = int16(t0 >> 15) + anl[j] = int16(t0 - (int32(anh[j]) << 15)) + } + anh[i] = int16(t2 >> 20) + anl[i] = int16((t2 >> 5) - (int32(anh[i]) << 15)) + + t0 = mpy_32(kh, kl, kh, kl) // K*K + t0 = L_abs(t0) + t0 = 0x7fffffff - t0 // 1 - K*K + hi = int16(t0 >> 16) + lo = int16((t0 >> 1) - (int32(hi) << 15)) + + t0 = (int32(alpH) * int32(lo)) >> 15 + t0 += (int32(alpL) * int32(hi)) >> 15 + t0 += int32(alpH) * int32(hi) + t0 <<= 1 + jn := norm_l(t0) + t0 = t0 << uint(jn) + alpH = int16(t0 >> 16) + alpL = int16((t0 >> 1) - (int32(alpH) << 15)) + alpExp += jn + + copy(ah[1:1+i], anh[1:1+i]) + copy(al[1:1+i], anl[1:1+i]) + } + + a[0] = 4096 + for i := 1; i <= cM; i++ { + t0 = (int32(ah[i]) << 15) + int32(al[i]) + a[i] = int16((t0 + 0x2000) >> 14) + st.oldA[i] = a[i] + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_lsp.go b/internal/capability/transcode/amrnb/dsp_enc_lsp.go new file mode 100644 index 0000000..d3779dd --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_lsp.go @@ -0,0 +1,54 @@ +package amrnb + +// Encoder LSP analysis/quantization wrapper, ported from opencore-amrnb lsp.cpp +// (lsp). Per frame it converts the analysis A(z) to LSPs (azLsp), quantizes them +// (qPlsf3/qPlsf5), and interpolates both the unquantized LP filters (az, for the +// perceptual weighting) and the quantized ones (azQ, for synthesis) across the +// four subframes, returning the LSF codebook indices. Bit-exact with the +// reference. + +// lspEncState holds the encoder's LSP predictor memories. +type lspEncState struct { + lspOld [cM]int16 + lspOldQ [cM]int16 + q qPlsfState +} + +func (st *lspEncState) reset() { + copy(st.lspOld[:], lsp_init_data[:cM]) + copy(st.lspOldQ[:], lsp_init_data[:cM]) + st.q.reset() +} + +// lsp processes one frame's LP analysis. az holds the unquantized A(z) with the +// per-window filters already placed (A_mid at [MP1] for MR122, A_new at [3*MP1]); +// on return az and azQ hold the interpolated unquantized/quantized filters for +// all four subframes. The LSF indices are written to anap; the count is +// returned. Mirrors lsp. +func (st *lspEncState) lsp(mode int, az, azQ, anap []int16) int { + var lspNew [cM]int16 + if mode == dMR122 { + var lspMid, lspMidQ, lspNewQ [cM]int16 + azLsp(az[cMP1:], lspMid[:], st.lspOld[:]) + azLsp(az[3*cMP1:], lspNew[:], lspMid[:]) + intLpc1and3_2(st.lspOld[:], lspMid[:], lspNew[:], az) + + idx := st.q.qPlsf5(lspMid[:], lspNew[:], lspMidQ[:], lspNewQ[:]) + intLpc1and3(st.lspOldQ[:], lspMidQ[:], lspNewQ[:], azQ) + copy(anap[:5], idx[:]) + copy(st.lspOldQ[:], lspNewQ[:]) + copy(st.lspOld[:], lspNew[:]) + return 5 + } + + var lspNewQ [cM]int16 + azLsp(az[3*cMP1:], lspNew[:], st.lspOld[:]) + intLpc1to3_2(st.lspOld[:], lspNew[:], az) + + idx := st.q.qPlsf3(mode, lspNew[:], lspNewQ[:]) + intLpc1to3(st.lspOldQ[:], lspNewQ[:], azQ) + copy(anap[:3], idx[:]) + copy(st.lspOldQ[:], lspNewQ[:]) + copy(st.lspOld[:], lspNew[:]) + return 3 +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_olpitch.go b/internal/capability/transcode/amrnb/dsp_enc_olpitch.go new file mode 100644 index 0000000..2458a76 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_olpitch.go @@ -0,0 +1,137 @@ +package amrnb + +// Open-loop pitch estimation, ported from opencore-amrnb pitch_ol.cpp +// (Pitch_ol, Lag_max) and calc_cor.cpp (comp_corr). pitchOl scales the weighted +// speech, computes its autocorrelation over the pitch-lag range, finds the best +// lag in three sub-ranges, and selects among them favouring longer lags unless +// a shorter lag's normalized correlation is significantly stronger. The DTX/VAD +// tone-detection side path is omitted (dtx=0). Bit-exact with the reference for +// the active-speech path. + +const cTHRESHOLD = 27853 // 0.85, the open-loop lag-selection threshold + +// lagMax finds the lag in [lagMin, lagMax] with maximum correlation and returns +// it together with its normalized correlation. Mirrors Lag_max (dtx=0 path). +func lagMax(corr []int32, scaled []int16, pitMax int, scalFac, scalFlag int16, Lframe, lagMax, lagMin int) (int16, int16) { + max := int32(minInt32) + pMax := int16(lagMax) + for i := lagMax; i >= lagMin; i-- { + if corr[i] >= max { + max = corr[i] + pMax = int16(i) + } + } + + var t0 int32 + base := pitMax - int(pMax) + for m := 0; m < Lframe; m++ { + v := int32(scaled[base+m]) + t0 += v * v + } + t0 <<= 1 + t0 = invSqrt(t0) + if scalFlag != 0 { + if t0 > 0x3fffffff { + t0 = maxInt32 + } else { + t0 <<= 1 + } + } + + maxH := int16(max >> 16) + maxL := int16((max >> 1) - (int32(maxH) << 15)) + enerH := int16(t0 >> 16) + enerL := int16((t0 >> 1) - (int32(enerH) << 15)) + t0 = mpy_32(maxH, maxL, enerH, enerL) + + var corMax int16 + if scalFlag != 0 { + t0 = L_shr(t0, scalFac) + if t0 > 0x0000FFFF { + corMax = maxInt16 + } else if t0 < int32(-0x10000) { + corMax = minInt16 + } else { + corMax = int16(t0 >> 1) + } + } else { + corMax = int16(t0) + } + return pMax, corMax +} + +// pitchOl returns the open-loop pitch lag for the weighted speech in +// signal[off-pitMax : off+Lframe], using the given pitch-lag bounds and frame +// length. Mirrors Pitch_ol (dtx=0). signal[off] is the current frame start. +func pitchOl(mode int, signal []int16, off, pitMin, pitMax, Lframe int) int16 { + // energy of signal[-pitMax .. Lframe-1] + var t0 int32 + overflow := false + for i := -pitMax; i < Lframe; i++ { + v := int32(signal[off+i]) + t0 += (v * v) << 1 + if t0 < 0 { + t0 = maxInt32 + overflow = true + break + } + } + + n := pitMax + Lframe + var scaledArr [cPIT_MAX + cL_FRAME]int16 // stack: max pitMax+Lframe + scaled := scaledArr[:n] + var scalFac int16 + switch { + case overflow: // t0 == MAX_32 + for k := 0; k < n; k++ { + scaled[k] = int16(int32(signal[off-pitMax+k]) >> 3) + } + scalFac = 3 + case t0 < 1048576: + for k := 0; k < n; k++ { + scaled[k] = int16(int32(signal[off-pitMax+k]) << 3) + } + scalFac = -3 + default: + for k := 0; k < n; k++ { + scaled[k] = signal[off-pitMax+k] + } + scalFac = 0 + } + + // correlation by lag: corr[lag] = 2 * sum_m scaled[pitMax+m]·scaled[pitMax+m-lag]. + // With the fixed window coef = scaled[pitMax:pitMax+Lframe] this is one sliding + // FIR over scaled: dst[pitMax-lag] = sum_m scaled[pitMax-lag+m]·coef[m] equals + // corr[lag]>>1. firRaw (AVX2) vectorises it; the wrapping int32 accumulation + // makes the lane-parallel order bit-identical. + var corrArr [cPIT_MAX + 1]int32 // stack + corr := corrArr[:pitMax+1] + var dstArr [cPIT_MAX + 1]int32 + dst := dstArr[:pitMax-pitMin+1] + firRaw(dst, scaled, scaled[pitMax:pitMax+Lframe]) + for lag := pitMin; lag <= pitMax; lag++ { + corr[lag] = dst[pitMax-lag] << 1 + } + + scalFlag := int16(0) + if mode == dMR122 { + scalFlag = 1 + } + + j := pitMin << 2 + pMax1, max1 := lagMax(corr, scaled, pitMax, scalFac, scalFlag, Lframe, pitMax, j) + i := j - 1 + j = pitMin << 1 + pMax2, max2 := lagMax(corr, scaled, pitMax, scalFac, scalFlag, Lframe, i, j) + i = j - 1 + pMax3, max3 := lagMax(corr, scaled, pitMax, scalFac, scalFlag, Lframe, i, pitMin) + + if int16((int32(max1)*cTHRESHOLD)>>15) < max2 { + max1 = max2 + pMax1 = pMax2 + } + if int16((int32(max1)*cTHRESHOLD)>>15) < max3 { + pMax1 = pMax3 + } + return pMax1 +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_olwgh.go b/internal/capability/transcode/amrnb/dsp_enc_olwgh.go new file mode 100644 index 0000000..68a2d5b --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_olwgh.go @@ -0,0 +1,134 @@ +package amrnb + +// Weighted open-loop pitch analysis for MR102, ported from opencore-amrnb +// p_ol_wgh.cpp (Pitch_ol_wgh, Lag_max) and calc_cor.cpp (comp_corr). Unlike the +// plain Pitch_ol used by the other modes, MR102 weights the lag correlations by +// a fixed curve and, when the previous frame tracked pitch well, by a window +// centred on the running median lag — then adapts whether to keep weighting. +// It also emits a per-half-frame gain flag that gates the median update and the +// closed-loop lag history. Bit-exact with the reference (non-DTX path). + +// pitchOLWghtState is the weighted open-loop pitch state. Mirrors +// pitchOLWghtState. +type pitchOLWghtState struct { + oldT0Med int16 // running median of past lags + adaW int16 // adaptive weighting factor, Q15 + wghtFlg int16 // whether the median-window weighting is applied +} + +func (st *pitchOLWghtState) reset() { + st.oldT0Med = 40 + st.adaW = 0 + st.wghtFlg = 0 +} + +// compCorr computes corr[-i] = 2·Σ_j sig[j]·sig[j-i] (wrapping int32) for every +// lag i in [pitMin, pitMax], where corr is indexed corr[corrOff-i]. Mirrors +// comp_corr. +func compCorr(sig []int16, sigOff, Lframe, pitMax, pitMin int, corr []int32, corrOff int) { + // corr[corrOff-i] = 2·sum_j sig[sigOff+j]·sig[sigOff+j-i]. With the fixed + // window coef = sig[sigOff:sigOff+Lframe] over x = sig[sigOff-pitMax:], this + // is one sliding FIR: dst[pitMax-i] = sum_j sig[sigOff-i+j]·coef[j] equals + // corr[corrOff-i]>>1. firRaw (AVX2) vectorises it, bit-identically (wrapping). + var dstArr [cPIT_MAX + 1]int32 + dst := dstArr[:pitMax-pitMin+1] + firRaw(dst, sig[sigOff-pitMax:], sig[sigOff:sigOff+Lframe]) + for i := pitMin; i <= pitMax; i++ { + corr[corrOff-i] = dst[pitMax-i] << 1 + } +} + +// lagMaxWght finds the best (most weighted-correlated) lag and the open-loop +// gain flag. Mirrors the weighted Lag_max (non-DTX path: cor_max is unused). +func lagMaxWght(corr []int32, corrOff int, sig []int16, sigOff int, Lframe, lagMax, lagMin, oldLag, wghtFlg int16) (pMax, gainFlg int16) { + wwi := 250 // &corrweight[250] + wei := 123 + int(lagMax) - int(oldLag) // &corrweight[123 + lag_max - old_lag] + max := int32(-2147483648) // MIN_32 + pMax = lagMax + for i := lagMax; i >= lagMin; i-- { + hi, lo := L_Extract(corr[corrOff-int(i)]) + t0 := mpy32_16(hi, lo, corrweight[wwi]) + wwi-- + if wghtFlg > 0 { + hi, lo = L_Extract(t0) + t0 = mpy32_16(hi, lo, corrweight[wei]) + wei-- + } + if t0 >= max { + max = t0 + pMax = i + } + } + + var t0, t1 int32 + for j := 0; j < int(Lframe); j++ { + a := sig[sigOff+j] + b := sig[sigOff+j-int(pMax)] + t0 = L_mac(t0, a, b) + t1 = L_mac(t1, b, b) + } + temp := round_(t1) + t1 = L_msu(t0, temp, 13107) // t0 - round(t1)*0.4 + gainFlg = round_(t1) + return +} + +// pitchOlWgh runs the MR102 weighted open-loop pitch over one half-frame of the +// weighted speech (sig[sigOff-pitMax : sigOff+Lframe]), updating the state and +// the gain flag, and maintaining the median-lag history. Mirrors Pitch_ol_wgh +// (non-DTX). idx selects the half-frame (0 or 1). +func (st *pitchOLWghtState) pitchOlWgh(sig []int16, sigOff, pitMin, pitMax, Lframe int, oldLags, olGainFlg []int16, idx int) int16 { + // energy of the (history + frame) signal, to choose the scaling + var t0 int32 + for i := -pitMax; i < Lframe; i++ { + t0 = L_mac(t0, sig[sigOff+i], sig[sigOff+i]) + } + + // scaled signal occupies scal[pitMax-pitMax : pitMax+Lframe]; use a buffer + // with the same [pitMax | Lframe] layout as the input. + var scalArr [cPIT_MAX + cL_FRAME]int16 // stack + scal := scalArr[:pitMax+Lframe] + scalOff := pitMax + switch { + case t0 == maxInt32: + for i := -pitMax; i < Lframe; i++ { + scal[scalOff+i] = shr(sig[sigOff+i], 3) + } + case t0 < 1048576: + for i := -pitMax; i < Lframe; i++ { + scal[scalOff+i] = shl(sig[sigOff+i], 3) + } + default: + for i := -pitMax; i < Lframe; i++ { + scal[scalOff+i] = sig[sigOff+i] + } + } + + var corrArr [cPIT_MAX + 1]int32 // stack + corr := corrArr[:pitMax+1] + corrOff := pitMax + compCorr(scal, scalOff, Lframe, pitMax, pitMin, corr, corrOff) + + pMax1, gainFlg := lagMaxWght(corr, corrOff, scal, scalOff, int16(Lframe), + int16(pitMax), int16(pitMin), st.oldT0Med, st.wghtFlg) + olGainFlg[idx] = gainFlg + + if olGainFlg[idx] > 0 { + for i := 4; i > 0; i-- { + oldLags[i] = oldLags[i-1] + } + oldLags[0] = pMax1 + st.oldT0Med = gmedN(oldLags, 5) + st.adaW = 32767 + } else { + st.oldT0Med = pMax1 + st.adaW = mult(st.adaW, 29491) // ada_w *= 0.9 + } + + if st.adaW < 9830 { // < 0.3 + st.wghtFlg = 0 + } else { + st.wghtFlg = 1 + } + return pMax1 +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_pitch.go b/internal/capability/transcode/amrnb/dsp_enc_pitch.go new file mode 100644 index 0000000..98d1837 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_pitch.go @@ -0,0 +1,192 @@ +package amrnb + +// Pitch-lag encoding, ported from opencore-amrnb enc_lag3.cpp and enc_lag6.cpp. +// encLag3/encLag6 map a (T0, T0_frac) closed-loop pitch lag to the index the +// bitstream carries — the exact inverses of the decoder's decLag3/decLag6. +// Bit-exact with the reference. + +// encLag3 encodes a 1/3-resolution pitch lag. deltaFlag==0 selects the 1st/3rd +// subframe (absolute); otherwise the lag is coded relative to T0_min (or, with +// flag4, the 4-bit scheme). Mirrors Enc_lag3. +func encLag3(T0, T0frac, T0prev, T0min, T0max, deltaFlag, flag4 int16) int16 { + var index, i, tmpInd, uplag, tmpLag, temp1, temp2 int16 + + if deltaFlag == 0 { // 1st or 3rd subframe + if T0-85 <= 0 { + index = (T0 << 1) + T0 - 58 + T0frac + } else { + index = T0 + 112 + } + } else { // 2nd or 4th subframe + if flag4 == 0 { + i = T0 - T0min + index = i + (i << 1) + 2 + T0frac + } else { + tmpLag = T0prev + temp1 = tmpLag - T0min + temp2 = temp1 - 5 + if temp2 > 0 { + tmpLag = T0min + 5 + } + temp1 = T0max - tmpLag + temp2 = temp1 - 4 + if temp2 > 0 { + tmpLag = T0max - 4 + } + uplag = T0 + (T0 << 1) + T0frac + i = tmpLag - 2 + tmpInd = i + (i << 1) + temp1 = tmpInd - uplag + if temp1 >= 0 { + index = T0 - tmpLag + 5 + } else { + i = tmpLag + 1 + i += i << 1 + if i > uplag { + index = uplag - tmpInd + 3 + } else { + index = T0 - tmpLag + 11 + } + } + } + } + return index +} + +// gPitch computes the optimal adaptive-codebook (pitch) gain in Q14, the ratio +// / saturated to 1.2, where xn is the pitch target and y1 the +// filtered adaptive excitation. It also returns the normalized correlations in +// gCoeff for later gain quantization. Mirrors G_pitch (g_pitch.cpp). +func gPitch(mode int, xn, y1, gCoeff []int16, Lsubfr int) int16 { + // + var s int32 + for i := 0; i < Lsubfr; i++ { + s += int32(y1[i]) * int32(y1[i]) + } + var yy, expYy int16 + if s >= 0 && s < 0x40000000 { + s = (s << 1) + 1 + expYy = norm_l(s) + yy = round_(s << uint(expYy)) + } else { + s = 0 + for i := 0; i < Lsubfr; i++ { + tmp := y1[i] >> 2 + s += int32(tmp) * int32(tmp) + } + s = (s << 1) + 1 + expYy = norm_l(s) + yy = round_(s << uint(expYy)) + expYy -= 4 + } + + // with overflow detection + s = 0 + overflow := false + for i := 0; i < Lsubfr; i++ { + L := int32(xn[i]) * int32(y1[i]) + s1 := s + s = s1 + L + if (s1^L) > 0 && (s1^s) < 0 { + overflow = true + break + } + } + var xy, expXy int16 + if !overflow { + s = (s << 1) + 1 + expXy = norm_l(s) + xy = round_(s << uint(expXy)) + } else { + s = 0 + for i := 0; i < Lsubfr; i++ { + L := int32(y1[i] >> 2) + s += int32(xn[i]) * L + } + s = (s << 1) + 1 + expXy = norm_l(s) + xy = round_(s << uint(expXy)) + expXy -= 4 + } + + gCoeff[0] = yy + gCoeff[1] = 15 - expYy + gCoeff[2] = xy + gCoeff[3] = 15 - expXy + + if xy < 4 { + return 0 + } + xy >>= 1 + gain := div_s(xy, yy) + gain = shr(gain, expXy-expYy) + if gain > 19661 { + gain = 19661 + } + if mode == dMR122 { + gain &^= 3 + } + return gain +} + +// encLag6 encodes a 1/6-resolution pitch lag (MR122). deltaFlag==0 selects the +// 1st/3rd subframe. Mirrors Enc_lag6. +func encLag6(T0, T0frac, T0min, deltaFlag int16) int16 { + var index, i int16 + if deltaFlag == 0 { + if T0 <= 94 { + i = (T0 << 3) - (T0 << 1) - 105 + index = i + T0frac + } else { + index = T0 + 368 + } + } else { + temp := T0 - T0min + i = (temp << 3) - (temp << 1) + 3 + index = i + T0frac + } + return index +} + +// gCode computes the optimum innovation (fixed-codebook) gain +// / in Q1, used by the MR122 gain quantizer. Mirrors G_code. +func gCode(xn2, y2 []int16) int16 { + var s int32 + for i := 0; i < cL_SUBFR; i++ { + s += int32(xn2[i]) * int32(y2[i]>>1) + } + s <<= 1 + expXy := norm_l(s + 1) // avoid the all-zeros case + var xy int16 + if expXy < 17 { + xy = int16(s >> uint(17-expXy)) + } else { + xy = int16(s << uint(expXy-17)) + } + if xy <= 0 { + return 0 + } + + s = 0 + for i := 0; i < cL_SUBFR; i++ { + temp := y2[i] >> 1 + s += (int32(temp) * int32(temp)) >> 2 + } + s <<= 3 + expYy := norm_l(s) + var yy int16 + if expYy < 16 { + yy = int16(s >> uint(16-expYy)) + } else { + yy = int16(s << uint(expYy-16)) + } + + gain := div_s(xy, yy) + i := expXy + 5 - expYy // 15-1+9-18 = 5 + if i > 1 { + gain >>= uint(i - 1) + } else { + gain <<= uint(1 - i) + } + return gain +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_pre.go b/internal/capability/transcode/amrnb/dsp_enc_pre.go new file mode 100644 index 0000000..b81fbfd --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_pre.go @@ -0,0 +1,64 @@ +package amrnb + +// Per-subframe target-signal and impulse-response setup, ported from +// opencore-amrnb spreproc.cpp (subframePreProc). For each subframe it builds the +// perceptually weighted LP filters A(z/γ1)/A(z/γ2), the weighted synthesis +// impulse response h1, the LP residual (exc/res2), and the target signal xn for +// the pitch search — feeding the closed-loop pitch and codebook searches. Uses +// weightAi/synFilt/residu (all already ported). Bit-exact with the reference. + +// perceptual-weighting spectral expansion factors (cod_amr.cpp). +var gamma1 = [cM]int16{30802, 28954, 27217, 25584, 24049, 22606, 21250, 19975, 18777, 17650} +var gamma1_12k2 = [cM]int16{29491, 26542, 23888, 21499, 19349, 17414, 15672, 14105, 12694, 11425} +var gamma2 = [cM]int16{19661, 11797, 7078, 4247, 2548, 1529, 917, 550, 330, 198} + +// subframePreProc computes, for one subframe: +// - h1[0:L_SUBFR] the weighted synthesis impulse response, +// - exc/res2 the LP residual, +// - error the synthesis error, +// - xn[0:L_SUBFR] the pitch-search target. +// +// a/aq are the unquantized/quantized LP coefficients for this subframe (M+1 +// each). speech is positioned so speech[speechOff-M : speechOff+L_SUBFR] is the +// current subframe with history. memErr and memW0 are the synthesis-error and +// weighting-filter memories (M each), read but not updated here. Mirrors +// subframePreProc. +func subframePreProc(mode int, a, aq, speech []int16, speechOff int, memErr, memW0 []int16, exc, h1, xn, res2, errSig []int16) { + var ap1, ap2 [cMP1]int16 + g1 := gamma1[:] + if mode == dMR122 || mode == dMR102 { + g1 = gamma1_12k2[:] + } + weightAi(a, g1, ap1[:]) + weightAi(a, gamma2[:], ap2[:]) + + // h1 = impulse response of A(z/γ1) / (Aq(z)·A(z/γ2)) + var aiZero [cL_SUBFR]int16 + copy(aiZero[:cMP1], ap1[:cMP1]) // Ap1 then zeros + zero := make([]int16, cM) + synFilt(aq, aiZero[:], h1, cL_SUBFR, zero, false) + for i := range zero { + zero[i] = 0 + } + synFilt(ap2[:], h1, h1, cL_SUBFR, zero, false) + + // LP residual and synthesis error + residu(aq, speech, speechOff, cL_SUBFR, res2) + copy(exc[:cL_SUBFR], res2[:cL_SUBFR]) + + // error = synthesis of the residual (with error memory) + memErrCopy := make([]int16, cM) + copy(memErrCopy, memErr[:cM]) + synFilt(aq, exc, errSig, cL_SUBFR, memErrCopy, false) + + // xn = weighted error (target for pitch search) + // Residu(Ap1, error): error needs M history -> supplied as memErr before it. + var eb [cM + cL_SUBFR]int16 + copy(eb[:cM], memErr[:cM]) + copy(eb[cM:], errSig[:cL_SUBFR]) + residu(ap1[:], eb[:], cM, cL_SUBFR, xn) + + memW0Copy := make([]int16, cM) + copy(memW0Copy, memW0[:cM]) + synFilt(ap2[:], xn, xn, cL_SUBFR, memW0Copy, false) +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_qgain.go b/internal/capability/transcode/amrnb/dsp_enc_qgain.go new file mode 100644 index 0000000..4ea465a --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_qgain.go @@ -0,0 +1,138 @@ +package amrnb + +// Gain quantization (general modes), ported from opencore-amrnb qua_gain.cpp +// (Qua_gain) and calc_en.cpp (calc_filt_energies). calcFiltEnergies forms the +// five quadratic-form energy coefficients of the gain error from the target, +// the adaptive codebook y1 and the filtered innovation y2; quaGain searches the +// gain VQ table for the (pitch, code) gain pair minimizing that error, returning +// the index and the dequantized gains — the exact encoder counterpart of +// decGain. Bit-exact with the reference. + +const ( + cVQ_SIZE_HIGHRATES = 128 + cVQ_SIZE_LOWRATES = 64 +) + +// calcFiltEnergies computes the five energy coefficients (frac/exp) of the gain +// MSE. gCoeff[0..3] are the / correlations from gPitch. For +// MR795/MR475 it also returns the optimum unconstrained code gain. Mirrors +// calc_filt_energies. +func calcFiltEnergies(mode int, xn, xn2, y1, y2, gCoeff []int16) (fracCoeff, expCoeff [5]int16, codGainFrac, codGainExp int16) { + fracCoeff[0] = gCoeff[0] + expCoeff[0] = gCoeff[1] + fracCoeff[1] = negate(gCoeff[2]) + expCoeff[1] = gCoeff[3] + 1 + + var s1, s2, s3 int32 + if mode != dMR795 && mode != dMR475 { + s1, s2, s3 = 1, 1, 1 + } + var scaledY2 [cL_SUBFR]int16 + for i := 0; i < cL_SUBFR; i++ { + tmp := y2[i] >> 3 + scaledY2[i] = tmp + s1 = L_mac(s1, tmp, tmp) + s2 = L_mac(s2, xn[i], tmp) + s3 = L_mac(s3, y1[i], tmp) + } + exp := norm_l(s1) + fracCoeff[2] = int16(L_shl(s1, exp) >> 16) + expCoeff[2] = -3 - exp + exp = norm_l(s2) + fracCoeff[3] = negate(int16(L_shl(s2, exp) >> 16)) + expCoeff[3] = 7 - exp + exp = norm_l(s3) + fracCoeff[4] = int16(L_shl(s3, exp) >> 16) + expCoeff[4] = 7 - exp + + if mode == dMR795 || mode == dMR475 { + s1 = 0 + for i := 0; i < cL_SUBFR; i++ { + s1 += int32(xn2[i]) * int32(scaledY2[i]) + } + s1 <<= 1 + exp = norm_l(s1) + frac := int16(L_shl(s1, exp) >> 16) + exp = 6 - exp + if frac <= 0 { + codGainFrac = 0 + codGainExp = 0 + } else { + codGainFrac = div_s(shr(frac, 1), fracCoeff[2]) + codGainExp = (exp - expCoeff[2]) - 14 + } + } + return +} + +// quaGain searches the mode's gain VQ table for the (pitch, code) gain pair +// minimizing the weighted gain MSE, returning the index and dequantized gains +// plus the predictor-update energies. Mirrors Qua_gain. +func quaGain(mode int, expGcode0, fracGcode0 int16, fracCoeff, expCoeff []int16, gpLimit int16) (index, gainPit, gainCod, quaEnerMR122, quaEner int16) { + var tableGain []int16 + var tableLen int + if mode == dMR102 || mode == dMR74 || mode == dMR67 { + tableLen = cVQ_SIZE_HIGHRATES + tableGain = table_gain_highrates + } else { + tableLen = cVQ_SIZE_LOWRATES + tableGain = table_gain_lowrates + } + + gcode0 := int16(pow2(14, fracGcode0)) + expCode := expGcode0 - 11 + + var expMax [5]int16 + expMax[0] = expCoeff[0] - 13 + expMax[1] = expCoeff[1] - 14 + temp := shl(expCode, 1) + 15 + expMax[2] = add(expCoeff[2], temp) + expMax[3] = add(expCoeff[3], expCode) + expMax[4] = add(expCoeff[4], expCode+1) + + eMax := expMax[0] + for i := 1; i < 5; i++ { + if expMax[i] > eMax { + eMax = expMax[i] + } + } + eMax++ + + var coeff, coeffLo [5]int16 + for i := 0; i < 5; i++ { + j := eMax - expMax[i] + coeff[i], coeffLo[i] = L_Extract(L_shr(int32(fracCoeff[i])<<16, j)) + } + + distMin := int32(maxInt32) + for i := 0; i < tableLen; i++ { + p := i << 2 + gPitch := tableGain[p] + gCode := tableGain[p+1] + if gPitch <= gpLimit { + gc := mult(gCode, gcode0) + g2Pitch := mult(gPitch, gPitch) + g2Code := mult(gc, gc) + gPitCod := mult(gc, gPitch) + L_tmp := mpy32_16(coeff[0], coeffLo[0], g2Pitch) + L_tmp = L_add(L_tmp, mpy32_16(coeff[1], coeffLo[1], gPitch)) + L_tmp = L_add(L_tmp, mpy32_16(coeff[2], coeffLo[2], g2Code)) + L_tmp = L_add(L_tmp, mpy32_16(coeff[3], coeffLo[3], gc)) + L_tmp = L_add(L_tmp, mpy32_16(coeff[4], coeffLo[4], gPitCod)) + if L_tmp < distMin { + distMin = L_tmp + index = int16(i) + } + } + } + + p := int(index) << 2 + gainPit = tableGain[p] + gCode := tableGain[p+1] + quaEnerMR122 = tableGain[p+2] + quaEner = tableGain[p+3] + + L_tmp := L_shr(L_mult(gCode, gcode0), 10-expGcode0) + gainCod = int16(L_tmp >> 16) + return +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_qgain475.go b/internal/capability/transcode/amrnb/dsp_enc_qgain475.go new file mode 100644 index 0000000..ac5d0d3 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_qgain475.go @@ -0,0 +1,196 @@ +package amrnb + +// MR475 joint two-subframe gain quantization, ported from opencore-amrnb +// qgain475.cpp (MR475_gain_quant, MR475_quant_store_results, +// MR475_update_unq_pred) and the target-energy helper calc_en.cpp +// (calc_target_energy). The 4.75 kbit/s mode quantizes the pitch and code gains +// of two consecutive subframes jointly with a single 8-bit index into +// table_gain_MR475, using an "unquantized" gain predictor (predUnq) to drive the +// search and the real predictor (pred) for the reconstructed gains. Bit-exact +// with the reference. + +const ( + cMIN_QUA_ENER = -5443 + cMIN_QUA_ENER_MR122 = -32768 + cMAX_QUA_ENER = 3037 + cMAX_QUA_ENER_MR122 = 18284 +) + +// calcTargetEnergy returns the energy of the LTP target xn as a normalized +// (exponent, fraction) pair. Mirrors calc_target_energy. +func calcTargetEnergy(xn []int16) (enExp, enFrac int16) { + var s int32 + for i := 0; i < cL_SUBFR; i++ { + s += int32(xn[i]) * int32(xn[i]) // 32-bit MAC, wraps like the reference + } + if s < 0 { // overflow wrapped negative + s = maxInt32 + } + exp := norm_l(s) + enFrac = int16(L_shl(s, exp) >> 16) + enExp = 16 - exp + return +} + +// mr475StoreResults dequantizes one subframe's gains from a table entry p +// (p[0]=gain_pit, p[1]=gain_code) given the predicted code gain gcode0, and +// advances the predictor with the resulting quantized energy error. Mirrors +// MR475_quant_store_results. +func (pred *gcPredState) mr475StoreResults(p []int16, gcode0, expGcode0 int16) (gainPit, gainCod int16) { + gainPit = p[0] + gCode := p[1] + + L := (int32(gCode) * int32(gcode0)) << 1 + L = L_shr(L, 10-expGcode0) + gainCod = int16(L >> 16) + + exp, frac := log2(int32(gCode)) + exp -= 12 + tmp := shr_r(frac, 5) + quaEnerMR122 := (exp << 10) + tmp + L2 := mpy32_16(exp, frac, 24660) << 13 + quaEner := int16((L2 + 0x00008000) >> 16) + + pred.update(quaEnerMR122, quaEner) + return +} + +// mr475UpdateUnqPred advances the "unquantized" predictor with the optimum +// (unquantized) code gain of a subframe. Mirrors MR475_update_unq_pred. +func (pred *gcPredState) mr475UpdateUnqPred(expGcode0, fracGcode0, codGainExp, codGainFrac int16) { + var quaEner, quaEnerMR122 int16 + if codGainFrac <= 0 { + quaEner = cMIN_QUA_ENER + quaEnerMR122 = cMIN_QUA_ENER_MR122 + } else { + fg := int16(pow2(14, fracGcode0)) + if codGainFrac >= fg { + codGainFrac >>= 1 + codGainExp++ + } + frac := div_s(codGainFrac, fg) + tmp := codGainExp - expGcode0 - 1 + exp, fr := log2(int32(frac)) + exp += tmp + quaEnerMR122 = shr_r(fr, 5) + (exp << 10) + if quaEnerMR122 > cMAX_QUA_ENER_MR122 { + quaEner = cMAX_QUA_ENER + quaEnerMR122 = cMAX_QUA_ENER_MR122 + } else { + L := L_shl(mpy32_16(exp, fr, 24660), 13) + quaEner = round_(L) + } + } + pred.update(quaEnerMR122, quaEner) +} + +// mr475GainQuant runs the joint pitch+code gain VQ for a subframe pair: it +// searches table_gain_MR475 for the entry minimizing the combined weighted +// error of both subframes (respecting the pitch-gain limit), stores the +// reconstructed gains for both subframes, and returns the table index. The sf0 +// energy terms were stashed in st by the even-subframe pass; the sf1 terms are +// passed in. Mirrors MR475_gain_quant. +func (st *coderState) mr475GainQuant(sf1Code []int16, sf1ExpGcode0, sf1FracGcode0 int16, + sf1ExpCoeff, sf1FracCoeff []int16, sf1ExpTargetEn, sf1FracTargetEn, gpLimit int16) (index, sf0GainPit, sf0GainCod, sf1GainPit, sf1GainCod int16) { + + sf0Gcode0 := int16(pow2(14, st.sf0FracGcode0)) + sf1Gcode0 := int16(pow2(14, sf1FracGcode0)) + + // per-coefficient exponents (0..4: sf0, 5..9: sf1) + var expMax [10]int16 + exp := st.sf0ExpGcode0 - 11 + expMax[0] = st.sf0ExpCoeff[0] - 13 + expMax[1] = st.sf0ExpCoeff[1] - 14 + expMax[2] = st.sf0ExpCoeff[2] + (15 + (exp << 1)) + expMax[3] = st.sf0ExpCoeff[3] + exp + expMax[4] = st.sf0ExpCoeff[4] + (1 + exp) + exp = sf1ExpGcode0 - 11 + expMax[5] = sf1ExpCoeff[0] - 13 + expMax[6] = sf1ExpCoeff[1] - 14 + expMax[7] = sf1ExpCoeff[2] + (15 + (exp << 1)) + expMax[8] = sf1ExpCoeff[3] + exp + expMax[9] = sf1ExpCoeff[4] + (1 + exp) + + // scale the two target energies to a common exponent, then nudge the sf0 + // coefficient exponents so the two subframes' errors are comparable. + sf0FracTargetEn := st.sf0FracTargetEn + if e := st.sf0ExpTargetEn - sf1ExpTargetEn; e > 0 { + sf1FracTargetEn >>= uint(e) + } else { + sf0FracTargetEn >>= uint(-e) + } + adj := int16(0) + if tmp := shr_r(sf1FracTargetEn, 1); tmp > sf0FracTargetEn { + adj = 1 + } else if (sf0FracTargetEn+3)>>2 > sf1FracTargetEn { + adj = -1 + } + for i := 0; i < 5; i++ { + expMax[i] += adj + } + + // common exponent for all 10 coefficients + mx := expMax[0] + for i := 9; i > 0; i-- { + if expMax[i] > mx { + mx = expMax[i] + } + } + mx++ // avoid overflow + + var coeff, coeffLo [10]int16 + for i := 0; i < 10; i++ { + var fc int16 + if i < 5 { + fc = st.sf0FracCoeff[i] + } else { + fc = sf1FracCoeff[i-5] + } + L := L_shr(int32(fc)<<16, mx-expMax[i]) + coeff[i] = int16(L >> 16) + coeffLo[i] = int16((L >> 1) - ((L >> 16) << 15)) + } + + // exhaustive search over the 256-entry joint codebook + distMin := int32(maxInt32) + index = 0 + for i := 0; i < cMR475_VQ_SIZE; i++ { + b := i * 4 + gPitch := table_gain_MR475[b] + gCode := int16((int32(table_gain_MR475[b+1]) * int32(sf0Gcode0)) >> 15) + g2Pitch := int16((int32(gPitch) * int32(gPitch)) >> 15) + g2Code := int16((int32(gCode) * int32(gCode)) >> 15) + gPitCod := int16((int32(gCode) * int32(gPitch)) >> 15) + L := mpy32_16(coeff[0], coeffLo[0], g2Pitch) + + mpy32_16(coeff[1], coeffLo[1], gPitch) + + mpy32_16(coeff[2], coeffLo[2], g2Code) + + mpy32_16(coeff[3], coeffLo[3], gCode) + + mpy32_16(coeff[4], coeffLo[4], gPitCod) + + tmp := gPitch - gpLimit + gPitch1 := table_gain_MR475[b+2] + if tmp <= 0 && gPitch1 <= gpLimit { + gCode1 := int16((int32(table_gain_MR475[b+3]) * int32(sf1Gcode0)) >> 15) + g2Pitch = int16((int32(gPitch1) * int32(gPitch1)) >> 15) + g2Code = int16((int32(gCode1) * int32(gCode1)) >> 15) + gPitCod = int16((int32(gCode1) * int32(gPitch1)) >> 15) + L += mpy32_16(coeff[5], coeffLo[5], g2Pitch) + + mpy32_16(coeff[6], coeffLo[6], gPitch1) + + mpy32_16(coeff[7], coeffLo[7], g2Code) + + mpy32_16(coeff[8], coeffLo[8], gCode1) + + mpy32_16(coeff[9], coeffLo[9], gPitCod) + if L < distMin { + distMin = L + index = int16(i) + } + } + } + + // reconstruct sf0 gains (updates the real predictor), re-predict, then sf1 + base := int(index) << 2 + sf0GainPit, sf0GainCod = st.pred.mr475StoreResults(table_gain_MR475[base:], sf0Gcode0, st.sf0ExpGcode0) + gp := st.pred.gcPred(dMR475, sf1Code) + sf1Gcode0 = int16(pow2(14, gp.fracGcode0)) + sf1GainPit, sf1GainCod = st.pred.mr475StoreResults(table_gain_MR475[base+2:], sf1Gcode0, gp.expGcode0) + return +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_qgain795.go b/internal/capability/transcode/amrnb/dsp_enc_qgain795.go new file mode 100644 index 0000000..8ef7992 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_qgain795.go @@ -0,0 +1,314 @@ +package amrnb + +// MR795 gain quantization, ported from opencore-amrnb qgain795.cpp +// (MR795_gain_quant, MR795_gain_code_quant3, MR795_gain_code_quant_mod), the +// gain adaptor g_adapt.cpp (gain_adapt) + gmed_n.cpp (gmed_n), and the unfiltered +// energies calc_en.cpp (calc_unfilt_energies). MR795 jointly searches the three +// nearest pitch-gain candidates against the code-gain VQ, then — when the LTP is +// strong enough (alpha>0) — re-quantizes the code gain with a perceptually +// adapted criterion. It emits two indices per subframe. Bit-exact with the +// reference. + +const ( + cLTP_GAIN_THR1 = 2721 // Q13 + cLTP_GAIN_THR2 = 5443 // Q13 + cLTPG_MEM_SIZE = 5 +) + +// gmedN returns the median of the first n values of ind, matching the +// index-based tie-breaking of the reference. Mirrors gmed_n. +func gmedN(ind []int16, n int) int16 { + tmp2 := make([]int16, n) + copy(tmp2, ind[:n]) + tmp := make([]int16, n) + ix := 0 + for i := 0; i < n; i++ { + max := int16(-32767) + for j := 0; j < n; j++ { + if tmp2[j] >= max { + max = tmp2[j] + ix = j + } + } + tmp2[ix] = -32768 + tmp[i] = int16(ix) + } + return ind[tmp[n>>1]] +} + +// gainAdaptState is the gain adaptor's persistent state. Mirrors GainAdaptState. +type gainAdaptState struct { + onset int16 + prevAlpha int16 + prevGc int16 + ltpgMem [cLTPG_MEM_SIZE]int16 +} + +// gainAdapt computes the gain adaptation factor alpha (Q15) from the LTP coding +// gain ltpg and the code gain, updating the onset/history state. Mirrors +// gain_adapt. +func (st *gainAdaptState) gainAdapt(ltpg, gainCod int16) int16 { + var adapt int16 + switch { + case ltpg <= cLTP_GAIN_THR1: + adapt = 0 + case ltpg <= cLTP_GAIN_THR2: + adapt = 1 + default: + adapt = 2 + } + + if tmp := shr_r(gainCod, 1); tmp > st.prevGc && gainCod > 200 { + st.onset = 8 + } else if st.onset != 0 { + st.onset-- + } + if st.onset != 0 && adapt < 2 { + adapt++ + } + + st.ltpgMem[0] = ltpg + filt := gmedN(st.ltpgMem[:], 5) + var result int16 + if adapt == 0 { + switch { + case filt > 5443: + result = 0 + case filt < 0: + result = 16384 + default: + filt = shl(filt, 2) + result = 16384 - mult(24660, filt) + } + } + if st.prevAlpha == 0 { + result = shr(result, 1) + } + + st.prevAlpha = result + st.prevGc = gainCod + for i := cLTPG_MEM_SIZE - 1; i > 0; i-- { + st.ltpgMem[i] = st.ltpgMem[i-1] + } + return result +} + +// calcUnfiltEnergies computes the unfiltered residual/excitation/cross/LTP-error +// energies (as 4 normalized exp/frac pairs) and the LTP coding gain ltpg. +// Mirrors calc_unfilt_energies. +func calcUnfiltEnergies(res, exc, code []int16, gainPit int16, Lsubfr int) (fracEn, expEn [4]int16, ltpg int16) { + var s1, s2, s3, s4 int32 + for i := 0; i < Lsubfr; i++ { + tmp1 := res[i] + tmp2 := exc[i] + s1 += int32(tmp1) * int32(tmp1) // wrapping MAC, like the reference + s2 += int32(tmp2) * int32(tmp2) + s3 += int32(tmp2) * int32(code[i]) + Ltemp := L_shl(L_mult(tmp2, gainPit), 1) + d := sub(tmp1, round_(Ltemp)) + s4 = L_mac(s4, d, d) + } + s1 <<= 1 + s2 <<= 1 + s3 <<= 1 + + if s1 < 0 { // overflow (sign bit set) + s1 = maxInt32 + } + if s1 < 400 { + fracEn[0] = 0 + expEn[0] = -15 + } else { + exp := norm_l(s1) + fracEn[0] = int16(L_shl(s1, exp) >> 16) + expEn[0] = 15 - exp + } + + if s2 < 0 { + s2 = maxInt32 + } + exp := norm_l(s2) + fracEn[1] = int16(L_shl(s2, exp) >> 16) + expEn[1] = 15 - exp + + exp = norm_l(s3) + fracEn[2] = int16(L_shl(s3, exp) >> 16) + expEn[2] = 2 - exp + + exp = norm_l(s4) + ltpResEn := int16(L_shl(s4, exp) >> 16) + expS4 := 15 - exp + fracEn[3] = ltpResEn + expEn[3] = expS4 + + if ltpResEn > 0 && fracEn[0] != 0 { + predGain := div_s(shr(fracEn[0], 1), ltpResEn) + e := sub(expS4, expEn[0]) + Ltemp := L_shr(int32(predGain)<<16, e+3) + ltpgExp, ltpgFrac := log2(Ltemp) + Ltemp = L_Comp(ltpgExp-27, ltpgFrac) + ltpg = round_(L_shl(Ltemp, 13)) // Q13 + } + return +} + +// mr795GainCodeQuant3 jointly searches the three pitch-gain candidates against +// the code-gain VQ for the minimum weighted error, returning the selected pitch +// and code gains, their indices, and the predictor-update energies. Mirrors +// MR795_gain_code_quant3. +func mr795GainCodeQuant3(expGcode0, gcode0 int16, gpCand, gpCind, fracCoeff, expCoeff []int16) (gainPit, gainPitInd, gainCod, gainCodInd, quaEnerMR122, quaEner int16) { + var coeff, coeffLo, expMax [5]int16 + expCode := expGcode0 - 10 + expMax[0] = expCoeff[0] - 13 + expMax[1] = expCoeff[1] - 14 + expMax[2] = expCoeff[2] + shl(expCode, 1) + 15 + expMax[3] = expCoeff[3] + expCode + expMax[4] = expCoeff[4] + (expCode + 1) + + eMax := expMax[0] + for i := 1; i < 5; i++ { + if expMax[i] > eMax { + eMax = expMax[i] + } + } + eMax = add(eMax, 1) + for i := 0; i < 5; i++ { + coeff[i], coeffLo[i] = L_Extract(L_shr(int32(fracCoeff[i])<<16, eMax-expMax[i])) + } + + distMin := int32(maxInt32) + var codInd, pitInd int16 + for j := 0; j < 3; j++ { + gPitch := gpCand[j] + g2Pitch := mult(gPitch, gPitch) + Ltmp0 := mpy32_16(coeff[0], coeffLo[0], g2Pitch) + Ltmp0 = L_add(Ltmp0, mpy32_16(coeff[1], coeffLo[1], gPitch)) + for i := 0; i < cNB_QUA_CODE; i++ { + gCode := mult(qua_gain_code[i*3], gcode0) + g2h, g2l := L_Extract(L_mult(gCode, gCode)) + gpch, gpcl := L_Extract(L_mult(gCode, gPitch)) + Ltmp := L_add(Ltmp0, mpy_32(coeff[2], coeffLo[2], g2h, g2l)) + Ltmp = L_add(Ltmp, mpy32_16(coeff[3], coeffLo[3], gCode)) + Ltmp = L_add(Ltmp, mpy_32(coeff[4], coeffLo[4], gpch, gpcl)) + if Ltmp < distMin { + distMin = Ltmp + codInd = int16(i) + pitInd = int16(j) + } + } + } + + base := int(codInd) * 3 + gCode := qua_gain_code[base] + quaEnerMR122 = qua_gain_code[base+1] + quaEner = qua_gain_code[base+2] + gainCod = int16(L_shr(L_mult(gCode, gcode0), 9-expGcode0) >> 16) + gainCodInd = codInd + gainPit = gpCand[pitInd] + gainPitInd = gpCind[pitInd] + return +} + +// mr795GainCodeQuantMod re-quantizes the code gain with the gain-adapted +// criterion once the LTP is strong (alpha>0), returning the new index and +// predictor energies and updating gainCod in place. Mirrors +// MR795_gain_code_quant_mod. +func mr795GainCodeQuantMod(gainPit, expGcode0, gcode0 int16, fracEn, expEn []int16, alpha, gainCodUnq int16, gainCod *int16) (index, quaEnerMR122, quaEner int16) { + var coeff, coeffLo, expCoeff [5]int16 + gainCode := shl(*gainCod, 10-expGcode0) // Q1 -> Q11(-ec0) + g2Pitch := mult(gainPit, gainPit) + oneAlpha := add(32767-alpha, 1) // 32768 - alpha + + Lt1 := L_shl(L_mult(alpha, fracEn[1]), 1) + Lt1 = L_mult(int16(Lt1>>16), g2Pitch) + expCoeff[1] = expEn[1] - 15 + + coeff[2] = mult(int16(L_shl(L_mult(alpha, fracEn[2]), 1)>>16), gainPit) + expCoeff[2] = add(expEn[2], expGcode0-10) + + coeff[3] = int16(L_shl(L_mult(alpha, fracEn[3]), 1) >> 16) + expCoeff[3] = add(expEn[3], shl(expGcode0, 1)-7) + + coeff[4] = mult(oneAlpha, fracEn[3]) + expCoeff[4] = add(expCoeff[3], 1) + + Lt0, sqExp := sqrtLExp(L_mult(alpha, fracEn[0])) + sqExp += 47 + expCoeff[0] = expEn[0] - sqExp + + eMax := expCoeff[0] + 31 + for i := 1; i <= 4; i++ { + if expCoeff[i] > eMax { + eMax = expCoeff[i] + } + } + Lt1 = L_shr(Lt1, eMax-expCoeff[1]) + for i := 2; i <= 4; i++ { + coeff[i], coeffLo[i] = L_Extract(L_shr(int32(coeff[i])<<16, eMax-expCoeff[i])) + } + newExp := eMax - 31 + tmp := newExp - expCoeff[0] + Lt0 = L_shr(Lt0, shr(tmp, 1)) + if tmp&0x1 != 0 { + coeff[0], coeffLo[0] = L_Extract(Lt0) + Lt0 = mpy32_16(coeff[0], coeffLo[0], 23170) // 1/sqrt(2) Q15 + } + + distMin := int32(maxInt32) + index = 0 + for i := 0; i < cNB_QUA_CODE; i++ { + gCode := mult(qua_gain_code[i*3], gcode0) + if gCode >= gainCode { + break + } + g2h, g2l := L_Extract(L_mult(gCode, gCode)) + d := sub(gCode, gainCodUnq) + d2h, d2l := L_Extract(L_mult(d, d)) + Ltmp := L_add(Lt1, mpy32_16(coeff[2], coeffLo[2], gCode)) + Ltmp = L_add(Ltmp, mpy_32(coeff[3], coeffLo[3], g2h, g2l)) + sq, e := sqrtLExp(Ltmp) + sq = L_shr(sq, shr(e, 1)) + t := round_(L_sub(sq, Lt0)) + Ltmp = L_mult(t, t) + Ltmp = L_add(Ltmp, mpy_32(coeff[4], coeffLo[4], d2h, d2l)) + if Ltmp < distMin { + distMin = Ltmp + index = int16(i) + } + } + + base := int(index) * 3 + gCode := qua_gain_code[base] + quaEnerMR122 = qua_gain_code[base+1] + quaEner = qua_gain_code[base+2] + *gainCod = int16(L_shr(L_mult(gCode, gcode0), 9-expGcode0) >> 16) + return +} + +// mr795GainQuant orchestrates the MR795 gain quantization for one subframe, +// updating *gainPit and returning the quantized gains, predictor energies and +// the two analysis indices. Mirrors MR795_gain_quant. +func (st *coderState) mr795GainQuant(res, exc, code, fracCoeff, expCoeff []int16, + expCodeEn, fracCodeEn, expGcode0, fracGcode0, codGainFrac, codGainExp, gpLimit int16, + gainPit *int16) (gainCod, quaEnerMR122, quaEner, gainPitIndex, gainCodIndex int16) { + + var gpCand, gpCind [3]int16 + qGainPitch(dMR795, gpLimit, gainPit, gpCand[:], gpCind[:]) + gcode0 := int16(pow2(14, fracGcode0)) + + gp, gpInd, gc, gcInd, qe122, qe := mr795GainCodeQuant3(expGcode0, gcode0, gpCand[:], gpCind[:], fracCoeff, expCoeff) + *gainPit = gp + gainPitIndex, gainCod = gpInd, gc + gainCodIndex, quaEnerMR122, quaEner = gcInd, qe122, qe + + fracEn, expEn, ltpg := calcUnfiltEnergies(res, exc, code, *gainPit, cL_SUBFR) + alpha := st.adapt.gainAdapt(ltpg, gainCod) + if fracEn[0] != 0 && alpha > 0 { + fracEn[3] = fracCodeEn + expEn[3] = expCodeEn + gainCodUnq := shl(codGainFrac, sub(codGainExp, expGcode0)+10) + gainCodIndex, quaEnerMR122, quaEner = mr795GainCodeQuantMod(*gainPit, expGcode0, gcode0, + fracEn[:], expEn[:], alpha, gainCodUnq, &gainCod) + } + return +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_qgainp.go b/internal/capability/transcode/amrnb/dsp_enc_qgainp.go new file mode 100644 index 0000000..7ce4716 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_qgainp.go @@ -0,0 +1,99 @@ +package amrnb + +// Separate pitch/code gain quantizers (MR122; pitch also used by MR795), ported +// from opencore-amrnb q_gain_p.cpp (q_gain_pitch) and q_gain_c.cpp +// (q_gain_code). qGainPitch picks the nearest quantized pitch gain; qGainCode +// picks the code-gain index minimizing the error to the predicted gain. The +// exact encoder counterparts of dGainPitch / dGainCode. Bit-exact with the +// reference. + +const ( + cNB_QUA_PITCH = 16 + cNB_QUA_CODE = 32 +) + +// qGainPitch quantizes the pitch gain (in place) to the nearest table entry +// within gpLimit, returning the index. For MR795 it also fills three candidate +// gains/indices. Mirrors q_gain_pitch. +func qGainPitch(mode int, gpLimit int16, gain *int16, gainCand, gainCind []int16) int16 { + errMin := abs_s(sub(*gain, qua_gain_pitch[0])) + index := int16(0) + for i := int16(1); i < cNB_QUA_PITCH; i++ { + if qua_gain_pitch[i] <= gpLimit { + err := abs_s(sub(*gain, qua_gain_pitch[i])) + if err < errMin { + errMin = err + index = i + } + } + } + + if mode == dMR795 { + var ii int16 + switch { + case index == 0: + ii = index + case index == cNB_QUA_PITCH-1 || qua_gain_pitch[index+1] > gpLimit: + ii = index - 2 + default: + ii = index - 1 + } + for i := 0; i < 3; i++ { + gainCind[i] = ii + gainCand[i] = qua_gain_pitch[ii] + ii++ + } + *gain = qua_gain_pitch[index] + } else if mode == dMR122 { + *gain = qua_gain_pitch[index] &^ 3 + } else { + *gain = qua_gain_pitch[index] + } + return index +} + +// qGainCode quantizes the fixed-codebook gain (in place) against the predicted +// gain, returning the index and the predictor-update energies. Mirrors +// q_gain_code. +func qGainCode(mode int, expGcode0, fracGcode0 int16, gain *int16) (index, quaEnerMR122, quaEner int16) { + var gQ0 int16 + if mode == dMR122 { + gQ0 = *gain >> 1 // Q1 -> Q0 + } else { + gQ0 = *gain + } + + gcode0 := int16(pow2(expGcode0, fracGcode0)) + if mode == dMR122 { + gcode0 = shl(gcode0, 4) + } else { + gcode0 = shl(gcode0, 5) + } + + errMin := gQ0 - int16((int32(gcode0)*int32(qua_gain_code[0]))>>15) + if errMin < 0 { + errMin = -errMin + } + index = 0 + for i := int16(1); i < cNB_QUA_CODE; i++ { + err := gQ0 - int16((int32(gcode0)*int32(qua_gain_code[i*3]))>>15) + if err < 0 { + err = -err + } + if err < errMin { + errMin = err + index = i + } + } + + p := int(index) * 3 + temp := int16((int32(gcode0) * int32(qua_gain_code[p])) >> 15) + if mode == dMR122 { + *gain = temp << 1 + } else { + *gain = temp + } + quaEnerMR122 = qua_gain_code[p+1] + quaEner = qua_gain_code[p+2] + return +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_qlsp.go b/internal/capability/transcode/amrnb/dsp_enc_qlsp.go new file mode 100644 index 0000000..11c9790 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_qlsp.go @@ -0,0 +1,321 @@ +package amrnb + +// Encoder LSF quantization, ported from opencore-amrnb q_plsf_3.cpp (Q_plsf_3, +// Vq_subvec3, Vq_subvec4) and lsfwt.cpp (Lsf_wt). Q_plsf_3 is the split-VQ LSF +// quantizer for all modes except 12.2 kbit/s: it converts the analysis LSPs to +// weighted LSFs, removes the MA prediction, and searches the LSF codebooks for +// the indices the bitstream carries. It is the exact encoder counterpart of the +// decoder's dPlsf3 — feeding its indices back through dPlsf3 reproduces the same +// quantized LSPs. Bit-exact with the reference. + +// qPlsfState holds the encoder LSF MA-predictor memory. Mirrors Q_plsfState. +type qPlsfState struct { + pastRQ [cM]int16 +} + +func (st *qPlsfState) reset() { + for i := range st.pastRQ { + st.pastRQ[i] = 0 + } +} + +// lsfWt computes the LSF weighting factors (squared, Q13<<3). Mirrors Lsf_wt. +func lsfWt(lsf, wf []int16) { + wf[0] = lsf[1] + for i := 1; i <= 8; i++ { + wf[i] = lsf[i+1] - lsf[i-1] + } + wf[9] = 16384 - lsf[8] // reference's pointer ends at lsf[M-2] here, not lsf[M-1] + + for i := 0; i < cM; i++ { + wgt := wf[i] + temp := wgt - 1843 + if temp > 0 { + temp = int16((int32(temp) * 6242) >> 15) + wgt = 1843 - temp + } else { + temp = int16((int32(wgt) * 28160) >> 15) + wgt = 3427 - temp + } + wf[i] = shl(wgt, 3) + } +} + +// vqSubvec3 finds the weighted nearest codebook entry for a 3-element LSF +// residual, writing the chosen entry back into lsfR1[0:3]. useHalf searches only +// every second codebook entry. Mirrors Vq_subvec3. +func vqSubvec3(lsfR1, dico, wf1 []int16, dicoSize int, useHalf bool) int16 { + distMin := int32(maxInt32) + index := int16(0) + stride := 0 + if useHalf { + stride = 3 + } + l0, l1, l2 := lsfR1[0], lsfR1[1], lsfR1[2] + w0, w1, w2 := wf1[0], wf1[1], wf1[2] + + di := 0 + advance := 3 + stride + for i := 0; i < dicoSize; i++ { + row := dico[di : di+3] // hoist the per-dim bounds checks + di += advance + t := int16((int32(w0) * int32(l0-row[0])) >> 15) + dist := int32(t) * int32(t) + t = int16((int32(w1) * int32(l1-row[1])) >> 15) + dist += int32(t) * int32(t) + t = int16((int32(w2) * int32(l2-row[2])) >> 15) + dist += int32(t) * int32(t) + + if dist < distMin { + distMin = dist + index = int16(i) + } + } + + pd := 3 * int(index) + if useHalf { + pd += 3 * int(index) + } + lsfR1[0] = dico[pd] + lsfR1[1] = dico[pd+1] + lsfR1[2] = dico[pd+2] + return index +} + +// vqSubvec4 finds the weighted nearest codebook entry for a 4-element LSF +// residual, writing the chosen entry back into lsfR1[0:4]. Mirrors Vq_subvec4. +func vqSubvec4(lsfR1, dico, wf1 []int16, dicoSize int) int16 { + distMin := int32(maxInt32) + index := int16(0) + l0, l1, l2, l3 := lsfR1[0], lsfR1[1], lsfR1[2], lsfR1[3] + w0, w1, w2, w3 := wf1[0], wf1[1], wf1[2], wf1[3] + + di := 0 + for i := 0; i < dicoSize; i++ { + row := dico[di : di+4] // hoist the per-dim bounds checks + di += 4 + t := int16((int32(w0) * int32(l0-row[0])) >> 15) + dist := int32(t) * int32(t) + t = int16((int32(w1) * int32(l1-row[1])) >> 15) + dist += int32(t) * int32(t) + t = int16((int32(w2) * int32(l2-row[2])) >> 15) + dist += int32(t) * int32(t) + t = int16((int32(w3) * int32(l3-row[3])) >> 15) + dist += int32(t) * int32(t) + + if dist < distMin { + distMin = dist + index = int16(i) + } + } + + pd := int(index) << 2 + lsfR1[0] = dico[pd] + lsfR1[1] = dico[pd+1] + lsfR1[2] = dico[pd+2] + lsfR1[3] = dico[pd+3] + return index +} + +// qPlsf3 quantizes the analysis LSPs for the given mode into three split-VQ +// indices and the quantized LSP vector, updating the predictor. Mirrors +// Q_plsf_3 (non-DTX path). Returns the three indices. +func (st *qPlsfState) qPlsf3(mode int, lsp1 []int16, lsp1q []int16) [3]int16 { + var lsf1, wf1, lsfP, lsfR1, lsf1q [cM]int16 + + lspLsf(lsp1, lsf1[:], cM) + lsfWt(lsf1[:], wf1[:]) + + for i := 0; i < cM; i++ { + temp := int16((int32(st.pastRQ[i]) * int32(pred_fac_3[i])) >> 15) + lsfP[i] = mean_lsf_3[i] + temp + lsfR1[i] = lsf1[i] - lsfP[i] + } + + var indice [3]int16 + switch { + case mode == dMR475 || mode == dMR515: + indice[0] = vqSubvec3(lsfR1[0:], dico1_lsf_3, wf1[0:], cDICO1_SIZE, false) + indice[1] = vqSubvec3(lsfR1[3:], dico2_lsf_3, wf1[3:], cDICO2_SIZE/2, true) + indice[2] = vqSubvec4(lsfR1[6:], mr515_3_lsf, wf1[6:], cMR515_3_SIZE) + case mode == dMR795: + indice[0] = vqSubvec3(lsfR1[0:], mr795_1_lsf, wf1[0:], cMR795_1_SIZE, false) + indice[1] = vqSubvec3(lsfR1[3:], dico2_lsf_3, wf1[3:], cDICO2_SIZE, false) + indice[2] = vqSubvec4(lsfR1[6:], dico3_lsf_3, wf1[6:], cDICO3_SIZE) + default: // MR59, MR67, MR74, MR102 + indice[0] = vqSubvec3(lsfR1[0:], dico1_lsf_3, wf1[0:], cDICO1_SIZE, false) + indice[1] = vqSubvec3(lsfR1[3:], dico2_lsf_3, wf1[3:], cDICO2_SIZE, false) + indice[2] = vqSubvec4(lsfR1[6:], dico3_lsf_3, wf1[6:], cDICO3_SIZE) + } + + for i := 0; i < cM; i++ { + lsf1q[i] = add(lsfR1[i], lsfP[i]) + st.pastRQ[i] = lsfR1[i] + } + + reorderLsf(lsf1q[:], cLSF_GAP, cM) + lsfLsp(lsf1q[:], lsp1q, cM) + return indice +} + +// LSF-5 split-VQ codebook sizes (q_plsf_5_tbl.cpp). +const ( + cDICO1_5_SIZE = 128 + cDICO2_5_SIZE = 256 + cDICO3_5_SIZE = 256 + cDICO4_5_SIZE = 256 + cDICO5_5_SIZE = 64 +) + +// vqSubvec finds the weighted nearest 4-element codebook entry (two LSFs from +// each of the two LSF vectors), writing it back into lsfR1[0:2]/lsfR2[0:2]. +// Mirrors Vq_subvec. +func vqSubvec(lsfR1, lsfR2, dico, wf1, wf2 []int16, dicoSize int) int16 { + distMin := int32(maxInt32) + index := int16(0) + w10, w11 := wf1[0], wf1[1] + w20, w21 := wf2[0], wf2[1] + aux1 := int32(lsfR1[0]) * int32(w10) + aux2 := int32(lsfR1[1]) * int32(w11) + aux3 := int32(lsfR2[0]) * int32(w20) + aux4 := int32(lsfR2[1]) * int32(w21) + + for i := 0; i < dicoSize; i++ { + row := dico[i*4 : i*4+4] // hoist the per-dim bounds checks + temp := int16((aux1 - int32(w10)*int32(row[0])) >> 15) + dist := int32(temp) * int32(temp) + if dist >= distMin { + continue + } + temp = int16((aux2 - int32(w11)*int32(row[1])) >> 15) + dist += int32(temp) * int32(temp) + if dist >= distMin { + continue + } + temp = int16((aux3 - int32(w20)*int32(row[2])) >> 15) + dist += int32(temp) * int32(temp) + if dist >= distMin { + continue + } + temp = int16((aux4 - int32(w21)*int32(row[3])) >> 15) + dist += int32(temp) * int32(temp) + if dist < distMin { + distMin = dist + index = int16(i) + } + } + + pd := int(index) << 2 + lsfR1[0] = dico[pd] + lsfR1[1] = dico[pd+1] + lsfR2[0] = dico[pd+2] + lsfR2[1] = dico[pd+3] + return index +} + +// vqSubvecS is vqSubvec with sign selection: each codebook entry is tried as-is +// and negated; the index LSB encodes the sign. Mirrors Vq_subvec_s. +func vqSubvecS(lsfR1, lsfR2, dico, wf1, wf2 []int16, dicoSize int) int16 { + distMin := int32(maxInt32) + index := int16(0) + sign := int16(0) + l10, l11 := lsfR1[0], lsfR1[1] + l20, l21 := lsfR2[0], lsfR2[1] + w10, w11 := wf1[0], wf1[1] + w20, w21 := wf2[0], wf2[1] + + for i := 0; i < dicoSize; i++ { + row := dico[i*4 : i*4+4] // hoist the per-dim bounds checks + temp := row[0] + t1 := int16((int32(w10) * int32(l10-temp)) >> 15) + t2 := int16((int32(w10) * int32(l10+temp)) >> 15) + dist1 := int32(t1) * int32(t1) + dist2 := int32(t2) * int32(t2) + + temp = row[1] + t1 = int16((int32(w11) * int32(l11-temp)) >> 15) + t2 = int16((int32(w11) * int32(l11+temp)) >> 15) + dist1 += int32(t1) * int32(t1) + dist2 += int32(t2) * int32(t2) + + if dist1 >= distMin && dist2 >= distMin { + continue + } + + temp = row[2] + t1 = int16((int32(w20) * int32(l20-temp)) >> 15) + t2 = int16((int32(w20) * int32(l20+temp)) >> 15) + dist1 += int32(t1) * int32(t1) + dist2 += int32(t2) * int32(t2) + + temp = row[3] + t1 = int16((int32(w21) * int32(l21-temp)) >> 15) + t2 = int16((int32(w21) * int32(l21+temp)) >> 15) + dist1 += int32(t1) * int32(t1) + dist2 += int32(t2) * int32(t2) + + if dist1 < distMin { + distMin = dist1 + index = int16(i) + sign = 0 + } + if dist2 < distMin { + distMin = dist2 + index = int16(i) + sign = 1 + } + } + + pd := int(index) << 2 + index <<= 1 + if sign != 0 { + lsfR1[0] = -dico[pd] + lsfR1[1] = -dico[pd+1] + lsfR2[0] = -dico[pd+2] + lsfR2[1] = -dico[pd+3] + index++ + } else { + lsfR1[0] = dico[pd] + lsfR1[1] = dico[pd+1] + lsfR2[0] = dico[pd+2] + lsfR2[1] = dico[pd+3] + } + return index +} + +// qPlsf5 quantizes two LSP vectors (MR122) into five indices and the two +// quantized LSP vectors, updating the predictor. The exact encoder counterpart +// of dPlsf5. Mirrors Q_plsf_5. +func (st *qPlsfState) qPlsf5(lsp1, lsp2, lsp1q, lsp2q []int16) [5]int16 { + var lsf1, lsf2, wf1, wf2, lsfP, lsfR1, lsfR2, lsf1q, lsf2q [cM]int16 + + lspLsf(lsp1, lsf1[:], cM) + lspLsf(lsp2, lsf2[:], cM) + lsfWt(lsf1[:], wf1[:]) + lsfWt(lsf2[:], wf2[:]) + + for i := 0; i < cM; i++ { + lsfP[i] = mean_lsf_5[i] + int16((int32(st.pastRQ[i])*cLSP_PRED_FAC_MR122)>>15) + lsfR1[i] = lsf1[i] - lsfP[i] + lsfR2[i] = lsf2[i] - lsfP[i] + } + + var indice [5]int16 + indice[0] = vqSubvec(lsfR1[0:], lsfR2[0:], dico1_lsf_5, wf1[0:], wf2[0:], cDICO1_5_SIZE) + indice[1] = vqSubvec(lsfR1[2:], lsfR2[2:], dico2_lsf_5, wf1[2:], wf2[2:], cDICO2_5_SIZE) + indice[2] = vqSubvecS(lsfR1[4:], lsfR2[4:], dico3_lsf_5, wf1[4:], wf2[4:], cDICO3_5_SIZE) + indice[3] = vqSubvec(lsfR1[6:], lsfR2[6:], dico4_lsf_5, wf1[6:], wf2[6:], cDICO4_5_SIZE) + indice[4] = vqSubvec(lsfR1[8:], lsfR2[8:], dico5_lsf_5, wf1[8:], wf2[8:], cDICO5_5_SIZE) + + for i := 0; i < cM; i++ { + lsf1q[i] = lsfR1[i] + lsfP[i] + lsf2q[i] = lsfR2[i] + lsfP[i] + st.pastRQ[i] = lsfR2[i] + } + + reorderLsf(lsf1q[:], cLSF_GAP, cM) + reorderLsf(lsf2q[:], cLSF_GAP, cM) + lsfLsp(lsf1q[:], lsp1q, cM) + lsfLsp(lsf2q[:], lsp2q, cM) + return indice +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_s108.go b/internal/capability/transcode/amrnb/dsp_enc_s108.go new file mode 100644 index 0000000..2681bcc --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_s108.go @@ -0,0 +1,206 @@ +package amrnb + +// Multi-pulse algebraic codebook search, ported from opencore-amrnb s10_8pf.cpp +// (search_10and8i40). This parameterized depth-first search places 8 (MR102) or +// 10 (MR122) pulses, two tracks at a time, using a precomputed interleaved +// scratch (temp1) to cache the partial ps/alp values of the second pulse of each +// pair across the first pulse's positions. The pulse-track ordering (ipos) is +// rotated across the outer loop. The single most intricate function in the +// codec; the exact incremental criterion arithmetic is preserved (bit-exact). +func search10and8i40(nbPulse, step, nbTracks int16, dn, rr, ipos, posMax, codvec []int16) { + const L = cL_CODE + gsmefr := nbPulse == 10 + + var index [10]int16 + var temp1 [2 * cL_CODE]int16 + + i0 := posMax[ipos[0]] + index[0] = i0 + + psk := int16(-1) + alpk := int16(1) + for i := int16(0); i < nbPulse; i++ { + codvec[i] = i + } + + var i1, i2, i3, i4, i5, i6, i7 int16 + + for outer := int16(1); outer < nbTracks; outer++ { + i1 = posMax[ipos[1]] + index[1] = i1 + + ps0 := int16(int32(dn[i0]) + int32(dn[i1])) + alp0 := int32(rr[int(i0)*L+int(i0)]) << 12 + alp0 += int32(rr[int(i1)*L+int(i1)]) << 12 + alp0 += int32(rr[int(i0)*L+int(i1)]) << 13 + alp0 += 0x8000 + + // ---- pulses 2 & 3 ---- + ti := 0 + for i3v := ipos[3]; i3v < L; i3v += step { + rrRow := rr[int(i3v)*L : int(i3v)*L+L] + s := int32(rrRow[int(i3v)]) >> 1 + s += int32(rrRow[int(i0)]) + s += int32(rrRow[int(i1)]) + temp1[ti] = ps0 + dn[i3v] + temp1[ti+1] = int16((s + 2) >> 2) + ti += 2 + } + + sq := int16(-1) + alp := int16(1) + ps := int16(0) + ia := ipos[2] + ib := ipos[3] + sbase := alp0 >> 12 + for j := ipos[2]; j < L; j += step { + rrRow := rr[int(j)*L : int(j)*L+L] + alp1 := (sbase + int32(rrRow[int(j)])) >> 1 + alp1 += int32(rrRow[int(i0)]) + alp1 += int32(rrRow[int(i1)]) + ps1 := dn[j] + ti = 0 + for i3v := ipos[3]; i3v < L; i3v += step { + ps2 := ps1 + temp1[ti] + sq2 := int16((int32(ps2) * int32(ps2)) >> 15) + alp2 := (alp1 + int32(rrRow[int(i3v)])) >> 2 + alp2 = (alp2 + int32(temp1[ti+1])) >> 1 + ti += 2 + if int32(sq2)*int32(alp) > int32(sq)*alp2 { + sq, ps, alp, ia, ib = sq2, ps2, int16(alp2), j, i3v + } + } + } + i2, i3 = ia, ib + index[2], index[3] = ia, ib + + // ---- pulses 4 & 5 ---- + alp0 = (int32(alp) << 15) + 0x8000 + ti = 0 + for i5v := ipos[5]; i5v < L; i5v += step { + rrRow := rr[int(i5v)*L : int(i5v)*L+L] + s := int32(rrRow[int(i5v)]) >> 1 + s += int32(rrRow[int(i0)]) + int32(rrRow[int(i1)]) + int32(rrRow[int(i2)]) + int32(rrRow[int(i3)]) + temp1[ti] = ps + dn[i5v] + temp1[ti+1] = int16((s + 2) >> 2) + ti += 2 + } + sq, alp, ps, ia, ib = -1, 1, 0, ipos[4], ipos[5] + for j := ipos[4]; j < L; j += step { + rrRow := rr[int(j)*L : int(j)*L+L] + alp1 := alp0 + (int32(rrRow[int(j)]) << 11) + alp1 += int32(rrRow[int(i0)]) << 12 + alp1 += int32(rrRow[int(i1)]) << 12 + alp1 += int32(rrRow[int(i2)]) << 12 + alp1 += int32(rrRow[int(i3)]) << 12 + ps1 := dn[j] + ti = 0 + for i5v := ipos[5]; i5v < L; i5v += step { + ps2 := ps1 + temp1[ti] + alp2 := alp1 + (int32(rrRow[int(i5v)]) << 12) + alp16 := int16((alp2 + (int32(temp1[ti+1]) << 14)) >> 16) + ti += 2 + sq2 := int16((int32(ps2) * int32(ps2)) >> 15) + if int32(sq2)*int32(alp) > int32(sq)*int32(alp16) { + sq, ps, alp, ia, ib = sq2, ps2, alp16, j, i5v + } + } + } + i4, i5 = ia, ib + index[4], index[5] = ia, ib + + // ---- pulses 6 & 7 ---- + alp0 = (int32(alp) << 15) + 0x8000 + ti = 0 + for i7v := ipos[7]; i7v < L; i7v += step { + rrRow := rr[int(i7v)*L : int(i7v)*L+L] + s := int32(rrRow[int(i7v)]) >> 1 + s += int32(rrRow[int(i0)]) + int32(rrRow[int(i1)]) + int32(rrRow[int(i2)]) + + int32(rrRow[int(i3)]) + int32(rrRow[int(i4)]) + int32(rrRow[int(i5)]) + temp1[ti] = ps + dn[i7v] + temp1[ti+1] = int16((s + 4) >> 3) + ti += 2 + } + sq, alp, ps, ia, ib = -1, 1, 0, ipos[6], ipos[7] + for j := ipos[6]; j < L; j += step { + rrRow := rr[int(j)*L : int(j)*L+L] + alp1 := alp0 + (int32(rrRow[int(j)]) << 10) + alp1 += int32(rrRow[int(i0)]) << 11 + alp1 += int32(rrRow[int(i1)]) << 11 + alp1 += int32(rrRow[int(i2)]) << 11 + alp1 += int32(rrRow[int(i3)]) << 11 + alp1 += int32(rrRow[int(i4)]) << 11 + alp1 += int32(rrRow[int(i5)]) << 11 + ps1 := dn[j] + ti = 0 + for i7v := ipos[7]; i7v < L; i7v += step { + ps2 := ps1 + temp1[ti] + alp2 := alp1 + (int32(rrRow[int(i7v)]) << 11) + alp16 := int16((alp2 + (int32(temp1[ti+1]) << 14)) >> 16) + ti += 2 + sq2 := int16((int32(ps2) * int32(ps2)) >> 15) + if int32(sq2)*int32(alp) > int32(sq)*int32(alp16) { + sq, ps, alp, ia, ib = sq2, ps2, alp16, j, i7v + } + } + } + i6, i7 = ia, ib + index[6], index[7] = ia, ib + + // ---- pulses 8 & 9 (MR122 only) ---- + if gsmefr { + alp0 = (int32(alp) << 15) + 0x8000 + ti = 0 + for i9v := ipos[9]; i9v < L; i9v += step { + rrRow := rr[int(i9v)*L : int(i9v)*L+L] + s := int32(rrRow[int(i9v)]) >> 1 + s += int32(rrRow[int(i0)]) + int32(rrRow[int(i1)]) + int32(rrRow[int(i2)]) + + int32(rrRow[int(i3)]) + int32(rrRow[int(i4)]) + int32(rrRow[int(i5)]) + + int32(rrRow[int(i6)]) + int32(rrRow[int(i7)]) + temp1[ti] = ps + dn[i9v] + temp1[ti+1] = int16((s + 4) >> 3) + ti += 2 + } + sq, alp, ps, ia, ib = -1, 1, 0, ipos[8], ipos[9] + for j := ipos[8]; j < L; j += step { + rrRow := rr[int(j)*L : int(j)*L+L] + alp1 := alp0 + (int32(rrRow[int(j)]) << 9) + alp1 += int32(rr[int(i0)*L+int(j)]) << 10 + alp1 += int32(rr[int(i1)*L+int(j)]) << 10 + alp1 += int32(rr[int(i2)*L+int(j)]) << 10 + alp1 += int32(rr[int(i3)*L+int(j)]) << 10 + alp1 += int32(rr[int(i4)*L+int(j)]) << 10 + alp1 += int32(rr[int(i5)*L+int(j)]) << 10 + alp1 += int32(rr[int(i6)*L+int(j)]) << 10 + alp1 += int32(rr[int(i7)*L+int(j)]) << 10 + ps1 := dn[j] + ti = 0 + for i9v := ipos[9]; i9v < L; i9v += step { + ps2 := ps1 + temp1[ti] + sq2 := int16((int32(ps2) * int32(ps2)) >> 15) + alp2 := alp1 + (int32(rrRow[int(i9v)]) << 10) + alp16 := int16((alp2 + (int32(temp1[ti+1]) << 13)) >> 16) + ti += 2 + if int32(sq2)*int32(alp) > int32(sq)*int32(alp16) { + sq, ps, alp, ia, ib = sq2, ps2, alp16, j, i9v + } + } + } + index[8], index[9] = ia, ib + } + + // ---- keep the best ---- + if int32(alpk)*int32(sq) > int32(psk)*int32(alp) { + psk = sq + alpk = alp + copy(codvec[:nbPulse], index[:nbPulse]) + } + + // rotate ipos[1..nbPulse-1] left by one + pos := ipos[1] + for j := int16(1); j < nbPulse-1; j++ { + ipos[j] = ipos[j+1] + } + ipos[nbPulse-1] = pos + } +} diff --git a/internal/capability/transcode/amrnb/dsp_enc_subfr.go b/internal/capability/transcode/amrnb/dsp_enc_subfr.go new file mode 100644 index 0000000..64a35bc --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_enc_subfr.go @@ -0,0 +1,88 @@ +package amrnb + +// Per-subframe codebook dispatch and post-processing, ported from opencore-amrnb +// cbsearch.cpp (cbsearch) and spstproc.cpp (subframePostProc). cbsearch routes +// to the mode's algebraic codebook search (and applies the MR102/MR122 pitch +// sharpening around it); subframePostProc forms the total excitation, runs the +// local synthesis, and updates the synthesis/error/weighting filter memories +// for the next subframe. Bit-exact with the reference. + +// cbsearch runs the fixed-codebook search for the mode, writing the innovation +// code and filtered code y and the analysis indices into anap (returning the +// count). Mirrors cbsearch. +func cbsearch(x, h []int16, T0, pitchSharp, gainPit int16, res2, code, y []int16, mode, subNr int, anap []int16) int { + switch { + case mode == dMR475 || mode == dMR515: + idx, sgn := code2i40_9bits(int16(subNr), x, h, T0, pitchSharp, code, y) + anap[0], anap[1] = idx, sgn + return 2 + case mode == dMR59: + idx, sgn := code2i40_11bits(x, h, T0, pitchSharp, code, y) + anap[0], anap[1] = idx, sgn + return 2 + case mode == dMR67: + idx, sgn := code3i40_14bits(x, h, T0, pitchSharp, code, y) + anap[0], anap[1] = idx, sgn + return 2 + case mode == dMR74 || mode == dMR795: + idx, sgn := code4i40_17bits(x, h, T0, pitchSharp, code, y) + anap[0], anap[1] = idx, sgn + return 2 + case mode == dMR102: + ps := shl(pitchSharp, 1) + for i := T0; i < cL_SUBFR; i++ { + h[i] = add(h[i], mult(h[i-T0], ps)) + } + code8i40_31bits(x, res2, h, code, y, anap[0:7]) + for i := T0; i < cL_SUBFR; i++ { + code[i] = add(code[i], mult(code[i-T0], ps)) + } + return 7 + default: // MR122 + ps := shl(gainPit, 1) + for i := T0; i < cL_SUBFR; i++ { + h[i] = add(h[i], int16((int32(h[i-T0])*int32(ps))>>15)) + } + code10i40_35bits(x, res2, h, code, y, anap[0:10]) + for i := T0; i < cL_SUBFR; i++ { + code[i] = add(code[i], mult(code[i-T0], ps)) + } + return 10 + } +} + +// subframePostProc builds the total excitation (gain_pit·adaptive + +// gain_code·code), runs the local synthesis (updating mem_syn), and refreshes +// the error and weighting-filter memories. sharp is updated with the clamped +// pitch gain. Mirrors subframePostProc. +func subframePostProc(speech []int16, speechOff, mode, iSubfr int, gainPit, gainCode int16, aq, synth, xn, code, y1, y2, memSyn, memErr, memW0, exc []int16, excOff int, sharp *int16) { + var tempShift, kShift, pitchFac int16 + if mode != dMR122 { + tempShift, kShift, pitchFac = 1, 16-2-1, gainPit + } else { + tempShift, kShift, pitchFac = 2, 16-4-1, gainPit>>1 + } + + if gainPit < cSHARPMAX { + *sharp = gainPit + } else { + *sharp = cSHARPMAX + } + + base := excOff + iSubfr + for i := 0; i < cL_SUBFR; i++ { + L := (int32(exc[base+i]) * int32(pitchFac)) << 1 + L += (int32(code[i]) * int32(gainCode)) << 1 + L <<= uint(tempShift) + exc[base+i] = int16((L + 0x8000) >> 16) + } + + synFilt(aq, exc[base:base+cL_SUBFR], synth[iSubfr:iSubfr+cL_SUBFR], cL_SUBFR, memSyn, true) + + for i, j := cL_SUBFR-cM, 0; i < cL_SUBFR; i, j = i+1, j+1 { + memErr[j] = speech[speechOff+iSubfr+i] - synth[iSubfr+i] + temp := int16((int32(y1[i]) * int32(gainPit)) >> 14) + temp += int16((int32(y2[i]) * int32(gainCode)) >> uint(kShift)) + memW0[j] = xn[i] - temp + } +} diff --git a/internal/capability/transcode/amrnb/dsp_gain.go b/internal/capability/transcode/amrnb/dsp_gain.go new file mode 100644 index 0000000..0b3dd9a --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_gain.go @@ -0,0 +1,182 @@ +package amrnb + +// Fixed-codebook gain prediction, ported from opencore-amrnb gc_pred.cpp plus +// the 32-bit composite operators it needs (L_Extract, L_Comp, Mpy_32_16 from +// the ETSI basicop set). gc_pred runs the MA energy predictor that yields the +// predicted code gain (gcode0) used by the gain dequantizers; gc_pred_update +// feeds back the quantized energy each subframe. Bit-exact with the reference. + +const ( + cNPRED = 4 + cMEAN_ENER_MR122 = 783741 // Q17 + cMIN_ENERGY = -14336 // Q10 + cMIN_ENERGY_MR122 = -2381 // Q10 +) + +// MA prediction coefficients (gc_pred.cpp). +var gcPred = [cNPRED]int16{5571, 4751, 2785, 1556} +var gcPredMR122 = [cNPRED]int16{44, 37, 22, 12} + +// L_Extract splits a 32-bit value into a 16-bit high part and a 15-bit low +// part such that L ≈ (hi<<16) + (lo<<1). Mirrors L_Extract. +func L_Extract(L int32) (hi, lo int16) { + hi = int16(L >> 16) + lo = int16((L >> 1) - (int32(hi) << 15)) + return hi, lo +} + +// L_Comp recombines a (hi,lo) pair back into a 32-bit value. Mirrors L_Comp. +func L_Comp(hi, lo int16) int32 { + return L_mac(L_deposit_h(hi), lo, 1) +} + +// mpy32_16 multiplies a 32-bit value (given as hi,lo) by a 16-bit value, +// matching the optimized opencore-amrnb Mpy_32_16 (truncated low-part shift). +func mpy32_16(hi, lo, n int16) int32 { + return L_add(L_mult(hi, n), ((int32(lo)*int32(n))>>15)<<1) +} + +// gcPredState is the MA energy-predictor memory (two parallel histories: the +// 20·log10 form for most modes and the log2 form for MR122). Mirrors +// gc_predState. +type gcPredState struct { + pastQuaEn [cNPRED]int16 // Q10, 20*log10(qua_err) + pastQuaEnMR122 [cNPRED]int16 // Q10, log2(qua_err) +} + +// reset initializes both predictor histories to their energy floor. Mirrors +// gc_pred_reset. +func (st *gcPredState) reset() { + for i := 0; i < cNPRED; i++ { + st.pastQuaEn[i] = cMIN_ENERGY + st.pastQuaEnMR122[i] = cMIN_ENERGY_MR122 + } +} + +// update shifts the new quantized energies into the predictor memory. Mirrors +// gc_pred_update. +func (st *gcPredState) update(quaEnerMR122, quaEner int16) { + st.pastQuaEn[3] = st.pastQuaEn[2] + st.pastQuaEnMR122[3] = st.pastQuaEnMR122[2] + st.pastQuaEn[2] = st.pastQuaEn[1] + st.pastQuaEnMR122[2] = st.pastQuaEnMR122[1] + st.pastQuaEn[1] = st.pastQuaEn[0] + st.pastQuaEnMR122[1] = st.pastQuaEnMR122[0] + st.pastQuaEnMR122[0] = quaEnerMR122 + st.pastQuaEn[0] = quaEner +} + +// gcPredResult carries gc_pred's outputs: the exponent/fraction of the +// predicted gain factor gcode0, and (MR795 only) of the innovation energy. +type gcPredResult struct { + expGcode0, fracGcode0 int16 + expEn, fracEn int16 +} + +// gcPred computes the predicted fixed-codebook gain factor from the innovation +// energy and the predictor memory. Mirrors gc_pred (gc_pred.cpp). +func (st *gcPredState) gcPred(mode int, code []int16) gcPredResult { + var r gcPredResult + + var enerCode int32 + for i := cL_SUBFR >> 2; i != 0; i-- { + base := (cL_SUBFR>>2 - i) * 4 + for k := 0; k < 4; k++ { + tmp := code[base+k] + enerCode += (int32(tmp) * int32(tmp)) >> 3 + } + } + enerCode <<= 4 + if enerCode>>31 != 0 { // saturation + enerCode = maxInt32 + } + + if mode == dMR122 { + enerCode = int32(round_(enerCode)) * 26214 << 1 + exp, frac := log2(enerCode) + lTemp1 := int32(exp-30) << 16 + enerCode = lTemp1 + (int32(frac) << 1) + + ener := int32(cMEAN_ENER_MR122) + for i := 0; i < cNPRED; i++ { + lt := (int32(st.pastQuaEnMR122[i]) * int32(gcPredMR122[i])) << 1 + ener = L_add(ener, lt) + } + + lTemp1 = L_sub(ener, enerCode) + r.expGcode0 = int16(lTemp1 >> 17) + lTemp2 := int32(r.expGcode0) << 15 + lTemp1 >>= 2 + r.fracGcode0 = int16(lTemp1 - lTemp2) + return r + } + + // all modes except 12.2 + expCode := norm_l(enerCode) + enerCode = L_shl(enerCode, expCode) + exp, frac := log2Norm(enerCode, expCode) + + lTemp2 := (int32(exp) * -24660) << 1 + lTmp := (int32(frac) * -24660) >> 15 + if lTmp&0x00010000 != 0 { // sign-extend + lTmp |= int32(-0x10000) // 0xffff0000 + } + lTmp <<= 1 + lTmp = L_add(lTmp, lTemp2) + + switch mode { + case dMR102: + lTemp2 = int32(16678) << 7 + lTmp = L_add(lTmp, lTemp2) + case dMR795: + r.fracEn = int16(enerCode >> 16) + r.expEn = int16(-11 - expCode) + lTemp2 = int32(17062) << 7 + lTmp = L_add(lTmp, lTemp2) + case dMR74: + lTemp2 = int32(32588) << 6 + lTmp = L_add(lTmp, lTemp2) + case dMR67: + lTemp2 = int32(32268) << 6 + lTmp = L_add(lTmp, lTemp2) + default: // MR59, MR515, MR475 + lTemp2 = int32(16678) << 7 + lTmp = L_add(lTmp, lTemp2) + } + + // gcode0 = sum(pred[i]*past_qua_en[i]) - ener_code + mean_ener (Q24) + if lTmp > 0x001fffff { + lTmp = maxInt32 + } else if lTmp < -2097152 { + lTmp = minInt32 + } else { + lTmp <<= 10 + } + for i := 0; i < 4; i++ { + lTemp2 = (int32(gcPred[i]) * int32(st.pastQuaEn[i])) << 1 + lTmp = L_add(lTmp, lTemp2) + } + gcode0 := int16(lTmp >> 16) // Q8 + + // gcode0 = pow(2, 0.166*gcode0) + if mode == dMR74 { // IS641 bit-exactness + lTmp = int32(gcode0) * 5439 << 1 + } else { + lTmp = int32(gcode0) * 5443 << 1 + } + if lTmp < 0 { + lTmp = ^((^lTmp) >> 8) + } else { + lTmp >>= 8 // -> Q16 + } + r.expGcode0 = int16(lTmp >> 16) + var lTemp1 int32 + if lTmp < 0 { + lTemp1 = ^((^lTmp) >> 1) + } else { + lTemp1 = lTmp >> 1 + } + lTemp2 = int32(r.expGcode0) << 15 + r.fracGcode0 = int16(L_sub(lTemp1, lTemp2)) + return r +} diff --git a/internal/capability/transcode/amrnb/dsp_lsp.go b/internal/capability/transcode/amrnb/dsp_lsp.go new file mode 100644 index 0000000..1ae0172 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_lsp.go @@ -0,0 +1,157 @@ +package amrnb + +// LSP/LSF conversions and interpolation, ported function-by-function from the +// opencore-amrnb common sources (lsp_az.cpp, lsp_lsf.cpp, int_lpc.cpp). These +// turn the decoded line-spectral representation into the per-subframe LP +// synthesis coefficients A(z) (order 10). Bit-exact with the reference. + +// getLspPol finds the coefficients of F1(z) or F2(z) from the (cosine-domain) +// LSPs by expanding the product polynomial. f must have length 6. Mirrors +// Get_lsp_pol (lsp_az.cpp). +func getLspPol(lsp []int16, f []int32) { + fi := 0 + li := 0 + f[fi] = 0x01000000 // f[0] = 1.0 + fi++ + f[fi] = (-int32(lsp[li])) << 10 // f[1] = -2.0 * lsp[0] + fi++ + li++ + li++ // advance lsp pointer + + for i := int16(2); i <= 5; i++ { + f[fi] = f[fi-2] + for j := int16(1); j < i; j++ { + hi := int16(f[fi-1] >> 16) + lo := int16((f[fi-1] >> 1) - (int32(hi) << 15)) + t0 := int32(hi) * int32(lsp[li]) + t0 += (int32(lo) * int32(lsp[li])) >> 15 + f[fi] += f[fi-2] + f[fi] -= t0 << 2 + fi-- + } + f[fi] -= int32(lsp[li]) << 10 + li++ + fi += int(i) + li++ + } +} + +// lspAz converts line spectral pairs to order-10 LP coefficients a[0:11]. +// Mirrors Lsp_Az (lsp_az.cpp). +func lspAz(lsp []int16, a []int16) { + var f1, f2 [6]int32 + getLspPol(lsp[0:], f1[:]) + getLspPol(lsp[1:], f2[:]) + for i := 5; i > 0; i-- { + f1[i] += f1[i-1] + f2[i] -= f2[i-1] + } + a[0] = 4096 + for i, j := 1, 10; i <= 5; i, j = i+1, j-1 { + t0 := f1[i] + f2[i] + t1 := f1[i] - f2[i] + t0 += 1 << 12 + t1 += 1 << 12 + a[i] = int16(t0 >> 13) + a[j] = int16(t1 >> 13) + } +} + +// lsfLsp converts normalized LSFs (Q15, 0..0.5) to LSPs (cosine domain) by +// table lookup and interpolation. Mirrors Lsf_lsp (lsp_lsf.cpp). +func lsfLsp(lsf, lsp []int16, m int16) { + for i := int16(0); i < m; i++ { + ind := lsf[i] >> 8 // b8-b15 + offset := lsf[i] & 0x00ff // b0-b7 + L_tmp := (int32(lspLsfTable[ind+1]) - int32(lspLsfTable[ind])) * int32(offset) >> 8 + lsp[i] = lspLsfTable[ind] + int16(L_tmp) + } +} + +// lspLsf converts LSPs (cosine domain) to normalized LSFs (Q15) via the acos +// table and per-segment slope. Mirrors Lsp_lsf (lsp_lsf.cpp). +func lspLsf(lsp, lsf []int16, m int16) { + ind := int16(63) + for i := m - 1; i >= 0; i-- { + temp := lsp[i] + for lspLsfTable[ind] < temp { + ind-- + } + L_tmp := (int32(temp) - int32(lspLsfTable[ind])) * int32(lspLsfSlope[ind]) + L_tmp = (L_tmp + 0x800) >> 12 + lsf[i] = int16(L_tmp) + (ind << 8) + } +} + +// intLpc1and3 interpolates LSPs across the four subframes (old/mid/new LSP +// sets) and produces A(z) for each, into Az (length 4*cMP1). Used by modes that +// transmit a mid-frame LSP set. Mirrors Int_lpc_1and3 (int_lpc.cpp). +func intLpc1and3(lspOld, lspMid, lspNew []int16, Az []int16) { + var lsp [cM]int16 + for i := 0; i < cM; i++ { + lsp[i] = (lspOld[i] >> 1) + (lspMid[i] >> 1) + } + lspAz(lsp[:], Az[0:]) // subframe 1 + lspAz(lspMid, Az[cMP1:]) // subframe 2 + for i := 0; i < cM; i++ { + lsp[i] = (lspMid[i] >> 1) + (lspNew[i] >> 1) + } + lspAz(lsp[:], Az[2*cMP1:]) // subframe 3 + lspAz(lspNew, Az[3*cMP1:]) // subframe 4 +} + +// intLpc1to3 interpolates between only the past-frame and present-frame LSP +// sets (no mid set) across the four subframes. Mirrors Int_lpc_1to3. +func intLpc1to3(lspOld, lspNew []int16, Az []int16) { + var lsp [cM]int16 + for i := 0; i < cM; i++ { + temp := lspOld[i] - (lspOld[i] >> 2) + lsp[i] = temp + (lspNew[i] >> 2) + } + lspAz(lsp[:], Az[0:]) // subframe 1 + for i := 0; i < cM; i++ { + lsp[i] = (lspNew[i] >> 1) + (lspOld[i] >> 1) + } + lspAz(lsp[:], Az[cMP1:]) // subframe 2 + for i := 0; i < cM; i++ { + temp := lspNew[i] - (lspNew[i] >> 2) + lsp[i] = temp + (lspOld[i] >> 2) + } + lspAz(lsp[:], Az[2*cMP1:]) // subframe 3 + lspAz(lspNew, Az[3*cMP1:]) // subframe 4 +} + +// intLpc1and3_2 interpolates only subframes 1 and 3 (leaving 2 and 4 as the +// directly-computed A_mid/A_new), used for the unquantized analysis filters. +// Mirrors Int_lpc_1and3_2. +func intLpc1and3_2(lspOld, lspMid, lspNew, az []int16) { + var lsp [cM]int16 + for i := 0; i < cM; i++ { + lsp[i] = (lspOld[i] >> 1) + (lspMid[i] >> 1) + } + lspAz(lsp[:], az[0:]) // subframe 1 + for i := 0; i < cM; i++ { + lsp[i] = (lspMid[i] >> 1) + (lspNew[i] >> 1) + } + lspAz(lsp[:], az[2*cMP1:]) // subframe 3 +} + +// intLpc1to3_2 interpolates subframes 1, 2 and 3 (leaving 4 as A_new). Mirrors +// Int_lpc_1to3_2. +func intLpc1to3_2(lspOld, lspNew, az []int16) { + var lsp [cM]int16 + for i := 0; i < cM; i++ { + temp := lspOld[i] - (lspOld[i] >> 2) + lsp[i] = temp + (lspNew[i] >> 2) + } + lspAz(lsp[:], az[0:]) // subframe 1 + for i := 0; i < cM; i++ { + lsp[i] = (lspNew[i] >> 1) + (lspOld[i] >> 1) + } + lspAz(lsp[:], az[cMP1:]) // subframe 2 + for i := 0; i < cM; i++ { + temp := lspNew[i] - (lspNew[i] >> 2) + lsp[i] = temp + (lspOld[i] >> 2) + } + lspAz(lsp[:], az[2*cMP1:]) // subframe 3 +} diff --git a/internal/capability/transcode/amrnb/dsp_mathops.go b/internal/capability/transcode/amrnb/dsp_mathops.go new file mode 100644 index 0000000..d9ec2f9 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_mathops.go @@ -0,0 +1,120 @@ +package amrnb + +// Higher-level fixed-point math operators, ported function-by-function from the +// opencore-amrnb common sources (log2_norm.cpp, log2.cpp, pow2.cpp, +// inv_sqrt.cpp, sqrt_l.cpp, shr_r.cpp, l_shr_r.cpp). Each uses the ROM tables +// in tables_math.go and is bit-exact with the reference by construction. + +// shr_r is shr with rounding: shr(var1, var2) plus 1 if the most significant +// discarded bit is set. Mirrors shr_r.cpp. +func shr_r(var1, var2 int16) int16 { + if var2 > 15 { + return 0 + } + out := shr(var1, var2) + if var2 > 0 { + if var1&(int16(1)< 31 { + return 0 + } + result := L_shr(L_var1, var2) + if var2 > 0 { + if L_var1&(int32(1)< shift right + L_x = L_shr(L_x, 1) + } + exp = shr(exp, 1) + exp = add(exp, 1) + L_x = L_shr(L_x, 9) + i := extract_h(L_x) + L_x = L_shr(L_x, 1) + a := extract_l(L_x) & 0x7fff + i = sub(i, 16) + L_y := L_deposit_h(inv_sqrt_tbl[i]) + tmp := sub(inv_sqrt_tbl[i], inv_sqrt_tbl[i+1]) + L_y = L_msu(L_y, tmp, a) + return L_shr(L_y, exp) // denormalization +} + +// sqrtLExp returns sqrt(L_x) together with the right shift (exp) that must be +// applied to the result to denormalize it: the true value is the return value +// shifted right by exp/2. Mirrors sqrt_l_exp (sqrt_l.cpp). +func sqrtLExp(L_x int32) (result int32, exp int16) { + if L_x <= 0 { + return 0, 0 + } + e := norm_l(L_x) &^ 1 // next lower even normalization exponent (clear bit 0) + L_x = L_shl(L_x, e) + L_x = L_shr(L_x, 9) + i := extract_h(L_x) // 16 <= i <= 63 + L_x = L_shr(L_x, 1) + a := extract_l(L_x) & 0x7fff + i = sub(i, 16) // 0 <= i <= 47 + L_y := L_deposit_h(sqrt_l_tbl[i]) + tmp := sub(sqrt_l_tbl[i], sqrt_l_tbl[i+1]) + L_y = L_msu(L_y, tmp, a) + return L_y, e +} diff --git a/internal/capability/transcode/amrnb/dsp_postproc.go b/internal/capability/transcode/amrnb/dsp_postproc.go new file mode 100644 index 0000000..6962fb1 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_postproc.go @@ -0,0 +1,67 @@ +package amrnb + +// Output post-processing building blocks, ported from opencore-amrnb +// weight_a.cpp (Weight_Ai), preemph.cpp (preemphasis) and post_pro.cpp +// (Post_Process). weightAi applies gamma bandwidth expansion to an LP filter; +// preemphasis is the 1st-order y[n]=x[n]-g·x[n-1]; postProcess is the decoder's +// output high-pass biquad with the ×2 output scaling. Bit-exact with the +// reference. + +// weightAi computes the bandwidth-expanded coefficients a_exp[i] = a[i]·fac[i-1] +// (with a_exp[0]=a[0]). Mirrors Weight_Ai. +func weightAi(a []int16, fac []int16, aExp []int16) { + aExp[0] = a[0] + for i := 1; i <= cM; i++ { + aExp[i] = int16((int32(a[i])*int32(fac[i-1]) + 0x4000) >> 15) + } +} + +// preemphasis applies y[n] = x[n] - g·x[n-1] in place over L samples, carrying +// one sample of memory. Mirrors preemphasis (preemph.cpp). +func preemphasis(memPre *int16, signal []int16, g int16, L int) { + temp := signal[L-1] + for i := L - 1; i >= 1; i-- { + signal[i] = sub(signal[i], mult(g, signal[i-1])) + } + signal[0] = sub(signal[0], mult(g, *memPre)) + *memPre = temp +} + +// post-process output high-pass biquad coefficients (post_pro.cpp). +var postB = [3]int16{7699, -15398, 7699} +var postA = [3]int16{8192, 15836, -7667} + +// postProcessState holds the biquad's double-precision input/output memory. +type postProcessState struct { + x0, x1 int16 + y1hi, y1lo, y2hi, y2lo int16 +} + +// process applies the output high-pass filter (with ×2 gain) in place over lg +// samples. The feedback memory is kept as (hi,lo) double precision. Mirrors +// Post_Process. +func (st *postProcessState) process(signal []int16, lg int) { + a1, a2 := int32(postA[1]), int32(postA[2]) + b0, b1, b2 := int32(postB[0]), int32(postB[1]), int32(postB[2]) + for i := 0; i < lg; i++ { + x2 := st.x1 + st.x1 = st.x0 + st.x0 = signal[i] + + L := int32(st.y1hi) * a1 + L += (int32(st.y1lo) * a1) >> 15 + L += int32(st.y2hi) * a2 + L += (int32(st.y2lo) * a2) >> 15 + L += int32(st.x0) * b0 + L += int32(st.x1) * b1 + L += int32(x2) * b2 + + L = L_shl(L, 3) + signal[i] = round_(L_shl(L, 1)) + + st.y2hi = st.y1hi + st.y2lo = st.y1lo + st.y1hi = int16(L >> 16) + st.y1lo = int16((L >> 1) - (int32(st.y1hi) << 15)) + } +} diff --git a/internal/capability/transcode/amrnb/dsp_pstfilt.go b/internal/capability/transcode/amrnb/dsp_pstfilt.go new file mode 100644 index 0000000..c25c3d8 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_pstfilt.go @@ -0,0 +1,108 @@ +package amrnb + +// Adaptive formant post-filter, ported from opencore-amrnb pstfilt.cpp. For each +// subframe it builds the weighted LP filters A(z/γ3) and A(z/γ4), forms the +// residual through A(z/γ3), derives a spectral-tilt compensation coefficient +// from the truncated impulse response, applies it via preemphasis, synthesizes +// through 1/A(z/γ4), and finally matches the energy back to the synthesis with +// AGC. Uses the building blocks weightAi/residu/synFilt/preemphasis/agc; this is +// the orchestration that chains them. Bit-exact with the reference. + +const ( + cL_H = 22 // truncated impulse-response length + cMU = 26214 // tilt compensation factor (0.8) +) + +// γ bandwidth-expansion factors (pstfilt.cpp). +var gamma3MR122 = [cM]int16{22938, 16057, 11240, 7868, 5508, 3856, 2699, 1889, 1322, 925} +var gamma3 = [cM]int16{18022, 9912, 5451, 2998, 1649, 907, 499, 274, 151, 83} +var gamma4MR122 = [cM]int16{24576, 18432, 13824, 10368, 7776, 5832, 4374, 3281, 2461, 1846} +var gamma4 = [cM]int16{22938, 16057, 11240, 7868, 5508, 3856, 2699, 1889, 1322, 925} + +// postFilterState holds the formant post-filter memory across frames. +type postFilterState struct { + res2 [cL_SUBFR]int16 + memSynPst [cM]int16 + preemph int16 + agc agcState + synthBuf [cM + cL_FRAME]int16 // M history + current frame +} + +func (st *postFilterState) reset() { + *st = postFilterState{} +} + +// postFilter post-filters one 160-sample synthesis frame in place. az4 holds the +// four per-subframe LP coefficient sets (4*cMP1). Mirrors Post_Filter. +func (st *postFilterState) postFilter(mode int, syn []int16, az4 []int16) { + var ap3, ap4 [cMP1]int16 + var h [cL_H]int16 + + synWork := st.synthBuf[cM:] // syn_work[i] == synthBuf[M+i] + copy(synWork[:cL_FRAME], syn[:cL_FRAME]) + + azOff := 0 + for iSubfr := 0; iSubfr < cL_FRAME; iSubfr += cL_SUBFR { + az := az4[azOff:] + if mode == dMR122 || mode == dMR102 { + weightAi(az, gamma3MR122[:], ap3[:]) + weightAi(az, gamma4MR122[:], ap4[:]) + } else { + weightAi(az, gamma3[:], ap3[:]) + weightAi(az, gamma4[:], ap4[:]) + } + + // Residual through A(z/γ3); syn_work[iSubfr] == synthBuf[M+iSubfr]. + residu(ap3[:], st.synthBuf[:], cM+iSubfr, cL_SUBFR, st.res2[:]) + + // Truncated impulse response of A(z/γ3)/A(z/γ4). + copy(h[:cMP1], ap3[:cMP1]) + for i := cMP1; i < cL_H; i++ { + h[i] = 0 + } + synFilt(ap4[:], h[:], h[:], cL_H, h[cMP1:], false) + + // Tilt compensation coefficient from the impulse-response autocorr. + var lTmp int32 + for i := cL_H - 1; i >= 0; i-- { + t2 := int32(h[i]) * int32(h[i]) + if t2 == 0x40000000 { + break // reference sets overflow and stops without adding + } + lTmp = L_add(lTmp, t2<<1) + } + temp1 := int16(lTmp >> 16) + + lTmp = 0 + for i := cL_H - 2; i >= 0; i-- { + t2 := int32(h[i]) * int32(h[i+1]) + if t2 == 0x40000000 { + break + } + lTmp = L_add(lTmp, t2<<1) + } + temp2 := int16(lTmp >> 16) + + if temp2 <= 0 { + temp2 = 0 + } else { + lt := (int32(temp2) * cMU) >> 15 + if lt&0x00010000 != 0 { + lt |= int32(-0x10000) + } + temp2 = int16(lt) + temp2 = div_s(temp2, temp1) + } + + preemphasis(&st.preemph, st.res2[:], temp2, cL_SUBFR) + + synFilt(ap4[:], st.res2[:], syn[iSubfr:], cL_SUBFR, st.memSynPst[:], true) + + st.agc.agc(synWork[iSubfr:], syn[iSubfr:], cAGC_FAC, cL_SUBFR) + + azOff += cMP1 + } + + // Carry the last M samples as history for the next frame. + copy(st.synthBuf[:cM], synWork[cL_FRAME-cM:cL_FRAME]) +} diff --git a/internal/capability/transcode/amrnb/dsp_synfilt.go b/internal/capability/transcode/amrnb/dsp_synfilt.go new file mode 100644 index 0000000..eaa66e3 --- /dev/null +++ b/internal/capability/transcode/amrnb/dsp_synfilt.go @@ -0,0 +1,81 @@ +package amrnb + +// LP synthesis and analysis filters, ported from opencore-amrnb syn_filt.cpp +// and residu.cpp (order 10). synFilt runs the all-pole synthesis 1/A(z) over a +// subframe, carrying M samples of filter memory; residu runs the all-zero +// analysis A(z). Both accumulate products in wrapping int32 (the reference's +// non-saturating MAC/MSU), so a straightforward per-output loop is bit-exact; +// synFilt's output clamp reproduces the reference's exact boundary behaviour. + +// synClamp reproduces the Syn_filt overflow clamp: in-range values are an +// arithmetic >>12, out-of-range saturate to ±MAX_16. The asymmetric boundary +// (s == 0x07ffffff -> MIN_16) is a reference quirk preserved deliberately. +func synClamp(s int32) int16 { + if uint32(s+134217728) < 0x0fffffff { + return int16(s >> 12) + } else if s > 0x07ffffff { + return maxInt16 + } + return minInt16 +} + +// synFilt computes y = x convolved with 1/A(z) over lg samples, where a[0:M+1] +// are the LP coefficients (a[0]=4096, Q12). mem[0:M] holds the M past output +// samples; if update is true it is refreshed with the last M outputs. Mirrors +// Syn_filt. +func synFilt(a, x, y []int16, lg int, mem []int16, update bool) { + var yy [cL_FRAME + cM]int16 + copy(yy[:cM], mem[:cM]) + + af := a[:cMP1] // hoist bounds: a[0..M] + xf := x[:lg] + for i := 0; i < lg; i++ { + s := int32(0x800) + int32(af[0])*int32(xf[i]) + yw := yy[i : i+cM] // yy[cM+i-j] == yw[cM-j], all in [0,cM) + s -= int32(af[1]) * int32(yw[cM-1]) + s -= int32(af[2]) * int32(yw[cM-2]) + s -= int32(af[3]) * int32(yw[cM-3]) + s -= int32(af[4]) * int32(yw[cM-4]) + s -= int32(af[5]) * int32(yw[cM-5]) + s -= int32(af[6]) * int32(yw[cM-6]) + s -= int32(af[7]) * int32(yw[cM-7]) + s -= int32(af[8]) * int32(yw[cM-8]) + s -= int32(af[9]) * int32(yw[cM-9]) + s -= int32(af[10]) * int32(yw[cM-10]) + out := synClamp(s) + yy[cM+i] = out + y[i] = out + } + + if update { + copy(mem[:cM], y[lg-cM:lg]) + } +} + +// residu computes the LP residual A(z)·input over inputLen samples. buf holds +// the input with at least M samples of history before start (buf[start-M:start] +// are the past samples). Output goes to residual[0:inputLen]. Output is +// truncated (not saturated) to 16 bits, matching the reference. Mirrors Residu. +func residu(coef, buf []int16, start, inputLen int, residual []int16) { + // res[i] = (0x800 + sum_{j=0..M} coef[j]·buf[start+i-j]) >> 12. Reversing the + // coefficients turns the inner sum into a fixed-window FIR over buf[start-M:] + // (res[i] = sum_k arev[k]·buf[start-M+i+k]), so it vectorises via firRaw. The + // reference accumulates in plain wrapping int32, so the lane-parallel order is + // bit-identical, and adding the 0x800 rounding bias after the sum is exact + // (integer addition is associative mod 2^32). + var arev [cMP1]int16 + for k := 0; k <= cM; k++ { + arev[k] = coef[cM-k] + } + var dstArr [cL_SUBFR]int32 + var dst []int32 + if inputLen <= cL_SUBFR { + dst = dstArr[:inputLen] + } else { + dst = make([]int32, inputLen) + } + firRaw(dst, buf[start-cM:], arev[:]) + for i := 0; i < inputLen; i++ { + residual[i] = int16((0x800 + dst[i]) >> 12) + } +} diff --git a/internal/capability/transcode/amrnb/mode.go b/internal/capability/transcode/amrnb/mode.go new file mode 100644 index 0000000..296fb31 --- /dev/null +++ b/internal/capability/transcode/amrnb/mode.go @@ -0,0 +1,128 @@ +// Package amrnb implements the AMR-NB (3GPP TS 26.071 / 26.090) narrowband +// speech codec in pure Go, with RFC 4867 RTP payload framing. +// +// AMR-NB operates on 8 kHz mono audio in 20 ms frames (160 samples). It defines +// eight speech coding modes (0..7) spanning 4.75 to 12.2 kbit/s, plus a comfort +// noise (SID) frame used by DTX. +package amrnb + +// SampleRate is the fixed AMR-NB sampling rate in Hz. +const SampleRate = 8000 + +// FrameSamples is the number of PCM samples in one 20 ms AMR-NB frame. +const FrameSamples = 160 + +// Mode identifies an AMR-NB speech coding mode (0..7). +type Mode int + +const ( + Mode0475 Mode = iota // 4.75 kbit/s + Mode0515 // 5.15 kbit/s + Mode0590 // 5.90 kbit/s + Mode0670 // 6.70 kbit/s + Mode0740 // 7.40 kbit/s + Mode0795 // 7.95 kbit/s + Mode1020 // 10.2 kbit/s + Mode1220 // 12.2 kbit/s + numSpeechModes +) + +// Frame type (FT) values carried in the RFC 4867 ToC byte. Values 0..7 are the +// speech modes; the remainder describe comfort noise and no-data frames. Note +// that AMR-NB places SID at FT 8 (AMR-WB uses FT 9). +const ( + FTMode0475 = 0 + FTMode0515 = 1 + FTMode0590 = 2 + FTMode0670 = 3 + FTMode0740 = 4 + FTMode0795 = 5 + FTMode1020 = 6 + FTMode1220 = 7 + FTSID = 8 // comfort noise (SID) + FTSpeechLost = 14 // speech lost (RFC 4867) + FTNoData = 15 // no data +) + +// modeBits is the number of speech bits per AMR-NB mode (0..7). These are the +// canonical bit counts from 3GPP TS 26.101 (and RFC 4867 Table 1a). +var modeBits = [numSpeechModes]int{ + 95, // 4.75 + 103, // 5.15 + 118, // 5.90 + 134, // 6.70 + 148, // 7.40 + 159, // 7.95 + 204, // 10.2 + 244, // 12.2 +} + +// sidBits is the number of bits in an AMR-NB SID (comfort noise) frame. +const sidBits = 39 + +// frameBitsByFT returns the number of payload bits for a given frame type and +// whether the frame type is a recognized one that carries bits. SID and speech +// modes carry bits; SPEECH_LOST and NO_DATA carry none. +func frameBitsByFT(ft int) (bits int, ok bool) { + switch { + case ft >= FTMode0475 && ft <= FTMode1220: + return modeBits[ft], true + case ft == FTSID: + return sidBits, true + case ft == FTSpeechLost, ft == FTNoData: + return 0, true + default: + return 0, false + } +} + +// frameBytesByFT returns the number of octet-aligned bytes occupied by a frame +// of the given type (bits rounded up to whole octets). +func frameBytesByFT(ft int) (bytes int, ok bool) { + bits, ok := frameBitsByFT(ft) + if !ok { + return 0, false + } + return (bits + 7) / 8, true +} + +// Bits returns the number of speech bits produced by this mode. +func (m Mode) Bits() int { + if m < 0 || m >= numSpeechModes { + return 0 + } + return modeBits[m] +} + +// Bytes returns the octet-aligned byte count of a frame in this mode. +func (m Mode) Bytes() int { return (m.Bits() + 7) / 8 } + +// FrameType returns the RFC 4867 frame-type value for this mode. +func (m Mode) FrameType() int { return int(m) } + +// Valid reports whether m is a defined speech mode. +func (m Mode) Valid() bool { return m >= Mode0475 && m < numSpeechModes } + +// Bitrate returns the nominal bit rate of the mode in bit/s. +func (m Mode) Bitrate() int { + switch m { + case Mode0475: + return 4750 + case Mode0515: + return 5150 + case Mode0590: + return 5900 + case Mode0670: + return 6700 + case Mode0740: + return 7400 + case Mode0795: + return 7950 + case Mode1020: + return 10200 + case Mode1220: + return 12200 + default: + return 0 + } +} diff --git a/internal/capability/transcode/amrnb/payload.go b/internal/capability/transcode/amrnb/payload.go new file mode 100644 index 0000000..cab4383 --- /dev/null +++ b/internal/capability/transcode/amrnb/payload.go @@ -0,0 +1,237 @@ +package amrnb + +import "errors" + +// RFC 4867 RTP payload framing for AMR-NB. Two formats are supported: +// +// - octet-aligned (octet-align=1): every field starts on an octet boundary. +// Layout: [CMR(1 byte)] [ToC byte per frame] [speech bytes per frame]. +// - bandwidth-efficient: fields are bit-packed with no padding. +// Layout: CMR(4 bits) then, per frame, ToC(6 bits) then the speech bits. +// +// Only the fields VoiceBlender needs are modelled. Interleaving and the +// CRC/robust-sorting options are not used (they are negotiated off by default +// and we never offer them). + +// noCMR is the "no mode request" CMR value (15) emitted when we have no +// preference to signal to the peer. +const noCMR = 15 + +// frame is one decoded ToC entry plus its raw speech bits. +type frame struct { + ft int // frame type (FT) + q bool // quality bit (Q): true = good frame + data []byte // speech bits, octet-aligned, left-justified in the final byte +} + +var ( + errShortPayload = errors.New("amrnb: payload too short") + errBadFrameType = errors.New("amrnb: unknown frame type") +) + +// packPayloadOctet builds an octet-aligned payload carrying the given frames +// with the supplied CMR (use noCMR for none). +func packPayloadOctet(cmr int, frames []frame) []byte { + out := []byte{byte(cmr&0x0F) << 4} + for i, f := range frames { + last := i == len(frames)-1 + toc := byte(f.ft&0x0F) << 3 + if !last { + toc |= 0x80 // F bit: more frames follow + } + if f.q { + toc |= 0x04 // Q bit + } + out = append(out, toc) + } + for _, f := range frames { + nb, _ := frameBytesByFT(f.ft) + buf := make([]byte, nb) + copy(buf, f.data) + out = append(out, buf...) + } + return out +} + +// unpackPayloadOctet parses an octet-aligned payload into its frames and CMR. +func unpackPayloadOctet(p []byte) (cmr int, frames []frame, err error) { + if len(p) < 1 { + return 0, nil, errShortPayload + } + cmr = int(p[0] >> 4) + idx := 1 + // Read ToC bytes until the F bit clears. + var tocs []byte + for { + if idx >= len(p) { + return 0, nil, errShortPayload + } + toc := p[idx] + idx++ + tocs = append(tocs, toc) + if toc&0x80 == 0 { + break + } + } + for _, toc := range tocs { + ft := int(toc>>3) & 0x0F + q := toc&0x04 != 0 + nb, ok := frameBytesByFT(ft) + if !ok { + return 0, nil, errBadFrameType + } + if idx+nb > len(p) { + return 0, nil, errShortPayload + } + data := make([]byte, nb) + copy(data, p[idx:idx+nb]) + idx += nb + frames = append(frames, frame{ft: ft, q: q, data: data}) + } + return cmr, frames, nil +} + +// packPayloadBandwidthEfficient builds a bandwidth-efficient payload. +func packPayloadBandwidthEfficient(cmr int, frames []frame) []byte { + w := &bitWriter{} + w.writeBits(uint32(cmr&0x0F), 4) + for i, f := range frames { + last := i == len(frames)-1 + fBit := uint32(0) + if !last { + fBit = 1 + } + w.writeBits(fBit, 1) + w.writeBits(uint32(f.ft&0x0F), 4) + q := uint32(0) + if f.q { + q = 1 + } + w.writeBits(q, 1) + } + for _, f := range frames { + bits, _ := frameBitsByFT(f.ft) + w.writeBitsFromBytes(f.data, bits) + } + return w.bytes() +} + +// unpackPayloadBandwidthEfficient parses a bandwidth-efficient payload. +func unpackPayloadBandwidthEfficient(p []byte) (cmr int, frames []frame, err error) { + r := &bitReader{data: p} + c, ok := r.readBits(4) + if !ok { + return 0, nil, errShortPayload + } + cmr = int(c) + type toc struct { + ft int + q bool + } + var tocs []toc + for { + fBit, ok := r.readBits(1) + if !ok { + return 0, nil, errShortPayload + } + ft, ok := r.readBits(4) + if !ok { + return 0, nil, errShortPayload + } + qBit, ok := r.readBits(1) + if !ok { + return 0, nil, errShortPayload + } + if _, valid := frameBitsByFT(int(ft)); !valid { + return 0, nil, errBadFrameType + } + tocs = append(tocs, toc{ft: int(ft), q: qBit == 1}) + if fBit == 0 { + break + } + } + for _, t := range tocs { + bits, _ := frameBitsByFT(t.ft) + data, ok := r.readBitsToBytes(bits) + if !ok { + return 0, nil, errShortPayload + } + frames = append(frames, frame{ft: t.ft, q: t.q, data: data}) + } + return cmr, frames, nil +} + +// bitWriter accumulates bits MSB-first into a byte slice. +type bitWriter struct { + buf []byte + cur byte + nimb int // number of bits filled in cur +} + +func (w *bitWriter) writeBit(b uint32) { + w.cur |= byte(b&1) << uint(7-w.nimb) + w.nimb++ + if w.nimb == 8 { + w.buf = append(w.buf, w.cur) + w.cur = 0 + w.nimb = 0 + } +} + +func (w *bitWriter) writeBits(v uint32, n int) { + for i := n - 1; i >= 0; i-- { + w.writeBit(v >> uint(i)) + } +} + +// writeBitsFromBytes writes the first n bits of data, MSB-first. +func (w *bitWriter) writeBitsFromBytes(data []byte, n int) { + for i := 0; i < n; i++ { + bit := uint32(0) + if byteIdx := i / 8; byteIdx < len(data) { + bit = uint32(data[byteIdx]>>uint(7-i%8)) & 1 + } + w.writeBit(bit) + } +} + +func (w *bitWriter) bytes() []byte { + out := w.buf + if w.nimb > 0 { + out = append(out, w.cur) + } + return out +} + +// bitReader reads bits MSB-first from a byte slice. +type bitReader struct { + data []byte + pos int // bit position +} + +func (r *bitReader) readBits(n int) (uint32, bool) { + var v uint32 + for i := 0; i < n; i++ { + if r.pos >= len(r.data)*8 { + return 0, false + } + bit := uint32(r.data[r.pos/8]>>uint(7-r.pos%8)) & 1 + v = (v << 1) | bit + r.pos++ + } + return v, true +} + +// readBitsToBytes reads n bits and returns them left-justified in a byte slice. +func (r *bitReader) readBitsToBytes(n int) ([]byte, bool) { + out := make([]byte, (n+7)/8) + for i := 0; i < n; i++ { + if r.pos >= len(r.data)*8 { + return nil, false + } + bit := r.data[r.pos/8] >> uint(7-r.pos%8) & 1 + out[i/8] |= bit << uint(7-i%8) + r.pos++ + } + return out, true +} diff --git a/internal/capability/transcode/amrnb/simd.go b/internal/capability/transcode/amrnb/simd.go new file mode 100644 index 0000000..3bb9014 --- /dev/null +++ b/internal/capability/transcode/amrnb/simd.go @@ -0,0 +1,37 @@ +package amrnb + +// SIMD-accelerated non-saturating int16 FIR, used by the hot filter kernels. +// The reference accumulates products into a plain int32 (no per-term +// saturation), and integer addition is associative modulo 2^32, so accumulating +// in the lane-parallel order an AVX2 kernel uses yields a bit-identical result. +// firRawGeneric is the portable fallback and the oracle the assembly is +// fuzz-tested against. + +// firRawGeneric computes the raw (pre-round, pre-shift) FIR sums +// dst[o] = sum_t x[o+t]*coef[t] for every output o, accumulating in wrapping +// int32. The amd64 build vectorises this across 8 outputs at a time; this is +// the portable fallback and the fuzz-test oracle. Requires +// len(x) >= len(dst)+len(coef)-1. +func firRawGeneric(dst []int32, x []int16, coef []int16) { + taps := len(coef) + for o := range dst { + xw := x[o : o+taps] + var s int32 + for t, c := range coef { + s += int32(xw[t]) * int32(c) + } + dst[o] = s + } +} + +// firDotGeneric computes the wrapping-int32 dot product sum_i a[i]*b[i] over the +// first len(a) elements (requires len(b) >= len(a)). The amd64 build vectorises +// it with VPMADDWD; this is the portable fallback and the fuzz-test oracle. +func firDotGeneric(a, b []int16) int32 { + bb := b[:len(a)] + var s int32 + for i, av := range a { + s += int32(av) * int32(bb[i]) + } + return s +} diff --git a/internal/capability/transcode/amrnb/simd_amd64.go b/internal/capability/transcode/amrnb/simd_amd64.go new file mode 100644 index 0000000..4cfbf6a --- /dev/null +++ b/internal/capability/transcode/amrnb/simd_amd64.go @@ -0,0 +1,46 @@ +//go:build amd64 + +package amrnb + +import "golang.org/x/sys/cpu" + +var useAVX2 = cpu.X86.HasAVX2 + +// firRaw computes dst[o] = sum_t x[o+t]*coef[t] (wrapping int32). On AVX2 it +// vectorises across 8 outputs per iteration; otherwise it falls back. +func firRaw(dst []int32, x []int16, coef []int16) { + if useAVX2 && len(dst) >= 8 && len(coef) <= maxFIRTaps { + var c32 [maxFIRTaps]int32 + for i, c := range coef { + c32[i] = int32(c) + } + firRawAVX2(dst, x, c32[:len(coef)]) + return + } + firRawGeneric(dst, x, coef) +} + +// maxFIRTaps bounds the stack-resident sign-extended coefficient buffer. It +// covers the longest FIR used: the open-loop pitch correlation window, which +// spans a whole frame (L_FRAME = 160 taps). +const maxFIRTaps = 256 + +// firRawAVX2 is implemented in simd_amd64.s. coef32 holds sign-extended taps. +// +//go:noescape +func firRawAVX2(dst []int32, x []int16, coef32 []int32) + +// firDot computes the wrapping-int32 dot product sum_i a[i]*b[i] over len(a) +// elements. On AVX2 it uses VPMADDWD across 16 int16 per iteration; otherwise it +// falls back. +func firDot(a, b []int16) int32 { + if useAVX2 && len(a) >= 16 { + return firDotAVX2(a, b) + } + return firDotGeneric(a, b) +} + +// firDotAVX2 is implemented in simd_amd64.s. It reads len(a) int16 from a and b. +// +//go:noescape +func firDotAVX2(a, b []int16) int32 diff --git a/internal/capability/transcode/amrnb/simd_amd64.s b/internal/capability/transcode/amrnb/simd_amd64.s new file mode 100644 index 0000000..09f5b65 --- /dev/null +++ b/internal/capability/transcode/amrnb/simd_amd64.s @@ -0,0 +1,121 @@ +//go:build amd64 + +#include "textflag.h" + +// func firRawAVX2(dst []int32, x []int16, coef32 []int32) +// dst[o] = sum_t x[o+t]*coef32[t], vectorised across 8 outputs per iteration: +// lane k accumulates output og+k, so no per-output horizontal reduction. +TEXT ·firRawAVX2(SB), NOSPLIT, $0-72 + MOVQ dst_base+0(FP), DI + MOVQ dst_len+8(FP), CX // number of outputs + MOVQ x_base+24(FP), SI // &x[og] (group base) + MOVQ coef32_base+48(FP), BX + MOVQ coef32_len+56(FP), R9 // taps + + MOVQ CX, R10 + SHRQ $3, R10 // R10 = groups of 8 outputs + JZ firtail + +grouploop: + VPXOR Y0, Y0, Y0 + MOVQ SI, R11 // x cursor for this group + MOVQ BX, R12 // coef cursor + MOVQ R9, R13 // tap counter + +taploop: + VPMOVSXWD (R11), Y1 // 8 int16 x[og+t..og+t+7] -> 8 int32 + VPBROADCASTD (R12), Y2 // coef32[t] in all 8 lanes + VPMULLD Y2, Y1, Y3 + VPADDD Y3, Y0, Y0 + ADDQ $2, R11 // next tap shifts x window by 1 int16 + ADDQ $4, R12 + DECQ R13 + JNZ taploop + + VMOVDQU Y0, (DI) + ADDQ $32, DI // 8 int32 stored + ADDQ $16, SI // next group: x base += 8 int16 + DECQ R10 + JNZ grouploop + +firtail: + ANDQ $7, CX // remaining outputs (<8) + JZ firdone + +tailout: + MOVQ SI, R11 + MOVQ BX, R12 + MOVQ R9, R13 + XORL AX, AX + +tailtap: + MOVWLSX (R11), DX + MOVL (R12), R8 + IMULL R8, DX + ADDL DX, AX + ADDQ $2, R11 + ADDQ $4, R12 + DECQ R13 + JNZ tailtap + + MOVL AX, (DI) + ADDQ $4, DI + ADDQ $2, SI // next single output: x base += 1 int16 + DECQ CX + JNZ tailout + +firdone: + VZEROUPPER + RET + +// func firDotAVX2(a, b []int16) int32 +// Returns sum_i a[i]*b[i] (wrapping int32) over len(a) elements. VPMADDWD folds +// each int16 pair into an int32 (exact mod 2^32); lanes accumulate, then a +// horizontal sum + scalar tail. All adds wrap, so the order matches the scalar. +TEXT ·firDotAVX2(SB), NOSPLIT, $0-52 + MOVQ a_base+0(FP), SI + MOVQ a_len+8(FP), CX + MOVQ b_base+24(FP), DI + VPXOR Y0, Y0, Y0 // accumulator: 8 int32 lanes + + MOVQ CX, R10 + SHRQ $4, R10 // groups of 16 int16 + JZ dtail + +dloop: + VMOVDQU (SI), Y1 // 16 int16 a + VMOVDQU (DI), Y2 // 16 int16 b + VPMADDWD Y2, Y1, Y3 // 8 int32: a[2k]*b[2k] + a[2k+1]*b[2k+1] + VPADDD Y3, Y0, Y0 + ADDQ $32, SI + ADDQ $32, DI + DECQ R10 + JNZ dloop + +dtail: + // horizontal sum of the 8 int32 lanes -> AX + VEXTRACTI128 $1, Y0, X1 + VPADDD X1, X0, X0 // 4 int32 + VPSHUFD $0x4E, X0, X1 // [2,3,0,1] + VPADDD X1, X0, X0 // 2 int32 + VPSHUFD $0xB1, X0, X1 // [1,0,3,2] + VPADDD X1, X0, X0 // 1 int32 + MOVD X0, AX + + ANDQ $15, CX // remaining elements (<16) + JZ ddone + +dscalar: + MOVWLSX (SI), DX + MOVWLSX (DI), R8 + IMULL R8, DX + ADDL DX, AX + ADDQ $2, SI + ADDQ $2, DI + DECQ CX + JNZ dscalar + +ddone: + VZEROUPPER + MOVL AX, ret+48(FP) + RET diff --git a/internal/capability/transcode/amrnb/simd_noasm.go b/internal/capability/transcode/amrnb/simd_noasm.go new file mode 100644 index 0000000..491fa16 --- /dev/null +++ b/internal/capability/transcode/amrnb/simd_noasm.go @@ -0,0 +1,9 @@ +//go:build !amd64 + +package amrnb + +// firRaw falls back to the portable implementation on non-amd64 targets. +func firRaw(dst []int32, x []int16, coef []int16) { firRawGeneric(dst, x, coef) } + +// firDot falls back to the portable implementation on non-amd64 targets. +func firDot(a, b []int16) int32 { return firDotGeneric(a, b) } diff --git a/internal/capability/transcode/amrnb/tables_acelp.go b/internal/capability/transcode/amrnb/tables_acelp.go new file mode 100644 index 0000000..532ddcc --- /dev/null +++ b/internal/capability/transcode/amrnb/tables_acelp.go @@ -0,0 +1,62 @@ +// Code generated from opencore-amrnb — DO NOT EDIT. +// +// ROM tables extracted verbatim from the Apache-2.0 opencore-amrnb C +// reference; element counts are verified against the C array sizes. + +package amrnb + +var startPos = []int16{ + 0, 2, 0, 3, 0, 2, 0, 3, 1, 3, 2, 4, + 1, 4, 1, 4, +} + +var gray = []int16{ + 0, 1, 3, 2, 6, 4, 5, 7, +} + +var dgray = []int16{ + 0, 1, 3, 2, 5, 6, 4, 7, +} + +var ph_imp_low_MR795 = []int16{ + 26777, 801, 2505, -683, -1382, 582, 604, -1274, 3511, -5894, 4534, -499, + -1940, 3011, -5058, 5614, -1990, -1061, -1459, 4442, -700, -5335, 4609, 452, + -589, -3352, 2953, 1267, -1212, -2590, 1731, 3670, -4475, -975, 4391, -2537, + 949, -1363, -979, 5734, +} + +var ph_imp_mid_MR795 = []int16{ + 30274, 3831, -4036, 2972, -1048, -1002, 2477, -3043, 2815, -2231, 1753, -1611, + 1714, -1775, 1543, -1008, 429, -169, 472, -1264, 2176, -2706, 2523, -1621, + 344, 826, -1529, 1724, -1657, 1701, -2063, 2644, -3060, 2897, -1978, 557, + 780, -1369, 842, 655, +} + +var ph_imp_low = []int16{ + 14690, 11518, 1268, -2761, -5671, 7514, -35, -2807, -3040, 4823, 2952, -8424, + 3785, 1455, 2179, -8637, 8051, -2103, -1454, 777, 1108, -2385, 2254, -363, + -674, -2103, 6046, -5681, 1072, 3123, -5058, 5312, -2329, -3728, 6924, -3889, + 675, -1775, 29, 10145, +} + +var ph_imp_mid = []int16{ + 30274, 3831, -4036, 2972, -1048, -1002, 2477, -3043, 2815, -2231, 1753, -1611, + 1714, -1775, 1543, -1008, 429, -169, 472, -1264, 2176, -2706, 2523, -1621, + 344, 826, -1529, 1724, -1657, 1701, -2063, 2644, -3060, 2897, -1978, 557, + 780, -1369, 842, 655, +} + +// inter_6_pred_lt is the 1/6-resolution pitch interpolation FIR +// (pred_lt.cpp), FIR_SIZE = L_INTER10*UP_SAMP_MAX+1 = 61. +var inter_6_pred_lt = []int16{ + 29443, 28346, 25207, 20449, 14701, 8693, 3143, -1352, -4402, -5865, -5850, -4673, + -2783, -672, 1211, 2536, 3130, 2991, 2259, 1170, 0, -1001, -1652, -1868, + -1666, -1147, -464, 218, 756, 1060, 1099, 904, 550, 135, -245, -514, + -634, -602, -451, -231, 0, 191, 308, 340, 296, 198, 78, -36, + -120, -163, -165, -132, -79, -19, 34, 73, 91, 89, 70, 38, + 0, +} + +// inter6Pitch: 1/6-resolution interpolation FIR for the closed-loop pitch +// search (inter_36_tab.cpp), FIR_SIZE = UP_SAMP_MAX*L_INTER_SRCH+1 = 25. +var inter6Pitch = []int16{29519, 28316, 24906, 19838, 13896, 7945, 2755, -1127, -3459, -4304, -3969, -2899, -1561, -336, 534, 970, 1023, 823, 516, 220, 0, -131, -194, -215, 0} diff --git a/internal/capability/transcode/amrnb/tables_gain.go b/internal/capability/transcode/amrnb/tables_gain.go new file mode 100644 index 0000000..833df82 --- /dev/null +++ b/internal/capability/transcode/amrnb/tables_gain.go @@ -0,0 +1,186 @@ +// Code generated from opencore-amrnb — DO NOT EDIT. +// +// ROM tables extracted verbatim from the Apache-2.0 opencore-amrnb C +// reference; element counts are verified against the C array sizes. + +package amrnb + +var qua_gain_pitch = []int16{ + 0, 3277, 6556, 8192, 9830, 11469, 12288, 13107, 13926, 14746, 15565, 16384, + 17203, 18022, 18842, 19661, +} + +var qua_gain_code = []int16{ + 159, -3776, -22731, 206, -3394, -20428, 268, -3005, -18088, 349, -2615, -15739, + 419, -2345, -14113, 482, -2138, -12867, 554, -1932, -11629, 637, -1726, -10387, + 733, -1518, -9139, 842, -1314, -7906, 969, -1106, -6656, 1114, -900, -5416, + 1281, -694, -4173, 1473, -487, -2931, 1694, -281, -1688, 1948, -75, -445, + 2241, 133, 801, 2577, 339, 2044, 2963, 545, 3285, 3408, 752, 4530, + 3919, 958, 5772, 4507, 1165, 7016, 5183, 1371, 8259, 5960, 1577, 9501, + 6855, 1784, 10745, 7883, 1991, 11988, 9065, 2197, 13231, 10425, 2404, 14474, + 12510, 2673, 16096, 16263, 3060, 18429, 21142, 3448, 20763, 27485, 3836, 23097, + 27485, 3836, 23097, +} + +var table_gain_highrates = []int16{ + 577, 662, -2692, -16214, 806, 1836, -1185, -7135, 3109, 1052, -2008, -12086, + 4181, 1387, -1600, -9629, 2373, 1425, -1560, -9394, 3248, 1985, -1070, -6442, + 1827, 2320, -840, -5056, 941, 3314, -313, -1885, 2351, 2977, -471, -2838, + 3616, 2420, -777, -4681, 3451, 3096, -414, -2490, 2955, 4301, 72, 434, + 1848, 4500, 139, 836, 3884, 5416, 413, 2484, 1187, 7210, 835, 5030, + 3083, 9000, 1163, 7002, 7384, 883, -2267, -13647, 5962, 1506, -1478, -8900, + 5155, 2134, -963, -5800, 7944, 2009, -1052, -6335, 6507, 2250, -885, -5327, + 7670, 2752, -588, -3537, 5952, 3016, -452, -2724, 4898, 3764, -125, -751, + 6989, 3588, -196, -1177, 8174, 3978, -43, -260, 6064, 4404, 107, 645, + 7709, 5087, 320, 1928, 5523, 6021, 569, 3426, 7769, 7126, 818, 4926, + 6060, 7938, 977, 5885, 5594, 11487, 1523, 9172, 10581, 1356, -1633, -9831, + 9049, 1597, -1391, -8380, 9794, 2035, -1033, -6220, 8946, 2415, -780, -4700, + 10296, 2584, -681, -4099, 9407, 2734, -597, -3595, 8700, 3218, -356, -2144, + 9757, 3395, -277, -1669, 10177, 3892, -75, -454, 9170, 4528, 148, 891, + 10152, 5004, 296, 1781, 9114, 5735, 497, 2993, 10500, 6266, 628, 3782, + 10110, 7631, 919, 5534, 8844, 8727, 1117, 6728, 8956, 12496, 1648, 9921, + 12924, 976, -2119, -12753, 11435, 1755, -1252, -7539, 12138, 2328, -835, -5024, + 11388, 2368, -810, -4872, 10700, 3064, -429, -2580, 12332, 2861, -530, -3192, + 11722, 3327, -307, -1848, 11270, 3700, -150, -904, 10861, 4413, 110, 663, + 12082, 4533, 150, 902, 11283, 5205, 354, 2132, 11960, 6305, 637, 3837, + 11167, 7534, 900, 5420, 12128, 8329, 1049, 6312, 10969, 10777, 1429, 8604, + 10300, 17376, 2135, 12853, 13899, 1681, -1316, -7921, 12580, 2045, -1026, -6179, + 13265, 2439, -766, -4610, 14033, 2989, -465, -2802, 13452, 3098, -413, -2482, + 12396, 3658, -167, -1006, 13510, 3780, -119, -713, 12880, 4272, 62, 374, + 13533, 4861, 253, 1523, 12667, 5457, 424, 2552, 13854, 6106, 590, 3551, + 13031, 6483, 678, 4084, 13557, 7721, 937, 5639, 12957, 9311, 1213, 7304, + 13714, 11551, 1532, 9221, 12591, 15206, 1938, 11667, 15113, 1540, -1445, -8700, + 15072, 2333, -832, -5007, 14527, 2511, -723, -4352, 14692, 3199, -365, -2197, + 15382, 3560, -207, -1247, 14133, 3960, -50, -300, 15102, 4236, 50, 298, + 14332, 4824, 242, 1454, 14846, 5451, 422, 2542, 15306, 6083, 584, 3518, + 14329, 6888, 768, 4623, 15060, 7689, 930, 5602, 14406, 9426, 1231, 7413, + 15387, 9741, 1280, 7706, 14824, 14271, 1844, 11102, 13600, 24939, 2669, 16067, + 16396, 1969, -1082, -6517, 16817, 2832, -545, -3283, 15713, 2843, -539, -3248, + 16104, 3336, -303, -1825, 16384, 3963, -49, -294, 16940, 4579, 165, 992, + 15711, 4599, 171, 1030, 16222, 5448, 421, 2537, 16832, 6382, 655, 3945, + 15745, 7141, 821, 4944, 16326, 7469, 888, 5343, 16611, 8624, 1100, 6622, + 17028, 10418, 1379, 8303, 15905, 11817, 1565, 9423, 16878, 14690, 1887, 11360, + 16515, 20870, 2406, 14483, 18142, 2083, -999, -6013, 19401, 3178, -375, -2257, + 17508, 3426, -264, -1589, 20054, 4027, -25, -151, 18069, 4249, 54, 326, + 18952, 5066, 314, 1890, 17711, 5402, 409, 2461, 19835, 6192, 610, 3676, + 17950, 7014, 795, 4784, 21318, 7877, 966, 5816, 17910, 9289, 1210, 7283, + 19144, 9290, 1210, 7284, 20517, 11381, 1510, 9089, 18075, 14485, 1866, 11234, + 19999, 17882, 2177, 13108, 18842, 32764, 3072, 18494, +} + +var table_gain_lowrates = []int16{ + 10813, 28753, 2879, 17333, 20480, 2785, -570, -3431, 18841, 6594, 703, 4235, + 6225, 7413, 876, 5276, 17203, 10444, 1383, 8325, 21626, 1269, -1731, -10422, + 21135, 4423, 113, 683, 11304, 1556, -1430, -8609, 19005, 12820, 1686, 10148, + 17367, 2498, -731, -4398, 17858, 4833, 244, 1472, 9994, 2498, -731, -4398, + 17530, 7864, 964, 5802, 14254, 1884, -1147, -6907, 15892, 3153, -387, -2327, + 6717, 1802, -1213, -7303, 18186, 20193, 2357, 14189, 18022, 3031, -445, -2678, + 16711, 5857, 528, 3181, 8847, 4014, -30, -180, 15892, 8970, 1158, 6972, + 18022, 1392, -1594, -9599, 16711, 4096, 0, 0, 8192, 655, -2708, -16305, + 15237, 13926, 1808, 10884, 14254, 3112, -406, -2444, 14090, 4669, 193, 1165, + 5406, 2703, -614, -3697, 13434, 6553, 694, 4180, 12451, 901, -2237, -13468, + 12451, 2662, -637, -3833, 3768, 655, -2708, -16305, 14745, 23511, 2582, 15543, + 19169, 2457, -755, -4546, 20152, 5079, 318, 1913, 6881, 4096, 0, 0, + 20480, 8560, 1089, 6556, 19660, 737, -2534, -15255, 19005, 4259, 58, 347, + 7864, 2088, -995, -5993, 11468, 12288, 1623, 9771, 15892, 1474, -1510, -9090, + 15728, 4628, 180, 1086, 9175, 1433, -1552, -9341, 16056, 7004, 793, 4772, + 14827, 737, -2534, -15255, 15073, 2252, -884, -5321, 5079, 1228, -1780, -10714, + 13271, 17326, 2131, 12827, 16547, 2334, -831, -5002, 15073, 5816, 518, 3118, + 3932, 3686, -156, -938, 14254, 8601, 1096, 6598, 16875, 778, -2454, -14774, + 15073, 3809, -107, -646, 6062, 614, -2804, -16879, 9338, 9256, 1204, 7251, + 13271, 1761, -1247, -7508, 13271, 3522, -223, -1343, 2457, 1966, -1084, -6529, + 11468, 5529, 443, 2668, 10485, 737, -2534, -15255, 11632, 3194, -367, -2212, + 1474, 778, -2454, -14774, +} + +// table_gain_MR475 is the 4.75k joint pitch+code gain VQ (qgain475_tab.cpp). +// 4 values per entry: gain_pit, gain_code, then two unused slots reused by the +// decoder only for gain_pit/gain_code (energies are recomputed). +var table_gain_MR475 = []int16{ + 812, 128, 542, 140, 2873, 1135, 2266, 3402, 2067, 563, 12677, 647, + 4132, 1798, 5601, 5285, 7689, 374, 3735, 441, 10912, 2638, 11807, 2494, + 20490, 797, 5218, 675, 6724, 8354, 5282, 1696, 1488, 428, 5882, 452, + 5332, 4072, 3583, 1268, 2469, 901, 15894, 1005, 14982, 3271, 10331, 4858, + 3635, 2021, 2596, 835, 12360, 4892, 12206, 1704, 13432, 1604, 9118, 2341, + 3968, 1538, 5479, 9936, 3795, 417, 1359, 414, 3640, 1569, 7995, 3541, + 11405, 645, 8552, 635, 4056, 1377, 16608, 6124, 11420, 700, 2007, 607, + 12415, 1578, 11119, 4654, 13680, 1708, 11990, 1229, 7996, 7297, 13231, 5715, + 2428, 1159, 2073, 1941, 6218, 6121, 3546, 1804, 8925, 1802, 8679, 1580, + 13935, 3576, 13313, 6237, 6142, 1130, 5994, 1734, 14141, 4662, 11271, 3321, + 12226, 1551, 13931, 3015, 5081, 10464, 9444, 6706, 1689, 683, 1436, 1306, + 7212, 3933, 4082, 2713, 7793, 704, 15070, 802, 6299, 5212, 4337, 5357, + 6676, 541, 6062, 626, 13651, 3700, 11498, 2408, 16156, 716, 12177, 751, + 8065, 11489, 6314, 2256, 4466, 496, 7293, 523, 10213, 3833, 8394, 3037, + 8403, 966, 14228, 1880, 8703, 5409, 16395, 4863, 7420, 1979, 6089, 1230, + 9371, 4398, 14558, 3363, 13559, 2873, 13163, 1465, 5534, 1678, 13138, 14771, + 7338, 600, 1318, 548, 4252, 3539, 10044, 2364, 10587, 622, 13088, 669, + 14126, 3526, 5039, 9784, 15338, 619, 3115, 590, 16442, 3013, 15542, 4168, + 15537, 1611, 15405, 1228, 16023, 9299, 7534, 4976, 1990, 1213, 11447, 1157, + 12512, 5519, 9475, 2644, 7716, 2034, 13280, 2239, 16011, 5093, 8066, 6761, + 10083, 1413, 5002, 2347, 12523, 5975, 15126, 2899, 18264, 2289, 15827, 2527, + 16265, 10254, 14651, 11319, 1797, 337, 3115, 397, 3510, 2928, 4592, 2670, + 7519, 628, 11415, 656, 5946, 2435, 6544, 7367, 8238, 829, 4000, 863, + 10032, 2492, 16057, 3551, 18204, 1054, 6103, 1454, 5884, 7900, 18752, 3468, + 1864, 544, 9198, 683, 11623, 4160, 4594, 1644, 3158, 1157, 15953, 2560, + 12349, 3733, 17420, 5260, 6106, 2004, 2917, 1742, 16467, 5257, 16787, 1680, + 17205, 1759, 4773, 3231, 7386, 6035, 14342, 10012, 4035, 442, 4194, 458, + 9214, 2242, 7427, 4217, 12860, 801, 11186, 825, 12648, 2084, 12956, 6554, + 9505, 996, 6629, 985, 10537, 2502, 15289, 5006, 12602, 2055, 15484, 1653, + 16194, 6921, 14231, 5790, 2626, 828, 5615, 1686, 13663, 5778, 3668, 1554, + 11313, 2633, 9770, 1459, 14003, 4733, 15897, 6291, 6278, 1870, 7910, 2285, + 16978, 4571, 16576, 3849, 15248, 2311, 16023, 3244, 14459, 17808, 11847, 2763, + 1981, 1407, 1400, 876, 4335, 3547, 4391, 4210, 5405, 680, 17461, 781, + 6501, 5118, 8091, 7677, 7355, 794, 8333, 1182, 15041, 3160, 14928, 3039, + 20421, 880, 14545, 852, 12337, 14708, 6904, 1920, 4225, 933, 8218, 1087, + 10659, 4084, 10082, 4533, 2735, 840, 20657, 1081, 16711, 5966, 15873, 4578, + 10871, 2574, 3773, 1166, 14519, 4044, 20699, 2627, 15219, 2734, 15274, 2186, + 6257, 3226, 13125, 19480, 7196, 930, 2462, 1618, 4515, 3092, 13852, 4277, + 10460, 833, 17339, 810, 16891, 2289, 15546, 8217, 13603, 1684, 3197, 1834, + 15948, 2820, 15812, 5327, 17006, 2438, 16788, 1326, 15671, 8156, 11726, 8556, + 3762, 2053, 9563, 1317, 13561, 6790, 12227, 1936, 8180, 3550, 13287, 1778, + 16299, 6599, 16291, 7758, 8521, 2551, 7225, 2645, 18269, 7489, 16885, 2248, + 17882, 2884, 17265, 3328, 9417, 20162, 11042, 8320, 1286, 620, 1431, 583, + 5993, 2289, 3978, 3626, 5144, 752, 13409, 830, 5553, 2860, 11764, 5908, + 10737, 560, 5446, 564, 13321, 3008, 11946, 3683, 19887, 798, 9825, 728, + 13663, 8748, 7391, 3053, 2515, 778, 6050, 833, 6469, 5074, 8305, 2463, + 6141, 1865, 15308, 1262, 14408, 4547, 13663, 4515, 3137, 2983, 2479, 1259, + 15088, 4647, 15382, 2607, 14492, 2392, 12462, 2537, 7539, 2949, 12909, 12060, + 5468, 684, 3141, 722, 5081, 1274, 12732, 4200, 15302, 681, 7819, 592, + 6534, 2021, 16478, 8737, 13364, 882, 5397, 899, 14656, 2178, 14741, 4227, + 14270, 1298, 13929, 2029, 15477, 7482, 15815, 4572, 2521, 2013, 5062, 1804, + 5159, 6582, 7130, 3597, 10920, 1611, 11729, 1708, 16903, 3455, 16268, 6640, + 9306, 1007, 9369, 2106, 19182, 5037, 12441, 4269, 15919, 1332, 15357, 3512, + 11898, 14141, 16101, 6854, 2010, 737, 3779, 861, 11454, 2880, 3564, 3540, + 9057, 1241, 12391, 896, 8546, 4629, 11561, 5776, 8129, 589, 8218, 588, + 18728, 3755, 12973, 3149, 15729, 758, 16634, 754, 15222, 11138, 15871, 2208, + 4673, 610, 10218, 678, 15257, 4146, 5729, 3327, 8377, 1670, 19862, 2321, + 15450, 5511, 14054, 5481, 5728, 2888, 7580, 1346, 14384, 5325, 16236, 3950, + 15118, 3744, 15306, 1435, 14597, 4070, 12301, 15696, 7617, 1699, 2170, 884, + 4459, 4567, 18094, 3306, 12742, 815, 14926, 907, 15016, 4281, 15518, 8368, + 17994, 1087, 2358, 865, 16281, 3787, 15679, 4596, 16356, 1534, 16584, 2210, + 16833, 9697, 15929, 4513, 3277, 1085, 9643, 2187, 11973, 6068, 9199, 4462, + 8955, 1629, 10289, 3062, 16481, 5155, 15466, 7066, 13678, 2543, 5273, 2277, + 16746, 6213, 16655, 3408, 20304, 3363, 18688, 1985, 14172, 12867, 15154, 15703, + 4473, 1020, 1681, 886, 4311, 4301, 8952, 3657, 5893, 1147, 11647, 1452, + 15886, 2227, 4582, 6644, 6929, 1205, 6220, 799, 12415, 3409, 15968, 3877, + 19859, 2109, 9689, 2141, 14742, 8830, 14480, 2599, 1817, 1238, 7771, 813, + 19079, 4410, 5554, 2064, 3687, 2844, 17435, 2256, 16697, 4486, 16199, 5388, + 8028, 2763, 3405, 2119, 17426, 5477, 13698, 2786, 19879, 2720, 9098, 3880, + 18172, 4833, 17336, 12207, 5116, 996, 4935, 988, 9888, 3081, 6014, 5371, + 15881, 1667, 8405, 1183, 15087, 2366, 19777, 7002, 11963, 1562, 7279, 1128, + 16859, 1532, 15762, 5381, 14708, 2065, 20105, 2155, 17158, 8245, 17911, 6318, + 5467, 1504, 4100, 2574, 17421, 6810, 5673, 2888, 16636, 3382, 8975, 1831, + 20159, 4737, 19550, 7294, 6658, 2781, 11472, 3321, 19397, 5054, 18878, 4722, + 16439, 2373, 20430, 4386, 11353, 26526, 11593, 3068, 2866, 1566, 5108, 1070, + 9614, 4915, 4939, 3536, 7541, 878, 20717, 851, 6938, 4395, 16799, 7733, + 10137, 1019, 9845, 964, 15494, 3955, 15459, 3430, 18863, 982, 20120, 963, + 16876, 12887, 14334, 4200, 6599, 1220, 9222, 814, 16942, 5134, 5661, 4898, + 5488, 1798, 20258, 3962, 17005, 6178, 17929, 5929, 9365, 3420, 7474, 1971, + 19537, 5177, 19003, 3006, 16454, 3788, 16070, 2367, 8664, 2743, 9445, 26358, + 10856, 1287, 3555, 1009, 5606, 3622, 19453, 5512, 12453, 797, 20634, 911, + 15427, 3066, 17037, 10275, 18883, 2633, 3913, 1268, 19519, 3371, 18052, 5230, + 19291, 1678, 19508, 3172, 18072, 10754, 16625, 6845, 3134, 2298, 10869, 2437, + 15580, 6913, 12597, 3381, 11116, 3297, 16762, 2424, 18853, 6715, 17171, 9887, + 12743, 2605, 8937, 3140, 19033, 7764, 18347, 3880, 20475, 3682, 19602, 3380, + 13044, 19373, 10526, 23124, +} diff --git a/internal/capability/transcode/amrnb/tables_lpc.go b/internal/capability/transcode/amrnb/tables_lpc.go new file mode 100644 index 0000000..385460f --- /dev/null +++ b/internal/capability/transcode/amrnb/tables_lpc.go @@ -0,0 +1,80 @@ +// Code generated from opencore-amrnb — DO NOT EDIT. +// +// ROM tables extracted verbatim from the Apache-2.0 opencore-amrnb C +// reference; element counts are verified against the C array sizes. + +package amrnb + +var window_200_40 = []int16{ + 2621, 2623, 2629, 2638, 2651, 2668, 2689, 2713, 2741, 2772, 2808, 2847, + 2890, 2936, 2986, 3040, 3097, 3158, 3223, 3291, 3363, 3438, 3517, 3599, + 3685, 3774, 3867, 3963, 4063, 4166, 4272, 4382, 4495, 4611, 4731, 4853, + 4979, 5108, 5240, 5376, 5514, 5655, 5800, 5947, 6097, 6250, 6406, 6565, + 6726, 6890, 7057, 7227, 7399, 7573, 7750, 7930, 8112, 8296, 8483, 8672, + 8863, 9057, 9252, 9450, 9650, 9852, 10055, 10261, 10468, 10677, 10888, 11101, + 11315, 11531, 11748, 11967, 12187, 12409, 12632, 12856, 13082, 13308, 13536, 13764, + 13994, 14225, 14456, 14688, 14921, 15155, 15389, 15624, 15859, 16095, 16331, 16568, + 16805, 17042, 17279, 17516, 17754, 17991, 18228, 18465, 18702, 18939, 19175, 19411, + 19647, 19882, 20117, 20350, 20584, 20816, 21048, 21279, 21509, 21738, 21967, 22194, + 22420, 22644, 22868, 23090, 23311, 23531, 23749, 23965, 24181, 24394, 24606, 24816, + 25024, 25231, 25435, 25638, 25839, 26037, 26234, 26428, 26621, 26811, 26999, 27184, + 27368, 27548, 27727, 27903, 28076, 28247, 28415, 28581, 28743, 28903, 29061, 29215, + 29367, 29515, 29661, 29804, 29944, 30081, 30214, 30345, 30472, 30597, 30718, 30836, + 30950, 31062, 31170, 31274, 31376, 31474, 31568, 31659, 31747, 31831, 31911, 31988, + 32062, 32132, 32198, 32261, 32320, 32376, 32428, 32476, 32521, 32561, 32599, 32632, + 32662, 32688, 32711, 32729, 32744, 32755, 32763, 32767, 32767, 32741, 32665, 32537, + 32359, 32129, 31850, 31521, 31143, 30716, 30242, 29720, 29151, 28538, 27879, 27177, + 26433, 25647, 24821, 23957, 23055, 22117, 21145, 20139, 19102, 18036, 16941, 15820, + 14674, 13505, 12315, 11106, 9879, 8637, 7381, 6114, 4838, 3554, 2264, 971, +} + +var window_160_80 = []int16{ + 2621, 2624, 2633, 2648, 2668, 2695, 2727, 2765, 2809, 2859, 2915, 2976, + 3043, 3116, 3194, 3279, 3368, 3464, 3565, 3671, 3783, 3900, 4023, 4151, + 4285, 4423, 4567, 4716, 4870, 5029, 5193, 5362, 5535, 5714, 5897, 6084, + 6277, 6473, 6674, 6880, 7089, 7303, 7521, 7742, 7968, 8197, 8430, 8667, + 8907, 9151, 9398, 9648, 9902, 10158, 10417, 10680, 10945, 11212, 11482, 11755, + 12030, 12307, 12586, 12867, 13150, 13435, 13722, 14010, 14299, 14590, 14882, 15175, + 15469, 15764, 16060, 16356, 16653, 16950, 17248, 17546, 17844, 18141, 18439, 18736, + 19033, 19330, 19625, 19920, 20214, 20507, 20799, 21090, 21380, 21668, 21954, 22239, + 22522, 22803, 23083, 23360, 23635, 23907, 24177, 24445, 24710, 24972, 25231, 25488, + 25741, 25991, 26238, 26482, 26722, 26959, 27192, 27422, 27647, 27869, 28087, 28300, + 28510, 28715, 28916, 29113, 29305, 29493, 29676, 29854, 30028, 30197, 30361, 30519, + 30673, 30822, 30966, 31105, 31238, 31366, 31489, 31606, 31718, 31825, 31926, 32021, + 32111, 32195, 32273, 32346, 32413, 32475, 32530, 32580, 32624, 32662, 32695, 32721, + 32742, 32756, 32765, 32767, 32767, 32756, 32720, 32661, 32578, 32471, 32341, 32188, + 32012, 31813, 31592, 31349, 31084, 30798, 30492, 30165, 29818, 29453, 29068, 28666, + 28247, 27810, 27358, 26891, 26408, 25913, 25404, 24883, 24350, 23807, 23255, 22693, + 22124, 21548, 20965, 20378, 19786, 19191, 18593, 17994, 17395, 16796, 16199, 15604, + 15012, 14424, 13842, 13265, 12696, 12135, 11582, 11039, 10507, 9986, 9477, 8981, + 8499, 8031, 7579, 7143, 6723, 6321, 5937, 5571, 5225, 4898, 4591, 4305, + 4041, 3798, 3577, 3378, 3202, 3048, 2918, 2812, 2729, 2669, 2633, 2621, +} + +var window_232_8 = []int16{ + 2621, 2623, 2627, 2634, 2644, 2656, 2671, 2689, 2710, 2734, 2760, 2789, + 2821, 2855, 2893, 2933, 2975, 3021, 3069, 3120, 3173, 3229, 3288, 3350, + 3414, 3481, 3550, 3622, 3697, 3774, 3853, 3936, 4021, 4108, 4198, 4290, + 4385, 4482, 4582, 4684, 4788, 4895, 5004, 5116, 5230, 5346, 5464, 5585, + 5708, 5833, 5960, 6090, 6221, 6355, 6491, 6629, 6769, 6910, 7054, 7200, + 7348, 7498, 7649, 7803, 7958, 8115, 8274, 8434, 8597, 8761, 8926, 9093, + 9262, 9432, 9604, 9778, 9952, 10129, 10306, 10485, 10665, 10847, 11030, 11214, + 11399, 11586, 11773, 11962, 12152, 12342, 12534, 12727, 12920, 13115, 13310, 13506, + 13703, 13901, 14099, 14298, 14497, 14698, 14898, 15100, 15301, 15504, 15706, 15909, + 16112, 16316, 16520, 16724, 16928, 17132, 17337, 17541, 17746, 17950, 18155, 18359, + 18564, 18768, 18972, 19175, 19379, 19582, 19785, 19987, 20189, 20390, 20591, 20792, + 20992, 21191, 21390, 21588, 21785, 21981, 22177, 22372, 22566, 22759, 22951, 23143, + 23333, 23522, 23710, 23897, 24083, 24268, 24451, 24633, 24814, 24994, 25172, 25349, + 25525, 25699, 25871, 26042, 26212, 26380, 26546, 26711, 26874, 27035, 27195, 27353, + 27509, 27664, 27816, 27967, 28115, 28262, 28407, 28550, 28691, 28830, 28967, 29102, + 29234, 29365, 29493, 29619, 29743, 29865, 29985, 30102, 30217, 30330, 30440, 30548, + 30654, 30757, 30858, 30956, 31052, 31146, 31237, 31326, 31412, 31495, 31576, 31655, + 31730, 31804, 31874, 31942, 32008, 32071, 32131, 32188, 32243, 32295, 32345, 32392, + 32436, 32477, 32516, 32552, 32585, 32615, 32643, 32668, 32690, 32709, 32726, 32740, + 32751, 32759, 32765, 32767, 32767, 32097, 30112, 26895, 22576, 17333, 11380, 4962, +} + +// lag_h/lag_l: lag-window (bandwidth expansion) coefficients, +// double precision (lag_wind_tab.cpp). +var lag_h = []int16{32728, 32619, 32438, 32187, 31867, 31480, 31029, 30517, 29946, 29321} +var lag_l = []int16{11904, 17280, 30720, 25856, 24192, 28992, 24384, 7360, 19520, 14784} diff --git a/internal/capability/transcode/amrnb/tables_lsp.go b/internal/capability/transcode/amrnb/tables_lsp.go new file mode 100644 index 0000000..7688086 --- /dev/null +++ b/internal/capability/transcode/amrnb/tables_lsp.go @@ -0,0 +1,946 @@ +// Code generated from opencore-amrnb — DO NOT EDIT. +// +// ROM tables extracted verbatim from the Apache-2.0 opencore-amrnb C +// reference; element counts are verified against the C array sizes. + +package amrnb + +var past_rq_init = []int16{ + -258, -318, -439, -634, -656, -773, -711, -502, -268, -193, -2, 125, + 122, -39, -9, 105, 129, 283, 372, 575, -277, -324, -197, -487, + -445, -362, -292, -27, 177, 543, 342, 517, 516, 130, 27, -104, + -120, -140, -74, -56, -564, -943, -1520, -965, -814, -526, -322, -2, + 159, 657, -312, -284, -386, -597, -493, -526, -418, -229, 105, 449, + -557, -870, -1075, -919, -950, -752, -709, -316, 62, 486, -314, -191, + -203, -330, -160, -103, -51, 131, 338, 515, +} + +var mean_lsf_3 = []int16{ + 1546, 2272, 3778, 5488, 6972, 8382, 10047, 11229, 12766, 13714, +} + +var pred_fac_3 = []int16{ + 9556, 10769, 12571, 13292, 14381, 11651, 10588, 9767, 8593, 6484, +} + +var dico1_lsf_3 = []int16{ + 6, 82, -131, 154, -56, -735, 183, -65, -265, 9, -210, -361, + 113, 718, 1817, 1010, 1214, 1573, 857, 1333, 2276, 827, 1568, 1933, + 717, 1989, 2206, 838, 1172, 1823, 721, 1000, 2154, 286, 476, 1509, + -247, -531, 230, 147, -82, 569, 26, -177, -944, -27, -273, 692, + -164, -264, -183, 224, 790, 1039, 899, 946, 601, 485, 771, 1150, + 524, 677, 903, -140, 375, 778, 410, 676, 429, 301, 530, 1009, + 719, 646, 38, 226, 367, 40, 145, -45, -505, 290, 121, -121, + 302, 127, 166, -124, -383, -956, -358, -455, -977, 715, 878, 894, + 978, 923, 211, 477, 272, 64, 188, -78, 17, -143, -65, 38, + 643, 586, 621, -134, -426, -651, 347, 545, 2820, 1188, 2726, 2442, + 142, -80, 1735, 283, 130, 461, -262, -399, -1145, -411, 155, 430, + 329, 375, 779, 53, -226, -139, -129, -236, 1682, 285, 744, 1327, + 738, 697, 1664, 312, 409, 266, 325, 720, 135, 1, 221, 453, + 8, 203, 145, 299, 640, 760, 29, 468, 638, 103, 429, 379, + 420, 954, 932, 1326, 1210, 1258, 704, 1012, 1152, -166, -444, -266, + -316, -130, -376, 191, 1151, 1904, -240, -543, -1260, -112, 268, 1207, + 70, 1062, 1583, 278, 1360, 1574, -258, -272, -768, 19, 563, 2240, + -3, -265, 135, -295, -591, -388, 140, 354, -206, -260, -504, -795, + -433, -718, -1319, 109, 331, 962, -429, -87, 652, -296, 426, 1019, + -239, 775, 851, 489, 1334, 1073, -334, -332, 25, 543, 1206, 1807, + 326, 61, 727, 578, 849, 1405, -208, -277, 329, -152, 64, 669, + -434, -678, -727, -454, -71, 251, 605, 480, 254, -482, 11, 996, + -289, 395, 486, 722, 1049, 1440, -30, -316, -786, -106, -115, -619, + 861, 1474, 1412, 1055, 1366, 1184, 812, 1237, 925, 42, -251, -576, + 342, 141, -454, -168, -80, 1359, -342, -656, -1763, 100, 821, 725, + 990, 747, 800, 332, 440, 568, 663, 379, 852, 112, 165, -369, + 597, 910, 282, -8, 834, 1281, -352, 572, 695, 462, 2246, 1806, + 345, 190, 1374, 416, 915, 2166, 168, -82, 280, -516, -446, 840, + 47, 533, 44, -362, -711, -1143, 22, 193, 1472, -85, 233, 1813, + -62, 579, 1504, 550, 944, 1749, 723, 650, 1148, 972, 884, 1395, + -425, 643, 0, 1000, 952, 1098, 249, 1446, 672, -334, -87, 2172, + -554, 1882, 2672, 140, 1826, 1853, 920, 1749, 2590, 1076, 1933, 2038, + -137, -443, -1555, 1269, 1174, 468, -493, -122, 1521, -451, 1033, 1214, + 482, 1695, 1118, 815, 649, 384, -446, -692, 107, -319, -605, -118, + -207, -505, 525, -468, -12, 2736, 75, 1934, 1305, 880, 2358, 2267, + 1285, 1575, 2004, -48, -304, -1186, -435, -461, -251, -366, -404, -547, + -289, -605, -597, -538, -810, -165, -120, 3, 356, 639, 1241, 1502, + 96, 177, 750, -435, -585, -1174, -356, 109, -79, -485, 288, 2005, + 9, 1116, 731, 880, 2134, 946, -265, 1585, 1065, 1157, 1210, 843, + -498, -668, 431, 374, 321, -229, 1440, 2101, 1381, 449, 461, 1155, + -105, 39, -384, -263, 367, 182, -371, -660, 773, -188, 1151, 971, + 1333, 1632, 1435, 774, 1267, 1221, -482, -832, -1489, -237, -210, 860, + 890, 1615, 1064, 472, 1062, 1192, 185, 1077, 989, -568, -992, -1704, + -449, -902, -2043, -142, -377, -458, -210, -554, -1029, -11, 1133, 2265, + -329, -675, -893, -250, 657, 1187, 519, 1510, 1779, 520, 539, 1403, + 527, 1421, 1302, -563, -871, -1248, -147, -463, 879, -76, 2334, 2840, + 563, 2573, 2385, 632, 1926, 2920, 719, 2023, 1840, -545, -723, 1108, + 129, -125, 884, 1417, 1632, 925, -94, 1566, 1751, -341, 1533, 1551, + 591, 395, -274, -76, 981, 2831, 153, 2985, 1844, 1032, 2565, 2749, + 1508, 2832, 1879, 791, 1199, 538, -190, -453, 1489, -278, -548, 1158, + -245, 1941, 2044, 1024, 1560, 1650, 512, 253, 466, -62, -323, 1151, + -473, -376, 507, -433, 1380, 2162, 899, 1943, 1445, 134, 704, 440, + 460, 525, -28, -450, 279, 1338, 0, 971, 252, -445, -627, -991, + -348, -602, -1424, 398, 712, 1656, -107, 314, -178, 93, 2226, 2238, + 518, 849, 656, -462, -711, -447, 174, -34, 1191, -119, 42, 1005, + -372, 274, 758, 1036, 2352, 1838, 675, 1724, 1498, 430, 1286, 2133, + -129, -439, 0, -373, 800, 2144, 6, 1587, 2478, 478, 596, 2128, + -428, -736, 1505, 385, 178, 980, 139, 449, 1225, -526, -842, -982, + 145, 1554, 1242, 623, 1448, 656, 349, 1016, 1482, 31, -280, 415, + -316, 724, 1641, 360, 1058, 556, -436, -358, 1201, -355, 1123, 1939, + 401, 1584, 2248, -527, -1012, 355, 233, 238, 2233, -550, -897, -639, + -365, -501, 1957, 389, 1860, 1621, 162, 1132, 1264, -237, 1174, 1390, + -640, -411, 116, -228, 1694, 2298, 1639, 2186, 2267, 562, 1273, 2658, + 323, 338, 1774, 578, 1107, 852, 22, 594, 934, -143, 718, 446, +} + +var dico2_lsf_3 = []int16{ + 50, 71, -9, -338, -698, -1407, 102, -138, -820, -310, -469, -1147, + 414, 67, -267, 1060, 814, 1441, 1548, 1360, 1272, 1754, 1895, 1661, + 2019, 2133, 1820, 1808, 2318, 1845, 644, -93, 454, 858, 329, -136, + 489, -258, -128, -198, -745, -41, -52, -265, -985, 346, 137, 479, + -1741, -748, -684, -1163, -1725, -367, -895, -1145, -784, -488, -946, -968, + -85, -390, -725, 215, -340, -171, 1020, 916, 1969, 564, 179, 746, + 662, 977, 1734, 887, 622, 914, 939, 856, 1165, 309, 688, 803, + 917, 161, 570, 118, -20, -283, -816, -42, 204, -1228, -325, -462, + -963, -202, -143, -988, -484, -361, -702, -978, -477, -302, -790, -1188, + -100, -786, -1088, -1054, -947, -1684, -202, -843, -782, -1039, -1378, -901, + -624, -110, -85, 356, 213, -10, -493, 364, 774, 425, 822, 479, + -83, 557, 520, -992, -1560, -572, -603, -741, -26, -502, -638, -903, + 209, 306, 147, -316, -593, -596, -85, -211, -225, -918, -529, 117, + 233, -439, -738, 1101, 751, 633, 1457, 1716, 1511, 1765, 1457, 910, + 1122, 1156, 849, 1354, 868, 470, -871, -1150, -1796, -871, -861, -992, + -118, 155, 212, -1051, -849, -606, -1117, -1849, -2750, -1019, -1427, -1869, + 370, -184, -414, 959, 493, 104, 958, 1039, 543, 154, 653, 201, + 1249, 507, 150, 663, 503, 230, 623, 777, 675, 659, 88, -110, + 843, 244, 224, 382, 541, 302, 724, 433, 666, 1166, 734, 341, + -138, 20, -397, -1183, -424, -46, -321, -352, -124, 1333, 1021, 1080, + 262, 366, 723, 922, 283, -551, 31, -636, -611, -689, -697, -415, + -952, -779, -201, -1329, -598, -359, -953, -1285, 166, 493, 305, 221, + 846, 703, 610, 840, 936, 774, -723, -1324, -1261, -357, -1025, -1388, + -1096, -1376, -365, -1416, -1881, -608, -1798, -1727, -674, -545, -1173, -703, + 678, 786, 148, -123, 696, 1288, 644, 350, -10, 414, 614, 15, + 137, 344, -211, -814, -1512, -819, -391, -930, -588, 47, -591, -898, + -909, -1097, -163, -1272, -1167, -157, -1464, -1525, -389, -1274, -1188, -624, + 671, 213, 454, 124, -274, -525, -729, -496, -152, -1344, 122, 135, + -2905, -589, -394, -1728, 441, -50, 1476, 904, 787, 316, 236, -440, + -347, 217, 413, -911, -917, 121, -455, -932, 202, -92, -465, -375, + 488, 390, 474, 876, 729, 316, -1815, -1312, -669, 87, 962, 432, + 563, -249, -1058, 250, 285, 1105, 1141, 427, 696, -1038, -1664, -1582, + -948, 346, 160, -309, -272, -858, 670, 624, 1250, -944, -408, -666, + -606, -320, -384, -492, 230, 65, 334, -50, -16, -16, -690, -1397, + 1791, 1716, 1399, 2478, 2063, 1404, 1245, 1471, 1426, -382, -1037, -2, + 173, -398, 1145, 1491, 2024, 1801, 772, 1274, 1506, 1429, 1735, 2001, + 1079, 1218, 1273, -1154, -1851, -1329, -808, -1133, -1096, -451, -1033, -1722, + 65, 578, -84, -1476, -2434, -1778, -765, -1366, -494, -218, -594, -931, + 337, -236, 562, 2357, 2662, 1938, 1489, 1276, 874, 189, 358, 374, + -1519, -2281, -2346, -967, -1271, -2095, -628, -1188, -1542, 1661, 1043, 546, + 565, 1061, 732, -64, -836, -434, -436, -96, 203, 1078, 1216, 1636, + 907, 1534, 986, 326, 965, 845, 142, -84, 197, 470, 2379, 1570, + 1133, 470, 1214, 395, 1376, 1200, 1125, 1042, 348, -543, -1234, -376, + -215, -181, 481, -1947, -1621, -210, -750, -1185, 390, 29, -399, 27, + 820, 1236, 755, 695, 979, 409, -174, 1197, 1035, 912, 1356, 1846, + -992, -1437, 484, -1485, -1700, 208, -412, 1204, 1432, -271, 896, 1144, + -416, 1777, 1434, -1696, -2644, -204, -1789, -1551, 1033, -1656, -1559, 1303, + -1253, -1589, 1081, -669, -1095, -66, -682, 320, -345, 659, 305, 1069, + -1292, -804, -19, -1635, -1291, 29, -1683, -497, 71, -287, -7, -100, + -494, -962, -237, 852, 1881, 1740, -1217, -1387, 227, -660, 302, 373, + 96, 1087, 1257, -1074, -1669, 160, 485, 2076, 1798, -934, -220, 552, + -596, -612, 237, 336, 1720, 879, 643, 629, 434, 1267, 522, 1633, + 15, 244, -441, 1475, 717, 184, 1819, 1590, 1709, 988, 261, 937, + 2093, 2345, 1520, 2139, 1858, 1606, -577, -579, -1203, -956, 135, -488, + -464, 51, -338, -629, -348, -723, 1146, 2073, 1442, 2192, 1466, 911, + -1444, -1572, -2278, 1400, 710, 1297, 1335, 633, 928, 1434, 2194, 2594, + 2422, 2204, 1881, 982, 2242, 1854, 380, 792, 1145, -63, -539, 414, + -252, -964, -314, -1261, -683, -780, -831, -526, -1005, -1666, -1135, -424, + -1611, -452, -299, 1268, 1048, 642, 1147, 853, 856, -675, -336, 139, + 2268, 1343, 1418, 29, 768, 797, -1224, 423, 564, -1318, -1082, 245, + -1302, -812, 573, -1298, -1617, 646, -968, 834, 723, 993, 1652, 2027, + -191, -817, 432, 662, 60, 198, 626, 997, 1330, 1648, 1963, 1289, + -1597, -93, -45, -1088, 37, -84, 1653, 2607, 2337, 1065, 2040, 2377, + 1139, 2326, 2118, 859, 357, 1510, 664, 1227, 1099, 479, 1360, 912, + 1897, 1754, 2019, 1168, 1909, 1784, 399, 34, 256, -593, -304, -1053, + 547, 1694, 1407, 647, -99, -341, 1492, 1647, 1190, 38, -644, -212, + 395, 846, 222, -704, -765, -716, -724, -1964, -2804, -150, 291, -82, + 1233, 1459, 1007, -140, -155, 153, 439, 297, 1568, -1529, -410, -636, + 1536, 455, -237, -1328, -139, -260, 531, 554, 868, 269, 1264, 606, + -233, 883, 463, 742, 600, -120, -73, 421, 212, -439, -58, 804, + -1286, -1241, 728, 294, -490, 50, -591, -905, -1254, 42, -687, 147, + -25, 273, 596, -311, 1213, 601, -754, 849, 584, 429, 607, 587, + -602, -166, 461, -796, -823, 777, 1380, 910, 1755, 119, 1417, 972, + -219, -880, -1596, -1049, -1010, 438, -713, -1379, 78, 0, -447, -1179, + -1136, -1319, -1573, 2248, 1767, 1309, 946, 1583, 1432, 1150, 482, 436, + -469, -1108, 618, -447, -966, 1088, -1252, -1515, -114, -1104, -2008, -579, + 210, 613, 497, -1975, -1437, 642, -1269, -856, 1011, -1646, -1185, 1063, + -1555, -672, 1204, -1692, -1114, 623, -979, -1326, -1277, 539, -147, 894, + -1354, -897, -434, 888, 475, 428, 153, -384, 338, -1492, -511, 359, + -974, -1115, -470, 105, -550, 677, -937, -1145, 877, 380, -260, 210, + 1685, 924, 1256, 1775, 1190, 1095, 1419, 631, 533, 627, 299, -347, + -411, -534, 647, -650, 29, -595, -378, -1367, 1563, 1402, 1121, 1465, + 1089, 1410, 648, -2096, -1090, -6, 311, -194, -869, -639, -831, 416, + -1162, -1224, 1349, -1247, -941, 1813, -2193, -1987, 453, -619, -1367, -956, + -1606, -1972, -1507, -1175, -1057, -1104, -377, 601, 201, 1876, 825, 374, + -430, -1323, 29, -1397, -1249, -1331, -1007, -1504, 960, -1401, -2009, 197, + -1379, -1949, -236, -1077, 123, 422, 615, 1269, 546, -306, 1526, 904, + 1194, 1788, 1177, -626, -884, -1526, 199, 766, 1504, -1065, 862, 197, + -1034, -1773, -887, -800, 145, 599, -1134, -519, 626, -1205, -1926, 500, + -910, -1041, -1395, -1476, -1567, -969, -523, 842, 34, 1794, 646, 862, + -1207, -1888, -1002, -78, -9, -672, 1044, 759, 80, -600, 1139, 1019, + 57, 2000, 1422, -833, 1414, 1121, -1202, 1630, 1260, -461, 1420, 1244, + 1537, 975, 253, -283, 324, -359, 599, -195, 106, 588, 62, -587, + -757, 645, 205, 51, 1201, 758, -1209, 673, -390, -624, 1581, 941, + -151, 1023, 735, 2820, 1301, 690, -302, 524, -99, -900, -1588, -1189, + 1084, 251, 238, 2014, 1792, 1010, 1245, 1633, 1741, -1227, -1540, -1208, + -621, 456, -109, 40, -65, 788, -805, -699, -1350, -583, 904, 832, + -801, 532, 594, 1972, 1408, 1351, -1177, -1880, -2114, -773, 568, 948, + -1015, 1079, 1260, -1111, 482, -130, 1778, 1044, 780, -1491, 245, 912, + -316, -1141, -917, -536, -1442, -2346, -785, -1546, -1988, -2003, 257, 909, + -1849, -633, -1209, -1538, -1918, -1054, 1606, 2239, 1576, -567, -1500, -1544, + -1279, 195, 1369, -817, 293, 1219, -525, 630, 1197, -1698, -2425, -1840, + -303, 731, 747, -1169, -251, 269, -950, -75, 1684, -1182, -453, 1005, + -1599, 585, 378, -2075, -571, -427, -529, -1159, -1171, -283, -205, -564, + -796, 1246, 717, 2277, 927, 539, -454, 559, 440, -717, 1460, 1615, + -1030, 1052, 1610, -1169, -138, 847, 226, 39, -612, -1251, -106, -729, + -651, 968, 1302, -714, -636, 1727, 353, 1069, 410, -798, -156, 1099, + -574, 918, 446, -1310, 1012, 466, 1408, 1591, 765, 1429, 1380, 1757, + 1949, 1956, 2378, 1578, 2047, 2148, 916, 98, -7, 1893, 1418, 2141, + 348, 1405, 1579, 152, 1134, 1801, -267, 154, 1395, -1166, 469, 1054, + -1142, -405, -1073, -1341, -2264, -1581, -364, 869, 1706, -1162, 549, 1550, + -1225, -1932, -1666, -1485, -1977, -2055, -1727, -906, -98, -1897, 233, 1492, + 892, 108, -331, -1728, -1170, -1700, -1060, 1980, 1790, -1070, -1741, -1909, + -11, 1539, 1317, -1600, 94, 497, 421, 443, -197, -1578, -349, -994, + -599, -539, 1140, -965, -1419, -129, -1341, 175, -447, -375, 1311, 2055, + -371, -650, -307, -1073, 605, 365, -2057, -113, 430, 652, 914, 967, + -1012, -1586, -2323, 1505, 1248, 559, 262, -486, -401, -1727, 1342, 1546, + 50, 56, 432, -330, 119, -604, -1517, -1080, -810, 946, 1127, 1055, + -1400, -1703, -1712, -1270, -704, -1317, 807, 1821, 1143, 2760, 1606, 2171, + 1120, 409, -150, -147, 404, 959, 2439, 1911, 2189, -906, -141, -866, + -904, -142, -458, -557, -708, -1679, -830, -1431, -1583, -1842, -1346, -1086, + -1604, -272, 915, -1196, 772, 1056, -638, -1234, -1897, -500, -81, -822, + -1289, -1613, -735, -117, 785, 168, -1090, 1133, 922, -1096, -746, 1384, + 287, -547, -1063, -1376, -2201, -1204, -2176, -1570, -1757, -1511, -2241, -771, + -1737, 1099, 830, -1588, 724, 1243, -1542, 693, 805, -1690, -240, 1665, + -1700, -4, -668, 2149, 816, 1042, -818, -1841, 22, -764, -507, 449, + -1151, -617, 289, -843, -1596, -240, 498, -234, -657, -752, 480, 1678, + -319, -481, 193, -811, 171, -119, -2128, -202, -848, 1717, 1140, 1700, +} + +var dico3_lsf_3 = []int16{ + 67, -17, 66, -12, -1690, -581, -104, -272, -1076, -1186, -1845, -376, + -1140, -926, -420, -58, -259, -656, -1134, -553, 1788, 1227, 455, 129, + 462, 441, -240, -528, 840, 514, 130, -75, 1114, 623, 153, 216, + 1068, 564, -6, -276, 1119, 727, 190, -68, 704, 306, 119, -264, + 329, 61, -100, 156, 364, 123, 183, -208, -171, -123, 220, -65, + -306, -62, 402, 17, -660, -938, -266, 0, 385, 235, 276, 285, + 320, 268, -336, -200, -724, 17, -84, 381, -544, 429, 494, 519, + -117, 288, 304, 329, 643, 157, 701, 508, 1200, 625, 796, 608, + 998, 421, 492, 632, 1204, 780, 446, 132, 1257, 844, 547, 449, + 829, 658, 541, 470, 1132, 1258, 918, 639, 547, 51, 423, 279, + 9, 392, 83, 94, 542, 543, 229, -147, -198, 129, 194, -185, + -863, -1321, -302, 30, -597, -629, -19, 114, -900, -1081, 466, 353, + -1483, -1573, 15, -143, -1708, -2059, -751, 196, -1876, -2067, -642, -258, + -2335, -1470, -450, -564, -584, -186, -872, -414, -1805, -988, -1125, -1310, + -726, -1129, 28, 169, -1039, -864, -718, -246, 484, 36, -233, -49, + 265, 67, 289, 467, 178, 543, 810, 540, 84, 282, 672, 703, + -975, -777, 129, 287, -938, -227, 955, 595, -1617, -289, 836, 649, + -1847, -215, 1106, 718, -2034, -1085, 650, 440, -2101, -529, 907, 575, + -2011, -336, 670, 204, -2389, -692, 360, 137, -2156, -2204, -9, 280, + -266, 119, 39, 193, 78, -59, -120, 226, -975, -858, -781, -1095, + -619, -413, -451, -842, -1216, -1321, -813, -883, -1376, -1615, -394, -428, + -737, -1113, -549, -790, -880, -975, -967, -642, -985, -886, -1273, -1361, + -473, -804, -1401, -1407, 160, -265, -919, -275, -248, -250, -718, -380, + 97, -103, -375, -229, -415, -193, -135, -555, 628, 361, 119, 216, + 579, 364, 391, 209, 634, 522, -154, -148, 526, 389, 170, 33, + 105, 267, 64, 380, -1503, -1000, -30, -369, -1070, 58, 647, 223, + -1520, -291, 621, 307, -1531, 156, 762, 404, -2029, 141, 734, 499, + -1849, -650, 306, 512, -187, -104, -59, 438, 134, -230, 156, -186, + -61, -260, -16, 10, -569, -3, -421, -297, -1725, -521, -346, 178, + -1362, -59, -44, 157, -2146, -461, -470, -349, -2170, -1, -369, -121, + -1579, -373, -900, -1015, -1117, -591, -613, -784, -561, 122, -75, -449, + -4, -171, -123, -372, 192, 168, -76, -132, 252, -107, 340, 210, + 392, 509, 272, 181, -109, 145, 218, 119, -416, -263, 485, 265, + -181, -8, -286, 226, -244, -218, 69, -290, -158, 191, -1, -64, + -592, -90, 213, -96, 255, 435, 178, -80, -369, -18, -33, -80, + -42, 415, 140, -222, 1143, 651, 649, 329, 767, 556, 249, 235, + 948, 413, 442, 279, 141, 339, 356, 557, -470, -170, 99, 237, + -569, -800, 352, 565, 282, 473, 470, 332, -199, -690, -1284, -917, + -193, -426, -800, -1122, -26, -371, -490, -193, 637, 595, 519, 330, + 408, -115, 79, 12, 477, 87, -103, -376, -666, -347, -277, -291, + -510, -481, 169, 297, -829, -738, -205, -171, -320, -540, 328, 283, + -859, -958, 442, -2, 556, 686, 130, 56, 1383, 1012, 755, 427, + 612, 741, 628, 553, -339, -796, 134, 277, -633, -1085, -2, -246, + -880, -1035, -1607, -1064, -994, -474, -1138, -488, -414, -795, 73, -206, + -8, -139, 439, 204, -176, -578, 23, 131, -269, -757, -191, 245, + -109, -338, 112, 316, 120, -406, -118, 611, -180, -186, -645, 115, + -173, 34, -518, -489, -151, 61, -583, -844, 220, -138, -681, -1020, + 391, -17, -598, -321, 157, -295, 129, 155, -926, -875, -987, 285, + 241, -83, -125, -125, 620, 597, 432, 92, 393, 78, 409, 61, + -393, -739, -413, -748, 83, 54, 361, 27, -1084, 130, -337, -694, + -1565, 297, 318, -19, -1873, 36, 51, -317, -2323, -246, 231, -84, + -2306, -783, 40, -179, -2233, -930, -474, -462, -754, -86, -288, -626, + -2411, -455, -63, 171, -1099, -1094, -26, -143, -1193, -455, -406, -381, + -605, -210, -96, -51, -580, -476, -276, -15, -1195, -634, -1203, -881, + -378, -221, -669, -952, 594, 178, -403, -676, 763, 327, 601, 290, + 172, 300, 203, 157, -56, -336, 356, 24, -228, -296, -259, -29, + -186, 263, 416, 14, -353, 373, -12, -216, 257, 96, 174, 57, + -1526, -616, -954, -499, -497, -152, -333, 125, 105, 200, 179, -97, + -331, -224, 765, 697, 760, 256, 301, 59, 455, -85, 204, 288, + -514, 240, 251, -109, 256, 417, -34, -413, 101, 430, 384, 156, + -31, -10, 206, 426, 589, 145, 143, 71, 808, 906, 333, 349, + 986, 938, 589, 331, 1300, 824, 187, 509, 1062, 653, 379, 466, + 1462, 937, 401, 274, 787, 861, 265, 2, 609, 553, 28, 305, + 926, 340, 106, 386, 241, -267, -147, 225, -178, -534, 347, 502, + -643, -381, 397, 30, -651, -733, -435, 398, -407, -726, -484, -248, + -789, -914, -438, -476, -498, -390, 75, -295, -964, -590, -606, 150, + -121, -49, -155, -78, 935, 550, 389, 38, -321, 127, 424, 315, + -285, -113, 283, 259, 658, 203, 322, 486, 903, 505, 748, 417, + 611, 423, 555, 512, 239, -83, -578, -19, -339, -731, 349, 13, + -934, -1399, -114, -360, 107, 692, 182, 90, -1243, -1538, -1551, -725, + -568, -903, -1363, -525, -517, -853, -861, -1004, -168, -690, -835, 63, + -137, -556, -547, 144, -286, -817, 485, 319, -147, -408, 526, 246, + -347, -434, 297, -28, -290, -471, -1110, -1285, -460, -359, -988, -794, + 1347, 1299, 690, 523, 1216, 1068, 1094, 757, 825, 1140, 752, 494, + 1252, 1365, 1195, 898, 521, 1053, 532, 432, -334, -216, -313, -263, + -160, 52, -472, -155, 127, 136, -380, 44, 851, 410, -162, -489, + 123, -255, -796, -667, 1090, 917, 789, 493, 1397, 1197, 558, 202, + -51, -118, -342, -701, 83, 108, -42, -441, 61, 95, 287, 256, + -27, 89, 524, 531, 351, 227, 592, 545, 697, 155, -164, 307, + 638, 274, -489, -50, 754, 240, -166, -124, -116, -579, -1212, -63, + 190, -295, -1040, -1296, 147, -376, -177, -113, 841, 1241, 1051, 668, + 2, 293, 551, 304, -1096, -953, -248, 376, -750, -965, 87, 516, + -275, -516, 689, 391, -379, -643, 876, 594, -390, -1013, -645, 573, + -107, -568, -689, -826, -1025, -27, -328, -203, 861, 749, 548, 233, + -1660, -1043, 451, 108, -660, -620, 430, 236, 21, -396, -1158, -631, + 1372, 1298, 967, 577, 1125, 1125, 589, 454, -323, -865, -467, 153, + -468, -699, -804, -509, -392, -718, -204, -35, -603, -1093, -567, -162, + -505, -1004, -102, 350, 219, 224, 423, 252, 395, 591, 608, 363, + -746, -96, 373, 172, 171, 295, 714, 339, 233, 77, 107, 277, + 157, 153, -499, -356, 1547, 1073, 576, 494, -292, -339, -504, -592, + -903, -72, -619, -481, -1594, -1117, -567, -254, -793, -507, -564, -291, + -492, -532, 502, 560, -382, 427, 600, 230, -227, 477, 251, 75, + 285, 842, 813, 476, -1310, -1333, 186, 377, -587, -917, 643, 381, + -1186, -553, 411, 82, -1127, -820, -174, -540, -604, 119, 543, 205, + -380, 657, 909, 567, 112, -298, -374, 114, -857, -251, 56, 159, + 401, 345, -34, -140, -111, -607, 41, 614, 355, -114, -77, 474, + 578, 56, 1450, 924, 1098, 1420, 741, 400, 246, 22, 588, 313, + -121, 327, 831, 472, -1138, -608, 856, 552, -1241, -1072, 638, 600, + -358, 254, -333, -303, -646, 739, 358, 74, 1226, 1671, 1221, 849, + 2241, 1624, 983, 636, 1841, 1477, 749, 384, 350, 263, 87, 128, + -1902, -941, -144, -64, -1734, -255, 288, -31, -2644, -1238, 366, 235, + -1643, -1092, -1344, -304, -541, -1075, -1116, 123, -1178, -252, -816, -180, + -1016, 533, 565, 233, -487, -430, -188, 334, 867, 1236, 534, 171, + -1590, -1607, 635, 630, -2196, 310, 924, 412, -2358, -328, 956, 529, + -2639, -377, 630, 278, -2602, 317, 799, 299, -2406, 133, 340, 31, + -2156, -1468, 131, 125, -1184, -490, -139, 46, -744, 447, 891, 564, + 67, -451, 646, 604, -553, -429, -876, 396, 162, -66, 1305, 915, + 479, 579, 1088, 794, 450, 278, 566, 324, -1057, -154, 148, -177, + -2545, 168, 1070, 592, -2351, -42, 819, 345, -2344, -707, 721, 250, + -2175, -1497, -309, 122, -78, -73, 120, 173, -4, 262, -263, -261, + -431, -64, -405, -732, -2609, 116, -83, -193, -1525, -944, -477, -725, + -508, 307, 170, 172, 832, 417, 832, 686, -225, 177, 894, 818, + -482, -389, 1279, 1039, -383, 201, -350, 40, 730, 635, 226, 526, + 503, 462, 338, 398, 535, 714, 40, -282, 1482, 1471, 1085, 731, + 1561, 1072, 909, 693, 1419, 1282, 889, 879, 1153, 728, 1186, 840, + -226, 1130, 949, 689, -494, -986, -1556, -128, -568, -721, -713, -26, + 317, 524, 70, 135, -405, -865, -1766, -652, -174, -801, 885, 773, + -153, -91, 1099, 751, -506, -1149, 853, 646, 241, 782, 519, 539, + 1853, 1700, 1101, 684, -1249, -1486, -464, 188, -893, -1409, -1312, -341, + -135, 438, -175, 18, 1111, 976, 319, 208, -1430, -1768, 83, 458, + -530, -1000, 307, 129, -840, -15, -29, -356, -911, -924, -1147, -242, + -119, -528, 127, -133, -761, -765, 190, -83, -315, 895, 522, 231, + -222, 102, -63, -428, 316, 699, 379, 70, 25, 716, 314, -108, + 507, 874, 566, 238, 108, 941, 519, 195, 425, -60, -427, 257, + 139, -103, -630, 446, 334, 370, 412, 48, -172, -690, -283, 557, + 187, -286, 158, 483, 140, 270, -344, -631, 924, 579, -116, 132, + 142, 466, -68, -64, 230, -145, -302, -542, -803, -912, 1018, 737, + -773, 1015, 630, 297, -2596, 95, 445, 336, -2122, 491, 510, 191, + -1253, 161, -2, -324, -1450, -633, -712, -105, -842, -254, -411, 100, + -640, -290, 1010, 763, -650, 313, 1169, 730, 140, 505, 1030, 766, + 772, 287, 1067, 823, 495, 749, 305, 323, -164, 462, 78, 399, + -342, -874, 69, 597, -16, 620, 621, 337, -138, -444, -265, 218, + 84, -450, 953, 666, -222, -803, 541, 604, -921, -1376, 244, 116, + -841, -723, 630, 588, 140, 663, 294, 368, 935, 1046, 881, 759, + 1746, 1464, 916, 628, 436, 963, 281, 1, -119, 74, 542, 213, + 1, -567, 301, 241, 260, 435, 222, 396, 936, 957, 1108, 703, + 510, 506, 808, 478, 601, 694, 960, 620, 972, 741, 980, 600, + 834, 717, 767, 684, 643, 972, 935, 638, 501, 661, 720, 851, + -105, -632, -303, -117, -429, 130, 789, 442, -522, -188, 704, 373, + -759, 42, 814, 523, -531, -1137, 373, 578, -682, -1203, -455, 285, + -1163, -1577, -1098, 44, 81, -82, 712, 363, 477, 246, 954, 622, + 1604, 1622, 1277, 891, 1409, 859, 924, 892, 774, 1041, 947, 1142, + 40, -546, -75, 288, -616, -106, -697, -26, -169, -160, -891, -739, + -279, -384, -1029, -350, 1781, 1308, 1046, 816, 1580, 1533, 1472, 1178, + 1505, 1076, 1216, 899, 890, 904, 564, 654, 920, 692, 1021, 856, + -493, 132, 177, 505, 71, 195, -28, 97, 456, 351, -164, 88, + 439, 278, -40, 350, 1395, 949, 234, -95, -805, -472, 38, -163, + 367, -98, 489, 523, 1025, 1178, 1212, 906, 319, 1314, 814, 461, + -123, -543, -804, 447, -748, -324, -897, -1127, -737, -501, -789, -713, + 715, 777, 1239, 922, 1949, 1939, 1368, 865, 730, 880, 758, 388, + -871, 454, 17, -251, -381, -810, -1583, 239, -521, -966, -792, 259, + -890, -1358, -770, -73, 166, 349, -212, 323, -840, -301, 473, 435, + -679, -464, 728, 351, -156, -199, 667, 432, 29, -252, 415, 480, + -731, -379, 145, 559, -528, -631, -1158, -159, 445, 273, 123, 639, + 373, -126, 800, 568, 84, -162, 720, 712, -830, -536, -185, 222, + 408, 452, 501, 771, -897, -1355, -67, 442, -792, -1406, 566, 602, + 167, -326, 509, 330, -95, -626, -730, -344, 1668, 1217, 779, 455, + 1316, 828, 584, 719, 404, -31, 1013, 789, 89, 107, 891, 549, + 871, 1581, 917, 671, 866, 1479, 1289, 854, 391, 1068, 1122, 812, + 78, -562, 345, 563, 429, -103, 417, 787, -122, -437, 411, 788, + -913, -417, 602, 754, -226, -16, 151, 760, -700, 118, -104, -14, + -1128, 48, 284, 393, -390, -419, -639, -116, -910, 306, 316, -13, + 1207, 984, 821, 669, -1195, -693, 140, -213, -884, -416, -199, -558, + -616, 245, -404, -664, 262, 56, -617, -724, -85, -491, -320, -656, + -570, -831, -129, -528, -1506, -63, -367, -385, -358, -321, 4, 51, + -366, -214, 319, 511, 146, 671, -17, -291, -110, 464, -139, -496, + -202, 220, -312, -631, -660, -73, -655, -820, -662, -653, -1288, -857, + -430, -953, -959, -264, -49, -468, -72, -381, -350, -563, -193, -407, + 55, -408, -803, 11, -309, 649, 188, -198, -512, 461, -79, -458, + -1318, -263, -134, -523, -1657, -435, -495, -765, 57, -347, -414, 434, + -1141, -242, -664, -857, 34, -68, -707, -338, +} + +var mr515_3_lsf = []int16{ + 419, 163, -30, -262, -455, -789, -1430, -721, 1006, 664, 269, 25, + 619, 260, 183, 96, -968, -1358, -388, 135, -693, 835, 456, 154, + 1105, 703, 569, 363, 1625, 1326, 985, 748, -220, 219, 76, -208, + -1455, -1662, 49, 149, -964, -172, -752, -336, 625, 209, -250, -66, + -1017, -838, -2, 317, -2168, -1485, -138, 123, -1876, -2099, -521, 85, + -967, -366, -695, -881, -921, -1011, -763, -949, -124, -256, -352, -660, + 178, 463, 354, 304, -1744, -591, -282, 79, -2249, 175, 867, 499, + -138, -180, -181, -21, -2291, -1241, -460, -520, -771, 451, -10, -308, + 271, -65, 4, 214, -279, -435, -43, -348, -670, 35, -65, -211, + 806, 535, 85, 297, 57, 239, 722, 493, 225, 661, 840, 547, + -540, -376, 14, 349, 469, 721, 331, 162, -544, -752, -62, -10, + 398, -88, 724, 701, -19, -533, -94, 601, 136, -71, -681, -747, + -166, -344, 261, -50, 161, -52, 485, 337, -1675, 50, 190, -93, + -2282, -231, -194, -82, -95, -595, -154, 128, 894, 501, 588, 457, + -345, 206, 122, 110, -631, -227, -569, 3, 408, 239, 397, 226, + -197, -2, 128, 491, 1281, 904, 292, 215, 538, 306, 259, 509, + -677, -1047, 13, 321, -679, -588, -358, -212, -558, 243, 646, 479, + 486, 342, 634, 532, 107, 802, 331, 136, -112, -398, -1031, -286, + -326, -705, 288, 272, 1299, 1144, 1178, 860, -423, 121, -385, -148, + -295, -302, -834, -819, 16, -24, -201, -476, 555, 91, -245, 294, + -38, -379, -962, -1221, -1191, -1518, -273, -395, -390, -1013, -645, 573, + -1843, -1030, 505, 468, 744, 947, 609, 493, -689, -1172, -628, -135, + -1026, 195, 411, 196, 1582, 1147, 575, 337, -1239, -777, -648, -142, + 595, 825, 967, 735, -1206, -970, -81, -342, -745, 13, -72, 375, + 454, 19, 1407, 921, -1647, -172, 861, 562, 928, 1537, 1063, 740, + -2472, -952, 264, 82, -502, -965, -1334, 123, 867, 1236, 534, 171, + -2320, -460, 780, 363, -1190, -617, 252, -61, -174, 34, 1011, 788, + -2333, 247, 423, 153, -16, -355, 262, 449, -1576, -1073, -544, -371, + -615, -305, 1051, 805, 687, 528, 6, -182, 935, 875, 1002, 809, + 199, 257, 126, 76, -584, -1138, 599, 556, -1105, -1391, -1591, -519, + -977, -1325, 108, 347, -722, -975, 365, 101, -145, 681, 249, -153, + 0, -334, -570, 159, 412, 285, -336, -617, -953, -966, 887, 689, + -1251, 84, -185, -398, -592, 433, 1044, 653, 85, 329, -40, 361, + -433, -705, 466, 574, -154, 654, 592, 290, -167, 72, 349, 175, + 674, 297, 977, 720, 1235, 1204, 757, 488, -400, -269, 538, 372, + -1350, -1387, -1194, -91, 1262, 876, 775, 700, -599, -38, -430, -722, + 1976, 1630, 991, 608, 111, 276, -226, -96, -947, -388, -11, -7, + -303, -531, -839, 338, 1734, 1710, 1405, 1013, -516, -855, -645, 210, + -688, -416, 513, 230, -822, -637, -1146, -320, -952, -658, -694, 183, + -114, -623, 818, 674, -191, -204, 731, 635, 51, 1221, 883, 576, + -954, -431, 826, 598, -342, -755, -900, -407, -1126, -354, -206, -512, + -547, -810, -357, -620, 66, 515, -73, -410, -872, -945, -1444, -1227, + 191, -17, -544, -231, -1540, -544, -901, -886, +} + +var mr795_1_lsf = []int16{ + -890, -1550, -2541, -819, -970, 175, -826, -1234, -762, -599, -22, 634, + -811, -987, -902, -323, 203, 26, -383, -235, -781, -399, 1262, 906, + -932, -1399, -1380, -624, 93, 87, -414, -539, -691, 37, 633, 510, + -387, -476, -1330, 399, 66, 263, -407, -49, -335, -417, 1041, 1865, + -779, -1089, -1440, -746, -858, 832, -581, -759, -371, -673, -506, 2088, + -560, -634, -1179, 271, 241, 14, -438, -244, -397, 463, 1202, 1047, + -606, -797, -1438, -51, -323, 481, -224, -584, -527, 494, 881, 682, + -433, -306, -1002, 554, 659, 222, 171, -160, -353, 681, 1798, 1565, + -852, -1181, -1695, -336, -666, 114, -581, -756, -744, -195, 375, 497, + -465, -804, -1098, 154, 282, -131, -50, -191, -719, 323, 732, 1542, + -722, -819, -1404, 105, -250, 185, -178, -502, -742, 321, 510, 1111, + -323, -567, -966, 127, 484, 338, -160, 52, -338, 732, 1367, 1554, + -626, -802, -1696, -286, -586, 676, -695, -343, -370, -490, 295, 1893, + -630, -574, -1014, -80, 645, -69, -6, -318, -364, 782, 1450, 1038, + -313, -733, -1395, 120, 60, 477, -264, -585, -123, 711, 1245, 633, + -91, -355, -1016, 771, 758, 261, 253, 81, -474, 930, 2215, 1720, + -808, -1099, -1925, -560, -782, 169, -804, -1074, -188, -626, -55, 1405, + -694, -716, -1194, -660, 354, 329, -514, -55, -543, 366, 1033, 1182, + -658, -959, -1357, -55, -184, 93, -605, -286, -662, 404, 449, 827, + -286, -350, -1263, 628, 306, 227, -16, 147, -623, 186, 923, 2146, + -674, -890, -1606, -443, -228, 339, -369, -790, -409, 231, 86, 1469, + -448, -581, -1061, 594, 450, -177, -124, -170, -447, 671, 1159, 1404, + -476, -667, -1511, -77, -138, 716, -177, -372, -381, 451, 934, 915, + -250, -432, -822, 272, 828, 446, 26, 19, -31, 698, 1692, 2168, + -646, -977, -1924, -179, -473, 268, -379, -745, -691, 11, 127, 1033, + -488, -917, -825, 61, 323, 135, 147, -145, -686, 685, 786, 1682, + -506, -848, -1297, 35, 90, 222, -23, -346, -670, 455, 591, 1287, + -203, -593, -1086, 652, 352, 437, 39, 63, -457, 841, 1265, 2105, + -520, -882, -1584, -328, -711, 1421, -596, -342, -70, 209, 173, 1928, + -423, -598, -921, 421, 605, -38, -2, -245, -127, 896, 1969, 1135, + -379, -518, -1579, 173, 118, 753, -55, -381, -52, 985, 1021, 753, + -2, -291, -891, 753, 992, 423, 264, 131, -196, 895, 2274, 2543, + -635, -1088, -2499, -529, -982, 526, -764, -830, -548, -436, 316, 599, + -675, -940, -746, -57, 236, -11, -201, -81, -798, 16, 845, 1558, + -737, -985, -1212, -468, 17, 290, -279, -584, -700, 183, 822, 705, + -265, -492, -1187, 421, 152, 468, -390, 166, -268, 39, 1550, 1868, + -635, -966, -1571, -453, -492, 910, -284, -1027, -75, -181, -133, 1852, + -445, -624, -1174, 420, 367, -49, -389, -212, -169, 707, 1073, 1208, + -539, -710, -1449, 83, -163, 484, -236, -543, -355, 338, 1175, 814, + -246, -309, -958, 606, 760, 60, 166, -8, -163, -306, 1849, 2563, + -747, -1025, -1783, -419, -446, 209, -718, -566, -534, -506, 693, 857, + -463, -697, -1082, 325, 431, -206, -15, -8, -763, 545, 919, 1518, + -611, -783, -1313, 256, -55, 208, -165, -348, -662, 321, 680, 930, + -326, -429, -951, 484, 446, 570, -197, 72, -73, 909, 1455, 1741, + -563, -737, -1974, -124, -416, 718, -478, -404, -314, -16, 446, 1636, + -551, -537, -750, -58, 638, 214, 55, -185, -271, 1148, 1301, 1212, + -483, -671, -1264, 117, 285, 543, -204, -391, -111, 513, 1538, 854, + -114, -190, -978, 877, 595, 464, 260, 260, -311, 748, 2283, 2216, + -517, -945, -2171, -326, -708, 378, -812, -691, -232, -560, 687, 1409, + -732, -690, -836, -359, 645, 386, -265, 62, -678, 145, 1644, 1208, + -555, -988, -1233, -78, 14, 114, -327, -358, -489, 392, 677, 697, + -201, -236, -1140, 693, 449, 178, -243, 256, -433, 611, 1385, 2456, + -612, -901, -1464, -307, -17, 499, -315, -667, -254, 256, 428, 1463, + -486, -422, -1056, 655, 370, 18, -102, -185, -276, 755, 1578, 1335, + -488, -603, -1418, 182, -93, 870, -73, -458, -348, 835, 862, 957, + -282, -333, -746, 547, 839, 428, 273, -89, 13, 940, 1708, 2576, + -418, -1084, -1758, -44, -358, 259, -497, -643, -560, 99, 557, 961, + -421, -766, -917, 295, 326, 184, 175, 15, -626, 532, 878, 1981, + -443, -768, -1275, 221, 156, 268, 39, -363, -505, 695, 772, 1140, + -162, -459, -912, 709, 444, 658, 25, 303, -312, 1268, 1410, 1715, + -297, -766, -1836, -263, -108, 1070, -406, -13, -129, 57, 438, 2734, + -374, -487, -835, 304, 696, 164, 104, -235, 5, 1611, 1900, 1399, + -229, -582, -1325, 405, 192, 817, -87, -438, 111, 1028, 1199, 993, + 68, -175, -934, 1033, 1117, 451, 478, 200, -248, 2127, 2696, 2042, + -835, -1323, -2131, -799, -692, 466, -812, -1032, -469, -622, 288, 920, + -701, -841, -1070, -411, 512, 8, -390, -91, -744, -30, 1043, 1161, + -822, -1148, -1156, -294, -46, 110, -411, -374, -678, 214, 531, 668, + -406, -420, -1194, 487, 232, 303, -318, 91, -472, 123, 1232, 2445, + -722, -952, -1495, -738, -675, 1332, -543, -606, -211, -95, -98, 1508, + -549, -514, -1193, 473, 211, 73, -288, -112, -389, 537, 1332, 1258, + -567, -755, -1545, 71, -283, 632, -170, -481, -493, 681, 1002, 817, + -356, -331, -877, 419, 706, 346, 241, -34, -326, 377, 1950, 1883, + -727, -1075, -1625, -233, -543, 116, -524, -806, -585, -73, 478, 729, + -288, -925, -1143, 173, 447, -52, 68, -229, -606, 449, 529, 1797, + -591, -875, -1363, 183, -144, 324, -103, -452, -666, 623, 488, 1176, + -238, -511, -1004, 326, 552, 458, 136, 108, -319, 626, 1343, 1883, + -490, -646, -1730, -186, -449, 984, -738, -76, -170, -550, 755, 2560, + -496, -510, -947, 210, 694, -52, 84, -322, -199, 1090, 1625, 1224, + -376, -603, -1396, 343, 74, 632, -175, -502, -32, 972, 1332, 734, + 52, -295, -1113, 1065, 918, 160, 393, 107, -397, 1214, 2649, 1741, + -632, -1201, -1891, -719, -277, 353, -651, -880, -122, -211, 209, 1338, + -562, -714, -1059, -208, 388, 159, -320, -61, -551, 293, 1092, 1443, + -648, -865, -1253, -49, -143, 305, -401, -227, -585, 561, 532, 927, + -117, -443, -1188, 507, 436, 292, -79, 233, -458, 671, 1025, 2396, + -633, -842, -1525, -308, -286, 640, -373, -621, -407, 418, 253, 1305, + -315, -581, -1137, 572, 685, -281, 61, -68, -371, 991, 1101, 1498, + -493, -683, -1362, -47, 164, 704, -256, -314, -268, 631, 949, 1052, + -118, -348, -833, 68, 1180, 568, 152, 117, 34, 1113, 1902, 2239, + -601, -959, -1706, -143, -489, 480, -332, -655, -574, 54, 353, 1192, + -462, -652, -796, 150, 549, 112, 195, -111, -515, 679, 1108, 1647, + -558, -749, -1217, -9, 272, 341, -53, -265, -535, 489, 843, 1298, + -120, -482, -1032, 632, 543, 408, 179, 306, -526, 1124, 1464, 2244, + -417, -786, -1562, -224, -384, 1364, -377, -459, -25, 385, 489, 2174, + -332, -651, -829, 544, 553, 61, 22, -113, -89, 1128, 1725, 1524, + -216, -373, -1653, 161, 316, 908, -165, -222, -67, 1362, 1175, 789, + 73, -252, -767, 738, 932, 616, 362, 246, -126, 787, 2654, 3027, + -691, -1106, -2190, -565, -588, 524, -590, -979, -490, -263, 397, 982, + -577, -837, -945, -22, 435, -49, -190, -118, -629, -88, 1240, 1513, + -636, -1051, -1019, -291, 189, 259, -257, -470, -629, 145, 945, 894, + -326, -364, -1094, 543, 260, 630, -202, 189, -209, 357, 1379, 2091, + -569, -1075, -1449, -714, -239, 919, -420, -705, -84, -109, -114, 2407, + -413, -529, -1177, 482, 368, 131, -186, -72, -131, 861, 1255, 1220, + -611, -658, -1341, 227, -121, 631, -176, -489, -218, 745, 1175, 957, + -321, -148, -936, 671, 966, 216, 340, -3, -143, 469, 1848, 2437, + -729, -961, -1683, -213, -254, 321, -511, -438, -521, -126, 725, 903, + -340, -685, -1032, 316, 480, 20, 23, -89, -551, 353, 1051, 1789, + -544, -757, -1364, 298, -25, 436, -100, -392, -519, 467, 754, 1078, + -210, -398, -1078, 620, 658, 630, 33, 147, -178, 921, 1687, 1921, + -325, -528, -1978, 2, -285, 910, -371, -490, -230, 0, 597, 2010, + -496, -395, -834, 37, 945, 245, 181, -160, -144, 1481, 1373, 1357, + -355, -601, -1270, 298, 322, 672, -193, -336, 77, 1089, 1533, 922, + 177, -39, -1125, 996, 781, 536, 456, 366, -432, 1415, 2440, 2279, + -466, -758, -2325, -303, -509, 387, -727, -557, 66, -145, 643, 1248, + -544, -676, -916, -225, 862, 588, -152, 40, -533, 423, 1423, 1558, + -572, -843, -1145, -128, 85, 461, -238, -257, -584, 605, 748, 861, + 24, -202, -1409, 797, 487, 303, -181, 364, -182, 616, 1378, 2942, + -494, -852, -1441, -292, 61, 812, -84, -723, -182, 555, 532, 1506, + -365, -493, -1057, 822, 588, 11, -14, -18, -230, 1001, 1401, 1451, + -474, -569, -1292, 302, 62, 1062, -70, -376, -222, 982, 974, 1149, + -196, -234, -795, 479, 1098, 499, 362, 58, 70, 1147, 2069, 2857, + -487, -878, -1824, 73, -288, 348, -358, -500, -508, 199, 721, 1242, + -78, -697, -795, 361, 536, 196, 374, 110, -735, 847, 1051, 1896, + -366, -713, -1182, 315, 320, 429, 72, -215, -450, 759, 886, 1363, + -30, -428, -834, 861, 627, 796, 118, 468, -279, 1355, 1883, 1893, + -188, -642, -1612, 63, -175, 1198, -418, -211, 51, 414, 587, 2601, + -234, -557, -858, 424, 889, 222, 136, -101, 83, 1413, 2278, 1383, + -84, -445, -1389, 414, 313, 1045, 29, -343, 65, 1552, 1647, 980, + 183, -91, -829, 1273, 1413, 360, 553, 272, -107, 1587, 3149, 2603, +} + +var mean_lsf_5 = []int16{ + 1384, 2077, 3420, 5108, 6742, 8122, 9863, 11092, 12714, 13701, +} + +var dico1_lsf_5 = []int16{ + -451, -1065, -529, -1305, -450, -756, -497, -863, -384, -619, -413, -669, + -317, -538, -331, -556, -414, -508, -424, -378, -274, -324, -434, -614, + -226, -500, -232, -514, -263, -377, -298, -410, -151, -710, -174, -818, + -149, -412, -156, -429, -288, -462, -186, -203, -170, -302, -191, -321, + -131, -147, -297, -395, -228, -214, -245, -192, -67, -316, -71, -327, + -104, -205, -94, -183, -143, -38, -193, -95, 16, -76, -124, -248, + 23, -237, 24, -244, 18, -136, 44, -111, -33, -24, -25, 0, + 149, 19, 23, -143, 158, -169, 174, -181, 133, -55, 165, -26, + 111, 84, 98, 75, 87, 183, -115, -11, -8, 130, 11, 170, + 254, 77, 205, 17, 183, 112, 262, 194, 202, 287, 95, 189, + -42, -105, 234, 179, 39, 186, 163, 345, 332, 199, 299, 161, + -54, 285, -78, 281, -133, 141, -182, 111, 249, 341, 271, 364, + 93, 403, 75, 391, 92, 510, -138, 220, -185, -29, -34, 361, + -115, 320, 3, 554, 99, 286, 218, 591, -245, 406, -268, 453, + 0, 580, 25, 606, 275, 532, 148, 450, -73, 739, -285, 518, + -288, 94, -203, 674, -140, -74, 205, 714, -114, 299, 176, 923, + 182, 557, 240, 705, -16, 513, 485, 593, 293, 384, 451, 617, + -38, 50, 563, 529, 303, 209, 459, 363, 433, 452, 450, 454, + 367, 606, 477, 741, 432, 353, 368, 267, 361, 716, 273, 583, + 453, 166, 510, 172, 201, 629, 274, 191, 568, 639, 302, 298, + 634, 387, 643, 350, 587, 560, 612, 565, 600, 788, 487, 672, + 512, 1015, 321, 333, 357, 854, -125, 413, 474, 712, 17, -151, + 564, 285, 270, -241, 971, 889, 489, 220, 510, 896, 549, 924, + 327, 825, 290, 911, 540, 1108, 158, 805, 199, 957, 511, 730, + 100, 874, 13, 791, 435, 632, 676, 972, 249, 900, 467, 1218, + 781, 1074, 585, 785, -23, 669, 267, 1043, 619, 1084, 615, 1145, + 622, 905, 916, 1049, 80, 331, 584, 1075, 89, 639, 988, 961, + 770, 720, 798, 699, 492, 447, 899, 627, 271, 1188, 725, 1333, + 87, 603, 832, 1603, 616, 1127, 890, 1505, 1000, 1156, 866, 1009, + 995, 827, 1149, 858, 817, 1450, 773, 1320, 500, 1389, 312, 1153, + -20, 1084, 64, 1283, 2, 1172, 399, 1869, 514, 1706, 502, 1636, + 886, 1522, 416, 600, 1131, 1350, 1275, 1390, 889, 1795, 914, 1766, + 227, 1183, 1250, 1826, 505, 1854, 919, 2353, -199, 431, 152, 1735, + -213, -28, 392, 1334, -153, -52, 978, 1151, -323, -400, 813, 1703, + -136, 84, 1449, 2015, -331, -143, -137, 1192, -256, 534, -157, 1031, + -307, -439, 542, 731, -329, -420, -97, 616, -362, -168, -322, 366, + -247, -110, -211, 89, -196, -309, 20, 59, -364, -463, -286, 89, + -336, 175, -432, 141, -379, -190, -434, -196, -79, 150, -278, -227, + -280, 166, -555, -422, -155, 541, -366, 54, -29, -83, -301, -774, + 186, 628, -397, -264, 242, 293, -197, -585, 124, 410, 53, -133, + 10, 340, -570, -1065, 65, -446, 68, -493, 383, 937, -357, -711, + -359, -250, -677, -1068, 292, -26, 363, 6, 607, 1313, -127, -10, + 1513, 1886, 713, 972, 1469, 2181, 1443, 2016, +} + +var dico2_lsf_5 = []int16{ + -1631, -1600, -1796, -2290, -1027, -1770, -1100, -2025, -1277, -1388, -1367, -1534, + -947, -1461, -972, -1524, -999, -1222, -1020, -1172, -815, -987, -992, -1371, + -1216, -1006, -1289, -1094, -744, -1268, -755, -1293, -862, -923, -905, -984, + -678, -1051, -685, -1050, -1087, -985, -1062, -679, -989, -641, -1127, -976, + -762, -654, -890, -806, -833, -1091, -706, -629, -621, -806, -640, -812, + -775, -634, -779, -543, -996, -565, -1075, -580, -546, -611, -572, -619, + -760, -290, -879, -526, -823, -462, -795, -253, -553, -415, -589, -439, + -533, -340, -692, -935, -505, -772, -702, -1131, -263, -306, -971, -483, + -445, -74, -555, -548, -614, -129, -693, -234, -396, -246, -475, -250, + -265, -404, -376, -514, -417, -510, -300, -313, -334, -664, -463, -814, + -386, -704, -337, -615, -234, -201, -233, -239, -167, -567, -203, -619, + -147, -415, -115, -352, -166, -750, -171, -761, -270, -879, -264, -903, + -367, -744, 43, -475, 14, -653, 43, -670, 11, -448, -59, -521, + -126, -119, -155, -613, -42, -863, -27, -931, 136, -483, 183, -468, + 55, -298, 55, -304, 313, -609, 313, -720, 322, -167, 100, -541, + -3, -119, -111, -187, 233, -236, 260, -234, 26, -165, 134, -45, + -40, -549, 360, -203, 378, -388, 450, -383, 275, 20, 182, -103, + 246, -111, 431, 37, 462, -146, 487, -157, -284, -59, 503, -184, + 24, 53, -3, 54, 122, 259, 333, 66, 484, 104, 436, 68, + 195, 116, 190, 206, 269, -9, 482, 352, 382, 285, 399, 277, + 452, 256, 69, 186, 13, 297, -13, 259, -95, 30, 56, 394, + 196, 425, 205, 456, 281, 577, 15, 191, 375, 290, 407, 576, + -56, 227, 544, 405, 0, 549, -92, 528, -229, 351, -245, 338, + -362, 435, 167, 527, -75, 302, 91, 824, 129, 599, 496, 679, + 186, 749, 153, 737, -281, 600, -348, 615, -236, 769, 41, 881, + 38, 890, -220, 841, -357, 883, -393, 903, -634, 474, -444, 850, + -175, 678, -493, 242, -519, 785, -714, 582, -541, 366, -543, 434, + -597, 500, -765, 222, -702, 917, -743, 962, -869, 501, -899, 548, + -379, 200, -435, 157, -819, 214, -861, 157, -614, 40, -632, 94, + -883, -54, -741, 516, -501, 298, -614, -171, -870, -161, -865, -23, + -818, 93, -1015, -267, -662, -359, -549, 2, -442, -121, -377, 0, + -227, 33, -414, -126, -129, 212, -934, 34, -1082, -282, -1119, -268, + -710, -825, -420, -191, -1076, -928, -917, -93, -628, -358, 97, 7, + -206, -393, -101, 24, -203, 38, -168, 83, -599, -423, -279, 426, + -700, 118, -75, 206, -981, -673, -680, 417, -367, 37, -279, 474, + -129, -318, 319, 296, -626, -39, 343, 602, -696, -39, -303, 940, + 104, 233, -380, 137, -36, 269, -75, -214, 120, 43, -529, -477, + 459, 164, -202, -229, -49, -167, 609, 792, 98, -220, 915, 148, + 293, 283, 869, 91, 575, 394, 326, -78, 717, 67, 365, -323, + 616, -36, 731, 27, 619, 238, 632, 273, 448, 99, 801, 476, + 869, 273, 685, 64, 789, 72, 1021, 217, 793, 459, 734, 360, + 646, 480, 360, 322, 429, 464, 638, 430, 756, 363, 1000, 404, + 683, 528, 602, 615, 655, 413, 946, 687, 937, 602, 904, 604, + 555, 737, 786, 662, 467, 654, 362, 589, 929, 710, 498, 478, + 415, 420, 693, 883, 813, 683, 781, 925, 913, 939, 726, 732, + 491, 853, 531, 948, 734, 963, 315, 808, 761, 755, 1144, 760, + 655, 1076, 826, 1057, 1091, 838, 1003, 808, 1047, 1133, 659, 1101, + 992, 1050, 1074, 1075, 971, 694, 1226, 1054, 571, 841, 884, 1404, + 1379, 1096, 1080, 861, 1231, 735, 1284, 760, 1272, 991, 1367, 1053, + 1257, 700, 1050, 534, 988, 453, 1264, 599, 1140, 679, 1621, 815, + 1384, 521, 1317, 393, 1564, 805, 1448, 686, 1068, 648, 875, 307, + 1083, 361, 1047, 317, 1417, 964, 675, 571, 1152, 79, 1114, -47, + 1530, 311, 1721, 314, 1166, 689, 514, -94, 349, 282, 1412, 328, + 1025, 487, -65, 57, 805, 970, 36, 62, 769, -263, 791, -346, + 637, 699, -137, 620, 534, 541, -735, 194, 711, 300, -268, -863, + 926, 769, -708, -428, 506, 174, -892, -630, 435, 547, -1435, -258, + 621, 471, -1018, -1368, -393, 521, -920, -686, -25, 20, -982, -1156, + 340, 9, -1558, -1135, -352, 48, -1579, -402, -887, 6, -1156, -888, + -548, -352, -1643, -1168, -159, 610, -2024, -963, -225, 193, -1656, -1960, + -245, -493, -964, -1680, -936, -635, -1299, -1744, -1388, -604, -1540, -835, + -1397, -135, -1588, -290, -1670, -712, -2011, -1632, -1663, -27, -2258, -811, + -1157, 184, -1265, 189, -1367, 586, -2011, 201, -790, 712, -1210, 3, + -1033, 808, -1251, 830, -111, 635, -1636, 447, -463, -949, -445, -928, + -504, -1162, -501, -1211, 144, -351, -372, -1052, -283, -1059, -279, -1123, + -575, -1438, -587, -1614, -935, -984, 229, 690, -921, -719, -403, 1362, + -685, -465, 874, 397, -509, -46, 317, 1334, -485, 456, 813, 439, + -411, 339, 898, 1067, -425, 46, 1441, 497, -909, -800, 1465, 1046, + -254, -321, 1430, 1165, 68, 350, 1034, 666, 370, 11, 1311, 790, + 143, 232, 1041, 1562, -114, 663, 1616, 1078, 454, 579, 1275, 1040, + -76, 909, 752, 1067, 153, 512, 348, 1214, 614, 385, 1843, 808, + 269, 1034, 203, 1086, 652, 1017, 1783, 1130, 429, 1327, 387, 1384, + -49, 1183, -72, 1215, -416, 1001, 544, 1749, -352, 1223, -502, 1199, + -589, 569, -227, 1630, -142, 1578, -230, 1715, -714, 1288, -838, 1398, + 1131, 1357, -208, 1232, 437, 965, -929, 818, 811, 1410, 859, 1507, + 164, 1212, 1387, 1793, 484, 1874, 456, 2063, 996, 1170, 1326, 1402, + 1316, 1360, 1135, 1262, 1234, 1618, 1361, 1768, 1421, 1227, 1584, 1347, + 854, 672, 1685, 1566, 1139, 1270, 2016, 1825, 1773, 1581, 1532, 1460, + 1487, 946, 1659, 1021, 1744, 1212, 1392, 977, 1772, 1161, 1826, 1164, + 1718, 1429, 1973, 1591, 1185, 864, 2132, 1061, 1799, 814, 1838, 757, + 2104, 1315, 2054, 1258, 2113, 915, 2331, 930, 1467, 1147, 2590, 1439, + 2245, 1744, 2090, 1620, 2358, 1454, 2666, 1506, 1876, 1837, 2070, 1975, + 1739, 1577, 682, 1289, 1584, 2045, 1454, 2098, 2498, 2004, 2711, 2066, + 726, 1588, 2756, 2336, 228, 847, 2456, 1659, 36, 301, 1942, 1957, + -446, -96, 2154, 1396, 1533, 1101, 14, 608, -923, -732, 1383, 1982, + 1345, 952, -680, 321, 1281, 1268, -1594, 365, 941, 946, -1737, -822, + 2374, 2787, 1821, 2788, +} + +var dico3_lsf_5 = []int16{ + -1812, -2275, -1879, -2537, -1640, -1848, -1695, -2004, -1220, -1912, -1221, -2106, + -1559, -1588, -1573, -1556, -1195, -1615, -1224, -1727, -1359, -1151, -1616, -1948, + -1274, -1391, -1305, -1403, -1607, -1179, -1676, -1311, -1443, -1478, -1367, -898, + -1256, -1059, -1331, -1134, -982, -1133, -1149, -1504, -1080, -1308, -1020, -1183, + -980, -1486, -967, -1495, -988, -922, -1047, -1077, -838, -1179, -858, -1222, + -1131, -1041, -1064, -767, -872, -1157, -701, -880, -706, -906, -774, -1016, + -578, -1080, -801, -1478, -591, -1111, -592, -1146, -713, -1388, -640, -1376, + -597, -1059, -416, -903, -686, -832, -661, -708, -444, -868, -490, -921, + -374, -776, -619, -1170, -585, -549, -769, -795, -435, -659, -530, -741, + -498, -837, -357, -597, -279, -871, -243, -887, -282, -665, -280, -667, + -165, -560, -394, -903, -362, -410, -448, -583, -409, -574, -313, -357, + -637, -548, -570, -436, -896, -504, -382, -757, -58, -481, -165, -618, + -191, -374, -234, -382, -222, -683, -25, -480, -418, -359, -730, -353, + -324, -157, -432, -322, -394, -303, -284, -104, -601, -289, -556, -196, + -588, -150, -659, -608, -473, -24, -68, -448, -474, -8, -506, -45, + -748, -184, -844, -252, -901, -91, -584, -97, -652, 138, -764, -131, + -678, -12, -670, 165, -259, -3, -840, -107, -909, 37, -992, 44, + -854, -415, -839, 13, -1001, -271, -1026, -309, -798, -478, -832, -488, + -943, 168, -1112, -387, -1185, -101, -1183, -40, -941, -316, -1030, -770, + -1044, -625, -1081, -538, -1224, -299, -1312, -436, -1197, -663, -1167, -161, + -1216, -690, -1237, -831, -1432, -720, -1403, -493, -898, -740, -922, -801, + -1102, -402, -1579, -964, -1061, -638, -1269, -1438, -1499, -934, -1502, -895, + -1598, -564, -1723, -717, -606, -597, -1166, -1085, -1369, -468, -1946, -1493, + -1838, -953, -1932, -931, -1499, -188, -1635, -421, -1457, -338, -1448, -22, + -1942, -422, -2006, -249, -496, -114, -1910, -755, -1289, 174, -1451, -109, + -482, -257, -1221, -508, -1617, 151, -1694, 208, -654, 107, -1651, 29, + -1141, 279, -1215, 306, -1228, -506, -730, -175, -1236, -101, -969, 551, + -870, 278, -823, 315, -563, 376, -1051, 228, -507, 280, -599, 281, + -758, 253, -305, 379, -755, -134, -611, 660, -824, 536, -817, 646, + -413, 49, -341, 177, -453, 526, -482, 589, -71, 339, -657, 264, + -244, 295, -237, 315, -387, 569, -506, -9, -377, 14, -160, 661, + -216, 40, -308, -46, 95, 214, -242, 167, -86, 192, -56, 27, + -76, 31, 36, 309, -106, -182, -113, 74, -441, -22, 23, 139, + 81, -11, 44, 15, -87, -137, -118, -207, -158, -58, 272, -92, + -156, -441, 8, -136, 128, -221, 101, -218, 40, -197, -76, -456, + 9, -445, 33, -423, 226, 60, 73, -222, 156, -399, 280, -318, + 245, -341, 166, -499, 339, -190, 327, -219, 325, -137, -89, -596, + 100, -627, 144, -677, 487, 28, 252, -391, 214, -41, 282, -28, + 99, -286, 331, 49, 459, -388, 565, -369, 436, 28, 336, -9, + 397, -167, 618, 34, 596, -17, 561, -140, 299, 79, 522, 125, + 203, 2, 244, 288, 255, 211, 175, 82, 596, 187, 517, 108, + 381, 255, 365, 297, 497, 352, 327, -82, 25, 210, 371, 245, + 261, 3, 545, 449, 140, 294, 44, 295, 212, 347, 244, 494, + 331, 528, 201, 307, 349, 411, 613, 284, 614, 413, 464, 322, + 624, 397, 97, 200, -160, 384, 149, 362, 495, 525, 269, 585, + 33, 491, -121, 433, 427, 611, 498, 516, 171, 443, 497, 666, + 440, 275, 566, 575, 146, 639, 155, 670, -33, 173, 212, 696, + -166, 601, -191, 695, -489, 503, 175, 742, 214, 476, 372, 1083, + 578, 530, 586, 777, 425, 874, 315, 841, 374, 848, -165, 565, + 35, 991, -39, 1062, 329, 712, 786, 840, 645, 795, 661, 676, + 571, 918, 632, 1079, 673, 817, 318, 388, 874, 1012, 564, 848, + 880, 620, 557, 479, 671, 453, 692, 468, 840, 642, 844, 645, + 506, 428, 897, 567, 837, 387, 962, 499, 691, 561, 939, 926, + 783, 296, 790, 268, 1028, 530, 874, 329, 548, 143, 675, 291, + 503, 66, 1041, 359, 786, 97, 805, 33, 837, 470, 511, 49, + 1092, 327, 1174, 323, 3, 242, 872, 474, 689, 429, 1329, 678, + 1042, 620, 1109, 664, 321, 193, 889, 950, 1153, 874, 893, 635, + 877, 862, 948, 913, 1293, 665, 1320, 639, 997, 793, 1402, 1030, + 1176, 1012, 1110, 959, 1410, 925, 1403, 915, 543, 862, 1116, 1222, + 835, 1190, 835, 1190, 959, 1148, 1147, 1376, 1300, 1193, 1415, 1231, + 1335, 1341, 746, 1092, 1711, 1283, 1389, 1073, 1334, 1566, 1153, 1475, + 1645, 1137, 1825, 1220, 1056, 1382, 1521, 1730, 1632, 1545, 1620, 1542, + 855, 1596, 865, 1667, 693, 885, 1716, 1519, 1167, 1296, 2209, 1760, + 1952, 1493, 2020, 1482, 1534, 1866, 1694, 2008, 1566, 748, 1761, 825, + 294, 1392, 1084, 2058, 621, 1315, 365, 1287, 198, 1028, 488, 1408, + 249, 403, 1014, 1561, 324, 363, 1645, 1044, 193, 367, 2034, 1859, + -251, 579, 750, 994, -243, 30, 1325, 879, -28, -169, 624, 917, + -453, 159, 186, 1370, -614, 6, 537, 392, -94, -291, 781, 229, + -128, -298, 245, 491, -701, -648, 972, 789, -501, -640, 178, 255, + -365, -390, -255, 317, -958, -294, -191, 228, -775, -447, 157, -237, + -657, -720, -407, 92, -117, -611, 334, -230, -679, -1084, -144, -317, + -901, -861, -738, -360, -85, -727, -90, -787, 100, -22, -391, -263, + -56, -73, -337, -754, 5, -189, -706, -624, 89, -344, -135, -1113, + -353, -237, -684, -1135, -275, -1102, -269, -1203, 152, 145, -722, -1232, + 49, 80, -1248, -776, -248, 391, -732, -547, 469, 218, -255, -864, + 69, 366, -166, -485, -688, 191, -1212, -1196, -170, -169, -1308, -1631, + 321, 470, -1419, -1243, -64, 272, -1361, -248, 492, 565, -721, -609, + 195, 485, -573, -133, 427, 202, -171, -118, 199, 575, 2, -31, + 694, 755, -1366, -39, 552, 557, -489, 271, 680, 537, 13, -453, + 855, 954, -133, -52, -81, 738, -1169, 637, 1055, 1059, -95, 676, + 1259, 1081, 489, 305, -449, 954, -534, 996, -969, 866, -1058, 1059, + -1294, 618, -1416, 617, -458, 1366, -159, 1821, -774, -528, -14, 1110, + -1202, -901, -772, 433, -1256, -1255, -1011, -302, -602, -585, -759, -1618, + -760, -1549, -840, -1921, -816, -539, -1769, -2235, -227, -36, -2034, -1831, + -2107, -1126, -2471, -1816, -1470, 252, -2701, -415, -571, -467, 1509, 1554, + 2180, 1975, 2326, 2020, +} + +var dico4_lsf_5 = []int16{ + -1857, -1681, -1857, -1755, -2056, -1150, -2134, -1654, -1619, -1099, -1704, -1131, + -1345, -1608, -1359, -1638, -1338, -1293, -1325, -1265, -1664, -1649, -1487, -851, + -1346, -1832, -1413, -2188, -1282, -681, -1785, -1649, -966, -1082, -1183, -1676, + -1054, -1073, -1142, -1158, -1207, -744, -1274, -997, -934, -1383, -927, -1416, + -1010, -1305, -783, -955, -1049, -900, -993, -817, -737, -823, -972, -1189, + -738, -1094, -738, -1154, -784, -801, -810, -786, -892, -520, -1000, -818, + -644, -965, -577, -882, -541, -694, -671, -917, -595, -642, -646, -615, + -956, -621, -925, -515, -727, -483, -815, -485, -840, -578, -440, -713, + -578, -325, -657, -670, -386, -570, -441, -666, -514, -787, -392, -529, + -522, -453, -487, -423, -616, -585, -617, -157, -662, -268, -680, -348, + -322, -323, -632, -444, -304, -430, -332, -458, -277, -468, -659, -793, + -319, -636, -227, -554, -373, -347, -334, -210, -456, -192, -530, -242, + -216, -198, -366, -370, -338, -161, -409, -748, -107, -380, -294, -643, + -223, -665, -234, -741, -141, -496, -130, -510, -139, -327, -172, -305, + -306, -580, -164, -263, -262, -172, -67, -402, 31, -366, -10, -436, + -86, -527, 71, -377, -22, -609, -12, -678, -67, -319, 63, -191, + 35, -181, -39, -242, 126, -167, -140, -544, 155, -297, 174, -297, + 38, -8, 117, -380, 197, -452, 240, -522, 223, -103, 110, -187, + 87, -155, 169, -47, 157, 26, -83, -100, 128, 80, 209, -62, + 6, 7, 22, 5, 318, -20, 248, -45, -200, -63, 156, -69, + 250, -183, 369, -126, -113, -76, -142, -122, -64, -254, -31, 35, + -177, -71, -7, 171, 93, 27, 108, 212, -330, -209, -123, -70, + -279, 95, -96, 20, -188, -61, -314, 87, -300, -78, -354, -134, + 11, 122, -140, 122, -275, 152, -293, 140, -82, 138, -321, -111, + -480, -156, -359, 76, -254, -40, -635, -96, -522, 79, -507, 8, + -268, 303, -539, 68, -446, 61, -522, 306, 111, 189, -435, 122, + -379, 166, -571, -398, -632, -74, -747, -95, -455, 194, -952, 83, + -798, 192, -755, 192, -781, -162, -619, 234, -663, -297, -488, -109, + -964, -132, -838, -68, -843, 58, -1112, -86, -805, -299, -944, -253, + -778, -50, -965, -549, -352, -98, -992, -343, -1117, -315, -1117, -307, + -1155, -374, -637, -230, -1166, -43, -1299, -100, -925, -393, -1274, -600, + -689, -130, -1479, -312, -1321, -254, -1464, -442, -1292, -613, -1261, -503, + -1501, -368, -1322, 26, -1432, -66, -1743, -161, -1644, -467, -1760, -548, + -1393, -568, -1556, -871, -1495, -1034, -1387, -571, -1917, -528, -1783, -123, + -1897, -231, -2054, -323, -2052, -906, -1976, -567, -1917, -620, -2047, -989, + -1077, -370, -2031, -704, -2355, -749, -2740, -1089, -1909, 159, -2012, 248, + -626, -123, -2339, -962, -669, -408, -1379, -1174, -452, -364, -1044, -735, + -132, 183, -1620, -752, -547, -307, -777, -1261, -98, 41, -880, -1091, + -257, 97, -1602, -1833, 31, -26, -644, -561, -180, -546, -385, -1095, + -410, -802, -414, -827, -457, -970, -490, -1109, -215, -916, -144, -937, + -493, -1269, -517, -1507, 181, 101, -332, -889, -836, -937, -559, -429, + -629, -547, -183, -337, -545, -82, -250, -286, 5, -132, -348, -252, + -293, -472, -158, 100, -29, 197, -236, -424, -861, -213, -140, -7, + -427, -443, 187, -97, -684, -736, -293, 258, -368, -152, -150, 392, + -609, 175, -142, 299, -138, 152, -119, 329, -486, -52, 293, 198, + -183, 117, 175, 331, -58, -274, 231, 300, -288, 330, -305, 372, + -111, 409, -9, 423, 83, 256, 67, 367, -19, 248, 91, 113, + -35, 406, -191, 154, 238, 296, 5, 197, 141, 221, 313, 198, + 211, 421, 244, 334, 88, 426, -243, 454, 202, 552, -5, 403, + 291, 185, 219, 301, 251, 138, 128, 69, 197, 288, -140, -61, + 188, 361, 197, 598, 442, 273, 290, 143, 472, 482, 157, 370, + 415, 321, 372, 385, 402, 552, 155, 24, 550, 263, -11, 21, + 360, 227, 147, -254, 424, 97, 366, -13, 375, 141, 449, 232, + 396, 507, 474, 272, 701, 324, 362, -47, 587, 148, 543, 69, + 400, -51, 561, 59, 220, -10, 352, 147, 206, 211, 653, 185, + 563, 297, 565, 284, 594, 121, 766, 192, 398, 118, 642, 434, + 233, 264, 481, 467, 129, -165, 699, 239, 90, 26, 342, 474, + -55, 27, 388, 94, -172, 0, 725, 379, -60, 337, 370, 465, + 95, 319, 806, 595, 78, 260, 497, 851, 210, 560, 458, 574, + -464, 202, 497, 625, -202, 152, 48, 712, -20, 566, 100, 715, + 455, 468, 411, 605, 319, 646, 195, 615, 401, 538, 680, 739, + 201, 667, 434, 954, 454, 425, 646, 491, 606, 681, 416, 508, + 497, 822, 426, 815, 660, 647, 628, 716, 697, 466, 618, 457, + 685, 460, 365, 309, 721, 567, 836, 601, 609, 300, 825, 459, + 943, 687, 681, 533, 915, 598, 591, 243, 876, 451, 874, 420, + 786, 317, 732, 220, 922, 317, 1108, 367, 531, 466, 1028, 649, + 1053, 615, 1034, 553, 829, 602, 1021, 799, 927, 803, 878, 763, + 799, 496, 1373, 773, 585, 770, 803, 930, 1099, 793, 1222, 862, + 1209, 895, 1025, 727, 772, 845, 1172, 1115, 867, 1021, 830, 1013, + 841, 910, 506, 703, 1239, 1077, 620, 819, 1196, 1083, 1155, 1081, + 1142, 907, 1547, 1121, 1309, 648, 1343, 612, 1484, 988, 1479, 937, + 985, 1328, 955, 1341, 429, 910, 841, 1338, 564, 1179, 412, 1156, + 1427, 1320, 1434, 1330, 640, 760, 1726, 1410, 190, 555, 1073, 1005, + 426, 257, 839, 980, 235, 231, 1520, 1167, 109, 293, 1014, 1569, + 305, 142, 1148, 539, -291, -108, 1213, 972, 22, -216, 667, 828, + -482, 438, 453, 1431, -581, -422, 789, 387, -358, -454, 174, 780, + -36, -372, 390, -134, -629, 160, -306, 751, -1258, -331, 177, 522, + -248, 574, -251, 639, -531, 407, -596, 394, -419, 789, -617, 801, + -986, 399, -857, 727, -7, 518, -703, 310, -1143, -24, -1002, 287, + -960, 363, -1299, 312, -1534, 245, -1557, 305, 28, 153, -859, -175, + -33, 332, -1398, -154, 212, 410, -593, -197, -1092, -704, -904, -65, + 282, 367, -918, -686, 345, 93, -258, -357, 696, 644, -693, -28, + 448, 493, -273, 193, 527, 546, -243, -513, 384, -136, 273, -353, + 512, -142, 537, -198, 941, 750, 83, 248, 578, 861, -56, 592, + 842, 44, 892, 24, 33, 890, -16, 982, 831, 1398, 1535, 1898, + 1716, 1376, 1948, 1465, +} + +var dico5_lsf_5 = []int16{ + -1002, -929, -1096, -1203, -641, -931, -604, -961, -779, -673, -835, -788, + -416, -664, -458, -766, -652, -521, -662, -495, -1023, -509, -1023, -428, + -444, -552, -368, -449, -479, -211, -1054, -903, -316, -249, -569, -591, + -569, -275, -541, -191, -716, -188, -842, -264, -333, -248, -318, -228, + -275, 1, -567, -228, -115, -221, -238, -374, -197, -507, -222, -579, + -258, -432, -61, -244, -345, 2, -338, 39, -215, -169, -58, 0, + -56, -6, -203, -131, 1, -186, -5, -211, 6, -380, 11, -418, + -116, 131, -134, 113, 89, -4, 71, -2, -19, -192, 262, 24, + 189, 151, -133, -109, 186, -153, 166, -219, 37, 139, 193, 171, + 337, 124, 158, -61, 141, 226, -13, 190, 231, 34, 354, 109, + 316, 201, 244, 164, 330, -85, 390, -84, 254, 327, 257, 335, + 491, 147, 476, 105, 54, 77, 437, 370, 421, 314, 449, 342, + 329, 126, 673, 292, 571, 388, 243, 193, 653, 320, 621, 280, + 194, 380, 517, 581, 45, 323, 111, 422, 489, 395, 734, 534, + 622, 546, 486, 502, 318, 572, 189, 550, 385, 422, -157, 153, + -125, 382, -197, 386, -263, 334, 228, 697, -188, 1, 51, 297, + -507, 213, -376, 397, -24, 255, -547, 89, -502, -94, 387, 179, + -620, 68, -684, 112, -642, -350, -260, 172, -438, -324, 264, 648, + -964, -4, -1121, 7, -134, 134, -1133, -306, 143, 96, -420, -497, + -1221, -350, -1527, -685, -161, 72, 873, 691, 732, 283, 921, 353, + 334, 475, 1095, 821, 864, 524, 843, 497, 714, 711, 788, 750, + 1076, 714, 1204, 753, +} + +var lspLsfTable = []int16{ + 32767, 32729, 32610, 32413, 32138, 31786, 31357, 30853, 30274, 29622, 28899, 28106, + 27246, 26320, 25330, 24279, 23170, 22006, 20788, 19520, 18205, 16846, 15447, 14010, + 12540, 11039, 9512, 7962, 6393, 4808, 3212, 1608, 0, -1608, -3212, -4808, + -6393, -7962, -9512, -11039, -12540, -14010, -15447, -16846, -18205, -19520, -20788, -22006, + -23170, -24279, -25330, -26320, -27246, -28106, -28899, -29622, -30274, -30853, -31357, -31786, + -32138, -32413, -32610, -32729, -32768, +} + +var lspLsfSlope = []int16{ + -26887, -8812, -5323, -3813, -2979, -2444, -2081, -1811, -1608, -1450, -1322, -1219, + -1132, -1059, -998, -946, -901, -861, -827, -797, -772, -750, -730, -713, + -699, -687, -677, -668, -662, -657, -654, -652, -652, -654, -657, -662, + -668, -677, -687, -699, -713, -730, -750, -772, -797, -827, -861, -901, + -946, -998, -1059, -1132, -1219, -1322, -1450, -1608, -1811, -2081, -2444, -2979, + -3813, -5323, -8812, -26887, +} + +var lsp_init_data = []int16{ + 30000, 26000, 21000, 15000, 8000, 0, -8000, -15000, -21000, -26000, +} + +var grid = []int16{ + 32760, 32723, 32588, 32364, 32051, 31651, 31164, 30591, 29935, 29196, 28377, 27481, + 26509, 25465, 24351, 23170, 21926, 20621, 19260, 17846, 16384, 14876, 13327, 11743, + 10125, 8480, 6812, 5126, 3425, 1714, 0, -1714, -3425, -5126, -6812, -8480, + -10125, -11743, -13327, -14876, -16384, -17846, -19260, -20621, -21926, -23170, -24351, -25465, + -26509, -27481, -28377, -29196, -29935, -30591, -31164, -31651, -32051, -32364, -32588, -32723, + -32760, +} diff --git a/internal/capability/transcode/amrnb/tables_math.go b/internal/capability/transcode/amrnb/tables_math.go new file mode 100644 index 0000000..678943d --- /dev/null +++ b/internal/capability/transcode/amrnb/tables_math.go @@ -0,0 +1,40 @@ +// Code generated from opencore-amrnb — DO NOT EDIT. +// +// ROM tables extracted verbatim from the Apache-2.0 opencore-amrnb C +// reference; element counts are verified against the C array sizes. + +package amrnb + +var log2_tbl = []int16{ + 0, 1455, 2866, 4236, 5568, 6863, 8124, 9352, 10549, 11716, 12855, 13967, + 15054, 16117, 17156, 18172, 19167, 20142, 21097, 22033, 22951, 23852, 24735, 25603, + 26455, 27291, 28113, 28922, 29716, 30497, 31266, 32023, 32767, +} + +var pow2_tbl = []int16{ + 16384, 16743, 17109, 17484, 17867, 18258, 18658, 19066, 19484, 19911, 20347, 20792, + 21247, 21713, 22188, 22674, 23170, 23678, 24196, 24726, 25268, 25821, 26386, 26964, + 27554, 28158, 28774, 29405, 30048, 30706, 31379, 32066, 32767, +} + +var inv_sqrt_tbl = []int16{ + 32767, 31790, 30894, 30070, 29309, 28602, 27945, 27330, 26755, 26214, 25705, 25225, + 24770, 24339, 23930, 23541, 23170, 22817, 22479, 22155, 21845, 21548, 21263, 20988, + 20724, 20470, 20225, 19988, 19760, 19539, 19326, 19119, 18919, 18725, 18536, 18354, + 18176, 18004, 17837, 17674, 17515, 17361, 17211, 17064, 16921, 16782, 16646, 16514, + 16384, +} + +var sqrt_l_tbl = []int16{ + 16384, 16888, 17378, 17854, 18318, 18770, 19212, 19644, 20066, 20480, 20886, 21283, + 21674, 22058, 22435, 22806, 23170, 23530, 23884, 24232, 24576, 24915, 25249, 25580, + 25905, 26227, 26545, 26859, 27170, 27477, 27780, 28081, 28378, 28672, 28963, 29251, + 29537, 29819, 30099, 30377, 30652, 30924, 31194, 31462, 31727, 31991, 32252, 32511, + 32767, 32767, +} + +var overflow_tbl = []int32{ + 2147483647, 1073741823, 536870911, 268435455, 134217727, 67108863, 33554431, 16777215, 8388607, 4194303, 2097151, 1048575, + 524287, 262143, 131071, 65535, 32767, 16383, 8191, 4095, 2047, 1023, 511, 255, + 127, 63, 31, 15, 7, 3, 1, 0, +} diff --git a/internal/capability/transcode/amrnb/tables_olp.go b/internal/capability/transcode/amrnb/tables_olp.go new file mode 100644 index 0000000..506a8ab --- /dev/null +++ b/internal/capability/transcode/amrnb/tables_olp.go @@ -0,0 +1,33 @@ +// Code generated from opencore-amrnb corrwght_tab.cpp. DO NOT EDIT. + +package amrnb + +// corrweight is the open-loop pitch weighting curve used by Pitch_ol_wgh (MR102). +var corrweight = [251]int16{ + 20473, 20506, 20539, 20572, 20605, 20644, 20677, 20716, 20749, 20788, + 20821, 20860, 20893, 20932, 20972, 21011, 21050, 21089, 21129, 21168, + 21207, 21247, 21286, 21332, 21371, 21417, 21456, 21502, 21542, 21588, + 21633, 21679, 21725, 21771, 21817, 21863, 21909, 21961, 22007, 22059, + 22105, 22158, 22210, 22263, 22315, 22367, 22420, 22472, 22531, 22584, + 22643, 22702, 22761, 22820, 22879, 22938, 23003, 23062, 23128, 23193, + 23252, 23324, 23390, 23455, 23527, 23600, 23665, 23744, 23816, 23888, + 23967, 24045, 24124, 24202, 24288, 24366, 24451, 24537, 24628, 24714, + 24805, 24904, 24995, 25094, 25192, 25297, 25395, 25500, 25611, 25723, + 25834, 25952, 26070, 26188, 26313, 26444, 26575, 26706, 26844, 26988, + 27132, 27283, 27440, 27597, 27761, 27931, 28108, 28285, 28475, 28665, + 28869, 29078, 29295, 29524, 29760, 30002, 30258, 30527, 30808, 31457, + 32767, 32767, 32767, 32767, 32767, 32767, 32767, 31457, 30808, 30527, + 30258, 30002, 29760, 29524, 29295, 29078, 28869, 28665, 28475, 28285, + 28108, 27931, 27761, 27597, 27440, 27283, 27132, 26988, 26844, 26706, + 26575, 26444, 26313, 26188, 26070, 25952, 25834, 25723, 25611, 25500, + 25395, 25297, 25192, 25094, 24995, 24904, 24805, 24714, 24628, 24537, + 24451, 24366, 24288, 24202, 24124, 24045, 23967, 23888, 23816, 23744, + 23665, 23600, 23527, 23455, 23390, 23324, 23252, 23193, 23128, 23062, + 23003, 22938, 22879, 22820, 22761, 22702, 22643, 22584, 22531, 22472, + 22420, 22367, 22315, 22263, 22210, 22158, 22105, 22059, 22007, 21961, + 21909, 21863, 21817, 21771, 21725, 21679, 21633, 21588, 21542, 21502, + 21456, 21417, 21371, 21332, 21286, 21247, 21207, 21168, 21129, 21089, + 21050, 21011, 20972, 20932, 20893, 20860, 20821, 20788, 20749, 20716, + 20677, 20644, 20605, 20572, 20539, 20506, 20473, 20434, 20401, 20369, + 20336, +} diff --git a/internal/capability/transcode/amrnb/tables_sort.go b/internal/capability/transcode/amrnb/tables_sort.go new file mode 100644 index 0000000..9868e4a --- /dev/null +++ b/internal/capability/transcode/amrnb/tables_sort.go @@ -0,0 +1,120 @@ +// Code generated from opencore-amrnb (3GPP TS 26.101) — DO NOT EDIT. +// +// Per-mode RFC 4867 / IETF bit-reordering tables (bitreorder_tab.cpp) and the +// parameter bit widths (bitno_tab.cpp). reorderBits[m][k] is the ETS serial bit +// index that supplies the k-th transmitted (IETF storage order) speech bit, so +// the table is a permutation of 0..modeBits[m]-1. bitno[m] lists the bit width +// of each encoder parameter, MSB-first, summing to modeBits[m]. + +package amrnb + +// bitno gives the bit width of each serial parameter per speech mode (0..7). +var bitno = [numSpeechModes][]int16{ + Mode0475: {8, 8, 7, 8, 7, 2, 8, 4, 7, 2, 4, 7, 2, 8, 4, 7, 2}, + Mode0515: {8, 8, 7, 8, 7, 2, 6, 4, 7, 2, 6, 4, 7, 2, 6, 4, 7, 2, 6}, + Mode0590: {8, 9, 9, 8, 9, 2, 6, 4, 9, 2, 6, 8, 9, 2, 6, 4, 9, 2, 6}, + Mode0670: {8, 9, 9, 8, 11, 3, 7, 4, 11, 3, 7, 8, 11, 3, 7, 4, 11, 3, 7}, + Mode0740: {8, 9, 9, 8, 13, 4, 7, 5, 13, 4, 7, 8, 13, 4, 7, 5, 13, 4, 7}, + Mode0795: {9, 9, 9, 8, 13, 4, 4, 5, 6, 13, 4, 4, 5, 8, 13, 4, 4, 5, 6, 13, 4, 4, 5}, + Mode1020: {8, 9, 9, 8, 1, 1, 1, 1, 10, 10, 7, 7, 5, 1, 1, 1, 1, 10, 10, 7, 7, 8, 1, 1, 1, 1, 10, 10, 7, 7, 5, 1, 1, 1, 1, 10, 10, 7, 7}, + Mode1220: {7, 8, 9, 8, 6, 9, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 5, 6, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 5, 9, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 5, 6, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 5}, +} + +// reorderBits maps transmitted bit position -> ETS serial bit index per mode. +var reorderBits = [numSpeechModes][]int16{ + Mode0475: { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 23, 24, 25, 26, 27, 28, 48, 49, 61, 62, 82, 83, 47, 46, 45, 44, + 81, 80, 79, 78, 17, 18, 20, 22, 77, 76, 75, 74, 29, 30, 43, 42, + 41, 40, 38, 39, 16, 19, 21, 50, 51, 59, 60, 63, 64, 72, 73, 84, + 85, 93, 94, 32, 33, 35, 36, 53, 54, 56, 57, 66, 67, 69, 70, 87, + 88, 90, 91, 34, 55, 68, 89, 37, 58, 71, 92, 31, 52, 65, 86, + }, + Mode0515: { + 7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8, + 23, 24, 25, 26, 27, 46, 65, 84, 45, 44, 43, 64, 63, 62, 83, 82, + 81, 102, 101, 100, 42, 61, 80, 99, 28, 47, 66, 85, 18, 41, 60, 79, + 98, 29, 48, 67, 17, 20, 22, 40, 59, 78, 97, 21, 30, 49, 68, 86, + 19, 16, 87, 39, 38, 58, 57, 77, 35, 54, 73, 92, 76, 96, 95, 36, + 55, 74, 93, 32, 51, 33, 52, 70, 71, 89, 90, 31, 50, 69, 88, 37, + 56, 75, 94, 34, 53, 72, 91, + }, + Mode0590: { + 0, 1, 4, 5, 3, 6, 7, 2, 13, 15, 8, 9, 11, 12, 14, 10, + 16, 28, 74, 29, 75, 27, 73, 26, 72, 30, 76, 51, 97, 50, 71, 96, + 117, 31, 77, 52, 98, 49, 70, 95, 116, 53, 99, 32, 78, 33, 79, 48, + 69, 94, 115, 47, 68, 93, 114, 46, 67, 92, 113, 19, 21, 23, 22, 18, + 17, 20, 24, 111, 43, 89, 110, 64, 65, 44, 90, 25, 45, 66, 91, 112, + 54, 100, 40, 61, 86, 107, 39, 60, 85, 106, 36, 57, 82, 103, 35, 56, + 81, 102, 34, 55, 80, 101, 42, 63, 88, 109, 41, 62, 87, 108, 38, 59, + 84, 105, 37, 58, 83, 104, + }, + Mode0670: { + 0, 1, 4, 3, 5, 6, 13, 7, 2, 8, 9, 11, 15, 12, 14, 10, + 28, 82, 29, 83, 27, 81, 26, 80, 30, 84, 16, 55, 109, 56, 110, 31, + 85, 57, 111, 48, 73, 102, 127, 32, 86, 51, 76, 105, 130, 52, 77, 106, + 131, 58, 112, 33, 87, 19, 23, 53, 78, 107, 132, 21, 22, 18, 17, 20, + 24, 25, 50, 75, 104, 129, 47, 72, 101, 126, 54, 79, 108, 133, 46, 71, + 100, 125, 128, 103, 74, 49, 45, 70, 99, 124, 42, 67, 96, 121, 39, 64, + 93, 118, 38, 63, 92, 117, 35, 60, 89, 114, 34, 59, 88, 113, 44, 69, + 98, 123, 43, 68, 97, 122, 41, 66, 95, 120, 40, 65, 94, 119, 37, 62, + 91, 116, 36, 61, 90, 115, + }, + Mode0740: { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 26, 87, 27, 88, 28, 89, 29, 90, 30, 91, 51, 80, 112, 141, 52, + 81, 113, 142, 54, 83, 115, 144, 55, 84, 116, 145, 58, 119, 59, 120, 21, + 22, 23, 17, 18, 19, 31, 60, 92, 121, 56, 85, 117, 146, 20, 24, 25, + 50, 79, 111, 140, 57, 86, 118, 147, 49, 78, 110, 139, 48, 77, 53, 82, + 114, 143, 109, 138, 47, 76, 108, 137, 32, 33, 61, 62, 93, 94, 122, 123, + 41, 42, 43, 44, 45, 46, 70, 71, 72, 73, 74, 75, 102, 103, 104, 105, + 106, 107, 131, 132, 133, 134, 135, 136, 34, 63, 95, 124, 35, 64, 96, 125, + 36, 65, 97, 126, 37, 66, 98, 127, 38, 67, 99, 128, 39, 68, 100, 129, + 40, 69, 101, 130, + }, + Mode0795: { + 8, 7, 6, 5, 4, 3, 2, 14, 16, 9, 10, 12, 13, 15, 11, 17, + 20, 22, 24, 23, 19, 18, 21, 56, 88, 122, 154, 57, 89, 123, 155, 58, + 90, 124, 156, 52, 84, 118, 150, 53, 85, 119, 151, 27, 93, 28, 94, 29, + 95, 30, 96, 31, 97, 61, 127, 62, 128, 63, 129, 59, 91, 125, 157, 32, + 98, 64, 130, 1, 0, 25, 26, 33, 99, 34, 100, 65, 131, 66, 132, 54, + 86, 120, 152, 60, 92, 126, 158, 55, 87, 121, 153, 117, 116, 115, 46, 78, + 112, 144, 43, 75, 109, 141, 40, 72, 106, 138, 36, 68, 102, 134, 114, 149, + 148, 147, 146, 83, 82, 81, 80, 51, 50, 49, 48, 47, 45, 44, 42, 39, + 35, 79, 77, 76, 74, 71, 67, 113, 111, 110, 108, 105, 101, 145, 143, 142, + 140, 137, 133, 41, 73, 107, 139, 37, 69, 103, 135, 38, 70, 104, 136, + }, + Mode1020: { + 7, 6, 5, 4, 3, 2, 1, 0, 16, 15, 14, 13, 12, 11, 10, 9, + 8, 26, 27, 28, 29, 30, 31, 115, 116, 117, 118, 119, 120, 72, 73, 161, + 162, 65, 68, 69, 108, 111, 112, 154, 157, 158, 197, 200, 201, 32, 33, 121, + 122, 74, 75, 163, 164, 66, 109, 155, 198, 19, 23, 21, 22, 18, 17, 20, + 24, 25, 37, 36, 35, 34, 80, 79, 78, 77, 126, 125, 124, 123, 169, 168, + 167, 166, 70, 67, 71, 113, 110, 114, 159, 156, 160, 202, 199, 203, 76, 165, + 81, 82, 92, 91, 93, 83, 95, 85, 84, 94, 101, 102, 96, 104, 86, 103, + 87, 97, 127, 128, 138, 137, 139, 129, 141, 131, 130, 140, 147, 148, 142, 150, + 132, 149, 133, 143, 170, 171, 181, 180, 182, 172, 184, 174, 173, 183, 190, 191, + 185, 193, 175, 192, 176, 186, 38, 39, 49, 48, 50, 40, 52, 42, 41, 51, + 58, 59, 53, 61, 43, 60, 44, 54, 194, 179, 189, 196, 177, 195, 178, 187, + 188, 151, 136, 146, 153, 134, 152, 135, 144, 145, 105, 90, 100, 107, 88, 106, + 89, 98, 99, 62, 47, 57, 64, 45, 63, 46, 55, 56, + }, + Mode1220: { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 23, + 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 38, 141, 39, + 142, 40, 143, 41, 144, 42, 145, 43, 146, 44, 147, 45, 148, 46, 149, 47, + 97, 150, 200, 48, 98, 151, 201, 49, 99, 152, 202, 86, 136, 189, 239, 87, + 137, 190, 240, 88, 138, 191, 241, 91, 194, 92, 195, 93, 196, 94, 197, 95, + 198, 29, 30, 31, 32, 33, 34, 35, 50, 100, 153, 203, 89, 139, 192, 242, + 51, 101, 154, 204, 55, 105, 158, 208, 90, 140, 193, 243, 59, 109, 162, 212, + 63, 113, 166, 216, 67, 117, 170, 220, 36, 37, 54, 53, 52, 58, 57, 56, + 62, 61, 60, 66, 65, 64, 70, 69, 68, 104, 103, 102, 108, 107, 106, 112, + 111, 110, 116, 115, 114, 120, 119, 118, 157, 156, 155, 161, 160, 159, 165, 164, + 163, 169, 168, 167, 173, 172, 171, 207, 206, 205, 211, 210, 209, 215, 214, 213, + 219, 218, 217, 223, 222, 221, 73, 72, 71, 76, 75, 74, 79, 78, 77, 82, + 81, 80, 85, 84, 83, 123, 122, 121, 126, 125, 124, 129, 128, 127, 132, 131, + 130, 135, 134, 133, 176, 175, 174, 179, 178, 177, 182, 181, 180, 185, 184, 183, + 188, 187, 186, 226, 225, 224, 229, 228, 227, 232, 231, 230, 235, 234, 233, 238, + 237, 236, 96, 199, + }, +} diff --git a/internal/capability/transcode/amrnb/vendored_export.go b/internal/capability/transcode/amrnb/vendored_export.go new file mode 100644 index 0000000..15977ec --- /dev/null +++ b/internal/capability/transcode/amrnb/vendored_export.go @@ -0,0 +1,22 @@ +// 本文件是本项目为 vendored 包**新增**的导出桥,**不是**上游 `v1.0.0` 的内容。 +// 出处与许可证见同目录 VENDORED.md(上游 = github.com/VoiceBlender/goamr-nb +// v1.0.0,Apache-2.0,LICENSE / NOTICE 已逐字保留)。 +// +// 为什么需要它:上游公开 API 以 **RTP 载荷**(RFC 4867 的 octet-aligned / +// bandwidth-efficient 打包)为输入;而 `.amr` **存储格式**(RFC 4867 §5)是 +// 「`#!AMR\n` 魔数 + 每帧『1 字节 ToC + ceil(bits/8) 字节语音位』」,没有 CMR +// 字节、也不用 F 位。调用方要按 ToC 分帧就必须知道每种帧类型占多少字节,而这张 +// 表(RFC 4867 Table 1a)在上游包内是未导出的。 +// +// 两条路:① 在本项目的 codec 里再抄一份规格表(两处表格会各自漂移); +// ② 在这里开一个只做转发的薄导出。本包选 ②:全仓只有一份帧长表,且它来自被 +// vendored 的那份实现本身。 +package amrnb + +// FrameBytesByFT 返回帧类型 ft(ToC 字节的高 4 位,0..15)在 **octet-aligned** +// 打包下占用的字节数(**不含** ToC 字节本身),以及该帧类型是否合法。 +// +// 语音模式返回 ceil(bits/8)(RFC 4867 Table 1a:12/13/15/17/19/20/26/31), +// SID(FT 8)返回 5,SPEECH_LOST(FT 14)与 NO_DATA(FT 15)返回 0, +// 保留值(FT 9..13)返回 false。 +func FrameBytesByFT(ft int) (int, bool) { return frameBytesByFT(ft) } diff --git a/internal/capability/transcode/api.go b/internal/capability/transcode/api.go new file mode 100644 index 0000000..aa1bb86 --- /dev/null +++ b/internal/capability/transcode/api.go @@ -0,0 +1,60 @@ +package transcode + +import "context" + +// Request 是一次转码请求(design/25 §2.1)。 +type Request struct { + // Data 是源字节。 + Data []byte + // Source 是源格式;可留空,留空时按魔数嗅探,嗅不出即 ErrUnsupportedFormat + // (deny-by-default)。 + Source Format + // Target 是目标格式;留空即 ErrUnsupportedFormat。 + Target Format +} + +// Result 是转码结果(design/25 §2.1)。 +type Result struct { + // Data 是目标字节。 + Data []byte + // Format 是目标格式(= Request.Target)。 + Format Format + // Source 是实际识别到的源格式(回给调用方,便于日志与 DTO)。 + Source Format + // SampleRate 是采样率(Hz);能确定时给出,否则 0。 + SampleRate int + // Channels 是声道数;能确定时给出,否则 0。 + Channels int + // DurationMs 是时长;能从容器/帧数**确证**时给出,否则 0(绝不估算, + // 与 design/24 §10.4 的 durationMs 纪律同源)。 + DurationMs int64 + // Frames 是已解码帧数(AMR-NB 一帧 20ms = 160 样本);不适用时 0。 + Frames int +} + +// Transcoder 是转码能力对外的唯一接口:**进程内、同步、纯本地** +// (不触网、不落盘、不查库)。实例由组合根构造并经依赖注入交给调用方, +// 绝不设包级全局变量(design/25 §1.3.2 / AGENTS §10 红线)。 +type Transcoder interface { + // Transcode 把 req.Data 从 req.Source 转成 req.Target。失败一律返回 + // errors.go 的哨兵之一(或包裹它),绝不返回部分结果假装成功。 + Transcode(ctx context.Context, req Request) (Result, error) + // Supports 报告这条路是否可用(能力门;调用方据此决定走原生路径还是诚实拒绝)。 + Supports(src, dst Format) bool + // Formats 报告当前注册的所有格式(发现;顺序无保证)。 + Formats() []Format +} + +// Codec 是一条「源→目标」转换路(design/25 §2.2)。实现必须是**纯函数**: +// 给定相同输入产出相同输出,不读写外部状态、不发网络请求、不做缓存 +// (缓存若需要,由调用方按内容寻址去做)。 +type Codec interface { + // From 返回源格式。 + From() Format + // To 返回目标格式。 + To() Format + // Transcode 转换 in 并返回结果。它必须自己执行「本格式可确证」的上限 + // (时长、产物字节),并在循环内遵守 ctx;engine 负责的是与格式无关的 + // 那一层(输入字节上限、并发槽、超时闸、日志)。 + Transcode(ctx context.Context, in []byte, limits Limits) (Result, error) +} diff --git a/internal/capability/transcode/codec_amr_nb.go b/internal/capability/transcode/codec_amr_nb.go new file mode 100644 index 0000000..3872342 --- /dev/null +++ b/internal/capability/transcode/codec_amr_nb.go @@ -0,0 +1,190 @@ +package transcode + +import ( + "context" + "fmt" + "time" + + "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/transcode/amrnb" +) + +// 本文件实现 design/25 §3.1 的**唯一必需**那条路:`amr → wav`。 +// +// 为什么必须:QQ OneBot v11 的语音是 AMR(`.amr`),浏览器**播不了** +// (Chrome 的 AMR 支持表为 No,只有 ChromeOS 支持 AMR-NB/WB),所以 +// 「能点开听」在零转码下不可达(design/25 §3.2)。 +// +// 解码器是 vendored 的 github.com/VoiceBlender/goamr-nb v1.0.0(Apache-2.0, +// 见 amrnb/VENDORED.md):纯 Go、CGO_ENABLED=0 可构建、非 amd64 走 +// simd_noasm.go 兜底 ⇒ 交叉编译不破。 +// +// 目标参数 = AMR-NB 的原生参数:**8 kHz、单声道、PCM16,不重采样、不变声道** +// (design/25 §3.1/§3.2:重采样到 16 kHz 只会把文件放大一倍且不增加任何信息)。 +const ( + // amrStorageHeaderBytes 是 AMR-NB 存储格式的魔数长度(`#!AMR\n`)。 + amrStorageHeaderBytes = len(amrNBMagic) + // amrFrameMillis 是一帧的时长:AMR-NB 一帧固定 20 ms。 + amrFrameMillis = 20 + // amrSampleRate / amrChannels / amrFrameSamples 是 AMR-NB 的原生参数, + // 取自 vendored 解码器自己的常量,不在这里另抄一份。 + amrSampleRate = amrnb.SampleRate + amrChannels = 1 + amrFrameSamples = amrnb.FrameSamples + // amrPayloadNoCMR 是 octet-aligned 载荷首字节的 CMR = 15「no mode request」。 + amrPayloadNoCMR = 0xF0 + // amrToCFrameTypeShift / amrToCFrameTypeMask 抽出 ToC 字节里的帧类型(FT): + // ToC 布局为 F(bit7) FT(bits6-3) Q(bit2) P(bits1-0)(RFC 4867 §4.3.1)。 + amrToCFrameTypeShift = 3 + amrToCFrameTypeMask = 0x0F + // amrRTPHeaderBytes 是每帧送进解码器的 RTP 载荷头长度:CMR 1 字节 + ToC 1 字节。 + amrRTPHeaderBytes = 2 + // amrMaxSpeechBytes 是单帧语音位的上界:12.2 kbps 一帧 244 位 ⇒ 31 字节 + // (RFC 4867 Table 1a)。用常量而非 `amrnb.Mode1220.Bytes()`:后者是方法调用, + // 不是常量表达式。 + amrMaxSpeechBytes = 31 + // amrFramePayloadBytes 是逐帧复用的载荷缓冲容量(不逐帧分配)。 + amrFramePayloadBytes = amrRTPHeaderBytes + amrMaxSpeechBytes +) + +// amrNBToWAV 是 `amr → wav` 这条转换路。它是**纯函数**:同样的输入产出同样的输出, +// 不读写外部状态、不发网络请求、不做缓存(design/25 §2.2)。 +type amrNBToWAV struct{} + +// From 返回源格式。 +func (amrNBToWAV) From() Format { return FormatAMR } + +// To 返回目标格式。 +func (amrNBToWAV) To() Format { return FormatWAV } + +// Transcode 把 AMR-NB 字节解成 8 kHz 单声道 PCM16 的 WAV。 +// +// 三层上限里「格式相关」的两层在这里执行,且都在**解码前**: +// - 输入字节上限由 engine 执行(与格式无关,故不在本文件); +// - 时长上限:帧数 × 20 ms 是**可确证**的(不是估算),超限即 ErrInputTooLarge; +// - 产物字节上限:帧数 × 160 样本 × 2 字节 + 44 字节头是可**精确算出**的, +// 超限即 ErrOutputTooLarge,且此时一个样本都还没解(比「边解码边计数」更早、 +// 更省内存,design/25 §5.1 允许「在解码前」)。 +// +// 结构预检(魔数 / 帧长 / 截断 / 未知帧类型)同样在解码前完成:损坏的字节不会 +// 解出一半再报错。ctx 在逐帧循环里被查,超时/取消立即返回,不泄漏 goroutine。 +func (amrNBToWAV) Transcode(ctx context.Context, in []byte, limits Limits) (Result, error) { + frames, err := amrFrameCount(in) + if err != nil { + return Result{}, err + } + if duration := amrDuration(frames); duration > limits.MaxDuration { + return Result{}, fmt.Errorf("transcode: amr -> wav: %w: duration %s > %s", + ErrInputTooLarge, duration, limits.MaxDuration) + } + pcmBytes := frames * amrFrameSamples * wavPCMBytesPerSample + if total := wavHeaderBytes + pcmBytes; total > limits.MaxOutputBytes { + return Result{}, fmt.Errorf("transcode: amr -> wav: %w: %d bytes > %d", + ErrOutputTooLarge, total, limits.MaxOutputBytes) + } + return amrDecodeToWAV(ctx, in, frames, pcmBytes) +} + +// amrDecodeToWAV 逐帧解码并**直接写进目标缓冲**(不是先攒 []int16 再复制一份 +// WAV):峰值内存 = 输入 + 输出 ≈ 70 MB,与 design/25 §2.5 登记的口径一致。 +// frames / pcmBytes 由调用方在解码前算定(故这里不再分配不可控的内存)。 +func amrDecodeToWAV(ctx context.Context, in []byte, frames, pcmBytes int) (Result, error) { + hdr, err := wavHeader(pcmBytes, amrSampleRate, amrChannels) + if err != nil { + return Result{}, err + } + out := make([]byte, wavHeaderBytes+pcmBytes) + copy(out, hdr[:]) + dec := amrnb.NewDecoder(amrnb.DecoderConfig{OctetAligned: true}) + payload := make([]byte, 0, amrFramePayloadBytes) + off := wavHeaderBytes + decoded := 0 + err = amrWalk(in, func(frame []byte) error { + if cerr := ctx.Err(); cerr != nil { + return cerr + } + // 存储帧(ToC + 语音位)= octet-aligned 载荷去掉 CMR 首字节, + // 因此补回 CMR 即可直接交给 vendored 解码器。 + payload = append(payload[:0], amrPayloadNoCMR, frame[0]) + payload = append(payload, frame[1:]...) + samples, derr := dec.Decode(payload) + if derr != nil { + return fmt.Errorf("transcode: amr frame %d: %w: %v", decoded, ErrDecodeFailed, derr) + } + if len(samples) != amrFrameSamples { + return fmt.Errorf("transcode: amr frame %d: %w: got %d samples, want %d", + decoded, ErrDecodeFailed, len(samples), amrFrameSamples) + } + writePCM16LE(out[off:], samples) + off += amrFrameSamples * wavPCMBytesPerSample + decoded++ + return nil + }) + if err != nil { + return Result{}, err + } + return Result{ + Data: out, + Format: FormatWAV, + Source: FormatAMR, + SampleRate: amrSampleRate, + Channels: amrChannels, + DurationMs: amrDurationMs(frames), + Frames: frames, + }, nil +} + +// amrWalk 逐帧走一遍 AMR-NB 存储格式(RFC 4867 §5):`#!AMR\n` 魔数,随后每帧 +// 「1 字节 ToC + ceil(bits/8) 字节语音位」。它只分帧、只做结构检查,**不解码**, +// 因此可以安全用于「解码前」的帧数 / 时长 / 产物上限判定。 +// +// visit 拿到的是含 ToC 的完整存储帧。visit 返回的错误经 %w 包裹上抛(保持 +// ctx 超时的 errors.Is 链)。 +func amrWalk(in []byte, visit func(frame []byte) error) error { + if len(in) < amrStorageHeaderBytes { + return fmt.Errorf("transcode: amr: %w: header truncated (%d bytes)", ErrDecodeFailed, len(in)) + } + if string(in[:amrStorageHeaderBytes]) != amrNBMagic { + return fmt.Errorf("transcode: amr: %w: not an AMR-NB stream (`#!AMR\\n` magic missing)", ErrDecodeFailed) + } + body := in[amrStorageHeaderBytes:] + for index := 0; len(body) > 0; index++ { + ft := int(body[0]>>amrToCFrameTypeShift) & amrToCFrameTypeMask + size, ok := amrnb.FrameBytesByFT(ft) + if !ok { + return fmt.Errorf("transcode: amr frame %d: %w: reserved frame type %d", index, ErrDecodeFailed, ft) + } + if len(body) < 1+size { + return fmt.Errorf("transcode: amr frame %d: %w: truncated (need %d bytes, have %d)", + index, ErrDecodeFailed, 1+size, len(body)) + } + if err := visit(body[:1+size]); err != nil { + return fmt.Errorf("transcode: amr: %w", err) + } + body = body[1+size:] + } + return nil +} + +// amrFrameCount 数出存储流里的帧数(结构预检)。空流(只有魔数、没有帧)按 +// 「坏的输入」处理:一条语音消息连一帧音频都没有,诚实报 ErrDecodeFailed +// 比交出一份 0 样本的 WAV 更不容易被误当成成功。 +func amrFrameCount(in []byte) (int, error) { + frames := 0 + if err := amrWalk(in, func([]byte) error { frames++; return nil }); err != nil { + return 0, err + } + if frames == 0 { + return 0, fmt.Errorf("transcode: amr: %w: no audio frames", ErrDecodeFailed) + } + return frames, nil +} + +// amrDuration 由帧数**确证**时长(一帧 20 ms),绝不估算。 +func amrDuration(frames int) time.Duration { + return time.Duration(frames*amrFrameMillis) * time.Millisecond +} + +// amrDurationMs 是 amrDuration 的毫秒数形式(DTO 用)。 +func amrDurationMs(frames int) int64 { + return int64(frames) * amrFrameMillis +} diff --git a/internal/capability/transcode/codec_amr_nb_test.go b/internal/capability/transcode/codec_amr_nb_test.go new file mode 100644 index 0000000..11b1a21 --- /dev/null +++ b/internal/capability/transcode/codec_amr_nb_test.go @@ -0,0 +1,346 @@ +package transcode + +import ( + "context" + "encoding/binary" + "errors" + "math" + "os" + "path/filepath" + "testing" + "time" +) + +// 黄金向量(design/25 §4.1 采纳 opencore-amrnb 为正确性基准、§8-T2 的验收判据)。 +// +// 夹具怎么来的(可复现;生成命令原文,2026-10-05 在 Windows + ffmpeg 8.1.1 上执行): +// +// FF=/d/Bak/PROLIB/ffmpeg-8.1.1-full_build/bin/ffmpeg +// SIG="aevalsrc=0.6*sin(2*PI*440*t)*(0.6+0.4*sin(2*PI*3*t))+0.2*sin(2*PI*1200*t):s=8000:d=0.5" +// for br in 4750 5150 5900 6700 7400 7950 10200 12200; do +// $FF -f lavfi -i "$SIG" -ac 1 -c:a libopencore_amrnb -b:a $br -y mode$br.amr +// $FF -c:a libopencore_amrnb -i mode$br.amr -f s16le -ac 1 -ar 8000 -y mode$br.pcm +// done +// # mixed_modes.amr = 把 8 个文件按帧交错拼接(每帧自带 ToC,故混合模式合法流) +// +// 样例是**自造的合成信号**(三音正弦 + 幅度包络),不是任何真实语音;参考 PCM 由 +// 本机 ffmpeg 的 `libopencore_amrnb` 解码器产出(`-f s16le -ar 8000 -ac 1`)。 +// 夹具体积很小(AMR ≈3 KB + PCM ≈75 KB),因此**测试不需要装 ffmpeg**。 +// +// 断言强度(**逐样本**,不是逐字节——原因见下): +// +// vendored 的 goamr-nb 是**部分移植**(其 dsp_decoder.go:13 自述 +// Cb_gain_average 与自适应相位色散未移植),实测与 opencore-amrnb +// **逐字节不一致**。因此硬套「逐字节相等」会是一条永远红的假断言。 +// 改成:帧数 / 样本数 / 时长**精确相等**(这一条仍是逐样本级的强断言), +// 再对全部样本逐个做「绝对差 ≤ goldenMaxAbsDiff」+ 全局 SNR / 相关系数 / +// 能量比三档下限。实测基线与阈值的余量记录在下面的常量旁。 +const ( + // goldenMinSNRDB 实测最差 16.0 dB(mode10200),阈值留 ~25% 余量。 + goldenMinSNRDB = 12.0 + // goldenMaxAbsDiff 实测最差 11192(mixed_modes),阈值取满量程的一半。 + goldenMaxAbsDiff = 16384 + // goldenMinCorrelation 实测最差 0.9874,阈值 0.97。 + goldenMinCorrelation = 0.97 + // goldenMaxRMSDrift 实测最差 |1−rms 比| = 0.021,阈值 0.05。 + goldenMaxRMSDrift = 0.05 +) + +// goldenCases 是把夹具名映射到「人类可读说明」的表:8 个编码模式 + 1 个混合模式。 +var goldenCases = []struct { + name string + what string +}{ + {"mode4750", "AMR-NB 4.75 kbit/s(MR475)"}, + {"mode5150", "AMR-NB 5.15 kbit/s(MR515)"}, + {"mode5900", "AMR-NB 5.90 kbit/s(MR59)"}, + {"mode6700", "AMR-NB 6.70 kbit/s(MR67)"}, + {"mode7400", "AMR-NB 7.40 kbit/s(MR74)"}, + {"mode7950", "AMR-NB 7.95 kbit/s(MR795)"}, + {"mode10200", "AMR-NB 10.2 kbit/s(MR102)"}, + {"mode12200", "AMR-NB 12.2 kbit/s(MR122)"}, + {"mixed_modes", "逐帧交错的混合模式流"}, +} + +// TestGoldenAgainstOpenCORE 对每个夹具做一次完整转码,再逐样本与参考 PCM 比对。 +func TestGoldenAgainstOpenCORE(t *testing.T) { + eng := NewEngine(DefaultLimits(), WithCodecs(DefaultCodecs()...)) + if err := eng.RegistrationError(); err != nil { + t.Fatalf("codec registration: %v", err) + } + for _, tc := range goldenCases { + t.Run(tc.name, func(t *testing.T) { + src := readFixture(t, tc.name+".amr") + want := readFixture(t, tc.name+".pcm") + res, err := eng.Transcode(context.Background(), Request{ + Data: src, Source: FormatAMR, Target: FormatWAV, + }) + if err != nil { + t.Fatalf("transcode %s (%s): %v", tc.name, tc.what, err) + } + wantSamples := len(want) / wavPCMBytesPerSample + assertGoldenMeta(t, res, wantSamples) + got := wavDataSamples(t, res.Data) + if len(got) != wantSamples { + t.Fatalf("sample count: got %d, want %d (逐样本对照的前提)", len(got), wantSamples) + } + assertSamplesClose(t, got, int16Samples(want), tc.name) + }) + } +} + +// assertGoldenMeta 断言与参考 PCM 精确一致的那部分元信息:样本数、帧数、时长、 +// 目标参数。这几项是**逐样本级**的强断言,不掺容差。 +func assertGoldenMeta(t *testing.T, res Result, wantSamples int) { + t.Helper() + if res.Format != FormatWAV || res.Source != FormatAMR { + t.Errorf("formats: got %s -> %s, want amr -> wav", res.Source, res.Format) + } + if res.SampleRate != amrSampleRate || res.Channels != amrChannels { + t.Errorf("params: got %d Hz / %d ch, want %d Hz / %d ch", + res.SampleRate, res.Channels, amrSampleRate, amrChannels) + } + wantFrames := wantSamples / amrFrameSamples + if res.Frames != wantFrames { + t.Errorf("frames: got %d, want %d", res.Frames, wantFrames) + } + if wantMs := int64(wantFrames * amrFrameMillis); res.DurationMs != wantMs { + t.Errorf("duration: got %d ms, want %d ms", res.DurationMs, wantMs) + } + if len(res.Data) != wavHeaderBytes+wantSamples*wavPCMBytesPerSample { + t.Fatalf("wav size: got %d, want %d", len(res.Data), wavHeaderBytes+wantSamples*wavPCMBytesPerSample) + } +} + +// assertSamplesClose 逐样本比对:绝对差上限 + SNR + 相关系数 + 能量比。 +func assertSamplesClose(t *testing.T, got, want []int16, name string) { + t.Helper() + stat := comparePCM(got, want) + if stat.maxAbsDiff > goldenMaxAbsDiff { + t.Errorf("%s: max |diff| = %d > %d", name, stat.maxAbsDiff, goldenMaxAbsDiff) + } + if stat.snrDB < goldenMinSNRDB { + t.Errorf("%s: SNR = %.1f dB < %.1f dB", name, stat.snrDB, goldenMinSNRDB) + } + if stat.corr < goldenMinCorrelation { + t.Errorf("%s: correlation = %.5f < %.5f", name, stat.corr, goldenMinCorrelation) + } + if math.Abs(1-stat.rmsRatio) > goldenMaxRMSDrift { + t.Errorf("%s: rms ratio = %.3f, drift > %.3f", name, stat.rmsRatio, goldenMaxRMSDrift) + } + t.Logf("%s: frames=%d snr=%.1f dB max|d|=%d corr=%.5f rms_ratio=%.4f", + name, len(want)/amrFrameSamples, stat.snrDB, stat.maxAbsDiff, stat.corr, stat.rmsRatio) +} + +// pcmStat 是一次比对的结果(逐样本统计量)。 +type pcmStat struct { + snrDB float64 + maxAbsDiff int + corr float64 + rmsRatio float64 +} + +// comparePCM 逐样本统计 got 与 want 的差、SNR、相关系数与能量比。 +func comparePCM(got, want []int16) pcmStat { + var sig, noise, dot, gotPower, wantPower float64 + stat := pcmStat{} + for i := range got { + g, w := float64(got[i]), float64(want[i]) + diff := g - w + if d := int(math.Abs(diff)); d > stat.maxAbsDiff { + stat.maxAbsDiff = d + } + sig += w * w + noise += diff * diff + dot += g * w + gotPower += g * g + wantPower += w * w + } + stat.snrDB = snrDB(sig, noise) + stat.corr = correlation(dot, gotPower, wantPower) + stat.rmsRatio = math.Sqrt(gotPower / wantPower) + return stat +} + +// snrDB 算信号/噪声能量比(dB);噪声为零时返回 +Inf(完全一致就该是 +Inf)。 +func snrDB(signal, noise float64) float64 { + if noise == 0 { + return math.Inf(1) + } + return 10 * math.Log10(signal/noise) +} + +// correlation 算归一化相关系数;任一序列零能量时返回 0。 +func correlation(dot, aPower, bPower float64) float64 { + if aPower == 0 || bPower == 0 { + return 0 + } + return dot / math.Sqrt(aPower*bPower) +} + +// readFixture 读一份夹具,并断言它非空(夹具被清空/截断时测试立刻红)。 +func readFixture(t *testing.T, name string) []byte { + t.Helper() + data, err := os.ReadFile(filepath.Join("testdata", name)) + if err != nil { + t.Fatalf("fixture %s: %v", name, err) + } + if len(data) == 0 { + t.Fatalf("fixture %s is empty", name) + } + return data +} + +// wavDataSamples 从一份 WAV 里取出 PCM16 样本(只认本包写出的 44 字节头形态)。 +func wavDataSamples(t *testing.T, wav []byte) []int16 { + t.Helper() + if len(wav) < wavHeaderBytes { + t.Fatalf("wav too short: %d bytes", len(wav)) + } + if string(wav[wavOffRiffID:wavOffChunkSize]) != wavRiffID || string(wav[wavOffWaveID:wavOffFmtID]) != wavWaveID { + t.Fatalf("wav: bad RIFF/WAVE magic") + } + if string(wav[wavOffFmtID:wavOffFmtSize]) != wavFmtID || string(wav[wavOffDataID:wavOffDataSize]) != wavDataID { + t.Fatalf("wav: bad fmt/data chunk ids") + } + size := int(binary.LittleEndian.Uint32(wav[wavOffDataSize:])) + if size != len(wav)-wavHeaderBytes { + t.Fatalf("wav: data size %d != payload %d", size, len(wav)-wavHeaderBytes) + } + out := make([]int16, size/wavPCMBytesPerSample) + for i := range out { + out[i] = int16(binary.LittleEndian.Uint16(wav[wavHeaderBytes+i*wavPCMBytesPerSample:])) + } + return out +} + +// int16Samples 把小端 PCM16 字节转成样本。 +func int16Samples(pcm []byte) []int16 { + out := make([]int16, len(pcm)/wavPCMBytesPerSample) + for i := range out { + out[i] = int16(binary.LittleEndian.Uint16(pcm[i*wavPCMBytesPerSample:])) + } + return out +} + +// TestAMRRejectsBrokenInput 覆盖 design/25 §8-T2 要求的边界样本:空文件、坏头、 +// 截断帧、保留帧类型、只有魔数。它们一律 ErrDecodeFailed(「坏了」,与「不会」 +// 的 ErrUnsupportedFormat 分开),且绝不返回半份结果。 +func TestAMRRejectsBrokenInput(t *testing.T) { + codec := amrNBToWAV{} + good := readFixture(t, "mode12200.amr") + headerOnly := []byte(amrNBMagic) + badMagic := append([]byte("XXXXXX"), good[amrStorageHeaderBytes:]...) + truncated := good[:len(good)-1] + reservedFT := append([]byte(amrNBMagic), 0x48, 0x00) // FT=9(保留值) + + cases := []struct { + name string + data []byte + }{ + {"empty file", nil}, + {"magic only", headerOnly}, + {"bad magic", badMagic}, + {"truncated last frame", truncated}, + {"reserved frame type", reservedFT}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + res, err := codec.Transcode(context.Background(), tc.data, DefaultLimits()) + if !errors.Is(err, ErrDecodeFailed) { + t.Fatalf("err = %v, want ErrDecodeFailed", err) + } + if len(res.Data) != 0 { + t.Fatalf("broken input produced %d bytes of output", len(res.Data)) + } + }) + } +} + +// TestAMRSilentFramesPinned 把「已知边界」钉住:SID(FT 8)与 no-data(FT 15) +// 帧在 vendored 解码器里产出 **160 样本静音**(错误隐藏/CNG 未移植,见 +// amrnb/VENDORED.md)。这条测试不假装它做得好——只是把行为写死,避免悄悄漂移。 +func TestAMRSilentFramesPinned(t *testing.T) { + codec := amrNBToWAV{} + // FT=15(no data):ToC = 15<<3 = 0x78,零字节载荷;两帧。 + noData := append([]byte(amrNBMagic), 0x78, 0x78) + res, err := codec.Transcode(context.Background(), noData, DefaultLimits()) + if err != nil { + t.Fatalf("no-data stream: %v", err) + } + if res.Frames != 2 || res.DurationMs != 2*amrFrameMillis { + t.Fatalf("no-data stream: frames=%d duration=%d", res.Frames, res.DurationMs) + } + samples := wavDataSamples(t, res.Data) + if len(samples) != 2*amrFrameSamples { + t.Fatalf("no-data stream: %d samples, want %d", len(samples), 2*amrFrameSamples) + } + for i, s := range samples { + if s != 0 { + t.Fatalf("no-data stream: sample %d = %d, want silence (CNG 未移植)", i, s) + } + } + // FT=8(SID):ToC = 8<<3 = 0x40 + 5 字节载荷 ⇒ 仍是一帧静音。 + sid := append([]byte(amrNBMagic), 0x40, 0, 0, 0, 0, 0) + res, err = codec.Transcode(context.Background(), sid, DefaultLimits()) + if err != nil { + t.Fatalf("sid stream: %v", err) + } + if res.Frames != 1 { + t.Fatalf("sid stream: frames=%d, want 1", res.Frames) + } +} + +// TestAMRDeterministic 断言同一输入两次转码逐字节相同(纯函数契约)。 +func TestAMRDeterministic(t *testing.T) { + codec := amrNBToWAV{} + in := readFixture(t, "mixed_modes.amr") + first, err := codec.Transcode(context.Background(), in, DefaultLimits()) + if err != nil { + t.Fatalf("first pass: %v", err) + } + second, err := codec.Transcode(context.Background(), in, DefaultLimits()) + if err != nil { + t.Fatalf("second pass: %v", err) + } + if len(first.Data) != len(second.Data) { + t.Fatalf("size drift: %d vs %d", len(first.Data), len(second.Data)) + } + for i := range first.Data { + if first.Data[i] != second.Data[i] { + t.Fatalf("byte %d drifted: %d vs %d", i, first.Data[i], second.Data[i]) + } + } +} + +// TestAMRLimitsRejectedBeforeDecode 断言「格式相关」的两层闸:可确证的时长与 +// 精确可算的产物字节数都在**解码前**判定(因此报的是上限哨兵,而不是解到一半 +// 才发现)。 +func TestAMRLimitsRejectedBeforeDecode(t *testing.T) { + codec := amrNBToWAV{} + in := readFixture(t, "mode12200.amr") // 26 帧 = 520 ms + limits := DefaultLimits() + + t.Run("duration over limit", func(t *testing.T) { + l := limits + l.MaxDuration = time.Duration(10*amrFrameMillis) * time.Millisecond + if _, err := codec.Transcode(context.Background(), in, l); !errors.Is(err, ErrInputTooLarge) { + t.Fatalf("err = %v, want ErrInputTooLarge", err) + } + }) + t.Run("output over limit", func(t *testing.T) { + l := limits + l.MaxOutputBytes = wavHeaderBytes + 10*amrFrameSamples*wavPCMBytesPerSample + if _, err := codec.Transcode(context.Background(), in, l); !errors.Is(err, ErrOutputTooLarge) { + t.Fatalf("err = %v, want ErrOutputTooLarge", err) + } + }) + t.Run("deadline respected", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond) + defer cancel() + <-ctx.Done() + if _, err := codec.Transcode(ctx, in, limits); !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("err = %v, want context.DeadlineExceeded(由 engine 归为 ErrTimeout)", err) + } + }) +} diff --git a/internal/capability/transcode/codecs.go b/internal/capability/transcode/codecs.go new file mode 100644 index 0000000..8194293 --- /dev/null +++ b/internal/capability/transcode/codecs.go @@ -0,0 +1,16 @@ +package transcode + +// DefaultCodecs 是全仓**唯一**的编解码器注册点(design/25 §6.3): +// +// 新增一种格式 = 在下面追加一行(+ 一个实现文件 + 一个 Format 常量)。 +// +// 本期只发一条路:`amr → wav`(design/25 §3.1)。`silk` 不做、`amr-wb` 后置、 +// 编码方向不做、无界重采样/音量归一/降噪不做(§3.3 逐条有理由)。 +// +// 一条单测把它的条数钉死在设计上(见 codecs_test.go 的 designPublishedRoutes), +// 防止「加了实现却忘了注册」(或反之)。 +func DefaultCodecs() []Codec { + return []Codec{ + amrNBToWAV{}, + } +} diff --git a/internal/capability/transcode/codecs_test.go b/internal/capability/transcode/codecs_test.go new file mode 100644 index 0000000..5cda50c --- /dev/null +++ b/internal/capability/transcode/codecs_test.go @@ -0,0 +1,48 @@ +package transcode + +import "testing" + +// designPublishedRoutes 是 design/25 §3.1「格式矩阵」里**已发布**的路数: +// 本期只有一条 —— `amr → wav`。 +// +// 这条常量刻意写在测试里:谁加了实现却忘了在 DefaultCodecs 注册(或反过来), +// 这条断言就会红,逼他回设计里对一次账。这就是 design/25 §6.3 要的「把值钉死在 +// 设计上」,与只测边界逻辑互补。 +const designPublishedRoutes = 1 + +// TestDefaultCodecsMatchesPublishedMatrix 断言注册点与设计矩阵一致。 +func TestDefaultCodecsMatchesPublishedMatrix(t *testing.T) { + codecs := DefaultCodecs() + if len(codecs) != designPublishedRoutes { + t.Fatalf("DefaultCodecs() registers %d routes, design/25 §3.1 publishes %d: "+ + "加了实现要在此处同步注册(并回设计对账)", len(codecs), designPublishedRoutes) + } + route := codecs[0] + if route.From() != FormatAMR || route.To() != FormatWAV { + t.Fatalf("DefaultCodecs()[0] = %s -> %s, want amr -> wav", route.From(), route.To()) + } +} + +// TestEnginePublishesOnlyTheDesignRoute 断言引擎对外报告的路也是那一条: +// Supports 只认 amr→wav,Formats 只报 amr/wav。 +func TestEnginePublishesOnlyTheDesignRoute(t *testing.T) { + eng := NewEngine(DefaultLimits(), WithCodecs(DefaultCodecs()...)) + if err := eng.RegistrationError(); err != nil { + t.Fatalf("registration: %v", err) + } + if !eng.Supports(FormatAMR, FormatWAV) { + t.Fatalf("amr -> wav must be supported") + } + for _, pair := range [][2]Format{ + {FormatWAV, FormatAMR}, {FormatAMR, FormatAMR}, {FormatWAV, FormatWAV}, + {FormatAMRWB, FormatWAV}, {FormatAMR, FormatAMRWB}, + } { + if eng.Supports(pair[0], pair[1]) { + t.Errorf("Supports(%q, %q) = true, want false(本期只发 amr -> wav)", pair[0], pair[1]) + } + } + formats := eng.Formats() + if len(formats) != 2 || formats[0] != FormatAMR || formats[1] != FormatWAV { + t.Fatalf("Formats = %v, want [amr wav]", formats) + } +} diff --git a/internal/capability/transcode/engine.go b/internal/capability/transcode/engine.go new file mode 100644 index 0000000..6442326 --- /dev/null +++ b/internal/capability/transcode/engine.go @@ -0,0 +1,262 @@ +package transcode + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" +) + +// Engine 是 Transcoder 的默认实现:把「格式无关」的那一层(输入字节上限、 +// 并发信号量、单次超时、日志)收在一处,把「格式相关」的那一层交给注册进来的 +// Codec(design/25 §1.3.2 / §2.2)。 +// +// 它是普通结构体,由组合根构造并注入;**没有包级全局变量** +// (AGENTS §10 红线)。它可安全并发使用。 +type Engine struct { + limits Limits + reg *Registry + // sem 是全局并发信号量。容量 = Limits.Concurrency;槽满即 ErrBusy + // (快速失败、不入队)。 + sem chan struct{} + log *slog.Logger + // codecs 是选项里攒下的待注册列表,在 NewEngine 末尾统一注册(见 WithCodecs)。 + codecs []Codec + // regErr 记录注册期的第一个错误(不该发生:DefaultCodecs 是已知良好的)。 + // 它不阻塞构造,但绝不静默——组合根可读 RegistrationError 并告警。 + regErr error +} + +// Compile-time assertion that *Engine satisfies the Transcoder interface. +var _ Transcoder = (*Engine)(nil) + +// Option 配置 Engine。 +type Option func(*Engine) + +// WithCodecs 把 codecs 交给引擎(在 NewEngine 的所有选项都应用完之后统一注册, +// 这样即使调用方按 design/25 §6.1 的顺序先写 WithCodecs 再写 WithLogger, +// 注册失败的告警也能落到正确的日志器上)。注册失败(重复 / 不合法)是程序错误: +// 记 Error 日志并留在 RegistrationError 里,绝不静默吞掉。 +func WithCodecs(codecs ...Codec) Option { + return func(e *Engine) { + e.codecs = append(e.codecs, codecs...) + } +} + +// WithLogger 设置引擎日志。日志一律带 source=capability/transcode +// (AGENTS §10:日志必须标来源)。 +func WithLogger(l *slog.Logger) Option { + return func(e *Engine) { + e.log = loggerFor(l) + } +} + +// loggerFor 把 nil 日志收敛成一个丢弃日志:本包不因为「没人给日志」而崩, +// 也不因为「没人给日志」而少一次调用记录(默认丢弃,责任在组合根)。 +func loggerFor(l *slog.Logger) *slog.Logger { + if l == nil { + return slog.New(slog.DiscardHandler) + } + return l +} + +// NewEngine 构造一个引擎。limits 的零值字段由 DefaultLimits 补足; +// 未给 WithCodecs 时注册表为空 ⇒ Supports 恒 false、Transcode 恒 +// ErrUnsupportedFormat(design/25 §8-T1 的空注册表行为)。 +func NewEngine(limits Limits, opts ...Option) *Engine { + e := &Engine{ + limits: limits.withDefaults(), + reg: NewRegistry(), + sem: make(chan struct{}, limits.withDefaults().Concurrency), + log: loggerFor(nil), + } + for _, opt := range opts { + if opt != nil { + opt(e) + } + } + e.registerCodecs() + return e +} + +// registerCodecs 注册 WithCodecs 攒下的编解码器(在所有选项都生效之后)。 +func (e *Engine) registerCodecs() { + for _, c := range e.codecs { + if err := e.reg.Register(c); err != nil { + if e.regErr == nil { + e.regErr = err + } + e.log.Error("transcode codec registration skipped", "error", err) + } + } + e.codecs = nil +} + +// RegistrationError 返回构造期第一个编解码器注册错误(正常为 nil)。 +func (e *Engine) RegistrationError() error { return e.regErr } + +// Limits 返回生效的限额(零值已补默认)。 +func (e *Engine) Limits() Limits { return e.limits } + +// Supports 报告 src→dst 这条路是否可用(能力门)。 +func (e *Engine) Supports(src, dst Format) bool { + _, ok := e.reg.Lookup(src, dst) + return ok +} + +// Formats 报告当前注册的所有格式(发现)。 +func (e *Engine) Formats() []Format { return e.reg.Formats() } + +// Transcode 把 req.Data 从 req.Source 转成 req.Target(design/25 §2.1)。 +// +// 失败一律返回 errors.go 的哨兵之一(或包裹它)——ctx 截止返回 ErrTimeout, +// 调用方主动取消返回 context.Canceled 的包裹(取消是调用方自己的决定, +// 与「转码失败」不是一回事)。进入编解码器之前的每一道闸都在**解码前**: +// 未知格式、越限、槽满都不会解出一个字节。 +// +// ctx 为 nil 时按 context.Background() 处理;ctx 超时是硬闸,绝不无界等待, +// 也不泄漏 goroutine(本调用全程在调用方 goroutine 里跑完)。 +func (e *Engine) Transcode(ctx context.Context, req Request) (Result, error) { + start := time.Now() + if ctx == nil { + ctx = context.Background() + } + src, err := e.resolveSource(req) + if err != nil { + return Result{}, e.reject(ctx, req, src, err, start) + } + if req.Target == "" { + return Result{}, e.reject(ctx, req, src, + fmt.Errorf("%w: empty target format", ErrUnsupportedFormat), start) + } + codec, ok := e.reg.Lookup(src, req.Target) + if !ok { + return Result{}, e.reject(ctx, req, src, + fmt.Errorf("%w: no codec registered", ErrUnsupportedFormat), start) + } + if len(req.Data) > e.limits.MaxInputBytes { + return Result{}, e.reject(ctx, req, src, + fmt.Errorf("%w: %d bytes > %d", ErrInputTooLarge, len(req.Data), e.limits.MaxInputBytes), start) + } + if err := ctx.Err(); err != nil { + return Result{}, e.reject(ctx, req, src, timeoutError(err), start) + } + if !e.acquire() { + return Result{}, e.reject(ctx, req, src, + fmt.Errorf("%w: %d slots busy", ErrBusy, e.limits.Concurrency), start) + } + defer e.release() + + callCtx, cancel := context.WithTimeout(ctx, e.limits.Timeout) + defer cancel() + res, err := codec.Transcode(callCtx, req.Data, e.limits) + if err != nil { + return Result{}, e.reject(ctx, req, src, classify(callCtx, err), start) + } + if len(res.Data) == 0 { + return Result{}, e.reject(ctx, req, src, + fmt.Errorf("%w: codec returned empty output", ErrEncodeFailed), start) + } + e.log.Info("transcode ok", + "source_format", FormatName(res.Source), + "target_format", FormatName(res.Format), + "in_bytes", len(req.Data), + "out_bytes", len(res.Data), + "frames", res.Frames, + "duration_ms", res.DurationMs, + "elapsed_ms", time.Since(start).Milliseconds()) + return res, nil +} + +// resolveSource 定出这次请求的实际来源格式:显式给出就用它,留空则按魔数嗅探, +// 嗅不出即 ErrUnsupportedFormat(deny-by-default)。 +func (e *Engine) resolveSource(req Request) (Format, error) { + if req.Source != "" { + return req.Source, nil + } + sniffed, ok := Sniff(req.Data) + if !ok { + return "", fmt.Errorf("%w: magic not recognized", ErrUnsupportedFormat) + } + return sniffed, nil +} + +// acquire 非阻塞地取一个并发槽。拿不到即 false——绝不排队。 +func (e *Engine) acquire() bool { + select { + case e.sem <- struct{}{}: + return true + default: + return false + } +} + +// release 归还一个并发槽。 +func (e *Engine) release() { <-e.sem } + +// reject 记一次失败日志并把错误包上**路由上下文**后返回(绝不静默、绝不裸返回): +// 最终文案形如 `transcode: amr -> wav: unsupported format: no codec registered`。 +// 可预期的输入问题(哨兵)记 Warn;不该发生的(非哨兵:编解码器违约)记 Error。 +func (e *Engine) reject(ctx context.Context, req Request, src Format, err error, start time.Time) error { + level := slog.LevelWarn + if !isSentinelError(err) { + level = slog.LevelError + } + e.log.Log(ctx, level, "transcode failed", + "source_format", FormatName(src), + "target_format", FormatName(req.Target), + "in_bytes", len(req.Data), + "elapsed_ms", time.Since(start).Milliseconds(), + "error", err) + return fmt.Errorf("transcode: %s: %w", routeLabel(src, req.Target), err) +} + +// routeLabel 是错误/日志里的路由可读名;格式未知(嗅探失败 / 目标留空)时用 +// `` 占位,绝不留一段空白让人猜。 +func routeLabel(src, dst Format) string { + from, to := FormatName(src), FormatName(dst) + if from == "" { + from = "" + } + if to == "" { + to = "" + } + return from + " -> " + to +} + +// classify 把编解码器返回的错误收敛到 design/25 §2.4 的口径:ctx 截止即 +// ErrTimeout;调用方取消保持 context.Canceled 的包裹;其余补一层来源后上抛 +// (编解码器自己用哨兵,engine 不替它猜,但也不裸返回)。 +func classify(callCtx context.Context, err error) error { + switch { + case errors.Is(err, context.DeadlineExceeded), errors.Is(callCtx.Err(), context.DeadlineExceeded): + return timeoutError(context.DeadlineExceeded) + case errors.Is(err, context.Canceled), errors.Is(callCtx.Err(), context.Canceled): + return fmt.Errorf("canceled: %w", context.Canceled) + default: + return fmt.Errorf("codec failed: %w", err) + } +} + +// timeoutError 把 ctx 截止统一成 ErrTimeout 的包裹,并保留原始原因。 +func timeoutError(cause error) error { + return fmt.Errorf("%w: %w", ErrTimeout, cause) +} + +// isSentinelError 报告 err 是否属于本包已登记的哨兵(含包内构造期错误)。 +// 逐条列举而不是查一个包级切片:本包因此**没有任何包级状态** +// (AGENTS §10:禁包级全局变量,除 var ErrXxx)。 +func isSentinelError(err error) bool { + return errors.Is(err, ErrUnsupportedFormat) || + errors.Is(err, ErrInputTooLarge) || + errors.Is(err, ErrOutputTooLarge) || + errors.Is(err, ErrDecodeFailed) || + errors.Is(err, ErrEncodeFailed) || + errors.Is(err, ErrTimeout) || + errors.Is(err, ErrBusy) || + errors.Is(err, ErrNoBackend) || + errors.Is(err, ErrDuplicate) || + errors.Is(err, ErrInvalidCodec) || + errors.Is(err, ErrBadArgs) +} diff --git a/internal/capability/transcode/engine_test.go b/internal/capability/transcode/engine_test.go new file mode 100644 index 0000000..3896e7b --- /dev/null +++ b/internal/capability/transcode/engine_test.go @@ -0,0 +1,360 @@ +package transcode + +import ( + "bytes" + "context" + "errors" + "fmt" + "log/slog" + "strings" + "sync/atomic" + "testing" + "time" + + "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" +) + +// stubCodec 是 engine 测试用的编解码器桩:它不碰真实音频,只记录调用次数并按 +// 需要阻塞 / 报错,因此可以精确检验「闸在解码之前」这类顺序性断言。 +type stubCodec struct { + from, to Format + calls atomic.Int32 + // entered 在 Transcode 进入时被关闭(并发用例用来确认槽已被占)。 + entered chan struct{} + // block 非 nil 时,Transcode 会等到它被关闭或 ctx 结束。 + block chan struct{} + // waitCtx 为 true 时,Transcode 会一直等到 ctx 结束(超时用例)。 + waitCtx bool + err error + result Result +} + +func newStubCodec() *stubCodec { return &stubCodec{from: FormatAMR, to: FormatWAV} } + +func (c *stubCodec) From() Format { return c.from } +func (c *stubCodec) To() Format { return c.to } + +func (c *stubCodec) Transcode(ctx context.Context, in []byte, _ Limits) (Result, error) { + c.calls.Add(1) + if c.entered != nil { + select { + case c.entered <- struct{}{}: + default: + } + } + if c.block != nil { + select { + case <-c.block: + case <-ctx.Done(): + return Result{}, ctx.Err() + } + } + if c.waitCtx { + <-ctx.Done() + return Result{}, ctx.Err() + } + if c.err != nil { + return Result{}, c.err + } + res := c.result + if res.Data == nil { + res.Data = in + } + if res.Source == "" { + res.Source = c.from + } + if res.Format == "" { + res.Format = c.to + } + return res, nil +} + +// amrStubBytes 是一段「魔数与目标路都对得上」的最小输入。 +func amrStubBytes(payload ...byte) []byte { + return append([]byte(amrNBMagic), payload...) +} + +// TestEngineEmptyRegistry 是 design/25 §8-T1 的空注册表行为:Supports 恒 false、 +// Transcode 恒 ErrUnsupportedFormat(且 Formats 为空)。 +func TestEngineEmptyRegistry(t *testing.T) { + eng := NewEngine(DefaultLimits()) + if eng.Supports(FormatAMR, FormatWAV) { + t.Fatalf("empty registry reports support for amr -> wav") + } + if got := eng.Formats(); len(got) != 0 { + t.Fatalf("empty registry formats = %v, want none", got) + } + _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }) + if !errors.Is(err, ErrUnsupportedFormat) { + t.Fatalf("err = %v, want ErrUnsupportedFormat", err) + } +} + +// TestEngineInputTooLargeDecodesNothing 断言输入字节闸在**编解码器之前**:桩一次都 +// 没被调用(design/25 §8-T1「越限 ⇒ ErrInputTooLarge 且一行都没解码」)。 +func TestEngineInputTooLargeDecodesNothing(t *testing.T) { + stub := newStubCodec() + limits := DefaultLimits() + limits.MaxInputBytes = len(amrNBMagic) + eng := NewEngine(limits, WithCodecs(stub)) + data := amrStubBytes(0x3c, 0x00) + _, err := eng.Transcode(context.Background(), Request{Data: data, Source: FormatAMR, Target: FormatWAV}) + if !errors.Is(err, ErrInputTooLarge) { + t.Fatalf("err = %v, want ErrInputTooLarge", err) + } + if n := stub.calls.Load(); n != 0 { + t.Fatalf("codec was invoked %d times before the input-size gate", n) + } +} + +// TestEngineBusyFailsFast 断言并发槽满时**快速失败、不入队**(design/25 §5.2), +// 且槽随后会被归还。 +func TestEngineBusyFailsFast(t *testing.T) { + stub := newStubCodec() + stub.block = make(chan struct{}) + stub.entered = make(chan struct{}, 1) + limits := DefaultLimits() + limits.Concurrency = 1 + eng := NewEngine(limits, WithCodecs(stub)) + + done := make(chan error, 1) + go func() { + _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }) + done <- err + }() + <-stub.entered + + start := time.Now() + _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }) + if !errors.Is(err, ErrBusy) { + t.Fatalf("err = %v, want ErrBusy", err) + } + if elapsed := time.Since(start); elapsed > busyFailBudget { + t.Fatalf("busy rejection took %s (should fail fast, not queue)", elapsed) + } + close(stub.block) + if err := <-done; err != nil { + t.Fatalf("first call: %v", err) + } + if _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }); err != nil { + t.Fatalf("slot not released after the first call: %v", err) + } +} + +// busyFailBudget 是「快速失败」的时间预算:ErrBusy 必须来自非阻塞的 select, +// 而不是等出来的。取值远大于调度抖动(同一台机器上实测 <1ms)。 +const busyFailBudget = 500 * time.Millisecond + +// TestEngineTimeout 断言单次 ctx 超时是硬闸:超时即 ErrTimeout,且调用方拿得到 +// (不无界等待、不泄漏 goroutine)。 +func TestEngineTimeout(t *testing.T) { + stub := newStubCodec() + stub.waitCtx = true + limits := DefaultLimits() + limits.Timeout = 20 * time.Millisecond + eng := NewEngine(limits, WithCodecs(stub)) + start := time.Now() + _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }) + if !errors.Is(err, ErrTimeout) { + t.Fatalf("err = %v, want ErrTimeout", err) + } + if elapsed := time.Since(start); elapsed > time.Second { + t.Fatalf("timeout took %s, want ~%s", elapsed, limits.Timeout) + } +} + +// TestEngineRejectsBadRequests 覆盖「进入编解码器之前」的几种诚实拒绝。 +func TestEngineRejectsBadRequests(t *testing.T) { + eng := NewEngine(DefaultLimits(), WithCodecs(newStubCodec())) + cases := []struct { + name string + req Request + }{ + {"empty target", Request{Data: amrStubBytes(0x3c), Source: FormatAMR}}, + {"source not sniffable", Request{Data: []byte("not audio at all"), Target: FormatWAV}}, + {"no route for sniffed source", Request{Data: []byte("RIFF\x00\x00\x00\x00WAVE"), Target: FormatWAV}}, + {"unknown source name", Request{Data: amrStubBytes(0x3c), Source: Format("mp3"), Target: FormatWAV}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := eng.Transcode(context.Background(), tc.req) + if !errors.Is(err, ErrUnsupportedFormat) { + t.Fatalf("err = %v, want ErrUnsupportedFormat", err) + } + }) + } +} + +// TestEngineSniffsSourceWhenOmitted 断言 Source 留空时按魔数嗅探并回填到 Result。 +func TestEngineSniffsSourceWhenOmitted(t *testing.T) { + eng := NewEngine(DefaultLimits(), WithCodecs(newStubCodec())) + res, err := eng.Transcode(context.Background(), Request{Data: amrStubBytes(0x3c), Target: FormatWAV}) + if err != nil { + t.Fatalf("transcode: %v", err) + } + if res.Source != FormatAMR { + t.Fatalf("sniffed source = %q, want amr", res.Source) + } +} + +// TestEngineCodecContractViolations 断言「不该发生的」被当成失败:空产物 ⇒ +// ErrEncodeFailed;非哨兵错误 ⇒ 原样上抛(但记 ERROR 级日志,绝不静默)。 +func TestEngineCodecContractViolations(t *testing.T) { + t.Run("empty output", func(t *testing.T) { + stub := newStubCodec() + stub.result = Result{Data: []byte{}} + stub.err = nil + eng := NewEngine(DefaultLimits(), WithCodecs(stub)) + _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }) + if !errors.Is(err, ErrEncodeFailed) { + t.Fatalf("err = %v, want ErrEncodeFailed", err) + } + }) + t.Run("non-sentinel error is not swallowed", func(t *testing.T) { + var buf bytes.Buffer + stub := newStubCodec() + stub.err = errBoom + eng := NewEngine(DefaultLimits(), WithCodecs(stub), WithLogger(testLogger(&buf))) + _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }) + if !errors.Is(err, errBoom) { + t.Fatalf("err = %v, want the codec error to pass through", err) + } + assertLogContains(t, &buf, "level=ERROR", "transcode failed", "source=capability/transcode") + }) +} + +// errBoom 是「编解码器违约」用的非哨兵错误。 +var errBoom = errors.New("boom") + +// testLogger 造一个按组合根口径接线的日志器(source=capability/transcode)。 +func testLogger(buf *bytes.Buffer) *slog.Logger { + return logger.WithSource(slog.New(slog.NewTextHandler(buf, nil)), logSource) +} + +// logSource 是本能力的日志来源(design/25 §5.3;组合根必须照此接线)。 +const logSource = "capability/transcode" + +// assertLogContains 断言日志里出现了给定的每一段子串。 +func assertLogContains(t *testing.T, buf *bytes.Buffer, wants ...string) { + t.Helper() + got := buf.String() + for _, want := range wants { + if !strings.Contains(got, want) { + t.Fatalf("log missing %q; got:\n%s", want, got) + } + } +} + +// TestEngineLogsEveryOutcome 断言 design/25 §5.3 要求的字段与来源都在: +// 成功记 Info(含 in/out 字节数、帧数、时长、耗时),失败记 Warn。 +// 同时断言「只记长度、不记字节内容」。 +func TestEngineLogsEveryOutcome(t *testing.T) { + t.Run("success", func(t *testing.T) { + var buf bytes.Buffer + stub := newStubCodec() + eng := NewEngine(DefaultLimits(), WithCodecs(stub), WithLogger(testLogger(&buf))) + if _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }); err != nil { + t.Fatalf("transcode: %v", err) + } + assertLogContains(t, &buf, "level=INFO", "transcode ok", "source=capability/transcode", + "source_format=amr", "target_format=wav", "in_bytes=7", "out_bytes=7", "frames=0") + }) + t.Run("unsupported is a warning", func(t *testing.T) { + var buf bytes.Buffer + eng := NewEngine(DefaultLimits(), WithLogger(testLogger(&buf))) + if _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }); !errors.Is(err, ErrUnsupportedFormat) { + t.Fatalf("err = %v", err) + } + assertLogContains(t, &buf, "level=WARN", "transcode failed", "source=capability/transcode") + }) +} + +// TestEngineRegistrationErrorsAreVisible 断言注册失败不阻塞构造、但**可见** +// (绝不静默):nil 注册项、重复注册都会进 RegistrationError,且好的一路仍可用。 +func TestEngineRegistrationErrorsAreVisible(t *testing.T) { + cases := []struct { + name string + codecs []Codec + wantErr error + }{ + {"nil codec", []Codec{nil, newStubCodec()}, ErrInvalidCodec}, + {"duplicate codec", []Codec{newStubCodec(), newStubCodec()}, ErrDuplicate}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + eng := NewEngine(DefaultLimits(), WithCodecs(tc.codecs...), WithLogger(testLogger(&buf))) + err := eng.RegistrationError() + if !errors.Is(err, tc.wantErr) { + t.Fatalf("RegistrationError = %v, want %v", err, tc.wantErr) + } + if !eng.Supports(FormatAMR, FormatWAV) { + t.Fatalf("good codec was not registered") + } + assertLogContains(t, &buf, "level=ERROR", "transcode codec registration skipped") + }) + } +} + +// TestEngineDefaultLimits 断言零值 Limits 被补成 design/25 §2.5 的具名默认, +// 且显式给出的值不被覆盖。 +func TestEngineDefaultLimits(t *testing.T) { + got := NewEngine(Limits{}).Limits() + want := DefaultLimits() + if got != want { + t.Fatalf("zero limits = %+v, want defaults %+v", got, want) + } + custom := Limits{MaxInputBytes: 1, MaxOutputBytes: 2, MaxDuration: 3, Concurrency: 4, Timeout: 5} + if got := NewEngine(custom).Limits(); got != custom { + t.Fatalf("explicit limits = %+v, want untouched %+v", got, custom) + } +} + +// TestEngineNilContextIsSafe 断言 ctx 为 nil 不会 panic(按 Background 处理)。 +func TestEngineNilContextIsSafe(t *testing.T) { + eng := NewEngine(DefaultLimits(), WithCodecs(newStubCodec())) + if _, err := eng.Transcode(nil, Request{ //nolint:staticcheck // 故意传 nil ctx + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }); err != nil { + t.Fatalf("nil ctx: %v", err) + } +} + +// TestEngineErrorsAreWrapped 断言对外错误都能被 errors.Is 命中哨兵,且带上下文 +// (不是裸 err):文案形状是 `transcode: <源> -> <目标>: <原因>`。 +func TestEngineErrorsAreWrapped(t *testing.T) { + stub := newStubCodec() + stub.err = fmt.Errorf("stub: %w", ErrDecodeFailed) + eng := NewEngine(DefaultLimits(), WithCodecs(stub)) + _, err := eng.Transcode(context.Background(), Request{ + Data: amrStubBytes(0x3c), Source: FormatAMR, Target: FormatWAV, + }) + if !errors.Is(err, ErrDecodeFailed) { + t.Fatalf("err = %v, want ErrDecodeFailed", err) + } + if !strings.Contains(err.Error(), "stub") { + t.Fatalf("wrapped error lost its cause: %v", err) + } + const wantPrefix = "transcode: amr -> wav: " + if !strings.HasPrefix(err.Error(), wantPrefix) { + t.Fatalf("err = %q, want the %q prefix(带路由上下文)", err.Error(), wantPrefix) + } +} diff --git a/internal/capability/transcode/errors.go b/internal/capability/transcode/errors.go new file mode 100644 index 0000000..3ffaffa --- /dev/null +++ b/internal/capability/transcode/errors.go @@ -0,0 +1,50 @@ +// Package transcode 是 design/25「转码能力」的本地能力包实现:一条 +// 「字节 + 源格式 + 目标格式 → 字节 + 目标格式 + 元信息」的进程内、同步、 +// 纯函数式转换(不触网、不落盘、不读数据库)。它被设计为可被其它模块、 +// 编译期系统插件与工具面(agent / MCP)复用,落地方式见 design/25 §1.3.2 +// 的「两层注册」。 +// +// 本包的所有失败一律是 errors.go 里的具名哨兵(或包裹它),绝不返回部分 +// 结果假装成功(design/25 §2.4 / §5.4)。 +package transcode + +import "errors" + +// design/25 §2.4 的八个哨兵。调用方用 errors.Is 判定;一律 %w 包裹, +// 绝不静默吞掉。错误文案是哨兵 + 可读短句:上游/解码器的错误原文只进日志, +// 不进用户可见正文(与 channelbridge 的「原因短语不含上游回显」同一条纪律)。 +var ( + // ErrUnsupportedFormat 源或目标格式未知 / 魔数嗅不出 / 注册表里没有这条路。 + // 调用方应诚实提示;按内容发生的失败不熔断。 + ErrUnsupportedFormat = errors.New("transcode: unsupported format") + // ErrInputTooLarge 输入字节超过 MaxInputBytes,或可确证的时长超过 + // MaxDuration。在解码前拒绝(一行都不解)。 + ErrInputTooLarge = errors.New("transcode: input too large") + // ErrOutputTooLarge 解码产物超过 MaxOutputBytes。在解码前即可确证时先拒绝, + // 否则边走边计数、命中即停。 + ErrOutputTooLarge = errors.New("transcode: output too large") + // ErrDecodeFailed 字节损坏 / 头非法 / 帧截断。与「格式不支持」分开: + // 一个是「不会」,一个是「坏了」。 + ErrDecodeFailed = errors.New("transcode: decode failed") + // ErrEncodeFailed 目标侧构造失败(如 WAV 头写不出)。 + ErrEncodeFailed = errors.New("transcode: encode failed") + // ErrTimeout ctx 超时被检出。记 WARN、诚实提示、不重试(同步路径由调用方决定)。 + ErrTimeout = errors.New("transcode: timeout") + // ErrBusy 并发槽已满,快速失败、不排队(绝不入无界队列)。 + ErrBusy = errors.New("transcode: busy") + // ErrNoBackend 选了外部后端但主机上找不到 / 不可用。本期默认路径是进程内 + // 纯 Go,故暂无触发点;哨兵先行登记,供 design/25 §4.3 的可选后端复用。 + ErrNoBackend = errors.New("transcode: no backend") +) + +// 以下两个不是 design/25 §2.4 的八个转码哨兵:它们描述的是「注册表里放进了 +// 不合法的东西」和「工具面的入参不合法」,属包内自有的构造期/入参错误。 +var ( + // ErrDuplicate 同一个「源→目标」被注册了两次(与 registry.Register 的 + // 「重名报错」纪律同源)。 + ErrDuplicate = errors.New("transcode: codec already registered") + // ErrInvalidCodec 注册项不合法:nil、源或目标格式为空、或源等于目标。 + ErrInvalidCodec = errors.New("transcode: invalid codec") + // ErrBadArgs 工具面入参不合法(非对象、缺字段、类型不对)。 + ErrBadArgs = errors.New("transcode: bad arguments") +) diff --git a/internal/capability/transcode/errors_test.go b/internal/capability/transcode/errors_test.go new file mode 100644 index 0000000..fb795c2 --- /dev/null +++ b/internal/capability/transcode/errors_test.go @@ -0,0 +1,67 @@ +package transcode + +import ( + "errors" + "strings" + "testing" +) + +// TestSentinelInventoryMatchesDesign 把 design/25 §2.4 的**八个**哨兵钉在测试里: +// 少一个、多一个、或者文案离开 `transcode: ` 前缀都会红。它是「清单型断言」—— +// 与只测边界逻辑互补(design/25 §6.3 的同一手法)。 +func TestSentinelInventoryMatchesDesign(t *testing.T) { + designSentinels := []struct { + name string + err error + }{ + {"ErrUnsupportedFormat", ErrUnsupportedFormat}, + {"ErrInputTooLarge", ErrInputTooLarge}, + {"ErrOutputTooLarge", ErrOutputTooLarge}, + {"ErrDecodeFailed", ErrDecodeFailed}, + {"ErrEncodeFailed", ErrEncodeFailed}, + {"ErrTimeout", ErrTimeout}, + {"ErrBusy", ErrBusy}, + {"ErrNoBackend", ErrNoBackend}, + } + if len(designSentinels) != 8 { + t.Fatalf("design/25 §2.4 是八个哨兵,这里列了 %d 个", len(designSentinels)) + } + const prefix = "transcode: " + for _, s := range designSentinels { + if s.err == nil { + t.Fatalf("%s is nil", s.name) + } + if !strings.HasPrefix(s.err.Error(), prefix) { + t.Errorf("%s = %q, want the %q prefix", s.name, s.err.Error(), prefix) + } + if !isSentinelError(s.err) { + t.Errorf("%s is missing from knownErrors", s.name) + } + } + // 两两不同(防止复制粘贴出的同一个值被当成两个哨兵)。 + for i := range designSentinels { + for j := i + 1; j < len(designSentinels); j++ { + if errors.Is(designSentinels[i].err, designSentinels[j].err) { + t.Errorf("%s and %s are the same error", designSentinels[i].name, designSentinels[j].name) + } + } + } + // 包内自有的构造期/入参错误也必须可见(它们不是 §2.4 的八个)。 + for _, s := range []error{ErrDuplicate, ErrInvalidCodec, ErrBadArgs} { + if !isSentinelError(s) { + t.Errorf("%q is missing from knownErrors", s) + } + } +} + +// TestTimeoutErrorKeepsBothCauses 断言超时错误同时能被判为 ErrTimeout、也保留 +// 原始 ctx 原因(%w 链):调用方既能走哨兵分支,也能看到「为什么」。 +func TestTimeoutErrorKeepsBothCauses(t *testing.T) { + err := timeoutError(errTimeoutCause) + if !errors.Is(err, ErrTimeout) || !errors.Is(err, errTimeoutCause) { + t.Fatalf("err = %v, want both ErrTimeout and the cause", err) + } +} + +// errTimeoutCause 是超时链里的原始原因桩。 +var errTimeoutCause = errors.New("stub deadline") diff --git a/internal/capability/transcode/format.go b/internal/capability/transcode/format.go new file mode 100644 index 0000000..82f8372 --- /dev/null +++ b/internal/capability/transcode/format.go @@ -0,0 +1,63 @@ +package transcode + +import ( + "bytes" + "strings" +) + +// Format 是小写、无点的音频格式标识("amr" / "wav" / …)。 +// +// 口径与 mediafetch.AudioFormatOf(internal/channel/mediafetch/inline_voice.go:62) +// 归一出的格式名、以及 voice.Audio{Format}(internal/runtime/multimodal/voice) +// 的格式名**同一份**:全仓的「字符串 ↔ Format」转换只有本文件的 FormatOf / +// FormatName 两处,调用方不得各写一份映射(design/25 §6.2)。 +type Format string + +const ( + // FormatAMR 是 AMR-NB(窄带),魔数 `#!AMR\n`。 + FormatAMR Format = "amr" + // FormatAMRWB 是 AMR-WB(宽带),魔数 `#!AMR-WB\n`。**本期不实现** + // (design/25 §3.3 后置):登记它是为了让嗅探与报错诚实——「认得出但没这条路」 + // 比「认不出」更能说清问题。 + FormatAMRWB Format = "amr-wb" + // FormatWAV 是 RIFF/WAVE 容器(本期只发 PCM16 一种载荷)。 + FormatWAV Format = "wav" +) + +// 魔数与容器标识。AMR 文件的头来自 RFC 4867 §5(存储格式);RIFF/WAVE 的四块 +// 结构见 design/25 §0.5 登记的 WAV/RIFF 规范。 +const ( + amrNBMagic = "#!AMR\n" + amrWBMagic = "#!AMR-WB\n" + riffMagic = "RIFF" + waveMagic = "WAVE" + // riffChunkSizeOffset 是 RIFF 头里 "WAVE" 标识的起始偏移("RIFF" + 4 字节长度)。 + riffChunkSizeOffset = 8 +) + +// FormatOf 把外部格式名归一成 Format:去空白、转小写、去掉开头的点 +// (`.amr` → `amr`)。它不判断格式是否支持——「认不认得」由 Sniff / +// FormatOf 决定,「支不支持」由 Registry.Lookup 决定,两件事分开。 +func FormatOf(name string) Format { + return Format(strings.ToLower(strings.TrimPrefix(strings.TrimSpace(name), "."))) +} + +// FormatName 返回 Format 的规范字符串形式(= 归一化后的格式名)。 +func FormatName(f Format) string { return string(f) } + +// Sniff 按魔数嗅探 data 的音频格式。嗅不出即返回 false(deny-by-default: +// 类型不明的字节绝不进解码器)。 +func Sniff(data []byte) (Format, bool) { + switch { + case bytes.HasPrefix(data, []byte(amrWBMagic)): + return FormatAMRWB, true + case bytes.HasPrefix(data, []byte(amrNBMagic)): + return FormatAMR, true + case len(data) >= riffChunkSizeOffset+len(waveMagic) && + bytes.HasPrefix(data, []byte(riffMagic)) && + bytes.HasPrefix(data[riffChunkSizeOffset:], []byte(waveMagic)): + return FormatWAV, true + default: + return "", false + } +} diff --git a/internal/capability/transcode/format_test.go b/internal/capability/transcode/format_test.go new file mode 100644 index 0000000..608b422 --- /dev/null +++ b/internal/capability/transcode/format_test.go @@ -0,0 +1,73 @@ +package transcode + +import "testing" + +// TestFormatOfNormalizes 断言「字符串 → Format」只有一处转换,且归一化口径稳定: +// 去空白、转小写、去前导点(design/25 §6.2:与 mediafetch.AudioFormatOf 同源)。 +func TestFormatOfNormalizes(t *testing.T) { + cases := map[string]Format{ + "amr": FormatAMR, + ".amr": FormatAMR, + "AMR": FormatAMR, + " .AmR": FormatAMR, + "wav": FormatWAV, + ".WAV": FormatWAV, + "": Format(""), + } + for in, want := range cases { + if got := FormatOf(in); got != want { + t.Errorf("FormatOf(%q) = %q, want %q", in, got, want) + } + } +} + +// TestFormatName 断言 FormatName 是 FormatOf 的逆(对已归一的格式名)。 +func TestFormatName(t *testing.T) { + for _, f := range []Format{FormatAMR, FormatAMRWB, FormatWAV} { + if got := FormatOf(FormatName(f)); got != f { + t.Errorf("FormatOf(FormatName(%q)) = %q, want %q", f, got, f) + } + } +} + +// TestSniff 断言魔数嗅探:认得出(AMR-NB / AMR-WB / WAV)、嗅不出即 false +// (deny-by-default)。 +func TestSniff(t *testing.T) { + riffWAV := append([]byte("RIFF\x24\x00\x00\x00WAVE"), make([]byte, 8)...) + cases := []struct { + name string + data []byte + want Format + ok bool + }{ + {"amr-nb magic", []byte(amrNBMagic), FormatAMR, true}, + {"amr-nb stream", amrStubBytes(0x3c, 1, 2, 3), FormatAMR, true}, + {"amr-wb magic", []byte(amrWBMagic), FormatAMRWB, true}, + {"wav", riffWAV, FormatWAV, true}, + {"empty", nil, "", false}, + {"short", []byte("#!AM"), "", false}, + {"text", []byte("hello world"), "", false}, + {"riff without wave", []byte("RIFF\x24\x00\x00\x00AVI "), "", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, ok := Sniff(tc.data) + if ok != tc.ok || got != tc.want { + t.Fatalf("Sniff = (%q, %v), want (%q, %v)", got, ok, tc.want, tc.ok) + } + }) + } +} + +// TestAMRWBMagicIsRecognizedButUnsupported 断言「认得出但没这条路」: +// AMR-WB 的魔数能被嗅出(报错因此能说清问题),但注册表里没有它 → +// ErrUnsupportedFormat(design/25 §3.3 后置)。 +func TestAMRWBMagicIsRecognizedButUnsupported(t *testing.T) { + eng := NewEngine(DefaultLimits(), WithCodecs(DefaultCodecs()...)) + if got, ok := Sniff([]byte(amrWBMagic)); !ok || got != FormatAMRWB { + t.Fatalf("Sniff(amr-wb) = (%q, %v)", got, ok) + } + if eng.Supports(FormatAMRWB, FormatWAV) { + t.Fatalf("amr-wb -> wav must stay unsupported this round") + } +} diff --git a/internal/capability/transcode/limits.go b/internal/capability/transcode/limits.go new file mode 100644 index 0000000..b0f8908 --- /dev/null +++ b/internal/capability/transcode/limits.go @@ -0,0 +1,71 @@ +package transcode + +import "time" + +// 三层上限 + 并发 + 超时的默认取值(design/25 §2.5,取值照设计,不得自行改)。 +// 全部具名:本包不存在裸数字。 +const ( + // DefaultMaxInputBytes 是单次转码的输入字节上限(5 MiB)。取值与 + // mediafetch.MaxVoiceBytes 相同但**不共用常量**:一个是「入站内联上限」, + // 一个是「转码输入上限」,共用会让「改一个静默改另一个」(design/25 §2.5)。 + DefaultMaxInputBytes = 5 << 20 + // DefaultMaxOutputBytes 是解码产物的字节上限(64 MiB)。AMR-NB 是唯一会放大的 + // 那条路:12.2 kbps ≈ 1525 B/s → 8 kHz 单声道 PCM16 = 16000 B/s ≈ 10.5×, + // 加帧头最坏约 12.8×,5 MiB 输入最坏 ⇒ ≈64 MiB。没有这条闸就是 + // 「上传 5 MB、内存里长到 64 MB」。 + DefaultMaxOutputBytes = 64 << 20 + // DefaultMaxDuration 是可确证的时长上限(10 分钟):「正常语音」与「把整段录音 + // 伪装成语音」的分界。可从 AMR 帧数确证(一帧 20ms),绝不估算。 + DefaultMaxDuration = 10 * time.Minute + // DefaultConcurrency 是全局并发槽数(信号量容量)。 + DefaultConcurrency = 2 + // DefaultTimeout 是单次调用的 ctx 超时上限。 + DefaultTimeout = 10 * time.Second +) + +// Limits 是一次转码的资源约束(design/25 §2.5)。零值字段由 Engine 用上面的 +// 具名默认补足,因此 `Limits{}` 是安全的「全默认」。 +type Limits struct { + // MaxInputBytes 输入字节上限;命中即 ErrInputTooLarge(解码前拒绝)。 + MaxInputBytes int + // MaxOutputBytes 产物字节上限;命中即 ErrOutputTooLarge。 + MaxOutputBytes int + // MaxDuration 可确证的时长上限;命中即 ErrInputTooLarge。 + MaxDuration time.Duration + // Concurrency 全局并发槽数;槽满即 ErrBusy(快速失败,不排队)。 + Concurrency int + // Timeout 单次调用的超时;命中即 ErrTimeout。 + Timeout time.Duration +} + +// DefaultLimits 返回一组全默认的限额(design/25 §2.5 的取值)。 +func DefaultLimits() Limits { + return Limits{ + MaxInputBytes: DefaultMaxInputBytes, + MaxOutputBytes: DefaultMaxOutputBytes, + MaxDuration: DefaultMaxDuration, + Concurrency: DefaultConcurrency, + Timeout: DefaultTimeout, + } +} + +// withDefaults 用默认值补足零值字段。负数与零一并按「未配置」处理:限额的 +// 零值若照字面生效会变成「一字节都不许」,那不是任何调用方想要的行为。 +func (l Limits) withDefaults() Limits { + if l.MaxInputBytes <= 0 { + l.MaxInputBytes = DefaultMaxInputBytes + } + if l.MaxOutputBytes <= 0 { + l.MaxOutputBytes = DefaultMaxOutputBytes + } + if l.MaxDuration <= 0 { + l.MaxDuration = DefaultMaxDuration + } + if l.Concurrency <= 0 { + l.Concurrency = DefaultConcurrency + } + if l.Timeout <= 0 { + l.Timeout = DefaultTimeout + } + return l +} diff --git a/internal/capability/transcode/limits_test.go b/internal/capability/transcode/limits_test.go new file mode 100644 index 0000000..1592c74 --- /dev/null +++ b/internal/capability/transcode/limits_test.go @@ -0,0 +1,51 @@ +package transcode + +import ( + "testing" + "time" +) + +// TestDefaultLimitsPinnedToDesign 把 design/25 §2.5 的**取值**钉在测试里: +// 谁改了默认限额(比如把输入上限从 5 MiB 调大),这条断言就会红,逼他回设计对账。 +// 设计原文:5 MiB 进 / 64 MiB 出 / 10 分钟 / 并发 2 / 超时 10s。 +func TestDefaultLimitsPinnedToDesign(t *testing.T) { + got := DefaultLimits() + if got.MaxInputBytes != 5<<20 { + t.Errorf("MaxInputBytes = %d, want %d (5 MiB)", got.MaxInputBytes, 5<<20) + } + if got.MaxOutputBytes != 64<<20 { + t.Errorf("MaxOutputBytes = %d, want %d (64 MiB)", got.MaxOutputBytes, 64<<20) + } + if got.MaxDuration != 10*time.Minute { + t.Errorf("MaxDuration = %s, want %s", got.MaxDuration, 10*time.Minute) + } + if got.Concurrency != 2 { + t.Errorf("Concurrency = %d, want 2", got.Concurrency) + } + if got.Timeout != 10*time.Second { + t.Errorf("Timeout = %s, want %s", got.Timeout, 10*time.Second) + } +} + +// TestLimitsWithDefaults 断言零值与负值都被补成默认(限额的零值若照字面生效 +// 会变成「一字节都不许」,那不是任何调用方想要的行为)。 +func TestLimitsWithDefaults(t *testing.T) { + zero := Limits{}.withDefaults() + if zero != DefaultLimits() { + t.Fatalf("zero limits = %+v, want %+v", zero, DefaultLimits()) + } + negative := Limits{ + MaxInputBytes: -1, MaxOutputBytes: -1, MaxDuration: -1, Concurrency: -1, Timeout: -1, + }.withDefaults() + if negative != DefaultLimits() { + t.Fatalf("negative limits = %+v, want %+v", negative, DefaultLimits()) + } +} + +// TestWAVSizeLimitIsBelowOutputLimit 断言 64 MiB 的产物上限落在 44 字节头能如实 +// 声明的范围内(否则会出现「闸放过了、头写不出」的死角)。 +func TestWAVSizeLimitIsBelowOutputLimit(t *testing.T) { + if uint64(DefaultMaxOutputBytes) > uint64(wavMaxDataBytes) { + t.Fatalf("MaxOutputBytes %d exceeds the RIFF limit %d", DefaultMaxOutputBytes, wavMaxDataBytes) + } +} diff --git a/internal/capability/transcode/registry.go b/internal/capability/transcode/registry.go new file mode 100644 index 0000000..fdfbdab --- /dev/null +++ b/internal/capability/transcode/registry.go @@ -0,0 +1,86 @@ +package transcode + +import ( + "fmt" + "sort" + "sync" +) + +// Registry 承载「哪些 源→目标 可用」(design/25 §1.3.2 第一层)。它是**包内**的 +// 编解码器注册表,与全局能力注册表(internal/capability/registry)是两件事: +// 后者表达「agent 可选用的能力 + 门控策略」,前者表达「本能力认识哪几条路」。 +// +// 新增一种格式 = 往这里多一个注册项(见 codecs.go 的 DefaultCodecs); +// 并发安全。 +type Registry struct { + mu sync.RWMutex + codecs map[route]Codec +} + +// route 是「源→目标」这一对,作注册表的键。用结构体而非字符串拼接:拼接会让 +// 格式名里出现分隔符时产生歧义(`a->b` 与 `a-` + `>b`)。 +type route struct { + from Format + to Format +} + +// NewRegistry 返回一个空且并发安全的编解码器注册表。 +func NewRegistry() *Registry { + return &Registry{codecs: make(map[route]Codec)} +} + +// Register 注册一条转换路。nil、源/目标为空、源等于目标都返回 ErrInvalidCodec; +// 同一对重复注册返回 ErrDuplicate。 +func (r *Registry) Register(c Codec) error { + if c == nil { + return fmt.Errorf("transcode: register codec: %w: nil codec", ErrInvalidCodec) + } + from, to := c.From(), c.To() + if from == "" || to == "" { + return fmt.Errorf("transcode: register codec %q -> %q: %w: empty format", from, to, ErrInvalidCodec) + } + if from == to { + return fmt.Errorf("transcode: register codec %q -> %q: %w: identity route", from, to, ErrInvalidCodec) + } + r.mu.Lock() + defer r.mu.Unlock() + key := route{from: from, to: to} + if _, exists := r.codecs[key]; exists { + return fmt.Errorf("transcode: register codec %q -> %q: %w", from, to, ErrDuplicate) + } + r.codecs[key] = c + return nil +} + +// Lookup 返回 from→to 这条路上的编解码器。没有这条路时 ok 为 false。 +func (r *Registry) Lookup(from, to Format) (Codec, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + c, ok := r.codecs[route{from: from, to: to}] + return c, ok +} + +// Formats 返回当前注册涉及的全部格式(源与目标去重后的并集,已排序)。 +// 它是「发现」面:调用方据此知道本能力认识哪些格式名。 +func (r *Registry) Formats() []Format { + r.mu.RLock() + defer r.mu.RUnlock() + seen := make(map[Format]struct{}, len(r.codecs)*2) + for key := range r.codecs { + seen[key.from] = struct{}{} + seen[key.to] = struct{}{} + } + out := make([]Format, 0, len(seen)) + for f := range seen { + out = append(out, f) + } + sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) + return out +} + +// Len 返回已注册的「源→目标」路数。供启动期自检与测试断言使用。 +func (r *Registry) Len() int { + r.mu.RLock() + defer r.mu.RUnlock() + return len(r.codecs) +} diff --git a/internal/capability/transcode/registry_test.go b/internal/capability/transcode/registry_test.go new file mode 100644 index 0000000..b09d46e --- /dev/null +++ b/internal/capability/transcode/registry_test.go @@ -0,0 +1,80 @@ +package transcode + +import ( + "errors" + "testing" +) + +// TestRegistryRegisterAndLookup 断言注册 / 查找 / 发现三件事。 +func TestRegistryRegisterAndLookup(t *testing.T) { + reg := NewRegistry() + codec := newStubCodec() + if err := reg.Register(codec); err != nil { + t.Fatalf("register: %v", err) + } + got, ok := reg.Lookup(FormatAMR, FormatWAV) + if !ok || got != Codec(codec) { + t.Fatalf("lookup = (%v, %v), want the registered codec", got, ok) + } + if _, ok := reg.Lookup(FormatWAV, FormatAMR); ok { + t.Fatalf("reverse route must not exist") + } + if n := reg.Len(); n != 1 { + t.Fatalf("Len = %d, want 1", n) + } + if formats := reg.Formats(); len(formats) != 2 || formats[0] != FormatAMR || formats[1] != FormatWAV { + t.Fatalf("Formats = %v, want [amr wav](去重且有序)", formats) + } +} + +// TestRegistryRejectsBadCodecs 断言不合法注册项被拒(nil / 空格式 / 自我转换), +// 且 repeated 注册返回 ErrDuplicate。 +func TestRegistryRejectsBadCodecs(t *testing.T) { + reg := NewRegistry() + cases := []struct { + name string + codec Codec + }{ + {"nil", nil}, + {"empty from", &stubCodec{from: "", to: FormatWAV}}, + {"empty to", &stubCodec{from: FormatAMR, to: ""}}, + {"identity", &stubCodec{from: FormatWAV, to: FormatWAV}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if err := reg.Register(tc.codec); !errors.Is(err, ErrInvalidCodec) { + t.Fatalf("err = %v, want ErrInvalidCodec", err) + } + }) + } + if err := reg.Register(newStubCodec()); err != nil { + t.Fatalf("register: %v", err) + } + if err := reg.Register(newStubCodec()); !errors.Is(err, ErrDuplicate) { + t.Fatalf("duplicate err = %v, want ErrDuplicate", err) + } + if n := reg.Len(); n != 1 { + t.Fatalf("Len = %d after a rejected duplicate, want 1", n) + } +} + +// TestRegistryFormatsDedupe 断言多路共享格式时 Formats 去重。 +func TestRegistryFormatsDedupe(t *testing.T) { + reg := NewRegistry() + if err := reg.Register(&stubCodec{from: FormatAMR, to: FormatWAV}); err != nil { + t.Fatalf("register: %v", err) + } + if err := reg.Register(&stubCodec{from: FormatWAV, to: FormatAMRWB}); err != nil { + t.Fatalf("register: %v", err) + } + want := []Format{FormatAMR, FormatAMRWB, FormatWAV} + got := reg.Formats() + if len(got) != len(want) { + t.Fatalf("Formats = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("Formats = %v, want %v(有序)", got, want) + } + } +} diff --git a/internal/capability/transcode/testdata/mixed_modes.amr b/internal/capability/transcode/testdata/mixed_modes.amr new file mode 100644 index 0000000..4407bae Binary files /dev/null and b/internal/capability/transcode/testdata/mixed_modes.amr differ diff --git a/internal/capability/transcode/testdata/mixed_modes.pcm b/internal/capability/transcode/testdata/mixed_modes.pcm new file mode 100644 index 0000000..44da1e9 Binary files /dev/null and b/internal/capability/transcode/testdata/mixed_modes.pcm differ diff --git a/internal/capability/transcode/testdata/mode10200.amr b/internal/capability/transcode/testdata/mode10200.amr new file mode 100644 index 0000000..1ed0259 Binary files /dev/null and b/internal/capability/transcode/testdata/mode10200.amr differ diff --git a/internal/capability/transcode/testdata/mode10200.pcm b/internal/capability/transcode/testdata/mode10200.pcm new file mode 100644 index 0000000..2f12c07 Binary files /dev/null and b/internal/capability/transcode/testdata/mode10200.pcm differ diff --git a/internal/capability/transcode/testdata/mode12200.amr b/internal/capability/transcode/testdata/mode12200.amr new file mode 100644 index 0000000..57ce64a Binary files /dev/null and b/internal/capability/transcode/testdata/mode12200.amr differ diff --git a/internal/capability/transcode/testdata/mode12200.pcm b/internal/capability/transcode/testdata/mode12200.pcm new file mode 100644 index 0000000..f69808e Binary files /dev/null and b/internal/capability/transcode/testdata/mode12200.pcm differ diff --git a/internal/capability/transcode/testdata/mode4750.amr b/internal/capability/transcode/testdata/mode4750.amr new file mode 100644 index 0000000..dd78a4b Binary files /dev/null and b/internal/capability/transcode/testdata/mode4750.amr differ diff --git a/internal/capability/transcode/testdata/mode4750.pcm b/internal/capability/transcode/testdata/mode4750.pcm new file mode 100644 index 0000000..965d874 Binary files /dev/null and b/internal/capability/transcode/testdata/mode4750.pcm differ diff --git a/internal/capability/transcode/testdata/mode5150.amr b/internal/capability/transcode/testdata/mode5150.amr new file mode 100644 index 0000000..9819378 Binary files /dev/null and b/internal/capability/transcode/testdata/mode5150.amr differ diff --git a/internal/capability/transcode/testdata/mode5150.pcm b/internal/capability/transcode/testdata/mode5150.pcm new file mode 100644 index 0000000..9e32997 Binary files /dev/null and b/internal/capability/transcode/testdata/mode5150.pcm differ diff --git a/internal/capability/transcode/testdata/mode5900.amr b/internal/capability/transcode/testdata/mode5900.amr new file mode 100644 index 0000000..b24c77f Binary files /dev/null and b/internal/capability/transcode/testdata/mode5900.amr differ diff --git a/internal/capability/transcode/testdata/mode5900.pcm b/internal/capability/transcode/testdata/mode5900.pcm new file mode 100644 index 0000000..d8486e3 Binary files /dev/null and b/internal/capability/transcode/testdata/mode5900.pcm differ diff --git a/internal/capability/transcode/testdata/mode6700.amr b/internal/capability/transcode/testdata/mode6700.amr new file mode 100644 index 0000000..e41ba01 Binary files /dev/null and b/internal/capability/transcode/testdata/mode6700.amr differ diff --git a/internal/capability/transcode/testdata/mode6700.pcm b/internal/capability/transcode/testdata/mode6700.pcm new file mode 100644 index 0000000..b0faf03 Binary files /dev/null and b/internal/capability/transcode/testdata/mode6700.pcm differ diff --git a/internal/capability/transcode/testdata/mode7400.amr b/internal/capability/transcode/testdata/mode7400.amr new file mode 100644 index 0000000..966d314 Binary files /dev/null and b/internal/capability/transcode/testdata/mode7400.amr differ diff --git a/internal/capability/transcode/testdata/mode7400.pcm b/internal/capability/transcode/testdata/mode7400.pcm new file mode 100644 index 0000000..8178f9f Binary files /dev/null and b/internal/capability/transcode/testdata/mode7400.pcm differ diff --git a/internal/capability/transcode/testdata/mode7950.amr b/internal/capability/transcode/testdata/mode7950.amr new file mode 100644 index 0000000..b02d18e Binary files /dev/null and b/internal/capability/transcode/testdata/mode7950.amr differ diff --git a/internal/capability/transcode/testdata/mode7950.pcm b/internal/capability/transcode/testdata/mode7950.pcm new file mode 100644 index 0000000..d736aa9 Binary files /dev/null and b/internal/capability/transcode/testdata/mode7950.pcm differ diff --git a/internal/capability/transcode/tool.go b/internal/capability/transcode/tool.go new file mode 100644 index 0000000..ca05609 --- /dev/null +++ b/internal/capability/transcode/tool.go @@ -0,0 +1,129 @@ +package transcode + +import ( + "context" + "encoding/base64" + "fmt" + + "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" +) + +// 本文件是 design/25 §1.3.2 的**第二层注册**:把转码暴露成 agent 可选用的内置 +// 工具(KindTool),走**既有**的 sdk.Registrar → internal/capability/registry。 +// 与 vision.RegisterTools / io.Register 逐字同族,因此面板、策略、审计三面零新增 +// 代码即覆盖;**不新增 Kind**(validKind 闭集不动),策略默认零值 = owner-only。 +// +// ⚠️ 本工具是 design/25 §8-T1 的「RegisterTool 壳」:T4 定义的形态是 +// `{name, to}`(服务端存储名进、新存储名出,薄壳:读存储 → 调 core → 写存储), +// 它依赖 design/24 §10 的音频副本存储与 `GET /api/v1/media/{name}`——那两件**尚未 +// 落地**。因此本壳用「字节进、字节出」(base64 承载)先把**注册链路**打通并可测, +// T4 落地后再换入参形态;这条差异已登记在交接报告里。 +// +// 红线提醒:转码产物是**字节**,不进上下文。因此本壳工具另设一个远小于 +// MaxOutputBytes 的结果上限(MaxToolResultBytes),让「工具结果」不可能把上下文 +// 灌爆——要转大文件请走进程内 DI 路径,不要走模型工具面。 +const ( + // ToolName 是转码工具在全局能力注册表里的名字。 + ToolName = "transcode" + // MaxToolResultBytes 是本壳工具允许回给模型的产物体积上限(解码后字节数)。 + // 它**不是** design/25 §2.5 的 MaxOutputBytes(那是能力上限,64 MiB);这里更小 + // 是因为工具结果会进模型上下文。 + MaxToolResultBytes = 256 << 10 +) + +// 入参字段名。抽成常量以免同一个键在三处各写一份字符串。 +const ( + toolArgData = "data_base64" + toolArgSource = "source" + toolArgTarget = "target" +) + +// Tool 把 Transcoder 包装成 sdk.Tool,使 agent 能直接调用转码(design/25 §9-2 +// 默认「暴露,owner-only」)。它可安全并发使用。 +type Tool struct { + tr Transcoder +} + +// Compile-time assertion that *Tool satisfies the SDK Tool interface. +var _ sdk.Tool = (*Tool)(nil) + +// NewTool 用给定转码器构造工具。 +func NewTool(tr Transcoder) *Tool { return &Tool{tr: tr} } + +// Name 返回工具标识。 +func (t *Tool) Name() string { return ToolName } + +// Description 说明该工具做什么。它让模型知道「转码是本地能力、同步、不看身份」。 +func (t *Tool) Description() string { + return "Convert audio bytes between formats locally (e.g. amr -> wav). " + + "Input and output are base64-encoded bytes; the result carries format, " + + "sample rate, channels, duration and frame count." +} + +// Parameters 返回输入 JSON Schema:data_base64(必填)+ target(必填)+ source(可选)。 +func (t *Tool) Parameters() (map[string]any, error) { + return map[string]any{ + "type": "object", + "properties": map[string]any{ + toolArgData: map[string]any{ + "type": "string", + "description": "The source audio bytes, base64-encoded (standard alphabet).", + }, + toolArgTarget: map[string]any{ + "type": "string", + "description": "Target format name, lowercase and without a dot, e.g. \"wav\".", + }, + toolArgSource: map[string]any{ + "type": "string", + "description": "Optional source format name; omitted means sniff by magic bytes.", + }, + }, + "required": []any{toolArgData, toolArgTarget}, + }, nil +} + +// Execute 解码入参、调用转码器,并把结果包成模型可读的对象。 +// 失败一律返回 errors.go 的哨兵(经 %w 保留),绝不假装成功。 +func (t *Tool) Execute(ctx context.Context, args any) (any, error) { + if t.tr == nil { + return nil, fmt.Errorf("transcode: tool %q: %w: nil transcoder", ToolName, ErrUnsupportedFormat) + } + req, err := toolRequest(args) + if err != nil { + return nil, err + } + res, err := t.tr.Transcode(ctx, req) + if err != nil { + return nil, fmt.Errorf("transcode: tool %q: %w", ToolName, err) + } + if len(res.Data) > MaxToolResultBytes { + return nil, fmt.Errorf("transcode: tool %q: %w: result %d bytes > %d (tool face is bounded; use the in-process API for larger audio)", + ToolName, ErrOutputTooLarge, len(res.Data), MaxToolResultBytes) + } + return map[string]any{ + "format": FormatName(res.Format), + "source": FormatName(res.Source), + "data_base64": base64.StdEncoding.EncodeToString(res.Data), + "sample_rate": res.SampleRate, + "channels": res.Channels, + "duration_ms": res.DurationMs, + "frames": res.Frames, + }, nil +} + +// RegisterTool 经 SDK 辅助函数把转码工具注册到 r(design/25 §1.3.2 第二层)。 +// 与 vision.RegisterTools 同族:tr 为 nil 时不注册(返回 nil,不制造一个必然 +// 失败的能力条目)。 +func RegisterTool(r sdk.Registrar, tr Transcoder) error { + if r == nil { + return sdk.ErrNilRegistrar + } + if tr == nil { + return nil + } + tool := NewTool(tr) + if err := sdk.RegisterTool(r, tool); err != nil { + return fmt.Errorf("transcode: register tool %q: %w", tool.Name(), err) + } + return nil +} diff --git a/internal/capability/transcode/tool_args.go b/internal/capability/transcode/tool_args.go new file mode 100644 index 0000000..9ee543c --- /dev/null +++ b/internal/capability/transcode/tool_args.go @@ -0,0 +1,56 @@ +package transcode + +import ( + "encoding/base64" + "fmt" +) + +// 本文件解析工具面入参(sdk.Tool.Execute 的 args)。它与 SDK 的「args 是一个 +// JSON 对象」约定对齐:非对象、缺字段、类型不对一律 ErrBadArgs,绝不猜测。 +// 参数名在 tool.go 的常量块里统一定义。 + +// toolRequest 把工具入参解成一次转码请求。 +// +// source 可省略(留空 ⇒ 由 engine 按魔数嗅探);data_base64 与 target 必填。 +// base64 用标准字母表(与 Execute 输出同一份编码,往返无损)。 +func toolRequest(args any) (Request, error) { + m, ok := args.(map[string]any) + if !ok { + return Request{}, fmt.Errorf("transcode: %w: args must be an object", ErrBadArgs) + } + data, err := toolDataArg(m) + if err != nil { + return Request{}, err + } + target := FormatOf(toolStringArg(m, toolArgTarget)) + if target == "" { + return Request{}, fmt.Errorf("transcode: %w: missing %s", ErrBadArgs, toolArgTarget) + } + return Request{ + Data: data, + Source: FormatOf(toolStringArg(m, toolArgSource)), + Target: target, + }, nil +} + +// toolStringArg 读一个字符串字段;字段缺失或不是字符串都算空串。 +func toolStringArg(m map[string]any, key string) string { + s, _ := m[key].(string) + return s +} + +// toolDataArg 解出源字节:Base64 标准字母表,空串/非法 Base64/空字节都拒。 +func toolDataArg(m map[string]any) ([]byte, error) { + raw := toolStringArg(m, toolArgData) + if raw == "" { + return nil, fmt.Errorf("transcode: %w: missing %s", ErrBadArgs, toolArgData) + } + data, err := base64.StdEncoding.DecodeString(raw) + if err != nil { + return nil, fmt.Errorf("transcode: %w: %s is not valid base64: %v", ErrBadArgs, toolArgData, err) + } + if len(data) == 0 { + return nil, fmt.Errorf("transcode: %w: %s decodes to zero bytes", ErrBadArgs, toolArgData) + } + return data, nil +} diff --git a/internal/capability/transcode/tool_test.go b/internal/capability/transcode/tool_test.go new file mode 100644 index 0000000..0db36ff --- /dev/null +++ b/internal/capability/transcode/tool_test.go @@ -0,0 +1,201 @@ +package transcode + +import ( + "context" + "encoding/base64" + "errors" + "testing" + + "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" + "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" +) + +// stubTranscoder 是工具面测试用的最小 Transcoder(只为验证包装层,不碰真实音频)。 +type stubTranscoder struct { + supports bool + res Result + err error + // lastReq 记录最后一次透传的请求(验证 source/target 归一化)。 + lastReq Request +} + +func (s *stubTranscoder) Transcode(_ context.Context, req Request) (Result, error) { + s.lastReq = req + if s.err != nil { + return Result{}, s.err + } + return s.res, nil +} + +func (s *stubTranscoder) Supports(_, _ Format) bool { return s.supports } +func (s *stubTranscoder) Formats() []Format { return []Format{FormatAMR, FormatWAV} } + +// TestToolParameters 断言入参 schema 的形状(对象、必填 data_base64/target)。 +func TestToolParameters(t *testing.T) { + tool := NewTool(&stubTranscoder{}) + if tool.Name() != ToolName { + t.Fatalf("Name = %q, want %q", tool.Name(), ToolName) + } + if tool.Description() == "" { + t.Fatalf("Description must not be empty") + } + schema, err := tool.Parameters() + if err != nil { + t.Fatalf("Parameters: %v", err) + } + if schema["type"] != "object" { + t.Fatalf("schema type = %v, want object", schema["type"]) + } + required, ok := schema["required"].([]any) + if !ok || len(required) != 2 { + t.Fatalf("required = %v, want [data_base64 target]", schema["required"]) + } + props, ok := schema["properties"].(map[string]any) + if !ok { + t.Fatalf("properties missing") + } + for _, key := range []string{toolArgData, toolArgTarget, toolArgSource} { + if _, ok := props[key]; !ok { + t.Fatalf("properties missing %q", key) + } + } +} + +// TestToolExecuteDecodesArgs 断言入参归一化与结果字段。 +func TestToolExecuteDecodesArgs(t *testing.T) { + stub := &stubTranscoder{supports: true, res: Result{ + Data: []byte("wav-bytes"), Format: FormatWAV, Source: FormatAMR, + SampleRate: amrSampleRate, Channels: amrChannels, DurationMs: 40, Frames: 2, + }} + out, err := NewTool(stub).Execute(context.Background(), map[string]any{ + toolArgData: base64.StdEncoding.EncodeToString([]byte("amr-bytes")), + toolArgTarget: ".WAV", + }) + if err != nil { + t.Fatalf("Execute: %v", err) + } + if string(stub.lastReq.Data) != "amr-bytes" || stub.lastReq.Target != FormatWAV { + t.Fatalf("request = %+v, want the decoded bytes and normalized target", stub.lastReq) + } + if stub.lastReq.Source != "" { + t.Fatalf("source = %q, want empty(留空即嗅探)", stub.lastReq.Source) + } + res, ok := out.(map[string]any) + if !ok { + t.Fatalf("result type = %T, want map[string]any", out) + } + encoded, _ := res["data_base64"].(string) + if decoded, err := base64.StdEncoding.DecodeString(encoded); err != nil || string(decoded) != "wav-bytes" { + t.Fatalf("data_base64 round trip failed: %q (%v)", encoded, err) + } + if res["format"] != "wav" || res["source"] != "amr" || res["frames"] != 2 || res["duration_ms"] != int64(40) { + t.Fatalf("result fields = %v", res) + } +} + +// TestToolRejectsBadArgs 断言入参不合法一律 ErrBadArgs,绝不猜测。 +func TestToolRejectsBadArgs(t *testing.T) { + tool := NewTool(&stubTranscoder{supports: true}) + cases := []struct { + name string + args any + }{ + {"not an object", "data"}, + {"nil args", nil}, + {"missing data", map[string]any{toolArgTarget: "wav"}}, + {"missing target", map[string]any{toolArgData: base64.StdEncoding.EncodeToString([]byte("x"))}}, + {"bad base64", map[string]any{toolArgData: "!!!not base64!!!", toolArgTarget: "wav"}}, + {"empty base64 payload", map[string]any{toolArgData: "", toolArgTarget: "wav"}}, + {"wrong type", map[string]any{toolArgData: 42, toolArgTarget: "wav"}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if _, err := tool.Execute(context.Background(), tc.args); !errors.Is(err, ErrBadArgs) { + t.Fatalf("err = %v, want ErrBadArgs", err) + } + }) + } +} + +// TestToolPropagatesSentinels 断言工具面不吞哨兵(%w 链保持)。 +func TestToolPropagatesSentinels(t *testing.T) { + stub := &stubTranscoder{err: ErrUnsupportedFormat} + _, err := NewTool(stub).Execute(context.Background(), map[string]any{ + toolArgData: base64.StdEncoding.EncodeToString([]byte("x")), + toolArgTarget: "wav", + }) + if !errors.Is(err, ErrUnsupportedFormat) { + t.Fatalf("err = %v, want ErrUnsupportedFormat", err) + } +} + +// TestToolResultCap 断言工具结果体量有上限(产物是字节,不进上下文的红线: +// 要转大文件请走进程内 DI,不要走模型工具面)。 +func TestToolResultCap(t *testing.T) { + big := make([]byte, MaxToolResultBytes+1) + _, err := NewTool(&stubTranscoder{res: Result{Data: big}}).Execute(context.Background(), map[string]any{ + toolArgData: base64.StdEncoding.EncodeToString([]byte("x")), + toolArgTarget: "wav", + }) + if !errors.Is(err, ErrOutputTooLarge) { + t.Fatalf("err = %v, want ErrOutputTooLarge", err) + } +} + +// TestToolNilTranscoderFailsLoudly 断言没接转码器时不假装成功。 +func TestToolNilTranscoderFailsLoudly(t *testing.T) { + _, err := NewTool(nil).Execute(context.Background(), map[string]any{ + toolArgData: base64.StdEncoding.EncodeToString([]byte("x")), + toolArgTarget: "wav", + }) + if err == nil { + t.Fatalf("nil transcoder must fail") + } +} + +// TestRegisterToolIntoGlobalRegistry 是「两层注册」第二层的落点验收:走**既有** +// 全局能力注册表,进去的是 KindTool、策略是零值(fail-closed = owner-only), +// **不新增 Kind**(validKind 闭集不动)。 +func TestRegisterToolIntoGlobalRegistry(t *testing.T) { + caps := registry.New() + if err := RegisterTool(caps, NewEngine(DefaultLimits(), WithCodecs(DefaultCodecs()...))); err != nil { + t.Fatalf("RegisterTool: %v", err) + } + cap, err := caps.Get(ToolName) + if err != nil { + t.Fatalf("Get(%q): %v", ToolName, err) + } + if cap.Kind != registry.KindTool { + t.Fatalf("kind = %q, want %q(不为转码新增 Kind)", cap.Kind, registry.KindTool) + } + if cap.Policy.AllowInGroup || cap.Policy.AllowInStranger { + t.Fatalf("policy = %+v, want zero value (owner-only, fail-closed)", cap.Policy) + } + if _, ok := cap.Value.(*Tool); !ok { + t.Fatalf("value type = %T, want *Tool", cap.Value) + } + if len(caps.List(registry.KindTool)) != 1 { + t.Fatalf("tool listing did not include the transcode tool") + } +} + +// TestRegisterToolNilArgs 断言注册入口的 nil 纪律(与 vision.RegisterTools 同族)。 +func TestRegisterToolNilArgs(t *testing.T) { + if err := RegisterTool(nil, &stubTranscoder{}); !errors.Is(err, sdk.ErrNilRegistrar) { + t.Fatalf("nil registrar err = %v, want sdk.ErrNilRegistrar", err) + } + caps := registry.New() + if err := RegisterTool(caps, nil); err != nil { + t.Fatalf("nil transcoder must be a no-op, got %v", err) + } + if len(caps.List(registry.KindTool)) != 0 { + t.Fatalf("nil transcoder registered a capability") + } +} + +// TestStubTranscoderSatisfiesInterface 断言测试桩确实实现了接口(防止接口漂移 +// 时测试静默失去意义)。 +func TestStubTranscoderSatisfiesInterface(t *testing.T) { + var _ Transcoder = (*stubTranscoder)(nil) + var _ sdk.Tool = (*Tool)(nil) +} diff --git a/internal/capability/transcode/wav.go b/internal/capability/transcode/wav.go new file mode 100644 index 0000000..902911c --- /dev/null +++ b/internal/capability/transcode/wav.go @@ -0,0 +1,104 @@ +package transcode + +import ( + "encoding/binary" + "fmt" + "math" +) + +// WAV/RIFF 的 44 字节规范头(design/25 §0.5 登记的 WAV/RIFF 规范)。 +// 四块:RIFF + WAVE + fmt + data。本期只发 PCM16 一种载荷,且**不引入音频库** +// ——头是手写的,只用 encoding/binary。 +// +// 之所以目标容器选 WAV 而不是 mp3/ogg:WAV 是「只要解码器、不要编码器」的一条路 +// (PCM + 44 字节头),而纯 Go 的 mp3 编码器生态远不如解码器成熟(design/25 §3.1)。 +const ( + // wavHeaderBytes 是规范头的字节数。 + wavHeaderBytes = 44 + // wavFormatPCM 是 fmt 块里的音频格式号:1 = PCM(线性量化)。 + wavFormatPCM = 1 + // wavBitsPerSample 是每样本位数:AMR-NB 解码出来就是 16 位定点。 + wavBitsPerSample = 16 + // wavFmtChunkBytes 是 fmt 子块的长度(PCM 的规范值 16)。 + wavFmtChunkBytes = 16 + // wavPCMBytesPerSample 是单个样本占的字节数。 + wavPCMBytesPerSample = wavBitsPerSample / 8 + + // riffChunkSizeBase 是 RIFF 长度字段里除 data 之外的部分: + // chunkSize = riffChunkSizeBase + dataBytes。 + riffChunkSizeBase = wavHeaderBytes - 8 + + // 四块的标识与头内各字段的偏移(规范里定死的值,具名以免裸数字)。 + wavRiffID = "RIFF" + wavWaveID = "WAVE" + wavFmtID = "fmt " + wavDataID = "data" + + wavOffRiffID = 0 + wavOffChunkSize = 4 + wavOffWaveID = 8 + wavOffFmtID = 12 + wavOffFmtSize = 16 + wavOffAudioFormat = 20 + wavOffChannels = 22 + wavOffSampleRate = 24 + wavOffByteRate = 28 + wavOffBlockAlign = 32 + wavOffBitsPerSamp = 34 + wavOffDataID = 36 + wavOffDataSize = 40 +) + +// wavMaxDataBytes 是 RIFF 的 32 位长度字段能表达的 data 上限。超出即无法用 +// 44 字节头如实描述 ⇒ ErrEncodeFailed(绝不写出一个长度撒谎的头)。 +const wavMaxDataBytes = math.MaxUint32 - riffChunkSizeBase + +// wavHeader 装配一个 44 字节的 RIFF/WAVE 头,声明 dataBytes 字节的 PCM16 载荷。 +func wavHeader(dataBytes, sampleRate, channels int) ([wavHeaderBytes]byte, error) { + var hdr [wavHeaderBytes]byte + if sampleRate <= 0 || channels <= 0 { + return hdr, fmt.Errorf("transcode: wav header: %w: sample_rate=%d channels=%d", + ErrEncodeFailed, sampleRate, channels) + } + if dataBytes < 0 || dataBytes > wavMaxDataBytes { + return hdr, fmt.Errorf("transcode: wav header: %w: %d data bytes exceed the RIFF limit", + ErrEncodeFailed, dataBytes) + } + blockAlign := channels * wavPCMBytesPerSample + copy(hdr[wavOffRiffID:], wavRiffID) + binary.LittleEndian.PutUint32(hdr[wavOffChunkSize:], uint32(riffChunkSizeBase+dataBytes)) + copy(hdr[wavOffWaveID:], wavWaveID) + copy(hdr[wavOffFmtID:], wavFmtID) + binary.LittleEndian.PutUint32(hdr[wavOffFmtSize:], wavFmtChunkBytes) + binary.LittleEndian.PutUint16(hdr[wavOffAudioFormat:], wavFormatPCM) + binary.LittleEndian.PutUint16(hdr[wavOffChannels:], uint16(channels)) + binary.LittleEndian.PutUint32(hdr[wavOffSampleRate:], uint32(sampleRate)) + binary.LittleEndian.PutUint32(hdr[wavOffByteRate:], uint32(sampleRate*blockAlign)) + binary.LittleEndian.PutUint16(hdr[wavOffBlockAlign:], uint16(blockAlign)) + binary.LittleEndian.PutUint16(hdr[wavOffBitsPerSamp:], wavBitsPerSample) + copy(hdr[wavOffDataID:], wavDataID) + binary.LittleEndian.PutUint32(hdr[wavOffDataSize:], uint32(dataBytes)) + return hdr, nil +} + +// writePCM16LE 把样本按小端 PCM16 写进 dst(dst 必须够长:2 字节/样本)。 +func writePCM16LE(dst []byte, samples []int16) { + for i, s := range samples { + binary.LittleEndian.PutUint16(dst[i*wavPCMBytesPerSample:], uint16(s)) + } +} + +// encodePCM16WAV 把样本装成一份完整 WAV(44 字节头 + PCM16 数据)。 +// 主路径(codec_amr_nb.go)不用它:那条路直接写进目标缓冲,避免「先攒一份 PCM、 +// 再复制一份 WAV」把峰值内存翻倍(design/25 §2.5 的瞬时上限)。 +func encodePCM16WAV(samples []int16, sampleRate, channels int) ([]byte, error) { + dataBytes := len(samples) * wavPCMBytesPerSample + hdr, err := wavHeader(dataBytes, sampleRate, channels) + if err != nil { + return nil, err + } + out := make([]byte, wavHeaderBytes+dataBytes) + copy(out, hdr[:]) + writePCM16LE(out[wavHeaderBytes:], samples) + return out, nil +} diff --git a/internal/capability/transcode/wav_test.go b/internal/capability/transcode/wav_test.go new file mode 100644 index 0000000..9c199b6 --- /dev/null +++ b/internal/capability/transcode/wav_test.go @@ -0,0 +1,110 @@ +package transcode + +import ( + "encoding/binary" + "errors" + "math" + "testing" +) + +// TestWAVHeaderFields 逐字段断言 44 字节头(design/25 §3.1:不重采样、8 kHz、 +// 单声道、PCM16)。 +func TestWAVHeaderFields(t *testing.T) { + const samples = 160 + dataBytes := samples * wavPCMBytesPerSample + hdr, err := wavHeader(dataBytes, amrSampleRate, amrChannels) + if err != nil { + t.Fatalf("wavHeader: %v", err) + } + if len(hdr) != wavHeaderBytes { + t.Fatalf("header length = %d, want %d", len(hdr), wavHeaderBytes) + } + if string(hdr[wavOffRiffID:wavOffChunkSize]) != wavRiffID || + string(hdr[wavOffWaveID:wavOffFmtID]) != wavWaveID || + string(hdr[wavOffFmtID:wavOffFmtSize]) != wavFmtID || + string(hdr[wavOffDataID:wavOffDataSize]) != wavDataID { + t.Fatalf("chunk ids wrong: %q %q %q %q", + hdr[wavOffRiffID:wavOffChunkSize], hdr[wavOffWaveID:wavOffFmtID], + hdr[wavOffFmtID:wavOffFmtSize], hdr[wavOffDataID:wavOffDataSize]) + } + checks := []struct { + name string + got uint32 + want uint32 + }{ + {"RIFF size", binary.LittleEndian.Uint32(hdr[wavOffChunkSize:]), uint32(riffChunkSizeBase + dataBytes)}, + {"fmt size", binary.LittleEndian.Uint32(hdr[wavOffFmtSize:]), wavFmtChunkBytes}, + {"sample rate", binary.LittleEndian.Uint32(hdr[wavOffSampleRate:]), uint32(amrSampleRate)}, + {"byte rate", binary.LittleEndian.Uint32(hdr[wavOffByteRate:]), uint32(amrSampleRate * wavPCMBytesPerSample)}, + {"data size", binary.LittleEndian.Uint32(hdr[wavOffDataSize:]), uint32(dataBytes)}, + } + for _, c := range checks { + if c.got != c.want { + t.Errorf("%s = %d, want %d", c.name, c.got, c.want) + } + } + if got := binary.LittleEndian.Uint16(hdr[wavOffAudioFormat:]); got != wavFormatPCM { + t.Errorf("audio format = %d, want %d (PCM)", got, wavFormatPCM) + } + if got := binary.LittleEndian.Uint16(hdr[wavOffChannels:]); got != amrChannels { + t.Errorf("channels = %d, want %d", got, amrChannels) + } + if got := binary.LittleEndian.Uint16(hdr[wavOffBlockAlign:]); got != wavPCMBytesPerSample { + t.Errorf("block align = %d, want %d", got, wavPCMBytesPerSample) + } + if got := binary.LittleEndian.Uint16(hdr[wavOffBitsPerSamp:]); got != wavBitsPerSample { + t.Errorf("bits per sample = %d, want %d", got, wavBitsPerSample) + } +} + +// TestEncodePCM16WAVRoundTrip 断言样本 → WAV → 样本往返无损(小端正确)。 +func TestEncodePCM16WAVRoundTrip(t *testing.T) { + samples := []int16{0, 1, -1, math.MaxInt16, math.MinInt16, 1234, -4321} + wav, err := encodePCM16WAV(samples, amrSampleRate, amrChannels) + if err != nil { + t.Fatalf("encodePCM16WAV: %v", err) + } + got := wavDataSamples(t, wav) + if len(got) != len(samples) { + t.Fatalf("round trip samples = %d, want %d", len(got), len(samples)) + } + for i := range samples { + if got[i] != samples[i] { + t.Fatalf("sample %d = %d, want %d", i, got[i], samples[i]) + } + } +} + +// TestWAVHeaderRejectsImpossibleDeclarations 断言不可能的头参数被拒(不写出 +// 一个长度撒谎的头)。 +func TestWAVHeaderRejectsImpossibleDeclarations(t *testing.T) { + cases := []struct { + name string + dataBytes, rate, chans int + }{ + {"zero rate", 2, 0, 1}, + {"negative rate", 2, -1, 1}, + {"zero channels", 2, amrSampleRate, 0}, + {"negative data", -1, amrSampleRate, 1}, + {"data beyond RIFF limit", int(wavMaxDataBytes) + 1, amrSampleRate, 1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if _, err := wavHeader(tc.dataBytes, tc.rate, tc.chans); !errors.Is(err, ErrEncodeFailed) { + t.Fatalf("err = %v, want ErrEncodeFailed", err) + } + }) + } +} + +// TestWritePCM16LEIsLittleEndian 断言字节序:-2 = 0xFE 0xFF。 +func TestWritePCM16LEIsLittleEndian(t *testing.T) { + dst := make([]byte, 2*wavPCMBytesPerSample) + writePCM16LE(dst, []int16{-2, 258}) + if dst[0] != 0xFE || dst[1] != 0xFF { + t.Fatalf("-2 写成 %#x %#x, want 0xfe 0xff", dst[0], dst[1]) + } + if dst[2] != 0x02 || dst[3] != 0x01 { + t.Fatalf("258 写成 %#x %#x, want 0x02 0x01", dst[2], dst[3]) + } +}