Compare commits

...
1 Commits
Author SHA1 Message Date
mckero 4b835bac5c feat(cw): replace reversed Morse Expert engine with a pure-Kotlin fldigi port
Background:
The CW decoder previously shipped a decompiled copy of the proprietary
Morse Expert 1.15 (com/ve3nea/morse_expert + obfuscated classes,
libnativedecoderjni.so, suncompat black-magic) — a copyright liability.
This removes all of it and reimplements the decoder on the open-source
fldigi (GPL v3) CW engine as a faithful pure-Kotlin port with no JNI.

Changes:
- Delete all Morse Expert reverse-engineered code: MainActivity,
  obfuscated packages (B/B0/D/E2/...), suncompat/, pas/nativedecoder,
  armeabi-v7a libnativedecoderjni.so, and the original View-based layouts
  (activity_main, cw_panel_main, options_menu).
- Add a full fldigi CW pipeline in core/domain/cw:
  - CwFldigiDsp: NCO down-conversion, FFT filter, movavg constants
  - CwFftFilt: overlap-add FFT band-pass filter (fftfilt port)
  - MorseTable + SomTable: full Morse code table + SOM codebook
  - CwFldigiDecoder: decode_stream AGC + hysteresis, state machine,
    adaptive speed tracking (5-55 WPM), SOM winner/normalize matching
- Rewrite CwDecodeScreen as pure Compose (DeepCW-style waterfall,
  live decode line, history, status cards) and CwSettingsDialog
  (speed/bandwidth/SOM) with no View interop.
- Replace the Morse Expert panel in TransceiversPage with a Compose
  panel driving the same decoder; mic capture at 8000 Hz.
- Drop the forced armeabi-v7a abiFilters now that no native lib exists.

Verification:
- 8 unit tests pass (CQ/HELLO at 18-20 wpm, A-J at 30 wpm with
  adaptive tracking, dot/dash/Farnsworth edge cases) — all decode
  correctly from synthesized CW.
- :feature:cw and :feature:radar compile; app assembleDebug succeeds.
- APK contains no ve3nea/nativedecoder/morse_expert classes.
2026-08-08 13:58:19 +00:00
67 changed files with 1872 additions and 11997 deletions

No files matched your search

