Files
Cyrene_v2/internal/capability/transcode/amrnb/dsp_enc_coder.go
T
AskaEth 4a2d276881
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(capability): 转码能力包(vendor 纯 Go AMR-NB 解码器 + 黄金向量逐样本对照 + 8 哨兵 + 注册面)
2026-10-05 13:02:28 +08:00

357 lines
13 KiB
Go

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)
}