Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,68 @@
package com.resonate.resonate

import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
class MainActivity : FlutterActivity() {

private val channelName = "voice_control_channel"

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)

MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelName
).setMethodCallHandler { call, result ->
when (call.method) {
"setVoiceProfile" -> {
val selectedVoice = call.argument<String>("selectedVoice")
if (selectedVoice != null) {
applyVoiceProfile(selectedVoice)
result.success(null)
} else {
result.error(
"INVALID_ARGUMENT",
"selectedVoice must not be null",
null
)
}
}
"setPreviewEnabled" -> {
val isPreviewEnabled = call.argument<Boolean>("isPreviewEnabled")
if (isPreviewEnabled != null) {
setVoicePreviewEnabled(isPreviewEnabled)
result.success(null)
} else {
result.error(
"INVALID_ARGUMENT",
"isPreviewEnabled must not be null",
null
)
}
}
else -> result.notImplemented()
}
}
}

/**
* Apply the selected voice profile in the native audio processing layer.
* Replace the log statement with real DSP / audio-engine calls.
*/
private fun applyVoiceProfile(profile: String) {
// TODO: forward to native audio engine
Log.d("VoiceControl", "Voice profile set: $profile")
}

/**
* Start or stop audio preview in the native audio processing layer.
* Replace the log statement with real DSP / audio-engine calls.
*/
private fun setVoicePreviewEnabled(enabled: Boolean) {
// TODO: forward to native audio engine
Log.d("VoiceControl", "Preview enabled: $enabled")
}
}
81 changes: 72 additions & 9 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,83 @@ import flutter_local_notifications

@main
@objc class AppDelegate: FlutterAppDelegate {


private let voiceControlChannelName = "voice_control_channel"

override func application(

_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?

) -> Bool {
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
GeneratedPluginRegistrant.register(with: registry)
}
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { registry in
GeneratedPluginRegistrant.register(with: registry)
}
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self)

// ── Voice control channel ────────────────────────────────────────────────
guard let controller = window?.rootViewController as? FlutterViewController else {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

let voiceChannel = FlutterMethodChannel(
name: voiceControlChannelName,
binaryMessenger: controller.binaryMessenger
)

voiceChannel.setMethodCallHandler { [weak self] call, result in
switch call.method {
case "setVoiceProfile":
guard
let args = call.arguments as? [String: Any],
let selectedVoice = args["selectedVoice"] as? String
else {
result(FlutterError(
code: "INVALID_ARGUMENT",
message: "selectedVoice must not be nil",
details: nil
))
return
}
self?.applyVoiceProfile(selectedVoice)
result(nil)
Comment thread
Arunodoy18 marked this conversation as resolved.

case "setPreviewEnabled":
guard
let args = call.arguments as? [String: Any],
let isPreviewEnabled = args["isPreviewEnabled"] as? Bool
else {
result(FlutterError(
code: "INVALID_ARGUMENT",
message: "isPreviewEnabled must not be nil",
details: nil
))
return
}
self?.setVoicePreviewEnabled(isPreviewEnabled)
result(nil)

default:
result(FlutterMethodNotImplemented)
}
}
// ────────────────────────────────────────────────────────────────────────

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

/// Apply the selected voice profile in the native audio processing layer.
/// Replace the print with real AVAudioEngine / DSP calls.
private func applyVoiceProfile(_ profile: String) {
// TODO: forward to native audio engine
print("VoiceControl: voice profile set: \(profile)")
}

/// Start or stop audio preview in the native audio processing layer.
/// Replace the print with real AVAudioEngine / DSP calls.
private func setVoicePreviewEnabled(_ enabled: Bool) {
// TODO: forward to native audio engine
print("VoiceControl: preview enabled: \(enabled)")
}
}
50 changes: 50 additions & 0 deletions lib/controllers/voice_profile_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:get/get.dart';
import '../services/voice_control_service.dart';

class VoiceProfileController extends GetxController {
/// Available predefined voice profiles.
final List<String> voiceProfiles = const [
'Default',
'Deep',
'Soft',
'Energetic',
'Calm',
];

/// Currently selected voice profile.
var selectedVoice = 'Default'.obs;

/// Whether preview playback is active.
var isPreviewEnabled = false.obs;

@override
void onInit() {
super.onInit();
// Notify native layer of the initial defaults.
_sendVoiceProfile();
_sendPreviewState();
}
Comment thread
Arunodoy18 marked this conversation as resolved.

/// Called when the user picks a different voice profile.
Future<void> onVoiceProfileChanged(String? voice) async {
if (voice == null || voice == selectedVoice.value) return;
selectedVoice.value = voice;
await _sendVoiceProfile();
}

/// Called when the user toggles the preview switch.
Future<void> onPreviewToggled(bool value) async {
isPreviewEnabled.value = value;
await _sendPreviewState();
}
Comment thread
Arunodoy18 marked this conversation as resolved.

// ─── private helpers ───────────────────────────────────────────────────────

Future<void> _sendVoiceProfile() async {
await VoiceControlService.setVoiceProfile(selectedVoice.value);
}

Future<void> _sendPreviewState() async {
await VoiceControlService.setPreviewEnabled(isPreviewEnabled.value);
}
}
5 changes: 5 additions & 0 deletions lib/routes/app_pages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import 'package:resonate/views/screens/welcome_screen.dart';
import '../bindings/tabview_binding.dart';
import '../views/screens/about_app_screen.dart';
import '../views/screens/contribute_screen.dart';
import '../views/screens/voice_profile_screen.dart';