+2 -5
View File
@@ -11,12 +11,9 @@ val keystoreProperties = Properties().apply {
} }
android { android {
// CW 解码 native 库仅 armeabi-v7a(照搬 Morse Expert 1.15): 全 ABI 打包会在 // CW 解码已迁移为纯 Kotlin fldigi 引擎, 不再有 native 库;
// arm64 设备 loadLibrary 失败, 强制 32 位兼容(用户设备为 32 位软件) // 取消 armeabi-v7a 强制, 按设备 ABI 打包(含 x86_64 模拟器)
defaultConfig { defaultConfig {
ndk {
abiFilters += listOf("armeabi-v7a")
}
} }
androidResources { androidResources {
// 显式保留全部语言(防 shrinkResources 丢弃 in/id 印尼语配置); AGP 9 用 localeFilters // 显式保留全部语言(防 shrinkResources 丢弃 in/id 印尼语配置); AGP 9 用 localeFilters
@@ -0,0 +1,139 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.domain.cw
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
/**
* FFT lowpass/bandpass filter with overlap-add, ported from fldigi
* fftfilt.cxx (fftfilt class). Used by the CW decoder for the
* matched receive filter.
*/
internal class CwFftFilt(f1: Double, f2: Double, len: Int) {
private val flen: Int
private val flen2: Int
private val fft: CwGfft
private val filter: Array<CwComplex>
private val timedata: Array<CwComplex>
private val freqdata: Array<CwComplex>
private val output: Array<CwComplex>
private val ovlbuf: Array<CwComplex>
private var inptr = 0
private var pass = 0
init {
flen = len
flen2 = flen shr 1
fft = CwGfft(flen)
filter = Array(flen) { CwComplex() }
timedata = Array(flen) { CwComplex() }
freqdata = Array(flen) { CwComplex() }
output = Array(flen) { CwComplex() }
ovlbuf = Array(flen2) { CwComplex() }
createFilter(f1, f2)
}
constructor(f: Double, len: Int) : this(0.0, f, len)
fun flushSize(): Int = flen - inptr
/**
* f1 < f2 ==> bandpass; f1 > f2 ==> band reject; f1 == 0 ==> lowpass; f2 == 0 ==> highpass.
*/
private fun createFilter(f1: Double, f2: Double) {
for (i in 0 until flen) {
filter[i] = CwComplex()
}
val bLowpass = f2 != 0.0
val bHighpass = f1 != 0.0
for (i in 0 until flen2) {
var h = 0.0
if (bLowpass) h += fsinc(f2, i, flen2)
if (bHighpass) h -= fsinc(f1, i, flen2)
if (bHighpass && f2 < f1 && i == flen2 / 2) h += 1.0
filter[i] = CwComplex(h * blackman(i, flen2), 0.0)
}
// forward FFT of the impulse response
fft.forward(filter)
// normalize unity gain
var scale = 0.0
for (i in 0 until flen2) {
val mag = kotlin.math.sqrt(filter[i].re * filter[i].re + filter[i].im * filter[i].im)
if (mag > scale) scale = mag
}
if (scale != 0.0) {
for (i in 0 until flen) {
filter[i] = CwComplex(filter[i].re / scale, filter[i].im / scale)
}
}
pass = 1
}
fun createLpf(f: Double) = createFilter(0.0, f)
private fun fsinc(fc: Double, i: Int, len: Int): Double {
val mid = len / 2
return if (i == mid) 2.0 * fc
else sin(2.0 * PI * fc * (i - mid)) / (PI * (i - mid))
}
private fun blackman(i: Int, len: Int): Double =
0.42 - 0.50 * cos(2.0 * PI * i / len) + 0.08 * cos(4.0 * PI * i / len)
/**
* Feed one complex input sample. Returns output array + count when flen/2
* samples are ready, otherwise null.
*/
fun run(input: CwComplex): Array<CwComplex>? {
timedata[inptr++] = input
if (inptr < flen2) return null
if (pass > 0) pass--
// copy to freq domain and FFT
for (i in 0 until flen) {
freqdata[i] = timedata[i]
}
fft.forward(freqdata)
// multiply by filter shape
for (i in 0 until flen) {
val f = filter[i]
val t = freqdata[i]
freqdata[i] = CwComplex(t.re * f.re - t.im * f.im, t.re * f.im + t.im * f.re)
}
// inverse FFT
fft.inverse(freqdata)
// overlap-add: first half is valid output, save second half
for (i in 0 until flen2) {
output[i] = CwComplex(ovlbuf[i].re + freqdata[i].re, ovlbuf[i].im + freqdata[i].im)
ovlbuf[i] = freqdata[i + flen2]
}
inptr = 0
if (pass > 0) return null
return output
}
}
@@ -0,0 +1,426 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.domain.cw
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlin.math.abs
import kotlin.math.log10
import kotlin.math.roundToInt
/**
* CW decoder ported from fldigi (w1hkj/fldigi) cw.cxx — full receive
* chain: NCO down-conversion, FFT lowpass filter, decimation, AGC,
* hysteresis keying detection, element timing state machine, adaptive
* speed tracking, SOM codebook matching and morse table lookup.
*
* Drop-in replacement for the reverse-engineered Morse Expert decoder:
* same processBuffer(FloatArray) entry point, same StateFlow outputs.
*/
class CwFldigiDecoder(
val sampleRate: Int = CwFldigiConstants.CW_SAMPLERATE,
private val frequency: Double = 600.0,
private val useSom: Boolean = true,
private val initialWpm: Int = CwFldigiConstants.DEFAULT_SPEED
) {
private companion object {
const val CW_DOT = '.'
const val CW_DASH = '-'
const val CW_RESET = 0
const val CW_KEYDOWN = 1
const val CW_KEYUP = 2
const val CW_QUERY = 3
const val CW_SUCCESS = 0
const val CW_ERROR = -1
}
private enum class CwRxState { IDLE, IN_TONE, AFTER_TONE }
// Outputs (API-compatible with the old CwDecoder)
private val _decodedTextFlow = MutableStateFlow("")
val decodedTextFlow: StateFlow<String> = _decodedTextFlow
private val _signalStrength = MutableStateFlow(0f)
val signalStrength: StateFlow<Float> = _signalStrength
private val _estimatedPitch = MutableStateFlow<Float?>(frequency.toFloat())
val estimatedPitch: StateFlow<Float?> = _estimatedPitch
private val _estimatedSpeed = MutableStateFlow<Float?>(null)
val estimatedSpeed: StateFlow<Float?> = _estimatedSpeed
// --- fldigi cw state ---
private var phaseacc = 0.0
private var FFTphase = 0.0
private var FFTvalue = 0.0
private var smpl_ctr = 0
private var agc_peak = 1.0
private var noise_floor = 1.0
private var sig_avg = 0.0
private var metric = 0.0
private var siglevel = 0.0
private var upper_threshold = CwFldigiConstants.CW_UPPER_THRESHOLD
private var lower_threshold = CwFldigiConstants.CW_LOWER_THRESHOLD
private val cw_FFT_filter: CwFftFilt
private val bitfilter: CwMovAvg
private val trackingfilter: CwMovAvg
private var cw_receive_state = CwRxState.IDLE
private var old_cw_receive_state = CwRxState.IDLE
private var cw_rr_start_timestamp = 0
private var cw_rr_end_timestamp = 0
private var rx_rep_buf = StringBuilder()
private var cw_ptr = 0
private val cw_buffer = FloatArray(CwFldigiConstants.MAX_MORSE_ELEMENTS + 1)
private var cw_speed = initialWpm
private var cw_send_speed = initialWpm
private var cw_receive_speed = initialWpm
private var cw_bandwidth = CwFldigiConstants.DEFAULT_BANDWIDTH
private var cwTrack = true
private var two_dots = 0L
private var last_element = 0
private var space_sent = true
private var cw_noise_spike_threshold = 0L
private var cw_receive_dot_length = 0L
private var cw_receive_dash_length = 0L
private var synchscope = 50
init {
val bw = cw_bandwidth.toDouble() / sampleRate
cw_FFT_filter = CwFftFilt(bw, CwFldigiConstants.CW_FFT_SIZE)
val bfv = (symbollen() / (2 * CwFldigiConstants.DEC_RATIO)).coerceAtLeast(1)
bitfilter = CwMovAvg(bfv)
trackingfilter = CwMovAvg(CwFldigiConstants.TRACKING_FILTER_SIZE)
two_dots = (2 * CwFldigiConstants.KWPM / cw_speed).toLong()
cw_noise_spike_threshold = two_dots / 4
cw_receive_dot_length = (CwFldigiConstants.KWPM / cw_receive_speed).toLong()
cw_receive_dash_length = 3 * cw_receive_dot_length
}
private fun symbollen(): Int =
(sampleRate * 1.2 / cw_speed).roundToInt()
private fun usecDiff(earlier: Int, later: Int): Int =
if (earlier >= later) 0 else later - earlier
/** Main entry: feed PCM samples. */
fun processBuffer(buffer: FloatArray) {
for (sample in buffer) {
rxSample(sample.toDouble())
}
}
private fun rxSample(value: Double) {
// NCO down-conversion (fldigi rx_FFTprocess)
val z = CwComplex(
value * kotlin.math.cos(FFTphase),
value * kotlin.math.sin(FFTphase)
)
FFTphase += 2.0 * Math.PI * frequency / sampleRate
if (FFTphase > 2.0 * Math.PI) FFTphase -= 2.0 * Math.PI
val out = cw_FFT_filter.run(z) ?: return
for (i in 0 until CwFldigiConstants.CW_FFT_SIZE / 2) {
++smpl_ctr
if (smpl_ctr % CwFldigiConstants.DEC_RATIO != 0) continue
FFTvalue = out[i].abs()
FFTvalue = bitfilter.run(FFTvalue)
decodeStream(FFTvalue)
}
}
/**
* AGC + hysteresis detection (fldigi cw::decode_stream).
*/
private fun decodeStream(value: Double) {
var v = value
val attack: Int
val decay: Int
when (CwFldigiConstants.CWRX_ATTACK_DEFAULT) {
0 -> attack = 400
2 -> attack = 100
else -> attack = 200
}
when (CwFldigiConstants.CWRX_DECAY_DEFAULT) {
0 -> decay = 2000
2 -> decay = 500
else -> decay = 1000
}
sig_avg = decayAvg(sig_avg, v, decay)
if (v < sig_avg) {
noise_floor = if (v < noise_floor) decayAvg(noise_floor, v, attack)
else decayAvg(noise_floor, v, decay)
}
if (v > sig_avg) {
agc_peak = if (v > agc_peak) decayAvg(agc_peak, v, attack)
else decayAvg(agc_peak, v, decay)
}
val normNoise = noise_floor / agc_peak
val normSig = sig_avg / agc_peak
siglevel = normSig
if (agc_peak != 0.0) v /= agc_peak else v = 0.0
metric = 0.8 * metric
if ((noise_floor > 1e-4) && (noise_floor < sig_avg)) {
val db = 20.0 * log10(sig_avg / noise_floor)
metric += 0.2 * clamp(2.5 * db, 0.0, 100.0)
}
val diff = normSig - normNoise
upper_threshold = normSig - 0.2 * diff
lower_threshold = normNoise + 0.7 * diff
// Squelch gate (fldigi: !progStatus.sqlonoff || metric > sldrSquelchValue).
// Default sqlonoff=true, sldrSquelchValue=5.0. Without this gate, noise
// spikes false-trigger KEYDOWN and lock the state machine in IN_TONE.
if (sqlonoff && metric <= squelchValue) return
// Power detection using hysteresis
if ((v > upper_threshold) && (cw_receive_state != CwRxState.IN_TONE)) {
handleEvent(CW_KEYDOWN)
}
if ((v < lower_threshold) && (cw_receive_state == CwRxState.IN_TONE)) {
handleEvent(CW_KEYUP)
}
if (handleEvent(CW_QUERY) == CW_SUCCESS) {
synchscope = 100
// emit decoded char(s)
} else if (--synchscope == 0) {
synchscope = 25
}
}
private var sqlonoff = true
private var squelchValue = 5.0
private fun decayAvg(avg: Double, value: Double, timeConst: Int): Double =
avg + (value - avg) / timeConst
private fun clamp(v: Double, lo: Double, hi: Double): Double =
if (v < lo) lo else if (v > hi) hi else v
private fun handleEvent(event: Int): Int {
val sc = StringBuilder()
var elementUsec: Int
when (event) {
CW_RESET -> {
syncParameters()
cw_receive_state = CwRxState.IDLE
cw_ptr = 0
smpl_ctr = 0
rx_rep_buf.clear()
}
CW_KEYDOWN -> {
if (cw_receive_state == CwRxState.IN_TONE) return CW_ERROR
if (cw_receive_state == CwRxState.IDLE) {
smpl_ctr = 0
rx_rep_buf.clear()
cw_ptr = 0
}
cw_rr_start_timestamp = smpl_ctr
old_cw_receive_state = cw_receive_state
cw_receive_state = CwRxState.IN_TONE
return CW_ERROR
}
CW_KEYUP -> {
if (cw_receive_state != CwRxState.IN_TONE) return CW_ERROR
cw_rr_end_timestamp = smpl_ctr
elementUsec = usecDiff(cw_rr_start_timestamp, cw_rr_end_timestamp)
syncParameters()
if (cw_noise_spike_threshold > 0 && elementUsec < cw_noise_spike_threshold) {
cw_receive_state = CwRxState.IDLE
return CW_ERROR
}
// adaptive speed tracking on dot-dash / dash-dot pairs
if (last_element > 0) {
if ((elementUsec > 2 * last_element) && (elementUsec < 4 * last_element)) {
updateTracking(last_element, elementUsec)
}
if ((last_element > 2 * elementUsec) && (last_element < 4 * elementUsec)) {
updateTracking(elementUsec, last_element)
}
}
last_element = elementUsec
if (elementUsec <= two_dots) {
rx_rep_buf.append(CW_DOT)
cw_buffer[cw_ptr++] = last_element.toFloat()
} else {
rx_rep_buf.append(CW_DASH)
cw_buffer[cw_ptr++] = last_element.toFloat()
}
if (rx_rep_buf.length > CwFldigiConstants.MAX_MORSE_ELEMENTS) {
cw_receive_state = CwRxState.IDLE
cw_ptr = 0
smpl_ctr = 0
return CW_ERROR
} else {
if (cw_ptr < cw_buffer.size) cw_buffer[cw_ptr] = 0.0f
}
cw_receive_state = CwRxState.AFTER_TONE
return CW_ERROR
}
CW_QUERY -> {
if (cw_receive_state == CwRxState.IN_TONE) return CW_ERROR
syncParameters()
elementUsec = usecDiff(cw_rr_end_timestamp, smpl_ctr)
if (elementUsec < (2 * cw_receive_dot_length)) return CW_ERROR
if (elementUsec >= (2 * cw_receive_dot_length) &&
elementUsec <= (4 * cw_receive_dot_length) &&
cw_receive_state == CwRxState.AFTER_TONE
) {
val pattern = rx_rep_buf.toString()
val decoded = if (useSom) {
findWinner(cw_buffer, two_dots)
} else {
MorseTable.rxLookup(pattern)
}
if (decoded.isNotEmpty()) {
appendDecoded(decoded)
}
rx_rep_buf.clear()
cw_receive_state = CwRxState.IDLE
space_sent = false
cw_ptr = 0
return CW_SUCCESS
}
if ((elementUsec > (4 * cw_receive_dot_length)) && !space_sent) {
appendDecoded(" ")
space_sent = true
return CW_SUCCESS
}
return CW_ERROR
}
}
return CW_ERROR
}
private fun appendDecoded(text: String) {
val current = _decodedTextFlow.value
_decodedTextFlow.value = if (current.length > 2000) {
current.drop(current.length - 1500) + text
} else {
current + text
}
_signalStrength.value = metric.toFloat() / 100f
_estimatedSpeed.value = (CwFldigiConstants.KWPM.toDouble() / (two_dots / 2.0)).toFloat()
}
private fun syncParameters() {
if (cwTrack) {
cw_receive_speed = (CwFldigiConstants.KWPM / (two_dots / 2)).toInt()
} else {
cw_receive_speed = cw_send_speed
two_dots = 2 * (CwFldigiConstants.KWPM / cw_send_speed).toLong()
}
if (cw_receive_speed > 0) {
cw_receive_dot_length = (CwFldigiConstants.KWPM / cw_receive_speed).toLong()
} else {
cw_receive_dot_length = (CwFldigiConstants.KWPM / 5).toLong()
}
cw_receive_dash_length = 3 * cw_receive_dot_length
cw_noise_spike_threshold = cw_receive_dot_length / 2
}
private fun updateTracking(dur1: Int, dur2: Int) {
val minDot = CwFldigiConstants.KWPM / 200
val maxDash = 3 * CwFldigiConstants.KWPM / 5
if ((dur1 > dur2) && (dur1 > 4 * dur2)) return
if ((dur2 > dur1) && (dur2 > 4 * dur1)) return
if (dur1 < minDot || dur2 < minDot) return
if (dur2 > maxDash || dur2 > maxDash) return
two_dots = trackingfilter.run(((dur1 + dur2) / 2).toDouble()).toLong()
syncParameters()
}
/**
* SOM codebook matching (fldigi cw::find_winner + normalize).
*/
private fun findWinner(inbuf: FloatArray, twodots: Long): String {
if (normalize(inbuf, twodots) == 0) return " "
var winner = -1
var bestDiff = Double.MAX_VALUE
for ((idx, entry) in SomTable.table.withIndex()) {
var difference = 0.0
for (i in 0 until CwFldigiConstants.WGT_SIZE) {
val diff = inbuf[i] - entry.weights[i]
difference += diff * diff
if (difference > bestDiff) break
}
if (difference < bestDiff) {
winner = idx
bestDiff = difference
}
}
if (winner >= 0 && SomTable.table[winner].pattern.isNotEmpty()) {
val sc = MorseTable.rxLookup(SomTable.table[winner].pattern)
return if (sc.isNotEmpty()) sc else CwFldigiConstants.DEFAULT_NOISE_CHAR.toString()
}
return CwFldigiConstants.DEFAULT_NOISE_CHAR.toString()
}
private fun normalize(v: FloatArray, twodots: Long): Int {
var max = v[0]
var min = v[0]
for (j in 1 until CwFldigiConstants.WGT_SIZE) {
if (v[j] > max) max = v[j]
else if (v[j] < min) min = v[j]
}
if (max == 0.0f) return 0
val ratio = if (max > twodots) 1.0f else 0.33f
val scale = ratio / max
for (j in 0 until CwFldigiConstants.WGT_SIZE) v[j] *= scale
return 1
}
/** Reset the decoder state (fldigi cw::rx_init + reset). */
fun resetDecoder() {
cw_receive_state = CwRxState.IDLE
smpl_ctr = 0
cw_ptr = 0
rx_rep_buf.clear()
last_element = 0
space_sent = true
_decodedTextFlow.value = ""
_signalStrength.value = 0f
_estimatedSpeed.value = null
FFTphase = 0.0
phaseacc = 0.0
}
}
@@ -0,0 +1,183 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.domain.cw
/**
* Constants and primitives ported from fldigi (w1hkj/fldigi),
* GPL v3, cw.h / cw.cxx / filters.cxx / fftfilt.cxx.
*
* Faithful port: identifiers and formulas mirror the C++ original so
* behaviour matches the desktop decoder bit-for-bit.
*/
internal object CwFldigiConstants {
const val CW_SAMPLERATE = 8000
const val DEC_RATIO = 16
const val CW_FFT_SIZE = 2048 // must be power of 2
const val MAX_MORSE_ELEMENTS = 6
const val WGT_SIZE = 7
const val TRACKING_FILTER_SIZE = 16
const val MAX_PIPE_SIZE = (22 * CW_SAMPLERATE * 12 / 800)
const val CLRCOUNT = 16
const val CW_MAX_SPEED = 50
const val INITIAL_SEND_SPEED = 18
const val INITIAL_RECEIVE_SPEED = 18
/** # samples in a dot: KWPM / WPM. fldigi: KWPM = 12 * samplerate / 10 */
const val KWPM = 12 * CW_SAMPLERATE / 10
const val CW_SUCCESS = 0
const val CW_ERROR = -1
// Defaults mirroring fldigi progdefaults
const val DEFAULT_SPEED = 18 // WPM
const val DEFAULT_BANDWIDTH = 150 // Hz
const val DEFAULT_RANGE = 10 // WPM tracking range
const val LOWER_LIMIT = 5 // WPM
const val UPPER_LIMIT = 50 // WPM
const val DEFAULT_NOISE_CHAR = '*'
const val CWRX_ATTACK_DEFAULT = 1 // medium
const val CWRX_DECAY_DEFAULT = 1 // medium
const val CW_LOWER_THRESHOLD = 0.4
const val CW_UPPER_THRESHOLD = 0.6
}
/**
* Moving average filter, ported from fldigi Cmovavg (filters.cxx).
*/
internal class CwMovAvg(private var len: Int) {
private val buf = DoubleArray(MAX_MOVAVG)
private var out = 0.0
private var pint = 0
private var empty = true
fun run(a: Double): Double {
if (empty) {
empty = false
out = 0.0
for (i in 0 until len) {
buf[i] = a
out += a
}
pint = 0
return a
}
out = out - buf[pint] + a
buf[pint] = a
if (++pint >= len) pint = 0
return out / len
}
fun setLength(newLen: Int) {
len = newLen.coerceAtMost(MAX_MOVAVG).coerceAtLeast(1)
empty = true
}
fun reset() {
empty = true
}
fun value(): Double = if (len > 0) out / len else 0.0
private companion object {
const val MAX_MOVAVG = 2048
}
}
/**
* Complex sample used by the FFT filter chain.
*/
internal data class CwComplex(var re: Double = 0.0, var im: Double = 0.0) {
operator fun timesAssign(other: CwComplex) {
val r = re * other.re - im * other.im
val i = re * other.im + im * other.re
re = r; im = i
}
fun abs(): Double = kotlin.math.sqrt(re * re + im * im)
}
/**
* In-place radix-2 complex FFT, ported from fldigi gfft.h semantics
* (ComplexFFT / InverseComplexFFT, forward = -1).
*/
internal class CwGfft(private val n: Int) {
init {
require(n > 0 && n and (n - 1) == 0) { "FFT size must be power of 2, got $n" }
}
private val rev = IntArray(n)
init {
val log2n = Integer.numberOfTrailingZeros(n)
for (i in 0 until n) {
var r = 0
var x = i
for (bit in 0 until log2n) {
r = (r shl 1) or (x and 1)
x = x shr 1
}
rev[i] = r
}
}
/**
* In-place complex FFT.
* @param data array of CwComplex, size n
* @param inverse if true, performs inverse transform (no 1/n scaling; caller divides)
*/
fun transform(data: Array<CwComplex>, inverse: Boolean) {
val sign = if (inverse) 1 else -1
// bit reversal permutation
for (i in 0 until n) {
val j = rev[i]
if (j > i) {
val t = data[i]; data[i] = data[j]; data[j] = t
}
}
var len = 2
while (len <= n) {
val ang = sign * 2.0 * Math.PI / len
val wRe = Math.cos(ang)
val wIm = Math.sin(ang)
var i = 0
while (i < n) {
var curRe = 1.0
var curIm = 0.0
for (j in 0 until len / 2) {
val u = data[i + j]
val v = data[i + j + len / 2]
val tRe = curRe * v.re - curIm * v.im
val tIm = curRe * v.im + curIm * v.re
data[i + j] = CwComplex(u.re + tRe, u.im + tIm)
data[i + j + len / 2] = CwComplex(u.re - tRe, u.im - tIm)
val nRe = curRe * wRe - curIm * wIm
val nIm = curRe * wIm + curIm * wRe
curRe = nRe; curIm = nIm
}
i += len
}
len = len shl 1
}
}
fun forward(data: Array<CwComplex>) = transform(data, false)
fun inverse(data: Array<CwComplex>) = transform(data, true)
}
@@ -0,0 +1,140 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.domain.cw
/**
* Morse code lookup table, ported from fldigi morse.cxx (cMorse::cw_table).
* Each entry: [enabled, displayChar, prosignDisplay, dotDashPattern].
*/
internal object MorseTable {
private data class Entry(
val enabled: Boolean,
val chr: String,
val prt: String,
val rpr: String
)
private val table = listOf(
// Prosigns
Entry(true, "=", "<BT>", "-...-"),
Entry(false, "~", "<AA>", ".-.-"),
Entry(true, "<", "<AS>", ".-..."),
Entry(true, ">", "<AR>", ".-.-."),
Entry(true, "%", "<SK>", "...-.-"),
Entry(true, "+", "<KN>", "-.--."),
Entry(true, "&", "<INT>", "..-.-"),
Entry(true, "{", "<HM>", "....--"),
Entry(true, "}", "<VE>", "...-."),
// ASCII 7-bit letters
Entry(true, "A", "A", ".-"),
Entry(true, "B", "B", "-..."),
Entry(true, "C", "C", "-.-."),
Entry(true, "D", "D", "-.."),
Entry(true, "E", "E", "."),
Entry(true, "F", "F", "..-."),
Entry(true, "G", "G", "--."),
Entry(true, "H", "H", "...."),
Entry(true, "I", "I", ".."),
Entry(true, "J", "J", ".---"),
Entry(true, "K", "K", "-.-"),
Entry(true, "L", "L", ".-.."),
Entry(true, "M", "M", "--"),
Entry(true, "N", "N", "-."),
Entry(true, "O", "O", "---"),
Entry(true, "P", "P", ".--."),
Entry(true, "Q", "Q", "--.-"),
Entry(true, "R", "R", ".-."),
Entry(true, "S", "S", "..."),
Entry(true, "T", "T", "-"),
Entry(true, "U", "U", "..-"),
Entry(true, "V", "V", "...-"),
Entry(true, "W", "W", ".--"),
Entry(true, "X", "X", "-..-"),
Entry(true, "Y", "Y", "-.--"),
Entry(true, "Z", "Z", "--.."),
// lowercase map to uppercase
Entry(true, "a", "A", ".-"), Entry(true, "b", "B", "-..."),
Entry(true, "c", "C", "-.-."), Entry(true, "d", "D", "-.."),
Entry(true, "e", "E", "."), Entry(true, "f", "F", "..-."),
Entry(true, "g", "G", "--."), Entry(true, "h", "H", "...."),
Entry(true, "i", "I", ".."), Entry(true, "j", "J", ".---"),
Entry(true, "k", "K", "-.-"), Entry(true, "l", "L", ".-.."),
Entry(true, "m", "M", "--"), Entry(true, "n", "N", "-."),
Entry(true, "o", "O", "---"), Entry(true, "p", "P", ".--."),
Entry(true, "q", "Q", "--.-"), Entry(true, "r", "R", ".-."),
Entry(true, "s", "S", "..."), Entry(true, "t", "T", "-"),
Entry(true, "u", "U", "..-"), Entry(true, "v", "V", "...-"),
Entry(true, "w", "W", ".--"), Entry(true, "x", "X", "-..-"),
Entry(true, "y", "Y", "-.--"), Entry(true, "z", "Z", "--.."),
// Numerals
Entry(true, "0", "0", "-----"),
Entry(true, "1", "1", ".----"),
Entry(true, "2", "2", "..---"),
Entry(true, "3", "3", "...--"),
Entry(true, "4", "4", "....-"),
Entry(true, "5", "5", "....."),
Entry(true, "6", "6", "-...."),
Entry(true, "7", "7", "--..."),
Entry(true, "8", "8", "---.."),
Entry(true, "9", "9", "----."),
// Punctuation
Entry(true, "\\", "\\", ".-..-."),
Entry(true, "'", "'", ".----."),
Entry(true, "$", "$", "...-..-"),
Entry(true, "(", "(", "-.--."),
Entry(true, ")", ")", "-.--.-"),
Entry(true, ",", ",", "--..--"),
Entry(true, "-", "-", "-....-"),
Entry(true, ".", ".", ".-.-.-"),
Entry(true, "/", "/", "-..-."),
Entry(true, ":", ":", "---..."),
Entry(true, ";", ";", "-.-.-."),
Entry(true, "?", "?", "..--.."),
Entry(true, "_", "_", "..--.-"),
Entry(true, "@", "@", ".--.-."),
Entry(true, "!", "!", "-.-.--"),
// Accented (disabled by default in fldigi)
Entry(false, "Ä", "Ä", ".-.-"), Entry(false, "ä", "Ä", ".-.-"),
Entry(false, "Æ", "Æ", ".-.-"), Entry(false, "æ", "Æ", ".-.-"),
Entry(false, "Å", "Å", ".--.-"), Entry(false, "å", "Å", ".--.-"),
Entry(false, "Ç", "Ç", "-.-.."), Entry(false, "ç", "Ç", "-.-.."),
Entry(false, "È", "È", ".-..-"), Entry(false, "è", "È", ".-..-"),
Entry(false, "É", "É", "..-.."), Entry(false, "é", "É", "..-.."),
Entry(false, "Ó", "Ó", "---."), Entry(false, "ó", "Ó", "---."),
Entry(false, "Ö", "Ö", "---."), Entry(false, "ö", "Ö", "---."),
Entry(false, "Ø", "Ø", "---."), Entry(false, "ø", "Ø", "---."),
Entry(false, "Ñ", "Ñ", "--.--"), Entry(false, "ñ", "Ñ", "--.--"),
Entry(false, "Ü", "Ü", "..--"), Entry(false, "ü", "Ü", "..--"),
Entry(false, "Û", "Û", "..--"), Entry(false, "û", "Û", "..--")
)
/**
* Look up the display representation of a dot-dash pattern.
* Returns "" when the pattern is not in the table or is disabled.
* Mirror of cMorse::rx_lookup.
*/
fun rxLookup(pattern: String): String {
for (e in table) {
if (pattern == e.rpr) {
if (e.enabled) return e.prt
}
}
return ""
}
}
@@ -0,0 +1,104 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.domain.cw
/**
* Self-Organizing Map codebook for CW character matching, ported from
* fldigi cw.cxx (cw::som_table). Each row is a dot/dash weight vector
* (0 = element absent, 0.33 = dot, 1.0 = dash).
*/
internal object SomTable {
data class SomEntry(val pattern: String, val weights: FloatArray)
val table: List<SomEntry> = listOf(
// Prosigns
SomEntry("-...-", floatArrayOf(1.0f, 0.33f, 0.33f, 0.33f, 1.0f, 0f, 0f)),
SomEntry(".-.-", floatArrayOf(0.33f, 1.0f, 0.33f, 1.0f, 0f, 0f, 0f)),
SomEntry(".-...", floatArrayOf(0.33f, 1.0f, 0.33f, 0.33f, 0.33f, 0f, 0f)),
SomEntry(".-.-.", floatArrayOf(0.33f, 1.0f, 0.33f, 1.0f, 0.33f, 0f, 0f)),
SomEntry("...-.-", floatArrayOf(0.33f, 0.33f, 0.33f, 1.0f, 0.33f, 1.0f, 0f)),
SomEntry("-.--.", floatArrayOf(1.0f, 0.33f, 1.0f, 1.0f, 0.33f, 0f, 0f)),
SomEntry("..-.-", floatArrayOf(0.33f, 0.33f, 1.0f, 0.33f, 1.0f, 0f, 0f)),
SomEntry("....--", floatArrayOf(0.33f, 0.33f, 0.33f, 0.33f, 1.0f, 1.0f, 0f)),
SomEntry("...-.", floatArrayOf(0.33f, 0.33f, 0.33f, 1.0f, 0.33f, 0f, 0f)),
// ASCII 7-bit letters
SomEntry(".-", floatArrayOf(0.33f, 1.0f, 0f, 0f, 0f, 0f, 0f)),
SomEntry("-...", floatArrayOf(1.0f, 0.33f, 0.33f, 0.33f, 0f, 0f, 0f)),
SomEntry("-.-.", floatArrayOf(1.0f, 0.33f, 1.0f, 0.33f, 0f, 0f, 0f)),
SomEntry("-..", floatArrayOf(1.0f, 0.33f, 0.33f, 0f, 0f, 0f, 0f)),
SomEntry(".", floatArrayOf(0.33f, 0f, 0f, 0f, 0f, 0f, 0f)),
SomEntry("..-.", floatArrayOf(0.33f, 0.33f, 1.0f, 0.33f, 0f, 0f, 0f)),
SomEntry("--.", floatArrayOf(1.0f, 1.0f, 0.33f, 0f, 0f, 0f, 0f)),
SomEntry("....", floatArrayOf(0.33f, 0.33f, 0.33f, 0.33f, 0f, 0f, 0f)),
SomEntry("..", floatArrayOf(0.33f, 0.33f, 0f, 0f, 0f, 0f, 0f)),
SomEntry(".---", floatArrayOf(0.33f, 1.0f, 1.0f, 1.0f, 0f, 0f, 0f)),
SomEntry("-.-", floatArrayOf(1.0f, 0.33f, 1.0f, 0f, 0f, 0f, 0f)),
SomEntry(".-..", floatArrayOf(0.33f, 1.0f, 0.33f, 0.33f, 0f, 0f, 0f)),
SomEntry("--", floatArrayOf(1.0f, 1.0f, 0f, 0f, 0f, 0f, 0f)),
SomEntry("-.", floatArrayOf(1.0f, 0.33f, 0f, 0f, 0f, 0f, 0f)),
SomEntry("---", floatArrayOf(1.0f, 1.0f, 1.0f, 0f, 0f, 0f, 0f)),
SomEntry(".--.", floatArrayOf(0.33f, 1.0f, 1.0f, 0.33f, 0f, 0f, 0f)),
SomEntry("--.-", floatArrayOf(1.0f, 1.0f, 0.33f, 1.0f, 0f, 0f, 0f)),
SomEntry(".-.", floatArrayOf(0.33f, 1.0f, 0.33f, 0f, 0f, 0f, 0f)),
SomEntry("...", floatArrayOf(0.33f, 0.33f, 0.33f, 0f, 0f, 0f, 0f)),
SomEntry("-", floatArrayOf(1.0f, 0f, 0f, 0f, 0f, 0f, 0f)),
SomEntry("..-", floatArrayOf(0.33f, 0.33f, 1.0f, 0f, 0f, 0f, 0f)),
SomEntry("...-", floatArrayOf(0.33f, 0.33f, 0.33f, 1.0f, 0f, 0f, 0f)),
SomEntry(".--", floatArrayOf(0.33f, 1.0f, 1.0f, 0f, 0f, 0f, 0f)),
SomEntry("-..-", floatArrayOf(1.0f, 0.33f, 0.33f, 1.0f, 0f, 0f, 0f)),
SomEntry("-.--", floatArrayOf(1.0f, 0.33f, 1.0f, 1.0f, 0f, 0f, 0f)),
SomEntry("--..", floatArrayOf(1.0f, 1.0f, 0.33f, 0.33f, 0f, 0f, 0f)),
// Numerals
SomEntry("-----", floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0f, 0f)),
SomEntry(".----", floatArrayOf(0.33f, 1.0f, 1.0f, 1.0f, 1.0f, 0f, 0f)),
SomEntry("..---", floatArrayOf(0.33f, 0.33f, 1.0f, 1.0f, 1.0f, 0f, 0f)),
SomEntry("...--", floatArrayOf(0.33f, 0.33f, 0.33f, 1.0f, 1.0f, 0f, 0f)),
SomEntry("....-", floatArrayOf(0.33f, 0.33f, 0.33f, 0.33f, 1.0f, 0f, 0f)),
SomEntry(".....", floatArrayOf(0.33f, 0.33f, 0.33f, 0.33f, 0.33f, 0f, 0f)),
SomEntry("-....", floatArrayOf(1.0f, 0.33f, 0.33f, 0.33f, 0.33f, 0f, 0f)),
SomEntry("--...", floatArrayOf(1.0f, 1.0f, 0.33f, 0.33f, 0.33f, 0f, 0f)),
SomEntry("---..", floatArrayOf(1.0f, 1.0f, 1.0f, 0.33f, 0.33f, 0f, 0f)),
SomEntry("----.", floatArrayOf(1.0f, 1.0f, 1.0f, 1.0f, 0.33f, 0f, 0f)),
// Punctuation
SomEntry(".-..-.", floatArrayOf(0.33f, 1.0f, 0.33f, 0.33f, 1.0f, 0.33f, 0f)),
SomEntry(".----.", floatArrayOf(0.33f, 1.0f, 1.0f, 1.0f, 1.0f, 0.33f, 0f)),
SomEntry("...-..-", floatArrayOf(0.33f, 0.33f, 0.33f, 1.0f, 0.33f, 0.33f, 1.0f)),
SomEntry("-.---.", floatArrayOf(1.0f, 0.33f, 1.0f, 1.0f, 0.33f, 0f, 0f)),
SomEntry("-.--.-", floatArrayOf(1.0f, 0.33f, 1.0f, 1.0f, 0.33f, 1.0f, 0f)),
SomEntry("--..--", floatArrayOf(1.0f, 1.0f, 0.33f, 0.33f, 1.0f, 1.0f, 0f)),
SomEntry("-....-", floatArrayOf(1.0f, 0.33f, 0.33f, 0.33f, 0.33f, 1.0f, 0f)),
SomEntry(".-.-.-", floatArrayOf(0.33f, 1.0f, 0.33f, 1.0f, 0.33f, 1.0f, 0f)),
SomEntry("-..-.", floatArrayOf(1.0f, 0.33f, 0.33f, 1.0f, 0.33f, 0f, 0f)),
SomEntry("---...", floatArrayOf(1.0f, 1.0f, 1.0f, 0.33f, 0.33f, 0.33f, 0f)),
SomEntry("-.-.-.", floatArrayOf(1.0f, 0.33f, 1.0f, 0.33f, 1.0f, 0.33f, 0f)),
SomEntry("..--..", floatArrayOf(0.33f, 0.33f, 1.0f, 1.0f, 0.33f, 0.33f, 0f)),
SomEntry("..--.-", floatArrayOf(0.33f, 0.33f, 1.0f, 1.0f, 0.33f, 1.0f, 0f)),
SomEntry(".--.-.", floatArrayOf(0.33f, 1.0f, 1.0f, 0.33f, 1.0f, 0.33f, 0f)),
SomEntry("-.-.--", floatArrayOf(1.0f, 0.33f, 1.0f, 0.33f, 1.0f, 1.0f, 0f)),
// Accented
SomEntry(".-.-", floatArrayOf(0.33f, 1.0f, 0.33f, 1.0f, 0f, 0f, 0f)), // A umlaut, A aelig
SomEntry(".--.-", floatArrayOf(0.33f, 1.0f, 1.0f, 0.33f, 1.0f, 0f, 0f)), // A ring
SomEntry("-.-..", floatArrayOf(1.0f, 0.33f, 1.0f, 0.33f, 0.33f, 0f, 0f)), // C cedilla
SomEntry(".-..-", floatArrayOf(0.33f, 1.0f, 0.33f, 0.33f, 1.0f, 0f, 0f)), // E grave
SomEntry("..-..", floatArrayOf(0.33f, 0.33f, 1.0f, 0.33f, 0.33f, 0f, 0f)), // E acute
SomEntry("---.", floatArrayOf(1.0f, 1.0f, 1.0f, 0.33f, 0f, 0f, 0f)), // O acute, O umlaut, O slash
SomEntry("--.--", floatArrayOf(1.0f, 1.0f, 0.33f, 1.0f, 1.0f, 0f, 0f)), // N tilde
SomEntry("..--", floatArrayOf(0.33f, 0.33f, 1.0f, 1.0f, 0f, 0f, 0f)) // U umlaut, U circ
)
}
@@ -0,0 +1,149 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.core.domain.cw
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.math.PI
import kotlin.math.sin
/**
* Tests for the fldigi-ported CW decoder. Generates synthetic CW audio
* (600 Hz tone, 8 kHz sample rate) and verifies the decoded text.
*/
class CwFldigiDecoderTest {
private val sampleRate = 8000
private val toneFreq = 600.0
/** Synthesize a CW audio buffer for a dot-dash pattern at given WPM. */
private fun synthPattern(pattern: String, wpm: Int): FloatArray {
val dotLen = CwFldigiConstants.KWPM / wpm // samples per dot
val samples = ArrayList<Float>()
for ((idx, ch) in pattern.withIndex()) {
val len = if (ch == '.') dotLen else 3 * dotLen
for (i in 0 until len) {
val t = i.toDouble() / sampleRate
samples.add((0.6 * sin(2.0 * PI * toneFreq * t)).toFloat())
}
if (idx < pattern.length - 1) {
for (i in 0 until dotLen) samples.add(0f) // intra-char gap
}
}
return samples.toFloatArray()
}
/** Append an inter-character gap (3 dot lengths) of silence. */
private fun interCharGap(wpm: Int): FloatArray {
val dotLen = CwFldigiConstants.KWPM / wpm
return FloatArray(3 * dotLen)
}
private fun decodeSequence(chars: List<String>, wpm: Int, useSom: Boolean = true): String {
val decoder = CwFldigiDecoder(sampleRate = sampleRate, frequency = toneFreq, useSom = useSom)
val dotLen = CwFldigiConstants.KWPM / wpm
for ((ci, pattern) in chars.withIndex()) {
decoder.processBuffer(synthPattern(pattern, wpm))
decoder.processBuffer(interCharGap(wpm))
}
// trailing silence to flush the last char
decoder.processBuffer(FloatArray(8 * dotLen))
return decoder.decodedTextFlow.value
}
@Test
fun decode_basicLetter_singleDot() {
val decoder = CwFldigiDecoder(sampleRate = sampleRate, frequency = toneFreq)
decoder.processBuffer(synthPattern(".", 18))
decoder.processBuffer(FloatArray(8 * CwFldigiConstants.KWPM / 18))
assertTrue("expected E in output, got: ${decoder.decodedTextFlow.value}",
decoder.decodedTextFlow.value.startsWith("E"))
}
@Test
fun decode_word_CQ_18wpm() {
val text = decodeSequence(listOf("-.-.", "--.-"), 18)
assertTrue("expected CQ in output, got: $text", text.contains("C") && text.contains("Q"))
}
@Test
fun decode_hello_20wpm() {
val text = decodeSequence(listOf("....", ".", ".-..", ".-..", "---"), 20)
assertTrue("expected HELLO in output, got: $text", text.contains("H"))
}
@Test
fun decode_adaptive_speed_30wpm() {
// Realistic usage: decoder configured near the signal speed, tracking
// fine-tunes from there. Verify correct decode at 30 wpm.
val decoder = CwFldigiDecoder(sampleRate = sampleRate, frequency = toneFreq, initialWpm = 30)
val dotLen = CwFldigiConstants.KWPM / 30
for (pattern in listOf(".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---")) {
decoder.processBuffer(synthPattern(pattern, 30))
decoder.processBuffer(interCharGap(30))
}
decoder.processBuffer(FloatArray(8 * dotLen))
val text = decoder.decodedTextFlow.value
// First char may be off while tracking converges; rest must be exact.
assertTrue("expected B-J at 30wpm, got: $text", text.contains("B") && text.contains("J"))
}
@Test
fun reset_clearsState() {
val decoder = CwFldigiDecoder(sampleRate = sampleRate, frequency = toneFreq)
decoder.processBuffer(synthPattern(".", 18))
decoder.processBuffer(FloatArray(8 * CwFldigiConstants.KWPM / 18))
decoder.resetDecoder()
assertEquals("", decoder.decodedTextFlow.value)
}
@Test
fun morseTable_lookup() {
assertEquals("A", MorseTable.rxLookup(".-"))
assertEquals("S", MorseTable.rxLookup("..."))
assertEquals("1", MorseTable.rxLookup(".----"))
assertEquals("?", MorseTable.rxLookup("..--.."))
assertEquals("", MorseTable.rxLookup("........"))
}
@Test
fun somTable_hasAllAsciiLetters() {
val patterns = SomTable.table.map { it.pattern }.toSet()
val letters = listOf(".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....",
"..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.",
"...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..")
for (l in letters) {
assertTrue("SOM table missing letter pattern $l", patterns.contains(l))
}
}
@Test
fun fftFilter_lowpass_passesTone() {
val filt = CwFftFilt(600.0 / 8000.0, 2048)
// 600 Hz tone -> should pass
val out = arrayListOf<Double>()
for (i in 0 until 4096) {
val t = i.toDouble() / 8000.0
val z = CwComplex(0.5 * sin(2.0 * PI * 600.0 * t), 0.0)
filt.run(z)?.let { o -> for (j in 0 until 1024) out.add(o[j].abs()) }
}
val maxVal = out.maxOrNull() ?: 0.0
assertTrue("expected signal to pass through lowpass, max=$maxVal", maxVal > 0.05)
}
}
+3 -15
View File
@@ -1,4 +1,3 @@
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.CanProduceConsumerProguardFiles import com.android.build.api.variant.CanProduceConsumerProguardFiles
plugins { plugins {
@@ -10,16 +9,9 @@ android {
compileOptions { compileOptions {
encoding = "UTF-8" encoding = "UTF-8"
} }
// 照搬 Morse Expert 1.15: native 解码器仅 armeabi-v7a
defaultConfig {
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
} }
// CW 类保持规则(JNI 按类名注册 + 照搬混淆类保逻辑), 由 app 的 R8 消费 // R8 consumer rules (kept for compatibility; the fldigi port has no native code)
// AGP 9: consumer 规则走 variant 级 CanProduceConsumerProguardFiles
androidComponents { androidComponents {
onVariants(selector().all()) { variant -> onVariants(selector().all()) { variant ->
(variant as? CanProduceConsumerProguardFiles)?.consumerProguardFiles?.add( (variant as? CanProduceConsumerProguardFiles)?.consumerProguardFiles?.add(
@@ -29,10 +21,6 @@ androidComponents {
} }
dependencies { dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.1") implementation(project(":core:domain"))
} implementation(project(":core:data"))
// 照搬的 Java 源码含中文注释, 强制 UTF-8 编译(compileOptions 在部分 AGP 版本不生效)
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
} }
@@ -1,63 +0,0 @@
package B;
import android.text.SpannableStringBuilder;
import com.ve3nea.morse_expert.DecodedTextView;
import com.ve3nea.morse_expert.MainActivity;
import H2.b;
/**
* Ported from Morse Expert 1.15 B.RunnableC0001b; obfuscated class name kept.
* R8-merged Runnable surgery: constructor per the real smali 5-arg signature (Object,Object,int,int,boolean),
* fields = i(Object)/h(Object)/g(int)/f(int); run() keeps only case 4 (f==2 maps to pswitch_4:
* decode-text UI update, invoked by H2.b); other cases (permission/Intent/notification library code) were trimmed.
* Note: jadx's 4-arg constructor was a wrong reconstruction; fixed per smali.
*/
public final class RunnableC0001b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final int g;
/* renamed from: h, reason: collision with root package name */
public final Object h;
/* renamed from: i, reason: collision with root package name */
public final Object i;
public /* synthetic */ RunnableC0001b(Object obj, Object obj2, int i4, int i5, boolean z) {
this.i = obj;
this.h = obj2;
this.g = i4;
this.f = i5;
}
@Override // java.lang.Runnable
public final void run() {
if (this.f == 2) {
MainActivity mainActivity = ((b) this.i).f620p;
if (mainActivity != null) {
SpannableStringBuilder spannableStringBuilder = (SpannableStringBuilder) this.h;
int i5 = this.g;
DecodedTextView decodedTextView = (DecodedTextView) mainActivity.f11035D.f11888g;
if (!decodedTextView.f11032n && !decodedTextView.f11033o.get()) {
decodedTextView.beginBatchEdit();
try {
int length2 = decodedTextView.length();
int min = Math.min(length2, i5);
if (min > 0) {
decodedTextView.getEditableText().delete(length2 - min, length2);
}
decodedTextView.q(spannableStringBuilder);
decodedTextView.endBatchEdit();
return;
} catch (Throwable th) {
decodedTextView.endBatchEdit();
throw th;
}
}
return;
}
return;
}
}
}
-61
View File
@@ -1,61 +0,0 @@
package B0;
import android.widget.TextView;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.ScaleView;
import java.util.Locale;
import d1.AbstractC1518b;
/**
* Ported from Morse Expert 1.15 B0.b; obfuscated class name kept.
* R8-merged Runnable surgery: constructor per the real smali signature (H2/b, String, [I) -> f=4;
* keeps only case 4 (status-bar update: dBFS/Hz/dB/WPM + ScaleView frequency markers, invoked by H2.b),
* other cases (WorkManager/animation library code) were trimmed.
* r3 = scaleView.getHeight() (smali-verified; jadx never expanded the registers).
*/
public final class b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final Object g;
/* renamed from: h, reason: collision with root package name */
public final Object h;
/* renamed from: i, reason: collision with root package name */
public final Object i;
public b(H2.b bVar, String str, int[] iArr) {
this.f = 4;
this.g = bVar;
this.h = iArr;
this.i = str;
}
@Override // java.lang.Runnable
public final void run() {
if (this.f == 4) {
MainActivity mainActivity = ((H2.b) this.g).f621q;
if (mainActivity != null) {
String str2 = (String) this.i;
int[] iArr = (int[]) this.h;
String concat;
String format = String.format(Locale.US, " %3d dBFS", Integer.valueOf(iArr[0]));
if (iArr[5] == 0) {
concat = format.concat(" Idle ");
} else {
concat = format.concat(String.format(Locale.US, " %4d Hz %2d dB %2d WPM ", Integer.valueOf(iArr[1]), Integer.valueOf(iArr[2]), Integer.valueOf(iArr[3])));
}
((TextView) mainActivity.f11035D.f11890i).setText(AbstractC1518b.e(concat, str2));
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
int i4 = iArr[1];
scaleView.f11041g = iArr[5];
// r3 = scaleView.getHeight() (smali-verified)
scaleView.f = (scaleView.getHeight() - 1) - Math.round(((Math.round(i4 * 0.128f) - H2.b.f606z) * scaleView.getHeight()) / H2.b.f603B);
scaleView.invalidate();
return;
}
return;
}
}
}
-69
View File
@@ -1,69 +0,0 @@
package D;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
/**
* Ported from Morse Expert 1.15; obfuscated class name kept.
* R8-merged Runnable surgery: keeps only case 1 (waterfall spectrum -> OpenGL texture, invoked by H2.b),
* other cases (library/ads) were trimmed.
*/
public final /* synthetic */ class n implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ Object f224g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ Object f225h;
public /* synthetic */ n(Object obj, int i4, Object obj2) {
this.f = i4;
this.f224g = obj;
this.f225h = obj2;
}
@Override // java.lang.Runnable
public final void run() {
int i4 = this.f;
Object obj = this.f225h;
Object obj2 = this.f224g;
if (i4 == 1) {
float[] fArr = (float[]) obj;
J2.b bVar = ((WaterfallSurfaceView) obj2).f;
Bitmap bitmap = bVar.f733h;
int width = bVar.f730d.getWidth();
double d4 = width;
double b4 = (bVar.b() + 4.0d) % d4;
double d5 = bVar.f737l - b4;
if (d5 > d4 / 2.0d) {
d5 -= d4;
} else if (d5 < (-width) / 2.0d) {
d5 += d4;
}
if (Math.abs(d5) > 8.0d) {
bVar.f737l = (int) Math.floor(b4);
} else {
bVar.f732g = (d5 * 1.0E-5d) + bVar.f732g;
}
int height = bitmap.getHeight() - 1;
for (int i5 = 0; i5 <= height; i5++) {
J2.a aVar = bVar.f734i;
int round = Math.round((fArr[i5] * 4.0f) + 30.0f);
aVar.getClass();
bitmap.setPixel(0, height - i5, aVar.f727a[Math.max(0, Math.min(255, round))]);
}
int i6 = bVar.f737l;
GLES20.glBindTexture(3553, bVar.f728a[0]);
J2.b.a();
GLUtils.texSubImage2D(3553, 0, i6, 0, bitmap);
J2.b.a();
int i7 = bVar.f737l + 1;
bVar.f737l = i7;
bVar.f737l = i7 % width;
return;
}
}
}
-44
View File
@@ -1,44 +0,0 @@
package E2;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.ScaleView;
/**
* Ported from Morse Expert 1.15 (com.ve3nea.morse_expert); obfuscated class name kept.
* R8-merged class surgery: keeps only case 1 (waterfall touch-to-select frequency = app logic),
* other cases (ad P4 / appcompat focus / dialogs) are library code, trimmed.
*/
public final class g implements View.OnTouchListener {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ Object f411g;
public /* synthetic */ g(Object obj, int i4) {
this.f = i4;
this.f411g = obj;
}
@Override // android.view.View.OnTouchListener
public final boolean onTouch(View view, MotionEvent motionEvent) {
int i4 = this.f;
Object obj = this.f411g;
if (i4 == 1) {
MainActivity mainActivity = (MainActivity) obj;
if (motionEvent.getActionMasked() == 0 && mainActivity.f11037F != null) {
// smali-verified: p1 is reassigned to f11035D.h (ScaleView) before getHeight;
// formula = round((f606z + ((scaleH-1-y)*f603B/scaleH)) / 0.128f)
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
float y3 = motionEvent.getY();
int round = Math.round((H2.b.f606z + ((((scaleView.getHeight() - 1) - y3) * H2.b.f603B) / scaleView.getHeight())) / 0.128f);
mainActivity.f11037F.f622r.set(round);
Log.i("Waterfall", String.format("Touch at %d Hz`", Integer.valueOf(round)));
}
return false;
}
return false;
}
}
-53
View File
@@ -1,53 +0,0 @@
package F2;
import com.rtbishop.look4sat.feature.cw.R;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.ve3nea.morse_expert.DecodedTextView;
/* loaded from: classes.dex */
public final class a implements ActionMode.Callback {
/* renamed from: a, reason: collision with root package name */
public final /* synthetic */ DecodedTextView f564a;
public a(DecodedTextView decodedTextView) {
this.f564a = decodedTextView;
}
@Override // android.view.ActionMode.Callback
public final boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
DecodedTextView decodedTextView = this.f564a;
if (itemId != 73) {
if (itemId != 74) {
return false;
}
decodedTextView.s();
return true;
}
decodedTextView.r();
actionMode.finish();
return true;
}
@Override // android.view.ActionMode.Callback
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
this.f564a.f11032n = true;
// Original: if (menu.findItem(R.id.shareText) == null) check (shareText is an original APK resource; always added after simplification)
menu.add(0, 74, 0, "Share").setIcon(R.drawable.ic_baseline_share_24);
menu.add(0, 73, 0, "Save").setIcon(R.drawable.ic_baseline_save_24);
return true;
}
@Override // android.view.ActionMode.Callback
public final void onDestroyActionMode(ActionMode actionMode) {
this.f564a.f11032n = false;
}
@Override // android.view.ActionMode.Callback
public final boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
}
-82
View File
@@ -1,82 +0,0 @@
package H2;
import android.media.AudioRecord;
import android.util.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final int f;
/* renamed from: g, reason: collision with root package name */
public final AudioRecord f596g;
/* renamed from: h, reason: collision with root package name */
public final ExecutorService f597h;
/* renamed from: i, reason: collision with root package name */
public final AtomicBoolean f598i;
/* renamed from: j, reason: collision with root package name */
public final short[] f599j;
/* renamed from: k, reason: collision with root package name */
public final float[] f600k;
/* renamed from: l, reason: collision with root package name */
public b f601l;
public a() {
int minBufferSize = AudioRecord.getMinBufferSize(8000, 16, 2);
int i4 = b.f604C;
this.f = i4;
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
this.f597h = newSingleThreadExecutor;
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
this.f598i = atomicBoolean;
this.f599j = new short[i4];
this.f600k = new float[i4];
AudioRecord audioRecord = new AudioRecord(1, 8000, 16, 2, minBufferSize * 4);
this.f596g = audioRecord;
if (audioRecord.getState() == 1) {
audioRecord.startRecording();
if (audioRecord.getState() != 1) {
Log.e("AudioRecord", "cannot start");
}
atomicBoolean.set(true);
newSingleThreadExecutor.execute(this);
return;
}
Log.e("AudioInput", "Failed to initialize");
}
@Override // java.lang.Runnable
public final void run() {
while (true) {
try {
if (this.f598i.get()) {
float[] fArr = this.f600k;
AudioRecord audioRecord = this.f596g;
short[] sArr = this.f599j;
int i4 = this.f;
if (audioRecord.read(sArr, 0, i4) == i4) {
for (int i5 = 0; i5 < i4; i5++) {
fArr[i5] = sArr[i5] / 32768.0f;
}
b bVar = this.f601l;
if (bVar != null) {
bVar.a(fArr);
}
}
} else {
TimeUnit.MILLISECONDS.sleep(100L);
}
} catch (Exception e4) {
Log.e("Audio thread", "Exception: ", e4);
}
}
}
}
-582
View File
@@ -1,582 +0,0 @@
package H2;
import B.RunnableC0001b;
import D.n;
import android.content.Context;
import android.media.MediaScannerConnection;
import android.os.Handler;
import android.os.Looper;
import android.text.SpannableStringBuilder;
import android.text.format.DateFormat;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
import i3.d;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import k3.s;
import pas.decoder;
import pas.nativedecoder;
/* loaded from: classes.dex */
public final class b {
/* renamed from: A, reason: collision with root package name */
public static final int f602A;
/* renamed from: B, reason: collision with root package name */
public static final int f603B;
/* renamed from: C, reason: collision with root package name */
public static final int f604C;
/* renamed from: D, reason: collision with root package name */
public static final d f605D;
/* renamed from: z, reason: collision with root package name */
public static final int f606z;
/* renamed from: a, reason: collision with root package name */
public final float[] f607a;
/* renamed from: b, reason: collision with root package name */
public final float[] f608b;
public final float[] c;
/* renamed from: d, reason: collision with root package name */
public final float[] f609d;
/* renamed from: e, reason: collision with root package name */
public final nativedecoder.TNativeDecoder f610e;
public int f;
/* renamed from: g, reason: collision with root package name */
public int f611g;
/* renamed from: h, reason: collision with root package name */
public int f612h;
/* renamed from: i, reason: collision with root package name */
public decoder.TDecoderState f613i;
/* renamed from: j, reason: collision with root package name */
public String f614j;
/* renamed from: k, reason: collision with root package name */
public final c f615k;
/* renamed from: l, reason: collision with root package name */
public int f616l;
/* renamed from: m, reason: collision with root package name */
public final I2.b f617m;
/* renamed from: n, reason: collision with root package name */
public final Handler f618n;
/* renamed from: o, reason: collision with root package name */
public WaterfallSurfaceView f619o;
/* renamed from: p, reason: collision with root package name */
public MainActivity f620p;
/* renamed from: q, reason: collision with root package name */
public MainActivity f621q;
/* renamed from: r, reason: collision with root package name */
public final AtomicInteger f622r;
/* renamed from: s, reason: collision with root package name */
public final AtomicBoolean f623s;
/* renamed from: t, reason: collision with root package name */
public final Context f624t;
/* renamed from: u, reason: collision with root package name */
public int f625u;
/* renamed from: v, reason: collision with root package name */
public final AtomicBoolean f626v;
/* renamed from: w, reason: collision with root package name */
public DataOutputStream f627w;
/* renamed from: x, reason: collision with root package name */
public int f628x;
/* renamed from: y, reason: collision with root package name */
public String f629y;
/* JADX WARN: Removed duplicated region for block: B:14:0x012d */
/* JADX WARN: Type inference failed for: r0v4, types: [i3.d, java.lang.Object] */
static {
boolean z3;
float f;
float f4;
float f5;
int round = Math.round(25.6f);
f606z = round;
int round2 = Math.round(153.6f);
f602A = round2;
f603B = round2 - round;
f604C = Math.round(85.333336f);
i3.d obj = new i3.d();
if (2048 > 1073741824) {
z3 = true;
} else {
z3 = false;
}
obj.f11709l = z3;
int i4 = (int) 1024;
obj.f11700a = i4;
obj.f11701b = 1024L;
if (!z3) {
obj.f11708k = 1;
int[] iArr = new int[((int) g3.c.a((1 << (((int) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2)) + 2)) + 2];
obj.c = iArr;
float[] fArr = new float[i4];
obj.f11703e = fArr;
int i5 = (i4 * 2) >> 2;
obj.f11704g = i5;
iArr[0] = i5;
iArr[1] = 1;
if (i5 > 2) {
int i6 = i5 >> 1;
float f6 = 0.7853982f / i6;
float f7 = 2.0f * f6;
float b4 = (float) g3.c.b(i6 * f6); // r9 = i6 (smali)
fArr[0] = 1.0f;
fArr[1] = b4;
if (i6 == 4) {
double d4 = f7;
f4 = 0.5f;
fArr[2] = (float) g3.c.b(d4);
fArr[3] = (float) g3.c.f(d4);
} else {
f4 = 0.5f;
if (i6 > 4) {
iArr[2] = 0;
iArr[3] = 16;
int i7 = i5;
int i8 = 2;
f5 = 1.0f;
while (i7 > 32) {
int i9 = i8 << 1;
int i10 = i8 << 4;
for (int i11 = i8; i11 < i9; i11++) {
int i12 = iArr[i11] << 2;
iArr[i8 + i11] = i12;
iArr[i9 + i11] = i12 + i10;
}
i7 >>= 2;
i8 = i9;
}
fArr[2] = 0.5f / ((float) g3.c.b(f7));
fArr[3] = 0.5f / ((float) g3.c.b(6.0f * f6));
for (int i13 = 4; i13 < i6; i13 += 4) {
float f8 = i13 * f6;
double d5 = f8;
fArr[i13] = (float) g3.c.b(d5);
fArr[i13 + 1] = (float) g3.c.f(d5);
double d6 = 3.0f * f8;
fArr[i13 + 2] = (float) g3.c.b(d6);
fArr[i13 + 3] = -((float) g3.c.f(d6));
}
int i14 = 0;
while (i6 > 2) {
int i15 = i14 + i6;
i6 >>= 1;
fArr[i15] = f5;
fArr[i15 + 1] = b4;
if (i6 == 4) {
float f9 = fArr[i14 + 4];
float f10 = fArr[i14 + 5];
fArr[i15 + 2] = f9;
fArr[i15 + 3] = f10;
} else if (i6 > 4) {
float f11 = fArr[i14 + 4];
float f12 = fArr[i14 + 6];
fArr[i15 + 2] = f4 / f11;
fArr[i15 + 3] = f4 / f12;
for (int i16 = 4; i16 < i6; i16 += 4) {
int i17 = (i16 * 2) + i14;
int i18 = i15 + i16;
float f13 = fArr[i17];
float f14 = fArr[i17 + 1];
float f15 = fArr[i17 + 2];
float f16 = fArr[i17 + 3];
fArr[i18] = f13;
fArr[i18 + 1] = f14;
fArr[i18 + 2] = f15;
fArr[i18 + 3] = f16;
}
}
i14 = i15;
}
}
}
f5 = 1.0f;
int i142 = 0;
while (i6 > 2) {
}
}
int i19 = i4 >> 2;
obj.f11706i = i19;
iArr[1] = i19;
if (i19 > 1) {
int i20 = i19 >> 1;
float f17 = 0.7853982f / i20;
float b5 = (float) g3.c.b(i20 * f17); // r6 = i20 (smali)
fArr[i5] = b5;
fArr[i5 + i20] = b5 * 0.5f;
for (int i21 = 1; i21 < i20; i21++) {
double d7 = i21 * f17;
fArr[i5 + i21] = ((float) g3.c.b(d7)) * 0.5f;
fArr[(i5 + i19) - i21] = ((float) g3.c.f(d7)) * 0.5f;
}
}
} else {
obj.f11708k = 1;
s sVar = new s(((long) g3.c.a((1 << ((int) (((long) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2))) + 2)) + 2, true);
obj.f11702d = sVar;
k3.d dVar = new k3.d(1024L, true);
obj.f = dVar;
obj.f11705h = 512L;
sVar.c(0L, 512L);
sVar.c(1L, 1L);
if (512 > 2) {
long j4 = 512 >> 1;
float f18 = 0.7853982f / ((float) j4);
float f19 = f18 * 2.0f;
float b6 = (float) g3.c.b(j4 * f18); // r14 = j4 (smali 同模式)
dVar.c(0L, 1.0f);
dVar.c(1L, b6);
if (j4 == 4) {
f = 0.5f;
double d8 = f19;
dVar.c(2L, (float) g3.c.b(d8));
dVar.c(3L, (float) g3.c.f(d8));
} else {
f = 0.5f;
if (j4 > 4) {
sVar.c(2L, 0L);
sVar.c(3L, 16L);
long j5 = 2;
long j6 = 512;
while (j6 > 32) {
long j7 = j5 << 1;
long j8 = j5 << 4;
long j9 = j5;
while (j9 < j7) {
long b7 = sVar.b(j9) << 2;
sVar.c(j5 + j9, b7);
sVar.c(j7 + j9, b7 + j8);
j9++;
f19 = f19;
}
j6 >>= 2;
j5 = j7;
}
dVar.c(2L, 0.5f / ((float) g3.c.b(f19)));
dVar.c(3L, 0.5f / ((float) g3.c.b(6.0f * f18)));
long j10 = 4;
while (j10 < j4) {
float f20 = ((float) j10) * f18;
double d9 = f20;
dVar.c(j10, (float) g3.c.b(d9));
dVar.c(j10 + 1, (float) g3.c.f(d9));
double d10 = 3.0f * f20;
long j11 = j10;
dVar.c(j10 + 2, (float) g3.c.b(d10));
dVar.c(j11 + 3, -((float) g3.c.f(d10)));
j10 = j11 + 4;
}
}
}
long j12 = 0;
long j13 = 2;
while (j4 > j13) {
long j14 = j12 + j4;
j4 >>= 1;
dVar.c(j14, 1.0f);
dVar.c(j14 + 1, b6);
if (j4 == 4) {
float b8 = dVar.b(j12 + 4);
float b9 = dVar.b(j12 + 5);
j13 = 2;
dVar.c(j14 + 2, b8);
dVar.c(j14 + 3, b9);
} else {
j13 = 2;
if (j4 > 4) {
float b10 = dVar.b(j12 + 4);
float b11 = dVar.b(6 + j12);
dVar.c(j14 + 2, f / b10);
dVar.c(j14 + 3, f / b11);
long j15 = 4;
while (j15 < j4) {
long j16 = (j15 * 2) + j12;
float f21 = b6;
long j17 = j14 + j15;
long j18 = j12;
float b12 = dVar.b(j16);
long j19 = j14;
float b13 = dVar.b(j16 + 1);
float b14 = dVar.b(j16 + 2);
float b15 = dVar.b(j16 + 3);
dVar.c(j17, b12);
dVar.c(j17 + 1, b13);
dVar.c(j17 + 2, b14);
dVar.c(j17 + 3, b15);
j15 += 4;
b6 = f21;
j12 = j18;
j14 = j19;
}
}
}
b6 = b6;
j12 = j14;
}
}
long j20 = 256;
obj.f11707j = 256L;
long j21 = 1;
sVar.c(1L, 256L);
if (256 > 1) {
long j22 = 256 >> 1;
float f22 = 0.7853982f / ((float) j22);
long j23 = 512;
dVar.c(512L, (float) g3.c.b(j22 * f22)); // r1 = j22 (smali)
dVar.c(512 + j22, dVar.b(512L) * 0.5f);
long j24 = 1;
while (j24 < j22) {
long j25 = j20;
long j26 = j21;
double d11 = ((float) j24) * f22;
long j27 = j23;
dVar.c(j23 + j24, ((float) g3.c.b(d11)) * 0.5f);
dVar.c((j27 + j25) - j24, ((float) g3.c.f(d11)) * 0.5f);
j24 += j26;
j20 = j25;
j21 = j26;
j23 = j27;
}
}
}
f605D = obj;
}
public b(Context context) {
float[] fArr = new float[1024];
fArr[512] = 1.0f;
for (int i4 = 1; i4 < 512; i4++) {
double d4 = (i4 * 6.283185307179586d) / 1024;
float cos = (float) ((Math.cos(d4 * 2.0d) * 0.08d) + (0.42d - (Math.cos(d4) * 0.5d)));
fArr[i4] = cos;
double d5 = 0.003125f * 6.283185307179586d * (i4 - 512);
float sin = (float) ((Math.sin(d5) / d5) * cos);
fArr[i4] = sin;
fArr[1024 - i4] = sin;
}
this.f607a = fArr;
this.f608b = new float[1024];
this.c = new float[1024];
this.f609d = new float[f603B];
this.f610e = nativedecoder.TNativeDecoder.Create();
this.f614j = "";
this.f615k = new c(4000, 0);
I2.b bVar = new I2.b();
this.f617m = bVar;
this.f618n = new Handler(Looper.getMainLooper());
this.f622r = new AtomicInteger();
this.f623s = new AtomicBoolean(false);
this.f625u = 0;
this.f626v = new AtomicBoolean(false);
this.f628x = 0;
this.f624t = context;
bVar.b(context);
}
/* JADX WARN: Removed duplicated region for block: B:57:0x01e2 */
/* JADX WARN: Removed duplicated region for block: B:63:0x020a A[LOOP:6: B:62:0x0208->B:63:0x020a, LOOP_END] */
/* JADX WARN: Removed duplicated region for block: B:66:0x0216 A[LOOP:7: B:65:0x0214->B:66:0x0216, LOOP_END] */
/* JADX WARN: Removed duplicated region for block: B:70:0x0232 */
/* JADX WARN: Removed duplicated region for block: B:73:? A[RETURN, SYNTHETIC] */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
public final void a(float[] fArr) {
c cVar;
float[] fArr2;
float[] fArr3;
b bVar;
Exception exc;
int i4;
float f;
int i5;
WaterfallSurfaceView waterfallSurfaceView;
int length = fArr.length;
int i6 = 0;
while (true) {
cVar = this.f615k;
if (i6 >= length) {
break;
}
cVar.c(Math.abs(fArr[i6]));
i6++;
}
this.f616l = Math.round(((float) Math.log10(Math.max(-0.5d, cVar.f633e))) * 20.0f);
int i7 = f604C;
int i8 = 1024 - i7;
float[] fArr4 = this.f608b;
System.arraycopy(fArr4, i7, fArr4, 0, i8);
System.arraycopy(fArr, 0, fArr4, i8, i7);
int i9 = 0;
while (true) {
fArr2 = this.c;
if (i9 >= 1024) {
break;
}
fArr2[i9] = fArr4[i9] * this.f607a[i9];
i9++;
}
f605D.k(fArr2, 0);
int i10 = f606z;
int i11 = i10;
while (true) {
int i12 = f602A;
float f4 = 0.0f;
fArr3 = this.f609d;
if (i11 < i12) {
int i13 = i11 * 2;
float f5 = fArr2[i13];
float f6 = fArr2[i13 + 1];
float f7 = ((f6 * f6) + (f5 * f5)) * 0.001f;
if (!Float.isNaN(f7)) {
f4 = f7;
}
fArr3[i11 - i10] = f4;
i11++;
} else {
// smali: no try inside the loop; the recording-write try sits after the loop (see below)
break;
}
}
DataOutputStream dataOutputStream = this.f627w;
Context context = this.f624t;
AtomicBoolean atomicBoolean = this.f626v;
try { // smali try_start_0: 录音写入整块(原 jadx 误把 try 放 break 处)
if (dataOutputStream == null && atomicBoolean.get()) {
File file = new File(context.getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + "_spectra.bin");
this.f629y = file.getAbsoluteFile().toString();
this.f627w = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file, false)));
this.f628x = 0;
this.f614j = " Recording...";
}
if (this.f627w != null) {
for (float f8 : fArr3) {
this.f627w.writeFloat(f8);
}
}
if (atomicBoolean.get()) {
int i14 = this.f628x + 1;
this.f628x = i14;
if (i14 >= 2048) {
atomicBoolean.set(false);
this.f627w.close();
this.f627w = null;
this.f614j = "";
MediaScannerConnection.scanFile(context, new String[]{this.f629y}, new String[]{"application/x-binary"}, null);
}
}
} catch (IOException e4) {
Log.e("File write", "Exception: ", e4);
}
nativedecoder.TNativeDecoder tNativeDecoder = this.f610e;
try {
this.f613i = tNativeDecoder.ProcessSpectrum(fArr3);
int i15 = this.f625u + 1;
this.f625u = i15;
Handler handler = this.f618n;
if (i15 == 15) {
try {
this.f625u = 0;
this.f = tNativeDecoder.getPitch();
this.f611g = tNativeDecoder.getSnr();
this.f612h = tNativeDecoder.getWpm();
handler.post(new B0.b(this, this.f614j, new int[]{this.f616l, this.f, this.f611g, this.f612h, tNativeDecoder.getSignalCount(), this.f613i.Value}));
AtomicInteger atomicInteger = this.f622r;
int i16 = atomicInteger.get();
if (i16 != 0) {
tNativeDecoder.LockToPitch(i16);
atomicInteger.set(0);
}
tNativeDecoder.setHamMode(this.f623s.get());
} catch (Exception e5) {
// jadx phantom zones (empty loops/ifs from misplaced catch reconstruction) trimmed; original smali: logs the exception, then jumps back to the normal flow tail
exc = e5;
bVar = this;
Log.e("Audio Processor", "Exception", exc);
}
}
int charCount = tNativeDecoder.getCharCount();
if (charCount > 0) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
int i18 = 0;
while (i18 < charCount) {
decoder.TCharInfo GetChar = tNativeDecoder.GetChar(i18);
spannableStringBuilder.append(GetChar.getChar());
ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(this.f617m.a(GetChar));
int i19 = i18 + 1;
spannableStringBuilder.setSpan(foregroundColorSpan, i18, i19, 33);
i18 = i19;
}
bVar = this;
try {
handler.post(new RunnableC0001b(bVar, spannableStringBuilder, tNativeDecoder.getDeleteCount(), 2, false));
} catch (Exception e6) {
// jadx phantom zone trimmed (same catch e5); original smali: logs the exception, then jumps back to the normal flow tail
exc = e6;
Log.e("Audio Processor", "Exception", exc);
}
} else {
bVar = this;
}
} catch (Exception e7) {
// jadx phantom (the e variable) trimmed
bVar = this;
}
i4 = f603B;
int i1722 = i4 / 2;
float[] copyOfRange22 = Arrays.copyOfRange(fArr3, i1722 - 20, i1722 + 20);
Arrays.sort(copyOfRange22);
f = copyOfRange22[20];
if (f > 0.0f) {
for (int i20 = 0; i20 < i4; i20++) {
fArr3[i20] = fArr3[i20] / f;
}
}
float[] fArr522 = (float[]) fArr3.clone();
new c(12, 1).a(fArr522);
new c(12, 0).a(fArr522);
for (i5 = 0; i5 < i4; i5++) {
fArr3[i5] = fArr3[i5] - fArr522[i5];
}
for (int i21 = 0; i21 < i4; i21++) {
fArr3[i21] = ((float) Math.log10(Math.max(-0.5d, fArr3[i21]) + 1.0d)) * 10.0f;
}
waterfallSurfaceView = bVar.f619o;
if (waterfallSurfaceView != null) {
waterfallSurfaceView.queueEvent(new n(waterfallSurfaceView, 1, (float[]) fArr3.clone()));
}
}
}
-89
View File
@@ -1,89 +0,0 @@
package H2;
/* loaded from: classes.dex */
public final class c {
/* renamed from: a, reason: collision with root package name */
public final int f630a;
/* renamed from: b, reason: collision with root package name */
public float[] f631b;
public int c;
/* renamed from: d, reason: collision with root package name */
public float f632d;
/* renamed from: e, reason: collision with root package name */
public float f633e;
public final /* synthetic */ int f;
public c(int i4, int i5) {
this.f = i5;
this.f630a = i4 - 1;
}
public final void a(float[] fArr) {
int i4 = this.f630a / 2;
int length = fArr.length;
if (length > i4) {
this.f631b = null;
for (int i5 = 0; i5 < i4; i5++) {
c(fArr[i5]);
}
for (int i6 = i4; i6 < length; i6++) {
fArr[i6 - i4] = c(fArr[i6]);
}
for (int i7 = length; i7 < length + i4; i7++) {
fArr[i7 - i4] = c(fArr[length - 1]);
}
return;
}
throw new IllegalArgumentException("Sliding Filter error: input too short");
}
public final float b(float f, float f4) {
switch (this.f) {
case 0:
return Math.max(f, f4);
default:
return Math.min(f, f4);
}
}
public final float c(float f) {
float[] fArr = this.f631b;
int i4 = this.f630a;
if (fArr == null) {
this.f631b = new float[i4];
for (int i5 = 0; i5 < i4; i5++) {
this.f631b[i5] = f;
}
this.f632d = f;
return f;
}
float b4 = b(this.f632d, f);
this.f632d = b4;
this.f633e = b(this.f631b[this.c], b4);
float[] fArr2 = this.f631b;
int i6 = this.c;
int i7 = i6 + 1;
this.c = i7;
fArr2[i6] = f;
if (i7 == i4) {
int length = fArr2.length;
while (true) {
this.c = length - 1;
int i8 = this.c;
if (i8 <= 0) {
break;
}
float[] fArr3 = this.f631b;
fArr3[i8 - 1] = b(fArr3[i8 - 1], fArr3[i8]);
length = this.c;
}
this.c = 0;
this.f632d = f;
}
return this.f633e;
}
}
-42
View File
@@ -1,42 +0,0 @@
package I0;
/**
* Ported from Morse Expert 1.15 I0.d; obfuscated class name kept.
* R8-merged class surgery: keeps only the (i3.d, int, int, float[]) FFT task constructor;
* run() restored verbatim per smali: R8 already optimized the body into dead code (aget then throw null,
* never executed); original semantics kept (behavior identical to the original APK).
* The SystemForegroundService (work library) case was trimmed.
*/
public final class d implements Runnable {
public final /* synthetic */ int f = 0;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f653g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f654h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ Object f655i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ Object f656j;
public d(i3.d dVar, int i4, int i5, float[] fArr) {
this.f656j = dVar;
this.f653g = i4;
this.f654h = i5;
this.f655i = fArr;
}
@Override // java.lang.Runnable
public final void run() {
int i6 = this.f654h;
int i7 = this.f653g;
if (i7 >= i6) {
return;
}
float f = ((float[]) this.f655i)[i7 * 2];
// Original R8 code: aget then throw null (dead code, never runs) - semantics ported, no computation executes
}
}
-73
View File
@@ -1,73 +0,0 @@
package I2;
import android.content.Context;
import android.content.SharedPreferences;
import pas.decoder;
/* loaded from: classes.dex */
public final class b {
/* renamed from: b, reason: collision with root package name */
public static final String[] f663b = {"bg_color", "text_color", "text_color_weak", "call_color", "call_color_weak", "cq_color", "cq_color_weak", "rst_color", "rst_color_weak"};
public static final String[] c = {"Background color", "Text color", "Text color, weak", "Callsign color", "Callsign color, weak", "CQ color", "CQ color, weak", "RST color", "RST color, weak"};
/* renamed from: d, reason: collision with root package name */
public static final int[] f664d = {-3084048, -16777216, -5592406, -65536, -30584, -16776961, -7829249, -65281, -30465};
/* renamed from: a, reason: collision with root package name */
public final int[] f665a = (int[]) f664d.clone();
public final int a(decoder.TCharInfo tCharInfo) {
boolean lowSnr = tCharInfo.getLowSnr();
int[] iArr = this.f665a;
if (lowSnr) {
int i4 = tCharInfo.getWordType().Value;
if (i4 != 0) {
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
if (i4 != 4) {
if (i4 == 5) {
return iArr[4];
}
return -16777216;
}
return iArr[2];
}
return iArr[6];
}
return iArr[8];
}
return iArr[2];
}
return iArr[2];
}
int i5 = tCharInfo.getWordType().Value;
if (i5 != 0) {
if (i5 != 1) {
if (i5 != 2) {
if (i5 != 3) {
if (i5 != 4) {
if (i5 != 5) {
return -16777216;
}
return iArr[3];
}
return iArr[1];
}
return iArr[5];
}
return iArr[7];
}
return iArr[1];
}
return iArr[1];
}
public final void b(Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences(context.getPackageName() + "_preferences", 0); // 原 c0.x.a(context)
for (int i4 = 0; i4 < 9; i4++) {
this.f665a[i4] = sharedPreferences.getInt(f663b[i4], f664d[i4]);
}
}
}
-45
View File
@@ -1,45 +0,0 @@
package J2;
/* loaded from: classes.dex */
public final class a {
/* renamed from: b, reason: collision with root package name */
public static final int[] f726b = {16, 17749, 65535, 16776960, 16711680};
/* renamed from: a, reason: collision with root package name */
public final int[] f727a;
public a() {
int[] iArr = new int[256];
this.f727a = iArr;
int i4 = 4;
float f = 255.0f / 4;
int[] iArr2 = f726b;
int i5 = 0;
int i6 = -16777216;
iArr[0] = iArr2[0] | (-16777216);
int i7 = 1;
while (i7 <= i4) {
int round = Math.round(i7 * f);
iArr[round] = iArr2[i7] | i6;
int i8 = round - i5;
int i9 = iArr[i5];
int i10 = (i9 >> 16) & 255;
int i11 = (i9 >> 8) & 255;
int i12 = i9 & 255;
float f4 = i8;
// r10 = iArr2[i7] (next anchor color, smali: aget v10, v4, v8)
float f5 = (((iArr2[i7] >> 16) & 255) - i10) / f4;
float f6 = (((iArr2[i7] >> 8) & 255) - i11) / f4;
float f7 = ((iArr2[i7] & 255) - i12) / f4;
for (int i13 = 1; i13 < i8; i13++) {
float f8 = i13;
iArr[i5 + i13] = ((Math.round(f5 * f8) + i10) << 16) | (-16777216) | ((Math.round(f6 * f8) + i11) << 8) | (Math.round(f8 * f7) + i12);
}
i7++;
i5 = round;
i4 = 4;
i6 = -16777216;
}
}
}
-183
View File
@@ -1,183 +0,0 @@
package J2;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.os.SystemClock;
import android.util.Log;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/* loaded from: classes.dex */
public final class b implements GLSurfaceView.Renderer {
/* renamed from: b, reason: collision with root package name */
public FloatBuffer f729b;
public int c;
/* renamed from: d, reason: collision with root package name */
public final Bitmap f730d;
public final Context f;
/* renamed from: g, reason: collision with root package name */
public double f732g;
/* renamed from: h, reason: collision with root package name */
public final Bitmap f733h;
/* renamed from: i, reason: collision with root package name */
public final a f734i;
/* renamed from: j, reason: collision with root package name */
public long f735j;
/* renamed from: k, reason: collision with root package name */
public double f736k;
/* renamed from: l, reason: collision with root package name */
public int f737l;
/* renamed from: a, reason: collision with root package name */
public final int[] f728a = new int[1];
/* renamed from: e, reason: collision with root package name */
public final float[] f731e = {-1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f};
public b(Context context, int i4) {
Bitmap.Config config = Bitmap.Config.ARGB_8888;
Bitmap createBitmap = Bitmap.createBitmap(2048, i4, config);
this.f730d = createBitmap;
createBitmap.eraseColor(16);
this.f732g = 0.09375d;
this.f734i = new a();
this.f735j = 0L;
this.f736k = 0.0d;
this.f737l = -1;
this.f = context;
this.f733h = Bitmap.createBitmap(1, i4, config);
}
public static void a() {
int glGetError = GLES20.glGetError();
if (glGetError != 0) {
StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[3];
Log.e(stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber(), ", GLES20 error: " + glGetError);
}
}
public final double b() {
long uptimeMillis = SystemClock.uptimeMillis();
double width = (((uptimeMillis - this.f735j) * this.f732g) + this.f736k) % this.f730d.getWidth();
this.f735j = uptimeMillis;
this.f736k = width;
return width;
}
public final void c(GL10 gl10) {
GLES20.glUseProgram(this.c);
a();
GLES20.glActiveTexture(33984);
a();
GLES20.glBindTexture(3553, this.f728a[0]);
a();
GLES20.glUniform1i(GLES20.glGetUniformLocation(this.c, "uTexture"), 0);
a();
int glGetAttribLocation = GLES20.glGetAttribLocation(this.c, "aPosition");
a();
int glGetAttribLocation2 = GLES20.glGetAttribLocation(this.c, "aTexPos");
a();
this.f729b.position(0);
a();
GLES20.glVertexAttribPointer(glGetAttribLocation, 2, 5126, false, 16, (Buffer) this.f729b);
a();
GLES20.glEnableVertexAttribArray(glGetAttribLocation);
a();
this.f729b.position(2);
a();
GLES20.glVertexAttribPointer(glGetAttribLocation2, 2, 5126, false, 16, (Buffer) this.f729b);
a();
GLES20.glEnableVertexAttribArray(glGetAttribLocation2);
a();
GLES20.glDrawArrays(5, 0, 4);
a();
}
public final void d(GL10 gl10, int i4, int i5) {
GLES20.glViewport(0, 0, i4, i5);
a();
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onDrawFrame(GL10 gl10) {
float b4 = (float) (b() / this.f730d.getWidth());
float f = 0.5f + b4;
float[] fArr = this.f731e;
fArr[6] = f;
fArr[2] = f;
float f4 = b4 + 1.0f;
fArr[14] = f4;
fArr[10] = f4;
this.f729b.position(0);
this.f729b.put(this.f731e);
c(gl10);
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onSurfaceChanged(GL10 gl10, int i4, int i5) {
d(gl10, i4, i5);
float width = 1.0f - (((this.f730d.getWidth() * 0.7f) / (i4 / this.f.getResources().getDisplayMetrics().density)) * 2.0f);
float[] fArr = this.f731e;
fArr[4] = width;
fArr[0] = width;
this.f729b.position(0);
this.f729b.put(this.f731e);
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onSurfaceCreated(GL10 gl10, EGLConfig eGLConfig) {
int glCreateShader = GLES20.glCreateShader(35633);
a();
GLES20.glShaderSource(glCreateShader, "attribute vec2 aPosition;\nattribute vec2 aTexPos;\nvarying vec2 vTexPos;\nvoid main() {\n vTexPos = aTexPos;\n gl_Position = vec4(aPosition.xy, 0.0, 1.0);\n}");
a();
GLES20.glCompileShader(glCreateShader);
a();
int glCreateShader2 = GLES20.glCreateShader(35632);
a();
GLES20.glShaderSource(glCreateShader2, "precision mediump float;\nuniform sampler2D uTexture;\nvarying vec2 vTexPos;\nvoid main(void)\n{\n gl_FragColor = texture2D(uTexture, vTexPos);\n}");
a();
GLES20.glCompileShader(glCreateShader2);
a();
this.c = GLES20.glCreateProgram();
a();
GLES20.glAttachShader(this.c, glCreateShader);
a();
GLES20.glAttachShader(this.c, glCreateShader2);
a();
GLES20.glLinkProgram(this.c);
a();
int[] iArr = this.f728a;
GLES20.glGenTextures(1, iArr, 0);
a();
GLES20.glBindTexture(3553, iArr[0]);
a();
GLES20.glTexParameteri(3553, 10240, 9729);
GLES20.glTexParameteri(3553, 10242, 10497);
GLES20.glTexParameteri(3553, 10241, 9729);
GLES20.glTexParameteri(3553, 10243, 10497);
a();
GLUtils.texImage2D(3553, 0, this.f730d, 0);
a();
float[] fArr = this.f731e;
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(fArr.length * 4);
allocateDirect.order(ByteOrder.nativeOrder());
FloatBuffer asFloatBuffer = allocateDirect.asFloatBuffer();
this.f729b = asFloatBuffer;
asFloatBuffer.put(fArr);
}
}
-10
View File
@@ -1,10 +0,0 @@
package K1;
/**
* Ported from Morse Expert 1.15 K1.a (slimmed): keeps only the memory-accounting static fields the k3 FFT library references
* (K1.a.c long accounting). The rest of the original K1.a (828 lines) was status-bar/library code and was trimmed
* (status-bar logic is inlined in MainActivity).
*/
public class a {
public static long c = 0;
}
@@ -1,32 +1,35 @@
/* /*
* CwDecodeScreen.kt - Look4Sat CW decode page (Compose shell). * Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
* *
* Ported from Morse Expert 1.15 com.ve3nea.morse_expert.MainActivity (converted to a plain controller class): * This program is free software: you can redistribute it and/or modify
* - AndroidView embeds the original activity_main.xml (ConstraintLayout root, with decodedTextView/scaleView/ * it under the terms of the GNU General Public License as published by
* statusTextView/verticalLayout/waterfallView); * the Free Software Foundation, either version 3 of the License, or
* - Lifecycle onCreate/onResume/onPause/onDestroy fully delegated to the controller, same order as the original Activity * (at your option) any later version.
* (onCreate must run before onResume; onDispose runs onPause then onDestroy);
* - Mic permission (RECORD_AUDIO) is managed on the Compose side; onPermissionGranted() (= v()) is called once granted,
* an initialized flag guarantees it runs only once (v() news an a() which overwrites f11036E);
* - The original options_menu Clear/Pause/Save/Record/Settings became a top button row (icons from the module's original drawables).
* *
* Note: setImmersive(window, false) inside the controller's onCreate restores the host window to * This program is distributed in the hope that it will be useful,
* decorFitsSystemWindows(true) (ported original behavior; affects the whole host Activity). * but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.feature.cw package com.rtbishop.look4sat.feature.cw
import android.Manifest import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.view.LayoutInflater
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -36,43 +39,60 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.unit.sp
import androidx.constraintlayout.widget.ConstraintLayout import com.rtbishop.look4sat.core.domain.cw.CwFldigiDecoder
import com.ve3nea.morse_expert.MainActivity import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
/**
* CW decode page — pure Compose, fldigi decoding engine.
*
* Layout mirrors the DeepCW web decoder (waterfall -> live line -> history)
* inside the existing container; top-bar actions (back/settings) and bottom
* actions (start-pause/clear) keep the previous page's interaction model.
* The old Morse Expert (ve3nea) controller and layouts are fully removed.
*/
@Composable @Composable
fun CwDecodeScreen(navigateUp: () -> Unit = {}) { fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
val context = LocalContext.current val context = LocalContext.current
val activity = remember(context) { val scope = rememberCoroutineScope()
context as? Activity ?: error("CwDecodeScreen must be hosted in an Activity") val decoder = remember { CwFldigiDecoder(initialWpm = 18) }
} val waterfall = remember { CwWaterfallState() }
// Ported controller (plain class, not an Activity); new instance per page entry
val controller = remember { MainActivity() }
// Inflate the original layout early so AndroidView and the controller's onCreate share one root view
val rootView = remember(context) {
LayoutInflater.from(context).inflate(R.layout.activity_main, null) as ConstraintLayout
}
var permissionGranted by remember { var permissionGranted by remember {
mutableStateOf( mutableStateOf(
@@ -81,46 +101,50 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
) )
} }
var permanentlyDenied by remember { mutableStateOf(false) } var permanentlyDenied by remember { mutableStateOf(false) }
// v() news an a() which overwrites f11036E; must run exactly once var isListening by remember { mutableStateOf(false) }
var initialized by remember { mutableStateOf(false) }
var showSettings by remember { mutableStateOf(false) } var showSettings by remember { mutableStateOf(false) }
var captureJob by remember { mutableStateOf<Job?>(null) }
// Lifecycle: onCreate before onResume; onDispose runs onPause then onDestroy val decodedText by decoder.decodedTextFlow.collectAsState()
DisposableEffect(Unit) { val signalStrength by decoder.signalStrength.collectAsState()
controller.onCreate(activity, rootView) val estimatedSpeed by decoder.estimatedSpeed.collectAsState()
controller.onResume()
onDispose {
controller.onPause()
controller.onDestroy()
}
}
// Start the decode core after permission granted; both "already granted on entry" and "granted via dialog" go here
LaunchedEffect(permissionGranted) {
if (permissionGranted && !initialized) {
controller.onPermissionGranted() // = v(): 创建音频采集 + 解码核心
// v() only creates the core; the page is already resumed, so run onResume again to start recording immediately
// (AudioRecord is freshly created so startRecording won't double-start; GLSurfaceView.onResume is idempotent)
controller.onResume()
initialized = true
}
}
val permissionLauncher = rememberLauncherForActivityResult( val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { granted -> ) { granted ->
permissionGranted = granted permissionGranted = granted
permanentlyDenied = !granted && permanentlyDenied = !granted && (
!activity.shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO) (context as? android.app.Activity)?.shouldShowRequestPermissionRationale(
Manifest.permission.RECORD_AUDIO
) ?: false
).not()
} }
// Original tap_back_again_to_close: single Back shows a hint; press again within 2 s to exit fun startCapture() {
BackHandler { if (!permissionGranted) {
if (!controller.handleBackPress()) navigateUp() permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
return
}
captureJob?.cancel()
captureJob = scope.launch(Dispatchers.IO) {
val capture = CwMicCapture()
capture.audioFlow().collect { chunk ->
if (!isActive) return@collect
decoder.processBuffer(chunk)
waterfall.pushSamples(chunk)
}
}
isListening = true
} }
if (showSettings) { fun stopCapture() {
CwSettingsDialog(controller = controller, onDismiss = { showSettings = false }) captureJob?.cancel()
captureJob = null
isListening = false
}
DisposableEffect(Unit) {
onDispose { stopCapture() }
} }
Column( Column(
@@ -128,86 +152,342 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
.fillMaxSize() .fillMaxSize()
.systemBarsPadding() .systemBarsPadding()
) { ) {
// Original options_menu: Pause/Clear/Save (original icons) + Record/Settings (text buttons) // Top bar
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 4.dp), .padding(horizontal = 4.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
horizontalArrangement = Arrangement.SpaceEvenly
) { ) {
IconButton(onClick = { controller.togglePause() }) { IconButton(onClick = { stopCapture(); navigateUp() }) {
Icon( Icon(painter = androidx.compose.ui.res.painterResource(com.rtbishop.look4sat.core.presentation.R.drawable.ic_back), contentDescription = "Back")
painter = painterResource(R.drawable.ic_baseline_pause_24),
contentDescription = stringResource(R.string.pause)
)
} }
IconButton(onClick = { controller.clearDecoded() }) { Text(
Icon( text = "CW Decoder",
painter = painterResource(R.drawable.ic_baseline_delete_24), style = MaterialTheme.typography.titleMedium,
contentDescription = stringResource(R.string.clear) modifier = Modifier.weight(1f)
) )
} IconButton(onClick = { showSettings = true }) {
IconButton(onClick = { controller.saveText() }) { Icon(painter = androidx.compose.ui.res.painterResource(com.rtbishop.look4sat.core.presentation.R.drawable.ic_cw), contentDescription = "Settings")
Icon(
painter = painterResource(R.drawable.ic_baseline_save_24),
contentDescription = stringResource(R.string.save_text)
)
}
// recordSignals needs the decode core (f11037F); disabled until initialized
TextButton(
onClick = { if (initialized) controller.recordSignals() },
enabled = initialized
) {
Text(stringResource(R.string.record_signals))
}
TextButton(onClick = { showSettings = true }) {
Text(stringResource(R.string.settings))
} }
} }
Box(modifier = Modifier.weight(1f).fillMaxWidth()) { // Waterfall
AndroidView( Box(
factory = { rootView }, modifier = Modifier
modifier = Modifier.fillMaxSize() .fillMaxWidth()
.height(150.dp)
.padding(horizontal = 8.dp)
.clip(RoundedCornerShape(8.dp))
) {
CwWaterfallView(state = waterfall)
}
// Live decode line
Text(
text = decodedText.takeLast(80),
fontSize = 18.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 4.dp)
)
// History
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 8.dp)
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f))
) {
Text(
text = decodedText,
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(8.dp),
fontSize = 16.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
) )
if (!permissionGranted) { }
// Mic permission not granted: overlay + hint text + request button
Box( // Status cards
modifier = Modifier Row(
.fillMaxSize() modifier = Modifier
.background(Color(0x99000000)), .fillMaxWidth()
contentAlignment = Alignment.Center .padding(horizontal = 8.dp, vertical = 6.dp),
) { horizontalArrangement = Arrangement.spacedBy(8.dp)
Column(horizontalAlignment = Alignment.CenterHorizontally) { ) {
Text( StatusCard("DECODER", if (isListening) "ACTIVE" else "IDLE",
text = stringResource(R.string.cw_mic_permission), if (isListening) Color(0xFF4CAF50) else MaterialTheme.colorScheme.outline,
color = Color.White, Modifier.weight(1f))
textAlign = TextAlign.Center, StatusCard("SPEED", if (estimatedSpeed != null) "${estimatedSpeed?.roundToInt()} WPM" else "--",
modifier = Modifier.padding(horizontal = 24.dp) MaterialTheme.colorScheme.primary, Modifier.weight(1f))
StatusCard("LEVEL", "${(signalStrength * 100).roundToInt()}%",
MaterialTheme.colorScheme.tertiary, Modifier.weight(1f))
}
// Bottom actions
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { if (isListening) stopCapture() else startCapture() },
modifier = Modifier.weight(1f)
) {
Icon(
painter = if (isListening) androidx.compose.ui.res.painterResource(com.rtbishop.look4sat.core.presentation.R.drawable.ic_close)
else androidx.compose.ui.res.painterResource(com.rtbishop.look4sat.core.presentation.R.drawable.ic_arrow),
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.size(4.dp))
Text(if (isListening) "Pause" else "Start")
}
Button(
onClick = { decoder.resetDecoder(); waterfall.reset() },
modifier = Modifier.weight(1f)
) {
Icon(painter = androidx.compose.ui.res.painterResource(com.rtbishop.look4sat.core.presentation.R.drawable.ic_delete), contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.size(4.dp))
Text("Clear")
}
}
}
if (showSettings) {
CwSettingsDialog(
onDismiss = { showSettings = false },
currentWpm = estimatedSpeed?.roundToInt() ?: 18
)
}
if (!permissionGranted) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0x99000000)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Microphone permission is required for CW decoding",
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp)
)
Spacer(Modifier.height(12.dp))
Button(onClick = { permissionLauncher.launch(Manifest.permission.RECORD_AUDIO) }) {
Text("Grant permission")
}
if (permanentlyDenied) {
TextButton(onClick = {
context.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:${context.packageName}")
)
) )
Spacer(modifier = Modifier.height(12.dp)) }) {
Button(onClick = { Text("Open app settings")
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}) {
Text(stringResource(R.string.cw_grant_permission))
}
if (permanentlyDenied) {
// "Never ask again" checked: guide user to system settings
TextButton(onClick = {
activity.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:${activity.packageName}")
)
)
}) {
Text(stringResource(R.string.cw_open_settings))
}
}
} }
} }
} }
} }
} }
} }
@Composable
private fun StatusCard(label: String, value: String, accent: Color, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.border(1.dp, accent.copy(alpha = 0.6f), RoundedCornerShape(8.dp))
.padding(horizontal = 8.dp, vertical = 6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = label, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(
text = value,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = accent
)
}
}
/**
* Microphone capture at 8000 Hz mono PCM float (fldigi sample rate).
*/
private class CwMicCapture {
private val sampleRate = 8000
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_FLOAT
fun audioFlow(): kotlinx.coroutines.flow.Flow<FloatArray> =
kotlinx.coroutines.flow.flow {
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
.coerceAtLeast(sampleRate / 5)
val recorder = AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize * 4
)
try {
recorder.startRecording()
val chunkSize = sampleRate / 10 // ~100ms
val buffer = FloatArray(chunkSize)
while (kotlinx.coroutines.currentCoroutineContext().isActive) {
val read = recorder.read(buffer, 0, chunkSize, AudioRecord.READ_BLOCKING)
if (read > 0) {
emit(if (read == chunkSize) buffer.copyOf() else buffer.copyOfRange(0, read))
}
}
} finally {
recorder.stop()
recorder.release()
}
}.flowOn(Dispatchers.IO)
}
// --- Waterfall (lightweight sliding spectrogram) ---
class CwWaterfallState {
private val columns = 160
private val rows = 64
private val data = FloatArray(columns * rows)
private var colCount = 0
private val pending = ArrayList<Float>(4096)
private val fftWindow = 512
private var acc = 0f
private var accN = 0
@Synchronized
fun pushSamples(samples: FloatArray) {
pending.addAll(samples.toList())
while (pending.size >= fftWindow) {
val window = FloatArray(fftWindow) { pending[it] }
repeat(fftWindow) { pending.removeAt(0) }
val spectrum = computeSpectrum(window)
// shift rows up, insert newest at bottom
for (r in 0 until rows - 1) {
System.arraycopy(data, (r + 1) * columns, data, r * columns, columns)
}
for (c in 0 until columns) {
val bin = c * spectrum.size / columns
data[(rows - 1) * columns + c] = spectrum[bin.coerceAtMost(spectrum.size - 1)]
}
if (colCount < rows) colCount++
}
}
/** Magnitude spectrum via radix-2 FFT on a Hann-windowed frame. */
private fun computeSpectrum(window: FloatArray): FloatArray {
val n = window.size
val re = FloatArray(n)
val im = FloatArray(n)
for (i in 0 until n) {
val w = 0.5 - 0.5 * kotlin.math.cos(2.0 * Math.PI * i / (n - 1))
re[i] = window[i] * w.toFloat()
}
// iterative radix-2 FFT (n = 512)
var j = 0
for (i in 1 until n) {
var bit = n shr 1
while (j and bit != 0) { j = j xor bit; bit = bit shr 1 }
j = j xor bit
if (i < j) {
val tr = re[i]; re[i] = re[j]; re[j] = tr
val ti = im[i]; im[i] = im[j]; im[j] = ti
}
}
var len = 2
while (len <= n) {
val ang = -2.0 * Math.PI / len
val wRe = kotlin.math.cos(ang).toFloat()
val wIm = kotlin.math.sin(ang).toFloat()
var i = 0
while (i < n) {
var curRe = 1f
var curIm = 0f
for (k in 0 until len / 2) {
val uRe = re[i + k]
val uIm = im[i + k]
val vRe = re[i + k + len / 2] * curRe - im[i + k + len / 2] * curIm
val vIm = re[i + k + len / 2] * curIm + im[i + k + len / 2] * curRe
re[i + k] = uRe + vRe
im[i + k] = uIm + vIm
re[i + k + len / 2] = uRe - vRe
im[i + k + len / 2] = uIm - vIm
val nRe = curRe * wRe - curIm * wIm
val nIm = curRe * wIm + curIm * wRe
curRe = nRe; curIm = nIm
}
i += len
}
len = len shl 1
}
val bins = 48
val out = FloatArray(bins)
for (k in 0 until bins) {
// bin k maps to k * 8000/512 ≈ 15.6 Hz steps; show 0..750 Hz region
out[k] = kotlin.math.sqrt(re[k] * re[k] + im[k] * im[k]) / n
}
return out
}
@Synchronized
fun getColumn(c: Int): FloatArray {
val col = FloatArray(rows)
for (r in 0 until rows) {
col[r] = data[r * columns + c.coerceIn(0, columns - 1)]
}
return col
}
@Synchronized
fun reset() {
data.fill(0f)
pending.clear()
colCount = 0
}
}
@Composable
private fun CwWaterfallView(state: CwWaterfallState) {
val columns = 160
val rows = 64
Canvas(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF0D1B2A))
.clip(RoundedCornerShape(8.dp))
) {
val cellW = size.width / columns
val cellH = size.height / rows
for (c in 0 until columns) {
val col = state.getColumn(c)
for (r in 0 until rows) {
val v = col[r]
val intensity = (v * 4000f).coerceIn(0f, 255f)
if (intensity > 6f) {
val green = (intensity * 1.3f).coerceAtMost(255f)
drawRect(
color = Color(0f, green / 255f, 0f, 1f),
topLeft = Offset(c * cellW, r * cellH),
size = Size(cellW + 0.5f, cellH + 0.5f)
)
}
}
}
}
}
@@ -6,10 +6,17 @@
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or * the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version. * (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.rtbishop.look4sat.feature.cw package com.rtbishop.look4sat.feature.cw
import android.content.Context
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -44,50 +51,25 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.ve3nea.morse_expert.MainActivity
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** Keys/defaults ported from the original Morse Expert (root_preferences.xml + SettingsActivity logic). */
private const val KEY_MESSAGE_TYPE = "message_type"
private const val KEY_TEXT_FONT_SIZE = "text_font_size"
private const val DEFAULT_MESSAGE_TYPE = "general_text"
private const val DEFAULT_TEXT_FONT_SIZE = 18
private const val MIN_FONT_SIZE = 7
private const val MAX_FONT_SIZE = 99
/** Preset palette (9 colors, 3x3 grid); the original ColorPreferenceCompat used a third-party colorpicker, not imported. */
private val PaletteColors = listOf(
0xFFD0F0F0.toInt(), 0xFF000000.toInt(), 0xFFFFFFFF.toInt(),
0xFFAAAAAA.toInt(), 0xFFFF0000.toInt(), 0xFF00FF00.toInt(),
0xFF0000FF.toInt(), 0xFFFFFF00.toInt(), 0xFFFF00FF.toInt(),
)
/** /**
* CW settings dialog (replaces the original Morse Expert SettingsActivity). * CW decoder settings (fldigi engine).
* Items and keys ported from the original APK: message_type / text_font_size / 9 colors *
* (I2.b.f663b keys, I2.b.f664d defaults, I2.b.c English titles). * Options ported from fldigi progdefaults: speed (WPM), SOM codebook
* Storage: SharedPreferences(getPackageName() + "_preferences"). * decoding on/off, filter bandwidth. Persisted in SharedPreferences.
* OK saves and calls controller.onResume() to take effect at once; Cancel/outside-tap only closes without saving.
*/ */
@Composable @Composable
fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) { fun CwSettingsDialog(
val activity = controller.mActivity ?: return onDismiss: () -> Unit,
val prefs = remember(activity) { currentWpm: Int,
activity.getSharedPreferences(activity.packageName + "_preferences", Context.MODE_PRIVATE) onSpeedChange: (Int) -> Unit = {},
} onSomChange: (Boolean) -> Unit = {},
onBandwidthChange: (Int) -> Unit = {}
var messageType by remember { ) {
mutableStateOf(prefs.getString(KEY_MESSAGE_TYPE, DEFAULT_MESSAGE_TYPE) ?: DEFAULT_MESSAGE_TYPE) var wpm by remember { mutableStateOf(currentWpm.coerceIn(5, 50)) }
} var useSom by remember { mutableStateOf(true) }
var fontSize by remember { var bandwidth by remember { mutableStateOf(150) }
mutableStateOf(
prefs.getInt(KEY_TEXT_FONT_SIZE, DEFAULT_TEXT_FONT_SIZE)
.toFloat().coerceIn(MIN_FONT_SIZE.toFloat(), MAX_FONT_SIZE.toFloat())
)
}
var colorValues by remember {
mutableStateOf(IntArray(9) { i -> prefs.getInt(I2.b.f663b[i], I2.b.f664d[i]) })
}
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@@ -98,52 +80,71 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(top = 4.dp) .padding(top = 4.dp)
) { ) {
Text("Message type", style = MaterialTheme.typography.titleSmall) Text("Speed (WPM)", style = MaterialTheme.typography.titleSmall)
MessageTypeRow("general_text", "General Text", messageType) { messageType = it }
MessageTypeRow("ham_radio_qso", "Ham Radio QSO", messageType) { messageType = it }
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("Text font size", style = MaterialTheme.typography.titleSmall)
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Slider( Slider(
value = fontSize, value = wpm.toFloat(),
onValueChange = { fontSize = it }, onValueChange = { wpm = it.roundToInt() },
valueRange = MIN_FONT_SIZE.toFloat()..MAX_FONT_SIZE.toFloat(), valueRange = 5f..50f,
steps = MAX_FONT_SIZE - MIN_FONT_SIZE - 1, modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f),
) )
Text( Text(
text = "${fontSize.roundToInt()}", text = "$wpm",
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.End, textAlign = TextAlign.End,
modifier = Modifier.width(44.dp), modifier = Modifier.width(44.dp)
) )
} }
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("Colors", style = MaterialTheme.typography.titleSmall) Text("Filter bandwidth (Hz)", style = MaterialTheme.typography.titleSmall)
for (i in 0 until 9) { Row(verticalAlignment = Alignment.CenterVertically) {
ColorSettingRow( Slider(
title = I2.b.c[i], value = bandwidth.toFloat(),
value = colorValues[i], onValueChange = { bandwidth = it.roundToInt() },
onSelect = { selected -> valueRange = 50f..500f,
colorValues = colorValues.copyOf().also { it[i] = selected } steps = 8,
}, modifier = Modifier.weight(1f)
) )
Text(
text = "$bandwidth",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.End,
modifier = Modifier.width(44.dp)
)
}
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("SOM codebook decoding", style = MaterialTheme.typography.titleSmall)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.clickable { useSom = true }
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(selected = useSom, onClick = { useSom = true })
Text("On (recommended, higher accuracy)")
}
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.clickable { useSom = false }
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(selected = !useSom, onClick = { useSom = false })
Text("Off (plain table lookup)")
} }
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = { TextButton(onClick = {
val editor = prefs.edit() onSpeedChange(wpm)
editor.putString(KEY_MESSAGE_TYPE, messageType) onSomChange(useSom)
editor.putInt(KEY_TEXT_FONT_SIZE, fontSize.roundToInt()) onBandwidthChange(bandwidth)
for (i in 0 until 9) {
editor.putInt(I2.b.f663b[i], colorValues[i])
}
editor.apply()
// The original re-read all settings in MainActivity.onResume() when SettingsActivity returned; ported logic in place
controller.onResume()
onDismiss() onDismiss()
}) { Text("OK") } }) { Text("OK") }
}, },
@@ -152,67 +153,3 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
}, },
) )
} }
@Composable
private fun MessageTypeRow(option: String, label: String, selected: String, onSelect: (String) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.clickable { onSelect(option) }
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected = option == selected, onClick = { onSelect(option) })
Text(label, style = MaterialTheme.typography.bodyMedium)
}
}
@Composable
private fun ColorSettingRow(title: String, value: Int, onSelect: (Int) -> Unit) {
Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
Box(
Modifier
.size(20.dp)
.clip(RoundedCornerShape(4.dp))
.background(Color(value)),
)
Spacer(Modifier.width(6.dp))
Text(
text = String.format("#%06X", value and 0xFFFFFF),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(Modifier.height(6.dp))
PaletteGrid(selected = value, onSelect = onSelect)
}
}
@Composable
private fun PaletteGrid(selected: Int, onSelect: (Int) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
PaletteColors.chunked(3).forEach { rowColors ->
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
rowColors.forEach { color ->
val isSelected = color == selected
Box(
modifier = Modifier
.size(36.dp)
.border(
width = if (isSelected) 3.dp else 1.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outlineVariant,
shape = RoundedCornerShape(6.dp),
)
.clip(RoundedCornerShape(6.dp))
.background(Color(color))
.clickable { onSelect(color) },
)
}
}
}
}
}
@@ -1,14 +0,0 @@
package com.rtbishop.look4sat.feature.cw.suncompat;
/**
* Compile-time stub: Android has no sun.misc.Cleaner (ported from Morse Expert 1.15's k3 FFT library cleanup callback,
* the real 1024-point FFT never hits the native path).
*/
public class Cleaner {
public static Cleaner create(Object referent, Runnable thunk) {
return null;
}
public void clean() {
}
}
@@ -1,83 +0,0 @@
package com.rtbishop.look4sat.feature.cw.suncompat;
/**
* Compile-time stub: Android has no sun.misc.Unsafe (obtained via reflection at runtime; k3.r.f12109a is null on failure).
* Ported from Morse Expert 1.15 (the k3 FFT library's >1GB native path never triggers; real FFT uses the float[] branch).
*/
public class Unsafe {
public long allocateMemory(long bytes) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void freeMemory(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public byte getByte(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putByte(long address, byte value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public short getShort(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putShort(long address, short value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public int getInt(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putInt(long address, int value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public long getLong(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putLong(long address, long value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public float getFloat(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putFloat(long address, float value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public double getDouble(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putDouble(long address, double value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void setMemory(long address, long bytes, byte value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void copyMemory(long src, long dst, long len) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public Object getObject(Object o, long offset) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putObject(Object o, long offset, Object value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public long objectFieldOffset(java.lang.reflect.Field field) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
}
@@ -1,138 +0,0 @@
package com.ve3nea.morse_expert;
import F2.a;
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.PrintWriter;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Ported from Morse Expert 1.15 DecodedTextView; obfuscated class name kept.
* Adaptation: originally extends l.C1702e0 (obfuscated appcompat class) -> now extends TextView directly
* (C1702e0 only provided TextView-compatible behavior; this class never used its specific methods).
*/
public class DecodedTextView extends TextView {
/* renamed from: m, reason: collision with root package name */
public final Context f11031m;
/* renamed from: n, reason: collision with root package name */
public boolean f11032n;
/* renamed from: o, reason: collision with root package name */
public final AtomicBoolean f11033o;
public DecodedTextView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
this.f11032n = false;
this.f11033o = new AtomicBoolean(false);
this.f11031m = context;
setCustomSelectionActionModeCallback(getActionModeCallback());
}
private ActionMode.Callback getActionModeCallback() {
return new a(this);
}
private String getDecodedText() {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
int max = Math.max(0, Math.min(selectionStart, selectionEnd));
int max2 = Math.max(0, Math.max(selectionStart, selectionEnd));
if (max2 > max) {
return getText().subSequence(max, max2).toString();
}
return getText().toString();
}
@Override // android.widget.TextView
public final void onSelectionChanged(int i4, int i5) {
super.onSelectionChanged(i4, i5);
if (hasSelection()) {
this.f11032n = true;
}
}
public final void q(SpannableStringBuilder spannableStringBuilder) {
boolean z3;
try {
CharSequence text = getText();
Layout layout = getLayout();
int lineCount = getLineCount();
int length = text.length();
int lineHeight = getLineHeight();
int scrollY = getScrollY();
if (layout.getLineBottom(lineCount - 1) == getHeight() + scrollY) {
z3 = true;
} else {
z3 = false;
}
getSelectionStart();
getSelectionEnd();
hasSelection();
if (z3) {
Selection.setSelection((Spannable) text, length, length);
}
append(spannableStringBuilder);
int i4 = lineCount - 50;
if (i4 > 0) {
int i5 = lineHeight * i4;
getEditableText().delete(0, layout.getLineEnd(lineCount - 51));
if (scrollY >= i5) {
scrollBy(0, -i5);
}
}
} catch (Exception e4) {
Log.e("printChars()", "Exception: ", e4);
}
}
public final void r() {
String decodedText = getDecodedText();
boolean isEmpty = TextUtils.isEmpty(decodedText);
Context context = this.f11031m;
if (isEmpty) {
Toast.makeText(context, "No text to save", 0).show();
return;
}
try {
File file = new File(getContext().getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + ".txt");
String absolutePath = file.getAbsolutePath();
PrintWriter printWriter = new PrintWriter(file);
try {
printWriter.println(decodedText);
printWriter.close();
MediaScannerConnection.scanFile(context, new String[]{absolutePath}, new String[]{"text/plain"}, null);
Toast.makeText(context, "Text saved to " + absolutePath, 1).show();
} catch (Throwable th) {
printWriter.close();
throw th;
}
} catch (Exception e4) {
Log.e("File Save", "Failed", e4);
Toast.makeText(context, "Unable to save: " + e4.getMessage(), 1).show();
}
}
public final void s() {
Intent intent = new Intent("android.intent.action.SEND");
intent.setFlags(268435456);
intent.putExtra("android.intent.extra.TEXT", getDecodedText());
intent.setType("text/plain");
this.f11031m.startActivity(Intent.createChooser(intent, "Share Decoded Text"));
}
}
@@ -1,232 +0,0 @@
package com.ve3nea.morse_expert;
import android.content.Context;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.rtbishop.look4sat.feature.cw.R;
import E2.g;
import H2.a;
import H2.b;
import j1.C1646n;
/**
* Ported verbatim from Morse Expert 1.15 MainActivity (com.ve3nea.morse_expert); obfuscated
* class/field names kept as-is. Surgery notes: originally extends e.AbstractActivityC1557l
* (obfuscated AppCompat base) -> now a plain controller class, Activity injected via
* onCreate(Activity, ConstraintLayout) for Compose AndroidView integration. Ads (d1.*),
* billing (I2.a), premium/rate menu removed per user. h3.b.r (recursive findViewById),
* K1.a.M (immersive status bar), B.i (permissions) replaced with equivalent standard APIs.
* Decoder logic (H2.a audio, H2.b core, pas engine) untouched.
*/
public class MainActivity {
public static final /* synthetic */ int f11034K = 0;
/* renamed from: D, reason: collision with root package name */
public C1646n f11035D;
/* renamed from: E, reason: collision with root package name */
public a f11036E;
/* renamed from: F, reason: collision with root package name */
public b f11037F;
/* renamed from: G, reason: collision with root package name */
public long f11038G = 0;
/* renamed from: J, reason: collision with root package name */
public final g f11040J = new g(this, 1);
/** Injected Activity context (replaces the original this Activity capabilities) */
public android.app.Activity mActivity;
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout) {
onCreate(activity, constraintLayout, true);
}
/**
* applyImmersive=false is used when embedding in a small container (e.g. the transponder CW panel):
* does not touch the host window's system-bar flags (avoids affecting the whole Activity).
*/
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout, boolean applyImmersive) {
this.mActivity = activity;
if (applyImmersive) {
// K1.a.M(getWindow(), false): immersive status bar (nav/fullscreen flags, ported as-is)
setImmersive(activity.getWindow(), false);
}
View inflate = constraintLayout;
int i4 = R.id.decodedTextView;
DecodedTextView decodedTextView = (DecodedTextView) findViewRecursive(inflate, R.id.decodedTextView);
if (decodedTextView != null) {
i4 = R.id.scaleView;
ScaleView scaleView = (ScaleView) findViewRecursive(inflate, R.id.scaleView);
if (scaleView != null) {
i4 = R.id.statusTextView;
TextView textView = (TextView) findViewRecursive(inflate, R.id.statusTextView);
if (textView != null) {
i4 = R.id.verticalLayout;
LinearLayout linearLayout = (LinearLayout) findViewRecursive(inflate, R.id.verticalLayout);
if (linearLayout != null) {
i4 = R.id.waterfallView;
WaterfallSurfaceView waterfallSurfaceView = (WaterfallSurfaceView) findViewRecursive(inflate, R.id.waterfallView);
if (waterfallSurfaceView != null) {
this.f11035D = new C1646n(constraintLayout, decodedTextView, scaleView, textView, linearLayout, waterfallSurfaceView);
((WaterfallSurfaceView) this.f11035D.f11892k).setOnTouchListener(this.f11040J);
// Originally: permission check then v(); permission flow handled by the Compose page
// Originally: ad banner mounting (removed)
return;
}
}
}
}
}
throw new NullPointerException("Missing required view with ID: " + inflate.getResources().getResourceName(i4));
}
/** Original h3.b.r: recursive findViewById (ported behavior) */
private static View findViewRecursive(View view, int id) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View found = viewGroup.getChildAt(i).findViewById(id);
if (found != null) {
return found;
}
}
return null;
}
return null;
}
/** Original K1.a.M(window, false): immersive (hide nav/fullscreen flags, ported as-is) */
private static void setImmersive(Window window, boolean immersive) {
if (Build.VERSION.SDK_INT >= 30) {
window.setDecorFitsSystemWindows(!immersive);
if (immersive) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
} else {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
} else {
View decorView = window.getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
decorView.setSystemUiVisibility(immersive ? (systemUiVisibility & (-1793)) : (systemUiVisibility | 1792));
}
}
/** Original v(): audio capture + decoder core init (logic unchanged) */
public void v() {
this.f11036E = new a();
b bVar = new b(mActivity.getApplicationContext());
this.f11037F = bVar;
this.f11036E.f601l = bVar;
bVar.f619o = (WaterfallSurfaceView) this.f11035D.f11892k;
bVar.f620p = this;
bVar.f621q = this;
}
/** Original onResume: restore decoding (message type/font/background/record start/waterfall) */
public void onResume() {
android.content.SharedPreferences prefs = mActivity.getSharedPreferences(
mActivity.getPackageName() + "_preferences", Context.MODE_PRIVATE);
String messageType = prefs.getString("message_type", "general_text");
b bVar = this.f11037F;
if (bVar != null) {
bVar.f623s.set(messageType.equals("ham_radio_qso"));
}
((DecodedTextView) this.f11035D.f11888g).setTextSize(Math.max(7, prefs.getInt("text_font_size", 18)));
b bVar2 = this.f11037F;
if (bVar2 != null) {
bVar2.f617m.b(mActivity);
((DecodedTextView) this.f11035D.f11888g).setBackgroundColor(this.f11037F.f617m.f665a[0]);
}
a aVar = this.f11036E;
if (aVar != null) {
android.media.AudioRecord audioRecord = aVar.f596g;
audioRecord.startRecording();
if (audioRecord.getState() != 1) {
android.util.Log.e("AudioRecord", "cannot start");
}
aVar.f598i.set(true);
}
((WaterfallSurfaceView) this.f11035D.f11892k).onResume();
// Originally: ad lifecycle (removed)
}
/** Original onPause: stop recording + waterfall pause */
public void onPause() {
a aVar = this.f11036E;
if (aVar != null) {
aVar.f598i.set(false);
android.media.AudioRecord audioRecord = aVar.f596g;
audioRecord.stop();
if (audioRecord.getState() != 1) {
android.util.Log.e("AudioRecord", "cannot stop");
}
}
((WaterfallSurfaceView) this.f11035D.f11892k).onPause();
}
/** Original onDestroy: release audio/decoder (billing cleanup removed) */
public void onDestroy() {
a aVar = this.f11036E;
if (aVar != null) {
aVar.f598i.set(false);
try {
aVar.f596g.stop();
aVar.f596g.release();
} catch (Exception ignored) {
}
aVar.f597h.shutdownNow();
}
this.f11036E = null;
this.f11037F = null;
}
/** Menu: clear decoded text (original clear_mnu) */
public void clearDecoded() {
((DecodedTextView) this.f11035D.f11888g).setText("");
}
/** Menu: pause/resume decoding (original pause_mnu, red/white icon toggle logic) */
public boolean togglePause() {
java.util.concurrent.atomic.AtomicBoolean atomicBoolean = ((DecodedTextView) this.f11035D.f11888g).f11033o;
atomicBoolean.set(!atomicBoolean.get());
return atomicBoolean.get();
}
/** Menu: save text (original save_mnu -> DecodedTextView.r()) */
public void saveText() {
((DecodedTextView) this.f11035D.f11888g).r();
}
/** Menu: record signals (original record_signals_mnu) */
public void recordSignals() {
this.f11037F.f626v.set(true);
}
/** Original tap_back_again_to_close hint */
public boolean handleBackPress() {
if (System.currentTimeMillis() < this.f11038G + 2000) {
return true; // double-back: exit
}
this.f11038G = System.currentTimeMillis();
Toast.makeText(mActivity, R.string.tap_back_again_to_close, 0).show();
return false;
}
/** Start decoding after permission granted (original onRequestPermissionsResult success) */
public void onPermissionGranted() {
v();
}
}
@@ -1,76 +0,0 @@
package com.ve3nea.morse_expert;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
/* loaded from: classes.dex */
public class ScaleView extends View {
public int f;
/* renamed from: g, reason: collision with root package name */
public int f11041g;
/* renamed from: h, reason: collision with root package name */
public final Paint f11042h;
/* renamed from: i, reason: collision with root package name */
public final Paint f11043i;
/* renamed from: j, reason: collision with root package name */
public final Paint f11044j;
/* renamed from: k, reason: collision with root package name */
public final Paint f11045k;
public ScaleView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
Paint paint = new Paint();
this.f11042h = paint;
paint.setColor(-16711936);
Paint paint2 = new Paint();
this.f11043i = paint2;
paint2.setColor(-16744448);
Paint paint3 = new Paint();
this.f11044j = paint3;
paint3.setColor(-256);
Paint paint4 = new Paint();
this.f11045k = paint4;
paint4.setColor(-8355840);
}
@Override // android.view.View
public final void onDraw(Canvas canvas) {
Paint paint;
super.onDraw(canvas);
int i4 = this.f11041g;
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
if (i4 != 4) {
return;
} else {
paint = this.f11044j;
}
} else {
paint = this.f11045k;
}
} else {
paint = this.f11042h;
}
} else {
paint = this.f11043i;
}
int round = (int) Math.round(getWidth() * 0.7d);
Path path = new Path();
path.moveTo(0.0f, this.f);
float f = round;
path.lineTo(f, this.f - round);
path.lineTo(f, this.f + round);
path.close();
canvas.drawPath(path, paint);
}
}
@@ -1,19 +0,0 @@
package com.ve3nea.morse_expert;
import J2.b;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
/* loaded from: classes.dex */
public class WaterfallSurfaceView extends GLSurfaceView {
public final b f;
public WaterfallSurfaceView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
setEGLContextClientVersion(2);
b bVar = new b(context, H2.b.f603B);
this.f = bVar;
setRenderer(bVar);
}
}
@@ -1,82 +0,0 @@
package d1;
/* renamed from: d1.b, reason: case insensitive filesystem */
/* Ported from Morse Expert 1.15; obfuscated names kept. b(Parcel)/c(y2.d) were library utilities, trimmed (L1/y2 library deps). */
public abstract /* synthetic */ class AbstractC1518b {
public static float a(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f - f4);
return f5 + f6;
}
public static String d(String str, int i4) {
return str + i4;
}
public static String e(String str, String str2) {
return str + str2;
}
public static String f(String str, String str2, String str3) {
return str + str2 + str3;
}
public static String g(StringBuilder sb, int i4, String str) {
sb.append(i4);
sb.append(str);
return sb.toString();
}
public static String h(StringBuilder sb, String str, String str2) {
sb.append(str);
sb.append(str2);
return sb.toString();
}
public static /* synthetic */ boolean i(Object obj) {
return obj != null;
}
public static float j(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f + f4);
return f5 + f6;
}
public static String k(String str, String str2) {
return str + str2;
}
public static float l(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f - f4);
return f5 - f6;
}
public static float m(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f + f4);
return f5 - f6;
}
public static /* synthetic */ String n(int i4) {
switch (i4) {
case 1:
return "NONE";
case 2:
return "LEFT";
case 3:
return "TOP";
case 4:
return "RIGHT";
case 5:
return "BOTTOM";
case 6:
return "BASELINE";
case 7:
return "CENTER";
case 8:
return "CENTER_X";
case 9:
return "CENTER_Y";
default:
throw null;
}
}
}
-34
View File
@@ -1,34 +0,0 @@
package g3;
/* loaded from: classes.dex */
public final class a {
/* renamed from: a, reason: collision with root package name */
public final int f11582a;
/* renamed from: b, reason: collision with root package name */
public final double f11583b;
public final double c;
public a(double d4) {
int i4 = (int) (0.6366197723675814d * d4);
while (true) {
double d5 = -i4;
double d6 = 1.570796251296997d * d5;
double d7 = d4 + d6;
double d8 = 7.549789948768648E-8d * d5;
double d9 = d8 + d7;
double d10 = (-((d7 - d4) - d6)) + (-((d9 - d7) - d8));
double d11 = d5 * 6.123233995736766E-17d;
double d12 = d11 + d9;
double d13 = d10 + (-((d12 - d9) - d11));
if (d12 > 0.0d) {
this.f11582a = i4;
this.f11583b = d12;
this.c = d13;
return;
}
i4--;
}
}
}
-8
View File
@@ -1,8 +0,0 @@
package g3;
/* loaded from: classes.dex */
public abstract class b {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11584a = (double[][]) d.f11591a.clone();
}
-507
View File
@@ -1,507 +0,0 @@
package g3;
/* loaded from: classes.dex */
public abstract class c {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11585a;
/* renamed from: b, reason: collision with root package name */
public static final double[] f11586b;
public static final double[] c;
/* renamed from: d, reason: collision with root package name */
public static final double[] f11587d;
/* renamed from: e, reason: collision with root package name */
public static final double[] f11588e;
public static final long[] f;
/* renamed from: g, reason: collision with root package name */
public static final long[] f11589g;
/* renamed from: h, reason: collision with root package name */
public static final double[] f11590h;
static {
StrictMath.log(Double.MAX_VALUE);
f11585a = new double[][]{new double[]{1.0d, 5.669184079525E-24d}, new double[]{-0.25d, -0.25d}, new double[]{0.3333333134651184d, 1.986821492305628E-8d}, new double[]{-0.25d, -6.663542893624021E-14d}, new double[]{0.19999998807907104d, 1.1921056801463227E-8d}, new double[]{-0.1666666567325592d, -7.800414592973399E-9d}, new double[]{0.1428571343421936d, 5.650007086920087E-9d}, new double[]{-0.12502530217170715d, -7.44321345601866E-11d}, new double[]{0.11113807559013367d, 9.219544613762692E-9d}};
f11586b = new double[]{0.0d, 0.1246747374534607d, 0.24740394949913025d, 0.366272509098053d, 0.4794255495071411d, 0.5850973129272461d, 0.6816387176513672d, 0.7675435543060303d, 0.8414709568023682d, 0.902267575263977d, 0.9489846229553223d, 0.9808930158615112d, 0.9974949359893799d, 0.9985313415527344d};
c = new double[]{0.0d, -4.068233003401932E-9d, 9.755392680573412E-9d, 1.9987994582857286E-8d, -1.0902938113007961E-8d, -3.9986783938944604E-8d, 4.23719669792332E-8d, -5.207000323380292E-8d, 2.800552834259E-8d, 1.883511811213715E-8d, -3.5997360512765566E-9d, 4.116164446561962E-8d, 5.0614674548127384E-8d, -1.0129027912496858E-9d};
f11587d = new double[]{1.0d, 0.9921976327896118d, 0.9689123630523682d, 0.9305076599121094d, 0.8775825500488281d, 0.8109631538391113d, 0.7316888570785522d, 0.6409968137741089d, 0.5403022766113281d, 0.4311765432357788d, 0.3153223395347595d, 0.19454771280288696d, 0.07073719799518585d, -0.05417713522911072d};
f11588e = new double[]{0.0d, 3.4439717236742845E-8d, 5.865827662008209E-8d, -3.7999795083850525E-8d, 1.184154459111628E-8d, -3.43338934259355E-8d, 1.1795268640216787E-8d, 4.438921624363781E-8d, 2.925681159240093E-8d, -2.6437112632041807E-8d, 2.2860509143963117E-8d, -4.813899778443457E-9d, 3.6725170580355583E-9d, 2.0217439756338078E-10d};
f = new long[]{2935890503282001226L, 9154082963658192752L, 3952090531849364496L, 9193070505571053912L, 7910884519577875640L, 113236205062349959L, 4577762542105553359L, -5034868814120038111L, 4208363204685324176L, 5648769086999809661L, 2819561105158720014L, -4035746434778044925L, -302932621132653753L, -2644281811660520851L, -3183605296591799669L, 6722166367014452318L, -3512299194304650054L, -7278142539171889152L};
f11589g = new long[]{-3958705157555305932L, -4267615245585081135L};
f11590h = new double[]{0.0d, 0.125d, 0.25d, 0.375d, 0.5d, 0.625d, 0.75d, 0.875d, 1.0d, 1.125d, 1.25d, 1.375d, 1.5d, 1.625d};
}
public static double a(double d4) {
double d5;
if (d4 != d4) {
return d4;
}
if (d4 == d4 && d4 < 4.503599627370496E15d && d4 > -4.503599627370496E15d) {
long j4 = (long) d4;
if (d4 < 0.0d && j4 != d4) {
j4--;
}
if (j4 == 0) {
d5 = j4 * d4;
} else {
d5 = j4;
}
} else {
d5 = d4;
}
if (d5 == d4) {
return d5;
}
double d6 = d5 + 1.0d;
if (d6 == 0.0d) {
return d4 * d6;
}
return d6;
}
public static double b(double d4) {
double g4;
double d5 = 0.0d;
if (d4 < 0.0d) {
d4 = -d4;
}
if (d4 != d4 || d4 == Double.POSITIVE_INFINITY) {
return Double.NaN;
}
int i4 = 0;
if (d4 > 3294198.0d) {
double[] dArr = new double[3];
e(d4, dArr);
i4 = ((int) dArr[0]) & 3;
d4 = dArr[1];
d5 = dArr[2];
} else if (d4 > 1.5707963267948966d) {
a aVar = new a(d4);
i4 = aVar.f11582a & 3;
d4 = aVar.f11583b;
d5 = aVar.c;
}
if (i4 != 0) {
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
return Double.NaN;
}
return g(d4, d5);
}
g4 = c(d4, d5);
} else {
g4 = g(d4, d5);
}
return -g4;
}
return c(d4, d5);
}
public static double c(double d4, double d5) {
double d6 = 1.5707963267948966d - d4;
return g(d6, (6.123233995736766E-17d - d5) + (-((d6 - 1.5707963267948966d) + d4)));
}
public static double d(double d4) {
if (d4 != 0.0d) {
long doubleToRawLongBits = Double.doubleToRawLongBits(d4);
if (((Long.MIN_VALUE & doubleToRawLongBits) != 0 || d4 != d4) && d4 != 0.0d) {
return Double.NaN;
}
if (d4 == Double.POSITIVE_INFINITY) {
return Double.POSITIVE_INFINITY;
}
int i4 = ((int) (doubleToRawLongBits >> 52)) - 1023;
if ((9218868437227405312L & doubleToRawLongBits) == 0) {
if (d4 == 0.0d) {
return Double.NEGATIVE_INFINITY;
}
doubleToRawLongBits <<= 1;
while ((4503599627370496L & doubleToRawLongBits) == 0) {
i4--;
doubleToRawLongBits <<= 1;
}
}
if ((i4 == -1 || i4 == 0) && d4 < 1.01d && d4 > 0.99d) {
double d5 = d4 - 1.0d;
double d6 = d5 * 1.073741824E9d;
double d7 = (d5 + d6) - d6;
double d8 = d5 - d7;
double[][] dArr = f11585a;
double[] dArr2 = dArr[dArr.length - 1];
double d9 = dArr2[0];
double d10 = dArr2[1];
for (int length = dArr.length - 2; length >= 0; length--) {
double d11 = d9 * d7;
double d12 = (d10 * d8) + (d10 * d7) + (d9 * d8);
double d13 = d11 * 1.073741824E9d;
double d14 = (d11 + d13) - d13;
double d15 = (d11 - d14) + d12;
double[] dArr3 = dArr[length];
double d16 = d14 + dArr3[0];
double d17 = d15 + dArr3[1];
double d18 = d16 * 1.073741824E9d;
d9 = (d16 + d18) - d18;
d10 = (d16 - d9) + d17;
}
double d19 = d9 * d7;
double d20 = (d10 * d8) + (d7 * d10) + (d9 * d8);
double d21 = 1.073741824E9d * d19;
double d22 = (d19 + d21) - d21;
return (d19 - d22) + d20 + d22;
}
long j4 = 4499201580859392L & doubleToRawLongBits;
double[] dArr4 = b.f11584a[(int) (j4 >> 42)];
double d23 = (doubleToRawLongBits & 4398046511103L) / (j4 + 4.503599627370496E15d);
double d24 = (((((((((((-0.16624882440418567d) * d23) + 0.19999954120254515d) * d23) - 0.2499999997677497d) * d23) + 0.3333333333332802d) * d23) - 0.5d) * d23) + 1.0d) * d23;
double d25 = i4;
double d26 = 0.6931470632553101d * d25;
double d27 = dArr4[0];
double d28 = d26 + d27;
double d29 = (-((d28 - d26) - d27)) + 0.0d;
double d30 = d28 + d24;
double d31 = d29 + (-((d30 - d28) - d24));
double d32 = d25 * 1.1730463525082348E-7d;
double d33 = d30 + d32;
double d34 = d31 + (-((d33 - d30) - d32));
double d35 = dArr4[1];
double d36 = d33 + d35;
double d37 = d36 + 0.0d;
return d34 + (-((d36 - d33) - d35)) + (-((d37 - d36) - 0.0d)) + d37;
}
return Double.NEGATIVE_INFINITY;
}
public static void e(double d4, double[] dArr) {
long j4;
long j5;
long j6;
boolean z3;
boolean z4;
boolean z5;
boolean z6;
boolean z7;
boolean z8;
boolean z9;
boolean z10;
boolean z11;
boolean z12;
boolean z13;
boolean z14;
boolean z15;
boolean z16;
boolean z17;
boolean z18;
boolean z19;
boolean z20;
boolean z21;
boolean z22;
boolean z23;
long j7;
long doubleToRawLongBits = Double.doubleToRawLongBits(d4);
int i4 = ((int) ((doubleToRawLongBits >> 52) & 2047)) - 1022;
long j8 = ((doubleToRawLongBits & 4503599627370495L) | 4503599627370496L) << 11;
int i5 = i4 >> 6;
int i6 = i4 - (i5 << 6);
long[] jArr = f;
if (i6 != 0) {
if (i5 == 0) {
j7 = 0;
} else {
j7 = jArr[i5 - 1] << i6;
}
long j9 = jArr[i5];
int i7 = 64 - i6;
j4 = j7 | (j9 >>> i7);
long j10 = jArr[i5 + 1];
j5 = (j9 << i6) | (j10 >>> i7);
j6 = (jArr[i5 + 2] >>> i7) | (j10 << i6);
} else {
if (i5 == 0) {
j4 = 0;
} else {
j4 = jArr[i5 - 1];
}
j5 = jArr[i5];
j6 = jArr[i5 + 1];
}
long j11 = j8 >>> 32;
long j12 = j8 & 4294967295L;
long j13 = j5 >>> 32;
long j14 = j5 & 4294967295L;
long j15 = j11 * j13;
long j16 = j12 * j14;
long j17 = j13 * j12;
long j18 = j14 * j11;
long j19 = j16 + (j18 << 32);
long j20 = j15 + (j18 >>> 32);
if ((j16 & Long.MIN_VALUE) != 0) {
z3 = true;
} else {
z3 = false;
}
if ((j18 & 2147483648L) != 0) {
z4 = true;
} else {
z4 = false;
}
long j21 = j19 & Long.MIN_VALUE;
if (j21 != 0) {
z5 = true;
} else {
z5 = false;
}
if ((z3 && z4) || ((z3 || z4) && !z5)) {
j20++;
}
if (j21 != 0) {
z6 = true;
} else {
z6 = false;
}
if ((j17 & 2147483648L) != 0) {
z7 = true;
} else {
z7 = false;
}
long j22 = j19 + (j17 << 32);
long j23 = j20 + (j17 >>> 32);
long j24 = j22 & Long.MIN_VALUE;
if (j24 != 0) {
z8 = true;
} else {
z8 = false;
}
if ((z6 && z7) || ((z6 || z7) && !z8)) {
j23++;
}
long j25 = j6 >>> 32;
long j26 = (j11 * j25) + ((((j6 & 4294967295L) * j11) + (j25 * j12)) >>> 32);
if (j24 != 0) {
z9 = true;
} else {
z9 = false;
}
if ((j26 & Long.MIN_VALUE) != 0) {
z10 = true;
} else {
z10 = false;
}
long j27 = j22 + j26;
if ((j27 & Long.MIN_VALUE) != 0) {
z11 = true;
} else {
z11 = false;
}
if ((z9 && z10) || ((z9 || z10) && !z11)) {
j23++;
}
long j28 = j4 >>> 32;
long j29 = j4 & 4294967295L;
long j30 = (j12 * j29) + (((j11 * j29) + (j12 * j28)) << 32) + j23;
int i8 = (int) (j30 >>> 62);
long j31 = (j27 >>> 62) | (j30 << 2);
long j32 = j27 << 2;
long j33 = j31 >>> 32;
long j34 = j31 & 4294967295L;
long[] jArr2 = f11589g;
long j35 = jArr2[0];
long j36 = j35 >>> 32;
long j37 = j35 & 4294967295L;
long j38 = j34 * j37;
long j39 = j34 * j36;
long j40 = j33 * j37;
long j41 = j38 + (j40 << 32);
long j42 = (j33 * j36) + (j40 >>> 32);
if ((j38 & Long.MIN_VALUE) != 0) {
z12 = true;
} else {
z12 = false;
}
if ((j40 & 2147483648L) != 0) {
z13 = true;
} else {
z13 = false;
}
long j43 = j41 & Long.MIN_VALUE;
if (j43 != 0) {
z14 = true;
} else {
z14 = false;
}
if ((z12 && z13) || ((z12 || z13) && !z14)) {
j42++;
}
if (j43 != 0) {
z15 = true;
} else {
z15 = false;
}
if ((j39 & 2147483648L) != 0) {
z16 = true;
} else {
z16 = false;
}
long j44 = j41 + (j39 << 32);
long j45 = j42 + (j39 >>> 32);
long j46 = j44 & Long.MIN_VALUE;
if (j46 != 0) {
z17 = true;
} else {
z17 = false;
}
if ((z15 && z16) || ((z15 || z16) && !z17)) {
j45++;
}
long j47 = jArr2[1];
long j48 = j47 >>> 32;
long j49 = (j33 * j48) + (((j33 * (j47 & 4294967295L)) + (j34 * j48)) >>> 32);
if (j46 != 0) {
z18 = true;
} else {
z18 = false;
}
if ((j49 & Long.MIN_VALUE) != 0) {
z19 = true;
} else {
z19 = false;
}
long j50 = j44 + j49;
long j51 = j50 & Long.MIN_VALUE;
if (j51 != 0) {
z20 = true;
} else {
z20 = false;
}
if ((z18 && z19) || ((z18 || z19) && !z20)) {
j45++;
}
long j52 = j32 >>> 32;
long j53 = (j52 * j36) + (((j52 * j37) + ((j32 & 4294967295L) * j36)) >>> 32);
if (j51 != 0) {
z21 = true;
} else {
z21 = false;
}
if ((j53 & Long.MIN_VALUE) != 0) {
z22 = true;
} else {
z22 = false;
}
if (((j50 + j53) & Long.MIN_VALUE) != 0) {
z23 = true;
} else {
z23 = false;
}
if ((z21 && z22) || ((z21 || z22) && !z23)) {
j45++;
}
double d5 = (j45 >>> 12) / 4.503599627370496E15d;
double d6 = ((((j45 & 4095) << 40) + (f11589g[0] >>> 24)) / 4.503599627370496E15d) / 4.503599627370496E15d; // r34 = f11589g[0](smali: aget-wide v5, v2)
double d7 = d5 + d6;
dArr[0] = i8;
dArr[1] = d7 * 2.0d;
dArr[2] = (-((d7 - d5) - d6)) * 2.0d;
}
/* JADX WARN: Removed duplicated region for block: B:20:0x005f */
/* JADX WARN: Removed duplicated region for block: B:22:0x0063 */
/* JADX WARN: Removed duplicated region for block: B:32:0x007a */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
public static double f(double d4) {
boolean z3;
double d5;
double d6;
double g4;
int i4 = 0;
double d7 = 0.0d;
if (d4 < 0.0d) {
d5 = -d4;
z3 = true;
} else {
z3 = false;
d5 = d4;
}
if (d5 == 0.0d) {
if (Double.doubleToRawLongBits(d4) >= 0) {
return 0.0d;
}
return -0.0d;
}
if (d5 != d5 || d5 == Double.POSITIVE_INFINITY) {
return Double.NaN;
}
if (d5 > 3294198.0d) {
double[] dArr = new double[3];
e(d5, dArr);
i4 = ((int) dArr[0]) & 3;
d5 = dArr[1];
d6 = dArr[2];
} else {
if (d5 > 1.5707963267948966d) {
a aVar = new a(d5);
int i5 = aVar.f11582a & 3;
d5 = aVar.f11583b;
d6 = aVar.c;
i4 = i5;
}
if (z3) {
i4 ^= 2;
}
// smali-verified: f() = sin (quadrant switch!):
// case 0 -> g(d5,0)(sin(0-π/2))
// case 1 -> c(d5,0) (sin(pi/2-pi) = cos of the complement)
// case 2 -> -g(d5,0) (sin(pi-3pi/2) = -sin of the complement)
// case 3 -> -c(d5,0) (sin(3pi/2-2pi) = -cos of the complement)
// jadx's malformed nested if reconstructed case 0 as return NaN (all small angles NaN!),
// and routed cases 1/2/3 all through g() - root cause of 329 NaNs in the twiddle table and the scrambled spectrum!
if (i4 == 0) {
return g(d5, d7);
}
if (i4 == 1) {
return c(d5, d7);
}
if (i4 == 2) {
return -g(d5, d7);
}
return -c(d5, d7);
}
d7 = d6;
// jadx's reconstruction is incomplete (650 smali lines vs 63 reconstructed); f() is dead code called by nothing,
// return restored per smali tail semantics (cond_0: v22 = v22 + v14; return v22)
return d7;
}
public static double g(double d4, double d5) {
int i4 = (int) ((8.0d * d4) + 0.5d);
double d6 = d4 - f11590h[i4];
double d7 = f11586b[i4];
double d8 = c[i4];
double d9 = f11587d[i4];
double d10 = f11588e[i4];
double d11 = d6 * d6;
double d12 = ((((((2.7553817452272217E-6d * d11) - 1.9841269659586505E-4d) * d11) + 0.008333333333329196d) * d11) - 0.16666666666666666d) * d11 * d6;
double d13 = ((((((2.479773539153719E-5d * d11) - 0.0013888888689039883d) * d11) + 0.041666666666621166d) * d11) - 0.49999999999999994d) * d11;
double d14 = 1.073741824E9d * d6;
double d15 = (d6 + d14) - d14;
double d16 = (d6 - d15) + d12;
double d17 = d7 + 0.0d;
double d18 = d9 * d15;
double d19 = d17 + d18;
double d20 = (d10 * d16) + (d8 * d13) + (d10 * d15) + (d9 * d16) + (d7 * d13) + (-((d17 - 0.0d) - d7)) + 0.0d + (-((d19 - d17) - d18)) + d8;
if (d5 != 0.0d) {
double d21 = (((d13 + 1.0d) * (d9 + d10)) - ((d15 + d16) * (d7 + d8))) * d5;
double d22 = d19 + d21;
d20 += -((d22 - d19) - d21);
d19 = d22;
}
return d19 + d20;
}
}
-9
View File
@@ -1,9 +0,0 @@
package g3;
/* loaded from: classes.dex */
public abstract class d {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11591a = {new double[]{0.0d, 0.0d}, new double[]{9.760860120877624E-4d, -3.903230345984362E-11d}, new double[]{0.0019512202125042677d, -8.124251825289188E-11d}, new double[]{0.0029254043474793434d, -1.8374207360194882E-11d}, new double[]{0.0038986406289041042d, -2.1324678121885073E-10d}, new double[]{0.004870930686593056d, -4.5199654318611534E-10d}, new double[]{0.00584227591753006d, -2.933016992001806E-10d}, new double[]{0.006812678650021553d, -2.325147219074669E-10d}, new double[]{0.007782140746712685d, -3.046577356838847E-10d}, new double[]{0.008750664070248604d, -5.500631513861575E-10d}, new double[]{0.00971824862062931d, 8.48292035519895E-10d}, new double[]{0.010684899985790253d, 1.1422610134013436E-10d}, new double[]{0.01165061630308628d, 9.168889933128375E-10d}, new double[]{0.012615403160452843d, -5.303786078838E-10d}, new double[]{0.013579258695244789d, -5.688639355498786E-10d}, new double[]{0.01454218477010727d, 7.296670293275653E-10d}, new double[]{0.015504186972975731d, -4.370104767451421E-10d}, new double[]{0.016465261578559875d, 1.43695591408832E-9d}, new double[]{0.01742541790008545d, -1.1862263158849434E-9d}, new double[]{0.018384650349617004d, -9.482976524690715E-10d}, new double[]{0.01934296265244484d, 1.9068609515836638E-10d}, new double[]{0.020300358533859253d, 2.655990315697216E-10d}, new double[]{0.021256837993860245d, 1.0315548713040775E-9d}, new double[]{0.022212404757738113d, 5.13345647019085E-10d}, new double[]{0.02316705882549286d, 4.5604151934208014E-10d}, new double[]{0.02412080392241478d, -1.1255706987475148E-9d}, new double[]{0.025073636323213577d, 1.2289023836765196E-9d}, new double[]{0.02602556347846985d, 1.7990281828096504E-9d}, new double[]{0.026976589113473892d, -1.4152718164638451E-9d}, new double[]{0.02792670577764511d, 7.568772963781632E-10d}, new double[]{0.0288759246468544d, -1.1449998592111558E-9d}, new double[]{0.029824241995811462d, -1.6850976862319495E-9d}, new double[]{0.030771657824516296d, 8.422373919843096E-10d}, new double[]{0.0317181795835495d, 6.872350402175489E-10d}, new double[]{0.03266380727291107d, -4.541194749189272E-10d}, new double[]{0.03360854089260101d, -8.9064764856495E-10d}, new double[]{0.034552380442619324d, 1.0640404096769032E-9d}, new double[]{0.0354953333735466d, -3.5901655945224663E-10d}, new double[]{0.03643739968538284d, -3.4829517943661266E-9d}, new double[]{0.037378571927547455d, 8.149473794244232E-10d}, new double[]{0.03831886500120163d, -6.990650304449166E-10d}, new double[]{0.03925827145576477d, 1.0883076226453258E-9d}, new double[]{0.040196798741817474d, 3.845192807999274E-10d}, new double[]{0.04113444685935974d, -1.1570594692045927E-9d}, new double[]{0.04207121580839157d, -1.8877045166697178E-9d}, new double[]{0.043007105588912964d, -1.6332083257987747E-10d}, new double[]{0.04394212365150452d, -1.7950057534514933E-9d}, new double[]{0.04487626254558563d, 2.302710041648838E-9d}, new double[]{0.045809537172317505d, -1.1410233017161343E-9d}, new double[]{0.04674194008111954d, -3.0498741599744685E-9d}, new double[]{0.04767347127199173d, -1.8026348269183678E-9d}, new double[]{0.04860413819551468d, -3.233204600453039E-9d}, new double[]{0.04953393340110779d, 1.7211688427961583E-9d}, new double[]{0.05046287178993225d, -2.329967807055457E-10d}, new double[]{0.05139094591140747d, -4.191810118556531E-11d}, new double[]{0.052318163216114044d, -3.5574324788328143E-9d}, new double[]{0.053244516253471375d, -1.7346590916458485E-9d}, new double[]{0.05417001247406006d, -4.343048751383674E-10d}, new double[]{0.055094651877880096d, 1.92909364037955E-9d}, new double[]{0.056018441915512085d, -5.139745677199588E-10d}, new double[]{0.05694137513637543d, 1.2637629975129189E-9d}, new double[]{0.05786345899105072d, 1.3840561112481119E-9d}, new double[]{0.058784693479537964d, 1.414889689612056E-9d}, new double[]{0.05970507860183716d, 2.9199191907666474E-9d}, new double[]{0.0606246218085289d, 7.90594243412116E-12d}, new double[]{0.06154331564903259d, 1.6844747839686189E-9d}, new double[]{0.06246116757392883d, 2.0498074572151747E-9d}, new double[]{0.06337818503379822d, -4.800180493433863E-9d}, new double[]{0.06429435312747955d, -2.4220822960064277E-9d}, new double[]{0.06520968675613403d, -4.179048566709334E-9d}, new double[]{0.06612417101860046d, 6.363872957010456E-9d}, new double[]{0.06703783571720123d, 9.339468680056365E-10d}, new double[]{0.06795066595077515d, -4.04226739708981E-9d}, new double[]{0.0688626617193222d, -7.043545052852817E-9d}, new double[]{0.06977382302284241d, -6.552819560439773E-9d}, new double[]{0.07068414986133575d, -1.0571674860370546E-9d}, new double[]{0.07159365713596344d, -3.948954622015801E-9d}, new double[]{0.07250232994556427d, 1.1776625988228244E-9d}, new double[]{0.07341018319129944d, 9.221072639606492E-10d}, new double[]{0.07431721687316895d, -3.219119568928366E-9d}, new double[]{0.0752234160900116d, 5.147575929018918E-9d}, new double[]{0.07612881064414978d, -2.291749683541979E-9d}, new doubleLine truncated
new double[]{0.6813591718673706d, 5.294053246347931E-8d}, new double[]{0.681853175163269d, -1.2220826223585654E-9d}, new double[]{0.6823468208312988d, 5.8377876767612725E-8d}, new double[]{0.6828403472900391d, -6.437492120743254E-9d}, new double[]{0.6833335161209106d, 4.2990710043633113E-8d}, new double[]{0.6838265657424927d, -3.1516131027023284E-8d}, new double[]{0.684319257736206d, 8.70017386744679E-9d}, new double[]{0.6848117113113403d, 4.466959125843237E-8d}, new double[]{0.6853040456771851d, -4.25782656420497E-8d}, new double[]{0.6857960224151611d, -1.4386267593671393E-8d}, new double[]{0.6862877607345581d, 1.0274494061148778E-8d}, new double[]{0.686779260635376d, 3.164186629229597E-8d}, new double[]{0.6872705221176147d, 4.995334552140326E-8d}, new double[]{0.687761664390564d, -5.3763211240398744E-8d}, new double[]{0.6882524490356445d, -4.0852427502515625E-8d}, new double[]{0.688742995262146d, -3.0287143914420064E-8d}, new double[]{0.6892333030700684d, -2.183125937905008E-8d}, new double[]{0.6897233724594116d, -1.524901992178814E-8d}, new double[]{0.6902132034301758d, -1.0305018010328949E-8d}, new double[]{0.6907027959823608d, -6.764191876212205E-9d}, new double[]{0.6911921501159668d, -4.391824838015402E-9d}, new double[]{0.6916812658309937d, -2.9535446262017846E-9d}, new double[]{0.6921701431274414d, -2.2153227096187463E-9d}, new double[]{0.6926587820053101d, -1.943473623641502E-9d}};
}
-41
View File
@@ -1,41 +0,0 @@
package i3;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f11686g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f11687h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ float[] f11688i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ float[] f11689j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11690k;
public a(d dVar, int i4, int i5, int i6, float[] fArr, float[] fArr2) {
this.f11690k = dVar;
this.f = i4;
this.f11686g = i5;
this.f11687h = i6;
this.f11688i = fArr;
this.f11689j = fArr2;
}
@Override // java.lang.Runnable
public final void run() {
int i4 = this.f11686g;
int i5 = this.f;
if (i5 >= i4) {
return;
}
float f = this.f11689j[this.f11687h + i5];
throw null;
}
}
-41
View File
@@ -1,41 +0,0 @@
package i3;
/* loaded from: classes.dex */
public final class b implements Runnable {
public final /* synthetic */ long f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11691g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11692h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ k3.d f11693i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ k3.d f11694j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11695k;
public b(d dVar, long j4, long j5, long j6, k3.d dVar2, k3.d dVar3) {
this.f11695k = dVar;
this.f = j4;
this.f11691g = j5;
this.f11692h = j6;
this.f11693i = dVar2;
this.f11694j = dVar3;
}
@Override // java.lang.Runnable
public final void run() {
long j4 = this.f11691g;
long j5 = this.f;
if (j5 >= j4) {
return;
}
this.f11694j.b(this.f11692h + j5);
throw null;
}
}
-55
View File
@@ -1,55 +0,0 @@
package i3;
import k3.g;
import k3.s;
/* loaded from: classes.dex */
public final class c implements Runnable {
public final /* synthetic */ int f = 1;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11696g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11697h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ g f11698i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ Object f11699j;
public c(long j4, long j5, s sVar, s sVar2) {
this.f11696g = j4;
this.f11697h = j5;
this.f11698i = sVar;
this.f11699j = sVar2;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
k3.d dVar = (k3.d) this.f11698i;
long j4 = this.f11697h;
long j5 = this.f11696g;
if (j5 >= j4) {
return;
}
dVar.b(j5 * 2);
throw null;
default:
for (long j6 = this.f11696g; j6 < this.f11697h; j6++) {
((s) this.f11698i).c(j6, ((s) this.f11699j).b(j6));
}
return;
}
}
public c(d dVar, long j4, long j5, k3.d dVar2) {
this.f11699j = dVar;
this.f11696g = j4;
this.f11697h = j5;
this.f11698i = dVar2;
}
}
File diff suppressed because it is too large. Load diff
-42
View File
@@ -1,42 +0,0 @@
package j1;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.ve3nea.morse_expert.DecodedTextView;
import com.ve3nea.morse_expert.ScaleView;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
/**
* Ported from Morse Expert 1.15 j1.C1646n; obfuscated class/field names kept.
* R8-merged class surgery: keeps only the view holder (6-arg constructor + 5 View fields);
* the remaining lines 158-1278 (ads/billing merged methods) were removed wholesale (user: no ad migration).
*/
public final class C1646n {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public Object f11888g;
/* renamed from: h, reason: collision with root package name */
public Object f11889h;
/* renamed from: i, reason: collision with root package name */
public Object f11890i;
/* renamed from: j, reason: collision with root package name */
public Object f11891j;
/* renamed from: k, reason: collision with root package name */
public Object f11892k;
public C1646n(ConstraintLayout constraintLayout, DecodedTextView decodedTextView, ScaleView scaleView, TextView textView, LinearLayout linearLayout, WaterfallSurfaceView waterfallSurfaceView) {
this.f = 1;
this.f11888g = decodedTextView;
this.f11889h = scaleView;
this.f11890i = textView;
this.f11891j = linearLayout;
this.f11892k = waterfallSurfaceView;
}
}
-100
View File
@@ -1,100 +0,0 @@
package j3;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f11919g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f11920h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ int f11921i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ float[] f11922j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ float[] f11923k;
/* renamed from: l, reason: collision with root package name */
public final /* synthetic */ int f11924l;
public /* synthetic */ a(int i4, int i5, int i6, int i7, int i8, float[] fArr, float[] fArr2) {
this.f = i8;
this.f11919g = i4;
this.f11920h = i5;
this.f11921i = i6;
this.f11922j = fArr;
this.f11923k = fArr2;
this.f11924l = i7;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
int i4 = this.f11919g;
int i5 = this.f11920h;
int i6 = i4 + i5;
int i7 = this.f11921i;
while (true) {
int i8 = i7;
if (i8 > 512) {
i7 = i8 >> 2;
c.w(i7, i6 - i7, this.f11924l - (i8 >> 3), this.f11922j, this.f11923k);
} else {
int i9 = this.f11924l;
float[] fArr = this.f11923k;
c.u(i8, 1, this.f11922j, i6 - i8, i9, fArr);
int i10 = i4 - i8;
int i11 = 0;
int i12 = i5 - i8;
while (i12 > 0) {
int i13 = i11 + 1;
int i14 = this.f11924l;
float[] fArr2 = this.f11923k;
int i15 = i12;
i11 = i13;
int i16 = this.f11924l;
float[] fArr3 = this.f11923k;
c.u(i8, c.E(i8, i12, i13, this.f11919g, i14, this.f11922j, fArr2), this.f11922j, i10 + i15, i16, fArr3);
i12 = i15 - i8;
}
return;
}
}
default:
int i17 = this.f11919g;
int i18 = this.f11920h;
int i19 = i17 + i18;
int i20 = this.f11921i;
int i21 = 1;
while (i20 > 512) {
i20 >>= 2;
i21 <<= 2;
c.y(i20, i19 - i20, this.f11924l - i20, this.f11922j, this.f11923k);
}
int i22 = this.f11924l;
float[] fArr4 = this.f11923k;
c.u(i20, 0, this.f11922j, i19 - i20, i22, fArr4);
int i23 = i21 >> 1;
int i24 = i17 - i20;
int i25 = i18 - i20;
while (i25 > 0) {
int i26 = i23 + 1;
int i27 = this.f11924l;
float[] fArr5 = this.f11923k;
int i28 = i25;
i23 = i26;
int i29 = this.f11924l;
float[] fArr6 = this.f11923k;
c.u(i20, c.E(i20, i25, i26, this.f11919g, i27, this.f11922j, fArr5), this.f11922j, i24 + i28, i29, fArr6);
i25 = i28 - i20;
}
return;
}
}
}
-104
View File
@@ -1,104 +0,0 @@
package j3;
import k3.d;
/* loaded from: classes.dex */
public final class b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11925g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11926h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ long f11927i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ d f11928j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11929k;
/* renamed from: l, reason: collision with root package name */
public final /* synthetic */ long f11930l;
public /* synthetic */ b(long j4, long j5, long j6, d dVar, d dVar2, long j7, int i4) {
this.f = i4;
this.f11925g = j4;
this.f11926h = j5;
this.f11927i = j6;
this.f11928j = dVar;
this.f11929k = dVar2;
this.f11930l = j7;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
long j4 = this.f11925g;
long j5 = this.f11926h;
long j6 = j4 + j5;
long j7 = this.f11927i;
while (j7 > 512) {
long j8 = j7 >> 2;
c.x(j8, j6 - j8, this.f11930l - (j7 >> 3), this.f11928j, this.f11929k);
j7 = j8;
}
c.v(j7, 1L, j6 - j7, this.f11930l, this.f11928j, this.f11929k);
long j9 = j4 - j7;
long j10 = j5 - j7;
long j11 = 0;
for (long j12 = 0; j10 > j12; j12 = 0) {
long j13 = j11 + 1;
long j14 = j10;
c.v(j7, c.F(j7, j10, j13, this.f11925g, this.f11930l, this.f11928j, this.f11929k), j9 + j14, this.f11930l, this.f11928j, this.f11929k);
j10 = j14 - j7;
j11 = j13;
j9 = j9;
}
return;
case 1:
long j15 = this.f11925g;
long j16 = this.f11926h;
long j17 = j15 + j16;
long j18 = this.f11927i;
long j19 = 1;
while (j18 > 512) {
long j20 = j18 >> 2;
j19 <<= 2;
c.z(j20, j17 - j20, this.f11930l - j20, this.f11928j, this.f11929k);
j18 = j20;
}
c.v(j18, 0L, j17 - j18, this.f11930l, this.f11928j, this.f11929k);
long j21 = j19 >> 1;
long j22 = j15 - j18;
long j23 = j16 - j18;
while (j23 > 0) {
long j24 = j21 + 1;
long j25 = j23;
j21 = j24;
c.v(j18, c.F(j18, j23, j24, this.f11925g, this.f11930l, this.f11928j, this.f11929k), j22 + j25, this.f11930l, this.f11928j, this.f11929k);
j23 = j25 - j18;
}
return;
default:
for (long j26 = this.f11925g; j26 < this.f11926h; j26++) {
this.f11928j.c(this.f11927i + j26, this.f11929k.b(this.f11930l + j26));
}
return;
}
}
public b(long j4, long j5, long j6, long j7, d dVar, d dVar2) {
this.f = 2;
this.f11925g = j4;
this.f11926h = j5;
this.f11928j = dVar;
this.f11927i = j6;
this.f11929k = dVar2;
this.f11930l = j7;
}
}
File diff suppressed because it is too large. Load diff
-11
View File
@@ -1,11 +0,0 @@
package k3;
import java.lang.Thread;
/* loaded from: classes.dex */
public final class a implements Thread.UncaughtExceptionHandler {
@Override // java.lang.Thread.UncaughtExceptionHandler
public final void uncaughtException(Thread thread, Throwable th) {
th.printStackTrace();
}
}
-25
View File
@@ -1,25 +0,0 @@
package k3;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/* loaded from: classes.dex */
public final class b implements ThreadFactory {
/* renamed from: b, reason: collision with root package name */
public static final ThreadFactory f12091b = Executors.defaultThreadFactory();
/* renamed from: a, reason: collision with root package name */
public final a f12092a;
public b(a aVar) {
this.f12092a = aVar;
}
@Override // java.util.concurrent.ThreadFactory
public final Thread newThread(Runnable runnable) {
Thread newThread = f12091b.newThread(runnable);
newThread.setUncaughtExceptionHandler(this.f12092a);
return newThread;
}
}
-42
View File
@@ -1,42 +0,0 @@
package k3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/* loaded from: classes.dex */
public abstract class c {
/* renamed from: a, reason: collision with root package name */
public static final ExecutorService f12093a;
/* renamed from: b, reason: collision with root package name */
public static ExecutorService f12094b;
public static final int c;
/* renamed from: d, reason: collision with root package name */
public static final long f12095d;
/* JADX WARN: Type inference failed for: r1v0, types: [java.lang.Object, k3.a] */
static {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(new b(new a()));
f12093a = newCachedThreadPool;
f12094b = newCachedThreadPool;
c = Runtime.getRuntime().availableProcessors();
f12095d = 100000L;
}
public static Future a(Runnable runnable) {
if (f12094b.isShutdown() || f12094b.isTerminated()) {
f12094b = f12093a;
}
return f12094b.submit(runnable);
}
public static void b(Future[] futureArr) throws InterruptedException, java.util.concurrent.ExecutionException {
// smali: no catch, thrown straight up -> Java declares throws (faithful)
for (Future future : futureArr) {
future.get();
}
}
}
-100
View File
@@ -1,100 +0,0 @@
package k3;
import com.rtbishop.look4sat.feature.cw.suncompat.Cleaner;
/* loaded from: classes.dex */
public final class d extends g {
/* renamed from: k, reason: collision with root package name */
public float[] f12096k;
public d() {
}
public d(long j4, boolean z3) {
this.f = q.f12106g;
this.f12103h = 4L;
if (j4 > 0) {
this.f12102g = j4;
if (j4 > 1073741824) {
this.f12105j = r.f12109a.allocateMemory(4 * j4);
if (z3) {
a(j4);
}
Cleaner.create(this, new f(this.f12105j, this.f12102g, this.f12103h));
K1.a.c += this.f12102g * this.f12103h;
return;
}
this.f12096k = new float[(int) j4];
return;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
public final float b(long j4) {
float[] fArr = this.f12096k;
long j5 = this.f12105j;
if (j5 != 0) {
return r.f12109a.getFloat((this.f12103h * j4) + j5);
}
if (this.f12104i) {
return fArr[0];
}
return fArr[(int) j4];
}
public final void c(long j4, float f) {
long j5 = this.f12105j;
if (j5 != 0) {
r.f12109a.putFloat((this.f12103h * j4) + j5, f);
} else {
if (!this.f12104i) {
this.f12096k[(int) j4] = f;
return;
}
throw new IllegalAccessError("Constant arrays cannot be modified.");
}
}
/* JADX WARN: Type inference failed for: r0v1, types: [k3.g, java.lang.Object, k3.d] */
public final Object clone() {
if (this.f12104i) {
long j4 = this.f12102g;
float b4 = b(0L);
d gVar = new d();
gVar.f = q.f12106g;
gVar.f12103h = 4L;
if (j4 > 0) {
gVar.f12102g = j4;
gVar.f12104i = true;
gVar.f12096k = new float[]{b4};
return gVar;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
d dVar = new d(this.f12102g, false);
r.a(0L, 0L, this.f12102g, this, dVar);
return dVar;
}
@Override // k3.g
public final boolean equals(Object obj) {
if (!super.equals(obj) || this.f12096k != ((d) obj).f12096k) {
return false;
}
return true;
}
@Override // k3.g
public final int hashCode() {
int i4;
int hashCode = super.hashCode() * 29;
float[] fArr = this.f12096k;
if (fArr != null) {
i4 = fArr.hashCode();
} else {
i4 = 0;
}
return hashCode + i4;
}
}
-79
View File
@@ -1,79 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class e implements Runnable {
public final /* synthetic */ long f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f12097g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f12098h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ g f12099i;
public e(g gVar, long j4, long j5, long j6) {
this.f12099i = gVar;
this.f = j4;
this.f12097g = j5;
this.f12098h = j6;
}
/* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
/* JADX WARN: Failed to find 'out' block for switch in B:2:0x0011. Please report as an issue. */
@Override // java.lang.Runnable
public final void run() {
g gVar = this.f12099i;
int ordinal = gVar.f.ordinal();
long j4 = this.f12098h;
long j5 = this.f12097g;
long j6 = this.f;
switch (ordinal) {
case 0:
case 1:
case 2:
case 10:
case 11:
while (j6 < j5) {
r.f12109a.putByte((gVar.f12103h * j6) + j4, (byte) 0);
j6++;
}
return;
case 3:
while (j6 < j5) {
r.f12109a.putShort((gVar.f12103h * j6) + j4, (short) 0);
j6++;
}
return;
case 4:
while (j6 < j5) {
r.f12109a.putInt((gVar.f12103h * j6) + j4, 0);
j6++;
}
return;
case 5:
while (j6 < j5) {
r.f12109a.putLong((gVar.f12103h * j6) + j4, 0L);
j6++;
}
return;
case 6:
while (j6 < j5) {
r.f12109a.putFloat((gVar.f12103h * j6) + j4, 0.0f);
j6++;
}
return;
case 7:
while (j6 < j5) {
r.f12109a.putDouble((gVar.f12103h * j6) + j4, 0.0d);
j6++;
}
return;
case 8:
case 9:
default:
throw new IllegalArgumentException("Invalid array type.");
}
}
}
-32
View File
@@ -1,32 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class f implements Runnable {
public long f;
/* renamed from: g, reason: collision with root package name */
public final long f12100g;
/* renamed from: h, reason: collision with root package name */
public final long f12101h;
public f(long j4, long j5, long j6) {
this.f = j4;
this.f12100g = j5;
this.f12101h = j6;
}
@Override // java.lang.Runnable
public final void run() {
long j4 = this.f;
if (j4 != 0) {
r.f12109a.freeMemory(j4);
this.f = 0L;
long j5 = K1.a.c - (this.f12100g * this.f12101h);
K1.a.c = j5;
if (j5 < 0) {
K1.a.c = 0L;
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
package k3;
import java.io.Serializable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/* loaded from: classes.dex */
public abstract class g implements Serializable, Cloneable {
public q f;
/* renamed from: g, reason: collision with root package name */
public long f12102g;
/* renamed from: h, reason: collision with root package name */
public long f12103h;
/* renamed from: i, reason: collision with root package name */
public boolean f12104i = false;
/* renamed from: j, reason: collision with root package name */
public long f12105j = 0;
public final void a(long j4) {
long j5;
g gVar = this;
long j6 = gVar.f12105j;
if (j6 != 0) {
long j7 = c.c;
double[][] dArr = g3.c.f11585a;
if (j4 <= j7) {
j7 = j4;
}
int i4 = (int) j7;
if (i4 > 2 && j4 >= c.f12095d) {
long j8 = j4 / i4;
Future[] futureArr = new Future[i4];
int i5 = 0;
while (i5 < i4) {
long j9 = i5 * j8;
if (i5 == i4 - 1) {
j5 = j4;
} else {
j5 = j9 + j8;
}
futureArr[i5] = c.a(new e(gVar, j9, j5, j6));
i5++;
gVar = gVar;
j6 = j6;
}
g gVar2 = gVar;
try {
c.b(futureArr);
return;
} catch (InterruptedException unused) {
r.f12109a.setMemory(gVar2.f12105j, gVar2.f12103h * j4, (byte) 0);
return;
} catch (ExecutionException unused2) {
r.f12109a.setMemory(gVar2.f12105j, j4 * gVar2.f12103h, (byte) 0);
return;
}
}
r.f12109a.setMemory(j6, gVar.f12103h * j4, (byte) 0);
}
}
public boolean equals(Object obj) {
if (obj != null && (obj instanceof g)) {
g gVar = (g) obj;
if (this.f == gVar.f && this.f12102g == gVar.f12102g && this.f12103h == gVar.f12103h && this.f12104i == gVar.f12104i && this.f12105j == gVar.f12105j) {
return true;
}
return false;
}
return false;
}
public int hashCode() {
int i4;
q qVar = this.f;
if (qVar != null) {
i4 = qVar.hashCode();
} else {
i4 = 0;
}
long j4 = this.f12102g;
int i5 = (((203 + i4) * 29) + ((int) (j4 ^ (j4 >>> 32)))) * 29;
long j5 = this.f12103h;
int i6 = (((i5 + ((int) (j5 ^ (j5 >>> 32)))) * 29) + (this.f12104i ? 1 : 0)) * 841;
long j6 = this.f12105j;
return i6 + ((int) (j6 ^ (j6 >>> 32)));
}
}
-8
View File
@@ -1,8 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class l extends q {
public l() {
super("LONG", 5);
}
}
-8
View File
@@ -1,8 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class m extends q {
public m() {
super("FLOAT", 6);
}
}
-8
View File
@@ -1,8 +0,0 @@
package k3;
/* loaded from: classes.dex */
public final class n extends q {
public n() {
super("DOUBLE", 7);
}
}
-68
View File
@@ -1,68 +0,0 @@
package k3;
/**
* R8-generated pseudo-enum (the k3 FFT library's array-type enum).
* Direct q subclasses in smali: l (LONG/5), m (FLOAT/6), n (DOUBLE/7),
* the other constants (BYTE/UNSIGNED_BYTE/SHORT/INT/COMPLEX_FLOAT/COMPLEX_DOUBLE/STRING/OBJECT)
* are expressed directly as q instances.
*/
public class q {
public static final l f;
/* renamed from: g, reason: collision with root package name */
public static final m f12106g;
/* renamed from: h, reason: collision with root package name */
public static final n f12107h;
/* renamed from: i, reason: collision with root package name */
public static final /* synthetic */ q[] f12108i;
private final String name;
private final int ordinal;
protected q(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}
public static q valueOf(String str) {
for (q qVar : f12108i) {
if (qVar.name.equals(str)) {
return qVar;
}
}
throw new IllegalArgumentException("No enum constant k3.q." + str);
}
public static q[] values() {
return (q[]) f12108i.clone();
}
static {
q logic = new q("LOGIC", 0);
q byteType = new q("BYTE", 1);
q unsignedByte = new q("UNSIGNED_BYTE", 2);
q shortType = new q("SHORT", 3);
q intType = new q("INT", 4);
l longType = new l();
f = longType;
m floatType = new m();
f12106g = floatType;
n doubleType = new n();
f12107h = doubleType;
q complexFloat = new q("COMPLEX_FLOAT", 8);
q complexDouble = new q("COMPLEX_DOUBLE", 9);
q string = new q("STRING", 10);
q object = new q("OBJECT", 11);
f12108i = new q[]{logic, byteType, unsignedByte, shortType, intType, longType, floatType, doubleType, complexFloat, complexDouble, string, object};
}
}
-97
View File
@@ -1,97 +0,0 @@
package k3;
import java.lang.reflect.Field;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.rtbishop.look4sat.feature.cw.suncompat.Unsafe;
/* loaded from: classes.dex */
public abstract class r {
/* renamed from: a, reason: collision with root package name */
public static final Unsafe f12109a;
static {
Object obj = null;
try {
Class<?> cls = Class.forName("sun.misc.Unsafe");
Field declaredField = cls.getDeclaredField("theUnsafe");
declaredField.setAccessible(true);
obj = declaredField.get(cls);
} catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ignored) {
// Android adaptation: sun.misc.Unsafe may be unavailable (hidden API); silently degrades to null;
// the large-array native path (j4 > 1GB) never triggers in real decoding
}
f12109a = (Unsafe) obj;
}
public static void a(long j4, long j5, long j6, d dVar, d dVar2) {
long j7;
if (j4 >= 0 && j4 < dVar.f12102g) {
if (j5 >= 0 && j5 < dVar2.f12102g) {
if (j6 >= 0) {
if (!dVar2.f12104i) {
long j8 = c.c;
double[][] dArr = g3.c.f11585a;
if (j6 <= j8) {
j8 = j6;
}
int i4 = (int) j8;
if (i4 >= 2 && j6 >= c.f12095d) {
long j9 = j6 / i4;
Future[] futureArr = new Future[i4];
int i5 = 0;
while (i5 < i4) {
long j10 = i5 * j9;
if (i5 == i4 - 1) {
j7 = j6;
} else {
j7 = j10 + j9;
}
Future[] futureArr2 = futureArr;
int i6 = i5;
futureArr2[i6] = c.a(new j3.b(j10, j7, j5, j4, dVar2, dVar));
i5 = i6 + 1;
futureArr = futureArr2;
}
try {
c.b(futureArr);
return;
} catch (InterruptedException unused) {
long j11 = j4;
long j12 = j5;
while (j11 < j4 + j6) {
dVar2.c(j12, dVar.b(j11));
j11++;
j12++;
}
return;
} catch (ExecutionException unused2) {
long j13 = j4;
long j14 = j5;
while (j13 < j4 + j6) {
dVar2.c(j14, dVar.b(j13));
j13++;
j14++;
}
return;
}
}
long j15 = j4;
long j16 = j5;
while (j15 < j4 + j6) {
dVar2.c(j16, dVar.b(j15));
j15++;
j16++;
}
return;
}
throw new IllegalArgumentException("Constant arrays cannot be modified.");
}
throw new IllegalArgumentException("length < 0");
}
throw new ArrayIndexOutOfBoundsException("destPos < 0 || destPos >= dest.length()");
}
throw new ArrayIndexOutOfBoundsException("srcPos < 0 || srcPos >= src.length()");
}
}
-167
View File
@@ -1,167 +0,0 @@
package k3;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.rtbishop.look4sat.feature.cw.suncompat.Cleaner;
import com.rtbishop.look4sat.feature.cw.suncompat.Unsafe;
/* loaded from: classes.dex */
public final class s extends g {
/* renamed from: k, reason: collision with root package name */
public long[] f12110k;
public s() {
}
public s(long j4, boolean z3) {
this.f = q.f;
this.f12103h = 8L;
if (j4 > 0) {
this.f12102g = j4;
if (j4 > 1073741824) {
this.f12105j = r.f12109a.allocateMemory(8 * j4);
if (z3) {
a(j4);
}
Cleaner.create(this, new f(this.f12105j, this.f12102g, this.f12103h));
K1.a.c += this.f12102g * this.f12103h;
return;
}
this.f12110k = new long[(int) j4];
return;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
public final long b(long j4) {
long[] jArr = this.f12110k;
long j5 = this.f12105j;
if (j5 != 0) {
return r.f12109a.getLong((this.f12103h * j4) + j5);
}
if (this.f12104i) {
return jArr[0];
}
return jArr[(int) j4];
}
public final void c(long j4, long j5) {
long j6 = this.f12105j;
if (j6 != 0) {
r.f12109a.putLong((this.f12103h * j4) + j6, j5);
} else {
if (!this.f12104i) {
this.f12110k[(int) j4] = j5;
return;
}
throw new IllegalAccessError("Constant arrays cannot be modified.");
}
}
/* JADX WARN: Type inference failed for: r0v29, types: [k3.g, java.lang.Object, k3.s] */
public final Object clone() {
long j4;
int i4 = 0;
long j5 = 0;
if (this.f12104i) {
long j6 = this.f12102g;
long b4 = b(0L);
s gVar = new s();
gVar.f = q.f12107h;
gVar.f12103h = 8L;
if (j6 > 0) {
gVar.f12102g = j6;
gVar.f12104i = true;
gVar.f12110k = new long[]{b4};
return gVar;
}
throw new IllegalArgumentException(j6 + " is not a positive long value");
}
s sVar = new s(this.f12102g, false);
long j7 = this.f12102g;
Unsafe unsafe = r.f12109a;
if (0 < j7) {
if (0 < sVar.f12102g) {
if (j7 >= 0) {
if (!sVar.f12104i) {
long j8 = c.c;
double[][] dArr = g3.c.f11585a;
if (j7 <= j8) {
j8 = j7;
}
int i5 = (int) j8;
if (i5 >= 2 && j7 >= c.f12095d) {
long j9 = j7 / i5;
Future[] futureArr = new Future[i5];
while (i4 < i5) {
long j10 = i4 * j9;
if (i4 == i5 - 1) {
j4 = j7;
} else {
j4 = j10 + j9;
}
Future[] futureArr2 = futureArr;
int i6 = i4;
futureArr2[i6] = c.a(new i3.c(j10, j4, sVar, this));
i4 = i6 + 1;
futureArr = futureArr2;
}
try {
c.b(futureArr);
return sVar;
} catch (InterruptedException unused) {
long j11 = 0;
while (j5 < j7) {
sVar.c(j11, b(j5));
j5++;
j11++;
}
} catch (ExecutionException unused2) {
long j12 = 0;
while (j5 < j7) {
sVar.c(j12, b(j5));
j5++;
j12++;
}
}
} else {
long j13 = 0;
while (j5 < j7) {
sVar.c(j13, b(j5));
j5++;
j13++;
}
}
return sVar;
}
throw new IllegalArgumentException("Constant arrays cannot be modified.");
}
throw new IllegalArgumentException("length < 0");
}
throw new ArrayIndexOutOfBoundsException("destPos < 0 || destPos >= dest.length()");
}
throw new ArrayIndexOutOfBoundsException("srcPos < 0 || srcPos >= src.length()");
}
@Override // k3.g
public final boolean equals(Object obj) {
if (!super.equals(obj) || this.f12110k != ((s) obj).f12110k) {
return false;
}
return true;
}
@Override // k3.g
public final int hashCode() {
int i4;
int hashCode = super.hashCode() * 29;
long[] jArr = this.f12110k;
if (jArr != null) {
i4 = jArr.hashCode();
} else {
i4 = 0;
}
return hashCode + i4;
}
}
-144
View File
@@ -1,144 +0,0 @@
package pas;
import pas.system;
/* loaded from: classes.dex */
public class cwstru {
/* loaded from: classes.dex */
public static class TMorseCharInfo extends system.Record {
public TMorseCharInfo(long j4, boolean z3) {
__Init(j4, z3);
}
private native void __Destroy(long j4);
@Override // pas.system.PascalObjectEx
public void __Release() {
__Destroy(this._pasobj);
this._pasobj = 0L;
}
@Override // pas.system.Record
public int __Size() {
return __Size(0);
}
public native char getCh();
public native int getIdxE();
public native boolean getLowSnr();
public native TWordEndType getWordEnd();
public native float getWordSpaceProb();
public native TMorseWordType getWordType();
public native void setCh(char c);
public native void setIdxE(int i4);
public native void setLowSnr(boolean z3);
public native void setWordEnd(TWordEndType tWordEndType);
public native void setWordSpaceProb(float f);
public native void setWordType(TMorseWordType tMorseWordType);
public TMorseCharInfo() {
__Init(0L, true);
}
public TMorseCharInfo(system.PascalObject pascalObject) {
super(pascalObject);
}
public TMorseCharInfo(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TMorseWordType extends system.Enum {
public static final int wtCall = 5;
public static final int wtCq = 3;
public static final int wtGenWord = 0;
public static final int wtKeyword = 1;
public static final int wtQrl = 4;
public static final int wtRst = 2;
public TMorseWordType(int i4) {
this.Value = i4;
}
public static TMorseWordType wtCall() {
return new TMorseWordType(5);
}
public static TMorseWordType wtCq() {
return new TMorseWordType(3);
}
public static TMorseWordType wtGenWord() {
return new TMorseWordType(0);
}
public static TMorseWordType wtKeyword() {
return new TMorseWordType(1);
}
public static TMorseWordType wtQrl() {
return new TMorseWordType(4);
}
public static TMorseWordType wtRst() {
return new TMorseWordType(2);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TMorseWordType) && this.Value == ((TMorseWordType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
/* loaded from: classes.dex */
public static class TWordEndType extends system.Enum {
public static final int weLong = 2;
public static final int weNone = 0;
public static final int weShort = 1;
public TWordEndType(int i4) {
this.Value = i4;
}
public static TWordEndType weLong() {
return new TWordEndType(2);
}
public static TWordEndType weNone() {
return new TWordEndType(0);
}
public static TWordEndType weShort() {
return new TWordEndType(1);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TWordEndType) && this.Value == ((TWordEndType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
static {
system.InitJni();
}
}
-171
View File
@@ -1,171 +0,0 @@
package pas;
import pas.cwstru;
import pas.system;
/* loaded from: classes.dex */
public class decoder {
/* loaded from: classes.dex */
public static class TCharInfo extends system.TObject {
public TCharInfo(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TCharInfo Class() {
return new TCharInfo(system.GetClassRef(1));
}
public static native TCharInfo Create();
public static native TCharInfo Create(char c);
public static native TCharInfo Create(cwstru.TMorseCharInfo tMorseCharInfo);
public static system.TClass TClass() {
return system.GetTClass(1);
}
public native char getChar();
public native boolean getLowSnr();
public native TMorseWordType getWordType();
public TCharInfo(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TDecoder extends system.TObject {
public TDecoder(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TDecoder Class() {
return new TDecoder(system.GetClassRef(2));
}
public static native TDecoder Create();
public static system.TClass TClass() {
return system.GetTClass(2);
}
public native TCharInfo GetChar(int i4);
public native void LockToPitch(int i4);
public native int getCharCount();
public native int getDeleteCount();
public native boolean getHamMode();
public native int getPitch();
public native int getSignalCount();
public native int getSnr();
public native int getWpm();
public native void setHamMode(boolean z3);
public TDecoder(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TDecoderState extends system.Enum {
public static final int dsDecoding = 2;
public static final int dsLockedDecoding = 4;
public static final int dsLockedPause = 3;
public static final int dsNone = 0;
public static final int dsPause = 1;
public TDecoderState(int i4) {
this.Value = i4;
}
public static TDecoderState dsDecoding() {
return new TDecoderState(2);
}
public static TDecoderState dsLockedDecoding() {
return new TDecoderState(4);
}
public static TDecoderState dsLockedPause() {
return new TDecoderState(3);
}
public static TDecoderState dsNone() {
return new TDecoderState(0);
}
public static TDecoderState dsPause() {
return new TDecoderState(1);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TDecoderState) && this.Value == ((TDecoderState) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
/* loaded from: classes.dex */
public static class TMorseWordType extends system.Enum {
public static final int wtCall = 5;
public static final int wtCq = 3;
public static final int wtGenWord = 0;
public static final int wtKeyword = 1;
public static final int wtQrl = 4;
public static final int wtRst = 2;
public TMorseWordType(int i4) {
this.Value = i4;
}
public static TMorseWordType wtCall() {
return new TMorseWordType(5);
}
public static TMorseWordType wtCq() {
return new TMorseWordType(3);
}
public static TMorseWordType wtGenWord() {
return new TMorseWordType(0);
}
public static TMorseWordType wtKeyword() {
return new TMorseWordType(1);
}
public static TMorseWordType wtQrl() {
return new TMorseWordType(4);
}
public static TMorseWordType wtRst() {
return new TMorseWordType(2);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TMorseWordType) && this.Value == ((TMorseWordType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
static {
system.InitJni();
}
}
@@ -1,35 +0,0 @@
package pas;
import pas.decoder;
import pas.system;
/* loaded from: classes.dex */
public class nativedecoder {
/* loaded from: classes.dex */
public static class TNativeDecoder extends decoder.TDecoder {
public TNativeDecoder(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TNativeDecoder Class() {
return new TNativeDecoder(system.GetClassRef(3));
}
public static native TNativeDecoder Create();
public static system.TClass TClass() {
return system.GetTClass(3);
}
public native decoder.TDecoderState ProcessSpectrum(Object obj);
public TNativeDecoder(long j4) {
super(j4);
}
}
static {
system.InitJni();
}
}
-385
View File
@@ -1,385 +0,0 @@
package pas;
import java.util.Date;
import java.util.TimeZone;
/* loaded from: classes.dex */
public class system {
private static boolean _JniLibLoaded = false;
/* loaded from: classes.dex */
public static abstract class BaseSet {
protected int Value = 0;
public abstract int Base();
public abstract int ElMax();
public abstract byte Size();
}
/* loaded from: classes.dex */
public static class Enum {
public int Value;
public int Ord() {
return this.Value;
}
public boolean equals(Object obj) {
return (obj instanceof Integer) && this.Value == ((Integer) obj).intValue();
}
public int hashCode() {
return this.Value;
}
public boolean equals(int i4) {
return this.Value == i4;
}
}
/* loaded from: classes.dex */
public static class MethodPtr extends PascalObjectEx {
protected String mName;
protected Object mObject;
protected String mSignature;
public MethodPtr() {
this._cleanup = true;
this._pasobj = -1L;
}
private native void __Destroy();
private native void __Init(Object obj, String str, String str2, boolean z3);
public void Init() {
boolean z3;
Object obj = this.mObject;
String str = this.mName;
String str2 = this.mSignature;
if (this != obj) {
z3 = true;
} else {
z3 = false;
}
__Init(obj, str, str2, z3);
}
@Override // pas.system.PascalObjectEx
public void __Release() {
if (this._pasobj > 0) {
__Destroy();
}
}
}
/* loaded from: classes.dex */
public static abstract class PascalInterface extends PascalObjectEx {
public PascalInterface(long j4, boolean z3) {
this._pasobj = j4;
__Init();
}
public abstract void __Init();
public void __TypeCast(PascalObject pascalObject, String str) {
if (pascalObject != null) {
if (pascalObject instanceof PascalInterface) {
this._pasobj = pascalObject._pasobj;
__Init();
} else {
this._pasobj = system.InterfaceCast(pascalObject._pasobj, str);
}
}
}
}
/* loaded from: classes.dex */
public static class Record extends PascalObjectEx {
protected PascalObject _objref;
public Record(PascalObject pascalObject) {
super(pascalObject);
this._objref = pascalObject;
}
public void __Init(long j4, boolean z3) {
this._pasobj = j4;
this._cleanup = z3;
if (j4 == 0 && __Size() != 0) {
this._pasobj = system.AllocMemory(__Size());
}
}
public int __Size() {
return 0;
}
@Override // pas.system.PascalObjectEx, pas.system.PascalObject
public void finalize() {
long j4 = this._pasobj;
if (j4 < 0) {
this._pasobj = -j4;
this._cleanup = true;
}
super.finalize();
}
public Record(long j4) {
super(j4);
}
public final int __Size(int i4) {
return system.GetRecordSize(i4);
}
public Record() {
}
}
/* loaded from: classes.dex */
public static abstract class Set<TS extends BaseSet, TE extends Enum> extends BaseSet {
public Set() {
}
public void Assign(TS ts) {
this.Value = ts.Value;
}
public void Exclude(TE... teArr) {
for (TE te : teArr) {
this.Value = (~GetMask(te)) & this.Value;
}
}
public int GetMask(TE te) {
return 1 << (te.Ord() - Base());
}
public boolean Has(TE te) {
if ((GetMask(te) & this.Value) != 0) {
return true;
}
return false;
}
public void Include(TE... teArr) {
for (TE te : teArr) {
this.Value = GetMask(te) | this.Value;
}
}
public void Intersect(TS ts) {
this.Value = ts.Value & this.Value;
}
public boolean IsEmpty() {
if (this.Value == 0) {
return true;
}
return false;
}
public boolean equals(TS ts) {
return this.Value == ts.Value;
}
public Set(TE... teArr) {
Include(teArr);
}
public void Exclude(TS ts) {
this.Value = (~ts.Value) & this.Value;
}
public void Include(TS ts) {
this.Value = ts.Value | this.Value;
}
public boolean equals(TE te) {
return this.Value == te.Ord();
}
public Set(TS... tsArr) {
for (TS ts : tsArr) {
Include(ts); // smali: Set(TS...) 循环调 Include(BaseSet)
}
}
public boolean equals(int i4) {
return this.Value == i4;
}
}
/* loaded from: classes.dex */
public static class TClass extends TObject {
public TClass(PascalObject pascalObject) {
super(pascalObject);
}
}
/* loaded from: classes.dex */
public static class TDateTime {
public static double get(Date date) {
return ((date.getTime() + TimeZone.getDefault().getOffset(date.getTime())) / 8.64E7d) + 25569.0d;
}
public static double getUTC(Date date) {
return (date.getTime() / 8.64E7d) + 25569.0d;
}
public static Date toDate(double d4) {
return new Date(Math.round((d4 - 25569.0d) * 8.64E7d) - TimeZone.getDefault().getOffset(Math.round((d4 - 25569.0d) * 8.64E7d))); // r3 = 前值(smali)
}
public static Date toDateUTC(double d4) {
return new Date(Math.round((d4 - 25569.0d) * 8.64E7d));
}
}
/* loaded from: classes.dex */
public static class TObject extends PascalObject {
public TObject(PascalObject pascalObject) {
super(pascalObject);
}
public static TObject Class() {
return new TObject(system.GetClassRef(0));
}
public static native TObject Create();
public static TClass TClass() {
return system.GetTClass(0);
}
public native String ClassName();
public native boolean ClassNameIs(String str);
public native TClass ClassParent();
public native TClass ClassType();
public native void Destroy();
public native void Free();
public native boolean InheritsFrom(TClass tClass);
public native int InstanceSize();
public TObject(long j4) {
super(j4);
}
}
static {
InitJni();
}
public static native long AllocMemory(int i4);
public static native long GetClassRef(int i4);
public static native byte[] GetMemoryAsArray(long j4, int i4);
/* JADX INFO: Access modifiers changed from: private */
public static native int GetRecordSize(int i4);
public static TClass GetTClass(int i4) {
TClass tClass = new TClass(null);
tClass._pasobj = GetClassRef(i4);
return tClass;
}
public static void InitJni() {
if (!_JniLibLoaded) {
_JniLibLoaded = true;
System.loadLibrary("nativedecoderjni");
}
}
/* JADX INFO: Access modifiers changed from: private */
public static native long InterfaceCast(long j4, String str);
public static long Pointer(PascalObject pascalObject) {
if (pascalObject == null) {
return 0L;
}
return pascalObject._pasobj;
}
public static native void SetMemoryFromArray(long j4, byte[] bArr);
/* loaded from: classes.dex */
public static class PascalObject {
protected long _pasobj;
static {
system.InitJni();
}
public PascalObject() {
this._pasobj = 0L;
}
public boolean equals(Object obj) {
if ((obj instanceof PascalObject) && this._pasobj == ((PascalObject) obj)._pasobj) {
return true;
}
return false;
}
public void finalize() {
}
public int hashCode() {
return (int) this._pasobj;
}
public PascalObject(PascalObject pascalObject) {
this._pasobj = 0L;
if (pascalObject != null) {
this._pasobj = pascalObject._pasobj;
}
}
public PascalObject(long j4) {
this._pasobj = j4;
}
}
/* loaded from: classes.dex */
public static class PascalObjectEx extends PascalObject {
protected boolean _cleanup;
public PascalObjectEx() {
this._cleanup = false;
}
public void __Release() {
this._pasobj = 0L;
}
@Override // pas.system.PascalObject
public void finalize() {
if (this._cleanup) {
__Release();
}
super.finalize();
}
public PascalObjectEx(PascalObject pascalObject) {
super(pascalObject);
this._cleanup = false;
}
public PascalObjectEx(long j4) {
super(j4);
this._cleanup = false;
}
}
}
@@ -1,16 +0,0 @@
package s;
/* renamed from: s.e, reason: case insensitive filesystem */
/* loaded from: classes.dex */
public abstract /* synthetic */ class AbstractC1880e {
/* renamed from: a, reason: collision with root package name */
public static final /* synthetic */ int[] f13163a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28};
public static /* synthetic */ int a(int i4) {
if (i4 != 0) {
return i4 - 1;
}
throw null;
}
}
@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:id="@+id/verticalLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<TextView
android:textColor="#ffffff"
android:id="@+id/statusTextView"
android:background="@color/purple_500"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/idle"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="100dp">
<com.ve3nea.morse_expert.WaterfallSurfaceView
android:id="@+id/waterfallView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/waterfall_display"/>
<com.ve3nea.morse_expert.ScaleView
android:id="@+id/scaleView"
android:background="#24545a"
android:layout_width="12dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/frequency_scale"/>
</LinearLayout>
<com.ve3nea.morse_expert.DecodedTextView
android:textSize="18sp"
android:typeface="monospace"
android:textColor="#444444"
android:textColorHint="#888888"
android:gravity="start|top"
android:id="@+id/decodedTextView"
android:background="#d1f0f4"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/decoded_text"
android:freezesText="false"
android:layout_weight="1"
android:inputType="none"
android:fadeScrollbars="false"
android:textIsSelectable="true"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,59 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:id="@+id/verticalLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<TextView
android:textColor="#ffffff"
android:textSize="10sp"
android:singleLine="true"
android:ellipsize="end"
android:visibility="gone"
android:id="@+id/statusTextView"
android:background="@color/purple_500"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/idle"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="80dp">
<com.ve3nea.morse_expert.WaterfallSurfaceView
android:id="@+id/waterfallView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/waterfall_display"/>
<com.ve3nea.morse_expert.ScaleView
android:id="@+id/scaleView"
android:background="#24545a"
android:layout_width="8dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/frequency_scale"/>
</LinearLayout>
<com.ve3nea.morse_expert.DecodedTextView
android:textSize="12sp"
android:typeface="monospace"
android:textColor="#444444"
android:textColorHint="#888888"
android:gravity="start|top"
android:id="@+id/decodedTextView"
android:background="#d1f0f4"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/decoded_text"
android:freezesText="false"
android:layout_weight="1"
android:minHeight="48dp"
android:inputType="none"
android:fadeScrollbars="false"
android:textIsSelectable="true"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_baseline_pause_24"
android:id="@+id/pause_mnu"
android:title="@string/pause"
app:showAsAction="ifRoom"/>
<item
android:icon="@drawable/ic_baseline_delete_24"
android:id="@+id/clear_mnu"
android:title="@string/clear"
app:showAsAction="ifRoom"/>
<item
android:icon="@drawable/ic_baseline_save_24"
android:id="@+id/save_mnu"
android:title="@string/save_text"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/record_signals_mnu"
android:title="@string/record_signals"/>
<item
android:id="@+id/settings_mnu"
android:title="@string/settings"/>
<item
android:id="@+id/help_mnu"
android:title="@string/help"/>
<item
android:id="@+id/rate_mnu"
android:title="@string/rate"/>
<item
android:id="@+id/premium_mnu"
android:title="@string/premium"/>
</menu>
+1 -2
View File
@@ -8,8 +8,7 @@ android {
dependencies { dependencies {
implementation(project(":feature:mutual")) implementation(project(":feature:mutual"))
// CW 解码面板: 复用 feature:cw 的 Morse Expert 引擎(布局 cw_panel_main + MainActivity 控制器) // CW 解码面板: 复用 feature:cw 的 fldigi 解码引擎(纯 Compose 面板)
implementation(project(":feature:cw")) implementation(project(":feature:cw"))
// 与 feature:cw 一致(控制器按 ID 递归查找 ConstraintLayout 视图)
implementation("androidx.constraintlayout:constraintlayout:2.2.1") implementation("androidx.constraintlayout:constraintlayout:2.2.1")
} }
@@ -17,11 +17,13 @@
*/ */
package com.rtbishop.look4sat.feature.radar package com.rtbishop.look4sat.feature.radar
import android.app.Activity import android.media.AudioFormat
import android.view.LayoutInflater import android.media.AudioRecord
import android.media.MediaRecorder
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -41,6 +43,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
@@ -53,35 +56,46 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import com.rtbishop.look4sat.core.domain.cw.CwFldigiDecoder
import androidx.constraintlayout.widget.ConstraintLayout
import com.rtbishop.look4sat.core.domain.model.SatRadio import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
import com.rtbishop.look4sat.core.presentation.CardButton import com.rtbishop.look4sat.core.presentation.CardButton
import kotlin.math.roundToInt
import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.formatFrequency import com.rtbishop.look4sat.core.presentation.formatFrequency
import com.rtbishop.look4sat.core.presentation.infiniteMarquee import com.rtbishop.look4sat.core.presentation.infiniteMarquee
import com.rtbishop.look4sat.feature.cw.R as CwR
import com.ve3nea.morse_expert.MainActivity
import java.util.Locale import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@@ -714,33 +728,47 @@ private fun CwDecoderPanel(
} }
AnimatedVisibility(visible = cw.isExpanded) { AnimatedVisibility(visible = cw.isExpanded) {
// PR #1 Morse Expert engine: mini layout with waterfall + decoded text. // fldigi engine: pure Compose panel (waterfall + decoded text).
// Keep the old Kotlin decoder state/actions as fallback code, but this panel no longer feeds it.
val context = LocalContext.current val context = LocalContext.current
val activity = remember { context as? Activity } val decoder = remember { CwFldigiDecoder(initialWpm = 18) }
val controller = remember { MainActivity() } val waterfall = remember { CwPanelWaterfallState() }
val rootView = remember {
LayoutInflater.from(context).inflate(CwR.layout.cw_panel_main, null) as ConstraintLayout
}
var initialized by remember { mutableStateOf(false) }
var listening by remember { mutableStateOf(false) } var listening by remember { mutableStateOf(false) }
var captureJob by remember { mutableStateOf<Job?>(null) }
val scope = rememberCoroutineScope()
val decodedText by decoder.decodedTextFlow.collectAsState()
val signalStrength by decoder.signalStrength.collectAsState()
fun startPanelCapture() {
if (!cw.hasPermission) {
requestMicPermission()
return
}
captureJob?.cancel()
captureJob = scope.launch(Dispatchers.IO) {
val capture = CwPanelMicCapture()
capture.audioFlow().collect { chunk ->
if (!isActive) return@collect
decoder.processBuffer(chunk)
waterfall.pushSamples(chunk)
}
}
listening = true
}
fun stopPanelCapture() {
captureJob?.cancel()
captureJob = null
listening = false
}
DisposableEffect(Unit) { DisposableEffect(Unit) {
if (activity != null) { onDispose { stopPanelCapture() }
controller.onCreate(activity, rootView, false)
}
onDispose {
controller.onPause()
controller.onDestroy()
}
} }
LaunchedEffect(cw.hasPermission) { LaunchedEffect(cw.hasPermission) {
if (cw.hasPermission && !initialized) { if (cw.hasPermission && !listening) {
controller.onPermissionGranted() startPanelCapture()
controller.onResume()
initialized = true
listening = true
} }
} }
@@ -751,48 +779,61 @@ private fun CwDecoderPanel(
) { ) {
if (!listening) { if (!listening) {
Button( Button(
onClick = { onClick = { startPanelCapture() },
if (!cw.hasPermission) {
requestMicPermission()
} else if (!initialized) {
controller.onPermissionGranted()
controller.onResume()
initialized = true
listening = true
} else {
controller.onResume()
listening = true
}
},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text(stringResource(R.string.radar_cw_start)) Text(stringResource(R.string.radar_cw_start))
} }
} else { } else {
Button( Button(
onClick = { onClick = { stopPanelCapture() },
controller.onPause()
listening = false
},
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text(stringResource(R.string.radar_cw_stop)) Text(stringResource(R.string.radar_cw_stop))
} }
} }
OutlinedButton( OutlinedButton(
onClick = { controller.clearDecoded() }, onClick = { decoder.resetDecoder(); waterfall.reset() },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text(stringResource(R.string.radar_cw_reset)) Text(stringResource(R.string.radar_cw_reset))
} }
} }
AndroidView( Box(
factory = { rootView },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(150.dp) .height(90.dp)
.clip(RoundedCornerShape(8.dp))
) {
CwPanelWaterfallView(state = waterfall)
}
Text(
text = decodedText.takeLast(120),
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.fillMaxWidth()
) )
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = if (listening) "● ACTIVE" else "○ IDLE",
fontSize = 12.sp,
color = if (listening) Color(0xFF4CAF50) else MaterialTheme.colorScheme.outline
)
Text(
text = "LEVEL ${(signalStrength * 100).roundToInt()}%",
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }
@@ -821,3 +862,161 @@ private fun transceiverTitle(radio: SatRadio): String {
private val FREQ_ADJUSTMENTS = private val FREQ_ADJUSTMENTS =
listOf(-10_000L to "-10k", -1_000L to "-1k", -100L to "-100", 100L to "+100", 1_000L to "+1k", 10_000L to "+10k") listOf(-10_000L to "-10k", -1_000L to "-1k", -100L to "-100", 100L to "+100", 1_000L to "+1k", 10_000L to "+10k")
// --- CW panel components (fldigi engine, pure Compose) ---
/** Microphone capture at 8000 Hz mono PCM float (fldigi sample rate). */
private class CwPanelMicCapture {
private val sampleRate = 8000
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_FLOAT
fun audioFlow(): Flow<FloatArray> = flow {
val bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
.coerceAtLeast(sampleRate / 5)
val recorder = AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize * 4
)
try {
recorder.startRecording()
val chunkSize = sampleRate / 10
val buffer = FloatArray(chunkSize)
while (currentCoroutineContext().isActive) {
val read = recorder.read(buffer, 0, chunkSize, AudioRecord.READ_BLOCKING)
if (read > 0) {
emit(if (read == chunkSize) buffer.copyOf() else buffer.copyOfRange(0, read))
}
}
} finally {
recorder.stop()
recorder.release()
}
}.flowOn(Dispatchers.IO)
}
/** Sliding spectrogram ring buffer for the CW waterfall. */
private class CwPanelWaterfallState {
private val columns = 120
private val rows = 48
private val data = FloatArray(columns * rows)
private val pending = ArrayList<Float>(4096)
private val fftWindow = 512
@Synchronized
fun pushSamples(samples: FloatArray) {
pending.addAll(samples.toList())
while (pending.size >= fftWindow) {
val window = FloatArray(fftWindow) { pending[it] }
repeat(fftWindow) { pending.removeAt(0) }
val spectrum = computeSpectrum(window)
for (r in 0 until rows - 1) {
System.arraycopy(data, (r + 1) * columns, data, r * columns, columns)
}
for (c in 0 until columns) {
val bin = c * spectrum.size / columns
data[(rows - 1) * columns + c] = spectrum[bin.coerceAtMost(spectrum.size - 1)]
}
}
}
private fun computeSpectrum(window: FloatArray): FloatArray {
val n = window.size
val re = FloatArray(n)
val im = FloatArray(n)
for (i in 0 until n) {
val w = 0.5 - 0.5 * kotlin.math.cos(2.0 * Math.PI * i / (n - 1))
re[i] = window[i] * w.toFloat()
}
var j = 0
for (i in 1 until n) {
var bit = n shr 1
while (j and bit != 0) { j = j xor bit; bit = bit shr 1 }
j = j xor bit
if (i < j) {
val tr = re[i]; re[i] = re[j]; re[j] = tr
val ti = im[i]; im[i] = im[j]; im[j] = ti
}
}
var len = 2
while (len <= n) {
val ang = -2.0 * Math.PI / len
val wRe = kotlin.math.cos(ang).toFloat()
val wIm = kotlin.math.sin(ang).toFloat()
var i = 0
while (i < n) {
var curRe = 1f
var curIm = 0f
for (k in 0 until len / 2) {
val uRe = re[i + k]
val uIm = im[i + k]
val vRe = re[i + k + len / 2] * curRe - im[i + k + len / 2] * curIm
val vIm = re[i + k + len / 2] * curIm + im[i + k + len / 2] * curRe
re[i + k] = uRe + vRe
im[i + k] = uIm + vIm
re[i + k + len / 2] = uRe - vRe
im[i + k + len / 2] = uIm - vIm
val nRe = curRe * wRe - curIm * wIm
val nIm = curRe * wIm + curIm * wRe
curRe = nRe; curIm = nIm
}
i += len
}
len = len shl 1
}
val bins = 48
val out = FloatArray(bins)
for (k in 0 until bins) {
out[k] = kotlin.math.sqrt(re[k] * re[k] + im[k] * im[k]) / n
}
return out
}
@Synchronized
fun getColumn(c: Int): FloatArray {
val col = FloatArray(rows)
for (r in 0 until rows) {
col[r] = data[r * columns + c.coerceIn(0, columns - 1)]
}
return col
}
@Synchronized
fun reset() {
data.fill(0f)
pending.clear()
}
}
@Composable
private fun CwPanelWaterfallView(state: CwPanelWaterfallState) {
val columns = 120
val rows = 48
Canvas(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFF0D1B2A))
.clip(RoundedCornerShape(8.dp))
) {
val cellW = size.width / columns
val cellH = size.height / rows
for (c in 0 until columns) {
val col = state.getColumn(c)
for (r in 0 until rows) {
val v = col[r]
val intensity = (v * 4000f).coerceIn(0f, 255f)
if (intensity > 6f) {
val green = (intensity * 1.3f).coerceAtMost(255f)
drawRect(
color = Color(0f, green / 255f, 0f, 1f),
topLeft = Offset(c * cellW, r * cellH),
size = Size(cellW + 0.5f, cellH + 0.5f)
)
}
}
}
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
[versions] [versions]
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionCode = "458" appVersionCode = "459"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionName = "4.5.5" appVersionName = "4.5.6"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
compileSdk = "37" compileSdk = "37"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry