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.stage.Stage;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@@ -45,6 +49,10 @@ public final class PlayerApp extends Application {
|
||||
private volatile MoviePlayer moviePlayer;
|
||||
private Stage stage;
|
||||
|
||||
/** Master A/V clock anchor, set by the audio thread. */
|
||||
private volatile long clockStartPts = -1;
|
||||
private volatile long clockStartNanos = -1;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
// 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));
|
||||
PlaybackEngine engine = PlaybackEngine.open(iso, streamName, 0)) {
|
||||
|
||||
long basePts = -1;
|
||||
long baseNanos = -1;
|
||||
startAudio(engine);
|
||||
awaitClock();
|
||||
long basePts = clockStartPts;
|
||||
long baseNanos = clockStartNanos;
|
||||
|
||||
PlaybackEngine.VideoFrame frame;
|
||||
while ((frame = engine.nextVideoFrame()) != null) {
|
||||
@@ -175,17 +185,8 @@ public final class PlayerApp extends Application {
|
||||
frame.vPlane(), frame.vStride());
|
||||
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 sleepNanos = targetNanos - System.nanoTime();
|
||||
if (sleepNanos > 0) {
|
||||
long millis = sleepNanos / 1_000_000;
|
||||
int nanos = (int) (sleepNanos % 1_000_000);
|
||||
Thread.sleep(millis, nanos);
|
||||
}
|
||||
sleepUntil(targetNanos);
|
||||
}
|
||||
|
||||
MoviePlayer mp = moviePlayer;
|
||||
@@ -200,6 +201,67 @@ public final class PlayerApp extends Application {
|
||||
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) {
|
||||
WritableImage image = new WritableImage(width, height);
|
||||
image.getPixelWriter().setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), argb, 0, width);
|
||||
|
||||
Reference in New Issue
Block a user