package com.mojang.blaze3d.audio; import com.google.common.collect.Sets; import com.mojang.logging.LogUtils; import java.nio.IntBuffer; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.OptionalLong; import java.util.Set; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.SharedConstants; import net.minecraft.util.Mth; import org.jetbrains.annotations.Nullable; import org.lwjgl.openal.AL; import org.lwjgl.openal.AL10; import org.lwjgl.openal.ALC; import org.lwjgl.openal.ALC10; import org.lwjgl.openal.ALC11; import org.lwjgl.openal.ALCCapabilities; import org.lwjgl.openal.ALCapabilities; import org.lwjgl.openal.ALUtil; import org.lwjgl.system.MemoryStack; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class Library { static final Logger LOGGER = LogUtils.getLogger(); private static final int NO_DEVICE = 0; private static final int DEFAULT_CHANNEL_COUNT = 30; private long currentDevice; private long context; private boolean supportsDisconnections; @Nullable private String defaultDeviceName; private static final Library.ChannelPool EMPTY = new Library.ChannelPool() { @Nullable @Override public Channel acquire() { return null; } @Override public boolean release(Channel channel) { return false; } @Override public void cleanup() { } @Override public int getMaxCount() { return 0; } @Override public int getUsedCount() { return 0; } }; private Library.ChannelPool staticChannels = EMPTY; private Library.ChannelPool streamingChannels = EMPTY; private final Listener listener = new Listener(); public Library() { this.defaultDeviceName = getDefaultDeviceName(); } /** * Initializes the OpenAL device and context. * @throws IllegalStateException if an error occurs during initialization. * * @param deviceSpecifier A string specifying the name of the audio device to use, or null to use the default device. * @param enableHrtf Whether to enable HRTF (head-related transfer function) for spatial audio. */ public void init(@Nullable String deviceSpecifier, boolean enableHrtf) { this.currentDevice = openDeviceOrFallback(deviceSpecifier); this.supportsDisconnections = false; ALCCapabilities aLCCapabilities = ALC.createCapabilities(this.currentDevice); if (OpenAlUtil.checkALCError(this.currentDevice, "Get capabilities")) { throw new IllegalStateException("Failed to get OpenAL capabilities"); } else if (!aLCCapabilities.OpenALC11) { throw new IllegalStateException("OpenAL 1.1 not supported"); } else { try (MemoryStack memoryStack = MemoryStack.stackPush()) { IntBuffer intBuffer = this.createAttributes(memoryStack, aLCCapabilities.ALC_SOFT_HRTF && enableHrtf); this.context = ALC10.alcCreateContext(this.currentDevice, intBuffer); } if (OpenAlUtil.checkALCError(this.currentDevice, "Create context")) { throw new IllegalStateException("Unable to create OpenAL context"); } else { ALC10.alcMakeContextCurrent(this.context); int i = this.getChannelCount(); int j = Mth.clamp((int)Mth.sqrt(i), 2, 8); int k = Mth.clamp(i - j, 8, 255); this.staticChannels = new Library.CountingChannelPool(k); this.streamingChannels = new Library.CountingChannelPool(j); ALCapabilities aLCapabilities = AL.createCapabilities(aLCCapabilities); OpenAlUtil.checkALError("Initialization"); if (!aLCapabilities.AL_EXT_source_distance_model) { throw new IllegalStateException("AL_EXT_source_distance_model is not supported"); } else { AL10.alEnable(512); if (!aLCapabilities.AL_EXT_LINEAR_DISTANCE) { throw new IllegalStateException("AL_EXT_LINEAR_DISTANCE is not supported"); } else { OpenAlUtil.checkALError("Enable per-source distance models"); LOGGER.info("OpenAL initialized on device {}", this.getCurrentDeviceName()); this.supportsDisconnections = ALC10.alcIsExtensionPresent(this.currentDevice, "ALC_EXT_disconnect"); } } } } } private IntBuffer createAttributes(MemoryStack memoryStack, boolean enableHrtf) { int i = 5; IntBuffer intBuffer = memoryStack.callocInt(11); int j = ALC10.alcGetInteger(this.currentDevice, 6548); if (j > 0) { intBuffer.put(6546).put(enableHrtf ? 1 : 0); intBuffer.put(6550).put(0); } intBuffer.put(6554).put(1); return intBuffer.put(0).flip(); } /** * {@return the number of channels available for audio playback} */ private int getChannelCount() { try (MemoryStack memoryStack = MemoryStack.stackPush()) { int i = ALC10.alcGetInteger(this.currentDevice, 4098); if (OpenAlUtil.checkALCError(this.currentDevice, "Get attributes size")) { throw new IllegalStateException("Failed to get OpenAL attributes"); } IntBuffer intBuffer = memoryStack.mallocInt(i); ALC10.alcGetIntegerv(this.currentDevice, 4099, intBuffer); if (OpenAlUtil.checkALCError(this.currentDevice, "Get attributes")) { throw new IllegalStateException("Failed to get OpenAL attributes"); } int j = 0; while (j < i) { int k = intBuffer.get(j++); if (k == 0) { break; } int l = intBuffer.get(j++); if (k == 4112) { return l; } } } return 30; } /** * {@return the name of the currently selected audio device, or {@code Unknown} if it cannot be determined} */ @Nullable public static String getDefaultDeviceName() { if (!ALC10.alcIsExtensionPresent(0L, "ALC_ENUMERATE_ALL_EXT")) { return null; } else { ALUtil.getStringList(0L, 4115); return ALC10.alcGetString(0L, 4114); } } /** * {@return the name of the default audio device, or {@code null} if it cannot be determined} */ public String getCurrentDeviceName() { String string = ALC10.alcGetString(this.currentDevice, 4115); if (string == null) { string = ALC10.alcGetString(this.currentDevice, 4101); } if (string == null) { string = "Unknown"; } return string; } /** * Checks if the default audio device has changed since the last time this method was called. *
	 * If the default device has changed, updates the stored default device name accordingly.
	 * @return {@code true} if the default device has changed since the last time this method was called, {@code false} otherwise
	 */
	public synchronized boolean hasDefaultDeviceChanged() {
		String string = getDefaultDeviceName();
		if (Objects.equals(this.defaultDeviceName, string)) {
			return false;
		} else {
			this.defaultDeviceName = string;
			return true;
		}
	}
	/**
	 * Opens the specified audio device, or the default device if the specifier is null.
	 * @return The handle of the opened device.
	 * @throws IllegalStateException if the device cannot be opened.
	 * 
	 * @param deviceSpecifier The name of the audio device to open, or null to open the default device.
	 */
	private static long openDeviceOrFallback(@Nullable String deviceSpecifier) {
		OptionalLong optionalLong = OptionalLong.empty();
		if (deviceSpecifier != null) {
			optionalLong = tryOpenDevice(deviceSpecifier);
		}
		if (optionalLong.isEmpty()) {
			optionalLong = tryOpenDevice(getDefaultDeviceName());
		}
		if (optionalLong.isEmpty()) {
			optionalLong = tryOpenDevice(null);
		}
		if (optionalLong.isEmpty()) {
			throw new IllegalStateException("Failed to open OpenAL device");
		} else {
			return optionalLong.getAsLong();
		}
	}
	/**
	 * Attempts to open the specified audio device.
	 * @return an {@linkplain OptionalLong} containing the handle of the opened device if successful, or empty if the device could not be opened
	 * 
	 * @param deviceSpecifier A string specifying the name of the audio device to open, or null to use the default device.
	 */
	private static OptionalLong tryOpenDevice(@Nullable String deviceSpecifier) {
		long l = ALC10.alcOpenDevice(deviceSpecifier);
		return l != 0L && !OpenAlUtil.checkALCError(l, "Open device") ? OptionalLong.of(l) : OptionalLong.empty();
	}
	/**
	 * Cleans up all resources used by the library.
	 */
	public void cleanup() {
		this.staticChannels.cleanup();
		this.streamingChannels.cleanup();
		ALC10.alcDestroyContext(this.context);
		if (this.currentDevice != 0L) {
			ALC10.alcCloseDevice(this.currentDevice);
		}
	}
	public Listener getListener() {
		return this.listener;
	}
	/**
	 * Acquires a sound channel based on the given mode.
	 */
	@Nullable
	public Channel acquireChannel(Library.Pool pool) {
		return (pool == Library.Pool.STREAMING ? this.streamingChannels : this.staticChannels).acquire();
	}
	/**
	 * Releases a channel.
	 * @return whether the channel was successfully released
	 * 
	 * @param channel The channel to release.
	 */
	public void releaseChannel(Channel channel) {
		if (!this.staticChannels.release(channel) && !this.streamingChannels.release(channel)) {
			throw new IllegalStateException("Tried to release unknown channel");
		}
	}
	public String getDebugString() {
		return String.format(
			Locale.ROOT,
			"Sounds: %d/%d + %d/%d",
			this.staticChannels.getUsedCount(),
			this.staticChannels.getMaxCount(),
			this.streamingChannels.getUsedCount(),
			this.streamingChannels.getMaxCount()
		);
	}
	/**
	 * {@return A list of strings representing the names of available sound devices, or an empty list if no devices are available.}
	 */
	public List