ixfx
Version:
Bundle of ixfx libraries
1,784 lines (1,768 loc) • 94.4 kB
JavaScript
import { n as __exportAll } from "./chunk-CaR5F9JI.js";
import { E as omitChars, H as continuously, T as indexOfCharCode, h as intervalToMs, w as chunkByLength } from "./src-BUqDa_u7.js";
import { C as numberTest, M as resultThrow, b as isPowerOfTwo, v as integerTest } from "./src-C_hvyftg.js";
import { n as getErrorMessage } from "./src-B7f_ks6F.js";
import { D as mapWithEmptyFallback } from "./src-CxEyGbiK.js";
import { v as QueueMutable } from "./src-DOorb7Rs.js";
import { n as SimpleEventEmitter } from "./src-CRR1VQls.js";
import { f as scale, mt as maxFast, pt as max, rt as clamp, w as numberArrayCompute } from "./src-Cebc3sfq.js";
import { W as resolveEl } from "./src-BoQVyMUt.js";
import { l as number } from "./src-DIuRzACc.js";
import { E as retryTask, R as eventRace, T as retryFunction, U as debounce, a as to, i as init, o as waitFor, r as StateMachineWithEvents } from "./src-C3g81yvt.js";
import { h as shortGuid, u as string } from "./src-BH_hkHiA.js";
import { r as manualCapture } from "./src-CHw3eZpa.js";
//#region ../packages/io/src/codec.ts
/**
* Handles utf-8 text encoding/decoding
*/
var Codec = class {
enc = new TextEncoder();
dec = new TextDecoder(`utf-8`);
/**
* Convert string to Uint8Array buffer
* @param text
* @returns
*/
toBuffer(text) {
return this.enc.encode(text);
}
/**
* Returns a string from a provided buffer
* @param buffer
* @returns
*/
fromBuffer(buffer) {
return this.dec.decode(buffer);
}
};
//#endregion
//#region ../packages/io/src/string-receive-buffer.ts
/**
* Receives text
*/
var StringReceiveBuffer = class {
buffer = ``;
stream;
constructor(onData, separator = `\n`) {
this.onData = onData;
this.separator = separator;
}
async close() {
const s = this.stream;
if (!s) return;
await s.abort();
await s.close();
}
clear() {
this.buffer = ``;
}
writable() {
if (this.stream === void 0) this.stream = this.createWritable();
return this.stream;
}
createWritable() {
const b = this;
return new WritableStream({
write(chunk) {
b.add(chunk);
},
close() {
b.clear();
}
});
}
addImpl(string_) {
const pos = string_.indexOf(this.separator);
if (pos < 0) {
this.buffer += string_;
return ``;
}
const part = string_.substring(0, pos);
try {
this.onData(this.buffer + part);
string_ = string_.substring(part.length + this.separator.length);
} catch (ex) {
console.warn(ex);
}
this.buffer = ``;
return string_;
}
add(string_) {
while (string_.length > 0) string_ = this.addImpl(string_);
}
};
//#endregion
//#region ../packages/io/src/string-write-buffer.ts
/**
* Buffers a queue of strings.
*
* When text is queued via {@link add}, it is chopped up
* into chunks and sent in serial to the `dataHandler` function.
* Data is processed at a set rate, by default 10ms.
*
* ```js
* const dataHandler = (data:string) => {
* // Do something with queued data.
* // eg. send to serial port
* }
*
* // Create a buffer with a chunk size of 100 characters
* const b = new StringWriteBuffer(dataHandler, { chunkSize: 100 });
* b.add('some text'); // Write to buffer
* // dataHandler will be called until queued data is empty
* ```
*
* It's also possible to get the buffer as a WritableStream<string>:
* ```js
* const dataHandler = (data:string) => { ... }
* const b = new StringWriteBuffer(dataHandler, 100);
* const s = b.writable();
* ```
*
* Other functions:
* ```js
* b.close(); // Close buffer
* b.clear(); // Clear queued data, but don't close anything
* ```
*/
var StringWriteBuffer = class {
paused = false;
queue = new QueueMutable();
writer;
stream;
closed = false;
chunkSize;
/**
* Constructor
* @param dataHandler Calback to 'send' data onwards
* @param opts Options
*/
constructor(dataHandler, opts = {}) {
this.dataHandler = dataHandler;
this.chunkSize = opts.chunkSize ?? -1;
this.writer = continuously(async () => {
await this.onWrite();
}, opts.interval ?? 10);
}
/**
* Close writer (async)
*/
async close() {
if (this.closed) return;
const w = this.stream?.getWriter();
w?.releaseLock();
await w?.close();
this.closed = true;
}
/**
* Clear queued data.
*
* Throws an error if {@link close} has been called.
*/
clear() {
if (this.closed) throw new Error(`Buffer closed`);
this.queue = new QueueMutable();
}
/**
* Gets the buffer as a writable stream.
*
* Do not close stream directly, use .close on this class instead.
*
* Throws an error if .close() has been called.
* @returns Underlying stream
*/
writable() {
if (this.closed) throw new Error(`Buffer closed`);
if (this.stream === void 0) this.stream = this.createWritable();
return this.stream;
}
createWritable() {
const b = this;
return new WritableStream({
write(chunk) {
b.add(chunk);
},
close() {
b.clear();
}
});
}
/**
* Run in a `continunously` loop to process queued data
* @returns _False_ if queue is empty and loop should stop. _True_ if it shoud continue.
*/
async onWrite() {
if (this.queue.isEmpty) return false;
if (this.paused) {
console.warn(`WriteBuffer.onWrite: paused...`);
return true;
}
const s = this.queue.dequeue();
if (s === void 0) return false;
await this.dataHandler(s);
return true;
}
/**
* Returns _true_ if {@link close} has been called.
*/
get isClosed() {
return this.closed;
}
/**
* Adds some queued data to send.
* Longer strings are automatically chunked up according to the buffer's settings.
*
* Throws an error if {@link close} has been called.
* @param stringToQueue
*/
add(stringToQueue) {
if (this.closed) throw new Error(`Buffer closed`);
if (this.chunkSize > 0) this.queue.enqueue(...chunkByLength(stringToQueue, this.chunkSize));
else this.queue.enqueue(stringToQueue);
this.writer.start();
}
};
//#endregion
//#region ../packages/io/src/generic-state-transitions.ts
const genericStateTransitionsInstance = {
ready: `connecting`,
connecting: [`connected`, `closed`],
connected: [`closed`],
closed: `connecting`
};
//#endregion
//#region ../packages/io/src/ble-device.ts
var BleDevice = class extends SimpleEventEmitter {
states;
codec;
rx;
tx;
gatt;
verboseLogging = false;
rxBuffer;
txBuffer;
constructor(device, config) {
super();
this.device = device;
this.config = config;
this.verboseLogging = config.debug;
this.txBuffer = new StringWriteBuffer(async (data) => {
await this.writeInternal(data);
}, config);
this.rxBuffer = new StringReceiveBuffer((line) => {
this.fireEvent(`data`, { data: line });
});
this.codec = new Codec();
this.states = new StateMachineWithEvents(genericStateTransitionsInstance, { initial: `ready` });
this.states.addEventListener(`change`, (event) => {
this.fireEvent(`change`, event);
this.verbose(`${event.priorState} -> ${event.newState}`);
if (event.priorState === `connected`) {
this.rxBuffer.clear();
this.txBuffer.clear();
}
});
device.addEventListener(`gattserverdisconnected`, () => {
if (this.isClosed) return;
this.verbose(`GATT server disconnected`);
this.states.state = `closed`;
});
this.verbose(`ctor ${device.name} ${device.id}`);
}
get isConnected() {
return this.states.state === `connected`;
}
get isClosed() {
return this.states.state === `closed`;
}
write(txt) {
if (this.states.state !== `connected`) throw new Error(`Cannot write while state is ${this.states.state}`);
this.txBuffer.add(txt);
}
async writeInternal(txt) {
this.verbose(`writeInternal ${txt}`);
const tx = this.tx;
if (tx === void 0) throw new Error(`Unexpectedly without tx characteristic`);
try {
await tx.writeValue(this.codec.toBuffer(txt));
} catch (error) {
this.warn(error);
}
}
disconnect() {
if (this.states.state !== `connected`) return;
this.gatt?.disconnect();
}
async connect() {
const attempts = this.config.connectAttempts ?? 3;
this.states.state = `connecting`;
this.verbose(`connect`);
const gatt = this.device.gatt;
if (gatt === void 0) throw new Error(`Gatt not available on device`);
await retryFunction(async () => {
this.verbose(`connect.retry`);
const server = await gatt.connect();
this.verbose(`Getting primary service`);
const service = await server.getPrimaryService(this.config.service);
this.verbose(`Getting characteristics`);
const rx = await service.getCharacteristic(this.config.rxGattCharacteristic);
const tx = await service.getCharacteristic(this.config.txGattCharacteristic);
rx.addEventListener(`characteristicvaluechanged`, (event) => {
this.onRx(event);
});
this.rx = rx;
this.tx = tx;
this.gatt = gatt;
this.states.state = `connected`;
await rx.startNotifications();
return true;
}, {
limitAttempts: attempts,
startAt: 200
});
}
onRx(event) {
if (this.rx === void 0) return;
const view = event.target.value;
if (view === void 0) return;
let text = this.codec.fromBuffer(view.buffer);
const plzStop = indexOfCharCode(text, 19);
const plzStart = indexOfCharCode(text, 17);
if (plzStart && plzStop < plzStart) {
this.verbose(`Tx plz start`);
text = omitChars(text, plzStart, 1);
this.txBuffer.paused = false;
}
if (plzStop && plzStop > plzStart) {
this.verbose(`Tx plz stop`);
text = omitChars(text, plzStop, 1);
this.txBuffer.paused = true;
}
this.rxBuffer.add(text);
}
verbose(m) {
if (this.verboseLogging) console.info(this.config.name, m);
}
log(m) {
console.log(this.config.name, m);
}
warn(m) {
console.warn(this.config.name, m);
}
};
//#endregion
//#region ../packages/io/src/nordic-ble-device.ts
var nordic_ble_device_exports = /* @__PURE__ */ __exportAll({
NordicBleDevice: () => NordicBleDevice,
defaultOpts: () => defaultOpts
});
const defaultOpts = {
chunkSize: 20,
service: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`,
txGattCharacteristic: `6e400002-b5a3-f393-e0a9-e50e24dcca9e`,
rxGattCharacteristic: `6e400003-b5a3-f393-e0a9-e50e24dcca9e`,
name: `NordicDevice`,
connectAttempts: 5,
debug: false
};
var NordicBleDevice = class extends BleDevice {
constructor(device, opts = {}) {
super(device, {
...defaultOpts,
...opts
});
}
};
//#endregion
//#region ../packages/io/src/audio/visualiser.ts
/**
* Visualiser component
*
* Usage: import visualiser.js. Instantiate on document load, and pass in the
* parent element into the constructor.
*
* eg: const v = new Visualiser(document.getElementById('renderer'));
*
* Data must be passed to the component via renderFreq or renderWave.
*
* Draws on https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Visualizations_with_Web_Audio_API
*/
var AudioVisualiser = class {
freqMaxRange = 200;
audio;
parent;
lastPointer = {
x: 0,
y: 0
};
pointerDown = false;
pointerClicking = false;
pointerClickDelayMs = 100;
pointerDelaying = false;
waveTracker;
freqTracker;
el;
constructor(parentElement, audio) {
this.audio = audio;
this.parent = parentElement;
this.waveTracker = number();
this.freqTracker = number();
parentElement.innerHTML = `
<section>
<button id="rendererComponentToggle">🔼</button>
<div>
<h1>Visualiser</h1>
<div style="display:flex; flex-wrap: wrap">
<div class="visPanel">
<h2>Frequency distribution</h2>
<br />
<canvas id="rendererComponentFreqData" height="200" width="400"></canvas>
</div>
<div class="visPanel">
<h2>Waveform</h2>
<button id="rendererComponentWaveReset">Reset</button>
<div>
Press and hold on wave to measure
</div>
<br />
<canvas id="rendererComponentWaveData" height="200" width="400"></canvas>
</div>
</div>
</div>
</section>
`;
this.el = parentElement.children[0];
document.getElementById(`rendererComponentToggle`)?.addEventListener(`click`, () => {
this.setExpanded(!this.isExpanded());
});
this.el.addEventListener(`pointermove`, (e) => {
this.onPointer(e);
});
this.el.addEventListener(`pointerup`, () => {
this.pointerDelaying = false;
this.pointerDown = false;
});
this.el.addEventListener(`pointerdown`, () => {
this.pointerDelaying = true;
setTimeout(() => {
if (this.pointerDelaying) {
this.pointerDelaying = false;
this.pointerDown = true;
}
}, this.pointerClickDelayMs);
});
this.el.addEventListener(`pointerleave`, () => {
this.pointerDelaying = false;
this.pointerDown = false;
});
document.getElementById(`rendererComponentWaveReset`)?.addEventListener(`click`, () => {
this.clear();
});
}
renderFreq(freq) {
if (!this.isExpanded()) return;
if (!freq) return;
const canvas = document.getElementById(`rendererComponentFreqData`);
if (canvas === null) throw new Error(`Cannot find canvas element`);
const g = canvas.getContext(`2d`);
if (g === null) throw new Error(`Cannot create drawing context`);
const bins = freq.length;
const canvasWidth = canvas.clientWidth;
const canvasHeight = canvas.clientHeight;
g.clearRect(0, 0, canvasWidth, canvasHeight);
const pointer = this.getPointerRelativeTo(canvas);
const width = canvasWidth / bins;
const minMax = numberArrayCompute(freq);
for (let index = 0; index < bins; index++) {
if (!Number.isFinite(freq[index])) continue;
const valueRelative = (freq[index] - minMax.min) / this.freqMaxRange;
const height = Math.abs(canvasHeight * valueRelative);
const offset = canvasHeight - height;
const hue = index / bins * 360;
const left = index * width;
g.fillStyle = `hsl(${hue}, 100%, 50%)`;
if (pointer.y > 0 && pointer.y <= canvasHeight && pointer.x >= left && pointer.x <= left + width) {
if (this.freqTracker.id !== index.toString()) this.freqTracker = number({ id: index.toString() });
this.freqTracker.seen(freq[index]);
const freqMma = this.freqTracker.getMinMaxAvg();
g.fillStyle = `black`;
if (this.audio) g.fillText(`Frequency (${index}) at pointer: ${this.audio.getFrequencyAtIndex(index).toLocaleString(`en`)} - ${this.audio.getFrequencyAtIndex(index + 1).toLocaleString(`en`)}`, 2, 10);
g.fillText(`Raw value: ${freq[index].toFixed(2)}`, 2, 20);
g.fillText(`Min: ${freqMma.min.toFixed(2)}`, 2, 40);
g.fillText(`Max: ${freqMma.max.toFixed(2)}`, 60, 40);
g.fillText(`Avg: ${freqMma.avg.toFixed(2)}`, 120, 40);
}
g.fillRect(left, offset, width, height);
}
}
isExpanded() {
const contentsElement = this.el.querySelector(`div`);
if (contentsElement === null) throw new Error(`contents div not found`);
return contentsElement.style.display === ``;
}
setExpanded(value) {
const contentsElement = this.el.querySelector(`div`);
const button = this.el.querySelector(`button`);
if (button === null) throw new Error(`Button element not found`);
if (contentsElement === null) throw new Error(`Contents element not found`);
if (value) {
contentsElement.style.display = ``;
button.innerText = `🔼`;
} else {
contentsElement.style.display = `none`;
button.innerText = `🔽`;
}
}
clear() {
this.clearCanvas(document.getElementById(`rendererComponentFreqData`));
this.clearCanvas(document.getElementById(`rendererComponentWaveData`));
}
clearCanvas(canvas) {
if (canvas === null) throw new Error(`Canvas is null`);
const g = canvas.getContext(`2d`);
if (g === null) throw new Error(`Cannot create drawing context`);
g.fillStyle = `white`;
g.fillRect(0, 0, canvas.clientWidth, canvas.clientHeight);
}
renderWave(wave, bipolar = true) {
if (!this.isExpanded()) return;
if (!wave) return;
const canvas = document.getElementById(`rendererComponentWaveData`);
if (canvas === null) throw new Error(`Cannot find wave canvas`);
const g = canvas.getContext(`2d`);
if (g === null) throw new Error(`Cannot create drawing context for wave`);
const canvasWidth = canvas.clientWidth;
const canvasHeight = canvas.clientHeight;
const pointer = this.getPointerRelativeTo(canvas);
const infoAreaHeight = 20;
const infoAreaWidth = 60;
const bins = wave.length;
g.fillStyle = `white`;
g.fillRect(0, 0, infoAreaWidth, infoAreaHeight);
const width = canvasWidth / bins;
g.fillStyle = `rgba(255, 255, 255, 0.03)`;
g.fillRect(0, 20, canvasWidth, canvasHeight);
g.fillStyle = `red`;
if (bipolar) g.fillRect(0, canvasHeight / 2, canvasWidth, 1);
else g.fillRect(0, canvasHeight - 1, canvasWidth, 1);
g.lineWidth = 1;
g.strokeStyle = `black`;
g.beginPath();
let x = 0;
for (let index = 0; index < bins; index++) {
const height = wave[index] * canvasHeight;
const y = bipolar ? canvasHeight / 2 - height : canvasHeight - height;
if (index === 0) g.moveTo(x, y);
else g.lineTo(x, y);
x += width;
if (this.pointerDown) this.waveTracker.seen(wave[index]);
}
g.lineTo(canvasWidth, bipolar ? canvasHeight / 2 : canvasHeight);
g.stroke();
if (this.pointerDown) {
const waveMma = this.waveTracker.getMinMaxAvg();
g.fillStyle = `rgba(255,255,0,1)`;
g.fillRect(infoAreaWidth, 0, 150, 20);
g.fillStyle = `black`;
g.fillText(`Min: ` + waveMma.min.toFixed(2), 60, 10);
g.fillText(`Max: ` + waveMma.max.toFixed(2), 110, 10);
g.fillText(`Avg: ` + waveMma.avg.toFixed(2), 160, 10);
} else this.waveTracker.reset();
if (pointer.y > 0 && pointer.y <= canvasHeight && pointer.x >= 0 && pointer.x <= canvasWidth) {
g.fillStyle = `black`;
g.fillText(`Level: ` + (1 - pointer.y / canvasHeight).toFixed(2), 2, 10);
}
}
getPointerRelativeTo(elem) {
const rect = elem.getBoundingClientRect();
return {
x: this.lastPointer.x - rect.left - window.scrollX,
y: this.lastPointer.y - rect.top - window.scrollY
};
}
onPointer(event) {
this.lastPointer = {
x: event.pageX,
y: event.pageY
};
event.preventDefault();
}
};
//#endregion
//#region ../packages/io/src/audio/analyser.ts
/**
* Basic audio analyser. Returns back waveform and FFT analysis. Use {@link analyserPeakLevel} if you want sound level, or {@link analyserFrequency} if you just want FFT results.
*
* ```js
* const onData = (freq, wave, analyser) => {
* // Demo: Get FFT results just for 100Hz-1KHz.
* const freqSlice = analyser.sliceByFrequency(100,1000,freq);
*
* // Demo: Get FFT value for a particular frequency (1KHz)
* const amt = freq[analyser.getIndexForFrequency(1000)];
* }
* analyserBasic(onData, {fftSize: 512});
* ```
*
* An `Analyser` instance is returned and can be controlled:
* ```js
* const analyser = analyserBasic(onData);
* analyser.paused = true;
* ```
*
* Note: Browers won't allow microphone access unless the call has come from a user-interaction, eg pointerup event handler.
*
* @param onData Handler for data
* @param opts Options
* @returns Analyser instance
*/
const analyserBasic = (onData, opts = {}) => new AudioAnalyser((node, analyser) => {
const freq = new Float32Array(node.frequencyBinCount);
const wave = new Float32Array(node.fftSize);
node.getFloatFrequencyData(freq);
node.getFloatTimeDomainData(wave);
onData(freq, wave, analyser);
}, opts);
/**
* Basic audio analyser. Returns FFT analysis. Use {@link analyserPeakLevel} if you want the sound level, or {@link analyserBasic} if you also want the waveform.
*
* ```js
* const onData = (freq, analyser) => {
* // Demo: Print out each sound frequency (Hz) and amount of energy in that band
* for (let i=0;i<freq.length;i++) {
* const f = analyser.getFrequencyAtIndex(0);
* console.log(`${i}. frequency: ${f} amount: ${freq[i]}`);
* }
* }
* analyserFrequency(onData, {fftSize:512});
* ```
*
* Note: Browers won't allow microphone access unless the call has come from a user-interaction, eg pointerup event handler.
*
* @param onData
* @param opts
* @returns
*/
const analyserFrequency = (onData, opts = {}) => new AudioAnalyser((node, analyser) => {
const freq = new Float32Array(node.frequencyBinCount);
node.getFloatFrequencyData(freq);
onData(freq, analyser);
}, opts);
/**
* Basic audio analyser which reports the peak sound level.
*
* ```js
* analyserPeakLevel(level => {
* console.log(level);
* });
* ```
*
* Note: Browers won't allow microphone access unless the call has come from a user-interaction, eg pointerup event handler.
* @param onData
* @param opts
* @returns
*/
const analyserPeakLevel = (onData, opts = {}) => new AudioAnalyser((node, analyser) => {
const wave = new Float32Array(node.fftSize);
node.getFloatTimeDomainData(wave);
onData(maxFast(wave), analyser);
}, opts);
/**
* Helper for doing audio analysis. It takes case of connecting the audio stream, running in a loop and pause capability.
*
* Provide a function which works with an [AnalyserNode](https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode), and does something with the result.
* ```js
* const myAnalysis = (node, analyser) => {
* const freq = new Float32Array(node.frequencyBinCount);
* node.getFloatFrequencyData(freq);
* // Do something with frequency data...
* }
* const a = new Analyser(myAnalysis);
* ```
*
* Helper functions provide ready-to-use Analysers:
* * {@link analyserPeakLevel} peak decibel reading
* * {@link analyserFrequency} FFT results
* * {@link analyserBasic} FFT results and waveform
*
* Note: Browers won't allow microphone access unless the call has come from a user-interaction, eg pointerup event handler.
*
*/
var AudioAnalyser = class {
showVis;
fftSize;
smoothingTimeConstant;
#isPaused = false;
debug;
#initInProgress = false;
visualiser;
audioCtx;
analyserNode;
analyse;
constructor(analyse, opts = {}) {
this.showVis = opts.showVis ?? false;
this.fftSize = opts.fftSize ?? 1024;
this.debug = opts.debug ?? false;
this.smoothingTimeConstant = opts.smoothingTimeConstant ?? .8;
resultThrow(integerTest(this.fftSize, `positive`, `opts.fftSize`), numberTest(this.smoothingTimeConstant, `percentage`, `opts.smoothingTimeConstant`));
if (!isPowerOfTwo(this.fftSize)) throw new Error(`fftSize must be a power of two from 32 to 32768 (${this.fftSize})`);
if (this.fftSize < 32) throw new Error(`fftSize must be at least 32`);
if (this.fftSize > 32768) throw new Error(`fftSize must be no greater than 32768`);
this.analyse = analyse;
this.paused = false;
this.init();
const visualiserEl = document.querySelector(`#audio-visualiser`);
if (visualiserEl) {
const visualiser = new AudioVisualiser(visualiserEl, this);
visualiser.setExpanded(this.showVis);
this.visualiser = visualiser;
}
}
init() {
if (this.#initInProgress) {
if (this.debug) console.debug(`Init already in progress`);
return;
}
this.#initInProgress = true;
navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => {
this.onMicSuccess(stream);
}).catch((error) => {
this.#initInProgress = false;
console.error(error);
});
}
get paused() {
return this.#isPaused;
}
set paused(v) {
if (v === this.#isPaused) return;
this.#isPaused = v;
if (v) {
if (this.debug) console.log(`Paused`);
} else {
if (this.debug) console.log(`Unpaused`);
window.requestAnimationFrame(this.analyseLoop.bind(this));
}
}
setup(context, stream) {
const analyser = context.createAnalyser();
analyser.fftSize = this.fftSize;
analyser.smoothingTimeConstant = this.smoothingTimeConstant;
context.createMediaStreamSource(stream).connect(analyser);
return analyser;
}
onMicSuccess(stream) {
try {
const context = new AudioContext();
context.addEventListener(`statechange`, () => {
if (this.debug) console.log(`Audio context state: ${context.state}`);
});
this.audioCtx = context;
this.analyserNode = this.setup(context, stream);
window.requestAnimationFrame(this.analyseLoop.bind(this));
} catch (error) {
this.#initInProgress = false;
console.error(error);
}
}
analyseLoop() {
if (this.paused) {
if (this.debug) console.log(`Paused`);
return;
}
const a = this.analyserNode;
if (a === void 0) {
console.warn(`Analyser undefined`);
return;
}
try {
this.analyse(a, this);
} catch (error) {
console.error(error);
}
window.requestAnimationFrame(this.analyseLoop.bind(this));
}
/**
* Returns the maximum FFT value within the given frequency range
*/
getFrequencyRangeMax(lowFreq, highFreq, freqData) {
return max(this.sliceByFrequency(lowFreq, highFreq, freqData));
}
/**
* Returns a sub-sampling of frequency analysis data that falls between
* `lowFreq` and `highFreq`.
* @param lowFreq Low frequency
* @param highFreq High frequency
* @param freqData Full-spectrum frequency data
* @returns Sub-sampling of analysis
*/
sliceByFrequency(lowFreq, highFreq, freqData) {
const lowIndex = this.getIndexForFrequency(lowFreq);
const highIndex = this.getIndexForFrequency(highFreq);
return freqData.slice(lowIndex, highIndex);
}
/**
* Returns the starting frequency for a given binned frequency index.
* @param index Array index
* @returns Sound frequency
*/
getFrequencyAtIndex(index) {
const a = this.analyserNode;
const ctx = this.audioCtx;
if (a === void 0) throw new Error(`Analyser not available`);
if (ctx === void 0) throw new Error(`Audio context not available`);
resultThrow(integerTest(index, `positive`, `index`));
if (index > a.frequencyBinCount) throw new Error(`Index ${index} exceeds frequency bin count ${a.frequencyBinCount}`);
return index * ctx.sampleRate / (a.frequencyBinCount * 2);
}
/**
* Returns a binned array index for a given frequency
* @param freq Sound frequency
* @returns Array index into frequency bins
*/
getIndexForFrequency(freq) {
const a = this.analyserNode;
if (a === void 0) throw new Error(`Analyser not available`);
const nyquist = a.context.sampleRate / 2;
const index = Math.round(freq / nyquist * a.frequencyBinCount);
if (index < 0) return 0;
if (index >= a.frequencyBinCount) return a.frequencyBinCount - 1;
return index;
}
};
//#endregion
//#region ../packages/io/src/audio/from-audio-element.ts
/**
* Scans page for <AUDIO> elements and creates playable controllers for them.
* It uses the element's 'id' attribute as a way of fetching one later.
*
* ```js
* const ae = new AudioElements();
* ae.init(); // Initialise
*
* const a = ae.get('kick'); // Get the source that had id 'kick'
* ```
*/
var AudioElements = class {
#initialised = false;
#sources = /* @__PURE__ */ new Map();
filterType = `lowpass`;
constructor() {}
init() {
if (this.#initialised) return;
this.#initialised = true;
for (const element of document.querySelectorAll(`audio`)) this.#sources.set(element.id, createFromAudioElement(element, this.filterType));
}
/**
* Gets a BasicAudio instance by key
* @param key
* @returns BasicAudio instance, or undefined
*/
get(key) {
this.init();
return this.#sources.get(key);
}
};
/**
* Create a BasicAudioElement instance from an <AUDIO> tag in the HTML document.
*
* See {@link AudioElements} to automatically create sources from all <AUDIO> elements.
* @param audioElementOrQuery Element or query (eg '#some-id')
* @param filterType Filter type. Defaults to 'lowpass'
* @returns
*/
function createFromAudioElement(audioElementOrQuery, filterType = `lowpass`) {
const el = resolveEl(audioElementOrQuery);
const context = new AudioContext();
const source = context.createMediaElementSource(el);
const pan = context.createStereoPanner();
const gain = context.createGain();
const filter = context.createBiquadFilter();
filter.type = filterType;
source.connect(gain);
gain.connect(pan);
pan.connect(filter);
filter.connect(context.destination);
return {
pan,
gain,
filter,
id: el.id,
ctx: context,
el
};
}
//#endregion
//#region ../packages/io/src/audio/from-oscillator.ts
/**
* Initialise audio with an oscillator source
* @param oscillatorOptions
* @returns BasicAudio instance
*/
function createOscillator(oscillatorOptions = {}) {
const context = new AudioContext();
const oscType = oscillatorOptions.type ?? `sawtooth`;
const oscFreq = oscillatorOptions.frequency ?? 440;
const id = oscillatorOptions.id ?? shortGuid();
const source = context.createOscillator();
source.type = oscType;
source.frequency.setValueAtTime(oscFreq, context.currentTime);
const pan = context.createStereoPanner();
const gain = context.createGain();
const filter = context.createBiquadFilter();
source.connect(gain);
gain.connect(pan);
pan.connect(filter);
filter.connect(context.destination);
return {
pan,
gain,
filter,
ctx: context,
osc: source,
id
};
}
//#endregion
//#region ../packages/io/src/audio/index.ts
var audio_exports = /* @__PURE__ */ __exportAll({
AudioAnalyser: () => AudioAnalyser,
AudioElements: () => AudioElements,
AudioVisualiser: () => AudioVisualiser,
analyserBasic: () => analyserBasic,
analyserFrequency: () => analyserFrequency,
analyserPeakLevel: () => analyserPeakLevel,
createFromAudioElement: () => createFromAudioElement,
createOscillator: () => createOscillator
});
//#endregion
//#region ../packages/io/src/midi/midi-fns.ts
/**
* Sends a note on and note off
* @param port
* @param channel
* @param note
* @param velocity
* @param duration
* @param delay
*/
const sendNote = (port, channel, note, velocity, duration = 200, delay) => {
const noteOn = {
channel,
note,
velocity,
command: `noteon`
};
const noteOff = {
channel,
note,
velocity: 0,
command: `noteoff`
};
port.send(pack(noteOn), delay);
port.send(pack(noteOff), window.performance.now() + duration);
};
/**
* Parses MIDI data from an array into a MidiMessage
*
* ```js
* function onMidiMessage(event: MIDIMessageEvent) {
* const msg = unpack(event.data);
* // { command, channel, note, velocity }
* }
*
* // Where 'input' is a MIDIInput
* input.addEventListener(`midimessage`, onMidiMessage);
* ```
* @param data
* @returns
*/
const unpack = (data) => {
let command;
const first = data[0];
const second = data[1];
const third = data[2];
let channel = 0;
if (first >= 144 && first <= 159) {
channel = first - 143;
command = third === 0 ? `noteoff` : `noteon`;
} else if (first >= 128 && first <= 143) {
channel = first - 127;
command = `noteoff`;
} else if (first >= 160 && first <= 175) {
channel = first - 159;
command = `poly-at`;
} else if (first >= 176 && first <= 191) {
channel = first - 175;
command = `cc`;
} else if (first >= 192 && first <= 207) {
channel = first - 191;
command = `progchange`;
} else if (first >= 208 && first <= 223) {
channel = first - 207;
command = `at`;
} else if (first >= 224 && first <= 239) {
channel = first - 223;
command = `pitchbend`;
}
if (command === void 0) throw new Error(`Unknown command: '${command}'`);
else return {
command,
note: second,
velocity: third,
channel
};
};
/**
* Packs a MidiMessage into an array for sending to a MIDIOutput.
*
* ```js
* const msg: Midi.MidiMessage = {
* command: `cc`,
* channel: 1,
* velocity: 50,
* note: 40
* }
*
* // Where 'output' is a MIDIOutput
* output.send(pack(msg));
* ```
* @param message
* @returns
*/
const pack = (message) => {
const data = new Uint8Array(3);
data[1] = message.note;
data[2] = message.velocity;
switch (message.command) {
case `cc`:
data[0] = message.channel + 175;
break;
case `noteon`:
data[0] = message.channel + 143;
break;
case `noteoff`:
data[0] = message.channel + 127;
break;
case `pitchbend`:
data[0] = message.channel + 223;
break;
case `poly-at`:
data[0] = message.channel + 159;
break;
case `progchange`:
data[0] = message.channel + 191;
break;
case `at`:
data[0] = message.channel + 207;
break;
default: throw new Error(`Command not supported '${message.command}'`);
}
return Array.from(data);
};
//#endregion
//#region ../packages/io/src/midi/notes.ts
const notesRaw = `0 C-1 8.176
1 C#-1 8.662
2 D-1 9.177
3 D#-1 9.723
4 E-1 10.301
5 F-1 10.913
6 F#-1 11.562
7 G-1 12.250
8 G#-1 12.978
9 A-1 13.750
10 A#-1 14.568
11 B-1 15.434
12 C0 16.352
13 C#0 17.324
14 D0 18.354
15 D#0 19.445
16 E0 20.602
17 F0 21.827
18 F#0 23.125
19 G0 24.500
20 G#0 25.957
21 A0 27.500
22 A#0 29.135
23 B0 30.868
24 C1 32.703
25 C#1 34.648
26 D1 36.708
27 D#1 38.891
28 E1 41.203
29 F1 43.654
30 F#1 46.249
31 G1 48.999
32 G#1 51.913
33 A1 55.000
34 A#1 58.270
35 B1 61.735
36 C2 65.406
37 C#2 69.296
38 D2 73.416
39 D#2 77.782
40 E2 82.407
41 F2 87.307
42 F#2 92.499
43 G2 97.999
44 G#2 103.826
45 A2 110.000
46 A#2 116.541
47 B2 123.471
48 C3 130.813
49 C#3 138.591
50 D3 146.832
51 D#3 155.563
52 E3 164.814
53 F3 174.614
54 F#3 184.997
55 G3 195.998
56 G#3 207.652
57 A3 220.000
58 A#3 233.082
59 B3 246.942
60 C4 261.626
61 C#4 277.183
62 D4 293.665
63 D#4 311.127
64 E4 329.628
65 F4 349.228
66 F#4 369.994
67 G4 391.995
68 G#4 415.305
69 A4 440.000
70 A#4 466.164
71 B4 493.883
72 C5 523.251
73 C#5 554.365
74 D5 587.330
75 D#5 622.254
76 E5 659.255
77 F5 698.456
78 F#5 739.989
79 G5 783.991
80 G#5 830.609
81 A5 880.000
82 A#5 932.328
83 B5 987.767
84 C6 1046.502
85 C#6 1108.731
86 D6 1174.659
87 D#6 1244.508
88 E6 1318.510
89 F6 1396.913
90 F#6 1479.978
91 G6 1567.982
92 G#6 1661.219
93 A6 1760.000
94 A#6 1864.655
95 B6 1975.533
96 C7 2093.005
97 C#7 2217.461
98 D7 2349.318
99 D#7 2489.016
100 E7 2637.020
101 F7 2793.826
102 F#7 2959.955
103 G7 3135.963
104 G#7 3322.438
105 A7 3520.000
106 A#7 3729.310
107 B7 3951.066
108 C8 4186.009
109 C#8 4434.922
110 D8 4698.636
111 D#8 4978.032
112 E8 5274.041
113 F8 5587.652
114 F#8 5919.911
115 G8 6271.927
116 G#8 6644.875
117 A8 7040.000
118 A#8 7458.620
119 B8 7902.133
120 C9 8372.018
121 C#9 8869.844
122 D9 9397.273
123 D#9 9956.063
124 E9 10548.080
125 F9 11175.300
126 F#9 11839.820
127 G9 12543.850`;
const notesParsed = [];
const getParsedNotes = () => {
if (notesParsed.length > 0) return notesParsed;
const lines = notesRaw.split("\n");
for (const line of lines) {
const s = line.split(`\t`);
if (s.length !== 3) {
console.warn(`Expected three elements, got ${s.length}. Line:`, s);
continue;
}
notesParsed.push([
Number.parseInt(s[0]),
s[1].toUpperCase(),
Number.parseFloat(s[2])
]);
}
return notesParsed;
};
const noteNameToNumber = (name) => {
const notes = getParsedNotes();
name = name.toUpperCase();
const n = notes.find((n) => n[1] === name);
if (n) return n[0];
return NaN;
};
const noteNameToFrequency = (name) => {
const notes = getParsedNotes();
name = name.toUpperCase();
const n = notes.find((n) => n[1] === name);
if (n) return n[2];
return NaN;
};
const noteNumberToName = (number) => {
const n = getParsedNotes().find((n) => n[0] === number);
if (n) return n[1];
return ``;
};
const noteNumberToFrequency = (number) => {
const n = getParsedNotes().find((n) => n[0] === number);
if (n) return n[2];
return NaN;
};
//#endregion
//#region ../packages/io/src/midi/manager.ts
/**
* Midi Manager makes simplifies connecting to ports
* and having omni input/output
*
* ```js
* const midi = new MidiManager(); // By default connects to all ins and outs
* midi.addEventListener(`message`, event => {
* // Do something with received MIDI data from any input
* })
*
* midi.scan();
* midi.send({ command: `cc`, note: 10, channel: 1, velocity: 20 });
*
* // Sends note '10' on channel 1, velocity 100, duration 200ms
* midi.sendNote(1, 10, 100, 200);
* ```
* Events:
* * open/close: Connected port is open/closed
* * deviceConnected/deviceDisconnected: A port is newly available or unavailable
* * message: MIDI event received
*/
var MidiManager = class extends SimpleEventEmitter {
verbose = true;
#state;
#access;
#inUse = [];
#known = [];
#omniInput = true;
#omniOutput = true;
#connectAllInputsDebounced = debounce(() => this.#connectAllInputs(), 1e3);
#connectAllOutputsDebounced = debounce(() => this.#connectAllOutputs(), 1e3);
constructor() {
super();
this.#state = {
initialised: false,
errorReason: ``
};
this.#throwIfNotSupported();
}
*getInUse() {
for (const p of this.#inUse) yield p;
}
*getInUseInput() {
for (const p of this.#inUse) if (p.type === `input`) yield p;
}
*getInUseOutput() {
for (const p of this.#inUse) if (p.type === `output`) yield p;
}
*known() {
for (const p of this.#known) yield p;
}
*knownInput() {
for (const p of this.#known) if (p.type === `input`) yield p;
}
*knownOutput() {
for (const p of this.#known) if (p.type === `output`) yield p;
}
async scan() {
await this.#init();
const a = this.#access;
if (!a) return;
a.inputs.forEach((port) => {
this.#updatePort(port);
});
a.outputs.forEach((port) => {
this.#updatePort(port);
});
if (this.#omniInput) this.#connectAllInputsDebounced();
if (this.#omniOutput) this.#connectAllOutputsDebounced();
}
/**
* Sends a message to a port.
*
* If port is omitted, all open output ports are used.
* @param message
* @param port
* @param timestamp
*/
send(message, port, timestamp) {
const packed = pack(message);
if (typeof port === `undefined`) {
for (const p of this.#inUse) if (p.type === `output`) p.send(packed, timestamp);
} else port.send(packed, timestamp);
}
sendNote(channel, note, velocity, duration, delay, port) {
if (port === void 0) for (const port of this.getInUseOutput()) this.sendNote(channel, note, velocity, duration, delay, port);
else sendNote(port, channel, note, velocity, duration, delay);
}
#updatePort(p) {
if (p.state === `connected`) {
this.#onPortConnected(p);
if (p.connection === `open`) this.#onPortOpen(p);
} else if (p.state === `disconnected`) this.#onPortDisconnected(p);
}
#onMessage = (event) => {
const raw = event.data;
const port = event.currentTarget;
if (!raw) return;
const data = unpack(raw);
if (data.command === `noteoff` || data.command === `noteon`) {
this.fireEvent(`message`, {
...data,
port,
raw,
frequency: noteNumberToFrequency(data.note),
noteName: noteNumberToName(data.note)
});
return;
}
this.fireEvent(`message`, {
...data,
port,
raw
});
};
#onPortOpen(port) {
const inUse = this.#inUse.find((p) => p.id === port.id);
this.#logVerbose(`onPortOpen: id: ${port.id} name: ${port.name} (${port.type})`);
if (inUse) {
this.#logVerbose(`-- bug, port already in use?`);
return;
}
this.#onPortConnected(port);
if (port.type === `input`) port.addEventListener(`midimessage`, this.#onMessage);
this.#inUse = [...this.#inUse, port];
this.fireEvent(`open`, { port });
}
#onPortClose(port) {
if (!this.#inUse.find((p) => p.id === port.id)) return;
if (port.type === `input`) port.removeEventListener(`midimessage`, this.#onMessage);
this.#inUse = this.#inUse.filter((p) => p.id !== port.id);
this.fireEvent(`close`, { port });
}
/**
* New device connected, but not necessarily open
* @param port
*/
#onPortConnected(port) {
if (this.#known.find((p) => p.id === port.id)) return;
this.#known = [...this.#known, port];
this.fireEvent(`deviceConnected`, { port });
}
/**
* Device disconnected
* @param port
* @returns
*/
#onPortDisconnected(port) {
if (!this.#known.find((p) => p.id === port.id)) return;
this.#onPortClose(port);
this.#known = this.#known.filter((p) => p.id !== port.id);
this.fireEvent(`deviceDisconnected`, { port });
}
#isPortInUse(port) {
return this.#inUse.find((p) => p.id === port.id) !== void 0;
}
async closeAll(what = `both`) {
for (const p of this.#inUse) {
if (p.type == `input` && (what === `both` || what === `input`)) await p.close();
if (p.type == `output` && (what === `both` || what === `output`)) await p.close();
}
}
async setOmniInput(value) {
this.#omniInput = value;
await this.closeAll(`input`);
if (value) await this.#connectAllInputs();
}
get omniInput() {
return this.#omniInput;
}
async setOmniOutput(value) {
this.#omniOutput = value;
await this.closeAll(`output`);
if (value) await this.#connectAllOutputs();
}
get omniOutput() {
return this.#omniOutput;
}
async #connectAllInputs() {
const a = this.#access;
if (!a) return;
a.inputs.forEach(async (input) => {
if (input.connection === `closed`) {
if (this.#isPortInUse(input)) throw new Error(`Bug: Input closed, but inUse?`);
await input.open();
}
});
}
async #connectAllOutputs() {
const a = this.#access;
if (!a) return;
a.outputs.forEach(async (output) => {
if (output.connection === `closed`) {
if (this.#isPortInUse(output)) throw new Error(`Bug: Output closed, but inUse?`);
await output.open();
}
});
}
dumpToStringLines() {
const returnValue = [];
const portToString = (p) => ` - ${p.name} (${p.type}) state: ${p.state} conn: ${p.connection} id: ${p.id}`;
returnValue.push(`MidiManager`);
returnValue.push(`In Use:`);
returnValue.push(...mapWithEmptyFallback(this.#inUse, portToString, ` (none)`));
returnValue.push(`Known:`);
returnValue.push(...mapWithEmptyFallback(this.#known, portToString, ` (none)`));
return returnValue;
}
#onStateChange(event) {
const port = event.port;
if (port === null) return;
if (port.state === `connected`) {
if (port.connection === `open`) this.#onPortOpen(port);
else if (port.connection === `closed`) {
this.#onPortClose(port);
this.#onPortConnected(port);
if (this.#omniInput && port.type === `input`) this.#connectAllInputsDebounced();
else if (this.#omniOutput && port.type === `output`) this.#connectAllOutputsDebounced();
}
} else if (port.state === `disconnected`) this.#onPortDisconnected(port);
}
/**
* Opens `port`.
*
* If `exclusive` is _true_, all other ports of that type
* (ie. input or output) are closed before the port is opened.
*
* If `exclusive` is _false_ (default), already open ports are left open.
* @param port
* @param exclusive
*/
async open(port, exclusive = false) {
if (exclusive) {
if (port.type === `input`) await this.closeAll(`input`);
else if (port.type === `output`) await this.closeAll(`output`);
}
port.open();
}
#logVerbose(message) {
if (!this.verbose) return;
console.log(`MIDI`, message);
}
#setState(state) {
this.#state = {
...this.#state,
...state
};
this.#logVerbose(`State change: ${JSON.stringify(this.#state)}`);
}
async #init() {
if (this.#state.initialised && this.#access !== void 0) return;
if ((await navigator.permissions.query({
name: "midi",
software: true,
sysex: false
})).state === `denied`) {
this.#access = void 0;
this.#setState({
initialised: false,
errorReason: `Permission denied`
});
return;
}
this.#access = await navigator.requestMIDIAccess({
software: true,
sysex: false
});
this.#access.addEventListener(`statechange`, (event) => {
this.#onStateChange(event);
});
this.#setState({
initialised: true,
errorReason: ``
});
}
#isSupported() {
if (!navigator.requestMIDIAccess) return false;
return true;
}
#throwIfNotSupported() {
if (!window.isSecureContext) throw new Error(`Code is not running in a secure context. Load it via https`);
if (!this.#isSupported()) throw new Error(`MIDI not supported in this browser`);
}
findKnownPort(fn) {
return this.#known.find(fn);
}
*filterKnownPort(fn) {
yield* this.#known.filter(fn);
}
findInUsePort(fn) {
return this.#inUse.find(fn);
}
*filterInUsePort(fn) {
yield* this.#inUse.filter(fn);
}
};
//#endregion
//#region ../packages/io/src/midi/control.ts
var Feedback = class {
channel = 0;
cc = -1;
note = -1;
output;
portName;
constructor(options = {}) {
this.channel = options.channel ?? -1;
this.cc = options.cc ?? -1;
this.note = options.note ?? -1;
this.output = options.output;
this.portName = options.portName;
}
setOutputPort(port) {
if (port.type === `input`) return false;
if (this.portName !== void 0) {
if (port.name !== this.portName) return false;
}
this.output = port;
return true;
}
sendRaw(value) {
if (!this.output) return false;
if (this.channel < 0) return false;
if (this.cc < 0 && this.note < 0) return false;
let message;
if (this.cc >= 0) {
message = {
channel: this.channel,
command: `cc`,
note: this.cc,
velocity: value
};
console.log(message);
this.output.send(pack(message));
return true;
}
console.log(`sendNote: ch: ${this.channel} note: ${this.note} vel: ${value}`);
sendNote(this.output, this.channel, this.note, value, 200);
return true;
}
};
var Control = class Control extends SimpleEventEmitter {
static controlCount = 0;
inputChannel = 1;
inputCommand = `cc`;
inputNote = -1;
inputVelocityScale = [0, 127];
feedbackChannel = 1;
feedbackCommand = `cc`;
feedbackNote = -1;
feedbackVelocity = 1;
name = `Control-${Control.controlCount++}`;
lastMessage;
onInputMessage(message) {
if (this.inputChannel >= 0 && message.channel !== this.inputChannel) return false;
if (this.inputNote >= 0 && message.note !== this.inputNote) return false;
if (this.inputCommand !== void 0 && message.command !== this.inputCommand) return false;
this.lastMessage = message;
this.fireEvent(`change`, {
velocity: message.velocity,
velocityScaled: this.#scaleVelocity(message.velocity),
control: this
});
return true;
}
#scaleVelocity(v) {
return scale(v, this.inputVelocityScale[0], this.inputVelocityScale[1]);
}
get scaledVelocity() {
if (this.lastMessage) return this.#scaleVelocity(this.lastMessage.velocity);
return NaN;
}
};
//#endregion
//#region ../packages/io/src/midi/midi-fighter.ts
/**
* Connects to a DJ Tech Tools Midi Fighter controller.
*
* Use the 'state' event and wait for state to be 'ready'.
*
* ```js
* const mf = new MidiFighter();
* mf.addEventListener(`state`, event => {
* if (event.state === `ready`) {
* // Can work with device now
* mf.bank = 1;
* }
* });
* mf.addEventListener(`encoder`, event => {
* // Do something with encoder value
* });
* mf.setPort(someMidiInputPort);
* mf.setPort(someMidiOutputPort);
* ```
* Assumes default settings are loaded on the controller
*
* Supports
* * Listening for encoder moves and button presses
* * Changing colour pip below each encoder
* * Setting LED bar for each encoder
* * Changing banks, or detecting when the user has done so via the physical buttons
*
* Events:
* * bankChange: Current bank has changed
* * sideButton: Side button pressed
* * switch: Encoder has been pressed
* * encoder: Encoder has been moved
* * state: Midi Fighter has both input/output ports or not.
*/
var MidiFighter = class extends SimpleEventEmitter {
encoders = [];
#currentBank = -1;
#state = `disconnected`;
#inputPort;
#outputPort;
/**
* If true, messages sent to Midi Fighter are printed to console
*/
logOutgoing = false;
/**
* Channel bank change events are received on
*/
bankChangeChannel = 4;
/**
* Channel side button press events are received on
*/
sideButtonChannel = 4;
constructor() {
super();
for (let bank = 1; bank < 5; bank++) for (let encoder = 1; encoder < 17; encoder++) {
const enc = new MidiFighterEncoder(this, {
bank,
encoder
});
this.encoders.push(enc);
enc.addEventListener(`encoder`, (event) => {
this.fireEvent(`encoder`, event);
});
enc.addEventListener(`switch`, (event) => {
this.fireEvent(`switch`, event);
});
}
}
/**
* Our input/output port has changed state
* @param event
* @returns
*/
#onPortState = (event) => {
const port = event.port;
if (!port) return;
if (port === this.#outputPort && (port.state === `disconnected` || port.connection === `closed`)) this.#outputPortUnbind();
if (port === this.#inputPort && (port.state === `disconnected` || port.connection === `closed`)) this.#inputPortUnbind();
if (this.#outputPort !== void 0 && this.#inputPort !== void 0) this.#setState(`ready`);
else this.#setState(`disconnected`);
};
/**
* Unsubscribe from events of current input port, if we have one
*/
#inputPortUnbind() {
const ip = this.#inputPort;
if (ip !== void 0) {
ip.removeEventListener(`statechange`, this.#onPortState);
ip.removeEventListener(`midimessage`, this.#onMessage);
}
this.#inputPort = void 0;
}
/**
* Unsubcribe from events of current output port, if we have one
*/
#outputPortUnbind() {
const op = this.#outputPort;
if (op !== void 0) op.removeEventListener(`statechange`, this.#onPortState);
this.#outputPort = void 0;
}
/**
* Sets a port for this instance to use.
* This will need to be called separately for the input and output ports
* @param port
*/
setPort(port) {
if (port.name === `Midi Fighter Twister`) {
if (port.type === `output`) {
this.#outputPortUnbind();
this.#outputPort = port;
if (this.#outputPort !== void 0) this.#outputPort.addEventListener(`statechange`, this.#onPortState);
} else if (port.type === `input`) {
this.#inputPortUnbind();
this.#inputPort = port;
if (port !== void 0) {
this.#inputPort.addEventListener(`midimessage`, this.#onMessage);
this.#inputPort.addEventListener(`statechange`, this.#onPortState);
}
}
}
if (this.#outputPort !== void 0 && this.#inputPort !== void 0) this.#setState(`ready`);
}
#setState(state) {
const previous = this.#state;
if (previous === state) return;
this.#state = state;
this.fireEvent(`state`, {
previous,
state,
mf: this
});
}
#onMessage = (event) => {
const data = event.data;
if (!data) return;
const message = unpack(data);
if (message.channel === this.bankChangeChannel) {
if (message.command === `cc` && message.note < 4) {
this.#onBankChange(message.note + 1, false);
return;
}
}
if (message.channel === this.sideButtonChannel && message.command == `cc`) {
let buttonBank = -1;
let position;
let side;
if (message.note === 8) {
buttonBank = 1;
position = `top`;
side = `left`;
} else if (message.note === 10) {
buttonBank = 1;
side = `left`;
position = `bottom`;
} else if (message.note === 11) {
buttonBank = 1;
side = `right`;
pos