summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt2
-rw-r--r--src/client/kotlin/space/autistic/radio/client/cli/OfflineSimulator.kt4
-rw-r--r--src/client/kotlin/space/autistic/radio/client/fmsim/FastModulatedNoise.kt86
-rw-r--r--src/client/kotlin/space/autistic/radio/client/fmsim/FmFullMixer.kt4
-rw-r--r--src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt185
-rw-r--r--src/client/kotlin/space/autistic/radio/client/fmsim/NoiseFloor.kt31
-rw-r--r--src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt53
-rw-r--r--src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt41
-rw-r--r--src/client/resources/pirate-radio.client-mixins.json (renamed from src/main/resources/pirate-radio.client-mixins.json)0
9 files changed, 378 insertions, 28 deletions
diff --git a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt
index 1a68c21..b3c9f17 100644
--- a/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt
+++ b/src/client/kotlin/space/autistic/radio/client/PirateRadioClient.kt
@@ -16,6 +16,7 @@ import space.autistic.radio.PirateRadio.MOD_ID
 import space.autistic.radio.PirateRadioEntityTypes
 import space.autistic.radio.client.entity.ElectronicsTraderEntityRenderer
 import space.autistic.radio.client.entity.DisposableTransmitterEntityRenderer
+import space.autistic.radio.client.fmsim.FmFullThread
 import space.autistic.radio.client.fmsim.FmSimulatorMode
 import space.autistic.radio.client.gui.FmReceiverScreen
 import space.autistic.radio.client.sound.PirateRadioSoundInstance
