This commit is contained in:
Coloryr committed 2026-10-19 21:29:35 +08:00
1 parent 0844327474
commit 6fc22ab542
5 files changed
+268 -13

No files matched your search

@@ -3,11 +3,13 @@ package com.coloryr.allmusic.client.core;
import com.coloryr.allmusic.client.core.objs.PlayTaskObj;
import com.coloryr.allmusic.client.core.player.decoder.BuffPack;
import com.coloryr.allmusic.client.core.player.decoder.IDecoder;
import com.coloryr.allmusic.client.core.player.decoder.flac.FlacDecoder;
import com.coloryr.allmusic.client.core.player.decoder.m4a.M4ADecoder;
import com.coloryr.allmusic.client.core.player.decoder.mp3.Mp3Decoder;
import com.coloryr.allmusic.client.core.player.decoder.ogg.OggDecoder;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.io.CloseMode;
import org.lwjgl.BufferUtils;
@@ -41,6 +43,7 @@ public class AllMusicPlayer extends InputStream {
private int index = -1;
private final IntBuffer source;
private long local;
private long contentLength = -1;
private boolean isRun;
private boolean isChat;
private int chatCount = 0;
@@ -85,6 +88,7 @@ public class AllMusicPlayer extends InputStream {
public void connect() throws IOException {
streamClose();
contentLength = -1;
HttpGet request = new HttpGet(nowTask.url);
request.setHeader("Range", "bytes=" + local + "-");
response = AllMusicCore.client.execute(request);
@@ -96,6 +100,20 @@ public class AllMusicPlayer extends InputStream {
if (entity == null) {
throw new IOException("Response entity is null");
}
Header contentRange = response.getFirstHeader("Content-Range");
if (contentRange != null) {
String value = contentRange.getValue();
int slash = value.lastIndexOf('/');
if (slash >= 0 && slash + 1 < value.length()) {
try {
contentLength = Long.parseLong(value.substring(slash + 1).trim());
} catch (NumberFormatException ignored) {
}
}
}
if (contentLength < 0 && local == 0) {
contentLength = entity.getContentLength();
}
content = new BufferedInputStream(entity.getContent());
}
@@ -205,6 +223,8 @@ public class AllMusicPlayer extends InputStream {
|| (head[0] == (byte) 0xFF && head[1] == (byte) 0xE3)
|| (head[0] == (byte) 0xFF && head[1] == (byte) 0xE2)) {
decoder = new Mp3Decoder(this);
} else if (head[0] == 'f' && head[1] == 'L' && head[2] == 'a' && head[3] == 'C') {
decoder = new FlacDecoder(this);
} else {
decoder = new OggDecoder(this);
}
@@ -432,4 +452,13 @@ public class AllMusicPlayer extends InputStream {
this.local = local;
connect();
}
/**
* Returns the total number of bytes of the resource currently being played, or -1 if the
* server did not report it. This is used to estimate byte positions when seeking in formats
* that do not carry their own seek table.
*/
public long getContentLength() {
return contentLength;
}
}
@@ -117,6 +117,20 @@ public abstract class AbstractFlacLowLevelInput implements FlacLowLevelInput {
resetCrcs();
}
// Seeks to the given byte position if it is still contained in the current byte buffer.
// This never touches the underlying stream, so it is cheap, and it allows a few bytes to be
// re-read (for example after probing a frame header). Returns true on success, or false if
// the position is not buffered anymore and a full seek is required.
public boolean seekWithinBuffer(long pos) {
if (byteBufferLen <= 0 || pos < byteBufferStartPos || pos >= byteBufferStartPos + byteBufferLen)
return false;
byteBufferIndex = (int) (pos - byteBufferStartPos);
bitBuffer = 0; // The bits are read from the byte buffer again below
bitBufferLen = 0;
resetCrcs();
return true;
}
/*-- CRC calculations --*/
// Either returns silently or throws an exception.
@@ -17,11 +17,11 @@
package com.coloryr.allmusic.client.core.player.decoder.flac;
import com.coloryr.allmusic.client.core.AllMusicCore;
import com.coloryr.allmusic.client.core.AllMusicPlayer;
import com.coloryr.allmusic.client.core.player.decoder.BuffPack;
import com.coloryr.allmusic.client.core.player.decoder.IDecoder;
import java.io.EOFException;
import java.io.IOException;
/**
@@ -44,7 +44,7 @@ import java.io.IOException;
* dec.readAudioBlock(samples, ...);
*
* &#x2F;/ Seek to some position and continue reading
* dec.seekAndReadAudioBlock(..., samples, ...);
* dec.set(60000);
* dec.readAudioBlock(samples, ...);
* dec.readAudioBlock(samples, ...);
*
@@ -57,6 +57,10 @@ import java.io.IOException;
*/
public final class FlacDecoder implements AutoCloseable, IDecoder {
// The maximum number of times the byte position is re-estimated while seeking in a stream
// that does not contain a seek table.
private static final int MAX_SEEK_ATTEMPTS = 8;
private final AllMusicPlayer player;
/*---- Fields ----*/
private final BuffPack pack = new BuffPack();
@@ -72,6 +76,12 @@ public final class FlacDecoder implements AutoCloseable, IDecoder {
private int[][] samples;
private byte[] sampleBytes;
// Samples that were decoded but not handed out yet. This happens after a seek when the
// requested sample lies in the middle of a block: [decodedOffset, decodedOffset +
// decodedLength) of every channel still has to be returned by decodeFrame().
private int decodedOffset;
private int decodedLength;
// Constructs a new FLAC decoder to read the given file.
// This immediately reads the basic header but not metadata blocks.
public FlacDecoder(AllMusicPlayer player) {
@@ -129,11 +139,16 @@ public final class FlacDecoder implements AutoCloseable, IDecoder {
@Override
public BuffPack decodeFrame() throws Exception {
int blockSamples = readAudioBlock(samples, 0);
int blockSamples = decodedLength;
if (blockSamples == 0) {
blockSamples = readAudioBlock(samples, 0);
decodedOffset = 0;
}
decodedLength = 0;
int sampleBytesLen = 0;
for (int i = 0; i < blockSamples; i++) {
for (int ch = 0; ch < streamInfo.numChannels; ch++) {
int val = samples[ch][i];
int val = samples[ch][decodedOffset + i];
if (streamInfo.sampleDepth == 24) {
float temp = val / 16777216f;
val = (int) (temp * 0x7FFF);
@@ -159,6 +174,8 @@ public final class FlacDecoder implements AutoCloseable, IDecoder {
input.close();
input = null;
}
decodedOffset = 0;
decodedLength = 0;
}
@Override
@@ -178,6 +195,8 @@ public final class FlacDecoder implements AutoCloseable, IDecoder {
int bytesPerSample = streamInfo.sampleDepth / 8;
samples = new int[streamInfo.numChannels][65536];
sampleBytes = new byte[65536 * streamInfo.numChannels * bytesPerSample];
decodedOffset = 0;
decodedLength = 0;
return true;
}
@@ -194,6 +213,171 @@ public final class FlacDecoder implements AutoCloseable, IDecoder {
@Override
public void set(int time) {
AllMusicCore.bridge.sendMessage("不支持中间播放");
if (input == null || streamInfo == null || frameDec == null) {
return;
}
try {
long sample = (long) time * streamInfo.sampleRate / 1000L;
if (sample < 0) sample = 0;
if (streamInfo.numSamples > 0 && sample >= streamInfo.numSamples) {
sample = streamInfo.numSamples - 1;
}
seekToSample(sample);
} catch (Exception e) {
e.printStackTrace();
}
}
/*---- Seeking ----*/
// Seeks to the given sample index (counted from the beginning of the audio stream) and
// positions the decoder so that the next decoded block starts at that sample. The stream is
// moved with a seek (a range request), so the data before the target is not read or decoded.
private void seekToSample(long target) throws IOException {
decodedOffset = 0;
decodedLength = 0;
// A seek table maps sample indices to byte positions, so it allows jumping directly to
// the closest known frame before the target and decoding only the frames in between.
SeekTable.SeekPoint point = findSeekPoint(target);
if (point != null) {
input.seekTo(metadataEndPos + point.fileOffset);
skipSamples(target - point.sampleOffset);
return;
}
long length = input.getLength();
if (streamInfo.numSamples <= 0 || length <= metadataEndPos) {
// The total length of the stream is unknown, so no byte position can be estimated;
// fall back to decoding from the beginning of the audio data.
input.seekTo(metadataEndPos);
skipSamples(target);
return;
}
// There is no seek table: estimate the byte position from the average bitrate. Scanning
// forward from a position always finds the frame after it, so the position is moved back
// by a safety margin to make sure that the frame before the target (or an even earlier
// one) is found. If the bitrate is not constant the margin is doubled until it is enough.
long pos = estimatePosition(target, streamInfo.numSamples, metadataEndPos, length);
long margin = Math.max(65536L, streamInfo.maxFrameSize * 4L);
for (int i = 0; i < MAX_SEEK_ATTEMPTS; i++) {
pos = Math.max(metadataEndPos, pos - margin);
input.seekTo(pos);
long frameSample = scanToNextFrame();
if (frameSample < 0) {
break; // No frame was found, the estimate was too close to the end of the stream
}
if (frameSample <= target) {
skipSamples(target - frameSample);
return;
}
// The frame found is still after the target: back off further and try again.
margin = Math.min(margin * 2, Math.max(1L, pos - metadataEndPos));
}
// Decoding from the beginning is always correct, it is only used as a last resort.
input.seekTo(metadataEndPos);
skipSamples(target);
}
// Returns the seek point with the greatest sample offset that is not after the given sample,
// or null if the stream does not contain a usable seek table.
private SeekTable.SeekPoint findSeekPoint(long sample) {
if (seekTable == null || seekTable.points == null) return null;
SeekTable.SeekPoint result = null;
for (SeekTable.SeekPoint point : seekTable.points) {
if (point == null || point.sampleOffset < 0) continue; // Placeholder point
if (point.sampleOffset > sample) continue;
if (result == null || point.sampleOffset > result.sampleOffset) result = point;
}
return result;
}
// Returns the byte position that corresponds to the given sample index, assuming that the
// samples are spread evenly over the byte range [start, end). Pure function.
private static long estimatePosition(long sample, long totalSamples, long start, long end) {
if (sample <= 0 || totalSamples <= 0 || end <= start) return start;
long pos = start + (long) ((double) sample / totalSamples * (end - start));
if (pos < start) return start;
if (pos >= end) return end - 1;
return pos;
}
// Scans forward from the current byte position for the next valid FLAC frame header, leaves
// the stream at the beginning of that frame, and returns its sample index, or -1 if the end
// of the stream was reached. This is used to seek in streams without a seek table.
private long scanToNextFrame() throws IOException {
long pos = input.getPosition();
int previous = -1;
while (true) {
int current;
try {
current = input.readByte();
} catch (EOFException e) {
return -1;
}
if (current == -1) return -1;
// The sync code is 14 one-bits followed by a zero reserved bit, so the first two
// bytes of a frame are 0xFF and 0xF8 or 0xF9.
if (previous == 0xFF && (current & 0xFE) == 0xF8) {
long start = pos - 1;
if (!input.seekWithinBuffer(start)) input.seekTo(start);
long index = -1;
try {
FrameInfo info = FrameInfo.readFrame(input);
if (info == null) return -1; // Clean end of stream
index = getSampleIndex(info);
} catch (DataFormatException e) {
// A false sync code inside a frame; keep searching
} catch (EOFException e) {
return -1;
}
if (index >= 0) {
// Rewind so that the frame can be decoded from its very first byte
if (!input.seekWithinBuffer(start)) input.seekTo(start);
return index;
}
// Continue scanning right after the sync code that turned out to be invalid
if (!input.seekWithinBuffer(start + 1)) input.seekTo(start + 1);
pos = start + 1;
previous = -1;
continue;
}
previous = current;
pos++;
}
}
// Returns the absolute sample index of the given frame header, or -1 if the header does not
// carry one (which the FLAC format guarantees, so this should not happen).
private long getSampleIndex(FrameInfo info) {
if (info.sampleOffset >= 0) return info.sampleOffset;
if (info.frameIndex >= 0) return (long) info.frameIndex * streamInfo.maxBlockSize;
return -1;
}
// Decodes and discards audio blocks until the given number of samples has been skipped. The
// block that contains the target is kept for the following decodeFrame() calls, so playback
// continues exactly at the requested sample.
private void skipSamples(long skip) throws IOException {
decodedOffset = 0;
decodedLength = 0;
while (skip > 0) {
int blockSamples = readAudioBlock(samples, 0);
if (blockSamples == 0) return; // End of stream
if (skip >= blockSamples) {
skip -= blockSamples;
} else {
decodedOffset = (int) skip;
decodedLength = blockSamples - (int) skip;
return;
}
}
}
}
@@ -47,6 +47,19 @@ public interface FlacLowLevelInput extends AutoCloseable {
// increments for each bit consumed, maxes out at 7, then resets to 0 and repeats.
public int getBitPosition();
/*---- Seeking ----*/
// Seeks to the given byte position in the stream, a non-negative value. After this call,
// getPosition() returns pos, getBitPosition() returns 0, and the CRC calculations are reset.
// This method may throw UnsupportedOperationException if this stream cannot seek.
public void seekTo(long pos) throws IOException;
// Tries to seek to the given byte position using only data that is still held in memory,
// without touching the underlying stream. Returns true if the position was reached, or false
// if the position is not buffered anymore (in which case seekTo() has to be used).
// The stream must be at a byte boundary (i.e. getBitPosition() == 0).
public boolean seekWithinBuffer(long pos);
/*---- Reading bitwise integers ----*/
// Reads the next given number of bits (0 <= n <= 32) as an unsigned integer (i.e. zero-extended to int32).
@@ -21,15 +21,22 @@ import com.coloryr.allmusic.client.core.AllMusicPlayer;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
/**
* A FLAC input stream based on a {@link RandomAccessFile}.
* A FLAC input stream backed by the stream of an {@link AllMusicPlayer}.
* <p>
* The player streams the music over HTTP and can re-open its stream at any byte position
* (by issuing a range request), which is what {@link #seekTo(long)} uses. Whenever the
* stream is re-opened, all the data that was buffered for the previous position is dropped.
* </p>
*/
public final class SeekableFileFlacInput extends AbstractFlacLowLevelInput {
/*---- Fields ----*/
// The player that provides the stream; also knows the total length and can seek it.
private final AllMusicPlayer player;
// The underlying byte-based input stream to read from.
private BufferedInputStream raf;
@@ -37,24 +44,32 @@ public final class SeekableFileFlacInput extends AbstractFlacLowLevelInput {
public SeekableFileFlacInput(AllMusicPlayer player) {
super();
this.player = player;
this.raf = new BufferedInputStream(player);
}
/*---- Methods ----*/
public long getLength() {
try {
return raf.available();
} catch (IOException e) {
throw new RuntimeException(e);
}
// The total length of the resource; -1 if the server did not tell us.
return player.getContentLength();
}
protected int readUnderlying(byte[] buf, int off, int len) throws IOException {
return raf.read(buf, off, len);
}
// Closes the underlying RandomAccessFile stream (very important).
// Re-opens the underlying stream at the given byte position and discards all the data
// that was buffered for the previous position. This is cheap compared to reading (and
// decoding) everything in between the old and the new position.
public void seekTo(long pos) throws IOException {
if (pos < 0) throw new IllegalArgumentException("Negative seek position");
player.setLocal(pos);
raf = new BufferedInputStream(player);
positionChanged(pos);
}
// Closes the underlying stream (very important).
public void close() throws IOException {
if (raf != null) {
raf.close();