forked from chibitronics/ltc-npm-modulate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
modulate.js
164 lines (137 loc) · 5.93 KB
/
modulate.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
var FskEncoder = require("./afsk-encoder.js");
var Modulator = function (params) {
if (!params)
params = new Object();
if ("rate" in params)
this.rate = params.rate;
if ("lbr" in params)
this.lbr = params.lbr;
else
this.lbr = false;
this.encoder = new FskEncoder(this.rate, this.lbr);
// Create a "script node" that will actually generate audio samples.
this.script_node = Modulator.prototype.script_node;
// Start out in a not-playing state
this.playing = false;
}
Modulator.prototype = {
encoder: null, // FskEncoder object
outputAudioBuffer: null, // AudioBuffer object
uiCallback: null, // UI object for callback
scriptNode: null, // Re-used script node object for audio generation
can_stop: true, // Whether we can stop (usually we can)
silence: function (msecs) {
var bufLen = Math.ceil(window.audioContext.sampleRate / (1000.0 / msecs));
this.outputAudioBuffer = window.audioContext.createBuffer(1, bufLen, window.audioContext.sampleRate);
var outputFloatArray = this.outputAudioBuffer.getChannelData(0);
for (var i = 0; i < outputFloatArray.length; i++)
outputFloatArray[i] = 0;
this.script_node_offset = 0;
},
processAudio: function (ev) {
var outl = ev.outputBuffer.getChannelData(0);
var outr = ev.outputBuffer.getChannelData(1);
// If we're not playing, but still being called, just fill the channel with silence.
if (!this.playing) {
for (var i = 0; i < outl.length; i++)
outl[i] = outr[i] = 0;
// Some browsers crash when you stop playing
if (this.can_stop)
this.script_node.disconnect();
return;
}
var outputFloatArray = this.outputAudioBuffer.getChannelData(0);
for (var i = 0; i < outl.length; i++) {
if (this.script_node_offset >= outputFloatArray.length) {
// If there's more data to play, reset the output float array.
if (this.get_more_data())
outputFloatArray = this.outputAudioBuffer.getChannelData(0);
// Otherwise, fill the buffer with 0s, and we'll stop playing on the next iteration.
else {
for (var j = 0; j < outputFloatArray.length; j++)
outputFloatArray[j] = 0;
}
this.script_node_offset = 0;
}
outl[i] = outr[i] = outputFloatArray[this.script_node_offset++];
}
},
// immediately play the modulated audio exactly once. Useful for debugging single packets
playBuffer: function (obj, func) {
console.log("-- playAudioBuffer --");
var bufferNode = window.audioContext.createBufferSource();
bufferNode.buffer = this.outputAudioBuffer;
bufferNode.connect(window.audioContext.destination); // Connect to speakers
bufferNode.addEventListener("ended", function () {
var playTimeEnd = performance.now();
var timeElapsed = playTimeEnd - this.playTimeStart;
console.log("got audio ended event after " + timeElapsed.toFixed(2) + "ms");
if (obj && func)
func.call(obj);
}.bind(this));
this.playTimeStart = performance.now();
bufferNode.start(0); // play immediately
},
// Plays through an entire file. You need to set the callback so once
// a single audio packet is finished, the next can start. The index
// tells where to start playing. You could, in theory, start modulating
// part-way through an audio stream by setting index to a higher number on your
// first call.
playLoop: function (obj, end_func, param) {
this.get_more_data = function () {
if (!end_func.call(obj, param)) {
this.playing = false;
return false;
}
return true;
};
this.script_node.onaudioprocess = function (ev) {
Modulator.prototype.processAudio.call(this, ev);
}.bind(this);
if (!this.playing) {
this.playing = true;
this.script_node.connect(window.audioContext.destination);
}
},
modulatePcm: function (data, type) {
var bufLen = Math.ceil(data.length * 8 * this.encoder.samplesPerBit());
var modulatedData = new Float32Array(bufLen);
if (type === undefined)
type = 16;
var timeStart = 0;
var timeEnd = 0;
if ((typeof performance) === "object")
timeStart = performance.now();
this.encoder.modulate(data, modulatedData); // writes outputFloatArray in-place
if ((typeof performance) === "object")
timeEnd = performance.now();
var timeElapsed = timeEnd - timeStart;
console.log("Rendered " + data.length + " data bytes in " +
timeElapsed.toFixed(2) + "ms");
if (type === 16) {
var pcmData = new Int16Array(modulatedData.length);
for (var i = 0; i < modulatedData.length; i++) {
// Map -1 .. 1 to -32767 .. 32768
pcmData[i] = Math.round((modulatedData[i]) * 32767);
}
return pcmData;
}
else {
var pcmData = new Uint8Array(new ArrayBuffer(modulatedData.length * 2));
for (var i = 0; i < modulatedData.length; i++) {
// Map -1 .. 1 to -32767 .. 32768
var sample = Math.round((modulatedData[i]) * 32767);
// Javascript doesn't really do two's compliment
if (sample < 0)
sample = (0xffff - ~sample);
pcmData[(i * 2) + 0] = Math.round(sample) & 0xff;
pcmData[(i * 2) + 1] = Math.round(sample >> 8) & 0xff;
}
return pcmData;
}
}
}
// AMD exports
if (typeof module !== "undefined" && module.exports) {
module.exports = Modulator;
}