@@ -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