Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ jobs:
"docs/research/acoustic-1-phy-sources.md:docs/research/acoustic-1-phy-sources_RU.md"
"docs/research/acoustic-2-measurement-sources.md:docs/research/acoustic-2-measurement-sources_RU.md"
"docs/research/device-acceptance-sources.md:docs/research/device-acceptance-sources_RU.md"
"docs/research/live-audio-adapter-sources.md:docs/research/live-audio-adapter-sources_RU.md"
"docs/operations/device-acceptance.md:docs/operations/device-acceptance_RU.md"
"spec/protocol-v1.md:spec/protocol-v1_RU.md"
"spec/acoustic-1.md:spec/acoustic-1_RU.md"
"spec/acoustic-2.md:spec/acoustic-2_RU.md"
"spec/live-audio-adapter-v1.md:spec/live-audio-adapter-v1_RU.md"
)
for pair in "${pairs[@]}"; do
source_path="${pair%%:*}"
Expand Down
22 changes: 18 additions & 4 deletions apps/audio_modem/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';

import 'bridge/wav_bootstrap_bridge.dart';
import 'platform/live_audio_adapter.dart';
import 'platform/wav_file_adapter.dart';

Future<void> main() async {
Expand All @@ -19,7 +20,11 @@ Future<void> main() async {
);
}
runApp(
AudioModemApp(bridge: bridge, fileAdapter: const PlatformWavFileAdapter()),
AudioModemApp(
bridge: bridge,
fileAdapter: const PlatformWavFileAdapter(),
liveAudioAdapter: const UnavailableLiveAudioAdapter(),
),
);
}

Expand All @@ -28,10 +33,12 @@ class AudioModemApp extends StatelessWidget {
super.key,
required this.bridge,
required this.fileAdapter,
this.liveAudioAdapter = const UnavailableLiveAudioAdapter(),
});

final WavBootstrapBridge bridge;
final WavFileAdapter fileAdapter;
final LiveAudioAdapter liveAudioAdapter;

@override
Widget build(BuildContext context) {
Expand All @@ -47,7 +54,11 @@ class AudioModemApp extends StatelessWidget {
scaffoldBackgroundColor: const Color(0xFFFFFCF5),
useMaterial3: true,
),
home: TransferWorkbench(bridge: bridge, fileAdapter: fileAdapter),
home: TransferWorkbench(
bridge: bridge,
fileAdapter: fileAdapter,
liveAudioAdapter: liveAudioAdapter,
),
);
}
}
Expand All @@ -57,10 +68,12 @@ class TransferWorkbench extends StatefulWidget {
super.key,
required this.bridge,
required this.fileAdapter,
required this.liveAudioAdapter,
});

final WavBootstrapBridge bridge;
final WavFileAdapter fileAdapter;
final LiveAudioAdapter liveAudioAdapter;

