Skip to content

Commit e5ab004

Browse files
committed
Extracted reconstructBytes logic from the decoder
1 parent 6c3a385 commit e5ab004

1 file changed

Lines changed: 19 additions & 25 deletions

File tree

  • AudioCoder/src/main/java/org/operatorfoundation/audiocoder/mfsk

AudioCoder/src/main/java/org/operatorfoundation/audiocoder/mfsk/MFSKDecoder.kt

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,48 +71,42 @@ object MFSKDecoder
7171
baseFrequencyHz + toneIndex * mode.toneSpacingHz
7272
}
7373

74-
val output = ByteArray(byteCount)
75-
var bitPosition = 0
76-
77-
for (symbolIndex in 0 until symbolCount)
78-
{
79-
// Slice exactly one symbol window from the sample buffer.
80-
// copyOfRange allocates a new array; at 15.625 symbols/sec this is negligible.
81-
// If profiling ever shows this as a bottleneck, GoertzelFilter could be extended
82-
// to accept an offset+length into the full buffer instead.
74+
val symbols = IntArray(symbolCount) { symbolIndex ->
8375
val windowStart = symbolIndex * samplesPerSymbol
8476
val window = samples.copyOfRange(windowStart, windowStart + samplesPerSymbol)
85-
86-
// --- Run the Goertzel filter bank ---
87-
val energies = DoubleArray(mode.toneCount) { toneIndex ->
77+
val energies = DoubleArray(mode.toneCount) { toneIndex ->
8878
GoertzelFilter.energy(window, toneFrequencies[toneIndex], sampleRate)
8979
}
80+
// !! is safe: maxByOrNull returns null only for empty collections,
81+
// and toneCount is always >= 8 by MFSKMode's design.
82+
energies.indices.maxByOrNull { energies[it] }!!
83+
}
9084

91-
// Always pick the highest-energy tone as the symbol decision.
92-
// !! is safe: maxByOrNull returns null only for empty collections, and
93-
// toneCount is always >= 8 by MFSKMode's design.
94-
val winnerToneIndex = energies.indices.maxByOrNull { energies[it] }!!
85+
return reconstructBytes(symbols, byteCount, mode.bitsPerSymbol)
86+
}
87+
88+
private fun reconstructBytes(symbols: IntArray, byteCount: Int, bitsPerSymbol: Int): ByteArray
89+
{
90+
val output = ByteArray(byteCount)
91+
val totalBits = byteCount * 8
92+
var bitPosition = 0
9593

96-
// --- Unpack bitsPerSymbol bits from the winner, MSB-first ---
97-
// The encoder built toneIndex by shifting input bits in MSB-first, so reversing
98-
// means extracting from the most significant bit of winnerToneIndex downward.
99-
for (bitOffset in 0 until mode.bitsPerSymbol)
94+
for (toneIndex in symbols)
95+
{
96+
for (bitOffset in 0 until bitsPerSymbol)
10097
{
101-
// Stop exactly at the real data boundary — don't decode zero-padding bits.
10298
if (bitPosition >= totalBits) break
10399

104-
val bitInSymbol = mode.bitsPerSymbol - 1 - bitOffset
105-
val bit = (winnerToneIndex ushr bitInSymbol) and 1
100+
val bitInSymbol = bitsPerSymbol - 1 - bitOffset
101+
val bit = (toneIndex ushr bitInSymbol) and 1
106102

107-
// Place bit into the output byte at its MSB-first position.
108103
val byteIndex = bitPosition / 8
109104
val bitInByte = 7 - (bitPosition % 8)
110105

111106
if (bit == 1)
112107
{
113108
output[byteIndex] = (output[byteIndex].toInt() or (1 shl bitInByte)).toByte()
114109
}
115-
// bit == 0: output bytes are zero-initialised by ByteArray constructor, no action needed.
116110

117111
bitPosition++
118112
}

0 commit comments

Comments
 (0)