├── .gitignore ├── docker ├── build.bat ├── build.sh └── Dockerfile ├── demo.html ├── package.json ├── css └── style.css ├── minify.js ├── dnd.js ├── LICENSE ├── polyfills.js ├── licenses └── license.libopenmpt.txt ├── chiptune3.min.js ├── README.md ├── chiptune3.js ├── index.html ├── chiptune3.worklet.min.js ├── convert.html ├── demo.js └── chiptune3.worklet.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.br 3 | .npmrc 4 | -------------------------------------------------------------------------------- /docker/build.bat: -------------------------------------------------------------------------------- 1 | docker build --build-arg BASE=https://lib.openmpt.org/files/libopenmpt/src/ --build-arg FILE=libopenmpt-0.7.13+release -t emscripten:libopenmpt . 2 | docker create --name mpt emscripten:libopenmpt 3 | docker cp mpt:/src/libopenmpt/bin/libopenmpt.js ../libopenmpt.worklet.js 4 | docker rm mpt 5 | docker rmi emscripten:libopenmpt 6 | docker rmi emscripten/emsdk:latest 7 | 8 | cd .. 9 | npm run minify 10 | brotli -f *.min.js libopenmpt.worklet.js 11 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chiptune Worklet Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chiptune3", 3 | "version": "0.7.13", 4 | "description": "Chiptune3 the AudioWorklet successor of chiptune2", 5 | "browser": "./chiptune3.js", 6 | "author": "drsnuggles", 7 | "license": "MIT", 8 | "type": "module", 9 | "repository": "https://github.com/DrSnuggles/chiptune", 10 | "homepage": "https://github.com/DrSnuggles/chiptune#readme", 11 | "bugs": "https://github.com/DrSnuggles/chiptune/issues", 12 | "scripts": { 13 | "minify": "node minify.js" 14 | }, 15 | "files": [ 16 | "chiptune3.js", 17 | "chiptune3.worklet.js", 18 | "libopenmpt.worklet.js" 19 | ], 20 | "devDependencies": { 21 | "terser": "^5.27.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | # relases: https://lib.openmpt.org/files/libopenmpt/src/libopenmpt-0.7.4+release.makefile.tar.gz 2 | # autobuilds: https://builds.openmpt.org/builds/auto/libopenmpt/src.makefile/0.7.5-pre.0/libopenmpt-0.7.5-pre.0+r20329.makefile.tar.gz 3 | # https://builds.openmpt.org/builds/auto/libopenmpt/src.makefile/0.8.0-pre.4/libopenmpt-0.8.0-pre.4+r20328.makefile.tar.gz 4 | docker build --build-arg BASE=https://lib.openmpt.org/files/libopenmpt/src/ --build-arg FILE=libopenmpt-0.7.13+release -t emscripten:libopenmpt . && \ 5 | docker create --name mpt emscripten:libopenmpt && \ 6 | docker cp mpt:/src/libopenmpt/bin/libopenmpt.js ../libopenmpt.worklet.js && \ 7 | docker rm mpt && \ 8 | docker rmi emscripten:libopenmpt 9 | docker rmi emscripten/emsdk:latest 10 | 11 | cd .. 12 | npm run minify 13 | brotli -f *.min.js libopenmpt.worklet.js 14 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 12pt sans-serif; 3 | } 4 | table, 5 | input[type="range"] 6 | { 7 | width: 100%; 8 | } 9 | 10 | fieldset { 11 | border-radius: 10px; 12 | border: 1px dotted #00F; 13 | } 14 | legend { 15 | font-style: italic; 16 | font-weight: bold; 17 | } 18 | pre { 19 | font-size: small; 20 | } 21 | 22 | #audioModal { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | width: 100vw; 27 | height: 100vh; 28 | background: #FFFD; 29 | font: 500% sans-serif; 30 | line-height: 100vh; 31 | text-align: center; 32 | } 33 | #audioModal.fadeOut { 34 | animation: fadeOut 500ms forwards; 35 | } 36 | @keyframes fadeOut { 37 | 0% { 38 | opacity: 1; 39 | } 40 | 100% { 41 | opacity: 0; 42 | z-index: -604; 43 | } 44 | } 45 | 46 | #info { 47 | width: calc(100% - 427px); 48 | } 49 | #myCanvas { 50 | position: absolute; 51 | top: 0; 52 | right: 0; 53 | height: 240px; 54 | } 55 | #songSel { 56 | width: 100%; 57 | height: calc(100vh - 260px); 58 | } -------------------------------------------------------------------------------- /minify.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { minify } from 'terser' 3 | 4 | main() 5 | async function main() { 6 | const config = { 7 | compress: { 8 | dead_code: true, 9 | drop_console: false, 10 | drop_debugger: true, 11 | keep_classnames: false, 12 | keep_fargs: true, 13 | keep_fnames: false, 14 | keep_infinity: false 15 | }, 16 | mangle: { 17 | eval: false, 18 | keep_classnames: false, 19 | keep_fnames: false, 20 | toplevel: false, 21 | safari10: false 22 | }, 23 | module: false, 24 | sourceMap: false, 25 | output: { 26 | comments: '' 27 | } 28 | } 29 | let code = fs.readFileSync('chiptune3.js', 'utf8') 30 | code = code.replace('./chiptune3.worklet.js','./chiptune3.worklet.min.js') 31 | let minified = await minify(code, config) 32 | fs.writeFileSync('chiptune3.min.js', minified.code) 33 | 34 | code = fs.readFileSync('chiptune3.worklet.js', 'utf8') 35 | minified = await minify(code, config) 36 | fs.writeFileSync('chiptune3.worklet.min.js', minified.code) 37 | } -------------------------------------------------------------------------------- /dnd.js: -------------------------------------------------------------------------------- 1 | // 2 | // DrSnuggles: Drop Handler 3 | // 4 | export function dnd(domListener, callback) { 5 | // preventDefaults on all drag related events 6 | let events = ['drop','dragdrop','dragenter','dragleave','dragover'] 7 | events.forEach(ev => { 8 | domListener.addEventListener(ev, preventDefaults, false) 9 | }) 10 | // handler on drop 11 | events = ['drop','dragdrop'] 12 | events.forEach(ev => { 13 | domListener.addEventListener(ev, dropHandler, false) 14 | }) 15 | function dropHandler(e) { 16 | let file = null 17 | if (e.dataTransfer.items) { 18 | for (let i = 0; i < e.dataTransfer.items.length; i++) { 19 | if (e.dataTransfer.items[i].kind === 'file') { 20 | file = e.dataTransfer.items[i].getAsFile() 21 | break // just first file 22 | } 23 | } 24 | } else { 25 | for (let i = 0; i < e.dataTransfer.files.length; i++) { 26 | file = e.dataTransfer.files[i] 27 | break // just first file 28 | } 29 | } 30 | 31 | if (!file) return 32 | 33 | const reader = new FileReader() 34 | reader.onloadend = (ev) => { 35 | callback( ev.target.result ) 36 | } 37 | reader.readAsArrayBuffer(file) 38 | } 39 | function preventDefaults(ev) { 40 | ev.preventDefault() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All code in this project is MIT (X11) licensed. The only exception are the compiled libopenmpt parts which remain under the OpenMPT project BSD license. 2 | 3 | License text below: 4 | 5 | Copyright © 2013-2024 The chiptune2.js and chiptune3 contributers. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /polyfills.js: -------------------------------------------------------------------------------- 1 | export function atob(input) { 2 | /* 3 | This code was written by Tyler Akins and has been placed in the 4 | public domain. It would be nice if you left this header intact. 5 | Base64 code from Tyler Akins -- http://rumkin.com 6 | */ 7 | const keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 8 | 9 | let output = '' 10 | let chr1, chr2, chr3 11 | let enc1, enc2, enc3, enc4 12 | let i = 0 13 | // remove all characters that are not A-Z, a-z, 0-9, +, /, or = 14 | input = input.replace(/[^A-Za-z0-9\+\/\=]/g, '') 15 | do { 16 | enc1 = keyStr.indexOf(input.charAt(i++)) 17 | enc2 = keyStr.indexOf(input.charAt(i++)) 18 | enc3 = keyStr.indexOf(input.charAt(i++)) 19 | enc4 = keyStr.indexOf(input.charAt(i++)) 20 | 21 | chr1 = (enc1 << 2) | (enc2 >> 4) 22 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2) 23 | chr3 = ((enc3 & 3) << 6) | enc4 24 | 25 | output = output + String.fromCharCode(chr1) 26 | if (enc3 !== 64) output = output + String.fromCharCode(chr2) 27 | if (enc4 !== 64) output = output + String.fromCharCode(chr3) 28 | } while (i < input.length) 29 | return output 30 | } 31 | 32 | export const performance = { 33 | now() { 34 | return Date.now() 35 | } 36 | } 37 | 38 | export const crypto = { 39 | getRandomValues(array) { 40 | for (let i = 0; i < array.length; i++) { 41 | array[i] = (Math.random() * 256) | 0 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # 4.0.7 works 2 | # watch https://github.com/emscripten-core/emscripten/blob/main/ChangeLog.md 3 | FROM emscripten/emsdk:latest 4 | 5 | ARG BASE 6 | ARG FILE 7 | 8 | # update / install 9 | RUN apt update -y && apt upgrade -y && apt install pkg-config -y 10 | 11 | # download 12 | RUN wget $BASE$FILE.makefile.tar.gz 13 | 14 | # unpack 15 | RUN mkdir libopenmpt && tar xzvf $FILE.makefile.tar.gz -C ./libopenmpt --strip-components=1 16 | 17 | WORKDIR /src/libopenmpt 18 | 19 | # next line updates EXPORTED_FUNCTIONS and since 3.1.57 DEFAULT_LIBRARY_FUNCS_TO_INCLUDE 20 | # 4.0.7 no HEAP8,HEAPU32 export by default 21 | RUN sed -i "s/SO_LDFLAGS += -s EXPORTED_FUNCTIONS=\"\['_malloc','_free'\]\"/SO_LDFLAGS += -s EXPORTED_FUNCTIONS=\"\['_malloc','_free','stackAlloc','stackSave','stackRestore','UTF8ToString','HEAP8','HEAPU32'\]\" -s DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=\"['stackAlloc','stackSave','stackRestore']\"/g" build/make/config-emscripten.mk 22 | 23 | # build 24 | RUN make CONFIG=emscripten EMSCRIPTEN_TARGET=audioworkletprocessor 25 | 26 | # add tersered polyfills.js 27 | RUN sed -i '1 i\function atob(r){const t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";let e,o,n,a,c,h,d,f="",i=0;r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");do{a=t.indexOf(r.charAt(i++)),c=t.indexOf(r.charAt(i++)),h=t.indexOf(r.charAt(i++)),d=t.indexOf(r.charAt(i++)),e=a<<2|c>>4,o=(15&c)<<4|h>>2,n=(3&h)<<6|d,f+=String.fromCharCode(e),64!==h&&(f+=String.fromCharCode(o)),64!==d&&(f+=String.fromCharCode(n))}while(iDate.now()};const crypto={getRandomValues(r){for(let t=0;t{this.processNode=new AudioWorkletNode(this.context,"libopenmpt-processor",{numberOfInputs:0,numberOfOutputs:1,outputChannelCount:[2]}),this.processNode.port.onmessage=this.handleMessage_.bind(this),this.processNode.port.postMessage({cmd:"config",val:this.config}),this.fireEvent("onInitialized"),this.processNode.connect(this.gain),this.destination&&this.gain.connect(this.destination)})).catch((t=>console.error(t)))}handleMessage_(t){switch(t.data.cmd){case"meta":this.meta=t.data.meta,this.duration=t.data.meta.dur,this.fireEvent("onMetadata",this.meta);break;case"pos":this.currentTime=t.data.pos,this.order=t.data.order,this.pattern=t.data.pattern,this.row=t.data.row,this.fireEvent("onProgress",t.data);break;case"end":this.fireEvent("onEnded");break;case"err":this.fireEvent("onError",{type:t.data.val});break;case"fullAudioData":this.fireEvent("onFullAudioData",t.data);break;default:console.log("Received unknown message",t.data)}}fireEvent(t,e){const s=this.handlers;s.length&&s.forEach((function(s){s.eventName===t&&s.handler(e)}))}addHandler(t,e){this.handlers.push({eventName:t,handler:e})}onInitialized(t){this.addHandler("onInitialized",t)}onEnded(t){this.addHandler("onEnded",t)}onError(t){this.addHandler("onError",t)}onMetadata(t){this.addHandler("onMetadata",t)}onProgress(t){this.addHandler("onProgress",t)}onFullAudioData(t){this.addHandler("onFullAudioData",t)}postMsg(t,e){this.processNode&&this.processNode.port.postMessage({cmd:t,val:e})}load(t){fetch(t).then((t=>t.arrayBuffer())).then((t=>this.play(t))).catch((t=>{this.fireEvent("onError",{type:"Load"})}))}play(t){this.postMsg("play",t)}stop(){this.postMsg("stop")}pause(){this.postMsg("pause")}unpause(){this.postMsg("unpause")}togglePause(){this.postMsg("togglePause")}setRepeatCount(t){this.postMsg("repeatCount",t)}setPitch(t){this.postMsg("setPitch",t)}setTempo(t){this.postMsg("setTempo",t)}setPos(t){this.postMsg("setPos",t)}setOrderRow(t,e){this.postMsg("setOrderRow",{o:t,r:e})}setVol(t){this.gain.gain.value=t}selectSubsong(t){this.postMsg("selectSubsong",t)}seek(t){this.setPos(t)}getCurrentTime(){return this.currentTime}decodeAll(t){this.postMsg("decodeAll",t)}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chiptune.js 2 | 3 | ## Info 4 | This is a javascript library that can play module music files. It is based on the [libopenmpt](https://lib.openmpt.org/libopenmpt) C/C++ library. To translate libopenmpt into Javascript [emscripten](https://github.com/kripken/emscripten) was used. 5 | 6 | Modernized ES6 module version with libopenmpt AudioWorklet backend 7 | 8 | **Please note**: The compiled `libopenmpt.worklet.js` in this repository is maybe outdated. 9 | 10 | Latest released and beta versions are available via "npm i chiptune". 11 | 12 | ## Demo 13 | See: https://DrSnuggles.github.io/chiptune 14 | 15 | Modland demo player: https://DrSnuggles.github.io/chiptune/demo.html 16 | 17 | Drop in your favorite songs. 18 | 19 | ## How to use 20 | - HTML: Include latest release version via https://drsnuggles.github.io/chiptune/chiptune3.min.js 21 | - NPM: "npm i chiptune3" will install latest release but there are also upcoming versions available 22 | - See index.html or demo.html for working examples 23 | 24 | ## Features 25 | 26 | * Play all tracker formats supported by libopenmpt (including mod, xm, s3m, it) 27 | * Simple Javascript API 28 | * Pause/Resume 29 | * Play local and remote files 30 | * Stereo playback 31 | * Module metadata 32 | * Looping mode 33 | * Volume control 34 | * Position control 35 | * Pattern data 36 | 37 | ## Build 38 | Docker was used to build the library 39 | 40 | CD into docker and run build.bat (Win) or build.sh (Linux) 41 | 42 | You can minify by "npm run minify" 43 | 44 | ## Chiptune Maintainers 45 | - v3: [DrSnuggles](https://github.com/DrSnuggles) 46 | - v1/v2: [deskjet](https://github.com/deskjet) 47 | 48 | ## v3 History 49 | - 2025-04-22: Emscripten 4.0.7 50 | - 2025-02-02: libopenmpt 0.7.13 + Emscripten 4.0.2 51 | - 2025-02-02: Issue #1: Convert to AudioBuffer, see convert.html 52 | - 2024-06-15: libopenmpt 0.7.8 + Emscripten 3.1.61 53 | - 2024-05-12: libopenmpt 0.7.7 + Emscripten 3.1.59 54 | - 2024-04-20: Emscripten 3.1.57 changes 55 | - 2024-03-24: Bumped to 0.7.6 56 | - 2024-03-18: Bumped to 0.7.5 using Emscripten 3.1.56 57 | - 2024-03-13: Bumped to 0.7.4 58 | - 2024-02-04: Metadata contains song, bugfixes and minify 59 | - 2024-01-24: Added config object, Modland player 60 | - 2024-01-23: Drag'n'Drop files. Build library using Docker. 61 | - 2024-01-22: Libopenmpt 0.7.3 compiled with Emscripten 3.1.51 62 | 63 | ## License 64 | 65 | All code in this project is MIT (X11) licensed. The only exception are the compiled libopenmpt parts which remain under the OpenMPT project BSD license. 66 | 67 | License text below: 68 | 69 | >Copyright © 2013-2024 The chiptune2.js/chiptune3.js contributers. 70 | > 71 | >Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 72 | > 73 | >The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 74 | > 75 | >THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 76 | 77 | -------------------------------------------------------------------------------- /chiptune3.js: -------------------------------------------------------------------------------- 1 | /* 2 | chiptune3 (worklet version) 3 | based on: https://deskjet.github.io/chiptune2.js/ 4 | */ 5 | 6 | const defaultCfg = { 7 | repeatCount: -1, // -1 = play endless, 0 = play once, do not repeat 8 | stereoSeparation: 100, // percents 9 | interpolationFilter: 0, // https://lib.openmpt.org/doc/group__openmpt__module__render__param.html 10 | context: false, 11 | } 12 | 13 | export class ChiptuneJsPlayer { 14 | constructor(cfg) { 15 | this.config = {...defaultCfg, ...cfg} 16 | 17 | if (this.config.context) { 18 | if (!this.config.context.destination) { 19 | //console.error('This is not an audio context.') 20 | throw('ChiptuneJsPlayer: This is not an audio context') 21 | } 22 | this.context = this.config.context 23 | this.destination = false 24 | } else { 25 | this.context = new AudioContext() 26 | this.destination = this.context.destination // output to speakers 27 | } 28 | delete this.config.context // remove from config, just used here and after init not changeable 29 | 30 | // make gainNode 31 | this.gain = this.context.createGain() 32 | this.gain.gain.value = 1 33 | 34 | this.handlers = [] 35 | 36 | // worklet 37 | this.context.audioWorklet.addModule( new URL('./chiptune3.worklet.js', import.meta.url) ) 38 | .then(()=>{ 39 | this.processNode = new AudioWorkletNode(this.context, 'libopenmpt-processor', { 40 | numberOfInputs: 0, 41 | numberOfOutputs: 1, 42 | outputChannelCount: [2] 43 | }) 44 | // message port 45 | this.processNode.port.onmessage = this.handleMessage_.bind(this) 46 | this.processNode.port.postMessage({cmd:'config', val:this.config}) 47 | this.fireEvent('onInitialized') 48 | 49 | // audio routing 50 | this.processNode.connect(this.gain) 51 | if (this.destination) this.gain.connect(this.destination) // also connect to output if no gainNode was given 52 | }) 53 | .catch(e=>console.error(e)) 54 | } 55 | 56 | // msg from worklet 57 | handleMessage_(msg) { 58 | switch (msg.data.cmd) { 59 | case 'meta': 60 | this.meta = msg.data.meta 61 | this.duration = msg.data.meta.dur 62 | this.fireEvent('onMetadata', this.meta) 63 | break 64 | case 'pos': 65 | //this.meta.pos = msg.data.pos 66 | this.currentTime = msg.data.pos 67 | this.order = msg.data.order 68 | this.pattern = msg.data.pattern 69 | this.row = msg.data.row 70 | this.fireEvent('onProgress', msg.data) 71 | break 72 | case 'end': 73 | this.fireEvent('onEnded') 74 | break 75 | case 'err': 76 | this.fireEvent('onError', {type: msg.data.val}) 77 | break 78 | case 'fullAudioData': 79 | this.fireEvent('onFullAudioData', msg.data) 80 | break 81 | default: 82 | console.log('Received unknown message',msg.data) 83 | } 84 | } 85 | 86 | // handlers 87 | fireEvent(eventName, response) { 88 | const handlers = this.handlers 89 | if (handlers.length) { 90 | handlers.forEach(function (handler) { 91 | if (handler.eventName === eventName) { 92 | handler.handler(response) 93 | } 94 | }) 95 | } 96 | } 97 | addHandler(eventName, handler) { this.handlers.push({eventName: eventName, handler: handler}) } 98 | onInitialized(handler) { this.addHandler('onInitialized', handler) } 99 | onEnded(handler) { this.addHandler('onEnded', handler) } 100 | onError(handler) { this.addHandler('onError', handler) } 101 | onMetadata(handler) { this.addHandler('onMetadata', handler) } 102 | onProgress(handler) { this.addHandler('onProgress', handler) } 103 | onFullAudioData(handler) { this.addHandler('onFullAudioData', handler) } 104 | 105 | // methods 106 | postMsg(cmd, val) { 107 | if (this.processNode) 108 | this.processNode.port.postMessage({cmd:cmd,val:val}) 109 | } 110 | load(url) { 111 | fetch(url) 112 | .then(response => response.arrayBuffer()) 113 | .then(arrayBuffer => this.play(arrayBuffer)) 114 | .catch(e=>{this.fireEvent('onError', {type: 'Load'})}) 115 | } 116 | play(val) { this.postMsg('play', val) } 117 | stop() { this.postMsg('stop') } 118 | pause() { this.postMsg('pause') } 119 | unpause() { this.postMsg('unpause') } 120 | togglePause() { this.postMsg('togglePause') } 121 | setRepeatCount(val) { this.postMsg('repeatCount', val) } 122 | setPitch(val) { this.postMsg('setPitch', val) } 123 | setTempo(val) { this.postMsg('setTempo', val) } 124 | setPos(val) { this.postMsg('setPos', val) } 125 | setOrderRow(o,r) { this.postMsg('setOrderRow', {o:o,r:r}) } 126 | setVol(val) { this.gain.gain.value = val } 127 | selectSubsong(val) { this.postMsg('selectSubsong', val) } 128 | // compatibility 129 | seek(val) { this.setPos(val) } 130 | getCurrentTime() { return this.currentTime } 131 | decodeAll(ab) { this.postMsg('decodeAll', ab) } 132 | } 133 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chiptune Worklet Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | Chiptune3 15 | This is an updated Chiptune ES6 module version to work with libopenmpt AudioWorklet.
16 | Drop in your favorite module.
17 | Usage:
 18 | // Import ES6 module
 19 | import {ChiptuneJsPlayer} from 'https://DrSnuggles.github.io/chiptune/chiptune3.js'
 20 | 
 21 | // Create instance (wherever you want)
 22 | window.chiptune = new ChiptuneJsPlayer()
 23 | // if you already have an audioContext you can provide here, but then chiptune does not route to speakers! Connect chiptune.gain
 24 | 
 25 | // Wait for ready
 26 | chiptune.onInitialized(() => {
 27 | 	// Play awesome music
 28 | 	chiptune.load('https://deskjet.github.io/chiptune2.js/tunes/chipsounds.mod')
 29 | })
