SpeechRenderer.cs
#nullable enable
using System;
using System.Collections.Generic;

namespace SharpTalk
{

    public class SpeechRenderer
    {
        private VoiceData _voice;
        private SynthInputDump _dump = null!; // set at start of Render()

        private class ControlBlock
        {
            public short curP_START_Targ;
            public short curP_END_Targ;
            public short prevP_END_Targ;
            public short nextP_START_Targ;
            public int curTarget_TIME;
            public int curTarget_STEP;
            public int curTarget_OFFS;
            public int HEAD_offs;
            public int HEAD_step;
            public int TAIL_offs;
            public int TAIL_step;
            public int TAIL_START_time;
            public int onset_END_TIME;
            public short onset_VAL;
            public int ptrToTargetList; // index into _diphEntries; -1 = no list
        }

        private ControlBlock[] _cb = new ControlBlock[15];
        private short[] _controlData = new short[15];
        private short[] _diphEntries = new short[400];
        private int _nextDiphEntryIdx;

        // Current-phoneme context
        private int _curPhon, _prevPhon, _nextPhon, _prev2Phon;
        private uint _curPhonFlags, _prevPhonFlags, _nextPhonFlags, _prev2PhonFlags;
        private int _curPhonCtrl, _prevPhonCtrl, _nextPhonCtrl, _prev2PhonCtrl;
        private int _curPhonDur;
        private int _curPhonMaxDur;
        private long _curPhonPctOfMaxDur, _curPhonPctOfMaxDur1, _curPhonPctOfMaxDur2;

        // Shared during InitCtrlsForNewPhon iteration
        private int _transLevel, _transTime;
        private int _curBlockIndex;

        private int _durDoneInPhon;
        private int _curPhonBufIndex;
        private bool _startingNewPhon;
        private bool _bigBang = true;

        // Block index constants
        public const int kF1 = 0; public const int kF2 = 1; public const int kF3 = 2;
        public const int kBW1 = 3; public const int kBW2 = 4; public const int kBW3 = 5;
        public const int kFNZ = 6; public const int kAV = 7; public const int kAF = 8;
        public const int kAp2 = 9; public const int kAp3 = 10; public const int kAp4 = 11;
        public const int kAp5 = 12; public const int kAp6 = 13; public const int kAB = 14;
        private const int kNumOfBlocks = 15;

        // Block type constants (match Tables.CtrlBlockTypeTbl)
        public const int kFreqType = 0; public const int kBWType = 1; public const int kFNZType = 2;
        public const int kSourceAmpType = 3; public const int kResonAmpType = 4;

        // Phoneme numbers
        private const int _IY_ = 0; private const int _ER_ = 9; private const int _AY_ = 11;
        private const int _OY_ = 12; private const int _UW_ = 15; private const int _YU_ = 16;
        private const int _SIL_ = 23; private const int _LX_ = 25; private const int _EL_ = 26;
        private const int _EN_ = 27; private const int _w_ = 28; private const int _y_ = 29;
        private const int _r_ = 30; private const int _l_ = 31; private const int _h_ = 32;
        private const int _m_ = 33; private const int _n_ = 34; private const int _NG_ = 35;
        private const int _f_ = 36; private const int _v_ = 37; private const int _TH_ = 38;
        private const int _DH_ = 39; private const int _s_ = 40; private const int _z_ = 41;
        private const int _SH_ = 42; private const int _ZH_ = 43; private const int _p_ = 44;
        private const int _b_ = 45; private const int _t_ = 46; private const int _d_ = 47;
        private const int _k_ = 48; private const int _g_ = 49; private const int _CH_ = 50;
        private const int _JH_ = 51; private const int _TX_ = 52; private const int _DX_ = 53;
        private const int _QX_ = 54; private const int _DD_ = 55;

        // Phoneme flag constants
        private const uint kVowelF = 1 << 0; private const uint kConsonantF = 1 << 1;
        private const uint kVoicedF = 1 << 2; private const uint kVowel1F = 1 << 3;
        private const uint kSonorantF = 1 << 4; private const uint kSonorant1F = 1 << 5;
        private const uint kNasalF = 1 << 6; private const uint kLiqGlideF = 1 << 7;
        private const uint kSonorConsonF = 1 << 8; private const uint kPlosiveF = 1 << 9;
        private const uint kPlosFricF = 1 << 10; private const uint kObstF = 1 << 11;
        private const uint kStopF = 1 << 12; private const uint kAlveolarF = 1 << 13;
        private const uint kVelar = 1 << 14; private const uint kLabialF = 1 << 15;
        private const uint kDentalF = 1 << 16; private const uint kPalatalF = 1 << 17;
        private const uint kYGlideStartF = 1 << 18; private const uint kYGlideEndF = 1 << 19;
        private const uint kGStopF = 1 << 20; private const uint kFrontF = 1 << 21;
        private const uint kDiphthongF = 1 << 22; private const uint kAffricateF = 1 << 24;
        private const uint kLiqGlide2F = 1 << 25; private const uint kVocLiq = 1 << 26;
        private const uint kFric = 1 << 27;

        // Ctrl field masks
        private const int kPlosive_Release = 0x4000;
        private const int kPrimOrEmphStress = 0x1400;
        private const int kStressField = 0x1C00;
        private const int kSyllableTypeField = 0x000F;

        private const int kNoValue = -1;
        private const int kMaxBandWidth = 1000;
        private const int C_V_type = 0;
        private const int V_C_type = 1;
        private const int kFrontR = 0; private const int kMiddleR = 1;
        private const int kBackR = 2; private const int kRoundR = 3;
        private const int kConsonantR = 4;
        private const int kStepSizeRes = 3;
        private const int k1pct = 655;
        private const int kFrameTime = 5;
        private const int kSizeOf1xTbl = 100;
        private const int kOneHalf = 0x8000;

