Play ATRAC3+ audio through the system audio line
The audio thread writes decoded PCM to a SourceDataLine and acts as the master A/V clock; video frames are now paced against that shared clock so audio and video stay in sync.
This commit is contained in:
@@ -29,6 +29,10 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioFormat;
|
||||||
|
import javax.sound.sampled.AudioSystem;
|
||||||
|
import javax.sound.sampled.SourceDataLine;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
@@ -45,6 +49,10 @@ public final class PlayerApp extends Application {
|
|||||||
private volatile MoviePlayer moviePlayer;
|
private volatile MoviePlayer moviePlayer;
|
||||||
private Stage stage;
|
private Stage stage;
|
||||||
|
|
||||||
|
/** Master A/V clock anchor, set by the audio thread. */
|
||||||
|
private volatile long clockStartPts = -1;
|
||||||
|
private volatile long clockStartNanos = -1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) {
|
public void start(Stage stage) {
|
||||||
// Gradle's --args splits on whitespace; rejoin so paths with spaces work.
|
// Gradle's --args splits on whitespace; rejoin so paths with spaces work.
|
||||||
@@ -164,8 +172,10 @@ public final class PlayerApp extends Application {
|
|||||||
try (UmdIsoReader iso = new UmdIsoReader(Path.of(isoPath));
|
try (UmdIsoReader iso = new UmdIsoReader(Path.of(isoPath));
|
||||||
PlaybackEngine engine = PlaybackEngine.open(iso, streamName, 0)) {
|
PlaybackEngine engine = PlaybackEngine.open(iso, streamName, 0)) {
|
||||||
|
|
||||||
long basePts = -1;
|
startAudio(engine);
|
||||||
long baseNanos = -1;
|
awaitClock();
|
||||||
|
long basePts = clockStartPts;
|
||||||
|
long baseNanos = clockStartNanos;
|
||||||
|
|
||||||
PlaybackEngine.VideoFrame frame;
|
PlaybackEngine.VideoFrame frame;
|
||||||
while ((frame = engine.nextVideoFrame()) != null) {
|
while ((frame = engine.nextVideoFrame()) != null) {
|
||||||
@@ -175,17 +185,8 @@ public final class PlayerApp extends Application {
|
|||||||
frame.vPlane(), frame.vStride());
|
frame.vPlane(), frame.vStride());
|
||||||
blitVideo(argb, frame.width(), frame.height());
|
blitVideo(argb, frame.width(), frame.height());
|
||||||
|
|
||||||
if (basePts < 0) {
|
|
||||||
basePts = frame.pts();
|
|
||||||
baseNanos = System.nanoTime();
|
|
||||||
}
|
|
||||||
long targetNanos = baseNanos + (frame.pts() - basePts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP;
|
long targetNanos = baseNanos + (frame.pts() - basePts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP;
|
||||||
long sleepNanos = targetNanos - System.nanoTime();
|
sleepUntil(targetNanos);
|
||||||
if (sleepNanos > 0) {
|
|
||||||
long millis = sleepNanos / 1_000_000;
|
|
||||||
int nanos = (int) (sleepNanos % 1_000_000);
|
|
||||||
Thread.sleep(millis, nanos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MoviePlayer mp = moviePlayer;
|
MoviePlayer mp = moviePlayer;
|
||||||
@@ -200,6 +201,67 @@ public final class PlayerApp extends Application {
|
|||||||
player.start();
|
player.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Plays decoded PCM through the system audio line, acting as the master A/V clock. */
|
||||||
|
private void startAudio(PlaybackEngine engine) {
|
||||||
|
Thread audio = new Thread(() -> {
|
||||||
|
SourceDataLine line = null;
|
||||||
|
try {
|
||||||
|
line = openLine();
|
||||||
|
PlaybackEngine.AudioChunk chunk;
|
||||||
|
while ((chunk = engine.nextAudioChunk()) != null) {
|
||||||
|
if (clockStartPts < 0) {
|
||||||
|
clockStartPts = chunk.pts();
|
||||||
|
clockStartNanos = System.nanoTime();
|
||||||
|
}
|
||||||
|
long targetNanos = clockStartNanos
|
||||||
|
+ (chunk.pts() - clockStartPts) * 1_000_000_000L / PlaybackEngine.MPEG_TIMESTAMP;
|
||||||
|
sleepUntil(targetNanos);
|
||||||
|
line.write(chunk.pcm(), 0, chunk.pcm().length);
|
||||||
|
}
|
||||||
|
line.drain();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (line != null) {
|
||||||
|
line.close();
|
||||||
|
}
|
||||||
|
if (clockStartPts < 0) {
|
||||||
|
clockStartPts = 0;
|
||||||
|
clockStartNanos = System.nanoTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, "audio-player");
|
||||||
|
audio.setDaemon(true);
|
||||||
|
audio.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SourceDataLine openLine() throws Exception {
|
||||||
|
AudioFormat format = new AudioFormat(PlaybackEngine.AUDIO_SAMPLE_RATE, 16, 2, true, false);
|
||||||
|
SourceDataLine line = AudioSystem.getSourceDataLine(format);
|
||||||
|
line.open(format, PlaybackEngine.AUDIO_SAMPLE_RATE * 4);
|
||||||
|
line.start();
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Waits until the audio thread anchors the master clock, or falls back. */
|
||||||
|
private void awaitClock() throws InterruptedException {
|
||||||
|
long deadline = System.nanoTime() + 5_000_000_000L;
|
||||||
|
while (clockStartPts < 0 && System.nanoTime() < deadline) {
|
||||||
|
Thread.sleep(2);
|
||||||
|
}
|
||||||
|
if (clockStartPts < 0) {
|
||||||
|
clockStartPts = 0;
|
||||||
|
clockStartNanos = System.nanoTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sleepUntil(long targetNanos) throws InterruptedException {
|
||||||
|
long sleepNanos = targetNanos - System.nanoTime();
|
||||||
|
if (sleepNanos > 0) {
|
||||||
|
Thread.sleep(sleepNanos / 1_000_000, (int) (sleepNanos % 1_000_000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void blitVideo(int[] argb, int width, int height) {
|
private void blitVideo(int[] argb, int width, int height) {
|
||||||
WritableImage image = new WritableImage(width, height);
|
WritableImage image = new WritableImage(width, height);
|
||||||
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), argb, 0, width);
|
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), argb, 0, width);
|
||||||
|
|||||||
Reference in New Issue
Block a user