306 lines
7.8 KiB
JavaScript
306 lines
7.8 KiB
JavaScript
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 = [];
|
|
this.fftIndices_.forEach(function(index) {
|
|
values.push({
|
|
freq: index.freq,
|
|
value: this.buffer_[index.index],
|
|
});
|
|
}.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;
|
|
};
|