Mercurial > web
comparison chiptune2.js @ 6:ac2f9715807c
first version
author | Paper <mrpapersonic@gmail.com> |
---|---|
date | Sun, 30 May 2021 18:07:38 -0400 |
parents | |
children | 8f0b52a3cb69 |
comparison
equal
deleted
inserted
replaced
3:f13aa00c92f0 | 6:ac2f9715807c |
---|---|
1 // constants | |
2 const OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT = 2 | |
3 const OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH = 1 | |
4 | |
5 // audio context | |
6 var ChiptuneAudioContext = window['AudioContext'] || window['webkitAudioContext']; | |
7 | |
8 // config | |
9 var ChiptuneJsConfig = function (repeatCount, stereoSeparation, interpolationFilter, context) | |
10 { | |
11 this.repeatCount = repeatCount; | |
12 this.stereoSeparation = stereoSeparation; | |
13 this.interpolationFilter = interpolationFilter; | |
14 this.context = context; | |
15 } | |
16 | |
17 ChiptuneJsConfig.prototype.constructor = ChiptuneJsConfig; | |
18 | |
19 // player | |
20 var ChiptuneJsPlayer = function (config) { | |
21 this.config = config; | |
22 this.context = config.context || new ChiptuneAudioContext(); | |
23 this.currentPlayingNode = null; | |
24 this.handlers = []; | |
25 this.touchLocked = true; | |
26 } | |
27 | |
28 ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer; | |
29 | |
30 // event handlers section | |
31 ChiptuneJsPlayer.prototype.fireEvent = function (eventName, response) { | |
32 var handlers = this.handlers; | |
33 if (handlers.length) { | |
34 handlers.forEach(function (handler) { | |
35 if (handler.eventName === eventName) { | |
36 handler.handler(response); | |
37 } | |
38 }) | |
39 } | |
40 } | |
41 | |
42 ChiptuneJsPlayer.prototype.addHandler = function (eventName, handler) { | |
43 this.handlers.push({eventName: eventName, handler: handler}); | |
44 } | |
45 | |
46 ChiptuneJsPlayer.prototype.onEnded = function (handler) { | |
47 this.addHandler('onEnded', handler); | |
48 } | |
49 | |
50 ChiptuneJsPlayer.prototype.onError = function (handler) { | |
51 this.addHandler('onError', handler); | |
52 } | |
53 | |
54 // metadata | |
55 ChiptuneJsPlayer.prototype.duration = function() { | |
56 return libopenmpt._openmpt_module_get_duration_seconds(this.currentPlayingNode.modulePtr); | |
57 } | |
58 | |
59 ChiptuneJsPlayer.prototype.getCurrentRow = function() { | |
60 return libopenmpt._openmpt_module_get_current_row(this.currentPlayingNode.modulePtr); | |
61 } | |
62 | |
63 ChiptuneJsPlayer.prototype.getCurrentPattern = function() { | |
64 return libopenmpt._openmpt_module_get_current_pattern(this.currentPlayingNode.modulePtr); | |
65 } | |
66 | |
67 ChiptuneJsPlayer.prototype.getCurrentOrder = function() { | |
68 return libopenmpt._openmpt_module_get_current_order(this.currentPlayingNode.modulePtr); | |
69 } | |
70 | |
71 ChiptuneJsPlayer.prototype.metadata = function() { | |
72 var data = {}; | |
73 var keys = Pointer_stringify(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';'); | |
74 var keyNameBuffer = 0; | |
75 for (var i = 0; i < keys.length; i++) { | |
76 keyNameBuffer = libopenmpt._malloc(keys[i].length + 1); | |
77 writeAsciiToMemory(keys[i], keyNameBuffer); | |
78 data[keys[i]] = Pointer_stringify(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer)); | |
79 libopenmpt._free(keyNameBuffer); | |
80 } | |
81 return data; | |
82 } | |
83 | |
84 ChiptuneJsPlayer.prototype.module_ctl_set = function(ctl, value) { | |
85 return libopenmpt.ccall('openmpt_module_ctl_set', 'number', ['number', 'string', 'string'], [this.currentPlayingNode.modulePtr, ctl, value]) === 1; | |
86 } | |
87 | |
88 // playing, etc | |
89 ChiptuneJsPlayer.prototype.unlock = function() { | |
90 | |
91 var context = this.context; | |
92 var buffer = context.createBuffer(1, 1, 22050); | |
93 var unlockSource = context.createBufferSource(); | |
94 | |
95 unlockSource.buffer = buffer; | |
96 unlockSource.connect(context.destination); | |
97 unlockSource.start(0); | |
98 | |
99 this.touchLocked = false; | |
100 } | |
101 | |
102 ChiptuneJsPlayer.prototype.load = function(input, callback) { | |
103 | |
104 if (this.touchLocked) { | |
105 this.unlock(); | |
106 } | |
107 | |
108 var player = this; | |
109 | |
110 if (input instanceof File) { | |
111 var reader = new FileReader(); | |
112 reader.onload = function() { | |
113 return callback(reader.result); // no error | |
114 }.bind(this); | |
115 reader.readAsArrayBuffer(input); | |
116 } else { | |
117 var xhr = new XMLHttpRequest(); | |
118 xhr.open('GET', input, true); | |
119 xhr.responseType = 'arraybuffer'; | |
120 xhr.onload = function(e) { | |
121 if (xhr.status === 200) { | |
122 return callback(xhr.response); // no error | |
123 } else { | |
124 player.fireEvent('onError', {type: 'onxhr'}); | |
125 } | |
126 }.bind(this); | |
127 xhr.onerror = function() { | |
128 player.fireEvent('onError', {type: 'onxhr'}); | |
129 }; | |
130 xhr.onabort = function() { | |
131 player.fireEvent('onError', {type: 'onxhr'}); | |
132 }; | |
133 xhr.send(); | |
134 } | |
135 } | |
136 | |
137 ChiptuneJsPlayer.prototype.play = function(buffer) { | |
138 this.stop(); | |
139 var processNode = this.createLibopenmptNode(buffer, this.config); | |
140 if (processNode == null) { | |
141 return; | |
142 } | |
143 | |
144 // set config options on module | |
145 libopenmpt._openmpt_module_set_repeat_count(processNode.modulePtr, this.config.repeatCount); | |
146 libopenmpt._openmpt_module_set_render_param(processNode.modulePtr, OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT, this.config.stereoSeparation); | |
147 libopenmpt._openmpt_module_set_render_param(processNode.modulePtr, OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH, this.config.interpolationFilter); | |
148 | |
149 this.currentPlayingNode = processNode; | |
150 processNode.connect(this.context.destination); | |
151 } | |
152 | |
153 ChiptuneJsPlayer.prototype.stop = function() { | |
154 if (this.currentPlayingNode != null) { | |
155 this.currentPlayingNode.disconnect(); | |
156 this.currentPlayingNode.cleanup(); | |
157 this.currentPlayingNode = null; | |
158 } | |
159 } | |
160 | |
161 ChiptuneJsPlayer.prototype.togglePause = function() { | |
162 if (this.currentPlayingNode != null) { | |
163 this.currentPlayingNode.togglePause(); | |
164 } | |
165 } | |
166 | |
167 ChiptuneJsPlayer.prototype.createLibopenmptNode = function(buffer, config) { | |
168 // TODO error checking in this whole function | |
169 | |
170 var maxFramesPerChunk = 4096; | |
171 var processNode = this.context.createScriptProcessor(2048, 0, 2); | |
172 processNode.config = config; | |
173 processNode.player = this; | |
174 var byteArray = new Int8Array(buffer); | |
175 var ptrToFile = libopenmpt._malloc(byteArray.byteLength); | |
176 libopenmpt.HEAPU8.set(byteArray, ptrToFile); | |
177 processNode.modulePtr = libopenmpt._openmpt_module_create_from_memory(ptrToFile, byteArray.byteLength, 0, 0, 0); | |
178 processNode.paused = false; | |
179 processNode.leftBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); | |
180 processNode.rightBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); | |
181 processNode.cleanup = function() { | |
182 if (this.modulePtr != 0) { | |
183 libopenmpt._openmpt_module_destroy(this.modulePtr); | |
184 this.modulePtr = 0; | |
185 } | |
186 if (this.leftBufferPtr != 0) { | |
187 libopenmpt._free(this.leftBufferPtr); | |
188 this.leftBufferPtr = 0; | |
189 } | |
190 if (this.rightBufferPtr != 0) { | |
191 libopenmpt._free(this.rightBufferPtr); | |
192 this.rightBufferPtr = 0; | |
193 } | |
194 } | |
195 processNode.stop = function() { | |
196 this.disconnect(); | |
197 this.cleanup(); | |
198 } | |
199 processNode.pause = function() { | |
200 this.paused = true; | |
201 } | |
202 processNode.unpause = function() { | |
203 this.paused = false; | |
204 } | |
205 processNode.togglePause = function() { | |
206 this.paused = !this.paused; | |
207 } | |
208 processNode.onaudioprocess = function(e) { | |
209 var outputL = e.outputBuffer.getChannelData(0); | |
210 var outputR = e.outputBuffer.getChannelData(1); | |
211 var framesToRender = outputL.length; | |
212 if (this.ModulePtr == 0) { | |
213 for (var i = 0; i < framesToRender; ++i) { | |
214 outputL[i] = 0; | |
215 outputR[i] = 0; | |
216 } | |
217 this.disconnect(); | |
218 this.cleanup(); | |
219 return; | |
220 } | |
221 if (this.paused) { | |
222 for (var i = 0; i < framesToRender; ++i) { | |
223 outputL[i] = 0; | |
224 outputR[i] = 0; | |
225 } | |
226 return; | |
227 } | |
228 var framesRendered = 0; | |
229 var ended = false; | |
230 var error = false; | |
231 while (framesToRender > 0) { | |
232 var framesPerChunk = Math.min(framesToRender, maxFramesPerChunk); | |
233 var actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, this.context.sampleRate, framesPerChunk, this.leftBufferPtr, this.rightBufferPtr); | |
234 if (actualFramesPerChunk == 0) { | |
235 ended = true; | |
236 // modulePtr will be 0 on openmpt: error: openmpt_module_read_float_stereo: ERROR: module * not valid or other openmpt error | |
237 error = !this.modulePtr; | |
238 } | |
239 var rawAudioLeft = libopenmpt.HEAPF32.subarray(this.leftBufferPtr / 4, this.leftBufferPtr / 4 + actualFramesPerChunk); | |
240 var rawAudioRight = libopenmpt.HEAPF32.subarray(this.rightBufferPtr / 4, this.rightBufferPtr / 4 + actualFramesPerChunk); | |
241 for (var i = 0; i < actualFramesPerChunk; ++i) { | |
242 outputL[framesRendered + i] = rawAudioLeft[i]; | |
243 outputR[framesRendered + i] = rawAudioRight[i]; | |
244 } | |
245 for (var i = actualFramesPerChunk; i < framesPerChunk; ++i) { | |
246 outputL[framesRendered + i] = 0; | |
247 outputR[framesRendered + i] = 0; | |
248 } | |
249 framesToRender -= framesPerChunk; | |
250 framesRendered += framesPerChunk; | |
251 } | |
252 if (ended) { | |
253 this.disconnect(); | |
254 this.cleanup(); | |
255 error ? processNode.player.fireEvent('onError', {type: 'openmpt'}) : processNode.player.fireEvent('onEnded'); | |
256 } | |
257 } | |
258 return processNode; | |
259 } |