The audio carrier rate is 44,100 Hz.
Ring buffer size for processing is 2^11/44100 seconds, or about 46.4 ms.
In testing, the data comes in 3 pulses: the first pulse is empty, the second has data, and the third is truncated due to likely powerloss.
The Int16 stream must first be smoothed and cleaned. This is the method the original 60beats logic used to process the waveform but might be better served with a more modern and powerful reconstruction filter.
static Int16 FirstPassFilter(Int16 amplitude)
{
// possbly a 2-wide box filter
Int16 currentAmplitude = amplitude;
amplitude = (Int16)((previousPreviousAmplitude + (previousAmplitude * 2) + currentAmplitude) / 4);
previousPreviousAmplitude = previousAmplitude;
previousAmplitude = currentAmplitude;
return amplitude;
}
The Int16 value is then stored in a ring-buffer of 40 bytes size which is used to make room for repairing damaged waveforms. The Int16 16 samples ahead is read, which is from the prior loop of the ring buffer and is initially 0 and checked for peak/valley truncation.
previousValues.Push(amplitude);
// correct clipping at 255 or -256 using a parabala to synthesize the lost peak/vally
if (previousValues[16] == 255 || previousValues[16] == -256)
{
int weightedPrevious = previousValues[16 - 1]; // prev[15] - (prev[16] - prev[15]) * (iter+1)
int divisor = 0; // terations * (iterations + 1)
int iterations = 0;
for (; iterations < 6; iterations++)
{
divisor += (iterations * 2);
weightedPrevious += previousValues[16 - 1] + (previousValues[16] - previousValues[16 - 1]);
// break if we go out of range early
if (previousValues[16] == 255 && previousValues[16 + 1 + iterations] >= 255)
break;
if (previousValues[16] == -256 && previousValues[16 + 1 + iterations] <= -256)
break;
}
if (iterations > 2)
{
float a = (float)(previousValues[16 + 1 + iterations] - weightedPrevious) / (float)divisor; // (prev[17+iter] - prev[16] * (iter+1) + prev[15] * iter) / (iter * (iter+1))
float b = (float)(previousValues[16] - previousValues[16 - 1]) - a;
float c = (float)previousValues[16 - 1];
for (int v13 = iterations, v9Index = 16 - 1, x = 2; v13 > 3; v13--, v9Index++, x++)
{
previousValues[v9Index] = (Int16)(float)Math.Round((a * x * x) + (b * x) + c);
}
}
}
The maximum absolute amplitude is stored in another 40 entry ring-buffer. This buffer is checked to find the maximum amplitude in the sample window.
// get the max absolute amplitude of the last BUFFER frames
previousAbsValues.Push((Int16)Math.Abs((previousValues[16] == Int16.MinValue) ? Int16.MaxValue : previousValues[16]));
int maxAbsInWindow = 0;
for (int i = 0; i < previousAbsValues.Size; i++)
{
Int16 newValue = previousAbsValues[i];
if (newValue > maxAbsInWindow)
maxAbsInWindow = newValue;
}
Gather the min and max amplitude for the next 9 frames (after frame 18 rather than) and use it to find the true middle point of the waveform.
// get the min and max for the 9 frames at the read index
Int16 findMin = 0;
Int16 findMax = 0;
for (int i = 0; i < 9; i++)
{
Int16 newValue = previousValues[16 + 2 + i];
if (newValue > findMax)
findMax = newValue;
if (newValue < findMin)
findMin = newValue;
}
int rangeTop = Math.Max((int)findMax, 100);
int rangeBottom = Math.Min((int)findMin, -100);
int rangeMiddle = (rangeBottom + rangeTop) / 2;
Int16 rangedValue = (Int16)Math.Min(Math.Max(previousValues[16] - rangeMiddle, Int16.MinValue), Int16.MaxValue);
If the amplitude has gotten somewhat quiet, start to process the frames in the ring buffer. Attempt to process the half-save width counts stored so far. Note we haven't reached the code that stores these yet so this buffer will be empty initially. This test attempts to use multiple different dividing lines between the length for a long and a short half-wave looking for one that generates a valid bitstream.
if (maxAbsInWindow < 101)
//if (maxAbsInWindow < 101 || (countsBufferIndex > 0 && countsBuffer[countsBufferIndex-1] > 1000))
//if (maxAbsInWindow < 101 || (countsBufferIndex > 0 && countsBuffer[countsBufferIndex-1] > 100))
{
//if (countsBufferIndex != 0)
if (countsBufferIndex > 0)
{
// process counts data with various possible thresholds
rx_buffer_len = 0;
currentBitIndex = 0;
currentByte = 0;
self_checksumOK = false;
if (!processCountsWithThreshold(63))
{
for (int v60_ = 1; v60_ < 10; v60_++) // shouldn't this start at 1 as the above already tried?
{
rx_buffer_len = 0;
currentBitIndex = 0;
currentByte = 0;
if (processCountsWithThreshold(63 + v60_))
{
if (self_checksumOK)
break;
}
rx_buffer_len = 0;
currentBitIndex = 0;
currentByte = 0;
if (processCountsWithThreshold(63 - v60_))
{
if (self_checksumOK)
break;
}
}
}
if (self_checksumOK)
{
//Console.WriteLine(TimeSinceGood);
//TimeSinceGood = 0;
++self_checksumSuccessCount;
//finishedPacket();
}
else if ((uint)countsBufferIndex > 20)
{
++self_checksumErrorCount;
Console.Title = $"Checksum Success: {self_checksumSuccessCount} Checksum Errors: {self_checksumErrorCount} self_checksumErrorCount, Error Ratio: {(1.0f * self_checksumErrorCount / (self_checksumErrorCount + self_checksumSuccessCount))}";
}
countsBufferIndex = 0;
}
//return;
}//else
After handling good but older data, we're still in the middle of processing new data. Count the time between crosses of the adjusted 0 line (accounting for any waves that are way too short suggesting signal damage).
if (rangedValue > 1)
{
_MergedGlobals_currentValue = 1;
}
else if (rangedValue < -1)
{
_MergedGlobals_currentValue = 0;
}
{
_MergedGlobals_countsSinceLastTransition += 10;
if (_MergedGlobals_currentValue != _MergedGlobals_lastValue)
{
int stepsAgoCrossedZero = 10 * Math.Abs((int)rangedValue) / Math.Abs((int)rangedValue - (int)_MergedGlobals_previousValue);
_MergedGlobals_countsSinceLastTransition -= stepsAgoCrossedZero;
if (rangedValue > 1 || countsBufferIndex != 0)
{
if (countsBufferIndex == 0)
_MergedGlobals_countsSinceLastTransition = stepsAgoCrossedZero;
countsBuffer[countsBufferIndex] = _MergedGlobals_countsSinceLastTransition;
//TimeSinceGood = TimeSinceGood + (uint)_MergedGlobals_countsSinceLastTransition;
// nielk1 attempt to fix noise by merging overly-short inversion, another method might be detecting slope instead of time
if (countsBufferIndex > 4 && _MergedGlobals_countsSinceLastTransition < 35)
{
int total = countsBuffer[countsBufferIndex - 0] + countsBuffer[countsBufferIndex - 1] + countsBuffer[countsBufferIndex - 2];
if (total < 100)
{
countsBufferIndex -= 2;
countsBuffer[countsBufferIndex] = total;
}
}
if (countsBufferIndex < 4000)
countsBufferIndex++;
}
_MergedGlobals_countsSinceLastTransition = stepsAgoCrossedZero;
}
_MergedGlobals_lastValue = _MergedGlobals_currentValue;
_MergedGlobals_previousValue = rangedValue;
}
Below is the bitstream check that attempts to find a valid bitstream from the length of the data.
static bool processCountsWithThreshold(int threshold, int countsOffset = 0)
{
bool flipFlop = false;
bool doingFlip = false;
//handleBit(true);
bool WholeArray = false;
for (int i = countsOffset; i < countsBufferIndex; i++)
{
if (countsBuffer[i] >= (uint)threshold) // over writes current flipFlop value
{
if (doingFlip) // if we under-over, the data is bad since unders must be paired
{
return false;
}
WholeArray = handleBit(flipFlop);
}
else if (doingFlip) // 2nd under writes current flipFlop value
{
WholeArray = handleBit(flipFlop);
doingFlip = false;
}
else // first under skips write and just flipsFlops
{
doingFlip = true;
}
flipFlop = !flipFlop;
if (WholeArray)
break;
}
//Console.WriteLine();
return true;
}
static bool handleBit(bool bit)
{
int bit_ = bit ? 1 : 0;
/*{
Console.ForegroundColor = rx_buffer_len % 2 == 0 ? ConsoleColor.Gray : ConsoleColor.DarkGray;
Console.Write(bit_);
}*/
currentByte = (byte)((currentByte << 1) | bit_);
currentBitIndex++;
if (currentBitIndex == 8)
{
currentBitIndex = 0;
_MergedGlobals__rx_buffer[rx_buffer_len] = currentByte;
rx_buffer_len++;
currentByte = 0;
}
if (rx_buffer_len == 7 && currentBitIndex == 1)
{
_MergedGlobals__rx_buffer[7] = currentByte;
verifyChecksum();
return true;
}
return false;
}
Further documentation pending attempting an alternate reconstruction filter.