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 choose one from your filesystem.
+ Drag and drop an audio file anywhere or choose one from your filesystem.
+
+
+
+ Filter:
+
+ noop
+ derivative
+ integral
+ subtract-overlap
+ isolate-overlap
+ mono
+
+
-
- derivative-processor
- integral-processor
- subtract-overlap
-
-
- derivative-processor
- Approximates the first derivative of the audio as a function respect to time.
+ noop
+ Leaves the audio unchanged.
- integral-processor
- Approximates the first integral of the audio as a function respect to time. Plus C of zero.
+ derivative
+
+ Approximates the first derivative of the audio as a function respect to time.
+ Sounds like a high-pass filter .
+
+
+
+ integral
+
+ Approximates the first integral of the audio as a function respect to time. Plus C of zero. Sounds like a low-pass filter .
+
subtract-overlap
Inverts the amplitude of every even-numbered channel and merges all channels to mono. This effectively cancels the sounds the left and right channels share in common. Quick-and-dirty way of removing vocals.
+
+ isolate-overlap
+ The opposite of subtract-overlap. You think it'd be simple but I can't get it to work :/
+
+
+ mono
+
+ Converts the audio to mono by taking the arithmetic average of the samples.
+
+
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.
+}