Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdc6db5aff | ||
|
|
92499b1cf1 | ||
|
|
828fd0fb6f |
No files matched your search
@@ -103,9 +103,24 @@ class CwDeepDecoder(
|
||||
private val _decodedText = MutableStateFlow("")
|
||||
override val decodedText: StateFlow<String> = _decodedText.asStateFlow()
|
||||
|
||||
/**
|
||||
* Archived text, plus a provisional decode of audio not yet archived.
|
||||
*
|
||||
* Kept as one flow of the two parts concatenated. Without the provisional part the
|
||||
* transcript visibly shrank: audio leaving the 20 s window waits for a full
|
||||
* [ARCHIVE_SECONDS] batch before it is decoded into the archive, so for up to 15 s its
|
||||
* characters were in neither place - measured, up to 30 characters at 20 WPM would
|
||||
* vanish and reappear later, which reads as the box deleting text.
|
||||
*/
|
||||
private val _historyText = MutableStateFlow("")
|
||||
override val historyText: StateFlow<String> = _historyText.asStateFlow()
|
||||
|
||||
/** Permanently archived text; the provisional tail is appended to this for display. */
|
||||
private var committedText = ""
|
||||
|
||||
/** Provisional decode of the pending archive batch, replaced when it is archived. */
|
||||
private var pendingText = ""
|
||||
|
||||
private val _estimatedPitch = MutableStateFlow<Float?>(null)
|
||||
override val estimatedPitch: StateFlow<Float?> = _estimatedPitch.asStateFlow()
|
||||
|
||||
@@ -373,6 +388,10 @@ class CwDeepDecoder(
|
||||
private fun dropBufferedAudio() {
|
||||
buffer.reset()
|
||||
archiveSize = 0
|
||||
// The provisional text describes audio being discarded, so it goes with it.
|
||||
// Committed text stays: it was correct for audio that really was archived.
|
||||
pendingText = ""
|
||||
_historyText.value = committedText
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -467,9 +486,27 @@ class CwDeepDecoder(
|
||||
if (audio.size < CwDeepSpectrogram.FFT_LENGTH) return@withContext
|
||||
val spectrogram = CwDeepSpectrogram.compute(audio)
|
||||
val text = runInference(activeSession, activeEnvironment, spectrogram)
|
||||
if (text.isNotEmpty()) {
|
||||
_historyText.value += text
|
||||
}
|
||||
// This batch is final, so its provisional decode is superseded rather than kept.
|
||||
committedText += text
|
||||
pendingText = ""
|
||||
_historyText.value = committedText
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode the audio waiting to be archived, so it stays on screen until it is.
|
||||
*
|
||||
* Provisional: the batch is still growing, and the final decode sees all of it at once
|
||||
* with more context. Runs on the redecode cycle rather than per capture chunk -
|
||||
* decoding every 100 ms chunk separately measured 14x the inference load, over 250% of
|
||||
* one core, and a chunk that short carries under two dot-lengths of context anyway.
|
||||
*/
|
||||
private suspend fun decodePending(audio: FloatArray) = withContext(Dispatchers.Default) {
|
||||
val activeSession = session ?: return@withContext
|
||||
val activeEnvironment = environment ?: return@withContext
|
||||
if (audio.size < CwDeepSpectrogram.FFT_LENGTH) return@withContext
|
||||
val spectrogram = CwDeepSpectrogram.compute(audio)
|
||||
pendingText = runInference(activeSession, activeEnvironment, spectrogram)
|
||||
_historyText.value = committedText + pendingText
|
||||
}
|
||||
|
||||
/** Run the ONNX model over a pre-computed spectrogram and return the decoded text. */
|
||||
@@ -550,6 +587,8 @@ class CwDeepDecoder(
|
||||
buffer.reset()
|
||||
_decodedText.value = ""
|
||||
_historyText.value = ""
|
||||
committedText = ""
|
||||
pendingText = ""
|
||||
archiveSize = 0
|
||||
_estimatedPitch.value = null
|
||||
_detectedToneHz.value = null
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/* LotwSatelliteIds.kt - NORAD catalogue number to LoTW satellite name.
|
||||
*
|
||||
* Why the catalogue number and not the name: the same satellite carries different names in
|
||||
* different TLE sources, so a name-keyed table misses whenever the user switches source.
|
||||
* Measured across Celestrak amateur and AMSAT nasabare, 33 of the 49 satellites present in
|
||||
* both are named differently - NORAD 43017 is "RADFXSAT (FOX-1B)" in one and "AO-91" in the
|
||||
* other, 43700 is "ES'HAIL 2" against "QO-100". The catalogue number is identical in every
|
||||
* source, so it is the only stable key.
|
||||
*
|
||||
* LoTW rejects a QSO whose SAT_NAME is not spelled exactly as in its accepted list
|
||||
* (https://lotw.arrl.org/lotw-help/satellite-qsos: "if you enter the satellite name as AO7
|
||||
* instead of AO-7 the data will be rejected"), which is why this maps to the exact spelling
|
||||
* held in LotwSatellites rather than to whatever the TLE happens to say.
|
||||
*
|
||||
* Every number here was read out of live TLE data, never typed from memory. Entries cover the
|
||||
* satellites that both appear in the app's own sources (Sources.satelliteDataUrls) and are in
|
||||
* the LoTW list; the rest of that list is satellites no source still carries, so no user can
|
||||
* track them and no mapping is needed for them.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
object LotwSatelliteIds {
|
||||
|
||||
/**
|
||||
* NORAD catalogue number to the LoTW spelling. The trailing comment is one name the
|
||||
* satellite goes by in the sources, kept so a reader can recognise the entry.
|
||||
*
|
||||
* Three numbers had to be decided rather than derived, because one name matched several
|
||||
* catalogued objects. Each was settled by which object the amateur-specific sources carry:
|
||||
* - ARISS is 25544, the station itself. Celestrak's full catalogue also lists ISS (UNITY),
|
||||
* (ZVEZDA), (DESTINY) and (NAUKA), which are modules rather than stations you work.
|
||||
* - IO-117 is 53109: four sources name that number GREENCUBE (IO-117) and only R4UAB calls
|
||||
* it ROBUSTA 1F, which is a different satellite.
|
||||
* - TO-108 is 44881, present in all three amateur sources; 44879 is TIANQIN 1 and appears
|
||||
* only in the general catalogue.
|
||||
*/
|
||||
private val idToName: Map<Int, String> = mapOf(
|
||||
7530 to "AO-7", // AO-07
|
||||
14129 to "AO-10", // PHASE 3B (AO-10)
|
||||
20439 to "AO-16", // OSCAR 16 (PACSAT)
|
||||
20442 to "LO-19", // LO-19
|
||||
22825 to "AO-27", // AO-27
|
||||
23439 to "RS-15", // RADIO ROSTO (RS-15)
|
||||
24278 to "FO-29", // FO-29
|
||||
25544 to "ARISS", // ISS (ZARYA)
|
||||
26609 to "AO-40", // PHASE 3D (AO-40)
|
||||
26931 to "NO-44", // NO-44
|
||||
27607 to "SO-50", // SAUDISAT 1C (SO-50)
|
||||
28650 to "VO-52", // HAMSAT (VO-52)
|
||||
39444 to "AO-73", // AO-73
|
||||
40025 to "EO-79", // FUNCUBE-3 (EO-79)/QB50P1
|
||||
40074 to "UKUBE1", // UKUBE-1
|
||||
40908 to "CAS-3H", // LILACSAT-2
|
||||
40931 to "IO-86", // IO-86
|
||||
40967 to "AO-85", // FOX-1A (AO-85)
|
||||
41847 to "CAS-2T", // CAS-2T
|
||||
43017 to "AO-91", // AO-91
|
||||
43678 to "PO-101", // DIWATA-2B
|
||||
43700 to "QO-100", // ES'HAIL 2
|
||||
43803 to "JO-97", // JO-97
|
||||
44530 to "TAURUS", // TAURUS-1
|
||||
44881 to "TO-108", // CAS-6 (TO-108)
|
||||
44909 to "RS-44", // DOSAAF-85 (RS-44)
|
||||
50466 to "HO-113", // CAMSAT XW-3 (CAS-9)
|
||||
53109 to "IO-117", // GREENCUBE (IO-117)
|
||||
61781 to "AO-123" // AO-123
|
||||
)
|
||||
|
||||
/** The LoTW spelling for [catnum], or null when this satellite is not in the LoTW list. */
|
||||
fun nameFor(catnum: Int): String? = idToName[catnum]
|
||||
|
||||
/** True when [catnum] names a satellite LoTW accepts, so a QSO on it can be confirmed. */
|
||||
fun isKnown(catnum: Int): Boolean = catnum in idToName
|
||||
|
||||
/** Entry count, so a test can catch the table being emptied by a bad edit. */
|
||||
val size: Int get() = idToName.size
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.rtbishop.look4sat.core.domain.wavelog
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class LotwSatelliteIdsTest {
|
||||
|
||||
/**
|
||||
* Every name the table produces has to exist in the LoTW list verbatim, because LoTW
|
||||
* rejects a QSO whose SAT_NAME is spelled differently - AO7 for AO-7 is refused.
|
||||
*/
|
||||
@Test
|
||||
fun `every mapped name is spelled exactly as LoTW has it`() {
|
||||
val known = LotwSatellites.names
|
||||
for (catnum in catnums) {
|
||||
val name = LotwSatelliteIds.nameFor(catnum)
|
||||
assertTrue("$catnum maps to $name, which LoTW does not list", name in known)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The catalogue number is the key precisely so that the source a user happens to fetch
|
||||
* from cannot change the answer. These are the numbers whose names differ most between
|
||||
* Celestrak and AMSAT, so they are the ones worth pinning.
|
||||
*/
|
||||
@Test
|
||||
fun `names that differ between sources resolve to the LoTW spelling`() {
|
||||
assertEquals("AO-91", LotwSatelliteIds.nameFor(43017)) // RADFXSAT (FOX-1B) / AO-91
|
||||
assertEquals("QO-100", LotwSatelliteIds.nameFor(43700)) // ES'HAIL 2 / QO-100
|
||||
assertEquals("TO-108", LotwSatelliteIds.nameFor(44881)) // TIANYAN 01 / CAS-6 (TO-108)
|
||||
assertEquals("SO-50", LotwSatelliteIds.nameFor(27607)) // SAUDISAT 1C (SO-50) / SO-50
|
||||
assertEquals("AO-123", LotwSatelliteIds.nameFor(61781)) // ASRTU-1 (AO-123) / AO-123
|
||||
}
|
||||
|
||||
/**
|
||||
* ARISS is the station, not its modules. Celestrak's full catalogue lists ISS (UNITY),
|
||||
* (ZVEZDA), (DESTINY) and (NAUKA) as separate objects; matching on the name "ISS" pulled
|
||||
* all of them in, and a QSO cannot be worked through a module.
|
||||
*/
|
||||
@Test
|
||||
fun `ARISS is the station and not one of its modules`() {
|
||||
assertEquals("ARISS", LotwSatelliteIds.nameFor(25544))
|
||||
for (module in listOf(25575, 26400, 26700, 49044)) {
|
||||
assertNull("module $module must not map to a satellite", LotwSatelliteIds.nameFor(module))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One source (R4UAB) names 53109 as ROBUSTA 1F while four others call it GREENCUBE
|
||||
* (IO-117), and 53106 appears in that one source alone. Trusting either would have put a
|
||||
* second catalogue number on the same LoTW name.
|
||||
*/
|
||||
@Test
|
||||
fun `IO-117 resolves to the number the amateur sources agree on`() {
|
||||
assertEquals("IO-117", LotwSatelliteIds.nameFor(53109))
|
||||
assertNull(LotwSatelliteIds.nameFor(53106))
|
||||
}
|
||||
|
||||
/** 44879 is TIANQIN 1, catalogued but not an amateur satellite carrying TO-108. */
|
||||
@Test
|
||||
fun `TO-108 does not also match TIANQIN 1`() {
|
||||
assertEquals("TO-108", LotwSatelliteIds.nameFor(44881))
|
||||
assertNull(LotwSatelliteIds.nameFor(44879))
|
||||
}
|
||||
|
||||
/** No two numbers may share a name, or one of them is the wrong satellite. */
|
||||
@Test
|
||||
fun `each LoTW name is claimed by a single catalogue number`() {
|
||||
val names = catnums.mapNotNull { LotwSatelliteIds.nameFor(it) }
|
||||
assertEquals(names.size, names.toSet().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isKnown agrees with nameFor`() {
|
||||
assertTrue(LotwSatelliteIds.isKnown(27607))
|
||||
assertFalse(LotwSatelliteIds.isKnown(99999))
|
||||
assertNull(LotwSatelliteIds.nameFor(99999))
|
||||
}
|
||||
|
||||
/** Guards against an edit that empties or truncates the table. */
|
||||
@Test
|
||||
fun `table holds every entry`() {
|
||||
assertEquals(catnums.size, LotwSatelliteIds.size)
|
||||
}
|
||||
|
||||
private val catnums = listOf(
|
||||
7530, 14129, 20439, 20442, 22825, 23439, 24278, 25544, 26609, 26931,
|
||||
27607, 28650, 39444, 40025, 40074, 40908, 40931, 40967, 41847, 43017,
|
||||
43678, 43700, 43803, 44530, 44881, 44909, 50466, 53109, 61781
|
||||
)
|
||||
}
|
||||
@@ -93,6 +93,7 @@
|
||||
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* PERBAIKAN: teks CW tidak lagi terlihat menghapus tulisan saat audio menunggu didekode
|
||||
* BARU: air terjun CW menampilkan seluruh pita audio, nada di luar jangkauan dekoder tetap terlihat
|
||||
* BARU: teks CW mengikuti isi baru, dan berhenti mengikuti saat Anda menggulir ke atas
|
||||
* BARU: pembaca layar kini menjelaskan air terjun CW dan sel hari AMSAT
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* PERBAIKAN: teks CW tidak lagi terlihat menghapus tulisan saat audio menunggu didekode
|
||||
* BARU: air terjun CW menampilkan seluruh pita audio, nada di luar jangkauan dekoder tetap terlihat
|
||||
* BARU: teks CW mengikuti isi baru, dan berhenti mengikuti saat Anda menggulir ke atas
|
||||
* BARU: pembaca layar kini menjelaskan air terjun CW dan sel hari AMSAT
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* DÜZELTME: CW metni artık kod çözme beklerken yazıyı siliyormuş gibi görünmüyor
|
||||
* YENİ: CW şelalesi tüm ses bandını gösterir, çözücünün ulaşamadığı ton da görünür
|
||||
* YENİ: CW metni yeni içeriği takip eder, geri kaydırdığınızda takibi bırakır
|
||||
* YENİ: ekran okuyucular artık CW şelalesini ve AMSAT gün hücrelerini okuyor
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
|
||||
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* 修复: CW 文本框不再出现文字消失一段又补回来的现象
|
||||
* 新: CW 瀑布图显示完整音频频段, 解码范围外的音调也看得见
|
||||
* 新: CW 文本框自动跟随新内容, 往上翻阅时自动停止跟随
|
||||
* 新: 读屏软件现在可以播报 CW 瀑布图与 AMSAT 日格内容
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* FIX: the CW transcript no longer appears to delete text while audio waits to be decoded
|
||||
* NEW: the CW waterfall shows the whole audio band, so a tone the decoder cannot reach is still visible
|
||||
* NEW: the CW transcript follows new text, and stops following as soon as you scroll back
|
||||
* NEW: screen readers now describe the CW waterfall and the AMSAT day cells
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "466"
|
||||
appVersionCode = "467"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.5.9"
|
||||
appVersionName = "4.6.0"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compileSdk = "37"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
|
||||
Reference in new issue
Block a user