i18n: localize mutual match page to English

This commit is contained in:
atsunatsu committed 2026-08-07 19:53:36 +08:00
1 parent a8afc63bba
commit 80a58bed61
104 files changed
+18309 -63

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.22.1)
project(fldigi_cw)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 禁用 ANR 检测器等不必要的 Android 特性
set(CMAKE_ANDROID_EXCEPTIONS ON)
add_library(fldigi_cw SHARED
fldigi_cw_jni.cpp
fldigi/cw.cxx
fldigi/morse.cxx
fldigi/fftfilt.cxx
fldigi/filters.cxx
)
target_include_directories(fldigi_cw PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/fldigi
)
target_compile_definitions(fldigi_cw PRIVATE
FLDIGI_ANDROID
)
target_link_libraries(fldigi_cw
log
android
)
@@ -0,0 +1,115 @@
// ----------------------------------------------------------------------------
// android_compat.h -- fldigi desktop compatibility stubs for Android
// Replaces global variables that fldigi defines in fl_digi.h, configuration.h, etc.
// ----------------------------------------------------------------------------
#ifndef ANDROID_COMPAT_H
#define ANDROID_COMPAT_H
#include <string>
// Waterfall stub
struct waterfall_stub {
double Carrier() { return 700.0; }
bool Reverse() { return false; }
bool USB() { return true; }
void Bandwidth(int) {}
};
extern waterfall_stub* wf;
// progdefaults - fldigi global configuration
struct AndroidProgDefaults {
int CWspeed = 18;
double CWbandwidth = 200.0;
double CWfarnsworth = 18;
double CWupper = 0.8;
double CWlower = 0.2;
bool CWmfilt = true;
bool CWtrack = true;
int CWsweetspot = 700;
int CWnoise = '*';
int CWrisetime = 4;
int QSKshape = 0;
int CWdash2dot = 3;
int CW_cal_speed = 18;
bool CWusefarnsworth = false;
bool use_KNWDkeying = false;
bool use_ELCTkeying = false;
bool use_ICOMkeying = false;
bool use_YAESUkeying = false;
int CATkeying_compensation = 0;
bool StartAtSweetSpot = false;
bool CW_use_paren = false;
std::string CW_prosigns;
bool pretone = false;
bool use_nanoIO = false;
bool rx_lowercase = false;
bool CWuseSOMdecoding = true;
int CW_bandwidth = 200;
int CW_upper = 80;
int CW_lower = 20;
// Additional fields referenced by cw.cxx
int CWrange = 10;
int CWlowerlimit = 5;
int CWupperlimit = 60;
int CWpre = 0;
int CWpost = 0;
int CWkeycomp = 0;
int cwrx_attack = 0;
int cwrx_decay = 0;
int defCWspeed = 18;
int QSK = 0;
int QSKamp = 0;
int QSKfrequency = 0;
int QSKrisetime = 0;
bool CW_KEYLINE = false;
bool CW_KEYLINE_on_cat_port = false;
bool CW_KEYLINE_on_ptt_port = false;
bool PTT_KEYLINE = false;
bool use_FLRIGkeying = false;
int BaudRate = 0;
std::string CW_KEYLINE_serial_port_name;
};
extern AndroidProgDefaults progdefaults;
// NanoIO globals
extern bool use_nanoIO;
void set_nanoWPM(int wpm);
// UI/status stubs (fldigi desktop helpers, no-op on Android)
void put_cwRcvWPM(int);
void put_MODEstatus(const char*, ...);
void set_scope_xaxis_1(double);
void set_scope_xaxis(double);
template<typename T, int N, int M>
void set_scope(mbuffer<T,N,M>&, int, bool) {}
void set_nanoCW();
void update_Status();
// Fldigi math helpers
#define TWOPI (2.0 * M_PI)
inline double decayavg(double average, double input, int weight) {
if (weight <= 1) return input;
return ((input - average) / (double)weight) + average;
}
// progStatus
struct AndroidProgStatus {
int carrier = 0;
bool WK_online = false;
bool sqlonoff = false;
double sldrSquelchValue = 0.0;
bool show_channels = false;
};
extern AndroidProgStatus progStatus;
// Misc helpers that fldigi provides via misc.h / status.h / etc.
void set_scope_mode(int);
void put_rx_char(int c);
void put_echo_char(int c);
// Time helpers
double zmsec();
void MilliSleep(int);
#endif
+43
View File
@@ -0,0 +1,43 @@
// ----------------------------------------------------------------------------
// complex.h -- Complex arithmetic
//
// Copyright (C) 2006-2008
// Dave Freese, W1HKJ
// Copyright (C) 2008
// Stelios Bounanos, M0GLD
//
// This file is part of fldigi.
//
// Fldigi 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.
//
// Fldigi 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 fldigi. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
#ifndef _COMPLEX_H
#define _COMPLEX_H
#include <cmath>
#include <complex>
typedef std::complex<double> cmplx;
inline cmplx cmac (const cmplx *a, const cmplx *b, int ptr, int len) {
cmplx z;
ptr %= len;
for (int i = 0; i < len; i++) {
z += a[i] * b[ptr];
ptr = (ptr + 1) % len;
}
return z;
}
#endif
File diff suppressed because it is too large. Load diff
+197
View File
@@ -0,0 +1,197 @@
// ----------------------------------------------------------------------------
// cw.h -- morse code modem (Android adaptation)
// Copyright (C) 2006-2009 Dave Freese, W1HKJ
// Adapted from fldigi/src/include/cw.h (GPL v3).
// ----------------------------------------------------------------------------
#ifndef _CW_H
#define _CW_H
#include <cstring>
#include <string>
#include <vector>
#include "modem.h"
#include "filters.h"
#include "fftfilt.h"
#include "mbuffer.h"
#include "view_cw.h"
#define CW_SAMPLERATE 8000
#define CWMaxSymLen 4096
#define MAX_MORSE_ELEMENTS 6
#define CW_SUCCESS 0
#define CW_ERROR -1
#define ASC_NUL '\0'
#define ASC_SPACE ' '
#define KWPM (12 * CW_SAMPLERATE / 10)
#define CWKNUM ((KWPM) / 10)
#define TONE_SILENT 0
#define USECS_PER_SEC 1000000
#define INITIAL_SEND_SPEED 18
#define INITIAL_RECEIVE_SPEED 18
#define INITIAL_THRESHOLD (((KWPM) / INITIAL_RECEIVE_SPEED) * 2)
#define INITIAL_NOISE_THRESHOLD (((KWPM) / CW_MAX_SPEED) / 2)
#define TRACKING_FILTER_SIZE 16
#define MAX_PIPE_SIZE (22 * CW_SAMPLERATE * 12 / 800)
#define CW_MAX_SPEED 100
enum CW_RX_STATE {
RS_IDLE = 0,
RS_IN_TONE,
RS_AFTER_TONE
};
enum CW_EVENT {
CW_RESET_EVENT,
CW_KEYDOWN_EVENT,
CW_KEYUP_EVENT,
CW_QUERY_EVENT
};
class cw : public modem {
public:
#define CLRCOUNT 16
#define DEC_RATIO 16
#define WGT_SIZE 7
struct SOM_TABLE {
std::string rpr;
float wgt[WGT_SIZE];
};
protected:
int symbollen;
int fsymlen;
double phaseacc;
double FFTphase;
double FFTvalue;
unsigned int smpl_ctr;
double agc_peak;
bool use_matched_filter;
double upper_threshold;
double lower_threshold;
fftfilt *cw_FFT_filter;
Cmovavg *bitfilter;
Cmovavg *trackingfilter;
int bitfilterlen;
CW_RX_STATE cw_receive_state;
CW_RX_STATE old_cw_receive_state;
CW_EVENT cw_event;
double pipe[MAX_PIPE_SIZE + 1];
double clearpipe[MAX_PIPE_SIZE + 1];
mbuffer<double, MAX_PIPE_SIZE + 1, 4> scopedata;
int pipeptr;
int pipesize;
bool scope_clear;
// Config (from progdefaults, replaced for Android)
int cw_speed;
int cw_bandwidth;
int cw_squelch;
int cw_send_speed;
int cw_receive_speed;
bool usedefaultWPM;
int cw_upper_limit;
int cw_lower_limit;
long int cw_noise_spike_threshold;
int cw_in_sync;
long int cw_send_dot_length;
long int cw_send_dash_length;
int lastsym;
double risetime;
int knum;
int qnum;
int QSKshape;
double qskbuf[OUTBUFSIZE];
double qskphase;
bool firstelement;
bool lastelement;
double maxval;
long int cw_receive_dot_length;
long int cw_receive_dash_length;
std::string rx_rep_buf;
int cw_rr_current;
unsigned int cw_rr_start_timestamp;
unsigned int cw_rr_end_timestamp;
long int two_dots;
int in_replay;
double dot_tracking;
double dash_tracking;
// Android additions
std::string rx_text_buffer; // decoded text for JNI retrieval
inline double nco(double freq);
inline double qsknco();
void update_syncscope();
void clear_syncscope();
void update_Status();
void sync_parameters();
void reset_rx_filter();
int handle_event(int cw_event, std::string &sc);
inline int usec_diff(unsigned int earlier, unsigned int later);
void send_symbol(int symbol, int len, int state);
void send_ch(int c);
bool tables_init();
unsigned int tokenize_representation(char *representation);
void update_tracking(int dot, int dash);
static const SOM_TABLE som_table[];
float cw_buffer[512];
int cw_ptr;
int clrcount;
double lowerwpm;
double upperwpm;
int synchscope;
double noise_floor;
double sig_avg;
double siglevel;
bool use_paren;
std::string prosigns;
cmplx mixer(cmplx in);
int nusymbollen;
int nufsymlen;
int kpre;
int kpost;
double wpm;
double fwpm;
double cal_wpm;
void create_edges();
void sync_transmit_parameters();
void flush_audio();
void send_CW(int);
view_cw viewcw;
public:
cw();
~cw();
void init();
void rx_init();
void tx_init();
void restart() {}
int rx_process(const double *buf, int len);
void rx_FFTprocess(const double *buf, int len);
void rx_FIRprocess(const double *buf, int len);
void decode_stream(double);
int tx_process();
void incWPM();
void decWPM();
void toggleWPM();
double calWPM() { return cal_wpm; }
void calWPM(double val) { cal_wpm = val; }
int normalize(float *v, int n, int twodots);
std::string find_winner(float *inbuf, int twodots);
// Android: get and clear decoded text buffer
std::string get_rx_text() {
std::string result = rx_text_buffer;
rx_text_buffer.clear();
return result;
}
};
#endif
+315
View File
@@ -0,0 +1,315 @@
// ----------------------------------------------------------------------------
// fftfilt.cxx -- Fast convolution Overlap-Add filter
//
// Filter implemented using overlap-add FFT convolution method
// h(t) characterized by Windowed-Sinc impulse response
//
// Reference:
// "The Scientist and Engineer's Guide to Digital Signal Processing"
// by Dr. Steven W. Smith, http://www.dspguide.com
// Chapters 16, 18 and 21
//
// Copyright (C) 2006-2008 Dave Freese, W1HKJ
//
// This file is part of fldigi.
//
// Fldigi 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.
//
// Fldigi 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 fldigi. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
#include <config.h>
#include <memory.h>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <cmath>
#include <typeinfo>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <memory.h>
#include "misc.h"
#include "fftfilt.h"
//------------------------------------------------------------------------------
// initialize the filter
// create forward and reverse FFTs
//------------------------------------------------------------------------------
// probably only need a single instance of g_fft !!
// use for both forward and reverse
void fftfilt::clear_filter()
{
for (int i = 0; i < flen; i++) {
filter[i] = 0;
timedata[i] = 0;
freqdata[i] = 0;
output[i] = 0;
ht[i] = 0;
}
for (int i = 0; i < flen2; i++)
ovlbuf[i] = 0;
inptr = 0;
}
void fftfilt::init_filter()
{
flen2 = flen >> 1;
fft = new g_fft<double>(flen);
filter = new cmplx[flen];
timedata = new cmplx[flen];
freqdata = new cmplx[flen];
output = new cmplx[flen];
ovlbuf = new cmplx[flen2];
ht = new cmplx[flen];
}
// number of samples needed to completely flush the filter
int fftfilt::flush_size()
{
return flen - inptr;
}
//------------------------------------------------------------------------------
// fft filter
// f1 < f2 ==> band pass filter
// f1 > f2 ==> band reject filter
// f1 == 0 ==> low pass filter
// f2 == 0 ==> high pass filter
//------------------------------------------------------------------------------
fftfilt::fftfilt(double f1, double f2, int len)
{
flen = len;
init_filter();
create_filter(f1, f2);
}
//------------------------------------------------------------------------------
// low pass filter
//------------------------------------------------------------------------------
fftfilt::fftfilt(double f, int len)
{
flen = len;
init_filter();
create_lpf(f);
}
fftfilt::~fftfilt()
{
if (fft) delete fft;
if (filter) delete [] filter;
if (timedata) delete [] timedata;
if (freqdata) delete [] freqdata;
if (output) delete [] output;
if (ovlbuf) delete [] ovlbuf;
if (ht) delete [] ht;
}
void fftfilt::create_filter(double f1, double f2)
{
clear_filter();
// initialize the filter to zero
for (int i = 0; i < flen; i++) ht[i] = 0;
// create the filter shape coefficients by fft
// filter values initialized to the ht response h(t)
bool b_lowpass, b_highpass;//, window;
b_lowpass = (f2 != 0);
b_highpass = (f1 != 0);
for (int i = 0; i < flen2; i++) {
ht[i] = 0;
//combine lowpass / highpass
// lowpass @ f2
if (b_lowpass) ht[i] += fsinc(f2, i, flen2);
// highighpass @ f1
if (b_highpass) ht[i] -= fsinc(f1, i, flen2);
}
// highpass is delta[flen2/2] - h(t)
if (b_highpass && f2 < f1) ht[flen2 / 2] += 1;
for (int i = 0; i < flen2; i++)
ht[i] *= _blackman(i, flen2);
// this may change since green fft is in place fft
memcpy(filter, ht, flen * sizeof(cmplx));
// ht is flen complex points with imaginary all zero
// first half describes h(t), second half all zeros
// perform the cmplx forward fft to obtain H(w)
// filter is flen/2 complex values
fft->ComplexFFT(filter);
// fft->transform(ht, filter);
// normalize the output filter for unity gain
double scale = 0, mag;
for (int i = 0; i < flen2; i++) {
mag = abs(filter[i]);
if (mag > scale) scale = mag;
}
if (scale != 0) {
for (int i = 0; i < flen; i++)
filter[i] /= scale;
}
// perform the reverse fft to obtain h(t)
// for testing
// uncomment to obtain filter characteristics
/*
cmplx *revht = new cmplx[flen];
memcpy(revht, filter, flen * sizeof(cmplx));
fft->InverseComplexFFT(revht);
std::fstream fspec;
fspec.open("fspec.csv", std::ios::out);
fspec << "i,imp.re,imp.im,filt.re,filt.im,filt.abs,revimp.re,revimp.im\n";
for (int i = 0; i < flen2; i++)
fspec
<< i << "," << ht[i].real() << "," << ht[i].imag() << ","
<< filter[i].real() << "," << filter[i].imag() << ","
<< abs(filter[i]) << ","
<< revht[i].real() << "," << revht[i].imag() << ","
<< std::endl;
fspec.close();
delete [] revht;
*/
// start output after 2 full passes are complete
pass = 1;
}
/*
* Filter with fast convolution (overlap-add algorithm).
*/
int fftfilt::run(const cmplx & in, cmplx **out)
{
// collect flen/2 input samples
timedata[inptr++] = in;
if (inptr < flen2)
return 0;
if (pass) --pass; // filter output is not stable until 2 passes
// FFT transpose to the frequency domain
memcpy(freqdata, timedata, flen * sizeof(cmplx));
fft->ComplexFFT(freqdata);
// multiply with the filter shape
for (int i = 0; i < flen; i++)
freqdata[i] *= filter[i];
// transform back to time domain
fft->InverseComplexFFT(freqdata);
// overlap and add
// save the second half for overlapping next inverse FFT
for (int i = 0; i < flen2; i++) {
output[i] = ovlbuf[i] + freqdata[i];
ovlbuf[i] = freqdata[i+flen2];
}
// clear inbuf pointer
inptr = 0;
// signal the caller there is flen/2 samples ready
if (pass) return 0;
*out = output;
return flen2;
}
//------------------------------------------------------------------------------
// rtty filter
//------------------------------------------------------------------------------
//bool print_filter = true; // flag to inhibit printing multiple copies
void fftfilt::rtty_filter(double f)
{
// Raised cosine filter designed iaw Section 1.2.6 of
// Telecommunications Measurements, Analysis, and Instrumentation
// by Dr. Kamilo Feher / Engineers of Hewlett-Packard
//
// Frequency scaling factor determined hueristically by testing various values
// and measuring resulting decoder CER with input s/n = - 9 dB
//
// K CER
// 1.0 .0244
// 1.1 .0117
// 1.2 .0081
// 1.3 .0062
// 1.4 .0054
// 1.5 .0062
// 1.6 .0076
f *= 1.4;
double dht;
for( int i = 0; i < flen2; ++i ) {
double x = (double)i/(double)(flen2);
// raised cosine response (changed for -1.0...+1.0 times Nyquist-f
// instead of books versions ranging from -1..+1 times samplerate)
dht =
x <= 0 ? 1.0 :
x > 2.0 * f ? 0.0 :
cos((M_PI * x) / (f * 4.0));
dht *= dht; // cos^2
// amplitude equalized nyquist-channel response
dht /= sinc(2.0 * i * f);
filter[i] =
cmplx( dht*cos((double)i* - 0.5*M_PI),
dht*sin((double)i* - 0.5*M_PI) );
filter[(flen-i)%flen] =
cmplx( dht*cos((double)i*+0.5*M_PI),
dht*sin((double)i*+0.5*M_PI) );
}
// perform the reverse fft to obtain h(t)
// for testing
// uncomment to obtain filter characteristics
/*
cmplx *revht = new cmplx[flen];
memcpy(revht, filter, flen * sizeof(cmplx));
fft->InverseComplexFFT(revht);
std::fstream fspec;
fspec.open("rtty_filter.csv", std::ios::out);
fspec << "i,filt.re,filt.im,filt.abs,,revimp.re,revimp.im\n";
for (int i = 0; i < flen; i++)
fspec
<< i << ","
<< filter[i].real() << "," << filter[i].imag() << "," << abs(filter[i])
<< ",," << revht[i].real() << "," << revht[i].imag()
<< std::endl;
fspec.close();
delete [] revht;
*/
// start output after 2 full passes are complete
pass = 1;
}
+78
View File
@@ -0,0 +1,78 @@
// ----------------------------------------------------------------------------
// Copyright (C) 2014
// David Freese, W1HKJ
//
// This file is part of fldigi
//
// fldigi 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.
//
// fldigi 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 <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
#ifndef _FFTFILT_H
#define _FFTFILT_H
#include "complex.h"
#include "gfft.h"
//----------------------------------------------------------------------
class fftfilt {
enum {NONE, BLACKMAN, HAMMING, HANNING};
protected:
int flen;
int flen2;
g_fft<double> *fft;
g_fft<double> *ift;
cmplx *ht;
cmplx *filter;
cmplx *timedata;
cmplx *freqdata;
cmplx *ovlbuf;
cmplx *output;
int inptr;
int pass;
int window;
inline double fsinc(double fc, int i, int len) {
return (i == len/2) ? 2.0 * fc:
sin(2 * M_PI * fc * (i - len/2)) / (M_PI * (i - len/2));
}
inline double _blackman(int i, int len) {
return (0.42 -
0.50 * cos(2.0 * M_PI * i / len) +
0.08 * cos(4.0 * M_PI * i / len));
}
void init_filter();
void clear_filter();
public:
fftfilt(double f1, double f2, int len);
fftfilt(double f, int len);
~fftfilt();
// f1 < f2 ==> bandpass
// f1 > f2 ==> band reject
void create_filter(double f1, double f2);
void create_lpf(double f) {
create_filter(0, f);
}
void create_hpf(double f) {
create_filter(f, 0);
}
void rtty_filter(double);
int run(const cmplx& in, cmplx **out);
int flush_size();
};
#endif
+551
View File
@@ -0,0 +1,551 @@
// ----------------------------------------------------------------------------
//
// filters.cxx -- Several Digital Filter classes used in fldigi
//
// Copyright (C) 2006-2008 Dave Freese, W1HKJ
//
// These filters are based on the gmfsk design and the design notes given in
// "Digital Signal Processing, A Practical Guid for Engineers and Scientists"
// by Steven W. Smith.
//
// This file is part of fldigi.
//
// Fldigi 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.
//
// Fldigi 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 fldigi. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
#include <config.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "filters.h"
#include <iostream>
//=====================================================================
// C_FIR_filter
//
// a class of Finite Impulse Response (FIR) filters with
// decimate in time capability
//
//=====================================================================
C_FIR_filter::C_FIR_filter () {
pointer = counter = length = 0;
decimateratio = 1;
ifilter = qfilter = (double *)0;
ffreq = 0.0;
}
C_FIR_filter::~C_FIR_filter() {
if (ifilter) delete [] ifilter;
if (qfilter) delete [] qfilter;
}
void C_FIR_filter::init(int len, int dec, double *itaps, double *qtaps) {
length = len;
decimateratio = dec;
if (ifilter) {
delete [] ifilter;
ifilter = (double *)0;
}
if (qfilter) {
delete [] qfilter;
qfilter = (double *)0;
}
for (int i = 0; i < FIRBufferLen; i++)
ibuffer[i] = qbuffer[i] = 0.0;
if (itaps) {
ifilter = new double[len];
for (int i = 0; i < len; i++) ifilter[i] = itaps[i];
}
if (qtaps) {
qfilter = new double[len];
for (int i = 0; i < len; i++) qfilter[i] = qtaps[i];
}
pointer = len;
counter = 0;
}
//=====================================================================
// Create a band pass FIR filter with 6 dB corner frequencies
// of 'f1' and 'f2'. (0 <= f1 < f2 <= 0.5)
//=====================================================================
double * C_FIR_filter::bp_FIR(int len, int hilbert, double f1, double f2)
{
double *fir;
double t, h, x;
fir = new double[len];
for (int i = 0; i < len; i++) {
t = i - (len - 1.0) / 2.0;
h = i * (1.0 / (len - 1.0));
if (!hilbert) {
x = (2 * f2 * sinc(2 * f2 * t) -
2 * f1 * sinc(2 * f1 * t)) * hamming(h);
} else {
x = (2 * f2 * cosc(2 * f2 * t) -
2 * f1 * cosc(2 * f1 * t)) * hamming(h);
// The actual filter code assumes the impulse response
// is in time reversed order. This will be anti-
// symmetric so the minus sign handles that for us.
x = -x;
}
fir[i] = x;
}
return fir;
}
//=====================================================================
// Filter will be a lowpass with
// length = len
// decimation = dec
// 0.5 frequency point = freq
//=====================================================================
void C_FIR_filter::init_lowpass (int len, int dec, double freq) {
double *fi = bp_FIR(len, 0, 0.0, freq);
ffreq = freq;
init (len, dec, fi, fi);
delete [] fi;
}
//=====================================================================
// Filter will be a bandpass with
// length = len
// decimation = dec
// 0.5 frequency points of f1 (low) and f2 (high)
//=====================================================================
void C_FIR_filter::init_bandpass (int len, int dec, double f1, double f2) {
double *fi = bp_FIR (len, 0, f1, f2);
init (len, dec, fi, fi);
delete [] fi;
}
//=====================================================================
// Filter will the Hilbert form
//=====================================================================
void C_FIR_filter::init_hilbert (int len, int dec) {
double *fi = bp_FIR(len, 0, 0.05, 0.45);
double *fq = bp_FIR(len, 1, 0.05, 0.45);
init (len, dec, fi, fq);
delete [] fi;
delete [] fq;
}
//=====================================================================
// Run
// passes a cmplx value (in) and receives the cmplx value (out)
// function returns 0 if the filter is not yet stable
// returns 1 when stable and decimated cmplx output value is valid
//=====================================================================
int C_FIR_filter::run (const cmplx &in, cmplx &out) {
ibuffer[pointer] = in.real();
qbuffer[pointer] = in.imag();
counter++;
if (counter == decimateratio)
out = cmplx ( mac(&ibuffer[pointer - length], ifilter, length),
mac(&qbuffer[pointer - length], qfilter, length) );
pointer++;
if (pointer == FIRBufferLen) {
/// memmove is necessary if length >= FIRBufferLen/2 , theoretically possible.
memmove (ibuffer, ibuffer + FIRBufferLen - length, length * sizeof (double) );
memmove (qbuffer, qbuffer + FIRBufferLen - length, length * sizeof (double) );
pointer = length;
}
if (counter == decimateratio) {
counter = 0;
return 1;
}
return 0;
}
//=====================================================================
// Run the filter for the Real part of the cmplx variable
//=====================================================================
int C_FIR_filter::Irun (const double &in, double &out) {
double *iptr = ibuffer + pointer;
pointer++;
counter++;
*iptr = in;
if (counter == decimateratio) {
out = mac(iptr - length, ifilter, length);
}
if (pointer == FIRBufferLen) {
iptr = ibuffer + FIRBufferLen - length;
memcpy(ibuffer, iptr, length * sizeof(double));
pointer = length;
}
if (counter == decimateratio) {
counter = 0;
return 1;
}
return 0;
}
//=====================================================================
// Run the filter for the Imaginary part of the cmplx variable
//=====================================================================
int C_FIR_filter::Qrun (const double &in, double &out) {
double *qptr = ibuffer + pointer;
pointer++;
counter++;
*qptr = in;
if (counter == decimateratio) {
out = mac(qptr - length, qfilter, length);
}
if (pointer == FIRBufferLen) {
qptr = qbuffer + FIRBufferLen - length;
memcpy(qbuffer, qptr, length * sizeof(double));
pointer = length;
}
if (counter == decimateratio) {
counter = 0;
return 1;
}
return 0;
}
//=====================================================================
// Moving average filter
//
// Simple in concept, sublime in implementation ... the fastest filter
// in the west. Also optimal for the processing of time domain signals
// characterized by a transition edge. The is the perfect signal filter
// for CW, RTTY and other signals of that type. For a given filter size
// it provides the greatest s/n improvement while retaining the sharpest
// leading edge on the filtered signal.
//=====================================================================
Cmovavg::Cmovavg (int filtlen)
{
len = filtlen;
in = new double[len];
empty = true;
}
Cmovavg::~Cmovavg()
{
if (in) delete [] in;
}
double Cmovavg::run(double a)
{
if (!in) {
return a;
}
if (empty) {
empty = false;
out = 0;
for (int i = 0; i < len; i++) {
in[i] = a;
out += a;
}
pint = 0;
return a;
}
out = out - in[pint] + a;
in[pint] = a;
if (++pint >= len) pint = 0;
return out / len;
}
void Cmovavg::setLength(int filtlen)
{
if (filtlen > len) {
if (in) delete [] in;
in = new double[filtlen];
}
len = filtlen;
empty = true;
}
void Cmovavg::reset()
{
empty = true;
}
//=====================================================================
// Sliding FFT filter
// Sliding Fast Fourier Transform
//
// The sliding FFT ingeniously exploits the properties of a time-delayed
// input and the property of linearity for its derivation.
//
// First of all, the N-point transform of a sequence x(n) is equal to the
// summation of the transforms of N separate transforms where each transform
// has just one of the original samples at it's original sample time.
//
//i.e.
// transform of [x0, x1, x2, x3, x4, x5,...xN-1]
// is equal to
// transform of [x0, 0, 0, 0, 0, 0,...0]
// + transform of [0, x1, 0, 0, 0, 0,...0]
// + transform of [0, 0, x2, 0, 0, 0,...0]
// + transform of [0, 0, 0, x3, 0, 0,...0]
// .
// .
// .
// + transform of [0, 0, 0, 0, 0, 0,...xN-1]
//
// Secondly, the transform of a time-delayed sequence is a phase-rotated
// version of the transform of the original sequence. i.e.
//
// If x(n) transforms to X(k),
// Then x(n-m) transforms to X(k)(Wn)^(-mk)
//
// where N is the FFT size, m is the delay in sample periods, and WN is the
// familiar phase-rotating coefficient or twiddle factor e^(-j2p/N)
//
// Therefore, if the N-point transform X(k) of an individual sample is considered,
// and then the sample is moved back in time by one sample period, all frequency
// bins of X(k) are phase-rotated by 2pk/N radians.
//
// The important thing here is that the transform is not performed again because
// the previous frequency results can be used by simply application of the correct
// coefficients.
//
// This is the technique that is applied when the rectangular sampling window
// slides along by one sample. The contributions of all samples that are
// included in both the original and the new windows are simply phase rotated.
// The end effects are that the transform of the new sample must be added, and
// the transform of the oldest sample that disappeared off the end must be
// subtracted. These end-effects are easy to perform if we treat the new sample
// as occurring at time t = 0, because the transform of a single sample at t = 0,
// say (a + bj), simply has all frequency bins equal to (a + bj). Similarly, the
// oldest sample that has just disappeared off the end of the window is exactly N
// samples old. I.e. it occurred at t = -N. The transform of this sample,
// say (c + dj), is also straightforward since every frequency bin has now been
// phase-rotated an integer number of times from when the sample was at t = 0.
// (The kth frequency bin has been rotated by 2pk radians). The transform of the
// sample at t = -N is therefore the same as if it was still at t = 0. I.e. it
// has all frequency bins equal to (c + dj).
//
// All that is needed therefore is to
// phase rotate each frequency bin in F(k) by WN^(k) and then
// add [(a + bj) + (c + dj)] to each frequency bin.
//
// One cmplx multiplication and two cmplx additions per frequency bin are
// therefore required, per sample period, regardless of the size of the transform.
//
// For example, a traditional 1024-point FFT needs 5120 cmplx multiplies
// and 10240 cmplx additions to calculate all 1024 frequency bins. A 1024-point
// Sliding FFT however needs 1024 cmplx multiplies and 2048 cmplx additions
// for all 1024 frequency bins, and as each frequency bin is calculated separately,
// it is only necessary to calculate the ones that are of interest.
//
// One drawback of the Sliding FFT is that in using feedback from previous
// frequency bins, there is potential for instability if the coefficients are not
// infinitely precise. Without infinite precision, stability can be guaranteed by
// making each phase-rotation coefficient have a magnitude of slightly less than
// unity. E.g. 0.9999.
//
// This then has to taken into account when the Nth sample is subtracted, because
// the factor 0.9999 has been applied N times to the transform of this sample.
// The sample cannot therefore be directly subtracted, it must first be multiplied
// by the factor of 0.9999^N. This unfortunately means there is another multipli-
// cation to perform per frequency bin. Another drawback is that a circular buffer
// is needed in which to keep N samples, so that the oldest sample, (from t= -N),
// can be subtracted each time.
//
// This filter is ideal for extracting a finite number of frequency bins
// with a very long kernel length. The filter only needs to calculate the
// values for the bins of interest and not the entire spectrum. It does
// require the store of the history associated with those bins over the
// kernel length.
//
// Use in the MFSK / DOMINO modem for extraction of the frequency spectra
//
//=====================================================================
struct sfft::vrot_bins_pair {
cmplx vrot;
cmplx bins;
} ;
sfft::sfft(int len, int _first, int _last)
{
vrot_bins = new vrot_bins_pair[len];
delay = new cmplx[len];
fftlen = len;
first = _first;
last = _last;
ptr = 0;
double phi = 0.0, tau = 2.0 * M_PI/ len;
k2 = 1.0;
for (int i = 0; i < fftlen; i++) {
vrot_bins[i].vrot = cmplx( K1 * cos (phi), K1 * sin (phi) );
phi += tau;
delay[i] = vrot_bins[i].bins = 0.0;
k2 *= K1;
}
count = 0;
}
sfft::~sfft()
{
delete [] vrot_bins;
delete [] delay;
}
void sfft::reset()
{
for (int i = 0; i < fftlen; i++) delay[i] = vrot_bins[i].bins = 0.0;
count = 0;
}
bool sfft::is_stable()
{
return (count >= fftlen);
}
// Sliding FFT, cmplx input, cmplx output
// FFT is computed for each value from first to last
// Values are not stable until more than "len" samples have been processed.
// Copies the frequencies to a pointer with a given stride.
void sfft::run(const cmplx& input, cmplx * __restrict__ result, int stride )
{
cmplx & de = delay[ptr];
const cmplx z( input.real() - k2 * de.real(), input.imag() - k2 * de.imag());
de = input;
++ptr ;
if( ptr >= fftlen ) ptr = 0 ;
// It is more efficient to have vrot and bins very close to each other.
for( vrot_bins_pair
* __restrict__ itr = vrot_bins + first,
* __restrict__ end = vrot_bins + last ;
itr != end ;
++itr, result += stride ) {
*result = itr->bins = itr->bins * itr->vrot + z * itr->vrot;
}
if (count < fftlen) count++;
}
// ============================================================================
// Goertzel filter
// Optimized implementation of a DFT for a single frequency of interest
// SR = sample rate
// N = Block size (does not need to be a factor of 2!)
// bin size = SR / N
// K = frequency bin of interest = (N * freq / SR)
// N should be selected to make K an integer if possible
//
// Q0 = current sample
// Q1 = previous sample (1 delay)
// Q2 = previous sample (2 delay)
// w = (2 * pi * K / N)
// k1 = cos(w)
// k2 = sin(w)
// k3 = 2.0 * k1
// Q0, Q1, Q2 are initialized to zero
// Iterate N times:
// Q0 = k3*Q1 - Q2 + sample
// Q2 = Q1
// Q1 = Q0
//
// After N interations:
// real = (Q1 - Q2 * k1)
// imag = Q2 * k2
// or
// mag = Q1*Q1 + Q2*Q2 - Q1*Q2*k1
// ============================================================================
goertzel::goertzel(int n, double freq, double sr)
{
double w;
w = 2 * M_PI * freq / sr;
k1 = cos(w);
k2 = sin(w);
k3 = 2.0 * k1;
Q0 = Q1 = Q2 = 0.0;
count = N = n;
}
goertzel::~goertzel()
{
}
void goertzel::reset()
{
Q0 = Q1 = Q2 = 0.0;
count = N;
}
void goertzel::reset(int n, double freq, double sr)
{
double w;
w = 2 * M_PI * freq / sr;
k1 = cos(w);
k2 = sin(w);
k3 = 2.0 * k1;
Q0 = Q1 = Q2 = 0.0;
count = N = n;
}
bool goertzel::run(double sample)
{
Q0 = sample + k3*Q1 - Q2;
Q2 = Q1;
Q1 = Q0;
if (count) { --count; return false; }
return true;
}
double goertzel::real()
{
return ((0.5*k3*Q1 - Q2)/N);
}
double goertzel::imag()
{
return ((k2*Q1)/N);
}
double goertzel::mag()
{
return (Q2*Q2 + Q1*Q1 - k3*Q2*Q1);
}
+183
View File
@@ -0,0 +1,183 @@
// ----------------------------------------------------------------------------
//
// filters.h -- Several Digital Filter classes used in fldigi
//
// Copyright (C) 2006-2008
// Dave Freese, W1HKJ
//
// This file is part of fldigi. These filters are based on the
// gmfsk design and the design notes given in
// "Digital Signal Processing", A Practical Guid for Engineers and Scientists
// by Steven W. Smith.
//
// Fldigi 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.
//
// Fldigi 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 fldigi. If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
#ifndef _FILTER_H
#define _FILTER_H
#include "complex.h"
#ifndef M_PI
#define M_PI (3.1415926535897932385)
#endif
//=====================================================================
// FIR filters
//=====================================================================
class C_FIR_filter {
#define FIRBufferLen 4096
private:
int length;
int decimateratio;
double *ifilter;
double *qfilter;
double ffreq;
double ibuffer[FIRBufferLen];
double qbuffer[FIRBufferLen];
int pointer;
int counter;
cmplx fu;
inline double sinc(double x) {
if (fabs(x) < 1e-10)
return 1.0;
else
return sin(M_PI * x) / (M_PI * x);
}
inline double cosc(double x) {
if (fabs(x) < 1e-10)
return 0.0;
else
return (1.0 - cos(M_PI * x)) / (M_PI * x);
}
inline double hamming(double x) {
return 0.54 - 0.46 * cos(2 * M_PI * x);
}
inline double mac(const double *a, const double *b, unsigned int size) {
double sum = 0.0;
double sum2 = 0.0;
double sum3 = 0.0;
double sum4 = 0.0;
// Reduces read-after-write dependencies : Each subsum does not wait for the others.
// The CPU can therefore schedule each line independently.
for (; size > 3; size -= 4, a += 4, b+=4)
{
sum += a[0] * b[0];
sum2 += a[1] * b[1];
sum3 += a[2] * b[2];
sum4 += a[3] * b[3];
}
for (; size; --size)
sum += (*a++) * (*b++);
return sum + sum2 + sum3 + sum4 ;
}
protected:
public:
C_FIR_filter ();
~C_FIR_filter ();
void init (int len, int dec, double *ifil, double *qfil);
void init_lowpass (int len, int dec, double freq );
void init_bandpass (int len, int dec, double freq1, double freq2);
void init_hilbert (int len, int dec);
double *bp_FIR(int len, int hilbert, double f1, double f2);
void clear();
int run (const cmplx &in, cmplx &out);
int Irun (const double &in, double &out);
int Qrun (const double &in, double &out);
};
//=====================================================================
// Moving average filter
//=====================================================================
class Cmovavg {
#define MAXMOVAVG 2048
private:
double *in;
double out;
int len, pint;
bool empty;
public:
Cmovavg(int filtlen = 64);
~Cmovavg();
double run(double a);
void setLength(int filtlen);
void reset();
double value() { return out / (len > 0 ? len : 1); }
};
//=====================================================================
// Sliding FFT
//=====================================================================
class sfft {
#define K1 0.99999999999L
private:
int fftlen;
int first;
int last;
int ptr;
struct vrot_bins_pair ;
vrot_bins_pair * __restrict__ vrot_bins ;
cmplx * __restrict__ delay;
double k2;
int count;
public:
sfft(int len, int first, int last);
~sfft();
bool is_stable();
void reset();
void run(const cmplx& input, cmplx * __restrict__ result, int stride );
};
//=============================================================================
// Goertzel DFT
//=============================================================================
class goertzel {
private:
int N;
int count;
double Q0;
double Q1;
double Q2;
double k1;
double k2;
double k3;
public:
goertzel(int n, double freq, double sr);
~goertzel();
void reset();
void reset(int n, double freq, double sr);
bool run(double v);
double real();
double imag();
double mag();
};
#endif /* _FILTER_H */
File diff suppressed because it is too large. Load diff
+258
View File
@@ -0,0 +1,258 @@
// ----------------------------------------------------------------------------
// mbuffer.h
//
// Copyright (C) 2007
// Stelios Bounanos, M0GLD
//
// This file is part of fldigi.
//
// fldigi 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.
//
// fldigi 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 <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------
// A simple vector wrapper for fldigi's double-buffering needs.
// Most vector operations are provided for mbuffers by redirecting them to the
// current vector.
// The template arguments are
// 1) T - the type
// 2) S - the apparent mbuffer size, i.e., the size of each vector. Defaults to 0.
// A mbuffer instantiated with S == 0 is not very useful until resized
// with alloc().
// 3) N - the number of vectors that we can cycle between. Defaults to 1.
// Things to note:
// 1) There is a T* conversion operator
// 2) Operations that modify the length of the container are not provided
// 3) Comparison operators are not implemented (but see (1)!)
// 4) mbuffer<T, 0, N> is meant to be used when we don't know the size at
// compile time, in which case we resize with alloc. The compiler will treat
// mbuffers resized to different lengths in this way as objects of the same type.
#ifndef MBUFFER_H
#define MBUFFER_H
#include <vector>
#include <algorithm>
#ifndef NDEBUG
#include <iosfwd>
#include <iterator>
#endif
#ifndef NDEBUG
template <typename T, std::size_t S, std::size_t N>
class mbuffer;
template <typename T, std::size_t S, std::size_t N>
std::ostream& operator<<(std::ostream& o, const mbuffer<T, S, N>& b);
#endif // NDEBUG
template <typename T, std::size_t S = 0, std::size_t N = 1>
class mbuffer
{
protected:
std::vector<T> data[N];
mutable std::size_t cur;
public:
explicit mbuffer(void)
{
alloc(S);
}
explicit mbuffer(std::size_t n)
{
alloc(n);
}
mbuffer(const std::vector<T>& v)
{
data[0] = v;
// resize 1 to N only
alloc(data[0].size(), 1);
}
mbuffer(const T* a, std::size_t n)
{
data[0].assign(a, a + n);
// resize 1 to N only
alloc(n, 1);
}
void alloc(std::size_t n, std::size_t start = 0)
{
for (size_t i = start; i < N; ++i)
data[i].resize(n);
cur = 0;
}
typedef typename std::vector<T>::iterator iterator;
typedef typename std::vector<T>::const_iterator const_iterator;
iterator begin(void) { return data[cur].begin(); }
const_iterator begin(void) const { return data[cur].begin(); }
iterator end(void) { return data[cur].end(); }
const_iterator end(void) const { return data[cur].end(); }
typedef typename std::vector<T>::reverse_iterator reverse_iterator;
typedef typename std::vector<T>::const_reverse_iterator const_reverse_iterator;
reverse_iterator rbegin(void) { return reverse_iterator(end()); }
const_reverse_iterator rbegin(void) const { return const_reverse_iterator(end()); }
reverse_iterator rend(void) { return reverse_iterator(begin()); }
const_reverse_iterator rend(void) const { return const_reverse_iterator(begin()); }
typedef typename std::vector<T>::value_type value_type;
typedef typename std::vector<T>::reference reference;
typedef typename std::vector<T>::const_reference const_reference;
typedef typename std::vector<T>::size_type size_type;
typedef typename std::vector<T>::difference_type difference_type;
// These should be the same for all vectors in data[]
size_type size(void) { return data[0].size(); }
size_type max_size(void) { return data[0].max_size(); }
size_type capacity(void) { return data[0].capacity(); }
bool empty(void) { return data[0].empty(); }
// Instead of these, we provide a conversion operator for T*
// reference operator[](size_type i) { return data[cur][i]; }
// const_reference operator[](size_type i) const { return data[cur][i]; }
reference at(size_type i) { return data[cur].at(i); }
const_reference at(size_type i) const { return data[cur].at(i); }
reference front(void) { return data[cur].front(); }
const_reference front(void) const { return data[cur].front(); }
reference back(void) { return data[cur].back(); }
const_reference back(void) const { return data[cur].back(); }
// Operations that modify the size of data[cur] might invalidate
// pointers to internal buffers. The rest of the data vectors would need
// to be resized, e.g. with check_size below. For this reason these
// operations are not provided, but are included here for completeness
// void check_size(void)
// {
// for (size_t i = 0; i < N; ++i)
// if (data[i].size() != data[cur].size())
// data[i].resize(data[cur].size());
// }
// mbuffer<T, S, N>& operator=(const mbuffer<T, S, N>& o)
// {
// for (int i = 0; i < N; ++i)
// std::copy(o.data[i].begin(), o.data[i].end(), data[i].begin());
// return *this;
// }
// std::vector<T>& operator=(const std::vector<T>& o)
// {
// std::copy(o.begin(), o.end(), data[cur].begin());
// check_size();
// return data[cur];
// }
// The methods below would modify the length of the vector.
// We would need to check_size() before returning from them.
// There is no vector::assign; the one provided here fills the vector
// with copies of the same value without causing a resize.
void assign(const_reference v) { std::fill_n(begin(), size(), v); }
// template <typename input_iterator>
// void assign(input_iterator first, input_iterator last)
// {
// data[cur].assign(first, last);
// }
// void push_back(const_reference v) { data[cur].push_back(v); }
// void pop_back(void) { data[cur].pop_back(); }
// iterator insert(iterator pos, const_reference v)
// {
// return data[cur].insert(pos, v);
// }
// iterator insert(iterator pos, size_type n, const_reference v)
// {
// return data[cur].insert(pos, n, v);
// }
// template <typename input_iterator>
// void insert(iterator pos, input_iterator first, input_iterator last)
// {
// data[cur].insert(pos, first, last);
// }
// iterator erase(iterator pos) { return data[cur].erase(pos); }
// iterator erase(iterator first, iterator last) { return data[cur].erase(first, last); }
// void clear(void) { data[cur].clear(); }
void swap(mbuffer<T, S, N>& o)
{
for (int i = 0; i < N; ++i)
std::swap(data[i].begin(), data[i].end(), o.data[i].begin());
}
// void swap(std::vector<T>& o)
// {
// std::swap(data[cur].begin(), data[cur].end(), o.begin());
// check_size();
// o.check_size();
// }
// and now for something completely different
void next(void) const { if (++cur == N) cur = 0; }
void prev(void) const { if (cur > 0) --cur; }
void reset(void) const { cur = 0; }
T* c_array(void) { return &data[cur][0]; }
operator T*(void) { return c_array(); }
const T* c_array(void) const { return &data[cur][0]; }
operator const T*(void) const { return c_array(); }
std::vector<T>& vec(void) { return data[cur]; }
const std::vector<T>& vec(void) const { return data[cur]; }
// We also do not provide vector<T> conversions
// operator std::vector<T>&(void) { return vec(); }
// operator const std::vector<T>&(void) const { return vec(); }
std::size_t idx(void) { return cur; }
std::size_t nvec(void) { return N; }
std::vector<T>* vecp(std::size_t i) { return &data[i]; }
#ifndef NDEBUG
friend std::ostream& operator<<<>(std::ostream& o, const mbuffer<T, S, N>& b);
#endif // NDEBUG
};
#ifndef NDEBUG
template <typename T, std::size_t S, std::size_t N>
std::ostream& operator<<(std::ostream& o, const mbuffer<T, S, N>& b)
{
for (std::size_t i = 0; i < N; ++i) {
o << '<' << i << ">\n";
copy(b.data[i].begin(), b.data[i].end(),
std::ostream_iterator<T>(o, "\n"));
}
return o;
}
#endif // NDEBUG
template <typename T, std::size_t S, std::size_t N>
inline
void swap(mbuffer<T, S, N>& a, mbuffer<T, S, N>& b) { a.swap(b); }
#endif // MBUFFER_H
// Local Variables:
// mode: c++
// c-file-style: "linux"
// End:
+160
View File
@@ -0,0 +1,160 @@
// ----------------------------------------------------------------------------
// modem.h -- minimal modem base class for fldigi CW decoder (Android)
// Extracted from fldigi/src/include/modem.h (GPL v3).
// ----------------------------------------------------------------------------
#ifndef _MODEM_H
#define _MODEM_H
#include <string>
#include <cmath>
#include "morse.h"
#include "filters.h"
#define OUTBUFSIZE 65536
enum trx_mode {
MODE_CW = 0
};
struct mode_info_t {
const char *sname;
unsigned int iface_io;
};
namespace Digiscope {
enum scope_mode { SCOPE, SCOPE2, PHASE, PHASE2, WATERFALL, NONE };
}
class modem {
public:
static double frequency;
static double tx_frequency;
static bool freqlock;
static unsigned long tx_sample_count;
static unsigned int tx_sample_rate;
static bool XMLRPC_CPS_TEST;
protected:
cMorse *morse;
trx_mode mode;
bool stopflag;
int fragmentsize;
int samplerate;
bool reverse;
int sigsearch;
bool sig_start;
bool sig_stop;
double bandwidth;
double freqerr;
double rx_corr;
double tx_corr;
double PTTphaseacc;
double PTTchannel[OUTBUFSIZE];
bool cwTrack;
bool cwLock;
double cwRcvWPM;
double cwXmtWPM;
double squelch;
double metric;
double syncpos;
int backspaces;
unsigned char *txstr;
unsigned char *txptr;
double outbuf[OUTBUFSIZE];
bool historyON;
Digiscope::scope_mode scopemode;
int scptr;
double s2n_ncount, s2n_sum, s2n_sum2, s2n_metric;
bool s2n_valid;
unsigned cap;
std::string audio_filename;
bool play_audio;
bool CW_EOT;
public:
modem();
virtual ~modem() { delete morse; }
virtual void init();
virtual void tx_init() = 0;
virtual void rx_init() = 0;
virtual void restart() = 0;
virtual void rx_flush() {}
virtual int tx_process();
virtual int rx_process(const double *, int len) = 0;
virtual void Audio_filename(std::string nm) { audio_filename = nm; play_audio = true; }
virtual void shutdown() {}
virtual void set1(int, int) {}
virtual void set2(int, int) {}
virtual void makeTxViewer(int W, int H) {}
virtual void searchDown() {}
virtual void searchUp() {}
void HistoryON(bool val) { historyON = val; }
bool HistoryON() const { return historyON; }
trx_mode get_mode() const { return mode; }
const char *get_mode_name() const;
unsigned int iface_io() const;
virtual void set_freq(double);
int get_freq() const { return (int)(frequency + 0.5); }
void init_freqlock();
void set_freqlock(bool);
void set_sigsearch(int n) { sigsearch = n; freqerr = 0.0; }
bool freqlocked() const { return freqlock; }
double get_txfreq() const;
double get_txfreq_woffset() const;
void set_metric(double);
void display_metric(double);
double get_metric() const { return metric; }
void set_reverse(bool on);
bool get_reverse() const { return reverse; }
double get_bandwidth() const { return bandwidth; }
void set_bandwidth(double);
int get_samplerate() const { return samplerate; }
void set_samplerate(int);
void init_queues();
void ModulateXmtr(double *, int);
void ModulateStereo(double *, double *, int, bool sample_flag = true);
void ModulateVideo(double *, int);
void ModulateVideoStereo(double *, double *, int, bool sample_flag = true);
void videoText();
void pretone();
virtual void send_color_image(std::string) {}
virtual void send_Grey_image(std::string) {}
virtual void ifkp_send_image(std::string s = "", bool grey = false) {}
virtual void ifkp_send_avatar() {}
virtual void m_ifkp_send_avatar() {}
virtual void thor_send_image(std::string s = "", bool grey = false) {}
virtual void thor_send_avatar() {}
virtual void m_thor_send_avatar() {}
void set_stopflag(bool b) { stopflag = b; }
bool get_stopflag() const { return stopflag; }
unsigned get_cap() const { return cap; }
enum { CAP_AFC = 1 << 0, CAP_AFC_SR = 1 << 1, CAP_REV = 1 << 2,
CAP_IMG = 1 << 3, CAP_BW = 1 << 4, CAP_RX = 1 << 5,
CAP_TX = 1 << 6 };
bool get_cwTrack();
void set_cwTrack(bool);
bool get_cwLock();
void set_cwLock(bool);
double get_cwXmtWPM();
void set_cwXmtWPM(double);
double get_cwRcvWPM();
virtual void CW_KEYLINE(bool) {}
virtual void incWPM() {}
virtual void decWPM() {}
virtual void toggleWPM() {}
virtual void sync_parameters() {}
virtual void reset_rx_filter() {}
virtual void update_Status() {}
virtual void refresh_scope() {}
virtual void clear_viewer() {}
virtual void clear_ch(int n) {}
virtual int viewer_get_freq(int n) { return 0; }
double calWPM() { return 20; }
void calWPM(double) {}
void resetFSK() {}
void s2nreport() {}
void set_scope_mode(Digiscope::scope_mode sm) { scopemode = sm; }
};
#endif
+314
View File
@@ -0,0 +1,314 @@
/*
* morse.c -- morse code tables
*
* Copyright (C) 2017
*
* Fldigi 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.
*
* Fldigi 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 fldigi. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include <config.h>
#include <cstring>
#include <iostream>
#include "morse.h"
#include "configuration.h"
/* ---------------------------------------------------------------------- */
/*
* Morse code characters table. This table allows lookup of the Morse
* shape of a given alphanumeric character. Shapes are held as a string,
* with "-' representing dash, and ".' representing dot. The table ends
* with a NULL entry.
*
* This is the main table from which the other tables are computed.
*
* The Prosigns are also defined in the configuration.h file
* The user can specify the character which substitutes for the prosign
*/
bool CW_table_changed = false;
CWstruct cMorse::cw_table[] = {
// Prosigns
{1, "=", "<BT>", "-...-" }, // 0
{0, "~", "<AA>", ".-.-" }, // 1
{1, "<", "<AS>", ".-..." }, // 2
{1, ">", "<AR>", ".-.-." }, // 3
{1, "%", "<SK>", "...-.-" }, // 4
{1, "+", "<KN>", "-.--." }, // 5
{1, "&", "<INT>", "..-.-" }, // 6
{1, "{", "<HM>", "....--" }, // 7
{1, "}", "<VE>", "...-." }, // 8
// ASCII 7bit letters
{1, "A", "A", ".-" },
{1, "B", "B", "-..." },
{1, "C", "C", "-.-." },
{1, "D", "D", "-.." },
{1, "E", "E", "." },
{1, "F", "F", "..-." },
{1, "G", "G", "--." },
{1, "H", "H", "...." },
{1, "I", "I", ".." },
{1, "J", "J", ".---" },
{1, "K", "K", "-.-" },
{1, "L", "L", ".-.." },
{1, "M", "M", "--" },
{1, "N", "N", "-." },
{1, "O", "O", "---" },
{1, "P", "P", ".--." },
{1, "Q", "Q", "--.-" },
{1, "R", "R", ".-." },
{1, "S", "S", "..." },
{1, "T", "T", "-" },
{1, "U", "U", "..-" },
{1, "V", "V", "...-" },
{1, "W", "W", ".--" },
{1, "X", "X", "-..-" },
{1, "Y", "Y", "-.--" },
{1, "Z", "Z", "--.." },
//
{1, "a", "A", ".-" },
{1, "b", "B", "-..." },
{1, "c", "C", "-.-." },
{1, "d", "D", "-.." },
{1, "e", "E", "." },
{1, "f", "F", "..-." },
{1, "g", "G", "--." },
{1, "h", "H", "...." },
{1, "i", "I", ".." },
{1, "j", "J", ".---" },
{1, "k", "K", "-.-" },
{1, "l", "L", ".-.." },
{1, "m", "M", "--" },
{1, "n", "N", "-." },
{1, "o", "O", "---" },
{1, "p", "P", ".--." },
{1, "q", "Q", "--.-" },
{1, "r", "R", ".-." },
{1, "s", "S", "..." },
{1, "t", "T", "-" },
{1, "u", "U", "..-" },
{1, "v", "V", "...-" },
{1, "w", "W", ".--" },
{1, "x", "X", "-..-" },
{1, "y", "Y", "-.--" },
{1, "z", "Z", "--.." },
// Numerals
{1, "0", "0", "-----" },
{1, "1", "1", ".----" },
{1, "2", "2", "..---" },
{1, "3", "3", "...--" },
{1, "4", "4", "....-" },
{1, "5", "5", "....." },
{1, "6", "6", "-...." },
{1, "7", "7", "--..." },
{1, "8", "8", "---.." },
{1, "9", "9", "----." },
// Punctuation
{1, "\\", "\\", ".-..-." },
{1, "\'", "'", ".----." },
{1, "$", "$", "...-..-" },
{1, "(", "(", "-.--." },
{1, ")", ")", "-.--.-" },
{1, ",", ",", "--..--" },
{1, "-", "-", "-....-" },
{1, ".", ".", ".-.-.-" },
{1, "/", "/", "-..-." },
{1, ":", ":", "---..." },
{1, ";", ";", "-.-.-." },
{1, "?", "?", "..--.." },
{1, "_", "_", "..--.-" },
{1, "@", "@", ".--.-." },
{1, "!", "!", "-.-.--" },
// accented characters
{1, "Ä", "Ä", ".-.-" }, // A umlaut
{1, "ä", "Ä", ".-.-" }, // A umlaut
{0, "Æ", "Æ", ".-.-" }, // A aelig
{0, "æ", "Æ", ".-.-" }, // A aelig
{0, "Å", "Å", ".--.-" }, // A ring
{0, "å", "Å", ".--.-" }, // A ring
{1, "Ç", "Ç", "-.-.." }, // C cedilla
{1, "ç", "Ç", "-.-.." }, // C cedilla
{0, "È", "È", ".-..-" }, // E grave
{0, "è", "È", ".-..-" }, // E grave
{1, "É", "É", "..-.." }, // E acute
{1, "é", "É", "..-.." }, // E acute
{0, "Ó", "Ó", "---." }, // O acute
{0, "ó", "Ó", "---." }, // O acute
{1, "Ö", "Ö", "---." }, // O umlaut
{1, "ö", "Ö", "---." }, // O umlaut
{0, "Ø", "Ø", "---." }, // O slash
{0, "ø", "Ø", "---." }, // O slash
{1, "Ñ", "Ñ", "--.--" }, // N tilde
{1, "ñ", "Ñ", "--.--" }, // N tilde
{1, "Ü", "Ü", "..--" }, // U umlaut
{1, "ü", "Ü", "..--" }, // U umlaut
{0, "Û", "Û", "..--" }, // U circ
{0, "û", "Û", "..--" }, // U circ
// array termination
{0, "", "", ""}
};
/* ---------------------------------------------------------------------- */
void cMorse::enable(std::string s, bool val)
{
for (int i = 0; cw_table[i].rpr.length(); i++) {
if (cw_table[i].chr == s || cw_table[i].prt == s) {
cw_table[i].enabled = val;
return;
}
}
}
void cMorse::init()
{
// Update the char / prosign relationship
if (progdefaults.CW_prosigns.length() == 9) {
for (int i = 0; i < 9; i++) {
cw_table[i].chr = progdefaults.CW_prosigns[i];
}
}
enable("<AA>", 1);
enable("Ä", 0); enable("ä", 0);
enable("Æ", 0); enable("æ", 0);
enable("Å", 0); enable("å", 0);
enable("Ç", 0); enable("ç", 0);
enable("È", 0); enable("è", 0);
enable("É", 0); enable("é", 0);
enable("Ó", 0); enable("ó", 0);
enable("Ö", 0); enable("ö", 0);
enable("Ø", 0); enable("ø", 0);
enable("Ñ", 0); enable("ñ", 0);
enable("Ü", 0); enable("ü", 0);
enable("Û", 0); enable("û", 0);
if (progdefaults.A_umlaut)
{ enable("Ä", 1); enable("ä", 1); enable("<AA>", 0); }
if (progdefaults.A_aelig)
{ enable("Æ", 1); enable("æ", 1); enable("<AA>", 0); }
if (progdefaults.A_ring)
{ enable("Å", 1); enable("å", 1); }
if (progdefaults.C_cedilla)
{ enable("Ç", 1); enable("ç", 1); }
if (progdefaults.E_grave)
{ enable("È", 1); enable("è", 1); }
if (progdefaults.E_acute)
{ enable("É", 1); enable("é", 1); }
if (progdefaults.O_acute)
{ enable("Ó", 1); enable("ó", 1); }
if (progdefaults.O_umlaut)
{ enable("Ö", 1); enable("ö", 1); }
if (progdefaults.O_slash)
{ enable("Ø", 1); enable("ø", 1); }
if (progdefaults.N_tilde)
{ enable("Ñ", 1); enable("ñ", 1); }
if (progdefaults.U_umlaut)
{ enable("Ü", 1); enable("ü", 1); }
if (progdefaults.U_circ)
{ enable("Û", 1); enable("û", 1); }
enable ("\\", progdefaults.CW_backslash);
enable ("\'", progdefaults.CW_single_quote);
enable ("$", progdefaults.CW_dollar_sign);
enable ("(", progdefaults.CW_open_paren);
enable (")", progdefaults.CW_close_paren);
enable (":", progdefaults.CW_colon);
enable (";", progdefaults.CW_semi_colon);
enable ("_", progdefaults.CW_underscore);
enable ("@", progdefaults.CW_at_symbol);
enable ("!", progdefaults.CW_exclamation);
CW_table_changed = false;
utf8.reserve(4);
utf8.clear();
ptr = 0;
toprint.clear();
}
std::string cMorse::rx_lookup(std::string rx)
{
if (CW_table_changed) init();
for (int i = 0; cw_table[i].rpr.length(); i++) {
if (rx == cw_table[i].rpr) {
if (cw_table[i].enabled) {
if (progdefaults.CW_prosign_display)
return cw_table[i].chr;
return cw_table[i].prt;
}
}
}
return "";
}
std::string cMorse::tx_lookup(int c)
{
if (CW_table_changed) init();
toprint.clear();
c &= 0xFF;
utf8 += c;
// if (ptr < 4) utf8[ptr++] = c;
// if ( c > 0x7F && ptr == 1 )
// return "";
if (((utf8[0] & 0xFF) > 0x7F) && (utf8.length() == 1)) {
return "";
}
for (int i = 0; cw_table[i].rpr.length(); i++) {
if (utf8 == cw_table[i].chr) {
if (!cw_table[i].enabled) {
utf8.clear();
ptr = 0;
return "";
}
toprint = cw_table[i].prt;
utf8.clear();
ptr = 0;
return cw_table[i].rpr;
}
}
utf8.clear();
ptr = 0;
return "";
}
/*
Morse Code timing rules
The length of a dot is 1 time unit.
A dash is 3 time units.
The space between symbols (dots and dashes) of the same letter is 1 time unit.
The space between letters is 3 time units.
The space between words is 7 time units.
*/
int cMorse::tx_length(int c)
{
if (c == ' ') return 4;
std::string ms = tx_lookup(c);
if (ms.empty()) return 0;
int len = 0;
for (size_t i = 0; i < ms.length(); i++)
if (ms[i] == '.') len += 2;
else len += 4;
len += 2;
return len;
}
/* ---------------------------------------------------------------------- */
+58
View File
@@ -0,0 +1,58 @@
/*
* morse.h -- morse code tables
*
* Copyright (C) 2017
*
* Fldigi 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.
*
* Fldigi 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 fldigi. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef _MORSE_H
#define _MORSE_H
#include <string>
#define MorseTableSize 256
#define CW_DOT_REPRESENTATION '.'
#define CW_DASH_REPRESENTATION '-'
struct CWstruct {
bool enabled; // true if character is active
std::string chr; // utf-8 string representation of character
std::string prt; // utf-8 printable representation
std::string rpr; // Dot-dash code representation
};
class cMorse {
private:
static CWstruct cw_table[];
std::string utf8;
std::string toprint;
int ptr;
public:
cMorse() {
init();
}
~cMorse() {
}
void init();
void enable(std::string, bool);
std::string rx_lookup(std::string);
std::string tx_lookup(int);
std::string tx_print() { return toprint; }
int tx_length(int);
};
#endif
+24
View File
@@ -0,0 +1,24 @@
// ----------------------------------------------------------------------------
// view_cw.h -- placeholder for fldigi CW waterfall view (Android)
// Replaces fldigi/src/include/view_cw.h (FLTK-based). Stub for compilation.
// ----------------------------------------------------------------------------
#ifndef _VIEW_CW_H
#define _VIEW_CW_H
#include <vector>
class view_cw {
public:
view_cw() {}
~view_cw() {}
void restart() {}
void setFreq(double freq) {}
void setSampleRate(int sr) {}
// Data buffer for waterfall (populated by cw decoder, read by Android UI)
std::vector<float> spectrum;
};
#endif
+88
View File
@@ -0,0 +1,88 @@
// ----------------------------------------------------------------------------
// fldigi_cw_jni.cpp -- JNI wrapper for fldigi CW decoder (Android)
// Copyright (C) 2026 atsunatsu
// GPL v3
// ----------------------------------------------------------------------------
#include <jni.h>
#include <cstring>
#include "fldigi/cw.h"
#include "fldigi/android_compat.h"
// ============================================================================
// Global definitions (declared extern in android_compat.h / used by cw.cxx)
// ============================================================================
waterfall_stub* wf = new waterfall_stub();
AndroidProgDefaults progdefaults;
AndroidProgStatus progStatus;
bool use_nanoIO = false;
void set_nanoWPM(int wpm) {}
void set_nanoCW() {}
// UI/status stubs
void put_cwRcvWPM(int) {}
void put_MODEstatus(const char*, ...) {}
void set_scope_xaxis_1(double) {}
void set_scope_xaxis(double) {}
void update_Status() {}
// misc helpers
void set_scope_mode(int) {}
void put_rx_char(int c) {}
void put_echo_char(int c) {}
double zmsec() { return 0.0; }
void MilliSleep(int) {}
// ============================================================================
// JNI Exports
// ============================================================================
extern "C" JNIEXPORT jlong JNICALL
Java_com_rtbishop_look4sat_feature_cw_FldigiNative_create(JNIEnv* env, jobject thiz) {
cw* decoder = new cw();
decoder->init();
return reinterpret_cast<jlong>(decoder);
}
extern "C" JNIEXPORT void JNICALL
Java_com_rtbishop_look4sat_feature_cw_FldigiNative_destroy(JNIEnv* env, jobject thiz, jlong handle) {
cw* decoder = reinterpret_cast<cw*>(handle);
if (decoder) {
decoder->rx_init();
delete decoder;
}
}
extern "C" JNIEXPORT void JNICALL
Java_com_rtbishop_look4sat_feature_cw_FldigiNative_process(JNIEnv* env, jobject thiz, jlong handle, jfloatArray audio) {
cw* decoder = reinterpret_cast<cw*>(handle);
if (!decoder) return;
jsize len = env->GetArrayLength(audio);
jfloat* elements = env->GetFloatArrayElements(audio, nullptr);
if (!elements) return;
// Convert float to double
double* buf = new double[len];
for (jsize i = 0; i < len; i++) {
buf[i] = static_cast<double>(elements[i]);
}
decoder->rx_process(buf, len);
delete[] buf;
env->ReleaseFloatArrayElements(audio, elements, JNI_ABORT);
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_rtbishop_look4sat_feature_cw_FldigiNative_getDecodedText(JNIEnv* env, jobject thiz, jlong handle) {
cw* decoder = reinterpret_cast<cw*>(handle);
if (!decoder) return env->NewStringUTF("");
std::string text = decoder->get_rx_text();
return env->NewStringUTF(text.c_str());
}
@@ -59,7 +59,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.widget.ConstraintLayout
import com.ve3nea.morse_expert.MainActivity
@Composable
fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
@@ -67,8 +66,8 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
val activity = remember(context) {
context as? Activity ?: error("CwDecodeScreen must be hosted in an Activity")
}
// 照搬的控制器(普通类, 非 Activity); 每次进入页面新建实例
val controller = remember { MainActivity() }
// fldigi 解码器控制器; 每次进入页面新建实例
val controller = remember { FldigiCwController() }
// 提前 inflate 原版布局, 供 AndroidView 与控制器 onCreate 共用同一根视图
val rootView = remember(context) {
LayoutInflater.from(context).inflate(R.layout.activity_main, null) as ConstraintLayout
@@ -44,7 +44,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.ve3nea.morse_expert.MainActivity
import com.rtbishop.look4sat.feature.cw.FldigiCwController
import kotlin.math.roundToInt
/** 键名/默认值照搬原 Morse Expert(root_preferences.xml + SettingsActivity 逻辑)。 */
@@ -55,6 +55,30 @@ private const val DEFAULT_TEXT_FONT_SIZE = 18
private const val MIN_FONT_SIZE = 7
private const val MAX_FONT_SIZE = 99
/** 颜色键名(照搬原 Morse Expert I2.b.f663b) */
private val ColorKeys = arrayOf(
"bg_color", "text_color", "text_color_weak",
"call_color", "call_color_weak",
"cq_color", "cq_color_weak",
"rst_color", "rst_color_weak"
)
/** 颜色默认值(照搬原 Morse Expert I2.b.f664d,ARGB int) */
private val ColorDefaults = intArrayOf(
-3084048, -16777216, -5592406,
-65536, -30584,
-16776961, -7829249,
-65281, -30465
)
/** 颜色名称(照搬原 Morse Expert I2.b.c) */
private val ColorNames = arrayOf(
"Background color", "Text color", "Text color, weak",
"Callsign color", "Callsign color, weak",
"CQ color", "CQ color, weak",
"RST color", "RST color, weak"
)
/** 预置色板(9 色, 3x3 网格);原 ColorPreferenceCompat 用第三方 colorpicker, 不引入。 */
private val PaletteColors = listOf(
0xFFD0F0F0.toInt(), 0xFF000000.toInt(), 0xFFFFFFFF.toInt(),
@@ -70,7 +94,7 @@ private val PaletteColors = listOf(
* 点击 OK 保存并调用 controller.onResume() 立即生效; Cancel/点外部仅关闭不保存。
*/
@Composable
fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
fun CwSettingsDialog(controller: FldigiCwController, onDismiss: () -> Unit) {
val activity = controller.mActivity ?: return
val prefs = remember(activity) {
activity.getSharedPreferences(activity.packageName + "_preferences", Context.MODE_PRIVATE)
@@ -86,7 +110,7 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
)
}
var colorValues by remember {
mutableStateOf(IntArray(9) { i -> prefs.getInt(I2.b.f663b[i], I2.b.f664d[i]) })
mutableStateOf(IntArray(9) { i -> prefs.getInt(ColorKeys[i], ColorDefaults[i]) })
}
AlertDialog(
@@ -124,7 +148,7 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
Text("Colors", style = MaterialTheme.typography.titleSmall)
for (i in 0 until 9) {
ColorSettingRow(
title = I2.b.c[i],
title = ColorNames[i],
value = colorValues[i],
onSelect = { selected ->
colorValues = colorValues.copyOf().also { it[i] = selected }
@@ -139,7 +163,7 @@ fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
editor.putString(KEY_MESSAGE_TYPE, messageType)
editor.putInt(KEY_TEXT_FONT_SIZE, fontSize.roundToInt())
for (i in 0 until 9) {
editor.putInt(I2.b.f663b[i], colorValues[i])
editor.putInt(ColorKeys[i], colorValues[i])
}
editor.apply()
// 原 SettingsActivity 返回时由 MainActivity.onResume() 重新读取全部设置; 照搬逻辑已就位
@@ -0,0 +1,205 @@
package com.rtbishop.look4sat.feature.cw
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.view.View
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
* fldigi CW 解码器控制器。
* 替代 com.ve3nea.morse_expert.MainActivity,提供相同生命周期接口:
* onCreate / onResume / onPause / onDestroy / onPermissionGranted
* 内部使用 AudioRecord 采集麦克风,送入 fldigi 原生解码器。
*/
class FldigiCwController {
/** 兼容 CwSettingsDialog 对 controller.mActivity 的引用 */
var mActivity: Activity? = null
private set
private var nativeHandle: Long = 0L
private var audioRecord: AudioRecord? = null
private var isRunning = false
private var audioThread: Thread? = null
private val scope = CoroutineScope(Dispatchers.Main + Job())
private val _decodedText = MutableStateFlow("")
val decodedText: StateFlow<String> = _decodedText.asStateFlow()
// 轮询解码文本
private var pollJob: Job? = null
/** 创建解码器, 保存 Activity 引用。rootView 暂不处理(UI 由 AndroidView 管理)。 */
fun onCreate(activity: Activity, rootView: View?) {
mActivity = activity
if (nativeHandle == 0L) {
nativeHandle = FldigiNative.create()
}
}
/** 授予权限后启动录音和解码线程 */
fun onPermissionGranted() {
startAudioCapture()
}
/** 恢复录音和解码 */
fun onResume() {
if (hasPermission()) {
startAudioCapture()
}
// 启动轮询
pollJob?.cancel()
pollJob = scope.launch {
while (isActive) {
val text = FldigiNative.getDecodedText(nativeHandle)
if (text.isNotEmpty()) {
_decodedText.value += text
}
delay(100L)
}
}
}
/** 暂停录音 */
fun onPause() {
stopAudioCapture()
pollJob?.cancel()
pollJob = null
}
/** 销毁解码器 */
fun onDestroy() {
stopAudioCapture()
pollJob?.cancel()
if (nativeHandle != 0L) {
FldigiNative.destroy(nativeHandle)
nativeHandle = 0L
}
}
/** 获取当前解码文本(供 Compose 读取) */
fun getCurrentText(): String = _decodedText.value
/** 清除解码文本 */
fun clearText() {
_decodedText.value = ""
}
// ── 兼容 MainActivity 接口(CwDecodeScreen 顶部按钮用)──
/** 暂停/恢复解码 */
fun togglePause() {
if (isRunning) {
stopAudioCapture()
} else {
startAudioCapture()
}
}
/** 清除解码文本(= clearText) */
fun clearDecoded() = clearText()
/** 保存解码文本到文件。当前简单实现: 不落盘(可选扩展) */
fun saveText() {
// 可选: 把 _decodedText.value 写入外部存储
}
/** 录音信号保存。占位实现 */
fun recordSignals() {
// 可选扩展
}
/** 返回是否处理了返回键(占位,直接返回 false 让上层导航) */
fun handleBackPress(): Boolean = false
// ── 内部 ──
private fun hasPermission(): Boolean {
val ctx = mActivity ?: return false
return ctx.checkSelfPermission(Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
}
private fun startAudioCapture() {
if (isRunning) return
val ctx = mActivity ?: return
if (!hasPermission()) return
val sampleRate = 8000 // fldigi CW 解码器需要的采样率
val bufferSize = AudioRecord.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
).coerceAtLeast(4096)
try {
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize
)
} catch (e: SecurityException) {
return
}
val record = audioRecord ?: return
if (record.state != AudioRecord.STATE_INITIALIZED) {
record.release()
audioRecord = null
return
}
record.startRecording()
isRunning = true
// 16-bit PCM → float 转换送入解码器
val shortBuf = ShortArray(4096)
val floatBuf = FloatArray(4096)
audioThread = Thread {
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO)
while (isRunning && nativeHandle != 0L) {
val read = record.read(shortBuf, 0, shortBuf.size)
if (read > 0) {
// 转 float [-1.0, 1.0]
for (i in 0 until read) {
floatBuf[i] = shortBuf[i].toFloat() / 32768f
}
FldigiNative.process(nativeHandle, floatBuf.copyOf(read))
}
}
}.apply {
name = "fldigi-cw-audio"
start()
}
}
private fun stopAudioCapture() {
isRunning = false
audioThread?.join(500)
audioThread = null
try {
audioRecord?.apply {
if (recordingState == AudioRecord.RECORDSTATE_RECORDING) {
stop()
}
release()
}
} catch (_: Exception) { }
audioRecord = null
}
}
@@ -0,0 +1,23 @@
package com.rtbishop.look4sat.feature.cw
/**
* JNI 绑定: fldigi CW 解码器原生库。
* 对应 C++ 文件: fldigi_cw_jni.cpp
*/
object FldigiNative {
init {
System.loadLibrary("fldigi_cw")
}
/** 创建解码器实例, 返回 native handle (long) */
external fun create(): Long
/** 销毁解码器 */
external fun destroy(handle: Long)
/** 送入 PCM 音频数据 (float[], mono, 8000Hz), 解码 */
external fun process(handle: Long, audio: FloatArray)
/** 获取并清空积压的解码文本, 返回空字符串表示无新输出 */
external fun getDecodedText(handle: Long): String
}