Skip to content
Merged
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
40 changes: 37 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
id 'org.jetbrains.kotlin.jvm' version "${kotlin_version}"
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlin_version}"
id 'org.jetbrains.dokka' version "1.9.10"
id 'com.github.johnrengelman.shadow' version '8.1.1'
}

java.toolchain.languageVersion = JavaLanguageVersion.of(17)
Expand All @@ -22,24 +23,41 @@ version = "${minecraft_version}-${mod_version}"
//////////////////

repositories {
mavenCentral()
maven {
name 'Kotlin for Forge'
url 'https://thedarkcolour.github.io/KotlinForForge/'
content { includeGroup "thedarkcolour" }
}
}

configurations {
shade
implementation.extendsFrom shade
}

dependencies {
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"

annotationProcessor 'org.spongepowered:mixin:0.8.5:processor'

implementation "thedarkcolour:kotlinforforge:${kotlinforforge_version}"

shade "com.googlecode.soundlibs:jlayer:1.0.1.4"
}

minecraft {
mappings channel: mappings_channel, version: mappings_version
runs {
configureEach {
lazyToken('minecraft_classpath') {
configurations.shade
.copyRecursive()
.resolve()
.collect { it.absolutePath }
.join(File.pathSeparator)
}
}
client {
property 'forge.logging.markers', 'REGISTRIES'
property 'forge.logging.console.level', 'debug'
Expand Down Expand Up @@ -89,21 +107,37 @@ processResources {
}
}

shadowJar {
archiveClassifier = ''
configurations = [project.configurations.shade]

relocate 'javazoom', 'com.hemogoblins.betterrecords.shadow.javazoom'

finalizedBy 'reobfShadowJar'
}

reobf {
shadowJar {}
}

tasks.named('jar') {
archiveClassifier = 'slim'
}

assemble.dependsOn shadowJar

task deobfJar(type: Jar) {
from sourceSets.main.output
//noinspection GroovyAccessibility
archiveClassifier = 'deobf'
}

task sourcesJar(type: Jar) {
from sourceSets.main.allJava
//noinspection GroovyAccessibility
archiveClassifier = 'sources'
}

task javadocJar(type: Jar, dependsOn: javadoc) {
from javadoc.destinationDir
//noinspection GroovyAccessibility
archiveClassifier = 'javadoc'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import com.hemogoblins.betterrecords.BRConfig;
import com.hemogoblins.betterrecords.BetterRecords;
import com.hemogoblins.betterrecords.api.client.sound.AudioStreamRegistry;
import com.hemogoblins.betterrecords.api.client.sound.FileSoundInstance;
import com.hemogoblins.betterrecords.api.client.sound.RecordAudioStream;
import com.hemogoblins.betterrecords.client.sound.ProgressInputStream;
import com.hemogoblins.betterrecords.client.sound.RecordSoundManager;
import com.mojang.blaze3d.audio.Channel;
import com.mojang.blaze3d.audio.Library;
import com.mojang.blaze3d.audio.OggAudioStream;
import com.mojang.blaze3d.audio.SoundBuffer;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
Expand Down Expand Up @@ -110,7 +111,9 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) {
});
}

