diff --git a/THIRD_PARTY_NOTICES b/THIRD_PARTY_NOTICES index 39a4ef02ef..00b5de4f8a 100644 --- a/THIRD_PARTY_NOTICES +++ b/THIRD_PARTY_NOTICES @@ -342,6 +342,47 @@ for more details. Full license text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html +## DirectAudio + +**Library:** DirectAudio v1 (winedirectaudio.drv) +**Author:** The412Banner +**Source:** https://github.com/The412Banner/directaudio +**License:** LGPL-2.1-or-later (GNU Lesser General Public License v2.1 or later) + +DirectAudio is a native Wine → Android AAudio mmdevapi driver that provides +direct audio output without requiring PulseAudio or ALSA middleware. The +precompiled arm64ec binaries (winedirectaudio.drv and winedirectaudio.so) are +included in `app/src/main/assets/directaudio-20260812.tzst` and are extracted +at runtime into the Wine prefix when the user selects DirectAudio as their +audio driver. + +The driver derives from and links Wine's LGPL mmdevapi internals and is +modelled on winecoreaudio.drv, requiring it to remain under LGPL-2.1. + +Copyright © 2026 The412Banner. As required by LGPL-2.1, the complete source +code is available at: + https://github.com/The412Banner/directaudio + +Users may rebuild the driver from source or substitute their own build by +replacing the files in the wineprefix `lib/wine/aarch64-windows/` and +`lib/wine/aarch64-unix/` directories. + +### License Text: LGPL-2.1 + +This library is free software; you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or (at +your option) any later version. + +This library is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +for more details. + +Full license text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html + +--- + ## Achievement Sound "Achievement sound from [Pixabay](https://pixabay.com), [Pixabay Content License](https://pixabay.com/service/license-summary/). diff --git a/app/src/main/assets/directaudio-20260813-diagnostics.tzst b/app/src/main/assets/directaudio-20260813-diagnostics.tzst new file mode 100644 index 0000000000..55ff0bb206 Binary files /dev/null and b/app/src/main/assets/directaudio-20260813-diagnostics.tzst differ diff --git a/app/src/main/assets/directaudio-20260813.tzst b/app/src/main/assets/directaudio-20260813.tzst new file mode 100644 index 0000000000..914f9faf3a Binary files /dev/null and b/app/src/main/assets/directaudio-20260813.tzst differ diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt b/app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt index 66653ed568..5a7d30ec35 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -44,6 +45,28 @@ import com.winlator.contents.ContentProfile import com.winlator.xenvironment.components.PulseAudioComponent import java.util.Locale +/** + * Checks if DirectAudio is supported for the given wine version and container variant. + * DirectAudio is only supported on Proton 11 with bionic variant. + */ +private fun isDirectAudioSupported(wineVersion: String, containerVariant: String): Boolean { + return wineVersion.startsWith("proton-11", ignoreCase = true) && + wineVersion.contains("arm64ec", ignoreCase = true) && + containerVariant.equals(Container.BIONIC, ignoreCase = true) +} + +/** + * Returns a validated audio driver. If DirectAudio is selected but not supported + * for the given wine version and variant, returns "pulseaudio" instead. + */ +private fun getValidatedAudioDriver(config: ContainerData): String { + return if (config.audioDriver == "directaudio" && !isDirectAudioSupported(config.wineVersion, config.containerVariant)) { + "pulseaudio" + } else { + config.audioDriver + } +} + @Composable fun GeneralTabContent( state: ContainerConfigState, @@ -181,6 +204,7 @@ fun GeneralTabContent( graphicsDriverVersion = "", graphicsDriverConfig = newCfg.toString(), box64Version = "0.3.6", + audioDriver = getValidatedAudioDriver(config.copy(wineVersion = defaultGlibcWine, containerVariant = newVariant)), ) } else { val defaultBionicDriver = StringUtils.parseIdentifier(state.bionicGraphicsDrivers.first()) @@ -225,6 +249,7 @@ fun GeneralTabContent( graphicsDriverConfig = newCfg.toString(), box64Version = "0.3.7", dxwrapperConfig = currentConfig.toString(), + audioDriver = getValidatedAudioDriver(config.copy(wineVersion = newWine, containerVariant = newVariant)), ) } }, @@ -248,11 +273,18 @@ fun GeneralTabContent( ContentProfile.ContentType.CONTENT_TYPE_WINE } state.launchManifestContentInstall(manifestEntry, expectedType) { - state.config.value = config.copy(wineVersion = selectedId) + state.config.value = config.copy( + wineVersion = selectedId, + audioDriver = getValidatedAudioDriver(config.copy(wineVersion = selectedId)) + ) } return@SettingsListDropdown } - state.config.value = config.copy(wineVersion = selectedId.ifEmpty { state.bionicWineOptions.labels[idx] }) + val newWineVersion = selectedId.ifEmpty { state.bionicWineOptions.labels[idx] } + state.config.value = config.copy( + wineVersion = newWineVersion, + audioDriver = getValidatedAudioDriver(config.copy(wineVersion = newWineVersion)) + ) }, ) } @@ -314,14 +346,33 @@ fun GeneralTabContent( state = config.portraitMode, onCheckedChange = { state.config.value = config.copy(portraitMode = it) }, ) + // Sync audioDriverIndex when config.audioDriver changes (e.g., when wine version changes) + LaunchedEffect(config.audioDriver) { + val driverIndex = state.audioDrivers.indexOfFirst { + StringUtils.parseIdentifier(it) == config.audioDriver + } + if (driverIndex >= 0 && driverIndex != state.audioDriverIndex.value) { + state.audioDriverIndex.value = driverIndex + } + } SettingsListDropdown( colors = settingsTileColors(), title = { Text(text = stringResource(R.string.audio_driver)) }, value = state.audioDriverIndex.value, items = state.audioDrivers, onItemSelected = { - state.audioDriverIndex.value = it - state.config.value = config.copy(audioDriver = StringUtils.parseIdentifier(state.audioDrivers[it])) + val selectedDriver = StringUtils.parseIdentifier(state.audioDrivers[it]) + // If DirectAudio is selected but wine version is not Proton 11 or variant is not bionic, revert to PulseAudio + if (selectedDriver == "directaudio" && !isDirectAudioSupported(config.wineVersion, config.containerVariant)) { + val pulseaudioIndex = state.audioDrivers.indexOfFirst { + StringUtils.parseIdentifier(it) == "pulseaudio" + }.coerceAtLeast(0) + state.audioDriverIndex.value = pulseaudioIndex + state.config.value = config.copy(audioDriver = "pulseaudio") + } else { + state.audioDriverIndex.value = it + state.config.value = config.copy(audioDriver = selectedDriver) + } }, ) if (config.audioDriver == "pulseaudio") { diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index e9c423e026..d06c2759e4 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -5004,7 +5004,7 @@ private suspend fun setupWineSystemFiles( } // Always refresh components files - refreshComponentsFiles(context) + refreshComponentsFiles(context, container, imageFs, containerManager) // Normalize dxwrapper for state (dxvk includes version for extraction switch) if (xServerState.value.dxwrapper == "dxvk") { @@ -5172,7 +5172,12 @@ private suspend fun applyGeneralPatches( WinlatorPrefManager.putString("current_box64_version", "") } -private fun refreshComponentsFiles(context: Context) { +private fun refreshComponentsFiles( + context: Context, + container: Container, + imageFs: ImageFs, + containerManager: ContainerManager +) { val extractionPairs = listOf( "pulseaudio-gamenative-20260612.tzst" to File(context.filesDir, "pulseaudio") ) @@ -5182,6 +5187,14 @@ private fun refreshComponentsFiles(context: Context) { context.assets, TarCompressorUtils.Type.ZSTD ) + + // Extract DirectAudio driver files if the container uses DirectAudio + if (container.audioDriver == "directaudio") { + val containerDir = container.rootDir + if (!containerManager.extractDirectAudio(imageFs, containerDir)) { + Log.e("XServerScreen", "Failed to extract DirectAudio driver files") + } + } } /** @@ -6007,6 +6020,8 @@ private fun changeWineAudioDriver(audioDriver: String, container: Container, ima registryEditor.setStringValue("Software\\Wine\\Drivers", "Audio", "alsa") } else if (audioDriver == "pulseaudio") { registryEditor.setStringValue("Software\\Wine\\Drivers", "Audio", "pulse") + } else if (audioDriver == "directaudio") { + registryEditor.setStringValue("Software\\Wine\\Drivers", "Audio", "directaudio") } else if (audioDriver == "disabled") { registryEditor.setStringValue("Software\\Wine\\Drivers", "Audio", "") } diff --git a/app/src/main/java/com/winlator/container/ContainerManager.java b/app/src/main/java/com/winlator/container/ContainerManager.java index 13b45c0dd7..313e7cc9c2 100644 --- a/app/src/main/java/com/winlator/container/ContainerManager.java +++ b/app/src/main/java/com/winlator/container/ContainerManager.java @@ -335,6 +335,102 @@ public void onProgress(float progress) { } } + /** + * Extracts DirectAudio driver files (winedirectaudio.drv and winedirectaudio.so) + * from the bundled DirectAudio archive into the Wine installation, + * then copies the .drv to the container's system32 directory. + * + * Files are placed at: + * - imageFs/opt/wine/lib/wine/aarch64-windows/winedirectaudio.drv (source) + * - imageFs/opt/wine/lib/wine/aarch64-unix/winedirectaudio.so (stays here) + * - containerDir/.wine/drive_c/windows/system32/winedirectaudio.drv (copied) + * + * @param imageFs The ImageFs instance to get the Wine installation path + * @param containerDir The container root directory + * @return true if extraction succeeded, false otherwise + */ + /** + * Which DirectAudio build to install. Both archives ship in assets/: + * directaudio-20260813.tzst - release build (default) + * directaudio-20260813-diagnostics.tzst - identical driver plus logcat probes + * under the "DirectAudio" tag + * Point this at the -diagnostics archive when tracing an audio issue on a title. + * The release build already logs stream rebuilds ("DirectAudio: reopen: "), + * so the diagnostics build is only needed for per-callback detail. + */ + private static final String DIRECTAUDIO_ASSET = "directaudio-20260813.tzst"; + + public boolean extractDirectAudio(ImageFs imageFs, File containerDir) { + try { + // Extract the archive to a temporary directory + File tempDir = new File(context.getCacheDir(), "directaudio_temp"); + if (tempDir.exists()) { + FileUtils.delete(tempDir); + } + if (!tempDir.mkdirs()) { + Log.e("ContainerManager", "Failed to create temp directory for directaudio"); + return false; + } + + boolean extracted = TarCompressorUtils.extract( + TarCompressorUtils.Type.ZSTD, + context.getAssets(), + DIRECTAUDIO_ASSET, + tempDir, + null + ); + + if (!extracted) { + Log.e("ContainerManager", "Failed to extract directaudio archive"); + FileUtils.delete(tempDir); + return false; + } + + // Source files from extracted archive (files are in root of archive) + File drvSrc = new File(tempDir, "winedirectaudio.drv"); + File soSrc = new File(tempDir, "winedirectaudio.so"); + + if (!drvSrc.exists() || !soSrc.exists()) { + Log.e("ContainerManager", "DirectAudio files not found in archive"); + FileUtils.delete(tempDir); + return false; + } + + // Destination directories + File wineLibDir = new File(imageFs.getWinePath(), "lib/wine"); + File aarch64WindowsDir = new File(wineLibDir, "aarch64-windows"); + File aarch64UnixDir = new File(wineLibDir, "aarch64-unix"); + File system32Dir = new File(containerDir, ".wine/drive_c/windows/system32"); + + aarch64WindowsDir.mkdirs(); + aarch64UnixDir.mkdirs(); + system32Dir.mkdirs(); + + // Copy files to their destinations + File drvWineDest = new File(aarch64WindowsDir, "winedirectaudio.drv"); + File soWineDest = new File(aarch64UnixDir, "winedirectaudio.so"); + File drvSystem32Dest = new File(system32Dir, "winedirectaudio.drv"); + + boolean success = FileUtils.copy(drvSrc, drvWineDest) && + FileUtils.copy(soSrc, soWineDest) && + FileUtils.copy(drvSrc, drvSystem32Dest); + + // Clean up temp directory + FileUtils.delete(tempDir); + + if (success) { + Log.d("ContainerManager", "DirectAudio driver installed successfully"); + } else { + Log.e("ContainerManager", "Failed to copy DirectAudio files"); + } + + return success; + } catch (Exception e) { + Log.e("ContainerManager", "Error extracting DirectAudio: " + e.getMessage()); + return false; + } + } + public boolean extractContainerPatternFile(String wineVersion, ContentsManager contentsManager, File containerDir, OnExtractFileListener onExtractFileListener) { WineInfo wineInfo = WineInfo.fromIdentifier(context, contentsManager, wineVersion); if (WineInfo.isMainWineVersion(wineVersion)) { diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index ebf7459ebf..4f40977851 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -71,6 +71,7 @@ ALSA PulseAudio + DirectAudio (Proton 11 arm64ec only) Disabled