├── example ├── windows │ ├── runner │ │ ├── resources │ │ │ └── app_icon.ico │ │ ├── resource.h │ │ ├── utils.h │ │ ├── runner.exe.manifest │ │ ├── flutter_window.h │ │ ├── main.cpp │ │ ├── CMakeLists.txt │ │ ├── utils.cpp │ │ ├── flutter_window.cpp │ │ ├── Runner.rc │ │ ├── win32_window.h │ │ └── win32_window.cpp │ ├── .gitignore │ ├── flutter │ │ ├── generated_plugin_registrant.h │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugins.cmake │ │ └── CMakeLists.txt │ └── CMakeLists.txt ├── README.md ├── test │ └── widget_test.dart ├── .gitignore ├── analysis_options.yaml ├── lib │ ├── widgets │ │ ├── win_icon_widget.dart │ │ └── animated_progress_bar.dart │ └── main.dart ├── pubspec.yaml └── pubspec.lock ├── analysis_options.yaml ├── windows ├── icon_manager.h ├── .gitignore ├── include │ ├── win32audio │ │ └── win32audio_plugin_c_api.h │ ├── encoding.h │ └── Policyconfig.h ├── common.h ├── audio_manager.h ├── CMakeLists.txt ├── icon_manager.cpp ├── audio_manager.cpp └── win32audio_plugin_c_api.cpp ├── .gitignore ├── .metadata ├── LICENSE ├── CHANGELOG.md ├── pubspec.yaml ├── lib └── win32audio.dart └── README.md /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Far-Se/win32audio/HEAD/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | linter: 6 | rules: 7 | always_specify_types: true -------------------------------------------------------------------------------- /windows/icon_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.h" 4 | 5 | namespace win32audio { 6 | 7 | class IconManager { 8 | public: 9 | static HICON GetIconFromFile(std::wstring file, int index = 0); 10 | static bool GetIconData(HICON hIcon, int nColorBits, std::vector& buff); 11 | }; 12 | 13 | } // namespace win32audio 14 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | 11 | void RegisterPlugins(flutter::PluginRegistry* registry) { 12 | Win32audioPluginCApiRegisterWithRegistrar( 13 | registry->GetRegistrarForPlugin("Win32audioPluginCApi")); 14 | } 15 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # win32audio_example 2 | 3 | Demonstrates how to use the win32audio plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /windows/include/win32audio/win32audio_plugin_c_api.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_PLUGIN_WIN32AUDIO_PLUGIN_C_API_H_ 2 | #define FLUTTER_PLUGIN_WIN32AUDIO_PLUGIN_C_API_H_ 3 | 4 | #include 5 | 6 | #ifdef FLUTTER_PLUGIN_IMPL 7 | #define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) 8 | #else 9 | #define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) 10 | #endif 11 | 12 | #if defined(__cplusplus) 13 | extern "C" { 14 | #endif 15 | 16 | FLUTTER_PLUGIN_EXPORT void Win32audioPluginCApiRegisterWithRegistrar( 17 | FlutterDesktopPluginRegistrarRef registrar); 18 | 19 | #if defined(__cplusplus) 20 | } // extern "C" 21 | #endif 22 | 23 | #endif // FLUTTER_PLUGIN_WIN32AUDIO_PLUGIN_C_API_H_ 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | doc/ 28 | doc/api/ 29 | .dart_tool/ 30 | .packages 31 | build/ 32 | 33 | .history/ 34 | .vscode/ 35 | .undo-redo-ned.json 36 | .ned-agent-history.json 37 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | win32audio 7 | ) 8 | 9 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 10 | ) 11 | 12 | set(PLUGIN_BUNDLED_LIBRARIES) 13 | 14 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 15 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 16 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 18 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 19 | endforeach(plugin) 20 | 21 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 22 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 24 | endforeach(ffi_plugin) 25 | -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:win32audio_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data!.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 676cefaaff197f27424942307668886253e1ec35 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 676cefaaff197f27424942307668886253e1ec35 17 | base_revision: 676cefaaff197f27424942307668886253e1ec35 18 | - platform: windows 19 | create_revision: 676cefaaff197f27424942307668886253e1ec35 20 | base_revision: 676cefaaff197f27424942307668886253e1ec35 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | 49 | 50 | .dart_tool/ 51 | .packages 52 | build/ 53 | .history/ 54 | .vscode/ 55 | 56 | .pubspec.lock 57 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Far Se 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | 25 | rules: 26 | always_specify_types: true 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /windows/common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | // For ComPtr 18 | #include 19 | 20 | using Microsoft::WRL::ComPtr; 21 | 22 | namespace win32audio { 23 | 24 | // Clean up allocated strings automatically 25 | struct CoTaskMemFreeDeleter { 26 | void operator()(void *p) const { CoTaskMemFree(p); } 27 | }; 28 | using UniqueCoTaskMemString = std::unique_ptr; 29 | 30 | class ScopedCoInitialize { 31 | public: 32 | ScopedCoInitialize() { 33 | hr_ = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 34 | } 35 | ~ScopedCoInitialize() { 36 | if (SUCCEEDED(hr_)) { 37 | CoUninitialize(); 38 | } 39 | } 40 | HRESULT result() const { return hr_; } 41 | bool succeeded() const { return SUCCEEDED(hr_) || hr_ == RPC_E_CHANGED_MODE; } 42 | 43 | private: 44 | HRESULT hr_; 45 | }; 46 | 47 | struct ProcessVolume { 48 | int processId = 0; 49 | std::string processPath = ""; 50 | float maxVolume = 1.0f; 51 | float peakVolume = 0.0f; 52 | }; 53 | 54 | struct DeviceProps { 55 | std::wstring id; 56 | std::wstring name; 57 | std::wstring iconInfo; 58 | bool isActive = false; 59 | }; 60 | 61 | } // namespace win32audio 62 | -------------------------------------------------------------------------------- /windows/audio_manager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "common.h" 4 | 5 | namespace win32audio { 6 | 7 | class AudioManager { 8 | public: 9 | static std::vector EnumAudioDevices(EDataFlow deviceType, ERole eRole); 10 | static DeviceProps GetDefaultDevice(EDataFlow deviceType, ERole eRole); 11 | static HRESULT SetDefaultDevice(LPWSTR devID, bool console, bool multimedia, bool communications); 12 | static float GetVolume(EDataFlow deviceType, ERole eRole); 13 | static bool SetVolume(float volumeLevel, EDataFlow deviceType, ERole eRole); 14 | static bool SetAudioDeviceVolume(float volumeLevel, LPWSTR devID); 15 | static int SwitchDefaultDevice(EDataFlow deviceType, ERole role, bool console, bool multimedia, bool communications); 16 | 17 | // Session API 18 | static std::vector GetProcessVolumes(ERole eRole, int pID = 0, float volume = 0.0); 19 | static bool SetProcessVolumeByPath(ERole eRole, std::string path, float volume); 20 | 21 | // Sample Rate API 22 | static int GetSampleRate(LPWSTR devID); 23 | static bool SetSampleRate(LPWSTR devID, int sampleRate); 24 | 25 | private: 26 | static ComPtr GetAudioSessionEnumerator(ERole eRole); 27 | static float GetSetProcessMasterVolume(IAudioSessionControl *session, float volume = 0.0); 28 | static float GetPeakVolume(IAudioSessionControl *session); 29 | static std::string GetProcessNameFromPid(DWORD pid); 30 | static HRESULT GetDeviceProperty(IMMDevice *pDevice, DeviceProps *pOutput); 31 | }; 32 | 33 | } // namespace win32audio 34 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) 10 | { 11 | // Attach to console when present (e.g., 'flutter run') or create a 12 | // new console when running with a debugger. 13 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) 14 | { 15 | CreateAndAttachConsole(); 16 | } 17 | 18 | // Initialize COM, so that it is available for use in the library and/or 19 | // plugins. 20 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 21 | 22 | flutter::DartProject project(L"data"); 23 | 24 | std::vector command_line_arguments = 25 | GetCommandLineArguments(); 26 | 27 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 28 | 29 | FlutterWindow window(project); 30 | Win32Window::Size size(650, 800); 31 | Win32Window::Point origin(GetSystemMetrics(SM_CXSCREEN) / 2 - size.width / 2, GetSystemMetrics(SM_CYSCREEN) / 2 - size.height / 2); 32 | if (!window.CreateAndShow(L"win32audio_example", origin, size)) 33 | { 34 | return EXIT_FAILURE; 35 | } 36 | window.SetQuitOnClose(true); 37 | 38 | ::MSG msg; 39 | while (::GetMessage(&msg, nullptr, 0, 0)) 40 | { 41 | ::TranslateMessage(&msg); 42 | ::DispatchMessage(&msg); 43 | } 44 | 45 | ::CoUninitialize(); 46 | return EXIT_SUCCESS; 47 | } 48 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.5.0 2 | * Added setAudioMixerVolumeByPath and setAudioDeviceVolume functions by @blitzdose 3 | * Fixed various memory leaks detected by AI. 4 | * Added setSampleRate and getSampleRate. 5 | 6 | ## 1.4.0 7 | * Fixed some crash bugs 8 | * Now you can listen to audio device changes with : 9 | * `Audio.setupChangeListener` To setup, on main() 10 | * `Audio.addChangeListener` add it to initState(); or whereever you want 11 | * `Audio.removeChangeListener` to remove a listener. 12 | * All callback functions: 13 | `OnDeviceStateChanged` 14 | `OnDeviceAdded` 15 | `OnDeviceRemoved` 16 | `OnDefaultDeviceChanged` 17 | `OnPropertyValueChanged` 18 | 19 | ## 1.3.1 20 | * Fixed an issue with missing icon id error 21 | 22 | ## 1.3.0 23 | 24 | * Added `AudioRole` class, with ` console, multimedia, communications,` as parameters 25 | * This parameters is ignored on functions that have audioRole and also separate roles as paramters, such as `setDefaultDevice` and `switchDefaultDevice`. 26 | 27 | ## 1.2.0 28 | 29 | * Added new parameters for setDefaultDevice and switchDefaultDevice: 30 | * `{bool console = false, bool multimedia = true, bool communications = false}` 31 | 32 | ## 1.1.1 33 | 34 | * Tiny tweak at how Method sends data. 35 | 36 | ## 1.1.0 37 | 38 | * Changed icon extraction process because old one crashed on monitors with higher DPI 39 | * Added a new class `WinIcons` with 3 functions `extractExecutableIcon`, `extractWindowIcon`, `extractIconHandle` 40 | 41 | ## 1.0.2 42 | 43 | * Documentation still missing from pub.dev -.- 44 | 45 | ## 1.0.1 46 | 47 | * Fixed version to a more serios version 48 | * Generated Documentation 49 | 50 | ## 0.0.1 51 | 52 | * Initial Release with the main functionality of the package. 53 | 54 | -------------------------------------------------------------------------------- /example/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 37 | 38 | # Run the Flutter tool portions of the build. This must not be removed. 39 | add_dependencies(${BINARY_NAME} flutter_assemble) 40 | -------------------------------------------------------------------------------- /example/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr); 51 | std::string utf8_string; 52 | if (target_length == 0 || target_length > utf8_string.max_size()) { 53 | return utf8_string; 54 | } 55 | utf8_string.resize(target_length); 56 | int converted_length = ::WideCharToMultiByte( 57 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 58 | -1, utf8_string.data(), 59 | target_length, nullptr, nullptr); 60 | if (converted_length == 0) { 61 | return std::string(); 62 | } 63 | return utf8_string; 64 | } 65 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | return true; 30 | } 31 | 32 | void FlutterWindow::OnDestroy() { 33 | if (flutter_controller_) { 34 | flutter_controller_ = nullptr; 35 | } 36 | 37 | Win32Window::OnDestroy(); 38 | } 39 | 40 | LRESULT 41 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 42 | WPARAM const wparam, 43 | LPARAM const lparam) noexcept { 44 | // Give Flutter, including plugins, an opportunity to handle window messages. 45 | if (flutter_controller_) { 46 | std::optional result = 47 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 48 | lparam); 49 | if (result) { 50 | return *result; 51 | } 52 | } 53 | 54 | switch (message) { 55 | case WM_FONTCHANGE: 56 | flutter_controller_->engine()->ReloadSystemFonts(); 57 | break; 58 | } 59 | 60 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 61 | } 62 | -------------------------------------------------------------------------------- /windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # The Flutter tooling requires that developers have a version of Visual Studio 2 | # installed that includes CMake 3.14 or later. You should not increase this 3 | # version, as doing so will cause the plugin to fail to compile for some 4 | # customers of the plugin. 5 | cmake_minimum_required(VERSION 3.14) 6 | 7 | # Project-level configuration. 8 | set(PROJECT_NAME "win32audio") 9 | project(${PROJECT_NAME} LANGUAGES CXX) 10 | 11 | # This value is used when generating builds using this plugin, so it must 12 | # not be changed 13 | set(PLUGIN_NAME "win32audio_plugin") 14 | 15 | # Define the plugin library target. Its name must not be changed (see comment 16 | # on PLUGIN_NAME above). 17 | # "include/win32audio/win32audio_plugin_c_api.h" 18 | add_library(${PLUGIN_NAME} SHARED 19 | "win32audio_plugin_c_api.cpp" 20 | "audio_manager.cpp" 21 | "icon_manager.cpp" 22 | ) 23 | 24 | # Apply a standard set of build settings that are configured in the 25 | # application-level CMakeLists.txt. This can be removed for plugins that want 26 | # full control over build settings. 27 | apply_standard_settings(${PLUGIN_NAME}) 28 | 29 | # Symbols are hidden by default to reduce the chance of accidental conflicts 30 | # between plugins. This should not be removed; any symbols that should be 31 | # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro. 32 | set_target_properties(${PLUGIN_NAME} PROPERTIES 33 | CXX_VISIBILITY_PRESET hidden) 34 | target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) 35 | 36 | # Source include directories and library dependencies. Add any plugin-specific 37 | # dependencies here. 38 | target_include_directories(${PLUGIN_NAME} INTERFACE 39 | "${CMAKE_CURRENT_SOURCE_DIR}/include") 40 | target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin propsys) 41 | 42 | # List of absolute paths to libraries that should be bundled with the plugin. 43 | # This list could contain prebuilt libraries, or libraries created by an 44 | # external build triggered from this build file. 45 | set(win32audio_bundled_libraries 46 | "" 47 | PARENT_SCOPE 48 | ) 49 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | # dart pub publish 2 | name: win32audio 3 | description: A Windows plugin that fetches audio devices, sets default device, 4 | sets Master Volume and specific app volume and as an extra, extracts icon from 5 | exe/HWND/HICON 6 | version: 1.5.0 7 | homepage: https://github.com/Far-Se/win32audio 8 | 9 | environment: 10 | sdk: '>=2.17.5 <4.0.0' 11 | flutter: ">=2.5.0" 12 | 13 | dependencies: 14 | flutter: 15 | sdk: flutter 16 | plugin_platform_interface: ^2.0.2 17 | 18 | dev_dependencies: 19 | flutter_lints: ^6.0.0 20 | flutter_test: 21 | sdk: flutter 22 | 23 | # For information on the generic Dart part of this file, see the 24 | # following page: https://dart.dev/tools/pub/pubspec 25 | # The following section is specific to Flutter packages. 26 | flutter: 27 | # This section identifies this Flutter project as a plugin project. 28 | # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) 29 | # which should be registered in the plugin registry. This is required for 30 | # using method channels. 31 | # The Android 'package' specifies package in which the registered class is. 32 | # This is required for using method channels on Android. 33 | # The 'ffiPlugin' specifies that native code should be built and bundled. 34 | # This is required for using `dart:ffi`. 35 | # All these are used by the tooling to maintain consistency when 36 | # adding or updating assets for this project. 37 | plugin: 38 | platforms: 39 | windows: 40 | pluginClass: Win32audioPluginCApi 41 | # To add assets to your plugin package, add an assets section, like this: 42 | # assets: 43 | # - images/a_dot_burr.jpeg 44 | # - images/a_dot_ham.jpeg 45 | # 46 | # For details regarding assets in packages, see 47 | # https://flutter.dev/assets-and-images/#from-packages 48 | # 49 | # An image asset can refer to one or more resolution-specific "variants", see 50 | # https://flutter.dev/assets-and-images/#resolution-aware 51 | # To add custom fonts to your plugin package, add a fonts section here, 52 | # in this "flutter" section. Each entry in this list should have a 53 | # "family" key with the font family name, and a "fonts" key with a 54 | # list giving the asset and other descriptors for the font. For 55 | # example: 56 | # fonts: 57 | # - family: Schyler 58 | # fonts: 59 | # - asset: fonts/Schyler-Regular.ttf 60 | # - asset: fonts/Schyler-Italic.ttf 61 | # style: italic 62 | # - family: Trajan Pro 63 | # fonts: 64 | # - asset: fonts/TrajanPro.ttf 65 | # - asset: fonts/TrajanPro_Bold.ttf 66 | # weight: 700 67 | # 68 | # For details regarding fonts in packages, see 69 | # https://flutter.dev/custom-fonts/#from-packages 70 | -------------------------------------------------------------------------------- /windows/include/encoding.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | namespace Encoding { 4 | /** 5 | * @brief Converts a wide string to UTF-8. 6 | * 7 | * @param wstr 8 | * @return std::string 9 | */ 10 | inline std::string WideToUtf8(const std::wstring& wstr) 11 | { 12 | int count = WideCharToMultiByte( 13 | CP_UTF8, 14 | 0, 15 | wstr.c_str(), 16 | static_cast(wstr.length()), 17 | nullptr, 18 | 0, 19 | nullptr, 20 | nullptr); 21 | std::string str(count, 0); 22 | WideCharToMultiByte( 23 | CP_UTF8, 24 | 0, 25 | wstr.c_str(), 26 | -1, 27 | &str[0], 28 | count, 29 | nullptr, 30 | nullptr); 31 | return str; 32 | } 33 | 34 | /** 35 | * @brief Converts an UTF-8 string to a wide string. 36 | * 37 | * @param str 38 | * @return std::wstring 39 | */ 40 | inline std::wstring Utf8ToWide(const std::string& str) 41 | { 42 | int count = MultiByteToWideChar( 43 | CP_UTF8, 44 | 0, 45 | str.c_str(), 46 | static_cast(str.length()), 47 | nullptr, 48 | 0); 49 | std::wstring wstr(count, 0); 50 | MultiByteToWideChar( 51 | CP_UTF8, 52 | 0, 53 | str.c_str(), 54 | static_cast(str.length()), 55 | &wstr[0], 56 | count); 57 | return wstr; 58 | } 59 | 60 | /** 61 | * @brief Converts a wide string to ANSI. 62 | * 63 | * @param wstr 64 | * @return std::string 65 | */ 66 | inline std::string WideToAnsi(const std::wstring& wstr) 67 | { 68 | int count = WideCharToMultiByte( 69 | CP_ACP, 70 | 0, 71 | wstr.c_str(), 72 | static_cast(wstr.length()), 73 | nullptr, 74 | 0, 75 | nullptr, 76 | nullptr); 77 | std::string str(count, 0); 78 | WideCharToMultiByte(CP_ACP, 79 | 0, 80 | wstr.c_str(), 81 | -1, 82 | &str[0], 83 | count, 84 | nullptr, 85 | nullptr); 86 | return str; 87 | } 88 | 89 | /** 90 | * @brief Converts an ANSI string to a wide string. 91 | * 92 | * @param str 93 | * @return std::wstring 94 | */ 95 | inline std::wstring AnsiToWide(const std::string& str) 96 | { 97 | int count = MultiByteToWideChar( 98 | CP_ACP, 99 | 0, 100 | str.c_str(), 101 | static_cast(str.length()), 102 | nullptr, 103 | 0); 104 | std::wstring wstr(count, 0); 105 | MultiByteToWideChar( 106 | CP_ACP, 107 | 0, 108 | str.c_str(), 109 | static_cast(str.length()), 110 | &wstr[0], 111 | count); 112 | return wstr; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /example/lib/widgets/win_icon_widget.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:win32audio/win32audio.dart'; 4 | 5 | /// A simple in-memory cache for Windows icons to prevent redundant native calls. 6 | class IconCache { 7 | static final Map _cache = {}; 8 | static final Map> _pending = >{}; 9 | 10 | /// Retrieves an icon from the cache or fetches it natively if missing. 11 | /// Handles concurrent requests for the same icon by deduping futures. 12 | static Future getIcon(String path, {int iconID = 0}) async { 13 | final String key = "$path:$iconID"; 14 | 15 | // 1. Return synchronously if already cached (fastest) 16 | if (_cache.containsKey(key)) { 17 | return _cache[key]; 18 | } 19 | 20 | // 2. If a fetch is already in progress, join that future 21 | if (_pending.containsKey(key)) { 22 | return _pending[key]; 23 | } 24 | 25 | // 3. Start a new fetch 26 | final Future future = WinIcons().extractFileIcon(path, iconID: iconID); 27 | _pending[key] = future; 28 | 29 | try { 30 | final Uint8List? data = await future; 31 | if (data != null && data.isNotEmpty) { 32 | _cache[key] = data; 33 | } 34 | return data; 35 | } catch (e) { 36 | debugPrint("Error fetching icon for $path: $e"); 37 | return null; 38 | } finally { 39 | _pending.remove(key); 40 | } 41 | } 42 | 43 | static void clear() { 44 | _cache.clear(); 45 | _pending.clear(); 46 | } 47 | } 48 | 49 | class WinIconWidget extends StatefulWidget { 50 | final String path; 51 | final int iconID; 52 | final double width; 53 | final double height; 54 | 55 | const WinIconWidget({ 56 | super.key, 57 | required this.path, 58 | this.iconID = 0, 59 | this.width = 32, 60 | this.height = 32, 61 | }); 62 | 63 | @override 64 | State createState() => _WinIconWidgetState(); 65 | } 66 | 67 | class _WinIconWidgetState extends State { 68 | late Future _iconFuture; 69 | 70 | @override 71 | void initState() { 72 | super.initState(); 73 | _iconFuture = IconCache.getIcon(widget.path, iconID: widget.iconID); 74 | } 75 | 76 | @override 77 | void didUpdateWidget(covariant WinIconWidget oldWidget) { 78 | super.didUpdateWidget(oldWidget); 79 | if (widget.path != oldWidget.path || widget.iconID != oldWidget.iconID) { 80 | _iconFuture = IconCache.getIcon(widget.path, iconID: widget.iconID); 81 | } 82 | } 83 | 84 | @override 85 | void dispose() { 86 | IconCache.clear(); 87 | super.dispose(); 88 | } 89 | 90 | @override 91 | Widget build(BuildContext context) { 92 | return FutureBuilder( 93 | future: _iconFuture, 94 | builder: (BuildContext context, AsyncSnapshot snapshot) { 95 | if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { 96 | return Image.memory( 97 | snapshot.data!, 98 | width: widget.width, 99 | height: widget.height, 100 | gaplessPlayback: true, 101 | filterQuality: FilterQuality.medium, 102 | ); 103 | } 104 | return SizedBox(width: widget.width, height: widget.height, child: const Icon(Icons.spoke_outlined, color: Colors.grey)); 105 | }, 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: win32audio_example 2 | description: Demonstrates how to use the win32audio plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.17.5 <3.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | win32audio: 22 | # When depending on this package from a real application you should use: 23 | # win32audio: ^x.y.z 24 | # See https://dart.dev/tools/pub/dependencies#version-constraints 25 | # The example app is bundled with the plugin so we use a path dependency on 26 | # the parent directory to use the current plugin's version. 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.2 32 | ffi: ^2.1.3 33 | win32: ^5.5.4 34 | 35 | dev_dependencies: 36 | flutter_test: 37 | sdk: flutter 38 | 39 | # The "flutter_lints" package below contains a set of recommended lints to 40 | # encourage good coding practices. The lint set provided by the package is 41 | # activated in the `analysis_options.yaml` file located at the root of your 42 | # package. See that file for information about deactivating specific lint 43 | # rules and activating additional ones. 44 | flutter_lints: ^6.0.0 45 | 46 | # For information on the generic Dart part of this file, see the 47 | # following page: https://dart.dev/tools/pub/pubspec 48 | 49 | # The following section is specific to Flutter packages. 50 | flutter: 51 | 52 | # The following line ensures that the Material Icons font is 53 | # included with your application, so that you can use the icons in 54 | # the material Icons class. 55 | uses-material-design: true 56 | 57 | # To add assets to your application, add an assets section, like this: 58 | # assets: 59 | # - images/a_dot_burr.jpeg 60 | # - images/a_dot_ham.jpeg 61 | 62 | # An image asset can refer to one or more resolution-specific "variants", see 63 | # https://flutter.dev/assets-and-images/#resolution-aware 64 | 65 | # For details regarding adding assets from package dependencies, see 66 | # https://flutter.dev/assets-and-images/#from-packages 67 | 68 | # To add custom fonts to your application, add a fonts section here, 69 | # in this "flutter" section. Each entry in this list should have a 70 | # "family" key with the font family name, and a "fonts" key with a 71 | # list giving the asset and other descriptors for the font. For 72 | # example: 73 | # fonts: 74 | # - family: Schyler 75 | # fonts: 76 | # - asset: fonts/Schyler-Regular.ttf 77 | # - asset: fonts/Schyler-Italic.ttf 78 | # style: italic 79 | # - family: Trajan Pro 80 | # fonts: 81 | # - asset: fonts/TrajanPro.ttf 82 | # - asset: fonts/TrajanPro_Bold.ttf 83 | # weight: 700 84 | # 85 | # For details regarding fonts from package dependencies, 86 | # see https://flutter.dev/custom-fonts/#from-packages 87 | -------------------------------------------------------------------------------- /example/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "win32audio_example" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "win32audio_example" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "win32audio_example.exe" "\0" 98 | VALUE "ProductName", "win32audio_example" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates and shows a win32 window with |title| and position and size using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size to will treat the width height passed in to this function 35 | // as logical pixels and scale to appropriate for the default monitor. Returns 36 | // true if the window was created successfully. 37 | bool CreateAndShow(const std::wstring& title, 38 | const Point& origin, 39 | const Size& size); 40 | 41 | // Release OS resources associated with window. 42 | void Destroy(); 43 | 44 | // Inserts |content| into the window tree. 45 | void SetChildContent(HWND content); 46 | 47 | // Returns the backing Window handle to enable clients to set icon and other 48 | // window properties. Returns nullptr if the window has been destroyed. 49 | HWND GetHandle(); 50 | 51 | // If true, closing this window will quit the application. 52 | void SetQuitOnClose(bool quit_on_close); 53 | 54 | // Return a RECT representing the bounds of the current client area. 55 | RECT GetClientArea(); 56 | 57 | protected: 58 | // Processes and route salient window messages for mouse handling, 59 | // size change and DPI. Delegates handling of these to member overloads that 60 | // inheriting classes can handle. 61 | virtual LRESULT MessageHandler(HWND window, 62 | UINT const message, 63 | WPARAM const wparam, 64 | LPARAM const lparam) noexcept; 65 | 66 | // Called when CreateAndShow is called, allowing subclass window-related 67 | // setup. Subclasses should return false if setup fails. 68 | virtual bool OnCreate(); 69 | 70 | // Called when Destroy is called. 71 | virtual void OnDestroy(); 72 | 73 | private: 74 | friend class WindowClassRegistrar; 75 | 76 | // OS callback called by message pump. Handles the WM_NCCREATE message which 77 | // is passed when the non-client area is being created and enables automatic 78 | // non-client DPI scaling so that the non-client area automatically 79 | // responsponds to changes in DPI. All other messages are handled by 80 | // MessageHandler. 81 | static LRESULT CALLBACK WndProc(HWND const window, 82 | UINT const message, 83 | WPARAM const wparam, 84 | LPARAM const lparam) noexcept; 85 | 86 | // Retrieves a class instance pointer for |window| 87 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 88 | 89 | bool quit_on_close_ = false; 90 | 91 | // window handle for top level window. 92 | HWND window_handle_ = nullptr; 93 | 94 | // window handle for hosted content. 95 | HWND child_content_ = nullptr; 96 | }; 97 | 98 | #endif // RUNNER_WIN32_WINDOW_H_ 99 | -------------------------------------------------------------------------------- /example/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /example/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(win32audio_example LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "win32audio_example") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(SET CMP0063 NEW) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | # Generated plugin build rules, which manage building the plugins and adding 56 | # them to the application. 57 | include(flutter/generated_plugins.cmake) 58 | 59 | 60 | # === Installation === 61 | # Support files are copied into place next to the executable, so that it can 62 | # run in place. This is done instead of making a separate bundle (as on Linux) 63 | # so that building and running from within Visual Studio will work. 64 | set(BUILD_BUNDLE_DIR "$") 65 | # Make the "install" step default, as it's required to run. 66 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 67 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 68 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 69 | endif() 70 | 71 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 72 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 73 | 74 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 75 | COMPONENT Runtime) 76 | 77 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 78 | COMPONENT Runtime) 79 | 80 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 81 | COMPONENT Runtime) 82 | 83 | if(PLUGIN_BUNDLED_LIBRARIES) 84 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 85 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 86 | COMPONENT Runtime) 87 | endif() 88 | 89 | # Fully re-copy the assets directory on each build to avoid having stale files 90 | # from a previous install. 91 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 92 | install(CODE " 93 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 94 | " COMPONENT Runtime) 95 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 96 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 97 | 98 | # Install the AOT library on non-Debug builds only. 99 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 100 | CONFIGURATIONS Profile;Release 101 | COMPONENT Runtime) 102 | -------------------------------------------------------------------------------- /windows/icon_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "icon_manager.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace win32audio { 9 | 10 | // RAII helper for generic GDI objects 11 | struct GdiObjectDeleter { 12 | void operator()(HGDIOBJ obj) const { 13 | if (obj) DeleteObject(obj); 14 | } 15 | }; 16 | using ScopedGdiObject = std::unique_ptr::type, GdiObjectDeleter>; 17 | 18 | // RAII helper for HDC 19 | struct HdcDeleter { 20 | void operator()(HDC hdc) const { 21 | if (hdc) DeleteDC(hdc); 22 | } 23 | }; 24 | using ScopedHDC = std::unique_ptr::type, HdcDeleter>; 25 | 26 | struct ICONDIRENTRY { 27 | UCHAR nWidth; 28 | UCHAR nHeight; 29 | UCHAR nNumColorsInPalette; 30 | UCHAR nReserved; 31 | WORD nNumColorPlanes; 32 | WORD nBitsPerPixel; 33 | ULONG nDataLength; 34 | ULONG nOffset; 35 | }; 36 | 37 | HICON IconManager::GetIconFromFile(std::wstring file, int index) { 38 | HICON hIcon = NULL; 39 | LPCWSTR filePath = file.c_str(); 40 | 41 | if (file.find(L".dll") != std::string::npos || file.find(L".exe") != std::string::npos) { 42 | ExtractIconExW(filePath, index, &hIcon, NULL, 1); 43 | } else { 44 | HINSTANCE instance = GetModuleHandle(NULL); 45 | WORD iconID = (WORD)index; 46 | // ExtractAssociatedIconW takes LPWSTR (mutable) so we copy 47 | std::vector mutablePath(file.begin(), file.end()); 48 | mutablePath.push_back(0); 49 | hIcon = ExtractAssociatedIconW(instance, mutablePath.data(), &iconID); 50 | } 51 | return hIcon; 52 | } 53 | 54 | bool IconManager::GetIconData(HICON hIcon, int nColorBits, std::vector &buff) { 55 | if (!hIcon) return false; 56 | 57 | ScopedHDC dc(CreateCompatibleDC(NULL)); 58 | if (!dc) return false; 59 | 60 | ICONINFO iconInfoRaw; 61 | if (!GetIconInfo(hIcon, &iconInfoRaw)) { 62 | return false; 63 | } 64 | 65 | ScopedGdiObject hbmColor(iconInfoRaw.hbmColor); 66 | ScopedGdiObject hbmMask(iconInfoRaw.hbmMask); 67 | 68 | char icoHeader[6] = {0, 0, 1, 0, 1, 0}; 69 | buff.insert(buff.end(), std::begin(icoHeader), std::end(icoHeader)); 70 | 71 | BITMAPINFO bmInfo = {0}; 72 | bmInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 73 | bmInfo.bmiHeader.biBitCount = 0; 74 | if (!GetDIBits(dc.get(), (HBITMAP)hbmColor.get(), 0, 0, NULL, &bmInfo, DIB_RGB_COLORS)) { 75 | return false; 76 | } 77 | 78 | int nBmInfoSize = sizeof(BITMAPINFOHEADER); 79 | if (nColorBits < 24) { 80 | nBmInfoSize += sizeof(RGBQUAD) * (int)(1ULL << nColorBits); 81 | } 82 | 83 | std::vector bitmapInfo(nBmInfoSize); 84 | BITMAPINFO *pBmInfo = (BITMAPINFO *)bitmapInfo.data(); 85 | memcpy(pBmInfo, &bmInfo, sizeof(BITMAPINFOHEADER)); 86 | 87 | if (bmInfo.bmiHeader.biSizeImage == 0) { 88 | int stride = ((bmInfo.bmiHeader.biWidth * bmInfo.bmiHeader.biBitCount + 31) / 32) * 4; 89 | bmInfo.bmiHeader.biSizeImage = stride * abs(bmInfo.bmiHeader.biHeight); 90 | } 91 | 92 | if (bmInfo.bmiHeader.biSizeImage == 0) return false; 93 | 94 | std::vector bits(bmInfo.bmiHeader.biSizeImage); 95 | pBmInfo->bmiHeader.biBitCount = (WORD)nColorBits; 96 | pBmInfo->bmiHeader.biCompression = BI_RGB; 97 | 98 | if (!GetDIBits(dc.get(), (HBITMAP)hbmColor.get(), 0, bmInfo.bmiHeader.biHeight, bits.data(), pBmInfo, DIB_RGB_COLORS)) { 99 | return false; 100 | } 101 | 102 | BITMAPINFO maskInfo = {0}; 103 | maskInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 104 | if (!GetDIBits(dc.get(), (HBITMAP)hbmMask.get(), 0, 0, NULL, &maskInfo, DIB_RGB_COLORS) || 105 | maskInfo.bmiHeader.biBitCount != 1) { 106 | return false; 107 | } 108 | 109 | if (maskInfo.bmiHeader.biSizeImage == 0) { 110 | int stride = ((maskInfo.bmiHeader.biWidth * maskInfo.bmiHeader.biBitCount + 31) / 32) * 4; 111 | maskInfo.bmiHeader.biSizeImage = stride * abs(maskInfo.bmiHeader.biHeight); 112 | } 113 | 114 | std::vector maskBits(maskInfo.bmiHeader.biSizeImage); 115 | std::vector maskInfoBytes(sizeof(BITMAPINFO) + 2 * sizeof(RGBQUAD)); 116 | BITMAPINFO *pMaskInfo = (BITMAPINFO *)maskInfoBytes.data(); 117 | memcpy(pMaskInfo, &maskInfo, sizeof(maskInfo)); 118 | 119 | if (!GetDIBits(dc.get(), (HBITMAP)hbmMask.get(), 0, maskInfo.bmiHeader.biHeight, maskBits.data(), pMaskInfo, DIB_RGB_COLORS)) { 120 | return false; 121 | } 122 | 123 | ICONDIRENTRY dir; 124 | dir.nWidth = (UCHAR)(pBmInfo->bmiHeader.biWidth > 255 ? 0 : pBmInfo->bmiHeader.biWidth); 125 | dir.nHeight = (UCHAR)(pBmInfo->bmiHeader.biHeight > 255 ? 0 : pBmInfo->bmiHeader.biHeight); 126 | dir.nNumColorsInPalette = (nColorBits <= 8 ? (1 << nColorBits) : 0); 127 | if (nColorBits == 4) dir.nNumColorsInPalette = 16; 128 | dir.nReserved = 0; 129 | dir.nNumColorPlanes = 0; 130 | dir.nBitsPerPixel = pBmInfo->bmiHeader.biBitCount; 131 | dir.nDataLength = pBmInfo->bmiHeader.biSizeImage + pMaskInfo->bmiHeader.biSizeImage + nBmInfoSize; 132 | dir.nOffset = sizeof(dir) + sizeof(icoHeader); 133 | 134 | const char* dirPtr = reinterpret_cast(&dir); 135 | buff.insert(buff.end(), dirPtr, dirPtr + sizeof(dir)); 136 | 137 | int nBitsSize = pBmInfo->bmiHeader.biSizeImage; 138 | pBmInfo->bmiHeader.biHeight *= 2; 139 | pBmInfo->bmiHeader.biCompression = 0; 140 | pBmInfo->bmiHeader.biSizeImage += pMaskInfo->bmiHeader.biSizeImage; 141 | 142 | const char* bmiPtr = reinterpret_cast(&pBmInfo->bmiHeader); 143 | buff.insert(buff.end(), bmiPtr, bmiPtr + nBmInfoSize); 144 | 145 | const char* bitsPtr = reinterpret_cast(bits.data()); 146 | buff.insert(buff.end(), bitsPtr, bitsPtr + nBitsSize); 147 | 148 | const char* maskPtr = reinterpret_cast(maskBits.data()); 149 | buff.insert(buff.end(), maskPtr, maskPtr + pMaskInfo->bmiHeader.biSizeImage); 150 | 151 | return true; 152 | } 153 | 154 | } // namespace win32audio 155 | -------------------------------------------------------------------------------- /example/lib/widgets/animated_progress_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | String _defaultFormatValue(double value, int? fixed) { 4 | if (fixed != null) { 5 | return value.toStringAsFixed(fixed); 6 | } else { 7 | return value.toStringAsFixed(value.truncateToDouble() == value ? 0 : 2); 8 | } 9 | } 10 | 11 | class FAProgressBar extends StatefulWidget { 12 | FAProgressBar({ 13 | super.key, 14 | this.currentValue = 0, 15 | this.maxValue = 100, 16 | this.size = 30, 17 | this.animatedDuration = const Duration(milliseconds: 300), 18 | this.direction = Axis.horizontal, 19 | this.verticalDirection = VerticalDirection.down, 20 | BorderRadiusGeometry? borderRadius, 21 | this.border, 22 | this.backgroundColor = const Color(0x00FFFFFF), 23 | this.progressColor = const Color(0xFFFA7268), 24 | this.changeColorValue, 25 | this.changeProgressColor = const Color(0xFF5F4B8B), 26 | this.formatValue = _defaultFormatValue, 27 | this.formatValueFixed, 28 | this.displayText, 29 | this.displayTextStyle = const TextStyle(color: Color(0xFFFFFFFF), fontSize: 12), 30 | }) : _borderRadius = borderRadius ?? BorderRadius.circular(8); 31 | final double currentValue; 32 | final double maxValue; 33 | final double size; 34 | final Duration animatedDuration; 35 | final Axis direction; 36 | final VerticalDirection verticalDirection; 37 | final BorderRadiusGeometry _borderRadius; 38 | final BoxBorder? border; 39 | final Color backgroundColor; 40 | final Color progressColor; 41 | final int? changeColorValue; 42 | final Color changeProgressColor; 43 | final String Function(double value, int? fixed) formatValue; 44 | final int? formatValueFixed; 45 | final String? displayText; 46 | final TextStyle displayTextStyle; 47 | 48 | @override 49 | // ignore: library_private_types_in_public_api 50 | _FAProgressBarState createState() => _FAProgressBarState(); 51 | } 52 | 53 | class _FAProgressBarState extends State with SingleTickerProviderStateMixin { 54 | late Animation _animation; 55 | late AnimationController _controller; 56 | double _currentBegin = 0; 57 | double _currentEnd = 0; 58 | 59 | @override 60 | void initState() { 61 | _controller = AnimationController(duration: widget.animatedDuration, vsync: this); 62 | _animation = Tween(begin: _currentBegin, end: _currentEnd).animate(_controller); 63 | triggerAnimation(); 64 | super.initState(); 65 | } 66 | 67 | @override 68 | void didUpdateWidget(FAProgressBar old) { 69 | triggerAnimation(); 70 | super.didUpdateWidget(old); 71 | } 72 | 73 | void triggerAnimation() { 74 | setState(() { 75 | _currentBegin = _animation.value; 76 | 77 | if (widget.currentValue == 0 || widget.maxValue == 0) { 78 | _currentEnd = 0; 79 | } else { 80 | _currentEnd = widget.currentValue / widget.maxValue; 81 | } 82 | 83 | _animation = Tween(begin: _currentBegin, end: _currentEnd).animate(_controller); 84 | }); 85 | _controller.reset(); 86 | _controller.duration = widget.animatedDuration; 87 | _controller.forward(); 88 | } 89 | 90 | @override 91 | Widget build(BuildContext context) => AnimatedProgressBar( 92 | animation: _animation, 93 | widget: widget, 94 | ); 95 | 96 | @override 97 | void dispose() { 98 | _controller.dispose(); 99 | super.dispose(); 100 | } 101 | } 102 | 103 | class AnimatedProgressBar extends AnimatedWidget { 104 | const AnimatedProgressBar({ 105 | super.key, 106 | required Animation animation, 107 | required this.widget, 108 | }) : super(listenable: animation); 109 | 110 | final FAProgressBar widget; 111 | 112 | double transformValue(num x, num begin, num end, num before) { 113 | double y = (end * x - (begin - before)) * (1 / before); 114 | return y < 0 ? 0 : ((y > 1) ? 1 : y); 115 | } 116 | 117 | @override 118 | Widget build(BuildContext context) { 119 | final Animation animation = listenable as Animation; 120 | Color progressColor = widget.progressColor; 121 | 122 | if (widget.changeColorValue != null) { 123 | final ColorTween colorTween = ColorTween( 124 | begin: widget.progressColor, 125 | end: widget.changeProgressColor, 126 | ); 127 | 128 | progressColor = colorTween.transform(transformValue( 129 | animation.value, 130 | widget.changeColorValue ?? 0, 131 | widget.maxValue, 132 | 5, 133 | ))!; 134 | } 135 | 136 | List progressWidgets = []; 137 | Widget progressWidget = Container( 138 | decoration: BoxDecoration( 139 | color: progressColor, 140 | borderRadius: widget._borderRadius, 141 | border: widget.border, 142 | ), 143 | ); 144 | progressWidgets.add(progressWidget); 145 | 146 | if (widget.displayText != null) { 147 | Widget textProgress = Container( 148 | alignment: widget.direction == Axis.horizontal 149 | ? const FractionalOffset(0.95, 0.5) 150 | : (widget.verticalDirection == VerticalDirection.up ? const FractionalOffset(0.5, 0.05) : const FractionalOffset(0.5, 0.95)), 151 | child: Text( 152 | widget.formatValue.call(animation.value * widget.maxValue, widget.formatValueFixed) + widget.displayText!, 153 | softWrap: false, 154 | style: widget.displayTextStyle, 155 | ), 156 | ); 157 | progressWidgets.add(textProgress); 158 | } 159 | 160 | return Directionality( 161 | textDirection: TextDirection.ltr, 162 | child: Container( 163 | width: widget.direction == Axis.vertical ? widget.size : null, 164 | height: widget.direction == Axis.horizontal ? widget.size : null, 165 | decoration: BoxDecoration( 166 | color: widget.backgroundColor, 167 | borderRadius: widget._borderRadius, 168 | border: widget.border, 169 | ), 170 | child: Flex( 171 | direction: widget.direction, 172 | verticalDirection: widget.verticalDirection, 173 | children: [ 174 | Expanded( 175 | flex: (animation.value * 100).toInt(), 176 | child: Stack( 177 | children: progressWidgets, 178 | ), 179 | ), 180 | Expanded( 181 | flex: 100 - (animation.value * 100).toInt(), 182 | child: Container(), 183 | ) 184 | ], 185 | ), 186 | ), 187 | ); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /windows/include/Policyconfig.h: -------------------------------------------------------------------------------- 1 | /* The MIT License (MIT) 2 | 3 | Copyright (c) 2015 DefSound 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | // ---------------------------------------------------------------------------- 24 | // PolicyConfig.h 25 | // Undocumented COM-interface IPolicyConfig. 26 | // Use for set default audio render endpoint 27 | // @author EreTIk 28 | // ---------------------------------------------------------------------------- 29 | 30 | 31 | #pragma once 32 | #include "mmreg.h" 33 | 34 | 35 | interface DECLSPEC_UUID("f8679f50-850a-41cf-9c72-430f290290c8") IPolicyConfig; 36 | class DECLSPEC_UUID("870af99c-171d-4f9e-af0d-e63df40c2bc9") CPolicyConfigClient; 37 | // ---------------------------------------------------------------------------- 38 | // class CPolicyConfigClient 39 | // {870af99c-171d-4f9e-af0d-e63df40c2bc9} 40 | // 41 | // interface IPolicyConfig 42 | // {f8679f50-850a-41cf-9c72-430f290290c8} 43 | // 44 | // Query interface: 45 | // CComPtr PolicyConfig; 46 | // PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigClient)); 47 | // 48 | // @compatible: Windows 7 and Later 49 | // ---------------------------------------------------------------------------- 50 | interface IPolicyConfig : public IUnknown 51 | { 52 | public: 53 | 54 | virtual HRESULT GetMixFormat( 55 | PCWSTR, 56 | WAVEFORMATEX** 57 | ); 58 | 59 | virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat( 60 | PCWSTR, 61 | INT, 62 | WAVEFORMATEX** 63 | ); 64 | 65 | virtual HRESULT STDMETHODCALLTYPE ResetDeviceFormat( 66 | PCWSTR 67 | ); 68 | 69 | virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat( 70 | PCWSTR, 71 | WAVEFORMATEX*, 72 | WAVEFORMATEX* 73 | ); 74 | 75 | virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod( 76 | PCWSTR, 77 | INT, 78 | PINT64, 79 | PINT64 80 | ); 81 | 82 | virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod( 83 | PCWSTR, 84 | PINT64 85 | ); 86 | 87 | virtual HRESULT STDMETHODCALLTYPE GetShareMode( 88 | PCWSTR, 89 | struct DeviceShareMode* 90 | ); 91 | 92 | virtual HRESULT STDMETHODCALLTYPE SetShareMode( 93 | PCWSTR, 94 | struct DeviceShareMode* 95 | ); 96 | 97 | virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( 98 | PCWSTR, 99 | const PROPERTYKEY&, 100 | PROPVARIANT* 101 | ); 102 | 103 | virtual HRESULT STDMETHODCALLTYPE SetPropertyValue( 104 | PCWSTR, 105 | const PROPERTYKEY&, 106 | PROPVARIANT* 107 | ); 108 | 109 | virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint( 110 | __in PCWSTR wszDeviceId, 111 | __in ERole eRole 112 | ); 113 | 114 | virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility( 115 | PCWSTR, 116 | INT 117 | ); 118 | }; 119 | 120 | interface DECLSPEC_UUID("568b9108-44bf-40b4-9006-86afe5b5a620") IPolicyConfigVista; 121 | class DECLSPEC_UUID("294935CE-F637-4E7C-A41B-AB255460B862") CPolicyConfigVistaClient; 122 | // ---------------------------------------------------------------------------- 123 | // class CPolicyConfigVistaClient 124 | // {294935CE-F637-4E7C-A41B-AB255460B862} 125 | // 126 | // interface IPolicyConfigVista 127 | // {568b9108-44bf-40b4-9006-86afe5b5a620} 128 | // 129 | // Query interface: 130 | // CComPtr PolicyConfig; 131 | // PolicyConfig.CoCreateInstance(__uuidof(CPolicyConfigVistaClient)); 132 | // 133 | // @compatible: Windows Vista and Later 134 | // ---------------------------------------------------------------------------- 135 | interface IPolicyConfigVista : public IUnknown 136 | { 137 | public: 138 | 139 | virtual HRESULT GetMixFormat( 140 | PCWSTR, 141 | WAVEFORMATEX** 142 | ); // not available on Windows 7, use method from IPolicyConfig 143 | 144 | virtual HRESULT STDMETHODCALLTYPE GetDeviceFormat( 145 | PCWSTR, 146 | INT, 147 | WAVEFORMATEX** 148 | ); 149 | 150 | virtual HRESULT STDMETHODCALLTYPE SetDeviceFormat( 151 | PCWSTR, 152 | WAVEFORMATEX*, 153 | WAVEFORMATEX* 154 | ); 155 | 156 | virtual HRESULT STDMETHODCALLTYPE GetProcessingPeriod( 157 | PCWSTR, 158 | INT, 159 | PINT64, 160 | PINT64 161 | ); // not available on Windows 7, use method from IPolicyConfig 162 | 163 | virtual HRESULT STDMETHODCALLTYPE SetProcessingPeriod( 164 | PCWSTR, 165 | PINT64 166 | ); // not available on Windows 7, use method from IPolicyConfig 167 | 168 | virtual HRESULT STDMETHODCALLTYPE GetShareMode( 169 | PCWSTR, 170 | struct DeviceShareMode* 171 | ); // not available on Windows 7, use method from IPolicyConfig 172 | 173 | virtual HRESULT STDMETHODCALLTYPE SetShareMode( 174 | PCWSTR, 175 | struct DeviceShareMode* 176 | ); // not available on Windows 7, use method from IPolicyConfig 177 | 178 | virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( 179 | PCWSTR, 180 | const PROPERTYKEY&, 181 | PROPVARIANT* 182 | ); 183 | 184 | virtual HRESULT STDMETHODCALLTYPE SetPropertyValue( 185 | PCWSTR, 186 | const PROPERTYKEY&, 187 | PROPVARIANT* 188 | ); 189 | 190 | virtual HRESULT STDMETHODCALLTYPE SetDefaultEndpoint( 191 | __in PCWSTR wszDeviceId, 192 | __in ERole eRole 193 | ); 194 | 195 | virtual HRESULT STDMETHODCALLTYPE SetEndpointVisibility( 196 | PCWSTR, 197 | INT 198 | ); // not available on Windows 7, use method from IPolicyConfig 199 | }; 200 | 201 | // ---------------------------------------------------------------------------- -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.13.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.2" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.4.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.2" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.19.1" 44 | cupertino_icons: 45 | dependency: "direct main" 46 | description: 47 | name: cupertino_icons 48 | sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.0.8" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.3" 60 | ffi: 61 | dependency: "direct main" 62 | description: 63 | name: ffi 64 | sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.1.4" 68 | flutter: 69 | dependency: "direct main" 70 | description: flutter 71 | source: sdk 72 | version: "0.0.0" 73 | flutter_lints: 74 | dependency: "direct dev" 75 | description: 76 | name: flutter_lints 77 | sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" 78 | url: "https://pub.dev" 79 | source: hosted 80 | version: "6.0.0" 81 | flutter_test: 82 | dependency: "direct dev" 83 | description: flutter 84 | source: sdk 85 | version: "0.0.0" 86 | leak_tracker: 87 | dependency: transitive 88 | description: 89 | name: leak_tracker 90 | sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "11.0.2" 94 | leak_tracker_flutter_testing: 95 | dependency: transitive 96 | description: 97 | name: leak_tracker_flutter_testing 98 | sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "3.0.10" 102 | leak_tracker_testing: 103 | dependency: transitive 104 | description: 105 | name: leak_tracker_testing 106 | sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "3.0.2" 110 | lints: 111 | dependency: transitive 112 | description: 113 | name: lints 114 | sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "6.0.0" 118 | matcher: 119 | dependency: transitive 120 | description: 121 | name: matcher 122 | sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "0.12.17" 126 | material_color_utilities: 127 | dependency: transitive 128 | description: 129 | name: material_color_utilities 130 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 131 | url: "https://pub.dev" 132 | source: hosted 133 | version: "0.11.1" 134 | meta: 135 | dependency: transitive 136 | description: 137 | name: meta 138 | sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c 139 | url: "https://pub.dev" 140 | source: hosted 141 | version: "1.16.0" 142 | path: 143 | dependency: transitive 144 | description: 145 | name: path 146 | sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" 147 | url: "https://pub.dev" 148 | source: hosted 149 | version: "1.9.1" 150 | plugin_platform_interface: 151 | dependency: transitive 152 | description: 153 | name: plugin_platform_interface 154 | sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" 155 | url: "https://pub.dev" 156 | source: hosted 157 | version: "2.1.8" 158 | sky_engine: 159 | dependency: transitive 160 | description: flutter 161 | source: sdk 162 | version: "0.0.0" 163 | source_span: 164 | dependency: transitive 165 | description: 166 | name: source_span 167 | sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" 168 | url: "https://pub.dev" 169 | source: hosted 170 | version: "1.10.1" 171 | stack_trace: 172 | dependency: transitive 173 | description: 174 | name: stack_trace 175 | sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" 176 | url: "https://pub.dev" 177 | source: hosted 178 | version: "1.12.1" 179 | stream_channel: 180 | dependency: transitive 181 | description: 182 | name: stream_channel 183 | sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" 184 | url: "https://pub.dev" 185 | source: hosted 186 | version: "2.1.4" 187 | string_scanner: 188 | dependency: transitive 189 | description: 190 | name: string_scanner 191 | sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" 192 | url: "https://pub.dev" 193 | source: hosted 194 | version: "1.4.1" 195 | term_glyph: 196 | dependency: transitive 197 | description: 198 | name: term_glyph 199 | sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" 200 | url: "https://pub.dev" 201 | source: hosted 202 | version: "1.2.2" 203 | test_api: 204 | dependency: transitive 205 | description: 206 | name: test_api 207 | sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" 208 | url: "https://pub.dev" 209 | source: hosted 210 | version: "0.7.6" 211 | vector_math: 212 | dependency: transitive 213 | description: 214 | name: vector_math 215 | sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b 216 | url: "https://pub.dev" 217 | source: hosted 218 | version: "2.2.0" 219 | vm_service: 220 | dependency: transitive 221 | description: 222 | name: vm_service 223 | sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" 224 | url: "https://pub.dev" 225 | source: hosted 226 | version: "15.0.2" 227 | win32: 228 | dependency: "direct main" 229 | description: 230 | name: win32 231 | sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e 232 | url: "https://pub.dev" 233 | source: hosted 234 | version: "5.15.0" 235 | win32audio: 236 | dependency: "direct main" 237 | description: 238 | path: ".." 239 | relative: true 240 | source: path 241 | version: "1.5.0" 242 | sdks: 243 | dart: ">=3.8.0 <4.0.0" 244 | flutter: ">=3.18.0-18.0.pre.54" 245 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.cpp: -------------------------------------------------------------------------------- 1 | #include "win32_window.h" 2 | 3 | #include 4 | 5 | #include "resource.h" 6 | 7 | namespace { 8 | 9 | constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; 10 | 11 | // The number of Win32Window objects that currently exist. 12 | static int g_active_window_count = 0; 13 | 14 | using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); 15 | 16 | // Scale helper to convert logical scaler values to physical using passed in 17 | // scale factor 18 | int Scale(int source, double scale_factor) { 19 | return static_cast(source * scale_factor); 20 | } 21 | 22 | // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. 23 | // This API is only needed for PerMonitor V1 awareness mode. 24 | void EnableFullDpiSupportIfAvailable(HWND hwnd) { 25 | HMODULE user32_module = LoadLibraryA("User32.dll"); 26 | if (!user32_module) { 27 | return; 28 | } 29 | auto enable_non_client_dpi_scaling = 30 | reinterpret_cast( 31 | GetProcAddress(user32_module, "EnableNonClientDpiScaling")); 32 | if (enable_non_client_dpi_scaling != nullptr) { 33 | enable_non_client_dpi_scaling(hwnd); 34 | FreeLibrary(user32_module); 35 | } 36 | } 37 | 38 | } // namespace 39 | 40 | // Manages the Win32Window's window class registration. 41 | class WindowClassRegistrar { 42 | public: 43 | ~WindowClassRegistrar() = default; 44 | 45 | // Returns the singleton registar instance. 46 | static WindowClassRegistrar* GetInstance() { 47 | if (!instance_) { 48 | instance_ = new WindowClassRegistrar(); 49 | } 50 | return instance_; 51 | } 52 | 53 | // Returns the name of the window class, registering the class if it hasn't 54 | // previously been registered. 55 | const wchar_t* GetWindowClass(); 56 | 57 | // Unregisters the window class. Should only be called if there are no 58 | // instances of the window. 59 | void UnregisterWindowClass(); 60 | 61 | private: 62 | WindowClassRegistrar() = default; 63 | 64 | static WindowClassRegistrar* instance_; 65 | 66 | bool class_registered_ = false; 67 | }; 68 | 69 | WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; 70 | 71 | const wchar_t* WindowClassRegistrar::GetWindowClass() { 72 | if (!class_registered_) { 73 | WNDCLASS window_class{}; 74 | window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); 75 | window_class.lpszClassName = kWindowClassName; 76 | window_class.style = CS_HREDRAW | CS_VREDRAW; 77 | window_class.cbClsExtra = 0; 78 | window_class.cbWndExtra = 0; 79 | window_class.hInstance = GetModuleHandle(nullptr); 80 | window_class.hIcon = 81 | LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); 82 | window_class.hbrBackground = 0; 83 | window_class.lpszMenuName = nullptr; 84 | window_class.lpfnWndProc = Win32Window::WndProc; 85 | RegisterClass(&window_class); 86 | class_registered_ = true; 87 | } 88 | return kWindowClassName; 89 | } 90 | 91 | void WindowClassRegistrar::UnregisterWindowClass() { 92 | UnregisterClass(kWindowClassName, nullptr); 93 | class_registered_ = false; 94 | } 95 | 96 | Win32Window::Win32Window() { 97 | ++g_active_window_count; 98 | } 99 | 100 | Win32Window::~Win32Window() { 101 | --g_active_window_count; 102 | Destroy(); 103 | } 104 | 105 | bool Win32Window::CreateAndShow(const std::wstring& title, 106 | const Point& origin, 107 | const Size& size) { 108 | Destroy(); 109 | 110 | const wchar_t* window_class = 111 | WindowClassRegistrar::GetInstance()->GetWindowClass(); 112 | 113 | const POINT target_point = {static_cast(origin.x), 114 | static_cast(origin.y)}; 115 | HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); 116 | UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); 117 | double scale_factor = dpi / 96.0; 118 | 119 | HWND window = CreateWindow( 120 | window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 121 | Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 122 | Scale(size.width, scale_factor), Scale(size.height, scale_factor), 123 | nullptr, nullptr, GetModuleHandle(nullptr), this); 124 | 125 | if (!window) { 126 | return false; 127 | } 128 | 129 | return OnCreate(); 130 | } 131 | 132 | // static 133 | LRESULT CALLBACK Win32Window::WndProc(HWND const window, 134 | UINT const message, 135 | WPARAM const wparam, 136 | LPARAM const lparam) noexcept { 137 | if (message == WM_NCCREATE) { 138 | auto window_struct = reinterpret_cast(lparam); 139 | SetWindowLongPtr(window, GWLP_USERDATA, 140 | reinterpret_cast(window_struct->lpCreateParams)); 141 | 142 | auto that = static_cast(window_struct->lpCreateParams); 143 | EnableFullDpiSupportIfAvailable(window); 144 | that->window_handle_ = window; 145 | } else if (Win32Window* that = GetThisFromHandle(window)) { 146 | return that->MessageHandler(window, message, wparam, lparam); 147 | } 148 | 149 | return DefWindowProc(window, message, wparam, lparam); 150 | } 151 | 152 | LRESULT 153 | Win32Window::MessageHandler(HWND hwnd, 154 | UINT const message, 155 | WPARAM const wparam, 156 | LPARAM const lparam) noexcept { 157 | switch (message) { 158 | case WM_DESTROY: 159 | window_handle_ = nullptr; 160 | Destroy(); 161 | if (quit_on_close_) { 162 | PostQuitMessage(0); 163 | } 164 | return 0; 165 | 166 | case WM_DPICHANGED: { 167 | auto newRectSize = reinterpret_cast(lparam); 168 | LONG newWidth = newRectSize->right - newRectSize->left; 169 | LONG newHeight = newRectSize->bottom - newRectSize->top; 170 | 171 | SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, 172 | newHeight, SWP_NOZORDER | SWP_NOACTIVATE); 173 | 174 | return 0; 175 | } 176 | case WM_SIZE: { 177 | RECT rect = GetClientArea(); 178 | if (child_content_ != nullptr) { 179 | // Size and position the child window. 180 | MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, 181 | rect.bottom - rect.top, TRUE); 182 | } 183 | return 0; 184 | } 185 | 186 | case WM_ACTIVATE: 187 | if (child_content_ != nullptr) { 188 | SetFocus(child_content_); 189 | } 190 | return 0; 191 | } 192 | 193 | return DefWindowProc(window_handle_, message, wparam, lparam); 194 | } 195 | 196 | void Win32Window::Destroy() { 197 | OnDestroy(); 198 | 199 | if (window_handle_) { 200 | DestroyWindow(window_handle_); 201 | window_handle_ = nullptr; 202 | } 203 | if (g_active_window_count == 0) { 204 | WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); 205 | } 206 | } 207 | 208 | Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { 209 | return reinterpret_cast( 210 | GetWindowLongPtr(window, GWLP_USERDATA)); 211 | } 212 | 213 | void Win32Window::SetChildContent(HWND content) { 214 | child_content_ = content; 215 | SetParent(content, window_handle_); 216 | RECT frame = GetClientArea(); 217 | 218 | MoveWindow(content, frame.left, frame.top, frame.right - frame.left, 219 | frame.bottom - frame.top, true); 220 | 221 | SetFocus(child_content_); 222 | } 223 | 224 | RECT Win32Window::GetClientArea() { 225 | RECT frame; 226 | GetClientRect(window_handle_, &frame); 227 | return frame; 228 | } 229 | 230 | HWND Win32Window::GetHandle() { 231 | return window_handle_; 232 | } 233 | 234 | void Win32Window::SetQuitOnClose(bool quit_on_close) { 235 | quit_on_close_ = quit_on_close; 236 | } 237 | 238 | bool Win32Window::OnCreate() { 239 | // No-op; provided for subclasses. 240 | return true; 241 | } 242 | 243 | void Win32Window::OnDestroy() { 244 | // No-op; provided for subclasses. 245 | } 246 | -------------------------------------------------------------------------------- /lib/win32audio.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs, sort_constructors_first 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/services.dart'; 6 | 7 | /// Represents a single audio device (Input or Output) on Windows. 8 | class AudioDevice { 9 | /// The unique identifier of the audio device (MMDevice ID). 10 | String id = ""; 11 | 12 | /// The friendly name of the audio device. 13 | String name = ""; 14 | 15 | /// The file path to the resource containing the device's icon. 16 | /// This is usually a DLL or EXE file. 17 | String iconPath = ""; 18 | 19 | /// The resource ID of the icon within [iconPath]. 20 | int iconID = 0; 21 | 22 | /// Whether this device is currently the default or active device. 23 | bool isActive = false; 24 | 25 | @override 26 | String toString() { 27 | return "AudioDevice{id: $id, name: $name, iconPath: $iconPath, iconID: $iconID, isActive: $isActive}"; 28 | } 29 | 30 | /// Converts the [AudioDevice] instance to a map. 31 | Map toMap() { 32 | return { 33 | "id": id, 34 | "name": name, 35 | "iconPath": iconPath, 36 | "iconID": iconID, 37 | "isActive": isActive, 38 | }; 39 | } 40 | 41 | @override 42 | bool operator ==(Object other) { 43 | if (identical(this, other)) return true; 44 | 45 | return other is AudioDevice && other.id == id && other.name == name && other.iconPath == iconPath && other.iconID == iconID && other.isActive == isActive; 46 | } 47 | 48 | @override 49 | int get hashCode { 50 | return Object.hash(id, name, iconPath, iconID, isActive); 51 | } 52 | } 53 | 54 | /// Represents the volume control status of a specific process. 55 | class ProcessVolume { 56 | /// The Process ID (PID). 57 | int processId = 0; 58 | 59 | /// The full path to the process executable. 60 | String processPath = ""; 61 | 62 | /// The current master volume of the process (0.0 to 1.0). 63 | double maxVolume = 1.0; 64 | 65 | /// The peak volume currently being emitted by the process (0.0 to 1.0). 66 | double peakVolume = 0.0; 67 | 68 | @override 69 | String toString() { 70 | return "ProcessVolume{processId: $processId, processPath: $processPath, maxVolume: $maxVolume, peakVolume: $peakVolume}"; 71 | } 72 | 73 | /// Converts the [ProcessVolume] instance to a map. 74 | Map toMap() { 75 | return { 76 | "processId": processId, 77 | "processPath": processPath, 78 | "maxVolume": maxVolume, 79 | "peakVolume": peakVolume, 80 | }; 81 | } 82 | 83 | @override 84 | bool operator ==(Object other) { 85 | if (identical(this, other)) return true; 86 | 87 | return other is ProcessVolume && other.processId == processId && other.processPath == processPath && other.maxVolume == maxVolume && other.peakVolume == peakVolume; 88 | } 89 | 90 | @override 91 | int get hashCode { 92 | return Object.hash(processId, processPath, maxVolume, peakVolume); 93 | } 94 | } 95 | 96 | /// Enumeration of audio device types. 97 | enum AudioDeviceType { 98 | /// Playback devices (Speakers, Headphones). 99 | output, 100 | 101 | /// Recording devices (Microphones). 102 | input, 103 | } 104 | 105 | /// Enumeration of audio roles for Windows Multimedia devices. 106 | enum AudioRole { 107 | /// Games, system notification sounds, and voice commands. 108 | console, 109 | 110 | /// Music, movies, narration, and live music recording. 111 | multimedia, 112 | 113 | /// Voice chat and VoIP applications. 114 | communications, 115 | } 116 | 117 | /// The MethodChannel used for communicating with the native Windows implementation. 118 | const MethodChannel audioMethodChannel = MethodChannel("win32audio"); 119 | 120 | /// Main class for interacting with Windows Audio APIs. 121 | class Audio { 122 | /// Internal state to track if the listener for native events is active. 123 | static bool listenerActive = false; 124 | 125 | static final List _changeListeners = []; 126 | 127 | /// Initializes the audio event listener. 128 | /// 129 | /// This must be called before adding any listeners via [addChangeListener]. 130 | /// Usually handled automatically when you start interacting with the plugin. 131 | static Future setupChangeListener() async { 132 | if (!listenerActive) { 133 | listenerActive = true; 134 | await audioMethodChannel.invokeMethod('initAudioListener'); 135 | audioMethodChannel.setMethodCallHandler((MethodCall call) async { 136 | if (call.method == 'onAudioDeviceChange') { 137 | // 'name' represents the event type (e.g., OnDeviceStateChanged) 138 | // 'id' is the device ID 139 | for (void Function(String type, String id) listener in _changeListeners) { 140 | listener( 141 | call.arguments["name"] as String? ?? "", 142 | call.arguments["id"] as String? ?? "", 143 | ); 144 | } 145 | } 146 | }); 147 | } 148 | } 149 | 150 | /// Adds a callback that listens to audio device changes (Added, Removed, Default Changed, etc.). 151 | static void addChangeListener(void Function(String type, String id) callback) { 152 | _changeListeners.add(callback); 153 | // Ensure the native listener is running 154 | setupChangeListener(); 155 | } 156 | 157 | /// Removes a previously added audio device change listener. 158 | static void removeChangeListener(void Function(String type, String id) callback) { 159 | _changeListeners.remove(callback); 160 | } 161 | 162 | /// Returns a Future list of audio devices of a specified type. 163 | /// 164 | /// [audioDeviceType] specifies whether to search for output or input devices. 165 | /// [audioRole] filters the default status (defaults to [AudioRole.multimedia]). 166 | static Future?> enumDevices(AudioDeviceType audioDeviceType, {AudioRole audioRole = AudioRole.multimedia}) async { 167 | final Map arguments = {"deviceType": audioDeviceType.index, "role": audioRole.index}; 168 | final Map? map = await audioMethodChannel.invokeMethod("enumAudioDevices", arguments); 169 | 170 | if (map == null) return null; 171 | 172 | final List audioDevices = []; 173 | for (dynamic key in map.keys) { 174 | final AudioDevice audioDevice = AudioDevice(); 175 | final Map data = map[key]; 176 | audioDevice.id = data["id"] ?? ""; 177 | audioDevice.name = data["name"] ?? ""; 178 | 179 | final String iconInfo = data["iconInfo"] ?? ""; 180 | final List iconData = iconInfo.split(","); 181 | if (iconData.length == 2) { 182 | audioDevice.iconPath = iconData[0]; 183 | audioDevice.iconID = int.tryParse(iconData[1]) ?? -1; 184 | } else { 185 | audioDevice.iconPath = iconInfo; 186 | audioDevice.iconID = -1; 187 | } 188 | audioDevice.isActive = data["isActive"] ?? false; 189 | audioDevices.add(audioDevice); 190 | } 191 | return audioDevices; 192 | } 193 | 194 | /// Returns the default audio device for the given [AudioDeviceType]. 195 | static Future getDefaultDevice(AudioDeviceType audioDeviceType, {AudioRole audioRole = AudioRole.multimedia}) async { 196 | final Map arguments = {"deviceType": audioDeviceType.index, "role": audioRole.index}; 197 | final Map? map = await audioMethodChannel.invokeMethod("getDefaultDevice", arguments); 198 | 199 | if (map == null) return null; 200 | 201 | final AudioDevice audioDevice = AudioDevice(); 202 | audioDevice.id = map["id"] ?? ""; 203 | audioDevice.name = map["name"] ?? ""; 204 | 205 | final String iconInfo = map["iconInfo"] ?? ""; 206 | final List iconData = iconInfo.split(","); 207 | 208 | if (iconData.length == 2) { 209 | audioDevice.iconPath = iconData[0]; 210 | audioDevice.iconID = int.tryParse(iconData[1]) ?? -1; 211 | } else { 212 | audioDevice.iconPath = iconInfo; 213 | audioDevice.iconID = -1; 214 | } 215 | audioDevice.isActive = map["isActive"] ?? false; 216 | return audioDevice; 217 | } 218 | 219 | /// Sets the system default audio device using its [deviceID]. 220 | /// 221 | /// You can specify which role(s) to set this device as default for: 222 | /// * [console] 223 | /// * [multimedia] 224 | /// * [communications] 225 | static Future setDefaultDevice(String deviceID, {bool console = false, bool multimedia = true, bool communications = false}) async { 226 | final Map arguments = { 227 | "deviceID": deviceID, 228 | "console": console, 229 | "multimedia": multimedia, 230 | "communications": communications, 231 | }; 232 | 233 | final int? result = await audioMethodChannel.invokeMethod("setDefaultAudioDevice", arguments); 234 | return result ?? 0; 235 | } 236 | 237 | /// Gets the master volume level (0.0 to 1.0) for the current default device of the given type. 238 | static Future getVolume(AudioDeviceType audioDeviceType, {AudioRole audioRole = AudioRole.multimedia}) async { 239 | final Map arguments = {"deviceType": audioDeviceType.index, "role": audioRole.index}; 240 | final double? result = await audioMethodChannel.invokeMethod("getAudioVolume", arguments); 241 | return result ?? 0.0; 242 | } 243 | 244 | /// Sets the master volume for the default device of the given [audioDeviceType]. 245 | /// 246 | /// [volume] should be between 0.0 and 1.0. If > 1, it is treated as a percentage (divided by 100). 247 | static Future setVolume(double volume, AudioDeviceType audioDeviceType, {AudioRole audioRole = AudioRole.multimedia}) async { 248 | if (volume > 1) volume = (volume / 100).toDouble(); 249 | final Map arguments = {"deviceType": audioDeviceType.index, "volumeLevel": volume, "role": audioRole.index}; 250 | final int? result = await audioMethodChannel.invokeMethod("setAudioVolume", arguments); 251 | return result ?? 0; 252 | } 253 | 254 | /// Sets the volume of a specific audio device by its [deviceID]. 255 | /// 256 | /// [volume] should be between 0.0 and 1.0. 257 | static Future setAudioDeviceVolume(String deviceID, double volume) async { 258 | if (volume > 1) volume = (volume / 100).toDouble(); 259 | final Map arguments = {"deviceID": deviceID, "volumeLevel": volume}; 260 | final int? result = await audioMethodChannel.invokeMethod("setAudioDeviceVolume", arguments); 261 | return result ?? 0; 262 | } 263 | 264 | /// Switches the default device to the next available one, or toggles between available devices. 265 | /// (Logic depends on implementation of `switchDefaultDevice` in native code). 266 | static Future switchDefaultDevice(AudioDeviceType audioDeviceType, 267 | {AudioRole audioRole = AudioRole.multimedia, bool console = false, bool multimedia = true, bool communications = false}) async { 268 | final Map arguments = { 269 | "deviceType": audioDeviceType.index, 270 | "role": audioRole.index, 271 | "console": console, 272 | "multimedia": multimedia, 273 | "communications": communications, 274 | }; 275 | final bool? result = await audioMethodChannel.invokeMethod("switchDefaultDevice", arguments); 276 | return result ?? false; 277 | } 278 | 279 | /// Gets the current sample rate (in Hz) of the specified audio device. 280 | /// 281 | /// Returns the sample rate as an integer (e.g., 44100, 48000, 96000). 282 | /// Returns 0 if the operation fails. 283 | static Future getSampleRate(String deviceID) async { 284 | final Map arguments = {"deviceID": deviceID}; 285 | final int? result = await audioMethodChannel.invokeMethod("getSampleRate", arguments); 286 | return result ?? 0; 287 | } 288 | 289 | /// Sets the sample rate (in Hz) for the specified audio device. 290 | /// 291 | /// Common sample rates include: 44100, 48000, 96000, 192000. 292 | /// Returns true if successful, false otherwise. 293 | /// 294 | /// Note: This modifies the device's default format and may require 295 | /// administrative privileges or cause audio streams to restart. 296 | static Future setSampleRate(String deviceID, int sampleRate) async { 297 | final Map arguments = { 298 | "deviceID": deviceID, 299 | "sampleRate": sampleRate, 300 | }; 301 | final bool? result = await audioMethodChannel.invokeMethod("setSampleRate", arguments); 302 | return result ?? false; 303 | } 304 | 305 | /// Lists all active application audio sessions (mixers). 306 | /// 307 | /// Returns a list of [ProcessVolume] containing volume info for each app. 308 | static Future?> enumAudioMixer({AudioRole audioRole = AudioRole.multimedia}) async { 309 | final Map arguments = {"role": audioRole.index}; 310 | final Map? map = await audioMethodChannel.invokeMethod("enumAudioMixer", arguments); 311 | 312 | if (map == null) return null; 313 | 314 | final List processVolumes = []; 315 | for (dynamic key in map.keys) { 316 | final ProcessVolume processVolume = ProcessVolume(); 317 | final Map data = map[key]; 318 | processVolume.processId = key as int; 319 | processVolume.processPath = data["processPath"] ?? ""; 320 | processVolume.maxVolume = (data["maxVolume"] as num?)?.toDouble() ?? 0.0; 321 | processVolume.peakVolume = (data["peakVolume"] as num?)?.toDouble() ?? 0.0; 322 | processVolumes.add(processVolume); 323 | } 324 | return processVolumes; 325 | } 326 | 327 | /// Sets the volume for a specific process identified by [processID]. 328 | /// 329 | /// [volume] should be 0.0 to 1.0. 330 | static Future setAudioMixerVolume(int processID, double volume, {AudioRole audioRole = AudioRole.multimedia}) async { 331 | if (volume > 1) volume = (volume / 100).toDouble(); 332 | final Map arguments = {"processID": processID, "volumeLevel": volume, "role": audioRole.index}; 333 | final bool? result = await audioMethodChannel.invokeMethod("setAudioMixerVolume", arguments); 334 | return result ?? false; 335 | } 336 | 337 | /// Sets the volume for a specific process identified by its executable path [processPath]. 338 | /// 339 | /// [volume] should be 0.0 to 1.0. 340 | static Future setAudioMixerVolumeByPath(String processPath, double volume, {AudioRole audioRole = AudioRole.multimedia}) async { 341 | if (volume > 1) volume = (volume / 100).toDouble(); 342 | final Map arguments = {"processPath": processPath, "volumeLevel": volume, "role": audioRole.index}; 343 | final bool? result = await audioMethodChannel.invokeMethod("setAudioMixerVolumeByPath", arguments); 344 | return result ?? false; 345 | } 346 | } 347 | 348 | /// Helper function to convert a native icon location to bytes. 349 | @Deprecated("Use WinIcons class instead") 350 | Future nativeIconToBytes(String iconlocation, {int iconID = 0}) async { 351 | final Map arguments = {"iconLocation": iconlocation, "iconID": iconID}; 352 | final Uint8List? result = await audioMethodChannel.invokeMethod("iconToBytes", arguments); 353 | return result; 354 | } 355 | 356 | /// Utility class for extracting icons from files and windows on Windows. 357 | class WinIcons { 358 | /// Extracts the icon from a file path (e.g., .exe, .dll). 359 | /// 360 | /// [iconlocation] is the absolute path to the file. 361 | /// [iconID] is the resource ID index (default 0). 362 | Future extractFileIcon(String iconlocation, {int iconID = 0}) async { 363 | final Map arguments = {"iconLocation": iconlocation, "iconID": iconID}; 364 | final Uint8List? result = await audioMethodChannel.invokeMethod("iconToBytes", arguments); 365 | return result; 366 | } 367 | 368 | @Deprecated("Use extractFileIcon instead") 369 | Future extractExecutableIcon(String iconlocation, {int iconID = 0}) async { 370 | return await extractFileIcon(iconlocation, iconID: iconID); 371 | } 372 | 373 | /// Extracts the small icon associated with a specific Window Handle ([hWnd]). 374 | Future extractWindowIcon(int hWnd) async { 375 | final Map arguments = {"hWnd": hWnd}; 376 | final Uint8List? result = await audioMethodChannel.invokeMethod("getWindowIcon", arguments); 377 | return result; 378 | } 379 | 380 | /// Converts a raw Win32 icon handle ([hIcon]) to PNG bytes. 381 | Future extractIconHandle(int hIcon) async { 382 | final Map arguments = {"hIcon": hIcon}; 383 | final Uint8List? result = await audioMethodChannel.invokeMethod("getIconPng", arguments); 384 | return result; 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | 4 | import 'package:win32audio/win32audio.dart'; 5 | import 'widgets/animated_progress_bar.dart'; 6 | import 'widgets/win_icon_widget.dart'; 7 | 8 | void main() async { 9 | WidgetsFlutterBinding.ensureInitialized(); 10 | await Audio.setupChangeListener(); 11 | runApp(const MyApp()); 12 | } 13 | 14 | class MyApp extends StatefulWidget { 15 | const MyApp({super.key}); 16 | 17 | @override 18 | State createState() => _MyAppState(); 19 | } 20 | 21 | class _MyAppState extends State { 22 | AudioDevice defaultDevice = AudioDevice(); 23 | List audioDevices = []; 24 | AudioDeviceType audioDeviceType = AudioDeviceType.output; 25 | final WinIcons winIcons = WinIcons(); 26 | 27 | List mixerList = []; 28 | bool _stateFetchAudioMixerPeak = true; 29 | 30 | double __volume = 0.0; 31 | String fetchStatus = ""; 32 | List eventLog = []; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | Audio.addChangeListener((String type, String id) async { 38 | if (mounted) { 39 | setState(() { 40 | eventLog.insert(0, "$type: $id"); 41 | if (eventLog.length > 50) eventLog.removeLast(); 42 | }); 43 | } 44 | }); 45 | fetchAudioDevices(); 46 | Timer.periodic(const Duration(milliseconds: 150), (Timer timer) async { 47 | if (_stateFetchAudioMixerPeak) { 48 | mixerList = await Audio.enumAudioMixer() ?? []; 49 | if (mounted) setState(() {}); 50 | } 51 | }); 52 | } 53 | 54 | @override 55 | void dispose() { 56 | super.dispose(); 57 | } 58 | 59 | Future fetchAudioDevices() async { 60 | if (!mounted) return; 61 | audioDevices = await Audio.enumDevices(audioDeviceType) ?? []; 62 | __volume = await Audio.getVolume(audioDeviceType); 63 | defaultDevice = (await Audio.getDefaultDevice(audioDeviceType))!; 64 | 65 | mixerList = await Audio.enumAudioMixer() ?? []; 66 | 67 | if (mounted) { 68 | setState(() { 69 | fetchStatus = "Get"; 70 | }); 71 | } 72 | } 73 | 74 | Future _showSampleRateDialog(BuildContext context, AudioDevice device) async { 75 | // Get current sample rate 76 | int currentRate = await Audio.getSampleRate(device.id); 77 | 78 | if (!context.mounted) return; 79 | 80 | final int? selectedRate = await showDialog( 81 | context: context, 82 | builder: (BuildContext context) { 83 | return StatefulBuilder( 84 | builder: (BuildContext context, StateSetter setState) { 85 | int? tempSelectedRate = currentRate; 86 | 87 | return AlertDialog( 88 | title: Text('Sample Rate - ${device.name}'), 89 | content: RadioGroup( 90 | groupValue: tempSelectedRate, 91 | onChanged: (int? value) { 92 | if (value != null) { 93 | setState(() => tempSelectedRate = value); 94 | Navigator.of(context).pop(value); 95 | } 96 | }, 97 | child: Column( 98 | mainAxisSize: MainAxisSize.min, 99 | crossAxisAlignment: CrossAxisAlignment.start, 100 | children: [ 101 | Text('Current: ${currentRate > 0 ? "$currentRate Hz" : "Unknown"}'), 102 | const SizedBox(height: 20), 103 | const Text('Select new sample rate:'), 104 | const SizedBox(height: 10), 105 | ...[44100, 48000, 96000, 192000].map((int rate) { 106 | return ListTile(title: Text('$rate Hz'), leading: Radio(value: rate)); 107 | }), 108 | ], 109 | ), 110 | ), 111 | actions: [ 112 | TextButton( 113 | onPressed: () => Navigator.of(context).pop(), 114 | child: const Text('Cancel'), 115 | ), 116 | ], 117 | ); 118 | }, 119 | ); 120 | }, 121 | ); 122 | 123 | if (selectedRate != null && context.mounted) { 124 | bool success = await Audio.setSampleRate(device.id, selectedRate); 125 | 126 | if (context.mounted) { 127 | ScaffoldMessenger.of(context).showSnackBar( 128 | SnackBar( 129 | content: Text( 130 | success ? 'Sample rate changed to $selectedRate Hz' : 'Failed to change sample rate. Try running as administrator.', 131 | ), 132 | backgroundColor: success ? Colors.green : Colors.red, 133 | ), 134 | ); 135 | } 136 | } 137 | } 138 | 139 | @override 140 | Widget build(BuildContext context) { 141 | return MaterialApp( 142 | debugShowCheckedModeBanner: false, 143 | home: Scaffold( 144 | appBar: AppBar( 145 | title: ConstrainedBox( 146 | constraints: const BoxConstraints(maxWidth: double.infinity), 147 | child: Row( 148 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 149 | children: [ 150 | Container( 151 | alignment: Alignment.centerLeft, 152 | width: 100, 153 | child: TextButton( 154 | child: Text( 155 | fetchStatus, 156 | style: const TextStyle(color: Colors.white), 157 | strutStyle: const StrutStyle(forceStrutHeight: true), 158 | ), 159 | onPressed: () async { 160 | setState(() { 161 | fetchStatus = 'Getting...'; 162 | }); 163 | fetchAudioDevices(); 164 | }, 165 | ), 166 | ), 167 | Row( 168 | crossAxisAlignment: CrossAxisAlignment.end, 169 | children: [ 170 | Container( 171 | alignment: Alignment.centerRight, 172 | child: TextButton( 173 | child: Row( 174 | mainAxisAlignment: MainAxisAlignment.end, 175 | crossAxisAlignment: CrossAxisAlignment.center, 176 | children: [ 177 | Padding( 178 | padding: const EdgeInsets.only(right: 5), 179 | child: Align( 180 | alignment: Alignment.topCenter, 181 | heightFactor: 1.15, 182 | child: Text( 183 | defaultDevice.name, 184 | textAlign: TextAlign.justify, 185 | style: const TextStyle(color: Colors.black), 186 | strutStyle: const StrutStyle(forceStrutHeight: true), 187 | ), 188 | ), 189 | ), 190 | const Icon(Icons.swap_vertical_circle_outlined, color: Colors.black), 191 | ], 192 | ), 193 | onPressed: () async { 194 | await Audio.switchDefaultDevice(audioDeviceType); 195 | fetchAudioDevices(); 196 | setState(() {}); 197 | }), 198 | ), 199 | ], 200 | ), 201 | ], 202 | ), 203 | ), 204 | ), 205 | body: Column( 206 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 207 | children: [ 208 | //? SELECT AUDIO DEVICE 209 | //? DROPDOWN 210 | Column(children: [ 211 | DropdownButton( 212 | value: audioDeviceType, 213 | items: AudioDeviceType.values.map((AudioDeviceType e) { 214 | return DropdownMenuItem( 215 | value: e, 216 | child: Text(e.toString()), 217 | ); 218 | }).toList(), 219 | onChanged: (Object? e) async { 220 | audioDeviceType = e as AudioDeviceType; 221 | fetchStatus = e.toString(); 222 | fetchAudioDevices(); 223 | setState(() {}); 224 | }), 225 | ]), 226 | //? VOLUME INFO 227 | Flexible( 228 | //VOLUME 229 | flex: 1, 230 | fit: FlexFit.tight, 231 | child: Center( 232 | child: Text( 233 | "Volume:${(__volume * 100).toStringAsFixed(0)}%", 234 | style: const TextStyle(fontSize: 32), 235 | ), 236 | ), 237 | ), 238 | //? VOLUMESCROLLER 239 | Flexible( 240 | //SLIDER 241 | flex: 1, 242 | fit: FlexFit.tight, 243 | child: Slider( 244 | value: __volume, 245 | min: 0, 246 | max: 1, 247 | divisions: 25, 248 | onChanged: (double e) async { 249 | await Audio.setVolume(e.toDouble(), audioDeviceType); 250 | __volume = e; 251 | setState(() {}); 252 | }, 253 | ), 254 | ), 255 | //? LIST AUDIO DEVICES 256 | Flexible( 257 | flex: 2, 258 | fit: FlexFit.loose, 259 | child: ListView.builder( 260 | itemCount: audioDevices.length, 261 | itemBuilder: (BuildContext context, int index) { 262 | return ListTile( 263 | leading: WinIconWidget( 264 | path: audioDevices[index].iconPath, 265 | iconID: audioDevices[index].iconID, 266 | ), 267 | title: Text(audioDevices[index].name), 268 | trailing: Row( 269 | mainAxisSize: MainAxisSize.min, 270 | children: [ 271 | IconButton( 272 | icon: const Icon(Icons.graphic_eq), 273 | tooltip: 'Sample Rate', 274 | onPressed: () async { 275 | await _showSampleRateDialog(context, audioDevices[index]); 276 | }, 277 | ), 278 | IconButton( 279 | icon: Icon((audioDevices[index].isActive == true ? Icons.check_box_outlined : Icons.check_box_outline_blank)), 280 | onPressed: () async { 281 | await Audio.setDefaultDevice(audioDevices[index].id); 282 | fetchAudioDevices(); 283 | setState(() {}); 284 | }, 285 | ), 286 | ], 287 | ), 288 | ); 289 | })), 290 | const Divider( 291 | thickness: 5, 292 | height: 10, 293 | color: Color.fromARGB(12, 0, 0, 0), 294 | ), 295 | //? AUDIO MIXER 296 | SizedBox( 297 | child: CheckboxListTile( 298 | title: const Text("Countinously Fetch The Audio Mixer"), 299 | value: _stateFetchAudioMixerPeak, 300 | controlAffinity: ListTileControlAffinity.leading, 301 | onChanged: (bool? e) { 302 | setState(() { 303 | _stateFetchAudioMixerPeak = e!; 304 | }); 305 | }, 306 | ), 307 | ), 308 | //? LIST AUDIO MIXER 309 | Flexible( 310 | flex: 2, 311 | child: ListView.builder( 312 | itemCount: mixerList.length, 313 | itemBuilder: (BuildContext context, int index) { 314 | return Column( 315 | children: [ 316 | const Divider( 317 | height: 6, 318 | thickness: 3, 319 | color: Color.fromARGB(10, 0, 0, 0), 320 | indent: 100, 321 | endIndent: 100, 322 | ), 323 | Row( 324 | children: [ 325 | Flexible( 326 | child: ListTile( 327 | leading: WinIconWidget( 328 | path: mixerList[index].processPath, 329 | iconID: 0, // Usually 0 for exe icons 330 | ), 331 | title: Tooltip( 332 | message: mixerList[index].processPath, 333 | child: Text( 334 | mixerList[index].processPath.split('/').last.split('\\').last, 335 | ), 336 | ))), 337 | Flexible( 338 | child: Padding( 339 | padding: const EdgeInsets.only(right: 20), 340 | child: Column( 341 | children: [ 342 | Slider( 343 | value: mixerList[index].maxVolume, 344 | min: 0, 345 | max: 1, 346 | divisions: 25, 347 | onChanged: (double e) async { 348 | await Audio.setAudioMixerVolume(mixerList[index].processId, e.toDouble()); 349 | mixerList[index].maxVolume = e; 350 | setState(() {}); 351 | }, 352 | ), 353 | Padding( 354 | padding: const EdgeInsets.symmetric(horizontal: 25), 355 | child: FAProgressBar( 356 | currentValue: mixerList[index].peakVolume * mixerList[index].maxVolume * 100, 357 | size: 12, 358 | maxValue: 100, 359 | changeColorValue: 100, 360 | changeProgressColor: Colors.pink, 361 | progressColor: Colors.lightBlue, 362 | animatedDuration: const Duration(milliseconds: 300), 363 | direction: Axis.horizontal, 364 | verticalDirection: VerticalDirection.up, 365 | formatValueFixed: 2, 366 | ), 367 | ), 368 | ], 369 | ), 370 | ), 371 | ), 372 | ], 373 | ), 374 | ], 375 | ); 376 | }), 377 | ), 378 | const Divider( 379 | thickness: 5, 380 | height: 10, 381 | color: Color.fromARGB(12, 0, 0, 0), 382 | ), 383 | const Center(child: Text("Event Log")), 384 | //? EVENT LOG 385 | Flexible( 386 | flex: 2, 387 | child: Container( 388 | color: Colors.grey[200], 389 | child: ListView.builder( 390 | itemCount: eventLog.length, 391 | itemBuilder: (BuildContext context, int index) { 392 | return Padding( 393 | padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), 394 | child: Text( 395 | eventLog[index], 396 | style: const TextStyle(fontSize: 10), 397 | ), 398 | ); 399 | }), 400 | ), 401 | ) 402 | ], 403 | ), 404 | ), 405 | ); 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /windows/audio_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "audio_manager.h" 2 | #include "include/Policyconfig.h" 3 | #include "include/encoding.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | // For CLSID_MMDeviceEnumerator 11 | const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); 12 | const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); 13 | const IID IID_IAudioEndpointVolume = __uuidof(IAudioEndpointVolume); 14 | 15 | #pragma comment(lib, "propsys") 16 | 17 | // Define PKEY_AudioEngine_DeviceFormat manually if not found by linker 18 | #ifndef PKEY_AudioEngine_DeviceFormat 19 | EXTERN_C const PROPERTYKEY DECLSPEC_SELECTANY PKEY_AudioEngine_DeviceFormat = 20 | { { 0xf19f064d, 0x82c, 0x4e27, { 0xbc, 0x73, 0x68, 0x82, 0xa1, 0xbb, 0x8e, 0x4c } }, 0 }; 21 | #endif 22 | 23 | namespace win32audio { 24 | 25 | // Removed conflicting enum definition. Using system defined AudioSessionState. 26 | 27 | HRESULT AudioManager::GetDeviceProperty(IMMDevice *pDevice, DeviceProps *pOutput) { 28 | if (pDevice == nullptr || pOutput == nullptr) { 29 | return E_POINTER; 30 | } 31 | 32 | ComPtr pPropertyStore; 33 | HRESULT hr = pDevice->OpenPropertyStore(STGM_READ, &pPropertyStore); 34 | if (FAILED(hr)) { 35 | return hr; 36 | } 37 | 38 | PROPVARIANT varName = {0}; 39 | PropVariantInit(&varName); 40 | hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, &varName); 41 | if (SUCCEEDED(hr)) { 42 | if (IsPropVariantString(varName)) { 43 | STRRET strret; 44 | if (SUCCEEDED(PropVariantToStrRet(varName, &strret)) && strret.pOleStr) { 45 | pOutput->name = strret.pOleStr; 46 | CoTaskMemFree(strret.pOleStr); 47 | } else { 48 | hr = E_UNEXPECTED; 49 | } 50 | } else { 51 | hr = E_UNEXPECTED; 52 | } 53 | PropVariantClear(&varName); 54 | } 55 | 56 | PROPVARIANT varIconPath = {0}; 57 | PropVariantInit(&varIconPath); 58 | hr = pPropertyStore->GetValue(PKEY_DeviceClass_IconPath, &varIconPath); 59 | if (SUCCEEDED(hr)) { 60 | if (IsPropVariantString(varIconPath)) { 61 | STRRET strret; 62 | if (SUCCEEDED(PropVariantToStrRet(varIconPath, &strret)) && strret.pOleStr) { 63 | pOutput->iconInfo = strret.pOleStr; 64 | CoTaskMemFree(strret.pOleStr); 65 | } else { 66 | pOutput->iconInfo = L"missing,0"; 67 | hr = E_UNEXPECTED; 68 | } 69 | } else { 70 | pOutput->iconInfo = L"missing,0"; 71 | hr = E_UNEXPECTED; 72 | } 73 | PropVariantClear(&varIconPath); 74 | } 75 | 76 | return hr; 77 | } 78 | 79 | std::vector AudioManager::EnumAudioDevices(EDataFlow deviceType, ERole eRole) { 80 | std::vector output; 81 | ScopedCoInitialize com; 82 | if (!com.succeeded()) { 83 | OutputDebugString(L"Failed to initialize COM\n"); 84 | return output; 85 | } 86 | 87 | ComPtr pEnumerator; 88 | HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&pEnumerator); 89 | if (FAILED(hr) || !pEnumerator) { 90 | return output; 91 | } 92 | 93 | ComPtr pActive; 94 | std::wstring activeDevID; 95 | 96 | hr = pEnumerator->GetDefaultAudioEndpoint(deviceType, eRole, &pActive); 97 | if (SUCCEEDED(hr) && pActive) { 98 | LPWSTR activeID = nullptr; 99 | hr = pActive->GetId(&activeID); 100 | if (SUCCEEDED(hr) && activeID) { 101 | activeDevID = activeID; 102 | CoTaskMemFree(activeID); 103 | } 104 | } 105 | 106 | ComPtr pCollection; 107 | hr = pEnumerator->EnumAudioEndpoints(deviceType, DEVICE_STATE_ACTIVE, &pCollection); 108 | if (FAILED(hr) || !pCollection) { 109 | return output; 110 | } 111 | 112 | UINT cEndpoints = 0; 113 | hr = pCollection->GetCount(&cEndpoints); 114 | if (FAILED(hr)) { 115 | return output; 116 | } 117 | 118 | for (UINT n = 0; n < cEndpoints; ++n) { 119 | ComPtr pDevice; 120 | hr = pCollection->Item(n, &pDevice); 121 | if (FAILED(hr) || !pDevice) { 122 | continue; 123 | } 124 | 125 | DeviceProps device; 126 | if (SUCCEEDED(GetDeviceProperty(pDevice.Get(), &device))) { 127 | LPWSTR id = nullptr; 128 | hr = pDevice->GetId(&id); 129 | if (SUCCEEDED(hr) && id) { 130 | std::wstring currentID(id); 131 | device.id = currentID; 132 | device.isActive = (currentID == activeDevID); 133 | CoTaskMemFree(id); 134 | } 135 | output.push_back(device); 136 | } 137 | } 138 | 139 | return output; 140 | } 141 | 142 | DeviceProps AudioManager::GetDefaultDevice(EDataFlow deviceType, ERole eRole) { 143 | DeviceProps activeDevice; 144 | ScopedCoInitialize com; 145 | if (com.succeeded()) { 146 | ComPtr pEnumerator; 147 | HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&pEnumerator); 148 | if (SUCCEEDED(hr) && pEnumerator) { 149 | ComPtr pActive; 150 | hr = pEnumerator->GetDefaultAudioEndpoint(deviceType, eRole, &pActive); 151 | if (SUCCEEDED(hr) && pActive) { 152 | GetDeviceProperty(pActive.Get(), &activeDevice); 153 | LPWSTR aid; 154 | hr = pActive->GetId(&aid); 155 | if (SUCCEEDED(hr) && aid != nullptr) { 156 | activeDevice.id = aid; 157 | CoTaskMemFree(aid); 158 | } 159 | } 160 | } 161 | } 162 | return activeDevice; 163 | } 164 | 165 | HRESULT AudioManager::SetDefaultDevice(LPWSTR devID, bool console, bool multimedia, bool communications) { 166 | ComPtr pPolicyConfig; 167 | HRESULT hr = CoCreateInstance(__uuidof(CPolicyConfigVistaClient), 168 | NULL, CLSCTX_ALL, __uuidof(IPolicyConfigVista), (LPVOID *)&pPolicyConfig); 169 | if (SUCCEEDED(hr) && pPolicyConfig) { 170 | if (console) 171 | hr = pPolicyConfig->SetDefaultEndpoint(devID, eConsole); 172 | if (multimedia) 173 | hr = pPolicyConfig->SetDefaultEndpoint(devID, eMultimedia); 174 | if (communications) 175 | hr = pPolicyConfig->SetDefaultEndpoint(devID, eCommunications); 176 | } 177 | return hr; 178 | } 179 | 180 | float AudioManager::GetVolume(EDataFlow deviceType, ERole eRole) { 181 | float volumeLevel = 0.0f; 182 | ScopedCoInitialize com; 183 | if (com.succeeded()) { 184 | ComPtr enumerator; 185 | HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&enumerator); 186 | if (SUCCEEDED(hr) && enumerator) { 187 | ComPtr device; 188 | hr = enumerator->GetDefaultAudioEndpoint(deviceType, eRole, &device); 189 | if (SUCCEEDED(hr) && device) { 190 | ComPtr volumeControl; 191 | hr = device->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr, (void**)&volumeControl); 192 | if (SUCCEEDED(hr) && volumeControl) { 193 | hr = volumeControl->GetMasterVolumeLevelScalar(&volumeLevel); 194 | } 195 | } 196 | } 197 | } 198 | return volumeLevel; 199 | } 200 | 201 | bool AudioManager::SetVolume(float volumeLevel, EDataFlow deviceType, ERole eRole) { 202 | ScopedCoInitialize com; 203 | if (com.succeeded()) { 204 | ComPtr pEnumerator; 205 | HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&pEnumerator); 206 | if (SUCCEEDED(hr) && pEnumerator) { 207 | ComPtr pActive; 208 | pEnumerator->GetDefaultAudioEndpoint(deviceType, eRole, &pActive); 209 | if (pActive) { 210 | ComPtr m_spVolumeControl; 211 | hr = pActive->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (void **)&m_spVolumeControl); 212 | if (SUCCEEDED(hr) && m_spVolumeControl) { 213 | if (volumeLevel > 1.0f) 214 | volumeLevel = volumeLevel / 100.0f; 215 | m_spVolumeControl->SetMasterVolumeLevelScalar(volumeLevel, NULL); 216 | } 217 | } 218 | } 219 | } 220 | return true; 221 | } 222 | 223 | int AudioManager::SwitchDefaultDevice(EDataFlow deviceType, ERole role, bool console, bool multimedia, bool communications) { 224 | std::vector devices = EnumAudioDevices(deviceType, role); 225 | if (devices.empty()) { 226 | return 0; // No devices found 227 | } 228 | 229 | std::wstring activeDeviceId; 230 | for (const auto &device : devices) { 231 | if (!activeDeviceId.empty()) { 232 | activeDeviceId = device.id; 233 | break; 234 | } 235 | if (device.isActive) { 236 | activeDeviceId = L"x"; 237 | } 238 | } 239 | 240 | if (activeDeviceId.empty() || activeDeviceId == L"x") { 241 | activeDeviceId = devices[0].id; 242 | } 243 | 244 | try { 245 | SetDefaultDevice((LPWSTR)activeDeviceId.c_str(), console, multimedia, communications); 246 | return 1; // Success 247 | } catch (const std::exception &) { 248 | return 0; // Failed 249 | } 250 | } 251 | 252 | bool AudioManager::SetAudioDeviceVolume(float volumeLevel, LPWSTR devID) { 253 | ScopedCoInitialize com; 254 | if (!com.succeeded()) return false; 255 | 256 | ComPtr pEnumerator; 257 | HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnumerator)); 258 | if (FAILED(hr)) return false; 259 | 260 | ComPtr pDevice; 261 | hr = pEnumerator->GetDevice(devID, &pDevice); 262 | if (FAILED(hr)) return false; 263 | 264 | ComPtr pAudioVolume; 265 | hr = pDevice->Activate(IID_IAudioEndpointVolume, CLSCTX_ALL, NULL, (void**)&pAudioVolume); 266 | if (FAILED(hr)) return false; 267 | 268 | hr = pAudioVolume->SetMasterVolumeLevelScalar(volumeLevel, NULL); 269 | return SUCCEEDED(hr); 270 | } 271 | 272 | // Session API implementations 273 | 274 | ComPtr AudioManager::GetAudioSessionEnumerator(ERole eRole) { 275 | ComPtr deviceEnumerator; 276 | HRESULT x = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)&deviceEnumerator); 277 | if (FAILED(x) || !deviceEnumerator) return nullptr; 278 | 279 | ComPtr device; 280 | deviceEnumerator->GetDefaultAudioEndpoint(eRender, eRole, &device); 281 | if (!device) return nullptr; 282 | 283 | ComPtr sessionManager; 284 | device->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL, nullptr, (void **)&sessionManager); 285 | if (!sessionManager) return nullptr; 286 | 287 | ComPtr enumerator; 288 | sessionManager->GetSessionEnumerator(&enumerator); 289 | 290 | return enumerator; 291 | } 292 | 293 | std::string AudioManager::GetProcessNameFromPid(DWORD pid) { 294 | HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); 295 | if (handle) { 296 | TCHAR buffer[MAX_PATH]; 297 | if (GetModuleFileNameEx(handle, NULL, buffer, sizeof(buffer))) { 298 | CloseHandle(handle); 299 | std::wstring test(&buffer[0]); 300 | return Encoding::WideToUtf8(test); 301 | } 302 | CloseHandle(handle); 303 | } 304 | return ""; 305 | } 306 | 307 | float AudioManager::GetSetProcessMasterVolume(IAudioSessionControl *session, float volume) { 308 | ComPtr info; 309 | session->QueryInterface(__uuidof(ISimpleAudioVolume), (void **)&info); 310 | if (!info) return 0.0f; 311 | 312 | if (volume != 0.00) { 313 | info->SetMasterVolume(volume, NULL); 314 | } 315 | float maxVolume = 0.0f; 316 | info->GetMasterVolume(&maxVolume); 317 | return maxVolume; 318 | } 319 | 320 | float AudioManager::GetPeakVolume(IAudioSessionControl *session) { 321 | if (!session) return 0.0f; 322 | ComPtr info; 323 | session->QueryInterface(__uuidof(IAudioMeterInformation), (void **)&info); 324 | if (!info) return 0.0f; 325 | 326 | float peakVolume = 0.0f; 327 | info->GetPeakValue(&peakVolume); 328 | return peakVolume; 329 | } 330 | 331 | std::vector AudioManager::GetProcessVolumes(ERole eRole, int pID, float volume) { 332 | std::vector volumes; 333 | ScopedCoInitialize com; 334 | if (com.succeeded()) { 335 | ComPtr enumerator = GetAudioSessionEnumerator(eRole); 336 | if (!enumerator) return volumes; 337 | 338 | int sessionCount = 0; 339 | enumerator->GetCount(&sessionCount); 340 | 341 | std::map> sessionMap; 342 | 343 | for (int index = 0; index < sessionCount; index++) { 344 | ComPtr session; 345 | ComPtr session2; 346 | enumerator->GetSession(index, &session); 347 | if (!session) continue; 348 | 349 | session->QueryInterface(__uuidof(IAudioSessionControl2), (void **)&session2); 350 | if (!session2) continue; 351 | 352 | DWORD id = 0; 353 | if (FAILED(session2->GetProcessId(&id))) continue; 354 | 355 | if (id == 0) continue; 356 | 357 | // Use system defined AudioSessionState 358 | AudioSessionState state = AudioSessionStateInactive; 359 | if (FAILED(session->GetState(&state))) state = AudioSessionStateInactive; 360 | if (state == AudioSessionStateExpired) continue; 361 | 362 | if (pID == (int)id && volume != 0.00) { 363 | GetSetProcessMasterVolume(session.Get(), volume); 364 | continue; 365 | } 366 | 367 | if (pID != 0 && volume != 0.00) continue; 368 | 369 | std::string processPath = GetProcessNameFromPid(id); 370 | float maxVolume = GetSetProcessMasterVolume(session.Get()); 371 | float peakVolume = GetPeakVolume(session.Get()); 372 | 373 | ProcessVolume data; 374 | data.processPath = processPath; 375 | data.processId = (int)id; 376 | data.maxVolume = maxVolume; 377 | data.peakVolume = peakVolume; 378 | 379 | if (sessionMap.find(id) == sessionMap.end()) { 380 | sessionMap[id] = {data, (int)state}; 381 | } else { 382 | auto& existingPair = sessionMap[id]; 383 | bool currentActive = (state == AudioSessionStateActive); 384 | bool existingActive = (existingPair.second == (int)AudioSessionStateActive); 385 | 386 | if (currentActive && !existingActive) { 387 | existingPair = {data, (int)state}; 388 | } else if (currentActive == existingActive) { 389 | if (data.peakVolume > existingPair.first.peakVolume) { 390 | existingPair = {data, (int)state}; 391 | } 392 | } 393 | } 394 | } 395 | 396 | if (pID != 0 && volume != 0.00) { 397 | return std::vector{}; 398 | } 399 | 400 | for (auto const& [pid, val] : sessionMap) { 401 | volumes.push_back(val.first); 402 | } 403 | 404 | return volumes; 405 | } 406 | return std::vector{}; 407 | } 408 | 409 | bool AudioManager::SetProcessVolumeByPath(ERole eRole, std::string path, float volume) { 410 | ScopedCoInitialize com; 411 | if (com.succeeded()) { 412 | ComPtr enumerator = GetAudioSessionEnumerator(eRole); 413 | if (!enumerator) return false; 414 | 415 | int sessionCount = 0; 416 | enumerator->GetCount(&sessionCount); 417 | for (int index = 0; index < sessionCount; index++) { 418 | ComPtr session; 419 | ComPtr session2; 420 | enumerator->GetSession(index, &session); 421 | if (!session) continue; 422 | 423 | session->QueryInterface(__uuidof(IAudioSessionControl2), (void **)&session2); 424 | if (!session2) continue; 425 | 426 | DWORD id = 0; 427 | session2->GetProcessId(&id); 428 | std::string processPath = ""; 429 | if ((int)id != 0) 430 | processPath = GetProcessNameFromPid(id); 431 | else { 432 | continue; 433 | } 434 | if (path == processPath) { 435 | GetSetProcessMasterVolume(session.Get(), volume); 436 | break; 437 | } 438 | } 439 | } 440 | return true; 441 | } 442 | 443 | int AudioManager::GetSampleRate(LPWSTR devID) { 444 | ScopedCoInitialize com; 445 | if (!com.succeeded()) return 0; 446 | 447 | ComPtr pEnumerator; 448 | HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnumerator)); 449 | if (FAILED(hr)) return 0; 450 | 451 | ComPtr pDevice; 452 | hr = pEnumerator->GetDevice(devID, &pDevice); 453 | if (FAILED(hr)) return 0; 454 | 455 | ComPtr pAudioClient; 456 | hr = pDevice->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, (void**)&pAudioClient); 457 | if (FAILED(hr)) return 0; 458 | 459 | WAVEFORMATEX* pwfx = nullptr; 460 | hr = pAudioClient->GetMixFormat(&pwfx); 461 | if (FAILED(hr) || !pwfx) return 0; 462 | 463 | int sampleRate = pwfx->nSamplesPerSec; 464 | CoTaskMemFree(pwfx); 465 | 466 | return sampleRate; 467 | } 468 | 469 | bool AudioManager::SetSampleRate(LPWSTR devID, int sampleRate) { 470 | ScopedCoInitialize com; 471 | if (!com.succeeded()) return false; 472 | 473 | ComPtr pEnumerator; 474 | HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnumerator)); 475 | if (FAILED(hr)) return false; 476 | 477 | ComPtr pDevice; 478 | hr = pEnumerator->GetDevice(devID, &pDevice); 479 | if (FAILED(hr)) return false; 480 | 481 | ComPtr pPropertyStore; 482 | hr = pDevice->OpenPropertyStore(STGM_READWRITE, &pPropertyStore); 483 | if (FAILED(hr)) return false; 484 | 485 | // Get current format 486 | PROPVARIANT varFormat; 487 | PropVariantInit(&varFormat); 488 | hr = pPropertyStore->GetValue(PKEY_AudioEngine_DeviceFormat, &varFormat); 489 | if (FAILED(hr)) { 490 | PropVariantClear(&varFormat); 491 | return false; 492 | } 493 | 494 | if (varFormat.vt != VT_BLOB || !varFormat.blob.pBlobData) { 495 | PropVariantClear(&varFormat); 496 | return false; 497 | } 498 | 499 | // Copy the format and modify sample rate 500 | WAVEFORMATEX* pwfx = (WAVEFORMATEX*)varFormat.blob.pBlobData; 501 | 502 | // Create a new format with the desired sample rate 503 | std::vector formatBuffer(sizeof(WAVEFORMATEXTENSIBLE)); 504 | WAVEFORMATEXTENSIBLE* pWfxEx = (WAVEFORMATEXTENSIBLE*)formatBuffer.data(); 505 | 506 | if (pwfx->wFormatTag == WAVE_FORMAT_EXTENSIBLE) { 507 | memcpy(pWfxEx, pwfx, sizeof(WAVEFORMATEXTENSIBLE)); 508 | } else { 509 | // Convert to extensible format 510 | pWfxEx->Format = *pwfx; 511 | pWfxEx->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; 512 | pWfxEx->Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX); 513 | pWfxEx->Samples.wValidBitsPerSample = pwfx->wBitsPerSample; 514 | pWfxEx->dwChannelMask = pwfx->nChannels == 2 ? 3 : 0; // Stereo or mono 515 | pWfxEx->SubFormat = pwfx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT ? 516 | KSDATAFORMAT_SUBTYPE_IEEE_FLOAT : KSDATAFORMAT_SUBTYPE_PCM; 517 | } 518 | 519 | // Update sample rate and dependent fields 520 | pWfxEx->Format.nSamplesPerSec = sampleRate; 521 | pWfxEx->Format.nAvgBytesPerSec = sampleRate * pWfxEx->Format.nBlockAlign; 522 | 523 | PropVariantClear(&varFormat); 524 | 525 | // Set the new format 526 | PROPVARIANT varNewFormat; 527 | PropVariantInit(&varNewFormat); 528 | varNewFormat.vt = VT_BLOB; 529 | varNewFormat.blob.cbSize = sizeof(WAVEFORMATEXTENSIBLE); 530 | varNewFormat.blob.pBlobData = (BYTE*)pWfxEx; 531 | 532 | hr = pPropertyStore->SetValue(PKEY_AudioEngine_DeviceFormat, varNewFormat); 533 | if (FAILED(hr)) { 534 | return false; 535 | } 536 | 537 | hr = pPropertyStore->Commit(); 538 | return SUCCEEDED(hr); 539 | } 540 | 541 | } // namespace win32audio 542 | -------------------------------------------------------------------------------- /windows/win32audio_plugin_c_api.cpp: -------------------------------------------------------------------------------- 1 | #include "include/win32audio/win32audio_plugin_c_api.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "audio_manager.h" 13 | #include "icon_manager.h" 14 | #include "include/encoding.h" 15 | 16 | using namespace win32audio; 17 | 18 | namespace { 19 | 20 | constexpr wchar_t kWindowClassName[] = L"Win32AudioPluginMessageWindow"; 21 | constexpr UINT WM_AUDIO_EVENT = WM_USER + 101; 22 | 23 | struct AudioEvent { 24 | std::string name; 25 | std::string id; 26 | }; 27 | 28 | } // namespace 29 | 30 | class Win32audioPlugin : public flutter::Plugin, public IMMNotificationClient { 31 | public: 32 | static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar) { 33 | auto channel = std::make_unique>( 34 | registrar->messenger(), "win32audio", 35 | &flutter::StandardMethodCodec::GetInstance()); 36 | 37 | auto plugin = std::make_unique(std::move(channel)); 38 | plugin->channel_->SetMethodCallHandler( 39 | [plugin_pointer = plugin.get()](const auto &call, auto result) { 40 | plugin_pointer->HandleMethodCall(call, std::move(result)); 41 | }); 42 | 43 | registrar->AddPlugin(std::move(plugin)); 44 | } 45 | 46 | Win32audioPlugin(std::unique_ptr> channel) 47 | : channel_(std::move(channel)) {} 48 | 49 | ~Win32audioPlugin() override { 50 | Dispose(); 51 | } 52 | 53 | private: 54 | HWND window_handle_ = nullptr; 55 | std::unique_ptr> channel_; 56 | ComPtr device_enumerator_; 57 | ULONG ref_count_ = 1; 58 | 59 | void InitWindow() { 60 | WNDCLASS wc = {}; 61 | wc.lpfnWndProc = Win32audioPlugin::WndProc; 62 | wc.lpszClassName = kWindowClassName; 63 | wc.hInstance = GetModuleHandle(nullptr); 64 | RegisterClass(&wc); 65 | 66 | window_handle_ = CreateWindowEx(0, kWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, GetModuleHandle(nullptr), nullptr); 67 | if (window_handle_) { 68 | SetWindowLongPtr(window_handle_, GWLP_USERDATA, reinterpret_cast(this)); 69 | } 70 | } 71 | 72 | void DestroyHiddenWindow() { 73 | if (window_handle_) { 74 | DestroyWindow(window_handle_); 75 | window_handle_ = nullptr; 76 | } 77 | } 78 | 79 | static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { 80 | if (message == WM_AUDIO_EVENT) { 81 | auto* plugin = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); 82 | auto* event = reinterpret_cast(lparam); 83 | if (plugin && event) { 84 | plugin->HandleAudioEvent(event->name, event->id); 85 | } 86 | delete event; 87 | return 0; 88 | } 89 | return DefWindowProc(hwnd, message, wparam, lparam); 90 | } 91 | 92 | void HandleAudioEvent(const std::string& name, const std::string& id) { 93 | flutter::EncodableMap message = { 94 | {flutter::EncodableValue("name"), flutter::EncodableValue(name)}, 95 | {flutter::EncodableValue("id"), flutter::EncodableValue(id)}, 96 | }; 97 | channel_->InvokeMethod("onAudioDeviceChange", std::make_unique(message)); 98 | } 99 | 100 | void HandleMethodCall(const flutter::MethodCall &method_call, 101 | std::unique_ptr> result) { 102 | if (method_call.method_name().compare("initAudioListener") == 0) { 103 | Initialize(); 104 | result->Success(flutter::EncodableValue(true)); 105 | } else if (method_call.method_name().compare("enumAudioDevices") == 0) { 106 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 107 | int deviceType = std::get(args.at(flutter::EncodableValue("deviceType"))); 108 | int role = std::get(args.at(flutter::EncodableValue("role"))); 109 | 110 | std::vector devices = AudioManager::EnumAudioDevices((EDataFlow)deviceType, (ERole)role); 111 | 112 | flutter::EncodableMap map; 113 | int i = 0; 114 | for (const auto &device : devices) { 115 | flutter::EncodableMap deviceMap; 116 | deviceMap[flutter::EncodableValue("id")] = flutter::EncodableValue(Encoding::WideToUtf8(device.id)); 117 | deviceMap[flutter::EncodableValue("name")] = flutter::EncodableValue(Encoding::WideToUtf8(device.name)); 118 | deviceMap[flutter::EncodableValue("iconInfo")] = flutter::EncodableValue(Encoding::WideToUtf8(device.iconInfo)); 119 | deviceMap[flutter::EncodableValue("isActive")] = flutter::EncodableValue(device.isActive); 120 | map[i] = flutter::EncodableValue(deviceMap); 121 | i++; 122 | } 123 | result->Success(flutter::EncodableValue(map)); 124 | } else if (method_call.method_name().compare("getDefaultDevice") == 0) { 125 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 126 | int deviceType = std::get(args.at(flutter::EncodableValue("deviceType"))); 127 | int role = std::get(args.at(flutter::EncodableValue("role"))); 128 | 129 | DeviceProps device = AudioManager::GetDefaultDevice((EDataFlow)deviceType, (ERole)role); 130 | 131 | flutter::EncodableMap deviceMap; 132 | deviceMap[flutter::EncodableValue("id")] = flutter::EncodableValue(Encoding::WideToUtf8(device.id)); 133 | deviceMap[flutter::EncodableValue("name")] = flutter::EncodableValue(Encoding::WideToUtf8(device.name)); 134 | deviceMap[flutter::EncodableValue("iconInfo")] = flutter::EncodableValue(Encoding::WideToUtf8(device.iconInfo)); 135 | deviceMap[flutter::EncodableValue("isActive")] = flutter::EncodableValue(device.isActive); 136 | result->Success(flutter::EncodableValue(deviceMap)); 137 | } else if (method_call.method_name().compare("setDefaultAudioDevice") == 0) { 138 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 139 | std::string deviceID = std::get(args.at(flutter::EncodableValue("deviceID"))); 140 | bool console = std::get(args.at(flutter::EncodableValue("console"))); 141 | bool multimedia = std::get(args.at(flutter::EncodableValue("multimedia"))); 142 | bool communications = std::get(args.at(flutter::EncodableValue("communications"))); 143 | std::wstring deviceIDW = Encoding::Utf8ToWide(deviceID); 144 | 145 | HRESULT nativeFuncResult = AudioManager::SetDefaultDevice((LPWSTR)deviceIDW.c_str(), console, multimedia, communications); 146 | result->Success(flutter::EncodableValue((int)nativeFuncResult)); 147 | } else if (method_call.method_name().compare("getAudioVolume") == 0) { 148 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 149 | int deviceType = std::get(args.at(flutter::EncodableValue("deviceType"))); 150 | int role = std::get(args.at(flutter::EncodableValue("role"))); 151 | 152 | float nativeFuncResult = AudioManager::GetVolume((EDataFlow)deviceType, (ERole)role); 153 | result->Success(flutter::EncodableValue((double)nativeFuncResult)); 154 | } else if (method_call.method_name().compare("setAudioVolume") == 0) { 155 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 156 | int deviceType = std::get(args.at(flutter::EncodableValue("deviceType"))); 157 | int role = std::get(args.at(flutter::EncodableValue("role"))); 158 | double volumeLevel = std::get(args.at(flutter::EncodableValue("volumeLevel"))); 159 | 160 | AudioManager::SetVolume((float)volumeLevel, (EDataFlow)deviceType, (ERole)role); 161 | result->Success(flutter::EncodableValue((int)1)); 162 | } else if (method_call.method_name().compare("switchDefaultDevice") == 0) { 163 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 164 | int deviceType = std::get(args.at(flutter::EncodableValue("deviceType"))); 165 | int role = std::get(args.at(flutter::EncodableValue("role"))); 166 | bool console = std::get(args.at(flutter::EncodableValue("console"))); 167 | bool multimedia = std::get(args.at(flutter::EncodableValue("multimedia"))); 168 | bool communications = std::get(args.at(flutter::EncodableValue("communications"))); 169 | 170 | bool nativeFuncResult = AudioManager::SwitchDefaultDevice((EDataFlow)deviceType, (ERole)role, console, multimedia, communications); 171 | result->Success(flutter::EncodableValue(nativeFuncResult)); 172 | } else if (method_call.method_name().compare("setAudioDeviceVolume") == 0) { 173 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 174 | std::string deviceID = std::get(args.at(flutter::EncodableValue("deviceID"))); 175 | double volumeLevel = std::get(args.at(flutter::EncodableValue("volumeLevel"))); 176 | std::wstring deviceIDW = Encoding::Utf8ToWide(deviceID); 177 | 178 | AudioManager::SetAudioDeviceVolume((float)volumeLevel, (LPWSTR)deviceIDW.c_str()); 179 | result->Success(flutter::EncodableValue((int)1)); 180 | } else if (method_call.method_name().compare("iconToBytes") == 0) { 181 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 182 | std::string iconLocation = std::get(args.at(flutter::EncodableValue("iconLocation"))); 183 | int iconID = std::get(args.at(flutter::EncodableValue("iconID"))); 184 | std::wstring iconLocationW = Encoding::Utf8ToWide(iconLocation); 185 | 186 | HICON icon = IconManager::GetIconFromFile(iconLocationW, iconID); 187 | 188 | if (icon) { 189 | std::vector buff; 190 | bool resultIcon = IconManager::GetIconData(icon, 32, buff); 191 | if (!resultIcon) { 192 | buff.clear(); 193 | resultIcon = IconManager::GetIconData(icon, 24, buff); 194 | } 195 | 196 | DestroyIcon(icon); 197 | 198 | if (resultIcon) { 199 | std::vector buff_uint8(buff.begin(), buff.end()); 200 | result->Success(flutter::EncodableValue(buff_uint8)); 201 | } else { 202 | std::vector iconBytes = {204, 204, 204}; 203 | result->Success(flutter::EncodableValue(iconBytes)); 204 | } 205 | } else { 206 | std::vector iconBytes = {204, 204, 204}; 207 | result->Success(flutter::EncodableValue(iconBytes)); 208 | } 209 | 210 | } else if (method_call.method_name().compare("getWindowIcon") == 0) { 211 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 212 | int hWND = std::get(args.at(flutter::EncodableValue("hWnd"))); 213 | 214 | LRESULT iconResult = SendMessage((HWND)((LONG_PTR)hWND), WM_GETICON, 2, 0); 215 | if (iconResult == 0) 216 | iconResult = GetClassLongPtr((HWND)((LONG_PTR)hWND), -14); 217 | 218 | if (iconResult != 0) { 219 | HICON icon = (HICON)iconResult; 220 | std::vector buff; 221 | bool resultIcon = IconManager::GetIconData(icon, 32, buff); 222 | if (!resultIcon) { 223 | buff.clear(); 224 | resultIcon = IconManager::GetIconData(icon, 24, buff); 225 | } 226 | 227 | if (resultIcon) { 228 | std::vector buff_uint8(buff.begin(), buff.end()); 229 | result->Success(flutter::EncodableValue(buff_uint8)); 230 | } else { 231 | std::vector iconBytes = {204, 204, 204}; 232 | result->Success(flutter::EncodableValue(iconBytes)); 233 | } 234 | } else { 235 | std::vector iconBytes = {204, 204, 204}; 236 | result->Success(flutter::EncodableValue(iconBytes)); 237 | } 238 | } else if (method_call.method_name().compare("getIconPng") == 0) { 239 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 240 | int hIcon = std::get(args.at(flutter::EncodableValue("hIcon"))); 241 | std::vector buff; 242 | bool resultIcon = IconManager::GetIconData((HICON)((LONG_PTR)hIcon), 32, buff); 243 | if (!resultIcon) { 244 | buff.clear(); 245 | resultIcon = IconManager::GetIconData((HICON)((LONG_PTR)hIcon), 24, buff); 246 | } 247 | if (resultIcon) { 248 | std::vector buff_uint8(buff.begin(), buff.end()); 249 | result->Success(flutter::EncodableValue(buff_uint8)); 250 | } else { 251 | std::vector iconBytes = {204, 204, 204}; 252 | result->Success(flutter::EncodableValue(iconBytes)); 253 | } 254 | } else if (method_call.method_name().compare("enumAudioMixer") == 0) { 255 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 256 | int role = std::get(args.at(flutter::EncodableValue("role"))); 257 | 258 | std::vector devices = AudioManager::GetProcessVolumes((ERole)role); 259 | 260 | flutter::EncodableMap map; 261 | for (const auto &device : devices) { 262 | flutter::EncodableMap deviceMap; 263 | deviceMap[flutter::EncodableValue("processId")] = flutter::EncodableValue(device.processId); 264 | deviceMap[flutter::EncodableValue("processPath")] = flutter::EncodableValue(device.processPath); 265 | deviceMap[flutter::EncodableValue("maxVolume")] = flutter::EncodableValue(device.maxVolume); 266 | deviceMap[flutter::EncodableValue("peakVolume")] = flutter::EncodableValue(device.peakVolume); 267 | map[flutter::EncodableValue(device.processId)] = flutter::EncodableValue(deviceMap); 268 | } 269 | result->Success(flutter::EncodableValue(map)); 270 | } else if (method_call.method_name().compare("setAudioMixerVolume") == 0) { 271 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 272 | int role = std::get(args.at(flutter::EncodableValue("role"))); 273 | int processID = std::get(args.at(flutter::EncodableValue("processID"))); 274 | double volumeLevel = std::get(args.at(flutter::EncodableValue("volumeLevel"))); 275 | 276 | AudioManager::GetProcessVolumes((ERole)role, processID, (float)volumeLevel); 277 | result->Success(flutter::EncodableValue(true)); 278 | } else if (method_call.method_name().compare("setAudioMixerVolumeByPath") == 0) { 279 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 280 | int role = std::get(args.at(flutter::EncodableValue("role"))); 281 | std::string processPath = std::get(args.at(flutter::EncodableValue("processPath"))); 282 | double volumeLevel = std::get(args.at(flutter::EncodableValue("volumeLevel"))); 283 | 284 | bool nativeFuncResult = AudioManager::SetProcessVolumeByPath((ERole)role, processPath, (float)volumeLevel); 285 | result->Success(flutter::EncodableValue(nativeFuncResult)); 286 | } else if (method_call.method_name().compare("getSampleRate") == 0) { 287 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 288 | std::string deviceID = std::get(args.at(flutter::EncodableValue("deviceID"))); 289 | std::wstring deviceIDW = Encoding::Utf8ToWide(deviceID); 290 | 291 | int sampleRate = AudioManager::GetSampleRate((LPWSTR)deviceIDW.c_str()); 292 | result->Success(flutter::EncodableValue(sampleRate)); 293 | } else if (method_call.method_name().compare("setSampleRate") == 0) { 294 | const flutter::EncodableMap &args = std::get(*method_call.arguments()); 295 | std::string deviceID = std::get(args.at(flutter::EncodableValue("deviceID"))); 296 | int sampleRate = std::get(args.at(flutter::EncodableValue("sampleRate"))); 297 | std::wstring deviceIDW = Encoding::Utf8ToWide(deviceID); 298 | 299 | bool success = AudioManager::SetSampleRate((LPWSTR)deviceIDW.c_str(), sampleRate); 300 | result->Success(flutter::EncodableValue(success)); 301 | } else { 302 | result->NotImplemented(); 303 | } 304 | } 305 | 306 | void Initialize() { 307 | if (device_enumerator_) return; 308 | 309 | CoInitialize(nullptr); 310 | InitWindow(); 311 | HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, 312 | IID_PPV_ARGS(&device_enumerator_)); 313 | 314 | if (SUCCEEDED(hr) && device_enumerator_) { 315 | device_enumerator_->RegisterEndpointNotificationCallback(this); 316 | } 317 | } 318 | 319 | void Dispose() { 320 | if (device_enumerator_) { 321 | device_enumerator_->UnregisterEndpointNotificationCallback(this); 322 | device_enumerator_ = nullptr; 323 | } 324 | DestroyHiddenWindow(); 325 | } 326 | 327 | // IMMNotificationClient methods 328 | STDMETHODIMP OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state) override { 329 | if (window_handle_) { 330 | AudioEvent* event = new AudioEvent{"OnDeviceStateChanged", Encoding::WideToUtf8(device_id)}; 331 | PostMessage(window_handle_, WM_AUDIO_EVENT, 0, (LPARAM)event); 332 | } 333 | return S_OK; 334 | } 335 | 336 | STDMETHODIMP OnDeviceAdded(LPCWSTR device_id) override { 337 | if (window_handle_) { 338 | AudioEvent* event = new AudioEvent{"OnDeviceAdded", Encoding::WideToUtf8(device_id)}; 339 | PostMessage(window_handle_, WM_AUDIO_EVENT, 0, (LPARAM)event); 340 | } 341 | return S_OK; 342 | } 343 | 344 | STDMETHODIMP OnDeviceRemoved(LPCWSTR device_id) override { 345 | if (window_handle_) { 346 | AudioEvent* event = new AudioEvent{"OnDeviceRemoved", Encoding::WideToUtf8(device_id)}; 347 | PostMessage(window_handle_, WM_AUDIO_EVENT, 0, (LPARAM)event); 348 | } 349 | return S_OK; 350 | } 351 | 352 | STDMETHODIMP OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device_id) override { 353 | if (window_handle_) { 354 | std::string id = device_id ? Encoding::WideToUtf8(device_id) : ""; 355 | AudioEvent* event = new AudioEvent{"OnDefaultDeviceChanged", id}; 356 | PostMessage(window_handle_, WM_AUDIO_EVENT, 0, (LPARAM)event); 357 | } 358 | return S_OK; 359 | } 360 | 361 | STDMETHODIMP OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key) override { 362 | if (window_handle_) { 363 | AudioEvent* event = new AudioEvent{"OnPropertyValueChanged", Encoding::WideToUtf8(device_id)}; 364 | PostMessage(window_handle_, WM_AUDIO_EVENT, 0, (LPARAM)event); 365 | } 366 | return S_OK; 367 | } 368 | 369 | STDMETHODIMP QueryInterface(REFIID riid, void **ppv) override { 370 | if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMNotificationClient)) { 371 | *ppv = static_cast(this); 372 | AddRef(); 373 | return S_OK; 374 | } 375 | *ppv = nullptr; 376 | return E_NOINTERFACE; 377 | } 378 | 379 | STDMETHODIMP_(ULONG) AddRef() override { 380 | return InterlockedIncrement(&ref_count_); 381 | } 382 | 383 | STDMETHODIMP_(ULONG) Release() override { 384 | return InterlockedDecrement(&ref_count_); 385 | } 386 | }; 387 | 388 | void Win32audioPluginCApiRegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) { 389 | Win32audioPlugin::RegisterWithRegistrar( 390 | flutter::PluginRegistrarManager::GetInstance() 391 | ->GetRegistrar(registrar)); 392 | } 393 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # win32audio 2 | 3 | A comprehensive Flutter plugin for managing Windows audio devices, controlling volume, and extracting native icons. 4 | 5 | ![image](https://user-images.githubusercontent.com/20853986/175772236-0158ca3b-7dd0-41d4-b10e-7304f3be0b71.png) 6 | 7 | ## Features 8 | 9 | - 🎧 **Enumerate audio devices** (input/output) 10 | - 🔊 **Get and set master volume** for devices 11 | - 🎚️ **Control individual application volumes** (Audio Mixer) 12 | - 🔄 **Switch between audio devices** 13 | - 📊 **Get and set sample rates** for audio devices 14 | - 👂 **Listen to audio device changes** in real-time 15 | - 🎨 **Extract icons** from executables, DLLs, windows, and icon handles 16 | - 🎯 **Support for different audio roles** (Console, Multimedia, Communications) 17 | 18 | --- 19 | 20 | ## Table of Contents 21 | 22 | - [Installation](#installation) 23 | - [Quick Start](#quick-start) 24 | - [Audio Devices](#audio-devices) 25 | - [Enumerate Devices](#enumerate-devices) 26 | - [Get Default Device](#get-default-device) 27 | - [Set Default Device](#set-default-device) 28 | - [Switch Default Device](#switch-default-device) 29 | - [Volume Control](#volume-control) 30 | - [Get Volume](#get-volume) 31 | - [Set Volume](#set-volume) 32 | - [Set Device Volume](#set-device-volume) 33 | - [Audio Mixer (Per-Application Volume)](#audio-mixer-per-application-volume) 34 | - [Enumerate Audio Sessions](#enumerate-audio-sessions) 35 | - [Set Application Volume by Process ID](#set-application-volume-by-process-id) 36 | - [Set Application Volume by Path](#set-application-volume-by-path) 37 | - [Sample Rate Management](#sample-rate-management) 38 | - [Get Sample Rate](#get-sample-rate) 39 | - [Set Sample Rate](#set-sample-rate) 40 | - [Event Listeners](#event-listeners) 41 | - [Setup Change Listener](#setup-change-listener) 42 | - [Add Change Listener](#add-change-listener) 43 | - [Remove Change Listener](#remove-change-listener) 44 | - [Icon Extraction](#icon-extraction) 45 | - [Extract File Icon](#extract-file-icon) 46 | - [Extract Window Icon](#extract-window-icon) 47 | - [Extract Icon Handle](#extract-icon-handle) 48 | - [Data Structures](#data-structures) 49 | - [Complete Example](#complete-example) 50 | 51 | --- 52 | 53 | ## Installation 54 | 55 | Add this to your package's `pubspec.yaml` file: 56 | 57 | ```yaml 58 | dependencies: 59 | win32audio: ^latest_version 60 | ``` 61 | 62 | Then run: 63 | 64 | ```bash 65 | flutter pub get 66 | ``` 67 | 68 | --- 69 | 70 | ## Quick Start 71 | 72 | ```dart 73 | import 'package:win32audio/win32audio.dart'; 74 | 75 | void main() async { 76 | // Get all output devices 77 | List devices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 78 | 79 | // Get current volume 80 | double volume = await Audio.getVolume(AudioDeviceType.output); 81 | 82 | // Set volume to 50% 83 | await Audio.setVolume(0.5, AudioDeviceType.output); 84 | 85 | print('Found ${devices.length} output devices'); 86 | print('Current volume: ${(volume * 100).toStringAsFixed(0)}%'); 87 | } 88 | ``` 89 | 90 | --- 91 | 92 | ## Audio Devices 93 | 94 | ### Enumerate Devices 95 | 96 | Get a list of all available audio devices (input or output). 97 | 98 | ```dart 99 | // Get all output devices (speakers, headphones) 100 | List outputDevices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 101 | 102 | // Get all input devices (microphones) 103 | List inputDevices = await Audio.enumDevices(AudioDeviceType.input) ?? []; 104 | 105 | // Get devices for a specific audio role 106 | List commDevices = await Audio.enumDevices( 107 | AudioDeviceType.output, 108 | audioRole: AudioRole.communications, 109 | ) ?? []; 110 | 111 | // Print device information 112 | for (var device in outputDevices) { 113 | print('Device: ${device.name}'); 114 | print('ID: ${device.id}'); 115 | print('Is Active: ${device.isActive}'); 116 | print('Icon Path: ${device.iconPath}'); 117 | print('Icon ID: ${device.iconID}'); 118 | print('---'); 119 | } 120 | ``` 121 | 122 | ### Get Default Device 123 | 124 | Retrieve the current default audio device. 125 | 126 | ```dart 127 | // Get default output device 128 | AudioDevice? defaultOutput = await Audio.getDefaultDevice(AudioDeviceType.output); 129 | 130 | // Get default input device 131 | AudioDevice? defaultInput = await Audio.getDefaultDevice(AudioDeviceType.input); 132 | 133 | // Get default device for communications 134 | AudioDevice? defaultComm = await Audio.getDefaultDevice( 135 | AudioDeviceType.output, 136 | audioRole: AudioRole.communications, 137 | ); 138 | 139 | if (defaultOutput != null) { 140 | print('Default output device: ${defaultOutput.name}'); 141 | } 142 | ``` 143 | 144 | ### Set Default Device 145 | 146 | Set a specific device as the system default. 147 | 148 | ```dart 149 | // Get all devices 150 | List devices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 151 | 152 | if (devices.isNotEmpty) { 153 | // Set the first device as default for multimedia 154 | await Audio.setDefaultDevice( 155 | devices[0].id, 156 | multimedia: true, 157 | ); 158 | 159 | // Set as default for all roles 160 | await Audio.setDefaultDevice( 161 | devices[0].id, 162 | console: true, 163 | multimedia: true, 164 | communications: true, 165 | ); 166 | 167 | // Set as default only for communications (VoIP apps) 168 | await Audio.setDefaultDevice( 169 | devices[0].id, 170 | console: false, 171 | multimedia: false, 172 | communications: true, 173 | ); 174 | } 175 | ``` 176 | 177 | ### Switch Default Device 178 | 179 | Cycle to the next available audio device. 180 | 181 | ```dart 182 | // Switch to next output device 183 | await Audio.switchDefaultDevice(AudioDeviceType.output); 184 | 185 | // Switch to next input device 186 | await Audio.switchDefaultDevice(AudioDeviceType.input); 187 | 188 | // Switch with specific roles 189 | await Audio.switchDefaultDevice( 190 | AudioDeviceType.output, 191 | console: true, 192 | multimedia: true, 193 | communications: false, 194 | ); 195 | 196 | // Example: Toggle between devices with a button 197 | ElevatedButton( 198 | onPressed: () async { 199 | await Audio.switchDefaultDevice(AudioDeviceType.output); 200 | // Refresh UI 201 | setState(() {}); 202 | }, 203 | child: Text('Switch Audio Device'), 204 | ) 205 | ``` 206 | 207 | --- 208 | 209 | ## Volume Control 210 | 211 | ### Get Volume 212 | 213 | Get the master volume level (0.0 to 1.0). 214 | 215 | ```dart 216 | // Get output volume 217 | double outputVolume = await Audio.getVolume(AudioDeviceType.output); 218 | print('Output volume: ${(outputVolume * 100).toStringAsFixed(0)}%'); 219 | 220 | // Get input volume (microphone) 221 | double inputVolume = await Audio.getVolume(AudioDeviceType.input); 222 | print('Input volume: ${(inputVolume * 100).toStringAsFixed(0)}%'); 223 | 224 | // Get volume for specific audio role 225 | double commVolume = await Audio.getVolume( 226 | AudioDeviceType.output, 227 | audioRole: AudioRole.communications, 228 | ); 229 | ``` 230 | 231 | ### Set Volume 232 | 233 | Set the master volume level. 234 | 235 | ```dart 236 | // Set volume to 50% (0.5) 237 | await Audio.setVolume(0.5, AudioDeviceType.output); 238 | 239 | // Set volume to 75% 240 | await Audio.setVolume(0.75, AudioDeviceType.output); 241 | 242 | // You can also use percentage values (will be converted automatically) 243 | await Audio.setVolume(80, AudioDeviceType.output); // Sets to 80% 244 | 245 | // Set microphone volume 246 | await Audio.setVolume(0.6, AudioDeviceType.input); 247 | 248 | // Example: Volume slider 249 | Slider( 250 | value: currentVolume, 251 | min: 0.0, 252 | max: 1.0, 253 | divisions: 100, 254 | label: '${(currentVolume * 100).round()}%', 255 | onChanged: (double value) async { 256 | await Audio.setVolume(value, AudioDeviceType.output); 257 | setState(() { 258 | currentVolume = value; 259 | }); 260 | }, 261 | ) 262 | ``` 263 | 264 | ### Set Device Volume 265 | 266 | Set volume for a specific device by its ID. 267 | 268 | ```dart 269 | // Get all devices 270 | List devices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 271 | 272 | if (devices.isNotEmpty) { 273 | // Set volume for a specific device 274 | await Audio.setAudioDeviceVolume(devices[0].id, 0.7); 275 | 276 | // Set volume for multiple devices 277 | for (var device in devices) { 278 | await Audio.setAudioDeviceVolume(device.id, 0.5); 279 | } 280 | } 281 | ``` 282 | 283 | --- 284 | 285 | ## Audio Mixer (Per-Application Volume) 286 | 287 | ### Enumerate Audio Sessions 288 | 289 | Get a list of all applications currently using audio. 290 | 291 | ```dart 292 | // Get all audio sessions 293 | List audioSessions = await Audio.enumAudioMixer() ?? []; 294 | 295 | // Get sessions for specific audio role 296 | List commSessions = await Audio.enumAudioMixer( 297 | audioRole: AudioRole.communications, 298 | ) ?? []; 299 | 300 | // Print session information 301 | for (var session in audioSessions) { 302 | print('Process ID: ${session.processId}'); 303 | print('Process Path: ${session.processPath}'); 304 | print('Process Name: ${session.processPath.split('\\').last}'); 305 | print('Volume: ${(session.maxVolume * 100).toStringAsFixed(0)}%'); 306 | print('Peak Volume: ${(session.peakVolume * 100).toStringAsFixed(0)}%'); 307 | print('---'); 308 | } 309 | ``` 310 | 311 | ### Set Application Volume by Process ID 312 | 313 | Control volume for a specific application using its process ID. 314 | 315 | ```dart 316 | List sessions = await Audio.enumAudioMixer() ?? []; 317 | 318 | if (sessions.isNotEmpty) { 319 | // Set volume to 30% for the first application 320 | await Audio.setAudioMixerVolume(sessions[0].processId, 0.3); 321 | 322 | // Mute a specific application 323 | await Audio.setAudioMixerVolume(sessions[0].processId, 0.0); 324 | 325 | // Set to maximum volume 326 | await Audio.setAudioMixerVolume(sessions[0].processId, 1.0); 327 | } 328 | 329 | // Example: Find and control Chrome's volume 330 | for (var session in sessions) { 331 | if (session.processPath.toLowerCase().contains('chrome.exe')) { 332 | await Audio.setAudioMixerVolume(session.processId, 0.5); 333 | print('Set Chrome volume to 50%'); 334 | break; 335 | } 336 | } 337 | ``` 338 | 339 | ### Set Application Volume by Path 340 | 341 | Control volume for an application using its executable path. 342 | 343 | ```dart 344 | // Set volume for a specific application by path 345 | await Audio.setAudioMixerVolumeByPath( 346 | r'C:\Program Files\Google\Chrome\Application\chrome.exe', 347 | 0.4, 348 | ); 349 | 350 | // Set volume for Spotify 351 | await Audio.setAudioMixerVolumeByPath( 352 | r'C:\Users\YourName\AppData\Roaming\Spotify\Spotify.exe', 353 | 0.8, 354 | ); 355 | 356 | // With specific audio role 357 | await Audio.setAudioMixerVolumeByPath( 358 | r'C:\Program Files\Discord\Discord.exe', 359 | 0.6, 360 | audioRole: AudioRole.communications, 361 | ); 362 | ``` 363 | 364 | --- 365 | 366 | ## Sample Rate Management 367 | 368 | ### Get Sample Rate 369 | 370 | Get the current sample rate of an audio device. 371 | 372 | ```dart 373 | // Get all devices 374 | List devices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 375 | 376 | if (devices.isNotEmpty) { 377 | // Get sample rate for the first device 378 | int sampleRate = await Audio.getSampleRate(devices[0].id); 379 | 380 | if (sampleRate > 0) { 381 | print('Sample Rate: $sampleRate Hz'); 382 | } else { 383 | print('Failed to get sample rate'); 384 | } 385 | } 386 | 387 | // Example: Display sample rates for all devices 388 | for (var device in devices) { 389 | int rate = await Audio.getSampleRate(device.id); 390 | print('${device.name}: ${rate > 0 ? "$rate Hz" : "Unknown"}'); 391 | } 392 | ``` 393 | 394 | ### Set Sample Rate 395 | 396 | Change the sample rate of an audio device. 397 | 398 | ```dart 399 | List devices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 400 | 401 | if (devices.isNotEmpty) { 402 | String deviceId = devices[0].id; 403 | 404 | // Set to CD quality (44.1 kHz) 405 | bool success = await Audio.setSampleRate(deviceId, 44100); 406 | 407 | // Set to DVD quality (48 kHz) 408 | success = await Audio.setSampleRate(deviceId, 48000); 409 | 410 | // Set to high-res audio (96 kHz) 411 | success = await Audio.setSampleRate(deviceId, 96000); 412 | 413 | // Set to ultra high-res (192 kHz) 414 | success = await Audio.setSampleRate(deviceId, 192000); 415 | 416 | if (success) { 417 | print('Sample rate changed successfully'); 418 | } else { 419 | print('Failed to change sample rate. May require administrator privileges.'); 420 | } 421 | } 422 | 423 | // Example: Sample rate selector dialog 424 | Future showSampleRateDialog(BuildContext context, AudioDevice device) async { 425 | int currentRate = await Audio.getSampleRate(device.id); 426 | 427 | final rates = [44100, 48000, 96000, 192000]; 428 | 429 | showDialog( 430 | context: context, 431 | builder: (context) => AlertDialog( 432 | title: Text('Select Sample Rate'), 433 | content: Column( 434 | mainAxisSize: MainAxisSize.min, 435 | children: [ 436 | Text('Current: ${currentRate > 0 ? "$currentRate Hz" : "Unknown"}'), 437 | ...rates.map((rate) => ListTile( 438 | title: Text('$rate Hz'), 439 | onTap: () async { 440 | bool success = await Audio.setSampleRate(device.id, rate); 441 | Navigator.pop(context); 442 | ScaffoldMessenger.of(context).showSnackBar( 443 | SnackBar( 444 | content: Text(success 445 | ? 'Changed to $rate Hz' 446 | : 'Failed to change sample rate'), 447 | ), 448 | ); 449 | }, 450 | )), 451 | ], 452 | ), 453 | ), 454 | ); 455 | } 456 | ``` 457 | 458 | **Note:** Changing sample rates may require administrator privileges and will restart audio streams. 459 | 460 | --- 461 | 462 | ## Event Listeners 463 | 464 | ### Setup Change Listener 465 | 466 | Initialize the audio event listener system. 467 | 468 | ```dart 469 | void main() async { 470 | WidgetsFlutterBinding.ensureInitialized(); 471 | 472 | // Setup the change listener (required before adding listeners) 473 | await Audio.setupChangeListener(); 474 | 475 | runApp(MyApp()); 476 | } 477 | ``` 478 | 479 | ### Add Change Listener 480 | 481 | Listen to audio device changes in real-time. 482 | 483 | ```dart 484 | class _MyAppState extends State { 485 | List eventLog = []; 486 | 487 | @override 488 | void initState() { 489 | super.initState(); 490 | 491 | // Add a listener for audio device changes 492 | Audio.addChangeListener(_onAudioDeviceChange); 493 | } 494 | 495 | void _onAudioDeviceChange(String type, String id) { 496 | print('Event Type: $type'); 497 | print('Device ID: $id'); 498 | 499 | setState(() { 500 | eventLog.insert(0, '$type: $id'); 501 | if (eventLog.length > 50) eventLog.removeLast(); 502 | }); 503 | 504 | // Handle specific event types 505 | switch (type) { 506 | case 'OnDeviceStateChanged': 507 | print('Device state changed'); 508 | break; 509 | case 'OnDeviceAdded': 510 | print('New device added'); 511 | _refreshDevices(); 512 | break; 513 | case 'OnDeviceRemoved': 514 | print('Device removed'); 515 | _refreshDevices(); 516 | break; 517 | case 'OnDefaultDeviceChanged': 518 | print('Default device changed'); 519 | _refreshDefaultDevice(); 520 | break; 521 | case 'OnPropertyValueChanged': 522 | print('Device property changed'); 523 | break; 524 | } 525 | } 526 | 527 | void _refreshDevices() async { 528 | // Refresh your device list 529 | var devices = await Audio.enumDevices(AudioDeviceType.output); 530 | setState(() { 531 | // Update UI 532 | }); 533 | } 534 | 535 | void _refreshDefaultDevice() async { 536 | // Refresh default device 537 | var defaultDevice = await Audio.getDefaultDevice(AudioDeviceType.output); 538 | setState(() { 539 | // Update UI 540 | }); 541 | } 542 | } 543 | ``` 544 | 545 | **Event Types:** 546 | - `OnDeviceStateChanged` - Device state changed (enabled/disabled) 547 | - `OnDeviceAdded` - New audio device connected 548 | - `OnDeviceRemoved` - Audio device disconnected 549 | - `OnDefaultDeviceChanged` - Default device changed 550 | - `OnPropertyValueChanged` - Device property changed (volume, etc.) 551 | 552 | ### Remove Change Listener 553 | 554 | Remove a previously added listener. 555 | 556 | ```dart 557 | @override 558 | void dispose() { 559 | // Remove the listener when widget is disposed 560 | Audio.removeChangeListener(_onAudioDeviceChange); 561 | super.dispose(); 562 | } 563 | ``` 564 | 565 | **⚠️ Warning:** You may see this message in the console: 566 | 567 | ``` 568 | The 'win32audio' channel sent a message from native to Flutter on a non-platform thread. 569 | ``` 570 | 571 | This is expected behavior as the events come from a separate thread. It's safe to ignore. 572 | 573 | --- 574 | 575 | ## Icon Extraction 576 | 577 | ### Extract File Icon 578 | 579 | Extract icons from executable files or DLLs. 580 | 581 | ```dart 582 | final WinIcons winIcons = WinIcons(); 583 | 584 | // Extract icon from an executable 585 | Uint8List? iconBytes = await winIcons.extractFileIcon( 586 | r'C:\Windows\System32\notepad.exe', 587 | iconID: 0, 588 | ); 589 | 590 | // Extract icon from a DLL 591 | Uint8List? dllIcon = await winIcons.extractFileIcon( 592 | r'C:\Windows\System32\shell32.dll', 593 | iconID: 3, // Different icons have different IDs 594 | ); 595 | 596 | // Display the icon 597 | if (iconBytes != null) { 598 | Image.memory( 599 | iconBytes, 600 | width: 32, 601 | height: 32, 602 | ); 603 | } 604 | 605 | // Example: Extract icons for all audio devices 606 | Map deviceIcons = {}; 607 | 608 | List devices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 609 | 610 | for (var device in devices) { 611 | if (device.iconPath.isNotEmpty) { 612 | deviceIcons[device.id] = await winIcons.extractFileIcon( 613 | device.iconPath, 614 | iconID: device.iconID, 615 | ); 616 | } 617 | } 618 | 619 | // Use in a ListView 620 | ListView.builder( 621 | itemCount: devices.length, 622 | itemBuilder: (context, index) { 623 | return ListTile( 624 | leading: deviceIcons[devices[index].id] != null 625 | ? Image.memory( 626 | deviceIcons[devices[index].id]!, 627 | width: 32, 628 | height: 32, 629 | ) 630 | : Icon(Icons.speaker), 631 | title: Text(devices[index].name), 632 | ); 633 | }, 634 | ) 635 | ``` 636 | 637 | ### Extract Window Icon 638 | 639 | Extract the icon from a window using its handle (HWND). 640 | 641 | ```dart 642 | final WinIcons winIcons = WinIcons(); 643 | 644 | // Extract icon from a window handle 645 | int windowHandle = 123456; // HWND from win32 package or other source 646 | Uint8List? windowIcon = await winIcons.extractWindowIcon(windowHandle); 647 | 648 | if (windowIcon != null) { 649 | Image.memory( 650 | windowIcon, 651 | width: 32, 652 | height: 32, 653 | ); 654 | } 655 | 656 | // Example: Get icon from active window 657 | // (requires additional win32 package for getting window handles) 658 | import 'package:win32/win32.dart'; 659 | 660 | int hwnd = GetForegroundWindow(); 661 | if (hwnd != 0) { 662 | Uint8List? icon = await winIcons.extractWindowIcon(hwnd); 663 | // Display icon 664 | } 665 | ``` 666 | 667 | ### Extract Icon Handle 668 | 669 | Convert a raw Win32 icon handle (HICON) to PNG bytes. 670 | 671 | ```dart 672 | final WinIcons winIcons = WinIcons(); 673 | 674 | // Extract icon from an icon handle 675 | int iconHandle = 789012; // HICON from win32 API 676 | Uint8List? iconPng = await winIcons.extractIconHandle(iconHandle); 677 | 678 | if (iconPng != null) { 679 | Image.memory( 680 | iconPng, 681 | width: 32, 682 | height: 32, 683 | ); 684 | } 685 | ``` 686 | 687 | ### Deprecated Function 688 | 689 | The old `nativeIconToBytes` function is deprecated. Use `WinIcons` class instead: 690 | 691 | ```dart 692 | // ❌ Old way (deprecated) 693 | Uint8List? icon = await nativeIconToBytes(iconPath, iconID: iconID); 694 | 695 | // ✅ New way 696 | final WinIcons winIcons = WinIcons(); 697 | Uint8List? icon = await winIcons.extractFileIcon(iconPath, iconID: iconID); 698 | ``` 699 | 700 | --- 701 | 702 | ## Data Structures 703 | 704 | ### AudioDevice 705 | 706 | Represents a single audio device. 707 | 708 | ```dart 709 | class AudioDevice { 710 | String id; // Unique device identifier (MMDevice ID) 711 | String name; // Friendly name of the device 712 | String iconPath; // Path to the icon resource file 713 | int iconID; // Resource ID of the icon 714 | bool isActive; // Whether this is the default/active device 715 | } 716 | ``` 717 | 718 | ### ProcessVolume 719 | 720 | Represents an application's audio session. 721 | 722 | ```dart 723 | class ProcessVolume { 724 | int processId; // Process ID (PID) 725 | String processPath; // Full path to the executable 726 | double maxVolume; // Current volume (0.0 to 1.0) 727 | double peakVolume; // Current peak volume (0.0 to 1.0) 728 | } 729 | ``` 730 | 731 | ### AudioDeviceType 732 | 733 | Enumeration of device types. 734 | 735 | ```dart 736 | enum AudioDeviceType { 737 | output, // Playback devices (speakers, headphones) 738 | input, // Recording devices (microphones) 739 | } 740 | ``` 741 | 742 | ### AudioRole 743 | 744 | Enumeration of audio roles for Windows. 745 | 746 | ```dart 747 | enum AudioRole { 748 | console, // Games, system sounds, voice commands 749 | multimedia, // Music, movies, narration 750 | communications, // Voice chat, VoIP 751 | } 752 | ``` 753 | 754 | --- 755 | 756 | ## Complete Example 757 | 758 | Here's a complete example demonstrating multiple features: 759 | 760 | ```dart 761 | import 'package:flutter/material.dart'; 762 | import 'package:win32audio/win32audio.dart'; 763 | 764 | void main() async { 765 | WidgetsFlutterBinding.ensureInitialized(); 766 | await Audio.setupChangeListener(); 767 | runApp(MyApp()); 768 | } 769 | 770 | class MyApp extends StatefulWidget { 771 | @override 772 | _MyAppState createState() => _MyAppState(); 773 | } 774 | 775 | class _MyAppState extends State { 776 | List devices = []; 777 | List audioSessions = []; 778 | AudioDevice? defaultDevice; 779 | double currentVolume = 0.0; 780 | final WinIcons winIcons = WinIcons(); 781 | Map deviceIcons = {}; 782 | 783 | @override 784 | void initState() { 785 | super.initState(); 786 | Audio.addChangeListener(_onAudioChange); 787 | _loadDevices(); 788 | _loadAudioSessions(); 789 | } 790 | 791 | @override 792 | void dispose() { 793 | Audio.removeChangeListener(_onAudioChange); 794 | super.dispose(); 795 | } 796 | 797 | void _onAudioChange(String type, String id) { 798 | print('Audio event: $type - $id'); 799 | if (type == 'OnDefaultDeviceChanged' || type == 'OnDeviceAdded') { 800 | _loadDevices(); 801 | } 802 | } 803 | 804 | Future _loadDevices() async { 805 | devices = await Audio.enumDevices(AudioDeviceType.output) ?? []; 806 | defaultDevice = await Audio.getDefaultDevice(AudioDeviceType.output); 807 | currentVolume = await Audio.getVolume(AudioDeviceType.output); 808 | 809 | // Load device icons 810 | for (var device in devices) { 811 | if (device.iconPath.isNotEmpty) { 812 | deviceIcons[device.id] = await winIcons.extractFileIcon( 813 | device.iconPath, 814 | iconID: device.iconID, 815 | ); 816 | } 817 | } 818 | 819 | setState(() {}); 820 | } 821 | 822 | Future _loadAudioSessions() async { 823 | audioSessions = await Audio.enumAudioMixer() ?? []; 824 | setState(() {}); 825 | } 826 | 827 | @override 828 | Widget build(BuildContext context) { 829 | return MaterialApp( 830 | home: Scaffold( 831 | appBar: AppBar( 832 | title: Text('Win32Audio Demo'), 833 | actions: [ 834 | IconButton( 835 | icon: Icon(Icons.swap_horiz), 836 | onPressed: () async { 837 | await Audio.switchDefaultDevice(AudioDeviceType.output); 838 | _loadDevices(); 839 | }, 840 | ), 841 | ], 842 | ), 843 | body: Column( 844 | children: [ 845 | // Volume Control 846 | Padding( 847 | padding: EdgeInsets.all(16), 848 | child: Column( 849 | children: [ 850 | Text( 851 | 'Master Volume: ${(currentVolume * 100).toStringAsFixed(0)}%', 852 | style: TextStyle(fontSize: 20), 853 | ), 854 | Slider( 855 | value: currentVolume, 856 | onChanged: (value) async { 857 | await Audio.setVolume(value, AudioDeviceType.output); 858 | setState(() => currentVolume = value); 859 | }, 860 | ), 861 | ], 862 | ), 863 | ), 864 | 865 | // Device List 866 | Expanded( 867 | child: ListView.builder( 868 | itemCount: devices.length, 869 | itemBuilder: (context, index) { 870 | final device = devices[index]; 871 | return ListTile( 872 | leading: deviceIcons[device.id] != null 873 | ? Image.memory(deviceIcons[device.id]!, width: 32, height: 32) 874 | : Icon(Icons.speaker), 875 | title: Text(device.name), 876 | trailing: device.isActive 877 | ? Icon(Icons.check, color: Colors.green) 878 | : null, 879 | onTap: () async { 880 | await Audio.setDefaultDevice(device.id); 881 | _loadDevices(); 882 | }, 883 | ); 884 | }, 885 | ), 886 | ), 887 | 888 | Divider(), 889 | 890 | // Audio Sessions 891 | Expanded( 892 | child: ListView.builder( 893 | itemCount: audioSessions.length, 894 | itemBuilder: (context, index) { 895 | final session = audioSessions[index]; 896 | final appName = session.processPath.split('\\').last; 897 | 898 | return ListTile( 899 | title: Text(appName), 900 | subtitle: Slider( 901 | value: session.maxVolume, 902 | onChanged: (value) async { 903 | await Audio.setAudioMixerVolume(session.processId, value); 904 | _loadAudioSessions(); 905 | }, 906 | ), 907 | ); 908 | }, 909 | ), 910 | ), 911 | ], 912 | ), 913 | ), 914 | ); 915 | } 916 | } 917 | ``` 918 | 919 | --- 920 | 921 | ## License 922 | 923 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 924 | 925 | ## Contributing 926 | 927 | Contributions are welcome! Please feel free to submit a Pull Request. 928 | 929 | ## Issues 930 | 931 | If you encounter any issues or have feature requests, please file them on the [GitHub issue tracker](https://github.com/Far-Se/win32audio/issues). --------------------------------------------------------------------------------