如何用Java程序实现可调节多程序音量的虚拟音频输出设备?
Hey there! Great project idea—building a simplified Windows volume mixer in Java is totally doable, but you’re right that the standard AudioSystem class won’t cut it for per-app audio control. Let’s break down how to solve this core problem.
Why AudioSystem Isn’t Enough
The javax.sound.sampled.AudioSystem class operates at the device or system level—it can access audio input/output hardware, but it has no way to isolate audio streams from individual processes. Windows handles per-app audio via audio sessions (tied directly to running processes), and this functionality lives in native Windows APIs, not Java’s cross-platform standard library.
Solution: Use JNA to Call Windows Core Audio APIs
To tap into per-process audio control, you’ll need to use JNA (Java Native Access) to interact with Windows’ Core Audio COM interfaces. These interfaces let you enumerate active audio sessions, link them to their parent processes, and adjust their volume independently. The key interfaces you’ll work with are:
IAudioSessionManager2: Manages audio sessions for a target audio device.IAudioSessionEnumerator: Lists all active audio sessions on the system.IAudioSessionControl2: Retrieves process details (like executable name) for a session.ISimpleAudioVolume: Controls volume and mute state for an individual session.
Step 1: Add JNA Dependencies
First, include JNA in your project. If using Maven, add these to your pom.xml:
<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna-core</artifactId> <version>5.13.0</version> </dependency> <dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna-platform</artifactId> <version>5.13.0</version> </dependency>
Step 2: Map Core Audio Interfaces with JNA
You’ll need to define JNA interfaces that mirror Windows’ COM APIs. Here’s a simplified set of mappings (full implementations exist in open-source projects like jna-platform):
import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.platform.win32.Guid; import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.PointerByReference; // IAudioSessionManager2 interface public interface IAudioSessionManager2 extends com.sun.jna.platform.win32.COM.IUnknown { Guid.IID IID_IAUDIOSESSIONMANAGER2 = new Guid.IID("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"); WinNT.HRESULT GetSessionEnumerator(PointerByReference ppSessionEnum); } // IAudioSessionEnumerator interface public interface IAudioSessionEnumerator extends com.sun.jna.platform.win32.COM.IUnknown { Guid.IID IID_IAUDIOSESSIONENUMERATOR = new Guid.IID("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"); WinNT.HRESULT GetCount(WinDef.UINTByReference pdwCount); WinNT.HRESULT GetSession(WinDef.UINT dwSession, PointerByReference ppSession); } // IAudioSessionControl2 interface public interface IAudioSessionControl2 extends com.sun.jna.platform.win32.COM.IUnknown { Guid.IID IID_IAUDIOSESSIONCONTROL2 = new Guid.IID("87CE5498-68D6-44E5-9215-6DA47EF883D8"); WinNT.HRESULT GetProcessId(WinDef.DWORDByReference pdwProcessId); } // ISimpleAudioVolume interface public interface ISimpleAudioVolume extends com.sun.jna.platform.win32.COM.IUnknown { Guid.IID IID_ISIMPLEAUDIOVOLUME = new Guid.IID("87CE5498-68D6-44E5-9215-6DA47EF883D8"); WinNT.HRESULT SetMasterVolume(float fLevel, Pointer pEventContext); WinNT.HRESULT GetMasterVolume(WinDef.FLOATByReference pfLevel); }
Step 3: Enumerate Sessions and Control Volume
Here’s a simplified code snippet to list active audio sessions, link them to processes, and adjust their volume:
import com.sun.jna.platform.win32.AudioClient; import com.sun.jna.platform.win32.COM.COMUtils; import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.PointerByReference; public class AudioMixerController { public static void main(String[] args) { // Initialize audio client and get session manager AudioClient.IAudioClient audioClient = AudioClient.INSTANCE; PointerByReference pSessionManager = new PointerByReference(); WinNT.HRESULT hr = audioClient.GetService(AudioClient.IID_IAUDIOSESSIONMANAGER2, pSessionManager); COMUtils.checkRC(hr); IAudioSessionManager2 sessionManager = Native.load(pSessionManager.getValue(), IAudioSessionManager2.class); // Get session enumerator PointerByReference pSessionEnum = new PointerByReference(); hr = sessionManager.GetSessionEnumerator(pSessionEnum); COMUtils.checkRC(hr); IAudioSessionEnumerator sessionEnum = Native.load(pSessionEnum.getValue(), IAudioSessionEnumerator.class); // Get number of active sessions WinDef.UINTByReference sessionCount = new WinDef.UINTByReference(); hr = sessionEnum.GetCount(sessionCount); COMUtils.checkRC(hr); // Iterate through each session for (int i = 0; i < sessionCount.getValue().intValue(); i++) { PointerByReference pSessionControl = new PointerByReference(); hr = sessionEnum.GetSession(new WinDef.UINT(i), pSessionControl); COMUtils.checkRC(hr); // Get process ID for the session PointerByReference pSessionControl2 = new PointerByReference(); hr = ((com.sun.jna.platform.win32.COM.IUnknown) Native.load(pSessionControl.getValue(), com.sun.jna.platform.win32.COM.IUnknown.class)) .QueryInterface(IAudioSessionControl2.IID_IAUDIOSESSIONCONTROL2, pSessionControl2); COMUtils.checkRC(hr); IAudioSessionControl2 sessionControl2 = Native.load(pSessionControl2.getValue(), IAudioSessionControl2.class); WinDef.DWORDByReference processId = new WinDef.DWORDByReference(); hr = sessionControl2.GetProcessId(processId); COMUtils.checkRC(hr); // Get process name (implement using JNA's Kernel32 to fetch from PID) String processName = getProcessName(processId.getValue().intValue()); // Get volume control interface PointerByReference pSimpleVolume = new PointerByReference(); hr = ((com.sun.jna.platform.win32.COM.IUnknown) Native.load(pSessionControl.getValue(), com.sun.jna.platform.win32.COM.IUnknown.class)) .QueryInterface(ISimpleAudioVolume.IID_ISIMPLEAUDIOVOLUME, pSimpleVolume); COMUtils.checkRC(hr); ISimpleAudioVolume simpleVolume = Native.load(pSimpleVolume.getValue(), ISimpleAudioVolume.class); // Example: Set volume to 50% (range 0.0 to 1.0) hr = simpleVolume.SetMasterVolume(0.5f, null); COMUtils.checkRC(hr); System.out.printf("Adjusted volume for: %s (PID: %d)%n", processName, processId.getValue().intValue()); // Release COM interfaces to avoid memory leaks sessionControl2.Release(); simpleVolume.Release(); } sessionEnum.Release(); sessionManager.Release(); } private static String getProcessName(int pid) { // Implement using JNA's Kernel32 to open the process and fetch its executable path return "Unknown Process"; // Replace with actual implementation } }
Key Considerations
- COM Interface Cleanup: Always call
Release()on COM interfaces after use to prevent memory leaks. - Permissions: Your program may need elevated privileges to access audio sessions from system processes.
- Architecture Compatibility: Ensure your JNA binaries match your JVM’s architecture (64-bit JVM needs 64-bit JNA).
- Real-Time Updates: To track new/closed audio sessions, implement
IAudioSessionNotificationto listen for session events.
Next Steps for Your Mixer
Once you have per-session volume control working, you can expand with features like:
- A GUI (Swing/JavaFX) with volume sliders for each active app.
- Saving and restoring volume presets for specific programs.
- One-click mute/unmute for individual apps.
内容的提问来源于stack exchange,提问作者Will

