Code/AudioProcessor.Pitch.cs
#nullable enable
using System;
namespace SharpTalk
{
public sealed partial class AudioProcessor
{
// Pitch_RaiseAndFall
private void PitchRaiseAndFall()
{
const int kFallen = 0, kRaised = 1, kStart = 2, kFinished = 3;
int pState = kStart, lastState = kStart;
int wdIndex = 0, firstWord = 0, lastWord = 0;
long[] wdType = new long[64];
int stressCount = 1;
for (int index = 0; index < _phonBuf2InIndex; index++)
{
short curPhon = _phonBuf2[index];
long curCtrl = _phonCtrlBuf2[index];
uint curFlags = Tables.PhonFlags2[curPhon];
// Clause boundary (comma, semicolon, colon): restart pitch contour for next clause.
if ((curCtrl & kSilenceTypeField) != 0)
{
pState = kStart;
wdIndex = 0; firstWord = 0; lastWord = 0; stressCount = 1;
continue;
}
if (pState == kRaised && (curCtrl & kBoundryTypeField) == kWord_Start)
{
wdType[wdIndex] = (curCtrl & kContent_Word) != 0 ? kPitchRise1 : kPitchFall1;
if (wdIndex < 63) wdIndex++;
stressCount = 0;
lastWord = index;
if (lastState == kStart && pState == kRaised)
{ lastState = kRaised; firstWord = index; }
}
if ((curFlags & kVowelF) != 0)
{
if (pState == kStart)
{
if (CountVowelsTillBoundry(kTerm_End, index) == 0)
{
_phonCtrlBuf2[index] |= kPitchFall;
pState = kFinished;
break;
}
else if (CountStressVowelsTillBoundry(kTerm_End, index) == 0)
{
_phonCtrlBuf2[index] |= kPitchFall;
pState = kFinished;
}
else if ((curCtrl & kIsStressed) != 0)
{
_phonCtrlBuf2[index] |= kPitchRise;
pState = kRaised;
}
}
else if (pState == kRaised)
{
if ((curCtrl & kPrimOrEmphStress) != 0) stressCount++;
if (CountVowelsTillBoundry(kTerm_End, index) == 0)
{
_phonCtrlBuf2[index] |= kPitchFall;
pState = kFallen;
}
else if ((curCtrl & kPrimOrEmphStress) != 0 &&
CountStressVowelsTillBoundry(kTerm_End, index) == 0)
{
_phonCtrlBuf2[index] |= kPitchFall;
pState = kFallen;
}
}
}
}
wdIndex -= 2;
if (wdIndex >= 1 && pState != kFinished)
{
pState = kFallen;
for (int i = 0; i < wdIndex; i++)
{
if (pState == kFallen) { wdType[i] = kPitchRise1; pState = kRaised; }
else { wdType[i] = kPitchFall1; pState = kFallen; }
}
if (pState == kRaised)
{ wdType[wdIndex] = kPitchFall1; wdIndex++; }
bool action = false;
int wi = 0;
for (int index = firstWord; index < lastWord; index++)
{
short curPhon = _phonBuf2[index];
long curCtrl = _phonCtrlBuf2[index];
uint curFlags = Tables.PhonFlags2[curPhon];
if ((curCtrl & kBoundryTypeField) == kWord_Start) action = true;
if ((curFlags & kVowelF) != 0 && action)
{
if (!AnyStressVowelsRemain(index))
{
action = false;
if (wi < wdIndex) _phonCtrlBuf2[index] |= wdType[wi];
wi++;
}
}
}
}
}
private int CountVowelsTillBoundry(long boundary, int curIndex)
{
int count = 0;
for (int i = curIndex; i < _phonBuf2InIndex; i++)
{
if (i != curIndex && (PhonFlags2Safe(_phonBuf2[i]) & kVowelF) != 0) count++;
if ((_phonCtrlBuf2[i] & kSyllableTypeField) >= boundary) break;
}
return count;
}
private int CountStressVowelsTillBoundry(long boundary, int curIndex)
{
int count = 0;
for (int i = curIndex; i < _phonBuf2InIndex; i++)
{
if (i != curIndex &&
(_phonCtrlBuf2[i] & kPrimOrEmphStress) != 0 &&
(PhonFlags2Safe(_phonBuf2[i]) & kVowelF) != 0)
count++;
if ((_phonCtrlBuf2[i] & kSyllableTypeField) >= boundary) break;
}
return count;
}
private bool AnyStressVowelsRemain(int curIndex)
{
for (int i = curIndex + 1; i < _phonBuf2InIndex; i++)
{
if ((_phonCtrlBuf2[i] & kBoundryTypeField) == kWord_Start) break;
if ((_phonCtrlBuf2[i] & kPrimOrEmphStress) != 0 &&
(PhonFlags2Safe(_phonBuf2[i]) & kVowelF) != 0)
return true;
}
return false;
}
static uint PhonFlags2Safe(short p) =>
(p >= 0 && p < Tables.PhonFlags2.Length) ? Tables.PhonFlags2[p] : 0u;
// Calc_Ramp_Steps
private void CalcRampSteps()
{
const int kRampMode = 0; // const int kSusMode = 1;
int rampIndex = 0, mode = kRampMode, accum = 1;
for (int i = 0; i < _phonBuf2InIndex; i++)
{
long curCtrl = GetCtrl2(i);
long curSylType = curCtrl & kSyllableTypeField;
short curDur = _durBuf[i];
if (mode == kRampMode)
{
if ((curCtrl & kSilenceTypeField) != 0 || (curSylType & kTerm_End) != 0)
{
long step = ((long)(_baselineFallStart - _baselineFallEnd) << 16) / accum;
if ((curSylType & kTerm_End) != 0)
{
if (_endPunctuation == _Comma_ || _endPunctuation == _Quest_ ||
_endPunctuation == _Tilde_ || _endPunctuation == _Ellipsis_)
step >>= 1;
}
if (rampIndex < kMaxRamps)
_rampSteps[rampIndex++] = step;
accum = 1;
}
else
{
accum += curDur;
}
}
}
_curRamp = 0;
}
// Fill_Pitch_Buf
private void FillPitchBuf()
{
bool pitchIsFallen = true;
_pitchBufInIndex = 0;
int stressCounter = 0;
int curBaseline = 0;
_pitchTimeOffset = 0;
short raiseAmt = 0, fallAmt = 0, raiseAmt1 = 0, fallAmt1 = 0;
for (int i = 0; i < _phonBuf2InIndex; i++)
{
short curPhon = GetPhon2(i);
long curCtrl = GetCtrl2(i);
uint curFlags = Tables.PhonFlags2[curPhon];
long curStress = curCtrl & kStressField;
long curSylType = curCtrl & kSyllableTypeField;
short curDur = _durBuf[i];
long prevCtrl = GetCtrl2(i - 1);
// Phrase reset after silence boundary — must happen before pitch processing
// so the subsequent pitch rise isn't cancelled by the baseline wipe.
if (((prevCtrl & kSilenceTypeField) >> kSilenceTypeShift) != 0)
{
StoreF0AndTime((short)(0 - curBaseline), 0, kPhraseReset);
curBaseline = 0;
pitchIsFallen = true;
}
if ((curFlags & kVowelF) != 0)
{
// PITCH RISE
if ((curCtrl & kPitchRise) != 0 && pitchIsFallen)
{
raiseAmt = _vpRiseAmt;
if (_endPunctuation == _Quest_ || _endPunctuation == _Tilde_) raiseAmt >>= 1;
short timeT = (curCtrl & kPitchFall) != 0
? (short)((-80) / kFrameTime)
: (short)0;
StoreF0AndTime(raiseAmt, timeT, kPitchRiseFall_Flg);
curBaseline += raiseAmt;
pitchIsFallen = false;
}
// PITCH RISE1 / FALL1
if ((curCtrl & kPitchRise1) != 0)
{
raiseAmt1 = _vpRiseAmt1;
if (_endPunctuation == _Quest_ || _endPunctuation == _Tilde_) raiseAmt1 >>= 1;
StoreF0AndTime(raiseAmt1, 0, kPitchRiseFall1_Flg);
}
else if ((curCtrl & kPitchFall1) != 0)
{
fallAmt1 = _vpFallAmt1;
StoreF0AndTime(fallAmt1, 0, kPitchRiseFall1_Flg);
}
// PRIMARY / EMPHATIC STRESS
if ((curStress & kPrimOrEmphStress) != 0)
{
short pitchT;
if (curStress == kEmphaticStress)
pitchT = kHZ_28;
else
pitchT = kHZ_14;
pitchT += stressCounter switch
{
0 => kHZ_10,
1 => kHZ_9,
2 => kHZ_6,
3 => kHZ_4,
_ => 0,
};
if (_endPunctuation == _Quest_ || _endPunctuation == _Tilde_) pitchT >>= 1;
short timeT;
if ((curCtrl & kPitchFall) != 0 || (curSylType & kTerm_End) != 0)
timeT = (short)((-60) / kFrameTime);
else if (curStress == kEmphaticStress)
timeT = 0;
else
timeT = (short)(curDur >> 2);
pitchT = (short)((_vpStressGain * pitchT) >> 16);
if ((curSylType & kTerm_End) != 0 && curStress != kEmphaticStress)
pitchT = (short)(0 - kHZ_4);
StoreF0AndTime(pitchT, timeT, kPitchStress_Flg);
stressCounter++;
}
// PITCH FALL
if ((curCtrl & kPitchFall) != 0)
{
short timeT = (short)(curDur - (160 / kFrameTime));
if (timeT < 25 / kFrameTime) timeT = (short)(25 / kFrameTime);
if ((curSylType & kTerm_End) != 0)
{
fallAmt = _endPunctuation switch
{
_Comma_ => (short)(0 - kHZ_12),
_Period_ => (short)(0 - kHZ_20),
_Quest_ => (short)(0 - kHZ_7),
_Exclam_ => (short)(0 - kHZ_20),
_Tilde_ => (short)(0 - kHZ_4),
_Ellipsis_ => (short)(0 - kHZ_14),
_ => (short)(0 - kHZ_12),
};
}
else if ((curSylType & kVerb_End) != 0)
{
fallAmt = 0;
}
else
{
fallAmt = _vpFallAmt;
}
fallAmt = (short)(((long)_vpAssertiveness * fallAmt >> 16) - raiseAmt);
StoreF0AndTime(fallAmt, timeT, kPitchRiseFall_Flg);
curBaseline += fallAmt;
pitchIsFallen = true;
}
// Raise-type boundary (comma, question, or tilde)
if ((curSylType & kTerm_End) != 0 &&
(_endPunctuation == _Comma_ || _endPunctuation == _Quest_ || _endPunctuation == _Tilde_))
{
if (_endPunctuation == _Quest_)
{
StoreF0AndTime(kHZ_18, 0, kPitchBoundry_Flg);
StoreF0AndTime(kHZ_25, curDur, kPitchBoundry_Flg);
}
else if (_endPunctuation == _Tilde_)
{
// Hat: rises to peak at ~40% then falls back below start
StoreF0AndTime(kHZ_7, 0, kPitchBoundry_Flg);
StoreF0AndTime(kHZ_18, (short)(curDur * 2 / 5), kPitchBoundry_Flg);
StoreF0AndTime(kHZ_4, curDur, kPitchBoundry_Flg);
}
else
{
StoreF0AndTime(kHZ_7, 0, kPitchBoundry_Flg);
StoreF0AndTime(kHZ_10, curDur, kPitchBoundry_Flg);
}
}
}
_pitchTimeOffset += curDur;
}
}
private void StoreF0AndTime(short pitch, short time, short flags)
{
if (_pitchTimeOffset + time >= 0)
{
_pitchBufTime[_pitchBufInIndex] = (short)(_pitchTimeOffset + time);
_pitchTimeOffset = 0 - time;
}
else
{
_pitchBufTime[_pitchBufInIndex] = 0;
}
_pitchBufFreq[_pitchBufInIndex] = pitch;
_pitchBufFlags[_pitchBufInIndex] = flags;
if (_pitchBufInIndex < kPitchBufSize - 1)
_pitchBufInIndex++;
}
// StartNew_PitchClause
private void StartNewPitchClause()
{
_baselineStartOffset = _baselineFallStart;
_baselineEndOffset = _baselineFallEnd;
// (start_of_Paragraph_Flag adjustments omitted – not paragraph start)
}
private void StretchLastWordForTilde()
{
int pct = _endPunctuation == _Tilde_ ? 110
: _endPunctuation == _Ellipsis_ ? 125
: 0;
if (pct == 0) return;
int end = _phonBuf2InIndex - 1;
while (end > 0 && _phonBuf2[end] == _SIL_) end--;
int start = 0;
for (int i = end; i >= 0; i--)
{
if ((_phonCtrlBuf2[i] & kBoundryTypeField) == kWord_Start)
{
start = i;
break;
}
}
for (int i = start; i <= end; i++)
_durBuf[i] = (short)Math.Max(1, (_durBuf[i] * pct + 50) / 100);
}
private const uint kHasReleaseF = 1u << 23;
private const uint kFrontF_BE = 1u << 21;
private const long kPlosive_Release = 0x4000;
private void InsertPlosiveRelease()
{
if (_singing) return;
for (int i = 0; i < _phonBuf2InIndex; i++)
{
short cur = _phonBuf2[i];
short next = i + 1 < _phonBuf2InIndex ? _phonBuf2[i + 1] : _SIL_;
if (next != _SIL_) continue;
uint curFlags = Tables.PhonFlags2[cur >= 0 && cur < Tables.PhonFlags2.Length ? cur : 0];
if ((curFlags & kHasReleaseF) == 0) continue;
if (_phonBuf2InIndex >= kPhonBuf_Red_Zone) break;
// Shift everything after i+1 up by one slot
for (int k = _phonBuf2InIndex; k > i + 1; k--)
{
_phonBuf2[k] = _phonBuf2[k - 1];
_phonCtrlBuf2[k] = _phonCtrlBuf2[k - 1];
_durBuf[k] = _durBuf[k - 1];
_userPitchBuf2[k] = _userPitchBuf2[k - 1];
_userDurBuf2[k] = _userDurBuf2[k - 1];
_userNoteBuf2[k] = _userNoteBuf2[k - 1];
_userRateBuf2[k] = _userRateBuf2[k - 1];
}
_phonBuf2InIndex++;
// Decide IX vs AX, IX for /t/ or /d/, or if prev phoneme is front
short prevPhon = i > 0 ? _phonBuf2[i - 1] : _SIL_;
uint prevFlags = Tables.PhonFlags2[prevPhon >= 0 && prevPhon < Tables.PhonFlags2.Length ? prevPhon : 0];
bool useIX = (cur == _T_ || cur == _D_) || ((prevFlags & kFrontF_BE) != 0);
_phonBuf2[i + 1] = useIX ? _IX_ : _AX_;
_phonCtrlBuf2[i + 1] = _phonCtrlBuf2[i] | kPlosive_Release;
_durBuf[i + 1] = 25 / kFrameTime;
_userPitchBuf2[i + 1] = _userPitchBuf2[i];
_userDurBuf2[i + 1] = kDur_One;
_userNoteBuf2[i + 1] = 0;
_userRateBuf2[i + 1] = 0;
i++; // skip over the inserted release slot
}
}
}
} // namespace