1+ package org.operatorfoundation.audiocoder
2+
3+ class WSPRProcessor
4+ {
5+ companion object
6+ {
7+ // WSPR Audio Format Constants
8+ private const val WSPR_SAMPLE_RATE_HZ = 12000 // WSPR uses 12kHz sample rate
9+ private const val BYTES_PER_SHORT = 2
10+ private const val BYTE_MASK = 0xFF
11+ private const val BITS_PER_BYTE = 8
12+
13+ // WSPR Protocol Constants
14+ private const val WSPR_SYMBOL_DURATION_SECONDS = 0.683f // Each symbol is ~0.683 seconds
15+ private const val WSPR_SYMBOLS_PER_MESSAGE = 162 // WSPR messages have 162 symbols
16+ private const val WSPR_TRANSMISSION_DURATION_SECONDS = WSPR_SYMBOL_DURATION_SECONDS * WSPR_SYMBOLS_PER_MESSAGE // ~110.6 seconds
17+
18+ // Buffer Timing Constants
19+ private const val MINIMUM_DECODE_SECONDS = 120f // Minimum for decode attempt
20+ private const val RECOMMENDED_BUFFER_SECONDS = 180f // Recommended buffer for reliable decode (3 minutes for overlap)
21+ private const val BUFFER_OVERLAP_SECONDS = RECOMMENDED_BUFFER_SECONDS - WSPR_TRANSMISSION_DURATION_SECONDS // ~60 seconds overlap
22+
23+ // Buffer Size Calculations
24+ private const val MAXIMUM_BUFFER_SAMPLES = (WSPR_SAMPLE_RATE_HZ * RECOMMENDED_BUFFER_SECONDS ).toInt()
25+ private const val MINIMUM_DECODE_SAMPLES = (WSPR_SAMPLE_RATE_HZ * MINIMUM_DECODE_SECONDS ).toInt()
26+
27+ // Default WSPR Frequencies
28+ private const val DEFAULT_WSPR_20M_FREQUENCY_MHZ = 14.097
29+ private const val DEFAULT_WSPR_40M_FREQUENCY_MHZ = 7.040
30+ private const val DEFAULT_WSPR_80M_FREQUENCY_MHZ = 3.593
31+ }
32+
33+ private val audioBuffer = mutableListOf<Short >()
34+
35+ /* *
36+ * Adds audio samples to the WSPR processing buffer.
37+ */
38+ fun addSamples (samples : ShortArray )
39+ {
40+ audioBuffer.addAll(samples.toList())
41+
42+ // Maintain buffer size within limits
43+ if (audioBuffer.size > MAXIMUM_BUFFER_SAMPLES )
44+ {
45+ val samplesToRemove = audioBuffer.size - MAXIMUM_BUFFER_SAMPLES
46+
47+ // TODO: Consider saving these to a different buffer
48+ repeat(samplesToRemove)
49+ {
50+ audioBuffer.removeAt(0 )
51+ }
52+ }
53+ }
54+
55+ /* *
56+ * Gets the current buffer duration in seconds.
57+ */
58+ fun getBufferDurationSeconds (): Float
59+ {
60+ return audioBuffer.size.toFloat() / WSPR_SAMPLE_RATE_HZ
61+ }
62+
63+ /* *
64+ * Checks if buffer has enough data for a WSPR decode
65+ */
66+ fun isReadyForDecode (): Boolean
67+ {
68+ return audioBuffer.size >= MINIMUM_DECODE_SAMPLES
69+ }
70+
71+ /* *
72+ * Decodes WSPR from buffered audio data.
73+ */
74+ fun decodeBufferedWSPR (
75+ dialFrequencyMHz : Double = DEFAULT_WSPR_20M_FREQUENCY_MHZ ,
76+ useLowerSideband : Boolean = false
77+ ): Array <WSPRMessage >?
78+ {
79+ if (! isReadyForDecode()) { return null }
80+
81+ val audioBytes = convertShortsToBytes(audioBuffer.toShortArray())
82+ return CJarInterface .WSPRDecodeFromPcm (audioBytes, dialFrequencyMHz, useLowerSideband)
83+ }
84+
85+ /* *
86+ * Clears the audio buffer
87+ */
88+ fun clearBuffer ()
89+ {
90+ audioBuffer.clear()
91+ }
92+
93+ /* *
94+ * Gets recommended buffer duration for optimal WSPR decoding.
95+ */
96+ fun getRecommendedBufferSeconds (): Float = RECOMMENDED_BUFFER_SECONDS
97+
98+ /* *
99+ * Gets minimum buffer duration for WSPR decode attempts.
100+ */
101+ fun getMinimumBufferSeconds (): Float = MINIMUM_DECODE_SECONDS
102+
103+ /* *
104+ * Gets the actual WSPR transmission duration
105+ */
106+ fun getWSPRTransmissionSeconds (): Float = WSPR_TRANSMISSION_DURATION_SECONDS
107+
108+ /* *
109+ * Gets the buffer overlap duration (extra buffering beyond transmission time)
110+ */
111+ fun getBufferOverlapSeconds (): Float = BUFFER_OVERLAP_SECONDS
112+
113+ /* *
114+ * Converts 16-bit samples to byte array for AudioCoder processing
115+ */
116+ private fun convertShortsToBytes (samples : ShortArray ): ByteArray
117+ {
118+ val bytes = ByteArray (samples.size * BYTES_PER_SHORT )
119+
120+ for (sampleIndex in samples.indices)
121+ {
122+ val sample = samples[sampleIndex].toInt()
123+ val byteIndex = sampleIndex * BYTES_PER_SHORT
124+
125+ // Little Endian Byte order
126+ bytes[byteIndex] = (sample and BYTE_MASK ).toByte()
127+ bytes[byteIndex + 1 ] = ((sample shr BITS_PER_BYTE ) and BYTE_MASK ).toByte()
128+ }
129+
130+ return bytes
131+ }
132+
133+
134+ }
0 commit comments