Skip to content

Commit 4330546

Browse files
committed
Added WSPRProcessor
1 parent 25f79fa commit 4330546

File tree

2 files changed

+135
-1
lines changed

2 files changed

+135
-1
lines changed

AudioCoder/src/main/java/org/operatorfoundation/audiocoder/WsprFileManager.kt renamed to AudioCoder/src/main/java/org/operatorfoundation/audiocoder/WSPRFileManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import java.nio.ByteBuffer
1111
import java.util.Date
1212
import java.util.Locale
1313

14-
class WsprFileManager(private val context: Context)
14+
class WSPRFileManager(private val context: Context)
1515
{
1616
companion object
1717
{
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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

Comments
 (0)