diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 4bbf9d9..138b211 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -35,7 +35,9 @@ jobs: "docs/reference/presets.md:docs/reference/presets_RU.md" "docs/troubleshooting.md:docs/troubleshooting_RU.md" "docs/roadmap.md:docs/roadmap_RU.md" + "docs/research/acoustic-1-phy-sources.md:docs/research/acoustic-1-phy-sources_RU.md" "spec/protocol-v1.md:spec/protocol-v1_RU.md" + "spec/acoustic-1.md:spec/acoustic-1_RU.md" ) for pair in "${pairs[@]}"; do source_path="${pair%%:*}" diff --git a/README.md b/README.md index 01ef5c5..b6b5a6e 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,15 @@ AudioModem separates a versioned data object from the path that carries its PCM ## Status -AudioModem is in the **bootstrap** stage. The verified path is text → ADLP v1 object → lossless WAV → ADLP v1 object → verified text. Live capture/playback, Bluetooth audio routing, radio interfaces, encryption, file payloads in the CLI and release artifacts are **not implemented**. See the [roadmap](docs/roadmap.md) for the intended order of work. +AudioModem has verified Rust ADLP/WAV codecs, a native Flutter↔Rust bridge, and local WAV import/export. Two controlled WAV carriers are available: deterministic bootstrap and experimental Acoustic-1 B-FSK. The verified paths are text → ADLP v1 object → selected WAV carrier → ADLP v1 object → verified text. Live capture/playback, Bluetooth audio routing, radio interfaces, encryption, application-level file payload workflow and release artifacts are **not implemented**. See the [roadmap](docs/roadmap.md) for the intended order of work. | Area | Current status | Read more | | --- | --- | --- | | ADLP v1 container | Implemented and tested in Rust | [Protocol](spec/protocol-v1.md) | | Bootstrap WAV codec | Implemented for deterministic 48 kHz mono PCM WAV round trips | [Quick start](docs/guides/getting-started.md) | -| Flutter client | Cross-platform UI scaffold; no Rust bridge yet | [Platform support](docs/reference/platform-support.md) | -| Live acoustic modem | Designed, not implemented | [Audio routes](docs/guides/audio-routes.md) | +| Flutter client | Native Rust bridge plus local WAV import/export; web honestly reports native codec as unavailable | [Bridge architecture](docs/architecture/flutter-rust-bridge.md) | +| Acoustic-1 carrier | Experimental B-FSK controlled PCM/WAV codec with framing, Hamming(7,4), bounded sync and golden vectors; no live-route claim | [Acoustic-1 RFC](spec/acoustic-1.md) | +| Live acoustic modem | Not implemented: no capture/playback route adapter or device acceptance result | [Audio routes](docs/guides/audio-routes.md) | | Encryption and verified identity | Future RFC work | [Security](SECURITY.md) | ## Quick start: a reproducible WAV round trip @@ -29,6 +30,13 @@ cargo run -p adlp-cli -- decode hello.wav The command writes a lossless mono 48 kHz, signed 16-bit PCM WAV file and decodes the same object. This bootstrap symbol mapper is deliberately simple; it is **not** a robust over-the-air modulation scheme. The [quick-start guide](docs/guides/getting-started.md) explains the guarantee and its limits. +For a controlled experiment with the separate experimental carrier, use the explicit Acoustic-1 commands. A successful WAV round trip proves only this codec contract; it does not demonstrate a speaker-to-microphone link. + +```bash +cargo run -p adlp-cli -- encode-acoustic1-text acoustic1.wav N1 "Controlled test" balanced +cargo run -p adlp-cli -- decode-acoustic1 acoustic1.wav +``` + ## Documentation | English (canonical) | Russian translation | Purpose | @@ -37,6 +45,7 @@ The command writes a lossless mono 48 kHz, signed 16-bit PCM WAV file and decode | [Quick start](docs/guides/getting-started.md) | [Быстрый старт](docs/guides/getting-started_RU.md) | Reproducible WAV workflow | | [Audio routes](docs/guides/audio-routes.md) | [Аудиомаршруты](docs/guides/audio-routes_RU.md) | WAV, live PCM, cable, radio and Bluetooth boundaries | | [Protocol v1](spec/protocol-v1.md) | [Протокол v1](spec/protocol-v1_RU.md) | ADLP object and bootstrap WAV carrier | +| [Acoustic-1 RFC](spec/acoustic-1.md) | [Acoustic-1 RFC на русском](spec/acoustic-1_RU.md) | Experimental B-FSK carrier and compatibility boundary | | [Platform support](docs/reference/platform-support.md) | [Поддержка платформ](docs/reference/platform-support_RU.md) | Target runners and feature status | | [Transmission presets](docs/reference/presets.md) | [Пресеты передачи](docs/reference/presets_RU.md) | Reliable, Balanced, Fast and Narrowband IDs | | [Troubleshooting](docs/troubleshooting.md) | [Диагностика](docs/troubleshooting_RU.md) | Observable failures and useful reports | diff --git a/README_RU.md b/README_RU.md index c0bd601..56509bc 100644 --- a/README_RU.md +++ b/README_RU.md @@ -6,18 +6,19 @@ AudioModem отделяет версионированный объект данных от маршрута, который переносит его PCM-сигнал. Один и тот же объект ADLP можно экспортировать в WAV для воспроизводимой передачи, отправить будущим маршрутом «динамик → микрофон», подключить к line-level кабелю или направить через внешнее радиооборудование. Репозиторий содержит Rust protocol/core, Flutter-интерфейс и открытую спецификацию. -> **Статус перевода:** перевод `README.md`; синхронизировано 2026-08-19. Английский оригинал является нормативным для совместимости протокола, release commitments и решений реализации. +> **Статус перевода:** перевод `README.md`; синхронизировано 2026-08-20. Английский оригинал является нормативным для совместимости протокола, release commitments и решений реализации. ## Статус -AudioModem находится на стадии **bootstrap**. Проверенный путь выглядит так: text → ADLP v1 object → lossless WAV → ADLP v1 object → verified text. Live capture/playback, Bluetooth-аудиомаршрутизация, радиоинтерфейсы, шифрование, file payload в CLI и release artifacts **не реализованы**. Очерёдность работ приведена в [дорожной карте](docs/roadmap_RU.md). +AudioModem имеет проверенные Rust ADLP/WAV codecs, native Flutter↔Rust bridge и локальный WAV import/export. Доступны два контролируемых WAV carriers: детерминированный bootstrap и экспериментальный Acoustic-1 B-FSK. Проверенный путь выглядит так: text → ADLP v1 object → выбранный WAV carrier → ADLP v1 object → verified text. Live capture/playback, Bluetooth-аудиомаршрутизация, радиоинтерфейсы, шифрование, application-level file payload workflow и release artifacts **не реализованы**. Очерёдность работ приведена в [дорожной карте](docs/roadmap_RU.md). | Область | Текущий статус | Подробнее | | --- | --- | --- | | ADLP v1 container | Реализован и покрыт Rust-тестами | [Протокол](spec/protocol-v1_RU.md) | | Bootstrap WAV codec | Реализован для детерминированного 48 kHz mono PCM WAV round trip | [Быстрый старт](docs/guides/getting-started_RU.md) | -| Flutter client | Cross-platform UI scaffold; Rust bridge ещё нет | [Поддержка платформ](docs/reference/platform-support_RU.md) | -| Live acoustic modem | Спроектирован, не реализован | [Аудиомаршруты](docs/guides/audio-routes_RU.md) | +| Flutter client | Native Rust bridge и локальный WAV import/export; web честно сообщает, что native codec недоступен | [Архитектура bridge](docs/architecture/flutter-rust-bridge_RU.md) | +| Acoustic-1 carrier | Экспериментальный B-FSK controlled PCM/WAV codec с framing, Hamming(7,4), bounded sync и golden vectors; без live-route claim | [Acoustic-1 RFC](spec/acoustic-1_RU.md) | +| Live acoustic modem | Не реализован: нет capture/playback route adapter или device acceptance result | [Аудиомаршруты](docs/guides/audio-routes_RU.md) | | Шифрование и проверяемая идентичность | Будущая RFC-работа | [Безопасность](SECURITY.md) | ## Быстрый старт: воспроизводимый WAV round trip @@ -31,6 +32,13 @@ cargo run -p adlp-cli -- decode hello.wav Команда записывает lossless mono 48 kHz signed 16-bit PCM WAV и затем декодирует тот же объект. Bootstrap symbol mapper намеренно прост и **не** является надёжной схемой модуляции для передачи по воздуху. Гарантии и ограничения описаны в [руководстве быстрого старта](docs/guides/getting-started_RU.md). +Для controlled experiment с отдельным экспериментальным carrier используйте явные Acoustic-1 commands. Успешный WAV round trip доказывает только этот codec contract; он не демонстрирует speaker-to-microphone link. + +```bash +cargo run -p adlp-cli -- encode-acoustic1-text acoustic1.wav N1 "Контролируемый тест" balanced +cargo run -p adlp-cli -- decode-acoustic1 acoustic1.wav +``` + ## Документация | Английский оригинал | Русский перевод | Назначение | @@ -39,6 +47,7 @@ cargo run -p adlp-cli -- decode hello.wav | [Quick start](docs/guides/getting-started.md) | [Быстрый старт](docs/guides/getting-started_RU.md) | Воспроизводимый WAV workflow | | [Audio routes](docs/guides/audio-routes.md) | [Аудиомаршруты](docs/guides/audio-routes_RU.md) | Границы WAV, live PCM, cable, radio и Bluetooth | | [Protocol v1](spec/protocol-v1.md) | [Протокол v1](spec/protocol-v1_RU.md) | ADLP object и bootstrap WAV carrier | +| [Acoustic-1 RFC](spec/acoustic-1.md) | [Acoustic-1 RFC на русском](spec/acoustic-1_RU.md) | Экспериментальный B-FSK carrier и граница compatibility | | [Platform support](docs/reference/platform-support.md) | [Поддержка платформ](docs/reference/platform-support_RU.md) | Target runners и feature status | | [Transmission presets](docs/reference/presets.md) | [Пресеты передачи](docs/reference/presets_RU.md) | Reliable, Balanced, Fast и Narrowband IDs | | [Troubleshooting](docs/troubleshooting.md) | [Диагностика](docs/troubleshooting_RU.md) | Наблюдаемые ошибки и полезные отчёты | diff --git a/apps/audio_modem/lib/bridge/wav_bootstrap_bridge.dart b/apps/audio_modem/lib/bridge/wav_bootstrap_bridge.dart index 44eb725..3d866b8 100644 --- a/apps/audio_modem/lib/bridge/wav_bootstrap_bridge.dart +++ b/apps/audio_modem/lib/bridge/wav_bootstrap_bridge.dart @@ -9,12 +9,14 @@ class WavBuildResult { const WavBuildResult({ required this.sessionId, required this.profile, + required this.carrier, required this.sampleRateHz, required this.wavBytes, }); final int sessionId; final String profile; + final String carrier; final int sampleRateHz; final Uint8List wavBytes; } @@ -24,6 +26,7 @@ class WavDecodeResult { required this.sessionId, required this.senderCallsign, required this.profile, + required this.carrier, required this.text, required this.sampleRateHz, required this.samplesConsumed, @@ -32,6 +35,7 @@ class WavDecodeResult { final int sessionId; final String senderCallsign; final String profile; + final String carrier; final String text; final int sampleRateHz; final int samplesConsumed; @@ -45,9 +49,13 @@ abstract interface class WavBootstrapBridge { required String senderCallsign, required String text, required String profile, + required String carrier, }); - Future decodeWav(Uint8List wavBytes); + Future decodeWav({ + required Uint8List wavBytes, + required String carrier, + }); } class NativeWavBootstrapBridge implements WavBootstrapBridge { @@ -72,28 +80,38 @@ class NativeWavBootstrapBridge implements WavBootstrapBridge { required String senderCallsign, required String text, required String profile, + required String carrier, }) async { final result = await rust.encodeTextToWav( sessionId: sessionId, senderCallsign: senderCallsign, text: text, profile: profile, + carrier: carrier, ); return WavBuildResult( sessionId: result.sessionId, profile: result.profile, + carrier: result.carrier, sampleRateHz: result.sampleRateHz, wavBytes: result.wavBytes, ); } @override - Future decodeWav(Uint8List wavBytes) async { - final result = await rust.decodeWavText(wavBytes: wavBytes); + Future decodeWav({ + required Uint8List wavBytes, + required String carrier, + }) async { + final result = await rust.decodeWavText( + wavBytes: wavBytes, + carrier: carrier, + ); return WavDecodeResult( sessionId: result.sessionId, senderCallsign: result.senderCallsign, profile: result.profile, + carrier: result.carrier, text: result.text, sampleRateHz: result.sampleRateHz, samplesConsumed: result.samplesConsumed, @@ -115,9 +133,12 @@ class UnavailableWavBootstrapBridge implements WavBootstrapBridge { required String senderCallsign, required String text, required String profile, + required String carrier, }) => Future.error(StateError(reason)); @override - Future decodeWav(Uint8List wavBytes) => - Future.error(StateError(reason)); + Future decodeWav({ + required Uint8List wavBytes, + required String carrier, + }) => Future.error(StateError(reason)); } diff --git a/apps/audio_modem/lib/main.dart b/apps/audio_modem/lib/main.dart index d5a8736..ad2469e 100644 --- a/apps/audio_modem/lib/main.dart +++ b/apps/audio_modem/lib/main.dart @@ -70,6 +70,7 @@ class _TransferWorkbenchState extends State { final _callsign = TextEditingController(text: 'N1'); final _text = TextEditingController(text: 'Привет через AudioModem'); TransferPreset _preset = TransferPreset.balanced; + CarrierKind _carrier = CarrierKind.bootstrap; TransferRoute _route = TransferRoute.wav; int _tabIndex = 0; bool _isWorking = false; @@ -77,6 +78,7 @@ class _TransferWorkbenchState extends State { WavDecodeResult? _decodedWav; String? _activeWavName; Uint8List? _activeWavBytes; + CarrierKind? _activeCarrier; String? _bridgeError; @override @@ -108,16 +110,21 @@ class _TransferWorkbenchState extends State { senderCallsign: _callsign.text.trim(), text: _text.text, profile: _preset.bridgeProfile, + carrier: _carrier.bridgeCarrier, + ); + final decoded = await widget.bridge.decodeWav( + wavBytes: built.wavBytes, + carrier: _carrier.bridgeCarrier, ); - final decoded = await widget.bridge.decodeWav(built.wavBytes); if (!mounted) { return; } setState(() { _builtWav = built; _decodedWav = decoded; - _activeWavName = _suggestedWavName(built.sessionId); + _activeWavName = _suggestedWavName(built.sessionId, _carrier); _activeWavBytes = built.wavBytes; + _activeCarrier = _carrier; }); _showMessage('WAV собран и проверен Rust decoder.'); } catch (error) { @@ -135,7 +142,8 @@ class _TransferWorkbenchState extends State { Future _verifyCurrentWav() async { final wavBytes = _activeWavBytes; - if (wavBytes == null) { + final carrier = _activeCarrier; + if (wavBytes == null || carrier == null) { _showMessage('Сначала соберите или импортируйте WAV.'); return; } @@ -144,7 +152,10 @@ class _TransferWorkbenchState extends State { _bridgeError = null; }); try { - final decoded = await widget.bridge.decodeWav(wavBytes); + final decoded = await widget.bridge.decodeWav( + wavBytes: wavBytes, + carrier: carrier.bridgeCarrier, + ); if (!mounted) { return; } @@ -174,7 +185,7 @@ class _TransferWorkbenchState extends State { }); try { final saved = await widget.fileAdapter.saveWav( - suggestedName: _suggestedWavName(built.sessionId), + suggestedName: _suggestedWavName(built.sessionId, _carrier), bytes: built.wavBytes, ); if (!mounted) { @@ -215,13 +226,17 @@ class _TransferWorkbenchState extends State { } return; } - final decoded = await widget.bridge.decodeWav(selected.bytes); + final decoded = await widget.bridge.decodeWav( + wavBytes: selected.bytes, + carrier: _carrier.bridgeCarrier, + ); if (!mounted) { return; } setState(() { _activeWavName = selected.name; _activeWavBytes = selected.bytes; + _activeCarrier = _carrier; _decodedWav = decoded; }); _showMessage('WAV импортирован и проверен Rust decoder.'); @@ -240,7 +255,8 @@ class _TransferWorkbenchState extends State { } } - String _suggestedWavName(int sessionId) => 'adlp-$sessionId.wav'; + String _suggestedWavName(int sessionId, CarrierKind carrier) => + 'adlp-${carrier.fileStem}-$sessionId.wav'; void _showMessage(String message) { ScaffoldMessenger.of(context) @@ -309,7 +325,7 @@ class _TransferWorkbenchState extends State { ), const SizedBox(height: 8), const Text( - 'Rust создаёт версионированный ADLP object, кодирует canonical WAV в памяти и сразу декодирует его для проверки целостности.', + 'Rust создаёт версионированный ADLP object, кодирует выбранный canonical WAV carrier в памяти и сразу декодирует его для проверки целостности.', ), const SizedBox(height: 28), Wrap( @@ -375,6 +391,30 @@ class _TransferWorkbenchState extends State { .toList(), ), const SizedBox(height: 18), + Text('WAV carrier', style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: CarrierKind.values + .map( + (carrier) => ChoiceChip( + label: Text(carrier.label), + selected: _carrier == carrier, + onSelected: (_) => setState(() => _carrier = carrier), + tooltip: carrier.description, + ), + ) + .toList(), + ), + if (_carrier == CarrierKind.acoustic1) ...[ + const SizedBox(height: 8), + const Text( + 'Acoustic-1 — экспериментальный PCM carrier для контролируемых WAV tests; это не live-audio маршрут.', + style: TextStyle(fontSize: 12, height: 1.4), + ), + ], + const SizedBox(height: 18), Text( 'Маршрут аудио', style: Theme.of(context).textTheme.titleSmall, @@ -414,8 +454,8 @@ class _TransferWorkbenchState extends State { children: [ const CarrierStrip(), const SizedBox(height: 24), - const Text( - 'WAV BOOTSTRAP', + Text( + _carrier.label.toUpperCase(), style: TextStyle( letterSpacing: 1.4, fontSize: 12, @@ -433,6 +473,7 @@ class _TransferWorkbenchState extends State { const SizedBox(height: 18), Metric(label: 'Контейнер', value: 'ADLP / v1'), Metric(label: 'Профиль', value: _preset.profileId), + Metric(label: 'Carrier', value: _carrier.label), Metric(label: 'Маршрут', value: _route.label), Metric(label: 'Текст', value: '$bytes байт UTF-8'), const SizedBox(height: 24), @@ -499,7 +540,7 @@ class _TransferWorkbenchState extends State { ], const SizedBox(height: 12), const Text( - 'WAV хранится только в памяти. Export/import file adapter и live audio ещё не реализованы.', + 'WAV можно экспортировать и импортировать локально. Live audio, cable, Bluetooth и radio adapters ещё не реализованы.', style: TextStyle( fontSize: 12, height: 1.4, @@ -565,6 +606,7 @@ class _TransferWorkbenchState extends State { const SizedBox(height: 20), _receiptRow('Позывной', decoded.senderCallsign), _receiptRow('Профиль', decoded.profile), + _receiptRow('Carrier', decoded.carrier), _receiptRow('Текст', decoded.text), _receiptRow( 'Семплы', @@ -619,9 +661,9 @@ class _TransferWorkbenchState extends State { } enum TransferPreset { - reliable('Надёжный', 'Acoustic-1/Reliable', 'reliable'), - balanced('Сбалансированный', 'Acoustic-1/Balanced', 'balanced'), - fast('Быстрый', 'Acoustic-1/Fast', 'fast'); + reliable('Надёжный', 'ADLP / Reliable', 'reliable'), + balanced('Сбалансированный', 'ADLP / Balanced', 'balanced'), + fast('Быстрый', 'ADLP / Fast', 'fast'); const TransferPreset(this.label, this.profileId, this.bridgeProfile); final String label; @@ -629,6 +671,33 @@ enum TransferPreset { final String bridgeProfile; } +enum CarrierKind { + bootstrap( + 'WAV bootstrap', + 'bootstrap', + 'bootstrap', + 'Детерминированный lossless reference carrier.', + ), + acoustic1( + 'Acoustic-1', + 'acoustic1', + 'acoustic1', + 'Экспериментальный B-FSK carrier с Hamming FEC для контролируемых WAV tests.', + ); + + const CarrierKind( + this.label, + this.bridgeCarrier, + this.fileStem, + this.description, + ); + + final String label; + final String bridgeCarrier; + final String fileStem; + final String description; +} + enum TransferRoute { wav('WAV', Icons.audio_file_outlined), speaker('Динамик', Icons.volume_up_outlined), diff --git a/apps/audio_modem/lib/src/rust/api/wav_bootstrap.dart b/apps/audio_modem/lib/src/rust/api/wav_bootstrap.dart index 8c273cd..f7f561e 100644 --- a/apps/audio_modem/lib/src/rust/api/wav_bootstrap.dart +++ b/apps/audio_modem/lib/src/rust/api/wav_bootstrap.dart @@ -7,32 +7,39 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `parse_profile` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `fmt`, `fmt` +// These functions are ignored because they are not marked as `pub`: `as_str`, `parse_carrier`, `parse_profile` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `Carrier` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `fmt`, `fmt` -/// Encodes one text-only ADLP object into canonical 48 kHz mono 16-bit PCM WAV bytes. +/// Encodes one text-only ADLP object into a selected canonical 48 kHz WAV carrier. Future encodeTextToWav({ required int sessionId, required String senderCallsign, required String text, required String profile, + required String carrier, }) => AudioModemRust.instance.api.crateApiWavBootstrapEncodeTextToWav( sessionId: sessionId, senderCallsign: senderCallsign, text: text, profile: profile, + carrier: carrier, ); -/// Decodes and verifies canonical WAV bytes before exposing text metadata to Flutter. -Future decodeWavText({required List wavBytes}) => - AudioModemRust.instance.api.crateApiWavBootstrapDecodeWavText( - wavBytes: wavBytes, - ); +/// Decodes and verifies selected-carrier WAV bytes before exposing text metadata to Flutter. +Future decodeWavText({ + required List wavBytes, + required String carrier, +}) => AudioModemRust.instance.api.crateApiWavBootstrapDecodeWavText( + wavBytes: wavBytes, + carrier: carrier, +); class DecodedTextTransfer { final int sessionId; final String senderCallsign; final String profile; + final String carrier; final String text; final int sampleRateHz; final int samplesConsumed; @@ -41,6 +48,7 @@ class DecodedTextTransfer { required this.sessionId, required this.senderCallsign, required this.profile, + required this.carrier, required this.text, required this.sampleRateHz, required this.samplesConsumed, @@ -51,6 +59,7 @@ class DecodedTextTransfer { sessionId.hashCode ^ senderCallsign.hashCode ^ profile.hashCode ^ + carrier.hashCode ^ text.hashCode ^ sampleRateHz.hashCode ^ samplesConsumed.hashCode; @@ -63,6 +72,7 @@ class DecodedTextTransfer { sessionId == other.sessionId && senderCallsign == other.senderCallsign && profile == other.profile && + carrier == other.carrier && text == other.text && sampleRateHz == other.sampleRateHz && samplesConsumed == other.samplesConsumed; @@ -71,12 +81,14 @@ class DecodedTextTransfer { class EncodedWavTransfer { final int sessionId; final String profile; + final String carrier; final int sampleRateHz; final Uint8List wavBytes; const EncodedWavTransfer({ required this.sessionId, required this.profile, + required this.carrier, required this.sampleRateHz, required this.wavBytes, }); @@ -85,6 +97,7 @@ class EncodedWavTransfer { int get hashCode => sessionId.hashCode ^ profile.hashCode ^ + carrier.hashCode ^ sampleRateHz.hashCode ^ wavBytes.hashCode; @@ -95,6 +108,7 @@ class EncodedWavTransfer { runtimeType == other.runtimeType && sessionId == other.sessionId && profile == other.profile && + carrier == other.carrier && sampleRateHz == other.sampleRateHz && wavBytes == other.wavBytes; } diff --git a/apps/audio_modem/lib/src/rust/frb_generated.dart b/apps/audio_modem/lib/src/rust/frb_generated.dart index 482be42..80c3d61 100644 --- a/apps/audio_modem/lib/src/rust/frb_generated.dart +++ b/apps/audio_modem/lib/src/rust/frb_generated.dart @@ -87,6 +87,7 @@ class AudioModemRust abstract class AudioModemRustApi extends BaseApi { Future crateApiWavBootstrapDecodeWavText({ required List wavBytes, + required String carrier, }); Future crateApiWavBootstrapEncodeTextToWav({ @@ -94,6 +95,7 @@ abstract class AudioModemRustApi extends BaseApi { required String senderCallsign, required String text, required String profile, + required String carrier, }); } @@ -109,12 +111,14 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform @override Future crateApiWavBootstrapDecodeWavText({ required List wavBytes, + required String carrier, }) { return handler.executeNormal( NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_prim_u_8_loose(wavBytes, serializer); + sse_encode_String(carrier, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -127,14 +131,17 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform decodeErrorData: sse_decode_String, ), constMeta: kCrateApiWavBootstrapDecodeWavTextConstMeta, - argValues: [wavBytes], + argValues: [wavBytes, carrier], apiImpl: this, ), ); } TaskConstMeta get kCrateApiWavBootstrapDecodeWavTextConstMeta => - const TaskConstMeta(debugName: "decode_wav_text", argNames: ["wavBytes"]); + const TaskConstMeta( + debugName: "decode_wav_text", + argNames: ["wavBytes", "carrier"], + ); @override Future crateApiWavBootstrapEncodeTextToWav({ @@ -142,6 +149,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform required String senderCallsign, required String text, required String profile, + required String carrier, }) { return handler.executeNormal( NormalTask( @@ -151,6 +159,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform sse_encode_String(senderCallsign, serializer); sse_encode_String(text, serializer); sse_encode_String(profile, serializer); + sse_encode_String(carrier, serializer); pdeCallFfi( generalizedFrbRustBinding, serializer, @@ -163,7 +172,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform decodeErrorData: sse_decode_String, ), constMeta: kCrateApiWavBootstrapEncodeTextToWavConstMeta, - argValues: [sessionId, senderCallsign, text, profile], + argValues: [sessionId, senderCallsign, text, profile, carrier], apiImpl: this, ), ); @@ -172,7 +181,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform TaskConstMeta get kCrateApiWavBootstrapEncodeTextToWavConstMeta => const TaskConstMeta( debugName: "encode_text_to_wav", - argNames: ["sessionId", "senderCallsign", "text", "profile"], + argNames: ["sessionId", "senderCallsign", "text", "profile", "carrier"], ); @protected @@ -193,15 +202,16 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform DecodedTextTransfer dco_decode_decoded_text_transfer(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 6) - throw Exception('unexpected arr length: expect 6 but see ${arr.length}'); + if (arr.length != 7) + throw Exception('unexpected arr length: expect 7 but see ${arr.length}'); return DecodedTextTransfer( sessionId: dco_decode_CastedPrimitive_u_64(arr[0]), senderCallsign: dco_decode_String(arr[1]), profile: dco_decode_String(arr[2]), - text: dco_decode_String(arr[3]), - sampleRateHz: dco_decode_u_32(arr[4]), - samplesConsumed: dco_decode_u_32(arr[5]), + carrier: dco_decode_String(arr[3]), + text: dco_decode_String(arr[4]), + sampleRateHz: dco_decode_u_32(arr[5]), + samplesConsumed: dco_decode_u_32(arr[6]), ); } @@ -209,13 +219,14 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform EncodedWavTransfer dco_decode_encoded_wav_transfer(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 4) - throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); return EncodedWavTransfer( sessionId: dco_decode_CastedPrimitive_u_64(arr[0]), profile: dco_decode_String(arr[1]), - sampleRateHz: dco_decode_u_32(arr[2]), - wavBytes: dco_decode_list_prim_u_8_strict(arr[3]), + carrier: dco_decode_String(arr[2]), + sampleRateHz: dco_decode_u_32(arr[3]), + wavBytes: dco_decode_list_prim_u_8_strict(arr[4]), ); } @@ -271,6 +282,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform var var_sessionId = sse_decode_CastedPrimitive_u_64(deserializer); var var_senderCallsign = sse_decode_String(deserializer); var var_profile = sse_decode_String(deserializer); + var var_carrier = sse_decode_String(deserializer); var var_text = sse_decode_String(deserializer); var var_sampleRateHz = sse_decode_u_32(deserializer); var var_samplesConsumed = sse_decode_u_32(deserializer); @@ -278,6 +290,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform sessionId: var_sessionId, senderCallsign: var_senderCallsign, profile: var_profile, + carrier: var_carrier, text: var_text, sampleRateHz: var_sampleRateHz, samplesConsumed: var_samplesConsumed, @@ -291,11 +304,13 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform // Codec=Sse (Serialization based), see doc to use other codecs var var_sessionId = sse_decode_CastedPrimitive_u_64(deserializer); var var_profile = sse_decode_String(deserializer); + var var_carrier = sse_decode_String(deserializer); var var_sampleRateHz = sse_decode_u_32(deserializer); var var_wavBytes = sse_decode_list_prim_u_8_strict(deserializer); return EncodedWavTransfer( sessionId: var_sessionId, profile: var_profile, + carrier: var_carrier, sampleRateHz: var_sampleRateHz, wavBytes: var_wavBytes, ); @@ -366,6 +381,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform sse_encode_CastedPrimitive_u_64(self.sessionId, serializer); sse_encode_String(self.senderCallsign, serializer); sse_encode_String(self.profile, serializer); + sse_encode_String(self.carrier, serializer); sse_encode_String(self.text, serializer); sse_encode_u_32(self.sampleRateHz, serializer); sse_encode_u_32(self.samplesConsumed, serializer); @@ -379,6 +395,7 @@ class AudioModemRustApiImpl extends AudioModemRustApiImplPlatform // Codec=Sse (Serialization based), see doc to use other codecs sse_encode_CastedPrimitive_u_64(self.sessionId, serializer); sse_encode_String(self.profile, serializer); + sse_encode_String(self.carrier, serializer); sse_encode_u_32(self.sampleRateHz, serializer); sse_encode_list_prim_u_8_strict(self.wavBytes, serializer); } diff --git a/apps/audio_modem/rust/src/api/wav_bootstrap.rs b/apps/audio_modem/rust/src/api/wav_bootstrap.rs index 1d22fcd..a6bf36a 100644 --- a/apps/audio_modem/rust/src/api/wav_bootstrap.rs +++ b/apps/audio_modem/rust/src/api/wav_bootstrap.rs @@ -1,8 +1,11 @@ -//! Typed WAV bootstrap API: user text and callsign in, verified ADLP metadata out. +//! Typed WAV carrier API: user text and callsign in, verified ADLP metadata out. //! No live device, encryption, identity verification or file-object workflow is exposed here. use adlp_protocol::{ObjectKind, TransferProfile, WireObject}; -use audio_modem_core::{decode_wav, encode_wav, SAMPLE_RATE_HZ}; +use audio_modem_core::{ + acoustic1, decode_wav as decode_bootstrap_wav, encode_wav as encode_bootstrap_wav, + SAMPLE_RATE_HZ, +}; const MAX_BRIDGE_TEXT_BYTES: usize = 8 * 1024; @@ -10,6 +13,7 @@ const MAX_BRIDGE_TEXT_BYTES: usize = 8 * 1024; pub struct EncodedWavTransfer { pub session_id: u64, pub profile: String, + pub carrier: String, pub sample_rate_hz: u32, pub wav_bytes: Vec, } @@ -19,52 +23,78 @@ pub struct DecodedTextTransfer { pub session_id: u64, pub sender_callsign: String, pub profile: String, + pub carrier: String, pub text: String, pub sample_rate_hz: u32, pub samples_consumed: u32, } -/// Encodes one text-only ADLP object into canonical 48 kHz mono 16-bit PCM WAV bytes. +/// Encodes one text-only ADLP object into a selected canonical 48 kHz WAV carrier. pub fn encode_text_to_wav( session_id: u64, sender_callsign: String, text: String, profile: String, + carrier: String, ) -> Result { if session_id == 0 { return Err("session_id must be positive".to_owned()); } if text.len() > MAX_BRIDGE_TEXT_BYTES { - return Err("text exceeds the 8 KiB WAV bootstrap application limit".to_owned()); + return Err("text exceeds the 8 KiB WAV carrier application limit".to_owned()); } let profile = parse_profile(&profile)?; + let carrier = parse_carrier(&carrier)?; let object = WireObject::text(session_id, sender_callsign, text, profile) .map_err(|error| error.to_string())?; - let wav_bytes = encode_wav(&object).map_err(|error| error.to_string())?; + let wav_bytes = match carrier { + Carrier::Bootstrap => encode_bootstrap_wav(&object).map_err(|error| error.to_string())?, + Carrier::Acoustic1 => acoustic1::encode_wav(&object).map_err(|error| error.to_string())?, + }; Ok(EncodedWavTransfer { session_id, profile: profile.as_str().to_owned(), + carrier: carrier.as_str().to_owned(), sample_rate_hz: SAMPLE_RATE_HZ, wav_bytes, }) } -/// Decodes and verifies canonical WAV bytes before exposing text metadata to Flutter. -pub fn decode_wav_text(wav_bytes: Vec) -> Result { - let decoded = decode_wav(&wav_bytes).map_err(|error| error.to_string())?; - if decoded.object.manifest.object_kind != ObjectKind::Text { +/// Decodes and verifies selected-carrier WAV bytes before exposing text metadata to Flutter. +pub fn decode_wav_text(wav_bytes: Vec, carrier: String) -> Result { + let carrier = parse_carrier(&carrier)?; + let (object, sample_rate_hz, samples_consumed) = match carrier { + Carrier::Bootstrap => { + let decoded = decode_bootstrap_wav(&wav_bytes).map_err(|error| error.to_string())?; + ( + decoded.object, + decoded.sample_rate_hz, + decoded.samples_consumed, + ) + } + Carrier::Acoustic1 => { + let decoded = acoustic1::decode_wav(&wav_bytes).map_err(|error| error.to_string())?; + ( + decoded.object, + decoded.sample_rate_hz, + decoded.samples_consumed, + ) + } + }; + if object.manifest.object_kind != ObjectKind::Text { return Err("the WAV contains a non-text ADLP object".to_owned()); } - let text = String::from_utf8(decoded.object.payload) + let text = String::from_utf8(object.payload) .map_err(|_| "the ADLP text payload is not valid UTF-8".to_owned())?; - let samples_consumed = u32::try_from(decoded.samples_consumed) + let samples_consumed = u32::try_from(samples_consumed) .map_err(|_| "decoded sample count exceeds bridge return range".to_owned())?; Ok(DecodedTextTransfer { - session_id: decoded.object.manifest.session_id, - sender_callsign: decoded.object.manifest.sender_callsign, - profile: decoded.object.manifest.profile.as_str().to_owned(), + session_id: object.manifest.session_id, + sender_callsign: object.manifest.sender_callsign, + profile: object.manifest.profile.as_str().to_owned(), + carrier: carrier.as_str().to_owned(), text, - sample_rate_hz: decoded.sample_rate_hz, + sample_rate_hz, samples_consumed, }) } @@ -79,34 +109,98 @@ fn parse_profile(value: &str) -> Result { } } +#[derive(Clone, Copy)] +enum Carrier { + Bootstrap, + Acoustic1, +} + +impl Carrier { + fn as_str(self) -> &'static str { + match self { + Self::Bootstrap => "bootstrap", + Self::Acoustic1 => "acoustic1", + } + } +} + +fn parse_carrier(value: &str) -> Result { + match value { + "bootstrap" => Ok(Carrier::Bootstrap), + "acoustic1" => Ok(Carrier::Acoustic1), + _ => Err("carrier must be bootstrap or acoustic1".to_owned()), + } +} + #[cfg(test)] mod tests { use super::*; #[test] - fn bridge_round_trip_preserves_text_metadata() { + fn bridge_round_trip_preserves_bootstrap_text_metadata() { let encoded = encode_text_to_wav( 7, "N1".to_owned(), "Hello from Flutter".to_owned(), "balanced".to_owned(), + "bootstrap".to_owned(), ) .unwrap(); - let decoded = decode_wav_text(encoded.wav_bytes).unwrap(); + let decoded = decode_wav_text(encoded.wav_bytes, "bootstrap".to_owned()).unwrap(); assert_eq!(decoded.session_id, 7); assert_eq!(decoded.sender_callsign, "N1"); assert_eq!(decoded.profile, "balanced"); + assert_eq!(decoded.carrier, "bootstrap"); assert_eq!(decoded.text, "Hello from Flutter"); assert_eq!(decoded.sample_rate_hz, SAMPLE_RATE_HZ); } #[test] fn bridge_rejects_zero_session_and_unknown_profile() { - assert!( - encode_text_to_wav(0, "N1".to_owned(), "x".to_owned(), "balanced".to_owned()).is_err() - ); - assert!( - encode_text_to_wav(1, "N1".to_owned(), "x".to_owned(), "turbo".to_owned()).is_err() - ); + assert!(encode_text_to_wav( + 0, + "N1".to_owned(), + "x".to_owned(), + "balanced".to_owned(), + "bootstrap".to_owned(), + ) + .is_err()); + assert!(encode_text_to_wav( + 1, + "N1".to_owned(), + "x".to_owned(), + "turbo".to_owned(), + "bootstrap".to_owned(), + ) + .is_err()); + } + + #[test] + fn bridge_round_trips_experimental_acoustic1_metadata() { + let encoded = encode_text_to_wav( + 8, + "AC1".to_owned(), + "Controlled carrier".to_owned(), + "fast".to_owned(), + "acoustic1".to_owned(), + ) + .unwrap(); + let decoded = decode_wav_text(encoded.wav_bytes, "acoustic1".to_owned()).unwrap(); + assert_eq!(decoded.session_id, 8); + assert_eq!(decoded.profile, "fast"); + assert_eq!(decoded.carrier, "acoustic1"); + } + + #[test] + fn bridge_rejects_an_acoustic1_wav_with_bootstrap_decoder() { + let encoded = encode_text_to_wav( + 9, + "AC1".to_owned(), + "Carrier contract".to_owned(), + "balanced".to_owned(), + "acoustic1".to_owned(), + ) + .unwrap(); + assert!(decode_wav_text(encoded.wav_bytes, "bootstrap".to_owned()).is_err()); } } diff --git a/apps/audio_modem/rust/src/frb_generated.rs b/apps/audio_modem/rust/src/frb_generated.rs index 49d13a0..9924cee 100644 --- a/apps/audio_modem/rust/src/frb_generated.rs +++ b/apps/audio_modem/rust/src/frb_generated.rs @@ -69,10 +69,12 @@ fn wire__crate__api__wav_bootstrap__decode_wav_text_impl( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let api_wav_bytes = >::sse_decode(&mut deserializer); + let api_carrier = ::sse_decode(&mut deserializer); deserializer.end(); move |context| { transform_result_sse::<_, String>((move || { - let output_ok = crate::api::wav_bootstrap::decode_wav_text(api_wav_bytes)?; + let output_ok = + crate::api::wav_bootstrap::decode_wav_text(api_wav_bytes, api_carrier)?; Ok(output_ok) })()) } @@ -105,6 +107,7 @@ fn wire__crate__api__wav_bootstrap__encode_text_to_wav_impl( let api_sender_callsign = ::sse_decode(&mut deserializer); let api_text = ::sse_decode(&mut deserializer); let api_profile = ::sse_decode(&mut deserializer); + let api_carrier = ::sse_decode(&mut deserializer); deserializer.end(); move |context| { transform_result_sse::<_, String>((move || { @@ -113,6 +116,7 @@ fn wire__crate__api__wav_bootstrap__encode_text_to_wav_impl( api_sender_callsign, api_text, api_profile, + api_carrier, )?; Ok(output_ok) })()) @@ -137,6 +141,7 @@ impl SseDecode for crate::api::wav_bootstrap::DecodedTextTransfer { let mut var_sessionId = ::sse_decode(deserializer); let mut var_senderCallsign = ::sse_decode(deserializer); let mut var_profile = ::sse_decode(deserializer); + let mut var_carrier = ::sse_decode(deserializer); let mut var_text = ::sse_decode(deserializer); let mut var_sampleRateHz = ::sse_decode(deserializer); let mut var_samplesConsumed = ::sse_decode(deserializer); @@ -144,6 +149,7 @@ impl SseDecode for crate::api::wav_bootstrap::DecodedTextTransfer { session_id: var_sessionId, sender_callsign: var_senderCallsign, profile: var_profile, + carrier: var_carrier, text: var_text, sample_rate_hz: var_sampleRateHz, samples_consumed: var_samplesConsumed, @@ -156,11 +162,13 @@ impl SseDecode for crate::api::wav_bootstrap::EncodedWavTransfer { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { let mut var_sessionId = ::sse_decode(deserializer); let mut var_profile = ::sse_decode(deserializer); + let mut var_carrier = ::sse_decode(deserializer); let mut var_sampleRateHz = ::sse_decode(deserializer); let mut var_wavBytes = >::sse_decode(deserializer); return crate::api::wav_bootstrap::EncodedWavTransfer { session_id: var_sessionId, profile: var_profile, + carrier: var_carrier, sample_rate_hz: var_sampleRateHz, wav_bytes: var_wavBytes, }; @@ -257,6 +265,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::wav_bootstrap::DecodedTextTra self.session_id.into_into_dart().into_dart(), self.sender_callsign.into_into_dart().into_dart(), self.profile.into_into_dart().into_dart(), + self.carrier.into_into_dart().into_dart(), self.text.into_into_dart().into_dart(), self.sample_rate_hz.into_into_dart().into_dart(), self.samples_consumed.into_into_dart().into_dart(), @@ -281,6 +290,7 @@ impl flutter_rust_bridge::IntoDart for crate::api::wav_bootstrap::EncodedWavTran [ self.session_id.into_into_dart().into_dart(), self.profile.into_into_dart().into_dart(), + self.carrier.into_into_dart().into_dart(), self.sample_rate_hz.into_into_dart().into_dart(), self.wav_bytes.into_into_dart().into_dart(), ] @@ -312,6 +322,7 @@ impl SseEncode for crate::api::wav_bootstrap::DecodedTextTransfer { ::sse_encode(self.session_id, serializer); ::sse_encode(self.sender_callsign, serializer); ::sse_encode(self.profile, serializer); + ::sse_encode(self.carrier, serializer); ::sse_encode(self.text, serializer); ::sse_encode(self.sample_rate_hz, serializer); ::sse_encode(self.samples_consumed, serializer); @@ -323,6 +334,7 @@ impl SseEncode for crate::api::wav_bootstrap::EncodedWavTransfer { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { ::sse_encode(self.session_id, serializer); ::sse_encode(self.profile, serializer); + ::sse_encode(self.carrier, serializer); ::sse_encode(self.sample_rate_hz, serializer); >::sse_encode(self.wav_bytes, serializer); } diff --git a/apps/audio_modem/test/widget_test.dart b/apps/audio_modem/test/widget_test.dart index 906d7aa..19c31ed 100644 --- a/apps/audio_modem/test/widget_test.dart +++ b/apps/audio_modem/test/widget_test.dart @@ -7,19 +7,28 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; class _FakeWavBridge implements WavBootstrapBridge { + String? encodedCarrier; + String? decodedCarrier; + @override bool get isAvailable => true; @override - Future decodeWav(Uint8List wavBytes) async => - const WavDecodeResult( - sessionId: 7, - senderCallsign: 'N1', - profile: 'fast', - text: 'Привет через AudioModem', - sampleRateHz: 48000, - samplesConsumed: 512, - ); + Future decodeWav({ + required Uint8List wavBytes, + required String carrier, + }) async { + decodedCarrier = carrier; + return WavDecodeResult( + sessionId: 7, + senderCallsign: 'N1', + profile: 'fast', + carrier: carrier, + text: 'Привет через AudioModem', + sampleRateHz: 48000, + samplesConsumed: 512, + ); + } @override Future encodeText({ @@ -27,12 +36,17 @@ class _FakeWavBridge implements WavBootstrapBridge { required String senderCallsign, required String text, required String profile, - }) async => WavBuildResult( - sessionId: sessionId, - profile: profile, - sampleRateHz: 48000, - wavBytes: Uint8List.fromList([82, 73, 70, 70]), - ); + required String carrier, + }) async { + encodedCarrier = carrier; + return WavBuildResult( + sessionId: sessionId, + profile: profile, + carrier: carrier, + sampleRateHz: 48000, + wavBytes: Uint8List.fromList([82, 73, 70, 70]), + ); + } } class _FakeWavFileAdapter implements WavFileAdapter { @@ -62,8 +76,9 @@ void main() { tester, ) async { final fileAdapter = _FakeWavFileAdapter(); + final bridge = _FakeWavBridge(); await tester.pumpWidget( - AudioModemApp(bridge: _FakeWavBridge(), fileAdapter: fileAdapter), + AudioModemApp(bridge: bridge, fileAdapter: fileAdapter), ); expect(find.text('Соберите и проверьте WAV.'), findsOneWidget); @@ -77,6 +92,11 @@ void main() { final fastPreset = tester.widget(fastFinder); expect(fastPreset.selected, isTrue); + final acousticFinder = find.widgetWithText(ChoiceChip, 'Acoustic-1'); + await tester.ensureVisible(acousticFinder); + await tester.tap(acousticFinder); + await tester.pumpAndSettle(); + final buildButton = find.widgetWithText( FilledButton, 'Собрать и проверить WAV', @@ -87,6 +107,8 @@ void main() { expect(find.text('ПРОВЕРЕНО RUST'), findsOneWidget); expect(find.text('4 байт'), findsOneWidget); + expect(bridge.encodedCarrier, 'acoustic1'); + expect(bridge.decodedCarrier, 'acoustic1'); final exportButton = find.widgetWithText( OutlinedButton, @@ -96,7 +118,7 @@ void main() { await tester.tap(exportButton); await tester.pumpAndSettle(); - expect(fileAdapter.savedFile?.name, startsWith('adlp-')); + expect(fileAdapter.savedFile?.name, startsWith('adlp-acoustic1-')); }); testWidgets('receive workbench imports and verifies a user-selected WAV', ( diff --git a/crates/audio-modem-core/src/acoustic1.rs b/crates/audio-modem-core/src/acoustic1.rs new file mode 100644 index 0000000..44fb668 --- /dev/null +++ b/crates/audio-modem-core/src/acoustic1.rs @@ -0,0 +1,415 @@ +//! Experimental Acoustic-1 B-FSK carrier for controlled PCM/WAV tests. +//! +//! It transports an unchanged ADLP v1 wire object. This module deliberately +//! does not claim live-device timing recovery, AGC, or acoustic interoperability. + +use std::fmt; + +use adlp_protocol::{ProtocolError, TransferProfile, WireObject}; + +use crate::{make_canonical_wav, parse_canonical_wav, CodecError, SAMPLE_RATE_HZ}; + +pub const MAX_WIRE_BYTES: usize = 256; +pub const PREAMBLE_BITS: usize = 64; +pub const SYNC_WORD: u16 = 0xD391; +pub const MAX_SYNC_OFFSET_SAMPLES: usize = 480; + +const SINE_120: [i16; 48] = [ + 0, 16, 31, 46, 60, 73, 85, 95, 104, 111, 116, 119, 120, 119, 116, 111, 104, 95, 85, 73, 60, 46, + 31, 16, 0, -16, -31, -46, -60, -73, -85, -95, -104, -111, -116, -119, -120, -119, -116, -111, + -104, -95, -85, -73, -60, -46, -31, -16, +]; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DecodedAcousticTransfer { + pub object: WireObject, + pub sample_rate_hz: u32, + pub frame_start_candidate_samples: usize, + pub samples_consumed: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Acoustic1Error { + Codec(CodecError), + Protocol(ProtocolError), + WireObjectTooLarge, + FrameNotFound, + TruncatedSignal, + ProfileMismatch, +} + +impl fmt::Display for Acoustic1Error { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Codec(error) => write!(formatter, "WAV error: {error}"), + Self::Protocol(error) => write!(formatter, "ADLP error: {error}"), + Self::WireObjectTooLarge => { + write!(formatter, "Acoustic-1 wire object exceeds 256 bytes") + } + Self::FrameNotFound => { + write!(formatter, "Acoustic-1 preamble or sync word was not found") + } + Self::TruncatedSignal => write!(formatter, "truncated Acoustic-1 signal"), + Self::ProfileMismatch => write!( + formatter, + "Acoustic-1 symbol rate does not match ADLP profile" + ), + } + } +} + +impl std::error::Error for Acoustic1Error {} + +impl From for Acoustic1Error { + fn from(error: CodecError) -> Self { + Self::Codec(error) + } +} + +impl From for Acoustic1Error { + fn from(error: ProtocolError) -> Self { + Self::Protocol(error) + } +} + +#[derive(Clone, Copy)] +struct ProfileConfig { + profile: TransferProfile, + samples_per_symbol: usize, +} + +const PROFILE_CONFIGS: [ProfileConfig; 4] = [ + ProfileConfig { + profile: TransferProfile::Reliable, + samples_per_symbol: 480, + }, + ProfileConfig { + profile: TransferProfile::Balanced, + samples_per_symbol: 240, + }, + ProfileConfig { + profile: TransferProfile::Fast, + samples_per_symbol: 144, + }, + ProfileConfig { + profile: TransferProfile::Narrowband, + samples_per_symbol: 480, + }, +]; + +pub fn encode_wav(object: &WireObject) -> Result, Acoustic1Error> { + Ok(make_canonical_wav(&encode_samples(object)?)?) +} + +pub fn decode_wav(wav: &[u8]) -> Result { + let (sample_rate_hz, samples) = parse_canonical_wav(wav)?; + if sample_rate_hz != SAMPLE_RATE_HZ { + return Err(Acoustic1Error::Codec(CodecError::InvalidWav( + "sample rate must be 48 kHz", + ))); + } + + for config in PROFILE_CONFIGS { + let minimum_samples = (PREAMBLE_BITS + 16 + 56) * config.samples_per_symbol; + if samples.len() < minimum_samples { + continue; + } + let maximum_offset = MAX_SYNC_OFFSET_SAMPLES.min(samples.len() - minimum_samples); + for offset in 0..=maximum_offset { + if let Ok(decoded) = decode_candidate(&samples, offset, config) { + return Ok(decoded); + } + } + } + Err(Acoustic1Error::FrameNotFound) +} + +fn encode_samples(object: &WireObject) -> Result, Acoustic1Error> { + let wire = object.encode()?; + if wire.len() > MAX_WIRE_BYTES { + return Err(Acoustic1Error::WireObjectTooLarge); + } + let config = profile_config(object.manifest.profile); + let mut bits = Vec::with_capacity(PREAMBLE_BITS + 16 + 56 + wire.len() * 14); + for index in 0..PREAMBLE_BITS { + bits.push(index % 2 == 0); + } + append_raw_u16(&mut bits, SYNC_WORD); + for byte in (wire.len() as u32).to_be_bytes() { + append_hamming_byte(&mut bits, byte); + } + for byte in wire { + append_hamming_byte(&mut bits, byte); + } + Ok(modulate(&bits, config.samples_per_symbol)) +} + +fn decode_candidate( + samples: &[i16], + offset: usize, + config: ProfileConfig, +) -> Result { + let mut cursor = SymbolCursor::new(samples, offset, config.samples_per_symbol); + for index in 0..PREAMBLE_BITS { + if cursor.bit()? != (index % 2 == 0) { + return Err(Acoustic1Error::FrameNotFound); + } + } + if cursor.raw_u16()? != SYNC_WORD { + return Err(Acoustic1Error::FrameNotFound); + } + let mut length_bytes = [0_u8; 4]; + for byte in &mut length_bytes { + *byte = cursor.hamming_byte()?; + } + let length = u32::from_be_bytes(length_bytes) as usize; + if length > MAX_WIRE_BYTES { + return Err(Acoustic1Error::WireObjectTooLarge); + } + let mut wire = Vec::with_capacity(length); + for _ in 0..length { + wire.push(cursor.hamming_byte()?); + } + let object = WireObject::decode(&wire)?; + if object.manifest.profile != config.profile { + return Err(Acoustic1Error::ProfileMismatch); + } + Ok(DecodedAcousticTransfer { + object, + sample_rate_hz: SAMPLE_RATE_HZ, + frame_start_candidate_samples: offset, + samples_consumed: cursor.samples_consumed(), + }) +} + +fn profile_config(profile: TransferProfile) -> ProfileConfig { + PROFILE_CONFIGS + .iter() + .copied() + .find(|config| config.profile == profile) + .expect("all ADLP transfer profiles have an Acoustic-1 configuration") +} + +fn append_raw_u16(bits: &mut Vec, value: u16) { + for byte in value.to_be_bytes() { + append_raw_byte(bits, byte); + } +} + +fn append_raw_byte(bits: &mut Vec, value: u8) { + for offset in (0..8).rev() { + bits.push(value & (1 << offset) != 0); + } +} + +fn append_hamming_byte(bits: &mut Vec, value: u8) { + append_hamming_nibble(bits, value >> 4); + append_hamming_nibble(bits, value & 0x0F); +} + +fn append_hamming_nibble(bits: &mut Vec, value: u8) { + let d3 = value & 0b1000 != 0; + let d2 = value & 0b0100 != 0; + let d1 = value & 0b0010 != 0; + let d0 = value & 0b0001 != 0; + bits.extend_from_slice(&[d3 ^ d2 ^ d0, d3 ^ d1 ^ d0, d3, d2 ^ d1 ^ d0, d2, d1, d0]); +} + +fn modulate(bits: &[bool], samples_per_symbol: usize) -> Vec { + let mut samples = Vec::with_capacity(bits.len() * samples_per_symbol); + for bit in bits { + let frequency_multiple = if *bit { 1 } else { 2 }; + for index in 0..samples_per_symbol { + let phase = (index * frequency_multiple) % SINE_120.len(); + samples.push(SINE_120[phase] * 100); + } + } + samples +} + +fn symbol_energy(samples: &[i16], frequency_multiple: usize) -> i64 { + let mut in_phase = 0_i64; + let mut quadrature = 0_i64; + for (index, sample) in samples.iter().enumerate() { + let normalized = i64::from(*sample) / 100; + let phase = (index * frequency_multiple) % SINE_120.len(); + in_phase += normalized * i64::from(SINE_120[phase]); + quadrature += normalized * i64::from(SINE_120[(phase + 12) % SINE_120.len()]); + } + in_phase * in_phase + quadrature * quadrature +} + +struct SymbolCursor<'a> { + samples: &'a [i16], + offset: usize, + samples_per_symbol: usize, +} + +impl<'a> SymbolCursor<'a> { + fn new(samples: &'a [i16], offset: usize, samples_per_symbol: usize) -> Self { + Self { + samples, + offset, + samples_per_symbol, + } + } + + fn bit(&mut self) -> Result { + let end = self + .offset + .checked_add(self.samples_per_symbol) + .ok_or(Acoustic1Error::TruncatedSignal)?; + let symbol = self + .samples + .get(self.offset..end) + .ok_or(Acoustic1Error::TruncatedSignal)?; + self.offset = end; + Ok(symbol_energy(symbol, 1) > symbol_energy(symbol, 2)) + } + + fn raw_u16(&mut self) -> Result { + let mut value = 0_u16; + for _ in 0..16 { + value = (value << 1) | u16::from(self.bit()?); + } + Ok(value) + } + + fn hamming_byte(&mut self) -> Result { + Ok((self.hamming_nibble()? << 4) | self.hamming_nibble()?) + } + + fn hamming_nibble(&mut self) -> Result { + let mut codeword = [false; 7]; + for bit in &mut codeword { + *bit = self.bit()?; + } + let syndrome = usize::from(codeword[0] ^ codeword[2] ^ codeword[4] ^ codeword[6]) + | (usize::from(codeword[1] ^ codeword[2] ^ codeword[5] ^ codeword[6]) << 1) + | (usize::from(codeword[3] ^ codeword[4] ^ codeword[5] ^ codeword[6]) << 2); + if syndrome != 0 { + codeword[syndrome - 1] = !codeword[syndrome - 1]; + } + Ok((u8::from(codeword[2]) << 3) + | (u8::from(codeword[4]) << 2) + | (u8::from(codeword[5]) << 1) + | u8::from(codeword[6])) + } + + fn samples_consumed(&self) -> usize { + self.offset + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_object(profile: TransferProfile) -> WireObject { + WireObject::text(73, "AC1", "Acoustic-1 test", profile).unwrap() + } + + fn samples_with_single_symbol_flipped( + object: &WireObject, + symbol_index: usize, + extra_symbols: &[usize], + ) -> Vec { + let config = profile_config(object.manifest.profile); + let mut samples = encode_samples(object).unwrap(); + let original_bits = frame_bits(object).unwrap(); + for index in std::iter::once(symbol_index).chain(extra_symbols.iter().copied()) { + let start = index * config.samples_per_symbol; + let end = start + config.samples_per_symbol; + let replacement = modulate(&[!original_bits[index]], config.samples_per_symbol); + samples[start..end].copy_from_slice(&replacement); + } + samples + } + + fn frame_bits(object: &WireObject) -> Result, Acoustic1Error> { + let wire = object.encode()?; + let mut bits = Vec::new(); + for index in 0..PREAMBLE_BITS { + bits.push(index % 2 == 0); + } + append_raw_u16(&mut bits, SYNC_WORD); + for byte in (wire.len() as u32).to_be_bytes() { + append_hamming_byte(&mut bits, byte); + } + for byte in wire { + append_hamming_byte(&mut bits, byte); + } + Ok(bits) + } + + #[test] + fn every_profile_round_trips_through_acoustic1_wav() { + for config in PROFILE_CONFIGS { + let object = test_object(config.profile); + let decoded = decode_wav(&encode_wav(&object).unwrap()).unwrap(); + assert_eq!(decoded.object, object); + assert_eq!(decoded.sample_rate_hz, SAMPLE_RATE_HZ); + assert_eq!(decoded.frame_start_candidate_samples, 0); + } + } + + #[test] + fn golden_acoustic1_fixture_decodes_and_matches_deterministic_encoder() { + let fixture = include_bytes!("../tests/fixtures/acoustic-1-v1-text-balanced.wav"); + let expected = WireObject::text( + 1, + "AC1GOLD", + "AudioModem Acoustic-1 golden fixture v1", + TransferProfile::Balanced, + ) + .unwrap(); + let decoded = decode_wav(fixture).unwrap(); + assert_eq!(decoded.object, expected); + assert_eq!(decoded.sample_rate_hz, SAMPLE_RATE_HZ); + assert_eq!(decoded.samples_consumed, 358_560); + assert_eq!(encode_wav(&expected).unwrap().as_slice(), fixture); + } + + #[test] + fn bounded_leading_silence_is_recovered() { + let object = test_object(TransferProfile::Balanced); + let mut samples = vec![0; 137]; + samples.extend(encode_samples(&object).unwrap()); + let decoded = decode_wav(&make_canonical_wav(&samples).unwrap()).unwrap(); + assert_eq!(decoded.object, object); + assert!(decoded.frame_start_candidate_samples <= 137); + } + + #[test] + fn hamming_corrects_a_single_symbol_error_in_adlp_wire_data() { + let object = test_object(TransferProfile::Balanced); + let first_wire_codeword_symbol = PREAMBLE_BITS + 16 + 56; + let samples = samples_with_single_symbol_flipped(&object, first_wire_codeword_symbol, &[]); + let decoded = decode_wav(&make_canonical_wav(&samples).unwrap()).unwrap(); + assert_eq!(decoded.object, object); + } + + #[test] + fn double_symbol_error_is_rejected_by_adlp_integrity() { + let object = test_object(TransferProfile::Balanced); + let first_wire_codeword_symbol = PREAMBLE_BITS + 16 + 56; + let samples = samples_with_single_symbol_flipped( + &object, + first_wire_codeword_symbol, + &[first_wire_codeword_symbol + 1], + ); + assert!(decode_wav(&make_canonical_wav(&samples).unwrap()).is_err()); + } + + #[test] + fn bounded_additive_noise_preserves_frame() { + let object = test_object(TransferProfile::Fast); + let mut samples = encode_samples(&object).unwrap(); + for (index, sample) in samples.iter_mut().enumerate() { + let noise = ((index.wrapping_mul(17).wrapping_add(23) % 401) as i16) - 200; + *sample = sample.saturating_add(noise); + } + let decoded = decode_wav(&make_canonical_wav(&samples).unwrap()).unwrap(); + assert_eq!(decoded.object, object); + } +} diff --git a/crates/audio-modem-core/src/lib.rs b/crates/audio-modem-core/src/lib.rs index 62971b1..1c10e7e 100644 --- a/crates/audio-modem-core/src/lib.rs +++ b/crates/audio-modem-core/src/lib.rs @@ -4,6 +4,8 @@ //! between the ADLP container and a lossless audio transport. A future DSP //! profile can replace this simple symbol mapper without changing ADLP bytes. +pub mod acoustic1; + use std::fmt; use adlp_protocol::{ProtocolError, WireObject}; @@ -69,11 +71,11 @@ pub fn encode_wav(object: &WireObject) -> Result, CodecError> { for byte in wire { push_byte(&mut samples, byte); } - make_wav(&samples) + make_canonical_wav(&samples) } pub fn decode_wav(wav: &[u8]) -> Result { - let (sample_rate_hz, samples) = parse_wav(wav)?; + let (sample_rate_hz, samples) = parse_canonical_wav(wav)?; if sample_rate_hz != SAMPLE_RATE_HZ { return Err(CodecError::InvalidWav("sample rate must be 48 kHz")); } @@ -124,7 +126,7 @@ fn push_u32(samples: &mut Vec, value: u32) { } } -fn make_wav(samples: &[i16]) -> Result, CodecError> { +pub(crate) fn make_canonical_wav(samples: &[i16]) -> Result, CodecError> { let data_len = samples .len() .checked_mul(2) @@ -154,7 +156,7 @@ fn make_wav(samples: &[i16]) -> Result, CodecError> { Ok(wav) } -fn parse_wav(wav: &[u8]) -> Result<(u32, Vec), CodecError> { +pub(crate) fn parse_canonical_wav(wav: &[u8]) -> Result<(u32, Vec), CodecError> { if wav.len() < 44 || &wav[0..4] != b"RIFF" || &wav[8..12] != b"WAVE" { return Err(CodecError::InvalidWav("expected RIFF/WAVE header")); } diff --git a/crates/audio-modem-core/tests/fixtures/README.md b/crates/audio-modem-core/tests/fixtures/README.md index 74b918d..70c8dbf 100644 --- a/crates/audio-modem-core/tests/fixtures/README.md +++ b/crates/audio-modem-core/tests/fixtures/README.md @@ -14,3 +14,20 @@ | SHA-256 | `c13b9091604a6eaeb3bbb5570498ada82113c0c52e7cfb44fd9373c2cb001bf6` | The Rust regression test both decodes this immutable fixture and regenerates it from the fixed ADLP object. A byte difference is therefore a deliberate codec compatibility change and must be reviewed together with an updated fixture, hash, protocol rationale, and release note. + +## Acoustic-1 experimental carrier + +`acoustic-1-v1-text-balanced.wav` is the canonical controlled-test vector for the experimental Acoustic-1 B-FSK carrier. It is generated only with the repository CLI and must not be replaced by an editor export or a manually modified file. + +| Field | Canonical value | +| --- | --- | +| Generator | `cargo run -p adlp-cli -- encode-acoustic1-text` | +| Session ID | `1` | +| Callsign | `AC1GOLD` | +| Profile | `balanced` | +| UTF-8 text | `AudioModem Acoustic-1 golden fixture v1` | +| WAV shape | RIFF, mono, 16-bit PCM, 48 kHz | +| Size | `717,164` bytes | +| SHA-256 | `bbceae6d334284ede97ebe8113293fee53ea3e5e55822715802aa2a7819bc29b` | + +The Acoustic-1 test decodes the fixture, confirms the fixed ADLP object and regenerates the complete WAV byte-for-byte. A fixture change is therefore a carrier compatibility change. It requires an RFC update, an updated hash, a documented measurement rationale and a release note; it does not by itself establish live acoustic interoperability. diff --git a/crates/audio-modem-core/tests/fixtures/acoustic-1-v1-text-balanced.wav b/crates/audio-modem-core/tests/fixtures/acoustic-1-v1-text-balanced.wav new file mode 100644 index 0000000..fe0a659 Binary files /dev/null and b/crates/audio-modem-core/tests/fixtures/acoustic-1-v1-text-balanced.wav differ diff --git a/docs/README.md b/docs/README.md index edb3673..e6b9a58 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ This directory holds the human-readable technical documentation for AudioModem. | Section | English | Russian | Scope | | --- | --- | --- | --- | -| Guides | [Quick start](guides/getting-started.md) | [Быстрый старт](guides/getting-started_RU.md) | Verified text-to-WAV round trip | +| Guides | [Quick start](guides/getting-started.md) | [Быстрый старт](guides/getting-started_RU.md) | Verified bootstrap and controlled Acoustic-1 WAV workflows | | Guides | [Audio routes](guides/audio-routes.md) | [Аудиомаршруты](guides/audio-routes_RU.md) | Delivery routes and scope boundaries | | Reference | [Platform support](reference/platform-support.md) | [Поддержка платформ](reference/platform-support_RU.md) | Runners versus supported features | | Reference | [Transmission presets](reference/presets.md) | [Пресеты передачи](reference/presets_RU.md) | Profile identifiers and intent | @@ -14,7 +14,9 @@ This directory holds the human-readable technical documentation for AudioModem. | Project | [Roadmap](roadmap.md) | [Дорожная карта](roadmap_RU.md) | Verified work and upcoming milestones | | Architecture | [Flutter ↔ Rust WAV bridge](architecture/flutter-rust-bridge.md) | [Flutter ↔ Rust WAV bridge на русском](architecture/flutter-rust-bridge_RU.md) | Native facade and in-memory WAV verification boundary | | Research | [Flutter Rust Bridge integration](research/flutter-rust-bridge-integration.md) | [Flutter Rust Bridge integration на русском](research/flutter-rust-bridge-integration_RU.md) | Generated-code layout and regeneration command | +| Research | [Acoustic-1 PHY sources](research/acoustic-1-phy-sources.md) | [Источники Acoustic-1 PHY](research/acoustic-1-phy-sources_RU.md) | Design sources and explicit receiver/FEC constraints | | Specification | [ADLP v1](../spec/protocol-v1.md) | [ADLP v1 на русском](../spec/protocol-v1_RU.md) | Normative wire object and WAV bootstrap carrier | +| Specification | [Acoustic-1](../spec/acoustic-1.md) | [Acoustic-1 на русском](../spec/acoustic-1_RU.md) | Experimental B-FSK carrier and compatibility boundary | ## Translation convention diff --git a/docs/README_RU.md b/docs/README_RU.md index 9e831c6..d5314f2 100644 --- a/docs/README_RU.md +++ b/docs/README_RU.md @@ -4,11 +4,11 @@ Этот каталог содержит читаемую техническую документацию AudioModem. Английские файлы являются каноническими. Русские переводы используют суффикс `_RU.md`, ссылаются на английский оригинал и носят информативный характер, пока RFC явно не установит иное. -> **Translation of:** [docs/README.md](README.md). **Last synced:** 2026-08-19. +> **Translation of:** [docs/README.md](README.md). **Last synced:** 2026-08-20. | Раздел | Английский оригинал | Русский перевод | Область | | --- | --- | --- | --- | -| Guides | [Quick start](guides/getting-started.md) | [Быстрый старт](guides/getting-started_RU.md) | Проверенный text-to-WAV round trip | +| Guides | [Quick start](guides/getting-started.md) | [Быстрый старт](guides/getting-started_RU.md) | Проверенные bootstrap и controlled Acoustic-1 WAV workflows | | Guides | [Audio routes](guides/audio-routes.md) | [Аудиомаршруты](guides/audio-routes_RU.md) | Маршруты доставки и границы scope | | Reference | [Platform support](reference/platform-support.md) | [Поддержка платформ](reference/platform-support_RU.md) | Runners и supported features | | Reference | [Transmission presets](reference/presets.md) | [Пресеты передачи](reference/presets_RU.md) | Profile identifiers и назначение | @@ -16,7 +16,9 @@ | Project | [Roadmap](roadmap.md) | [Дорожная карта](roadmap_RU.md) | Проверенная работа и ближайшие milestones | | Architecture | [Flutter ↔ Rust WAV bridge](architecture/flutter-rust-bridge.md) | [Flutter ↔ Rust WAV bridge на русском](architecture/flutter-rust-bridge_RU.md) | Граница native facade и проверки WAV в памяти | | Research | [Flutter Rust Bridge integration](research/flutter-rust-bridge-integration.md) | [Flutter Rust Bridge integration на русском](research/flutter-rust-bridge-integration_RU.md) | Структура generated code и команда повторной генерации | +| Research | [Acoustic-1 PHY sources](research/acoustic-1-phy-sources.md) | [Источники Acoustic-1 PHY](research/acoustic-1-phy-sources_RU.md) | Источники дизайна и явные receiver/FEC constraints | | Specification | [ADLP v1](../spec/protocol-v1.md) | [ADLP v1 на русском](../spec/protocol-v1_RU.md) | Нормативный wire object и WAV bootstrap carrier | +| Specification | [Acoustic-1](../spec/acoustic-1.md) | [Acoustic-1 на русском](../spec/acoustic-1_RU.md) | Экспериментальный B-FSK carrier и граница compatibility | ## Правило поддержки перевода diff --git a/docs/architecture/flutter-rust-bridge.md b/docs/architecture/flutter-rust-bridge.md index af2552a..ca011d6 100644 --- a/docs/architecture/flutter-rust-bridge.md +++ b/docs/architecture/flutter-rust-bridge.md @@ -2,7 +2,7 @@ **Last reviewed:** 2026-08-20 · **English (canonical)** · [Русский](flutter-rust-bridge_RU.md) -The first native bridge exposes the existing Rust ADLP and WAV bootstrap implementation to Flutter. The app now has a separate local file adapter for selecting or saving WAV bytes, while the bridge itself still does not introduce audio capture, playback, Bluetooth, background transmission or encryption. Its purpose is to make one real app-to-core round trip observable and testable without duplicating protocol or codec logic in Dart. +The native bridge exposes Rust ADLP plus two controlled WAV codecs to Flutter: deterministic bootstrap and experimental Acoustic-1. The app has a separate local file adapter for selecting or saving WAV bytes, while the bridge itself still does not introduce audio capture, playback, Bluetooth, background transmission or encryption. Its purpose is to make one real app-to-core round trip observable and testable without duplicating protocol or codec logic in Dart. ## Boundary @@ -16,8 +16,8 @@ The bridge crate is a thin native facade. `adlp-protocol` remains responsible fo | Operation | Input | Output | Failure behavior | | --- | --- | --- | --- | -| `encodeTextToWav` | Positive `sessionId`, callsign, UTF-8 text, profile | Canonical 48 kHz mono 16-bit WAV bytes and transfer metadata | A typed Rust error becomes a Dart exception; no partial WAV is returned. | -| `decodeWav` | WAV bytes from memory | Verified text metadata, UTF-8 payload, sample rate and consumed samples | Framing, manifest or CRC-32C failure becomes an exception; no payload is returned. | +| `encodeTextToWav` | Positive `sessionId`, callsign, UTF-8 text, profile and explicit carrier (`bootstrap` or `acoustic1`) | Canonical 48 kHz mono 16-bit WAV bytes and transfer metadata | A typed Rust error becomes a Dart exception; no partial WAV is returned. | +| `decodeWav` | WAV bytes from memory and explicit carrier | Verified text metadata, UTF-8 payload, sample rate and consumed samples | Wrong-carrier, framing, manifest or CRC-32C failure becomes an exception; no payload is returned. | The Flutter app chooses the session value in this first slice. The screen uses a positive timestamp-derived value only as local transfer metadata; it is not a timestamp claim, identity claim or cryptographic nonce. @@ -25,9 +25,11 @@ The Flutter app chooses the session value in this first slice. The screen uses a `PlatformWavFileAdapter` opens local platform dialogs and returns opaque WAV bytes or submits a verified in-memory buffer for saving. It does not parse the waveform, decide protocol validity, or read a payload. The Flutter workbench sends all selected bytes to `decodeWav`; a failed decode retains no received object. This keeps user filesystem interaction outside the deterministic protocol and codec boundary. -## Profiles and limits +## Carriers, profiles and limits -The bridge accepts `reliable`, `balanced`, `fast` and `narrowband`, which map directly to ADLP profile IDs 1–4. In the bootstrap codec these IDs do not yet alter PCM modulation or error correction. The native facade rejects text longer than **8 KiB** before WAV allocation. This lower application limit protects a mobile or desktop UI from accidentally creating impractically large bootstrap WAV buffers while the general protocol limit remains larger for future profiles. +The bridge accepts `bootstrap` and `acoustic1` carriers. Bootstrap retains one deterministic symbol mapper for every profile. Acoustic-1 has a separate B-FSK mapper with profile-dependent symbol windows, Hamming(7,4), bounded frame acquisition and a 256-byte ADLP wire-object limit; its exact compatibility contract is in the [Acoustic-1 RFC](../../spec/acoustic-1.md). The UI keeps a carrier selection with an explicit experimental notice and reuses that selection for import, export and verification. + +The bridge accepts `reliable`, `balanced`, `fast` and `narrowband`, which map directly to ADLP profile IDs 1–4. The native facade rejects text longer than **8 KiB** before WAV allocation. This lower application limit protects a mobile or desktop UI from accidentally creating impractically large buffers while the general protocol limit remains larger for future profiles. ## Trust boundary diff --git a/docs/architecture/flutter-rust-bridge_RU.md b/docs/architecture/flutter-rust-bridge_RU.md index 54a040f..b4d9e66 100644 --- a/docs/architecture/flutter-rust-bridge_RU.md +++ b/docs/architecture/flutter-rust-bridge_RU.md @@ -4,7 +4,7 @@ > **Translation of:** [docs/architecture/flutter-rust-bridge.md](flutter-rust-bridge.md). **Last synced:** 2026-08-20. -Первый native bridge открывает Flutter доступ к существующей Rust-реализации ADLP и WAV bootstrap. В app теперь есть отдельный local file adapter для выбора и сохранения WAV bytes, тогда как сам bridge всё ещё не добавляет audio capture, playback, Bluetooth, background transmission или encryption. Его цель — сделать один реальный app-to-core round trip наблюдаемым и тестируемым без дублирования protocol или codec logic в Dart. +Native bridge открывает Flutter доступ к Rust ADLP и двум controlled WAV codecs: детерминированному bootstrap и экспериментальному Acoustic-1. В app есть отдельный local file adapter для выбора и сохранения WAV bytes, тогда как сам bridge всё ещё не добавляет audio capture, playback, Bluetooth, background transmission или encryption. Его цель — сделать один реальный app-to-core round trip наблюдаемым и тестируемым без дублирования protocol или codec logic в Dart. ## Граница @@ -18,8 +18,8 @@ Bridge crate — тонкий native facade. `adlp-protocol` остаётся о | Операция | Вход | Выход | Поведение при ошибке | | --- | --- | --- | --- | -| `encodeTextToWav` | Положительный `sessionId`, callsign, UTF-8 text, profile | Canonical 48 kHz mono 16-bit WAV bytes и transfer metadata | Typed Rust error становится Dart exception; partial WAV не возвращается. | -| `decodeWav` | WAV bytes из памяти | Verified text metadata, UTF-8 payload, sample rate и consumed samples | Framing, manifest или CRC-32C failure становится exception; payload не возвращается. | +| `encodeTextToWav` | Положительный `sessionId`, callsign, UTF-8 text, profile и явный carrier (`bootstrap` или `acoustic1`) | Canonical 48 kHz mono 16-bit WAV bytes и transfer metadata | Typed Rust error становится Dart exception; partial WAV не возвращается. | +| `decodeWav` | WAV bytes из памяти и явный carrier | Verified text metadata, UTF-8 payload, sample rate и consumed samples | Wrong-carrier, framing, manifest или CRC-32C failure становится exception; payload не возвращается. | В этом первом срезе Flutter app выбирает session value. Экран использует positive timestamp-derived value только как local transfer metadata; это не timestamp claim, identity claim или cryptographic nonce. @@ -27,9 +27,11 @@ Bridge crate — тонкий native facade. `adlp-protocol` остаётся о `PlatformWavFileAdapter` открывает локальные platform dialogs и возвращает opaque WAV bytes или передаёт проверенный in-memory buffer на сохранение. Он не анализирует waveform, не принимает решение о protocol validity и не читает payload. Flutter workbench передаёт все выбранные bytes в `decodeWav`; при failed decode полученный object не сохраняется. Это оставляет user filesystem interaction за пределами deterministic protocol и codec boundary. -## Profiles и limits +## Carriers, profiles и limits -Bridge принимает `reliable`, `balanced`, `fast` и `narrowband`, напрямую соответствующие ADLP profile IDs 1–4. В bootstrap codec эти IDs пока не меняют PCM modulation или error correction. Native facade отклоняет text длиннее **8 KiB** до WAV allocation. Этот более низкий application limit защищает mobile или desktop UI от случайного создания непрактично больших bootstrap WAV buffers, пока общий protocol limit остаётся больше для future profiles. +Bridge принимает carriers `bootstrap` и `acoustic1`. Bootstrap сохраняет один deterministic symbol mapper для каждого profile. Acoustic-1 использует отдельный B-FSK mapper с profile-dependent symbol windows, Hamming(7,4), bounded frame acquisition и лимитом 256 bytes для ADLP wire object; точный compatibility contract указан в [Acoustic-1 RFC](../../spec/acoustic-1_RU.md). UI хранит carrier selection с явным experimental notice и использует тот же выбор для import, export и verification. + +Bridge принимает `reliable`, `balanced`, `fast` и `narrowband`, напрямую соответствующие ADLP profile IDs 1–4. Native facade отклоняет text длиннее **8 KiB** до WAV allocation. Этот более низкий application limit защищает mobile или desktop UI от случайного создания непрактично больших buffers, пока общий protocol limit остаётся больше для future profiles. ## Trust boundary diff --git a/docs/guides/audio-routes.md b/docs/guides/audio-routes.md index 4c985f0..32eacf4 100644 --- a/docs/guides/audio-routes.md +++ b/docs/guides/audio-routes.md @@ -9,6 +9,7 @@ An AudioModem route delivers PCM samples. It does not define the data object. Th | Route | Delivery model | Bootstrap status | Requirement before “supported” | | --- | --- | --- | --- | | Lossless WAV | A file carries canonical PCM samples. | Implemented for text round trips and explicit local import/export in the Flutter workbench. | Acoustic interoperability observations across platforms and published fixture compatibility policy. | +| Controlled Acoustic-1 WAV | A file carries B-FSK PCM samples and the same ADLP v1 wire object. | Experimental: framing, bounded synchronisation, Hamming(7,4), profile-driven symbol windows and golden vector are implemented. | A route adapter plus declared speaker-to-microphone/device measurements. | | Speaker → microphone | Local acoustic simplex path. | Planned. | Synchronization, level handling, FEC, noisy-room measurements and device tests. | | Audio cable | Line-level PCM path. | Planned. | Device selection, sample-rate handling, gain guidance and cross-platform tests. | | OS-managed Bluetooth | A selected audio input/output route. | Planned. | Per-platform permission/routing adapters and device compatibility testing. | @@ -31,7 +32,7 @@ The adapter relies on `file_picker`, whose documented API supports custom extens ## Golden compatibility fixture -`crates/audio-modem-core/tests/fixtures/adlp-v1-text-balanced.wav` is a fixed canonical fixture for ADLP v1 WAV bootstrap. Its Rust regression test decodes the fixture and compares the whole fixture byte sequence with a fresh deterministic encoding of the documented input object. Any byte change is therefore a compatibility-affecting codec change and must be reviewed with an updated fixture, hash and protocol rationale. +`crates/audio-modem-core/tests/fixtures/adlp-v1-text-balanced.wav` is a fixed canonical fixture for ADLP v1 WAV bootstrap. `crates/audio-modem-core/tests/fixtures/acoustic-1-v1-text-balanced.wav` performs the same role for the experimental Acoustic-1 carrier. Their Rust regression tests decode the fixtures and compare whole byte sequences with fresh deterministic encodings of documented input objects. Any byte change is therefore a compatibility-affecting codec change and must be reviewed with an updated fixture, hash and protocol rationale. ## Callsigns and privacy diff --git a/docs/guides/audio-routes_RU.md b/docs/guides/audio-routes_RU.md index a014b91..00e59a6 100644 --- a/docs/guides/audio-routes_RU.md +++ b/docs/guides/audio-routes_RU.md @@ -11,6 +11,7 @@ | Маршрут | Модель доставки | Bootstrap status | Требование до статуса “supported” | | --- | --- | --- | --- | | Lossless WAV | Файл переносит canonical PCM samples. | Реализован для text round trips и явного локального import/export в Flutter workbench. | Наблюдения acoustic interoperability на разных платформах и опубликованная fixture compatibility policy. | +| Controlled Acoustic-1 WAV | Файл переносит B-FSK PCM samples и тот же ADLP v1 wire object. | Экспериментальный: реализованы framing, bounded synchronisation, Hamming(7,4), profile-driven symbol windows и golden vector. | Route adapter плюс declared speaker-to-microphone/device measurements. | | Динамик → микрофон | Local acoustic simplex path. | Планируется. | Synchronization, level handling, FEC, noisy-room measurements и device tests. | | Аудиокабель | Line-level PCM path. | Планируется. | Device selection, sample-rate handling, gain guidance и cross-platform tests. | | OS-managed Bluetooth | Выбранный audio input/output route. | Планируется. | Per-platform permission/routing adapters и device compatibility testing. | @@ -33,7 +34,7 @@ Adapter использует `file_picker`: его документирован ## Golden compatibility fixture -`crates/audio-modem-core/tests/fixtures/adlp-v1-text-balanced.wav` — фиксированный canonical fixture для ADLP v1 WAV bootstrap. Его Rust regression test декодирует fixture и сравнивает всю последовательность его байтов со свежей deterministic encoding документированного входного object. Поэтому любое различие байтов является изменением codec, влияющим на compatibility, и должно быть проверено вместе с обновлёнными fixture, hash и protocol rationale. +`crates/audio-modem-core/tests/fixtures/adlp-v1-text-balanced.wav` — фиксированный canonical fixture для ADLP v1 WAV bootstrap. `crates/audio-modem-core/tests/fixtures/acoustic-1-v1-text-balanced.wav` выполняет ту же роль для экспериментального Acoustic-1 carrier. Их Rust regression tests декодируют fixtures и сравнивают целые byte sequences со свежими deterministic encodings документированных входных objects. Поэтому любое различие байтов является изменением codec, влияющим на compatibility, и должно быть проверено вместе с обновлёнными fixture, hash и protocol rationale. ## Позывные и приватность diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 1334ce0..e7fb1f3 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -1,12 +1,12 @@ # Quick start: verify a WAV transfer -**Last reviewed:** 2026-08-19 · **English (canonical)** · [Русский](getting-started_RU.md) +**Last reviewed:** 2026-08-20 · **English (canonical)** · [Русский](getting-started_RU.md) This guide verifies the first implemented AudioModem path. It does not test a speaker, microphone, Bluetooth device or radio. It deliberately uses a lossless WAV file so a failed round trip can be assigned to the container or codec rather than to an unknown live-audio route. ## Prerequisites -Install a stable Rust toolchain and clone the repository. The current Flutter UI is not connected to the Rust core yet, so the diagnostic CLI is the supported entry point for this test. +Install a stable Rust toolchain and clone the repository. The native Flutter UI now uses the same Rust core and can locally save or open WAV files, but the diagnostic CLI remains the most direct reproducible entry point for this test. The web build intentionally does not expose the native codec. ```bash git clone https://github.com/matveyb9/AudioModem.git @@ -34,14 +34,25 @@ cargo run -p adlp-cli -- decode hello.wav The decoder prints the ADLP version, profile, session value, callsign, sample count and text. A decoder must reject malformed bootstrap framing or a CRC-32C mismatch before it returns the payload. +## Controlled Acoustic-1 WAV experiment + +Acoustic-1 is a separate experimental B-FSK carrier. It uses a different encoder and decoder, so both commands must name the carrier explicitly. Its framing, bounded synchronisation, Hamming(7,4) layer and limits are defined in the [Acoustic-1 RFC](../../spec/acoustic-1.md). + +```bash +cargo run -p adlp-cli -- encode-acoustic1-text acoustic1.wav N1 "Controlled test" balanced +cargo run -p adlp-cli -- decode-acoustic1 acoustic1.wav +``` + +An Acoustic-1 WAV round trip proves controlled PCM/WAV codec behavior only. It does not prove speaker-to-microphone synchronisation, room-noise tolerance, gain handling, radio compatibility or any live route. + ## What this proves—and what it does not | This test proves | This test does not prove | | --- | --- | | The implemented ADLP v1 object can survive one deterministic WAV encode/decode cycle. | That speech-band audio, a speaker, microphone, cable, Bluetooth adapter or radio can carry the waveform. | -| The final integrity check detects the intentional corrupted-frame cases covered by the Rust tests. | That a callsign belongs to a person, device or key. | +| The final integrity check detects the intentional corrupted-frame cases covered by the Rust tests; Acoustic-1 also has bounded noise, framing, FEC and golden-vector regression cases. | That a callsign belongs to a person, device or key. | | The test is portable between supported Rust development environments. | That a transmission is encrypted, confidential or authenticated. | ## Next steps -Read [Audio routes](audio-routes.md) to understand why WAV is the reference transport and what is required before a live route becomes supported. Read the [ADLP v1 specification](../../spec/protocol-v1.md) for the object layout. +Read [Audio routes](audio-routes.md) to understand why WAV is the reference transport and what is required before a live route becomes supported. Read the [ADLP v1 specification](../../spec/protocol-v1.md) for the object layout and the [Acoustic-1 RFC](../../spec/acoustic-1.md) for experimental carrier compatibility. diff --git a/docs/guides/getting-started_RU.md b/docs/guides/getting-started_RU.md index 9cc6814..fa9d263 100644 --- a/docs/guides/getting-started_RU.md +++ b/docs/guides/getting-started_RU.md @@ -2,13 +2,13 @@ [English (canonical)](getting-started.md) · **Русский перевод** -> **Translation of:** [docs/guides/getting-started.md](getting-started.md). **Last synced:** 2026-08-19. +> **Translation of:** [docs/guides/getting-started.md](getting-started.md). **Last synced:** 2026-08-20. Это руководство проверяет первый реализованный путь AudioModem. Оно не проверяет динамик, микрофон, Bluetooth-устройство или радиостанцию. Вместо этого используется lossless WAV, чтобы неудачный round trip можно было отнести к container или codec, а не к неизвестному live-аудиомаршруту. ## Предварительные требования -Установите стабильный Rust toolchain и клонируйте репозиторий. Текущий Flutter UI пока не соединён с Rust core, поэтому диагностический CLI — поддерживаемая точка входа для этого теста. +Установите стабильный Rust toolchain и клонируйте репозиторий. Native Flutter UI теперь использует тот же Rust core и может локально сохранять/открывать WAV files, но diagnostic CLI остаётся самым прямым воспроизводимым entry point для этого теста. Web build намеренно не открывает native codec. ```bash git clone https://github.com/matveyb9/AudioModem.git @@ -36,14 +36,25 @@ cargo run -p adlp-cli -- decode hello.wav Decoder выводит ADLP version, profile, session value, callsign, sample count и text. Он обязан отклонить malformed bootstrap framing или CRC-32C mismatch до возврата payload. +## Controlled Acoustic-1 WAV experiment + +Acoustic-1 — отдельный экспериментальный B-FSK carrier. Он использует другой encoder и decoder, поэтому обе команды должны явно называть carrier. Его framing, bounded synchronisation, Hamming(7,4) layer и limits определены в [Acoustic-1 RFC](../../spec/acoustic-1_RU.md). + +```bash +cargo run -p adlp-cli -- encode-acoustic1-text acoustic1.wav N1 "Контролируемый тест" balanced +cargo run -p adlp-cli -- decode-acoustic1 acoustic1.wav +``` + +Acoustic-1 WAV round trip доказывает только controlled PCM/WAV codec behavior. Он не доказывает speaker-to-microphone synchronisation, tolerance room noise, gain handling, radio compatibility или любой live route. + ## Что доказывает тест — и чего не доказывает | Тест доказывает | Тест не доказывает | | --- | --- | | Реализованный ADLP v1 object переживает один детерминированный WAV encode/decode cycle. | Что waveform пройдёт через speech-band audio, динамик, микрофон, кабель, Bluetooth adapter или радио. | -| Final integrity check обнаруживает intentional corrupted-frame cases из Rust-тестов. | Что callsign принадлежит человеку, устройству или ключу. | +| Final integrity check обнаруживает intentional corrupted-frame cases из Rust-тестов; Acoustic-1 также имеет bounded noise, framing, FEC и golden-vector regression cases. | Что callsign принадлежит человеку, устройству или ключу. | | Тест переносим между Rust development environments. | Что передача зашифрована, конфиденциальна или authenticated. | ## Следующие шаги -Прочитайте [Аудиомаршруты](audio-routes_RU.md), чтобы понять, почему WAV является reference transport и что требуется до статуса supported для live route. Формат объекта описан в [спецификации ADLP v1](../../spec/protocol-v1_RU.md). +Прочитайте [Аудиомаршруты](audio-routes_RU.md), чтобы понять, почему WAV является reference transport и что требуется до статуса supported для live route. Формат объекта описан в [спецификации ADLP v1](../../spec/protocol-v1_RU.md), а experimental carrier compatibility — в [Acoustic-1 RFC](../../spec/acoustic-1_RU.md). diff --git a/docs/reference/presets.md b/docs/reference/presets.md index 8d0d79c..ea367ae 100644 --- a/docs/reference/presets.md +++ b/docs/reference/presets.md @@ -1,22 +1,22 @@ # Transmission presets -**Last reviewed:** 2026-08-19 · **English (canonical)** · [Русский](presets_RU.md) +**Last reviewed:** 2026-08-20 · **English (canonical)** · [Русский](presets_RU.md) A preset is a named parameter choice for a physical-layer profile. It expresses a user-facing speed-versus-reliability intent while the `profile_id` remains inside the ADLP object. The receiver reads the ID from the manifest; the sender does not need a separate out-of-band setting for basic profile selection. -## Reserved profile identifiers +## Profile identifiers and controlled carrier behavior -| Profile ID | Preset | Intended route conditions | Bootstrap implementation | +| Profile ID | Preset | Bootstrap carrier | Experimental Acoustic-1 PCM/WAV carrier | | ---: | --- | --- | --- | -| `1` | Reliable | Noisy acoustic route, unknown gain or radio interface | Uses the shared bootstrap symbol mapper. | -| `2` | Balanced | Normal speaker/microphone or cable route | Uses the shared bootstrap symbol mapper. | -| `3` | Fast | Clean line route or reproducible WAV path | Uses the shared bootstrap symbol mapper. | -| `4` | Narrowband | Future radio-oriented profile | Uses the shared bootstrap symbol mapper. | +| `1` | Reliable | Shared deterministic symbol mapper. | 480 samples/symbol, nominal 100 baud. | +| `2` | Balanced | Shared deterministic symbol mapper. | 240 samples/symbol, nominal 200 baud. | +| `3` | Fast | Shared deterministic symbol mapper. | 144 samples/symbol, nominal 333.3 baud. | +| `4` | Narrowband | Shared deterministic symbol mapper. | 480 samples/symbol, nominal 100 baud; this is not a radio narrowband compliance claim. | -## Current limitation +## Current limitations -The IDs are real protocol fields, but in the bootstrap release they do not yet change modulation, bit rate, coding or error correction. They are reserved now so future Acoustic-1 profile work can add distinct PHY behavior without changing the ADLP wire object or the user’s conceptual model. +The deterministic bootstrap carrier still does not vary modulation, bit rate, coding or error correction by profile. The separate experimental Acoustic-1 carrier does vary the symbol window as shown above and applies fixed Hamming(7,4) FEC to its length and ADLP wire bytes. These controlled PCM/WAV facts do not establish a live acoustic reliability ranking for “Reliable”, “Balanced” or “Fast”. ## Future profile contract -Before a profile becomes selectable for live audio, its specification must define occupied band, nominal sample rate, framing, synchronization, coding/FEC behavior, preamble, acceptance metrics and golden test vectors. A UI label such as “Fast” is never sufficient as a compatibility definition. +Before a profile becomes selectable for live audio, its specification must define occupied band, nominal sample rate, framing, synchronization, coding/FEC behavior, preamble, acceptance metrics and golden test vectors. It must also have a route adapter and declared device measurements. A UI label such as “Fast” is never sufficient as a compatibility definition. diff --git a/docs/reference/presets_RU.md b/docs/reference/presets_RU.md index 58ea294..a95431e 100644 --- a/docs/reference/presets_RU.md +++ b/docs/reference/presets_RU.md @@ -2,23 +2,23 @@ [English (canonical)](presets.md) · **Русский перевод** -> **Translation of:** [docs/reference/presets.md](presets.md). **Last synced:** 2026-08-19. +> **Translation of:** [docs/reference/presets.md](presets.md). **Last synced:** 2026-08-20. Пресет — именованный выбор параметров physical-layer profile. Он выражает user-facing компромисс скорость/надёжность, а `profile_id` остаётся внутри ADLP object. Receiver читает ID из manifest; sender не нуждается в отдельной out-of-band настройке для базового выбора profile. -## Зарезервированные profile identifiers +## Profile identifiers и controlled carrier behavior -| Profile ID | Пресет | Предполагаемые условия маршрута | Bootstrap implementation | +| Profile ID | Пресет | Bootstrap carrier | Экспериментальный Acoustic-1 PCM/WAV carrier | | ---: | --- | --- | --- | -| `1` | Reliable | Шумный acoustic route, неизвестный gain или radio interface | Использует shared bootstrap symbol mapper. | -| `2` | Balanced | Обычный speaker/microphone или cable route | Использует shared bootstrap symbol mapper. | -| `3` | Fast | Чистый line route или reproducible WAV path | Использует shared bootstrap symbol mapper. | -| `4` | Narrowband | Будущий radio-oriented profile | Использует shared bootstrap symbol mapper. | +| `1` | Reliable | Shared deterministic symbol mapper. | 480 samples/symbol, nominal 100 baud. | +| `2` | Balanced | Shared deterministic symbol mapper. | 240 samples/symbol, nominal 200 baud. | +| `3` | Fast | Shared deterministic symbol mapper. | 144 samples/symbol, nominal 333.3 baud. | +| `4` | Narrowband | Shared deterministic symbol mapper. | 480 samples/symbol, nominal 100 baud; это не radio narrowband compliance claim. | -## Текущее ограничение +## Текущие ограничения -ID являются настоящими protocol fields, но в bootstrap release пока не меняют modulation, bit rate, coding или error correction. Они зарезервированы заранее, чтобы future Acoustic-1 profile work мог добавить отдельное PHY behavior без изменения ADLP wire object или user mental model. +ID являются настоящими protocol fields. Детерминированный bootstrap carrier всё ещё не меняет modulation, bit rate, coding или error correction по profile. Отдельный экспериментальный Acoustic-1 carrier меняет symbol window, как показано выше, и применяет фиксированный Hamming(7,4) FEC к своей длине и ADLP wire bytes. Эти controlled PCM/WAV facts не устанавливают live acoustic reliability ranking для “Reliable”, “Balanced” или “Fast”. ## Future profile contract -До того как profile станет selectable для live audio, его specification должна определить occupied band, nominal sample rate, framing, synchronization, coding/FEC behavior, preamble, acceptance metrics и golden test vectors. UI label вроде “Fast” не является достаточным compatibility definition. +До того как profile станет selectable для live audio, его specification должна определить occupied band, nominal sample rate, framing, synchronization, coding/FEC behavior, preamble, acceptance metrics и golden test vectors. Также нужны route adapter и declared device measurements. UI label вроде “Fast” не является достаточным compatibility definition. diff --git a/docs/research/acoustic-1-phy-sources.md b/docs/research/acoustic-1-phy-sources.md new file mode 100644 index 0000000..d1e5e45 --- /dev/null +++ b/docs/research/acoustic-1-phy-sources.md @@ -0,0 +1,19 @@ +# Acoustic-1 PHY research notes + +**Last reviewed:** 2026-08-20 · **English (canonical)** + +The first Acoustic-1 profile will use binary FSK as a deliberately small, inspectable baseline. This is a repository design decision, not a claim of acoustic interoperability. The required future acceptance evidence remains speaker-to-microphone measurements on declared device classes. + +| Source | Relevant finding | Design consequence for Acoustic-1 | +| --- | --- | --- | +| Tiny Tapeout FSK modem | The reference design frames `preamble → sync → payload → CRC`, uses one frequency per binary state, then compares two parallel Goertzel filters before validating integrity.[1] | Keep a versioned framing sequence, a distinctive synchronisation word, and explicit final integrity validation. | +| Sanchez, *FSK Demodulation and Bit String Extraction* | FSK maps binary states to discrete mark/space frequencies; non-coherent demodulation avoids a carrier-phase requirement, while thresholding discriminates the recovered symbols.[2] | Use energy comparison over a fixed symbol window rather than phase-coherent recovery in the first codec. | +| liquid-dsp FEC documentation | Hamming block codes use parity bits to correct one bit error in a block; an additional parity bit adds limited error detection while preserving one-bit correction.[3] | Use explicit Hamming(7,4) codewords only for an inspectable first FEC layer, and retain ADLP CRC-32C as final object-level integrity validation. | + +The cited hardware modem explicitly documents that free-running symbol blocks need phase alignment near symbol boundaries.[1] Acoustic-1 must therefore specify its timing assumption and test its receiver against bounded leading-sample offsets. A production receiver will later require a more robust timing-recovery mechanism; that is outside the first implementation. + +## References + +[1]: https://tinytapeout.com/chips/ttgf26a/tt_um_hydrocomms "FSK Modem — Tiny Tapeout" +[2]: https://arxiv.org/html/2402.17777v1 "FSK Demodulation and Bit String Extraction: A Python-Centric Approach in SDR Systems" +[3]: https://liquidsdr.org/doc/fec/ "liquid-dsp forward error-correction documentation" diff --git a/docs/research/acoustic-1-phy-sources_RU.md b/docs/research/acoustic-1-phy-sources_RU.md new file mode 100644 index 0000000..71d22f7 --- /dev/null +++ b/docs/research/acoustic-1-phy-sources_RU.md @@ -0,0 +1,21 @@ +# Исследовательские заметки по Acoustic-1 PHY + +[English (canonical)](acoustic-1-phy-sources.md) · **Русский перевод** + +> **Перевод:** [docs/research/acoustic-1-phy-sources.md](acoustic-1-phy-sources.md). **Последняя синхронизация:** 2026-08-20. + +Первый профиль Acoustic-1 использует binary FSK как намеренно небольшой и проверяемый baseline. Это решение дизайна репозитория, а не заявление acoustic interoperability. Необходимым будущим acceptance evidence остаются speaker-to-microphone measurements на объявленных device classes. + +| Источник | Значимое наблюдение | Следствие для дизайна Acoustic-1 | +| --- | --- | --- | +| Tiny Tapeout FSK modem | Reference design использует framing `preamble → sync → payload → CRC`, одну frequency для каждого binary state, затем сравнивает два parallel Goertzel filters до integrity validation.[1] | Сохранять versioned framing sequence, distinctive synchronisation word и explicit final integrity validation. | +| Sanchez, *FSK Demodulation and Bit String Extraction* | FSK отображает binary states в discrete mark/space frequencies; non-coherent demodulation не требует carrier-phase synchronisation, а thresholding различает recovered symbols.[2] | Использовать comparison энергии в fixed symbol window вместо phase-coherent recovery в первом codec. | +| liquid-dsp FEC documentation | Hamming block codes используют parity bits для исправления одной bit error в block; дополнительный parity bit добавляет limited error detection при сохранении one-bit correction.[3] | Использовать explicit Hamming(7,4) codewords только как inspectable первый FEC layer и сохранить ADLP CRC-32C как final object-level integrity validation. | + +Документированный hardware modem прямо предупреждает, что free-running symbol blocks требуют phase alignment около symbol boundaries.[1] Поэтому Acoustic-1 фиксирует timing assumption и проверяет receiver на bounded leading-sample offsets. Production receiver позднее потребует более robust timing-recovery mechanism; это вне первой реализации. + +## Ссылки + +[1]: https://tinytapeout.com/chips/ttgf26a/tt_um_hydrocomms "FSK Modem — Tiny Tapeout" +[2]: https://arxiv.org/html/2402.17777v1 "FSK Demodulation and Bit String Extraction: A Python-Centric Approach in SDR Systems" +[3]: https://liquidsdr.org/doc/fec/ "Документация liquid-dsp по forward error-correction" diff --git a/docs/roadmap.md b/docs/roadmap.md index 6467f3f..d3ad5f3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap -**Last reviewed:** 2026-08-19 · **English (canonical)** · [Русский](roadmap_RU.md) +**Last reviewed:** 2026-08-20 · **English (canonical)** · [Русский](roadmap_RU.md) This roadmap describes intended order, not dates or guaranteed delivery. It distinguishes verified repository state from research and planned product capability. @@ -8,9 +8,9 @@ This roadmap describes intended order, not dates or guaranteed delivery. It dist | --- | --- | --- | | GitHub-first bootstrap | Completed | Public repository policy, issue forms, CI workflows, protected main and static-site workflow. | | ADLP v1 and WAV reference | Completed | Rust container, CRC-32C, deterministic PCM/WAV round trip, CLI and initial specification. | -| Flutter ↔ Rust bridge | Next | The Flutter UI invokes real ADLP/WAV encode and decode operations through a reviewed bridge. | -| File object workflow | Planned | Select a file, preserve MIME/name metadata, encode/decode it and verify integrity. | -| Acoustic-1 | Planned | Framing, synchronization, FEC, profile behavior, measurements and golden vectors for speaker-to-microphone. | +| Flutter ↔ Rust bridge | Completed | Native UI invokes reviewed Rust ADLP/WAV encode/decode; web reports the native codec unavailable. | +| Local WAV file workflow | Completed | User-selected save/open dialogs export/import verified WAV bytes; file-object payloads remain future work. | +| Acoustic-1 | Experimental controlled carrier | B-FSK framing, bounded synchronisation, Hamming(7,4), profile-driven symbols and golden vectors exist for PCM/WAV tests; no speaker-to-microphone claim. | | Audio adapters | Planned | Capture/playback, cable, Bluetooth and radio-interface adapters with per-platform acceptance tests. | | Trust and encryption | Planned RFC | Key lifecycle, manual/QR exchange, authenticated encryption, verification UX and independent security review. | | Release engineering | Planned | Signed packages, compatibility matrix, changelog, SBOM/checksums and clear support policy. | diff --git a/docs/roadmap_RU.md b/docs/roadmap_RU.md index fd471b2..a57d8e7 100644 --- a/docs/roadmap_RU.md +++ b/docs/roadmap_RU.md @@ -2,7 +2,7 @@ [English (canonical)](roadmap.md) · **Русский перевод** -> **Translation of:** [docs/roadmap.md](roadmap.md). **Last synced:** 2026-08-19. +> **Translation of:** [docs/roadmap.md](roadmap.md). **Last synced:** 2026-08-20. Эта дорожная карта описывает intended order, а не даты или гарантированные сроки. Она отделяет verified repository state от research и planned product capability. @@ -10,9 +10,9 @@ | --- | --- | --- | | GitHub-first bootstrap | Завершён | Public repository policy, issue forms, CI workflows, protected main и static-site workflow. | | ADLP v1 и WAV reference | Завершён | Rust container, CRC-32C, deterministic PCM/WAV round trip, CLI и initial specification. | -| Flutter ↔ Rust bridge | Следующий | Flutter UI вызывает реальные ADLP/WAV encode и decode operations через reviewed bridge. | -| File object workflow | Планируется | Выбор файла, сохранение MIME/name metadata, encode/decode и integrity verification. | -| Acoustic-1 | Планируется | Framing, synchronization, FEC, profile behavior, measurements и golden vectors для speaker-to-microphone. | +| Flutter ↔ Rust bridge | Завершён | Native UI вызывает reviewed Rust ADLP/WAV encode/decode; web сообщает, что native codec недоступен. | +| Локальный WAV file workflow | Завершён | User-selected save/open dialogs экспортируют/импортируют проверенные WAV bytes; file-object payloads остаются будущей работой. | +| Acoustic-1 | Экспериментальный controlled carrier | B-FSK framing, bounded synchronisation, Hamming(7,4), profile-driven symbols и golden vectors существуют для PCM/WAV tests; speaker-to-microphone claim отсутствует. | | Audio adapters | Планируется | Capture/playback, cable, Bluetooth и radio-interface adapters с per-platform acceptance tests. | | Trust и encryption | Planned RFC | Key lifecycle, manual/QR exchange, authenticated encryption, verification UX и independent security review. | | Release engineering | Планируется | Signed packages, compatibility matrix, changelog, SBOM/checksums и clear support policy. | diff --git a/site/index.html b/site/index.html index 90155c8..b6bbe19 100644 --- a/site/index.html +++ b/site/index.html @@ -12,10 +12,10 @@