@override
State<TransferWorkbench> createState() => _TransferWorkbenchState();
Expand Down Expand Up @@ -91,7 +104,8 @@ class _TransferWorkbenchState extends State<TransferWorkbench> {
Future<void> _buildAndVerifyWav() async {
if (_route != TransferRoute.wav) {
_showMessage(
'Первый Rust bridge реализован только для WAV. Выберите WAV-маршрут.',
widget.liveAudioAdapter.availability.reason ??
'Live-audio маршрут недоступен. Выберите WAV-маршрут.',
);
return;
}
Expand Down Expand Up @@ -540,7 +554,7 @@ class _TransferWorkbenchState extends State<TransferWorkbench> {
],
const SizedBox(height: 12),
const Text(
'WAV можно экспортировать и импортировать локально. Live audio, cable, Bluetooth и radio adapters ещё не реализованы.',
'WAV можно экспортировать и импортировать локально. Live-audio adapter contract добавлен, но capture, playback, cable, Bluetooth и radio adapters ещё не реализованы.',
style: TextStyle(
fontSize: 12,
height: 1.4,
Expand Down
88 changes: 88 additions & 0 deletions apps/audio_modem/lib/platform/live_audio_adapter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Product-layer live-audio contract. This file deliberately does not initialize
// a recording/playback plugin, request permissions, or access audio hardware.

import 'dart:typed_data';

enum PcmSampleFormat { signedPcm16LittleEndian }

class PcmStreamFormat {
const PcmStreamFormat({
required this.sampleRateHz,
required this.channels,
required this.sampleFormat,
});

static const audioModemV1 = PcmStreamFormat(
sampleRateHz: 48000,
channels: 1,
sampleFormat: PcmSampleFormat.signedPcm16LittleEndian,
);

final int sampleRateHz;
final int channels;
final PcmSampleFormat sampleFormat;

bool get isAudioModemV1 =>
sampleRateHz == audioModemV1.sampleRateHz &&
channels == audioModemV1.channels &&
sampleFormat == audioModemV1.sampleFormat;
}

class LiveAudioAvailability {
const LiveAudioAvailability.unavailable(this.reason)
: isAvailable = false,
supportedFormat = PcmStreamFormat.audioModemV1;

const LiveAudioAvailability.available({required this.supportedFormat})
: isAvailable = true,
reason = null;

final bool isAvailable;
final String? reason;
final PcmStreamFormat supportedFormat;
}

abstract interface class LiveAudioAdapter {
LiveAudioAvailability get availability;

Future<void> startPlayback({
required Uint8List pcmFrames,
required PcmStreamFormat format,
});

Stream<Uint8List> startCapture({required PcmStreamFormat format});

Future<void> stop();

Future<void> dispose();
}

class UnavailableLiveAudioAdapter implements LiveAudioAdapter {
const UnavailableLiveAudioAdapter([
this._reason = 'Live-audio adapters are not implemented. Build or import a WAV instead.',
]);

final String _reason;

@override
LiveAudioAvailability get availability =>
LiveAudioAvailability.unavailable(_reason);

StateError _error() => StateError(_reason);

@override
Future<void> startPlayback({
required Uint8List pcmFrames,
required PcmStreamFormat format,
}) => Future.error(_error());

@override
Stream<Uint8List> startCapture({required PcmStreamFormat format}) =>
Stream.error(_error());

@override
Future<void> stop() => Future.error(_error());

@override
Future<void> dispose() => Future.value();
}
59 changes: 59 additions & 0 deletions apps/audio_modem/test/widget_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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/live_audio_adapter.dart';
import 'package:audio_modem/platform/wav_file_adapter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
Expand Down Expand Up @@ -72,6 +73,33 @@ class _FakeWavFileAdapter implements WavFileAdapter {
}

void main() {
test('live-audio v1 format is explicitly PCM16 mono at 48 kHz', () {
expect(PcmStreamFormat.audioModemV1.isAudioModemV1, isTrue);
const incompatible = PcmStreamFormat(
sampleRateHz: 44100,
channels: 2,
sampleFormat: PcmSampleFormat.signedPcm16LittleEndian,
);
expect(incompatible.isAudioModemV1, isFalse);
});

test('unavailable live-audio adapter cannot start a route', () async {
const adapter = UnavailableLiveAudioAdapter('No tested live route.');
expect(adapter.availability.isAvailable, isFalse);
expect(adapter.availability.reason, 'No tested live route.');
await expectLater(
adapter.startPlayback(
pcmFrames: Uint8List(0),
format: PcmStreamFormat.audioModemV1,
),
throwsA(isA<StateError>()),
);
await expectLater(
adapter.startCapture(format: PcmStreamFormat.audioModemV1).first,
throwsA(isA<StateError>()),
);
});

testWidgets('send workbench builds and verifies an in-memory WAV flow', (
tester,
) async {
Expand Down Expand Up @@ -145,4 +173,35 @@ void main() {
expect(find.text('WAV object проверен.'), findsOneWidget);
expect(find.text('Источник: received.wav'), findsOneWidget);
});

testWidgets('live route selection remains unavailable and does not encode', (
tester,
) async {
final bridge = _FakeWavBridge();
await tester.pumpWidget(
AudioModemApp(
bridge: bridge,
fileAdapter: _FakeWavFileAdapter(),
liveAudioAdapter: const UnavailableLiveAudioAdapter(
'No tested live route.',
),
),
);

final speakerRoute = find.text('Динамик');
await tester.ensureVisible(speakerRoute);
await tester.tap(speakerRoute);
await tester.pumpAndSettle();

final buildButton = find.widgetWithText(
FilledButton,
'Собрать и проверить WAV',
);
await tester.ensureVisible(buildButton);
await tester.tap(buildButton);
await tester.pumpAndSettle();

expect(find.text('No tested live route.'), findsOneWidget);
expect(bridge.encodedCarrier, isNull);
});
}
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ This directory holds the human-readable technical documentation for AudioModem.
| 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 |
| Research | [Acoustic-2 measurement sources](research/acoustic-2-measurement-sources.md) | [Источники измерений Acoustic-2](research/acoustic-2-measurement-sources_RU.md) | Controlled PCM transform and timing-acquisition constraints |
| Research | [Device-acceptance sources](research/device-acceptance-sources.md) | [Источники device acceptance](research/device-acceptance-sources_RU.md) | Reproducible evidence, metadata and privacy constraints |
| Research | [Live-audio adapter sources](research/live-audio-adapter-sources.md) | [Источники live-audio adapter](research/live-audio-adapter-sources_RU.md) | Session, permission, focus and lifecycle 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 |
| Specification | [Acoustic-2](../spec/acoustic-2.md) | [Acoustic-2 на русском](../spec/acoustic-2_RU.md) | Experimental controlled PCM measurement contract |
| Specification | [Live-audio adapter v1](../spec/live-audio-adapter-v1.md) | [Live-audio adapter v1 на русском](../spec/live-audio-adapter-v1_RU.md) | Typed unavailable-first contract for future routes |

## Translation convention

Expand Down
2 changes: 2 additions & 0 deletions docs/README_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
| Research | [Acoustic-1 PHY sources](research/acoustic-1-phy-sources.md) | [Источники Acoustic-1 PHY](research/acoustic-1-phy-sources_RU.md) | Источники дизайна и явные receiver/FEC constraints |
| Research | [Acoustic-2 measurement sources](research/acoustic-2-measurement-sources.md) | [Источники измерений Acoustic-2](research/acoustic-2-measurement-sources_RU.md) | Ограничения controlled PCM transforms и timing acquisition |
| Research | [Device-acceptance sources](research/device-acceptance-sources.md) | [Источники device acceptance](research/device-acceptance-sources_RU.md) | Ограничения reproducible evidence, metadata и privacy |
| Research | [Live-audio adapter sources](research/live-audio-adapter-sources.md) | [Источники live-audio adapter](research/live-audio-adapter-sources_RU.md) | Ограничения session, permission, focus и lifecycle |
| 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 |
| Specification | [Acoustic-2](../spec/acoustic-2.md) | [Acoustic-2 на русском](../spec/acoustic-2_RU.md) | Экспериментальный controlled PCM measurement contract |
| Specification | [Live-audio adapter v1](../spec/live-audio-adapter-v1.md) | [Live-audio adapter v1 на русском](../spec/live-audio-adapter-v1_RU.md) | Typed unavailable-first contract для будущих routes |

## Правило поддержки перевода

Expand Down
2 changes: 1 addition & 1 deletion docs/operations/device-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The committed fixture above is intentionally an **unexecuted template**, not a d
| Candidate-route gate | Repeated real runs, declared device/route settings, fixture hashes, failures and a maintainer review are available. | `experimental route candidate`. |
| Supported-route gate | A separate adapter RFC, reproducible target build, route-specific acceptance tests, published compatibility note and maintainer approval all exist. | `supported` for only the documented scope. |

The schema cannot establish the last three gates by itself. It only prevents incomplete evidence from being mistaken for a measurement. The existing [audio-route boundaries](../guides/audio-routes.md) and [platform matrix](../reference/platform-support.md) remain authoritative for current support status.
The [constrained live-audio adapter RFC](../../spec/live-audio-adapter-v1.md) closes only the contract gate with an unavailable implementation. It neither enables a route nor supplies the physical-route evidence, platform implementation or tests required by the later gates. The schema cannot establish the last three gates by itself. It only prevents incomplete evidence from being mistaken for a measurement. The existing [audio-route boundaries](../guides/audio-routes.md) and [platform matrix](../reference/platform-support.md) remain authoritative for current support status.

## References

Expand Down
2 changes: 1 addition & 1 deletion docs/operations/device-acceptance_RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Committed fixture выше намеренно является **unexecuted temp
| Candidate-route gate | Доступны repeated real runs, declared device/route settings, fixture hashes, failures и maintainer review. | `experimental route candidate`. |
| Supported-route gate | Существуют separate adapter RFC, reproducible target build, route-specific acceptance tests, published compatibility note и maintainer approval. | `supported` только для documented scope. |

Schema сама по себе не устанавливает последние три gates. Она лишь не даёт принять incomplete evidence за measurement. Текущие статусы остаются в [границах audio routes](../guides/audio-routes_RU.md) и [platform matrix](../reference/platform-support_RU.md).
[Constrained live-audio adapter RFC](../../spec/live-audio-adapter-v1_RU.md) закрывает только contract gate unavailable implementation'ом. Он не включает route и не даёт physical-route evidence, platform implementation или tests, требуемые поздними gates. Schema сама по себе не устанавливает последние три gates. Она лишь не даёт принять incomplete evidence за measurement. Текущие статусы остаются в [границах audio routes](../guides/audio-routes_RU.md) и [platform matrix](../reference/platform-support_RU.md).

## Ссылки

Expand Down
21 changes: 21 additions & 0 deletions docs/research/live-audio-adapter-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Constrained live-audio adapter research notes

**Last reviewed:** 2026-08-20 · **English (canonical)**

The first live-audio milestone must define a typed application boundary before choosing a runtime plugin or declaring a route. The contract will treat permission, session activation, interruption, playback, capture and disposal as separately observable states. It will not infer a stable sample rate, hardware route, acoustic result or platform support from a plugin dependency.

| Source | Relevant finding | RFC consequence |
| --- | --- | --- |
| Flutter cookbook, *Record or stream audio input* | Audio input requires user permission and may require platform-specific configuration; capture configuration, streaming, stopping and disposal are distinct operations.[1] | Require explicit permission and lifecycle outcomes; do not expose capture bytes until a platform adapter exists and reports a supported PCM format. |
| `audio_session` documentation | iOS has an app-wide shared audio session; Android attributes are applied per player/track. Activation can be denied, and interruption/device events require a policy.[2] | Make session activation, interruption and route-change events explicit; the core contract must not own platform-session policy silently. |
| Android audio-focus guidance | Playback should request focus immediately before use, handle focus loss, and abandon focus when finished; focus behavior differs by Android version and context.[3] | Playback is an opt-in command whose failure does not start output. No automatic resume or ducking policy is promised in v1. |

## RFC posture

The constrained adapter contract will ship only an unavailable implementation. It gives the Flutter workbench a stable dependency seam and a truthful status message while device-acceptance reports, a single-route platform RFC and hardware evidence are still absent. No recording package, audio-session package or microphone permission is added in this milestone.

## References

[1]: https://docs.flutter.dev/cookbook/audio/record "Record or stream audio input"
[2]: https://pub.dev/packages/audio_session "audio_session package documentation"
[3]: https://developer.android.com/media/optimize/audio-focus "Manage audio focus"
Loading
Loading