From 5440816f78c9baff2d9343d136f9ee420a247d1e Mon Sep 17 00:00:00 2001 From: Matthew Baranov Date: Thu, 20 Aug 2026 14:57:36 +0000 Subject: [PATCH] feat: add WAV file transfer and golden fixture --- apps/audio_modem/lib/main.dart | 135 ++++++++++++++++-- .../lib/platform/wav_file_adapter.dart | 67 +++++++++ .../Flutter/GeneratedPluginRegistrant.swift | 2 + apps/audio_modem/pubspec.lock | 119 ++++++++++++++- apps/audio_modem/pubspec.yaml | 1 + apps/audio_modem/test/widget_test.dart | 63 +++++++- crates/audio-modem-core/src/lib.rs | 19 +++ .../audio-modem-core/tests/fixtures/README.md | 16 +++ .../tests/fixtures/adlp-v1-text-balanced.wav | Bin 0 -> 40364 bytes docs/architecture/flutter-rust-bridge.md | 8 +- docs/architecture/flutter-rust-bridge_RU.md | 8 +- docs/guides/audio-routes.md | 18 ++- docs/guides/audio-routes_RU.md | 18 ++- site/index.html | 4 +- site/ru/index.html | 4 +- 15 files changed, 460 insertions(+), 22 deletions(-) create mode 100644 apps/audio_modem/lib/platform/wav_file_adapter.dart create mode 100644 crates/audio-modem-core/tests/fixtures/README.md create mode 100644 crates/audio-modem-core/tests/fixtures/adlp-v1-text-balanced.wav diff --git a/apps/audio_modem/lib/main.dart b/apps/audio_modem/lib/main.dart index 9437d20..d5a8736 100644 --- a/apps/audio_modem/lib/main.dart +++ b/apps/audio_modem/lib/main.dart @@ -1,10 +1,12 @@ // AudioModem Flutter workbench: UI stays transport-aware while Rust owns ADLP and WAV codec behavior. import 'dart:convert'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'bridge/wav_bootstrap_bridge.dart'; +import 'platform/wav_file_adapter.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -16,13 +18,20 @@ Future main() async { 'Rust/WAV bridge initialization failed: $error', ); } - runApp(AudioModemApp(bridge: bridge)); + runApp( + AudioModemApp(bridge: bridge, fileAdapter: const PlatformWavFileAdapter()), + ); } class AudioModemApp extends StatelessWidget { - const AudioModemApp({super.key, required this.bridge}); + const AudioModemApp({ + super.key, + required this.bridge, + required this.fileAdapter, + }); final WavBootstrapBridge bridge; + final WavFileAdapter fileAdapter; @override Widget build(BuildContext context) { @@ -38,15 +47,20 @@ class AudioModemApp extends StatelessWidget { scaffoldBackgroundColor: const Color(0xFFFFFCF5), useMaterial3: true, ), - home: TransferWorkbench(bridge: bridge), + home: TransferWorkbench(bridge: bridge, fileAdapter: fileAdapter), ); } } class TransferWorkbench extends StatefulWidget { - const TransferWorkbench({super.key, required this.bridge}); + const TransferWorkbench({ + super.key, + required this.bridge, + required this.fileAdapter, + }); final WavBootstrapBridge bridge; + final WavFileAdapter fileAdapter; @override State createState() => _TransferWorkbenchState(); @@ -61,6 +75,8 @@ class _TransferWorkbenchState extends State { bool _isWorking = false; WavBuildResult? _builtWav; WavDecodeResult? _decodedWav; + String? _activeWavName; + Uint8List? _activeWavBytes; String? _bridgeError; @override @@ -100,6 +116,8 @@ class _TransferWorkbenchState extends State { setState(() { _builtWav = built; _decodedWav = decoded; + _activeWavName = _suggestedWavName(built.sessionId); + _activeWavBytes = built.wavBytes; }); _showMessage('WAV собран и проверен Rust decoder.'); } catch (error) { @@ -116,6 +134,35 @@ class _TransferWorkbenchState extends State { } Future _verifyCurrentWav() async { + final wavBytes = _activeWavBytes; + if (wavBytes == null) { + _showMessage('Сначала соберите или импортируйте WAV.'); + return; + } + setState(() { + _isWorking = true; + _bridgeError = null; + }); + try { + final decoded = await widget.bridge.decodeWav(wavBytes); + if (!mounted) { + return; + } + setState(() => _decodedWav = decoded); + _showMessage('WAV повторно проверен Rust decoder.'); + } catch (error) { + if (!mounted) { + return; + } + setState(() => _bridgeError = error.toString()); + } finally { + if (mounted) { + setState(() => _isWorking = false); + } + } + } + + Future _exportBuiltWav() async { final built = _builtWav; if (built == null) { _showMessage('Сначала соберите WAV во вкладке «Передать».'); @@ -126,17 +173,66 @@ class _TransferWorkbenchState extends State { _bridgeError = null; }); try { - final decoded = await widget.bridge.decodeWav(built.wavBytes); + final saved = await widget.fileAdapter.saveWav( + suggestedName: _suggestedWavName(built.sessionId), + bytes: built.wavBytes, + ); if (!mounted) { return; } - setState(() => _decodedWav = decoded); - _showMessage('WAV повторно проверен Rust decoder.'); + if (saved == null) { + _showMessage('Экспорт WAV отменён.'); + } else { + _showMessage('WAV сохранён: ${saved.name}.'); + } + } catch (error) { + if (!mounted) { + return; + } + setState(() => _bridgeError = error.toString()); + _showMessage('Не удалось сохранить WAV.'); + } finally { + if (mounted) { + setState(() => _isWorking = false); + } + } + } + + Future _importAndVerifyWav() async { + if (!widget.bridge.isAvailable) { + _showMessage('Rust/WAV bridge недоступен в этой сборке.'); + return; + } + setState(() { + _isWorking = true; + _bridgeError = null; + }); + try { + final selected = await widget.fileAdapter.openWav(); + if (selected == null) { + if (mounted) { + _showMessage('Импорт WAV отменён.'); + } + return; + } + final decoded = await widget.bridge.decodeWav(selected.bytes); + if (!mounted) { + return; + } + setState(() { + _activeWavName = selected.name; + _activeWavBytes = selected.bytes; + _decodedWav = decoded; + }); + _showMessage('WAV импортирован и проверен Rust decoder.'); } catch (error) { if (!mounted) { return; } setState(() => _bridgeError = error.toString()); + _showMessage( + 'Импортированный файл не является корректной WAV передачей.', + ); } finally { if (mounted) { setState(() => _isWorking = false); @@ -144,6 +240,8 @@ class _TransferWorkbenchState extends State { } } + String _suggestedWavName(int sessionId) => 'adlp-$sessionId.wav'; + void _showMessage(String message) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); @@ -381,6 +479,12 @@ class _TransferWorkbenchState extends State { label: 'Decoder', value: '${decoded.sampleRateHz ~/ 1000} kHz / CRC ok', ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: _isWorking ? null : _exportBuiltWav, + icon: const Icon(Icons.save_alt_outlined), + label: const Text('Экспортировать WAV'), + ), ], if (_bridgeError case final error?) ...[ const SizedBox(height: 16), @@ -447,9 +551,16 @@ class _TransferWorkbenchState extends State { const SizedBox(height: 8), Text( decoded == null - ? 'Сначала соберите WAV во вкладке «Передать». Импорт файла и live receiver будут добавлены отдельными адаптерами.' + ? 'Сначала соберите WAV во вкладке «Передать» или импортируйте готовую передачу.' : 'Decoder вернул только объект, прошедший framing, manifest и CRC-32C проверку.', ), + if (_activeWavName case final name?) ...[ + const SizedBox(height: 8), + Text( + 'Источник: $name', + style: const TextStyle(color: Color(0xFF6D6A62)), + ), + ], if (decoded != null) ...[ const SizedBox(height: 20), _receiptRow('Позывной', decoded.senderCallsign), @@ -471,6 +582,14 @@ class _TransferWorkbenchState extends State { icon: const Icon(Icons.refresh), label: const Text('Повторно проверить WAV'), ), + const SizedBox(height: 12), + FilledButton.icon( + onPressed: _isWorking || !widget.bridge.isAvailable + ? null + : _importAndVerifyWav, + icon: const Icon(Icons.folder_open_outlined), + label: const Text('Импортировать WAV'), + ), ], ), ), diff --git a/apps/audio_modem/lib/platform/wav_file_adapter.dart b/apps/audio_modem/lib/platform/wav_file_adapter.dart new file mode 100644 index 0000000..0ac4077 --- /dev/null +++ b/apps/audio_modem/lib/platform/wav_file_adapter.dart @@ -0,0 +1,67 @@ +// Local WAV file adapter. It owns only user file dialogs; Rust owns ADLP and WAV validation. + +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; + +class SelectedWavFile { + const SelectedWavFile({required this.name, required this.bytes}); + + final String name; + final Uint8List bytes; +} + +class SavedWavFile { + const SavedWavFile({required this.name, required this.location}); + + final String name; + final Uri location; +} + +abstract interface class WavFileAdapter { + Future openWav(); + + Future saveWav({ + required String suggestedName, + required Uint8List bytes, + }); +} + +class PlatformWavFileAdapter implements WavFileAdapter { + const PlatformWavFileAdapter(); + + @override + Future openWav() async { + final selected = await FilePicker.pickFile( + dialogTitle: 'Выберите WAV передачу', + type: FileType.custom, + allowedExtensions: const ['wav'], + ); + if (selected == null) { + return null; + } + return SelectedWavFile( + name: selected.name, + bytes: await selected.readAsBytes(), + ); + } + + @override + Future saveWav({ + required String suggestedName, + required Uint8List bytes, + }) async { + final location = await FilePicker.saveFile( + dialogTitle: 'Сохранить WAV передачу', + fileName: suggestedName, + bytes: bytes, + mimeType: 'audio/wav', + type: FileType.custom, + allowedExtensions: const ['wav'], + ); + if (location == null) { + return null; + } + return SavedWavFile(name: suggestedName, location: location); + } +} diff --git a/apps/audio_modem/macos/Flutter/GeneratedPluginRegistrant.swift b/apps/audio_modem/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..ce65112 100644 --- a/apps/audio_modem/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/apps/audio_modem/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import file_picker_darwin func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) } diff --git a/apps/audio_modem/pubspec.lock b/apps/audio_modem/pubspec.lock index bc9a2b2..7f99a0f 100644 --- a/apps/audio_modem/pubspec.lock +++ b/apps/audio_modem/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + android_file_picker: + dependency: transitive + description: + name: android_file_picker + sha256: "665a5a57dfca27f91a715d300e4852a784f9f98e503dcff281bec9afb55767be" + url: "https://pub.dev" + source: hosted + version: "1.0.1" args: dependency: transitive description: @@ -64,6 +72,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51" + url: "https://pub.dev" + source: hosted + version: "0.3.5+4" cupertino_icons: dependency: "direct main" description: @@ -72,6 +88,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: a48d5da28e89bd02196e80d81ed8d7954923d00a0f4a68cc20b575038f023383 + url: "https://pub.dev" + source: hosted + version: "0.7.15" fake_async: dependency: transitive description: @@ -80,6 +104,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: afbaa8015d9efabd224f41084ed9fdeddfa65389ebd7cd3a9eb1476aca66b46d + url: "https://pub.dev" + source: hosted + version: "12.0.0" + file_picker_darwin: + dependency: transitive + description: + name: file_picker_darwin + sha256: "5d87d156c1d63920447a662b7117d3498c67e3444a44d2cb01ed955d6efa62ef" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + file_picker_linux: + dependency: transitive + description: + name: file_picker_linux + sha256: "93d3f62f97c657053e7b184fe0f5e22347d85067c053640a30b1ac8ad7844e3b" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + file_picker_platform_interface: + dependency: transitive + description: + name: file_picker_platform_interface + sha256: "9e7a7e01e179929241f0afeb2c8c69ac95e29e17c0abea36ed193881c6bef90c" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + file_picker_web: + dependency: transitive + description: + name: file_picker_web + sha256: f1af38b3c91fafe0ca97f659b5c6818a057473ef09bb8b722f9f3f5364aa7eed + url: "https://pub.dev" + source: hosted + version: "3.0.1" flutter: dependency: "direct main" description: flutter @@ -106,6 +186,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -170,6 +255,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" plugin_platform_interface: dependency: transitive description: @@ -255,6 +348,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d + url: "https://pub.dev" + source: hosted + version: "6.4.0" + windows_file_picker: + dependency: transitive + description: + name: windows_file_picker + sha256: "72cf23466e146f2c0f19e1d78be97ff6409dc15b0c090f8eb284f94f4a33de26" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + xml: + dependency: transitive + description: + name: xml + sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + url: "https://pub.dev" + source: hosted + version: "7.0.1" sdks: dart: ">=3.13.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.38.0" diff --git a/apps/audio_modem/pubspec.yaml b/apps/audio_modem/pubspec.yaml index 3b44ce2..03433f1 100644 --- a/apps/audio_modem/pubspec.yaml +++ b/apps/audio_modem/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: audio_modem_bridge: path: rust_builder flutter_rust_bridge: 2.12.0 + file_picker: ^12.0.0 dev_dependencies: flutter_test: diff --git a/apps/audio_modem/test/widget_test.dart b/apps/audio_modem/test/widget_test.dart index 6a7892c..906d7aa 100644 --- a/apps/audio_modem/test/widget_test.dart +++ b/apps/audio_modem/test/widget_test.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:audio_modem/bridge/wav_bootstrap_bridge.dart'; import 'package:audio_modem/main.dart'; +import 'package:audio_modem/platform/wav_file_adapter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -34,11 +35,36 @@ class _FakeWavBridge implements WavBootstrapBridge { ); } +class _FakeWavFileAdapter implements WavFileAdapter { + _FakeWavFileAdapter({this.openedFile}); + + final SelectedWavFile? openedFile; + SavedWavFile? savedFile; + + @override + Future openWav() async => openedFile; + + @override + Future saveWav({ + required String suggestedName, + required Uint8List bytes, + }) async { + savedFile = SavedWavFile( + name: suggestedName, + location: Uri.parse('memory://$suggestedName'), + ); + return savedFile; + } +} + void main() { testWidgets('send workbench builds and verifies an in-memory WAV flow', ( tester, ) async { - await tester.pumpWidget(AudioModemApp(bridge: _FakeWavBridge())); + final fileAdapter = _FakeWavFileAdapter(); + await tester.pumpWidget( + AudioModemApp(bridge: _FakeWavBridge(), fileAdapter: fileAdapter), + ); expect(find.text('Соберите и проверьте WAV.'), findsOneWidget); expect(find.text('Надёжный'), findsOneWidget); @@ -61,5 +87,40 @@ void main() { expect(find.text('ПРОВЕРЕНО RUST'), findsOneWidget); expect(find.text('4 байт'), findsOneWidget); + + final exportButton = find.widgetWithText( + OutlinedButton, + 'Экспортировать WAV', + ); + await tester.ensureVisible(exportButton); + await tester.tap(exportButton); + await tester.pumpAndSettle(); + + expect(fileAdapter.savedFile?.name, startsWith('adlp-')); + }); + + testWidgets('receive workbench imports and verifies a user-selected WAV', ( + tester, + ) async { + final fileAdapter = _FakeWavFileAdapter( + openedFile: SelectedWavFile( + name: 'received.wav', + bytes: Uint8List.fromList([82, 73, 70, 70]), + ), + ); + await tester.pumpWidget( + AudioModemApp(bridge: _FakeWavBridge(), fileAdapter: fileAdapter), + ); + + await tester.tap(find.byIcon(Icons.south_west)); + await tester.pumpAndSettle(); + + final importButton = find.widgetWithText(FilledButton, 'Импортировать WAV'); + await tester.ensureVisible(importButton); + await tester.tap(importButton); + await tester.pumpAndSettle(); + + expect(find.text('WAV object проверен.'), findsOneWidget); + expect(find.text('Источник: received.wav'), findsOneWidget); }); } diff --git a/crates/audio-modem-core/src/lib.rs b/crates/audio-modem-core/src/lib.rs index 3ba9cb1..62971b1 100644 --- a/crates/audio-modem-core/src/lib.rs +++ b/crates/audio-modem-core/src/lib.rs @@ -240,6 +240,8 @@ mod tests { use super::*; use adlp_protocol::TransferProfile; + const GOLDEN_WAV: &[u8] = include_bytes!("../tests/fixtures/adlp-v1-text-balanced.wav"); + #[test] fn wav_round_trip_preserves_adlp_object() { let object = @@ -257,4 +259,21 @@ mod tests { wav[first_wire_symbol..first_wire_symbol + SAMPLES_PER_BIT * 2].fill(0); assert!(decode_wav(&wav).is_err()); } + + #[test] + fn golden_wav_fixture_decodes_and_matches_deterministic_encoder() { + let expected = WireObject::text( + 1, + "GOLDEN1", + "AudioModem ADLP golden fixture v1", + TransferProfile::Balanced, + ) + .unwrap(); + let decoded = decode_wav(GOLDEN_WAV).unwrap(); + + assert_eq!(decoded.object, expected); + assert_eq!(decoded.sample_rate_hz, SAMPLE_RATE_HZ); + assert_eq!(decoded.samples_consumed, 20_160); + assert_eq!(encode_wav(&expected).unwrap(), GOLDEN_WAV); + } } diff --git a/crates/audio-modem-core/tests/fixtures/README.md b/crates/audio-modem-core/tests/fixtures/README.md new file mode 100644 index 0000000..74b918d --- /dev/null +++ b/crates/audio-modem-core/tests/fixtures/README.md @@ -0,0 +1,16 @@ +# ADLP WAV golden fixtures + +`adlp-v1-text-balanced.wav` is a canonical WAV bootstrap fixture for deterministic regression coverage. 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-text` | +| Session ID | `1` | +| Callsign | `GOLDEN1` | +| Profile | `balanced` | +| UTF-8 text | `AudioModem ADLP golden fixture v1` | +| WAV shape | RIFF, mono, 16-bit PCM, 48 kHz | +| Size | `40,364` bytes | +| 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. diff --git a/crates/audio-modem-core/tests/fixtures/adlp-v1-text-balanced.wav b/crates/audio-modem-core/tests/fixtures/adlp-v1-text-balanced.wav new file mode 100644 index 0000000000000000000000000000000000000000..84a36ab7d62e5187a4ad360c67c7fef8baf15a5a GIT binary patch literal 40364 zcmeH}F>X{r5JbN@fsepd2v~{CB7oQuazl^#9 z^oI*ee|?*L-PrUq{lwpZZX=(@yOniL&^wjP$mq0u8t+!tIYIAKG9#nY@@c$VS?2`3 zQ^|~sPRpn9Ze^Vl^iCx+GCD1v#=DhuPS87*%*g1pd>Zdo);U4%R5Bx@)ADJ&TUqA> zy;I4Ij84m^@or_E6ZB3cGcr0YpT@hDbxzPbmCVTKw0s)xR@ON|?^H4)qto(fyjxl4 z1ie$qJj3X3&!_aea>Cjj>f`3=bkv9DQ{L_4&ZqMZKMhO+)4-|*-2cAOe9F@vXg=lL zJ~V%Ke)`uh=O=D>Pkp-a?s;iG z<=uT!A2(0ub^Fl#@u!Wg<=y@7o=@AR^SXKJ@8)M--GIA)ny+oopHd&nQ$u$?^`X4m zpXR%H+7IQs`BNYFyfnWXPy6h~@7^c%@76!{anDQhyYcQm-Sbla-T7{PcJpz+@2$;C z`=tH2^Sk-bc`4s*-rarN{`7q*PyO9|H-G9wdDeQ5vPcsg%4p7u%o z-TBmq@^1g#^J$-y@753Xp*-z}^3;d&)Q9ra$IZL_X+Gs?Kkj_D5A~-!^&fs3mr%@j_pAGfSt@+&Ay!!R?X}5l|;o6HUC)Q9ra)}7zYhtAuLr+s$gckh$> zPyf0x+K*1lr@QCpiPrAWekf0EX+Gtt59O&3<=sB+{BHA7pWS)-zHXlO+0BRgQ-0>v m4bVP^*A3{l^Qlhz|4wrQd+pE{={B9)pXR%H+J846xBnkdB4Jwq literal 0 HcmV?d00001 diff --git a/docs/architecture/flutter-rust-bridge.md b/docs/architecture/flutter-rust-bridge.md index ac65d7c..af2552a 100644 --- a/docs/architecture/flutter-rust-bridge.md +++ b/docs/architecture/flutter-rust-bridge.md @@ -1,8 +1,8 @@ # Flutter ↔ Rust WAV bridge -**Last reviewed:** 2026-08-19 · **English (canonical)** · [Русский](flutter-rust-bridge_RU.md) +**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. It does not introduce audio capture, playback, Bluetooth, files selected by the user, 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 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. ## Boundary @@ -21,6 +21,10 @@ The bridge crate is a thin native facade. `adlp-protocol` remains responsible fo 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. +## File adapter boundary + +`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 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. diff --git a/docs/architecture/flutter-rust-bridge_RU.md b/docs/architecture/flutter-rust-bridge_RU.md index e6f323c..54a040f 100644 --- a/docs/architecture/flutter-rust-bridge_RU.md +++ b/docs/architecture/flutter-rust-bridge_RU.md @@ -2,9 +2,9 @@ [English (canonical)](flutter-rust-bridge.md) · **Русский перевод** -> **Translation of:** [docs/architecture/flutter-rust-bridge.md](flutter-rust-bridge.md). **Last synced:** 2026-08-19. +> **Translation of:** [docs/architecture/flutter-rust-bridge.md](flutter-rust-bridge.md). **Last synced:** 2026-08-20. -Первый native bridge открывает Flutter доступ к существующей Rust-реализации ADLP и WAV bootstrap. Он не добавляет audio capture, playback, Bluetooth, выбранные пользователем файлы, background transmission или encryption. Его цель — сделать один реальный app-to-core round trip наблюдаемым и тестируемым без дублирования protocol или codec logic в Dart. +Первый 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. ## Граница @@ -23,6 +23,10 @@ Bridge crate — тонкий native facade. `adlp-protocol` остаётся о В этом первом срезе Flutter app выбирает session value. Экран использует positive timestamp-derived value только как local transfer metadata; это не timestamp claim, identity claim или cryptographic nonce. +## Граница file adapter + +`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 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. diff --git a/docs/guides/audio-routes.md b/docs/guides/audio-routes.md index 26476e5..4c985f0 100644 --- a/docs/guides/audio-routes.md +++ b/docs/guides/audio-routes.md @@ -1,6 +1,6 @@ # Audio routes and transport boundaries -**Last reviewed:** 2026-08-19 · **English (canonical)** · [Русский](audio-routes_RU.md) +**Last reviewed:** 2026-08-20 · **English (canonical)** · [Русский](audio-routes_RU.md) An AudioModem route delivers PCM samples. It does not define the data object. The ADLP object, frame integrity and selected profile must stay independent of whether the samples are saved, played, recorded or passed through another device. This is the design that allows delayed file exchange and a one-way radio path to use the same data-link layer. @@ -8,7 +8,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. | Golden fixtures, documented CLI behavior and CI verification. | +| 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. | | 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. | @@ -23,6 +23,16 @@ object → ADLP frame → selected PHY profile → PCM → route route → PCM → selected PHY profile → ADLP frame → verified object ``` +## Local WAV file workflow + +The current Flutter workbench can save a verified in-memory WAV transfer through a user-selected save dialog and can select one `.wav` file through a local open dialog. The file adapter owns only the platform dialog and raw bytes. Every imported byte sequence is still passed to the Rust `decodeWav` bridge; the UI displays payload metadata only after framing, manifest and CRC-32C validation succeeds. Cancellation is not an error and does not create a transfer state. + +The adapter relies on `file_picker`, whose documented API supports custom extension filters, byte reads and save-file dialogs across Android, iOS, Linux, macOS, Windows and web.[1] The adapter makes no claim that a file can be played over a speaker, captured from a microphone, routed through Bluetooth, or received from an audio cable. + +## 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. + ## Callsigns and privacy A callsign is unencrypted display metadata in ADLP v1. It may be useful for a human operator but it is not identity proof. Future key exchange, encryption and signatures must be documented in a dedicated RFC and cannot be inferred from the presence of a callsign. @@ -30,3 +40,7 @@ A callsign is unencrypted display metadata in ADLP v1. It may be useful for a hu ## Design rule for adapters An adapter must report observable route facts—selected device, nominal sample rate, channel count, level or permission failure—without changing the ADLP object. Route diagnostics belong in the app’s event/reporting layer, while the codec remains deterministic and independently testable. + +## References + +[1]: https://pub.dev/packages/file_picker "file_picker package documentation" diff --git a/docs/guides/audio-routes_RU.md b/docs/guides/audio-routes_RU.md index 6acaca0..a014b91 100644 --- a/docs/guides/audio-routes_RU.md +++ b/docs/guides/audio-routes_RU.md @@ -2,7 +2,7 @@ [English (canonical)](audio-routes.md) · **Русский перевод** -> **Translation of:** [docs/guides/audio-routes.md](audio-routes.md). **Last synced:** 2026-08-19. +> **Translation of:** [docs/guides/audio-routes.md](audio-routes.md). **Last synced:** 2026-08-20. Маршрут AudioModem доставляет PCM samples. Он не определяет data object. ADLP object, frame integrity и выбранный profile должны оставаться независимыми от того, сохраняются ли samples, воспроизводятся, записываются или проходят через другое устройство. Это позволяет использовать один data-link layer для delayed file exchange и one-way radio path. @@ -10,7 +10,7 @@ | Маршрут | Модель доставки | Bootstrap status | Требование до статуса “supported” | | --- | --- | --- | --- | -| Lossless WAV | Файл переносит canonical PCM samples. | Реализован для text round trips. | Golden fixtures, documented CLI behavior и CI verification. | +| Lossless WAV | Файл переносит canonical PCM samples. | Реализован для text round trips и явного локального import/export в Flutter workbench. | Наблюдения acoustic interoperability на разных платформах и опубликованная fixture compatibility policy. | | Динамик → микрофон | 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. | @@ -25,6 +25,16 @@ object → ADLP frame → selected PHY profile → PCM → route route → PCM → selected PHY profile → ADLP frame → verified object ``` +## Локальный WAV file workflow + +Текущий Flutter workbench может сохранить проверенную WAV-передачу из памяти через выбранный пользователем save dialog и выбрать один `.wav` файл через локальный open dialog. File adapter владеет только platform dialog и raw bytes. Каждая импортированная последовательность байтов всё равно передаётся в Rust bridge `decodeWav`; UI показывает metadata payload только после успешной проверки framing, manifest и CRC-32C. Отмена диалога не считается ошибкой и не создаёт состояние передачи. + +Adapter использует `file_picker`: его документированный API поддерживает custom extension filters, чтение bytes и save-file dialogs на Android, iOS, Linux, macOS, Windows и web.[1] Adapter не утверждает, что файл можно воспроизвести через динамик, захватить с микрофона, направить по Bluetooth или получить через аудиокабель. + +## 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. + ## Позывные и приватность Позывной — незашифрованная display metadata в ADLP v1. Он может быть полезен оператору, но не является identity proof. Future key exchange, encryption и signatures должны быть описаны отдельным RFC и не могут подразумеваться из наличия callsign. @@ -32,3 +42,7 @@ route → PCM → selected PHY profile → ADLP frame → verified object ## Правило для adapters Adapter обязан сообщать наблюдаемые route facts — selected device, nominal sample rate, channel count, level или permission failure — не меняя ADLP object. Route diagnostics относятся к app event/reporting layer, а codec остаётся deterministic и independently testable. + +## Ссылки + +[1]: https://pub.dev/packages/file_picker "Документация пакета file_picker" diff --git a/site/index.html b/site/index.html index ec721ed..90155c8 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. It is the first reference transport for verifying the container and codec.

CLI / bootstrapcargo run -p adlp-cli -- encode-text hello.wav N1 "Hello" reliable
+      

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 →

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
next
• 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
• Live audio and encryption
planned
See roadmap →
diff --git a/site/ru/index.html b/site/ru/index.html index e839d8b..600dcbb 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-аудиотракта. Это первый reference transport для проверки контейнера и codec.

CLI / bootstrapcargo run -p adlp-cli -- encode-text hello.wav N1 "Привет" reliable
+      

Первый тест

Начните с воспроизводимого 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
Открыть краткое руководство →

Архитектура

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

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

Статус

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

✓ ADLP v1 + CRC-32C
готово
✓ Детерминированный PCM/WAV bootstrap
готово
• Flutter ↔ Rust bridge
следующее
• 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
• Live audio и encryption
план
Посмотреть дорожную карту →