| 93 | } |
| 94 | |
| 95 | void CwxLocalKeyer::encode(const QString& text, int wpm) |
| 96 | { |
| 97 | // Standard CW timing: 1 unit = 1.2 / WPM seconds. At 20 WPM → |
| 98 | // 60 ms/unit; matches the FlexRadio keyer's output within ±1 ms. |
| 99 | m_unitMs = qMax(1, static_cast<int>(1200.0 / wpm)); |
| 100 | |
| 101 | const auto& tbl = morseTable(); |
| 102 | bool firstChar = true; |
| 103 | for (QChar ch : text.toUpper()) { |
| 104 | if (ch == ' ') { |
| 105 | // Word gap = 7 units total = WordGap (4u, added here) + |
| 106 | // CharGap (3u, added by the next letter via the !firstChar |
| 107 | // branch below). Critically, do NOT reset firstChar to true: |
| 108 | // doing so would suppress that next CharGap and leave only |
| 109 | // 4u of silence, which falls inside ggmorse's 3u |
| 110 | // inter-character classification window and erases word |
| 111 | // boundaries on the TX decode panel (#2417). 4u + 3u = 7u |
| 112 | // also matches standard CW timing for the local sidetone |
| 113 | // listener. |
| 114 | m_elements.enqueue(Element::WordGap); |
| 115 | continue; |
| 116 | } |
| 117 | const auto it = tbl.find(ch); |
| 118 | if (it == tbl.end()) continue; |
| 119 | if (!firstChar) |
| 120 | m_elements.enqueue(Element::CharGap); |
| 121 | firstChar = false; |
| 122 | const QString& pattern = it.value(); |
| 123 | for (int i = 0; i < pattern.size(); ++i) { |
| 124 | if (i > 0) |
| 125 | m_elements.enqueue(Element::ElementGap); |
| 126 | m_elements.enqueue(pattern[i] == '.' ? Element::Dit : Element::Dah); |
| 127 | } |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | void CwxLocalKeyer::scheduleNext() |
| 132 | { |