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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! Transmit-envelope shaping shared by the continuous-phase FSK
//! transmit paths (WSPR, JT65, JT9, Q65).
//!
//! ## Why these four and not FT8/FT4/FST4
//!
//! WSJT-X generates its transmit audio two different ways, selected by
//! the sign of the `toneSpacing` argument `mainwindow.cpp` hands to
//! `Modulator::start`:
//!
//! - **Negative** — "Transmit a pre-computed, filtered waveform"
//! (`mainwindow.cpp`'s own comment). FT8, FT4 and FST4 take this
//! path; `gen_ft8wave.f90` / `gen_ft4wave.f90` / `gen_fst4wave.f90`
//! apply both GFSK symbol shaping *and* a raised-cosine envelope
//! ramp at each end before the samples ever reach the modulator.
//! This crate matches that already, via
//! [`super::gfsk::GfskCfg::ramp_samples`].
//! - **Positive** — plain CPFSK, generated sample-by-sample inside
//! `Modulator::modulate` (`m_phi += m_dphi; sample = m_amp·sin(m_phi)`).
//! WSPR, JT65, JT9 and Q65 take this path, and this crate's
//! `wspr::tx` / `jt65::tx` / `jt9::tx` / `q65::tx` match it — no
//! symbol shaping, deliberately (see issue #259: adding GFSK here
//! would *deviate* from WSJT-X, which shapes none of these).
//!
//! What this crate did *not* match is the envelope. WSJT-X's modulator
//! fades the CPFSK path out at the end of every transmission
//! (`Modulator.cpp`: `i0 = (m_symbolsLength - 0.017)·4·m_nsps`, then
//! `if (m_ic > i0) m_amp = 0.98 · m_amp`), while this crate wrote
//! `amplitude · cos(phase)` from the first sample to the last. A step
//! discontinuity in the envelope is a broadband click, independent of
//! any symbol-transition shaping.
//!
//! ## Why both ends, when WSJT-X's modulator only fades out
//!
//! `Modulator::start` assigns `m_amp = numeric_limits<qint16>::max()`
//! on every transmission, so the CPFSK path genuinely begins at full
//! amplitude — there is no fade-in anywhere in it.
//!
//! That is not a deliberate choice for slow modes, which was the
//! obvious hypothesis and is refuted by WSJT-X's own longest-period
//! mode: `gen_fst4wave.f90` ramps **up** as well as down
//! (`wave(1:nsps/4) *= (1 - cos(…))/2`), unconditionally
//! (`data lshape/.true./`), and FST4's periods run 15 s to 1800 s.
//! Its ramp is `nsps/4` where FT8's is `nsps/8` — *longer* for the
//! slower mode, the opposite of avoiding it. The CPFSK path simply
//! never received the treatment the pre-computed path got when
//! FT8/FT4/FST4 were migrated to it; `mainwindow.cpp` still carries a
//! commented-out `// toneSpacing=-4.0;` beside JT65's positive value,
//! marking an abandoned migration.
//!
//! So: fading out matches WSJT-X's intent for these modes, and fading
//! in matches what WSJT-X does everywhere it generates a buffer rather
//! than streaming one. This crate's transmit functions produce a
//! buffer, which is the `gen_*wave` situation, not the modulator's.
/// Raised-cosine ramp length for the CPFSK transmit paths, in
/// milliseconds.
///
/// Bracketed by WSJT-X's own choices rather than picked freely: its
/// modulator's exponential fade-out (`0.98` per sample at 48 kHz)
/// reaches −60 dB in ≈7.1 ms, `gen_ft8wave`'s `nsps/8` is 20 ms, and
/// `gen_fst4wave`'s `nsps/4` is 81 ms at FST4-60. 10 ms sits inside
/// that range and is under 2 % of a symbol for every protocol using
/// this helper (WSPR 683 ms, JT65 372 ms, JT9 580 ms, Q65 ≥ 128 ms).
pub const RAMP_MS: f32 = 10.0;
/// Ramp length in samples for `sample_rate`, capped at `nsps/8` so a
/// hypothetical short-symbol caller can never taper a meaningful
/// fraction of its first and last symbol.
/// Apply a raised-cosine ramp-up over the first `nramp` samples of
/// `out` and a ramp-down over the last `nramp`.
///
/// Same envelope shape as `gen_ft8wave.f90:69-73` and
/// `gen_fst4wave.f90:75-80` — `(1 − cos(2πi/2N))/2` rising,
/// `(1 + cos(2πi/2N))/2` falling — so every transmit path in this
/// crate tapers identically regardless of which synthesiser produced
/// the samples.
///
/// No-ops when `nramp` is 0. `nramp` is clamped to `out.len()/2` so
/// the two ends cannot overlap on a very short buffer.