class AppPages {
static final List<GetPage> pages = [
Expand Down Expand Up @@ -149,5 +150,9 @@ class AppPages {
name: AppRoutes.appPreferencesScreen,
page: () => const AppPreferencesScreen(),
),
GetPage(
name: AppRoutes.voiceProfileScreen,
page: () => VoiceProfileScreen(),
),
];
}
1 change: 1 addition & 0 deletions lib/routes/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ class AppRoutes {
static const userBlockedScreen = "/userBlockedScreen";
static const liveChapterScreen = "/liveChapterScreen";
static const appPreferencesScreen = "/appPreferencesScreen";
static const voiceProfileScreen = "/voiceProfileScreen";
}
24 changes: 24 additions & 0 deletions lib/services/voice_control_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/services.dart';

/// Lightweight signal-only bridge to native audio processing.
/// Flutter sends voice profile selection and preview toggle states;
/// all audio work is performed on the native side.
class VoiceControlService {
static const _channel = MethodChannel('voice_control_channel');

/// Sends the selected voice profile name to native code.
/// [selectedVoice] – identifier of the chosen voice profile.
static Future<void> setVoiceProfile(String selectedVoice) async {
await _channel.invokeMethod('setVoiceProfile', {
'selectedVoice': selectedVoice,
});
}

/// Notifies native code to enable or disable preview playback.
/// [isPreviewEnabled] – whether preview mode is active.
static Future<void> setPreviewEnabled(bool isPreviewEnabled) async {
await _channel.invokeMethod('setPreviewEnabled', {
'isPreviewEnabled': isPreviewEnabled,
});
}
Comment thread
Arunodoy18 marked this conversation as resolved.
}
124 changes: 124 additions & 0 deletions lib/views/screens/voice_profile_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:resonate/utils/ui_sizes.dart';
import '../../controllers/voice_profile_controller.dart';

class VoiceProfileScreen extends StatelessWidget {
VoiceProfileScreen({super.key});

final VoiceProfileController controller =
Get.put(VoiceProfileController());

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;

return Scaffold(
appBar: AppBar(
title: const Text('Voice Profile'),
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
elevation: 0,
),
backgroundColor: colorScheme.surface,
body: SafeArea(
child: ListView(
padding: EdgeInsets.symmetric(
horizontal: UiSizes.width_20,
vertical: UiSizes.height_20,
),
children: [
// ── Section: Voice Profile Selection ────────────────────────────
Text(
'Select Voice Profile',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: UiSizes.height_12),
_VoiceProfileDropdown(controller: controller),
SizedBox(height: UiSizes.height_30),

// ── Section: Preview Toggle ──────────────────────────────────────
_PreviewToggleTile(controller: controller),
],
),
),
);
}
}

// ─── Voice Profile Dropdown ───────────────────────────────────────────────────

class _VoiceProfileDropdown extends StatelessWidget {
const _VoiceProfileDropdown({required this.controller});

final VoiceProfileController controller;

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;

return Obx(
() => DropdownButtonFormField<String>(
value: controller.selectedVoice.value,
decoration: InputDecoration(
labelText: 'Voice Profile',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
prefixIcon: Icon(
Icons.record_voice_over_outlined,
color: colorScheme.primary,
),
),
items: controller.voiceProfiles
.map(
(profile) => DropdownMenuItem<String>(
value: profile,
child: Text(profile),
),
)
.toList(),
onChanged: controller.onVoiceProfileChanged,
),
);
}
}

// ─── Preview Toggle Tile ──────────────────────────────────────────────────────

class _PreviewToggleTile extends StatelessWidget {
const _PreviewToggleTile({required this.controller});

final VoiceProfileController controller;

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;

return Container(
decoration: BoxDecoration(
border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(12),
),
child: Obx(
() => SwitchListTile(
title: const Text('Preview Mode'),
subtitle: Text(
controller.isPreviewEnabled.value ? 'On' : 'Off',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
secondary: Icon(
Icons.headphones_outlined,
color: colorScheme.primary,
),
value: controller.isPreviewEnabled.value,
onChanged: controller.onPreviewToggled,
activeColor: colorScheme.primary,
),
),
);
}
}