diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java index ee175fce..dfe7d2a7 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/AudioTrack.java @@ -44,10 +44,27 @@ public interface AudioTrack extends AudioItem { void setPosition(long position); /** + * Set the track position marker. This will clear all existing markers. + * * @param marker Track position marker to place */ void setMarker(TrackMarker marker); + /** + * Adds a marker to the track. + * Markers can be used to execute code when the track reaches a certain position. + * + * @param marker The marker to add. + */ + void addMarker(TrackMarker marker); + + /** + * Removes a marker from the track. + * + * @param marker The marker to remove. + */ + void removeMarker(TrackMarker marker); + /** * @return Duration of the track in milliseconds */ diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java index b2565e47..be44fe8f 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/BaseAudioTrack.java @@ -87,6 +87,16 @@ public void setMarker(TrackMarker marker) { getActiveExecutor().setMarker(marker); } + @Override + public void addMarker(TrackMarker marker) { + getActiveExecutor().addMarker(marker); + } + + @Override + public void removeMarker(TrackMarker marker) { + getActiveExecutor().removeMarker(marker); + } + @Override public AudioFrame provide() { return getActiveExecutor().provide(); diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java index 777b6e18..641270e6 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/TrackMarkerTracker.java @@ -1,6 +1,8 @@ package com.sedmelluq.discord.lavaplayer.track; -import java.util.concurrent.atomic.AtomicReference; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import static com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler.MarkerState.*; @@ -8,46 +10,79 @@ * Tracks the state of a track position marker. */ public class TrackMarkerTracker { - private final AtomicReference current = new AtomicReference<>(); + private final List markerList = new CopyOnWriteArrayList<>(); /** - * Set a new track position marker. + * Set a new track position marker. This removes all previously set markers. * * @param marker Marker * @param currentTimecode Current timecode of the track when this marker is set */ public void set(TrackMarker marker, long currentTimecode) { - TrackMarker previous = current.getAndSet(marker); + if (marker == null) { + trigger(REMOVED); + } else { + trigger(OVERWRITTEN); - if (previous != null) { - previous.handler.handle(marker != null ? OVERWRITTEN : REMOVED); + add(marker, currentTimecode); } + } - if (marker != null && currentTimecode >= marker.timecode) { - trigger(marker, LATE); + public void add(TrackMarker marker, long currentTimecode) { + if (marker != null) { + if (currentTimecode >= marker.timecode) { + marker.handler.handle(LATE); + } else { + markerList.add(marker); + } } } + public void remove(TrackMarker marker) { + trigger(marker, REMOVED); + } + /** - * Remove the current marker. + * Removes the first marker in the list. + * + * @return The removed marker. Null if there are no markers. * - * @return The removed marker. + * @deprecated Use {@link #getMarkers()} and {@link #clear()} instead. */ + @Deprecated public TrackMarker remove() { - return current.getAndSet(null); + if (markerList.isEmpty()) { + return null; + } + + return markerList.remove(0); + } + + /** + * @return The current unmodifiable list of timecode markers stored in this tracker. + * @see #add(TrackMarker, long) + * @see #remove(TrackMarker) + * @see #clear() + */ + public List getMarkers() { + return Collections.unmodifiableList(markerList); + } + + public void clear() { + markerList.clear(); } /** - * Trigger and remove the marker with the specified state. + * Triggers and removes all markers with the specified state. * * @param state The state of the marker to pass to the handler. */ public void trigger(TrackMarkerHandler.MarkerState state) { - TrackMarker marker = current.getAndSet(null); - - if (marker != null) { + for (TrackMarker marker : markerList) { marker.handler.handle(state); } + + this.clear(); } /** @@ -56,10 +91,10 @@ public void trigger(TrackMarkerHandler.MarkerState state) { * @param timecode Timecode which was reached by normal playback. */ public void checkPlaybackTimecode(long timecode) { - TrackMarker marker = current.get(); - - if (marker != null && timecode >= marker.timecode) { - trigger(marker, REACHED); + for (TrackMarker marker : markerList) { + if (marker != null && timecode >= marker.timecode) { + trigger(marker, REACHED); + } } } @@ -69,15 +104,15 @@ public void checkPlaybackTimecode(long timecode) { * @param timecode Timecode which was reached by seeking. */ public void checkSeekTimecode(long timecode) { - TrackMarker marker = current.get(); - - if (marker != null && timecode >= marker.timecode) { - trigger(marker, BYPASSED); + for (TrackMarker marker : markerList) { + if (marker != null && timecode >= marker.timecode) { + trigger(marker, BYPASSED); + } } } private void trigger(TrackMarker marker, TrackMarkerHandler.MarkerState state) { - if (current.compareAndSet(marker, null)) { + if (markerList.remove(marker)) { marker.handler.handle(state); } } diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java index 7ecb1bcd..e88a255e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/AudioTrackExecutor.java @@ -43,12 +43,27 @@ public interface AudioTrackExecutor extends AudioFrameProvider { AudioTrackState getState(); /** - * Set track position marker. + * Set the track position marker. This will clear all existing markers. * * @param marker Track position marker to set. */ void setMarker(TrackMarker marker); + /** + * Adds a marker to the track. + * Markers can be used to execute code when the track reaches a certain position. + * + * @param marker The marker to add. + */ + void addMarker(TrackMarker marker); + + /** + * Removes a marker from the track. + * + * @param marker The marker to remove. + */ + void removeMarker(TrackMarker marker); + /** * @return True if this track threw an exception before it provided any audio. */ diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java index da19e580..980f7f0e 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/LocalAudioTrackExecutor.java @@ -238,6 +238,16 @@ public void setMarker(TrackMarker marker) { markerTracker.set(marker, getPosition()); } + @Override + public void addMarker(TrackMarker marker) { + markerTracker.add(marker, getPosition()); + } + + @Override + public void removeMarker(TrackMarker marker) { + markerTracker.remove(marker); + } + @Override public boolean failedBeforeLoad() { return trackException != null && !frameBuffer.hasReceivedFrames(); diff --git a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java index d19d7da0..bb8fc657 100644 --- a/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java +++ b/main/src/main/java/com/sedmelluq/discord/lavaplayer/track/playback/PrimordialAudioTrackExecutor.java @@ -63,6 +63,16 @@ public void setMarker(TrackMarker marker) { markerTracker.set(marker, position); } + @Override + public void addMarker(TrackMarker marker) { + markerTracker.add(marker, getPosition()); + } + + @Override + public void removeMarker(TrackMarker marker) { + markerTracker.remove(marker); + } + @Override public boolean failedBeforeLoad() { return false; @@ -100,6 +110,10 @@ public void applyStateToExecutor(AudioTrackExecutor executor) { executor.setPosition(position); } - executor.setMarker(markerTracker.remove()); + for (TrackMarker marker : markerTracker.getMarkers()) { + executor.addMarker(marker); + } + + markerTracker.clear(); } } diff --git a/settings.gradle.kts b/settings.gradle.kts index aa678dad..e9770fab 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,7 +8,8 @@ include( ":extensions:youtube-rotator", ":extensions:format-xm", ":natives", - ":natives-publish" + ":natives-publish", + ":testbot" ) // /~https://github.com/gradle/gradle/issues/19254 diff --git a/testbot/build.gradle.kts b/testbot/build.gradle.kts new file mode 100644 index 00000000..b7cc4956 --- /dev/null +++ b/testbot/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + java + application +} + +dependencies { + implementation(projects.main) + implementation(libs.base64) + implementation(libs.slf4j) + runtimeOnly(libs.logback.classic) +} + +application { + mainClass.set("com.sedmelluq.discord.lavaplayer.demo.LocalPlayerDemo") +} diff --git a/testbot/src/main/java/com/sedmelluq/discord/lavaplayer/demo/LocalPlayerDemo.java b/testbot/src/main/java/com/sedmelluq/discord/lavaplayer/demo/LocalPlayerDemo.java new file mode 100644 index 00000000..a2b50ab6 --- /dev/null +++ b/testbot/src/main/java/com/sedmelluq/discord/lavaplayer/demo/LocalPlayerDemo.java @@ -0,0 +1,79 @@ +package com.sedmelluq.discord.lavaplayer.demo; + +import com.sedmelluq.discord.lavaplayer.format.AudioDataFormat; +import com.sedmelluq.discord.lavaplayer.format.AudioPlayerInputStream; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.FunctionalResultHandler; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; +import com.sedmelluq.discord.lavaplayer.track.TrackMarker; +import com.sedmelluq.discord.lavaplayer.track.TrackMarkerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.LineUnavailableException; +import javax.sound.sampled.SourceDataLine; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats.COMMON_PCM_S16_BE; + +public class LocalPlayerDemo { + private static final Logger log = LoggerFactory.getLogger(LocalPlayerDemo.class); + + private static final long CROSSFADE_BEGIN = TimeUnit.SECONDS.toMillis(5); + private static final long CROSSFADE_PRELOAD = CROSSFADE_BEGIN + TimeUnit.SECONDS.toMillis(3); + + public static void main(String[] args) throws LineUnavailableException, IOException { + AudioPlayerManager manager = new DefaultAudioPlayerManager(); + AudioSourceManagers.registerRemoteSources(manager); + manager.getConfiguration().setOutputFormat(COMMON_PCM_S16_BE); + + AudioPlayer player = manager.createPlayer(); + + manager.loadItem("ytsearch: zmvgDMe5Wxo", new FunctionalResultHandler(null, playlist -> { + AudioTrack audioTrack = playlist.getTracks().get(0); + + applyMakers(audioTrack); + + player.playTrack(audioTrack); + }, null, null)); + + AudioDataFormat format = manager.getConfiguration().getOutputFormat(); + AudioInputStream stream = AudioPlayerInputStream.createStream(player, format, 10000L, false); + SourceDataLine.Info info = new DataLine.Info(SourceDataLine.class, stream.getFormat()); + SourceDataLine line = (SourceDataLine) AudioSystem.getLine(info); + + line.open(stream.getFormat()); + line.start(); + + byte[] buffer = new byte[COMMON_PCM_S16_BE.maximumChunkSize()]; + int chunkSize; + + while ((chunkSize = stream.read(buffer)) >= 0) { + line.write(buffer, 0, chunkSize); + } + } + + private static void applyMakers(AudioTrack track) { + final TrackMarkerHandler xfadeLoadHandler = (TrackMarkerHandler.MarkerState state) -> { + if (state == TrackMarkerHandler.MarkerState.REACHED) { + log.info("Fade begin handler has been reached"); + } + }; + + final TrackMarkerHandler xfadeBufferHandler = (TrackMarkerHandler.MarkerState state) -> { + if (state == TrackMarkerHandler.MarkerState.REACHED) { + log.info("Buffer begin handler has been reached"); + } + }; + + track.addMarker(new TrackMarker(track.getDuration() - CROSSFADE_BEGIN, xfadeLoadHandler)); + track.addMarker(new TrackMarker(track.getDuration() - CROSSFADE_PRELOAD, xfadeBufferHandler)); + } +}