Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
960f9e7
refactor recorder: extract controls into separate widget
o-bagge Jan 6, 2026
6a293e5
add custom audio waveform to the Sensors tab with device selector
o-bagge Jan 16, 2026
648a72c
fix scrolling problems with charts screen
o-bagge Jan 19, 2026
4f11d0d
auto select OpenEarable as audio source, improve UI of Chart tab, fix…
o-bagge Jan 19, 2026
4800892
add audio file to recording folder. Fixed some bugs: switching tabs d…
o-bagge Jan 23, 2026
cadb81f
fix bug
o-bagge Jan 26, 2026
0095fe6
starting of audio streaming is now manuallycontrolled
o-bagge Jan 26, 2026
f6a8d0b
microphone stream can now be turned on and off together with the sens…
o-bagge Jan 27, 2026
d2c65d0
microphone now also stops when the recording is stopeed with the 'tur…
o-bagge Jan 28, 2026
bd4dd92
disable streaming option for iOS, remove audio recorder app because i…
o-bagge Feb 2, 2026
29b6c6e
add missing trailing commas
o-bagge Feb 2, 2026
d424fc5
only use one instance of AudioRecorder for both streaming and recording
o-bagge Feb 4, 2026
65494b3
resolve merge conflict
o-bagge Mar 31, 2026
eac29ae
fix(audio): keep waveform repainting
DennisMoschina May 5, 2026
3daa7c3
chore(analyzer): clean sensor configuration warnings
DennisMoschina May 5, 2026
69a9edb
fix recorder on web
o-bagge May 13, 2026
164c9a4
fix recorder on macos
o-bagge Apr 21, 2026
53d57db
fix recorder in macos
o-bagge May 13, 2026
e281f41
remove export
o-bagge May 13, 2026
6f305ed
fix(recorder): cancel web sensor subscriptions
DennisMoschina May 20, 2026
1991efc
fix(web): replace deprecated html APIs
DennisMoschina May 20, 2026
e7cf4b3
style(recorder): format local recorder helpers
DennisMoschina May 20, 2026
68003fd
build(macos): refresh pod lockfile
DennisMoschina May 20, 2026
f988881
fix(sensors): remove duplicate stereo badge
TobiasRoeddiger May 21, 2026
519052c
fix(sensors): update system microphone level chart
TobiasRoeddiger May 21, 2026
43d34e1
refactor(sensor_values_page): extracted system microphone chart into …
DennisMoschina May 26, 2026
ad02706
feat(audio): add selectable microphone recording source
DennisMoschina May 21, 2026
fa31cd4
chore(microphone_configuration_card): removed unused code
DennisMoschina May 26, 2026
d80c06d
fix(audio): harden recorder monitoring lifecycle
DennisMoschina May 26, 2026
d69cb04
feat(audio): support cross-platform microphone recording
DennisMoschina May 26, 2026
c16ae2d
build(ios): set minimum deployment target to 13.1
DennisMoschina May 26, 2026
a05f87e
feat(audio): implement platform availability check for local micropho…
DennisMoschina May 27, 2026
27ccd2d
feat(app_upgrade): add highlights for version 1.3.0 with audio record…
DennisMoschina May 27, 2026
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 open_wearable/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
<!-- internet persmission, for fetching firmware update -->
<uses-permission android:name="android.permission.INTERNET"/>

<uses-permission android:name="android.permission.RECORD_AUDIO" />