v0.1 Experimental project · Rust + Flutter

Move data
through sound.

AudioModem turns text and files into a versioned object, then into PCM. Today that path is verified through WAV; later it can use a speaker, cable or radio audio interface.

Try WAVView source

No accounts, no cloud and no hidden network transport.

ADLP v1
Text or file
Versioned object
PCM signal

WAVaudio route

What it does

One object. Several delivery paths.

The data format does not depend on a speaker, microphone, cable or WAV file. A route delivers PCM; ADLP describes the object, profile and integrity check.

-

First test

Start with a reproducible WAV round trip.

WAV removes unknown live-audio characteristics. The current native app can export a verified WAV and import it for Rust-side validation; it is still not a live-audio route.

CLI / bootstrapcargo run -p adlp-cli -- encode-text hello.wav N1 "Hello" reliable
-cargo run -p adlp-cli -- decode hello.wav
Open the short guide →
+

First test

Start with a reproducible WAV round trip.

WAV removes unknown live-audio characteristics. The native app can export a verified WAV and import it for Rust-side validation; the experimental Acoustic-1 B-FSK carrier has controlled WAV tests, not a live-audio claim.

CLI / bootstrapcargo run -p adlp-cli -- encode-text hello.wav N1 "Hello" reliable
+ cargo run -p adlp-cli -- decode hello.wav
Open the short guide →

