Initial commit.

This commit is contained in:
Ian Gulliver
2016-01-24 20:44:30 -08:00
parent de63c01110
commit 59ee7d3e05
3 changed files with 341 additions and 0 deletions

309
metatron.js Normal file
View File

@@ -0,0 +1,309 @@
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext;
var metatron = {};
metatron.FREQS = [
[ 1209, 1336, 1477, 1633, 2200, ],
[ 697, 770, 852, 941 ],
];
metatron.POSSIBLE_VALUES = 1;
metatron.FREQS.forEach(function(freqSet) {
metatron.POSSIBLE_VALUES *= freqSet.length;
});
metatron.MARK_SECONDS = 0.2;
metatron.FFT_SIZE = 2048;
metatron.CYCLE_THRESHOLD = 4;
metatron.CYCLES_PER_MARK = 8;
metatron.checksum = function(str) {
var checksum = 0;
for (var i = 0; i < str.length; i++) {
checksum ^= str.charCodeAt(i);
}
return checksum;
};
metatron.Generator = function(tag) {
this.context_ = new AudioContext();
this.tag_ = tag;
};
metatron.Generator.prototype.addPair_ = function(index, value) {
var startOffset = metatron.MARK_SECONDS * index;
metatron.FREQS.forEach(function(choices) {
var freq = choices[value % choices.length];
value = Math.floor(value / choices.length);
var osc = this.context_.createOscillator();
osc.frequency.value = freq;
osc.connect(this.context_.destination);
osc.start(this.context_.currentTime + startOffset);
osc.stop(this.context_.currentTime + startOffset + metatron.MARK_SECONDS);
}.bind(this));
};
metatron.Generator.prototype.playValue_ = function(value) {
// Make sure we always change values.
value += 1;
this.lastValue_ = (this.lastValue_ + value) % metatron.POSSIBLE_VALUES;
this.addPair_(this.indexOffset_++, this.lastValue_);
};
metatron.Generator.prototype.playRaw_ = function(str) {
for (var i = 0; i < str.length; i++) {
var chr = str.charCodeAt(i);
// Big endian nibbles
this.playValue_(chr >> 4);
this.playValue_(chr % 16);
}
};
metatron.Generator.prototype.play = function(str) {
this.lastValue_ = 0;
this.indexOffset_ = 0;
// Resync the listener to the base value.
this.playValue_(0);
this.playRaw_(this.tag_);
this.playRaw_(String.fromCharCode(str.length));
this.playRaw_(str);
this.playRaw_(String.fromCharCode(metatron.checksum(str)));
};
metatron.Listener = function(tag) {
this.context_ = new AudioContext();
this.activeValue_ = null;
this.newValue_ = null;
this.votes_ = 0;
this.lastValue_ = 0;
this.state_ = metatron.Listener.STATE_.WAIT_TAG;
this.tag_ = [];
for (var i = 0; i < tag.length; i++) {
var chr = tag.charCodeAt(i);
this.tag_.push(chr >> 4);
this.tag_.push(chr % 16);
}
this.tagBuffer_ = [];
navigator.getUserMedia({
"audio": {
"mandatory": {
"googEchoCancellation": "false",
"googAutoGainControl": "false",
"googNoiseSuppression": "false",
"googHighpassFilter": "false",
},
"optional": []
}
},
function(stream) {
var sampleRate = this.context_.sampleRate;
this.analyser_ = this.context_.createAnalyser();
this.analyser_.fftSize = metatron.FFT_SIZE;
this.analyser_.smoothingTimeConstant = 0.0;
this.bufSize_ = this.analyser_.frequencyBinCount;
this.buffer_ = new Uint8Array(this.bufSize_);
var streamSource = this.context_.createMediaStreamSource(stream);
streamSource.connect(this.analyser_);
var hzPerBucket = sampleRate / metatron.FFT_SIZE;
this.fftIndices_ = [];
metatron.FREQS.forEach(function(freqSet) {
freqSet.forEach(function(freq) {
this.fftIndices_.push({
index: Math.round(freq / hzPerBucket),
freq: freq,
});
}.bind(this));
}.bind(this));
var interval = metatron.MARK_SECONDS / metatron.CYCLES_PER_MARK * 1000;
console.log('Poll interval:', interval, 'ms');
window.setInterval(this.analyse_.bind(this), interval);
}.bind(this),
function(error) {
}.bind(this));
};
metatron.Listener.STATE_ = {
WAIT_TAG: 1,
WAIT_LENGTH: 2,
WAIT_CONTENTS: 3,
WAIT_CHECKSUM: 4,
};
metatron.Listener.prototype.currentValue_ = function() {
this.analyser_.getByteFrequencyData(this.buffer_);
var values = [];
var minValue = 255;
this.fftIndices_.forEach(function(index) {
minValue = Math.min(minValue, this.buffer_[index.index]);
}.bind(this));
this.fftIndices_.forEach(function(index) {
values.push({
freq: index.freq,
value: this.buffer_[index.index] - minValue,
});
}.bind(this));
values.sort(function(a, b) {
return b.value - a.value;
});
var activeValues = values.slice(0, metatron.FREQS.length);
var outputValue = 0;
var error = false;
activeValues.forEach(function(value) {
var multiplier = 1, found = 0;
metatron.FREQS.forEach(function(freqSet) {
var index = freqSet.indexOf(value.freq);
if (index >= 0) {
found++;
outputValue += index * multiplier;
}
multiplier *= freqSet.length;
}.bind(this));
if (found != 1) {
error = true;
console.log('Wrong number of tones matched:', found, activeValues);
}
}.bind(this));
if (error) {
return null;
}
return outputValue;
};
metatron.Listener.prototype.analyse_ = function() {
var newValue = this.currentValue_();
if (newValue === null) {
return;
}
if (newValue === this.activeValue_) {
this.votes_ = 0;
this.newValue_ = null;
return;
}
if (newValue === this.newValue_) {
if (++this.votes_ == metatron.CYCLE_THRESHOLD) {
this.onValue_(this.newValue_);
this.activeValue_ = this.newValue_;
this.newValue_ = null;
this.votes_ = 0;
}
return;
}
this.newValue_ = newValue;
this.votes_ = 1;
};
metatron.Listener.prototype.onValue_ = function(value) {
var realValue = value;
if (this.lastValue_ >= realValue) {
realValue += metatron.POSSIBLE_VALUES;
}
realValue = realValue - this.lastValue_ - 1;
this.lastValue_ = value;
console.log(realValue);
switch (this.state_) {
case metatron.Listener.STATE_.WAIT_TAG:
this.tagBuffer_.push(realValue);
if (this.tagBuffer_.length > this.tag_.length) {
this.tagBuffer_.shift();
}
if (this.tag_.equals(this.tagBuffer_)) {
console.log('tag seen!');
this.state_ = metatron.Listener.STATE_.WAIT_LENGTH;
this.lengthBuffer_ = [];
}
break;
case metatron.Listener.STATE_.WAIT_LENGTH:
this.lengthBuffer_.push(realValue);
if (this.lengthBuffer_.length == 2) {
this.length_ = 0;
this.lengthBuffer_.forEach(function(part) {
this.length_ <<= 4;
this.length_ += part;
}.bind(this));
this.contentsParts_ = [];
this.state_ = metatron.Listener.STATE_.WAIT_CONTENTS;
}
break;
case metatron.Listener.STATE_.WAIT_CONTENTS:
this.contentsParts_.push(realValue);
if (this.contentsParts_.length == this.length_ * 2) {
var chrs = [];
for (i = 0; i < this.contentsParts_.length; i += 2) {
chrs.push(String.fromCharCode((this.contentsParts_[i] << 4)
+ this.contentsParts_[i + 1]));
}
this.contents_ = chrs.join('');
this.checksumParts_ = [];
this.state_ = metatron.Listener.STATE_.WAIT_CHECKSUM;
}
break;
case metatron.Listener.STATE_.WAIT_CHECKSUM:
this.checksumParts_.push(realValue);
if (this.checksumParts_.length == 2) {
var checksum = 0;
this.checksumParts_.forEach(function(part) {
checksum <<= 4;
checksum += part;
}.bind(this));
if (checksum == metatron.checksum(this.contents_)) {
console.log('checksum match');
document.getElementById('value').textContent = this.contents_;
} else {
console.log('corrupted message');
}
this.state_ = metatron.Listener.STATE_.WAIT_TAG;
}
break;
}
};
Array.prototype.equals = function(other) {
if (this.length != other.length) {
return false;
}
for (var i = 0; i < this.length; i++) {
if (this[i] != other[i]) {
return false;
}
}
return true;
};