/*
* Philip Crotwell
* University of South Carolina, 2019
* https://www.seis.sc.edu
*/
// converted from Steim2.java in seedCodec
// https://github.com/crotwell/seedcodec/
// constants for compression types
/** ascii */
export const ASCII = 0;
/** 16 bit integer, or java short */
export const SHORT = 1;
/** 24 bit integer */
export const INT24 = 2;
/** 32 bit integer, or java int */
export const INTEGER = 3;
/** ieee float */
export const FLOAT = 4;
/** ieee double*/
export const DOUBLE = 5;
/** Steim1 compression */
export const STEIM1 = 10;
/** Steim2 compression */
export const STEIM2 = 11;
/** CDSN 16 bit gain ranged */
export const CDSN = 16;
/** (A)SRO */
export const SRO = 30;
/** DWWSSN 16 bit */
export const DWWSSN = 32;
export class CodecException extends Error {
constructor(message: string) {
super(message);
this.message = message;
this.name = "CodecException";
}
}
export class UnsupportedCompressionType extends Error {
constructor(message: string) {
super(message);
this.message = message;
this.name = "UnsupportedCompressionType";
}
}
export function isFloatCompression(compressionType: number): boolean {
if (compressionType === FLOAT || compressionType === DOUBLE) {
return true;
}
return false;
}
/**
* A holder for compressed data independent of the file format.
*/
export class EncodedDataSegment {
compressionType: number;
dataView: DataView;
numSamples: number;
littleEndian: boolean;
constructor(
compressionType: number,
dataView: DataView,
numSamples: number,
littleEndian: boolean,
) {
this.compressionType = compressionType;
this.dataView = dataView;
this.numSamples = numSamples;
this.littleEndian = littleEndian;
}
isFloatCompression(): boolean {
return isFloatCompression(this.compressionType);
}
decode(): Int32Array | Float32Array | Float64Array {
return decompress(
this.compressionType,
this.dataView,
this.numSamples,
this.littleEndian,
);
}
}
/**
* Decompress the samples from the provided DataView and
* return an array of the decompressed values.
* Only 16 bit short, 32 bit int, 32 bit float and 64 bit double
* along with Steim1 and Steim2 are supported.
*
* @param compressionType compression format as defined in SEED blockette 1000
* @param dataView input DataView to be decoded
* @param numSamples the number of samples that can be decoded from array
* b
* @param littleEndian if true, dataView is little-endian (intel byte order) b.
* @returns array of length numSamples.
* @throws CodecException fail to decompress.
* @throws UnsupportedCompressionType unsupported compression type
*/
export function decompress(
compressionType: number,
dataView: DataView,
numSamples: number,
littleEndian: boolean,
): Int32Array | Float32Array | Float64Array {
// in case of record with no data points, ex detection blockette, which often have compression type
// set to 0, which messes up the decompresser even though it doesn't matter since there is no data.
if (numSamples === 0) {
return new Int32Array(0);
}
let out;
let offset = 0;
let i;
switch (compressionType) {
case SHORT:
case DWWSSN:
// 16 bit values
if (dataView.byteLength < 2 * numSamples) {
throw new CodecException(
"Not enough bytes for " +
numSamples +
" 16 bit data points, only " +
dataView.byteLength +
" bytes.",
);
}
out = new Int32Array(numSamples);
for (i = 0; i < numSamples; i++) {
out[i] = dataView.getInt16(offset, littleEndian);
offset += 2;
}
break;
case INTEGER:
// 32 bit integers
if (dataView.byteLength < 4 * numSamples) {
throw new CodecException(
"Not enough bytes for " +
numSamples +
" 32 bit data points, only " +
dataView.byteLength +
" bytes.",
);
}
out = new Int32Array(numSamples);
for (i = 0; i < numSamples; i++) {
out[i] = dataView.getInt32(offset, littleEndian);
offset += 4;
}
break;
case FLOAT:
// 32 bit floats
if (dataView.byteLength < 4 * numSamples) {
throw new CodecException(
"Not enough bytes for " +
numSamples +
" 32 bit data points, only " +
dataView.byteLength +
" bytes.",
);
}
out = new Float32Array(numSamples);
for (i = 0; i < numSamples; i++) {
out[i] = dataView.getFloat32(offset, littleEndian);
offset += 4;
}
break;
case DOUBLE:
// 64 bit doubles
if (dataView.byteLength < 8 * numSamples) {
throw new CodecException(
"Not enough bytes for " +
numSamples +
" 64 bit data points, only " +
dataView.byteLength +
" bytes.",
);
}
out = new Float64Array(numSamples);
for (i = 0; i < numSamples; i++) {
out[i] = dataView.getFloat64(offset, littleEndian);
offset += 8;
}
break;
case STEIM1:
// steim 1
out = decodeSteim1(dataView, numSamples, littleEndian, 0);
break;
case STEIM2:
// steim 2
out = decodeSteim2(dataView, numSamples, littleEndian, 0);
break;
default:
// unknown format????
throw new UnsupportedCompressionType(
"Type " + compressionType + " is not supported at this time.",
);
}
// end of switch ()
return out;
}
/**
* Decode the indicated number of samples from the provided byte array and
* return an integer array of the decompressed values. Being differencing
* compression, there may be an offset carried over from a previous data
* record. This offset value can be placed in bias, otherwise leave
* the value as 0.
*
* @param dataView input DataView to be decoded
* @param numSamples the number of samples that can be decoded from array
* b
* @param littleEndian if true, dataView is little-endian (intel byte order) b.
* @param bias the first difference value will be computed from this value.
* If set to 0, the method will attempt to use the X(0) constant instead.
* @returns int array of length numSamples.
* @throws CodecException - encoded data length is not multiple of 64
* bytes.
*/
export function decodeSteim1(
dataView: DataView,
numSamples: number,
littleEndian: boolean,
bias: number,
): Int32Array {
// Decode Steim1 compression format from the provided byte array, which contains numSamples number
// of samples. swapBytes is set to true if the value words are to be byte swapped. bias represents
// a previous value which acts as a starting constant for continuing differences integration. At the
// very start, bias is set to 0.
if (dataView.byteLength % 64 !== 0) {
throw new CodecException(
"encoded data length is not multiple of 64 bytes (" +
dataView.byteLength +
")",
);
}
const buf = new ArrayBuffer(4 * numSamples);
const samples = new Int32Array(buf);
let tempSamples;
const numFrames = dataView.byteLength / 64;
let current = 0;
let start = 0;
let firstData = 0;
let lastValue = 0;
let i, j;
for (i = 0; i < numFrames; i++) {
tempSamples = extractSteim1Samples(dataView, i * 64, littleEndian); // returns only differences except for frame 0
firstData = 0; // d(0) is byte 0 by default
if (i === 0) {
// special case for first frame
lastValue = bias; // assign our X(-1)
// x0 and xn are in 1 and 2 spots
start = tempSamples[1]; // X(0) is byte 1 for frame 0
// end = tempSamples[2]; // X(n) is byte 2 for frame 0
firstData = 3; // d(0) is byte 3 for frame 0
// if bias was zero, then we want the first sample to be X(0) constant
if (bias === 0) lastValue = start - tempSamples[3]; // X(-1) = X(0) - d(0)
}
for (j = firstData; j < tempSamples.length && current < numSamples; j++) {
samples[current] = lastValue + tempSamples[j]; // X(n) = X(n-1) + d(n)
lastValue = samples[current];
current++;
}
}
// end for each frame...
if (current !== numSamples) {
throw new CodecException(
"Number of samples decompressed doesn't match number in header: " +
current +
" !== " +
numSamples,
);
}
// ignore last sample check???
//if (end !== samples[numSamples-1]) {
// throw new SteimException("Last sample decompressed doesn't match value x(n) value in Steim1 record: "+samples[numSamples-1]+" !== "+end);
//}
return samples;
}
/**
* Extracts differences from the next 64 byte frame of the given compressed
* byte array (starting at offset) and returns those differences in an int
* array.
* An offset of 0 means that we are at the first frame, so include the header
* bytes in the returned int array...else, do not include the header bytes
* in the returned array.
*
* @param dataView byte array of compressed data differences
* @param offset index to begin reading compressed bytes for decoding
* @param littleEndian reverse the endian-ness of the compressed bytes being read
* @returns integer array of difference (and constant) values
*/
function extractSteim1Samples(
dataView: DataView,
offset: number,
littleEndian: boolean,
): Array {
/* get nibbles */
const nibbles = dataView.getInt32(offset, littleEndian);
let currNibble = 0;
const temp = []; // 4 samples * 16 longwords, can't be more than 64
let currNum = 0;
let i, n;
for (i = 0; i < 16; i++) {
// i is the word number of the frame starting at 0
//currNibble = (nibbles >>> (30 - i*2 ) ) & 0x03; // count from top to bottom each nibble in W(0)
currNibble = (nibbles >> (30 - i * 2)) & 0x03; // count from top to bottom each nibble in W(0)
//System.err.print("c(" + i + ")" + currNibble + ","); // DEBUG
// Rule appears to be:
// only check for byte-swap on actual value-atoms, so a 32-bit word in of itself
// is not swapped, but two 16-bit short *values* are or a single
// 32-bit int *value* is, if the flag is set to TRUE. 8-bit values
// are naturally not swapped.
// It would seem that the W(0) word is swap-checked, though, which is confusing...
// maybe it has to do with the reference to high-order bits for c(0)
switch (currNibble) {
case 0:
//System.out.println("0 means header info");
// only include header info if offset is 0
if (offset === 0) {
temp[currNum++] = dataView.getInt32(offset + i * 4, littleEndian);
}
break;
case 1:
//System.out.println("1 means 4 one byte differences");
for (n = 0; n < 4; n++) {
temp[currNum] = dataView.getInt8(offset + i * 4 + n);
currNum++;
}
break;
case 2:
//System.out.println("2 means 2 two byte differences");
for (n = 0; n < 4; n += 2) {
temp[currNum] = dataView.getInt16(offset + i * 4 + n, littleEndian);
currNum++;
}
break;
case 3:
//System.out.println("3 means 1 four byte difference");
temp[currNum++] = dataView.getInt32(offset + i * 4, littleEndian);
break;
default:
throw new CodecException("unreachable case: " + currNibble);
//System.out.println("default");
}
}
return temp;
}
/**
* Decode the indicated number of samples from the provided byte array and
* return an integer array of the decompressed values. Being differencing
* compression, there may be an offset carried over from a previous data
* record. This offset value can be placed in bias, otherwise leave
* the value as 0.
*
* @param dataView input byte array to be decoded
* @param numSamples the number of samples that can be decoded from array
* @param swapBytes if true, swap reverse the endian-ness of the elements of
* dataview
* @param bias the first difference value will be computed from this value.
* If set to 0, the method will attempt to use the X(0) constant instead.
* @returns int array of length numSamples.
* @throws SteimException - encoded data length is not multiple of 64
* bytes.
*/
export function decodeSteim2(
dataView: DataView,
numSamples: number,
swapBytes: boolean,
bias: number,
): Int32Array {
if (dataView.byteLength % 64 !== 0) {
throw new CodecException(
"encoded data length is not multiple of 64 bytes (" +
dataView.byteLength +
")",
);
}
const buf = new ArrayBuffer(4 * numSamples);
const samples = new Int32Array(buf);
let tempSamples;
const numFrames = dataView.byteLength / 64;
let current = 0;
let start = 0;
let firstData = 0;
let lastValue = 0;
//System.err.println("DEBUG: number of samples: " + numSamples + ", number of frames: " + numFrames + ", byte array size: " + b.length);
for (let i = 0; i < numFrames; i++) {
tempSamples = extractSteim2Samples(dataView, i * 64, swapBytes); // returns only differences except for frame 0
firstData = 0; // d(0) is byte 0 by default
if (i === 0) {
// special case for first frame
lastValue = bias; // assign our X(-1)
// x0 and xn are in 1 and 2 spots
start = tempSamples[1]; // X(0) is byte 1 for frame 0
// end = tempSamples[2]; // X(n) is byte 2 for frame 0
firstData = 3; // d(0) is byte 3 for frame 0
// if bias was zero, then we want the first sample to be X(0) constant
if (bias === 0) lastValue = start - tempSamples[3]; // X(-1) = X(0) - d(0)
}
//System.err.print("DEBUG: ");
for (
let j = firstData;
j < tempSamples.length && current < numSamples;
j++
) {
samples[current] = lastValue + tempSamples[j]; // X(n) = X(n-1) + d(n)
lastValue = samples[current];
current++;
} //System.err.println("DEBUG: end of frame " + i);
}
// end for each frame...
if (current !== numSamples) {
throw new CodecException(
"Number of samples decompressed doesn't match number in header: " +
current +
" !== " +
numSamples,
);
}
// ignore last sample check???
//if (end !== samples[numSamples-1]) {
// throw new SteimException("Last sample decompressed doesn't match value x(n) value in Steim2 record: "+samples[numSamples-1]+" !== "+end);
//}
return samples;
}
/**
* Extracts differences from the next 64 byte frame of the given compressed
* byte array (starting at offset) and returns those differences in an int
* array.
* An offset of 0 means that we are at the first frame, so include the header
* bytes in the returned int array...else, do not include the header bytes
* in the returned array.
*
* @param dataView byte array of compressed data differences
* @param offset index to begin reading compressed bytes for decoding
* @param swapBytes reverse the endian-ness of the compressed bytes being read
* @returns integer array of difference (and constant) values
*/
function extractSteim2Samples(
dataView: DataView,
offset: number,
swapBytes: boolean,
): Int32Array {
/* get nibbles */
const nibbles = dataView.getUint32(offset, swapBytes);
let currNibble = 0;
let dnib = 0;
const temp = new Int32Array(106); //max 106 = 7 samples * 15 long words + 1 nibble int
let tempInt;
let currNum = 0;
let diffCount = 0; // number of differences
let bitSize = 0; // bit size
let headerSize = 0; // number of header/unused bits at top
for (let i = 0; i < 16; i++) {
currNibble = (nibbles >> (30 - i * 2)) & 0x03;
switch (currNibble) {
case 0:
// "0 means header info"
// only include header info if offset is 0
if (offset === 0) {
temp[currNum++] = dataView.getInt32(offset + i * 4, swapBytes);
}
break;
case 1:
// "1 means 4 one byte differences " +currNum+" "+dataView.getInt8(offset+(i*4))+" "+dataView.getInt8(offset+(i*4)+1)+" "+dataView.getInt8(offset+(i*4)+2)+" "+dataView.getInt8(offset+(i*4)+3)
temp[currNum++] = dataView.getInt8(offset + i * 4);
temp[currNum++] = dataView.getInt8(offset + i * 4 + 1);
temp[currNum++] = dataView.getInt8(offset + i * 4 + 2);
temp[currNum++] = dataView.getInt8(offset + i * 4 + 3);
break;
case 2:
tempInt = dataView.getUint32(offset + i * 4, swapBytes);
dnib = (tempInt >> 30) & 0x03;
switch (dnib) {
case 1:
// "2,1 means 1 thirty bit difference"
temp[currNum++] = (tempInt << 2) >> 2;
break;
case 2:
// "2,2 means 2 fifteen bit differences"
temp[currNum++] = (tempInt << 2) >> 17; // d0
temp[currNum++] = (tempInt << 17) >> 17; // d1
break;
case 3:
// "2,3 means 3 ten bit differences"
temp[currNum++] = (tempInt << 2) >> 22; // d0
temp[currNum++] = (tempInt << 12) >> 22; // d1
temp[currNum++] = (tempInt << 22) >> 22; // d2
break;
default:
throw new CodecException(
`Unknown case currNibble=${currNibble} dnib=${dnib} for chunk ${i} offset ${offset}, nibbles: ${nibbles}`,
);
}
break;
case 3:
tempInt = dataView.getUint32(offset + i * 4, swapBytes);
dnib = (tempInt >> 30) & 0x03;
// for case 3, we are going to use a for-loop formulation that
// accomplishes the same thing as case 2, just less verbose.
diffCount = 0; // number of differences
bitSize = 0; // bit size
headerSize = 0; // number of header/unused bits at top
switch (dnib) {
case 0:
//System.out.println("3,0 means 5 six bit differences");
headerSize = 2;
diffCount = 5;
bitSize = 6;
break;
case 1:
//System.out.println("3,1 means 6 five bit differences");
headerSize = 2;
diffCount = 6;
bitSize = 5;
break;
case 2:
//System.out.println("3,2 means 7 four bit differences, with 2 unused bits");
headerSize = 4;
diffCount = 7;
bitSize = 4;
break;
default:
throw new CodecException(
`Unknown case currNibble=${currNibble} dnib=${dnib} for chunk ${i} offset ${offset}, nibbles: ${nibbles}`,
);
}
if (diffCount > 0) {
for (let d = 0; d < diffCount; d++) {
// for-loop formulation
temp[currNum++] =
(tempInt << (headerSize + d * bitSize)) >>
((diffCount - 1) * bitSize + headerSize);
}
}
break;
default:
throw new CodecException(`Unknown case currNibble=${currNibble}`);
}
}
return temp.slice(0, currNum);
}