From fdb6723e87d7ed980fbfc1d9dac36313fa282a75 Mon Sep 17 00:00:00 2001 From: Lewis Moten Date: Fri, 10 May 2024 04:42:51 -0400 Subject: [PATCH] wip: big refactor --- AudioSender.js | 115 ++++ CRC.js | 103 ++++ HammingEncoding.js | 53 ++ Humanize.js | 58 ++ InterleaverEncoding.js | 54 ++ PacketUtils.js | 147 +++++ Randomizer.js | 72 +++ StreamManager.js | 111 ++++ converters.js | 10 + index.html | 304 ++++++---- index.js | 1295 +++++++++++++--------------------------- style.css | 55 +- 12 files changed, 1384 insertions(+), 993 deletions(-) create mode 100644 AudioSender.js create mode 100644 CRC.js create mode 100644 HammingEncoding.js create mode 100644 Humanize.js create mode 100644 InterleaverEncoding.js create mode 100644 PacketUtils.js create mode 100644 Randomizer.js create mode 100644 StreamManager.js create mode 100644 converters.js diff --git a/AudioSender.js b/AudioSender.js new file mode 100644 index 0000000..b68cae9 --- /dev/null +++ b/AudioSender.js @@ -0,0 +1,115 @@ +let audioContext; +let CHANNELS = []; +let DESTINATION; +let ON_START; +let ON_STOP; +let ON_SEND; +let CHANNEL_OSCILLATORS = []; +let WAVE_FORM; + +let stopOscillatorsTimeoutId; + +export const changeConfiguration = ({ + channels, + destination, + startCallback, + stopCallback, + sendCallback, + waveForm +}) => { + CHANNELS = channels; + DESTINATION = destination; + ON_START = startCallback; + ON_STOP = stopCallback; + ON_SEND = sendCallback; + WAVE_FORM = waveForm; +} + +export const setAudioContext = ctx => audioContext = ctx; + +function getAudioContext() { + if(!audioContext) { + throw 'Audio context not provided.'; + } + if(audioContext.state === 'suspended') { + audioContext.resume(); + } + return audioContext; +} + +const getChannels = () => CHANNELS; +const getDestination = () => DESTINATION; + +export const now = () => getAudioContext().currentTime; + +export function beginAt(streamStartSeconds) { + stopTimeout(); + const oscillators = getOscillators(); + if(oscillators.length !== 0) stop(); + const audioContext = getAudioContext(); + const channels = getChannels(); + const channelCount = channels.length; + const destination = getDestination();; + // 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); + } + callFn(ON_START); + return oscillators; +} +function getOscillators() { + return CHANNEL_OSCILLATORS; +} +export function send(bits, startSeconds) { + const oscillators = getOscillators(); + getChannels().forEach((channel, i) => { + // send missing bits as zero + const isHigh = bits[i] ?? 0; + callFn(ON_SEND, isHigh); + const oscillator = oscillators[i]; + // already at correct frequency + if(oscillator.on === isHigh) return; + oscillator.on = isHigh; + const hz = channel[isHigh ? 1 : 0]; + oscillator.frequency.setValueAtTime(hz, startSeconds); + }); +} +const stopTimeout = () => { + if(stopOscillatorsTimeoutId) { + window.setTimeout(stopOscillatorsTimeoutId); + stopOscillatorsTimeoutId = undefined; + } +} +export function stopAt(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); + } + stopTimeout(); + stopOscillatorsTimeoutId = window.setTimeout( + stop, + (streamEndSeconds - now()) * 1000 + ); +} +export function stop() { + const time = now(); + const oscillators = getOscillators(); + oscillators.forEach( + oscillator => { + oscillator?.stop(time); + oscillator?.disconnect(); + } + ) + oscillators.length = 0; + callFn(ON_STOP); + stopTimeout(); +} +const callFn = (fn, ...args) => typeof fn === 'function' ? fn(...args) : 0; diff --git a/CRC.js b/CRC.js new file mode 100644 index 0000000..803c8b5 --- /dev/null +++ b/CRC.js @@ -0,0 +1,103 @@ +function calcCrc( + bytes, + size, + polynomial, + { + initialization = 0, + reflectIn = false, + reflectOut = false, + xorOut = 0 + } = {} +) { + if(bytes.length === 0) return 0; + const validBits = (1 << size) - 1; + const mostSignificantBit = 1 << size - 1; + const bitsBeforeLastByte = size - 8; + + // setup our initial value + let crc = initialization; + + function reverseBits(value, size) { + let reversed = 0; + for(let i = 0; i < size; i++) { + // if bit position is on + if(value & (1<= 32 && crc & mostSignificantBit) { + crc >>>= 0; + } + return crc; +} +export function check(bytes, bitCount = 8) { + switch(bitCount) { + case 8: return crc8(bytes); + case 16: return crc16(bytes); + case 32: return crc32(bytes); + default: return 0; + } +} +function crc8(bytes) { return calcCrc(bytes, 8, 0x07); } +function crc16(bytes) { + return calcCrc( + bytes, + 16, + 0x8005, + { + initialization: 0, + reflectIn: true, + reflectOut: true, + xorOut: 0 + } + ); +} +function crc32(bytes) { + return calcCrc( + bytes, + 32, + 0x04C11DB7, + { + initialization: 0xFFFFFFFF, + reflectIn: true, + reflectOut: true, + xorOut: 0x0 + } + ); +} \ No newline at end of file diff --git a/HammingEncoding.js b/HammingEncoding.js new file mode 100644 index 0000000..fa9c311 --- /dev/null +++ b/HammingEncoding.js @@ -0,0 +1,53 @@ +// Encoding to encode/decode data with Hamming Error Correction +export const DECODED_SIZE = 4; +export const ENCODED_SIZE = 7; + +export const blockSize = () => ({ + encoded: ENCODED_SIZE, + decoded: DECODED_SIZE +}); + +export const encode = (bits) => { + const encodedBits = []; + for(let i = 0; i < bits.length; i+= DECODED_SIZE) { + const block = bits.slice(i, i + DECODED_SIZE); + encodedBits.push(...encodeBlock(block)); + } + return encodedBits; +} +export const decode = bits => { + const decodedBits = []; + for(let i = 0; i < bits.length; i += ENCODED_SIZE) { + const block = bits.slice(i, i + ENCODED_SIZE); + decodedBits.push(...decodeBlock(block)); + } + return decodedBits; +} + +const encodeBlock = bits => { + if(bits.length !== DECODED_SIZE) return []; + return [ + bits[0] ^ bits[1] ^ bits[3], + bits[0] ^ bits[2] ^ bits[3], + bits[0], + bits[1] ^ bits[2] ^ bits[3], + bits[1], + bits[2], + bits[3] + ] +} + +const decodeBlock = bits => { + if(bits.length !== ENCODED_SIZE) return []; + const error_1 = bits[0] ^ bits[2] ^ bits[4] ^ bits[6]; + const error_2 = bits[1] ^ bits[2] ^ bits[5] ^ bits[6]; + const error_3 = bits[3] ^ bits[4] ^ bits[5] ^ bits[6]; + let error = (error_3 << 2) | (error_2 << 1) | error_1; + if(error !== 0) bits[error - 1] ^= 1; + return [ + bits[2], + bits[4], + bits[5], + bits[6] + ]; +} \ No newline at end of file diff --git a/Humanize.js b/Humanize.js new file mode 100644 index 0000000..44b3745 --- /dev/null +++ b/Humanize.js @@ -0,0 +1,58 @@ +export function byteSize(count) { + let unitIndex = 0; + const units = ['bytes', 'kb', 'mb', 'gb', 'tb', 'pb']; + while(count > 999) { + count /= 1024; + unitIndex++; + if(unitIndex === units.length - 1) break; + } + count = Math.floor(count * 10) * 0.1 + return `${count.toLocaleString()} ${units[unitIndex]}` +} +export function bitsPerSecond(bps) { + let unitIndex = 0; + const units = ['baud', 'Kbps', 'Mbps', 'Gbps', 'Tbps', 'Pbps']; + while(bps > 999) { + bps /= 1024; + unitIndex++; + if(unitIndex === units.length - 1) break; + } + bps = Math.floor(bps * 10) * 0.1 + return `${bps.toLocaleString()} ${units[unitIndex]}` +} +export function durationMilliseconds(milliseconds) { + const lookup = [ + {mod: 1000, singular: 'ms', plural: 'ms'}, + {mod: 60, singular: 's', plural: 's'}, + {mod: 60, singular: 'm', plural: 'm'}, + {mod: 24, singular: 'h', plural: 'h'}, + {mod: 7, singular: 'd', plural: 'd'}, + {mod: 53, singular: 'w', plural: 'w'}, + {mod: 10, singular: 'y', plural: 'y'}, + {mod: 10, singular: 'd', plural: 'd'}, + {mod: 10, singular: 'c', plural: 'c'}, + {mod: 10, singular: 'm', plural: 'm'}, + ] + const units = []; + let remaining = Math.floor(milliseconds); + for(let i = 0; i < lookup.length; i++) { + const {mod, singular, plural} = lookup[i]; + const value = remaining % mod; + units.push({value, singular, plural}); + remaining -= value; + if(remaining < mod) break; + remaining = remaining / mod; + } + + return units + // largest units first + .reverse() + // top 2 largest values + .filter((_, i) => i < 2) + // drop values of zero + .filter(({value}) => value > 0) + // humanize unit + .map(({value, singular, plural}) => value + (value === 1 ? singular : plural)) + // combine + .join(' '); +} \ No newline at end of file diff --git a/InterleaverEncoding.js b/InterleaverEncoding.js new file mode 100644 index 0000000..61d3c90 --- /dev/null +++ b/InterleaverEncoding.js @@ -0,0 +1,54 @@ +// InterleaverEncoding + +// Rolls / shifts elements of an array so that each +// block of N data elements contains the minimal +// amount of original data possible. +// Elements toward the end of an array are wrapped +// around to the beginning + +// This is primarily used for data transfer where +// multiple channels representing various bits in +// the same block may be subsceptable to noise and +// provide the wrong value for one or more bits in +// a block + +let BLOCK_SIZE = 1; + +export const blockSize = () => ({ + encoded: BLOCK_SIZE, + decoded: BLOCK_SIZE +}); + +export const changeConfiguration = ({ + blockSize +}) => { + BLOCK_SIZE = blockSize ?? 1; +} + +export const decode = bits => encode(bits, true); + +export const encode = (bits, undo = false) => { + // Block sizes of 1 or less are unable to move bits + if(BLOCK_SIZE <= 1) return bits; + + // We need at least 1 extra bit for one bit to escape the block + if(bits.length <= BLOCK_SIZE ) return bits; + + // loop through indexes of a block + for(let blockMovement = 1; blockMovement < BLOCK_SIZE; blockMovement++) { + // Move every N bit N blocks over... + bits.filter((_, i) => + // values to be moved to different blocks + i % BLOCK_SIZE === blockMovement + ).map((_,i,a) => { + // values moved N blocks + if(undo) i -= blockMovement; else i += blockMovement; + i = ((i % a.length) + a.length) % a.length; + return a[i]; + }).forEach((v, i) => { + // replace with new values + bits[blockMovement + (i * BLOCK_SIZE)] = v; + }) + }; + return bits; +} \ No newline at end of file diff --git a/PacketUtils.js b/PacketUtils.js new file mode 100644 index 0000000..026e998 --- /dev/null +++ b/PacketUtils.js @@ -0,0 +1,147 @@ +let SEGMENT_DURATION = 30; +let PACKET_SIZE_BITS = 8; +let DATA_SIZE_BITS = 8; +let DATA_SIZE_CRC_BITS = 8; +let BITS_PER_SEGMENT = 1; +let PACKET_ENCODING = false; +let PACKET_ENCODING_SIZE = 7; +let PACKET_DECODING_SIZE = 4; +let ENCODING; + +export const changeConfiguration = (config) => { + const { + segmentDurationMilliseconds, + packetSizeBitCount, + dataSizeBitCount, + dataSizeCrcBitCount, + bitsPerSegment, + packetEncoding, + packetEncodingBitCount, + packetDecodingBitCount + } = config; + SEGMENT_DURATION = segmentDurationMilliseconds; + PACKET_SIZE_BITS = packetSizeBitCount; + DATA_SIZE_BITS = dataSizeBitCount; + DATA_SIZE_CRC_BITS = dataSizeCrcBitCount; + BITS_PER_SEGMENT = bitsPerSegment; + PACKET_ENCODING = packetEncoding; + PACKET_ENCODING_SIZE = packetEncodingBitCount; + PACKET_DECODING_SIZE = packetDecodingBitCount; +} +export const setEncoding = (encoding) => { + ENCODING = encoding; +} +const encodePacket = (bits) => ENCODING.encode(bits); +export const getSegmentDurationMilliseconds = () => SEGMENT_DURATION; +export const getPacketMaxByteCount = () => 2 ** PACKET_SIZE_BITS; +export const getDataMaxByteCount = () => 2 ** DATA_SIZE_BITS; +export const getBitsPerSegment = () => BITS_PER_SEGMENT; +export const isPacketEncoded = () => PACKET_ENCODING; +export const packetEncodingBlockSize = () => isPacketEncoded() ? PACKET_ENCODING_SIZE : 0; +export const packetDecodingBlockSize = () => isPacketEncoded() ? PACKET_DECODING_SIZE : 0; +export const getPacketDataBitCount = () => { + if(isPacketEncoded()) return getPacketEncodingBlockCount() * PACKET_DECODING_SIZE; + return getPacketMaxBitCount(); +} +export function fromByteCountGetPacketLastUnusedBitCount(byteCount) { + const bitCount = byteCount * 8; + const availableBits = getPacketMaxBitCount(); + const dataBitsPerPacket = getPacketDataBitCount(); + let bitsInLastPacket = bitCount % dataBitsPerPacket; + let usedBits = bitsInLastPacket; + if(isPacketEncoded()) { + const blocks = Math.ceil(usedBits / packetDecodingBlockSize()) + usedBits = blocks * packetEncodingBlockSize(); + } + return availableBits - usedBits; +} +export function getPacketLastSegmentUnusedBitCount() { + return (BITS_PER_SEGMENT - (getPacketMaxBitCount() % BITS_PER_SEGMENT)); +} +export const getBaud = () => { + return Math.floor(getBitsPerSegment() / getSegmentDurationSeconds()); +} +export const getEffectiveBaud = () => { + return Math.floor(getPacketDataBitCount() / getPacketDurationSeconds()); +} +export const getEncodedPacketDataBitCount = () => { + return isPacketEncoded() ? getPacketEncodingBitCount() : 0; +} +export const getPacketUsedBitCount = () => + isPacketEncoded() ? getPacketEncodingBitCount() : getPacketMaxBitCount(); +export const getPacketUnusedBitCount = () => getPacketMaxBitCount() - getPacketUsedBitCount(); +export const getMaxPackets = () => + Math.ceil((getDataMaxByteCount() * 8) / getPacketUsedBitCount()); +export const getMaxDurationMilliseconds = () => getMaxPackets() * getPacketDurationMilliseconds(); +export const getPacketEncodingBitCount = () => getPacketEncodingBlockCount() * packetEncodingBlockSize(); +export const canSendPacket = () => { + const maxBits = getPacketMaxBitCount(); + if(maxBits < 1) return false; + return isPacketEncoded() ? maxBits >= packetEncodingBlockSize() : true; +} +export const getPacketEncodingBlockCount = () => + isPacketEncoded() ? Math.floor(getPacketMaxBitCount() / packetEncodingBlockSize()) : 0; +export const getPacketizationHeaderBitCount = () => DATA_SIZE_BITS + DATA_SIZE_CRC_BITS; +export const getPacketizationBitCountFromBitCount = (bitCount) => bitCount + getPacketizationHeaderBitCount(); +export const getPacketizationBitCountFromByteCount = (byteCount) => + getPacketizationBitCountFromBitCount(byteCount * 8); +export const getPacketizationByteCountFromByteCount = (byteCount) => + Math.ceil(getPacketizationBitCountFromByteCount(byteCount) / 8); + +export const getPacketizationByteCountFromBitCount = bitCount => + Math.ceil(getPacketizationBitCountFromBitCount(bitCount) / 8); + +export const getDataTransferDurationMillisecondsFromByteCount = (byteCount) => + getDataTransferDurationMilliseconds(getPacketizationBitCountFromByteCount(byteCount)); +export const getDataTransferDurationSeconds = (bitCount) => + getDataTransferDurationMilliseconds(bitCount) / 1000; +export const getPacketCount = (bitCount) => + canSendPacket() ? Math.ceil(bitCount / getPacketDataBitCount()) : 0; +export const getDataTransferDurationMilliseconds = (bitCount) => + getPacketCount(bitCount) * getPacketDurationMilliseconds(); +export const getPacketDurationSeconds = () => getPacketDurationMilliseconds() / 1000; +export const getSegmentDurationSeconds = () => getSegmentDurationMilliseconds() / 1000; +export const getPacketMaxBitCount = () => getPacketMaxByteCount() * 8; +export const getPacketSegmentCount = () => Math.ceil(getPacketMaxBitCount() / getBitsPerSegment()); +export const getPacketDurationMilliseconds = () => + getPacketSegmentCount() * getSegmentDurationMilliseconds(); +export const getPacketIndex = (transferStartedMilliseconds, time) => + Math.floor((time - transferStartedMilliseconds) / getPacketDurationMilliseconds()); + +export 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); + + return isPacketEncoded() ? encodePacket(packetBits) : packetBits; +} +export const getPacketBits = (bits, packetIndex) => + canSendPacket() ? getPacketUsedBits(bits, packetIndex) : []; + +export function getPacketSegmentIndex(transferStartedMilliseconds, time) { + return getTranserSegmentIndex(transferStartedMilliseconds, time) % getPacketSegmentCount(); +} +export function getTranserSegmentIndex(transferStartedMilliseconds, time) { + const transferMs = time - transferStartedMilliseconds; + const segmentMs = getSegmentDurationMilliseconds(); + return Math.floor(transferMs / segmentMs); +} +export function getPacketSegmentStartMilliseconds(transferStartedMilliseconds, packetIndex, segmentIndex) { + const packetStart = getPacketStartMilliseconds(transferStartedMilliseconds, packetIndex); + const segmentOffset = segmentIndex * getSegmentDurationMilliseconds(); + return packetStart + segmentOffset; +} +export function getPacketStartMilliseconds(transferStartedMilliseconds, packetIndex) { + if(packetIndex < 0) return 0; + if(packetIndex === 0) return transferStartedMilliseconds; + return transferStartedMilliseconds + (packetIndex * getPacketDurationMilliseconds()); +} +export function getPacketSegmentEndMilliseconds(transferStartedMilliseconds, packetIndex, segmentIndex) { + return getPacketSegmentStartMilliseconds(transferStartedMilliseconds, packetIndex, segmentIndex + 1) - 0.1; +} diff --git a/Randomizer.js b/Randomizer.js new file mode 100644 index 0000000..62fa682 --- /dev/null +++ b/Randomizer.js @@ -0,0 +1,72 @@ +const UNICODE_TEXT = [ + '\u0041', // Latin + '\u0410', // Cyrillic + '\u0391', // Greek + '\u05D0', // Hebrew + '\u0627', // Arabic + '\u0905', // Devanagari + '\u0E01', // Thai + '\u3042', // Japanese Hiragana + '\u30A2', // Japanese Katakana + '\uAC00', // Korean Hangul + '\u10D0', // Georgian + '\u0531', // Armenian + '\u4E00', // Chinese + '\u0F40', // Tibetan + '\u0985', // Bengali + '\u0A85', // Gujarati + '\u0A05', // Gurmukhi + '\u0C85', // Kannada + '\u1780', // Khmer + '\u0E81', // Lao + '\u0D05', // Malayalam + '\u1000', // Myanmar + '\u0D85', // Sinhala + '\u0B85', // Tamil + '\u0C05', // Telugu + '\u1200', // Amharic + '\u1000', // Burmese + '\u0C85', // Kannada + '\u0B05', // Oriya + '\u0D85' // Sinhala +]; +const UNICODE_EMOJI = [ + "\u{1F600}", "\u{1F601}", "\u{1F602}", "\u{1F923}", "\u{1F603}", "\u{1F604}", "\u{1F605}", "\u{1F606}", + "\u{1F609}", "\u{1F60A}", "\u{1F60B}", "\u{1F60E}", "\u{1F60D}", "\u{1F618}", "\u{1F617}", "\u{1F619}", + "\u{1F61A}", "\u{1F61B}", "\u{263A}", "\u{1F642}", "\u{1F60F}", "\u{1F60C}", "\u{1F61C}", "\u{1F61D}", + "\u{1F61E}", "\u{1F61F}", "\u{1F612}", "\u{1F613}", "\u{1F614}", "\u{1F615}", "\u{1F643}", "\u{1F610}", + "\u{1F611}", "\u{1F636}", "\u{1F607}", "\u{1F60F}", "\u{1F623}", "\u{1F625}", "\u{1F62E}", "\u{1F62F}", + "\u{1F62A}", "\u{1F62B}", "\u{1F634}", "\u{1F60D}", "\u{1F615}", "\u{1F625}", "\u{1F622}", "\u{1F62D}", + "\u{1F631}", "\u{1F616}", "\u{1F623}", "\u{1F624}", "\u{1F630}", "\u{1F621}", "\u{1F620}", "\u{1F637}", + "\u{1F912}", "\u{1F915}", "\u{1F922}", "\u{1F92A}", "\u{1F605}", "\u{1F624}", "\u{1F62C}", "\u{1F687}", + "\u{1F636}", "\u{1F610}", "\u{1F611}", "\u{1F974}", "\u{1F612}", "\u{1F644}", "\u{1F913}", "\u{1F615}", + "\u{1F62C}", "\u{1F636}", "\u{1F922}", "\u{1F927}", "\u{1F974}", "\u{1F975}", "\u{1F976}", "\u{1F92E}", + "\u{1F927}", "\u{1F976}", "\u{1F925}", "\u{1F92F}", "\u{1F975}", "\u{1F976}", "\u{1F92E}", "\u{1F925}", + "\u{1F924}", "\u{1F631}", "\u{1F634}", "\u{1F62C}", "\u{1F91E}", "\u{1F621}", "\u{1F608}", "\u{1F47F}", + "\u{1F480}", "\u{1F47B}", "\u{1F47D}", "\u{1F916}", "\u{1F608}", "\u{1F47A}", "\u{1F479}", "\u{1F47C}", + "\u{1F47E}", "\u{1F916}", "\u{1F4A9}", "\u{1F608}", "\u{1F4A4}", "\u{1F525}", "\u{1F4A3}", "\u{1F52E}", + "\u{1F4A2}", "\u{1F4A1}", "\u{1F6A8}", "\u{1F3B6}", "\u{1F519}", "\u{1F5E8}", "\u{1F4F3}", "\u{1F4F1}", + "\u{1F4F2}", "\u{1F514}", "\u{1F3A4}", "\u{1F4F9}", "\u{1F4F7}", "\u{1F4F8}", "\u{1F4F4}", "\u{1F4F6}", + "\u{1F3AF}", "\u{1F4FD}", "\u{1F4FC}", "\u{1F4E5}" +]; +const ASCII_CHARS = new Array(255) + .fill(0) + .map((_, code) => String.fromCharCode(code)) + .filter((_, code) => + // printable ascii + (code >= 32 && code <= 126) || + // extended ascii (accented letters, currency, etc.) + (code >= 160 && code <= 255) +); +const PRINTABLE_CHARS = [ + ...UNICODE_EMOJI, + ...ASCII_CHARS, + ...UNICODE_TEXT +]; + +function randomCharacter() { + const index = Math.floor(Math.random() * PRINTABLE_CHARS.length); + return PRINTABLE_CHARS[index]; +} + +export const text = (length) => new Array(length).fill(0).map(randomCharacter).join(''); diff --git a/StreamManager.js b/StreamManager.js new file mode 100644 index 0000000..4714b0e --- /dev/null +++ b/StreamManager.js @@ -0,0 +1,111 @@ +import { bitsToInt } from "./converters"; + +const BITS = []; +let BITS_PER_PACKET = 0; +let SEGMENTS_PER_PACKET = 0; +let BITS_PER_SEGMENT = 0; +let STREAM_HEADERS = []; +let SEGMENT_ENCODING = { + encode: bits => bits, + decode: bits => bits +}; +let PACKET_ENCODING = { + encode: bits => bits, + decode: bits => bits +} + +export const changeConfiguration = ({ + segmentsPerPacket, + bitsPerPacket, + bitsPerSegment, + streamHeaders +}) => { + BITS_PER_PACKET = bitsPerPacket; + SEGMENTS_PER_PACKET = segmentsPerPacket; + BITS_PER_SEGMENT = bitsPerSegment; + STREAM_HEADERS = streamHeaders; +} +const noEncoding = bits => bits; +export const setSegmentEncoding = ({ encode, decode } = {}) => { + SEGMENT_ENCODING.encode = encode ?? noEncoding; + SEGMENT_ENCODING.decode = decode ?? noEncoding; +} +export const setPacketEncoding = ({ encode, decode } = {}) => { + PACKET_ENCODING.encode = encode ?? noEncoding; + PACKET_ENCODING.decode = decode ?? noEncoding; +} +export const addBits = ( + packetIndex, + segmentIndex, + bits +) => { + if(BITS[packetIndex] === undefined) { + BITS[packetIndex] = []; + } + BITS[packetIndex][segmentIndex] = bits; +} +export const getPacketReceivedCount = () => { + if(BITS.length === 0) return 1; + return BITS.length; +} +export const getStreamBits = () => { + const bits = []; + const packetCount = getPacketReceivedCount(); + for(let packetIndex = 0; packetIndex < packetCount; packetIndex++) { + const packet = BITS[packetIndex] ?? []; + for(let segmentIndex = 0; segmentIndex < SEGMENTS_PER_PACKET; segmentIndex++) { + let segment = packet[segmentIndex] ?? []; + for(let bitIndex = 0; bitIndex < BITS_PER_SEGMENT; bitIndex++) { + const bit = segment[bitIndex]; + bits.push(bit ?? 0); + } + } + } + return bits; +} +export const getPacketSegmentBits = (packetIndex, segmentIndex) => BITS[packetIndex]?.[segmentIndex]; +export const getAllPacketBits = () => { + const packetCount = getPacketReceivedCount(); + const bits = []; + for(let packetIndex = 0; packetIndex < packetCount; packetIndex++) { + bits.push(...getPacketBits(packetIndex)); + } + return bits; +} +export const getPacketBits = (packetIndex, defaultBit = 0) => { + const bits = []; + const packet = BITS[packetIndex] ?? []; + for(let segmentIndex = 0; segmentIndex < SEGMENTS_PER_PACKET; segmentIndex++) { + let segment = packet[segmentIndex] ?? []; + segment = SEGMENT_ENCODING.decode(segment); + for(let bitIndex = 0; bitIndex < BITS_PER_SEGMENT; bitIndex++) { + const bit = segment[bitIndex]; + bits.push(bit ?? defaultBit); + if(bits.length === BITS_PER_PACKET) return bits; + } + } + while(bits.length < BITS_PER_PACKET) bits.push(defaultBit); + return bits; +} +export const getPacketBitsDecoded = (packetIndex, defaultBit = 0) => { + const bits = getPacketBits(packetIndex, defaultBit); + return PACKET_ENCODING.decode(bits); +} +const getStreamHeaderBits = name => { + const header = STREAM_HEADERS[name]; + if(!header) return []; + const { index, length } = header; + const packetCount = getPacketReceivedCount(); + const bits = []; + for(let packetIndex = 0; packetIndex < packetCount; packetIndex++) { + bits.push(...getPacketBitsDecoded(packetIndex, 1)); + if(bits.length >= index + length) break; + } + return bits.slice(index, index + length); +} +export const getTransferByteCount = () => { + const name = 'transfer byte count'; + const length = STREAM_HEADERS[name].length; + const bits = getStreamHeaderBits(name); + return bitsToInt(bits, length); +} diff --git a/converters.js b/converters.js new file mode 100644 index 0000000..bcc510b --- /dev/null +++ b/converters.js @@ -0,0 +1,10 @@ +export const bitsToInt = (bits, bitLength) => { + parseInt(bits + // only grab the bits we need + .slice(0, bitLength) + // combine into string + .join('') + // Assume missing bits were zeros + .padEnd(bitLength, '0') + ); +} diff --git a/index.html b/index.html index c5b6993..bc7bdf7 100644 --- a/index.html +++ b/index.html @@ -4,80 +4,89 @@ Data Over Audio - +

Data Over Audio

-

Message

-
- -
-

-
-
-

Data

- Original Bytes:
- Packetization Bytes:
- Packetization Bits:
- Bits per Packet:
- Data Bits per Packet:
- Error Correction:
- Error Blocks per packet:
- Error Bits per block:
- Error Correcting Bits per Packet:
- Unused bits per packet:
- Unused bits in last packet:
- Last segment unused channels in packet:
- Packet Count:
- Segments Per Packet:
- Total Segments:
- Segment Duration:
- Packet Duration:
- Total Duration:
-
-
-

Error Correcting

-

-
-
-

Bits Sent

-

-
-
- -

Bits Received

-

- Error Percent: N/A% -
-
-

Encoded Bits Received

-

- Error Percent: N/A% -
-
-

Decoded Bits Received

-

- Error Percent: N/A%
-
-
-

Decoded

- Bytes: N/A
- Length CRC-8: N/A
-
-
+

Communications

+
+

Send

+
+
+

Receive

+ +
- Text: -

-
-
+
+
+

Message

+
+ +
+

Received

+
+
+
+

+ +
+
+
+

Configuration

+
+

Audio

+ Wave Form:
+ Minimum Frequency:
+ Maximum Frequency:
+ Segment Duration: ms
+ FFT Size: 2^
+ Frequency Resolution Multiplier:
+ Channel Frequency Resolution Padding:
+ +

Packetization

+ Packet Size: + 2^ + +
+

Receiving

+ Amplitude Threshold:
+ Last Segment Percent: %
+ Smoothing Time Constant:
+

Encoding

+
+
+
+
+
+

Channels

+
+
    +
    +
    +

    Frequency Graph

    +



    Max Segments Displayed:
    +
    + +
    +

    Packetization Configuration

    +
    + Maximum Data:
    + Maximum Packets:
    + Maximum Duration:
    +

    Speed

    + Packetization:
    + Data Only:
    +
    +
    +
    +

    Packet Configuration

    +
    + Bytes per packet:
    + Bits per Packet:
    +

    Encoding

    + Packet Encoding:
    + Bits per block:
    + Blocks per packet:
    + Encoding bits per Packet:
    +

    Utilization

    + Data Bits per Packet:
    + Unused bits per packet:
    +

    Segments

    + Segments Per Packet:
    + Bits per segment: N/A
    + Last segment unused bits:
    +
    +
    +
    +

    Data

    +
    + Original Bytes:
    + Packetization Bytes:
    + Packetization Bits:
    + Packet Count:
    + Total Segments:
    +

    Transfer Time

    + Per Segment:
    + Per Packet:
    + Total:
    +

    Utilization

    + Unused bits in last packet:
    +
    +
    +
    +

    Original Data to send

    +
    +

    +
    +
    +
    +

    Encoded packets to send

    +
    +
    +
    +
    +
    +

    Encoded segments to send

    +
    +

    +
    +
    +
    +

    Decoded

    +
    + Bytes: N/A
    + Length CRC-8: N/A
    +

    Errors

    + Encoded Segments: N/A%
    + Encoded Packets: N/A%
    + Decoded Packets: N/A%
    +
    +
    +
    +

    Encoded Segments Received

    +
    +

    +
    +
    +
    +

    Encoded Packets Received

    +
    +

    +
    +
    +
    +

    Decoded Packets Received

    +
    +

    +
    +

    Selected

    +
    Channel: N/A
    Segment: N/A
    Bit: N/A
    -
    -

    Configuration

    - Wave Form:
    - Packet Size: - 2^ - -
    - Segment Duration: ms
    - Amplitude Threshold:
    - Minimum Frequency:
    - Maximum Frequency:
    - Last Segment Percent: %
    - FFT Size: 2^
    - Frequency Resolution Multiplier:
    - Channel Frequency Resolution Padding:
    - Smoothing Time Constant:
    -

    Data Integrity

    -
    -
    -
    -
    -

    Packet

    - Data Bits: N/A Bytes: N/A
    - Data Size Header Bits: N/A
    - Error Correction Bits: N/A
    - Total Bits: N/A Bytes: N/A
    - Channels: N/A
    - Segment Count: N/A
    - Segment Duration: N/As
    - Total Duration: N/As
    -
    -
    -

    Information

    +

    Audio Spectrum

    +
    Frequency Resolution: N/A
    Frequency Count: N/A
    - Samples Per Bit: 0
    Sample Rate: N/A per second.
    - Segments per second: N/A
    - Bits per segment: N/A
    +
    -

    Speed

    - Baud: N/A
    - Bytes/s: N/A
    - Effective Baud: N/A
    - Effective Bytes/s: N/A
    +

    Receiving

    +
    + Samples Per Bit: 0
    +
    -

    Channels

    -
    -
      - +

      Channel Tuning

      +
      +
      +
      diff --git a/index.js b/index.js index ec124fd..6c2fee5 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,16 @@ +import * as StreamManager from "./StreamManager"; +import * as HammingEncoding from './HammingEncoding'; +import * as InterleaverEncoding from './InterleaverEncoding'; +import * as PacketUtils from './PacketUtils'; +import * as Humanize from './Humanize'; +import * as Randomizer from './Randomizer'; +import * as AudioSender from './AudioSender'; +import * as CRC from './CRC.js'; + var audioContext; var sendButton; var textToSend; var isListeningCheckbox; -var samplesPerBitLabel; var microphoneStream; var microphoneNode; var analyser; @@ -14,33 +22,15 @@ var MAX_AMPLITUDE = 300; // Higher than 255 to give us space const MAXIMUM_PACKETIZATION_SIZE_BITS = 16; const CRC_BIT_COUNT = 8; -// bits after error correction is applied -let RECEIVED_STREAM_DECODED_BITS = []; -let RECEIVED_PACKET_DECODED_BITS = []; // bits as they arrived -let RECEIVED_PACKET_RAW_BITS = []; -let RECEIVED_STREAM_RAW_BITS = []; -// all bits after interliving is removed and trimed to packet data size -let RECEIVED_STREAM_ENCODED_BITS = []; -let RECEIVED_PACKET_ENCODED_BITS = []; +let RECEIVED_SEGMENT_BITS = []; // RECEIVED_SEGMENT_BITS[packetIndex][segmentIndex][channel] = bit // bits as they are sent let SENT_ORIGINAL_TEXT = ''; -let SENT_ORIGINAL_BYTES = []; let SENT_ORIGINAL_BITS = []; // original bits let SENT_ENCODED_BITS = []; // bits with error encoding let SENT_TRANSFER_BITS = []; // bits sent in the transfer -const CHANNEL_OSCILLATORS = []; - -// bit stream -let bitStarted; -let lastBitStarted; -let bitEnded; -let bitHighStrength = []; -let bitLowStrength = []; -let lastSegmentIndex = 0; - // interval and timeout ids var pauseGraphId; let stopOscillatorsTimeoutId; @@ -49,7 +39,6 @@ var sampleIntervalIds = []; let EXCLUDED_CHANNELS = []; var TEXT_TO_SEND = "U"; -var RANDOM_COUNT = 19; var MAX_BITS_DISPLAYED_ON_GRAPH = 79; var SEGMENT_DURATION = 30; var AMPLITUDE_THRESHOLD_PERCENT = .75; @@ -73,96 +62,20 @@ let SEGMENT_OVER = -1; let SEGMENT_SELECTED = -1; var SEND_VIA_SPEAKER = false; -var LAST_STREAM_STARTED; +var RECEIVED_STREAM_START_MS = -1; +let RECEIVED_STREAM_END_MS = -1; var MINIMUM_INTERVAL_MS = 3; // DO NOT SET THIS BELOW THE BROWSERS MINIMUM "real" INTERVAL const SAMPLING_INTERVAL_COUNT = 2; -var frequencyOverTime = []; +let SAMPLES = []; +let SAMPLE_LAST_COLLECTED = 0; // time when sample was last collected + var bitStart = []; -var samplesPerBit = []; -var bitSampleCount = 0; var PAUSE = false; var PAUSE_AFTER_END = true; var PACKET_SIZE_BITS = 5; // 32 bytes, 256 bits -const packetReceivedBits = []; -const packetUninterlievedBits = []; -const packetDecodedBits = []; -let packetDataByteCount = -1; -const UNICODE_TEXT = [ - '\u0041', // Latin - '\u0410', // Cyrillic - '\u0391', // Greek - '\u05D0', // Hebrew - '\u0627', // Arabic - '\u0905', // Devanagari - '\u0E01', // Thai - '\u3042', // Japanese Hiragana - '\u30A2', // Japanese Katakana - '\uAC00', // Korean Hangul - '\u10D0', // Georgian - '\u0531', // Armenian - '\u4E00', // Chinese - '\u0F40', // Tibetan - '\u0985', // Bengali - '\u0A85', // Gujarati - '\u0A05', // Gurmukhi - '\u0C85', // Kannada - '\u1780', // Khmer - '\u0E81', // Lao - '\u0D05', // Malayalam - '\u1000', // Myanmar - '\u0D85', // Sinhala - '\u0B85', // Tamil - '\u0C05', // Telugu - '\u1200', // Amharic - '\u1000', // Burmese - '\u0C85', // Kannada - '\u0B05', // Oriya - '\u0D85' // Sinhala -]; -const UNICODE_EMOJI = [ - "\u{1F600}", "\u{1F601}", "\u{1F602}", "\u{1F923}", "\u{1F603}", "\u{1F604}", "\u{1F605}", "\u{1F606}", - "\u{1F609}", "\u{1F60A}", "\u{1F60B}", "\u{1F60E}", "\u{1F60D}", "\u{1F618}", "\u{1F617}", "\u{1F619}", - "\u{1F61A}", "\u{1F61B}", "\u{263A}", "\u{1F642}", "\u{1F60F}", "\u{1F60C}", "\u{1F61C}", "\u{1F61D}", - "\u{1F61E}", "\u{1F61F}", "\u{1F612}", "\u{1F613}", "\u{1F614}", "\u{1F615}", "\u{1F643}", "\u{1F610}", - "\u{1F611}", "\u{1F636}", "\u{1F607}", "\u{1F60F}", "\u{1F623}", "\u{1F625}", "\u{1F62E}", "\u{1F62F}", - "\u{1F62A}", "\u{1F62B}", "\u{1F634}", "\u{1F60D}", "\u{1F615}", "\u{1F625}", "\u{1F622}", "\u{1F62D}", - "\u{1F631}", "\u{1F616}", "\u{1F623}", "\u{1F624}", "\u{1F630}", "\u{1F621}", "\u{1F620}", "\u{1F637}", - "\u{1F912}", "\u{1F915}", "\u{1F922}", "\u{1F92A}", "\u{1F605}", "\u{1F624}", "\u{1F62C}", "\u{1F687}", - "\u{1F636}", "\u{1F610}", "\u{1F611}", "\u{1F974}", "\u{1F612}", "\u{1F644}", "\u{1F913}", "\u{1F615}", - "\u{1F62C}", "\u{1F636}", "\u{1F922}", "\u{1F927}", "\u{1F974}", "\u{1F975}", "\u{1F976}", "\u{1F92E}", - "\u{1F927}", "\u{1F976}", "\u{1F925}", "\u{1F92F}", "\u{1F975}", "\u{1F976}", "\u{1F92E}", "\u{1F925}", - "\u{1F924}", "\u{1F631}", "\u{1F634}", "\u{1F62C}", "\u{1F91E}", "\u{1F621}", "\u{1F608}", "\u{1F47F}", - "\u{1F480}", "\u{1F47B}", "\u{1F47D}", "\u{1F916}", "\u{1F608}", "\u{1F47A}", "\u{1F479}", "\u{1F47C}", - "\u{1F47E}", "\u{1F916}", "\u{1F4A9}", "\u{1F608}", "\u{1F4A4}", "\u{1F525}", "\u{1F4A3}", "\u{1F52E}", - "\u{1F4A2}", "\u{1F4A1}", "\u{1F6A8}", "\u{1F3B6}", "\u{1F519}", "\u{1F5E8}", "\u{1F4F3}", "\u{1F4F1}", - "\u{1F4F2}", "\u{1F514}", "\u{1F3A4}", "\u{1F4F9}", "\u{1F4F7}", "\u{1F4F8}", "\u{1F4F4}", "\u{1F4F6}", - "\u{1F3AF}", "\u{1F4FD}", "\u{1F4FC}", "\u{1F4E5}" -]; -const ASCII_CHARS = new Array(255) - .fill(0) - .map((_, code) => String.fromCharCode(code)) - .filter((_, code) => - // printable ascii - (code >= 32 && code <= 126) || - // extended ascii (accented letters, currency, etc.) - (code >= 160 && code <= 255) -); -const PRINTABLE_CHARS = [ - ...UNICODE_EMOJI, - ...ASCII_CHARS, - ...UNICODE_TEXT -]; - -function randomCharacter() { - const index = Math.floor(Math.random() * PRINTABLE_CHARS.length); - return PRINTABLE_CHARS[index]; -} function handleWindowLoad() { - TEXT_TO_SEND = new Array(RANDOM_COUNT) - .fill(0) - .map(randomCharacter).join(''); - console.log('text', TEXT_TO_SEND); + TEXT_TO_SEND = Randomizer.text(5); // grab dom elements sendButton = document.getElementById('send-button'); @@ -172,8 +85,7 @@ function handleWindowLoad() { textToSend = document.getElementById('text-to-send'); textToSend.value = TEXT_TO_SEND; sentDataTextArea = document.getElementById('sent-data'); - samplesPerBitLabel = document.getElementById('samples-per-bit'); - receivedChannelGraph = document.getElementById('received-channel-graph'); + const receivedChannelGraph = document.getElementById('received-channel-graph'); receivedChannelGraph.addEventListener('mouseover', handleReceivedChannelGraphMouseover); receivedChannelGraph.addEventListener('mouseout', handleReceivedChannelGraphMouseout); receivedChannelGraph.addEventListener('mousemove', handleReceivedChannelGraphMousemove); @@ -181,23 +93,28 @@ function handleWindowLoad() { document.getElementById('wave-form').value = WAVE_FORM; document.getElementById('wave-form').addEventListener('change', (event) => { WAVE_FORM = event.target.value; + configurationChanged(); }); document.getElementById('packet-size-power').value = PACKET_SIZE_BITS; - document.getElementById('packet-size').innerText = friendlyByteSize(2 ** PACKET_SIZE_BITS); + document.getElementById('packet-size').innerText = Humanize.byteSize(2 ** PACKET_SIZE_BITS); 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('packet-size').innerText = Humanize.byteSize(2 ** PACKET_SIZE_BITS); + configurationChanged(); }); document.getElementById('pause-after-end').checked = PAUSE_AFTER_END; document.getElementById('error-correction-hamming').checked = HAMMING_ERROR_CORRECTION; document.getElementById('error-correction-hamming').addEventListener('change', event => { HAMMING_ERROR_CORRECTION = event.target.checked; - showSpeed(); + configurationChanged(); }) document.getElementById('periodic-interleaving').checked = PERIODIC_INTERLEAVING; document.getElementById('periodic-interleaving').addEventListener('change', event => { PERIODIC_INTERLEAVING = event.target.checked; + configurationChanged(); + StreamManager.setSegmentEncoding( + PERIODIC_INTERLEAVING ? InterleaverEncoding : undefined + ); }); document.getElementById('pause-after-end').addEventListener('change', event => { PAUSE_AFTER_END = event.target.checked; @@ -206,22 +123,21 @@ function handleWindowLoad() { document.getElementById('send-via-speaker').checked = SEND_VIA_SPEAKER; document.getElementById('send-via-speaker').addEventListener('input', event => { SEND_VIA_SPEAKER = event.target.checked; + configurationChanged(); }) document.getElementById('frequency-resolution-multiplier').value = FREQUENCY_RESOLUTION_MULTIPLIER; document.getElementById('frequency-resolution-multiplier').addEventListener('input', event => { FREQUENCY_RESOLUTION_MULTIPLIER = parseInt(event.target.value); - showSpeed(); + configurationChanged(); }) document.getElementById('channel-frequency-resolution-padding').value = CHANNEL_FREQUENCY_RESOLUTION_PADDING; document.getElementById('channel-frequency-resolution-padding').addEventListener('input', event => { CHANNEL_FREQUENCY_RESOLUTION_PADDING = parseInt(event.target.value); - showSpeed(); + configurationChanged(); }) document.getElementById('bit-duration-text').addEventListener('input', (event) => { SEGMENT_DURATION = parseInt(event.target.value); - bitSampleCount = 0; - samplesPerBit.length = 0; - showSpeed(); + configurationChanged(); }); document.getElementById('max-bits-displayed-on-graph').value= MAX_BITS_DISPLAYED_ON_GRAPH; document.getElementById('max-bits-displayed-on-graph').addEventListener('input', (event) => { @@ -239,14 +155,15 @@ function handleWindowLoad() { document.getElementById('amplitude-threshold-text').addEventListener('input', (event) => { AMPLITUDE_THRESHOLD_PERCENT = parseInt(event.target.value) / 100; AMPLITUDE_THRESHOLD = Math.floor(AMPLITUDE_THRESHOLD_PERCENT * 255); + configurationChanged(); }); document.getElementById('maximum-frequency').addEventListener('input', (event) => { MAXIMUM_FREQUENCY = parseInt(event.target.value); - showSpeed(); + configurationChanged(); }); document.getElementById('minimum-frequency').addEventListener('input', (event) => { MINIMUM_FREQUENCY = parseInt(event.target.value); - showSpeed(); + configurationChanged(); }); document.getElementById('last-bit-percent').addEventListener('input', (event) => { LAST_SEGMENT_PERCENT = parseInt(event.target.value) / 100; @@ -254,10 +171,9 @@ function handleWindowLoad() { document.getElementById('fft-size-power-text').addEventListener('input', (event) => { FFT_SIZE_POWER = parseInt(event.target.value); if(analyser) analyser.fftSize = 2 ** FFT_SIZE_POWER; - updateFrequencyResolution(); + configurationChanged(); resetGraphData(); }); - updateFrequencyResolution(); document.getElementById('smoothing-time-constant-text').addEventListener('input', event => { SMOOTHING_TIME_CONSTANT = parseFloat(event.target.value); if(analyser) analyser.smoothingTimeConstant = SMOOTHING_TIME_CONSTANT; @@ -266,45 +182,8 @@ function handleWindowLoad() { // wire up events sendButton.addEventListener('click', handleSendButtonClick); isListeningCheckbox.addEventListener('click', handleListeningCheckbox); - textToSend.addEventListener('input', handleTextToSendInput); - handleTextToSendInput(); - showSpeed(); -} -function friendlyByteSize(count) { - let unitIndex = 0; - const units = ['bytes', 'kb', 'mb', 'gb', 'tb', 'pb']; - while(count > 900) { - count /= 1024; - unitIndex++; - if(unitIndex === units.length - 1) break; - } - count = Math.floor(count * 10) * 0.1 - return `${count.toLocaleString()} ${units[unitIndex]}` -} - -function handleTextToSendInput() { - const text = textToSend.value; - const dataByteCount = text.length; - const dataBitCount = dataByteCount * 8; - 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); - const channelCount = getChannels().length; - const segmentCount = Math.ceil(totalBits / channelCount); - const totalDuration = ((segmentCount * SEGMENT_DURATION) / 1000); - - document.getElementById('error-correction-bits').innerText = errorCorrectionBits.toLocaleString(); - document.getElementById('data-bytes-to-send').innerText = dataByteCount.toLocaleString(); - document.getElementById('data-bits-to-send').innerText = dataBitCount.toLocaleString(); - document.getElementById('total-bytes-to-send').innerText = totalBytes.toLocaleString(); - document.getElementById('total-bits-to-send').innerText = totalBits.toLocaleString(); - document.getElementById('duration-to-send').innerText = totalDuration.toLocaleString(); - document.getElementById('packet-send-channel-count').innerText = channelCount.toLocaleString(); - 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(); + textToSend.addEventListener('input', configurationChanged); + configurationChanged(); } function updateFrequencyResolution() { @@ -314,33 +193,10 @@ function updateFrequencyResolution() { const frequencyCount = (sampleRate/2) / frequencyResolution; document.getElementById('frequency-resolution').innerText = frequencyResolution.toFixed(2); document.getElementById('frequency-count').innerText = frequencyCount.toFixed(2); - - showSpeed(); } -function showSpeed() { - const segmentsPerSecond = 1000 / SEGMENT_DURATION; - const channels = getChannels(); +function showChannelList() { const allChannels = getChannels(true); - const bitsPerSegment = channels.length; - const baud = bitsPerSegment * segmentsPerSecond; - const bytes = baud / 8; - document.getElementById('durations-per-second').innerText = segmentsPerSecond.toFixed(2); - document.getElementById('bits-per-duration').innerText = bitsPerSegment; - 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 * 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); - } else { - const effectiveBaud = baud; - 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); - } - const channelList = document.getElementById('channel-list'); channelList.innerHTML = ""; allChannels.forEach(([low, high], i) => { @@ -356,150 +212,126 @@ function showSpeed() { } else { EXCLUDED_CHANNELS.push(i); } - showSpeed(); + configurationChanged(); }) label.append(checkbox); const text = document.createTextNode(`Low: ${low} Hz High: ${high} Hz`); label.append(text); channelList.appendChild(li); }) - handleTextToSendInput(); drawChannels(); +} + +function handleAudioSenderStart() { + sendButton.innerText = 'Stop'; +} +function handleAudioSenderStop() { + sendButton.innerText = 'Send'; +} +function handleAudioSenderSend(bit) { + SENT_TRANSFER_BITS.push(bit); +} +function configurationChanged() { + updatePacketUtils(); + updateStreamManager(); + updateAudioSender(); + showChannelList(); + updateFrequencyResolution(); updatePacketStats(); } +function updateAudioSender() { + AudioSender.changeConfiguration({ + channels: getChannels(), + destination: SEND_VIA_SPEAKER ? audioContext.destination : getAnalyser(), + startCallback: handleAudioSenderStart, + stopCallback: handleAudioSenderStop, + sendCallback: handleAudioSenderSend, + waveForm: WAVE_FORM + }); +} +function updateStreamManager() { + StreamManager.setPacketEncoding( + HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined + ); + StreamManager.changeConfiguration({ + bitsPerPacket: PacketUtils.getPacketMaxBitCount(), + segmentsPerPacket: PacketUtils.getPacketSegmentCount(), + bitsPerSegment: getChannels().length, + streamHeaders: { + 'transfer byte count': { + index: 0, + length: MAXIMUM_PACKETIZATION_SIZE_BITS + }, + 'transfer byte count crc': { + index: MAXIMUM_PACKETIZATION_SIZE_BITS, + length: CRC_BIT_COUNT + }, + } + }); +} +function updatePacketUtils() { + PacketUtils.setEncoding( + HAMMING_ERROR_CORRECTION ? HammingEncoding : undefined + ); + const bitsPerSegment = getChannels().length; + PacketUtils.changeConfiguration({ + segmentDurationMilliseconds: SEGMENT_DURATION, + packetSizeBitCount: PACKET_SIZE_BITS, + dataSizeBitCount: MAXIMUM_PACKETIZATION_SIZE_BITS, + dataSizeCrcBitCount: CRC_BIT_COUNT, + bitsPerSegment, + packetEncoding: HAMMING_ERROR_CORRECTION, + packetEncodingBitCount: ERROR_CORRECTION_BLOCK_SIZE, + packetDecodingBitCount: ERROR_CORRECTION_DATA_SIZE, + }); +} function updatePacketStats() { const text = textToSend.value; const bits = textToBits(text); - const bitCount = getPacketizationBitCount(bits.length); - const byteCount = bitCount / 8; + const byteCount = text.length; + const bitCount = PacketUtils.getPacketizationBitCountFromBitCount(bits.length);; + + // # Packetization + document.getElementById('packetization-max-bytes').innerText = Humanize.byteSize(PacketUtils.getDataMaxByteCount()); + document.getElementById('packetization-max-packets').innerText = PacketUtils.getMaxPackets().toLocaleString(); + document.getElementById('packetization-max-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getMaxDurationMilliseconds()); + // ## Packetization Speed + document.getElementById('packetization-speed-bits-per-second').innerText = Humanize.bitsPerSecond(PacketUtils.getBaud()); + document.getElementById('packetization-speed-effective-bits-per-second').innerText = Humanize.bitsPerSecond(PacketUtils.getEffectiveBaud()); + + // Data document.getElementById('original-byte-count').innerText = textToBytes(text).length.toLocaleString(); - document.getElementById('packetization-byte-count').innerText = byteCount.toLocaleString(); + document.getElementById('packetization-byte-count').innerText = PacketUtils.getPacketizationByteCountFromBitCount(bits.length).toLocaleString(); document.getElementById('packetization-bit-count').innerText = bitCount.toLocaleString(); - document.getElementById('packet-bit-count').innerText = getPacketBitCount().toLocaleString(); - document.getElementById('packet-count').innerText = getPacketCount(bitCount).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(bitCount).toLocaleString(); - document.getElementById('last-segment-unused-channel-count').innerText = getPacketLastSegmentUnusedChannelCount().toLocaleString() - document.getElementById('packet-transfer-duration').innerText = getPacketDurationSeconds().toLocaleString() + 's'; - document.getElementById('segment-transfer-duration').innerText = getSegmentTransferDurationSeconds().toLocaleString() + 's'; - document.getElementById('data-transfer-duration').innerText = getDataTransferDurationSeconds(bitCount).toLocaleString() + 's'; - document.getElementById('segments-per-packet').innerText = getPacketSegmentCount().toLocaleString(); + document.getElementById('packet-count').innerText = PacketUtils.getPacketCount(bitCount).toLocaleString(); + // # Packet Config + document.getElementById('bits-per-packet').innerText = PacketUtils.getPacketMaxBitCount().toLocaleString(); + document.getElementById('bytes-per-packet').innerText = Humanize.byteSize(PacketUtils.getPacketMaxByteCount()); + // ## Packet Encoding + document.getElementById('packet-encoding').innerText = PacketUtils.isPacketEncoded() ? 'Yes' : 'No'; + document.getElementById('packet-encoding-block-count').innerText = PacketUtils.getPacketEncodingBlockCount().toLocaleString(); + document.getElementById('packet-encoding-bits-per-block').innerText = PacketUtils.packetEncodingBlockSize().toLocaleString(); + document.getElementById('packet-encoding-bit-count').innerText = PacketUtils.getEncodedPacketDataBitCount().toLocaleString(); + + document.getElementById('bits-per-segment').innerText = PacketUtils.getBitsPerSegment(); + + // Data + document.getElementById('packet-data-bit-count').innerText = PacketUtils.getPacketDataBitCount().toLocaleString(); + document.getElementById('packet-unused-bit-count').innerText = PacketUtils.getPacketUnusedBitCount().toLocaleString(); + document.getElementById('last-packet-unused-bit-count').innerText = PacketUtils.fromByteCountGetPacketLastUnusedBitCount(byteCount).toLocaleString(); + document.getElementById('last-segment-unused-bit-count').innerText = PacketUtils.getPacketLastSegmentUnusedBitCount().toLocaleString() + document.getElementById('packet-transfer-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getPacketDurationMilliseconds()); + document.getElementById('segment-transfer-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getSegmentDurationMilliseconds()); + document.getElementById('data-transfer-duration').innerText = Humanize.durationMilliseconds(PacketUtils.getDataTransferDurationMilliseconds(bitCount)); + document.getElementById('segments-per-packet').innerText = PacketUtils.getPacketSegmentCount().toLocaleString(); document.getElementById('total-segments').innerText = getTotalSegmentCount(bitCount).toLocaleString(); - document.getElementById('packet-error-bits-per-block').innerText = (HAMMING_ERROR_CORRECTION ? ERROR_CORRECTION_BLOCK_SIZE : 0).toLocaleString(); - document.getElementById('packet-error-bit-count').innerText = getPacketErrorBitCount(); } -function calcCrc( - bytes, - size, - polynomial, - { - initialization = 0, - reflectIn = false, - reflectOut = false, - xorOut = 0 - } = {} -) { - if(bytes.length === 0) return 0; - const validBits = (1 << size) - 1; - const mostSignificantBit = 1 << size - 1; - const bitsBeforeLastByte = size - 8; - // setup our initial value - let crc = initialization; - - function reverseBits(value, size) { - let reversed = 0; - for(let i = 0; i < size; i++) { - // if bit position is on - if(value & (1<= 32 && crc & mostSignificantBit) { - crc >>>= 0; - } - return crc; -} -function crc(bytes, bitCount) { - switch(bitCount) { - case 8: return crc8(bytes); - case 16: return crc16(bytes); - case 32: return crc32(bytes); - default: return 0; - } -} -function crc8(bytes) { return calcCrc(bytes, 8, 0x07); } -function crc16(bytes) { - return calcCrc( - bytes, - 16, - 0x8005, - { - initialization: 0, - reflectIn: true, - reflectOut: true, - xorOut: 0 - } - ); -} -function crc32(bytes) { - return calcCrc( - bytes, - 32, - 0x04C11DB7, - { - initialization: 0xFFFFFFFF, - reflectIn: true, - reflectOut: true, - xorOut: 0x0 - } - ); -} function drawChannels() { const sampleRate = getAudioContext().sampleRate; const fftSize = 2 ** FFT_SIZE_POWER; const frequencyResolution = sampleRate / fftSize; - //const frequencyCount = (sampleRate/2) / frequencyResolution; const channels = getChannels(); const channelCount = channels.length; const canvas = document.getElementById('channel-frequency-graph'); @@ -508,9 +340,6 @@ function drawChannels() { const channelHeight = height / channelCount; const bandHeight = channelHeight / 2; - const nyquistFrequency = audioContext.sampleRate / 2; - const frequencySegments = Math.floor(nyquistFrequency / frequencyResolution); - for(let i = 0; i < channelCount; i++) { const [low, high] = channels[i]; let top = channelHeight * i; @@ -545,108 +374,6 @@ function percentInFrequency(hz, frequencyResolution) { const percent = hzInSegement / frequencyResolution; return percent; } -function nibbleToHamming(nibble) { - if(nibble.length !== ERROR_CORRECTION_DATA_SIZE) return []; - return [ - nibble[0] ^ nibble[1] ^ nibble[3], - nibble[0] ^ nibble[2] ^ nibble[3], - nibble[0], - nibble[1] ^ nibble[2] ^ nibble[3], - nibble[1], - nibble[2], - nibble[3] - ] -} -function hammingToNibble(hamming) { - if(hamming.length !== ERROR_CORRECTION_BLOCK_SIZE) return []; - const error_1 = hamming[0] ^ hamming[2] ^ hamming[4] ^ hamming[6]; - const error_2 = hamming[1] ^ hamming[2] ^ hamming[5] ^ hamming[6]; - const error_3 = hamming[3] ^ hamming[4] ^ hamming[5] ^ hamming[6]; - let error = (error_3 << 2) | (error_2 << 1) | error_1; - if(error !== 0) { - // don't mutate the array - hamming = hamming.slice(); - hamming[error - 1] ^= 1; // flip - } - return [ - hamming[2], - hamming[4], - hamming[5], - hamming[6] - ]; -} - -function getFrequency(bit) { - return bit ? MAXIMUM_FREQUENCY : MINIMUM_FREQUENCY; -} -function removeInterleaving(bits) { - return applyInterleaving(bits, true); -} -function applyInterleaving(bits, undo = false) { - // Not turned on - if(!PERIODIC_INTERLEAVING) return bits; - - // Only applicable for error correction - if(!HAMMING_ERROR_CORRECTION) return bits; - - const channels = getChannels(); - const channelCount = channels.length; - - // We need at least 1 extra channel for one bit to escape the block - if(channelCount < ERROR_CORRECTION_BLOCK_SIZE + 1) return bits; - - const blockCount = Math.ceil(channelCount / ERROR_CORRECTION_BLOCK_SIZE); - // need another block to swap bits with - if(blockCount < 2) return bits; - - // make a copy - bits = bits.slice(); - - // ensure last segment has enough bits to swap - while(bits.length % channelCount !== 0) bits.push(0); - - // Loop through each segment - for(let i = 0; i < bits.length; i+= channelCount) { - - // Grab the bits for the segment - let segment = bits.slice(i, i + channelCount); - segment = staggerValues(segment, ERROR_CORRECTION_BLOCK_SIZE, undo); - - // update the bits with the modified segment - bits.splice(i, channelCount, ...segment); - } - return bits; -} - -function staggerValues(values, blockSize, undo) { - // loop through bit indexes of a block - for(let blockMovement = 1; blockMovement < blockSize; blockMovement++) { - values.filter((_, i) => - // values to be moved to different blocks - i % blockSize === blockMovement - ).map((_,i,a) => { - // bit values moved N blocks - if(undo) i -= blockMovement; else i += blockMovement; - i = ((i % a.length) + a.length) % a.length; - return a[i]; - }).forEach((v, i) => { - // replace with new values - values[blockMovement + (i * blockSize)] = v; - }) - }; - return values; -} - -function applyErrorCorrection(bits) { - if(!HAMMING_ERROR_CORRECTION) return bits; - const encodedBits = []; - 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; -} function getChannels(includeExcluded = false) { var audioContext = getAudioContext(); const sampleRate = audioContext.sampleRate; @@ -688,14 +415,13 @@ function sendBytes(bytes) { const bits = bytesToBits(bytes); SENT_ORIGINAL_TEXT = bytesToText(bytes); - SENT_ORIGINAL_BYTES = bytes; SENT_ORIGINAL_BITS = bits.slice(); // packetization headers // data length const dataLengthBits = numberToBits(bytes.length, MAXIMUM_PACKETIZATION_SIZE_BITS); // crc on data length - const dataLengthCrcBits = numberToBits(crc(bitsToBytes(dataLengthBits), CRC_BIT_COUNT), CRC_BIT_COUNT); + const dataLengthCrcBits = numberToBits(CRC.check(bitsToBytes(dataLengthBits), CRC_BIT_COUNT), CRC_BIT_COUNT); // prefix with headers bits.unshift(...dataLengthBits, ...dataLengthCrcBits); @@ -705,47 +431,37 @@ function sendBytes(bytes) { SENT_TRANSFER_BITS.length = 0; SENT_ENCODED_BITS.length = 0; - // add 100ms delay before sending - const startSeconds = audioContext.currentTime + 0.1; - const startMilliseconds = startSeconds * 1000; + AudioSender.setAudioContext(getAudioContext()); - const packetBitCount = getPacketBitCount(); - const packetDurationSeconds = getPacketDurationSeconds(); - const packetCount = getPacketCount(bitCount); - const totalDurationSeconds = getDataTransferDurationSeconds(bitCount); - const totalDurationMilliseconds = getDataTransferDurationMilliseconds(bitCount); + const startSeconds = AudioSender.now() + 0.1; + const packetBitCount = PacketUtils.getPacketMaxBitCount(); + const packetDurationSeconds = PacketUtils.getPacketDurationSeconds(); + const packetCount = PacketUtils.getPacketCount(bitCount); + const totalDurationSeconds = PacketUtils.getDataTransferDurationSeconds(bitCount); const channelCount = getChannels().length; const errorCorrectionBits = []; - const interleavingBits = []; - createOscillators(startSeconds); + AudioSender.beginAt(startSeconds); // send all packets for(let i = 0; i < packetCount; i++) { - let packet = getPacketBits(bits, i); + let packet = PacketUtils.getPacketBits(bits, i); errorCorrectionBits.push(...packet); SENT_ENCODED_BITS.push(...packet); if(packet.length > packetBitCount) { - console.error('Too many bits in the packet.'); - disconnectOscillators(); + console.error('Too many bits in the packet. tried to send %s, limited to %s', packet.length, packetBitCount); + AudioSender.stop(); return; } packet = padArray(packet, packetBitCount, 0); - packet = applyInterleaving(packet); - interleavingBits.push(...packet); sendPacket(packet, startSeconds + (i * packetDurationSeconds)); } - stopOscillators(startSeconds + totalDurationSeconds); - stopOscillatorsTimeoutId = window.setTimeout( - disconnectOscillators, - startMilliseconds + totalDurationMilliseconds - ); - // show what was sent + AudioSender.stopAt(startSeconds + totalDurationSeconds); // original bits document.getElementById('sent-data').innerHTML = SENT_ORIGINAL_BITS.reduce(bitReducer( - getPacketDataBitCount(), + PacketUtils.getPacketMaxBitCount(), HAMMING_ERROR_CORRECTION ? ERROR_CORRECTION_DATA_SIZE : 8 ), ''); @@ -753,7 +469,7 @@ function sendBytes(bytes) { if(HAMMING_ERROR_CORRECTION) { document.getElementById('error-correcting-data').innerHTML = SENT_ENCODED_BITS.reduce(bitReducer( - getPacketErrorBitCount(), + PacketUtils.getPacketDataBitCount(), ERROR_CORRECTION_BLOCK_SIZE ), ''); } else { @@ -762,7 +478,7 @@ function sendBytes(bytes) { document.getElementById('actual-bits-to-send').innerHTML = SENT_TRANSFER_BITS.reduce(bitReducer( - getPacketBitCount() + getPacketLastSegmentUnusedChannelCount(), + PacketUtils.getPacketMaxBitCount() + PacketUtils.getPacketLastSegmentUnusedBitCount(), channelCount, (packetIndex, blockIndex) => `${blockIndex === 0 ? '' : '
      '}Segment ${blockIndex}: ` ), ''); @@ -774,201 +490,36 @@ function sendPacket(bits, packetStartSeconds) { const channels = getChannels(); const channelCount = channels.length; let bitCount = bits.length; - const segmentDurationSeconds = getSegmentTransferDurationSeconds(); + const segmentDurationSeconds = PacketUtils.getSegmentDurationSeconds(); for(let i = 0; i < bitCount; i += channelCount) { - const segmentBits = bits.slice(i, i + channelCount); + let segmentBits = bits.slice(i, i + channelCount); + if(PERIODIC_INTERLEAVING) { + segmentBits = InterleaverEncoding.encode(segmentBits); + } const segmentIndex = Math.floor(i / channelCount); var offsetSeconds = segmentIndex * segmentDurationSeconds; - changeOscillators(segmentBits, packetStartSeconds + offsetSeconds); + AudioSender.send(segmentBits, packetStartSeconds + offsetSeconds); } } - -function getDataTransferDurationMilliseconds(bitCount) { - return getPacketCount(bitCount) * getPacketDurationMilliseconds(); +function getNextPacketStartMilliseconds(priorPacketStartMilliseconds) { + return priorPacketStartMilliseconds + PacketUtils.getPacketDurationMilliseconds(); } -function getDataTransferDurationSeconds(bitCount) { - return getDataTransferDurationMilliseconds(bitCount) / 1000; -} -function getPacketDurationMilliseconds() { - return getPacketSegmentCount() * SEGMENT_DURATION; +function getPacketIndexEndMilliseconds(transferStartedMilliseconds, packetIndex) { + const start = transferStartedMilliseconds + (PacketUtils.getPacketDurationMilliseconds() * packetIndex) + return getPacketEndMilliseconds(start); } function getPacketEndMilliseconds(packetStartedMilliseconds) { - return (packetStartedMilliseconds + getPacketDurationMilliseconds()) - 1; + return getNextPacketStartMilliseconds(packetStartedMilliseconds) - 0.1; } function getTotalSegmentCount(bitCount) { - return getPacketCount(bitCount) * getPacketSegmentCount(); -} -function getPacketDurationSeconds() { - return getPacketDurationMilliseconds() / 1000; -} -function getSegmentTransferDurationSeconds() { - return SEGMENT_DURATION / 1000; -} -function getPacketByteCount() { - return 2 ** PACKET_SIZE_BITS; -} -function getPacketizationBitCount(bitCount) { - return bitCount + MAXIMUM_PACKETIZATION_SIZE_BITS + CRC_BIT_COUNT; -} -function getPacketBitCount() { - return getPacketByteCount() * 8; -} -function getPacketSegmentCount() { - return Math.ceil(getPacketBitCount() / getChannels().length); -} -function getPacketCount(bitCount) { - 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(bitCount / dataBitCount); -} -function getPacketBits(bits, packetIndex) { - if(!canSendPacket()) return []; - return getPacketUsedBits(bits, packetIndex); -} -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 getPacketUsedBitCount() { - return HAMMING_ERROR_CORRECTION ? getPacketErrorBitCount() : getPacketBitCount(); -} -function getPacketErrorBitCount() { - return getPacketErrorBlockCount() * ERROR_CORRECTION_BLOCK_SIZE; -} -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() { - const bitsAvailable = getPacketBitCount(); - let bitsUsed = bitsAvailable; - if(HAMMING_ERROR_CORRECTION) { - bitsUsed = getPacketErrorBitCount(); - } - return bitsAvailable - bitsUsed; -} -function getPacketLastUnusedBitCount(bitCount) { - const availableBits = getPacketBitCount(); - const dataBitsPerPacket = getPacketDataBitCount(); - let usedBits = bitCount % dataBitsPerPacket; - if(HAMMING_ERROR_CORRECTION) { - const blocks = Math.ceil(usedBits / ERROR_CORRECTION_DATA_SIZE); - usedBits = blocks * ERROR_CORRECTION_BLOCK_SIZE; - } - 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; + return PacketUtils.getPacketCount(bitCount) * PacketUtils.getPacketSegmentCount(); } function padArray(values, length, value) { values = values.slice();//copy while(values.length < length) values.push(value); return values; } -function createOscillators(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); - } - sendButton.innerText = 'Stop'; - return oscillators; -} -function getOscillators() { - return CHANNEL_OSCILLATORS; -} -function changeOscillators(bits, startSeconds) { - const oscillators = getOscillators(); - getChannels().forEach((channel, i) => { - // missing bits past end of bit stream set to zero - const isHigh = bits[i] ?? 0; - SENT_TRANSFER_BITS.push(isHigh); - const oscillator = oscillators[i]; - // already at correct frequency - if(oscillator.on === isHigh) return; - oscillator.on = isHigh; - const hz = channel[isHigh ? 1 : 0]; - oscillator.frequency.setValueAtTime(hz, startSeconds); - }); -} -function stopOscillators(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 disconnectOscillators() { - stopOscillators(getAudioContext().currentTime); - const oscillators = getOscillators(); - oscillators.forEach( - oscillator => oscillator.disconnect() - ) - oscillators.length = 0; - sendButton.innerText = 'Send'; - stopOscillatorsTimeoutId = undefined; -} + function stopGraph() { PAUSE = true; stopCollectingSamples(); @@ -1002,114 +553,155 @@ function resumeGraph() { } function collectSample() { const time = performance.now(); - if(frequencyOverTime.length !== 0) { - // we already have this sample - if(time === frequencyOverTime[0].time) return; - } - const channelCount = getChannels().length; + // Do nothing if we already collected the sample + if(time === SAMPLE_LAST_COLLECTED) return; + SAMPLE_LAST_COLLECTED = time; const frequencies = new Uint8Array(analyser.frequencyBinCount); analyser.getByteFrequencyData(frequencies); const length = audioContext.sampleRate / analyser.fftSize; - let processSegment = false; - const { - hasSignal: hadPriorSignal, - streamStarted: initialStreamStart = -1, - streamEnded: priorStreamEnded = -1, - segmentIndex: priorSegmentIndex = -1, - packetIndex: priorPacketIndex = -1 - } = frequencyOverTime[0] ?? {} - const data = { - time, - // frequencies: [...frequencies], - length, - streamEnded: priorStreamEnded - }; // Get amplitude of each channels set of frequencies - data.pairs = getChannels().map(hzSet => hzSet.map(hz => frequencies[Math.round(hz / length)])); - const hasSignal = data.hasSignal = data.pairs.some(amps => amps.some(amp => amp > AMPLITUDE_THRESHOLD)); - let processPacket = false; + const channelAmps = getChannels().map(hzSet => hzSet.map(hz => frequencies[Math.round(hz / length)])); + const hasSignal = channelAmps.some(amps => amps.some(amp => amp > AMPLITUDE_THRESHOLD)); if(hasSignal) { - if(hadPriorSignal) { - // continued bit stream - data.streamStarted = initialStreamStart; - data.streamEnded = priorStreamEnded; - data.packetIndex = priorPacketIndex; - - if(time > priorStreamEnded) { - processPacket = true; - // new packet - data.streamStarted = priorStreamEnded; - data.streamEnded = getPacketEndMilliseconds(initialStreamStart); - data.packetIndex = priorPacketIndex + 1; - } - } else { - if(pauseGraphId) { - window.clearTimeout(pauseGraphId); - pauseGraphId = undefined; - // recover prior bit stream - data.streamStarted = LAST_STREAM_STARTED; - data.streamEnded = getPacketEndMilliseconds(LAST_STREAM_STARTED); - } else { - // new bit stream - data.streamStarted = time; - LAST_STREAM_STARTED = time; - data.packetIndex = 0; - // clear last packet - packetReceivedBits.length = 0; - packetUninterlievedBits.length = 0; - packetDataByteCount = 0; - data.streamEnded = getPacketEndMilliseconds(time); - } - } - - // number of bit in the packet - const segmentIndex = data.segmentIndex = Math.floor((time - data.streamStarted) / SEGMENT_DURATION); - if(priorPacketIndex !== data.packetIndex || - (priorSegmentIndex !== segmentIndex && priorSegmentIndex > -1) - ) { - processSegment = true; + abandonPauseAfterLastSignalEnded(); + if(time > RECEIVED_STREAM_END_MS) { + resetReceivedData(); + // New stream + RECEIVED_STREAM_START_MS = time; + // Assume at least 1 full packet arriving + RECEIVED_STREAM_END_MS = getPacketEndMilliseconds(time); } } else { - data.segmentIndex = -1; - data.packetIndex = -1; - if(hadPriorSignal) { - // just stopped - data.streamStarted = -1; - data.streamEnded = -1; - // update all prior values with stream end if they don't have one - frequencyOverTime - .filter(fov => fov.streamStarted === initialStreamStart) - .filter(fov => fov.streamEnded === -1) - .forEach(fov => { - fov.streamEnded === time - }); - processSegment = true; - if(PAUSE_AFTER_END && !pauseGraphId) { - pauseGraphId = window.setTimeout(() => { - pauseGraphId = undefined; - if(PAUSE_AFTER_END) stopGraph(); - processSegmentReceived(initialStreamStart, priorPacketIndex, priorSegmentIndex); - processPacketReceived(priorPacketIndex, initialStreamStart, priorStreamEnded); - }, SEGMENT_DURATION * 2); - } - } else { - // continued stopping (or never started) - data.streamEnded = -1; - } + pauseAfterSignalEnds(); } - frequencyOverTime.unshift(data); - if(processSegment) processSegmentReceived(initialStreamStart, priorPacketIndex, priorSegmentIndex); - if(processPacket) processPacketReceived(priorPacketIndex, initialStreamStart, priorStreamEnded); - + if(time >= RECEIVED_STREAM_START_MS && time <= RECEIVED_STREAM_END_MS) { + // determine packet/segment index based on time as well as start/end times for packet + const packetIndex = PacketUtils.getPacketIndex(RECEIVED_STREAM_START_MS, time); + const segmentIndex = PacketUtils.getPacketSegmentIndex(RECEIVED_STREAM_START_MS, time); + SAMPLES.unshift({ + time, + pairs: channelAmps, + packetIndex, + segmentIndex, + streamStarted: RECEIVED_STREAM_START_MS, + }); + } + processSamples(); truncateGraphData(); } +function abandonPauseAfterLastSignalEnded() { + if(pauseGraphId) { + window.clearTimeout(pauseGraphId); + pauseGraphId = undefined; + } +} +function pauseAfterSignalEnds() { + // If we never had a signal, do nothing. + if(RECEIVED_STREAM_START_MS === -1) return; + // If we continue after a signal ends, do nothing. + if(!PAUSE_AFTER_END) return; + // If we are already setup to pause, do nothing + if(pauseGraphId) return; -function GET_SEGMENT_BITS(streamStarted, segmentIndex, originalOrder = false) { - const samples = frequencyOverTime.filter(f => + // pause after waiting for 2 segments to come through + let delay = PacketUtils.getSegmentDurationMilliseconds() * 2; + + // Long segments? Pause for no more than 400 milliseconds + delay = Math.min(400, delay); + + // we haven't paused yet. Let's prepare to pause + pauseGraphId = window.setTimeout(() => { + pauseGraphId = undefined; + // if user still wants to pause, stop the graph + if(PAUSE_AFTER_END) { + stopGraph(); + + // are we the sender as well? + // Stop sending the signal. + AudioSender.stop(); + } + }, delay); +} + +function hasSampleSegmentCompleted(now) { + return ({streamStarted, packetIndex, segmentIndex}) => now > + PacketUtils.getPacketSegmentEndMilliseconds(streamStarted, packetIndex, segmentIndex); +} +function hasSamplePacketCompleted(now) { + return ({streamStarted, packetIndex}) => now > + getPacketIndexEndMilliseconds(streamStarted, packetIndex); +} +function consolidateFotPackets(all, {streamStarted, packetIndex}) { + const isMatch = (fot) => { + fot.streamStarted === streamStarted && + fot.packetIndex === packetIndex + }; + + if(!all.some(isMatch)) + all.push({streamStarted, packetIndex}); + return all; +} +const consolidateUnprocessedSampleSegments = now => (all, { + streamStarted, + packetIndex, + segmentIndex, + processedSegment +}) => { + + const isMatch = (sample) => { + sample.streamStarted === streamStarted && + sample.packetIndex === packetIndex && + sample.segmentIndex === segmentIndex + }; + + if(!processedSegment) { + if(!all.some(isMatch)) { + const end = PacketUtils.getPacketSegmentEndMilliseconds(streamStarted, packetIndex, segmentIndex); + if(end < now) + all.push({ + streamStarted, + packetIndex, + segmentIndex + }); + } + } + return all; +} +const markSampleSegmentProcessed = sample => sample.processedSegment = true; +const hasNotProcessedPacket = sample => sample.processedPacket; +const markSamplePacketProcessed = sample => sample.processedPacket = true; + +function processSamples() { + const now = performance.now(); + // Process completed segments + SAMPLES + .reduce(consolidateUnprocessedSampleSegments(now), []) + .every(({ + streamStarted, packetIndex, segmentIndex + }) => { + processSegmentReceived(streamStarted, packetIndex, segmentIndex); + }); + + // Process completed packets + SAMPLES + .filter(hasNotProcessedPacket) + .reduce(consolidateFotPackets, []) + .filter(hasSamplePacketCompleted(now)) + .every(({ + streamStarted, packetIndex + }) => { + processPacketReceived(streamStarted, packetIndex); + }); +} + +function GET_SEGMENT_BITS(streamStarted, segmentIndex, packetIndex, originalOrder = false) { + + const samples = SAMPLES.filter(f => f.segmentIndex === segmentIndex && + f.packetIndex === packetIndex && f.streamStarted === streamStarted ); - const channelCount = frequencyOverTime[0].pairs.length; + const channelCount = SAMPLES[0].pairs.length; const channelFrequencyCount = 2; const sums = new Array(channelCount) .fill(0) @@ -1125,109 +717,72 @@ function GET_SEGMENT_BITS(streamStarted, segmentIndex, originalOrder = false) { }); }); const bitValues = sums.map((amps) => amps[0] > amps[1] ? 0 : 1); - return originalOrder ? bitValues : removeInterleaving(bitValues); + // if(packetIndex === 0 && segmentIndex === 1) { + // console.log(packetIndex, segmentIndex, bitValues.join('')) + // } + return originalOrder ? bitValues : InterleaverEncoding.decode(bitValues); } function resetReceivedData() { resetStream(); resetPacket(); } function resetStream() { - RECEIVED_STREAM_RAW_BITS.length = 0; - RECEIVED_STREAM_ENCODED_BITS.length = 0; - RECEIVED_STREAM_DECODED_BITS.length = 0; - RECEIVED_STREAM_DATA_LENGTH = -1; + RECEIVED_STREAM_END_MS = -1; + RECEIVED_STREAM_START_MS = -1; + RECEIVED_SEGMENT_BITS.length = 0; } function resetPacket() { - RECEIVED_PACKET_RAW_BITS.length = 0; - RECEIVED_PACKET_DECODED_BITS.length = 0; - RECEIVED_PACKET_ENCODED_BITS.length = 0; - packetReceivedBits.length = 0; - packetUninterlievedBits.length = 0; - packetDecodedBits.length = 0; } -function processPacketReceived(packetIndex, startMs, endMs) { - if(packetIndex === 0) resetStream(); - - // append to the stream - RECEIVED_STREAM_RAW_BITS.push(...RECEIVED_PACKET_RAW_BITS); - RECEIVED_STREAM_ENCODED_BITS.push(...RECEIVED_PACKET_ENCODED_BITS); - RECEIVED_STREAM_DECODED_BITS.push(...RECEIVED_PACKET_DECODED_BITS); - +function processPacketReceived(streamStarted, packetIndex) { + SAMPLES.filter( + fot => fot.streamStarted === streamStarted && + fot.packetIndex === packetIndex + ).forEach(markSamplePacketProcessed); resetPacket(); updateReceivedData(); } -function processSegmentReceived(packetStarted, packetIndex, segmentIndex) { - // is our segment long enough? - const samples = frequencyOverTime.filter( - fot => fot.streamStarted === packetStarted && - fot.segmentIndex === segmentIndex +function getTransferredCorrectedBits() { + const bits = []; + const packetCount = StreamManager.getPacketReceivedCount(); + for(let packetIndex = 0; packetIndex < packetCount; packetIndex++) { + let packetBits = StreamManager.getPacketBits(packetIndex); + if(HAMMING_ERROR_CORRECTION) { + bits.push(...HammingEncoding.decode(packetBits)); + } else { + bits.push(...packetBits); + } + } + return bits; +} +function processSegmentReceived(streamStarted, packetIndex, segmentIndex) { + + const isSegment = sample => ( + sample.streamStarted === streamStarted && + sample.packetIndex === packetIndex && + sample.segmentIndex === segmentIndex ); - let bitValues; - if(samples.length === 0) { - // nothing collected - return; - } else { - const sampleEnd = samples[0].time; - const sampleStart = packetStarted + (segmentIndex * SEGMENT_DURATION); - const sampleDuration = (sampleEnd - sampleStart) + MINIMUM_INTERVAL_MS; + let bitValues = GET_SEGMENT_BITS(streamStarted, segmentIndex, packetIndex, segmentIndex, true); - // not long enough to qualify as a segment - if((sampleDuration / SEGMENT_DURATION) < LAST_SEGMENT_PERCENT) return; + StreamManager.addBits(packetIndex, segmentIndex, bitValues); - bitValues = GET_SEGMENT_BITS(packetStarted, segmentIndex, true); - } - - packetReceivedBits.push(...bitValues); - - // NOTE: Can't remove interleaving unless we have error correction and > 7 channels - packetUninterlievedBits.push(...removeInterleaving(bitValues)); - - if(HAMMING_ERROR_CORRECTION) { - - const errorBitCount = getPacketErrorBitCount(); - const errorBits = packetUninterlievedBits.slice(0, errorBitCount); - packetDecodedBits.length = 0; - for(let i = 0; i < errorBits.length; i += ERROR_CORRECTION_BLOCK_SIZE) { - const blockBits = errorBits.slice(i, i + ERROR_CORRECTION_BLOCK_SIZE); - if(blockBits.length === ERROR_CORRECTION_BLOCK_SIZE) { - const nibble = hammingToNibble(blockBits); - packetDecodedBits.push(...nibble); - } - } - } else { - packetDecodedBits.length = 0; - packetDecodedBits.push(...packetUninterlievedBits); - } - - RECEIVED_PACKET_RAW_BITS.push(...bitValues); - RECEIVED_PACKET_ENCODED_BITS = packetUninterlievedBits.slice(0, getPacketUsedBitCount()); - RECEIVED_PACKET_DECODED_BITS = packetDecodedBits.slice(0, getPacketUsedBitCount()); + // mark samples as processed + SAMPLES.filter(isSegment).every(markSampleSegmentProcessed); updateReceivedData(); } function updateReceivedData() { const channelCount = getChannels().length; - let allRawBits = [ - ...RECEIVED_STREAM_RAW_BITS, - ...RECEIVED_PACKET_RAW_BITS - ]; - - let allEncodedBits = [ - ...RECEIVED_STREAM_ENCODED_BITS, - ...RECEIVED_PACKET_ENCODED_BITS - ]; - let allDecodedBits = [ - ...RECEIVED_STREAM_DECODED_BITS, - ...RECEIVED_PACKET_DECODED_BITS - ] + let allRawBits = StreamManager.getStreamBits(); + let allEncodedBits = StreamManager.getAllPacketBits(); + let allDecodedBits = getTransferredCorrectedBits(); // get packet data before removing decoded bits const transmissionByteCount = parseTransmissionByteCount(allDecodedBits); const transmissionByteCountCrc = parseTransmissionByteCountCrc(allDecodedBits) - const transmissionByteCountActualCrc = crc( + const transmissionByteCountActualCrc = CRC.check( bitsToBytes( numberToBits( transmissionByteCount, @@ -1256,11 +811,11 @@ function updateReceivedData() { let percentReceived = allRawBits.length / totalBitsTransferring; receivedProgess.style.width = `${Math.floor(Math.min(1, percentReceived) * 100)}%`; - document.getElementById('received-raw-bits').innerHTML = allRawBits + document.getElementById('received-encoded-segment-bits').innerHTML = allRawBits .reduce( bitExpectorReducer( SENT_TRANSFER_BITS, - getPacketBitCount() + getPacketLastSegmentUnusedChannelCount(), + PacketUtils.getPacketMaxBitCount() + PacketUtils.getPacketLastSegmentUnusedBitCount(), channelCount, (packetIndex, blockIndex) => `${blockIndex === 0 ? '' : '
      '}Segment ${blockIndex}: ` ), @@ -1270,7 +825,7 @@ function updateReceivedData() { .reduce( bitExpectorReducer( SENT_ENCODED_BITS, - getPacketUsedBitCount(), + PacketUtils.getPacketDataBitCount(), ERROR_CORRECTION_BLOCK_SIZE ), ''); @@ -1281,7 +836,7 @@ function updateReceivedData() { .reduce( bitExpectorReducer( SENT_ORIGINAL_BITS, - getPacketDataBitCount(), + PacketUtils.getPacketDataBitCount(), HAMMING_ERROR_CORRECTION ? ERROR_CORRECTION_DATA_SIZE : 8 ), ''); @@ -1309,18 +864,25 @@ function updateReceivedData() { function asHex(length) { return (number) => number.toString(16).padStart(length, '0').toUpperCase(); } -function parseTotalBitsTransferring(decodedBits) { - const dataByteCount = parseTransmissionByteCount(decodedBits); - const bitCount = getPacketizationBitCount(dataByteCount * 8); +function parseDataTransferDurationMilliseconds() { + const decodedBits = getTransferredCorrectedBits(); + const byteCount = parseTransmissionByteCount(decodedBits); + return PacketUtils.getDataTransferDurationMillisecondsFromByteCount(byteCount); +} +function parseTotalBitsTransferring() { + const dataByteCount = parseTransmissionByteCount(); + const bitCount = PacketUtils.getPacketizationBitCountFromByteCount(dataByteCount); const segments = getTotalSegmentCount(bitCount); return segments * getChannels().length; } -function parseTransmissionByteCountCrc(decodedBits) { +function parseTransmissionByteCountCrc() { + let decodedBits = getTransferredCorrectedBits(); const offset = MAXIMUM_PACKETIZATION_SIZE_BITS; decodedBits = decodedBits.slice(offset, offset + CRC_BIT_COUNT); return bitsToInt(decodedBits, CRC_BIT_COUNT); } -function parseTransmissionByteCount(decodedBits) { +function parseTransmissionByteCount() { + let decodedBits = getTransferredCorrectedBits(); decodedBits = decodedBits.slice(0, MAXIMUM_PACKETIZATION_SIZE_BITS); while(decodedBits.length < MAXIMUM_PACKETIZATION_SIZE_BITS) { // assume maximum value possible @@ -1346,21 +908,7 @@ function removeEncodedPadding(bits) { } // get header bits representing the size - let dataSizeBits = []; - let headerBitCount = MAXIMUM_PACKETIZATION_SIZE_BITS + CRC_BIT_COUNT; - - if(HAMMING_ERROR_CORRECTION) { - for(i = 0; i < blocksNeeded; i++) { - const block = bits.slice(i * blockSize, (i + 1) * blockSize); - dataSizeBits.push(...hammingToNibble(block)); - } - dataSizeBits.length = sizeBits; - headerBitCount = Math.ceil(headerBitCount / ERROR_CORRECTION_DATA_SIZE) * ERROR_CORRECTION_BLOCK_SIZE; - } else { - dataSizeBits = bits.slice(0, sizeBits); - } - // decode the size - const dataByteCount = bitsToInt(dataSizeBits, sizeBits); + const dataByteCount = StreamManager.getTransferByteCount(); // determine how many decoded bits need to be sent (including the size) const totalBits = (dataByteCount * 8) + MAXIMUM_PACKETIZATION_SIZE_BITS + CRC_BIT_COUNT; @@ -1454,22 +1002,24 @@ function htmlEncode(text) { return element.innerHTML; } function resetGraphData() { - frequencyOverTime.length = 0; + SAMPLES.length = 0; bitStart.length = 0; } function truncateGraphData() { const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH; const now = performance.now(); - let length = frequencyOverTime.length; + let length = SAMPLES.length; while(length !== 0) { - const time = frequencyOverTime[length-1].time; + const time = SAMPLES[length-1].time; if(now - time > duration) length--; else break; } - if(length !== frequencyOverTime.length) { - frequencyOverTime.length = length; + if(length !== SAMPLES.length) { + SAMPLES.length = length; bitStart.length = length; } + // remove processed segments + SAMPLES = SAMPLES.filter(s => !s.segmentProcessed); } function getAudioContext() { if(!audioContext) { @@ -1615,12 +1165,14 @@ function avgLabel(array) { } function drawSegmentIndexes(ctx, width, height) { // Do/did we have a stream? - if(!LAST_STREAM_STARTED) return; - const latest = frequencyOverTime[0].time; + if(!RECEIVED_STREAM_START_MS) return; + const latest = SAMPLES[0].time; // will any of the stream appear? - const packetDuration = getPacketDurationMilliseconds(); - const lastStreamEnded = LAST_STREAM_STARTED + packetDuration; + const segmentCount = PacketUtils.getPacketSegmentCount(); + const transferDuration = parseDataTransferDurationMilliseconds(); + const lastStreamEnded = RECEIVED_STREAM_START_MS + transferDuration; + const graphDuration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH; const graphEarliest = latest - graphDuration; // ended too long ago? @@ -1632,14 +1184,14 @@ function drawSegmentIndexes(ctx, width, height) { for(let time = latestSegmentEnded; time > graphEarliest; time -= SEGMENT_DURATION) { // too far back? - if(time < LAST_STREAM_STARTED) break; + if(time < RECEIVED_STREAM_START_MS) break; - // which segment are we looking at? - const segmentIndex = Math.floor(((time - LAST_STREAM_STARTED) / SEGMENT_DURATION)); - - // when did the segment begin/end - const segmentStart = LAST_STREAM_STARTED + (segmentIndex * SEGMENT_DURATION); - const segmentEnd = segmentStart + SEGMENT_DURATION; + const transferSegmentIndex = PacketUtils.getTranserSegmentIndex(RECEIVED_STREAM_START_MS, time); + const packetIndex = PacketUtils.getPacketIndex(RECEIVED_STREAM_START_MS, time); + const packetSegmentIndex = PacketUtils.getPacketSegmentIndex(RECEIVED_STREAM_START_MS, time); + const packetStarted = PacketUtils.getPacketStartMilliseconds(RECEIVED_STREAM_START_MS, packetIndex); + const segmentStart = PacketUtils.getPacketSegmentStartMilliseconds(RECEIVED_STREAM_START_MS, packetIndex, packetSegmentIndex); + const segmentEnd = PacketUtils.getPacketSegmentEndMilliseconds(RECEIVED_STREAM_START_MS, packetIndex, packetSegmentIndex); // where is the segments left x coordinate? const leftX = ((latest - segmentEnd) / graphDuration) * width; @@ -1647,30 +1199,39 @@ function drawSegmentIndexes(ctx, width, height) { // Draw segment index ctx.fontSize = '24px'; if(segmentStart < lastStreamEnded) { - let text = segmentIndex.toString(); + let text = packetSegmentIndex.toString(); let size = ctx.measureText(text); let textX = leftX + (segmentWidth / 2) - (size.width / 2); ctx.strokeStyle = 'black'; ctx.lineWidth = 2; ctx.textBaseline = 'bottom'; - let textY = segmentIndex % 2 === 0 ? height : height - 12; + let textY = transferSegmentIndex % 2 === 0 ? height : height - 12; ctx.strokeText(text, textX, textY); ctx.fillStyle = 'white'; ctx.fillText(text, textX, textY); } // draw sample count - const sampleCount = frequencyOverTime + const sampleCount = SAMPLES .filter(fot => - fot.streamStarted === LAST_STREAM_STARTED && - fot.segmentIndex === segmentIndex + fot.streamStarted === packetStarted && + fot.segmentIndex === packetSegmentIndex && + fot.packetIndex === packetIndex ) .length; + // if(sampleCount === 0) { + // console.log({ + // packetStarted, + // packetSegmentIndex, + // packetIndex, + // startTimes: SAMPLES.reduce((all, fot) => all.includes(fot.streamStarted) ? all : [...all, fot.streamStarted], []) + // }) + // } - text = sampleCount.toString(); - size = ctx.measureText(text); - textX = leftX + (segmentWidth / 2) - (size.width / 2); - textY = segmentIndex % 2 === 0 ? 5 : 17; + let text = sampleCount.toString(); + let size = ctx.measureText(text); + let textX = leftX + (segmentWidth / 2) - (size.width / 2); + let textY = transferSegmentIndex % 2 === 0 ? 5 : 17; ctx.strokeStyle = 'black'; ctx.lineWidth = 2; ctx.textBaseline = 'top'; @@ -1683,10 +1244,10 @@ function drawSegmentIndexes(ctx, width, height) { } function drawBitDurationLines(ctx, color) { const { width, height } = receivedGraph; - const newest = frequencyOverTime[0].time; + const newest = SAMPLES[0].time; const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH; - const streamTimes = frequencyOverTime.filter(({ + const streamTimes = SAMPLES.filter(({ streamStarted }) => { return streamStarted !== -1 @@ -1721,12 +1282,12 @@ function drawBitDurationLines(ctx, color) { function drawBitStart(ctx, color) { const { width, height } = receivedGraph; - const newest = frequencyOverTime[0].time; + const newest = SAMPLES[0].time; const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH; ctx.strokeStyle = color; for(let i = 0; i < bitStart.length; i++) { if(!bitStart[i]) continue; - const {time} = frequencyOverTime[i]; + const {time} = SAMPLES[i]; if(newest - time > duration) continue; const x = ((newest - time) / duration) * width; ctx.beginPath(); @@ -1741,7 +1302,7 @@ function getPercentY(percent) { } function drawFrequencyLineGraph(ctx, channel, highLowIndex, color, lineWidth, dashed) { const { width, height } = receivedGraph; - const newest = frequencyOverTime[0].time; + const newest = SAMPLES[0].time; const duration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH; const isSelected = channel === CHANNEL_SELECTED; const isOver = channel === CHANNEL_OVER; @@ -1749,8 +1310,8 @@ function drawFrequencyLineGraph(ctx, channel, highLowIndex, color, lineWidth, da ctx.setLineDash([5, 5]); } ctx.beginPath(); - for(let i = 0; i < frequencyOverTime.length; i++) { - const {pairs, time} = frequencyOverTime[i]; + for(let i = 0; i < SAMPLES.length; i++) { + const {pairs, time} = SAMPLES[i]; const x = getTimeX(time, newest); if(x === -1) continue; if(channel >= pairs.length) continue; @@ -1771,15 +1332,15 @@ function drawFrequencyLineGraph(ctx, channel, highLowIndex, color, lineWidth, da } } function drawFrequencyDots(ctx, channel, highLowIndex, color) { - const newest = frequencyOverTime[0].time; + const newest = SAMPLES[0].time; const radius = 2; const border = 0.5; ctx.fillStyle = color; ctx.strokeStyle = 'white'; ctx.lineWidth = border; const fullCircle = 2 * Math.PI; - for(let i = 0; i < frequencyOverTime.length; i++) { - const {pairs, time} = frequencyOverTime[i]; + for(let i = 0; i < SAMPLES.length; i++) { + const {pairs, time} = SAMPLES[i]; const x = getTimeX(time, newest); if(x === -1) continue; const amplitude = pairs[channel][highLowIndex]; @@ -1804,21 +1365,21 @@ function getTimePercent(time, newest) { } function getPacketSizeSegmentCount() { - const totalBits = getPacketBitCount(); + const totalBits = PacketUtils.getPacketMaxBitCount(); const channelCount = getChannels().length; return Math.ceil(totalBits / channelCount); } function drawChannelData() { // Do/did we have a stream? - if(!LAST_STREAM_STARTED) return; + if(!RECEIVED_STREAM_START_MS) return; - const latest = frequencyOverTime[0].time; + const latest = SAMPLES[0].time; // will any of the stream appear? - const packetBitCount = getPacketBitCount(); + const packetBitCount = PacketUtils.getPacketMaxBitCount(); - const packetDuration = getPacketDurationMilliseconds(); - const lastStreamEnded = LAST_STREAM_STARTED + packetDuration; + const packetDuration = PacketUtils.getPacketDurationMilliseconds(); + const lastStreamEnded = RECEIVED_STREAM_START_MS + packetDuration; const graphDuration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH; const graphEarliest = latest - graphDuration; // ended too long ago? @@ -1834,24 +1395,26 @@ function drawChannelData() { const {height, width} = canvas; // Loop through visible segments - const latestSegmentEnded = Math.min(latest, lastStreamEnded);//yyy + const latestSegmentEnded = Math.min(latest, lastStreamEnded); for(let time = latestSegmentEnded; time > graphEarliest; time -= SEGMENT_DURATION) { // too far back? - if(time < LAST_STREAM_STARTED) break; + if(time < RECEIVED_STREAM_START_MS) break; // which segment are we looking at? - const segmentIndex = Math.floor(((time - LAST_STREAM_STARTED) / SEGMENT_DURATION)); - - // when did the segment begin/end - const segmentStart = LAST_STREAM_STARTED + (segmentIndex * SEGMENT_DURATION); - const segmentEnd = segmentStart + SEGMENT_DURATION; + const segmentIndex = PacketUtils.getPacketSegmentIndex(RECEIVED_STREAM_START_MS, time); + // when did the segment begin + const packetIndex = PacketUtils.getPacketIndex(RECEIVED_STREAM_START_MS, time); + const segmentEnd = PacketUtils.getPacketSegmentEndMilliseconds(RECEIVED_STREAM_START_MS, packetIndex, segmentIndex); // where is the segments left x coordinate? const leftX = ((latest - segmentEnd) / graphDuration) * width; // what bits did we receive for the segment? - const segmentBits = GET_SEGMENT_BITS(LAST_STREAM_STARTED, segmentIndex, true); - + let segmentBits = StreamManager.getPacketSegmentBits(packetIndex, segmentIndex); + if(!segmentBits){ + // unprocessed bits - let's grab them from the samples + segmentBits = GET_SEGMENT_BITS(RECEIVED_STREAM_START_MS, segmentIndex, packetIndex, true); + } // draw segment data background let expectedBitCount = channelCount; if(segmentEnd === lastStreamEnded) { @@ -2044,7 +1607,7 @@ function realChannel(id) { } function drawFrequencyData(forcedDraw) { if(PAUSE && forcedDraw !== true) return; - if(frequencyOverTime.length === 0) { + if(SAMPLES.length === 0) { if(forcedDraw !== true) { requestAnimationFrame(drawFrequencyData); } @@ -2199,7 +1762,7 @@ function handleReceivedChannelGraphClick(e) { const bitIndex = CHANNEL_SELECTED + (SEGMENT_SELECTED * channelCount); document.getElementById('selected-bit').innerText = bitIndex.toLocaleString(); - const samples = frequencyOverTime + const samples = SAMPLES .filter(fot => fot.segmentIndex === SEGMENT_SELECTED) .map(fot => fot.pairs[CHANNEL_SELECTED]); samples.forEach(([low, high], i) => { @@ -2268,16 +1831,16 @@ function getChannelAndSegment(e) { // what segment are we over? // Do/did we have a stream? - if(!LAST_STREAM_STARTED) { + if(!RECEIVED_STREAM_START_MS) { return { channelIndex, segmentIndex: -1 }; } - const latest = frequencyOverTime[0].time; + const latest = SAMPLES[0]?.time ?? performance.now(); // will any of the stream appear? - const packetDuration = getPacketDurationMilliseconds(); - const lastStreamEnded = LAST_STREAM_STARTED + packetDuration; + const packetDuration = PacketUtils.getPacketDurationMilliseconds(); + const lastStreamEnded = RECEIVED_STREAM_START_MS + packetDuration; const graphDuration = SEGMENT_DURATION * MAX_BITS_DISPLAYED_ON_GRAPH; const graphEarliest = latest - graphDuration; // ended too long ago? @@ -2294,7 +1857,7 @@ function getChannelAndSegment(e) { for(let time = latestSegmentEnded; time > graphEarliest; time -= SEGMENT_DURATION) { // too far back? - if(time < LAST_STREAM_STARTED) { + if(time < RECEIVED_STREAM_START_MS) { return { channelIndex, segmentIndex: -1 @@ -2302,10 +1865,10 @@ function getChannelAndSegment(e) { }; // which segment are we looking at? - const segmentIndex = Math.floor(((time - LAST_STREAM_STARTED) / SEGMENT_DURATION)); + const segmentIndex = Math.floor(((time - RECEIVED_STREAM_START_MS) / SEGMENT_DURATION)); // when did the segment begin/end - const segmentStart = LAST_STREAM_STARTED + (segmentIndex * SEGMENT_DURATION); + const segmentStart = RECEIVED_STREAM_START_MS + (segmentIndex * SEGMENT_DURATION); const segmentEnd = segmentStart + SEGMENT_DURATION; // where is the segments left x coordinate? diff --git a/style.css b/style.css index 3f19aca..6f95622 100644 --- a/style.css +++ b/style.css @@ -4,19 +4,53 @@ body { .panels { display: flex; flex-wrap: wrap; + font-family: sans-serif; } .panels > div { border: 1px solid black; background-color: darkslategray; margin: 10px; padding: 10px; + padding-top: 0; + padding-left: 0; + padding-right: 0; border-radius: 10px; - color: rgb(75, 185, 75); + color: rgb(163, 197, 163); + max-height: 200px; + overflow: hidden; } -.panels > div h2 { +.panels > div.chart { + max-height: unset; + overflow: unset; +} +.panels > div.chart > div { + overflow: unset; + height: unset; +} +.panels > div > h2 { + margin-top: 0; + padding-left: 5px; + padding-right: 5px; background-color: gray; border-bottom: 4px solid darkolivegreen; color: black; + font-size: small; +} +.panels > div > div { + margin-left: 10px; + margin-right: 10px; + font-size: small; + height: 175px; + + overflow: auto; +} +.panels > div > div h4 { + font-size: small; + background-color: gray; + border-bottom: 1px solid rgb(58, 58, 58); + color: rgb(58, 58, 58); + margin-top: 3px; + margin-bottom: 3px; } canvas { background-color: black; @@ -34,6 +68,7 @@ canvas { position: relative; z-index: 2; left: 0; + width: 0; /* transition: width 0.3s ease; */ } .xprogress-container::after { @@ -48,7 +83,17 @@ canvas { background-color: rgb(41, 59, 10); color: rgb(75, 185, 75); width: 250px; - height: 300px; + min-height: 150px; + font-size: x-small; + border: 1px solid grey; + overflow: auto; + font-family: monospace; +} +.raw-data-small { + background-color: rgb(41, 59, 10); + color: rgb(75, 185, 75); + width: 250px; + min-height: 100px; font-size: x-small; border: 1px solid grey; overflow: auto; @@ -68,8 +113,8 @@ canvas { color: green; } ol { - overflow: auto; - height: 75px; + /* overflow: auto; */ + /* height: 75px; */ background-color: rgb(41, 59, 10); color: rgb(75, 185, 75); border: 1px solid grey;