mirror of
https://github.com/Coloryr/AllMusic.git
synced 2026-11-08 18:54:44 +00:00
flac
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
+14
@@ -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.
|
||||
|
||||
+189
-5
@@ -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, ...);
|
||||
*
|
||||
* // 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -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).
|
||||
|
||||
+23
-8
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user