diff --git a/index.html b/index.html index 6390b59..f60a989 100644 --- a/index.html +++ b/index.html @@ -9,12 +9,17 @@ D♭ - + + +

- Drop a file anywhere or + Drag and drop an audio file anywhere or +

+

+

- -
diff --git a/src/main.js b/src/main.js index 431a466..3e3237a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,12 +1,12 @@ -//@ts-ignore -import workletUrl from './processors.worklet.js' +import { audio, useProcessor } from './setup.js' -document.getElementById('choose').addEventListener('click', ev => { +document.getElementById('choose').addEventListener('click', () => { document.getElementById('file').click() }) -document.getElementById('file').addEventListener('input', ev => { - fileGuard(ev.target.files) +document.querySelector('input').addEventListener('input', function () { + //@ts-ignore + fileGuard(this.files) }) window.addEventListener('drop', async ev => { @@ -14,15 +14,22 @@ window.addEventListener('drop', async ev => { fileGuard(ev.dataTransfer.files) }) +// Prevent Chrome from replacing the document when a file's dropped over it document.addEventListener('dragover', ev => { ev.preventDefault() }) +/** + * @param {FileList} files + */ function fileGuard(files) { const candidates = [...files] - const file = candidates.find(file => file.type.startsWith('audio/')) + const file = + candidates.find(file => file.type.startsWith('audio/')) || + candidates.find(file => file.type.startsWith('video/')) if (!file) { + console.log(candidates.map(f => f.type)) alert("Be sure to drop an audio file your browser supports :)") return } @@ -30,44 +37,39 @@ function fileGuard(files) { init(file) } +const select = document.querySelector('select') +select.addEventListener('input', () => { + useProcessor(select.value) +}) + /** * @param {File} audioFile */ async function init(audioFile) { const audioEl = document.querySelector('audio') - const audio = new AudioContext() - await audio.audioWorklet.addModule('/dist/' + workletUrl) - - const dFlat = new AudioWorkletNode(audio, 'subtract-overlap', { - processorOptions: { - maxChannels: audio.destination.maxChannelCount - } - }) - const audioSrcNode = audio.createMediaElementSource(audioEl) - - audioSrcNode.connect(dFlat) - dFlat.connect(audio.destination) audioEl.src = URL.createObjectURL(audioFile) + useProcessor(select.value) - //@ts-ignore - if (navigator['mediaSession'] && typeof MediaMetadata !== 'undefined') { - //@ts-ignore - navigator['mediaSession'].metadata = new MediaMetadata({ - title: audioFile.name + audio.resume().then(() => audioEl.play()) + + document.title = `D♭ • ${audioFile.name}` + + if (navigator.mediaSession && typeof MediaMetadata !== 'undefined') { + navigator.mediaSession.metadata = new MediaMetadata({ + title: audioFile.name, + album: "D♭" }); - navigator['mediaSession'].setActionHandler('play', () => { + navigator.mediaSession.setActionHandler('play', () => { audioEl.play() }) - navigator['mediaSession'].setActionHandler('pause', () => { + navigator.mediaSession.setActionHandler('pause', () => { audioEl.pause() }) - navigator['mediaSession'].setActionHandler('seekbackward', () => { + navigator.mediaSession.setActionHandler('seekbackward', () => { audioEl.currentTime -= 5 }) - navigator['mediaSession'].setActionHandler('seekforward', () => { + navigator.mediaSession.setActionHandler('seekforward', () => { audioEl.currentTime += 5 }) } - - } diff --git a/src/processors.worklet.js b/src/processors.worklet.js index c0dcfc7..70573b6 100644 --- a/src/processors.worklet.js +++ b/src/processors.worklet.js @@ -2,6 +2,49 @@ const carry = Symbol() +class NoopProcessor extends AudioWorkletProcessor { + /** + * @param {Float32Array[][]} inputs + * @param {Float32Array[][]} outputs + */ + process(inputs, outputs) { + const input = inputs[0] + const output = outputs[0] + + for (let channel = 0; channel < input.length; channel++) { + output[channel].set(input[channel]) + } + return true + } +} +registerProcessor('noop', NoopProcessor) + +class Mono extends AudioWorkletProcessor { + /** + * @param {Float32Array[][]} inputs + * @param {Float32Array[][]} outputs + */ + process(inputs, outputs) { + // This processor has only one input and one output. + const input = inputs[0] + const output = outputs[0] + + for (let channel = 0; channel < input.length; channel++) { + for (let sample = 0; sample < input[0].length; sample++) { + // Mono: (L + R) / 2 + // More generally: 1/n * (\sum_{i = 0}^n C_i) + output[0][sample] += input[channel][sample] * (1 / input.length) + } + if (channel !== 0) { + output[channel].set(output[0]) + } + } + + return true + } +} +registerProcessor('mono', Mono) + class DerivativeProcessor extends AudioWorkletProcessor { constructor(options) { super() @@ -25,12 +68,10 @@ class DerivativeProcessor extends AudioWorkletProcessor { this[carry][channel] = input[channel][input.length - 1] } - return true; + return true } } - -registerProcessor('derivative-processor', DerivativeProcessor) - +registerProcessor('derivative', DerivativeProcessor) class IntegralProcessor extends AudioWorkletProcessor { @@ -56,33 +97,34 @@ class IntegralProcessor extends AudioWorkletProcessor { this[carry][channel] = input[channel][input.length - 1] } - return true; + return true } } - -registerProcessor('integral-processor', IntegralProcessor) +registerProcessor('integral', IntegralProcessor) -// class BitCrusher extends AudioWorkletProcessor { -// /** -// * @param {Float32Array[][]} inputs -// * @param {Float32Array[][]} outputs -// */ -// process(inputs, outputs) { -// const input = inputs[0] -// const output = outputs[0] +class BitCrusher extends AudioWorkletProcessor { + /** + * @param {Float32Array[][]} inputs + * @param {Float32Array[][]} outputs + */ + process(inputs, outputs) { + const input = inputs[0] + const output = outputs[0] -// for (let channel = 0; channel < input.length; channel++) { -// for (let sample = 0; sample < input[channel].length; sample++) { -// output[channel][sample] = input[channel][sample] ** 2 -// } -// } + let phase = 1 + const step = 2 * Math.pow(.5, 8) -// return true; -// } -// } - -// registerProcessor('bit-crusher', BitCrusher) + for (let channel = 0; channel < input.length; channel++) { + for (let sample = 0; sample < input[channel].length; sample++) { + output[channel][sample] = input[channel][sample] + } + } + + return true + } +} +registerProcessor('bit-crusher', BitCrusher) /** @@ -95,11 +137,12 @@ class SubtractOverlap extends AudioWorkletProcessor { * @param {Float32Array[][]} outputs */ process(inputs, outputs) { + // This processor has only one input and one output. const input = inputs[0] const output = outputs[0] for (let channel = 0; channel < input.length; channel++) { - for (let sample = 0; sample < input[0].length; sample++) { + for (let sample = 0; sample < input[channel].length; sample++) { output[0][sample] += (channel % 2 === 0 ? 1 : -1) * input[channel][sample] } if (channel !== 0) { @@ -107,8 +150,41 @@ class SubtractOverlap extends AudioWorkletProcessor { } } - return true; + return true } } - registerProcessor('subtract-overlap', SubtractOverlap) + +/** + * Like `subtract-overlap`, but isolates the overlap instead of removing it. + * Quick-and-dirty way to isolate vocals + */ +class IsolateOverlap extends AudioWorkletProcessor { + /** + * @param {Float32Array[][]} inputs + * @param {Float32Array[][]} outputs + */ + process(inputs, outputs) { + // This processor has only one input and one output. + const input = inputs[0] + const output = outputs[0] + + // FIXME: I can't get this filter to work :/ + + for (let channel = 0; channel < input.length; channel++) { + for (let sample = 0; sample < input[channel].length; sample++) { + output[0][sample] += (channel % 2 === 0 ? 1 : -1) * input[channel][sample] + } + } + + for (let channel = input.length - 1; 0 <= channel; channel--) { + for (let sample = 0; sample < input[channel].length; sample++) { + output[channel][sample] = input[channel][sample] - output[0][sample] + } + } + + + return true + } +} +registerProcessor('isolate-overlap', IsolateOverlap) diff --git a/src/setup.js b/src/setup.js new file mode 100644 index 0000000..4e343bb --- /dev/null +++ b/src/setup.js @@ -0,0 +1,57 @@ +//@ts-ignore +import workletUrl from './processors.worklet.js' + +const audioEl = document.querySelector('audio') +const audio = new AudioContext() +const audioSrcNode = audio.createMediaElementSource(audioEl) + +// Add the modules +audio.audioWorklet.addModule('/dist/' + workletUrl) + +let dFlat = null +// let dFlat = null new AudioWorkletNode(audio, 'subtract-overlap', { +// processorOptions: { +// maxChannels: audio.destination.maxChannelCount +// } +// }) + +// audioSrcNode.connect(dFlat) +// dFlat.connect(audio.destination) + +/** + * Swaps out the processor + * @param {string} name The name of the processor to use + */ +export function useProcessor(name) { + if (dFlat) { + dFlat.disconnect() + audioSrcNode.disconnect() + } + + dFlat = new AudioWorkletNode(audio, name, { + processorOptions: { + maxChannels: audio.destination.maxChannelCount + } + }) + audioSrcNode.connect(dFlat) + dFlat.connect(audio.destination) +} + +export { audio } + + +// const audioEl = document.querySelector('audio') +// const audio = new AudioContext() +// await audio.audioWorklet.addModule('/dist/' + workletUrl) + +// const dFlat = new AudioWorkletNode(audio, 'subtract-overlap', { +// // const dFlat = new AudioWorkletNode(audio, 'bit-crusher', { +// processorOptions: { +// maxChannels: audio.destination.maxChannelCount +// } +// }) +// const audioSrcNode = audio.createMediaElementSource(audioEl) + +// audioSrcNode.connect(dFlat) +// dFlat.connect(audio.destination) +// audioEl.src = URL.createObjectURL(audioFile) diff --git a/src/types.d.ts b/src/types.d.ts index c46efd4..b51b4b6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -14,3 +14,68 @@ function registerProcessor( name: string, processorCtor: AudioWorkletProcessorConstructor ): void + +// MediaSession definitions +interface Navigator { + readonly mediaSession?: MediaSession +} + +interface Window { + MediaSession?: MediaSession +} + +type MediaSessionPlaybackState = 'none' | 'paused' | 'playing' + +type MediaSessionAction = + | 'play' + | 'pause' + | 'seekbackward' + | 'seekforward' + | 'previoustrack' + | 'nexttrack' + +interface MediaSession { + // Current media session playback state. + playbackState: MediaSessionPlaybackState + // Current media session meta data. + metadata: MediaMetadata | null + + // Set/Unset actions handlers. + setActionHandler( + action: MediaSessionAction, + listener: (() => void) | null + ): void +} + +interface MediaImage { + // URL from which the user agent can fetch the image’s data. + src: string + // Specify the MediaImage object’s sizes. It follows the spec of sizes attribute in HTML link element. + sizes?: string + // A hint as to the media type of the image. + type?: string +} + +interface MediaMetadataInit { + // Media's title. + title?: string + // Media's artist. + artist?: string + // Media's album. + album?: string + // Media's artwork. + artwork?: MediaImage[] +} + +declare class MediaMetadata { + public title: string + // Media's artist. + public artist: string + // Media's album. + public album: string + // Media's artwork. + public artwork: MediaImage[] + + constructor(init?: MediaMetadataInit) + // Media's title. +}