/*
* Copyright 2016 Cel Skeggs
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.recording;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import ccre.log.Logger;
class StreamEncoder {
// We don't include the types - the decoder has to know them!
// This will be done with a metadata channel.
static final String MAGIC_STRING = "Encoded Recording Stream: version 0.1.0\n";
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
private final OutputStream real_output;
private boolean closed = false;
public StreamEncoder(OutputStream output) throws IOException {
this.real_output = output;
output.write(MAGIC_STRING.getBytes());
}
// ******* VARINT IMPLEMENTATIONS *******
// need to be synchronized with StreamDecoder!
// in units of 10 microseconds
private void writeTimeDelta(int delta) {
if (delta < 128) {
// optimize the most for the very common case of up to 1.27 ms
// encoded as a single byte
// bit format: 0xxxxxxx
out.write(delta < 0 ? 0 : (byte) delta);
} else if (delta < 32895) {
// optimize a bit less for the next most common case of up to 329 ms
// bit format: 1xxxxxxx xxxxxxxx
delta -= 128;
out.write(0b1000_0000 | (delta >> 8));
out.write(delta & 0xFF);
} else {
// don't both optimizing for this case; even if it happens, the
// longer delays mean that the bitrate is still low even if
// this is completely unoptimized.
// bit format: 11111111 11111111 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
out.write(0xFF);
out.write(0xFF);
out.write(delta >> 24);
out.write(delta >> 16);
out.write(delta >> 8);
out.write(delta);
}
}
private void writeChannelNumber(int channel) {
// optimize for channel numbers less than around 4096
if (channel < 0) {
throw new IllegalArgumentException();
} else if (channel < 128) {
// 0xxxxxxx
out.write(channel);
} else if (channel < 196) {
// 10xxxxxx
out.write(0b1000_0000 | (channel - 128));
} else if (channel < 224) {
// 110xxxxx
out.write(0b1100_0000 | (channel - 196));
} else if (channel < 240) {
// 1110xxxx
out.write(0b1110_0000 | (channel - 224));
} else if (channel < 4335) {
// 1111xxxx xxxxxxxx
channel -= 240; // to range 0-4094
out.write(0b1111_0000 | (channel >> 8));
out.write(channel & 0xFF);
} else {
// really don't bother optimizing for this many channels
// 11111111 11111111 xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
out.write(0xFF);
out.write(0xFF);
out.write(channel >> 24);
out.write(channel >> 16);
out.write(channel >> 8);
out.write(channel);
}
}
private void writeArrayLength(int length) {
if (length >= 0 && length < 255) {
// optimize for the common case of short arrays
out.write((byte) length);
} else {
// otherwise, it literally doesn't matter. The size of the data
// overwhelms any difference that this might make.
out.write(255);
out.write((byte) (length >> 24));
out.write((byte) (length >> 16));
out.write((byte) (length >> 8));
out.write((byte) length);
}
}
private void writeGeneralVarInt(long value) {
// needs one to ten bytes, depending on the value
// unify range of small positive and small negative values so that
// negative values aren't suddenly huge
if (value < 0) {
value = ((~value) << 1) | 1;
} else {
value <<= 1;
}
// now value should be treated as an unsigned integer!
while (true) {
int bits = (int) (value & 0x7F);
value >>>= 7; // drop the seven bits we just got
if (value == 0) {
out.write(bits);
break;
} else {
out.write(bits | 0x80);
}
}
}
private long lastTimestamp;
public void encode(RecordSnapshot rs) throws IOException {
if (closed) {
throw new IOException("Already closed!");
}
writeTimeDelta((int) (rs.timestamp - lastTimestamp));
writeChannelNumber(rs.channel);
lastTimestamp = rs.timestamp;
switch (rs.type) {
case RecordSnapshot.T_NULL:
// nothing else needed; 2 bytes common case
break;
case RecordSnapshot.T_BYTE:
// 3 bytes common case
out.write((byte) rs.value);
break;
case RecordSnapshot.T_SHORT:
// 4 bytes common case
out.write((byte) (rs.value >> 8));
out.write((byte) rs.value);
break;
case RecordSnapshot.T_INT:
// 6 bytes common case
out.write((byte) (rs.value >> 24));
out.write((byte) (rs.value >> 16));
out.write((byte) (rs.value >> 8));
out.write((byte) rs.value);
break;
case RecordSnapshot.T_LONG:
// 10 bytes common case
out.write((byte) (rs.value >> 56));
out.write((byte) (rs.value >> 48));
out.write((byte) (rs.value >> 40));
out.write((byte) (rs.value >> 32));
out.write((byte) (rs.value >> 24));
out.write((byte) (rs.value >> 16));
out.write((byte) (rs.value >> 8));
out.write((byte) rs.value);
break;
case RecordSnapshot.T_VARINT:
// common case varies significantly
writeGeneralVarInt(rs.value);
break;
case RecordSnapshot.T_BYTES:
// common case is 3 bytes + data length
writeArrayLength(rs.data.length);
out.write(rs.data, 0, rs.data.length);
break;
default:
Logger.warning("Invalid type for StreamEncoder: " + rs.type);
return;
}
if (out.size() >= 10000) {
// flush at least once for every 10 KB, aka around every thousand to
// five thousand samples.
this.flush();
}
}
public void flush() throws IOException {
if (closed) {
throw new IOException("Already closed!");
}
out.writeTo(real_output);
out.reset();
}
public void close() throws IOException {
if (closed) {
return;
}
flush();
closed = true;
real_output.close();
}
}