<!-- Provide required visibility configuration for API level 30 and above -->
<queries>
<!-- If your app checks for SMS support -->
Expand Down
2 changes: 1 addition & 1 deletion open_wearable/ios/Flutter/AppFrameworkInfo.plist
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
<string>13.1</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
Expand Down
2 changes: 1 addition & 1 deletion open_wearable/ios/Podfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '16.0'
platform :ios, '13.1'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
Expand Down
6 changes: 6 additions & 0 deletions open_wearable/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ PODS:
- flutter_archive (0.0.1):
- Flutter
- ZIPFoundation (= 0.9.19)
- flutter_headset_detector (3.1.0):
- Flutter
- iOSMcuManagerLibrary (1.10.1):
- SwiftCBOR (= 0.4.7)
- ZIPFoundation (= 0.9.19)
Expand Down Expand Up @@ -86,6 +88,7 @@ DEPENDENCIES:
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
- flutter_archive (from `.symlinks/plugins/flutter_archive/ios`)
- flutter_headset_detector (from `.symlinks/plugins/flutter_headset_detector/ios`)
- mcumgr_flutter (from `.symlinks/plugins/mcumgr_flutter/ios`)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
Expand Down Expand Up @@ -121,6 +124,8 @@ EXTERNAL SOURCES:
:path: Flutter
flutter_archive:
:path: ".symlinks/plugins/flutter_archive/ios"
flutter_headset_detector:
:path: ".symlinks/plugins/flutter_headset_detector/ios"
mcumgr_flutter:
:path: ".symlinks/plugins/mcumgr_flutter/ios"
open_file_ios:
Expand Down Expand Up @@ -151,6 +156,7 @@ SPEC CHECKSUMS:
file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_archive: ad8edfd7f7d1bb12058d05424ba93e27d9930efe
flutter_headset_detector: 37d2407c6c59aa6e8a9daecf732854862ff6dd4a
iOSMcuManagerLibrary: e9555825af11a61744fe369c12e1e66621061b58
mcumgr_flutter: 969e99cc15e9fe658242669ce1075bf4612aef8a
open_file_ios: 46184d802ee7959203f6392abcfa0dd49fdb5be0
Expand Down
6 changes: 3 additions & 3 deletions open_wearable/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.1;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down Expand Up @@ -602,7 +602,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.1;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
Expand Down Expand Up @@ -653,7 +653,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.1;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
Expand Down
2 changes: 2 additions & 0 deletions open_wearable/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
<string>This app uses the local network to host a webserver for tools integration.</string>
<key>NSMotionUsageDescription</key>
<string>This app requires access to device motion in order to provide sensor data.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app records microphone audio with local sensor recording sessions.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Needed for optional file selection functionality.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
Expand Down
2 changes: 1 addition & 1 deletion open_wearable/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import 'package:open_wearable/models/wearable_connector.dart'
hide WearableEvent;
import 'package:open_wearable/router.dart';
import 'package:open_wearable/theme/app_theme.dart';
import 'package:open_wearable/view_models/sensor_recorder_provider.dart';
import 'package:open_wearable/view_models/sensor_recorder_provider_facade.dart';
import 'package:open_wearable/widgets/app_banner.dart';
import 'package:open_wearable/widgets/global_app_banner_overlay.dart';
import 'package:open_wearable/widgets/app_toast.dart';
Expand Down
39 changes: 39 additions & 0 deletions open_wearable/lib/models/app_upgrade_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,45 @@ class AppUpgradeRegistry {
),
],
),
AppUpgradeHighlight(
version: '1.3.0',
eyebrow: 'OpenWearables 1.3.0',
title: 'Record audio alongside\nyour sensor data',
summary:
'Capture microphone audio in the local recorder and export synchronized session files.',
heroDescription:
'The local recorder now supports audio capture, so experiments can pair wearable sensor streams '
'with microphone recordings from the app. Select an input, monitor the waveform, and save audio '
'together with the rest of the session.',
accentColor: Color(0xFF8F6A67),
useHeroGradient: false,
features: <AppUpgradeFeatureHighlight>[
AppUpgradeFeatureHighlight(
icon: Icons.mic_rounded,
title: 'Audio recorder',
description:
'Record microphone audio directly from the app as part of a local recording session.',
),
AppUpgradeFeatureHighlight(
icon: Icons.graphic_eq_rounded,
title: 'Live waveform monitoring',
description:
'Preview incoming audio levels before and during capture to verify the selected input.',
),
AppUpgradeFeatureHighlight(
icon: Icons.input_rounded,
title: 'Selectable input source',
description:
'Choose from available microphone inputs and apply the selected source before recording.',
),
AppUpgradeFeatureHighlight(
icon: Icons.folder_zip_rounded,
title: 'Session exports',
description:
'Audio files are saved with the same recording session as sensor data for easier review and sharing.',
),
],
),
];

/// Returns the configured highlight for [version], if any.
Expand Down
15 changes: 15 additions & 0 deletions open_wearable/lib/models/audio_input_availability.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import 'package:flutter/foundation.dart';

