refactor audio system

This commit is contained in:
Bixilon 2021-11-27 23:39:35 +01:00
parent 8f010c6b84
commit 38993d28da
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
23 changed files with 487 additions and 280 deletions

View File

@ -18,7 +18,6 @@ import de.bixilon.minosoft.data.registries.blocks.properties.BlockProperties
import de.bixilon.minosoft.data.registries.blocks.types.Block
import de.bixilon.minosoft.data.registries.materials.Material
import de.bixilon.minosoft.data.registries.registries.Registries
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.gui.rendering.models.baked.block.BakedBlockModel
import de.bixilon.minosoft.util.KUtil.toBoolean
import de.bixilon.minosoft.util.KUtil.toInt
@ -35,11 +34,11 @@ data class BlockState(
val outlineShape: VoxelShape,
val hardness: Float,
val requiresTool: Boolean,
val breakSoundEvent: SoundEvent?,
val stepSoundEvent: SoundEvent?,
val placeSoundEvent: SoundEvent?,
val hitSoundEvent: SoundEvent?,
val fallSoundEvent: SoundEvent?,
val breakSoundEvent: ResourceLocation?,
val stepSoundEvent: ResourceLocation?,
val placeSoundEvent: ResourceLocation?,
val hitSoundEvent: ResourceLocation?,
val fallSoundEvent: ResourceLocation?,
val soundEventVolume: Float = 1.0f,
val soundEventPitch: Float = 1.0f,
) {

View File

@ -40,7 +40,6 @@ import kotlin.random.Random
open class CampfireBlock(resourceLocation: ResourceLocation, registries: Registries, data: Map<String, Any>) : Block(resourceLocation, registries, data) {
val lavaParticles = data["lava_particles"]?.toBoolean() ?: true
private val campfireCrackleSoundEvent = registries.soundEventRegistry[CAMPFIRE_CRACKLE_SOUND_RESOURCE_LOCATION]!!
private val cosySmokeParticle = registries.particleTypeRegistry[CampfireSmokeParticle.CosyFactory]!!
private val signalSmokeParticle = registries.particleTypeRegistry[CampfireSmokeParticle.SignalFactory]!!
private val lavaParticle = registries.particleTypeRegistry[LavaParticle]!!
@ -82,7 +81,7 @@ open class CampfireBlock(resourceLocation: ResourceLocation, registries: Registr
return
}
if (random.chance(10)) {
connection.world.playSoundEvent(campfireCrackleSoundEvent, blockPosition + Vec3(0.5f), 0.5f + random.nextFloat(), 0.6f + random.nextFloat() * 0.7f)
connection.world.playSoundEvent(CAMPFIRE_CRACKLE_SOUND, blockPosition + Vec3(0.5f), 0.5f + random.nextFloat(), 0.6f + random.nextFloat() * 0.7f)
}
if (lavaParticles && random.chance(20)) {
@ -104,7 +103,7 @@ open class CampfireBlock(resourceLocation: ResourceLocation, registries: Registr
}
companion object : BlockFactory<CampfireBlock> {
private val CAMPFIRE_CRACKLE_SOUND_RESOURCE_LOCATION = "minecraft:block.campfire.crackle".toResourceLocation()
private val CAMPFIRE_CRACKLE_SOUND = "minecraft:block.campfire.crackle".toResourceLocation()
override fun build(resourceLocation: ResourceLocation, registries: Registries, data: Map<String, Any>): CampfireBlock {
return CampfireBlock(resourceLocation, registries, data)

View File

@ -15,7 +15,6 @@ package de.bixilon.minosoft.data.registries.items
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.data.registries.registries.Registries
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.util.KUtil.nullCast
open class MusicDiscItem(
@ -24,9 +23,5 @@ open class MusicDiscItem(
data: Map<String, Any>,
) : Item(resourceLocation, registries, data) {
val analogOutput = data["analog_output"].nullCast<Item>() ?: 0
val sound: SoundEvent? = null
init {
this::sound.inject(data["sound"])
}
val sound: ResourceLocation? = registries.soundEventRegistry[data["sound"]]
}

View File

@ -40,7 +40,6 @@ import de.bixilon.minosoft.data.registries.other.containers.ContainerType
import de.bixilon.minosoft.data.registries.other.game.event.GameEvent
import de.bixilon.minosoft.data.registries.particle.ParticleType
import de.bixilon.minosoft.data.registries.registries.registry.*
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.data.registries.statistics.Statistic
import de.bixilon.minosoft.data.registries.versions.Version
import de.bixilon.minosoft.protocol.packets.c2s.play.EntityActionC2SP
@ -69,7 +68,7 @@ class Registries {
val dimensionRegistry: Registry<Dimension> = Registry()
val materialRegistry: Registry<Material> = Registry()
val fluidRegistry: Registry<Fluid> = Registry()
val soundEventRegistry: Registry<SoundEvent> = Registry()
val soundEventRegistry: ResourceLocationRegistry = ResourceLocationRegistry()
val villagerProfessionRegistry: Registry<VillagerProfession> = Registry()
@ -159,7 +158,7 @@ class Registries {
entityTypeRegistry.rawInitialize(pixlyzerData["entities"]?.compoundCast(), this, EntityType)
motiveRegistry.rawInitialize(pixlyzerData["motives"]?.compoundCast(), this, Motive, version.isFlattened())
soundEventRegistry.rawInitialize(pixlyzerData["sound_events"]?.compoundCast(), this, SoundEvent)
soundEventRegistry.rawInitialize(pixlyzerData["sound_events"]?.compoundCast())
particleTypeRegistry.rawInitialize(pixlyzerData["particles"]?.compoundCast(), this, ParticleType)
materialRegistry.rawInitialize(pixlyzerData["materials"]?.compoundCast(), this, Material)
enchantmentRegistry.rawInitialize(pixlyzerData["enchantments"]?.compoundCast(), this, Enchantment)

View File

@ -195,6 +195,7 @@ open class Registry<T : RegistryItem>(
BITS_16,
}
@Deprecated("TODO")
override fun iterator(): Iterator<T> {
return resourceLocationMap.values.iterator()
}

View File

@ -0,0 +1,81 @@
package de.bixilon.minosoft.data.registries.registries.registry
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.util.KUtil.toInt
import de.bixilon.minosoft.util.json.ResourceLocationJsonMap.toResourceLocationMap
class ResourceLocationRegistry(
override var parent: AbstractRegistry<ResourceLocation>? = null,
) : AbstractRegistry<ResourceLocation> {
private var initialized = false
private val idValueMap: MutableMap<Int, ResourceLocation> = mutableMapOf()
private val valueIdMap: MutableMap<ResourceLocation, Int> = mutableMapOf()
override val size: Int
get() {
val value = idValueMap.size
parent?.let {
return value + it.size
}
return value
}
override fun clear() {
idValueMap.clear()
valueIdMap.clear()
}
override fun get(any: Any?): ResourceLocation? {
check(any is Int) { "Don't know how to get $any" }
return idValueMap[any]
}
override fun get(id: Int): ResourceLocation? {
return idValueMap[id]
}
override fun getId(value: ResourceLocation): Int {
return valueIdMap[value] ?: -1
}
fun initialize(data: Map<ResourceLocation, Any>?, alternative: ResourceLocationRegistry? = null): ResourceLocationRegistry {
check(!initialized) { "Already initialized" }
if (data == null) {
if (alternative != null) {
parent = alternative
}
return this
}
for ((resourceLocation, value) in data) {
val id: Int = when (value) {
is Number -> value.toInt()
is Map<*, *> -> value["id"].toInt()
else -> throw IllegalArgumentException("Don't know what $value is!")
}
idValueMap[id] = resourceLocation
valueIdMap[resourceLocation] = id
}
if (idValueMap.isEmpty()) {
parent = alternative
}
initialized = true
return this
}
fun rawInitialize(data: Map<String, Any>?, alternative: ResourceLocationRegistry? = null): ResourceLocationRegistry {
return initialize(data?.toResourceLocationMap(), alternative)
}
override fun toString(): String {
return super.toString() + ": ${idValueMap.size}x"
}
@Deprecated("TODO")
override fun iterator(): Iterator<ResourceLocation> {
return idValueMap.values.iterator()
}
}

View File

@ -1,35 +0,0 @@
/*
* Minosoft
* Copyright (C) 2021 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.data.registries.sounds
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.data.registries.registries.Registries
import de.bixilon.minosoft.data.registries.registries.registry.RegistryItem
import de.bixilon.minosoft.data.registries.registries.registry.ResourceLocationDeserializer
data class SoundEvent(
override val resourceLocation: ResourceLocation,
) : RegistryItem() {
override fun toString(): String {
return resourceLocation.toString()
}
companion object : ResourceLocationDeserializer<SoundEvent> {
override fun deserialize(registries: Registries?, resourceLocation: ResourceLocation, data: Map<String, Any>): SoundEvent {
return SoundEvent(
resourceLocation = resourceLocation,
)
}
}
}

View File

@ -0,0 +1,19 @@
package de.bixilon.minosoft.data.world
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.gui.rendering.util.VecUtil.centerf
import glm_.vec3.Vec3
import glm_.vec3.Vec3i
interface AbstractAudioPlayer {
fun playSoundEvent(sound: ResourceLocation, position: Vec3i? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
playSoundEvent(sound, position?.centerf, volume, pitch)
}
fun playSoundEvent(sound: ResourceLocation, position: Vec3? = null, volume: Float = 1.0f, pitch: Float = 1.0f)
fun stopSound(sound: ResourceLocation)
// ToDo: Stop category
}

View File

@ -20,12 +20,10 @@ import de.bixilon.minosoft.data.registries.biomes.Biome
import de.bixilon.minosoft.data.registries.blocks.BlockState
import de.bixilon.minosoft.data.registries.blocks.types.FluidBlock
import de.bixilon.minosoft.data.registries.dimension.DimensionProperties
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.data.world.biome.accessor.BiomeAccessor
import de.bixilon.minosoft.data.world.biome.accessor.NoiseBiomeAccessor
import de.bixilon.minosoft.gui.rendering.particle.ParticleRenderer
import de.bixilon.minosoft.gui.rendering.particle.types.Particle
import de.bixilon.minosoft.gui.rendering.sound.AudioPlayer
import de.bixilon.minosoft.gui.rendering.util.VecUtil.blockPosition
import de.bixilon.minosoft.gui.rendering.util.VecUtil.chunkPosition
import de.bixilon.minosoft.gui.rendering.util.VecUtil.inChunkPosition
@ -59,7 +57,7 @@ import kotlin.random.Random
*/
class World(
val connection: PlayConnection,
) : BiomeAccessor {
) : BiomeAccessor, AbstractAudioPlayer {
val lock = ReadWriteLock()
var cacheBiomeAccessor: NoiseBiomeAccessor? = null
val chunks: LockMap<Vec2i, Chunk> = lockMapOf()
@ -76,7 +74,7 @@ class World(
var thunderGradient = 0.0f
private val random = Random
var audioPlayer: AudioPlayer? = null
var audioPlayer: AbstractAudioPlayer? = null
var particleRenderer: ParticleRenderer? = null
operator fun get(blockPosition: Vec3i): BlockState? {
@ -188,20 +186,13 @@ class World(
return ret.toMap()
}
fun playSoundEvent(resourceLocation: ResourceLocation, position: Vec3i? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
audioPlayer?.playSoundEvent(resourceLocation, position, volume, pitch)
override fun playSoundEvent(sound: ResourceLocation, position: Vec3?, volume: Float, pitch: Float) {
audioPlayer?.playSoundEvent(sound, position, volume, pitch)
}
fun playSoundEvent(resourceLocation: ResourceLocation, position: Vec3? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
audioPlayer?.playSoundEvent(resourceLocation, position, volume, pitch)
}
fun playSoundEvent(soundEvent: SoundEvent, position: Vec3i? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
audioPlayer?.playSoundEvent(soundEvent, position, volume, pitch)
}
fun playSoundEvent(soundEvent: SoundEvent, position: Vec3? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
audioPlayer?.playSoundEvent(soundEvent, position, volume, pitch)
override fun stopSound(sound: ResourceLocation) {
audioPlayer?.stopSound(sound)
}
fun addParticle(particle: Particle) {

View File

@ -43,9 +43,7 @@ class WeightedBakedModel(
}
private fun getModel(random: Random): BakedBlockModel {
val totalWeight = abs(random.nextLong() % totalWeight)
var weightLeft = totalWeight
var weightLeft = abs(random.nextLong() % totalWeight)
for ((model, weight) in models) {
weightLeft -= weight

View File

@ -13,35 +13,24 @@
package de.bixilon.minosoft.gui.rendering.sound
import com.google.gson.JsonArray
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.data.world.AbstractAudioPlayer
import de.bixilon.minosoft.gui.rendering.Rendering
import de.bixilon.minosoft.gui.rendering.input.camera.Camera
import de.bixilon.minosoft.gui.rendering.modding.events.CameraPositionChangeEvent
import de.bixilon.minosoft.gui.rendering.sound.sounds.Sound
import de.bixilon.minosoft.gui.rendering.sound.sounds.SoundList
import de.bixilon.minosoft.gui.rendering.util.VecUtil.centerf
import de.bixilon.minosoft.gui.rendering.util.vec.vec3.Vec3Util.EMPTY
import de.bixilon.minosoft.modding.event.invoker.CallbackEventInvoker
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import de.bixilon.minosoft.util.CountUpAndDownLatch
import de.bixilon.minosoft.util.KUtil.nullCast
import de.bixilon.minosoft.util.KUtil.synchronizedListOf
import de.bixilon.minosoft.util.KUtil.toBoolean
import de.bixilon.minosoft.util.KUtil.toInt
import de.bixilon.minosoft.util.KUtil.toResourceLocation
import de.bixilon.minosoft.util.KUtil.toSynchronizedList
import de.bixilon.minosoft.util.KUtil.unsafeCast
import de.bixilon.minosoft.util.Queue
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import de.bixilon.minosoft.util.nbt.tag.NBTUtil.listCast
import glm_.vec3.Vec3
import glm_.vec3.Vec3i
import org.lwjgl.openal.AL
import org.lwjgl.openal.ALC
import org.lwjgl.openal.ALC10.*
@ -54,14 +43,13 @@ import java.nio.IntBuffer
class AudioPlayer(
val connection: PlayConnection,
val rendering: Rendering,
) {
private val sounds: MutableMap<SoundEvent, SoundList> = mutableMapOf()
) : AbstractAudioPlayer {
private val soundManager = SoundManager(connection)
var initialized = false
private set
private var device = 0L
private var context = 0L
private var device = -1L
private var context = -1L
private val queue = Queue()
private lateinit var listener: SoundListener
@ -74,27 +62,12 @@ class AudioPlayer(
get() = sources.size
private fun preloadSounds() {
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.VERBOSE) { "Preloading sounds..." }
if (SoundConstants.DISABLE_PRELOADING) {
return
}
for (soundList in sounds.values) {
for (sound in soundList.sounds) {
if (SoundConstants.PRELOAD_ALL_SOUNDS || sound.preload) {
sound.load(connection.assetsManager)
}
}
}
}
fun init(latch: CountUpAndDownLatch) {
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.INFO) { "Loading OpenAL..." }
loadSounds()
preloadSounds()
soundManager.load()
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.VERBOSE) { "Preloading sounds..." }
soundManager.preload()
@ -125,31 +98,33 @@ class AudioPlayer(
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.INFO) { "OpenAL loaded!" }
initialized = true
connection.world.audioPlayer = this
latch.dec()
}
fun playSoundEvent(resourceLocation: ResourceLocation, position: Vec3i? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
connection.registries.soundEventRegistry[resourceLocation]?.let { playSoundEvent(it, position?.centerf, volume, pitch) }
}
fun playSoundEvent(resourceLocation: ResourceLocation, position: Vec3? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
connection.registries.soundEventRegistry[resourceLocation]?.let { playSoundEvent(it, position, volume, pitch) }
}
fun playSoundEvent(soundEvent: SoundEvent, position: Vec3i? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
playSoundEvent(soundEvent, position?.centerf, volume, pitch)
}
fun playSoundEvent(soundEvent: SoundEvent, position: Vec3? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
override fun playSoundEvent(sound: ResourceLocation, position: Vec3?, volume: Float, pitch: Float) {
if (!initialized) {
return
}
playSound(sounds[soundEvent]!!.getRandom(), position, volume, pitch)
playSound(soundManager[sound] ?: return, position, volume, pitch)
}
override fun stopSound(sound: ResourceLocation) {
queue += {
for (source in sources) {
if (!source.isPlaying) {
continue
}
if (source.sound?.soundEvent != sound) {
continue
}
source.stop()
}
}
}
@Synchronized
private fun getAvailableSource(): SoundSource? {
for (source in sources.toSynchronizedList()) {
if (source.available) {
@ -169,11 +144,8 @@ class AudioPlayer(
private fun playSound(sound: Sound, position: Vec3? = null, volume: Float = 1.0f, pitch: Float = 1.0f) {
queue += add@{
sound.load(connection.assetsManager)
if (sound.loadFailed) {
return@add
}
val source = getAvailableSource() ?: let {
// ToDo: Queue sound for later (and check a certain delay to not make the game feel laggy)
val source = getAvailableSource()
if (source == null) {
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.WARN) { "Can not play sound: No source available: $sound" }
return@add
}
@ -191,20 +163,23 @@ class AudioPlayer(
}
}
private fun calculateAvailableSources() {
var availableSources = 0
for (source in sources) {
if (source.available) {
availableSources++
}
}
this.availableSources = availableSources
}
fun startLoop() {
while (true) {
if (connection.wasConnected || connection.error != null) {
break
}
queue.work()
var availableSources = 0
for (source in sources) {
if (source.available) {
availableSources++
}
}
this.availableSources = availableSources
calculateAvailableSources()
Thread.sleep(1L)
}
@ -214,11 +189,8 @@ class AudioPlayer(
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.INFO) { "Unloading OpenAL..." }
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.VERBOSE) { "Unloading sounds..." }
for (soundList in sounds.values) {
for (sound in soundList.sounds) {
sound.unload()
}
}
soundManager.unload()
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.VERBOSE) { "Unloading sources..." }
for (source in sources.toSynchronizedList()) {
source.unload()
@ -229,53 +201,7 @@ class AudioPlayer(
alcSetThreadContext(MemoryUtil.NULL)
alcDestroyContext(context)
alcCloseDevice(device)
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.INFO) { "Unloaded OpenAL!" }
}
@Deprecated("Refactoring needed")
private fun loadSounds() {
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.VERBOSE) { "Loading sounds.json" }
val data = connection.assetsManager.readJsonAsset(SOUNDS_INDEX_FILE)
for ((soundEventResourceLocation, json) in data) {
check(json is Map<*, *>)
val soundEvent = connection.registries.soundEventRegistry[ResourceLocation(soundEventResourceLocation)]!!
val sounds: MutableSet<Sound> = mutableSetOf()
fun String.getSoundLocation(): ResourceLocation {
return ResourceLocation(ProtocolDefinition.DEFAULT_NAMESPACE, "sounds/${this}".replace('.', '/') + ".ogg") // ToDo: Resource Location
}
for (soundJson in json["sounds"]!!.listCast<Any>()!!) {
when (soundJson) {
is String -> {
sounds += Sound(soundJson.getSoundLocation())
}
is Map<*, *> -> {
sounds += Sound(
path = soundJson["name"].unsafeCast<String>().getSoundLocation(),
volume = soundJson["volume"]?.unsafeCast<Double>()?.toFloat() ?: 1.0f,
pitch = soundJson["pitch"]?.unsafeCast<Double>()?.toFloat() ?: 1.0f,
weight = soundJson["weight"]?.toInt() ?: 1,
stream = soundJson["stream"]?.toBoolean() ?: false,
attenuationDistance = soundJson["attenuation_distance"]?.toInt() ?: 16,
preload = soundJson["preload"]?.toBoolean() ?: false,
)
}
is JsonArray -> TODO()
}
}
this.sounds[soundEvent] = SoundList(
soundEvent = soundEvent,
sounds = sounds.toSet(),
subTitle = json["subtitle"].nullCast<String>()?.let { ResourceLocation(ProtocolDefinition.DEFAULT_NAMESPACE, it) },
)
}
}
companion object {
private val SOUNDS_INDEX_FILE = "minecraft:sounds.json".toResourceLocation()
}
}

View File

@ -0,0 +1,55 @@
package de.bixilon.minosoft.gui.rendering.sound
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.gui.rendering.sound.sounds.Sound
import de.bixilon.minosoft.gui.rendering.sound.sounds.SoundType
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.util.KUtil.toResourceLocation
import de.bixilon.minosoft.util.nbt.tag.NBTUtil.asCompound
import java.util.*
class SoundManager(
private val connection: PlayConnection,
) {
private val random = Random()
private val sounds: MutableMap<ResourceLocation, SoundType> = mutableMapOf()
fun load() {
val soundsIndex = connection.assetsManager.readJsonAsset(SOUNDS_INDEX_FILE)
for ((name, data) in soundsIndex) {
val resourceLocation = name.toResourceLocation()
sounds[resourceLocation] = SoundType(resourceLocation, data.asCompound())
}
}
@Synchronized
fun unload() {
for (soundType in sounds.values) {
for (sound in soundType.sounds) {
sound.unload()
}
}
}
@Synchronized
fun preload() {
for (soundType in sounds.values) {
for (sound in soundType.sounds) {
if (!sound.preload) {
continue
}
sound.load(connection.assetsManager)
}
}
}
operator fun get(sound: ResourceLocation): Sound? {
return sounds[sound]?.getSound(random)
}
companion object {
private val SOUNDS_INDEX_FILE = "minecraft:sounds.json".toResourceLocation()
}
}

View File

@ -61,11 +61,15 @@ class SoundSource {
var sound: Sound? = null
set(value) {
stop()
if (value?.loaded != true || value.loadFailed) {
val buffer = value?.buffer
if (buffer == null) {
field = null
return
}
alSourcei(source, AL_BUFFER, value.buffer)
if (buffer.unloaded) {
throw IllegalArgumentException("OpenAL buffer is not loaded: ${value.soundEvent}")
}
alSourcei(source, AL_BUFFER, buffer.buffer)
field = value
}
@ -73,7 +77,7 @@ class SoundSource {
get() = alGetSourcei(source, AL_SOURCE_STATE) == AL_PLAYING
val available: Boolean
get() = !isPlaying || System.currentTimeMillis() - playTime > (sound?.length ?: 0L) // ToDo: Allow pause
get() = !isPlaying || System.currentTimeMillis() - playTime > (sound?.data?.length ?: 0L) // ToDo: Allow pause
fun play() {
playTime = System.currentTimeMillis()

View File

@ -11,18 +11,25 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.rendering.sound.sounds
package de.bixilon.minosoft.gui.rendering.sound
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.util.KUtil.toResourceLocation
data class SoundList(
val soundEvent: SoundEvent,
val sounds: Set<Sound>,
val subTitle: ResourceLocation?,
) {
fun getRandom(): Sound {
// ToDo: Support weight
return sounds.random()
object SoundUtil {
fun ResourceLocation.sound(): ResourceLocation {
var path = ""
if (!this.path.startsWith("sounds/")) {
path += "sounds/"
}
path += this.path
if (!path.contains(".")) {
// ending
path += ".ogg"
}
return "$namespace:$path".toResourceLocation()
}
}

View File

@ -0,0 +1,38 @@
package de.bixilon.minosoft.gui.rendering.sound.sounds
import org.lwjgl.openal.AL10.*
import org.lwjgl.system.MemoryUtil.memFree
import java.nio.ShortBuffer
class OpenALBuffer(
val data: SoundData,
) {
val buffer: Int
private val pcm: ShortBuffer
var unloaded: Boolean = false
private set
init {
val pcm = data.createPCM()
this.pcm = pcm
this.buffer = alGenBuffers()
alBufferData(buffer, data.format, pcm, data.sampleRate)
}
@Synchronized
fun unload() {
if (unloaded) {
return
}
alDeleteBuffers(buffer)
memFree(pcm)
unloaded = true
}
protected fun finalize() {
unload()
}
}

View File

@ -15,102 +15,80 @@ package de.bixilon.minosoft.gui.rendering.sound.sounds
import de.bixilon.minosoft.data.assets.AssetsManager
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.gui.rendering.sound.SoundUtil.sound
import de.bixilon.minosoft.util.KUtil.toBoolean
import de.bixilon.minosoft.util.KUtil.toFloat
import de.bixilon.minosoft.util.KUtil.toInt
import de.bixilon.minosoft.util.KUtil.toResourceLocation
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import org.lwjgl.BufferUtils
import org.lwjgl.openal.AL10.*
import org.lwjgl.stb.STBVorbis.*
import org.lwjgl.stb.STBVorbisInfo
import org.lwjgl.system.MemoryUtil
import java.io.FileNotFoundException
import java.nio.ByteBuffer
data class Sound(
val soundEvent: ResourceLocation,
val path: ResourceLocation,
val volume: Float = 1.0f,
val pitch: Float = 1.0f,
val weight: Int = 1,
val stream: Boolean = false, // ToDo
val attenuationDistance: Int = 16,
val stream: Boolean = false, // ToDo: Implement
val attenuationDistance: Int = 16, // ToDo: Implement
val preload: Boolean = false,
// ToDo: type
) {
var length: Long = -1L
var data: SoundData? = null
private set
var loaded: Boolean = false
var buffer: OpenALBuffer? = null
private set
var loadFailed: Boolean = false
private set
var channels: Int = -1
private set
var sampleRate: Int = -1
private set
var samplesLength: Int = -1
private set
var sampleSeconds: Float = -1.0f
private set
var buffer = -1
private set
private var vorbisBuffer: ByteBuffer? = null
@Synchronized
fun load(assetsManager: AssetsManager) {
if (loaded || loadFailed) {
if (data != null) {
return
}
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.VERBOSE) { "Loading audio file: $path" }
try {
val vorbisBuffer = assetsManager.readByteAsset(path)
this.vorbisBuffer = vorbisBuffer
val error = BufferUtils.createIntBuffer(1)
val vorbis = stb_vorbis_open_memory(vorbisBuffer, error, null)
if (vorbis == MemoryUtil.NULL) {
throw IllegalStateException("Can not load vorbis: ${path}: ${error[0]}")
}
val info = stb_vorbis_get_info(vorbis, STBVorbisInfo.malloc())
channels = info.channels()
val format = when (channels) {
1 -> AL_FORMAT_MONO16
2 -> AL_FORMAT_STEREO16
else -> error("Don't know vorbis channels: $channels")
}
sampleRate = info.sample_rate()
samplesLength = stb_vorbis_stream_length_in_samples(vorbis)
sampleSeconds = stb_vorbis_stream_length_in_seconds(vorbis)
length = (sampleSeconds * 1000).toLong()
val pcm = BufferUtils.createShortBuffer(samplesLength)
pcm.limit(stb_vorbis_get_samples_short_interleaved(vorbis, channels, pcm) * channels)
//ToDo: Somehow crashed?: MemoryUtil.memFree(vorbisBuffer)
this.buffer = alGenBuffers()
alBufferData(buffer, format, pcm, sampleRate)
loaded = true
val data = SoundData(assetsManager, this)
this.data = data
this.buffer = OpenALBuffer(data)
} catch (exception: FileNotFoundException) {
loadFailed = true
Log.log(LogMessageType.AUDIO_LOADING, LogLevels.WARN) { "Can not load sound: $path: $exception" }
}
}
@Synchronized
fun unload() {
if (!loaded) {
return
data?.unload()
buffer?.unload()
}
protected fun finalize() {
unload()
}
companion object {
operator fun invoke(soundEvent: ResourceLocation, data: Any): Sound {
if (data is String) {
return Sound(
soundEvent = soundEvent,
path = data.toResourceLocation().sound(),
)
}
check(data is Map<*, *>)
// ToDo: "type" attribute: event
return Sound(
soundEvent = soundEvent,
path = data["name"].toResourceLocation(),
volume = data["volume"]?.toFloat() ?: 1.0f,
pitch = data["pitch"]?.toFloat() ?: 1.0f,
weight = data["weight"]?.toInt() ?: 1,
stream = data["stream"]?.toBoolean() ?: false,
attenuationDistance = data["attenuation_distance"]?.toInt() ?: 16,
preload = data["preload"]?.toBoolean() ?: false,
)
}
alDeleteBuffers(buffer)
vorbisBuffer?.let { MemoryUtil.memFree(it) }
buffer = -1
channels = -1
sampleRate = -1
samplesLength = -1
sampleSeconds = -1.0f
loaded = false
}
}

View File

@ -0,0 +1,80 @@
package de.bixilon.minosoft.gui.rendering.sound.sounds
import de.bixilon.minosoft.data.assets.AssetsManager
import org.lwjgl.BufferUtils
import org.lwjgl.openal.AL10.AL_FORMAT_MONO16
import org.lwjgl.openal.AL10.AL_FORMAT_STEREO16
import org.lwjgl.stb.STBVorbis.*
import org.lwjgl.stb.STBVorbisInfo
import org.lwjgl.system.MemoryUtil
import org.lwjgl.system.MemoryUtil.memFree
import java.nio.ByteBuffer
import java.nio.ShortBuffer
class SoundData(
val vorbis: Long,
val format: Int,
val buffer: ByteBuffer,
val length: Long,
val channels: Int,
val sampleRate: Int,
val samplesLength: Int,
val sampleSeconds: Float,
) {
private var unloaded = false
@Synchronized
fun unload() {
if (unloaded) {
return
}
memFree(buffer)
unloaded = true
}
protected fun finalize() {
unload()
}
fun createPCM(): ShortBuffer {
val pcm = BufferUtils.createShortBuffer(samplesLength)
pcm.limit(stb_vorbis_get_samples_short_interleaved(vorbis, channels, pcm) * channels)
return pcm
}
companion object {
operator fun invoke(assetsManager: AssetsManager, sound: Sound): SoundData {
val buffer = assetsManager.readByteAsset(sound.path)
val error = BufferUtils.createIntBuffer(1)
val vorbis = stb_vorbis_open_memory(buffer, error, null)
if (vorbis == MemoryUtil.NULL) {
throw IllegalStateException("Can not load vorbis: ${sound.path}: ${error[0]}")
}
val info = stb_vorbis_get_info(vorbis, STBVorbisInfo.malloc())
val channels = info.channels()
val format = when (channels) {
1 -> AL_FORMAT_MONO16
2 -> AL_FORMAT_STEREO16
else -> error("Don't know vorbis channels: $channels")
}
val sampleRate = info.sample_rate()
val samplesLength = stb_vorbis_stream_length_in_samples(vorbis)
val sampleSeconds = stb_vorbis_stream_length_in_seconds(vorbis)
val length = (sampleSeconds * 1000).toLong()
return SoundData(
vorbis = vorbis,
format = format,
buffer = buffer,
length = length,
channels = channels,
sampleRate = sampleRate,
samplesLength = samplesLength,
sampleSeconds = sampleSeconds
)
}
}
}

View File

@ -0,0 +1,68 @@
/*
* Minosoft
* Copyright (C) 2021 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.rendering.sound.sounds
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.util.KUtil.asList
import de.bixilon.minosoft.util.KUtil.toResourceLocation
import java.util.*
import kotlin.math.abs
data class SoundType(
val soundEvent: ResourceLocation,
val sounds: Set<Sound>,
val subtitle: ResourceLocation?,
) {
val totalWeight: Int
init {
var totalWeight = 0
for (sound in sounds) {
totalWeight += sound.weight
}
this.totalWeight = totalWeight
}
fun getSound(random: Random): Sound {
var weightLeft = abs(random.nextLong() % totalWeight)
for (sound in sounds) {
weightLeft -= sound.weight
if (weightLeft < 0) {
return sound
}
}
throw IllegalStateException("Could not find sound: This should never happen!")
}
companion object {
operator fun invoke(soundEvent: ResourceLocation, data: Map<String, Any>): SoundType {
// ToDo: "replace" attribute
val subtitle = data["subtitle"]?.toResourceLocation()
val sounds: MutableSet<Sound> = mutableSetOf()
for (soundData in data["sounds"].asList()) {
sounds += Sound(soundEvent, soundData)
}
return SoundType(
soundEvent = soundEvent,
sounds = sounds,
subtitle = subtitle,
)
}
}
}

View File

@ -14,7 +14,7 @@
package de.bixilon.minosoft.modding.event.events
import de.bixilon.minosoft.data.SoundCategories
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.modding.event.EventInitiators
import de.bixilon.minosoft.modding.event.events.connection.play.PlayConnectionEvent
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
@ -27,7 +27,7 @@ class PlaySoundEvent(
initiator: EventInitiators,
val category: SoundCategories?,
position: Vec3,
val soundEvent: SoundEvent,
val soundEvent: ResourceLocation,
val volume: Float,
val pitch: Float,
) : PlayConnectionEvent(connection, initiator), CancelableEvent {

View File

@ -13,7 +13,7 @@
package de.bixilon.minosoft.protocol.packets.s2c.play
import de.bixilon.minosoft.data.SoundCategories
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
import de.bixilon.minosoft.protocol.protocol.PlayInByteBuffer
import de.bixilon.minosoft.util.logging.Log
@ -21,7 +21,7 @@ import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
class EntitySoundEventS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
val soundEvent: SoundEvent = buffer.connection.registries.soundEventRegistry[buffer.readVarInt()]
val soundEvent: ResourceLocation = buffer.connection.registries.soundEventRegistry[buffer.readVarInt()]!!
val category: SoundCategories = SoundCategories[buffer.readVarInt()]
val entityId: Int = buffer.readVarInt()
val volume: Float = buffer.readFloat()
@ -30,5 +30,4 @@ class EntitySoundEventS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
override fun log() {
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Entity sound effect (soundEvent=$soundEvent, category=$category, entityId$entityId, volume=$volume, pitch=$pitch)" }
}
}

View File

@ -14,7 +14,7 @@ package de.bixilon.minosoft.protocol.packets.s2c.play
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.data.SoundCategories
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.modding.event.events.PlaySoundEvent
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
@ -27,7 +27,7 @@ import de.bixilon.minosoft.util.logging.LogMessageType
import glm_.vec3.Vec3
class NamedSoundEventS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
val soundEvent: SoundEvent?
val soundEvent: ResourceLocation?
val volume: Float
val pitch: Float
lateinit var position: Vec3

View File

@ -14,7 +14,7 @@ package de.bixilon.minosoft.protocol.packets.s2c.play
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.data.SoundCategories
import de.bixilon.minosoft.data.registries.sounds.SoundEvent
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.modding.event.events.PlaySoundEvent
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
@ -30,7 +30,7 @@ class SoundEventS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
var category: SoundCategories? = null
private set
val position: Vec3i
val soundEvent: SoundEvent
val soundEvent: ResourceLocation
val volume: Float
val pitch: Float
@ -39,7 +39,7 @@ class SoundEventS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
// category was moved to the top
this.category = SoundCategories[buffer.readVarInt()]
}
soundEvent = buffer.connection.registries.soundEventRegistry[buffer.readVarInt()]
soundEvent = buffer.connection.registries.soundEventRegistry[buffer.readVarInt()]!!
if (buffer.versionId >= ProtocolVersions.V_17W15A && buffer.versionId < ProtocolVersions.V_17W18A) {
buffer.readString() // parrot entity type
}

View File

@ -14,6 +14,7 @@ package de.bixilon.minosoft.protocol.packets.s2c.play
import de.bixilon.minosoft.data.SoundCategories
import de.bixilon.minosoft.data.registries.ResourceLocation
import de.bixilon.minosoft.protocol.network.connection.play.PlayConnection
import de.bixilon.minosoft.protocol.packets.s2c.PlayS2CPacket
import de.bixilon.minosoft.protocol.protocol.PlayInByteBuffer
import de.bixilon.minosoft.protocol.protocol.ProtocolVersions
@ -21,7 +22,6 @@ import de.bixilon.minosoft.util.BitByte.isBitMask
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import java.util.*
class StopSoundS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
val category: SoundCategories?
@ -31,7 +31,7 @@ class StopSoundS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
var category: SoundCategories? = null
var sound: ResourceLocation? = null
if (buffer.versionId < ProtocolVersions.V_17W45A) { // ToDo: these 2 values need to be switched in before 1.12.2
category = SoundCategories.valueOf(buffer.readString().uppercase(Locale.getDefault()))
category = SoundCategories.valueOf(buffer.readString().uppercase())
sound = buffer.readResourceLocation()
} else {
val flags = buffer.readByte()
@ -46,6 +46,11 @@ class StopSoundS2CP(buffer: PlayInByteBuffer) : PlayS2CPacket() {
this.sound = sound
}
override fun handle(connection: PlayConnection) {
sound?.let { connection.world.stopSound(it) }
// ToDo: Category
}
override fun log() {
Log.log(LogMessageType.NETWORK_PACKETS_IN, level = LogLevels.VERBOSE) { "Stop sound (category=$category, sound=$sound)" }
}