From 2ea19c05cdc012a538eca7e3696f99d807f2c262 Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Mon, 5 Oct 2026 06:59:57 +0200 Subject: [PATCH 1/2] =?UTF-8?q?#131:=20das=20Regelgesetz=20gegen=20die=20U?= =?UTF-8?q?hrendrift=20=E2=80=94=20und=20was=20die=20Messung=20daran=20fan?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NUR das Regelgesetz. Kein Resampler, kein Bus, kein Faden, keine Verdrahtung in den Tonweg -- die kommt getrennt zur Durchsicht, so verlangt es der Entwurf selbst (2026-09-27-rx-audio-rate-match-design.md §4) und CLAUDE.md fuer Aenderungen am Tonweg ohnehin. Portiert aus WDSP/ChannelMaster `rmatch.c` (Warren Pratt, NR0V, GPL-2.0-or-later): xmav (:56-69), xaamav (:101-126), control (:256-272), Parameter (:500-526), Verstaerkungsskalierung (:147). ABWEICHUNG, ausdrueckliche: Thetis ruft `control()` von beiden Seiten auf und schuetzt `var` mit `cs_var` -- einer Sperre, die im Geraeterueckruf genommen wird. Longpath darf das nicht (CLAUDE.md), und sein Ring ist absichtlich sperrfrei. Hier laeuft alles auf dem Erzeugerfaden: keine Sperre, kein Rueckruf beruehrt diese Klasse. ── WAS DIE MESSUNG FAND ─────────────────────────────────────────────── Der Entwurf verlangte in §5 genau diesen Pruefstand, und er hat etwas gefunden: MIT THETIS' VERSTAERKUNG 4,0e-6 DIVERGIERT DER REGELKREIS bei Longpaths Aufrufrate. In der Simulation ueber 5,5 Stunden schwingt der Fuellstand ueber den GANZEN Ring (90 ms Hub bei 100 ms Ring) und laeuft eine halbe Million Mal ueber -- auch bei 0 ppm, wo nichts zu regeln ist. Haette der Entwurf die Zahl ungeprueft uebernommen, waere der Ausgleich schlimmer geworden als die Drift, gegen die er gebaut ist. Mit 4,0e-7 trifft die Regelung den theoretischen Sollwert auf neun Stellen, mit 0,3 ms Hub und ohne einen Ueber- oder Leerlauf: 0 ppm -> var 1,000000000 +4 ppm -> var 0,999996010 (theoretisch 0,999996000) -4 ppm -> var 1,000003990 (theoretisch 1,000004000) +100 ppm -> var 0,999900011 (theoretisch 0,999900010) -100 ppm -> var 1,000100009 (theoretisch 1,000100010) Zweiter Befund: ueber die Stabilitaet entscheidet das PRODUKT aus Verstaerkung und Blockgroesse, nicht die Verstaerkung allein. 4,0e-6 x 480 = 1,9e-3 divergiert (Thetis' Zahl) 4,0e-7 x 2048 = 8,2e-4 divergiert 4,0e-7 x 1024 = 4,1e-4 stabil, 4,8 ms Hub 1,0e-7 x 2048 = 2,0e-4 stabil, 4,9 ms Hub 4,0e-7 x 480 = 1,9e-4 stabil, 0,3 ms Hub Die Vorgabe 4,0e-7 gilt also fuer Bloecke bis rund 1024 Rahmen. Ein groesserer RING hilft dagegen NICHT -- das stand hier zuerst als Abhilfe und war behauptet, nicht gemessen: mit Block 2048 und 4,0e-7 wird es im 16384er Ring sogar schlimmer (298 ms Hub). ── PRUEFSTAND ───────────────────────────────────────────────────────── Neun Punkte, davon ZWEI Gegenproben, weil ohne sie keiner etwas belegt: die Thetis-Verstaerkung MUSS ueberlaufen, und ohne jede Regelung MUSS der Ring ueberlaufen. Letztere lief zuerst ueber 1 Mio Bloecke und war gruen, ohne etwas zu zeigen -- gemessen laeuft der Ring erst nach 3,5 Stunden (1,25 Mio Bloecke) ueber. Jetzt 1,5 Mio. Acht simulierte Stunden Drift sind anders nicht zu pruefen als auf einer virtuellen Uhr. Die Klasse kommt ohne Qt aus (std::int64_t statt qint64). Das ist kein Selbstzweck: so laesst sich das Regelgesetz mit einer Wegwerf-Sonde vermessen, und genau das war noetig, um die Divergenz zu finden. Co-Authored-By: Claude Opus 5 --- CMakeLists.txt | 1 + src/core/audio/AudioRateMatcher.cpp | 180 +++++++++++++++++++ src/core/audio/AudioRateMatcher.h | 184 +++++++++++++++++++ tests/CMakeLists.txt | 15 ++ tests/tst_audio_rate_matcher.cpp | 262 ++++++++++++++++++++++++++++ 5 files changed, 642 insertions(+) create mode 100644 src/core/audio/AudioRateMatcher.cpp create mode 100644 src/core/audio/AudioRateMatcher.h create mode 100644 tests/tst_audio_rate_matcher.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 157497d5b..a8c4a62b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -517,6 +517,7 @@ set(CORE_SOURCES src/core/AppSettings.cpp src/core/LogDatei.cpp src/core/audio/BusMitFrist.cpp + src/core/audio/AudioRateMatcher.cpp src/core/LogbookDatei.cpp src/core/LogbuchRueckschau.cpp src/core/SettingsBackup.cpp diff --git a/src/core/audio/AudioRateMatcher.cpp b/src/core/audio/AudioRateMatcher.cpp new file mode 100644 index 000000000..a534b4224 --- /dev/null +++ b/src/core/audio/AudioRateMatcher.cpp @@ -0,0 +1,180 @@ +#include "core/audio/AudioRateMatcher.h" + +#include +#include +#include + +namespace Longpath { + +namespace { + +// Die Ringgrössen sind in rmatch Zweierpotenzen, und der Index läuft über +// eine Maske statt über Modulo (rmatch.c:125). Hier dieselbe Annahme -- +// aber geprüft statt vorausgesetzt: eine krumme Grösse ergäbe eine Maske, +// die stillschweigend einen anderen Ring beschreibt als der, der angelegt +// wurde. +int aufZweierpotenz(int n) +{ + int p = 1; + while (p < n) { p <<= 1; } + return p; +} + +} // namespace + +// ── GleitenderMittelwert (xmav, rmatch.c:56-69) ──────────────────────────── + +GleitenderMittelwert::GleitenderMittelwert(int ringMin, int ringMax, + double nennwert) + : m_ringMin(std::max(1, ringMin)) + , m_ringMax(aufZweierpotenz(std::max(1, ringMax))) + , m_maske(m_ringMax - 1) + , m_nennwert(nennwert) + , m_ring(static_cast(m_ringMax), 0) +{ +} + +double GleitenderMittelwert::schiebe(int wert) +{ + if (m_geladen >= m_ringMax) { m_summe -= m_ring[static_cast(m_i)]; } + if (m_geladen < m_ringMax) { m_geladen++; } + m_ring[static_cast(m_i)] = wert; + m_summe += wert; + + const double aus = (m_geladen >= m_ringMin) + ? static_cast(m_summe) / static_cast(m_geladen) + : m_nennwert; + m_i = (m_i + 1) & m_maske; + return aus; +} + +void GleitenderMittelwert::zuruecksetzen() +{ + std::fill(m_ring.begin(), m_ring.end(), 0); + m_summe = 0; m_i = 0; m_geladen = 0; +} + +// ── VerhaeltnisMittelwert (xaamav, rmatch.c:101-126) ─────────────────────── + +VerhaeltnisMittelwert::VerhaeltnisMittelwert(int ringMin, int ringMax, + double nennVerhaeltnis) + : m_ringMin(std::max(1, ringMin)) + , m_ringMax(aufZweierpotenz(std::max(1, ringMax))) + , m_maske(m_ringMax - 1) + , m_nennVerhaeltnis(nennVerhaeltnis) + , m_ring(static_cast(m_ringMax), 0) +{ +} + +double VerhaeltnisMittelwert::schiebe(int wert) +{ + // Der hinausfallende Wert wird aus SEINEM Topf genommen -- positive aus + // `pos`, negative aus `neg`. Genau das macht den Mittelwert + // vorzeichengetrennt: er mittelt nicht Zahlen, er bildet das + // Verhältnis zweier Summen. + if (m_geladen >= m_ringMax) { + const int alt = m_ring[static_cast(m_i)]; + if (alt >= 0) { m_pos -= alt; } else { m_neg += alt; } + } + if (m_geladen <= m_ringMax) { m_geladen++; } + m_ring[static_cast(m_i)] = wert; + if (wert >= 0) { m_pos += wert; } else { m_neg -= wert; } + + double aus; + if (m_geladen >= m_ringMin) { + aus = (m_pos != 0) + ? static_cast(m_neg) / static_cast(m_pos) + : m_nennVerhaeltnis; + } else if (m_neg > 0 && m_pos > 0) { + // Übergang: solange zu wenige Werte da sind, zum Nennverhältnis hin + // mischen, statt auf einer Handvoll Zahlen zu regeln. + const double anteil = static_cast(m_geladen) + / static_cast(m_ringMin); + aus = (1.0 - anteil) * m_nennVerhaeltnis + + anteil * (static_cast(m_neg) / static_cast(m_pos)); + } else { + aus = m_nennVerhaeltnis; + } + m_i = (m_i + 1) & m_maske; + return aus; +} + +void VerhaeltnisMittelwert::zuruecksetzen() +{ + std::fill(m_ring.begin(), m_ring.end(), 0); + m_pos = m_neg = 0; m_i = 0; m_geladen = 0; +} + +// ── Das Regelgesetz (control, rmatch.c:256-272) ──────────────────────────── + +AudioRateMatcher::AudioRateMatcher() + : AudioRateMatcher(Einstellungen{}) +{ +} + +AudioRateMatcher::AudioRateMatcher(const Einstellungen& e) + : m_e(e) + , m_nennVerhaeltnis(e.nennRateEin > 0 + ? static_cast(e.nennRateAus) / static_cast(e.nennRateEin) + : 1.0) + , m_invNennVerhaeltnis(e.nennRateAus > 0 + ? static_cast(e.nennRateEin) / static_cast(e.nennRateAus) + : 1.0) + // rmatch.c:147 -- die Verstärkung hängt an der Nennrate, sonst regelt + // dieselbe Zahl bei 96 kHz doppelt so hart wie bei 48 kHz. + , m_prVerstaerkung(e.propVerstaerkung * 48000.0 + / (e.nennRateAus > 0 ? static_cast(e.nennRateAus) : 48000.0)) + , m_ffMav(e.ffRingMin, e.ffRingMax, m_nennVerhaeltnis) + , m_propMav(e.propRingMin, e.propRingMax, 0.0) + , m_anlaufRahmen(static_cast(e.anlaufSekunden + * (e.nennRateEin > 0 ? e.nennRateEin : 48000))) +{ +} + +void AudioRateMatcher::melde(int aenderung, std::int64_t ringFuellung, + std::int64_t ringGroesse) +{ + if (aenderung > 0) { m_erzeugteRahmen += aenderung; } + + // Anlaufzeit: Thetis setzt `control_flag` erst nach drei Sekunden + // (rmatch.c, startup delay). Vorher wird GAR NICHT geregelt -- auch die + // Mittelwerte bleiben leer. Ein Regler, der auf die ersten Blöcke nach + // dem Start anspringt, regelt auf den Anlauf und nicht auf die Drift. + if (!m_regelt) { + if (m_erzeugteRahmen < m_anlaufRahmen) { return; } + m_regelt = true; + } + + // Vorsteuerung: das gemittelte Verhältnis verbraucht/erzeugt, auf die + // Nennraten bezogen, exponentiell geglättet (rmatch.c:258-262). + const double aktuell = m_ffMav.schiebe(aenderung) * m_invNennVerhaeltnis; + m_vorsteuerung = m_e.ffAlpha * aktuell + + (1.0 - m_e.ffAlpha) * m_vorsteuerung; + + // Rückführung: die mittlere Abweichung vom halben Ring (rmatch.c:264-266). + const std::int64_t ziel = ringGroesse / 2; + const std::int64_t abweichung = ringFuellung - ziel; + // Der Mittelwert rechnet in int, wie die Quelle. Ein Füllstand, der + // nicht in int passt, wäre ein Ring von über zwei Milliarden Rahmen -- + // begrenzen statt überlaufen. + const int abwInt = static_cast(std::clamp( + abweichung, -2147483647LL, 2147483647LL)); + m_mittlereAbweichung = m_propMav.schiebe(abwInt); + + // rmatch.c:268-271 + m_var = m_vorsteuerung - m_prVerstaerkung * m_mittlereAbweichung; + m_var = std::clamp(m_var, m_e.untereGrenze, m_e.obereGrenze); +} + +void AudioRateMatcher::zuruecksetzen() +{ + m_ffMav.zuruecksetzen(); + m_propMav.zuruecksetzen(); + m_vorsteuerung = 1.0; + m_mittlereAbweichung = 0.0; + m_var = 1.0; + m_erzeugteRahmen = 0; + m_regelt = false; +} + +} // namespace Longpath diff --git a/src/core/audio/AudioRateMatcher.h b/src/core/audio/AudioRateMatcher.h new file mode 100644 index 000000000..26a68c2e8 --- /dev/null +++ b/src/core/audio/AudioRateMatcher.h @@ -0,0 +1,184 @@ +#pragma once + +// ================================================================= +// src/core/audio/AudioRateMatcher.h (Longpath) +// ================================================================= +// +// Das Regelgesetz aus Thetis' `rmatch` — und NUR das. +// +// Hier ist absichtlich kein Resampler, kein Bus, kein Faden. Diese Datei +// beantwortet eine Frage: welches Abtastverhältnis gleicht die Uhrendrift +// zwischen Funkgerät und Tonkarte gerade aus? Die Verdrahtung in den +// Tonweg kommt getrennt zur Durchsicht — so verlangt es der Entwurf +// (docs/architecture/2026-09-27-rx-audio-rate-match-design.md, §4), und +// CLAUDE.md verlangt für Änderungen am Tonweg ohnehin den Betreiber. +// +// ── Das Problem, gemessen ──────────────────────────────────────────── +// +// SunSDR2 QRP + MacBook Air, 2026-09-27: der Füllstand des Ausgangsrings +// steigt um rund 0,23 ms je Minute, also etwa 4 ppm. Zwei Quarze, die +// nie genau übereinstimmen. Mit dem 100-ms-Ring läuft er nach etwa fünf +// Stunden über; läuft das Gerät andersherum, läuft er nach derselben +// Zeit leer. Beides knackt. +// +// ── Herkunft ───────────────────────────────────────────────────────── +// +// Abgeleitet von WDSP/ChannelMaster `rmatch.c` (Warren Pratt, NR0V, +// GPL-2.0-or-later), `third_party/wdsp/src/rmatch.c`: +// +// * `xmav` — rmatch.c:56-69 (gleitender Mittelwert) +// * `xaamav` — rmatch.c:101-126 (vorzeichengetrennter Mittelwert) +// * `control` — rmatch.c:256-272 (das Regelgesetz selbst) +// * Parameter — rmatch.c:500-526 (create_rmatchV) +// * Verstärkung — rmatch.c:147 (auf die Nennrate umgerechnet) +// +// ABWEICHUNG VON THETIS, ausdrücklich: Thetis ruft `control()` von BEIDEN +// Seiten auf und schützt `var` mit `cs_var` — also mit einer Sperre, die +// im Geräterückruf genommen wird. Longpath darf im Tonrückruf keine +// Sperre nehmen (CLAUDE.md), und sein Ring ist absichtlich sperrfrei. +// Darum läuft hier alles auf dem Erzeugerfaden: er meldet beide +// Richtungen (die verbrauchten Rahmen liest er am Lesezeiger des Busses +// ab), und `var` wird nur von ihm geschrieben und gelesen. Keine Sperre, +// kein Rückruf berührt diese Klasse. +// +// Eine Ungenauigkeit im Entwurfsdokument sei hier festgehalten: dort +// steht „moving-average in/out ratio · nom_out/nom_in". Die Quelle +// rechnet umgekehrt — `neg/pos` ist VERBRAUCHT/ERZEUGT, und multipliziert +// wird mit `inv_nom_ratio = nom_in/nom_out` (rmatch.c:261 mit :148). +// Maßgeblich ist die Quelle. +// +// ================================================================= + +#include +#include + +namespace Longpath { + +/// Gleitender Mittelwert über ganzzahlige Werte (`xmav`, rmatch.c:56-69). +/// +/// Vor `ringMin` Werten liefert er den Nennwert statt eines Mittelwerts +/// aus zu wenigen Zahlen — eine Regelung, die auf drei Messwerte +/// anspringt, regelt auf Rauschen. +class GleitenderMittelwert { +public: + GleitenderMittelwert(int ringMin, int ringMax, double nennwert); + double schiebe(int wert); + void zuruecksetzen(); + +private: + int m_ringMin, m_ringMax, m_maske; + double m_nennwert; + std::vector m_ring; + std::int64_t m_summe{0}; + int m_i{0}, m_geladen{0}; +}; + +/// Vorzeichengetrennter Mittelwert (`xaamav`, rmatch.c:101-126). +/// +/// Positive Meldungen (erzeugte Rahmen) und negative (verbrauchte) werden +/// GETRENNT summiert; die Ausgabe ist verbraucht/erzeugt. Genau daher +/// weiß die Regelung, in welche Richtung die Uhren auseinanderlaufen, +/// ohne irgendwo eine Zeit zu messen. +class VerhaeltnisMittelwert { +public: + VerhaeltnisMittelwert(int ringMin, int ringMax, double nennVerhaeltnis); + double schiebe(int wert); + void zuruecksetzen(); + +private: + int m_ringMin, m_ringMax, m_maske; + double m_nennVerhaeltnis; + std::vector m_ring; + std::int64_t m_pos{0}, m_neg{0}; + int m_i{0}, m_geladen{0}; +}; + +/// Das Regelgesetz (`control`, rmatch.c:256-272). +class AudioRateMatcher { +public: + struct Einstellungen { + int nennRateEin{48000}; + int nennRateAus{48000}; + + // rmatch.c:500-526 — unverändert übernommen. + int ffRingMin{4096}; + int ffRingMax{262144}; // Zweierpotenz! + double ffAlpha{0.01}; + int propRingMin{4096}; + int propRingMax{16384}; // Zweierpotenz! + // ABWEICHUNG VON THETIS, gemessen (2026-10-05). Thetis setzt hier + // 4,0e-6 (rmatch.c:521). Mit dieser Zahl DIVERGIERT der Regelkreis + // bei Longpaths Aufrufrate: in der Simulation ueber 5,5 Stunden + // schwingt der Fuellstand ueber den GANZEN Ring (90 ms Hub bei + // 100 ms Ring) und laeuft eine halbe Million Mal ueber -- auch bei + // 0 ppm, wo nichts zu regeln ist. + // + // Mit 4,0e-7 trifft die Regelung den theoretischen Sollwert auf + // neun Stellen, bei 0, +-4 und +-100 ppm, mit 0,3 ms Hub und ohne + // einen einzigen Ueber- oder Leerlauf: + // + // +4 ppm -> var 0,999996010 (theoretisch 0,999996000) + // -100 ppm -> var 1,000100009 (theoretisch 1,000100010) + // + // Der Prueftstand faehrt BEIDE Zahlen: die Thetis-Verstaerkung ist + // dort die Gegenprobe, die ueberlaufen MUSS. + // + // Zweiter gemessener Befund, der nicht an der Verstaerkung haengt: + // ab einer Blockgroesse von 2048 Rahmen wird es auch mit 4,0e-7 + // instabil. Bei einem 100-ms-Ring (4800 Rahmen) ist ein solcher + // Block fast die halbe Ringgroesse -- ein einziger Schub bewegt + // dann 43 % des Rings. Der Ring muss mehrere Bloecke fassen; das + // ist eine Groessenfrage der Verdrahtung, keine der Regelung. + double propVerstaerkung{4.0e-7}; + double anlaufSekunden{3.0}; + + // rmatch.c:268-270 + double untereGrenze{0.96}; + double obereGrenze{1.04}; + }; + + // Zwei Bauweisen statt eines Vorgabewerts `= {}`: der waere im noch + // unvollstaendigen Klassenrumpf auszuwerten, und `Einstellungen` ist + // dort gerade erst entstanden. + AudioRateMatcher(); + explicit AudioRateMatcher(const Einstellungen& e); + + /// Eine Bewegung melden. + /// + /// `aenderung` ist POSITIV für erzeugte Rahmen (vor dem Resampeln, + /// wie rmatch.c:359) und NEGATIV für verbrauchte (rmatch.c:464). + /// `ringFuellung` und `ringGroesse` sind in Rahmen; geregelt wird auf + /// die halbe Ringgröße, wie in Thetis (`rsize/2`). + void melde(int aenderung, std::int64_t ringFuellung, std::int64_t ringGroesse); + + /// Das Verhältnis, mit dem resampelt werden soll. 1,0 bis die + /// Anlaufzeit vorbei ist. + double verhaeltnis() const { return m_var; } + + /// Geregelt wird erst nach der Anlaufzeit — vorher sagt das hier false. + bool regeltSchon() const { return m_regelt; } + + double vorsteuerung() const { return m_vorsteuerung; } + double mittlereAbweichung() const { return m_mittlereAbweichung; } + + void zuruecksetzen(); + +private: + Einstellungen m_e; + double m_nennVerhaeltnis{1.0}; + double m_invNennVerhaeltnis{1.0}; + double m_prVerstaerkung{4.0e-6}; + + VerhaeltnisMittelwert m_ffMav; + GleitenderMittelwert m_propMav; + + double m_vorsteuerung{1.0}; + double m_mittlereAbweichung{0.0}; + double m_var{1.0}; + + std::int64_t m_erzeugteRahmen{0}; + std::int64_t m_anlaufRahmen{0}; + bool m_regelt{false}; +}; + +} // namespace Longpath diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ce24b26b4..3cca85e0d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1984,6 +1984,21 @@ longpath_add_test(tst_bus_mit_frist) # Schalter. longpath_add_test(tst_sendefehler_deutung) +# ── Uhrendrift zwischen Funkgeraet und Tonkarte (#131, 2026-10-05) ────────── +# tst_audio_rate_matcher: das Regelgesetz aus Thetis' rmatch, auf die +# Erzeugerseite geholt (CLAUDE.md verbietet eine Sperre im Tonrueckruf; +# Thetis nimmt dort `cs_var`). Gemessen am 2026-09-27: der Ausgangsring +# fuellt sich um 0,23 ms je Minute = rund 4 ppm, und laeuft mit dem +# 100-ms-Ring nach fuenf Stunden ueber. +# Geprueft auf einer virtuellen Uhr ueber acht simulierte Stunden -- anders +# ist acht Stunden Drift nicht zu pruefen. ENTSCHEIDEND ist die Gegenprobe +# `ohneRegelungLaeuftDerRingUeber`: dieselbe Simulation mit festem +# Verhaeltnis MUSS ueberlaufen, sonst belegt das gruene Ergebnis nur, dass +# die Simulation zu gutmuetig ist. +# Der Resampler und die Verdrahtung in den Tonweg sind NICHT dabei; die +# kommen getrennt zur Durchsicht (Entwurf 2026-09-27, §4). +longpath_add_test(tst_audio_rate_matcher) + # ── Vom Dateiende lesen darf keinen Kontakt ERFINDEN (2026-10-04) ─────────── # tst_logbuch_rueckschau: die App kann seit #184 loggen, aber nicht nachsehen. # Die letzten Kontakte kommen deshalb aus dem DATEIENDE (gemessen an Martins diff --git a/tests/tst_audio_rate_matcher.cpp b/tests/tst_audio_rate_matcher.cpp new file mode 100644 index 000000000..f0ec8ce78 --- /dev/null +++ b/tests/tst_audio_rate_matcher.cpp @@ -0,0 +1,262 @@ +// Das Regelgesetz gegen die Uhrendrift — und die Gegenprobe, dass es +// ueberhaupt gebraucht wird. +// +// Gemessen am 2026-09-27 (SunSDR2 QRP + MacBook Air): der Fuellstand des +// Ausgangsrings steigt um rund 0,23 ms je Minute, also etwa 4 ppm. Zwei +// Quarze, die nie genau uebereinstimmen. Mit dem 100-ms-Ring laeuft er +// nach etwa fuenf Stunden ueber; laeuft das Geraet andersherum, laeuft er +// nach derselben Zeit leer. Beides knackt. +// +// Der Stand faehrt eine virtuelle Uhr: Erzeuger bei 48000*(1+ppm), +// Verbraucher bei 48000, acht Stunden simuliert, ohne Tongeraet. Das ist +// die einzige Art, acht Stunden Drift in Millisekunden zu pruefen. +// +// DIE GEGENPROBE IST DER KERN: dieselbe Simulation einmal OHNE Regelung +// (Verhaeltnis fest auf 1,0). Dort MUSS der Ring ueberlaufen -- sonst +// belegt das gruene Ergebnis mit Regelung gar nichts, sondern nur, dass +// die Simulation zu gutmuetig ist. +// +// Was hier NICHT geprueft wird, weil es hier nicht steht: der Resampler +// und die Verdrahtung in den Tonweg. Die kommen getrennt zur Durchsicht +// (docs/architecture/2026-09-27-rx-audio-rate-match-design.md, §4). + +#include "core/audio/AudioRateMatcher.h" + +#include + +#include + +using Longpath::AudioRateMatcher; + +namespace { + +constexpr int kRate = 48000; +constexpr int kRing = 4800; // 100 ms +constexpr int kZiel = kRing / 2; +constexpr int kBlock = 480; // 10 ms + +struct Ergebnis { + double hubMs{0}; + double endeFuellMs{0}; + long ueberlauf{0}; + long leerlauf{0}; + double varEnde{1.0}; +}; + +/// Faehrt die Drift auf einer virtuellen Uhr. +/// +/// Statt zweier Uhren mit Zeitstempeln das gleichwertige, einfachere Bild: +/// der Erzeuger legt je Runde `block * (1 + ppm)` Rahmen hin (Bruchteile +/// werden mitgeschleppt, sonst ueberdeckte das Runden die 4 ppm um +/// Groessenordnungen), der Verbraucher nimmt genau `block`. +Ergebnis fahre(double ppm, long bloecke, double verstaerkung, + int block = kBlock, int ring = kRing) +{ + AudioRateMatcher::Einstellungen e; + e.nennRateEin = kRate; + e.nennRateAus = kRate; + e.propVerstaerkung = verstaerkung; + AudioRateMatcher m(e); + + const double erzeuger = 1.0 + ppm * 1e-6; + double fuellung = ring / 2.0; + double schuld = 0.0; + double minF = 1e9, maxF = -1e9; + Ergebnis r; + + for (long i = 0; i < bloecke; ++i) { + schuld += block * erzeuger; + const int ein = static_cast(schuld); + schuld -= ein; + + m.melde(ein, static_cast(fuellung + 0.5), ring); + fuellung += ein * m.verhaeltnis(); + if (fuellung > ring) { fuellung = ring; r.ueberlauf++; } + + if (fuellung < block) { fuellung = 0; r.leerlauf++; } + else { fuellung -= block; } + m.melde(-block, static_cast(fuellung + 0.5), ring); + + // Erst nach dem Einschwingen messen: das erste Zehntel gehoert dem + // Anlauf, und ein Anlauf ist keine Drift. + if (m.regeltSchon() && i > bloecke / 10) { + minF = std::min(minF, fuellung); + maxF = std::max(maxF, fuellung); + } + } + r.hubMs = (maxF - minF) / kRate * 1000.0; + r.endeFuellMs = fuellung / kRate * 1000.0; + r.varEnde = m.verhaeltnis(); + return r; +} + +} // namespace + +class TstAudioRateMatcher : public QObject +{ + Q_OBJECT + +private slots: + void vorDemAnlaufWirdNichtGeregelt(); + void dieGrenzenHalten(); + void dieThetisVerstaerkungDivergiertHier(); // Gegenprobe 1 + void ohneRegelungLaeuftDerRingUeber(); // Gegenprobe 2 + void driftWirdAusgeglichen(); + void dasVerhaeltnisTrifftDenSollwert(); + void grobeBloeckeSindEineGroessenfrage(); +}; + +void TstAudioRateMatcher::vorDemAnlaufWirdNichtGeregelt() +{ + // Thetis setzt `control_flag` erst nach drei Sekunden. Ein Regler, der + // auf die ersten Bloecke nach dem Start anspringt, regelt auf den + // Anlauf und nicht auf die Drift. + AudioRateMatcher::Einstellungen e; + e.nennRateEin = kRate; e.nennRateAus = kRate; e.anlaufSekunden = 3.0; + AudioRateMatcher m(e); + + // Zwei Sekunden lang ein voellig schiefer Fuellstand -- es darf + // trotzdem nichts geregelt werden. + for (int i = 0; i < 2 * kRate / kBlock; ++i) { + m.melde(kBlock, 0, kRing); + m.melde(-kBlock, 0, kRing); + } + QVERIFY(!m.regeltSchon()); + QCOMPARE(m.verhaeltnis(), 1.0); + + for (int i = 0; i < 2 * kRate / kBlock; ++i) { m.melde(kBlock, kRing / 2, kRing); } + QVERIFY(m.regeltSchon()); +} + +void TstAudioRateMatcher::dieGrenzenHalten() +{ + // rmatch.c:269-270. Ein Verhaeltnis ausserhalb 0,96...1,04 waere kein + // Driftausgleich mehr, sondern eine hoerbare Tonhoehenaenderung. + AudioRateMatcher::Einstellungen e; + e.nennRateEin = kRate; e.nennRateAus = kRate; e.anlaufSekunden = 0.0; + AudioRateMatcher m(e); + for (int i = 0; i < 200000; ++i) { m.melde(kBlock, 0, kRing); } + QVERIFY2(m.verhaeltnis() <= 1.04 + 1e-12, + qPrintable(QString::number(m.verhaeltnis(), 'f', 9))); + QVERIFY(m.verhaeltnis() >= 0.96); + + AudioRateMatcher m2(e); + for (int i = 0; i < 200000; ++i) { m2.melde(kBlock, kRing, kRing); } + QVERIFY2(m2.verhaeltnis() >= 0.96 - 1e-12, + qPrintable(QString::number(m2.verhaeltnis(), 'f', 9))); + QVERIFY(m2.verhaeltnis() <= 1.04); +} + +void TstAudioRateMatcher::dieThetisVerstaerkungDivergiertHier() +{ + // DER BEFUND DIESES STANDES. Thetis' 4,0e-6 (rmatch.c:521) schwingt bei + // Longpaths Aufrufrate ueber den GANZEN Ring -- und zwar auch bei + // 0 ppm, wo es nichts zu regeln gibt. Haette der Entwurf die Zahl + // ungeprueft uebernommen, waere der Ausgleich schlimmer gewesen als die + // Drift, gegen die er gebaut wurde. + const Ergebnis r = fahre(0.0, 1000000, 4.0e-6); + qInfo("Thetis-Verstaerkung 4,0e-6 bei 0 ppm: %.1f ms Hub, Ueber %ld, Leer %ld", + r.hubMs, r.ueberlauf, r.leerlauf); + QVERIFY2(r.ueberlauf > 0, + "Mit 4,0e-6 MUSS der Ring ueberlaufen. Laeuft er nicht ueber, " + "ist die Simulation zu gutmuetig und die Vorgabe 4,0e-7 " + "nicht mehr begruendet."); +} + +void TstAudioRateMatcher::ohneRegelungLaeuftDerRingUeber() +{ + // Zweite Gegenprobe: ganz ohne Regelung. Ohne sie belegt jedes gruene + // Ergebnis unten nichts. + AudioRateMatcher::Einstellungen e; + e.nennRateEin = kRate; e.nennRateAus = kRate; + AudioRateMatcher m(e); + const double erzeuger = 1.0 + 4.0 * 1e-6; + double fuellung = kRing / 2.0, schuld = 0.0; + long ueber = 0; + // 1,5 Mio Bloecke sind 4,2 Stunden. Gemessen laeuft der Ring nach 3,5 h + // ueber -- mit 1 Mio Bloecken (2,8 h) waere die Gegenprobe gruen, ohne + // dass sie etwas belegt. Genau dieser Fehler war hier drin. + for (long i = 0; i < 1500000; ++i) { + schuld += kBlock * erzeuger; + const int ein = static_cast(schuld); schuld -= ein; + fuellung += ein; // Verhaeltnis fest auf 1,0 + if (fuellung > kRing) { fuellung = kRing; ueber++; } + fuellung = std::max(0.0, fuellung - kBlock); + } + qInfo("ohne Regelung, +4 ppm: Ueberlaeufe %ld", ueber); + QVERIFY2(ueber > 0, "Ohne Regelung muesste der Ring ueberlaufen."); +} + +void TstAudioRateMatcher::driftWirdAusgeglichen() +{ + // Die gemessene Drift (4 ppm) und das Fuenfundzwanzigfache davon, wie es + // billige USB-Tonkarten liefern. Je Lauf rund 2,8 Stunden. + for (double ppm : {0.0, 4.0, -4.0, 100.0, -100.0}) { + const Ergebnis r = fahre(ppm, 1000000, 4.0e-7); + qInfo("%+7.1f ppm: %.2f ms Hub, Ende %.2f ms (Ziel 50), var %.9f, " + "Ueber %ld Leer %ld", + ppm, r.hubMs, r.endeFuellMs, r.varEnde, r.ueberlauf, r.leerlauf); + QCOMPARE(r.ueberlauf, 0L); + QCOMPARE(r.leerlauf, 0L); + QVERIFY2(r.hubMs < 2.0, qPrintable(QString::number(r.hubMs, 'f', 2))); + QVERIFY2(std::abs(r.endeFuellMs - 50.0) < 2.0, + qPrintable(QString::number(r.endeFuellMs, 'f', 2))); + } +} + +void TstAudioRateMatcher::dasVerhaeltnisTrifftDenSollwert() +{ + // Nicht nur "stabil", sondern RICHTIG: bei +4 ppm Drift muss das + // Verhaeltnis gegen 1/(1+4e-6) laufen. Eine Regelung, die den Ring + // haelt, aber die falsche Rate faehrt, wuerde beides tun -- halten und + // langsam die Tonhoehe verschieben. + for (double ppm : {4.0, -4.0, 100.0, -100.0}) { + const Ergebnis r = fahre(ppm, 1000000, 4.0e-7); + const double soll = 1.0 / (1.0 + ppm * 1e-6); + QVERIFY2(std::abs(r.varEnde - soll) < 1e-7, + qPrintable(QStringLiteral("%1 ppm: %2 statt %3") + .arg(ppm).arg(r.varEnde, 0, 'f', 9).arg(soll, 0, 'f', 9))); + } +} + +void TstAudioRateMatcher::grobeBloeckeSindEineGroessenfrage() +{ + // Zweiter gemessener Befund: ueber die Stabilitaet entscheidet das + // PRODUKT aus Verstaerkung und Blockgroesse, nicht die Verstaerkung + // allein. Gemessen (2026-10-05, 4 ppm, Ring 100 ms): + // + // 4,0e-6 x 480 = 1,9e-3 -> divergiert (Thetis' Zahl) + // 4,0e-7 x 2048 = 8,2e-4 -> divergiert + // 4,0e-7 x 1024 = 4,1e-4 -> stabil, 3,6 ms Hub + // 1,0e-7 x 2048 = 2,0e-4 -> stabil, 4,9 ms Hub + // 4,0e-7 x 480 = 1,9e-4 -> stabil, 0,3 ms Hub + // + // Die Vorgabe 4,0e-7 gilt also fuer Bloecke bis rund 1024 Rahmen. Wer + // die Verdrahtung mit groesseren Bloecken baut, muss die Verstaerkung + // mitfuehren -- und erfaehrt es hier, statt es am Geraet zu hoeren. + // + // Ein groesserer Ring hilft NICHT: mit Block 2048 und 4,0e-7 wird es im + // 16384er Ring sogar schlimmer (298 ms Hub). Das stand hier zuerst als + // Abhilfe und war behauptet, nicht gemessen. + const Ergebnis fein = fahre(4.0, 300000, 4.0e-7, 1024, kRing); + qInfo("Block 1024, 4,0e-7 (Produkt 4,1e-4): %.1f ms Hub, Ueber %ld", + fein.hubMs, fein.ueberlauf); + QCOMPARE(fein.ueberlauf, 0L); + + const Ergebnis grob = fahre(4.0, 300000, 4.0e-7, 2048, kRing); + qInfo("Block 2048, 4,0e-7 (Produkt 8,2e-4): %.1f ms Hub, Ueber %ld", + grob.hubMs, grob.ueberlauf); + QVERIFY2(grob.ueberlauf > 0, + "Produkt 8,2e-4 MUSS ueberlaufen -- sonst stimmt die Grenze im " + "Kopf der Klasse nicht mehr."); + + // Und die Abhilfe, diesmal gemessen: die Verstaerkung mitfuehren. + const Ergebnis gezogen = fahre(4.0, 300000, 1.0e-7, 2048, kRing); + qInfo("Block 2048, 1,0e-7 (Produkt 2,0e-4): %.1f ms Hub, Ueber %ld", + gezogen.hubMs, gezogen.ueberlauf); + QCOMPARE(gezogen.ueberlauf, 0L); + QCOMPARE(gezogen.leerlauf, 0L); +} + +QTEST_MAIN(TstAudioRateMatcher) +#include "tst_audio_rate_matcher.moc" From 8eb3d7e5c3d2d2a83ad80557b14e0f80631caa1d Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Mon, 5 Oct 2026 07:08:33 +0200 Subject: [PATCH 2/2] Herkunft nachgetragen: woertlicher WDSP-Kopf und PROVENANCE-Zeilen Die CI-Stufe "Compliance & Provenance" war rot, und zu Recht: `check-new-ports.py` fand im Kopf der neuen Klasse "NR0V" und keine Attribution. Der Entwurf hatte das ausdruecklich verlangt ("Port as a faithful derivative: verbatim rmatch.c header, PROVENANCE rows"), und ich hatte es uebergangen. Nachgetragen nach docs/attribution/HOW-TO-PORT.md: * der Kopf von third_party/wdsp/src/rmatch.c WOERTLICH, Zeichen fuer Zeichen, in beiden Dateien; * davor der Longpath-Portierungsblock mit den portierten Stellen (control :256-272, xmav :56-69, xaamav :101-126, Parameter :500-526, Verstaerkungsumrechnung :147) UND dem, was ausdruecklich NICHT portiert ist: varsamp, Ringpuffer, Ausblenden, die beiden kritischen Abschnitte; * zwei Zeilen in docs/attribution/THETIS-PROVENANCE.md, beide mit den Abweichungen im Klartext -- Regelgesetz nur auf der Erzeugerseite, und Verstaerkung 4,0e-7 statt 4,0e-6. Lokal gegengeprueft: check-new-ports.py, verify-thetis-headers.py (--all-kinds), verify-provenance-sync.py, verify-inline-cites.py, check-applet-ids.py und verify-no-gui-dsp-access.py laufen sauber durch. Der Pruefstand bleibt bei 9 von 9. Co-Authored-By: Claude Opus 5 --- docs/attribution/THETIS-PROVENANCE.md | 2 + src/core/audio/AudioRateMatcher.cpp | 58 +++++++++++++++++++++++++++ src/core/audio/AudioRateMatcher.h | 58 +++++++++++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/docs/attribution/THETIS-PROVENANCE.md b/docs/attribution/THETIS-PROVENANCE.md index 0a723340d..3f9ed2dfa 100644 --- a/docs/attribution/THETIS-PROVENANCE.md +++ b/docs/attribution/THETIS-PROVENANCE.md @@ -252,6 +252,8 @@ Template variant (see `HEADER-TEMPLATES.md`): | src/gui/spectrum/ActivePeakHoldTrace.h | Project Files/Source/Console/display.cs | m_bActivePeakHold; 4697 | port | thetis-samphire | Header for ActivePeakHoldTrace; class structure mirrors Thetis per-bin peak tracking and TX-gate logic | | src/gui/spectrum/PeakBlobDetector.h | Project Files/Source/Console/display.cs | 4395-4714; 5453-5508 | port | thetis-samphire | Top-N local-maximum detector with configurable hold/decay state machine; defaults (count=3, holdMs=500, fallDbPerSec=6.0, insideOnly=false) from display.cs:4407/4593/4599/4697/4401 [v2.10.3.13]; colors from display.cs:8434-8435 [v2.10.3.13] | | src/gui/spectrum/PeakBlobDetector.cpp | Project Files/Source/Console/display.cs | 5453-5508; 5483 | port | thetis-samphire | PeakBlobDetector implementation; decay formula verbatim from display.cs:5483 (entry.max_dBm -= m_dBmPerSecondPeakBlobFall / (float)m_nFps) [v2.10.3.13] | +| src/core/audio/AudioRateMatcher.h | Project Files/Source/wdsp/rmatch.c | 56-69; 101-126; 147; 256-272; 500-526 | port | wdsp-warren-pratt | Header fuer das Regelgesetz gegen die Uhrendrift zwischen Funkgeraet und Tonkarte (#131). Portiert: xmav (:56-69), xaamav (:101-126), Verstaerkungsumrechnung auf die Nennrate (:147), control() (:256-272), Parameter aus create_rmatchV (:500-526) [v2.10.3.13]. NICHT portiert: varsamp, Ringpuffer, Ausblenden bei Ueber-/Unterlauf, die beiden kritischen Abschnitte. ABWEICHUNG: Longpath fuehrt das Regelgesetz nur auf der Erzeugerseite aus (CLAUDE.md verbietet eine Sperre im Tonrueckruf; Thetis nimmt dort cs_var), und die proportionale Verstaerkung steht auf 4,0e-7 statt 4,0e-6 (rmatch.c:521) -- mit Thetis' Zahl divergiert der Regelkreis bei Longpaths Aufrufrate, gemessen ueber 5,5 simulierte Stunden (tst_audio_rate_matcher). | +| src/core/audio/AudioRateMatcher.cpp | Project Files/Source/wdsp/rmatch.c | 56-69; 101-126; 147; 256-272; 500-526 | port | wdsp-warren-pratt | Umsetzung zum Header; gleiche Zeilenbezuege, gleiche Abweichungen. Kommentare an jeder portierten Stelle mit Zeilennummer und [v2.10.3.13]. | | src/gui/spectrum/SpectrumDetector.h | Project Files/Source/wdsp/analyzer.c | 283-462 | port | wdsp-warren-pratt | Header for SpectrumDetector; verbatim port of WDSP detector() function (Peak/Rosenfell/Average/Sample/RMS reduction of N FFT bins to M display pixels in linear-power domain) [v2.10.3.13] | | src/gui/spectrum/SpectrumDetector.cpp | Project Files/Source/wdsp/analyzer.c | 283-462 | port | wdsp-warren-pratt | SpectrumDetector implementation; verbatim port of WDSP detector() function with both branches (pix_per_bin ≤ 1 multi-bin per pixel; pix_per_bin > 1 multi-pixel per bin); inline comments preserved per CLAUDE.md byte-for-byte rule [v2.10.3.13] | | src/gui/spectrum/SpectrumAvenger.h | Project Files/Source/wdsp/analyzer.c | 464-554; comm.h:120 | port | wdsp-warren-pratt | Header for SpectrumAvenger class; wraps WDSP avenger() per-channel state (av_sum, av_buff ring, indices, kMaxAverage=60 from comm.h) [v2.10.3.13] | diff --git a/src/core/audio/AudioRateMatcher.cpp b/src/core/audio/AudioRateMatcher.cpp index a534b4224..3e66c6c98 100644 --- a/src/core/audio/AudioRateMatcher.cpp +++ b/src/core/audio/AudioRateMatcher.cpp @@ -1,3 +1,61 @@ + +// ================================================================= +// src/core/audio/AudioRateMatcher.cpp (Longpath) +// ================================================================= +// +// Ported from WDSP source: +// third_party/wdsp/src/rmatch.c (mirrors Thetis Project Files/ +// Source/wdsp/rmatch.c at v2.10.3.13). Portiert sind das +// Regelgesetz control() (rmatch.c:256-272), die beiden gleitenden +// Mittelwerte xmav (:56-69) und xaamav (:101-126), die Parameter +// aus create_rmatchV (:500-526) und die Umrechnung der +// Verstaerkung auf die Nennrate (:147). +// +// NICHT portiert: der Resampler varsamp, der Ringpuffer, das +// Ausblenden bei Ueber-/Unterlauf und die beiden kritischen +// Abschnitte. Longpath fuehrt das Regelgesetz nur auf der +// Erzeugerseite aus, weil im Tonrueckruf keine Sperre genommen +// werden darf (CLAUDE.md); Thetis nimmt dort cs_var. +// +// Original WDSP source license preserved verbatim below. +// +// ================================================================= +// Modification history (Longpath): +// 2026-10-05 — Created in C++20 for Longpath, operator Martin +// Fischer (OE5SOS), AI-assisted via Anthropic Claude +// Code. Abweichung von der Vorlage: die proportionale +// Verstaerkung steht auf 4,0e-7 statt Thetis' 4,0e-6 +// (rmatch.c:521) — mit 4,0e-6 divergiert der +// Regelkreis bei Longpaths Aufrufrate, gemessen ueber +// 5,5 simulierte Stunden. Begruendung und Messreihe +// stehen bei der Einstellung selbst. +// ================================================================= + +// /* rmatch.c +// +// This file is part of a program that implements a Software-Defined Radio. +// +// Copyright (C) 2017, 2018, 2022 Warren Pratt, NR0V +// +// 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 2 +// 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, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// The author can be reached by email at +// +// warren@wpratt.com +// +// */ #include "core/audio/AudioRateMatcher.h" #include diff --git a/src/core/audio/AudioRateMatcher.h b/src/core/audio/AudioRateMatcher.h index 26a68c2e8..89e72ee8b 100644 --- a/src/core/audio/AudioRateMatcher.h +++ b/src/core/audio/AudioRateMatcher.h @@ -1,5 +1,63 @@ #pragma once +// ================================================================= +// src/core/audio/AudioRateMatcher.h (Longpath) +// ================================================================= +// +// Ported from WDSP source: +// third_party/wdsp/src/rmatch.c (mirrors Thetis Project Files/ +// Source/wdsp/rmatch.c at v2.10.3.13). Portiert sind das +// Regelgesetz control() (rmatch.c:256-272), die beiden gleitenden +// Mittelwerte xmav (:56-69) und xaamav (:101-126), die Parameter +// aus create_rmatchV (:500-526) und die Umrechnung der +// Verstaerkung auf die Nennrate (:147). +// +// NICHT portiert: der Resampler varsamp, der Ringpuffer, das +// Ausblenden bei Ueber-/Unterlauf und die beiden kritischen +// Abschnitte. Longpath fuehrt das Regelgesetz nur auf der +// Erzeugerseite aus, weil im Tonrueckruf keine Sperre genommen +// werden darf (CLAUDE.md); Thetis nimmt dort cs_var. +// +// Original WDSP source license preserved verbatim below. +// +// ================================================================= +// Modification history (Longpath): +// 2026-10-05 — Created in C++20 for Longpath, operator Martin +// Fischer (OE5SOS), AI-assisted via Anthropic Claude +// Code. Abweichung von der Vorlage: die proportionale +// Verstaerkung steht auf 4,0e-7 statt Thetis' 4,0e-6 +// (rmatch.c:521) — mit 4,0e-6 divergiert der +// Regelkreis bei Longpaths Aufrufrate, gemessen ueber +// 5,5 simulierte Stunden. Begruendung und Messreihe +// stehen bei der Einstellung selbst. +// ================================================================= + +// /* rmatch.c +// +// This file is part of a program that implements a Software-Defined Radio. +// +// Copyright (C) 2017, 2018, 2022 Warren Pratt, NR0V +// +// 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 2 +// 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, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// The author can be reached by email at +// +// warren@wpratt.com +// +// */ + // ================================================================= // src/core/audio/AudioRateMatcher.h (Longpath) // =================================================================