Architecture

A small app over an independent Rust core.

FlutterUI, routes, file selection and diagnostics
→
Rust bridgeOne codec implementation for the client
→
ADLP + DSPContainer, WAV and future PHY profiles
Architecture and stack →
-

Status

Current slice—without promises beyond the implementation.

✓ ADLP v1 + CRC-32C
done
✓ Deterministic PCM/WAV bootstrap
done
✓ Flutter ↔ Rust bridge
native text-to-WAV verification
✓ Local WAV import / export
native file dialogs + Rust validation
• Live audio and encryption
planned
See roadmap →
+

Status

Current slice—without promises beyond the implementation.

✓ ADLP v1 + CRC-32C
done
✓ Deterministic PCM/WAV bootstrap
done
✓ Flutter ↔ Rust bridge
native text-to-WAV verification
✓ Local WAV import / export
native file dialogs + Rust validation
✓ Experimental Acoustic-1
B-FSK controlled PCM/WAV codec + golden vector
• Live audio and encryption
planned
See roadmap →
diff --git a/site/ru/index.html b/site/ru/index.html index 600dcbb..d858211 100644 --- a/site/ru/index.html +++ b/site/ru/index.html @@ -12,10 +12,10 @@

v0.1 Экспериментальный проект · Rust + Flutter

