/* * @(#)SampleDeMux.java 1.3 01/03/13 * * Copyright (c) 1999-2001 Sun Microsystems, Inc. All Rights Reserved. * * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use, * modify and redistribute this software in source and binary code form, * provided that i) this copyright notice and license appear on all copies of * the software; and ii) Licensee does not utilize the software in a manner * which is disparaging to Sun. * * This software is provided "AS IS," without a warranty of any kind. ALL * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE * POSSIBILITY OF SUCH DAMAGES. * * This software is not designed or intended for use in on-line control of * aircraft, air traffic, aircraft navigation or aircraft communications; or in * the design, construction, operation or maintenance of any nuclear * facility. Licensee represents and warrants that it will not use or * redistribute the Software for such purposes. */ import java.io.IOException; import javax.media.*; import javax.media.protocol.*; import javax.media.Format; import javax.media.format.AudioFormat; /** * Demultiplexer for GSM file format */ /** * GSM * 8000 samples per sec. * 160 samples represent 20 milliseconds and GSM represents them * in 33 bytes. So frameSize is 33 bytes and there are 50 frames * in one second. One second is 1650 bytes. */ public class SampleDeMux implements Demultiplexer { private Time duration = Duration.DURATION_UNKNOWN; private Format format = null; private Track[] tracks = new Track[1]; // Only 1 track is there for Gsm private int numBuffers = 4; private int bufferSize; private int dataSize; private int encoding; private String encodingString; private int sampleRate; private int samplesPerBlock; private int bytesPerSecond = 1650; // 33 * 50 private int blockSize = 33; private int maxFrame = Integer.MAX_VALUE; // If we don't know the media length private long minLocation; private long maxLocation; private PullSourceStream stream = null; private long currentLocation = 0; protected DataSource source; protected SourceStream[] streams; protected boolean seekable = false; protected boolean positionable = false; private Object sync = new Object(); // synchronizing variable private static ContentDescriptor[] supportedFormat = new ContentDescriptor[] {new ContentDescriptor("audio.x_gsm")}; public ContentDescriptor [] getSupportedInputContentDescriptors() { return supportedFormat; } public void setSource(DataSource source) throws IOException, IncompatibleSourceException { if (!(source instanceof PullDataSource)) { throw new IncompatibleSourceException("DataSource not supported: " + source); } else { streams = ((PullDataSource) source).getStreams(); } if ( streams == null) { throw new IOException("Got a null stream from the DataSource"); } if (streams.length == 0) { throw new IOException("Got a empty stream array from the DataSource"); } this.source = source; this.streams = streams; positionable = (streams[0] instanceof Seekable); seekable = positionable && ((Seekable) streams[0]).isRandomAccess(); if (!supports(streams)) throw new IncompatibleSourceException("DataSource not supported: " + source); } /** * A Demultiplexer may support pull only or push only or both * pull and push streams. * Some Demultiplexer may have other requirements. * For e.g a quicktime Demultiplexer imposes an additional * requirement that * isSeekable() and isRandomAccess() be true */ protected boolean supports(SourceStream[] streams) { return ( (streams[0] != null) && (streams[0] instanceof PullSourceStream) ); } public boolean isPositionable() { return positionable; } public boolean isRandomAccess() { return seekable; } /** * Opens the plug-in software or hardware component and acquires * necessary resources. If all the needed resources could not be * acquired, it throws a ResourceUnavailableException. Data should not * be passed into the plug-in without first calling this method. */ public void open() { // throws ResourceUnavailableException; } /** * Closes the plug-in component and releases resources. No more data * will be accepted by the plug-in after a call to this method. The * plug-in can be reinstated after being closed by calling * open. */ public void close() { if (source != null) { try { source.stop(); source.disconnect(); } catch (IOException e) { // Internal error? } source = null; } } /** * This get called when the player/processor is started. */ public void start() throws IOException { if (source != null) source.start(); } /** * This get called when the player/processor is stopped. */ public void stop() { if (source != null) { try { source.stop(); } catch (IOException e) { // Internal errors? } } } /** * Resets the state of the plug-in. Typically at end of media or when media * is repositioned. */ public void reset() { } public Track[] getTracks() throws IOException, BadHeaderException { if (tracks[0] != null) return tracks; stream = (PullSourceStream) streams[0]; readHeader(); bufferSize = bytesPerSecond; tracks[0] = new GsmTrack((AudioFormat) format, /*enabled=*/ true, new Time(0), numBuffers, bufferSize, minLocation, maxLocation ); return tracks; } public Object[] getControls() { return new Object[0]; } public Object getControl(String controlType) { return null; } private void /* for now void */ readHeader() throws IOException, BadHeaderException { minLocation = getLocation(stream); // Should be zero long contentLength = stream.getContentLength(); if ( contentLength != SourceStream.LENGTH_UNKNOWN ) { double durationSeconds = contentLength / bytesPerSecond; duration = new Time(durationSeconds); maxLocation = contentLength; } else { maxLocation = Long.MAX_VALUE; } boolean signed = true; boolean bigEndian = false; format = new AudioFormat(AudioFormat.GSM, 8000, // sampleRate, 16, // sampleSizeInBits, 1, // channels, bigEndian ? AudioFormat.BIG_ENDIAN : AudioFormat.LITTLE_ENDIAN, signed ? AudioFormat.SIGNED : AudioFormat.UNSIGNED, (blockSize * 8), // frameSizeInBits Format.NOT_SPECIFIED, // No FRAME_RATE specified Format.byteArray); } // Contains 1 audio track public String getTrackLayout() { return "A"; } public Time setPosition(Time where, int rounding) { if (! seekable ) { return getMediaTime(); } long time = where.getNanoseconds(); long newPos; if (time < 0) time = 0; double newPosd = time * bytesPerSecond / 1000000000.0; double remainder = (newPosd % blockSize); newPos = (long) (newPosd - remainder); if (remainder > 0) { switch (rounding) { case Positionable.RoundUp: newPos += blockSize; break; case Positionable.RoundNearest: if (remainder > (blockSize / 2.0)) newPos += blockSize; break; } } if ( newPos > maxLocation ) newPos = maxLocation; newPos += minLocation; ((BasicTrack) tracks[0]).setSeekLocation(newPos); return where; } public Time getMediaTime() { long location; long seekLocation = ((BasicTrack) tracks[0]).getSeekLocation(); if (seekLocation != -1) location = seekLocation - minLocation; else location = getLocation(stream) - minLocation; return new Time( location / (double) bytesPerSecond ); } public Time getDuration() { if ( duration.equals(Duration.DURATION_UNKNOWN) && ( tracks[0] != null ) ) { long mediaSizeAtEOM = ((BasicTrack) tracks[0]).getMediaSizeAtEOM(); if (mediaSizeAtEOM > 0) { double durationSeconds = mediaSizeAtEOM / bytesPerSecond; duration = new Time(durationSeconds); } } return duration; } /** * Returns a descriptive name for the plug-in. * This is a user readable string. */ public String getName() { return "Parser for raw GSM"; } /** * Read numBytes from offset 0 */ public int readBytes(PullSourceStream pss, byte[] array, int numBytes) throws IOException { return readBytes(pss, array, 0, numBytes); } public int readBytes(PullSourceStream pss, byte[] array, int offset, int numBytes) throws IOException { if (array == null) { throw new NullPointerException(); } else if ((offset < 0) || (offset > array.length) || (numBytes < 0) || ((offset + numBytes) > array.length) || ((offset + numBytes) < 0)) { throw new IndexOutOfBoundsException(); } else if (numBytes == 0) { return 0; } int remainingLength = numBytes; int actualRead = 0; remainingLength = numBytes; while (remainingLength > 0) { actualRead = pss.read(array, offset, remainingLength); if (actualRead == -1) {// End of stream if (offset == 0) { throw new IOException("SampleDeMux: readBytes(): Reached end of stream while trying to read " + numBytes + " bytes"); } else { return offset; } } else if (actualRead == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) { return com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD; } else if (actualRead < 0) { throw new IOException("SampleDeMux: readBytes() read returned " + actualRead); } remainingLength -= actualRead; offset += actualRead; synchronized(sync) { currentLocation += actualRead; } } return numBytes; } protected final long getLocation(PullSourceStream pss) { synchronized(sync) { if ( (pss instanceof Seekable) ) return ((Seekable)pss).tell(); else return currentLocation; } } //////////////////////////////////////////////////////////// // Inner classes begin abstract private class BasicTrack implements Track { private Format format; private boolean enabled = true; protected Time duration; private Time startTime; private int numBuffers; private int dataSize; private PullSourceStream stream; private long minLocation; private long maxLocation; private long maxStartLocation; private SampleDeMux parser; private long sequenceNumber = 0; private TrackListener listener; private long seekLocation = -1L; private long mediaSizeAtEOM = -1L; // update when EOM implied by IOException occurs BasicTrack(SampleDeMux parser, Format format, boolean enabled, Time duration, Time startTime, int numBuffers, int dataSize, PullSourceStream stream) { this(parser, format, enabled, duration, startTime, numBuffers, dataSize, stream, 0L, Long.MAX_VALUE); } /** * Note to implementors who want to use this class. * If the maxLocation is not known, then * specify Long.MAX_VALUE for this parameter */ public BasicTrack(SampleDeMux parser, Format format, boolean enabled, Time duration, Time startTime, int numBuffers, int dataSize, PullSourceStream stream, long minLocation, long maxLocation) { this.parser = parser; this.format = format; this.enabled = enabled; this.duration = duration; this.startTime = startTime; this.numBuffers = numBuffers; this.dataSize = dataSize; this.stream = stream; this.minLocation = minLocation; this.maxLocation = maxLocation; maxStartLocation = maxLocation - dataSize; } public Format getFormat() { return format; } public void setEnabled(boolean t) { enabled = t; } public boolean isEnabled() { return enabled; } public Time getDuration() { return duration; } public Time getStartTime() { return startTime; } public int getNumberOfBuffers() { return numBuffers; } public void setTrackListener(TrackListener l) { listener = l; } public synchronized void setSeekLocation(long location) { seekLocation = location; } public synchronized long getSeekLocation() { return seekLocation; } public void readFrame(Buffer buffer) { if (buffer == null) return; if (!enabled) { buffer.setDiscard(true); return; } buffer.setFormat(format); // Need to do this every time ??? Object obj = buffer.getData(); byte[] data; long location; boolean needToSeek; synchronized(this) { if (seekLocation != -1) { location = seekLocation; seekLocation = -1; needToSeek = true; } else { location = parser.getLocation(stream); needToSeek = false; } } int needDataSize; if (location < minLocation) { buffer.setDiscard(true); return; } else if (location >= maxLocation) { buffer.setLength(0); buffer.setEOM(true); return; } else if (location > maxStartLocation) { needDataSize = dataSize - (int) (location - maxStartLocation); } else { needDataSize = dataSize; } if ( (obj == null) || (! (obj instanceof byte[]) ) || ( ((byte[])obj).length < needDataSize) ) { data = new byte[needDataSize]; buffer.setData(data); } else { data = (byte[]) obj; } try { if (needToSeek) { long pos = ((javax.media.protocol.Seekable)stream).seek(location); if ( pos == com.sun.media.protocol.BasicSourceStream.LENGTH_DISCARD) { buffer.setDiscard(true); return; } } int actualBytesRead = parser.readBytes(stream, data, needDataSize); buffer.setOffset(0); buffer.setLength(actualBytesRead); buffer.setSequenceNumber(++sequenceNumber); buffer.setTimeStamp(parser.getMediaTime().getNanoseconds()); } catch (IOException e) { if (maxLocation != Long.MAX_VALUE) { // Known maxLocation. So, this is a case of // deliberately reading past EOM System.err.println("readFrame: EOM " + e); buffer.setLength(0); // Need this?? buffer.setEOM(true); } else { // Unknown maxLocation, due to unknown content length // EOM reached before the required bytes could be read. long length = parser.streams[0].getContentLength(); if ( length != SourceStream.LENGTH_UNKNOWN ) { // If content-length is known, discard this buffer, update // maxLocation, maxStartLocation and mediaSizeAtEOM. // The next readFrame will read the remaining data till EOM. maxLocation = length; maxStartLocation = maxLocation - dataSize; mediaSizeAtEOM = maxLocation - minLocation; buffer.setLength(0); // Need this?? buffer.setDiscard(true); } else { // Content Length is still unknown after an IOException. // We can still discard this buffer and keep discarding // until content length is known. But this may go into // into an infinite loop, if there are real IO errors // So, return EOM maxLocation = parser.getLocation(stream); maxStartLocation = maxLocation - dataSize; mediaSizeAtEOM = maxLocation - minLocation; buffer.setLength(0); // Need this?? buffer.setEOM(true); } } } } public void readKeyFrame(Buffer buffer) { readFrame(buffer); } public boolean willReadFrameBlock() { return false; } public long getMediaSizeAtEOM() { return mediaSizeAtEOM; // updated when EOM implied by IOException occurs } } private class GsmTrack extends BasicTrack { private double sampleRate; private float timePerFrame = 0.020F; // 20 milliseconds GsmTrack(AudioFormat format, boolean enabled, Time startTime, int numBuffers, int bufferSize, long minLocation, long maxLocation) { super(SampleDeMux.this, format, enabled, SampleDeMux.this.duration, startTime, numBuffers, bufferSize, SampleDeMux.this.stream, minLocation, maxLocation); double sampleRate = format.getSampleRate(); int channels = format.getChannels(); int sampleSizeInBits = format.getSampleSizeInBits(); float bytesPerSecond; float bytesPerFrame; float samplesPerFrame; long durationNano = this.duration.getNanoseconds(); if (!( (durationNano == Duration.DURATION_UNKNOWN.getNanoseconds()) || (durationNano == Duration.DURATION_UNBOUNDED.getNanoseconds()) )) { maxFrame = mapTimeToFrame(this.duration.getSeconds()); } } GsmTrack(AudioFormat format, boolean enabled, Time startTime, int numBuffers, int bufferSize) { this(format, enabled, startTime, numBuffers, bufferSize, 0L, Long.MAX_VALUE); } // Frame numbers start from 0 private int mapTimeToFrame(double time) { double frameNumber = time / timePerFrame; return (int) frameNumber; } // Frame numbers start from 0 // 0-1 ==> 0, 1-2 ==> 1 public int mapTimeToFrame(Time t) { double time = t.getSeconds(); int frameNumber = mapTimeToFrame(time); if ( frameNumber > maxFrame) frameNumber = maxFrame; // Do we clamp it or return error System.out.println("mapTimeToFrame: " + (int) time + " ==> " + frameNumber + " ( " + frameNumber + " )"); return frameNumber; } public Time mapFrameToTime(int frameNumber) { if (frameNumber > maxFrame) frameNumber = maxFrame; // Do we clamp it or return error double time = timePerFrame * frameNumber; System.out.println("mapFrameToTime: " + frameNumber + " ==> " + time); return new Time(time); } } }