/// Platform availability for app-local microphone capture.
///
/// macOS is disabled because the current recorder plugin can hang while
/// stopping file-backed microphone sessions on that platform.
class AudioInputAvailability {
/// Whether local microphone selection, monitoring, and recording are enabled.
static bool get isSupported {
if (kIsWeb) {
return true;
}
return defaultTargetPlatform != TargetPlatform.macOS;
}
}
84 changes: 84 additions & 0 deletions open_wearable/lib/models/audio_input_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/// Describes an audio input that can be used for local audio recordings.
///
/// The app owns this model instead of exposing the recorder plugin's device
/// type through the UI. That keeps persisted selections and widgets isolated
/// from platform plugin implementation details.
class AudioInputSource {
/// Stable identifier used by the platform recorder to select this source.
final String id;

/// Human-readable label surfaced by the platform.
final String label;

/// Coarse category used for icons and explanatory UI.
final AudioInputSourceKind kind;

/// Whether this option delegates source selection to the operating system.
final bool isSystemDefault;

const AudioInputSource({
required this.id,
required this.label,
required this.kind,
this.isSystemDefault = false,
});

/// The synthetic source that lets the OS pick its current default input.
static const systemDefault = AudioInputSource(
id: '__system_default_audio_input__',
label: 'System Default',
kind: AudioInputSourceKind.systemDefault,
isSystemDefault: true,
);

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is AudioInputSource &&
other.id == id &&
other.label == label &&
other.kind == kind &&
other.isSystemDefault == isSystemDefault;
}

@override
int get hashCode => Object.hash(id, label, kind, isSystemDefault);
}

/// Coarse audio input categories used to keep UI copy and icons consistent.
enum AudioInputSourceKind {
systemDefault,
builtIn,
bluetooth,
wearable,
external,
unknown,
}

/// Classifies a platform microphone label for display purposes.
AudioInputSourceKind classifyAudioInputSourceLabel(String label) {
final normalized = label.toLowerCase();
if (normalized.contains('openearable') ||
normalized.contains('open earable') ||
normalized.contains('wearable')) {
return AudioInputSourceKind.wearable;
}
if (normalized.contains('bluetooth') ||
normalized.contains('ble') ||
normalized.contains('headset') ||
normalized.contains('airpods')) {
return AudioInputSourceKind.bluetooth;
}
if (normalized.contains('built-in') ||
normalized.contains('builtin') ||
RegExp(r'\bphone\b').hasMatch(normalized) ||
normalized.contains('internal')) {
return AudioInputSourceKind.builtIn;
}
if (normalized.contains('usb') ||
normalized.contains('external') ||
normalized.contains('line in')) {
return AudioInputSourceKind.external;
}
return AudioInputSourceKind.unknown;
}
18 changes: 15 additions & 3 deletions open_wearable/lib/models/bluetooth_auto_connector.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:open_earable_flutter/open_earable_flutter.dart' hide logger;
Expand Down Expand Up @@ -60,6 +61,14 @@ class BluetoothAutoConnector {
});

void start() async {
if (kIsWeb) {
logger.i(
'Bluetooth auto-connect is disabled on web because Web Bluetooth requires a direct user gesture.',
);
_stopInternal();
return;
}

final token = ++_sessionToken;
_stopInternal();
_connectedDeviceIds.clear();
Expand Down Expand Up @@ -271,7 +280,7 @@ class BluetoothAutoConnector {
}

Future<void> _applyIosScanCooldownIfNeeded() async {
if (!Platform.isIOS) {
if (kIsWeb || !Platform.isIOS) {
return;
}
final stoppedAt = _lastScanStoppedAt;
Expand All @@ -296,14 +305,17 @@ class BluetoothAutoConnector {
}

_isAttemptingConnection = true;
if (!Platform.isIOS) {

if (!kIsWeb && Platform.isAndroid) {
final hasPerm = await wearableManager.hasPermissions();
if (activeToken != _sessionToken) {
_isAttemptingConnection = false;
return;
}
if (!hasPerm) {
logger.w('Bluetooth permissions not granted. Showing permissions dialog.');
logger.w(
'Bluetooth permissions not granted. Showing permissions dialog.',
);
if (!_askedPermissionsThisSession) {
_askedPermissionsThisSession = true;
_showPermissionsDialog();
Expand Down
Loading
Loading