Add Sound API to allow sound instances to play custom audio streams (#2558)

* Allow sound instances to play custom audio streams

Adds a new interface FabricSoundInstance, which is injected into
vanilla's SoundInstance interface.

When loading an audio stream, the SoundSystem now calls
FabricSoundInstance.getAudioStream, allowing mods to provide their
own audio streams.

* Some post-review cleanup

 - Manually add the client sources as an interface injection source set,
   allowing us to put everything in the src/client dir (<3 modmuss50).

 - Apply some formatting changes from apple502j.

* Document the empty sound and its usage in sounds.json

* Fix one remaining @literal -> @code

* Fix checkstyle issues
This commit is contained in:
Jonathan Coates 2022-10-16 15:09:44 +01:00 committed by GitHub
parent 704e47e9d7
commit c4f28df547
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 389 additions and 0 deletions

View file

@ -0,0 +1,14 @@
archivesBaseName = "fabric-sound-api-v1"
version = getSubprojectVersion(project)
loom {
interfaceInjection {
interfaceInjectionSourceSets.add sourceSets.client
}
}
testDependencies(project, [
':fabric-api-base',
':fabric-resource-loader-v0',
':fabric-command-api-v2'
])

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.client.sound.v1;
import java.util.concurrent.CompletableFuture;
import net.minecraft.client.sound.AudioStream;
import net.minecraft.client.sound.SoundInstance;
import net.minecraft.client.sound.SoundLoader;
import net.minecraft.util.Identifier;
/**
* General purpose Fabric-provided extensions to {@link SoundInstance}.
*
* <p>This interface is implicitly implemented on all {@link SoundInstance}s via a mixin and interface injection.
*/
public interface FabricSoundInstance {
/**
* An empty sound, which may be used as a placeholder in your {@code sounds.json} file for sounds with custom audio
* streams.
*
* @see #getAudioStream(SoundLoader, Identifier, boolean)
*/
Identifier EMPTY_SOUND = new Identifier("fabric-sound-api-v1", "empty");
/**
* Loads the audio stream for this sound.
*
* <p>By default this will load {@code .ogg} files from active resource packs. It may be overridden to provide a
* custom {@link AudioStream} implementation which provides audio from another source, such as over the network or
* driven from user input.
*
* <h3>Usage Example</h3>
*
* <p>Creating a sound with a custom audio stream requires the following:
*
* <p>Firstly, an entry in {@code sounds.json}. The name can be set to any sound (though it is recommended to use
* the dummy {@link #EMPTY_SOUND}), and the "stream" property set to true:
*
* <pre>{@code
* {
* "custom_sound": {"sounds": [{"name": "fabric-sound-api-v1:empty", "stream": true}]}
* }
* }</pre>
*
* <p>You should then define your own implementation of {@link AudioStream}, which provides audio data to the sound
* engine.
*
* <p>Finally, you'll need an implementation of {@link SoundInstance} which overrides {@link #getAudioStream} to
* return your custom implementation. {@link SoundInstance#getSound()} should return the newly-added entry in
* {@code sounds.json}.
*
* <pre>{@code
* class CustomSound extends AbstractSoundInstance {
* CustomSound() {
* // Use the sound defined in sounds.json
* super(new Identifier("mod_id", "custom_sound"), SoundCategory.BLOCKS, SoundInstance.createRandom());
* }
*
* @Override
* public CompletableFuture<AudioStream> getAudioStream(SoundLoader loader, Identifier id, boolean repeatInstantly) {
* // Return your custom AudioStream implementation.
* return CompletableFuture.completedFuture(new CustomStream());
* }
* }
* }</pre>
*
* @param loader The default sound loader, capable of loading {@code .ogg} files.
* @param id The resolved sound ID, equal to {@link SoundInstance#getSound()}'s location.
* @param repeatInstantly Whether this sound should loop. This is true when the sound
* {@linkplain SoundInstance#isRepeatable() is repeatable} and has
* {@linkplain SoundInstance#getRepeatDelay() no delay}.
* @return the loaded audio stream
*/
default CompletableFuture<AudioStream> getAudioStream(SoundLoader loader, Identifier id, boolean repeatInstantly) {
return loader.loadStreamed(id, repeatInstantly);
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.client.sound;
import org.spongepowered.asm.mixin.Mixin;
import net.minecraft.client.sound.SoundInstance;
import net.fabricmc.fabric.api.client.sound.v1.FabricSoundInstance;
@Mixin(SoundInstance.class)
public interface SoundInstanceMixin extends FabricSoundInstance {
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.client.sound;
import java.util.concurrent.CompletableFuture;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import net.minecraft.client.sound.SoundInstance;
import net.minecraft.client.sound.SoundLoader;
import net.minecraft.client.sound.SoundSystem;
import net.minecraft.util.Identifier;
@Mixin(SoundSystem.class)
public class SoundSystemMixin {
@Redirect(
method = "play(Lnet/minecraft/client/sound/SoundInstance;)V",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/sound/SoundLoader;loadStreamed(Lnet/minecraft/util/Identifier;Z)Ljava/util/concurrent/CompletableFuture;"
)
)
private CompletableFuture<?> getStream(SoundLoader loader, Identifier id, boolean looping, SoundInstance sound) {
return sound.getAudioStream(loader, id, looping);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,12 @@
{
"required": true,
"package": "net.fabricmc.fabric.mixin.client.sound",
"compatibilityLevel": "JAVA_17",
"client": [
"SoundInstanceMixin",
"SoundSystemMixin"
],
"injectors": {
"defaultRequire": 1
}
}

View file

@ -0,0 +1,35 @@
{
"schemaVersion": 1,
"id": "fabric-sound-api-v1",
"name": "Fabric Sound API (v1)",
"version": "${version}",
"environment": "client",
"license": "Apache-2.0",
"icon": "assets/fabric-sound-api-v1/icon.png",
"contact": {
"homepage": "https://fabricmc.net",
"irc": "irc://irc.esper.net:6667/fabric",
"issues": "https://github.com/FabricMC/fabric/issues",
"sources": "https://github.com/FabricMC/fabric"
},
"authors": [
"FabricMC"
],
"depends": {
"fabricloader": ">=0.14.9",
"minecraft": ">=1.19.2"
},
"description": "Hooks for modifying Minecraft's sound system.",
"mixins": [
{
"config": "fabric-sound-api-v1.mixins.json",
"environment": "client"
}
],
"custom": {
"fabric-api:module-lifecycle": "stable",
"loom:injected_interfaces": {
"net/minecraft/class_1113": ["net/fabricmc/fabric/api/client/sound/v1/FabricSoundInstance"]
}
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.sound.client;
import net.minecraft.client.MinecraftClient;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
/**
* Plays a sine wave when the {@code /sine} client command is run.
*/
public class ClientSoundTest implements ClientModInitializer {
public static final String MOD_ID = "fabric-sound-api-v1-testmod";
@Override
public void onInitializeClient() {
ClientCommandRegistrationCallback.EVENT.register((dispatcher, access) -> {
dispatcher.register(ClientCommandManager.literal("sine").executes(o -> {
MinecraftClient client = o.getSource().getClient();
client.getSoundManager().play(new SineSound(client.player.getPos()));
return 0;
}));
});
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.sound.client;
import java.util.concurrent.CompletableFuture;
import net.minecraft.client.sound.AbstractSoundInstance;
import net.minecraft.client.sound.AudioStream;
import net.minecraft.client.sound.SoundInstance;
import net.minecraft.client.sound.SoundLoader;
import net.minecraft.sound.SoundCategory;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
class SineSound extends AbstractSoundInstance {
SineSound(Vec3d pos) {
super(new Identifier(ClientSoundTest.MOD_ID, "sine_wave"), SoundCategory.BLOCKS, SoundInstance.createRandom());
x = pos.x;
y = pos.y;
z = pos.z;
}
@Override
public CompletableFuture<AudioStream> getAudioStream(SoundLoader loader, Identifier id, boolean repeatInstantly) {
return CompletableFuture.completedFuture(new SineStream());
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.sound.client;
import java.nio.ByteBuffer;
import javax.sound.sampled.AudioFormat;
import org.lwjgl.BufferUtils;
import net.minecraft.client.sound.AudioStream;
/**
* An audio stream which plays a sine wave.
*/
class SineStream implements AudioStream {
private static final AudioFormat FORMAT = new AudioFormat(44100, 8, 1, false, false);
private static final double DT = 2 * Math.PI * 220 / 44100;
private static double value = 0;
@Override
public AudioFormat getFormat() {
return FORMAT;
}
@Override
public ByteBuffer getBuffer(int capacity) {
ByteBuffer buffer = BufferUtils.createByteBuffer(capacity);
for (int i = 0; i < capacity; i++) {
buffer.put(i, (byte) (Math.sin(value) * 127));
value = (value + DT) % Math.PI;
}
return buffer;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,10 @@
{
"sine_wave": {
"sounds": [
{
"name": "fabric-sound-api-v1:empty",
"stream": true
}
]
}
}

View file

@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"id": "fabric-sound-api-v1-testmod",
"name": "Fabric Sound API (v1) Test Mod",
"version": "1.0.0",
"environment": "client",
"license": "Apache-2.0",
"depends": {
"fabric-sound-api-v1": "*",
"fabric-command-api-v2": "*"
},
"entrypoints": {
"client": [
"net.fabricmc.fabric.test.sound.client.ClientSoundTest"
]
}
}

View file

@ -52,6 +52,7 @@ fabric-resource-conditions-api-v1-version=2.0.12
fabric-resource-loader-v0-version=0.7.0
fabric-screen-api-v1-version=1.0.27
fabric-screen-handler-api-v1-version=1.3.1
fabric-sound-api-v1-version=1.0.0
fabric-textures-v0-version=1.0.21
fabric-transfer-api-v1-version=2.1.1
fabric-transitive-access-wideners-v1-version=1.3.1

View file

@ -47,6 +47,7 @@ include 'fabric-resource-conditions-api-v1'
include 'fabric-resource-loader-v0'
include 'fabric-screen-api-v1'
include 'fabric-screen-handler-api-v1'
include 'fabric-sound-api-v1'
include 'fabric-textures-v0'
include 'fabric-transfer-api-v1'
include 'fabric-convention-tags-v1'