Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions THIRD_PARTY_NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
Binary file not shown.
Binary file added app/src/main/assets/directaudio-20260813.tzst
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -225,6 +249,7 @@ fun GeneralTabContent(
graphicsDriverConfig = newCfg.toString(),
box64Version = "0.3.7",
dxwrapperConfig = currentConfig.toString(),
audioDriver = getValidatedAudioDriver(config.copy(wineVersion = newWine, containerVariant = newVariant)),
)
}
},
Expand All @@ -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))
)
},
)
}
Expand Down Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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")
)
Expand All @@ -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")
}
}
}

/**
Expand Down Expand Up @@ -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", "")
}
Expand Down
96 changes: 96 additions & 0 deletions app/src/main/java/com/winlator/container/ContainerManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -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: <reason>"),
* 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)) {
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<string-array name="audio_driver_entries">
<item>ALSA</item>
<item>PulseAudio</item>
<item>DirectAudio (Proton 11 arm64ec only)</item>
<item>Disabled</item>
</string-array>
<string-array name="binding_type_entries">
Expand Down
Loading