Skip to content

Commit

Permalink
Whoopsiedaisy
Browse files Browse the repository at this point in the history
  • Loading branch information
kikugie committed Jul 27, 2024
1 parent dcace52 commit 003c311
Show file tree
Hide file tree
Showing 24 changed files with 271 additions and 156 deletions.
62 changes: 4 additions & 58 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,59 +1,5 @@
## Resource pack sounds (AKA sound packs)
This update allows packaging sounds as resource packs to share them with other players.
## Woah new update in less than a day

In your resource pack add `.wav` files in `assets/<namespace>/soundboard`.
You can create any subdirectories inside to organize files or
create other namespaces to override other sounds.
Created subdirectories will form collapsible groups in the soundboard menu.

By default, provided sounds won't look pretty.
You need to provide a translation key for them.
Translations are stored in `assets/<namespace>/lang`.
The default language is `en_us.json`.

Category names are prefixed with `soundboard.dir.<namespace>`.
For example, if you have a file in `assets/mysoundpack/soundboard/scary/ghost_boo.wav`,
you'll have the following translation file:
```json
{
"soundboard.dir.mysoundpack": "My first sound pack!",
"soundboard.dir.mysoundpack.scary": "Spooky sounds",
"soundboard.file.mysoundpack.scary.ghost_boo": "Very scary ghost sound"
}
```

