Skip to content

Commit 816172d

Browse files
author
Dr. Brandon Wiley
committed
Ported WSPR encoder to Kotlin (decoder is hard, remains as JNI)
1 parent 2fe9da7 commit 816172d

File tree

5 files changed

+580
-5
lines changed

5 files changed

+580
-5
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,4 @@ fastlane/test_output
7373
fastlane/readme.md
7474
.DS_Store
7575
.idea/vcs.xml
76+
.idea

AudioCoder/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ dependencies {
6868
implementation(libs.material)
6969

7070
testImplementation(libs.junit)
71+
androidTestImplementation("org.jetbrains.kotlin:kotlin-test-junit:2.0.21")
7172
androidTestImplementation(libs.androidx.junit)
7273
androidTestImplementation(libs.androidx.espresso.core)
7374
}
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package org.operatorfoundation.audiocoder
2+
3+
import androidx.test.ext.junit.runners.AndroidJUnit4
4+
import org.junit.Test
5+
import org.junit.runner.RunWith
6+
import org.junit.Assert.assertArrayEquals
7+
import org.junit.Assert.assertEquals
8+
import org.junit.Assert.assertTrue
9+
10+
@RunWith(AndroidJUnit4::class)
11+
class WSPREncoderTest {
12+
13+
@Test
14+
fun testBasicEncodingMatchesJNI() {
15+
val callsign = "W1ABC"
16+
val locator = "FN20"
17+
val power = 30
18+
val offset = 0
19+
val lsb = false
20+
21+
// Kotlin implementation
22+
val kotlinResult = WSPREncoder.encodeToFrequencies(
23+
WSPREncoder.WSPRMessage(callsign, locator, power, offset, lsb)
24+
)
25+
26+
// JNI implementation
27+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
28+
callsign, locator, power, offset, lsb
29+
)
30+
31+
// Should produce identical results
32+
assertEquals(162, kotlinResult.size)
33+
assertEquals(162, jniResult.size)
34+
assertArrayEquals(jniResult, kotlinResult)
35+
}
36+
37+
@Test
38+
fun testEncodingWithFrequencyOffset() {
39+
val callsign = "K1JT"
40+
val locator = "FN20"
41+
val power = 23
42+
val offset = 1500
43+
val lsb = false
44+
45+
val kotlinResult = WSPREncoder.encodeToFrequencies(
46+
WSPREncoder.WSPRMessage(callsign, locator, power, offset, lsb)
47+
)
48+
49+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
50+
callsign, locator, power, offset, lsb
51+
)
52+
53+
assertArrayEquals(jniResult, kotlinResult)
54+
}
55+
56+
@Test
57+
fun testLSBModeEncoding() {
58+
val callsign = "N2ABC"
59+
val locator = "EM79"
60+
val power = 37
61+
val offset = 0
62+
val lsb = true
63+
64+
val kotlinResult = WSPREncoder.encodeToFrequencies(
65+
WSPREncoder.WSPRMessage(callsign, locator, power, offset, lsb)
66+
)
67+
68+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
69+
callsign, locator, power, offset, lsb
70+
)
71+
72+
assertArrayEquals(jniResult, kotlinResult)
73+
}
74+
75+
@Test
76+
fun testVariousCallsignFormats() {
77+
val testCases = listOf(
78+
"W1ABC", // Standard US call
79+
"K1JT", // Short US call
80+
"AA1A", // 2-prefix call
81+
"VE3XYZ", // Canadian call
82+
"G4ABC", // UK call
83+
"DL1ABC", // German call
84+
"JA1ABC" // Japanese call
85+
)
86+
87+
val locator = "FN20"
88+
val power = 30
89+
90+
for (callsign in testCases) {
91+
val kotlinResult = WSPREncoder.encodeToFrequencies(
92+
WSPREncoder.WSPRMessage(callsign, locator, power)
93+
)
94+
95+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
96+
callsign, locator, power, 0, false
97+
)
98+
99+
assertArrayEquals(
100+
"Failed for callsign: $callsign",
101+
jniResult,
102+
kotlinResult
103+
)
104+
}
105+
}
106+
107+
@Test
108+
fun testVariousGridLocators() {
109+
val testCases = listOf(
110+
"FN20", // Northeast US
111+
"EM79", // Southeast US
112+
"CN87", // West coast US
113+
"IO91", // UK
114+
"JN59", // Central Europe
115+
"PM96", // Hawaii
116+
"QF22" // New Zealand
117+
)
118+
119+
val callsign = "W1ABC"
120+
val power = 30
121+
122+
for (locator in testCases) {
123+
val kotlinResult = WSPREncoder.encodeToFrequencies(
124+
WSPREncoder.WSPRMessage(callsign, locator, power)
125+
)
126+
127+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
128+
callsign, locator, power, 0, false
129+
)
130+
131+
assertArrayEquals(
132+
"Failed for locator: $locator",
133+
jniResult,
134+
kotlinResult
135+
)
136+
}
137+
}
138+
139+
@Test
140+
fun testVariousPowerLevels() {
141+
val testPowers = listOf(0, 3, 7, 10, 13, 17, 20, 23, 27, 30, 33, 37, 40, 43, 47, 50, 53, 57, 60)
142+
val callsign = "W1ABC"
143+
val locator = "FN20"
144+
145+
for (power in testPowers) {
146+
val kotlinResult = WSPREncoder.encodeToFrequencies(
147+
WSPREncoder.WSPRMessage(callsign, locator, power)
148+
)
149+
150+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
151+
callsign, locator, power, 0, false
152+
)
153+
154+
assertArrayEquals(
155+
"Failed for power: $power dBm",
156+
jniResult,
157+
kotlinResult
158+
)
159+
}
160+
}
161+
162+
@Test
163+
fun testPowerLevelCorrections() {
164+
val testCases = listOf(1, 2, 11, 12, 15, 25, 35, 45, 55)
165+
val callsign = "W1ABC"
166+
val locator = "FN20"
167+
168+
for (inputPower in testCases) {
169+
val kotlinResult = WSPREncoder.encodeToFrequencies(
170+
WSPREncoder.WSPRMessage(callsign, locator, inputPower)
171+
)
172+
173+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
174+
callsign, locator, inputPower, 0, false
175+
)
176+
177+
assertArrayEquals(
178+
"Failed for power correction: $inputPower dBm",
179+
jniResult,
180+
kotlinResult
181+
)
182+
}
183+
}
184+
185+
@Test
186+
fun testFrequencyRangeCorrectness() {
187+
val message = WSPREncoder.WSPRMessage(
188+
callsign = "W1ABC",
189+
locator = "FN20",
190+
powerDbm = 30,
191+
offsetHz = 0,
192+
lsbMode = false
193+
)
194+
195+
val frequencies = WSPREncoder.encodeToFrequencies(message)
196+
197+
val minFreq = frequencies.minOrNull()!!
198+
val maxFreq = frequencies.maxOrNull()!!
199+
200+
assertTrue(minFreq >= 150000)
201+
assertTrue(maxFreq <= 150450)
202+
assertTrue(maxFreq > minFreq)
203+
}
204+
205+
@Test
206+
fun testLSBModeInvertsSymbols() {
207+
val callsign = "W1ABC"
208+
val locator = "FN20"
209+
val power = 30
210+
211+
val usbResult = WSPREncoder.encodeToFrequencies(
212+
WSPREncoder.WSPRMessage(callsign, locator, power, 0, false)
213+
)
214+
215+
val lsbResult = WSPREncoder.encodeToFrequencies(
216+
WSPREncoder.WSPRMessage(callsign, locator, power, 0, true)
217+
)
218+
219+
var differencesFound = 0
220+
for (i in usbResult.indices) {
221+
if (usbResult[i] != lsbResult[i]) {
222+
differencesFound++
223+
}
224+
}
225+
226+
assertTrue(differencesFound > 0)
227+
}
228+
229+
@Test
230+
fun testOffsetAddsToAllFrequencies() {
231+
val callsign = "W1ABC"
232+
val locator = "FN20"
233+
val power = 30
234+
val offset = 1000
235+
236+
val noOffsetResult = WSPREncoder.encodeToFrequencies(
237+
WSPREncoder.WSPRMessage(callsign, locator, power, 0, false)
238+
)
239+
240+
val offsetResult = WSPREncoder.encodeToFrequencies(
241+
WSPREncoder.WSPRMessage(callsign, locator, power, offset, false)
242+
)
243+
244+
for (i in noOffsetResult.indices) {
245+
assertEquals(
246+
noOffsetResult[i] + (offset * 100),
247+
offsetResult[i]
248+
)
249+
}
250+
}
251+
252+
@Test
253+
fun testComprehensiveComparison() {
254+
val callsigns = listOf("W1ABC", "K1JT", "VE3XYZ")
255+
val locators = listOf("FN20", "EM79", "IO91")
256+
val powers = listOf(10, 23, 37)
257+
val offsets = listOf(0, 500, 1500)
258+
val lsbModes = listOf(false, true)
259+
260+
var testCount = 0
261+
for (callsign in callsigns) {
262+
for (locator in locators) {
263+
for (power in powers) {
264+
for (offset in offsets) {
265+
for (lsb in lsbModes) {
266+
val kotlinResult = WSPREncoder.encodeToFrequencies(
267+
WSPREncoder.WSPRMessage(callsign, locator, power, offset, lsb)
268+
)
269+
270+
val jniResult = CJarInterface.WSPREncodeToFrequencies(
271+
callsign, locator, power, offset, lsb
272+
)
273+
274+
assertArrayEquals(
275+
"Failed: $callsign $locator ${power}dBm offset=${offset}Hz lsb=$lsb",
276+
jniResult,
277+
kotlinResult
278+
)
279+
testCount++
280+
}
281+
}
282+
}
283+
}
284+
}
285+
286+
println("✓ Ran $testCount comprehensive comparison tests - all passed!")
287+
}
288+
}

0 commit comments

Comments
 (0)