        // Voice data
        private short[] _envelopeListTbl;
        private short[] _lociTbl;
        private short[] _voiceAvTbl;
        private short[] _voiceNoiseAmpTbl;
        private int _nasalTargFreq, _nasalBaseFreq, _locusOffset;
        private int _voiceBWgain1, _voiceBWgain2, _voiceBWgain3;

        public SpeechRenderer(VoiceData voice)
        {
            _voice = voice;
            for (int i = 0; i < _cb.Length; i++) _cb[i] = new ControlBlock();
            bool male = voice.VoiceType == 0;
            _envelopeListTbl = male ? Tables.MaleEnvTbl : Tables.FemaleEnvTbl;
            _lociTbl = male ? Tables.Male_Loci_Tbl : Tables.Female_Loci_Tbl;
            _voiceAvTbl = male ? Tables.avVolTblM : Tables.avVolTblF;
            _voiceNoiseAmpTbl = male ? Tables.Male_NoiseAmpTbl : Tables.Female_NoiseAmpTbl;
            _nasalTargFreq = voice.NasalTarg;
            _nasalBaseFreq = voice.NasalBase;
            _locusOffset = voice.Locus;
            _voiceBWgain1 = (voice.BwGain1 << 16) / 100;
            _voiceBWgain2 = (voice.BwGain2 << 16) / 100;
            _voiceBWgain3 = (voice.BwGain3 << 16) / 100;
        }

        public Frame[] Render(SynthInputDump dump)
        {
            _dump = dump;
            var frames = new List<Frame>();
            _curPhonBufIndex = 0;
            _durDoneInPhon = 0;
            _startingNewPhon = true;

            // Big-Bang: seed curP_END_Targ from the first phoneme's target (once per renderer)
            if (_bigBang)
            {
                _bigBang = false;
                SetPhonContext(0);
                for (_curBlockIndex = 0; _curBlockIndex < kNumOfBlocks; _curBlockIndex++)
                    _cb[_curBlockIndex].curP_END_Targ = (short)GetFirstTarget(0);
            }

            var pitchInterp = new PitchInterpolator(dump);
            int totalFrames = 0;
            for (int i = 0; i < dump.PhonBuf2InIndex; i++) totalFrames += dump.DurBuf[i];

            for (int i = 0; i < totalFrames; i++)
            {
                if (_durDoneInPhon >= _dump.DurBuf[_curPhonBufIndex])
                {
                    _curPhonBufIndex++;
                    _durDoneInPhon = 0;
                    _startingNewPhon = true;
                }
                if (_startingNewPhon) { InitCtrlsForNewPhon(); pitchInterp.DoNote(_curPhonBufIndex); _startingNewPhon = false; }

                short f0 = pitchInterp.Step();
                InterpolateFormants();
                frames.Add(SaveFrame(f0, (byte)_dump.PhonCtrlBuf2[_curPhonBufIndex]));
                _durDoneInPhon++;
            }
            return frames.ToArray();
        }

        private void SetPhonContext(int index)
        {
            _curPhon = GP(index); _curPhonFlags = PF(_curPhon); _curPhonCtrl = PC(index);
            _nextPhon = GP(index + 1); _nextPhonFlags = PF(_nextPhon); _nextPhonCtrl = PC(index + 1);
            _prevPhon = GP(index - 1); _prevPhonFlags = PF(_prevPhon); _prevPhonCtrl = PC(index - 1);
            _prev2Phon = GP(index - 2); _prev2PhonFlags = PF(_prev2Phon); _prev2PhonCtrl = PC(index - 2);
            _curPhonDur = (index >= 0 && index < _dump.DurBuf.Length) ? _dump.DurBuf[index] : 0;
        }

        private void FillPhonTargets()
        {
            for (int i = 0; i < kNumOfBlocks; i++) _cb[i].onset_END_TIME = 0;
            _nextDiphEntryIdx = 0;
            if ((_curPhonFlags & kPlosFricF) == 0 && _curPhon != _SIL_ && _curPhon >= 0 && _curPhon < Tables.MaxDurTbl.Length)
            {
                int maxDur = Tables.MaxDurTbl[_curPhon] / kFrameTime;
                _curPhonMaxDur = maxDur > 0 ? maxDur : 1;
                _curPhonPctOfMaxDur = ((long)_curPhonDur << 16) / _curPhonMaxDur;
                _curPhonPctOfMaxDur1 = (_curPhonPctOfMaxDur >> 1) + kOneHalf;
                _curPhonPctOfMaxDur2 = _curPhonPctOfMaxDur1 - (10L * k1pct);
            }
        }

