// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.video; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Vector; import javax.sound.sampled.AudioFormat; import org.infinity.resource.key.ResourceEntry; import org.infinity.util.io.StreamUtils; /** * Decodes a MVE video resource. */ public class MveDecoder { // MVE header signature public static final String MVE_SIGNATURE = "Interplay MVE File\u001a\u0000\u001a\u0000\u0000\u0001\u0033\u0011"; // supported MVE chunks public static final int MVE_CHUNK_NONE = -1; // used internally public static final int MVE_CHUNK_INIT_AUDIO = 0x0000; public static final int MVE_CHUNK_AUDIO_ONLY = 0x0001; public static final int MVE_CHUNK_INIT_VIDEO = 0x0002; public static final int MVE_CHUNK_VIDEO = 0x0003; public static final int MVE_CHUNK_SHUTDOWN = 0x0004; public static final int MVE_CHUNK_END = 0x0005; // supported MVE segments public static final int MVE_OC_END_OF_STREAM = 0x0000; public static final int MVE_OC_END_OF_CHUNK = 0x0001; public static final int MVE_OC_CREATE_TIMER = 0x0002; public static final int MVE_OC_AUDIO_BUFFERS = 0x0003; public static final int MVE_OC_PLAY_AUDIO = 0x0004; public static final int MVE_OC_VIDEO_BUFFERS = 0x0005; public static final int MVE_OC_UNKNOWN_06 = 0x0006; public static final int MVE_OC_PLAY_VIDEO = 0x0007; public static final int MVE_OC_AUDIO_DATA = 0x0008; public static final int MVE_OC_AUDIO_SILENCE = 0x0009; public static final int MVE_OC_VIDEO_MODE = 0x000a; public static final int MVE_OC_CREATE_GRADIENT = 0x000b; public static final int MVE_OC_PALETTE = 0x000c; public static final int MVE_OC_PALETTE_PACKED = 0x000d; public static final int MVE_OC_UNKNOWN_0E = 0x000e; public static final int MVE_OC_CODE_MAP = 0x000f; public static final int MVE_OC_UNKNOWN_10 = 0x0010; public static final int MVE_OC_VIDEO_DATA = 0x0011; public static final int MVE_OC_UNKNOWN_12 = 0x0012; public static final int MVE_OC_UNKNOWN_13 = 0x0013; public static final int MVE_OC_UNKNOWN_14 = 0x0014; public static final int MVE_OC_UNKNOWN_15 = 0x0015; // audio specific flags public static final int MVE_AUDIO_STEREO = 0x0001; public static final int MVE_AUDIO_16BIT = 0x0002; public static final int MVE_AUDIO_COMPRESSED = 0x0004; // video specific flags public static final int MVE_VIDEO_DELTA = 0x0001; private final MveInfo info; // contains all required information to decode the MVE resource private boolean initialized; /** * Creates an empty (closed) MveDecoder object. No data processing is possible until the * {@code open()} method has been called successfully. */ public MveDecoder() { initialized = false; info = new MveInfo(); } /** * Creates and initializes a new MveDecoder object of the specified resource entry. * @param entry The MVE resource. * @throws Exception */ public MveDecoder(ResourceEntry entry) throws Exception { if (entry == null) throw new NullPointerException(); initialized = false; info = new MveInfo(); open(entry.getResourceDataAsStream()); } /** * Creates and initializes a new MveDecoder object of the specified MVE data block. * @param mveData The MVE input data block. * @param mveOfs Start offset in the MVE data block. * @param mveLen Size of the MVE data block in bytes. * @throws Exception */ public MveDecoder(byte[] mveData, int mveOfs, int mveLen) throws Exception { if (mveData == null) throw new NullPointerException(); if (mveOfs < 0 || mveLen < 0 || mveOfs + mveLen > mveData.length) throw new Exception("Invalid input data offset or size specified"); initialized = false; info = new MveInfo(); open(new ByteArrayInputStream(mveData, mveOfs, mveLen)); } /** * Creates and initializes a new MveDecoder object of the specified MVE data stream. * @param mveIn The input stream containing MVE data. * @throws Exception */ public MveDecoder(InputStream mveIn) throws Exception { initialized = false; info = new MveInfo(); open(mveIn); } /** * Opens and initializes a new MVE resource. * @param entry The MVE resource. * @throws Exception */ public void open(ResourceEntry entry) throws Exception { if (entry == null) throw new NullPointerException(); open(entry.getResourceDataAsStream()); } /** * Opens and initializes a new MVE input data block. * @param mveData The MVE input data block. * @param mveOfs Start offset in the MVE data block. * @param mveLen Size of the MVE data block in bytes. * @throws Exception */ public void open(byte[] mveData, int mveOfs, int mveLen) throws Exception { if (mveData == null) throw new NullPointerException(); if (mveOfs < 0 || mveLen < 0 || mveOfs + mveLen > mveData.length) throw new Exception("Invalid input data offset or size specified"); open(new ByteArrayInputStream(mveData, mveOfs, mveLen)); } /** * Opens and initializes a new MVE data stream. * @param mveIn The input stream containing MVE data. * @throws Exception */ public void open(InputStream mveIn) throws Exception { close(); if (mveIn == null) throw new NullPointerException(); info.mveInput = mveIn; info.audioDecoder = MveAudioDecoder.createDecoder(info); info.videoDecoder = MveVideoDecoder.createDecoder(info); // 1. analyzing MVE header byte[] buf = new byte[MVE_SIGNATURE.length()]; if (info.mveInput.read(buf) < buf.length) throw new Exception("Unexpected end of file"); if (!Arrays.equals(MVE_SIGNATURE.getBytes(), buf)) throw new Exception("Invalid MVE signature found"); // 2. initializing MveChunk structure if (!info.chunk.init(info.mveInput)) throw new Exception("Error initializing MVE data stream"); // 3. processing initialization chunks while (info.chunk.getNextType() != MVE_CHUNK_NONE) { // looking for audio and video initialization chunks before processing any output chunk if (info.chunk.getNextType() == MVE_CHUNK_INIT_AUDIO || info.chunk.getNextType() == MVE_CHUNK_INIT_VIDEO) { if (info.chunk.loadChunk()) { if (!manageChunk(info.chunk)) { throw new Exception("Error processing chunk: " + String.format("0x%1$02x", info.chunk.getType())); } } else { throw new Exception("Unable to load next chunk"); } } else { break; } } initialized = (info.chunk.getNextType() != MVE_CHUNK_NONE); } /** * Closes a MVE data stream. Connected video and audio output objects are not affected. */ public void close() { initialized = false; info.close(); } /** * Returns whether a MVE resource is already attached to the decoder. * @return {@code true} if a MVE resource is already attached to the decoder. */ public boolean isOpen() { return initialized; } /** * Connects a video output object with the decoder. (Note: The decoder calls * {@code VideoBuffer.flip()} automatically after each successfully processed frame. * @param renderer The VideoBuffer object to render the decoded frames into. */ public void setVideoOutput(VideoBuffer videoOut) { info.videoOutput = videoOut; } /** * Returns the currently connected video output object. * @return The currently connected video output object, * or {@code null} if no video buffer is available. */ public VideoBuffer getVideoOutput() { return info.videoOutput; } /** * Connects an audio output queue with the default audio stream (stream 0). * The decoder will add new decoded audio blocks to the queue whenever a new frame * has been processed. The data can be accessed by the AudioQueue methods. * @param audioOut The AudioQueue object. */ public void setDefaultAudioOutput(AudioQueue audioOut) { info.audioOutput.set(0, audioOut); if (audioOut != null) { info.audioOutputMask |= 1; } else { info.audioOutputMask &= ~1; } } /** * Returns the currently connected audio output queue. * @return The currently connected AudioQueue object, or {@code null} if no * audio output has been defined yet. */ public AudioQueue getDefaultAudioOutput() { return info.audioOutput.get(0); } /** * Connects an audio output queue with the specified audio stream. Valid streams range from 0 to 15. * The decoder will add new decoded audio blocks to the queue whenever a new frame has been * processed. The data can be accessed by the AudioQueue methods. * @param index The audio stream to connect (range: 0..15). * @param audioOut The AudioQueue object. * @throws IndexOutOfBoundsException If index is out of range. */ public void setAudioOutput(int index, AudioQueue audioOut) throws IndexOutOfBoundsException { if (index < 0 || index > 15) throw new IndexOutOfBoundsException("Index out of bounds: " + index); info.audioOutput.set(index, audioOut); if (audioOut != null) { info.audioOutputMask |= (1 << index); } else { info.audioOutputMask &= ~(1 << index); } } /** * Returns the audio output queue of the specified stream. Valid streams range from 0 to 15. * @param index The audio stream to query (range: 0..15). * @return The connected AudioQueue object of the specified stream, or {@code null} if no * audio output had been defined yet for the specified stream. * @throws IndexOutOfBoundsException If index is out of range. */ public AudioQueue getAudioOutput(int index) throws IndexOutOfBoundsException { if (index < 0 || index > 15) throw new IndexOutOfBoundsException("Index out of bounds: " + index); return info.audioOutput.get(index); } /** * Returns the audio format detected in the MVE data stream. * @return The audio format detected in the MVE data stream. */ public AudioFormat getAudioFormat() { return info.audioFormat; } /** * Returns whether the MVE contains audio streams. * @return Whether the MVE contains audio streams. */ public boolean isAudioAvailable() { if (isOpen()) { return info.audioAvailable; } else { return false; } } /** * Returns the width of the MVE video stream in pixels. * @return The width of the MVE video stream in pixels. */ public int getVideoWidth() { if (isOpen()) { return info.width; } else { return 0; } } /** * Returns the height of the MVE video stream in pixels. * @return The height of the MVE video stream in pixels. */ public int getVideoHeight() { if (isOpen()) { return info.height; } else { return 0; } } /** * Returns the delay per frame of the video. * @return The delay per frame in microseconds. */ public int getFrameDelay() { if (isOpen()) { return info.frameDelay; } else { return 0; } } /** * If true, the frame delay does not change after each processed frame. * @return {@code true} if the frame delay is stable for the remaining frames, * {@code false} otherwise. */ public boolean isFrameDelayStable() { if (isOpen()) { return info.isFrameDelayStable; } else { return false; } } /** * Returns whether video has been initialized in the last processed frame. * @return {@code true} if video has been (re-)initialized in the last frame, * {@code false} otherwise. */ public boolean videoInitialized() { return info.videoInitialized; } /** * Returns whether audio has been initialized in the last processed frame. * @return {@code true} if audio has been (re-)initialized in the last frame, * {@code false} otherwise. */ public boolean audioInitialized() { return info.audioInitialized; } /** * Indicates whether the current frame contains video data. * @return {@code true} if the frame contains video data, {@code false} otherwise. */ public boolean frameHasVideo() { return (info.chunk.getType() == MVE_CHUNK_VIDEO); } /** * Indicates whether the current frame contains audio data. * @return {@code true} if the frame contains audio data, {@code false} otherwise. */ public boolean frameHasAudio() { return (info.chunk.getType() == MVE_CHUNK_VIDEO || info.chunk.getType() == MVE_CHUNK_AUDIO_ONLY); } /** * Determines whether the MVE data stream contains more frames. * @return {@code true} if there is at least one more frame is available, * {@code false} otherwise. */ public boolean hasNextFrame() { if (isOpen()) { switch (info.chunk.getNextType()) { case MVE_CHUNK_SHUTDOWN: case MVE_CHUNK_END: case MVE_CHUNK_NONE: return false; } return true; } else { return false; } } /** * Signals the decoder to process the MVE data stream until a new frame has been rendered. * The result can be accessed via the connected VideoBuffer (video) and AudioQueue(s) (audio). * @return {@code true} if a frame has been decoded successfully, {@code false} otherwise. * @throws Exception */ public boolean processNextFrame() throws Exception { while (hasNextFrame()) { if (!info.chunk.loadChunk()) throw new Exception("Error loading next chunk"); switch (info.chunk.getType()) { case MVE_CHUNK_AUDIO_ONLY: case MVE_CHUNK_VIDEO: if (!manageChunk(info.chunk)) throw new Exception("Error processing chunk"); return true; default: if (!manageChunk(info.chunk)) throw new Exception("Error processing chunk"); } } return false; } private boolean manageChunk(MveChunk chunk) throws Exception { if (chunk != null) { switch (chunk.getType()) { case MVE_CHUNK_INIT_AUDIO: case MVE_CHUNK_INIT_VIDEO: case MVE_CHUNK_AUDIO_ONLY: case MVE_CHUNK_VIDEO: case MVE_CHUNK_SHUTDOWN: case MVE_CHUNK_END: { while (chunk.hasNextSegment()) { if (!manageSegment(chunk, chunk.nextSegment())) { return false; } } return true; } default: throw new Exception("Invalid chunk type: " + String.format("0x%1$02x", chunk.getType())); } } return false; } private boolean manageSegment(MveChunk chunk, MveSegment segment) { if (segment != null) { try { switch (segment.getOpcode()) { case MVE_OC_END_OF_STREAM: // clean up stream specific data data info.audioDecoder.processAudio(segment); info.videoDecoder.processVideo(segment); //cleanUp(); return true; case MVE_OC_END_OF_CHUNK: // marks end of the current chunk switch (chunk.getType()) { case MVE_CHUNK_AUDIO_ONLY: return info.audioDecoder.processAudio(segment); case MVE_CHUNK_VIDEO: return info.videoDecoder.processVideo(segment); default: return true; } case MVE_OC_AUDIO_BUFFERS: // audio format initialization case MVE_OC_PLAY_AUDIO: // not needed by the decoder case MVE_OC_AUDIO_DATA: // decode a frame worth of audio data case MVE_OC_AUDIO_SILENCE: // generate a frame worth of silence return info.audioDecoder.processAudio(segment); case MVE_OC_CREATE_TIMER: // can be found in the first video chunk case MVE_OC_VIDEO_BUFFERS: // video format initialization case MVE_OC_PLAY_VIDEO: // outputs back buffer case MVE_OC_VIDEO_MODE: // not needed by the decoder case MVE_OC_CREATE_GRADIENT: // not used in IE games case MVE_OC_PALETTE: // initialize palette (indexed color mode only) case MVE_OC_PALETTE_PACKED: // modify palette (indexed color mode only) case MVE_OC_CODE_MAP: // provides code map for next video frame case MVE_OC_VIDEO_DATA: // decodes current code map into a video frame return info.videoDecoder.processVideo(segment); default: // processing unknown opcodes return true; } } catch (Exception e) { e.printStackTrace(); } } return false; } //----------------------------- INNER CLASSES ----------------------------- /** * Storage class for MVE related data. */ public static class MveInfo { // max. number of supported audio streams in MVEs public static final int AUDIOSTREAMS_MAX = 16; // The default audio format to use if no audio is defined in the MVE data stream private static final AudioFormat DefaultAudioFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 22050, 16, 2, 4, 22050, false); public final MveChunk chunk; // stores preprocessed data of the current chunk public final List<AudioQueue> audioOutput; // list of externally connected audio queues public InputStream mveInput; // the MVE data stream public VideoBuffer videoOutput; // externally connected video buffer public int width, height; // video width and height public boolean isPalette; // true if data uses palettes public int audioOutputMask; // bitmask tells which audio output has been set public AudioFormat audioFormat; // the audio format used by the current MVE public boolean audioCompressed; // indicates whether audio data is compressed public boolean audioAvailable; // is audio available in MVE public boolean isFrameDelayStable; // indicates whether a stable frame delay has been set (via opcode 0x02) public int frameDelay; // delay per frame in microseconds public int currentFrame; // index of the last processed video frame public boolean videoInitialized; // true if video has been (re-)initialized in the last frame public boolean audioInitialized; // true if audio has been (re-)initialized in the last frame private MveAudioDecoder audioDecoder; // audio decoder, used internally private MveVideoDecoder videoDecoder; // video decoder, used internally // initialize MVE related data private MveInfo() { chunk = new MveChunk(); mveInput = null; videoOutput = null; width = height = 0; isPalette = true; audioOutput = new Vector<AudioQueue>(AUDIOSTREAMS_MAX); for (int i = 0; i < AUDIOSTREAMS_MAX; i++) { audioOutput.add(null); } audioOutputMask = 0x0000; audioFormat = DefaultAudioFormat; audioCompressed = false; audioAvailable = false; isFrameDelayStable = false; frameDelay = 0; currentFrame = -1; videoDecoder = null; audioDecoder = null; } // called whenever a MVE data stream is closed private void close() { if (mveInput != null) { try { mveInput.close(); } catch (IOException e) { } mveInput = null; } if (audioDecoder != null) { audioDecoder.close(); audioDecoder = null; } if (videoDecoder != null) { videoDecoder.close(); videoDecoder = null; } audioFormat = DefaultAudioFormat; audioAvailable = false; width = height = 0; isPalette = false; isFrameDelayStable = false; frameDelay = 0; currentFrame = -1; } } /** * Stores preprocessed data of a single MVE chunk. */ public static class MveChunk { private boolean initialized; // indicates whether the object is in a valid state private InputStream in; // MVE data input stream private final List<MveSegment> segments; // list of chunk segments private int segmentIndex; // current segment to query private int curChunkSize, curChunkType; // current chunk size and type private int nextChunkSize, nextChunkType; // information about the next chunk to load // (MVE_CHUNK_NONE when no more chunks available) /** * Read chunk data from input stream. Input stream must already be positioned at the start * of a new chunk. */ private MveChunk() { initialized = false; segments = new ArrayList<MveSegment>(10); segmentIndex = 0; } /** * Must be called once to initialize a new MVE data stream. * <b>Note:</b> The MVE data stream must already be positioned right before a MVE chunk. * @param in The MVE data stream * @return {@code true} if initialization was successful, {@code false} otherwise. */ public boolean init(InputStream in) { if (in == null) throw new NullPointerException(); this.in = in; segments.clear(); segmentIndex = 0; curChunkSize = curChunkType = MVE_CHUNK_NONE; initialized = true; return peekNextChunk(); } /** * Loads and preprocesses the next available chunk. * @return {@code true} if another chunk is available, {@code false} otherwise. */ public boolean loadChunk() { if (initialized) { return initSegments(); } else return false; } /** * Returns the size of the next chunk to load. * @return Size of the next chunk or MVE_CHUNK_NONE if no more chunks available. */ public int getNextSize() { if (initialized) { return nextChunkSize; } else { return MVE_CHUNK_NONE; } } /** * Returns the type of the next chunk to load. * @return Chunk type or MVE_CHUNK_NONE if no more chunks available. */ public int getNextType() { if (initialized) { return nextChunkType; } else { return MVE_CHUNK_NONE; } } /** * Returns chunk type. */ public int getType() { if (initialized) { return curChunkType; } else { return MVE_CHUNK_NONE; } } /** * Returns chunk size without chunk header. */ public int getSize() { if (initialized) { return curChunkSize; } else { return MVE_CHUNK_NONE; } } /** * Returns whether more segments are available. */ public boolean hasNextSegment() { return (initialized && segmentIndex < segments.size()); } /** * Returns the next segment in line. */ public MveSegment nextSegment() { if (hasNextSegment()) { return segments.get(segmentIndex++); } else { return null; } } /** * Returns number of segments in the current chunk. * @return Number of segments in the current chunk. */ public int getSegmentCount() { if (initialized) { return segments.size(); } else { return 0; } } @Override public String toString() { return String.format("Chunk 0x%1$02x (%2$d bytes): segment #%3$d (opcode=%4$02x, size=%5$d)", curChunkType, curChunkSize, segmentIndex, segments.get(segmentIndex).opcode, segments.get(segmentIndex).size); } private boolean initSegments() { if (initialized && in != null) { segments.clear(); segmentIndex = 0; int curSize = 0; if (curChunkType != MVE_CHUNK_END) { curChunkSize = nextChunkSize; curChunkType = nextChunkType; while (curSize < curChunkSize) { try { int segmentSize = StreamUtils.readUnsignedShort(in); short segmentOpcode = StreamUtils.readUnsignedByte(in); short segmentVersion = StreamUtils.readUnsignedByte(in); curSize += 4; MveSegment segment = new MveSegment(in, segmentSize, segmentOpcode, segmentVersion); segments.add(segment); curSize += segmentSize; } catch (IOException e) { e.printStackTrace(); return false; } } return peekNextChunk(); } else { curChunkSize = curChunkType = MVE_CHUNK_NONE; initialized = false; } } return false; } private boolean peekNextChunk() { if (initialized && in != null) { if (curChunkType == MVE_CHUNK_END) { nextChunkSize = nextChunkType = MVE_CHUNK_NONE; } else { try { nextChunkSize = StreamUtils.readUnsignedShort(in); nextChunkType = StreamUtils.readUnsignedShort(in); } catch (IOException e) { e.printStackTrace(); return false; } } return true; } else { nextChunkSize = nextChunkType = MVE_CHUNK_NONE; return false; } } } /** * Stores preprocessed data of a single MVE segment. */ public static class MveSegment { private final int size, opcode, version; private final byte[] data; private int dataOfs, bitsAvail, bitsNext; private int dataOfs2, bitsAvail2, bitsNext2; /** * Read segment data from input stream, using size, opcode and version as parameters. * @param in The MVE data stream * @param size The size of the segment data without header size. * @param opcode The segment opcode. * @param version The opcode version. */ private MveSegment(InputStream in, int size, int opcode, int version) { this.size = size; this.opcode = opcode; this.version = version; data = new byte[size]; dataOfs = bitsAvail = bitsNext = 0; dataOfs2 = bitsAvail2 = bitsNext2 = 0; if (in != null) { int numRead = 0; while (numRead < size) { try { int n = in.read(data); if (n <= 0) break; numRead += n; } catch (IOException e) { } } } } /** * Returns segment opcode. * @return The segment opcode. */ public int getOpcode() { return opcode; } /** * Returns size of segment data (without initial size/opcode/version fields). * @return Segment data size without header size. */ public int getSize() { return size; } /** * Returns opcode version. * @return The opcode version. */ public int getVersion() { return version; } /** * Returns whether the buffer contains more data to fetch. * @return {@code true} if more data is available, {@code false} otherwise. */ public boolean available() { return (dataOfs < data.length || bitsAvail > 0); } /** * Returns whether the buffer contains more data to fetch from extra offset. * Note: This method is required for a number of video codes in direct color mode. * @return {@code true} if more data is available, {@code false} otherwise. */ public boolean availableExtra() { return (dataOfs2 < data.length || bitsAvail2 > 0); } /** * Returns the current offset in the internal buffer. * @return The current offset in bytes. */ public int getOffset() { return dataOfs; } /** * Returns the current extra offset in the internal buffer. * Note: This method is required for a number of video codes in direct color mode. * @return The current offset in bytes. */ public int getOffsetExtra() { return dataOfs2; } /** * Sets a new offset in the internal buffer. Clears all data already buffered. * @param newOfs The new offset in the internal buffer. */ public void setOffset(int newOfs) { if (newOfs >= 0 && newOfs < data.length) { dataOfs = newOfs; bitsNext = bitsAvail = 0; } } /** * Sets a new extra offset in the internal buffer. Clears all data already buffered. * Note: This method is required for a number of video codes in direct color mode. * @param newOfs The new offset in the internal buffer. */ public void setOffsetExtra(int newOfs) { if (newOfs >= 0 && newOfs < data.length) { dataOfs2 = newOfs; bitsNext2 = bitsAvail2 = 0; } } /** * Requests and returns specified number of bits from internal buffer. * @param bits Number of bits to fetch from the segment data. * @return Number of bits as an unsigned integer value. */ public int getBits(int bits) { int res = 0; if (bits > 0) { // prepare bits for output while (bits > bitsAvail) { if (dataOfs >= data.length) break; bitsNext |= (data[dataOfs++] & 0xff) << bitsAvail; bitsAvail += 8; } // output bits res = bitsNext; if (bits < 32) { res &= (1 << bits) - 1; bitsNext >>>= bits; } else { bitsNext = 0; } bitsAvail -= bits; } return res; } /** * Requests and returns specified number of bits from internal buffer's extra offset. * Note: This method is required for a number of video codes in direct color mode. * @param bits Number of bits to fetch from the segment data. * @return Number of bits as an unsigned integer value. */ public int getBitsExtra(int bits) { int res = 0; if (bits > 0) { // prepare bits for output while (bits > bitsAvail2) { if (dataOfs2 >= data.length) break; bitsNext2 |= (data[dataOfs2++] & 0xff) << bitsAvail2; bitsAvail2 += 8; } // output bits res = bitsNext2; if (bits < 32) { res &= (1 << bits) - 1; bitsNext2 >>>= bits; } else { bitsNext2 = 0; } bitsAvail2 -= bits; } return res; } /** * Provides direct access to underlying data. * @return Raw byte array of the segment data. */ public byte[] getData() { return data; } @Override public String toString() { return String.format("Segment 0x%1$02x (%2$d bytes): offset=#%3$d, ofsExtra=%4$d", opcode, size, dataOfs, dataOfs2); } } }