Передача данных
через звук.

AudioModem превращает текст и файлы в версионированный объект, затем в PCM. Сегодня этот путь проверяется через WAV; в будущем он сможет работать с динамиком, кабелем и радиоаудиотрактом.

Попробовать WAVИсходный код

Без аккаунтов, без облака и без скрытого сетевого транспорта.

ADLP v1
Текст или файл
Versioned object
PCM signal

WAVаудиомаршрут

Как это устроено

Один объект. Несколько путей доставки.

Формат данных не зависит от динамика, микрофона, кабеля или WAV-файла. Маршрут доставляет PCM; ADLP описывает объект, профиль и контроль целостности.

-

Первый тест

Начните с воспроизводимого WAV round trip.

WAV исключает неизвестные свойства live-аудиотракта. Текущее native app может экспортировать проверенный WAV и импортировать его для Rust-side validation; это всё ещё не live-audio маршрут.

CLI / bootstrapcargo run -p adlp-cli -- encode-text hello.wav N1 "Привет" reliable
-cargo run -p adlp-cli -- decode hello.wav
Открыть краткое руководство →
+

Первый тест

Начните с воспроизводимого WAV round trip.

WAV исключает неизвестные свойства live-аудиотракта. Native app может экспортировать проверенный WAV и импортировать его для Rust-side validation; экспериментальный Acoustic-1 B-FSK carrier имеет controlled WAV tests, а не live-audio claim.