        private void InitCtrlsForNewPhon()
        {
            SetPhonContext(_curPhonBufIndex);
            FillPhonTargets();

            for (_curBlockIndex = 0; _curBlockIndex < kNumOfBlocks; _curBlockIndex++)
            {
                var cb = _cb[_curBlockIndex];
                int bt = Tables.CtrlBlockTypeTbl[_curBlockIndex];

                cb.prevP_END_Targ = cb.curP_END_Targ;
                cb.nextP_START_Targ = (short)GetFirstTarget(_curPhonBufIndex + 1);
                cb.curTarget_OFFS = 0;
                cb.ptrToTargetList = -1;

                short rawTarg = GetTargetRaw(_curPhonBufIndex);
                if (rawTarg < kNoValue)
                {
                    // Diphthong envelope
                    GetDiphthongs(rawTarg & 0x7FFF);
                }
                else
                {
                    cb.curP_START_Targ = rawTarg;
                    cb.curTarget_STEP = 0;
                    cb.curTarget_TIME = _curPhonDur;

                    if (bt == kFreqType)
                    {
                        int artic = k1pct * 10;
                        if ((_curPhonCtrl & kStressField) != 0)
                            artic = (_curBlockIndex == kF2) ? k1pct * 25 : k1pct * 15;
                        cb.curP_START_Targ += (short)((((cb.prevP_END_Targ + cb.nextP_START_Targ) >> 1) - cb.curP_START_Targ) * artic >> 16);
                    }
                    cb.curP_END_Targ = cb.curP_START_Targ;
                }

                if (bt == kFreqType)
                    cb.nextP_START_Targ += (short)((cb.curP_END_Targ - cb.nextP_START_Targ) * (k1pct * 10) >> 16);

                // HEAD envelope
                _transLevel = (cb.prevP_END_Targ + cb.curP_START_Targ) >> 1;
                _transTime = 32 / kFrameTime;
                HeadRules(cb, bt);

                cb.HEAD_offs = 0; cb.HEAD_step = 0;
                if (_transTime > 0)
                {
                    cb.HEAD_offs = (_transLevel - cb.curP_START_Targ) << kStepSizeRes;
                    if (cb.HEAD_offs != 0)
                    {
                        int hs = (int)(((long)OvX(_transTime) * cb.HEAD_offs) >> 16);
                        cb.HEAD_step = hs;
                        cb.HEAD_offs = hs * _transTime;
                    }
                }

                // TAIL envelope
                _transLevel = (cb.curP_END_Targ + cb.nextP_START_Targ) >> 1;
                _transTime = 25 / kFrameTime;
                TailRules(cb, bt);

                cb.TAIL_offs = 0; cb.TAIL_step = 0;
                if (_transTime > 0)
                {
                    int ts = (_transLevel - cb.curP_END_Targ) << kStepSizeRes;
                    if (ts != 0)
                        cb.TAIL_step = (int)(((long)OvX(_transTime) * ts) >> 16);
                }
            }
            InsertBurst();
        }

        private void GetDiphthongs(int index)
        {
            var cb = _cb[_curBlockIndex];
            int bt = Tables.CtrlBlockTypeTbl[_curBlockIndex];

            short p1 = _envelopeListTbl[index];
            short t1 = _envelopeListTbl[index + 1];
            short p2 = _envelopeListTbl[index + 2];
            short t2 = _envelopeListTbl[index + 3];

            t1 = (short)ScalePrcnt(t1);
            t2 = (short)ScalePrcnt(t2);

            if (bt == kFreqType)
            {
                int artic = k1pct * 10;
                if (cb.prevP_END_Targ > 0) p1 += (short)(((cb.prevP_END_Targ - p1) * artic) >> 16);
                p1 += (short)AdjustColored(_curPhonBufIndex, 0);
                if (cb.nextP_START_Targ > 0) p2 += (short)(((cb.nextP_START_Targ - p2) * artic) >> 16);
                p2 += (short)AdjustColored(_curPhonBufIndex, 1);
            }

            int rampTime = t2 - t1;
            int diff = (p2 - p1) << kStepSizeRes;
            int step = rampTime > 0 ? (rampTime < kSizeOf1xTbl ? (int)(((long)OvX(rampTime) * diff) >> 16) : diff / rampTime) : 0;

            cb.curP_START_Targ = p1;
            cb.curTarget_TIME = t1;
            cb.curTarget_STEP = 0;
            cb.curP_END_Targ = p2;

            cb.ptrToTargetList = _nextDiphEntryIdx;
            _diphEntries[_nextDiphEntryIdx++] = (short)t2;
            _diphEntries[_nextDiphEntryIdx++] = (short)step;
            _diphEntries[_nextDiphEntryIdx++] = (short)_curPhonDur;
            _diphEntries[_nextDiphEntryIdx++] = 0;
        }

        private int ScalePrcnt(int pct)
        {
            long t = (pct * _curPhonPctOfMaxDur) >> 8;
            t = (_curPhonMaxDur * t / 100) >> 8;
            return t <= 0 ? 1 : (int)t;
        }

        private int AdjustColored(int index, int entry)
        {
            int cur = GP(index); int next = GP(index + 1); int prev = GP(index - 1);
            uint cf = PF(cur); uint nf = PF(next); uint pf = PF(prev);
            int ctrl = PC(index);
            int adj = 0;
            if (_curBlockIndex == kF3)
            {
                if ((cf & kVowel1F) != 0 && cur != _ER_ && ((pf & kLiqGlide2F) != 0 || (nf & kLiqGlide2F) != 0))
                    adj = -150;
            }
            else if (_curBlockIndex == kF2)
            {
                if (next == _LX_)
                {
                    if ((cf & kFrontF) != 0) adj = -150;
                    else if ((cur == _AY_ || cur == _OY_) && entry > 0) adj = -250;
                }
                if ((prev == _LX_ || prev == _l_ || prev == _w_) && (cf & kFrontF) != 0) adj = -150;
                if (cur == _UW_ && (pf & kAlveolarF) != 0) adj = 200;
                if (entry > 0 && (cur == _UW_ || cur == _YU_) && (nf & kAlveolarF) != 0) adj += 200;
                if ((ctrl & kStressField) != 0) adj >>= 1;
                else { adj += adj >> 1; if (entry > 0 && cur == _YU_) adj = 400; }
                if (adj > 400) adj = 400; else if (adj < -400) adj = -400;
            }
            return adj;
        }