30 |
31 | 32 |
33 |
34 | Methods 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 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
chiptune.load(url)
chiptune.play(arrayBuffer)Play buffer. This is also called by load.
chiptune.stop()
chiptune.pause()
chiptune.unpause()
chiptune.togglePause()
chiptune.setRepeatCount(val)-1 = endless; 0 = just play once
chiptune.selectSubsong(val)
chiptune.setPos(float)
70 | chiptune.seek(float) 71 |
chiptune.setVol(float)
chiptune.setPitch(float)
chiptune.setTempo(float)
87 |
88 |
89 | 90 |
91 |
92 | Events 93 | chiptune.onInitialized = () => {}
94 | chiptune.onEnded = () => {}
95 | chiptune.onError = (err) => {}
96 | chiptune.onMetadata = (meta) => {}
97 | chiptune.onProgress = (pos) => {} 98 |
99 |
100 | 101 |
102 |
103 | Metadata 104 |
105 |
106 | 107 | 108 |
👉 💻 👂 🎶
109 |
110 | 111 | 156 | 157 | -------------------------------------------------------------------------------- /chiptune3.worklet.min.js: -------------------------------------------------------------------------------- 1 | import libopenmptPromise from"./libopenmpt.worklet.js";const OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT=2,OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH=3;let libopenmpt;function asciiToStack(t){const e=libopenmpt.stackAlloc(t.length+1);return writeAsciiToMemory(t,e),e}function writeAsciiToMemory(t,e,o){for(let o=0;o>0]=t.charCodeAt(o);o||(libopenmpt.HEAP8[e>>0]=0)}libopenmptPromise().then((t=>{if(libopenmpt=t,!libopenmpt.stackSave)return;let e=libopenmpt.stackSave();libopenmpt.version=libopenmpt.UTF8ToString(libopenmpt._openmpt_get_string(asciiToStack("library_version"))),libopenmpt.build=libopenmpt.UTF8ToString(libopenmpt._openmpt_get_string(asciiToStack("build"))),libopenmpt.stackRestore(e)})).catch((t=>console.error(t)));class MPT extends AudioWorkletProcessor{constructor(){super(),this.port.onmessage=this.handleMessage_.bind(this),this.paused=!1,this.config={repeatCount:-1,stereoSeparation:100,interpolationFilter:0},this.channels=0}process(t,e,o){if(!this.modulePtr||!this.leftPtr||!this.rightPtr||this.paused)return!0;const p=e[0][0],s=e[0][1],r=libopenmpt._openmpt_module_read_float_stereo(this.modulePtr,sampleRate,p.length,this.leftPtr,this.rightPtr);if(0==r){return!this.modulePtr?this.port.postMessage({cmd:"err",val:"Process"}):this.port.postMessage({cmd:"end"}),!0}p.set(libopenmpt.HEAPF32.subarray(this.leftPtr/4,this.leftPtr/4+r)),s.set(libopenmpt.HEAPF32.subarray(this.rightPtr/4,this.rightPtr/4+r));let i={cmd:"pos",pos:libopenmpt._openmpt_module_get_position_seconds(this.modulePtr),order:libopenmpt._openmpt_module_get_current_order(this.modulePtr),pattern:libopenmpt._openmpt_module_get_current_pattern(this.modulePtr),row:libopenmpt._openmpt_module_get_current_row(this.modulePtr)};return this.port.postMessage(i),!0}handleMessage_(t){const e=t.data.val;switch(t.data.cmd){case"config":this.config=e;break;case"play":this.play(e,!1);break;case"pause":this.paused=!0;break;case"unpause":this.paused=!1;break;case"togglePause":this.paused=!this.paused;break;case"stop":this.stop();break;case"meta":this.meta();break;case"repeatCount":if(this.config.repeatCount=e,!this.modulePtr)return;libopenmpt._openmpt_module_set_repeat_count(this.modulePtr,this.config.repeatCount);break;case"setPitch":if(!libopenmpt.stackSave||!this.modulePtr)return;libopenmpt._openmpt_module_ctl_set(this.modulePtr,asciiToStack("play.pitch_factor"),asciiToStack(e.toString()));break;case"setTempo":if(!libopenmpt.stackSave||!this.modulePtr)return;libopenmpt._openmpt_module_ctl_set(this.modulePtr,asciiToStack("play.tempo_factor"),asciiToStack(e.toString()));break;case"selectSubsong":if(!this.modulePtr)return;libopenmpt._openmpt_module_select_subsong(this.modulePtr,e);break;case"setPos":if(!this.modulePtr)return;libopenmpt._openmpt_module_set_position_seconds(this.modulePtr,e);break;case"setOrderRow":if(!this.modulePtr)return;libopenmpt._openmpt_module_set_position_order_row(this.modulePtr,e.o,e.r);break;case"decodeAll":this.decodeAll(e);break;default:console.log("Received unknown message",t.data)}}decodeAll(t){this.play(t,!0);const e=[],o=[];let p=libopenmpt._openmpt_module_read_float_stereo(this.modulePtr,sampleRate,128,this.leftPtr,this.rightPtr);for(;0!=p;)e.push(...libopenmpt.HEAPF32.subarray(this.leftPtr/4,this.leftPtr/4+p)),o.push(...libopenmpt.HEAPF32.subarray(this.rightPtr/4,this.rightPtr/4+p)),p=libopenmpt._openmpt_module_read_float_stereo(this.modulePtr,sampleRate,128,this.leftPtr,this.rightPtr);let s={cmd:"fullAudioData",meta:this.getMeta(),data:[e,o]};this.port.postMessage(s),this.stop()}play(t,e=!1){this.stop();const o=new Int8Array(t),p=libopenmpt._malloc(o.byteLength);if(libopenmpt.HEAPU8.set(o,p),this.modulePtr=libopenmpt._openmpt_module_create_from_memory(p,o.byteLength,0,0,0),0!==this.modulePtr){if(libopenmpt.stackSave){const t=libopenmpt.stackSave();libopenmpt._openmpt_module_ctl_set(this.modulePtr,asciiToStack("render.resampler.emulate_amiga"),asciiToStack("1")),libopenmpt._openmpt_module_ctl_set(this.modulePtr,asciiToStack("render.resampler.emulate_amiga_type"),asciiToStack("a1200")),libopenmpt.stackRestore(t)}this.paused=e,this.leftPtr=libopenmpt._malloc(512),this.rightPtr=libopenmpt._malloc(512),libopenmpt._openmpt_module_set_repeat_count(this.modulePtr,this.config.repeatCount),libopenmpt._openmpt_module_set_render_param(this.modulePtr,2,this.config.stereoSeparation),libopenmpt._openmpt_module_set_render_param(this.modulePtr,3,this.config.interpolationFilter),e||this.meta()}else this.port.postMessage({cmd:"err",val:"ptr"})}stop(){this.modulePtr&&(0!=this.modulePtr&&(libopenmpt._openmpt_module_destroy(this.modulePtr),this.modulePtr=0),0!=this.leftBufferPtr&&(libopenmpt._free(this.leftBufferPtr),this.leftBufferPtr=0),0!=this.rightBufferPtr&&(libopenmpt._free(this.rightBufferPtr),this.rightBufferPtr=0),this.channels=0)}meta(){this.port.postMessage({cmd:"meta",meta:this.getMeta()})}getSong(){if(!libopenmpt.UTF8ToString||!this.modulePtr)return!1;let t={channels:[],instruments:[],samples:[],orders:[],numSubsongs:libopenmpt._openmpt_module_get_num_subsongs(this.modulePtr),patterns:[]};const e=libopenmpt._openmpt_module_get_num_channels(this.modulePtr);this.channel=e;for(let o=0;o 2 | 3 | 4 | Chiptune Worklet Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | Chiptune3 Drag'N'Drop converter 15 | Sampling rate: 8000-192000 kHz
16 | Stereo separation: 0=Mono, 100=Amigaaaaa
17 | Interpolation filter:
24 |
25 | File type: Used by recorder
26 | audioBitrateMode:
30 | audioBitsPerSecond: Bitrate for compressed audio
31 |
32 | 33 |
34 | Status: 35 |
36 |
37 | Converted songs 38 |
39 |
40 | 41 | 42 |
👉 💻 👂 🎶
43 |
44 | 45 | 185 | 186 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | import {LDR} from 'https://DrSnuggles.github.io/LDR/ldr-zip.min.js' 2 | import {kkRows} from 'https://DrSnuggles.github.io/kkRows/js/kk-rows.min.js' 3 | import {Visualizer} from 'https://DrSnuggles.github.io/visualizer/visualizer.min.js' 4 | import {dnd} from './dnd.js' 5 | import {ChiptuneJsPlayer} from './chiptune3.min.js' 6 | 7 | let isLoading = false 8 | 9 | function setMetadata() { 10 | const metadata = player.meta 11 | if(!metadata) return 12 | document.getElementById('title').innerHTML = metadata['title'] 13 | 14 | var subsongs = player.meta.songs 15 | document.getElementById('subsongs').style.display = (subsongs.length > 1) ? 'block' : 'none' 16 | if(subsongs.length > 1) { 17 | var select = document.getElementById('subsong') 18 | // remove old 19 | for (let i = select.options.length-1; i > -1; i--) select.removeChild(select.options[i]) 20 | var elt = document.createElement('option') 21 | elt.textContent = 'Play all subsongs' 22 | elt.value = -1 23 | select.appendChild(elt) 24 | for(var i = 0; i < subsongs.length; i++) { 25 | var elt = document.createElement('option') 26 | elt.textContent = subsongs[i] 27 | elt.value = i 28 | select.appendChild(elt) 29 | } 30 | select.selectedIndex = 0 31 | player.selectSubsong(-1) 32 | } 33 | 34 | document.getElementById('seekbar').value = 0 35 | updateDuration() 36 | 37 | document.getElementById('library-version').innerHTML = 'Version: '+ player.meta.libopenmptVersion +' ('+ player.meta.libopenmptBuild +')' 38 | } 39 | 40 | function updateDuration() { 41 | //var sec_num = player.duration() 42 | var sec_num = player.meta.dur 43 | var minutes = Math.floor(sec_num / 60) 44 | var seconds = Math.floor(sec_num % 60) 45 | if (seconds < 10) {seconds = '0' + seconds } 46 | document.getElementById('duration').innerHTML = minutes + ':' + seconds 47 | document.getElementById('seekbar').max = sec_num 48 | } 49 | 50 | // init ChiptunePlayer 51 | function initPlayer() { 52 | window.player = new ChiptuneJsPlayer({repeatCount: 0}) 53 | player.gain.gain.value = 0.5 54 | window.viz = new Visualizer(player.gain, myCanvas, {fft:11}) 55 | 56 | // listen to events 57 | player.onEnded((ev) => { 58 | if(document.getElementById('autoplay').checked) { 59 | nextSong() 60 | } 61 | }) 62 | player.onMetadata((meta) => { 63 | player.meta = meta 64 | setMetadata(document.getElementById('modfilename').innerHTML) 65 | }) 66 | player.onProgress((dat) => { 67 | document.getElementById('seekbar').value = dat.pos 68 | }) 69 | player.onError((err) => { 70 | nextSong() 71 | }) 72 | 73 | // need to wait till player finished init 74 | function lateInit() { 75 | if (!player.processNode) { 76 | setTimeout(()=>{lateInit()},100) 77 | return 78 | } 79 | // ready! 80 | nextSong() 81 | } 82 | lateInit() 83 | 84 | } 85 | 86 | window.nextSong = (url) => { 87 | if (isLoading) return 88 | if (url == undefined) { 89 | url = songSel.worker.postMessage({msg:'getRandom', callback:'songSelCallback'}) 90 | return 91 | } 92 | const parts = url.split('/') 93 | document.getElementById('modfilename').innerText = parts[parts.length-1] 94 | 95 | isLoading = true 96 | LDR.loadURL(url, (o)=>{ 97 | if (!o.dat) return // not yet ready (damn, i need a 2nd callback both in one is not nice) 98 | const buffer = o.dat 99 | 100 | player.play(buffer) 101 | isLoading = false 102 | 103 | pitch.value = 1 104 | tempo.value = 1 105 | sizeInKB.innerText = (buffer.byteLength/1024).toFixed(2) 106 | }) 107 | } 108 | 109 | // stupid no audio till user interaction policy thingy 110 | function userInteracted() { 111 | removeEventListener('keydown', userInteracted) 112 | removeEventListener('click', userInteracted) 113 | removeEventListener('touchend', userInteracted) 114 | removeEventListener('contextmenu', userInteracted) 115 | 116 | audioModal.classList.add('fadeOut') 117 | 118 | initPlayer() 119 | 120 | } 121 | addEventListener('keydown', userInteracted) 122 | addEventListener('click', userInteracted) 123 | addEventListener('touchend', userInteracted) 124 | addEventListener('contextmenu', userInteracted) 125 | 126 | 127 | init() 128 | function init() { 129 | const allowedExt = 'mptm mod s3m xm it 669 amf ams c67 dbm digi dmf dsm dsym dtm far fmt ice j2b m15 mdl med mms mt2 mtm mus nst okt plm psm pt36 ptm sfx sfx2 st26 stk stm stx stp symmod ult wow gdm mo3 oxm umx xpk ppm mmcmp'.split(' ') 130 | // removed: imf 131 | let data = [] 132 | 133 | let url = 'https://modland.com/allmods.zip' 134 | LDR.loadURL(url, (o)=>{ 135 | if (!o.dat) return // not finished yet 136 | o.dat = o.dat['allmods.txt'] 137 | const decoder = new TextDecoder() 138 | const txt = decoder.decode(o.dat) 139 | let rows = txt.split('\n') 140 | console.log(rows.length +' entries in '+ url) 141 | let found = 0 142 | for (let i = 0; i < rows.length; i++) { 143 | const cols = rows[i].split('\t') 144 | if (cols.length < 2) continue 145 | const tmp = cols[1].split('.') 146 | const ext = (tmp[tmp.length-1] == 'zip') ? tmp[tmp.length-2] : tmp[tmp.length-1] //last = ZIP 147 | if (allowedExt.indexOf(ext) !== -1) { 148 | const parts = cols[1].split('/') 149 | const songname = parts[parts.length-1].replace('.zip','').replace('.'+ext,'') // songname is always the last part 150 | let tracker, artist 151 | // modland 152 | tracker = parts[0] 153 | artist = (parts.length == 5) ? parts[2] : parts[1] 154 | data.push( ['https://modland.com/pub/modules/'+cols[1], tracker, artist, songname, (cols[0]/1024).toFixed(2)] ) 155 | found++ 156 | } 157 | } 158 | console.log(found +' ('+(found/rows.length*100).toFixed(2)+'%) entries can be played by libopenmpt') 159 | 160 | data = data.sort() 161 | 162 | // set html 163 | document.body.innerHTML = `
164 | 165 | 166 |
167 |   168 |
169 | Filename: ( kB) 170 |
171 | Title: () 172 |
173 | Position: 174 |
175 | Volume: 176 |
177 | Pitch: 178 |
179 | Tempo: 180 |
181 | 182 |
183 | 184 | 185 | 186 |
👉 💻 👂 🎶
` 187 | 188 | document.getElementById('songSel').setAttribute('data', JSON.stringify(data) ) 189 | // kkRows clicked/getRandom callback 190 | window.songSelCallback = (r) => { 191 | nextSong( r.rng ? r.rng[0] : r[0] ) 192 | } 193 | 194 | LDR.background = true 195 | 196 | oncontextmenu = (ev) => { 197 | nextSong() 198 | ev.preventDefault() 199 | } 200 | 201 | // dnd 202 | dnd(window, (aB) => { 203 | modfilename.innerHTML = '' 204 | sizeInKB.innerHTML = '' 205 | player.play(aB) 206 | setMetadata() 207 | }) 208 | }) 209 | } 210 | -------------------------------------------------------------------------------- /chiptune3.worklet.js: -------------------------------------------------------------------------------- 1 | /* 2 | AudioWorklet: DrSnuggles 3 | */ 4 | 5 | import libopenmptPromise from './libopenmpt.worklet.js' 6 | 7 | // consts 8 | const OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT = 2 9 | const OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH = 3 10 | 11 | // vars 12 | let libopenmpt 13 | 14 | // init 15 | libopenmptPromise() 16 | .then(res => { 17 | libopenmpt = res 18 | 19 | if (!libopenmpt.stackSave) return 20 | // set libopenmpt version to display later 21 | let stack = libopenmpt.stackSave() 22 | libopenmpt.version = libopenmpt.UTF8ToString(libopenmpt._openmpt_get_string(asciiToStack('library_version'))) 23 | libopenmpt.build = libopenmpt.UTF8ToString(libopenmpt._openmpt_get_string(asciiToStack('build'))) 24 | libopenmpt.stackRestore(stack) 25 | }) 26 | .catch(e => console.error(e)) 27 | 28 | // 29 | // Helpers 30 | // 31 | function asciiToStack(str) { 32 | const stackStr = libopenmpt.stackAlloc(str.length + 1) // DrS: needed to export in emscripten 33 | writeAsciiToMemory(str, stackStr) // no longer in Emscripten, see below 34 | return stackStr 35 | } 36 | function writeAsciiToMemory(str,buffer,dontAddNull){for(let i=0;i>0]=str.charCodeAt(i)}if(!dontAddNull)libopenmpt.HEAP8[buffer>>0]=0} 37 | 38 | 39 | // 40 | // Processor 41 | // 42 | class MPT extends AudioWorkletProcessor { 43 | constructor() { 44 | super() 45 | this.port.onmessage = this.handleMessage_.bind(this) 46 | this.paused = false 47 | this.config = { 48 | repeatCount: -1, // -1 = play endless, 0 = play once, do not repeat 49 | stereoSeparation: 100, // percents 50 | interpolationFilter: 0, // https://lib.openmpt.org/doc/group__openmpt__module__render__param.html 51 | } 52 | this.channels = 0 53 | } 54 | 55 | process(inputList, outputList, parameters) { 56 | if (!this.modulePtr || !this.leftPtr || !this.rightPtr || this.paused) return true //silence 57 | 58 | const left = outputList[0][0] 59 | const right = outputList[0][1] 60 | 61 | const actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, sampleRate, left.length, this.leftPtr, this.rightPtr) 62 | if (actualFramesPerChunk == 0) { 63 | //this.paused = true 64 | // modulePtr will be 0 on openmpt: error: openmpt_module_read_float_stereo: ERROR: module * not valid or other openmpt error 65 | const error = !this.modulePtr 66 | if (error) { 67 | this.port.postMessage({cmd:'err',val:'Process'}) 68 | } else { 69 | this.port.postMessage({cmd:'end'}) 70 | } 71 | return true 72 | } 73 | 74 | left.set(libopenmpt.HEAPF32.subarray(this.leftPtr / 4, this.leftPtr / 4 + actualFramesPerChunk)) 75 | right.set(libopenmpt.HEAPF32.subarray(this.rightPtr / 4, this.rightPtr / 4 + actualFramesPerChunk)) 76 | 77 | // post progress 78 | // openmpt_module_get_current_order 79 | 80 | let msg = { 81 | cmd: 'pos', 82 | pos: libopenmpt._openmpt_module_get_position_seconds(this.modulePtr), 83 | // pos in song 84 | order: libopenmpt._openmpt_module_get_current_order(this.modulePtr), 85 | pattern: libopenmpt._openmpt_module_get_current_pattern(this.modulePtr), 86 | row: libopenmpt._openmpt_module_get_current_row(this.modulePtr), 87 | // channel volumes 88 | //chVol: [], // ch0Left, ch0Right, ch1Left, ... 89 | } 90 | /* 91 | for (let i = 0; i < this.channels; i++) { 92 | msg.chVol.push( { 93 | left: libopenmpt._openmpt_module_get_current_channel_vu_left(this.modulePtr, i), 94 | right: libopenmpt._openmpt_module_get_current_channel_vu_right(this.modulePtr, i), 95 | }) 96 | } 97 | */ 98 | 99 | this.port.postMessage( msg ) 100 | 101 | return true // def. needed for Chrome 102 | } 103 | 104 | handleMessage_(msg) { 105 | //console.log('[Processor:Received]',msg.data) 106 | const v = msg.data.val 107 | switch (msg.data.cmd) { 108 | case 'config': 109 | this.config = v 110 | break 111 | case 'play': 112 | this.play(v, false) 113 | break 114 | case 'pause': 115 | this.paused = true 116 | break 117 | case 'unpause': 118 | this.paused = false 119 | break 120 | case 'togglePause': 121 | this.paused = !this.paused 122 | break 123 | case 'stop': 124 | this.stop() 125 | break 126 | case 'meta': 127 | this.meta() 128 | break 129 | case 'repeatCount': 130 | this.config.repeatCount = v 131 | if (!this.modulePtr) return 132 | libopenmpt._openmpt_module_set_repeat_count(this.modulePtr, this.config.repeatCount) 133 | break 134 | case 'setPitch': 135 | if (!libopenmpt.stackSave || !this.modulePtr) return 136 | libopenmpt._openmpt_module_ctl_set(this.modulePtr, asciiToStack('play.pitch_factor'), asciiToStack(v.toString())) 137 | break 138 | case 'setTempo': 139 | if (!libopenmpt.stackSave || !this.modulePtr) return 140 | libopenmpt._openmpt_module_ctl_set(this.modulePtr, asciiToStack('play.tempo_factor'), asciiToStack(v.toString())) 141 | break 142 | case 'selectSubsong': 143 | if (!this.modulePtr) return 144 | libopenmpt._openmpt_module_select_subsong(this.modulePtr, v) 145 | //this.meta() 146 | break 147 | case 'setPos': 148 | if (!this.modulePtr) return 149 | libopenmpt._openmpt_module_set_position_seconds(this.modulePtr, v) 150 | break 151 | case 'setOrderRow': 152 | if (!this.modulePtr) return 153 | libopenmpt._openmpt_module_set_position_order_row(this.modulePtr, v.o, v.r) 154 | break 155 | /* 156 | case 'toggleMute' 157 | // openmpt_module_ext_get_interface(mod_ext, interface_id, interface, interface_size) 158 | // openmpt_module_ext_interface_interactive 159 | // set_channel_mute_status 160 | // https://lib.openmpt.org/doc/group__libopenmpt__ext__c.html#ga0275a35da407cd092232a20d3535c9e4 161 | if (!this.modulePtr) return 162 | //const extPtr = libopenmpt.openmpt_module_ext_get_interface(mod_ext, interface_id, interface, interface_size) 163 | break 164 | */ 165 | case 'decodeAll': 166 | this.decodeAll(v) 167 | break 168 | default: 169 | console.log('Received unknown message',msg.data) 170 | } 171 | } // handleMessage_ 172 | 173 | decodeAll(buffer) { 174 | this.play(buffer, true) 175 | 176 | // now build the full audioData 177 | const left = [], right = [] 178 | const maxFramesPerChunk = 128 179 | let actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, sampleRate, maxFramesPerChunk, this.leftPtr, this.rightPtr) 180 | while (actualFramesPerChunk != 0) { 181 | left.push( ...libopenmpt.HEAPF32.subarray(this.leftPtr / 4, this.leftPtr / 4 + actualFramesPerChunk) ) 182 | right.push( ...libopenmpt.HEAPF32.subarray(this.rightPtr / 4, this.rightPtr / 4 + actualFramesPerChunk) ) 183 | actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, sampleRate, maxFramesPerChunk, this.leftPtr, this.rightPtr) 184 | } 185 | 186 | // post final data 187 | let msg = { 188 | cmd: 'fullAudioData', 189 | meta: this.getMeta(), 190 | data: [left,right] 191 | } 192 | this.port.postMessage( msg ) 193 | 194 | this.stop() 195 | } 196 | 197 | play(buffer, paused = false) { 198 | this.stop() 199 | 200 | const maxFramesPerChunk = 128 // thats what worklet is using 201 | const byteArray = new Int8Array(buffer) 202 | const ptrToFile = libopenmpt._malloc(byteArray.byteLength) 203 | libopenmpt.HEAPU8.set(byteArray, ptrToFile) 204 | this.modulePtr = libopenmpt._openmpt_module_create_from_memory(ptrToFile, byteArray.byteLength, 0, 0, 0) 205 | 206 | if(this.modulePtr === 0) { 207 | // could not create module 208 | this.port.postMessage({cmd:'err',val:'ptr'}) 209 | return 210 | } 211 | 212 | if (libopenmpt.stackSave) { 213 | const stack = libopenmpt.stackSave() 214 | libopenmpt._openmpt_module_ctl_set(this.modulePtr, asciiToStack('render.resampler.emulate_amiga'), asciiToStack('1')) 215 | libopenmpt._openmpt_module_ctl_set(this.modulePtr, asciiToStack('render.resampler.emulate_amiga_type'), asciiToStack('a1200')) 216 | //libopenmpt._openmpt_module_ctl_set('play.pitch_factor', e.target.value.toString()); 217 | 218 | libopenmpt.stackRestore(stack) 219 | } 220 | 221 | this.paused = paused 222 | this.leftPtr = libopenmpt._malloc(4 * maxFramesPerChunk) // 4x = float 223 | this.rightPtr = libopenmpt._malloc(4 * maxFramesPerChunk) 224 | 225 | // set config options on module 226 | libopenmpt._openmpt_module_set_repeat_count(this.modulePtr, this.config.repeatCount) 227 | libopenmpt._openmpt_module_set_render_param(this.modulePtr, OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT, this.config.stereoSeparation) 228 | libopenmpt._openmpt_module_set_render_param(this.modulePtr, OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH, this.config.interpolationFilter) 229 | 230 | // post back tracks metadata 231 | if (!paused) this.meta() 232 | } 233 | stop() { 234 | if (!this.modulePtr) return 235 | if (this.modulePtr != 0) { 236 | libopenmpt._openmpt_module_destroy(this.modulePtr) 237 | this.modulePtr = 0 238 | } 239 | if (this.leftBufferPtr != 0) { 240 | libopenmpt._free(this.leftBufferPtr) 241 | this.leftBufferPtr = 0 242 | } 243 | if (this.rightBufferPtr != 0) { 244 | libopenmpt._free(this.rightBufferPtr) 245 | this.rightBufferPtr = 0 246 | } 247 | this.channels = 0 248 | } 249 | meta() { 250 | this.port.postMessage({cmd: 'meta', meta: this.getMeta()}) 251 | } 252 | getSong() { 253 | if (!libopenmpt.UTF8ToString || !this.modulePtr) return false 254 | 255 | // https://lib.openmpt.org/doc/ 256 | let song = { 257 | channels: [], 258 | instruments: [], 259 | samples: [], 260 | orders: [], 261 | numSubsongs: libopenmpt._openmpt_module_get_num_subsongs(this.modulePtr), 262 | patterns: [], 263 | } 264 | // channels 265 | const chNum = libopenmpt._openmpt_module_get_num_channels(this.modulePtr) 266 | this.channel = chNum 267 | for (let i = 0; i < chNum; i++) { 268 | song.channels.push( libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_channel_name(this.modulePtr, i)) ) 269 | } 270 | // instruments 271 | for (let i = 0, e = libopenmpt._openmpt_module_get_num_instruments(this.modulePtr); i < e; i++) { 272 | song.instruments.push( libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_instrument_name(this.modulePtr, i)) ) 273 | } 274 | // samples 275 | for (let i = 0, e = libopenmpt._openmpt_module_get_num_samples(this.modulePtr); i < e; i++) { 276 | song.samples.push( libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_sample_name(this.modulePtr, i)) ) 277 | } 278 | // orders 279 | for (let i = 0, e = libopenmpt._openmpt_module_get_num_orders(this.modulePtr); i < e; i++) { 280 | song.orders.push( { 281 | name: libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_order_name(this.modulePtr, i)), 282 | pat: libopenmpt._openmpt_module_get_order_pattern(this.modulePtr, i), 283 | }) 284 | } 285 | // patterns 286 | for (let patIdx = 0, patNum = libopenmpt._openmpt_module_get_num_patterns(this.modulePtr); patIdx < patNum; patIdx++) { 287 | const pattern = { 288 | name: libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_pattern_name(this.modulePtr, patIdx)), 289 | rows: [], 290 | } 291 | // rows 292 | for(let rowIdx = 0, rowNum = libopenmpt._openmpt_module_get_pattern_num_rows(this.modulePtr, patIdx); rowIdx < rowNum; rowIdx++) { 293 | const row = [] 294 | // channels 295 | for (let chIdx = 0; chIdx < chNum; chIdx++) { 296 | const channel = [] 297 | for (let comIdx = 0; comIdx < 6; comIdx++) { 298 | /* commands 299 | OPENMPT_MODULE_COMMAND_NOTE = 0 300 | OPENMPT_MODULE_COMMAND_INSTRUMENT = 1 301 | OPENMPT_MODULE_COMMAND_VOLUMEEFFECT = 2 302 | OPENMPT_MODULE_COMMAND_EFFECT = 3 303 | OPENMPT_MODULE_COMMAND_VOLUME = 4 304 | OPENMPT_MODULE_COMMAND_PARAMETER = 5 305 | */ 306 | channel.push( libopenmpt._openmpt_module_get_pattern_row_channel_command(this.modulePtr, patIdx, rowIdx, chIdx, comIdx) ) 307 | } 308 | row.push( channel ) 309 | } 310 | pattern.rows.push( row ) 311 | } 312 | song.patterns.push( pattern ) 313 | } 314 | 315 | 316 | return song 317 | } 318 | getMeta() { 319 | if (!libopenmpt.UTF8ToString || !this.modulePtr) return false 320 | 321 | const data = {} 322 | data.dur = libopenmpt._openmpt_module_get_duration_seconds(this.modulePtr) 323 | if (data.dur == 0) { 324 | // looks like an error occured reading the mod 325 | this.port.postMessage({cmd:'err',val:'dur'}) 326 | } 327 | const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.modulePtr)).split(';') 328 | for (let i = 0; i < keys.length; i++) { 329 | const keyNameBuffer = libopenmpt._malloc(keys[i].length + 1) 330 | writeAsciiToMemory(keys[i], keyNameBuffer) 331 | data[keys[i]] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.modulePtr, keyNameBuffer)) 332 | libopenmpt._free(keyNameBuffer) 333 | } 334 | data.song = this.getSong() 335 | data.totalOrders = data.song.orders.length // libopenmpt._openmpt_module_get_num_orders(this.modulePtr) 336 | data.totalPatterns = data.song.patterns.length// libopenmpt._openmpt_module_get_num_patterns(this.modulePtr) 337 | data.songs = this.getSongs() 338 | data.libopenmptVersion = libopenmpt.version 339 | data.libopenmptBuild = libopenmpt.build 340 | return data 341 | } 342 | getSongs() { 343 | if (!libopenmpt.UTF8ToString) return '' // todo: ?? why string here 344 | 345 | const subsongs = libopenmpt._openmpt_module_get_num_subsongs(this.modulePtr) 346 | const names = [] 347 | for (let i = 0; i < subsongs; i++) { 348 | const namePtr = libopenmpt._openmpt_module_get_subsong_name(this.modulePtr, i) 349 | const name = libopenmpt.UTF8ToString(namePtr) 350 | if(name != "") { 351 | names.push(name) 352 | } else { 353 | names.push("Subsong " + (i + 1)) 354 | } 355 | libopenmpt._openmpt_free_string(namePtr) 356 | } 357 | return names 358 | } 359 | 360 | } 361 | 362 | registerProcessor('libopenmpt-processor', MPT) --------------------------------------------------------------------------------