From 1842319957532f6d6b6c3cc603c0a22205f1db9a Mon Sep 17 00:00:00 2001 From: Trek H Date: Tue, 23 Jul 2019 14:28:43 +0930 Subject: [PATCH] audio-player: added go wasm for access to go adpcm package --- cmd/audio-player/favicon.ico | Bin 0 -> 15406 bytes cmd/audio-player/index.html | 33 +- cmd/audio-player/main.go | 27 ++ cmd/audio-player/main.js | 37 ++- cmd/audio-player/pcm-player.min.js | 1 + cmd/audio-player/server.go | 32 ++ cmd/audio-player/wasm_exec.js | 465 +++++++++++++++++++++++++++++ go.mod | 1 + 8 files changed, 581 insertions(+), 15 deletions(-) create mode 100644 cmd/audio-player/favicon.ico create mode 100644 cmd/audio-player/main.go create mode 100644 cmd/audio-player/pcm-player.min.js create mode 100644 cmd/audio-player/server.go create mode 100644 cmd/audio-player/wasm_exec.js diff --git a/cmd/audio-player/favicon.ico b/cmd/audio-player/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..465b02c257d3a5388ce1a7744355834fbd37fa82 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/cmd/audio-player/index.html b/cmd/audio-player/index.html index 3e03e55e..12487c53 100644 --- a/cmd/audio-player/index.html +++ b/cmd/audio-player/index.html @@ -1,18 +1,23 @@ - - - Audio Player - - - - - - -
-
- -
+ + + + Audio Player + + + + + + + + +
+
+
- +
+ + \ No newline at end of file diff --git a/cmd/audio-player/main.go b/cmd/audio-player/main.go new file mode 100644 index 00000000..92d02c9a --- /dev/null +++ b/cmd/audio-player/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "syscall/js" + + "bitbucket.org/ausocean/av/codec/adpcm" +) + +func main() { + c := make(chan struct{}, 0) + + println("WASM Go Initialized") + // register functions + registerCallbacks() + + <-c +} + +func registerCallbacks() { + js.Global().Set("decode", js.FuncOf(decode)) +} + +func decode(this js.Value, args []js.Value) interface{} { + fmt.Println("calculating") + return adpcm.EncBytes(args[0].Int()) +} diff --git a/cmd/audio-player/main.js b/cmd/audio-player/main.js index 45299078..c18173fd 100644 --- a/cmd/audio-player/main.js +++ b/cmd/audio-player/main.js @@ -1,8 +1,10 @@ window.onload = function () { + initWasm() document.getElementById('input').addEventListener('change', playFile) } function playFile() { + console.log(decode(48000)) const input = event.target.files[0] const reader = new FileReader() @@ -11,7 +13,7 @@ function playFile() { reader.readAsArrayBuffer(input) } -function playData(array){ +function playData(array) { var data = new Uint8Array(array) console.log("playing file") var player = new PCMPlayer({ @@ -21,4 +23,37 @@ function playData(array){ flushingTime: 2000 }); player.feed(data) +} + +function initWasm() { + if (!WebAssembly.instantiateStreaming) { + // polyfill + WebAssembly.instantiateStreaming = async (resp, importObject) => { + const source = await (await resp).arrayBuffer() + return await WebAssembly.instantiate(source, importObject) + } + } + + const go = new Go() + let mod, inst + // memoryBytes is an Uint8Array pointing to the webassembly linear memory. + let memoryBytes; + console.log("Initializing wasm...") + WebAssembly.instantiateStreaming( + fetch('lib.wasm'), go.importObject).then( + result => { + mod = result.module + inst = result.instance + memoryBytes = new Uint8Array(inst.exports.mem.buffer) + console.log("Initialization complete.") + run() + } + ) + + async function run() { + await go.run(inst) + inst = await WebAssembly.instantiate(mod, go.importObject) // reset instance + } + + } \ No newline at end of file diff --git a/cmd/audio-player/pcm-player.min.js b/cmd/audio-player/pcm-player.min.js new file mode 100644 index 00000000..42c33624 --- /dev/null +++ b/cmd/audio-player/pcm-player.min.js @@ -0,0 +1 @@ +function PCMPlayer(t){this.init(t)}PCMPlayer.prototype.init=function(t){this.option=Object.assign({},{encoding:"16bitInt",channels:1,sampleRate:8e3,flushingTime:1e3},t),this.samples=new Float32Array,this.flush=this.flush.bind(this),this.interval=setInterval(this.flush,this.option.flushingTime),this.maxValue=this.getMaxValue(),this.typedArray=this.getTypedArray(),this.createContext()},PCMPlayer.prototype.getMaxValue=function(){var t={"8bitInt":128,"16bitInt":32768,"32bitInt":2147483648,"32bitFloat":1};return t[this.option.encoding]?t[this.option.encoding]:t["16bitInt"]},PCMPlayer.prototype.getTypedArray=function(){var t={"8bitInt":Int8Array,"16bitInt":Int16Array,"32bitInt":Int32Array,"32bitFloat":Float32Array};return t[this.option.encoding]?t[this.option.encoding]:t["16bitInt"]},PCMPlayer.prototype.createContext=function(){this.audioCtx=new(window.AudioContext||window.webkitAudioContext),this.gainNode=this.audioCtx.createGain(),this.gainNode.gain.value=1,this.gainNode.connect(this.audioCtx.destination),this.startTime=this.audioCtx.currentTime},PCMPlayer.prototype.isTypedArray=function(t){return t.byteLength&&t.buffer&&t.buffer.constructor==ArrayBuffer},PCMPlayer.prototype.feed=function(t){if(this.isTypedArray(t)){t=this.getFormatedValue(t);var e=new Float32Array(this.samples.length+t.length);e.set(this.samples,0),e.set(t,this.samples.length),this.samples=e}},PCMPlayer.prototype.getFormatedValue=function(t){t=new this.typedArray(t.buffer);var e,i=new Float32Array(t.length);for(e=0;e { + if (typeof global !== "undefined") { + // global already exists + } else if (typeof window !== "undefined") { + window.global = window; + } else if (typeof self !== "undefined") { + self.global = self; + } else { + throw new Error("cannot export Go (neither global, window nor self is defined)"); + } + + // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API). + const isNodeJS = global.process && global.process.title === "node"; + if (isNodeJS) { + global.require = require; + global.fs = require("fs"); + + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + + const util = require("util"); + global.TextEncoder = util.TextEncoder; + global.TextDecoder = util.TextDecoder; + } else { + let outputBuf = ""; + global.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + writeSync(fd, buf) { + outputBuf += decoder.decode(buf); + const nl = outputBuf.lastIndexOf("\n"); + if (nl != -1) { + console.log(outputBuf.substr(0, nl)); + outputBuf = outputBuf.substr(nl + 1); + } + return buf.length; + }, + write(fd, buf, offset, length, position, callback) { + if (offset !== 0 || length !== buf.length || position !== null) { + throw new Error("not implemented"); + } + const n = this.writeSync(fd, buf); + callback(null, n); + }, + open(path, flags, mode, callback) { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + callback(err); + }, + read(fd, buffer, offset, length, position, callback) { + const err = new Error("not implemented"); + err.code = "ENOSYS"; + callback(err); + }, + fsync(fd, callback) { + callback(null); + }, + }; + } + + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); + + global.Go = class { + constructor() { + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); + this._nextCallbackTimeoutID = 1; + + const mem = () => { + // The buffer may change when requesting more memory. + return new DataView(this._inst.exports.mem.buffer); + } + + const setInt64 = (addr, v) => { + mem().setUint32(addr + 0, v, true); + mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); + } + + const getInt64 = (addr) => { + const low = mem().getUint32(addr + 0, true); + const high = mem().getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = mem().getFloat64(addr, true); + if (f === 0) { + return undefined; + } + if (!isNaN(f)) { + return f; + } + + const id = mem().getUint32(addr, true); + return this._values[id]; + } + + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; + + if (typeof v === "number") { + if (isNaN(v)) { + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 0, true); + return; + } + if (v === 0) { + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 1, true); + return; + } + mem().setFloat64(addr, v, true); + return; + } + + switch (v) { + case undefined: + mem().setFloat64(addr, 0, true); + return; + case null: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 2, true); + return; + case true: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 3, true); + return; + case false: + mem().setUint32(addr + 4, nanHead, true); + mem().setUint32(addr, 4, true); + return; + } + + let ref = this._refs.get(v); + if (ref === undefined) { + ref = this._values.length; + this._values.push(v); + this._refs.set(v, ref); + } + let typeFlag = 0; + switch (typeof v) { + case "string": + typeFlag = 1; + break; + case "symbol": + typeFlag = 2; + break; + case "function": + typeFlag = 3; + break; + } + mem().setUint32(addr + 4, nanHead | typeFlag, true); + mem().setUint32(addr, ref, true); + } + + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); + } + + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + const a = new Array(len); + for (let i = 0; i < len; i++) { + a[i] = loadValue(array + i * 8); + } + return a; + } + + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const timeOrigin = Date.now() - performance.now(); + this.importObject = { + go: { + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + const code = mem().getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._refs; + this.exit(code); + }, + + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = mem().getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func nanotime() int64 + "runtime.nanotime": (sp) => { + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { this._resume(); }, + getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early + )); + mem().setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + const id = mem().getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + crypto.getRandomValues(loadSlice(sp + 8)); + }, + + // func stringVal(value string) ref + "syscall/js.stringVal": (sp) => { + storeValue(sp + 24, loadString(sp + 8)); + }, + + // func valueGet(v ref, p string) ref + "syscall/js.valueGet": (sp) => { + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp(); // see comment above + storeValue(sp + 32, result); + }, + + // func valueSet(v ref, p string, x ref) + "syscall/js.valueSet": (sp) => { + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); + }, + + // func valueIndex(v ref, i int) ref + "syscall/js.valueIndex": (sp) => { + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); + }, + + // valueSetIndex(v ref, i int, x ref) + "syscall/js.valueSetIndex": (sp) => { + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); + }, + + // func valueCall(v ref, m string, args []ref) (ref, bool) + "syscall/js.valueCall": (sp) => { + try { + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp(); // see comment above + storeValue(sp + 56, result); + mem().setUint8(sp + 64, 1); + } catch (err) { + storeValue(sp + 56, err); + mem().setUint8(sp + 64, 0); + } + }, + + // func valueInvoke(v ref, args []ref) (ref, bool) + "syscall/js.valueInvoke": (sp) => { + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp(); // see comment above + storeValue(sp + 40, result); + mem().setUint8(sp + 48, 1); + } catch (err) { + storeValue(sp + 40, err); + mem().setUint8(sp + 48, 0); + } + }, + + // func valueNew(v ref, args []ref) (ref, bool) + "syscall/js.valueNew": (sp) => { + try { + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp(); // see comment above + storeValue(sp + 40, result); + mem().setUint8(sp + 48, 1); + } catch (err) { + storeValue(sp + 40, err); + mem().setUint8(sp + 48, 0); + } + }, + + // func valueLength(v ref) int + "syscall/js.valueLength": (sp) => { + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); + }, + + // valuePrepareString(v ref) (ref, int) + "syscall/js.valuePrepareString": (sp) => { + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); + }, + + // valueLoadString(v ref, b []byte) + "syscall/js.valueLoadString": (sp) => { + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); + }, + + // func valueInstanceOf(v ref, t ref) bool + "syscall/js.valueInstanceOf": (sp) => { + mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); + }, + + "debug": (value) => { + console.log(value); + }, + } + }; + } + + async run(instance) { + this._inst = instance; + this._values = [ // TODO: garbage collection + NaN, + 0, + null, + true, + false, + global, + this._inst.exports.mem, + this, + ]; + this._refs = new Map(); + this.exited = false; + + const mem = new DataView(this._inst.exports.mem.buffer) + + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; + + const strPtr = (str) => { + let ptr = offset; + new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); + offset += str.length + (8 - (str.length % 8)); + return ptr; + }; + + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + + const keys = Object.keys(this.env).sort(); + argvPtrs.push(keys.length); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + + const argv = offset; + argvPtrs.forEach((ptr) => { + mem.setUint32(offset, ptr, true); + mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; + } + + _resume() { + if (this.exited) { + throw new Error("Go program has already exited"); + } + this._inst.exports.resume(); + if (this.exited) { + this._resolveExitPromise(); + } + } + + _makeFuncWrapper(id) { + const go = this; + return function () { + const event = { id: id, this: this, args: arguments }; + go._pendingEvent = event; + go._resume(); + return event.result; + }; + } + } + + if (isNodeJS) { + if (process.argv.length < 3) { + process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n"); + process.exit(1); + } + + const go = new Go(); + go.argv = process.argv.slice(2); + go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); + go.exit = process.exit; + WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { + process.on("exit", (code) => { // Node.js exits if no event handler is pending + if (code === 0 && !go.exited) { + // deadlock, make Go print error and stack traces + go._pendingEvent = { id: 0 }; + go._resume(); + } + }); + return go.run(result.instance); + }).catch((err) => { + throw err; + }); + } +})(); diff --git a/go.mod b/go.mod index c3d766c5..00293cdb 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480 github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884 github.com/mewkiz/flac v1.0.5 + github.com/pkg/errors v0.8.1 github.com/sergi/go-diff v1.0.0 // indirect github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect