diff --git a/index.html b/index.html
index 8787780..83a9f8f 100644
--- a/index.html
+++ b/index.html
@@ -19,6 +19,23 @@
Send
+
+
Data
+ Data Bytes:
+ Data Bits:
+ Bits per Packet:
+ Data Bits per Packet:
+ Error Correction:
+ Error Blocks per packet:
+ Unused bits per packet:
+ Unused bits in last packet:
+ Last segment unused channels in packet:
+ Packet Count:
+ Segments Per Packet:
+ Segment Duration:
+ Packet Duration:
+ Total Duration:
+
Encoded
diff --git a/index.js b/index.js
index 8162434..fbc7d68 100644
--- a/index.js
+++ b/index.js
@@ -34,6 +34,7 @@ let PERIODIC_INTERLEAVING = true;
let WAVE_FORM = "triangle";
const ERROR_CORRECTION_BLOCK_SIZE = 7;
+const ERROR_CORRECTION_DATA_SIZE = 4;
let CHANNEL_OVER = -1;
let CHANNEL_SELECTED = -1;
let SEGMENT_OVER = -1;
@@ -49,7 +50,7 @@ var samplesPerBit = [];
var bitSampleCount = 0;
var PAUSE = false;
var PAUSE_AFTER_END = true;
-var PACKET_SIZE_BITS = 8;
+var PACKET_SIZE_BITS = 5; // 32 bytes, 256 bits
var EXPECTED_ENCODED_BITS = [];
var EXPECTED_BITS = [];
@@ -87,6 +88,7 @@ function handleWindowLoad() {
document.getElementById('packet-size-power').addEventListener('input', event => {
PACKET_SIZE_BITS = parseInt(event.target.value);
document.getElementById('packet-size').innerText = friendlyByteSize(2 ** PACKET_SIZE_BITS);
+ showSpeed();
});
document.getElementById('pause-after-end').checked = PAUSE_AFTER_END;
document.getElementById('error-correction-hamming').checked = HAMMING_ERROR_CORRECTION;
@@ -185,7 +187,7 @@ function handleTextToSendInput() {
const text = textToSend.value;
const dataByteCount = text.length;
const dataBitCount = dataByteCount * 8;
- const nibblesToEncode = HAMMING_ERROR_CORRECTION ? Math.ceil((dataBitCount) / 4) : 0;
+ const nibblesToEncode = HAMMING_ERROR_CORRECTION ? Math.ceil((dataBitCount) / ERROR_CORRECTION_DATA_SIZE) : 0;
const errorCorrectionBits = nibblesToEncode * 3;
const totalBits = errorCorrectionBits + dataBitCount;
const totalBytes = Math.ceil(totalBits / 8);
@@ -203,6 +205,7 @@ function handleTextToSendInput() {
document.getElementById('packet-send-segment-count').innerText = segmentCount.toLocaleString();
document.getElementById('packet-send-segment-duration').innerText = (SEGMENT_DURATION / 1000).toLocaleString();
document.getElementById('data-size-header-bits').innerText = '0';
+ updatePacketStats();
}
function updateFrequencyResolution() {
@@ -228,7 +231,7 @@ function showSpeed() {
document.getElementById('data-transfer-speed-bits-per-second').innerText = baud.toFixed(2);
document.getElementById('data-transfer-speed-bytes-per-second').innerText = bytes.toFixed(2);
if(HAMMING_ERROR_CORRECTION) {
- const effectiveBaud = baud * 4 / ERROR_CORRECTION_BLOCK_SIZE;
+ const effectiveBaud = baud * ERROR_CORRECTION_DATA_SIZE / ERROR_CORRECTION_BLOCK_SIZE;
const effectiveBytes = effectiveBaud / 8;
document.getElementById('effective-speed-bits-per-second').innerText = effectiveBaud.toFixed(2);
document.getElementById('effective-speed-bytes-per-second').innerText = effectiveBytes.toFixed(2);
@@ -263,6 +266,25 @@ function showSpeed() {
})
handleTextToSendInput();
drawChannels();
+ updatePacketStats();
+}
+function updatePacketStats() {
+ const text = textToSend.value;
+ const bits = textToBits(text);
+ document.getElementById('data-byte-count').innerText = (bits.length / 8).toLocaleString();
+ document.getElementById('data-bit-count').innerText = bits.length.toLocaleString();
+ document.getElementById('packet-bit-count').innerText = getPacketBitCount().toLocaleString();
+ document.getElementById('packet-count').innerText = getPacketCount(bits).toLocaleString();
+ document.getElementById('packet-error-correction').innerText = HAMMING_ERROR_CORRECTION ? 'Yes' : 'No';
+ document.getElementById('packet-error-block-count').innerText = getPacketErrorBlockCount().toLocaleString();
+ document.getElementById('packet-data-bit-count').innerText = getPacketDataBitCount().toLocaleString();
+ document.getElementById('packet-unused-bit-count').innerText = getPacketUnusedBitCount().toLocaleString();
+ document.getElementById('last-packet-unused-bit-count').innerText = getPacketLastUnusedBitCount(bits).toLocaleString();
+ document.getElementById('last-segment-unused-channel-count').innerText = getPacketLastSegmentUnusedChannelCount().toLocaleString()
+ document.getElementById('packet-transfer-duration').innerText = getPacketDurationSeconds(bits).toLocaleString() + 's';
+ document.getElementById('segment-transfer-duration').innerText = getSegmentTransferDurationSeconds().toLocaleString() + 's';
+ document.getElementById('data-transfer-duration').innerText = getDataTransferDurationSeconds(bits).toLocaleString() + 's';
+ document.getElementById('segments-per-packet').innerText = getPacketSegmentCount().toLocaleString();
}
function drawChannels() {
const sampleRate = getAudioContext().sampleRate;
@@ -323,7 +345,7 @@ function percentInFrequency(hz, frequencyResolution) {
return percent;
}
function nibbleToHamming(nibble) {
- if(nibble.length !== 4) return [];
+ if(nibble.length !== ERROR_CORRECTION_DATA_SIZE) return [];
return [
nibble[0] ^ nibble[1] ^ nibble[3],
nibble[0] ^ nibble[2] ^ nibble[3],
@@ -414,10 +436,10 @@ function staggerValues(values, blockSize, undo) {
function applyErrorCorrection(bits) {
if(!HAMMING_ERROR_CORRECTION) return bits;
const encodedBits = [];
- for(let i = 0; i < bits.length; i+= 4) {
- const nibble = bits.slice(i, i + 4);
- while(nibble.length < 4) nibble.push(0);
- encodedBits.push(...nibbleToHamming(bits.slice(i, i + 4)));
+ for(let i = 0; i < bits.length; i+= ERROR_CORRECTION_DATA_SIZE) {
+ const nibble = bits.slice(i, i + ERROR_CORRECTION_DATA_SIZE);
+ while(nibble.length < ERROR_CORRECTION_DATA_SIZE) nibble.push(0);
+ encodedBits.push(...nibbleToHamming(bits.slice(i, i + ERROR_CORRECTION_DATA_SIZE)));
}
return encodedBits;
}
@@ -449,77 +471,247 @@ function logSent(text) {
sentDataTextArea.scrollTop = sentDataTextArea.scrollHeight;
}
function sendBits(bits) {
- const byteCount = bits.length / 8;
if(bits.length === 0) {
logSent('No bits to send!');
return;
} else if(bits.length % 8 !== 0 || bits.length === 0) {
logSent('Bit count must be divisible by 8.');
return;
- } else if(byteCount > (1 << PACKET_SIZE_BITS)) {
- logSent(`Can not transfer more than ${(1 << PACKET_SIZE_BITS)} bytes.`);
- return;
}
+ const channelCount = getChannels().length;
EXPECTED_BITS = bits.slice();
document.getElementById('sent-data').value = bits.reduce(bitReducer, '');
- EXPECTED_ENCODED_BITS = bits = applyInterleaving(
- applyErrorCorrection(bits)
+ EXPECTED_ENCODED_BITS = [];
+
+ const startSeconds = audioContext.currentTime + 0.1;
+ const startMilliseconds = startSeconds * 1000;
+
+ const packetCount = getPacketCount(bits);
+ const packetBitCount = getPacketBitCount();
+ const totalDurationSeconds = getDataTransferDurationSeconds(bits);
+ const totalDurationMilliseconds = getDataTransferDurationSeconds(bits);
+ const packetDurationSeconds = getPacketDurationSeconds();
+
+ sendButton.innerText = 'Stop';
+
+ createOscillators(startSeconds);
+ // send all packets
+ for(let i = 0; i < packetCount; i++) {
+ let packet = getPacketBits(bits, i);
+ if(packet.length > packetBitCount) {
+ console.error('cant send packet of %s bits with more than %s bits', packet.length, packetBitCount);
+ disconnectOscillators();
+ return;
+ }
+ packet = applyInterleaving(packet);
+ EXPECTED_ENCODED_BITS.push(...packet);
+ sendPacket(packet, startSeconds + (i * packetDurationSeconds));
+ }
+ stopOscillators(startSeconds + totalDurationSeconds);
+ stopTimeoutId = window.setTimeout(
+ disconnectOscillators,
+ startMilliseconds + totalDurationMilliseconds
);
document.getElementById('encoded-data').value = EXPECTED_ENCODED_BITS.reduce(bitReducer, '');
- var audioContext = getAudioContext();
- const channels = getChannels();
- const oscillators = [];
- const channelCount = channels.length;
-
- const currentTime = audioContext.currentTime + 0.1;
-
- const destination = SEND_VIA_SPEAKER ? audioContext.destination : getAnalyser();
-
- // create our oscillators
- for(let i = 0; i < channelCount; i++) {
- var oscillator = audioContext.createOscillator();
-
- oscillator.connect(destination);
- oscillator.type = WAVE_FORM;
- oscillators.push(oscillator);
- }
-
- // change our channel frequencies for the bit
- for(let i = 0; i < bits.length; i++) {
- const isHigh = bits[i];
- const channel = i % channelCount;
- const segment = Math.floor(i / channelCount);
- var offset = ((segment * SEGMENT_DURATION)/1000);
- var offset2 = (((segment+1) * SEGMENT_DURATION)/1000) - (1/100000);
- oscillators[channel].frequency.setValueAtTime(
- channels[channel][isHigh ? 1 : 0],
- currentTime + offset
- );
- oscillators[channel].frequency.setValueAtTime(
- channels[channel][isHigh ? 1 : 0],
- currentTime + offset2
- );
- }
-
- // start sending our signal
- oscillators.forEach(o => o.start(currentTime));
-
- // silence oscillators when done
- for(let i = bits.length; i < bits.length + channelCount; i++) {
- const channel = i % channelCount;
- const segment = Math.floor(i / channelCount);
- const offset = ((segment * SEGMENT_DURATION) / 1000);
- oscillators[channel].frequency.setValueAtTime(0, currentTime + offset);
- oscillators[channel].stop(currentTime + offset);
- }
-
// start the graph moving again
resumeGraph();
}
+let stopTimeoutId;
+
+function getDataTransferDurationMilliseconds(bits) {
+ return getPacketCount(bits) * getPacketDurationMilliseconds();
+}
+function getDataTransferDurationSeconds(bits) {
+ return getDataTransferDurationMilliseconds(bits) / 1000;
+}
+function getPacketDurationMilliseconds() {
+ return getPacketSegmentCount() * SEGMENT_DURATION;
+}
+function getPacketDurationSeconds() {
+ return getPacketDurationMilliseconds() / 1000;
+}
+function getSegmentTransferDurationSeconds() {
+ return SEGMENT_DURATION / 1000;
+}
+function getPacketSegmentCount() {
+ return Math.ceil(getPacketBitCount() / getChannels().length);
+}
+function getPacketCount(bits) {
+ if(!canSendPacket()) return 0;
+
+ // How many data bits will be encoded in our packet?
+ let dataBitCount = getPacketDataBitCount();
+
+ // Return the total number of packets needed to send all data
+ return Math.ceil(bits.length / dataBitCount);
+}
+function getPacketBits(bits, packetIndex) {
+ if(!canSendPacket()) return [];
+ const packetBits = getPacketUsedBits(bits, packetIndex);
+
+ // How many bits expected in our packet?
+ const packetBitCount = getPacketBitCount();
+
+ // pad the array to the entire packet size
+ return padArray(packetBits, packetBitCount, 0);
+}
+function getPacketUsedBits(bits, packetIndex) {
+ if(!canSendPacket()) return [];
+
+ // How many data bits will be in our packet?
+ const dataBitCount = getPacketDataBitCount();
+
+ // grab our data
+ const startIndex = packetIndex * dataBitCount;
+ const endIndex = startIndex + dataBitCount;
+ let packetBits = bits.slice(startIndex, endIndex);
+
+ // Are we using error correction?
+ if(HAMMING_ERROR_CORRECTION) {
+ // encode data bits
+ packetBits = applyErrorCorrection(packetBits);
+ }
+ return packetBits;
+}
+function getPacketErrorBlockCount() {
+ const bitCount = getPacketBitCount();
+ // No error correction?
+ if(!HAMMING_ERROR_CORRECTION) {
+ // No error blocks
+ return 0;
+ }
+ // How many error blocks can be in a packet?
+ return Math.floor(
+ bitCount /
+ ERROR_CORRECTION_BLOCK_SIZE
+ );
+}
+function getPacketByteCount() {
+ // Convert packet size as power of 2
+ return 2 ** PACKET_SIZE_BITS;
+}
+function getPacketBitCount() {
+ // 8 bits per byte
+ return getPacketByteCount() * 8;
+}
+function canSendPacket() {
+ const max = getPacketBitCount();
+ // Need at least 1 bit to send
+ if(max < 1) return false;
+ // Has error correction?
+ if(HAMMING_ERROR_CORRECTION) {
+ // Need enough bits to fit one or more blocks
+ return max >= ERROR_CORRECTION_BLOCK_SIZE;
+ }
+ // 1 or more is great without encoding
+ return true;
+}
+function getPacketLastSegmentUnusedChannelCount() {
+ const channelCount = getChannels().length;
+ return (channelCount - (getPacketBitCount() % channelCount));
+}
+function getPacketUnusedBitCount() {
+ return getPacketBitCount() - getPacketDataBitCount();
+}
+function getPacketLastUnusedBitCount(bits) {
+ const packetCount = getPacketCount(bits);
+ const availableBits = getPacketBitCount();
+ const usedBits = getPacketUsedBits(bits, packetCount-1).length;
+ return availableBits - usedBits;
+}
+function getPacketDataBitCount() {
+ const bitCount = getPacketBitCount();
+ // No error correction?
+ if(!HAMMING_ERROR_CORRECTION) {
+ // Return all bits available
+ return bitCount;
+ }
+ return getPacketErrorBlockCount() * ERROR_CORRECTION_DATA_SIZE;
+}
+function padArray(values, length, value) {
+ while(values.length < length) values.push(value);
+ return values;
+}
+
+const CHANNEL_OSCILLATORS = [];
+function getOscillators() {
+ return CHANNEL_OSCILLATORS;
+}
+function createOscillators(streamStartSeconds) {
+ console.log('create oscillators', streamStartSeconds);
+ const oscillators = getOscillators();
+ if(oscillators.length !== 0) disconnectOscillators();
+ var audioContext = getAudioContext();
+ const channels = getChannels();
+ const channelCount = channels.length;
+ const destination = SEND_VIA_SPEAKER ? audioContext.destination : getAnalyser();
+ // create our oscillators
+ for(let i = 0; i < channelCount; i++) {
+ const oscillator = audioContext.createOscillator();
+ oscillator.connect(destination);
+ oscillator.type = WAVE_FORM;
+ oscillator.start(streamStartSeconds);
+ oscillators.push(oscillator);
+ }
+ return oscillators;
+}
+function disconnectOscillators() {
+ console.log('disconnect oscillators');
+ stopOscillators(getAudioContext().currentTime);
+ const oscillators = getOscillators();
+ oscillators.forEach(
+ oscillator => oscillator.disconnect()
+ )
+ oscillators.length = 0;
+ sendButton.innerText = 'Send';
+ stopTimeoutId = undefined;
+}
+function stopOscillators(streamEndSeconds) {
+ console.log('stop oscillators', streamEndSeconds);
+ const channels = getChannels();
+ const oscillators = getOscillators();
+ const channelCount = channels.length;
+ // silence oscillators when done
+ for(let channel = 0; channel < channelCount; channel++) {
+ const oscillator = oscillators[channel];
+ oscillator?.stop(streamEndSeconds);
+ }
+}
+function sendPacket(bits, packetStartSeconds) {
+ console.log('packet start', packetStartSeconds);
+ const channels = getChannels();
+ const oscillators = getOscillators();
+ const channelCount = channels.length;
+ let bitCount = bits.length;
+ const lastChannel = bits.length % channelCount;
+ if(lastChannel !== channelCount - 1) {
+ // make sure all channels are set for the last segment
+ bitCount += (channelCount - lastChannel)
+ }
+
+ const segmentDurationSeconds = getSegmentTransferDurationSeconds();
+ // change our channel frequencies for the bit
+ for(let i = 0; i < bitCount; i++) {
+ // missing bits past end of bit stream set to zero
+ const isHigh = bits[i] ?? 0;
+
+ const channel = i % channelCount;
+
+ // already at correct frequency
+ if(oscillators[channel].on === isHigh) continue;
+ oscillators[channel].on = isHigh;
+ const segmentIndex = Math.floor(i / channelCount);
+ var offsetSeconds = segmentIndex * segmentDurationSeconds;
+ oscillators[channel].frequency.setValueAtTime(
+ channels[channel][isHigh ? 1 : 0],
+ packetStartSeconds + offsetSeconds
+ );
+ }
+}
function stopGraph() {
PAUSE = true;
stopCollectingSamples();
@@ -806,6 +998,10 @@ function textToBits(text) {
return bits.join('').split('').map(Number);
}
function handleSendButtonClick() {
+ if(stopTimeoutId) {
+ disconnectOscillators();
+ return;
+ }
receivedDataTextarea.value = '';
sentDataTextArea.value = '';
@@ -1080,37 +1276,12 @@ function getTimePercent(time, newest) {
if(newest - time > duration) return -1;
return ((newest - time) / duration);
}
-function getDataBitCount() {
- const text = document.getElementById('text-to-send').value;
- const dataByteCount = text.length;
- return dataByteCount * 8;
-}
-function getPacketSizeUnencodedBitCount() {
- return getDataBitCount();
-}
-function getErrorCorrectionBlocks() {
- if(!HAMMING_ERROR_CORRECTION) return 0;
- const dataBitCount = getPacketSizeUnencodedBitCount();
- return Math.ceil(dataBitCount / 4);
-}
-function getErrorCorrectionBitCount() {
- return getErrorCorrectionBlocks() * 3;
-}
-function getPacketSizeEncodedBitCount() {
- return getErrorCorrectionBitCount() + getPacketSizeUnencodedBitCount();
-}
-function getPacketSizeEncodedByteCount() {
- return Math.ceil(getPacketSizeEncodedBitCount() / 8);
-}
+
function getPacketSizeSegmentCount() {
- const totalBits = getPacketSizeEncodedBitCount();
+ const totalBits = getPacketBitCount();
const channelCount = getChannels().length;
return Math.ceil(totalBits / channelCount);
}
-function getPacketDurationMilliseconds() {
- const segmentCount = getPacketSizeSegmentCount();
- return segmentCount * SEGMENT_DURATION;
-}
function drawChannelData() {
// Do/did we have a stream?
if(!LAST_STREAM_STARTED) return;
@@ -1118,7 +1289,7 @@ function drawChannelData() {
const latest = frequencyOverTime[0].time;
// will any of the stream appear?
- const packetBitCount = getPacketSizeEncodedBitCount();
+ const packetBitCount = getPacketBitCount();
const packetDuration = getPacketDurationMilliseconds();
const lastStreamEnded = LAST_STREAM_STARTED + packetDuration;