You can see the example sound pack in the [Soundboard repository](https://github.com/kikugie/voicechat-soundboard/tree/multiaddon/src/main/resources/resourcepacks/default).
Translation files also support [OwO-lib rich translations](https://docs.wispforest.io/owo/rich-translations/)

## Translatable local sounds
You still can put `.wav` files in `.minecraft/config/soundboard`,
but this update allows you to customize their appearance.

To provide a sound name, put a `<filename>.properties` in its directory.
For the category name put a `.properties` file inside it.

Property files follow the [Java properties format](https://docs.oracle.com/cd/E23095_01/Platform.93/ATGProgGuide/html/s0204propertiesfileformat01.html), but only the `title` field matters.

For example, if you have the sound `.minecraft/config/soundboard/scary/ghost_boo.wav`,
you use these files:

```properties
# .minecraft/config/soundboard/scary/.properties
title=Spooky sounds
```

```properties
# .minecraft/config/soundboard/scary/ghost_boo.properties
title=Very scary ghost sound
```

## Audio editor
Ctrl-click on any sound to bring up the editor:
![Editor GUI](https://i.imgur.com/lpIdP65.png)

The audio editor allows you to cut the duration of the sound and modify the volume.
You can also use play button in the corner to play it locally.
(Others won't hear it, unless you have bad noise suppression)

The audio editor doesn't modify the original file and saves changes when closed,
so you can bring it up again to adjust the values.
This fixes a bug in 0.4, where sounds would play for half the length.
It also comes with a new feature: favourite sounds.
![Favourites](https://i.imgur.com/7o89tD5.png)
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ org.gradle.configureondemand=true

id=soundboard
name=Soundboard Core
version=0.4.0
version=0.5.0

kowoui.id=kowoui
kowoui.name=Kotlin OwO UI
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/dev/kikugie/soundboard/Soundboard.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.kikugie.soundboard

import dev.kikugie.soundboard.audio.SoundRegistry
import dev.kikugie.soundboard.config.AudioConfig
import dev.kikugie.soundboard.config.SoundboardConfig
import dev.kikugie.soundboard.entrypoint.SoundboardAccess
import dev.kikugie.soundboard.gui.SoundBrowser
Expand All @@ -23,6 +25,7 @@ object Soundboard {
if (ready) return
ready = true

AudioConfig // Inits the object
SoundRegistry.BASE_DIR.createDirectories()
ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(SoundRegistry)
keybind(GLFW.GLFW_KEY_J, "browser", SoundBrowser.Companion::open) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package dev.kikugie.soundboard.audio
import dev.kikugie.soundboard.entrypoint.SoundboardEntrypoint

class AudioScheduler(val entry: SoundboardEntrypoint) {
val playing: Boolean
get() = provider != null
var local: Boolean = false
private set
var provider: AudioProvider? = null
Expand Down
18 changes: 10 additions & 8 deletions src/main/kotlin/dev/kikugie/soundboard/audio/SoundEntry.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
package dev.kikugie.soundboard.audio

import dev.kikugie.kowoui.translation
import net.minecraft.text.Text
import java.io.InputStream

data class SoundEntry(
val name: String,
val path: String,
val supplier: () -> InputStream,
val title: String? = null,
var settings: AudioConfiguration? = null
private val _title: String? = null,
var settings: AudioConfiguration? = null,
) {
val id: SoundId by lazy { SoundId("$path/$name") }

fun title() = title?.translation() ?: run {
var (namespace, path) = SoundRegistry.splitPath(path)
path += ".$name"
if (path.startsWith('.')) path = path.drop(1)
"soundboard.file.$namespace.$path".translation(name)
val title: Text by lazy {
_title?.translation() ?: run {
var (namespace, path) = SoundRegistry.splitPath(path)
path += ".$name"
if (path.startsWith('.')) path = path.drop(1)
"soundboard.file.$namespace.$path".translation(name)
}
}
}
19 changes: 11 additions & 8 deletions src/main/kotlin/dev/kikugie/soundboard/audio/SoundGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import net.minecraft.text.Text
data class SoundGroup(
val path: String,
val entries: List<SoundEntry>,
val title: String? = null,
private val name: String? = null,
) {
fun title(): Text = title?.translation() ?: run {
val (namespace, path) = SoundRegistry.splitPath(path)
buildString {
append("soundboard.dir")
append(".$namespace")
if (path.isNotEmpty()) append(".$path")
}.fallbackTranslation(this@SoundGroup.path)
fun isEmpty() = entries.isEmpty()
val title: Text by lazy {
name?.translation() ?: run {
val (namespace, path) = SoundRegistry.splitPath(path)
buildString {
append("soundboard.dir")
append(".$namespace")
if (path.isNotEmpty()) append(".$path")
}.fallbackTranslation(this@SoundGroup.path)
}
}
}
3 changes: 3 additions & 0 deletions src/main/kotlin/dev/kikugie/soundboard/audio/SoundId.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
package dev.kikugie.soundboard.audio

import kotlinx.serialization.Serializable

@JvmInline
@Serializable
value class SoundId(val str: String)
14 changes: 14 additions & 0 deletions src/main/kotlin/dev/kikugie/soundboard/audio/SoundRegistry.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.kikugie.soundboard.audio

import dev.kikugie.soundboard.Soundboard
import dev.kikugie.soundboard.util.PropertiesReader
import dev.kikugie.soundboard.util.idOf
import dev.kikugie.soundboard.util.memoize
Expand Down Expand Up @@ -37,6 +38,8 @@ object SoundRegistry : SimpleResourceReloadListener<EntryMap> {

val groups get() = localEntries.values.asSequence() + resourceEntries.values.asSequence()
val entries get() = localEntries.allEntries + resourceEntries.allEntries
lateinit var favourites: SoundGroup
private set

operator fun get(id: SoundId): SoundEntry? =
localCache(id) ?: resourceCache(id)
Expand All @@ -48,6 +51,12 @@ object SoundRegistry : SimpleResourceReloadListener<EntryMap> {
.filter { it.isDirectory() }
.forEach { updatePath(it.getLocal()) }
localCache.clear()
updateFavourites()
}

fun updateFavourites() {
favourites = constructFavourites()
Soundboard.config.save()
}

override fun getFabricId(): Identifier = idOf("sound_registry")
Expand Down Expand Up @@ -124,6 +133,11 @@ object SoundRegistry : SimpleResourceReloadListener<EntryMap> {
return newModified != oldModified || force
}

private fun constructFavourites(): SoundGroup {
val favourites = Soundboard.config.favourites.mapNotNull(::get)
return SoundGroup("!favourites", favourites, "soundboard.favourites")
}

private fun Path.getLocal() = BASE_DIR.relativize(this).joinToString("/")

private fun Path.readTitle() = runCatching { PropertiesReader.decode(this)["title"] }.getOrNull()
Expand Down
8 changes: 4 additions & 4 deletions src/main/kotlin/dev/kikugie/soundboard/config/AudioConfig.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dev.kikugie.soundboard.config

import com.google.gson.GsonBuilder
import com.google.gson.*
import com.google.gson.reflect.TypeToken
import dev.kikugie.soundboard.audio.AudioConfiguration
import dev.kikugie.soundboard.audio.SoundEntry
Expand Down Expand Up @@ -38,14 +38,14 @@ object AudioConfig {

fun load() {
if (file.exists() && file.isReadable()) file.reader().use {
val token: TypeToken<ConfigEntries> = TypeToken.getParameterized(Map::class.java, String::class.java, AudioConfiguration::class.java) as TypeToken<ConfigEntries>
val conf: ConfigEntries = json.fromJson(it, token)
val token: TypeToken<MutableMap<String, AudioConfiguration>> = TypeToken.getParameterized(Map::class.java, String::class.java, AudioConfiguration::class.java) as TypeToken<MutableMap<String, AudioConfiguration>>
val conf = json.fromJson(it, token).mapKeys { e -> SoundId(e.key) }
configurations.clear()
configurations.putAll(conf)
}
}

fun save() {
file.writeText(json.toJson(configurations), Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
file.writeText(json.toJson(configurations.mapKeys { it.key.str }), Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)
}
}
16 changes: 11 additions & 5 deletions src/main/kotlin/dev/kikugie/soundboard/config/SoundboardConfig.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dev.kikugie.soundboard.config

import dev.kikugie.soundboard.LOGGER
import dev.kikugie.soundboard.audio.SoundId
import dev.kikugie.soundboard.util.runOn
import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
Expand All @@ -12,12 +15,15 @@ import kotlin.io.path.*
@Serializable
@OptIn(ExperimentalSerializationApi::class)
class SoundboardConfig(
val favourites: MutableList<SoundId> = mutableListOf(),
) {
fun save() = try {
file.createParentDirectories()
file.outputStream().use { json.encodeToStream(this, it) }
} catch (e: Exception) {
LOGGER.error("Failed to save config $file", e)
fun save() = runOn(Dispatchers.IO) {
try {
file.createParentDirectories()
file.outputStream().use { json.encodeToStream(this, it) }
} catch (e: Exception) {
LOGGER.error("Failed to save config $file", e)
}
}

companion object Loader {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dev.kikugie.soundboard.entrypoint

import dev.kikugie.soundboard.SoundRegistry
import dev.kikugie.soundboard.audio.SoundEntry

object SoundboardAccess {
private val _delegates = mutableListOf<SoundboardEntrypoint>()
Expand All @@ -14,5 +14,5 @@ object SoundboardAccess {
inline fun all(selector: SoundboardEntrypoint.() -> Boolean) = delegates.all(selector)
inline fun forEach(action: SoundboardEntrypoint.() -> Unit) = delegates.forEach(action)

fun play(entry: SoundRegistry.SoundEntry, local: Boolean) = forEach { scheduleStream(entry, local) }
fun play(entry: SoundEntry, local: Boolean) = forEach { scheduleStream(entry, local) }
}
6 changes: 3 additions & 3 deletions src/main/kotlin/dev/kikugie/soundboard/gui/ScreenManager.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev.kikugie.soundboard.gui

import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import dev.kikugie.soundboard.util.currentScreen
import net.minecraft.client.gui.screen.Screen
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance

abstract class ScreenManager(private val cls: KClass<out Screen>) {
fun open() = RenderSystem.recordRenderCall { MinecraftClient.getInstance().setScreen(cls.createInstance()) }
fun close() = MinecraftClient.getInstance().currentScreen?.let { if (it::class == cls) it.close() }
fun open() = RenderSystem.recordRenderCall { currentScreen = cls.createInstance() }
fun close() = currentScreen?.let { if (it::class == cls) it.close() }
}
14 changes: 11 additions & 3 deletions src/main/kotlin/dev/kikugie/soundboard/gui/SoundBrowser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import kotlin.math.ceil

class SoundBrowser : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, BROWSER) {
private lateinit var root: FlowLayout
private var favourites: FlowLayout? = null
private var scrollbar: ScrollContainerAccessor? = null
internal var settings: SoundSettingsWidget? = null

Expand All @@ -35,10 +36,17 @@ class SoundBrowser : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, BROWS
settings = null
}

fun createFavourites(container: FlowLayout = root.childById<FlowLayout>("container")!!) {
container.removeChild(favourites)
favourites = group(SoundRegistry.favourites, false)
if (favourites != null) container.child(0, favourites)
}

override fun build(root: FlowLayout) = with(root) {
this@SoundBrowser.root = root
SoundRegistry.update()
childById<FlowLayout>("container")?.apply {
createFavourites(this)
children(SoundRegistry.groups.mapNotNull { group(it, it.path.isEmpty()) }.toList())
} ?: error("Missing browser container")
scrollbar = childById<ScrollContainer<*>>("scroll") as? ScrollContainerAccessor
Expand All @@ -64,23 +72,23 @@ class SoundBrowser : BaseUIModelScreen<FlowLayout>(FlowLayout::class.java, BROWS
private fun group(group: SoundGroup, keepEmpty: Boolean): FlowLayout? =
if (group.entries.isEmpty() && !keepEmpty) null
else model.template<FlowLayout>("group").apply {
val path = SoundRegistry.BASE_DIR.resolve(group.path).takeIf { it.exists() }
val path = runCatching { SoundRegistry.BASE_DIR.resolve(group.path) }.getOrNull()?.takeIf { it.exists() }
val container = childById<CollapsibleContainer>("collapse")?.apply {
if (path != null) mouseDown { _, _, _ -> shiftDown then { Util.getOperatingSystem().open(path) } }
if (group.path in collapsedPaths) expanded = false
toggled { if (it) collapsedPaths += group.path else collapsedPaths -= group.path }
} ?: error("Missing group container")

val buttons = group.entries.map {
this@SoundBrowser.button(it.title()) { _ ->
this@SoundBrowser.button(it.title) { _ ->
settings?.update()
if (ctrlDown) settings(it)
else play(it, shiftDown)
}
}

container.titleLayout().children.filterIsInstance<LabelComponent>().firstOrNull()?.apply {
text = group.title()
text = group.title
if (path != null) tooltipText = DIRECTORY_TOOLTIP.translation()
} ?: error("Missing group header")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ class DurationCutterComponent(
val color = if (hovered) 0xFFFFFF else 0x808080
val u = if (hovered) 8F else 0F
context.drawLinePrecise(x + 1.5, y + 7.0, x + 1.5, y + height - 7.0, thickness, Color.ofRgb(color))
context.drawTexture(POINTER_TEXTURE, x - 2, y, 8, 8, u, 0F, 8, 8, 16, 16)
context.drawTexture(POINTER_TEXTURE, x - 2, y + height - 8, 8, 8, u, 8F, 8, 8, 16, 16)
context.drawTexture(POINTER_TEXTURE, x - 2, y, 8, 8, u, 0F, 8, 8, TEXTURE_SIZE, TEXTURE_SIZE)
context.drawTexture(POINTER_TEXTURE, x - 2, y + height - 8, 8, 8, u, 8F, 8, 8, TEXTURE_SIZE, TEXTURE_SIZE)
}

fun move(delta: Double) = moveTo(container.width * pos + delta)
Expand All @@ -122,5 +122,6 @@ class DurationCutterComponent(

companion object {
val POINTER_TEXTURE = idOf("textures/gui/pointer.png")
const val TEXTURE_SIZE = 16
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.kikugie.soundboard.gui.component

import dev.kikugie.kowoui.text
import io.wispforest.owo.ui.component.ButtonComponent
import net.minecraft.text.Text

abstract class DynamicButtonComponent : ButtonComponent(Text.empty(), {}) {
private var _text: Text = Text.empty()
private var _string: String = ""
abstract val string: String

override fun getMessage(): Text {
if (string != _string) {
_string = string
_text = string.text()
}
return _text
}
}
Loading

0 comments on commit 003c311

Please sign in to comment.