Skip to content

Commit

Permalink
nvm thank god i'm the best whoever did it
Browse files Browse the repository at this point in the history
i.e. use only 1 audio worklet in safari 18
  • Loading branch information
chee committed Sep 19, 2024
1 parent ade274a commit 81d348c
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 46 deletions.
28 changes: 0 additions & 28 deletions public/bento.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,6 @@ async function registerServiceWorker() {

registerServiceWorker()

let safari18 = navigator.userAgent.match(/18\.0 (Mobile\/[A-Z0-9]+ )?Safari/)
if (safari18) {
document.write(/*html*/ `
<p>
Sorry! Apple has broken <b>bento</b> in the latest version of Safari
by making it impossible to share an array buffer larger than 906404 bytes.
</p>
<p>
It'll be fixed at the end of October, but until then I am so sorry to say there is
no way to make <b>bento</b> work on Safari. 😭
</p>
<p>
Please come back when ${
safari18[1] ? "i" : "mac"
}OS 18.1 comes out at the end of October 2024
</p>
${
safari18[1]
? /*html*/ `<p>If Apple let us use different browser engines on the telephone i could recommend you use one of those instead</p>`
: ""
}
`)
throw new (class SafariError extends Error {})("safari 18.0")
}

import * as sounds from "./sounds/sounds.js"
import * as graphics from "./graphics/graphics.js"
import {
Expand Down
2 changes: 1 addition & 1 deletion public/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// update this when changing the service worker
const SERVICE_WORKER_VERSION = "v11"
const SERVICE_WORKER_VERSION = "v12"

const addResourcesToCache = async resources => {
const cache = await caches.open(SERVICE_WORKER_VERSION)
Expand Down
153 changes: 153 additions & 0 deletions public/sounds/safari18.audioworklet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {DYNAMIC_RANGE} from "../memory/constants.js"
import MemoryTree from "../memory/tree/tree.js"
import * as Memory from "../memory/memory.js"
import quietparty from "./quietparty.js"
import Step from "../memory/tree/step.js"
import {Scale, pitch2playbackrate} from "./scale.js"

/** the curve used to make the gain more satisfying */
let qcurve = new Float32Array(DYNAMIC_RANGE + 1)
for (let i = 0; i < qcurve.length; i++) {
qcurve[i] = 1.00001 - Math.sin((i / (qcurve.length + 1)) * Math.PI * 0.5)
}

class Safari18AudioWorklet extends AudioWorkletProcessor {
constructor(options) {
super()
let {buffer, layerNumber} = options.processorOptions
if (!buffer || layerNumber == null) {
let msg = "failed to instantiate BentoLayer, missing processorOption"
console.error(msg, {
buffer: typeof buffer,
layerNumber
})
throw new Error(msg)
}
this.memtree = MemoryTree.from(buffer)
// this.envelope = new Envelope(4, 4)
this.lastStep = -1
this.quiet = 0
this.pan = 0
this.layerNumber = layerNumber
this.attack = 0
this.release = 0

this.layerNumber = layerNumber
this.lastStepIndex = -1
this.tick = 0
// todo caching of portions
/** @type Map<string, Float32Array> */
this.cache = new Map()
this.portion = new Float32Array(0)
this.scale = Scale.HarmonicMinor
this.loop = false
this.point = 0
this.loops = 0
this.map = Memory.map(buffer)

/** @type {Step["view"]} */
this.portionStep
}
// :)
/**
* @param {Float32Array[][]} inputs
* @param {Float32Array[][]} outputs
* @param {Record<string, Float32Array>} parameters
*/
process(inputs, outputs, parameters) {
let layerIndex = this.layerNumber
let memtree = this.memtree
let layer = memtree.getLayer(layerIndex)
let stepIndex = memtree.getCurrentStepIndexInLayer(layerIndex)
if (memtree.active) {
this.tick += 128
let speed = this.map.layerSpeeds.at(this.layerNumber)
let samplesPerStep = memtree.samplesPerBeat / (4 * speed)
let internalTick =
((this.tick / samplesPerStep) | 0) % Memory.STEPS_PER_GRID
if (internalTick != this.internalTick) {
memtree.incrementStep(this.layerNumber)
this.port.postMessage("step-change")
}
this.internalTick = internalTick
} else {
this.tick = 0
}

if (layer.type == "off") {
return false
} else if (memtree.playing && memtree.paused) {
return true
} else if (!memtree.playing) {
this.lastStep = -1
return true
}

let currentStep = memtree.getCurrentStepIndexInLayer(layerIndex)
if (currentStep != this.lastStep) {
let step = memtree.getLayerStep(layerIndex, currentStep)
if (step.on) {
this.quiet = step.quiet
this.pan = step.pan
}
}

if (stepIndex != this.lastStepIndex) {
let step = memtree.getLayerStep(layerIndex, stepIndex)

if (step.on) {
let sound = memtree.getSound(layerIndex)
this.portionStep = step
// todo stereo?
if (step.state == "on") {
this.portion = sound.left.subarray(
step.start,
step.end || sound.length
)
this.point = 0
}

this.loops = 0
if (step.reversed) {
this.portion = this.portion.slice().reverse()
}
this.playbackRate = pitch2playbackrate(step.pitch, this.scale)
}
}

this.lastStepIndex = stepIndex

let quantumPortionLength = this.portion.length - this.point
if (this.point > this.portion.length) {
if (this.portionStep.loop && this.loops < this.portionStep.loop) {
this.loops += 1
this.point = 0
}
}

if (outputs[0]) {
let [[left, right]] = outputs

if (left && right) {
for (let i = 0; i < 128; i++) {
let p = this.point
if (i < quantumPortionLength) {
let s1 = this.portion[p | 0] || 0
let s2 = this.portion[(p | 0) + 1] || 0
let s = s1 + (p % 1) * (s2 - s1) || 0

;[left[i], right[i]] = quietparty(this.portionStep, [s, s])
} else {
left[i] = right[i] = 0
}
;[left[i], right[i]] = quietparty(this, [left[i], right[i]])
this.point += this.playbackRate
}
}
}

return true
}
}

registerProcessor("safari-18", Safari18AudioWorklet)
66 changes: 49 additions & 17 deletions public/sounds/sounds.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,56 @@ export async function play() {
await context.audioWorklet.addModule("/sounds/transport.audioworklet.js")
await context.audioWorklet.addModule("/sounds/sampler.audioworklet.js")
await context.audioWorklet.addModule("/sounds/quietparty.audioworklet.js")
await context.audioWorklet.addModule("/sounds/safari18.audioworklet.js")

/** @type AudioWorkletNode[] */
let transports = []

/** @type BentoSoundSource[] */
let layers = []

/**
* @param {number} layerIndex
* @param {keyof typeof LayerType} layerType
*/
export function safari18wire(layerIndex, layerType) {
let processorOptions = {buffer: sharedarraybuffer, layerNumber: layerIndex}

if (layerIndex in transports == false) {
if (layerIndex in layers) {
try {
layers[layerIndex].destroy()
} catch (error) {
console.warn("error disconnecting", error)
}
}
let transport = new AudioWorkletNode(context, "safari-18", {
processorOptions,
numberOfInputs: 0,
numberOfOutputs: 1,
channelCount: 2,
outputChannelCount: [2],
channelInterpretation: "speakers"
})
transports[layerIndex] = transport
transport.port.onmessage = event => {
if (event.data == "step-change") {
party.updateCurrentStep(memtree)
let layer = layers[layerIndex]
if (layer) {
let step = memtree.getStep(
memtree.getLayerCurrentStepAbsoluteIndex(layerIndex)
)
layer.play(step)
}
}
}
let source = new Passthru(context, transport)
source.connect(context.destination)
layers[layerIndex] = source
}
}

/**
* @param {number} layerIndex
* @param {keyof typeof LayerType} layerType
Expand Down Expand Up @@ -198,25 +241,12 @@ export function wire(layerIndex, layerType) {
let source = new Passthru(context, sampler)
source.connect(context.destination)
layers[layerIndex] = source
} else if (layerType == "synth") {
let quietparty = new AudioWorkletNode(context, "quiet-party", {
processorOptions,
numberOfInputs: 1,
numberOfOutputs: 1,
channelCount: 2,
outputChannelCount: [2],
channelInterpretation: "speakers"
})
let source = new Synth(context)
source.out.gain.value = 0.000001
source.connect(quietparty)
quietparty.connect(context.destination)
layers[layerIndex] = source
} else if (layerType == "off") {
delete layers[layerIndex]
} else {
}
}

let safari18 = navigator.userAgent.match(/18\.0 (Mobile\/[A-Z0-9]+ )?Safari/)

export async function start() {
await play()
if (alreadyFancy) {
Expand All @@ -227,7 +257,9 @@ export async function start() {
// todo write analysis to memory periodically
// let analysis = new Float32Array(analyzer.fftSize)

loop.layers(idx => wire(idx, memtree.getLayer(idx).type))
loop.layers(idx =>
(safari18 ? safari18wire : wire)(idx, memtree.getLayer(idx).type)
)
party.when("select-layer-type", message => {
wire(message.layer, message.type)
})
Expand Down

0 comments on commit 81d348c

Please sign in to comment.