@@ -36,6 +37,7 @@ object PirateRadioClient : ClientModInitializer {
     var mode = FmSimulatorMode.FULL
 
     override fun onInitializeClient() {
+        Thread.ofPlatform().daemon().name("fm-receiver").start(FmFullThread)
         PirateRadio.proxy = ClientProxy()
         EntityRendererRegistry.register(PirateRadioEntityTypes.ELECTRONICS_TRADER, ::ElectronicsTraderEntityRenderer)
         EntityRendererRegistry.register(
diff --git a/src/client/kotlin/space/autistic/radio/client/cli/OfflineSimulator.kt b/src/client/kotlin/space/autistic/radio/client/cli/OfflineSimulator.kt
index 2646ed2..c17d622 100644
--- a/src/client/kotlin/space/autistic/radio/client/cli/OfflineSimulator.kt
+++ b/src/client/kotlin/space/autistic/radio/client/cli/OfflineSimulator.kt
@@ -231,7 +231,7 @@ fun main(args: Array<String>) {
                 w.y = plus100k.get()
                 z.cmul(w)
                 floatView.put(i, floatView.get(i) + z.x)
-                floatView.put(i, floatView.get(i) + z.y)
+                floatView.put(i + 1, floatView.get(i + 1) + z.y)
             }
             for (i in 0 until floatBufferLo.capacity() step 2) {
                 z.x = floatBufferLo.get(i)
@@ -243,7 +243,7 @@ fun main(args: Array<String>) {
                 w.y = -minus100k.get()
                 z.cmul(w)
                 floatView.put(i, floatView.get(i) + z.x)
-                floatView.put(i, floatView.get(i) + z.y)
+                floatView.put(i + 1, floatView.get(i + 1) + z.y)
             }
             if (rfOutput) {
                 outputStream.write(outputBuffer.array())
diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/FastModulatedNoise.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FastModulatedNoise.kt
new file mode 100644
index 0000000..9639a38
--- /dev/null
+++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FastModulatedNoise.kt
@@ -0,0 +1,86 @@
+package space.autistic.radio.client.fmsim
+
+import org.joml.Vector2f
+import space.autistic.radio.client.complex.cmul
+import java.nio.FloatBuffer
+import java.util.concurrent.ThreadLocalRandom
+import java.util.function.Consumer
+
+// FIXME use more realistic model
+class FastModulatedNoise(which: Which) {
+
+    private val buffer = when (which) {
+        Which.BASE -> FloatBuffer.wrap(baseNoise)
+        Which.UPPER -> FloatBuffer.wrap(upperNoise)
+        Which.LOWER -> FloatBuffer.wrap(upperNoise)
+    }
+    private val flipSpectrum = which == Which.LOWER
+    private val outBuffer = FloatBuffer.allocate(2 * FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K)
+
+    // complex noise, in IQ format
+    fun generateNoise(power: Float, consumer: Consumer<FloatBuffer>) {
+        outBuffer.clear()
+        while (outBuffer.hasRemaining()) {
+            if (!buffer.hasRemaining()) {
+                buffer.clear()
+            }
+            if (flipSpectrum) {
+                outBuffer.put(buffer.get() * power)
+                outBuffer.put(-buffer.get() * power)
+            } else {
+                outBuffer.put(buffer.get() * power)
+                outBuffer.put(buffer.get() * power)
+            }
+        }
+        outBuffer.clear()
+        consumer.accept(outBuffer)
+    }
+
+    enum class Which {
+        LOWER, BASE, UPPER
+    }
+
+    companion object {
+        // 1 second
+        private val baseNoise = FloatArray(300000 * 2)
+        private val upperNoise = FloatArray(300000 * 2)
+
+        init {
+            val fmsim = FmFullModulator()
+            val buffer = FloatBuffer.wrap(baseNoise)
+            val input = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2)
+            val random = ThreadLocalRandom.current()
+            while (buffer.hasRemaining()) {
+                input.clear()
+                while (input.hasRemaining()) {
+                    val sample = random.nextFloat(1f)
+                    input.put(sample)
+                    input.put(sample)
+                }
+                input.clear()
+                fmsim.process(input, 1f, false) {
+                    if (buffer.remaining() < it.remaining()) {
+                        it.limit(it.position() + buffer.remaining())
+                    }
+                    buffer.put(it)
+                }
+            }
+            buffer.clear()
+            val plus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K)
+            val z = Vector2f()
+            val w = Vector2f()
+            for (i in baseNoise.indices step 2) {
+                z.x = baseNoise[i]
+                z.y = baseNoise[i + 1]
+                if (!plus100k.hasRemaining()) {
+                    plus100k.clear()
+                }
+                w.x = plus100k.get()
+                w.y = plus100k.get()
+                z.cmul(w)
+                upperNoise[i] = z.x
+                upperNoise[i] = z.y
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullMixer.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullMixer.kt
deleted file mode 100644
index 567d93f..0000000
--- a/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullMixer.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package space.autistic.radio.client.fmsim
-
-class FmFullMixer {
-}
\ No newline at end of file
diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt
new file mode 100644
index 0000000..0a6184f
--- /dev/null
+++ b/src/client/kotlin/space/autistic/radio/client/fmsim/FmFullThread.kt
@@ -0,0 +1,185 @@
+package space.autistic.radio.client.fmsim
+
+import org.joml.Vector2f
+import space.autistic.radio.client.PirateRadioClient
+import space.autistic.radio.client.complex.cmul
+import space.autistic.radio.client.sound.PirateRadioSoundInstance
+import space.autistic.radio.client.sound.ReceiverAudioStream
+import space.autistic.radio.entity.DisposableTransmitterEntity
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+import java.util.concurrent.ArrayBlockingQueue
+import kotlin.math.max
+import kotlin.math.min
+
+object FmFullThread : Runnable {
+    class FmTask(
+        val trackedTransmitters: Map<DisposableTransmitterEntity, PirateRadioSoundInstance.TrackedTransmitter>,
+        val noiseLevels: FloatArray,
+    )
+
+    // empty task, marker to shut off the thread
+    val EMPTY_TASK = FmTask(emptyMap(), FloatArray(3))
+
+    val trackedTransmitterQueue = ArrayBlockingQueue<FmTask>(8)
+
+    private class TtsModulator(
+        val buffer: FloatBuffer,
+        val modulator: FmFullModulator,
+        var power: Float,
+        var repeatTimeout: Int,
+        var mixingBuffer: FloatBuffer
+    )
+
+    // 3 seconds
+    private const val REPEAT_TIMEOUT = 8000 * 3
+
+    override fun run() {
+        var currentTask = EMPTY_TASK
+        val modulators = HashMap<DisposableTransmitterEntity, TtsModulator>()
+        val mixingBuffers = Array(3) { FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K * 2) }
+
+        val inputBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2)
+
+        val noiseGens = Array(3) { FastModulatedNoise(FastModulatedNoise.Which.entries[it]) }
+
+        // -120dB or so
+        val noiseFloor = NoiseFloor(1e-12f)
+
+        val plus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K)
+        val minus100k = FloatBuffer.wrap(FmFullConstants.CBUFFER_100K_300K)
+
+        val demodulator = FmFullDemodulator()
+
+        while (!Thread.interrupted()) {
+            currentTask = trackedTransmitterQueue.poll() ?: currentTask
+            if (currentTask === EMPTY_TASK) {
+                Thread.onSpinWait()
+                continue
+            }
+            modulators.keys.retainAll(currentTask.trackedTransmitters.keys)
+            currentTask.trackedTransmitters.forEach { (k, v) ->
+                modulators.compute(k) { _, modulator ->
+                    if (modulator != null) {
+                        modulator.power = v.power
+                        modulator.mixingBuffer = mixingBuffers[v.frequencyOffset + 1]
+                        return@compute modulator
+                    }
+                    val audioData = v.audio.getNow(null)
+                    if (audioData != null) {
+                        val buf = FloatBuffer.wrap(audioData)
+                        var actualSampleOffset = Math.floorMod(v.sampleOffset, (buf.capacity() + REPEAT_TIMEOUT))
+                        var repeatTimeout = max(0, actualSampleOffset - buf.capacity())
+                        if (repeatTimeout > 0) {
+                            actualSampleOffset = 0
+                            repeatTimeout = REPEAT_TIMEOUT - repeatTimeout
+                        }
+                        if (actualSampleOffset == buf.capacity()) {
+                            actualSampleOffset = 0
+                            repeatTimeout = REPEAT_TIMEOUT
+                        }
+                        buf.position(actualSampleOffset)
+                        TtsModulator(
+                            buf, FmFullModulator(), v.power, repeatTimeout, mixingBuffers[v.frequencyOffset + 1]
+                        )
+                    } else {
+                        null
+                    }
+                }
+            }
+
+            mixingBuffers.forEach {
+                it.clear()
+                while (it.hasRemaining()) it.put(0f)
+                it.clear()
+            }
+
+            modulators.values.forEach {
+                inputBuffer.clear()
+                while (inputBuffer.hasRemaining()) {
+                    val sample = if (it.repeatTimeout > 0) {
+                        it.repeatTimeout--
+                        0f
+                    } else {
+                        it.buffer.get()
+                    }
+                    if (!it.buffer.hasRemaining()) {
+                        it.repeatTimeout = REPEAT_TIMEOUT
+                        it.buffer.clear()
+                    }
+                    for (i in 0 until 2 * 6) inputBuffer.put(sample)
+                }
+                inputBuffer.clear()
+                val mixingBuffer = it.mixingBuffer
+                it.modulator.process(inputBuffer, it.power, false) { outputBuffer ->
+                    for (i in 0 until mixingBuffer.capacity()) {
+                        mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get())
+                    }
+                }
+            }
+
+            if (modulators.any { it.value.mixingBuffer === mixingBuffers[2] }) {
+                val floatBufferHi = mixingBuffers[2]
+                val floatView = mixingBuffers[1]
+                val z = Vector2f()
+                val w = Vector2f()
+                for (i in 0 until floatBufferHi.capacity() step 2) {
+                    z.x = floatBufferHi.get(i)
+                    z.y = floatBufferHi.get(i + 1)
+                    if (!plus100k.hasRemaining()) {
+                        plus100k.clear()
+                    }
+                    w.x = plus100k.get()
+                    w.y = plus100k.get()
+                    z.cmul(w)
+                    floatView.put(i, floatView.get(i) + z.x)
+                    floatView.put(i + 1, floatView.get(i + 1) + z.y)
+                }
+            }
+
+            if (modulators.any { it.value.mixingBuffer === mixingBuffers[0] }) {
+                val floatBufferLo = mixingBuffers[0]
+                val floatView = mixingBuffers[1]
+                val z = Vector2f()
+                val w = Vector2f()
+                for (i in 0 until floatBufferLo.capacity() step 2) {
+                    z.x = floatBufferLo.get(i)
+                    z.y = floatBufferLo.get(i + 1)
+                    if (!minus100k.hasRemaining()) {
+                        minus100k.clear()
+                    }
+                    w.x = minus100k.get()
+                    w.y = -minus100k.get()
+                    z.cmul(w)
+                    floatView.put(i, floatView.get(i) + z.x)
+                    floatView.put(i + 1, floatView.get(i + 1) + z.y)
+                }
+            }
+
+            noiseGens.forEachIndexed { index, v ->
+                if (currentTask.noiseLevels[index] != 0f) {
+                    v.generateNoise(currentTask.noiseLevels[index]) { outputBuffer ->
+                        val mixingBuffer = mixingBuffers[1]
+                        for (i in 0 until mixingBuffer.capacity()) {
+                            mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get())
+                        }
+                    }
+                }
+            }
+
+            noiseFloor.noiseBlock { outputBuffer ->
+                val mixingBuffer = mixingBuffers[1]
+                for (i in 0 until mixingBuffer.capacity()) {
+                    mixingBuffer.put(i, mixingBuffer.get(i) + outputBuffer.get())
+                }
+            }
+
+            demodulator.process(mixingBuffers[1], PirateRadioClient.stereo) { _, audioBuffer ->
+                // TODO stereo pilot
+                // we *want* backpressure
+                // FIXME use bigger buffers?
+                ReceiverAudioStream.bufferQueue.put(FloatBuffer.allocate(audioBuffer.capacity()).put(audioBuffer).clear())
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/client/kotlin/space/autistic/radio/client/fmsim/NoiseFloor.kt b/src/client/kotlin/space/autistic/radio/client/fmsim/NoiseFloor.kt
new file mode 100644
index 0000000..92df212
--- /dev/null
+++ b/src/client/kotlin/space/autistic/radio/client/fmsim/NoiseFloor.kt
@@ -0,0 +1,31 @@
+package space.autistic.radio.client.fmsim
+
+import java.nio.FloatBuffer
+import java.util.concurrent.ThreadLocalRandom
+import java.util.function.Consumer
+
+class NoiseFloor(level: Float) {
+    private val buffer = FloatBuffer.allocate(300000 * 2)
+    private val outputBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_48K_300K * 2)
+
+    init {
+        // FIXME is this how you generate IQ noise?
+        val random = ThreadLocalRandom.current()
+        val dLevel = level.toDouble()
+        while (buffer.hasRemaining()) {
+            buffer.put(random.nextGaussian(0.0, dLevel).toFloat())
+            buffer.put(0f)
+        }
+    }
+
+    // complex noise, in IQ format?
+    fun noiseBlock(consumer: Consumer<FloatBuffer>) {
+        outputBuffer.clear()
+        while (outputBuffer.hasRemaining()) {
+            if (!buffer.hasRemaining()) buffer.clear()
+            outputBuffer.put(buffer.get())
+        }
+        outputBuffer.clear()
+        consumer.accept(outputBuffer)
+    }
+}
\ No newline at end of file
diff --git a/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt
index 4a4f087..5fb80bc 100644
--- a/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt
+++ b/src/client/kotlin/space/autistic/radio/client/sound/PirateRadioSoundInstance.kt
@@ -1,7 +1,6 @@
 package space.autistic.radio.client.sound
 
 import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance
-import net.minecraft.client.MinecraftClient
 import net.minecraft.client.network.ClientPlayerEntity
 import net.minecraft.client.sound.*
 import net.minecraft.sound.SoundCategory
@@ -9,14 +8,28 @@ import net.minecraft.sound.SoundEvents
 import net.minecraft.util.Identifier
 import space.autistic.radio.PirateRadioEntityTypes
 import space.autistic.radio.client.PirateRadioClient
+import space.autistic.radio.client.flite.FliteWrapper
+import space.autistic.radio.client.fmsim.FmFullThread
+import space.autistic.radio.client.fmsim.FmFullThread.trackedTransmitterQueue
 import space.autistic.radio.client.fmsim.FmSimulatorMode
 import space.autistic.radio.entity.DisposableTransmitterEntity
+import java.lang.ref.WeakReference
+import java.nio.FloatBuffer
 import java.util.concurrent.CompletableFuture
 import kotlin.math.PI
 
 class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingSoundInstance(
     SoundEvents.INTENTIONALLY_EMPTY, SoundCategory.MUSIC, SoundInstance.createRandom()
 ) {
+    private val futuresCache = HashMap<String, WeakReference<CompletableFuture<FloatArray>>>()
+
+    class TrackedTransmitter(
+        val power: Float, val sampleOffset: Int, val audio: CompletableFuture<FloatArray>, val frequencyOffset: Int
+    ) {
+        override fun toString(): String {
+            return "TrackedTransmitter(power=$power, sampleOffset=$sampleOffset, audio=$audio, frequencyOffset=$frequencyOffset)"
+        }
+    }
 
     init {
         this.repeat = false
@@ -31,10 +44,11 @@ class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingS
             this.setDone()
             return
         }
+        // find relevant entities
         @Suppress("UNCHECKED_CAST") val trackedEntities: List<DisposableTransmitterEntity> =
             player.clientWorld.entities.filter { it.type == PirateRadioEntityTypes.DISPOSABLE_TRANSMITTER }
                 .filter { (it as DisposableTransmitterEntity).frequency <= PirateRadioClient.frequency + 1 && it.frequency >= PirateRadioClient.frequency - 1 }
-                .sortedByDescending { player.pos.squaredDistanceTo(it.pos) } as List<DisposableTransmitterEntity>
+                .sortedBy { player.pos.squaredDistanceTo(it.pos) } as List<DisposableTransmitterEntity>
         val main = trackedEntities.filter { it.frequency == PirateRadioClient.frequency }
             .take(if (PirateRadioClient.mode == FmSimulatorMode.FAST) 1 else 2)
         val lower = trackedEntities.filter { it.frequency == PirateRadioClient.frequency - 1 }
@@ -53,10 +67,37 @@ class PirateRadioSoundInstance(private val player: ClientPlayerEntity) : MovingS
             .fold(0f) { noise, entity ->
                 noise + getPowerReceived(entity.pos.squaredDistanceTo(player.pos))
             }
-        val mainLevels = main.map { getPowerReceived(it.pos.squaredDistanceTo(player.pos)) }
-        val lowerLevels = lower.map { getPowerReceived(it.pos.squaredDistanceTo(player.pos)) }
-        val upperLevels = upper.map { getPowerReceived(it.pos.squaredDistanceTo(player.pos)) }
-        // TODO
+        // updated tracked transmitters
+        val trackedTransmitters: MutableMap<DisposableTransmitterEntity, TrackedTransmitter> = HashMap()
+        listOf(lower, main, upper).flatten().associateWithTo(trackedTransmitters) {
+            val text = it.text
+            val audio = futuresCache[text]?.get() ?: CompletableFuture.supplyAsync {
+                lateinit var buffer: FloatBuffer
+                FliteWrapper.textToWave(text) {
+                    buffer = FloatBuffer.allocate(it.capacity())
+                    while (it.hasRemaining()) {
+                        val sample = (it.get().toFloat() + 0.5f) / 32767.5f
+                        buffer.put(sample)
+                    }
+                }
+                buffer.array()
+            }
+            futuresCache[text] = WeakReference(audio)
+            TrackedTransmitter(
+                getPowerReceived(it.pos.squaredDistanceTo(player.pos)),
+                it.age * (8000 / 20),
+                audio,
+                it.frequency - PirateRadioClient.frequency
+            )
+        }
+        // this can be empty but it is not EMPTY_TASK
+        trackedTransmitterQueue.offer(
+            FmFullThread.FmTask(
+                trackedTransmitters, floatArrayOf(lowerNoise, mainNoise, upperNoise)
+            )
+        )
+        volume = PirateRadioClient.volume.toFloat() / 10
+        volume *= volume
     }
 
     private fun getPowerReceived(rsq: Double): Float {
diff --git a/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt
index 5ca802b..274e727 100644
--- a/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt
+++ b/src/client/kotlin/space/autistic/radio/client/sound/ReceiverAudioStream.kt
@@ -1,34 +1,43 @@
 package space.autistic.radio.client.sound
 
 import it.unimi.dsi.fastutil.floats.FloatConsumer
-import net.minecraft.client.sound.AudioStream
-import net.minecraft.client.sound.ChannelList
-import java.nio.ByteBuffer
+import net.minecraft.client.sound.BufferedAudioStream
+import space.autistic.radio.client.fmsim.FmFullConstants
+import space.autistic.radio.client.fmsim.FmFullThread
+import space.autistic.radio.client.fmsim.FmFullThread.trackedTransmitterQueue
+import java.nio.FloatBuffer
+import java.util.Properties
+import java.util.concurrent.ArrayBlockingQueue
 import javax.sound.sampled.AudioFormat
+import kotlin.math.max
 
-object ReceiverAudioStream : AudioStream {
+object ReceiverAudioStream : BufferedAudioStream {
     private val format = AudioFormat(48000f, 16, 2, true, false)
 
+    val bufferQueue = ArrayBlockingQueue<FloatBuffer>(
+        max(
+            0,
+            System.getProperty("space.autistic.radio.buffers", "").toIntOrNull() ?: 250
+        )
+    )
+
+    private val skipBuffer = FloatBuffer.allocate(FmFullConstants.FFT_DATA_BLOCK_SIZE_LPF_48K_15K_3K1 * 2)
+        get() = field.clear()
+
     override fun close() {
-        // TODO, nop for now, should stop the processing
+        trackedTransmitterQueue.clear()
+        trackedTransmitterQueue.offer(FmFullThread.EMPTY_TASK)
     }
 
     override fun getFormat(): AudioFormat {
         return format
     }
 
-    override fun read(size: Int): ByteBuffer {
-        val channelList = ChannelList(size + 8192)
-
-        while (this.read(channelList) && channelList.currentBufferSize < size) {
+    override fun read(channelList: FloatConsumer): Boolean {
+        val buffer = bufferQueue.poll() ?: skipBuffer
+        while (buffer.hasRemaining()) {
+            channelList.accept(buffer.get())
         }
-
-        return channelList.buffer
-    }
-
-    private fun read(channelList: FloatConsumer): Boolean {
-        channelList.accept(0f)
-        channelList.accept(0f)
         return true
     }
 }
\ No newline at end of file
diff --git a/src/main/resources/pirate-radio.client-mixins.json b/src/client/resources/pirate-radio.client-mixins.json
index 27a5861..27a5861 100644
--- a/src/main/resources/pirate-radio.client-mixins.json
+++ b/src/client/resources/pirate-radio.client-mixins.json