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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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.client.sound.RecordPlayingSound;
import com.hemogoblins.betterrecords.api.client.sound.RecordAudioStream;
import com.hemogoblins.betterrecords.client.sound.ProgressInputStream;
import com.hemogoblins.betterrecords.client.sound.RecordSoundManager;
Expand Down Expand Up @@ -39,15 +39,15 @@ public abstract class MixinSoundEngine {
@Shadow @Final private ChannelAccess channelAccess;

@Unique
private final Map<FileSoundInstance, ChannelAccess.ChannelHandle> betterRecords$soundHandles = new HashMap<>();
private final Map<RecordPlayingSound, ChannelAccess.ChannelHandle> betterRecords$soundHandles = new HashMap<>();

/**
* Sounds whose audio is still being decoded off-thread. A stop() that
* arrives while a sound is in here cancels it before it ever plays.
* Only ever touched on the main thread.
*/
@Unique
private final Set<FileSoundInstance> betterRecords$loading = new HashSet<>();
private final Set<RecordPlayingSound> betterRecords$loading = new HashSet<>();

// TODO: Ability to boost and reduce the distance of sounds based on their network.
// For now, four chunks is pretty good.
Expand Down Expand Up @@ -82,30 +82,30 @@ public abstract class MixinSoundEngine {

@Inject(method="play", at = @At("HEAD"), cancellable = true)
public void onPlay(SoundInstance sound, CallbackInfo ci) {
if (!(sound instanceof FileSoundInstance fileSound)) {
if (!(sound instanceof RecordPlayingSound recordSound)) {
return;
}

// We fully handle our own file-backed sounds, so skip vanilla's handling.
ci.cancel();

File soundFile = new File(BRConfig.Client.INSTANCE.getCacheDirectory().get(), fileSound.getFile());
File soundFile = new File(BRConfig.Client.INSTANCE.getCacheDirectory().get(), recordSound.getFile());

// Mark the sound as loading so a stop() arriving before it is ready can cancel it,
betterRecords$loading.add(fileSound);
fileSound.setLoading(true);
betterRecords$loading.add(recordSound);
recordSound.setLoading(true);

CompletableFuture<SoundBuffer> bufferFuture =
CompletableFuture.supplyAsync(() -> betterRecords$decode(fileSound, soundFile), Util.backgroundExecutor());
CompletableFuture.supplyAsync(() -> betterRecords$decode(recordSound, soundFile), Util.backgroundExecutor());
bufferFuture

.thenAcceptBothAsync(
this.channelAccess.createHandle(Library.Pool.STATIC),
(soundBuffer, channelHandle) -> betterRecords$startPlaying(fileSound, soundBuffer, channelHandle),
(soundBuffer, channelHandle) -> betterRecords$startPlaying(recordSound, soundBuffer, channelHandle),
Minecraft.getInstance())
.exceptionally(throwable -> {
betterRecords$loading.remove(fileSound);
fileSound.setLoading(false);
betterRecords$loading.remove(recordSound);
recordSound.setLoading(false);
BetterRecords.INSTANCE.getLogger().error("Failed to play record sound", throwable);
return null;
});
Expand All @@ -115,16 +115,16 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) {
* Reads and decodes a cached audio file into a static buffer.
*/
@Unique
private SoundBuffer betterRecords$decode(FileSoundInstance fileSound, File soundFile) {
private SoundBuffer betterRecords$decode(RecordPlayingSound recordSound, File soundFile) {
if (!soundFile.exists()) {
throw new RuntimeException("Sound file not found: " + soundFile.getAbsolutePath());
}

try (FileInputStream stream = new FileInputStream(soundFile);
ProgressInputStream progressStream = new ProgressInputStream(stream, soundFile.length(), fileSound);
ProgressInputStream progressStream = new ProgressInputStream(stream, soundFile.length(), recordSound);
RecordAudioStream audioStream = AudioStreamRegistry.INSTANCE.open(progressStream)) {
SoundBuffer soundBuffer = new SoundBuffer(audioStream.readAll(), audioStream.getFormat());
fileSound.setLoadProgress(1.0F);
recordSound.setLoadProgress(1.0F);
return soundBuffer;
} catch (Exception e) {
throw new RuntimeException(e);
Expand All @@ -133,10 +133,10 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) {

/** Attaches the decoded buffer to a channel and starts playback. Runs on the main thread. */
@Unique
private void betterRecords$startPlaying(FileSoundInstance fileSound, SoundBuffer soundBuffer, ChannelAccess.ChannelHandle channelHandle) {
private void betterRecords$startPlaying(RecordPlayingSound recordSound, SoundBuffer soundBuffer, ChannelAccess.ChannelHandle channelHandle) {
// If the sound was stopped while it was still loading, don't start it.
boolean wanted = betterRecords$loading.remove(fileSound);
fileSound.setLoading(false);
boolean wanted = betterRecords$loading.remove(recordSound);
recordSound.setLoading(false);

if (channelHandle == null) {
return;
Expand All @@ -148,26 +148,26 @@ public void onPlay(SoundInstance sound, CallbackInfo ci) {
}

// TODO: Should this just be a blockpos?
Vec3 soundPos = new Vec3(fileSound.getX(), fileSound.getY(), fileSound.getZ());
Vec3 soundPos = new Vec3(recordSound.getX(), recordSound.getY(), recordSound.getZ());

channelHandle.execute((channel) -> {
channel.setPitch(fileSound.getPitch());
channel.setPitch(recordSound.getPitch());
channel.setVolume(betterRecords$calculateVolumeByDistance(soundPos));
channel.attachStaticBuffer(soundBuffer);
channel.play();
});

fileSound.setStartedPlayingAt(Util.getMillis());
betterRecords$soundHandles.put(fileSound, channelHandle);
recordSound.setStartedPlayingAt(Util.getMillis());
betterRecords$soundHandles.put(recordSound, channelHandle);
}

@Inject(method="stop", at = @At("HEAD"), cancellable = true)
public void onStop(SoundInstance sound, CallbackInfo ci) {
if (sound instanceof FileSoundInstance fileSound) {
if (sound instanceof RecordPlayingSound recordSound) {
// Cancel playback if the sound is still being decoded
betterRecords$loading.remove(fileSound);
betterRecords$loading.remove(recordSound);

ChannelAccess.ChannelHandle channelHandle = betterRecords$soundHandles.remove(fileSound);
ChannelAccess.ChannelHandle channelHandle = betterRecords$soundHandles.remove(recordSound);

if (channelHandle != null) {
channelHandle.execute(Channel::stop);
Expand All @@ -182,12 +182,12 @@ public void onStop(SoundInstance sound, CallbackInfo ci) {
*/
@Inject(method="tick", at = @At("TAIL"))
public void onTick(CallbackInfo ci) {
Iterator<Map.Entry<FileSoundInstance, ChannelAccess.ChannelHandle>> iterator =
Iterator<Map.Entry<RecordPlayingSound, ChannelAccess.ChannelHandle>> iterator =
betterRecords$soundHandles.entrySet().iterator();

while (iterator.hasNext()) {
Map.Entry<FileSoundInstance, ChannelAccess.ChannelHandle> entry = iterator.next();
FileSoundInstance sound = entry.getKey();
Map.Entry<RecordPlayingSound, ChannelAccess.ChannelHandle> entry = iterator.next();
RecordPlayingSound sound = entry.getKey();
ChannelAccess.ChannelHandle handle = entry.getValue();

if (handle.isStopped()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.hemogoblins.betterrecords.api.client.sound

/**
* Display state for currently playing sounds
*/
interface PlayingSound {

/** Human-readable name */
val name: String

/** True while the audio is still being downloaded or decoded */
val loading: Boolean

/** Download or decode progress in the range 0..1. */
val loadProgress: Float

/** Timestamp ([net.minecraft.Util.getMillis]) at which playback started. */
val startedPlayingAt: Long
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class RecordPlayerBlock(properties: Properties) : BaseEntityBlock(properties) {
override fun onRemove(state: BlockState, level: Level, pos: BlockPos, newState: BlockState, isMoving: Boolean) {
if (state.block != newState.block) {
level.getBlockEntity(pos).takeIf { it is RecordPlayerBlockEntity }.let {
(it as RecordPlayerBlockEntity).dropContents()
(it as RecordPlayerBlockEntity).stopPlaying()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.hemogoblins.betterrecords.block.entity
import com.hemogoblins.betterrecords.block.ModBlocks
import com.hemogoblins.betterrecords.block.RecordPlayerBlock
import com.hemogoblins.betterrecords.block.RecordPlayerBlock.Companion.FACING
import com.hemogoblins.betterrecords.capability.ModCapabilities
import com.hemogoblins.betterrecords.network.ModNetwork
import com.hemogoblins.betterrecords.network.clientbound.PlayRecordPacket
import com.hemogoblins.betterrecords.network.clientbound.StopRecordPacket
Expand Down Expand Up @@ -96,19 +95,22 @@ class RecordPlayerBlockEntity(
}

private fun sendPlayRecord() {
val cap = getSlottedItem().getCapability(ModCapabilities.MUSIC_HOLDER_CAPABILITY)
val checksum = cap.map { it.checksum }.orElse("")
val url = cap.map { it.url }.orElse("")
val name = cap.map { it.songName }.orElse("")

if (checksum.isEmpty()) return
val source = PlayRecordPacket.RecordSource.of(getSlottedItem()) ?: return

ModNetwork.channel.send(
PacketDistributor.TRACKING_CHUNK.with { level!!.getChunkAt(blockPos) },
PlayRecordPacket(blockPos, checksum, url, name),
PlayRecordPacket(blockPos, source),
)
}

fun stopPlaying() {
if (level?.isClientSide == false && isPlaying) {
togglePlaying(false)
sendStopRecord()
dropContents()
}
}

private fun sendStopRecord() {
ModNetwork.channel.send(
PacketDistributor.TRACKING_CHUNK.with { level!!.getChunkAt(blockPos) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket
import net.minecraft.world.Containers
import net.minecraft.world.SimpleContainer
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.RecordItem
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityType
Expand All @@ -31,10 +32,8 @@ open class SingleSlotBlockEntity<T : BlockEntity?>(
companion object {
const val NBT_TAG_INVENTORY = "inventory"

/** might want to change this to check capabilities by generic
* or just move out of static and override in subs */
fun isItemValid(itemStack: ItemStack): Boolean {
return itemStack.getCapability(ModCapabilities.MUSIC_HOLDER_CAPABILITY).isPresent
fun isItemValid(stack: ItemStack): Boolean {
return stack.getCapability(ModCapabilities.MUSIC_HOLDER_CAPABILITY).isPresent || stack.item is RecordItem
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemDisplayContext
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.item.RecordItem as VanillaRecordItem

class RecordPlayerRenderer(context: BlockEntityRendererProvider.Context) : BlockEntityRenderer<RecordPlayerBlockEntity> {

Expand Down Expand Up @@ -153,11 +154,38 @@ class RecordPlayerRenderer(context: BlockEntityRendererProvider.Context) : Block
mulPose(Axis.ZN.rotationDegrees(rotation))
}

itemRenderer.renderStatic(itemStack, ItemDisplayContext.FIXED, packedLight,
packedOverlay, poseStack, bufferSource, level, 1)
if (itemStack.item is VanillaRecordItem) {
renderVanillaDisc(itemStack, poseStack, bufferSource, packedLight, packedOverlay, level)
} else {
renderItem(itemStack, poseStack, bufferSource, packedLight, packedOverlay, level)
}

poseStack.popPose()
}

private fun renderVanillaDisc(
itemStack: ItemStack,
poseStack: PoseStack,
bufferSource: MultiBufferSource,
packedLight: Int,
packedOverlay: Int,
level: Level?
) {
val proxy = VanillaDiscProxies.proxyFor(itemStack, itemRenderer, level)
renderItem(proxy, poseStack, bufferSource, packedLight, packedOverlay, level)
}

private fun renderItem(
itemStack: ItemStack,
poseStack: PoseStack,
bufferSource: MultiBufferSource,
packedLight: Int,
packedOverlay: Int,
level: Level?
) {
itemRenderer.renderStatic(itemStack, ItemDisplayContext.FIXED, packedLight,
packedOverlay, poseStack, bufferSource, level, 1)
}
}

/** For adjusting values in 16ths (1 = 1 pixel). */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.hemogoblins.betterrecords.block.renderer

import com.hemogoblins.betterrecords.capability.ColorableCapability
import com.hemogoblins.betterrecords.capability.ModCapabilities
import com.hemogoblins.betterrecords.item.ModItems
import net.minecraft.client.renderer.entity.ItemRenderer
import net.minecraft.world.item.Item
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level

/**
* Creates proxy record itemstacks for rendering purposes out of vanilla discs.
* Samples the color of the disc, and stores it for future plays.
*/
object VanillaDiscProxies {

private const val DEFAULT_COLOR = 0xFFFFFF

private val proxies = mutableMapOf<Item, ItemStack>()

fun proxyFor(stack: ItemStack, itemRenderer: ItemRenderer, level: Level?): ItemStack {
return proxies.getOrPut(stack.item) {
val color = sampleDiscColor(stack, itemRenderer, level)

ItemStack(ModItems.RECORD.get()).apply {
getCapability(ModCapabilities.COLORABLE_CAPABILITY).ifPresent {
(it as ColorableCapability).color = color
}
}
}
}

private fun sampleDiscColor(stack: ItemStack, itemRenderer: ItemRenderer, level: Level?): Int {
return try {
val sprite = itemRenderer.getModel(stack, level, null, 0).particleIcon
val contents = sprite.contents()
val image = contents.originalImage

var rAcc = 0.0
var gAcc = 0.0
var bAcc = 0.0
var weight = 0.0

for (y in 0 until contents.height()) {
for (x in 0 until contents.width()) {
val argb = image.getPixelRGBA(x, y) // NativeImage is little-endian 0xAABBGGRR
val a = (argb ushr 24) and 0xFF
if (a < 128) continue

val r = argb and 0xFF
val g = (argb ushr 8) and 0xFF
val b = (argb ushr 16) and 0xFF

val max = maxOf(r, g, b)
if (max == 0) continue
val min = minOf(r, g, b)

val saturation = (max - min).toDouble() / max
val w = saturation * saturation * max
rAcc += r * w
gAcc += g * w
bAcc += b * w
weight += w
}
}

if (weight <= 0.0) return DEFAULT_COLOR

val r = (rAcc / weight).toInt().coerceIn(0, 255)
val g = (gAcc / weight).toInt().coerceIn(0, 255)
val b = (bAcc / weight).toInt().coerceIn(0, 255)
(r shl 16) or (g shl 8) or b
} catch (e: Exception) {
DEFAULT_COLOR
}
}
}
Loading
Loading