├── .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 |
31 |
32 |
33 |
88 |
89 |
90 |
91 |
99 |
100 |
101 |
102 |
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 |
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 |
Subsongs:
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)
--------------------------------------------------------------------------------