diff options
Diffstat (limited to 'packages/frontend/src/utility/chiptune2.ts')
| -rw-r--r-- | packages/frontend/src/utility/chiptune2.ts | 347 |
1 files changed, 347 insertions, 0 deletions
diff --git a/packages/frontend/src/utility/chiptune2.ts b/packages/frontend/src/utility/chiptune2.ts new file mode 100644 index 0000000000..220002ff1e --- /dev/null +++ b/packages/frontend/src/utility/chiptune2.ts @@ -0,0 +1,347 @@ +/* + * SPDX-FileCopyrightText: marie and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable */ + +const ChiptuneAudioContext = window.AudioContext || window.webkitAudioContext; + +let libopenmpt +let libopenmptLoadPromise + +type ChiptuneJsConfig = { + repeatCount: number | null; + context: AudioContext | null; +}; + +export function ChiptuneJsConfig (repeatCount?: number, context?: AudioContext) { + this.repeatCount = repeatCount; + this.context = context; +} + +ChiptuneJsConfig.prototype.constructor = ChiptuneJsConfig; + +export function ChiptuneJsPlayer (config: ChiptuneJsConfig) { + this.config = config; + this.audioContext = config.context || new ChiptuneAudioContext(); + this.context = this.audioContext.createGain(); + this.currentPlayingNode = null; + this.handlers = []; + this.touchLocked = true; + this.volume = 1; +} + +ChiptuneJsPlayer.prototype.initialize = function() { + if (libopenmptLoadPromise) return libopenmptLoadPromise; + if (libopenmpt) return Promise.resolve(); + + libopenmptLoadPromise = new Promise<void>(async (resolve, reject) => { + try { + const { Module } = await import('./libopenmpt/libopenmpt.js'); + await new Promise((resolve) => { + Module['onRuntimeInitialized'] = resolve; + }) + libopenmpt = Module; + resolve() + } catch (e) { + reject(e) + } finally { + libopenmptLoadPromise = undefined; + } + }) + + return libopenmptLoadPromise; +} + +ChiptuneJsPlayer.prototype.constructor = ChiptuneJsPlayer; + +ChiptuneJsPlayer.prototype.fireEvent = function (eventName: string, response) { + const handlers = this.handlers; + if (handlers.length > 0) { + for(const handler of handlers) { + if (handler.eventName === eventName) { + handler.handler(response); + } + } + } +}; + +ChiptuneJsPlayer.prototype.addHandler = function (eventName: string, handler: Function) { + this.handlers.push({ eventName, handler }); +}; + +ChiptuneJsPlayer.prototype.onEnded = function (handler: Function) { + this.addHandler('onEnded', handler); +}; + +ChiptuneJsPlayer.prototype.onError = function (handler: Function) { + this.addHandler('onError', handler); +}; + +ChiptuneJsPlayer.prototype.duration = function () { + return libopenmpt._openmpt_module_get_duration_seconds(this.currentPlayingNode.modulePtr); +}; + +ChiptuneJsPlayer.prototype.position = function () { + return libopenmpt._openmpt_module_get_position_seconds(this.currentPlayingNode.modulePtr); +}; + +ChiptuneJsPlayer.prototype.seek = function (position: number) { + if (this.currentPlayingNode) { + libopenmpt._openmpt_module_set_position_seconds(this.currentPlayingNode.modulePtr, position); + } +}; + +ChiptuneJsPlayer.prototype.metadata = function () { + const data = {}; + const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.currentPlayingNode.modulePtr)).split(';'); + let keyNameBuffer = 0; + for (const key of keys) { + keyNameBuffer = libopenmpt._malloc(key.length + 1); + libopenmpt.writeAsciiToMemory(key, keyNameBuffer); + data[key] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.currentPlayingNode.modulePtr, keyNameBuffer)); + libopenmpt._free(keyNameBuffer); + } + return data; +}; + +ChiptuneJsPlayer.prototype.unlock = function () { + const context = this.audioContext; + const buffer = context.createBuffer(1, 1, 22050); + const unlockSource = context.createBufferSource(); + unlockSource.buffer = buffer; + unlockSource.connect(this.context); + this.context.connect(context.destination); + unlockSource.start(0); + this.touchLocked = false; +}; + +ChiptuneJsPlayer.prototype.load = function (input) { + return this.initialize().then(() => new Promise((resolve, reject) => { + if(this.touchLocked) { + this.unlock(); + } + const player = this; + if (input instanceof File) { + const reader = new FileReader(); + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsArrayBuffer(input); + } else { + window.fetch(input).then((response) => { + response.arrayBuffer().then((arrayBuffer) => { + resolve(arrayBuffer); + }).catch((error) => { + reject(error); + }); + }).catch((error) => { + reject(error); + }); + } + })); +}; + +ChiptuneJsPlayer.prototype.play = function (buffer: ArrayBuffer) { + this.unlock(); + this.stop(); + const processNode = this.createLibopenmptNode(buffer, this.buffer); + if (processNode === null) { + return; + } + libopenmpt._openmpt_module_set_repeat_count(processNode.modulePtr, this.config.repeatCount || 0); + this.currentPlayingNode = processNode; + processNode.connect(this.context); + this.context.connect(this.audioContext.destination); +}; + +ChiptuneJsPlayer.prototype.stop = function () { + if (this.currentPlayingNode != null) { + this.currentPlayingNode.disconnect(); + this.currentPlayingNode.cleanup(); + this.currentPlayingNode = null; + } +}; + +ChiptuneJsPlayer.prototype.togglePause = function () { + if (this.currentPlayingNode != null) { + this.currentPlayingNode.togglePause(); + } +}; + +ChiptuneJsPlayer.prototype.getPattern = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_current_pattern(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getRow = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_current_row(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getNumPatterns = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_num_patterns(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getCurrentSpeed = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_current_speed(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getCurrentTempo = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_current_tempo(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_pattern_num_rows(this.currentPlayingNode.modulePtr, pattern); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row: number, channel: number) { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt.UTF8ToString(libopenmpt._openmpt_module_format_pattern_row_channel(this.currentPlayingNode.modulePtr, pattern, row, channel, 0, true)); + } + return ''; +}; + +ChiptuneJsPlayer.prototype.getCtls = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_ctls(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.version = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_get_library_version(); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: object) { + const maxFramesPerChunk = 4096; + const processNode = this.audioContext.createScriptProcessor(2048, 0, 2); + processNode.config = config; + processNode.player = this; + const byteArray = new Int8Array(buffer); + const ptrToFile = libopenmpt._malloc(byteArray.byteLength); + libopenmpt.HEAPU8.set(byteArray, ptrToFile); + processNode.modulePtr = libopenmpt._openmpt_module_create_from_memory(ptrToFile, byteArray.byteLength, 0, 0, 0); + processNode.nbChannels = libopenmpt._openmpt_module_get_num_channels(processNode.modulePtr); + processNode.patternIndex = -1; + processNode.paused = false; + processNode.leftBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); + processNode.rightBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); + processNode.perf = { 'current': 0, 'max': 0 }; + processNode.cleanup = function () { + if (this.modulePtr !== 0) { + libopenmpt._openmpt_module_destroy(this.modulePtr); + this.modulePtr = 0; + } + if (this.leftBufferPtr !== 0) { + libopenmpt._free(this.leftBufferPtr); + this.leftBufferPtr = 0; + } + if (this.rightBufferPtr !== 0) { + libopenmpt._free(this.rightBufferPtr); + this.rightBufferPtr = 0; + } + }; + processNode.stop = function () { + this.disconnect(); + this.cleanup(); + }; + processNode.pause = function () { + this.paused = true; + }; + processNode.unpause = function () { + this.paused = false; + }; + processNode.togglePause = function () { + this.paused = !this.paused; + }; + processNode.getProcessTime = function() { + const max = this.perf.max; + this.perf.max = 0; + return { 'current': this.perf.current, 'max': max }; + }; + processNode.onaudioprocess = function (e) { + let startTimeP1 = performance.now(); + const outputL = e.outputBuffer.getChannelData(0); + const outputR = e.outputBuffer.getChannelData(1); + let framesToRender = outputL.length; + if (this.ModulePtr === 0) { + for (let i = 0; i < framesToRender; ++i) { + outputL[i] = 0; + outputR[i] = 0; + } + this.disconnect(); + this.cleanup(); + return; + } + if (this.paused) { + for (let i = 0; i < framesToRender; ++i) { + outputL[i] = 0; + outputR[i] = 0; + } + return; + } + let framesRendered = 0; + let ended = false; + let error = false; + + const currentPattern = libopenmpt._openmpt_module_get_current_pattern(this.modulePtr); + const currentRow = libopenmpt._openmpt_module_get_current_row(this.modulePtr); + startTimeP1 = startTimeP1 - performance.now(); + if (currentPattern !== this.patternIndex) { + processNode.player.fireEvent('onPatternChange'); + } + processNode.player.fireEvent('onRowChange', { index: currentRow }); + + const startTimeP2 = performance.now(); + while (framesToRender > 0) { + const framesPerChunk = Math.min(framesToRender, maxFramesPerChunk); + const actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, this.context.sampleRate, framesPerChunk, this.leftBufferPtr, this.rightBufferPtr); + if (actualFramesPerChunk === 0) { + ended = true; + // modulePtr will be 0 on openmpt: error: openmpt_module_read_float_stereo: ERROR: module * not valid or other openmpt error + error = !this.modulePtr; + } + const rawAudioLeft = libopenmpt.HEAPF32.subarray(this.leftBufferPtr / 4, this.leftBufferPtr / 4 + actualFramesPerChunk); + const rawAudioRight = libopenmpt.HEAPF32.subarray(this.rightBufferPtr / 4, this.rightBufferPtr / 4 + actualFramesPerChunk); + for (let i = 0; i < actualFramesPerChunk; ++i) { + outputL[framesRendered + i] = rawAudioLeft[i]; + outputR[framesRendered + i] = rawAudioRight[i]; + } + for (let i = actualFramesPerChunk; i < framesPerChunk; ++i) { + outputL[framesRendered + i] = 0; + outputR[framesRendered + i] = 0; + } + framesToRender -= framesPerChunk; + framesRendered += framesPerChunk; + } + if (ended) { + this.disconnect(); + this.cleanup(); + error ? processNode.player.fireEvent('onError', { type: 'openmpt' }) : processNode.player.fireEvent('onEnded'); + } + this.perf.current = performance.now() - startTimeP2 + startTimeP1; + if (this.perf.current > this.perf.max) this.perf.max = this.perf.current; + }; + return processNode; +}; |