384 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			384 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| 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.
 | |
| 	 * <p>
 | |
| 	 * 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<String> getAvailableSoundDevices() {
 | |
| 		List<String> list = ALUtil.getStringList(0L, 4115);
 | |
| 		return list == null ? Collections.emptyList() : list;
 | |
| 	}
 | |
| 
 | |
| 	public boolean isCurrentDeviceDisconnected() {
 | |
| 		return this.supportsDisconnections && ALC11.alcGetInteger(this.currentDevice, 787) == 0;
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	interface ChannelPool {
 | |
| 		@Nullable
 | |
| 		Channel acquire();
 | |
| 
 | |
| 		boolean release(Channel channel);
 | |
| 
 | |
| 		void cleanup();
 | |
| 
 | |
| 		int getMaxCount();
 | |
| 
 | |
| 		int getUsedCount();
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	static class CountingChannelPool implements Library.ChannelPool {
 | |
| 		private final int limit;
 | |
| 		private final Set<Channel> activeChannels = Sets.newIdentityHashSet();
 | |
| 
 | |
| 		public CountingChannelPool(int limit) {
 | |
| 			this.limit = limit;
 | |
| 		}
 | |
| 
 | |
| 		@Nullable
 | |
| 		@Override
 | |
| 		public Channel acquire() {
 | |
| 			if (this.activeChannels.size() >= this.limit) {
 | |
| 				if (SharedConstants.IS_RUNNING_IN_IDE) {
 | |
| 					Library.LOGGER.warn("Maximum sound pool size {} reached", this.limit);
 | |
| 				}
 | |
| 
 | |
| 				return null;
 | |
| 			} else {
 | |
| 				Channel channel = Channel.create();
 | |
| 				if (channel != null) {
 | |
| 					this.activeChannels.add(channel);
 | |
| 				}
 | |
| 
 | |
| 				return channel;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public boolean release(Channel channel) {
 | |
| 			if (!this.activeChannels.remove(channel)) {
 | |
| 				return false;
 | |
| 			} else {
 | |
| 				channel.destroy();
 | |
| 				return true;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public void cleanup() {
 | |
| 			this.activeChannels.forEach(Channel::destroy);
 | |
| 			this.activeChannels.clear();
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public int getMaxCount() {
 | |
| 			return this.limit;
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public int getUsedCount() {
 | |
| 			return this.activeChannels.size();
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	@Environment(EnvType.CLIENT)
 | |
| 	public static enum Pool {
 | |
| 		STATIC,
 | |
| 		STREAMING;
 | |
| 	}
 | |
| }
 |