win32audio 1.5.0
win32audio: ^1.5.0 copied to clipboard
A Windows plugin that fetches audio devices, sets default device, sets Master Volume and specific app volume and as an extra, extracts icon from exe/HWND/HICON
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:win32audio/win32audio.dart';
import 'widgets/animated_progress_bar.dart';
import 'widgets/win_icon_widget.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Audio.setupChangeListener();
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
AudioDevice defaultDevice = AudioDevice();
List<AudioDevice> audioDevices = <AudioDevice>[];
AudioDeviceType audioDeviceType = AudioDeviceType.output;
final WinIcons winIcons = WinIcons();
List<ProcessVolume> mixerList = <ProcessVolume>[];
bool _stateFetchAudioMixerPeak = true;
double __volume = 0.0;
String fetchStatus = "";
List<String> eventLog = <String>[];
@override
void initState() {
super.initState();
Audio.addChangeListener((String type, String id) async {
if (mounted) {
setState(() {
eventLog.insert(0, "$type: $id");
if (eventLog.length > 50) eventLog.removeLast();
});
}
});
fetchAudioDevices();
Timer.periodic(const Duration(milliseconds: 150), (Timer timer) async {
if (_stateFetchAudioMixerPeak) {
mixerList = await Audio.enumAudioMixer() ?? <ProcessVolume>[];
if (mounted) setState(() {});
}
});
}
@override
void dispose() {
super.dispose();
}
Future<void> fetchAudioDevices() async {
if (!mounted) return;
audioDevices = await Audio.enumDevices(audioDeviceType) ?? <AudioDevice>[];
__volume = await Audio.getVolume(audioDeviceType);
defaultDevice = (await Audio.getDefaultDevice(audioDeviceType))!;
mixerList = await Audio.enumAudioMixer() ?? <ProcessVolume>[];
if (mounted) {
setState(() {
fetchStatus = "Get";
});
}
}
Future<void> _showSampleRateDialog(BuildContext context, AudioDevice device) async {
// Get current sample rate
int currentRate = await Audio.getSampleRate(device.id);
if (!context.mounted) return;
final int? selectedRate = await showDialog<int>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
int? tempSelectedRate = currentRate;
return AlertDialog(
title: Text('Sample Rate - ${device.name}'),
content: RadioGroup<int>(
groupValue: tempSelectedRate,
onChanged: (int? value) {
if (value != null) {
setState(() => tempSelectedRate = value);
Navigator.of(context).pop(value);
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Current: ${currentRate > 0 ? "$currentRate Hz" : "Unknown"}'),
const SizedBox(height: 20),
const Text('Select new sample rate:'),
const SizedBox(height: 10),
...<int>[44100, 48000, 96000, 192000].map((int rate) {
return ListTile(title: Text('$rate Hz'), leading: Radio<int>(value: rate));
}),
],
),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
],
);
},
);
},
);
if (selectedRate != null && context.mounted) {
bool success = await Audio.setSampleRate(device.id, selectedRate);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
success ? 'Sample rate changed to $selectedRate Hz' : 'Failed to change sample rate. Try running as administrator.',
),
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: double.infinity),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
alignment: Alignment.centerLeft,
width: 100,
child: TextButton(
child: Text(
fetchStatus,
style: const TextStyle(color: Colors.white),
strutStyle: const StrutStyle(forceStrutHeight: true),
),
onPressed: () async {
setState(() {
fetchStatus = 'Getting...';
});
fetchAudioDevices();
},
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Container(
alignment: Alignment.centerRight,
child: TextButton(
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 5),
child: Align(
alignment: Alignment.topCenter,
heightFactor: 1.15,
child: Text(
defaultDevice.name,
textAlign: TextAlign.justify,
style: const TextStyle(color: Colors.black),
strutStyle: const StrutStyle(forceStrutHeight: true),
),
),
),
const Icon(Icons.swap_vertical_circle_outlined, color: Colors.black),
],
),
onPressed: () async {
await Audio.switchDefaultDevice(audioDeviceType);
fetchAudioDevices();
setState(() {});
}),
),
],
),
],
),
),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
//? SELECT AUDIO DEVICE
//? DROPDOWN
Column(children: <Widget>[
DropdownButton<AudioDeviceType>(
value: audioDeviceType,
items: AudioDeviceType.values.map((AudioDeviceType e) {
return DropdownMenuItem<AudioDeviceType>(
value: e,
child: Text(e.toString()),
);
}).toList(),
onChanged: (Object? e) async {
audioDeviceType = e as AudioDeviceType;
fetchStatus = e.toString();
fetchAudioDevices();
setState(() {});
}),
]),
//? VOLUME INFO
Flexible(
//VOLUME
flex: 1,
fit: FlexFit.tight,
child: Center(
child: Text(
"Volume:${(__volume * 100).toStringAsFixed(0)}%",
style: const TextStyle(fontSize: 32),
),
),
),
//? VOLUMESCROLLER
Flexible(
//SLIDER
flex: 1,
fit: FlexFit.tight,
child: Slider(
value: __volume,
min: 0,
max: 1,
divisions: 25,
onChanged: (double e) async {
await Audio.setVolume(e.toDouble(), audioDeviceType);
__volume = e;
setState(() {});
},
),
),
//? LIST AUDIO DEVICES
Flexible(
flex: 2,
fit: FlexFit.loose,
child: ListView.builder(
itemCount: audioDevices.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: WinIconWidget(
path: audioDevices[index].iconPath,
iconID: audioDevices[index].iconID,
),
title: Text(audioDevices[index].name),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: const Icon(Icons.graphic_eq),
tooltip: 'Sample Rate',
onPressed: () async {
await _showSampleRateDialog(context, audioDevices[index]);
},
),
IconButton(
icon: Icon((audioDevices[index].isActive == true ? Icons.check_box_outlined : Icons.check_box_outline_blank)),
onPressed: () async {
await Audio.setDefaultDevice(audioDevices[index].id);
fetchAudioDevices();
setState(() {});
},
),
],
),
);
})),
const Divider(
thickness: 5,
height: 10,
color: Color.fromARGB(12, 0, 0, 0),
),
//? AUDIO MIXER
SizedBox(
child: CheckboxListTile(
title: const Text("Countinously Fetch The Audio Mixer"),
value: _stateFetchAudioMixerPeak,
controlAffinity: ListTileControlAffinity.leading,
onChanged: (bool? e) {
setState(() {
_stateFetchAudioMixerPeak = e!;
});
},
),
),
//? LIST AUDIO MIXER
Flexible(
flex: 2,
child: ListView.builder(
itemCount: mixerList.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: <Widget>[
const Divider(
height: 6,
thickness: 3,
color: Color.fromARGB(10, 0, 0, 0),
indent: 100,
endIndent: 100,
),
Row(
children: <Widget>[
Flexible(
child: ListTile(
leading: WinIconWidget(
path: mixerList[index].processPath,
iconID: 0, // Usually 0 for exe icons
),
title: Tooltip(
message: mixerList[index].processPath,
child: Text(
mixerList[index].processPath.split('/').last.split('\\').last,
),
))),
Flexible(
child: Padding(
padding: const EdgeInsets.only(right: 20),
child: Column(
children: <Widget>[
Slider(
value: mixerList[index].maxVolume,
min: 0,
max: 1,
divisions: 25,
onChanged: (double e) async {
await Audio.setAudioMixerVolume(mixerList[index].processId, e.toDouble());
mixerList[index].maxVolume = e;
setState(() {});
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: FAProgressBar(
currentValue: mixerList[index].peakVolume * mixerList[index].maxVolume * 100,
size: 12,
maxValue: 100,
changeColorValue: 100,
changeProgressColor: Colors.pink,
progressColor: Colors.lightBlue,
animatedDuration: const Duration(milliseconds: 300),
direction: Axis.horizontal,
verticalDirection: VerticalDirection.up,
formatValueFixed: 2,
),
),
],
),
),
),
],
),
],
);
}),
),
const Divider(
thickness: 5,
height: 10,
color: Color.fromARGB(12, 0, 0, 0),
),
const Center(child: Text("Event Log")),
//? EVENT LOG
Flexible(
flex: 2,
child: Container(
color: Colors.grey[200],
child: ListView.builder(
itemCount: eventLog.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
child: Text(
eventLog[index],
style: const TextStyle(fontSize: 10),
),
);
}),
),
)
],
),
),
);
}
}