        private void GetLocus(int iCons, int iVowel, int bType)
        {
            if (_curBlockIndex < kF1 || _curBlockIndex > kF3) return;
            int cons = GP(iCons); int vow = GP(iVowel);
            int vowRank, consRank;
            if (bType == C_V_type) { vowRank = Tables.Rank_FWD_Tbl[vow]; consRank = Tables.Rank_BKWD_Tbl[cons]; }
            else { vowRank = Tables.Rank_BKWD_Tbl[vow]; consRank = Tables.Rank_FWD_Tbl[cons]; }
            if (consRank != kConsonantR || vowRank == kConsonantR) return;

            uint vf = PF(vow); uint cf = PF(cons);
            bool f2y = (vf & kYGlideStartF) != 0;

            int v1Targ = (bType == C_V_type) ? GetFirstTarget(iVowel) : GetLastTarget(iVowel);

            int lociIdx = vowRank switch { kFrontR => Tables.Front_Loci_Tbl[cons], kMiddleR => Tables.Mid_Loci_Tbl[cons], _ => Tables.Back_Loci_Tbl[cons] };
            if (lociIdx == kNoValue) return;

            lociIdx = (lociIdx >> 1) + (_curBlockIndex - kF1) * 3;
            int lFreq = _lociTbl[lociIdx++] + _locusOffset;
            int lPcnt = _lociTbl[lociIdx++];
            _transTime = _lociTbl[lociIdx] / kFrameTime;

            if ((cf & kNasalF) == 0 && !f2y) _transTime -= _transTime >> 2;
            if (vowRank == kRoundR && _curBlockIndex != kF1 && (cf & (kDentalF | kPalatalF)) != 0)
                lPcnt = (lPcnt >> 1) + 50;
            if (f2y && _curBlockIndex == kF2) lPcnt = (25 - (lPcnt >> 2)) + lPcnt;

            _transLevel = lFreq + (lPcnt * (v1Targ - lFreq)) / 100;
        }

        private void HeadRules(ControlBlock cb, int bt)
        {
            if (bt == kFreqType)
            {
                if ((_curPhonFlags & kSonorant1F) != 0)
                {
                    if ((_curPhonFlags & kLiqGlideF) == 0)
                    {
                        _transTime = 45 / kFrameTime;
                        if ((_prevPhonFlags & kLiqGlideF) != 0)
                        {
                            _transLevel = (cb.prevP_END_Targ + _transLevel) >> 1;
                            if (_prevPhon == _l_ && _curBlockIndex == kF1) _transLevel += 80;
                            else if (_prevPhon == _r_ && _curBlockIndex != kF1) _transTime = 70 / kFrameTime;
                        }
                        else if (_curPhon == _h_) _transLevel = (cb.prevP_END_Targ + _transLevel) >> 1;
                    }
                    else
                    {
                        _transLevel = (cb.prevP_END_Targ + _transLevel) >> 1;
                        _transTime = 32 / kFrameTime;
                    }
                }
                if (_curPhon == _SIL_)
                {
                    _transLevel = cb.prevP_END_Targ; _transTime = _curPhonDur;
                }
                else
                {
                    GetLocus(_curPhonBufIndex - 1, _curPhonBufIndex, C_V_type);
                    GetLocus(_curPhonBufIndex, _curPhonBufIndex - 1, V_C_type);
                    if ((_prevPhonFlags & kStopF) != 0 && (_prevPhonFlags & kVoicedF) == 0 && _curBlockIndex == kF1)
                        _transLevel += 100;
                    if ((_curPhonFlags & kPlosFricF) != 0)
                    {
                        _transTime = (_curBlockIndex == kF1) ? 20 / kFrameTime : 30 / kFrameTime;
                        if ((_curPhonFlags & kStopF) != 0) _transTime = _curPhonDur;
                    }
                    if ((_curPhonFlags & kNasalF) != 0)
                    {
                        _transTime = (_curBlockIndex == kF1) ? 0 : _curPhonDur;
                        if ((_curPhon == _n_ || _curPhon == _EN_) && Tables.Rank_BKWD_Tbl[_prevPhon] == kFrontR)
                        {
                            if (_curBlockIndex == kF2) { _transLevel -= (_prevPhonFlags & kYGlideEndF) != 0 ? 200 : 100; }
                            else if (_curBlockIndex == kF3) _transLevel -= 100;
                        }
                        else if (_curPhon == _m_ && _curBlockIndex == kF2 && (_prevPhonFlags & kYGlideEndF) != 0)
                            _transLevel -= 150;
                    }
                }
                if ((_curPhonFlags & kPlosFricF) == 0 && Tables.Rank_BKWD_Tbl[_prevPhon] != kConsonantR && _transTime > 0)
                    _transTime = 1 + (int)((_curPhonPctOfMaxDur1 * _transTime) >> 16);
            }
            else if (bt == kFNZType)
            {
                if ((_prevPhonFlags & kNasalF) != 0 && (_curPhonFlags & kNasalF) == 0)
                { _transLevel = _nasalBaseFreq + ((_nasalTargFreq - _nasalBaseFreq) >> 1); _transTime = 80 / kFrameTime; }
                if ((_curPhonFlags & kNasalF) != 0) _transLevel = _nasalTargFreq;
            }
            else if (bt == kBWType)
            {
                if ((_curPhonFlags & kVoicedF) != 0)
                {
                    if ((_prevPhonFlags & kVoicedF) == 0 && _curBlockIndex == kBW1)
                    { _transTime = 50 / kFrameTime; _transLevel = (_cb[kF1].curP_START_Targ >> 3) + cb.curP_START_Targ; }
                    else _transTime = 40 / kFrameTime;
                }
                else _transTime = 20 / kFrameTime;

                if (_prevPhon == _SIL_)
                { _transLevel = (kBW3 - bt) * 50 + cb.curP_START_Targ; _transTime = 50 / kFrameTime; }
                else if (_curPhon == _SIL_)
                {
                    _transLevel = (kBW3 - bt) * 50 + cb.prevP_END_Targ;
                    if ((_prev2PhonFlags & kVoicedF) == 0 && (_prevPhonCtrl & kPlosive_Release) != 0 && _curBlockIndex == kBW1)
                        _transLevel = 250;
                    _transTime = 50 / kFrameTime;
                }
                if ((_prevPhonFlags & kNasalF) != 0)
                {
                    _transLevel = cb.curP_START_Targ;
                    if (_curBlockIndex == kBW2 && (_prevPhon == _n_ || _prevPhon == _EN_) && Tables.Rank_FWD_Tbl[_curPhon] != kFrontR)
                    { _transLevel += 60; _transTime = 60 / kFrameTime; }
                    else if (_curBlockIndex == kBW1) { _transLevel += 70; _transTime = 100 / kFrameTime; }
                }
                if ((_curPhonFlags & kNasalF) != 0) _transTime = 0;
            }
            else // kSourceAmpType / kResonAmpType
            {
                int ampT = cb.curP_START_Targ - 10;
                if (_transLevel < ampT || (_prevPhonFlags & kStopF) != 0 || _prevPhon == _JH_)
                {
                    _transLevel = ampT;
                    if ((_curPhonFlags & kPlosFricF) == 0) _transTime = 20 / kFrameTime;
                    if (_curBlockIndex == kAV)
                    {
                        if (_prevPhon == _SIL_ && (_curPhonFlags & kVoicedF) != 0)
                        { _transLevel -= 8; _transTime = 45 / kFrameTime; }
                        if ((_prevPhonFlags & kPlosFricF) != 0) _transLevel = ampT + 6;
                        if ((_prevPhonFlags & kStopF) != 0) _transLevel = cb.curP_START_Targ - 5;
                    }
                }
                if ((_curPhonFlags & kVoicedF) != 0 && (_prevPhonFlags & kNasalF) != 0) _transTime = 0;
                if ((_prevPhonFlags & kVoicedF) != 0 && (_curPhonFlags & kNasalF) != 0 && _curBlockIndex == kAV) _transTime = 0;
                ampT = cb.prevP_END_Targ - 10;
                if (_transLevel < ampT) { _transLevel = ampT - 3; if (_curPhon == _SIL_) _transTime = 70 / kFrameTime; }
                if (_curBlockIndex == kAp3 && (_curPhonFlags & kAffricateF) != 0)
                { _transTime = _curPhonDur - 2; _transLevel = cb.curP_START_Targ - 30; }
                if (_curBlockIndex == kAV && (_curPhonFlags & kPlosiveF) != 0) _transTime = 10 / kFrameTime;
                if (_curBlockIndex == kAF)
                {
                    if (_curPhon == _SIL_ || _curPhon == _f_ || _curPhon == _TH_ || _curPhon == _s_ || _curPhon == _SH_)
                    {
                        if ((_prevPhonFlags & kVoicedF) != 0 && (_prevPhonFlags & kPlosFricF) == 0)
                        {
                            if (_curPhon == _SIL_) { _transTime = 80 / kFrameTime; _transLevel = 52; }
                            else { _transTime = 45 / kFrameTime; _transLevel = 48; }
                        }
                    }
                }
            }
            if (_transTime > _curPhonDur) _transTime = _curPhonDur;
            if (_transTime > 130 / kFrameTime) _transTime = 130 / kFrameTime;
            if (_transTime < 0) _transTime = 0;
        }