/** Reads and decodes an OGG file into a static buffer. Runs off the main thread. */
/**
* Reads and decodes a cached audio file into a static buffer.
*/
@Unique
private SoundBuffer betterRecords$decode(FileSoundInstance fileSound, File soundFile) {
if (!soundFile.exists()) {
Expand All @@ -119,8 +122,8 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) {

try (FileInputStream stream = new FileInputStream(soundFile);
ProgressInputStream progressStream = new ProgressInputStream(stream, soundFile.length(), fileSound);
OggAudioStream oggStream = new OggAudioStream(progressStream)) {
SoundBuffer soundBuffer = new SoundBuffer(oggStream.readAll(), oggStream.getFormat());
RecordAudioStream audioStream = AudioStreamRegistry.INSTANCE.open(progressStream)) {
SoundBuffer soundBuffer = new SoundBuffer(audioStream.readAll(), audioStream.getFormat());
fileSound.setLoadProgress(1.0F);
return soundBuffer;
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.hemogoblins.betterrecords

import com.hemogoblins.betterrecords.api.client.MusicCache
import com.hemogoblins.betterrecords.api.client.sound.AudioStreamRegistry
import com.hemogoblins.betterrecords.block.ModBlocks
import com.hemogoblins.betterrecords.block.renderer.ModRenderers
import com.hemogoblins.betterrecords.capability.ModCapabilities
import com.hemogoblins.betterrecords.client.cache.FilesystemCache
import com.hemogoblins.betterrecords.client.gui.ModOverlays
import com.hemogoblins.betterrecords.client.sound.format.Mp3AudioStreamProvider
import com.hemogoblins.betterrecords.client.sound.format.OggAudioStreamProvider
import com.hemogoblins.betterrecords.client.screen.ModScreens
import com.hemogoblins.betterrecords.item.ModItems
import com.hemogoblins.betterrecords.menu.ModMenuTypes
Expand Down Expand Up @@ -68,6 +71,8 @@ object BetterRecords {
* Client side setup events
*/
private fun onClientSetup(event: FMLClientSetupEvent) {
AudioStreamRegistry.register(OggAudioStreamProvider)
AudioStreamRegistry.register(Mp3AudioStreamProvider)
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.hemogoblins.betterrecords.api.client.sound

import java.io.IOException
import java.io.InputStream

interface AudioStreamProvider {

/** Short, lowercase identifier for the format, e.g. `"ogg"` or `"mp3"`. */
val id: String

/**
* Whether this provider can decode a stream that begins with [header].
*
* [header] holds the first few bytes of the stream (see [AudioStreamRegistry.HEADER_LENGTH]);
* Implementations should sniff magic bytes to determine format.
*/
fun matches(header: ByteArray): Boolean

/**
* Wraps [input] in a [RecordAudioStream] that decodes it to PCM.
*/
@Throws(IOException::class)
fun create(input: InputStream): RecordAudioStream
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.hemogoblins.betterrecords.api.client.sound

import com.hemogoblins.betterrecords.BetterRecords
import java.io.IOException
import java.io.InputStream
import java.io.PushbackInputStream
import java.util.concurrent.CopyOnWriteArrayList

/**
* Registry of [AudioStreamProvider]s, used to pick the right decoder for a cached
* audio file at playback time.
*
* Detection is done by sniffing the first [HEADER_LENGTH] bytes of the stream, so
* the format does not have to be known ahead of time
*/
object AudioStreamRegistry {

/** Number of leading bytes handed to [AudioStreamProvider.matches] for sniffing. */
const val HEADER_LENGTH = 4

private val providers = CopyOnWriteArrayList<AudioStreamProvider>()

/** Registers [provider]. Providers are checked in registration order. */
fun register(provider: AudioStreamProvider) {
providers.add(provider)
BetterRecords.logger.info("Registered audio stream provider: {}", provider.id)
}

/** All registered providers. */
fun providers(): List<AudioStreamProvider> = providers.toList()

/**
* Sniffs [input] and wraps it in a [RecordAudioStream] using the first provider that [AudioStreamProvider.matches] its header.
*
* @throws IOException if no registered provider supports the stream.
*/
@Throws(IOException::class)
fun open(input: InputStream): RecordAudioStream {
val pushback = PushbackInputStream(input, HEADER_LENGTH)

val header = ByteArray(HEADER_LENGTH)
var read = 0
while (read < HEADER_LENGTH) {
val r = pushback.read(header, read, HEADER_LENGTH - read)
if (r < 0) break
read += r
}
if (read > 0) pushback.unread(header, 0, read)

val sniffed = if (read == HEADER_LENGTH) header else header.copyOf(read)
val provider = providers.firstOrNull { it.matches(sniffed) }
?: throw IOException(
"No audio stream provider for header [${sniffed.joinToString(" ") { "%02X".format(it) }}]"
)

BetterRecords.logger.debug("Decoding record audio using '{}' provider", provider.id)
return provider.create(pushback)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.hemogoblins.betterrecords.api.client.sound

import java.io.Closeable
import java.io.IOException
import java.nio.ByteBuffer
import javax.sound.sampled.AudioFormat

/**
* A source of decoded, signed 16-bit PCM audio ready for OpenAL.
*/
interface RecordAudioStream : Closeable {

/**
* The format of the PCM returned by [readAll]. Channels must be mono or stereo
* and the sample size 8 or 16 bit, those are the only layouts OpenAL accepts.
*/
val format: AudioFormat

/**
* Fully decodes the stream into a single direct [ByteBuffer] of PCM
*/
@Throws(IOException::class)
fun readAll(): ByteBuffer
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class RecordEtcherScreen(
val etchButton = Button.builder(Component.translatable("menu.${BetterRecords.ID}.record_etcher.etch")) {
try {
// TODO URL From text field
val url = "https://betterrecords-files.s3.amazonaws.com/fastcar.ogg"
val url = "https://betterrecords-files.s3.amazonaws.com/fastcar.mp3"

val cacheEntry = BetterRecords.cache.get(url ) { progressPercent ->
// TODO: Progress Bar
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.hemogoblins.betterrecords.client.sound.format

import com.hemogoblins.betterrecords.api.client.sound.AudioStreamProvider
import com.hemogoblins.betterrecords.api.client.sound.RecordAudioStream
import javazoom.jl.decoder.Bitstream
import javazoom.jl.decoder.BitstreamException
import javazoom.jl.decoder.Decoder
import javazoom.jl.decoder.DecoderException
import javazoom.jl.decoder.Header
import javazoom.jl.decoder.SampleBuffer
import org.lwjgl.BufferUtils
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
import javax.sound.sampled.AudioFormat

/**
* MP3 support, backed by the JLayer decoder.
*
* JLayer decodes MP3 frames into interleaved signed 16-bit PCM, which is exactly what OpenAL wants.
*/
object Mp3AudioStreamProvider : AudioStreamProvider {

override val id = "mp3"

override fun matches(header: ByteArray): Boolean {
// ID3v2-tagged mp3 files start with the literal "ID3".
if (header.size >= 3 &&
header[0] == 'I'.code.toByte() &&
header[1] == 'D'.code.toByte() &&
header[2] == '3'.code.toByte()
) {
return true
}
// Otherwise the file starts straight with an MPEG audio frame: 11 sync bits set.
return header.size >= 2 &&
header[0] == 0xFF.toByte() &&
(header[1].toInt() and 0xE0) == 0xE0
}

override fun create(input: InputStream): RecordAudioStream = Mp3AudioStream(input)

private class Mp3AudioStream(input: InputStream) : RecordAudioStream {

private val bitstream = Bitstream(input)
private val decoder = Decoder()
private var decodedFormat: AudioFormat? = null

override val format: AudioFormat
get() = decodedFormat
?: throw IllegalStateException("readAll() must be called before the format is known")

@Throws(IOException::class)
override fun readAll(): ByteBuffer {
val chunks = ArrayList<ShortArray>()
var totalShorts = 0
var sampleRate = -1
var channels = -1

try {
var header: Header? = bitstream.readFrame()
while (header != null) {
val output = decoder.decodeFrame(header, bitstream) as SampleBuffer
if (sampleRate < 0) {
sampleRate = output.sampleFrequency
channels = output.channelCount
}

val length = output.bufferLength
chunks.add(output.buffer.copyOf(length))
totalShorts += length

bitstream.closeFrame()
header = bitstream.readFrame()
}
} catch (e: BitstreamException) {
throw IOException("Failed to read MP3 stream", e)
} catch (e: DecoderException) {
throw IOException("Failed to decode MP3 stream", e)
}

if (sampleRate < 0 || channels < 1) {
throw IOException("MP3 stream contained no decodable audio frames")
}

// Signed 16-bit, little-endian (native order) PCM — the layout OpenAL expects.
decodedFormat = AudioFormat(sampleRate.toFloat(), 16, channels, true, false)

val buffer = BufferUtils.createByteBuffer(totalShorts * 2)
for (chunk in chunks) {
for (sample in chunk) {
buffer.putShort(sample)
}
}
buffer.flip()
return buffer
}

override fun close() {
try {
bitstream.close()
} catch (e: BitstreamException) {
throw IOException("Failed to close MP3 stream", e)
}
}
}
}
Loading
Loading