initial commit
@@ -0,0 +1 @@
|
||||
Place the three modem intro audio clips here: dial-tone.mp3, dtmf-beeps.mp3, modem-handshake.mp3. Only sound in the project (PRD.md 5.1/6).
|
||||
|
After Width: | Height: | Size: 769 B |
@@ -0,0 +1 @@
|
||||
Production web images live here. Large downloadable assets belong on files.moonweb.org (PRD.md 10).
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
chiptune3 (worklet version) - patched for <script> loading
|
||||
Source: npm chiptune3, only export+import.meta.url changed
|
||||
*/
|
||||
|
||||
const defaultCfg = {
|
||||
repeatCount: -1,
|
||||
stereoSeparation: 100,
|
||||
interpolationFilter: 0,
|
||||
context: false,
|
||||
}
|
||||
|
||||
class ChiptuneJsPlayer {
|
||||
constructor(cfg) {
|
||||
this.config = {...defaultCfg, ...cfg}
|
||||
|
||||
if (this.config.context) {
|
||||
if (!this.config.context.destination) {
|
||||
throw('ChiptuneJsPlayer: This is not an audio context')
|
||||
}
|
||||
this.context = this.config.context
|
||||
this.destination = false
|
||||
} else {
|
||||
this.context = new AudioContext()
|
||||
this.destination = this.context.destination
|
||||
}
|
||||
delete this.config.context
|
||||
|
||||
this.gain = this.context.createGain()
|
||||
this.gain.gain.value = 1
|
||||
|
||||
this.handlers = []
|
||||
|
||||
this.context.audioWorklet.addModule('/js/chiptune3.worklet.js')
|
||||
.then(()=>{
|
||||
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)
|
||||
if (this.destination) this.gain.connect(this.destination)
|
||||
})
|
||||
.catch(e=>console.error(e))
|
||||
}
|
||||
|
||||
handleMessage_(msg) {
|
||||
switch (msg.data.cmd) {
|
||||
case 'meta':
|
||||
this.meta = msg.data.meta
|
||||
this.duration = msg.data.meta.dur
|
||||
this.fireEvent('onMetadata', this.meta)
|
||||
break
|
||||
case 'pos':
|
||||
this.currentTime = msg.data.pos
|
||||
this.order = msg.data.order
|
||||
this.pattern = msg.data.pattern
|
||||
this.row = msg.data.row
|
||||
this.fireEvent('onProgress', msg.data)
|
||||
break
|
||||
case 'end':
|
||||
this.fireEvent('onEnded')
|
||||
break
|
||||
case 'err':
|
||||
this.fireEvent('onError', {type: msg.data.val})
|
||||
break
|
||||
case 'fullAudioData':
|
||||
this.fireEvent('onFullAudioData', msg.data)
|
||||
break
|
||||
default:
|
||||
console.log('Received unknown message',msg.data)
|
||||
}
|
||||
}
|
||||
|
||||
fireEvent(eventName, response) {
|
||||
const handlers = this.handlers
|
||||
if (handlers.length) {
|
||||
handlers.forEach(function (handler) {
|
||||
if (handler.eventName === eventName) {
|
||||
handler.handler(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
addHandler(eventName, handler) { this.handlers.push({eventName: eventName, handler: handler}) }
|
||||
onInitialized(handler) { this.addHandler('onInitialized', handler) }
|
||||
onEnded(handler) { this.addHandler('onEnded', handler) }
|
||||
onError(handler) { this.addHandler('onError', handler) }
|
||||
onMetadata(handler) { this.addHandler('onMetadata', handler) }
|
||||
onProgress(handler) { this.addHandler('onProgress', handler) }
|
||||
onFullAudioData(handler) { this.addHandler('onFullAudioData', handler) }
|
||||
|
||||
postMsg(cmd, val) {
|
||||
if (this.processNode)
|
||||
this.processNode.port.postMessage({cmd:cmd,val:val})
|
||||
}
|
||||
load(url) {
|
||||
fetch(url)
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(arrayBuffer => this.play(arrayBuffer))
|
||||
.catch(e=>{this.fireEvent('onError', {type: 'Load'})})
|
||||
}
|
||||
play(val) { this.postMsg('play', val) }
|
||||
stop() { this.postMsg('stop') }
|
||||
pause() { this.postMsg('pause') }
|
||||
unpause() { this.postMsg('unpause') }
|
||||
togglePause() { this.postMsg('togglePause') }
|
||||
setRepeatCount(val) { this.postMsg('repeatCount', val) }
|
||||
setPitch(val) { this.postMsg('setPitch', val) }
|
||||
setTempo(val) { this.postMsg('setTempo', val) }
|
||||
setPos(val) { this.postMsg('setPos', val) }
|
||||
setOrderRow(o,r) { this.postMsg('setOrderRow', {o:o,r:r}) }
|
||||
setVol(val) { this.gain.gain.value = val }
|
||||
selectSubsong(val) { this.postMsg('selectSubsong', val) }
|
||||
seek(val) { this.setPos(val) }
|
||||
getCurrentTime() { return this.currentTime }
|
||||
decodeAll(ab) { this.postMsg('decodeAll', ab) }
|
||||
}
|
||||
|
||||
window.ChiptuneJsPlayer = ChiptuneJsPlayer;
|
||||
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
AudioWorklet: DrSnuggles
|
||||
*/
|
||||
|
||||
import libopenmptPromise from './libopenmpt.worklet.js'
|
||||
|
||||
// consts
|
||||
const OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT = 2
|
||||
const OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH = 3
|
||||
|
||||
// vars
|
||||
let libopenmpt
|
||||
|
||||
// init
|
||||
libopenmptPromise()
|
||||
.then(res => {
|
||||
libopenmpt = res
|
||||
|
||||
if (!libopenmpt.stackSave) return
|
||||
// set libopenmpt version to display later
|
||||
let stack = 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(stack)
|
||||
})
|
||||
.catch(e => console.error(e))
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
function asciiToStack(str) {
|
||||
const stackStr = libopenmpt.stackAlloc(str.length + 1) // DrS: needed to export in emscripten
|
||||
writeAsciiToMemory(str, stackStr) // no longer in Emscripten, see below
|
||||
return stackStr
|
||||
}
|
||||
function writeAsciiToMemory(str,buffer,dontAddNull){for(let i=0;i<str.length;++i){libopenmpt.HEAP8[buffer++>>0]=str.charCodeAt(i)}if(!dontAddNull)libopenmpt.HEAP8[buffer>>0]=0}
|
||||
|
||||
|
||||
//
|
||||
// Processor
|
||||
//
|
||||
class MPT extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super()
|
||||
this.port.onmessage = this.handleMessage_.bind(this)
|
||||
this.paused = false
|
||||
this.config = {
|
||||
repeatCount: -1, // -1 = play endless, 0 = play once, do not repeat
|
||||
stereoSeparation: 100, // percents
|
||||
interpolationFilter: 0, // https://lib.openmpt.org/doc/group__openmpt__module__render__param.html
|
||||
}
|
||||
this.channels = 0
|
||||
}
|
||||
|
||||
process(inputList, outputList, parameters) {
|
||||
if (!this.modulePtr || !this.leftPtr || !this.rightPtr || this.paused) return true //silence
|
||||
|
||||
const left = outputList[0][0]
|
||||
const right = outputList[0][1]
|
||||
|
||||
const actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, sampleRate, left.length, this.leftPtr, this.rightPtr)
|
||||
if (actualFramesPerChunk == 0) {
|
||||
//this.paused = true
|
||||
// modulePtr will be 0 on openmpt: error: openmpt_module_read_float_stereo: ERROR: module * not valid or other openmpt error
|
||||
const error = !this.modulePtr
|
||||
if (error) {
|
||||
this.port.postMessage({cmd:'err',val:'Process'})
|
||||
} else {
|
||||
this.port.postMessage({cmd:'end'})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
left.set(libopenmpt.HEAPF32.subarray(this.leftPtr / 4, this.leftPtr / 4 + actualFramesPerChunk))
|
||||
right.set(libopenmpt.HEAPF32.subarray(this.rightPtr / 4, this.rightPtr / 4 + actualFramesPerChunk))
|
||||
|
||||
// post progress
|
||||
// openmpt_module_get_current_order
|
||||
|
||||
let msg = {
|
||||
cmd: 'pos',
|
||||
pos: libopenmpt._openmpt_module_get_position_seconds(this.modulePtr),
|
||||
// pos in song
|
||||
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),
|
||||
// channel volumes
|
||||
//chVol: [], // ch0Left, ch0Right, ch1Left, ...
|
||||
}
|
||||
/*
|
||||
for (let i = 0; i < this.channels; i++) {
|
||||
msg.chVol.push( {
|
||||
left: libopenmpt._openmpt_module_get_current_channel_vu_left(this.modulePtr, i),
|
||||
right: libopenmpt._openmpt_module_get_current_channel_vu_right(this.modulePtr, i),
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
this.port.postMessage( msg )
|
||||
|
||||
return true // def. needed for Chrome
|
||||
}
|
||||
|
||||
handleMessage_(msg) {
|
||||
//console.log('[Processor:Received]',msg.data)
|
||||
const v = msg.data.val
|
||||
switch (msg.data.cmd) {
|
||||
case 'config':
|
||||
this.config = v
|
||||
break
|
||||
case 'play':
|
||||
this.play(v, false)
|
||||
break
|
||||
case 'pause':
|
||||
this.paused = true
|
||||
break
|
||||
case 'unpause':
|
||||
this.paused = false
|
||||
break
|
||||
case 'togglePause':
|
||||
this.paused = !this.paused
|
||||
break
|
||||
case 'stop':
|
||||
this.stop()
|
||||
break
|
||||
case 'meta':
|
||||
this.meta()
|
||||
break
|
||||
case 'repeatCount':
|
||||
this.config.repeatCount = v
|
||||
if (!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(v.toString()))
|
||||
break
|
||||
case 'setTempo':
|
||||
if (!libopenmpt.stackSave || !this.modulePtr) return
|
||||
libopenmpt._openmpt_module_ctl_set(this.modulePtr, asciiToStack('play.tempo_factor'), asciiToStack(v.toString()))
|
||||
break
|
||||
case 'selectSubsong':
|
||||
if (!this.modulePtr) return
|
||||
libopenmpt._openmpt_module_select_subsong(this.modulePtr, v)
|
||||
//this.meta()
|
||||
break
|
||||
case 'setPos':
|
||||
if (!this.modulePtr) return
|
||||
libopenmpt._openmpt_module_set_position_seconds(this.modulePtr, v)
|
||||
break
|
||||
case 'setOrderRow':
|
||||
if (!this.modulePtr) return
|
||||
libopenmpt._openmpt_module_set_position_order_row(this.modulePtr, v.o, v.r)
|
||||
break
|
||||
/*
|
||||
case 'toggleMute'
|
||||
// openmpt_module_ext_get_interface(mod_ext, interface_id, interface, interface_size)
|
||||
// openmpt_module_ext_interface_interactive
|
||||
// set_channel_mute_status
|
||||
// https://lib.openmpt.org/doc/group__libopenmpt__ext__c.html#ga0275a35da407cd092232a20d3535c9e4
|
||||
if (!this.modulePtr) return
|
||||
//const extPtr = libopenmpt.openmpt_module_ext_get_interface(mod_ext, interface_id, interface, interface_size)
|
||||
break
|
||||
*/
|
||||
case 'decodeAll':
|
||||
this.decodeAll(v)
|
||||
break
|
||||
default:
|
||||
console.log('Received unknown message',msg.data)
|
||||
}
|
||||
} // handleMessage_
|
||||
|
||||
decodeAll(buffer) {
|
||||
this.play(buffer, true)
|
||||
|
||||
// now build the full audioData
|
||||
const left = [], right = []
|
||||
const maxFramesPerChunk = 128
|
||||
let actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, sampleRate, maxFramesPerChunk, this.leftPtr, this.rightPtr)
|
||||
while (actualFramesPerChunk != 0) {
|
||||
left.push( ...libopenmpt.HEAPF32.subarray(this.leftPtr / 4, this.leftPtr / 4 + actualFramesPerChunk) )
|
||||
right.push( ...libopenmpt.HEAPF32.subarray(this.rightPtr / 4, this.rightPtr / 4 + actualFramesPerChunk) )
|
||||
actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, sampleRate, maxFramesPerChunk, this.leftPtr, this.rightPtr)
|
||||
}
|
||||
|
||||
// post final data
|
||||
let msg = {
|
||||
cmd: 'fullAudioData',
|
||||
meta: this.getMeta(),
|
||||
data: [left,right]
|
||||
}
|
||||
this.port.postMessage( msg )
|
||||
|
||||
this.stop()
|
||||
}
|
||||
|
||||
play(buffer, paused = false) {
|
||||
this.stop()
|
||||
|
||||
const maxFramesPerChunk = 128 // thats what worklet is using
|
||||
const byteArray = new Int8Array(buffer)
|
||||
const ptrToFile = libopenmpt._malloc(byteArray.byteLength)
|
||||
libopenmpt.HEAPU8.set(byteArray, ptrToFile)
|
||||
this.modulePtr = libopenmpt._openmpt_module_create_from_memory(ptrToFile, byteArray.byteLength, 0, 0, 0)
|
||||
|
||||
if(this.modulePtr === 0) {
|
||||
// could not create module
|
||||
this.port.postMessage({cmd:'err',val:'ptr'})
|
||||
return
|
||||
}
|
||||
|
||||
if (libopenmpt.stackSave) {
|
||||
const stack = 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._openmpt_module_ctl_set('play.pitch_factor', e.target.value.toString());
|
||||
|
||||
libopenmpt.stackRestore(stack)
|
||||
}
|
||||
|
||||
this.paused = paused
|
||||
this.leftPtr = libopenmpt._malloc(4 * maxFramesPerChunk) // 4x = float
|
||||
this.rightPtr = libopenmpt._malloc(4 * maxFramesPerChunk)
|
||||
|
||||
// set config options on module
|
||||
libopenmpt._openmpt_module_set_repeat_count(this.modulePtr, this.config.repeatCount)
|
||||
libopenmpt._openmpt_module_set_render_param(this.modulePtr, OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT, this.config.stereoSeparation)
|
||||
libopenmpt._openmpt_module_set_render_param(this.modulePtr, OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH, this.config.interpolationFilter)
|
||||
|
||||
// post back tracks metadata
|
||||
if (!paused) this.meta()
|
||||
}
|
||||
stop() {
|
||||
if (!this.modulePtr) return
|
||||
if (this.modulePtr != 0) {
|
||||
libopenmpt._openmpt_module_destroy(this.modulePtr)
|
||||
this.modulePtr = 0
|
||||
}
|
||||
if (this.leftBufferPtr != 0) {
|
||||
libopenmpt._free(this.leftBufferPtr)
|
||||
this.leftBufferPtr = 0
|
||||
}
|
||||
if (this.rightBufferPtr != 0) {
|
||||
libopenmpt._free(this.rightBufferPtr)
|
||||
this.rightBufferPtr = 0
|
||||
}
|
||||
this.channels = 0
|
||||
}
|
||||
meta() {
|
||||
this.port.postMessage({cmd: 'meta', meta: this.getMeta()})
|
||||
}
|
||||
getSong() {
|
||||
if (!libopenmpt.UTF8ToString || !this.modulePtr) return false
|
||||
|
||||
// https://lib.openmpt.org/doc/
|
||||
let song = {
|
||||
channels: [],
|
||||
instruments: [],
|
||||
samples: [],
|
||||
orders: [],
|
||||
numSubsongs: libopenmpt._openmpt_module_get_num_subsongs(this.modulePtr),
|
||||
patterns: [],
|
||||
}
|
||||
// channels
|
||||
const chNum = libopenmpt._openmpt_module_get_num_channels(this.modulePtr)
|
||||
this.channel = chNum
|
||||
for (let i = 0; i < chNum; i++) {
|
||||
song.channels.push( libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_channel_name(this.modulePtr, i)) )
|
||||
}
|
||||
// instruments
|
||||
for (let i = 0, e = libopenmpt._openmpt_module_get_num_instruments(this.modulePtr); i < e; i++) {
|
||||
song.instruments.push( libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_instrument_name(this.modulePtr, i)) )
|
||||
}
|
||||
// samples
|
||||
for (let i = 0, e = libopenmpt._openmpt_module_get_num_samples(this.modulePtr); i < e; i++) {
|
||||
song.samples.push( libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_sample_name(this.modulePtr, i)) )
|
||||
}
|
||||
// orders
|
||||
for (let i = 0, e = libopenmpt._openmpt_module_get_num_orders(this.modulePtr); i < e; i++) {
|
||||
song.orders.push( {
|
||||
name: libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_order_name(this.modulePtr, i)),
|
||||
pat: libopenmpt._openmpt_module_get_order_pattern(this.modulePtr, i),
|
||||
})
|
||||
}
|
||||
// patterns
|
||||
for (let patIdx = 0, patNum = libopenmpt._openmpt_module_get_num_patterns(this.modulePtr); patIdx < patNum; patIdx++) {
|
||||
const pattern = {
|
||||
name: libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_pattern_name(this.modulePtr, patIdx)),
|
||||
rows: [],
|
||||
}
|
||||
// rows
|
||||
for(let rowIdx = 0, rowNum = libopenmpt._openmpt_module_get_pattern_num_rows(this.modulePtr, patIdx); rowIdx < rowNum; rowIdx++) {
|
||||
const row = []
|
||||
// channels
|
||||
for (let chIdx = 0; chIdx < chNum; chIdx++) {
|
||||
const channel = []
|
||||
for (let comIdx = 0; comIdx < 6; comIdx++) {
|
||||
/* commands
|
||||
OPENMPT_MODULE_COMMAND_NOTE = 0
|
||||
OPENMPT_MODULE_COMMAND_INSTRUMENT = 1
|
||||
OPENMPT_MODULE_COMMAND_VOLUMEEFFECT = 2
|
||||
OPENMPT_MODULE_COMMAND_EFFECT = 3
|
||||
OPENMPT_MODULE_COMMAND_VOLUME = 4
|
||||
OPENMPT_MODULE_COMMAND_PARAMETER = 5
|
||||
*/
|
||||
channel.push( libopenmpt._openmpt_module_get_pattern_row_channel_command(this.modulePtr, patIdx, rowIdx, chIdx, comIdx) )
|
||||
}
|
||||
row.push( channel )
|
||||
}
|
||||
pattern.rows.push( row )
|
||||
}
|
||||
song.patterns.push( pattern )
|
||||
}
|
||||
|
||||
|
||||
return song
|
||||
}
|
||||
getMeta() {
|
||||
if (!libopenmpt.UTF8ToString || !this.modulePtr) return false
|
||||
|
||||
const data = {}
|
||||
data.dur = libopenmpt._openmpt_module_get_duration_seconds(this.modulePtr)
|
||||
if (data.dur == 0) {
|
||||
// looks like an error occured reading the mod
|
||||
this.port.postMessage({cmd:'err',val:'dur'})
|
||||
}
|
||||
const keys = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata_keys(this.modulePtr)).split(';')
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const keyNameBuffer = libopenmpt._malloc(keys[i].length + 1)
|
||||
writeAsciiToMemory(keys[i], keyNameBuffer)
|
||||
data[keys[i]] = libopenmpt.UTF8ToString(libopenmpt._openmpt_module_get_metadata(this.modulePtr, keyNameBuffer))
|
||||
libopenmpt._free(keyNameBuffer)
|
||||
}
|
||||
data.song = this.getSong()
|
||||
data.totalOrders = data.song.orders.length // libopenmpt._openmpt_module_get_num_orders(this.modulePtr)
|
||||
data.totalPatterns = data.song.patterns.length// libopenmpt._openmpt_module_get_num_patterns(this.modulePtr)
|
||||
data.songs = this.getSongs()
|
||||
data.libopenmptVersion = libopenmpt.version
|
||||
data.libopenmptBuild = libopenmpt.build
|
||||
return data
|
||||
}
|
||||
getSongs() {
|
||||
if (!libopenmpt.UTF8ToString) return '' // todo: ?? why string here
|
||||
|
||||
const subsongs = libopenmpt._openmpt_module_get_num_subsongs(this.modulePtr)
|
||||
const names = []
|
||||
for (let i = 0; i < subsongs; i++) {
|
||||
const namePtr = libopenmpt._openmpt_module_get_subsong_name(this.modulePtr, i)
|
||||
const name = libopenmpt.UTF8ToString(namePtr)
|
||||
if(name != "") {
|
||||
names.push(name)
|
||||
} else {
|
||||
names.push("Subsong " + (i + 1))
|
||||
}
|
||||
libopenmpt._openmpt_free_string(namePtr)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerProcessor('libopenmpt-processor', MPT)
|
||||
|
After Width: | Height: | Size: 18 KiB |