        private void TailRules(ControlBlock cb, int bt)
        {
            if (bt == kFreqType)
            {
                if ((_curPhonFlags & kSonorant1F) != 0)
                {
                    _transTime = 45 / kFrameTime;
                    if ((_curPhonFlags & kLiqGlideF) == 0)
                    {
                        if ((_nextPhonFlags & kLiqGlideF) != 0)
                        {
                            if (_curBlockIndex == kF3) _transTime = 60 / kFrameTime;
                            if (_nextPhon == _l_ && _curBlockIndex == kF1) _transLevel += 80;
                        }
                        else if (_nextPhon == _h_) _transLevel = (cb.curP_END_Targ + _transLevel) >> 1;
                    }
                    else
                    {
                        if ((_nextPhonFlags & kLiqGlideF) == 0)
                        { _transLevel = (cb.curP_END_Targ + _transLevel) >> 1; _transTime = 20 / kFrameTime; }
                        else
                        { _transLevel = (cb.curP_END_Targ + _transLevel) >> 1; _transTime = 40 / kFrameTime; }
                    }
                }
                if (_nextPhon == _SIL_)
                {
                    _transTime = 0;
                }
                else
                {
                    GetLocus(_curPhonBufIndex + 1, _curPhonBufIndex, V_C_type);
                    GetLocus(_curPhonBufIndex, _curPhonBufIndex + 1, C_V_type);
                    if ((_curPhonFlags & kPlosFricF) != 0)
                    {
                        _transTime = (_curBlockIndex == kF1) ? 20 / kFrameTime : 30 / kFrameTime;
                        if ((_curPhonFlags & kStopF) != 0)
                        {
                            _transTime = _curPhonDur;
                            if ((_curPhonFlags & kVoicedF) == 0 && _curBlockIndex == kF1) _transLevel += 100;
                        }
                    }
                    if ((_curPhonFlags & kNasalF) != 0)
                    {
                        _transTime = (_curBlockIndex == kF1) ? 0 : _curPhonDur;
                        if ((_curPhon == _n_ || _curPhon == _EN_) && Tables.Rank_FWD_Tbl[_nextPhon] == kFrontR)
                        {
                            if (_curBlockIndex == kF2) { _transLevel -= 100; if ((_nextPhonFlags & kYGlideStartF) != 0) _transLevel -= 100; }
                            else if (_curBlockIndex == kF3) _transLevel -= 100;
                        }
                        else if (_curPhon == _m_ && _curBlockIndex == kF2 && (_nextPhonFlags & kYGlideStartF) != 0) _transLevel -= 150;
                    }
                }
                if ((_curPhonFlags & kPlosFricF) == 0 && Tables.Rank_FWD_Tbl[_nextPhon] != kConsonantR && _transTime > 0)
                    _transTime = 1 + (int)((_curPhonPctOfMaxDur2 * _transTime) >> 16);
            }
            else if (bt == kFNZType)
            {
                if ((_nextPhonFlags & kNasalF) != 0 && (_curPhonFlags & kNasalF) == 0)
                { _transLevel = _nasalTargFreq; _transTime = 80 / kFrameTime; }
            }
            else if (bt == kBWType)
            {
                if ((_curPhonFlags & kVoicedF) != 0)
                {
                    _transTime = 40 / kFrameTime;
                    if ((_nextPhonFlags & kVoicedF) == 0 && _curBlockIndex == kBW1)
                    { _transTime = 50 / kFrameTime; _transLevel = (_cb[kF1].curP_START_Targ >> 3) + cb.curP_END_Targ; }
                }
                else _transTime = 20 / kFrameTime;
                if (_nextPhon == _SIL_)
                { _transLevel = (kBW3 - bt) * 50 + cb.curP_END_Targ; _transTime = 50 / kFrameTime; }
                else if (_curPhon == _SIL_)
                { _transLevel = (kBW3 - bt) * 50 + cb.nextP_START_Targ; _transTime = 50 / kFrameTime; }
                if ((_nextPhonFlags & kNasalF) != 0)
                {
                    _transLevel = cb.curP_END_Targ;
                    if (_curBlockIndex == kBW2 && (_nextPhon == _n_ || _nextPhon == _EN_) && Tables.Rank_FWD_Tbl[_curPhon] != kFrontR)
                    { _transLevel += 60; _transTime = 60 / kFrameTime; }
                    else if (_curBlockIndex == kBW1) { _transLevel += 100; _transTime = 100 / kFrameTime; }
                }
                if ((_curPhonFlags & kNasalF) != 0) _transTime = 0;
            }
            else // kSourceAmpType / kResonAmpType
            {
                int ampT = cb.nextP_START_Targ - 10;
                if (_transLevel < ampT) { _transLevel = ampT; if (_curPhon == _SIL_) _transTime = 70 / kFrameTime; }

                bool gotoEnd = false;
                if (_curBlockIndex == kAV && _transLevel < cb.nextP_START_Targ)
                {
                    if (_curPhon != _v_ && _curPhon != _DH_ && _curPhon != _JH_ && _curPhon != _ZH_ && _curPhon != _z_)
                    {
                        _transTime = 0;
                        if ((_curPhonFlags & (kStopF | kAffricateF)) != 0)
                        {
                            if ((_curPhonFlags & kVoicedF) != 0)
                            { _transLevel = cb.curP_END_Targ - 3; _transTime = 45 / kFrameTime; }
                            else _transTime = 0;
                            gotoEnd = true;
                        }
                    }
                }
                if (!gotoEnd)
                {
                    if ((_curPhonFlags & kVoicedF) != 0 && (_nextPhonFlags & kNasalF) != 0) _transTime = 0;
                    if ((_curPhonFlags & kNasalF) != 0)
                    {
                        bool nextVoicedNonStop = (_nextPhonFlags & kVoicedF) != 0 && (_curPhonFlags & kPlosFricF) == 0 && (_nextPhonCtrl & kPlosive_Release) == 0;
                        _transTime = nextVoicedNonStop ? 0 : 40 / kFrameTime;
                    }
                    ampT = cb.curP_END_Targ - 10;
                    if ((_curPhonFlags & kPlosiveF) != 0)
                    {
                        _transTime = 15 / kFrameTime;
                        if ((_curPhonFlags & kStopF) != 0 || _curPhon == _DX_ || _curPhon == _QX_ || _curPhon == _DD_)
                            ampT = cb.curP_END_Targ;
                    }
                    if (_transLevel < ampT) { _transLevel = ampT - 3; _transTime = 20 / kFrameTime; }
                    if (_curBlockIndex == kAV)
                    {
                        if (_transLevel < ampT || (ampT > 0 && (_nextPhonCtrl & kPlosive_Release) != 0))
                        {
                            _transLevel = ampT + 3;
                            if (_nextPhon == _SIL_ || (_nextPhonCtrl & kPlosive_Release) != 0) _transTime = 75 / kFrameTime;
                        }
                    }
                    if (_nextPhon >= _p_)
                    {
                        if ((_curPhonFlags & kNasalF) == 0 || _curBlockIndex != kAV) _transTime = 0;
                    }
                    if (_curBlockIndex == kAF)
                    {
                        if (_curPhon == _f_ || _curPhon == _TH_ || _curPhon == _s_ || _curPhon == _SH_)
                        {
                            if ((_nextPhonFlags & kVoicedF) != 0 && (_nextPhonFlags & kPlosFricF) == 0)
                            { _transTime = 40 / kFrameTime; _transLevel = 52; }
                        }
                        if ((_curPhonFlags & kVowelF) != 0 && _nextPhon == _SIL_)
                        { _transTime = 130 / kFrameTime; _transLevel = 52; }
                    }
                }
            }
            if (_transTime > _curPhonDur) _transTime = _curPhonDur;
            if (_transTime > 130 / kFrameTime) _transTime = 130 / kFrameTime;
            _cb[_curBlockIndex].TAIL_START_time = _curPhonDur - _transTime;
            if (_transTime < 0) _transTime = 0;
        }

