Files
Konrad Beckmann 20d7fefaf9 Import mikmod
2021-08-02 02:19:41 +02:00

331 lines
8.8 KiB
TypeScript

/* MikMod Web Audio library
(c) 2021 Carlos Rafael Gimenes das Neves.
https://github.com/sezero/mikmod
https://github.com/carlosrafaelgn/mikmod/tree/master/libmikmod/webaudio
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU Library General Public License as
published by the Free Software Foundation; either version 2 of
the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
02111-1307, USA.
*/
class LibMikMod {
// Due to both LibMikMod and AudioWorklet's nature we can
// have only one module loaded at a time...
public static readonly WEB_VERSION = 1;
public static LIB_VERSION: string | null = null;
public static readonly ERROR_FILE_READ = 1;
public static readonly ERROR_MODULE_LOAD = 2;
public static readonly ERROR_PLAYBACK = 3;
public static initialized = false;
public static initializing = false;
public static initializationError = false;
public static infoSongName: string | null = null;
public static infoModType: string | null = null;
public static infoComment: string | null = null;
private static currentId = 0;
private static audioNode: AudioWorkletNode | null = null;
private static onload: ((audioNode: AudioWorkletNode) => void) | null = null;
private static onerror: ((errorCode: number, reason?: any) => void) | null = null;
private static onended: (() => void) | null = null;
public static isSupported(): boolean {
// Should we also check for HTTPS? Because, apparently, the browser already undefines
// AudioWorklet when not serving the files from a secure origin...
return (("AudioWorklet" in window) && ("AudioWorkletNode" in window) && ("WebAssembly" in window));
}
public static async init(audioContext: AudioContext, libPath?: string | null): Promise<void> {
if (LibMikMod.initialized || LibMikMod.initializing || LibMikMod.initializationError)
return;
if (!libPath)
libPath = "";
else if (!libPath.endsWith("/"))
libPath += "/";
LibMikMod.initializing = true;
LibMikMod.currentId = 0;
try {
const response = await fetch(libPath + "libmikmodclib.wasm?" + LibMikMod.WEB_VERSION);
const wasmBuffer = await response.arrayBuffer();
await audioContext.audioWorklet.addModule(libPath + "libmikmodprocessor.min.js?" + LibMikMod.WEB_VERSION);
await new Promise<void>(function (resolve, reject) {
const audioNode = new AudioWorkletNode(audioContext, "libmikmodprocessor");
audioNode.port.onmessage = function (ev) {
const message = ev.data as LibMikModResponse;
if (!message || message.messageId !== LibMikModMessageId.INIT || !LibMikMod.initializing || LibMikMod.currentId)
return;
if (message.errorStr) {
reject(message.errorStr);
} else {
LibMikMod.LIB_VERSION = ((message.id >>> 16) & 0xFF) + "." + ((message.id >>> 8) & 0xFF) + "." + (message.id & 0xFF);
resolve();
}
};
audioNode.onprocessorerror = function (ev) {
reject(ev);
};
LibMikMod.audioNode = audioNode;
LibMikMod.postMessage({
id: LibMikMod.currentId,
messageId: LibMikModMessageId.INIT,
buffer: wasmBuffer
});
});
LibMikMod.initialized = true;
} finally {
if (!LibMikMod.initialized)
LibMikMod.initializationError = true;
LibMikMod.initializing = false;
LibMikMod.stopModule();
}
}
private static postMessage(message: LibMikModMessage): void {
if (!LibMikMod.audioNode)
return;
if (message.buffer)
LibMikMod.audioNode.port.postMessage(message, [message.buffer]);
else
LibMikMod.audioNode.port.postMessage(message);
}
public static loadModule(options: LibMikModLoadOptions): void {
if (!LibMikMod.initialized)
throw new Error("Library not initialized");
if (!options)
throw new Error("Null options");
if (!options.audioContext)
throw new Error("Null audioContext");
const source = options.source;
if (!source)
throw new Error("Null source");
LibMikMod.stopModule();
const audioNode = new AudioWorkletNode(options.audioContext, "libmikmodprocessor");
LibMikMod.currentId++;
audioNode.port.onmessage = LibMikMod.handleResponse;
audioNode.onprocessorerror = LibMikMod.notifyProcessorError;
LibMikMod.audioNode = audioNode;
LibMikMod.infoSongName = null;
LibMikMod.infoModType = null;
LibMikMod.infoComment = null;
LibMikMod.onload = options.onload;
LibMikMod.onerror = options.onerror;
LibMikMod.onended = options.onended;
const id = LibMikMod.currentId,
initialOptions: LibMikModInitialOptions = {
hqMixer: options.hqMixer,
wrap: options.wrap,
loop: options.loop,
fadeout: options.fadeout,
reverb: options.reverb,
interpolation: options.interpolation,
noiseReduction: options.noiseReduction
};
if ("lastModified" in source) {
if ("arrayBuffer" in source) {
source.arrayBuffer().then(function (arrayBuffer) {
if (id !== LibMikMod.currentId)
return;
LibMikMod.postMessage({
id: LibMikMod.currentId,
messageId: LibMikModMessageId.LOAD_MODULE_BUFFER,
buffer: arrayBuffer,
options: initialOptions
});
}, function (reason) {
if (id !== LibMikMod.currentId)
return;
LibMikMod.notifyReaderError(reason);
});
} else {
const reader = new FileReader();
reader.onerror = function (ev) {
if (id !== LibMikMod.currentId)
return;
LibMikMod.notifyReaderError(ev);
};
reader.onload = function () {
if (id !== LibMikMod.currentId)
return;
if (!reader.result)
LibMikMod.notifyReaderError("Empty reader result");
else
LibMikMod.postMessage({
id: LibMikMod.currentId,
messageId: LibMikModMessageId.LOAD_MODULE_BUFFER,
buffer: reader.result as ArrayBuffer,
options: initialOptions
});
};
reader.readAsArrayBuffer(source);
}
} else {
LibMikMod.postMessage({
id: LibMikMod.currentId,
messageId: LibMikModMessageId.LOAD_MODULE_BUFFER,
buffer: source,
options: initialOptions
});
}
}
public static changeGeneralOptions(options?: LibMikModGeneralOptions): void {
if (!LibMikMod.initialized)
throw new Error("Library not initialized");
LibMikMod.postMessage({
id: LibMikMod.currentId,
messageId: LibMikModMessageId.CHANGE_GENERAL_OPTIONS,
options: options
});
}
private static cleanUp(): void {
LibMikMod.currentId++;
LibMikMod.infoSongName = null;
LibMikMod.infoModType = null;
LibMikMod.infoComment = null;
LibMikMod.audioNode = null;
LibMikMod.onload = null;
LibMikMod.onerror = null;
LibMikMod.onended = null;
}
public static stopModule(): void {
if (!LibMikMod.audioNode)
return;
LibMikMod.postMessage({
id: LibMikMod.currentId,
messageId: LibMikModMessageId.STOP_MODULE
});
LibMikMod.cleanUp();
}
private static notifyReaderError(reason?: any): void {
LibMikMod.notifyError(LibMikMod.ERROR_FILE_READ, reason);
}
private static notifyProcessorError(reason?: any): void {
LibMikMod.notifyError(LibMikMod.ERROR_PLAYBACK, reason);
}
private static notifyError(errorCode: number, reason?: any): void {
if (!LibMikMod.audioNode)
return;
const onerror = LibMikMod.onerror;
LibMikMod.cleanUp();
if (onerror)
onerror(errorCode, reason);
}
private static notifyEnded(): void {
if (!LibMikMod.audioNode)
return;
const onended = LibMikMod.onended;
LibMikMod.cleanUp();
if (onended)
onended();
}
private static handleResponse(ev: MessageEvent): void {
const message = ev.data as LibMikModResponse;
if (!message)
return;
switch (message.messageId) {
case LibMikModMessageId.LOAD_MODULE_BUFFER:
if (message.id !== LibMikMod.currentId || !LibMikMod.audioNode)
break;
if (message.errorCode) {
LibMikMod.notifyError(LibMikMod.ERROR_MODULE_LOAD, message.errorStr || message.errorCode.toString());
} else {
LibMikMod.infoSongName = (message.infoSongName || null);
LibMikMod.infoModType = (message.infoModType || null);
LibMikMod.infoComment = (message.infoComment || null);
if (LibMikMod.onload)
LibMikMod.onload(LibMikMod.audioNode);
}
break;
case LibMikModMessageId.PLAYBACK_ERROR:
if (message.id !== LibMikMod.currentId)
break;
LibMikMod.notifyProcessorError(message.errorStr || message.errorCode?.toString());
break;
case LibMikModMessageId.PLAYBACK_ENDED:
if (message.id !== LibMikMod.currentId)
break;
LibMikMod.notifyEnded();
break;
}
}
}