CLI / bootstrapcargo run -p adlp-cli -- encode-text hello.wav N1 "Привет" reliable
+ cargo run -p adlp-cli -- decode hello.wav
Открыть краткое руководство →

Архитектура

Небольшое приложение поверх независимого Rust core.

FlutterUI, маршруты, выбор файла и диагностика
→
Rust bridgeОдна реализация codec для клиента
→
ADLP + DSPКонтейнер, WAV и будущие PHY profiles
Архитектура и стек →
-

Статус

Текущий срез — без обещаний сверх реализации.

✓ ADLP v1 + CRC-32C
готово
✓ Детерминированный PCM/WAV bootstrap
готово
✓ Flutter ↔ Rust bridge
native text-to-WAV verification
✓ Локальный WAV import / export
native file dialogs + Rust validation
• Live audio и encryption
план
Посмотреть дорожную карту →
+

Статус

Текущий срез — без обещаний сверх реализации.

✓ ADLP v1 + CRC-32C
готово
✓ Детерминированный PCM/WAV bootstrap
готово
✓ Flutter ↔ Rust bridge
native text-to-WAV verification
✓ Локальный WAV import / export
native file dialogs + Rust validation
✓ Experimental Acoustic-1
B-FSK controlled PCM/WAV codec + golden vector
• Live audio и encryption
план
Посмотреть дорожную карту →
diff --git a/spec/acoustic-1.md b/spec/acoustic-1.md new file mode 100644 index 0000000..e73f01a --- /dev/null +++ b/spec/acoustic-1.md @@ -0,0 +1,78 @@ +# Acoustic-1: experimental audio PHY + +**Status:** Experimental draft · **Last reviewed:** 2026-08-20 · **English (canonical)** · [Русский](acoustic-1_RU.md) + +Acoustic-1 is the first AudioModem physical-layer profile intended for controlled PCM/WAV experiments. It carries an unchanged ADLP v1 wire object and is not a claim of speaker-to-microphone, radio, Bluetooth, or cross-device compatibility. The deterministic bootstrap carrier remains the reference transport for lossless tests; Acoustic-1 is a separate carrier with explicit framing and bounded error tolerance. + +## Scope and compatibility + +| Item | Acoustic-1 rule | +| --- | --- | +| Input and final integrity | An ADLP v1 wire object, including its existing CRC-32C. | +| Sample shape | Canonical RIFF/WAVE, mono, signed 16-bit PCM, 48 kHz. | +| Modulation | Binary FSK: one mark tone for `1`, one space tone for `0`. | +| FEC | Hamming(7,4) applied to length and wire-object nibbles. | +| Object limit | At most 256 encoded ADLP wire bytes per frame. | +| Carrier version | Acoustic-1 is selected by codec API; it does not alter the ADLP v1 version byte. | + +Binary FSK with a preamble, sync word, payload and final integrity check follows the structure of practical modem designs.[1] Acoustic-1 uses a non-coherent two-tone energy comparison rather than carrier-phase recovery, consistent with the distinction between coherent and non-coherent FSK detection.[2] + +> **Compatibility rule:** A decoder MUST not expose a received object until Hamming decoding, frame bounds, ADLP decoding and ADLP CRC-32C validation have all succeeded. + +## Signal parameters + +The encoder resets the phase at every symbol boundary. Samples use an integer 48-step sine table at an amplitude of `12,000`, avoiding floating-point dependence in encoded golden vectors. Mark uses 1,000 Hz and space uses 2,000 Hz; these are integer harmonics of the 48 kHz sample rate. + +| ADLP profile | Samples per symbol | Nominal symbol rate | Intended trade-off | +| --- | ---: | ---: | --- | +| `reliable` | 480 | 100 baud | More energy per bit. | +| `balanced` | 240 | 200 baud | Default controlled-test profile. | +| `fast` | 144 | 333.3 baud | Shorter symbol window; lower expected noise margin. | +| `narrowband` | 480 | 100 baud | Conservative profile identifier; this is **not** a radio narrowband compliance claim. | + +The profile travels inside the ADLP manifest. A decoder tries the fixed Acoustic-1 profile configurations, then MUST reject a frame if the recovered manifest profile does not equal the configuration that decoded it. + +## Frame layout + +Each entry below is transmitted most-significant bit first. The `wire_length` and ADLP wire object are FEC-protected; preamble and sync are raw symbols so the receiver can obtain frame alignment before applying FEC. + +| Field | Symbols | Encoding | +| --- | ---: | --- | +| Preamble | 64 | Alternating `1, 0, 1, 0, …`, beginning with `1`. | +| Sync | 16 | Raw `0xD391`. | +| Wire length | 56 | Four big-endian length bytes; each 4-bit nibble becomes one Hamming(7,4) codeword. | +| ADLP wire object | `14 × N` | Every byte is split into two nibbles; each nibble becomes one Hamming(7,4) codeword. | + +The object limit avoids unbounded WAV allocation. At 200 baud, the FEC payload rate is approximately 14.3 encoded wire bytes per second before the preamble and sync; large files require a later packetisation design. + +## Hamming(7,4) mapping + +Acoustic-1 uses positions `1..7` in this order: `p1, p2, d3, p4, d2, d1, d0`. For a nibble `d3 d2 d1 d0`, parity is even and is calculated as follows: + +```text +p1 = d3 XOR d2 XOR d0 +p2 = d3 XOR d1 XOR d0 +p4 = d2 XOR d1 XOR d0 +``` + +The decoder derives a three-bit syndrome and flips the indexed position when that syndrome is in `1..7`. Hamming block codes correct one bit error per codeword; they are not a replacement for end-to-end integrity, which remains the ADLP CRC-32C.[3] A two-bit corruption can be miscorrected, so a decoder must treat a failed ADLP CRC as a failed frame rather than returning payload. + +## Receiver algorithm + +The receiver parses canonical 48 kHz PCM WAV, then evaluates each declared profile configuration. It scans only the first 480 PCM samples for a frame start; this explicitly supports a bounded leading-silence/timing offset, not arbitrary timing recovery. For each candidate symbol window, it calculates in-phase and quadrature correlation energy against the fixed mark and space references, then chooses the higher energy. A candidate succeeds only if its preamble and sync match, the FEC length is within 256 bytes, its ADLP object validates, and its manifest profile equals the tried configuration. + +This bounded synchronisation rule is intentional. A documented FSK implementation warns that free-running blocks need close symbol-boundary alignment.[1] Future Acoustic revisions must add timing recovery, level estimation, band-pass filtering, interleaving and device measurements before any live-route support claim. + +## Required regression evidence + +The implementation must keep deterministic tests for all four ADLP profile identifiers, a leading-silence offset no greater than 480 samples, one corrected Hamming codeword error, an invalid double-corruption rejection through ADLP integrity, and bounded additive sample noise. An Acoustic-1 golden fixture must be decoded and regenerated byte-for-byte from its documented ADLP object. + +## Non-goals + +Acoustic-1 does not provide encryption, authentication, replay protection, packet segmentation, ARQ, live playback/capture, AGC, echo cancellation, Bluetooth routing, radio compliance, or a cross-device acoustic success guarantee. + +## References + +[1]: https://tinytapeout.com/chips/ttgf26a/tt_um_hydrocomms "FSK Modem — Tiny Tapeout" +[2]: https://arxiv.org/html/2402.17777v1 "FSK Demodulation and Bit String Extraction: A Python-Centric Approach in SDR Systems" +[3]: https://liquidsdr.org/doc/fec/ "liquid-dsp forward error-correction documentation" diff --git a/spec/acoustic-1_RU.md b/spec/acoustic-1_RU.md new file mode 100644 index 0000000..031e771 --- /dev/null +++ b/spec/acoustic-1_RU.md @@ -0,0 +1,80 @@ +# Acoustic-1: экспериментальный audio PHY + +[English (canonical)](acoustic-1.md) · **Русский перевод** + +> **Перевод:** [spec/acoustic-1.md](acoustic-1.md). **Последняя синхронизация:** 2026-08-20. + +Acoustic-1 — первый физический профиль AudioModem для контролируемых PCM/WAV экспериментов. Он переносит неизменённый ADLP v1 wire object и не является заявлением совместимости speaker-to-microphone, radio, Bluetooth или разных устройств. Детерминированный bootstrap carrier остаётся reference transport для lossless tests; Acoustic-1 — отдельный carrier с явным framing и ограниченной error tolerance. + +## Scope и compatibility + +| Элемент | Правило Acoustic-1 | +| --- | --- | +| Вход и final integrity | ADLP v1 wire object вместе с его существующим CRC-32C. | +| Shape samples | Canonical RIFF/WAVE, mono, signed 16-bit PCM, 48 kHz. | +| Modulation | Binary FSK: один mark tone для `1`, один space tone для `0`. | +| FEC | Hamming(7,4) для length и wire-object nibbles. | +| Лимит объекта | Не более 256 encoded ADLP wire bytes в одном frame. | +| Версия carrier | Acoustic-1 выбирается codec API и не изменяет ADLP v1 version byte. | + +Binary FSK с preamble, sync word, payload и final integrity check соответствует структуре практических modem designs.[1] Acoustic-1 использует non-coherent comparison энергии двух tones вместо carrier-phase recovery, что соответствует различию coherent и non-coherent FSK detection.[2] + +> **Правило compatibility:** Decoder НЕ ДОЛЖЕН возвращать полученный object, пока не пройдут Hamming decoding, frame bounds, ADLP decoding и ADLP CRC-32C validation. + +## Параметры сигнала + +Encoder сбрасывает phase на границе каждого symbol. Samples используют integer sine table из 48 значений с amplitude `12,000`, исключая floating-point зависимость encoded golden vectors. Mark использует 1,000 Hz, space — 2,000 Hz; обе частоты являются integer harmonics sample rate 48 kHz. + +| ADLP profile | Samples на symbol | Nominal symbol rate | Предполагаемый trade-off | +| --- | ---: | ---: | --- | +| `reliable` | 480 | 100 baud | Больше energy на bit. | +| `balanced` | 240 | 200 baud | Default profile для controlled tests. | +| `fast` | 144 | 333.3 baud | Более короткое symbol window и ожидаемо меньший noise margin. | +| `narrowband` | 480 | 100 baud | Консервативный profile identifier; это **не** заявление radio narrowband compliance. | + +Profile передаётся внутри ADLP manifest. Decoder перебирает фиксированные Acoustic-1 profile configurations и ОБЯЗАН отклонить frame, если recovered manifest profile не совпадает с configuration, которым он был декодирован. + +## Frame layout + +Каждое поле передаётся most-significant bit first. `wire_length` и ADLP wire object защищаются FEC; preamble и sync передаются raw symbols, чтобы receiver получил alignment до FEC. + +| Поле | Symbols | Encoding | +| --- | ---: | --- | +| Preamble | 64 | Alternating `1, 0, 1, 0, …`, начинается с `1`. | +| Sync | 16 | Raw `0xD391`. | +| Wire length | 56 | Четыре big-endian bytes длины; каждый 4-bit nibble становится Hamming(7,4) codeword. | +| ADLP wire object | `14 × N` | Каждый byte разбивается на два nibbles; каждый становится Hamming(7,4) codeword. | + +Лимит object предотвращает unbounded WAV allocation. При 200 baud FEC payload rate составляет примерно 14.3 encoded wire bytes в секунду без учёта preamble и sync; большие файлы потребуют будущего packetisation design. + +## Hamming(7,4) mapping + +Acoustic-1 использует positions `1..7` в порядке `p1, p2, d3, p4, d2, d1, d0`. Для nibble `d3 d2 d1 d0` parity является even и вычисляется так: + +```text +p1 = d3 XOR d2 XOR d0 +p2 = d3 XOR d1 XOR d0 +p4 = d2 XOR d1 XOR d0 +``` + +Decoder получает three-bit syndrome и переворачивает indexed position, когда syndrome находится в `1..7`. Hamming block codes исправляют одну bit error в codeword; они не заменяют end-to-end integrity, которым остаётся ADLP CRC-32C.[3] Two-bit corruption может быть неправильно «исправлен», поэтому failed ADLP CRC означает failed frame и не должен возвращать payload. + +## Алгоритм receiver + +Receiver разбирает canonical 48 kHz PCM WAV и оценивает каждую объявленную profile configuration. Он сканирует только первые 480 PCM samples в поиске frame start: это явно поддерживает bounded leading-silence/timing offset, а не arbitrary timing recovery. Для каждого candidate symbol window он вычисляет in-phase и quadrature correlation energy с fixed mark и space references, затем выбирает большую. Candidate успешен только при совпадении preamble/sync, FEC length не более 256 bytes, ADLP object validates и его manifest profile совпадает с test configuration. + +Это ограниченное правило synchronisation выбрано намеренно. Документированный FSK implementation предупреждает, что free-running blocks требуют близкого совпадения с symbol boundaries.[1] Future Acoustic revisions должны добавить timing recovery, level estimation, band-pass filtering, interleaving и device measurements до любого заявления о live-route support. + +## Обязательные regression evidence + +Implementation обязана иметь deterministic tests для всех четырёх ADLP profile identifiers, leading-silence offset не более 480 samples, одной исправленной Hamming codeword error, invalid double-corruption rejection через ADLP integrity и bounded additive sample noise. Acoustic-1 golden fixture должен декодироваться и пересоздаваться byte-for-byte из документированного ADLP object. + +## Non-goals + +Acoustic-1 не предоставляет encryption, authentication, replay protection, packet segmentation, ARQ, live playback/capture, AGC, echo cancellation, Bluetooth routing, radio compliance или guarantee успешной межустройственной acoustic передачи. + +## Ссылки + +[1]: https://tinytapeout.com/chips/ttgf26a/tt_um_hydrocomms "FSK Modem — Tiny Tapeout" +[2]: https://arxiv.org/html/2402.17777v1 "FSK Demodulation and Bit String Extraction: A Python-Centric Approach in SDR Systems" +[3]: https://liquidsdr.org/doc/fec/ "Документация liquid-dsp по forward error-correction" diff --git a/spec/protocol-v1.md b/spec/protocol-v1.md index ea15b35..ce714c6 100644 --- a/spec/protocol-v1.md +++ b/spec/protocol-v1.md @@ -1,10 +1,10 @@ # ADLP v1: bootstrap container and WAV transport -**Status:** Experimental. This document defines a deterministic bootstrap path for automated tests and lossless WAV exchange. It is not yet an over-the-air acoustic PHY specification. +**Status:** Experimental. This document defines the ADLP v1 wire object and deterministic bootstrap path for automated tests and lossless WAV exchange. It is not an over-the-air acoustic PHY specification; the separate [Acoustic-1 experimental carrier](acoustic-1.md) has its own controlled-test scope. ## Layering -`TransferManifest + payload` are encoded into an ADLP wire object. The bootstrap codec maps this object to mono 48 kHz, 16-bit PCM WAV. Future `Acoustic-1` modulation replaces the bootstrap symbol mapper, not the wire object. +`TransferManifest + payload` are encoded into an ADLP wire object. The bootstrap codec maps this object to mono 48 kHz, 16-bit PCM WAV. The separate `Acoustic-1` codec also carries this unchanged wire object and replaces only the symbol mapper, not the ADLP bytes. ## ADLP wire object diff --git a/spec/protocol-v1_RU.md b/spec/protocol-v1_RU.md index 66d2643..cccaf12 100644 --- a/spec/protocol-v1_RU.md +++ b/spec/protocol-v1_RU.md @@ -2,13 +2,13 @@ [English (canonical)](protocol-v1.md) · **Русский перевод** -> **Translation of:** [spec/protocol-v1.md](protocol-v1.md). **Last synced:** 2026-08-19. Английский оригинал является нормативным для совместимости реализации. +> **Translation of:** [spec/protocol-v1.md](protocol-v1.md). **Last synced:** 2026-08-20. Английский оригинал является нормативным для совместимости реализации. -**Статус:** Experimental. Этот документ определяет детерминированный bootstrap path для automated tests и lossless WAV exchange. Это ещё не over-the-air acoustic PHY specification. +**Статус:** Experimental. Этот документ определяет ADLP v1 wire object и детерминированный bootstrap path для automated tests и lossless WAV exchange. Он не является over-the-air acoustic PHY specification; отдельный [экспериментальный carrier Acoustic-1](acoustic-1_RU.md) имеет собственный controlled-test scope. ## Слои -`TransferManifest + payload` кодируются в ADLP wire object. Bootstrap codec отображает объект в mono 48 kHz 16-bit PCM WAV. Будущая модуляция `Acoustic-1` заменяет bootstrap symbol mapper, а не wire object. +`TransferManifest + payload` кодируются в ADLP wire object. Bootstrap codec отображает объект в mono 48 kHz 16-bit PCM WAV. Отдельный codec `Acoustic-1` также переносит неизменённый wire object и заменяет только symbol mapper, а не ADLP bytes. ## ADLP wire object diff --git a/tools/adlp-cli/src/main.rs b/tools/adlp-cli/src/main.rs index 7d772f1..ddff5bd 100644 --- a/tools/adlp-cli/src/main.rs +++ b/tools/adlp-cli/src/main.rs @@ -1,7 +1,7 @@ use std::{env, error::Error, fs, process}; use adlp_protocol::{ObjectKind, TransferProfile, WireObject}; -use audio_modem_core::{decode_wav, encode_wav}; +use audio_modem_core::{acoustic1, decode_wav, encode_wav}; fn main() { if let Err(error) = run() { @@ -13,47 +13,74 @@ fn main() { fn run() -> Result<(), Box> { let args: Vec = env::args().collect(); match args.get(1).map(String::as_str) { - Some("encode-text") => { - let output = args.get(2).ok_or("missing output WAV path")?; - let callsign = args.get(3).ok_or("missing callsign")?; - let text = args.get(4).ok_or("missing text")?; - let profile = args - .get(5) - .map(|value| parse_profile(value)) - .transpose()? - .unwrap_or(TransferProfile::Balanced); - let object = WireObject::text(1, callsign, text, profile)?; - fs::write(output, encode_wav(&object)?)?; - println!( - "encoded {} bytes as {} WAV transfer", - object.payload.len(), - profile.as_str() - ); - Ok(()) - } - Some("decode") => { - let input = args.get(2).ok_or("missing input WAV path")?; - let decoded = decode_wav(&fs::read(input)?)?; - let manifest = decoded.object.manifest; - println!("protocol: ADLP/{}", manifest.protocol_version); - println!("profile: {}", manifest.profile.as_str()); - println!("session: {}", manifest.session_id); - println!("callsign: {}", manifest.sender_callsign); - println!("samples: {}", decoded.samples_consumed); - match manifest.object_kind { - ObjectKind::Text => { - println!("text: {}", String::from_utf8(decoded.object.payload)?) - } - ObjectKind::File => { - println!("file payload: {} bytes", decoded.object.payload.len()) - } - } - Ok(()) - } + Some("encode-text") => encode_text(&args, false), + Some("encode-acoustic1-text") => encode_text(&args, true), + Some("decode") => decode(&args, false), + Some("decode-acoustic1") => decode(&args, true), _ => Err("unknown command".into()), } } +fn encode_text(args: &[String], acoustic1_carrier: bool) -> Result<(), Box> { + let output = args.get(2).ok_or("missing output WAV path")?; + let callsign = args.get(3).ok_or("missing callsign")?; + let text = args.get(4).ok_or("missing text")?; + let profile = args + .get(5) + .map(|value| parse_profile(value)) + .transpose()? + .unwrap_or(TransferProfile::Balanced); + let object = WireObject::text(1, callsign, text, profile)?; + let carrier = if acoustic1_carrier { + "Acoustic-1" + } else { + "bootstrap" + }; + let wav = if acoustic1_carrier { + acoustic1::encode_wav(&object)? + } else { + encode_wav(&object)? + }; + fs::write(output, wav)?; + println!( + "encoded {} bytes as {} {} WAV transfer", + object.payload.len(), + profile.as_str(), + carrier + ); + Ok(()) +} + +fn decode(args: &[String], acoustic1_carrier: bool) -> Result<(), Box> { + let input = args.get(2).ok_or("missing input WAV path")?; + let (object, samples_consumed) = if acoustic1_carrier { + let decoded = acoustic1::decode_wav(&fs::read(input)?)?; + (decoded.object, decoded.samples_consumed) + } else { + let decoded = decode_wav(&fs::read(input)?)?; + (decoded.object, decoded.samples_consumed) + }; + let manifest = object.manifest; + println!( + "carrier: {}", + if acoustic1_carrier { + "acoustic1" + } else { + "bootstrap" + } + ); + println!("protocol: ADLP/{}", manifest.protocol_version); + println!("profile: {}", manifest.profile.as_str()); + println!("session: {}", manifest.session_id); + println!("callsign: {}", manifest.sender_callsign); + println!("samples: {samples_consumed}"); + match manifest.object_kind { + ObjectKind::Text => println!("text: {}", String::from_utf8(object.payload)?), + ObjectKind::File => println!("file payload: {} bytes", object.payload.len()), + } + Ok(()) +} + fn parse_profile(value: &str) -> Result> { match value { "reliable" => Ok(TransferProfile::Reliable), @@ -65,5 +92,5 @@ fn parse_profile(value: &str) -> Result> { } fn usage() -> &'static str { - "Usage:\n adlp-cli encode-text [reliable|balanced|fast|narrowband]\n adlp-cli decode " + "Usage:\n adlp-cli encode-text [reliable|balanced|fast|narrowband]\n adlp-cli decode \n adlp-cli encode-acoustic1-text [reliable|balanced|fast|narrowband]\n adlp-cli decode-acoustic1 " }