        private void InsertBurst()
        {
            if ((_curPhonFlags & kPlosiveF) != 0)
            {
                int burstDur = Tables.BurstDurTbl[_curPhon] / kFrameTime;
                if ((_curPhonFlags & kStopF) != 0 && (_curPhonFlags & kVoicedF) == 0)
                {
                    if ((_nextPhonFlags & (kStopF | kNasalF)) != 0)
                        burstDur = (_nextPhonCtrl & kPrimOrEmphStress) != 0 ? 0 : burstDur >> 1;
                }
                int closureDur = _curPhonDur - burstDur;
                if ((_curPhonFlags & kAffricateF) != 0 && closureDur > 80 / kFrameTime) closureDur = 80 / kFrameTime;
                for (int i = kAp2; i <= kAB; i++) { _cb[i].onset_END_TIME = closureDur; _cb[i].onset_VAL = 0; }
            }

            if ((_prevPhonFlags & kStopF) != 0 && (_prevPhonFlags & kVoicedF) == 0 && (_curPhonFlags & kSonorant1F) != 0)
            {
                int rel = 40 / kFrameTime;
                _cb[kAV].onset_VAL = 0;
                _cb[kAF].onset_VAL = (short)(Tables.Rank_FWD_Tbl[_nextPhon] == kFrontR ? 48 : 54);
                if ((_curPhonCtrl & kVowelF) == 0) { rel = 25 / kFrameTime; _cb[kAF].onset_VAL -= 3; }
                if ((_curPhonCtrl & kLiqGlideF) != 0 || _curPhon == _ER_) _cb[kAF].onset_VAL += 3;
                if (_prev2Phon == _s_)
                {
                    if ((_prev2PhonCtrl & kSyllableTypeField) == 0) rel = 10 / kFrameTime;
                }
                else if ((_curPhonCtrl & kVowelF) == 0) rel += 20 / kFrameTime;
                if (rel >= _curPhonDur) rel = _curPhonDur - 1;
                if (rel > (_curPhonDur >> 1) && (_curPhonFlags & kVowelF) != 0 && (_curPhonCtrl & kPrimOrEmphStress) != 0)
                    rel = _curPhonDur >> 1;
                if ((_curPhonCtrl & kPlosive_Release) != 0) { rel = _curPhonDur; _cb[kAF].onset_VAL = 0; }
                _cb[kAV].onset_END_TIME = _cb[kAF].onset_END_TIME = _cb[kBW1].onset_END_TIME = _cb[kBW2].onset_END_TIME = rel;
                _cb[kBW1].onset_VAL = (short)(_cb[kBW1].curP_START_Targ + 250);
                _cb[kBW2].onset_VAL = (short)(_cb[kBW2].curP_START_Targ + 70);
            }

            if ((_curPhonFlags & kStopF) != 0 && (_curPhonFlags & kVoicedF) != 0 &&
                (_prevPhonFlags & kVoicedF) != 0 && (_nextPhonFlags & kVoicedF) == 0 && _curPhon != _TX_)
            {
                _cb[kAV].onset_END_TIME = _curPhonDur - (10 / kFrameTime);
                _cb[kBW1].onset_END_TIME = _cb[kBW2].onset_END_TIME = _cb[kBW3].onset_END_TIME = _curPhonDur;
                _cb[kAV].onset_VAL = 53;
                _cb[kBW1].onset_VAL = 1000; _cb[kBW2].onset_VAL = 1000; _cb[kBW3].onset_VAL = 1200;
            }
        }

