Minor refactoring

Compiles but not tested
This commit is contained in:
Minecon724 2025-01-29 18:26:35 +01:00
commit a4d8e56a4e
No known key found for this signature in database
GPG key ID: 3CCC4D267742C8E8
37 changed files with 993 additions and 969 deletions

38
pom.xml
View file

@ -19,10 +19,15 @@
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<!-- this one has no ipv6 -->
<repository>
<id>maxhenkel</id>
<url>https://maven.maxhenkel.de/repository/public</url>
</repository>
<repository>
<id>minebench-repo</id>
<url>https://repo.minebench.de/</url>
</repository>
</repositories>
<dependencies>
@ -51,5 +56,38 @@
<artifactId>ffmpeg</artifactId>
<version>0.8.0</version>
</dependency>
<dependency>
<groupId>de.themoep</groupId>
<artifactId>inventorygui</artifactId>
<version>1.6.4-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
<createDependencyReducedPom>false</createDependencyReducedPom>
<artifactSet>
<includes>
<include>de.themoep:inventorygui</include>
</includes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -1,137 +0,0 @@
package eu.m724.musicPlugin;
import eu.m724.musicPlugin.file.AudioFileStorage;
import eu.m724.musicPlugin.item.PortableMediaPlayer;
import eu.m724.musicPlugin.item.PortableMediaPlayers;
import eu.m724.musicPlugin.player.MusicPlayer;
import eu.m724.musicPlugin.file.AudioFile;
import eu.m724.musicPlugin.player.StaticMusicPlayer;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
public class MusicCommands implements CommandExecutor {
private final AudioFileStorage storage;
private MusicPlayer player;
public MusicCommands(AudioFileStorage storage) {
this.storage = storage;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equals("download")) {
try {
var cf = storage.download(URI.create(args[0]).toURL());
sender.sendMessage("Started download");
cf.handle((hash, ex) -> {
if (ex != null)
sender.sendMessage("ERROR downloading: " + ex.getMessage());
else
sender.spigot().sendMessage(
new ComponentBuilder("Hash: " + hash)
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy")))
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hash))
.append("\nClick here to convert to 48 kbps")
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to do that")))
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/convert " + hash + " 48000"))
.create()
);
return null;
});
} catch (IOException e) {
throw new RuntimeException(e);
}
} else if (command.getName().equals("convert")) {
try {
var cf = storage.convert(args[0], Integer.parseInt(args[1]));
sender.sendMessage("Converting " + args[0] + " to " + args[1] + "bps...");
cf.thenAccept(f -> {
sender.spigot().sendMessage(
new ComponentBuilder("Converted " + args[0] + " to " + args[1] + "bps! Click to play")
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to play")))
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/play " + args[0] + " " + args[1]))
.create()
);
}).exceptionally(ex -> {
sender.sendMessage("ERROR converting: " + ex.getMessage());
return null;
});
} catch (IOException e) {
throw new RuntimeException(e);
}
} else if (command.getName().equals("play")) {
if (args.length == 1) {
StringBuilder msg = new StringBuilder("Available bitrates:");
for (Map.Entry<Integer, File> entry : storage.getVersions(args[0]).entrySet()) {
msg.append(" ").append(entry.getKey());
}
sender.sendMessage(msg.toString());
return true;
}
var file = storage.get(args[0], Integer.parseInt(args[1]));
sender.sendMessage("Initializeng");
player = new StaticMusicPlayer(((Player)sender).getLocation());
player.init();
try (FileInputStream fis = new FileInputStream(file)) {
var song = new AudioFile(fis);
song.load();
player.setAudio(song);
player.unpause();
} catch (IOException e) {
throw new RuntimeException(e);
}
sender.sendMessage("done si r now playing it");
} else if (command.getName().equals("stop")) {
player.stop();
sender.sendMessage("stopedd");
} else if (command.getName().equals("pause")) {
player.pause();
sender.sendMessage("puased");
} else if (command.getName().equals("resume")) {
player.unpause();
sender.sendMessage("unapuised");
} else if (command.getName().equals("pmp")) {
var p = (Player) sender;
var pmp = PortableMediaPlayers.get(p.getItemInHand());
switch (args[0]) {
case "info" -> {
sender.sendMessage(String.valueOf(pmp.id));
sender.sendMessage(String.valueOf(pmp.premium));
sender.sendMessage(pmp.engraving);
}
case "create" -> {
pmp = PortableMediaPlayer.create(args[0].equals("yes"), args[1]);
p.getInventory().addItem(pmp.getItemStack());
}
case "play" -> {
var file = storage.get(args[1], Integer.parseInt(args[2]));
try (FileInputStream fis = new FileInputStream(file)) {
var song = new AudioFile(fis);
song.load();
pmp.play(song);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
return true;
}
}

View file

@ -1,61 +0,0 @@
package eu.m724.musicPlugin;
import de.maxhenkel.voicechat.api.BukkitVoicechatService;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import eu.m724.musicPlugin.file.AudioFileStorage;
import eu.m724.musicPlugin.item.ItemEvents;
import eu.m724.musicPlugin.item.speaker.BlockChecker;
import org.bukkit.plugin.java.JavaPlugin;
public final class MusicPlugin extends JavaPlugin {
@Override
public void onEnable() {
Statics.plugin = this;
BukkitVoicechatService service = getServer().getServicesManager().load(BukkitVoicechatService.class);
if (service != null) {
service.registerPlugin(
new MyVoicechatPlugin(
this::onApiStarted,
this::onPlayerConnected
)
);
}
getDataFolder().mkdir();
var mcmd = new MusicCommands(new AudioFileStorage(getDataFolder()));
getCommand("download").setExecutor(mcmd);
getCommand("convert").setExecutor(mcmd);
getCommand("play").setExecutor(mcmd);
getCommand("stop").setExecutor(mcmd);
getCommand("pause").setExecutor(mcmd);
getCommand("resume").setExecutor(mcmd);
getCommand("pmp").setExecutor(mcmd);
getServer().getPluginManager().registerEvents(new ItemEvents(), this);
// TODO do this better, maybe along events
new BlockChecker().runTaskTimerAsynchronously(this, 0, 20);
}
private void onApiStarted(VoicechatServerApi api) {
getLogger().info("registerating...");
var category = api.volumeCategoryBuilder()
.setId("musicc")
.setName("Music players")
.build();
api.registerVolumeCategory(category);
Statics.api = api;
getLogger().info("Sucess");
}
private void onPlayerConnected(VoicechatConnection connection) {
getLogger().info("Player connected: " + connection);
}
}

View file

@ -1,45 +0,0 @@
package eu.m724.musicPlugin;
import de.maxhenkel.voicechat.api.VoicechatApi;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import de.maxhenkel.voicechat.api.VoicechatPlugin;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import de.maxhenkel.voicechat.api.events.EventRegistration;
import de.maxhenkel.voicechat.api.events.PlayerConnectedEvent;
import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent;
import java.util.function.Consumer;
public class MyVoicechatPlugin implements VoicechatPlugin {
private final Consumer<VoicechatServerApi> apiConsumer;
private final Consumer<VoicechatConnection> playerConnected;
public MyVoicechatPlugin(Consumer<VoicechatServerApi> apiConsumer, Consumer<VoicechatConnection> playerConnected) {
this.apiConsumer = apiConsumer;
this.playerConnected = playerConnected;
}
@Override
public String getPluginId() {
return "myplgugonbo";
}
@Override
public void initialize(VoicechatApi api) {
VoicechatPlugin.super.initialize(api);
}
@Override
public void registerEvents(EventRegistration registration) {
registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted);
registration.registerEvent(PlayerConnectedEvent.class, this::onPlayerConnected);
}
public void onServerStarted(VoicechatServerStartedEvent event) {
apiConsumer.accept(event.getVoicechat());
}
public void onPlayerConnected(PlayerConnectedEvent event) {
playerConnected.accept(event.getConnection());
}
}

View file

@ -1,9 +0,0 @@
package eu.m724.musicPlugin;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
// TODO find a better way
public class Statics {
public static MusicPlugin plugin;
public static VoicechatServerApi api;
}

View file

@ -1,72 +0,0 @@
package eu.m724.musicPlugin.file;
import org.gagravarr.ogg.OggFile;
import org.gagravarr.opus.OpusAudioData;
import org.gagravarr.opus.OpusFile;
import java.io.*;
import java.util.ArrayList;
public class AudioFile {
private final InputStream inputStream;
private NotEncoder encoder;
private String title, artist;
private int duration;
public AudioFile(File file) {
try {
this.inputStream = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
public AudioFile(InputStream inputStream) {
this.inputStream = inputStream;
}
public void load() throws IOException {
if (encoder != null) return;
var list = new ArrayList<byte[]>();
var opus = new OpusFile(new OggFile(inputStream));
var frames = 0;
OpusAudioData packet;
while ((packet = opus.getNextAudioPacket()) != null) {
list.add(packet.getData());
frames += packet.getNumberOfFrames();
}
this.duration = frames / 50; // a frame is 20 ms long so 20 * 50 is 1000
System.out.printf("audiop file has %s framrs\n", frames);
this.title = opus.getTags().getTitle();
this.artist = opus.getTags().getArtist();
this.encoder = new NotEncoder(list.toArray(byte[][]::new));
}
public NotEncoder getEncoder() {
return encoder;
}
/**
* @return if data was loaded i.e. if load was called
*/
public boolean isLoaded() {
return encoder != null;
}
/** @return Track duration in seconds */
public int getTrackDuration() { return duration; }
/** @return Track title if available */
public String getTrackTitle() { return title; }
/** @return Track artist if available */
public String getTrackArtist() { return artist; }
}

View file

@ -1,39 +0,0 @@
package eu.m724.musicPlugin.file;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
public class AudioFileConverter {
private final FFmpeg ffmpeg;
public AudioFileConverter(FFmpeg ffmpeg) {
this.ffmpeg = ffmpeg;
}
public CompletableFuture<Void> convert(File input, File output, int bitrate) throws IOException {
return convert(input.getAbsolutePath(), output, bitrate);
}
public CompletableFuture<Void> convert(URL source, File output, int bitrate) throws IOException {
return convert(source.toString(), output, bitrate);
}
private CompletableFuture<Void> convert(String input, File output, int bitrate) throws IOException {
var builder = new FFmpegBuilder()
.setInput(input)
.addOutput(output.getAbsolutePath())
.setAudioChannels(1)
.setAudioSampleRate(48_000)
.setAudioBitRate(bitrate)
.done();
var executor = new FFmpegExecutor(ffmpeg);
return CompletableFuture.runAsync(executor.createJob(builder));
}
}

View file

@ -1,118 +0,0 @@
package eu.m724.musicPlugin.file;
import net.bramp.ffmpeg.FFmpeg;
import org.apache.commons.lang3.RandomStringUtils;
import java.io.*;
import java.net.URL;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
public class AudioFileStorage {
private final File basePath;
private final AudioFileConverter converter;
public AudioFileStorage(File basePath) {
this.basePath = basePath;
try {
this.converter = new AudioFileConverter(new FFmpeg()); // TODO better error handling and not here
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Get versions (bitrates) of an audio file
*
* @param hash TODO NOT SANITIZED
* @return Map of Bps to file, or null if no such hash
*/
public Map<Integer, File> getVersions(String hash) {
var dir = new File(basePath, hash);
var files = dir.listFiles();
if (files == null) return null;
return Arrays.stream(files).collect(Collectors.toMap(
f -> Integer.parseInt(f.getName().split("\\.")[0]),
f -> f
));
}
/**
* Get an audio file of given original hash and bitrate
*
* @param hash TODO NOT SANITIZED
* @param bitrate the bitrate in bps like 32000
*/
public File get(String hash, int bitrate) {
return new File(basePath, hash + "/" + bitrate + ".opus");
}
/**
* Get the original file with a hash
*/
public File getOriginal(String hash) {
return new File(basePath, hash + "/0.original");
}
/**
* Converts a (original) file to some bitrate
*
* @param hash the hash
* @param bitrate the target bitrate in bps
* @return the future with the new file
*/
public CompletableFuture<File> convert(String hash, int bitrate) throws IOException {
var file = get(hash, bitrate);
if (file.exists()) return CompletableFuture.completedFuture(file);
var og = getOriginal(hash);
if (!og.exists()) return null;
return converter.convert(og, file, bitrate).thenApply(v -> file);
}
/**
* Downloads an audio file and saves it as original
*
* @param url the url to download from
* @return the hash
*/
public CompletableFuture<String> download(URL url) {
return CompletableFuture.supplyAsync(() -> {
try {
var temp = new File(basePath, "temp_" + RandomStringUtils.randomAlphabetic(8));
var digest = MessageDigest.getInstance("SHA-256");
try (
var input = new BufferedInputStream(url.openStream());
var output = new FileOutputStream(temp)
) {
var dataBuffer = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(dataBuffer, 0, 1024)) != -1) {
output.write(dataBuffer, 0, bytesRead);
digest.update(dataBuffer, 0, bytesRead);
}
}
String hash = HexFormat.of().formatHex(digest.digest());
var og = getOriginal(hash);
og.getParentFile().mkdir();
if (!og.exists())
Files.move(temp.toPath(), og.toPath());
return hash;
} catch (IOException | NoSuchAlgorithmException e) {
throw new CompletionException(e);
}
});
}
}

View file

@ -1,47 +0,0 @@
package eu.m724.musicPlugin.file;
import de.maxhenkel.voicechat.api.opus.OpusEncoder;
public class NotEncoder implements OpusEncoder {
private final byte[][] opusFrames;
private int i = 0;
private final int size;
private boolean closed = false;
public NotEncoder(byte[][] opusFrames) {
this.opusFrames = opusFrames;
this.size = opusFrames.length;
}
public void seek(int frame) {
i = Math.clamp(frame, 0, size);
}
public int getFrame() { return i; }
public boolean hasRemaining() {
return i < size;
}
@Override
public byte[] encode(short[] rawAudio) {
if (i < size)
return opusFrames[i++];
return null;
}
@Override
public void resetState() {
closed = false;
i = 0;
}
@Override
public boolean isClosed() {
return closed;
}
@Override
public void close() { closed = true; }
}

View file

@ -1,35 +0,0 @@
package eu.m724.musicPlugin.item.speaker;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
import eu.m724.musicPlugin.file.AudioFile;
import eu.m724.musicPlugin.player.MovingMusicPlayer;
import eu.m724.musicPlugin.player.MusicPlayer;
import java.util.function.Consumer;
public abstract class Speaker {
private final MusicPlayer musicPlayer;
private Consumer<Void> destroyCallback;
protected Speaker(MusicPlayer musicPlayer) {
this.musicPlayer = musicPlayer;
musicPlayer.init();
}
public void destroy() {
musicPlayer.stop();
onDestroy();
destroyCallback.accept(null);
}
public void setDestroyCallback(Consumer<Void> consumer) {
this.destroyCallback = consumer;
}
public MusicPlayer getMusicPlayer() {
return musicPlayer;
}
abstract void onDestroy();
}

View file

@ -1,33 +0,0 @@
package eu.m724.musicPlugin.player;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
import de.maxhenkel.voicechat.api.audiochannel.EntityAudioChannel;
import org.bukkit.entity.Entity;
import java.util.UUID;
public class EntityMusicPlayer extends MusicPlayer {
private final Entity entity;
public EntityMusicPlayer(Entity entity) {
this.entity = entity;
}
@Override
AudioChannel createChannel() {
var channel = api.createEntityAudioChannel(
UUID.randomUUID(),
api.fromEntity(entity)
);
channel.setCategory("musicc");
channel.setDistance(32);
return channel;
}
@Override
public void setDistance(int distance) {
((EntityAudioChannel)this.channel).setDistance(distance);
}
}

View file

@ -1,41 +0,0 @@
package eu.m724.musicPlugin.player;
import de.maxhenkel.voicechat.api.ServerLevel;
import de.maxhenkel.voicechat.api.VoicechatConnection;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
import eu.m724.musicPlugin.Statics;
import org.bukkit.entity.Player;
import java.util.UUID;
public class LocalMusicPlayer extends MusicPlayer {
private final ServerLevel level;
private final VoicechatConnection connection;
public LocalMusicPlayer(Player player) throws NotConnectedException {
this.level = Statics.api.fromServerLevel(player.getWorld());
this.connection = Statics.api.getConnectionOf(player.getUniqueId());
if (connection == null) {
throw new NotConnectedException();
}
}
@Override
AudioChannel createChannel() {
var channel = api.createStaticAudioChannel(
UUID.randomUUID(),
level,
connection
);
channel.setCategory("musicc");
return channel;
}
@Override
public void setDistance(int distance) { } // distance doesn't apply to this one
public static class NotConnectedException extends Exception {}
}

View file

@ -1,56 +0,0 @@
package eu.m724.musicPlugin.player;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
import de.maxhenkel.voicechat.api.audiochannel.LocationalAudioChannel;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import java.util.UUID;
public class MovingMusicPlayer extends MusicPlayer {
private Entity entity;
private Location location;
public void moveTo(Location location) {
System.out.println("Mvoed to a");
this.location = location;
this.channel = createChannel();
if (isReady() && !isPlaying()) {
this.pause();
this.unpause();
}
}
public void attachTo(Entity entity) {
var channel = api.createEntityAudioChannel(
UUID.randomUUID(),
api.fromEntity(entity)
);
channel.setCategory("musicc");
channel.setDistance(4);
this.channel = channel;
}
@Override
AudioChannel createChannel() {
var channel = api.createLocationalAudioChannel(
UUID.randomUUID(),
api.fromServerLevel(location.getWorld()),
api.createPosition(location.getX(), location.getY(), location.getZ())
);
channel.setCategory("musicc");
channel.setDistance(4);
return channel;
}
@Override
public void setDistance(int distance) {
((LocationalAudioChannel)this.channel).setDistance(distance);
}
}

View file

@ -1,181 +0,0 @@
package eu.m724.musicPlugin.player;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer;
import eu.m724.musicPlugin.Statics;
import eu.m724.musicPlugin.file.AudioFile;
import java.io.IOException;
import java.util.function.Consumer;
public abstract class MusicPlayer {
// TODO find a better way
final VoicechatServerApi api = Statics.api;
AudioChannel channel;
private boolean ready = false, playing = false;
private AudioPlayer player;
private AudioFile audioFile;
private Consumer<TrackAction> onAction = (r) -> {};
abstract AudioChannel createChannel();
abstract public void setDistance(int distance);
/**
* Initializes this music player
*/
public void init() {
this.channel = createChannel();
}
/**
* Set the consumer that will be called after playback is paused, stopped, resumed etc<br>
* There can be only one for one music player
*
* @see TrackAction
*/
public void setOnAction(Consumer<TrackAction> onAction) {
this.onAction = onAction;
}
/* Playback control */
/**
* Sets audio file to play<br>
* If it's not loaded, it will be loaded synchronously.<br>
* The file is not rewinded. If you want to start from beginning, {@link MusicPlayer#seek(int)}
* After this, do {@link MusicPlayer#unpause()}
*
* @see MusicPlayer#pause()
* @see MusicPlayer#stop()
*/
public void setAudio(AudioFile audioFile) {
if (audioFile != null)
this.audioFile = audioFile;
if (ready)
stop();
if (!audioFile.isLoaded()) {
try {
System.out.println("Audio not already loaded, so I load it");
audioFile.load();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
System.out.printf("Audio loaded it's %d seconds long, so I'm guessing %d frames\n", audioFile.getTrackDuration(), audioFile.getTrackDuration() * 50);
ready = true;
unpause();
}
public void unpause() {
if (!ready || playing) return;
System.out.println("playback unpaused");
var sa = new short[960];
var enc = audioFile.getEncoder();
player = api.createAudioPlayer(channel, enc, () -> enc.hasRemaining() ? sa : null);
player.setOnStopped(this::onStop);
player.startPlaying();
// TODO this may take a long time and it might glitch if it does
onAction.accept(TrackAction.UNPAUSE);
playing = true;
System.out.println("playback unpaused now");
}
/**
* Stops playback and rewinds
*/
public void stop() {
if (!ready) return;
stopPlayback(false);
audioFile.getEncoder().resetState();
}
/**
* Pauses playback
*/
public void pause() {
if (!ready || playing) return;
stopPlayback(true);
}
/**
* Seek to some point in current track
*
* @param target the target time in milliseconds
*/
public void seek(int target) {
audioFile.getEncoder().seek(target / 20); // a frame is 20 ms usually
}
/* Internal methods */
private void stopPlayback(boolean pause) {
System.out.println("playback stopped");
playing = false;
ready = pause;
player.stopPlaying();
player = null;
onStop();
}
private void onStop() {
if (ready && !playing) { // paused
System.out.println("I detected pause");
onAction.accept(TrackAction.PAUSE);
playing = false;
} else if (ready) { // not paused and still playing
System.out.println("I detected end");
onAction.accept(TrackAction.DURATION);
ready = false;
} else { // not playing
System.out.println("I detected stop");
onAction.accept(TrackAction.STOP);
}
}
public enum TrackAction {
/**
* Song stopped playing because it played
*/
DURATION,
/**
* {@link MusicPlayer#stop()} was called, or started playing a new track
*/
STOP,
/**
* {@link MusicPlayer#pause()} was called
*/
PAUSE,
/**
* {@link MusicPlayer#unpause()} was called
*/
UNPAUSE
}
public boolean isReady() {
return ready;
}
public boolean isPlaying() {
return ready && playing;
}
}

View file

@ -0,0 +1,51 @@
package eu.m724.music_plugin;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DebugLogger {
static Logger logger;
public static void info(String message, Object... format) {
log(Level.INFO, message, format);
}
public static void warning(String message, Object... format) {
log(Level.WARNING, message, format);
}
public static void severe(String message, Object... format) {
log(Level.SEVERE, message, format);
}
public static void fine(String message, Object... format) {
log(Level.FINE, message, format);
}
public static void finer(String message, Object... format) {
log(Level.FINER, message, format);
}
private static void log(Level level, String message, Object... format) {
if (logger.getLevel().intValue() > level.intValue()) return;
var caller = Thread.currentThread().getStackTrace()[3].getClassName();
if (caller.startsWith("eu.m724.music_plugin."))
caller = caller.substring(21);
message = "[" + caller + "] " + message.formatted(format);
if (level.intValue() < Level.INFO.intValue()) { // levels below info are never logged even if set for some reason
// colors text gray (cyan is close to gray)
if (level == Level.FINE) {
message = "\033[38;5;250m" + message + "\033[39m";
} else {
message = "\033[38;5;245m" + message + "\033[39m";
}
level = Level.INFO;
}
logger.log(level, message);
}
}

View file

@ -0,0 +1,44 @@
package eu.m724.music_plugin;
import eu.m724.music_plugin.item.PortableMediaPlayer;
import eu.m724.music_plugin.item.PortableMediaPlayers;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.io.IOException;
public class MusicCommands implements CommandExecutor {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (command.getName().equals("pmp")) {
var p = (Player) sender;
var pmp = PortableMediaPlayers.get(p.getItemInHand());
switch (args[0]) {
case "info" -> {
sender.sendMessage(String.valueOf(pmp.id));
sender.sendMessage(String.valueOf(pmp.premium));
sender.sendMessage(pmp.engraving);
}
case "create" -> {
pmp = PortableMediaPlayer.create(args[0].equals("yes"), args[1]);
p.getInventory().addItem(pmp.getItemStack());
}
case "play" -> {
var storage = MusicPlugin.getStorage();
var path = storage.get(args[1], Integer.parseInt(args[2]));
try {
pmp.play(path.toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
return true;
}
}

View file

@ -0,0 +1,90 @@
package eu.m724.music_plugin;
import de.maxhenkel.voicechat.api.BukkitVoicechatService;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import eu.m724.music_plugin.audio.storage.AudioFileStorage;
import eu.m724.music_plugin.audio.Converter;
import eu.m724.music_plugin.audio.storage.Downloader;
import eu.m724.music_plugin.item.ItemEvents;
import eu.m724.music_plugin.item.speaker.BlockChecker;
import net.bramp.ffmpeg.FFmpeg;
import org.bukkit.NamespacedKey;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.IOException;
import java.util.logging.Level;
public final class MusicPlugin extends JavaPlugin {
private static MusicPlugin INSTANCE;
private static VoicechatServerApi VOICECHAT_API;
private static AudioFileStorage AUDIO_FILE_STORAGE;
private static Converter CONVERTER;
private static Downloader DOWNLOADER;
@Override
public void onEnable() {
var start = System.nanoTime();
INSTANCE = this;
getLogger().setLevel(Level.FINEST);
DebugLogger.logger = getLogger();
var service = getServer().getServicesManager().load(BukkitVoicechatService.class);
service.registerPlugin(new MyVoicechatPlugin(api -> VOICECHAT_API = api));
getDataFolder().mkdir();
var storagePath = getDataFolder().toPath().resolve("storage");
AUDIO_FILE_STORAGE = new AudioFileStorage(storagePath);
DOWNLOADER = new Downloader(storagePath);
try {
CONVERTER = new Converter(new FFmpeg());
} catch (IOException e) {
throw new RuntimeException("Failed to initialize FFmpeg", e);
}
getCommand("test").setExecutor(new TestCommand());
/*var mcmd = new MusicCommands(new AudioFileStorage(getDataFolder()));
getCommand("download").setExecutor(mcmd);
getCommand("convert").setExecutor(mcmd);
getCommand("play").setExecutor(mcmd);
getCommand("stop").setExecutor(mcmd);
getCommand("pause").setExecutor(mcmd);
getCommand("resume").setExecutor(mcmd);
getCommand("pmp").setExecutor(mcmd);*/
getServer().getPluginManager().registerEvents(new ItemEvents(), this);
// TODO do this better, maybe along events
new BlockChecker().runTaskTimerAsynchronously(this, 0, 20);
var end = System.nanoTime();
DebugLogger.fine("Enabled in %.3f milliseconds", (end - start) / 1000000.0);
}
public static MusicPlugin getInstance() {
return INSTANCE;
}
public static VoicechatServerApi getVoicechatApi() {
return VOICECHAT_API;
}
public static AudioFileStorage getStorage() {
return AUDIO_FILE_STORAGE;
}
public static Converter getConverter() {
return CONVERTER;
}
public static Downloader getDownloader() {
return DOWNLOADER;
}
public static NamespacedKey getNamespacedKey(String key) {
return new NamespacedKey(INSTANCE, key);
}
}

View file

@ -0,0 +1,42 @@
package eu.m724.music_plugin;
import de.maxhenkel.voicechat.api.VoicechatPlugin;
import de.maxhenkel.voicechat.api.VoicechatServerApi;
import de.maxhenkel.voicechat.api.events.EventRegistration;
import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent;
import java.util.function.Consumer;
public class MyVoicechatPlugin implements VoicechatPlugin {
private final Consumer<VoicechatServerApi> apiConsumer;
public MyVoicechatPlugin(Consumer<VoicechatServerApi> apiConsumer) {
this.apiConsumer = apiConsumer;
}
@Override
public String getPluginId() {
return "music724";
}
@Override
public void registerEvents(EventRegistration registration) {
registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted);
}
private void onServerStarted(VoicechatServerStartedEvent event) {
var api = event.getVoicechat();
var category = api.volumeCategoryBuilder()
.setId("music")
.setName("Music players")
.build();
api.registerVolumeCategory(category);
apiConsumer.accept(api);
DebugLogger.fine("Registered");
}
}

View file

@ -0,0 +1,99 @@
package eu.m724.music_plugin;
import eu.m724.music_plugin.audio.channel.LocationalChannel;
import eu.m724.music_plugin.audio.player.OpusFilePlayer;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.ComponentBuilder;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.gagravarr.opus.OpusFile;
import java.io.IOException;
import java.net.URI;
// TODO remove
public class TestCommand implements CommandExecutor {
private OpusFilePlayer opusFilePlayer;
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
var player = (Player) sender;
if (args[0].equals("download")) {
testDownload(player, args[1]);
} else if (args[0].equals("convert")) {
testConvert(player, args[1], Integer.parseInt(args[2]));
} else if (args[0].equals("play")) {
testPlay(player, args[1], Integer.parseInt(args[2]));
} else if (args[0].equals("pause")) {
opusFilePlayer.pause();
} else if (args[0].equals("unpause")) {
opusFilePlayer.unpause();
}
return true;
}
private void testDownload(Player player, String url) {
player.sendMessage("Downloading " + url);
MusicPlugin.getDownloader().download(URI.create(url)).handle((hash, ex) -> {
if (ex != null)
player.sendMessage("ERROR downloading: " + ex.getMessage());
else
player.spigot().sendMessage(
new ComponentBuilder("Hash: " + hash)
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to copy")))
.event(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, hash))
.append("\nClick here to convert to 48 kbps")
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to do that")))
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/convert " + hash + " 48000"))
.create()
);
return null;
});
}
private void testConvert(Player player, String hash, int bitrate) {
try {
MusicPlugin.getStorage().convert(MusicPlugin.getConverter(), hash, bitrate).handle((f, ex) -> {
if (ex != null)
player.sendMessage("ERROR converting: " + ex.getMessage());
else
player.spigot().sendMessage(
new ComponentBuilder("Converted " + hash.substring(0, 7) + "... to " + bitrate + "bps! Click to play")
.event(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text("Click to play")))
.event(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/test play " + hash + " " + bitrate))
.create()
);
return null;
});
} catch (IOException e) {
e.printStackTrace();
player.sendMessage("Error");
}
}
private void testPlay(Player player, String hash, int bitrate) {
var channel = new LocationalChannel(player.getLocation()).getAudioChannel();
if (opusFilePlayer != null) {
opusFilePlayer.stop();
}
try {
opusFilePlayer = new OpusFilePlayer(new OpusFile(MusicPlugin.getStorage().get(hash, bitrate).toFile()), channel);
opusFilePlayer.play();
opusFilePlayer.setOnTrackEvent(e -> {
player.sendMessage("Event: " + e);
});
player.sendMessage("Now playing");
} catch (IOException e) {
e.printStackTrace();
player.sendMessage("Error");
}
}
}

View file

@ -0,0 +1,56 @@
package eu.m724.music_plugin.audio;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
public class Converter {
private final FFmpeg ffmpeg;
public Converter(FFmpeg ffmpeg) {
this.ffmpeg = ffmpeg;
}
/**
* Converts a media file to OPUS with given bitrate.
*
* @param bitrate the target bitrate in Bps (bits per second)
* @return the future that results in {@code output} when done
* @throws IOException if something went wrong with FFmpeg
*/
public CompletableFuture<Path> convert(Path input, Path output, int bitrate) throws IOException {
return convert(input.toAbsolutePath().toString(), output, bitrate);
}
/**
* Converts a media file to OPUS with given bitrate.
*
* @deprecated It's better to use this plugin to download
* @param bitrate the target bitrate in Bps (bits per second)
* @return the future that results in {@code output} when done
* @throws IOException if something went wrong with FFmpeg
*/
@Deprecated(forRemoval = true)
public CompletableFuture<Path> convert(URL source, Path output, int bitrate) throws IOException {
return convert(source.toString(), output, bitrate);
}
private CompletableFuture<Path> convert(String input, Path output, int bitrate) throws IOException {
var builder = new FFmpegBuilder()
.setInput(input)
.addOutput(output.toAbsolutePath().toUri())
.setAudioChannels(1)
.setAudioSampleRate(48_000)
.setAudioBitRate(bitrate)
.done();
var executor = new FFmpegExecutor(ffmpeg);
return CompletableFuture.runAsync(executor.createJob(builder)).thenApply(v -> output);
}
}

View file

@ -0,0 +1,63 @@
package eu.m724.music_plugin.audio;
import de.maxhenkel.voicechat.api.opus.OpusEncoder;
import eu.m724.music_plugin.DebugLogger;
import org.apache.commons.lang3.NotImplementedException;
import org.gagravarr.opus.OpusFile;
import java.io.File;
import java.io.IOException;
public class OpusFileEncoder implements OpusEncoder {
private final OpusFile opusFile;
private boolean closed = false;
public OpusFileEncoder(OpusFile file) {
this.opusFile = file;
}
@Override
public byte[] encode(short[] rawAudio) {
try {
var packet = opusFile.getNextAudioPacket();
if (packet != null)
return packet.getData();
else
return new byte[0];
} catch (IOException e) {
throw new RuntimeException("Reading next packet", e);
}
}
@Override
public void resetState() {
// TODO
throw new NotImplementedException();
}
@Override
public boolean isClosed() {
return closed;
}
@Override
public void close() {
DebugLogger.fine("opus file just closed");
if (closed) return;
closed = true;
try {
opusFile.close();
} catch (IOException e) {
throw new RuntimeException("Closing opus file", e);
}
}
/* SUPPLIER */
private final short[] array = new short[960];
public short[] supplier() {
return !closed ? array : null;
}
}

View file

@ -0,0 +1,16 @@
package eu.m724.music_plugin.audio.channel;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
public abstract class AbstractChannel<T extends AudioChannel> {
protected T channel;
public T getAudioChannel() {
if (this.channel == null)
this.channel = doCreateChannel();
return this.channel;
}
protected abstract T doCreateChannel();
abstract void setDistance(int distance);
}

View file

@ -0,0 +1,36 @@
package eu.m724.music_plugin.audio.channel;
import de.maxhenkel.voicechat.api.audiochannel.EntityAudioChannel;
import eu.m724.music_plugin.MusicPlugin;
import org.bukkit.entity.Entity;
import java.util.UUID;
public class EntityChannel extends AbstractChannel<EntityAudioChannel> {
private final Entity entity;
public EntityChannel(Entity entity) {
this.entity = entity;
}
@Override
protected EntityAudioChannel doCreateChannel() {
var api = MusicPlugin.getVoicechatApi();
var channel = api.createEntityAudioChannel(
UUID.randomUUID(),
api.fromEntity(entity)
);
channel.setCategory("music");
channel.setDistance(32);
return channel;
}
@Override
public void setDistance(int distance) {
// we go getChannel() not just channel, because getChannel() creates it if it doesn't exist
getAudioChannel().setDistance(distance);
}
}

View file

@ -1,27 +1,29 @@
package eu.m724.musicPlugin.player;
package eu.m724.music_plugin.audio.channel;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
import de.maxhenkel.voicechat.api.audiochannel.LocationalAudioChannel;
import eu.m724.music_plugin.MusicPlugin;
import org.bukkit.Location;
import java.util.UUID;
public class StaticMusicPlayer extends MusicPlayer {
public class LocationalChannel extends AbstractChannel<LocationalAudioChannel> {
private final Location location;
public StaticMusicPlayer(Location location) {
public LocationalChannel(Location location) {
this.location = location;
}
@Override
AudioChannel createChannel() {
protected LocationalAudioChannel doCreateChannel() {
var api = MusicPlugin.getVoicechatApi();
var channel = api.createLocationalAudioChannel(
UUID.randomUUID(),
api.fromServerLevel(location.getWorld()),
api.createPosition(location.getX(), location.getY(), location.getZ())
);
channel.setCategory("musicc");
channel.setCategory("music");
channel.setDistance(32);
return channel;
@ -29,6 +31,7 @@ public class StaticMusicPlayer extends MusicPlayer {
@Override
public void setDistance(int distance) {
((LocationalAudioChannel)this.channel).setDistance(distance);
// we go getChannel() not just channel, because getChannel() creates it if it doesn't exist
getAudioChannel().setDistance(distance);
}
}

View file

@ -0,0 +1,129 @@
package eu.m724.music_plugin.audio.player;
import de.maxhenkel.voicechat.api.audiochannel.AudioChannel;
import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer;
import eu.m724.music_plugin.DebugLogger;
import eu.m724.music_plugin.MusicPlugin;
import eu.m724.music_plugin.audio.OpusFileEncoder;
import org.gagravarr.opus.OpusFile;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
public class OpusFilePlayer {
private final OpusFileEncoder encoder;
private AudioChannel channel;
private AudioPlayer audioPlayer;
private boolean playing = false;
private Consumer<TrackEvent> onTrackEvent = (r) -> {};
public OpusFilePlayer(OpusFile opusFile, AudioChannel channel) {
this.encoder = new OpusFileEncoder(opusFile);
this.channel = channel;
}
public OpusFilePlayer(File file, AudioChannel channel) throws IOException {
this(new OpusFile(file), channel);
}
/**
* Set the consumer that will be called after playback is paused, stopped, resumed etc<br>
* There can be only one for one music player
*
* @see TrackEvent
*/
public void setOnTrackEvent(Consumer<TrackEvent> onTrackEvent) {
this.onTrackEvent = onTrackEvent;
}
public void setChannel(AudioChannel channel) {
DebugLogger.finer("Changing channel...");
this.channel = channel;
if (!playing) {
// to not call events pointlessly
var ote = this.onTrackEvent;
this.onTrackEvent = (r) -> {};
// playing recreates the audio channel
pause();
play();
this.onTrackEvent = ote;
}
DebugLogger.fine("Channel changed");
}
/**
* Starts or resumes playback
*/
public void play() {
DebugLogger.finer("Playback starting...");
if (playing) return;
audioPlayer = MusicPlugin.getVoicechatApi().createAudioPlayer(channel, encoder, encoder::supplier);
audioPlayer.setOnStopped(this::onPlayerStopped);
audioPlayer.startPlaying();
playing = true;
onTrackEvent.accept(TrackEvent.START);
DebugLogger.fine("Playback started");
}
/**
* An alias for {@link #play()}
*/
public void unpause() {
play();
}
/**
* Pause playback<br>
* To resume, {@link #play()}
*/
public void pause() {
stop(false);
}
/**
* Stops playback (pauses and rewinds)
*/
public void stop() {
stop(true);
}
private void stop(boolean reset) {
DebugLogger.fine("Playback stopping... (reset: %s)", reset);
if (!playing) return;
playing = false;
audioPlayer.stopPlaying();
if (reset)
encoder.resetState();
DebugLogger.fine("Playback stopped (reset: %s)", reset);
}
private void onPlayerStopped() {
if (playing) {
DebugLogger.fine("onPlayerStopped called and playing, calling END");
onTrackEvent.accept(TrackEvent.END);
} else {
DebugLogger.fine("onPlayerStopped called and not playing, calling STOP");
onTrackEvent.accept(TrackEvent.STOP);
}
}
/**
* @return Is player playing (not paused)
*/
public boolean isPlaying() {
return playing;
}
}

View file

@ -0,0 +1,17 @@
package eu.m724.music_plugin.audio.player;
public enum TrackEvent {
/**
* Track ended
*/
END,
/**
* Track was stopped (or paused)<br>
* If it ended, {@link TrackEvent#END} is used instead
*/
STOP,
/**
* Track started playing (or unpaused)
*/
START
}

View file

@ -0,0 +1,89 @@
package eu.m724.music_plugin.audio.storage;
import eu.m724.music_plugin.audio.Converter;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class AudioFileStorage {
private final Path basePath;
public AudioFileStorage(Path basePath) {
this.basePath = basePath;
}
/**
* Get versions (bitrates) of an audio file<br>
* Returns a map of bitrate: file, or null if no such hash<br>
* Original is 0
*
* @param hash TODO NOT SANITIZED
* @return Map of Bps to file, or null if no such hash
*/
public Map<Integer, Path> getVersions(String hash) throws IOException {
assert isHashValid(hash);
var dir = basePath.resolve(hash);
try (var files = Files.list(dir)) {
return files.collect(Collectors.toMap(
k -> Integer.parseInt(k.getFileName().toString().split("\\.")[0]),
v -> v
));
} catch (NotDirectoryException e) {
return null;
}
}
/**
* Get an audio file of given original hash and bitrate
*
* @param hash the hex sha256 hash of the original
* @param bitrate the bitrate in bps like 32000
*/
public Path get(String hash, int bitrate) {
assert isHashValid(hash);
return basePath.resolve(hash).resolve(bitrate + ".opus");
}
/**
* Get the original file with a hash
*/
public Path getOriginal(String hash) {
assert isHashValid(hash);
return basePath.resolve(hash).resolve("0.original");
}
/**
* Checks if a hash is a valid SHA256 hash
* @param hash the hash in hex format
*/
private boolean isHashValid(String hash) {
var bytes = HexFormat.of().parseHex(hash);
return bytes.length == 32;
}
// TODO move those
/**
* Converts a (original) file to some bitrate
*
* @param hash the hash
* @param bitrate the target bitrate in bps
* @return the future with the new file
*/
public CompletableFuture<Path> convert(Converter converter, String hash, int bitrate) throws IOException {
var file = get(hash, bitrate);
if (Files.exists(file)) return CompletableFuture.completedFuture(file);
var og = getOriginal(hash);
if (!Files.exists(og)) return null;
return converter.convert(og, file, bitrate).thenApply(v -> file);
}
}

View file

@ -0,0 +1,86 @@
package eu.m724.music_plugin.audio.storage;
import eu.m724.music_plugin.DebugLogger;
import org.apache.commons.lang3.RandomStringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
public class Downloader {
private final Path basePath;
public Downloader(Path basePath) {
this.basePath = basePath;
}
/**
* Downloads an audio file and saves it as original
*
* @param uri the uri to download from
* @return the downloaded file's SHA-256 hash (in a future)
*/
public CompletableFuture<String> download(URI uri) {
DebugLogger.fine("About to download from " + uri);
try (
var client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
) {
var request = HttpRequest.newBuilder(uri).GET().build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()).thenApply(this::futureFunction);
}
}
private String futureFunction(HttpResponse<InputStream> response) {
var contentLength = response.headers().firstValue("Content-Length");
DebugLogger.finer("Connected. Size: %s", contentLength.orElse(null));
var temp = basePath.resolve("temp_" + RandomStringUtils.randomAlphabetic(8));
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
try (
var input = response.body();
var output = Files.newOutputStream(temp)
) {
var dataBuffer = new byte[1024 * 1024];
int bytesRead;
while ((bytesRead = input.read(dataBuffer, 0, dataBuffer.length)) != -1) {
output.write(dataBuffer, 0, bytesRead);
digest.update(dataBuffer, 0, bytesRead);
}
} catch (IOException e) {
throw new RuntimeException("Exception downloading from " + response.uri(), e);
}
String hash = HexFormat.of().formatHex(digest.digest());
DebugLogger.fine("Downloaded, hash: " + hash);
try {
Files.createDirectory(basePath.resolve(hash));
Files.move(temp, basePath.resolve(hash).resolve("0.original"));
} catch (FileAlreadyExistsException e) {
DebugLogger.fine("Already exists!");
} catch (IOException e) {
throw new CompletionException("Exception saving file", e);
}
DebugLogger.fine("Done");
return hash;
}
}

View file

@ -1,22 +1,13 @@
package eu.m724.musicPlugin.item;
package eu.m724.music_plugin.item;
import eu.m724.musicPlugin.item.speaker.BlockSpeaker;
import eu.m724.musicPlugin.item.speaker.Speaker;
import eu.m724.musicPlugin.player.MovingMusicPlayer;
import eu.m724.music_plugin.item.speaker.BlockSpeaker;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.block.Block;
import org.bukkit.entity.Item;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityDropItemEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.ItemDespawnEvent;
import org.bukkit.event.inventory.InventoryMoveItemEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.PlayerDropItemEvent;

View file

@ -1,8 +1,10 @@
package eu.m724.musicPlugin.item;
package eu.m724.music_plugin.item;
import eu.m724.musicPlugin.file.AudioFile;
import eu.m724.musicPlugin.item.speaker.Speaker;
import eu.m724.musicPlugin.player.MusicPlayer;
import eu.m724.music_plugin.DebugLogger;
import eu.m724.music_plugin.MusicPlugin;
import eu.m724.music_plugin.audio.player.OpusFilePlayer;
import eu.m724.music_plugin.audio.player.TrackEvent;
import eu.m724.music_plugin.item.speaker.Speaker;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
@ -11,14 +13,16 @@ import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class PortableMediaPlayer {
static NamespacedKey idKey = new NamespacedKey("tweaks724", "player_id");
static NamespacedKey dataKey = new NamespacedKey("tweaks724", "player_data");
static final NamespacedKey idKey = MusicPlugin.getNamespacedKey("player_id");
static final NamespacedKey dataKey = MusicPlugin.getNamespacedKey("player_data");;
public final int id;
@ -29,8 +33,8 @@ public class PortableMediaPlayer {
public final boolean premium;
public final String engraving;
private PlayedSong played;
private Speaker speaker;
private OpusFilePlayer player; // TODO rename?
private Speaker<?> speaker;
PortableMediaPlayer(int id, int storageSeconds, int audioBitrate, boolean premium, String engraving) {
this.id = id;
@ -44,7 +48,7 @@ public class PortableMediaPlayer {
return new PortableMediaPlayer(ThreadLocalRandom.current().nextInt(), 600, 32000, premium, engraving);
}
public void setSpeaker(Speaker speaker) {
public void setSpeaker(Speaker<?> speaker) {
if (speaker.equals(this.speaker))
return;
@ -54,80 +58,73 @@ public class PortableMediaPlayer {
this.speaker.destroy();
}
speaker.setDestroyCallback(c -> {
speaker.setDestroyCallback(v -> {
System.out.println("spekar rip");
if (played != null)
played.pause(); // this one stops the tracker not playback, which is stopped in Speaker
if (player != null)
player.pause();
this.speaker = null;
});
this.speaker = speaker;
if (played != null && !played.paused()) {
unpause();
}
player.setChannel(speaker.getChannel().getAudioChannel());
}
public void play(AudioFile audioFile) {
public void play(File file) throws IOException {
DebugLogger.finer("pmp play");
if (speaker == null) return;
played = new PlayedSong(audioFile);
//played = new Track(audioFile);
if (player != null)
player.stop();
speaker.getMusicPlayer().setOnAction(action -> {
if (action == MusicPlayer.TrackAction.PAUSE) {
// track paused
System.out.println("Okay its paused");
played.pause();
played.hint(played.file.getEncoder().getFrame());
} else if (action == MusicPlayer.TrackAction.UNPAUSE) { // track unpaused
System.out.println("Okay its unpaused");
played.unpause();
} else if (action == MusicPlayer.TrackAction.DURATION) { // track ended
System.out.println("Okay its ened");
next();
this.player = new OpusFilePlayer(file, speaker.getChannel().getAudioChannel());
player.setOnTrackEvent(event -> {
if (event == TrackEvent.START) {
DebugLogger.finer("I detected track START");
//played.unpause();
} else if (event == TrackEvent.STOP) {
DebugLogger.finer("I detected track STOP");
//played.pause();
//played.hint(played.file.getEncoder().getFrame());
} else if (event == TrackEvent.END) {
DebugLogger.finer("I detected track END");
//next();
}
});
unpause();
player.play();
}
void pause() {
System.out.println("pmp pase");
if (played == null || speaker == null) return;
System.out.println("pmp pasedd");
DebugLogger.finer("pmp pause");
speaker.getMusicPlayer().pause();
if (player != null)
player.pause();
}
void unpause() {
System.out.println("pmp unpase");
if (played == null || speaker == null) return;
System.out.println("pmp unpasedd");
DebugLogger.finer("pmp unpause");
speaker.getMusicPlayer().setAudio(played.file);
speaker.getMusicPlayer().seek(played.getProgress());
speaker.getMusicPlayer().unpause();
if (player != null)
player.unpause();
}
void stop() {
System.out.println("pmp stop");
DebugLogger.finer("pmp stop");
if (speaker != null)
speaker.destroy();
this.played = null;
if (player != null)
player.stop();
}
// TODO
void prev() {
System.out.println("pmp prev");
this.played = new PlayedSong(played.file);
unpause();
DebugLogger.fine("pmp previous (does nothing)");
}
// TODO
void next() {
System.out.println("pmp next");
this.played = null;
// TODO
System.out.println("pmp next (does nothing)");
}
/* Item functions */
@ -136,7 +133,7 @@ public class PortableMediaPlayer {
var is = new ItemStack(Material.IRON_INGOT);
var meta = is.getItemMeta();
meta.setItemName("Portable music player");
meta.setItemName("Portable Music Player");
meta.addEnchant(Enchantment.UNBREAKING, 1, false);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
meta.getPersistentDataContainer().set(idKey, PersistentDataType.INTEGER, id);
@ -151,7 +148,7 @@ public class PortableMediaPlayer {
private byte[] getData() {
var buffer = ByteBuffer.allocate(11 + engraving.length());
buffer.put((byte) 0); // version
buffer.put((byte) 0); // data format version
buffer.put((byte) (premium ? 1 : 0));
buffer.putShort((short) storageSeconds); // make int if 18 hours is not enough. or store as minutes

View file

@ -1,4 +1,4 @@
package eu.m724.musicPlugin.item;
package eu.m724.music_plugin.item;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;

View file

@ -1,4 +1,4 @@
package eu.m724.musicPlugin.item.speaker;
package eu.m724.music_plugin.item.speaker;
import org.bukkit.scheduler.BukkitRunnable;

View file

@ -1,17 +1,16 @@
package eu.m724.musicPlugin.item.speaker;
package eu.m724.music_plugin.item.speaker;
import eu.m724.musicPlugin.Statics;
import eu.m724.musicPlugin.player.StaticMusicPlayer;
import eu.m724.music_plugin.DebugLogger;
import eu.m724.music_plugin.MusicPlugin;
import eu.m724.music_plugin.audio.channel.LocationalChannel;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.metadata.FixedMetadataValue;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
public class BlockSpeaker extends Speaker {
public class BlockSpeaker extends Speaker<LocationalChannel> {
static final Map<Location, BlockSpeaker> speakers = new HashMap<>();
public static BlockSpeaker get(Location location) {
@ -25,7 +24,8 @@ public class BlockSpeaker extends Speaker {
return null;
var isTopSpeaker = below.getBlock().getType() == Material.NOTE_BLOCK;
System.out.println("SPekaer is top: " + isTopSpeaker);
DebugLogger.fine("SPekaer is top: " + isTopSpeaker);
return speakers.compute(isTopSpeaker ? below : location, (k, v) -> new BlockSpeaker(k));
}
@ -35,9 +35,9 @@ public class BlockSpeaker extends Speaker {
private boolean large;
public BlockSpeaker(Location location) {
super(new StaticMusicPlayer(location));
super(new LocationalChannel(location));
this.location = location;
this.location.getBlock().setMetadata("t_speaker", new FixedMetadataValue(Statics.plugin, true));
this.location.getBlock().setMetadata("t_speaker", new FixedMetadataValue(MusicPlugin.getInstance(), true));
}
@Override
@ -50,17 +50,18 @@ public class BlockSpeaker extends Speaker {
if (location.clone().add(0, 1, 0).getBlock().getType() == Material.NOTE_BLOCK) {
if (!large) {
System.out.println("Speaker now large");
getMusicPlayer().setDistance(64);
getChannel().setDistance(64);
this.large = true;
}
} else {
if (large) {
System.out.println("Speaker now small");
getMusicPlayer().setDistance(32);
getChannel().setDistance(32);
this.large = false;
}
}
if (getMusicPlayer().isPlaying()) {
// TODO bring it back
/*if (getMusicPlayer().isPlaying()) {
if (large) {
location.getWorld().spawnParticle(Particle.NOTE, location.clone().add(0.6, 0.5, 0), 1, 0, ThreadLocalRandom.current().nextDouble(-1, 0.5), ThreadLocalRandom.current().nextDouble(-0.4, 0.4));
location.getWorld().spawnParticle(Particle.NOTE, location.clone().add(-0.6, 0.5, 0), 1, 0, ThreadLocalRandom.current().nextDouble(-1, 0.5), ThreadLocalRandom.current().nextDouble(-0.4, 0.4));
@ -69,7 +70,7 @@ public class BlockSpeaker extends Speaker {
} else {
location.getWorld().spawnParticle(Particle.NOTE, location.clone().add(0, 0.7, 0), 1, ThreadLocalRandom.current().nextDouble(-0.4, 0.4), 0, ThreadLocalRandom.current().nextDouble(-0.4, 0.4));
}
}
}*/
} else {
System.out.println("Speaker disaper");
destroy();

View file

@ -0,0 +1,29 @@
package eu.m724.music_plugin.item.speaker;
import eu.m724.music_plugin.audio.channel.AbstractChannel;
import java.util.function.Consumer;
public abstract class Speaker<T extends AbstractChannel<?>> {
private final T channel;
private Consumer<Void> destroyCallback;
protected Speaker(T channel) {
this.channel = channel;
}
public void destroy() {
onDestroy();
destroyCallback.accept(null);
}
public void setDestroyCallback(Consumer<Void> consumer) {
this.destroyCallback = consumer;
}
public T getChannel() {
return this.channel;
}
abstract void onDestroy();
}

View file

@ -0,0 +1,21 @@
package eu.m724.music_plugin.library;
import java.util.ArrayList;
import java.util.List;
public class Library {
private List<Track> tracks = new ArrayList<>();
private int playingTrack;
public void addTrack(Track track) {
tracks.add(track);
}
public void removeTrack(Track track) {
tracks.remove(track);
}
public Track getPlayingTrack() {
return tracks.get(playingTrack);
}
}

View file

@ -1,14 +1,14 @@
package eu.m724.musicPlugin.item;
package eu.m724.music_plugin.library;
import eu.m724.musicPlugin.file.AudioFile;
import java.io.File;
public class PlayedSong {
public final AudioFile file;
public class Track {
public final File file;
private int progress = 0;
private long started = -1;
public PlayedSong(AudioFile file) {
public Track(File file) {
this.file = file;
}

View file

@ -1,7 +1,7 @@
name: music-plugin
version: 1
main: eu.m724.musicPlugin.MusicPlugin
api-version: '1.21'
version: ${project.version}
main: eu.m724.music_plugin.MusicPlugin
api-version: '1.21.1'
depend: [voicechat]