        private void InterpolateFormants()
        {
            // F1-FNZ: combined offset shifted at end
            for (int i = kF1; i <= kFNZ; i++)
            {
                var cb = _cb[i];
                if (cb.ptrToTargetList >= 0 && _durDoneInPhon > cb.curTarget_TIME)
                {
                    int p = cb.ptrToTargetList;
                    cb.curTarget_TIME = _diphEntries[p++];
                    cb.curTarget_STEP = _diphEntries[p++];
                    cb.ptrToTargetList = p;
                    cb.curP_START_Targ += (short)(cb.curTarget_OFFS >> kStepSizeRes);
                    cb.curTarget_OFFS = 0;
                }
                cb.curTarget_OFFS += cb.curTarget_STEP;

                int offset = cb.curTarget_OFFS + cb.HEAD_offs;
                if (cb.HEAD_offs != 0) cb.HEAD_offs -= cb.HEAD_step;
                if (_durDoneInPhon >= cb.TAIL_START_time) { offset += cb.TAIL_offs; cb.TAIL_offs += cb.TAIL_step; }

                _controlData[i] = (short)(cb.curP_START_Targ + (offset >> kStepSizeRes));
            }

            // AV-AB: HEAD and TAIL shifted separately (matches C's SaveFrame loop)
            for (int i = kAV; i <= kAB; i++)
            {
                var cb = _cb[i];
                int val = cb.curP_START_Targ + (cb.HEAD_offs >> kStepSizeRes);
                if (cb.HEAD_offs != 0) cb.HEAD_offs -= cb.HEAD_step;
                if (_durDoneInPhon >= cb.TAIL_START_time) { val += cb.TAIL_offs >> kStepSizeRes; cb.TAIL_offs += cb.TAIL_step; }
                _controlData[i] = (short)val;

                if (cb.onset_END_TIME > 0)
                {
                    if (_durDoneInPhon < cb.onset_END_TIME)
                        _controlData[i] = cb.onset_VAL;
                    else if (i >= kAp2 && _durDoneInPhon == cb.onset_END_TIME + 1 && _controlData[i] > 10)
                        _controlData[i] -= 10;
                }
            }
        }

        // Returns raw table value: >=0 direct Hz, -1 kNoValue, <-1 diphthong
        private short GetTargetRaw(int index)
        {
            int bt = Tables.CtrlBlockTypeTbl[_curBlockIndex];
            int cur = GP(index); uint cf = PF(cur); int ctrl = PC(index);
            int next = GP(index + 1); uint nf = PF(next);
            int prev = GP(index - 1); uint pf = PF(prev);
            short tv = -1;

            if (bt == kFreqType || bt == kBWType)
            {
                short[] tbl = GetVoiceFormantTable(_curBlockIndex);
                tv = tbl[cur];
                if (tv < kNoValue) return tv; // diphthong: return raw

                if (tv == kNoValue)
                {
                    tv = tbl[next];
                    if (tv == kNoValue)
                    {
                        tv = tbl[GP(index + 2)];
                        if (tv == kNoValue)
                        {
                            tv = tbl[prev];
                            if (tv < 0 && tv != kNoValue) tv = _envelopeListTbl[(tv & 0x7FFF) + 2];
                            if (tv == kNoValue) tv = Tables.DefaultTargTbl[_curBlockIndex];
                        }
                    }
                    if (tv < kNoValue) tv = _envelopeListTbl[tv & 0x7FFF];
                    if (_curBlockIndex == kF1 && (cf & kPlosFricF) != 0 && (cf & kObstF) == 0 && (pf & kVowelF) != 0) tv += 40;
                }
                if ((cur == _n_ || cur == _EN_) && _curBlockIndex == kBW2 && Tables.Rank_FWD_Tbl[next] != kFrontR) tv += 60;
                if ((cur == _n_ || cur == _EN_ || cur == _NG_) && _curBlockIndex == kBW3 &&
                    ((nf & kYGlideStartF) != 0 || (pf & kYGlideEndF) != 0)) tv = (short)kMaxBandWidth;
            }
            else if (bt == kFNZType)
                tv = (short)(((cf & kNasalF) != 0) ? _nasalTargFreq : _nasalBaseFreq);
            else if (bt == kSourceAmpType)
            {
                if (_curBlockIndex == kAV)
                {
                    tv = _voiceAvTbl[cur];
                    if ((ctrl & kPlosive_Release) != 0) tv -= (short)(((pf & kNasalF) != 0) ? 6 : 20);
                    if ((cf & kStopF) != 0 && (pf & kVoicedF) == 0) tv = 0;
                    if (cur == _h_ && (pf & kVoicedF) != 0 && (ctrl & kPrimOrEmphStress) == 0) tv = 54;
                }
                else if (cur == _h_)
                {
                    tv = (short)(Tables.Rank_FWD_Tbl[next] == kFrontR ? 58 : 62);
                    if ((ctrl & kStressField) == 0) tv -= 1;
                }
                else tv = 0;
            }
            else if (bt == kResonAmpType)
            {
                tv = Tables.NoiseIndexTbl[cur];
                if (tv == kNoValue) tv = 0;
                else
                {
                    int rank = (next == _SIL_) ? Tables.Rank_BKWD_Tbl[prev] : Tables.Rank_FWD_Tbl[next];
                    if (rank == kRoundR) rank = kBackR;
                    int idx2 = tv + (_curBlockIndex - kAp2) + rank * 6;
                    tv = _voiceNoiseAmpTbl[idx2];
                    if ((PC(index + 1) & kPlosive_Release) != 0 && tv >= 4) tv -= 4;
                }
            }
            return tv;
        }

        private int GetFirstTarget(int index)
        {
            short t = GetTargetRaw(index);
            if (t < kNoValue)
            {
                int i = t & 0x7FFF;
                t = _envelopeListTbl[i];
                if (Tables.CtrlBlockTypeTbl[_curBlockIndex] == kFreqType) t += (short)AdjustColored(index, 0);
            }
            return t;
        }

        private int GetLastTarget(int index)
        {
            short t = GetTargetRaw(index);
            if (t < kNoValue)
            {
                int i = (t & 0x7FFF) + 2;
                t = _envelopeListTbl[i];
                if (Tables.CtrlBlockTypeTbl[_curBlockIndex] == kFreqType) t += (short)AdjustColored(index, 1);
            }
            return t;
        }

        private short[] GetVoiceFormantTable(int bi)
        {
            bool m = _voice.VoiceType == 0;
            return bi switch
            {
                kF1 => m ? Tables.f1FreqTblM : Tables.f1FreqTblF,
                kF2 => m ? Tables.f2FreqTblM : Tables.f2FreqTblF,
                kF3 => m ? Tables.f3FreqTblM : Tables.f3FreqTblF,
                kBW1 => m ? Tables.b1FreqTblM : Tables.b1FreqTblF,
                kBW2 => m ? Tables.b2FreqTblM : Tables.b2FreqTblF,
                kBW3 => m ? Tables.b3FreqTblM : Tables.b3FreqTblF,
                _ => throw new ArgumentException()
            };
        }

        // Short helpers
        private int GP(int i) { if (i >= 0 && i < _dump.PhonBuf2.Length) return _dump.PhonBuf2[i]; return _SIL_; }
        private uint PF(int p) { if (p >= 0 && p < Tables.PhonFlags2.Length) return Tables.PhonFlags2[p]; return 0; }
        private int PC(int i) { if (i >= 0 && i < _dump.PhonCtrlBuf2.Length) return (int)_dump.PhonCtrlBuf2[i]; return 0; }
        private int OvX(int x) { if (x <= 0) return 0; if (x < kSizeOf1xTbl) return (int)Tables.One_Over_X_Tbl[x]; return (int)(65536L / x); }

        public static short LogToLin(short v)
        {
            if (v > 63) v = 63;
            if (v < 0) return 0;
            return Tables.LogToLin[v >> 1];
        }

        private Frame SaveFrame(short f0, byte phonCtrl)
        {
            var f = new Frame();
            f.F0 = f0;
            short curF1 = _controlData[kF1];
            short curF2 = _controlData[kF2];
            short curF3 = _controlData[kF3];
            while (curF2 - curF1 < 200) curF1 -= 10;
            while (curF3 - curF2 < 600) curF3 += 10;
            f.F1 = FormantSynth.HzToPitch(curF1);
            f.F2 = FormantSynth.HzToPitch(curF2);
            f.F3 = FormantSynth.HzToPitch(curF3);
            f.Bw1 = (short)((_controlData[kBW1] * _voiceBWgain1) >> 16);
            f.Bw2 = (short)((_controlData[kBW2] * _voiceBWgain2) >> 16);
            f.Bw3 = (short)((_controlData[kBW3] * _voiceBWgain3) >> 16);
            f.FNZ = FormantSynth.HzToPitch(_controlData[kFNZ]);
            f.Av = LogToLin(_controlData[kAV]);
            f.Af = LogToLin(_controlData[kAF]);
            f.A2 = LogToLin(_controlData[kAp2]);
            f.A3 = LogToLin(_controlData[kAp3]);
            f.A4 = LogToLin(_controlData[kAp4]);
            f.A5 = LogToLin(_controlData[kAp5]);
            f.A6 = LogToLin(_controlData[kAp6]);
            f.AB = LogToLin(_controlData[kAB]);
            f.PhonEdge = (short)(_durDoneInPhon == 0 ? 1 : 0);
            return f;
        }
    }
}  // namespace