Skip to content

Commit

Permalink
rip out scheduling stuff
Browse files Browse the repository at this point in the history
it didn't work the way I wanted it to
  • Loading branch information
sethbrasile committed Oct 4, 2024
1 parent 0bd361f commit aa14040
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 115 deletions.
3 changes: 0 additions & 3 deletions src/app/pages/synthesis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ const Content = {
// Add it to the DOM
keyboard.appendChild(key)

// We can't create the oscillator until the user interacts with the page, so wait for that
await initAudio()

// Create the oscillator for this key and set its frequency from the corresponding note
const osc = await createOscillator({
frequency: note.frequency,
Expand Down
93 changes: 19 additions & 74 deletions src/base-sound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,6 @@ export interface BaseSoundOptions {
*/
name?: string

/**
* @property trackPlays
*
* Allows disabling play/stop tracking for this sound. You should not need to worry about this unless you're facing performance issues.
* If you disable this option, this sound's play/stop methods may end up called out of order when called in quick succession. This is caused by the browser
* sometimes failing to schedule events in the correct order.
*
* @default true
*/
trackPlays?: boolean

/**
* @method setTimeout
*
Expand Down Expand Up @@ -101,30 +90,6 @@ export abstract class BaseSound implements Connectable, Playable {
*/
public name: string

/**
* @property playCalled
* This is a set of numbers, where each number is the audio context time that play was called. If play is called
* multiple times in the same audio context time, the second call will be ignored. Also lets us avoid calling stop
* before start was called. The browser will do this sometimes when binding start/stop to touch events.
*
* Acts as a register of called play times for this instance. This allows us to ensure that play isn't scheduled multiple
* times for the same audio context time.
*/
private playCalled = new Set<number>()

/**
* @property trackPlays
*
* Allows disabling play/stop tracking for this sound. You should not need to worry about this unless you're facing performance issues.
* If you disable this option, this sound's play/stop methods may end up called out of order when called in quick succession. This is caused by the browser
* sometimes failing to schedule events in the correct order.
*
* Disable this option by setting the `trackPlays` option to `false` when creating this sound.
*
* @default true
*/
private trackPlays = true

constructor(protected audioContext: AudioContext, opts?: BaseSoundOptions) {
const gainNode = audioContext.createGain()
const pannerNode = audioContext.createStereoPanner()
Expand All @@ -134,11 +99,6 @@ export abstract class BaseSound implements Connectable, Playable {

this.name = opts?.name || ''

// if not nullish
if (opts?.trackPlays != null) {
this.trackPlays = opts.trackPlays
}

if (opts?.setTimeout) {
this.setTimeout = opts.setTimeout
}
Expand Down Expand Up @@ -212,8 +172,8 @@ export abstract class BaseSound implements Connectable, Playable {
return this.controller.onPlayRamp(type, rampType)
}

public play(): void {
this.playAt(this.audioContext.currentTime)
public async play(): Promise<void> {
await this.playAt(this.audioContext.currentTime)
}

public playIn(when: number): void {
Expand Down Expand Up @@ -255,30 +215,21 @@ export abstract class BaseSound implements Connectable, Playable {
* @method playAt
*/
public async playAt(time: number): Promise<void> {
// Only care about this if trackPlays is true, this is optional for performance reasons
if (this.trackPlays) {
// If the audio source has already been scheduled to play at this time, don't schedule it
if (this.playCalled.has(time)) {
return
}

this.playCalled.add(time)
}

const { audioContext } = this
const { currentTime } = audioContext
const duration = this.duration.raw

await audioContext.resume()

this.setup()
this.audioSourceNode.start(time, this.startOffset)
this.startedPlayingAt = time

// schedule _isPlaying to false after duration
this.setTimeout(() => {
this._isPlaying = false
this.playCalled.delete(time)
}, this.duration.pojo.seconds * 1000)
// if duration exists, schedule _isPlaying to false after duration has elapsed
if (duration) {
this.setTimeout(() => {
this._isPlaying = false
}, this.duration.pojo.seconds * 1000)
}

if (time <= currentTime) {
this._isPlaying = true
Expand All @@ -299,8 +250,8 @@ export abstract class BaseSound implements Connectable, Playable {
* @param {number} seconds Number of seconds from "now" that the audio source
* should be stopped.
*/
public stopIn(seconds: number): void {
this.stopAt(this.audioContext.currentTime + seconds)
public async stopIn(seconds: number): Promise<void> {
await this.stopAt(this.audioContext.currentTime + seconds)
}

/**
Expand All @@ -315,23 +266,17 @@ export abstract class BaseSound implements Connectable, Playable {
* {{#crossLink "AudioContext"}}AudioContext's{{/crossLink}} "beginning of
* time") when the audio source should be stopped.
*/
public stopAt(time: number): void {
// Only care about this if trackPlays is true
if (this.trackPlays) {
// if play has not been called for this sound/time, no sense stopping it. Also if it has already been stopped.
if (!this.playCalled.has(time)) {
return
}

this.playCalled.delete(time)
}
public async stopAt(time: number): Promise<void> {
await this.audioContext.resume()

const node = this.audioSourceNode
const currentTime = this.audioContext.currentTime

const stop = (): void => {
this._isPlaying = false
node.stop(time)
if (this._isPlaying) {
this._isPlaying = false
node.stop(time)
}
}

if (time === currentTime) {
Expand All @@ -344,8 +289,8 @@ export abstract class BaseSound implements Connectable, Playable {
}
}

public stop(): void {
this.stopAt(this.audioContext.currentTime)
public async stop(): Promise<void> {
await this.stopAt(this.audioContext.currentTime)
}

public get isPlaying(): boolean {
Expand Down
66 changes: 28 additions & 38 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { a } from 'vitest/dist/chunks/suite.CcK46U-P.js'
import type { OscillatorOptsFilterValues } from './oscillator'
import { SampledNote } from './sampled-note'
import type { Connectable } from './interfaces/connectable'
Expand All @@ -24,49 +25,43 @@ const responses = new Map<string, Response>()

let audioContext: AudioContext

function throwIfContextNotExist(): void {
if (!audioContext) {
throw new Error('The audio context does not exist yet! You must call `initAudio()` in response to a user interaction before performing this action.')
}
}
async function unlockAudioContext(): Promise<void> {
if (audioContext.state !== 'suspended')
return

// async function unlockAudioContext(): Promise<void> {
// if (audioContext.state !== 'suspended')
// return
const b = document.body
const events = ['touchstart', 'touchend', 'mousedown', 'keydown']

// const b = document.body
// const events = ['touchstart', 'touchend', 'mousedown', 'keydown']

// async function unlock(): Promise<void> {
// await audioContext.resume().then(clean)
// }
async function unlock(): Promise<void> {
await audioContext.resume().then(clean)
}

// function clean(): void {
// events.forEach(e => b.removeEventListener(e, unlock))
// }
function clean(): void {
events.forEach(e => b.removeEventListener(e, unlock))
}

// events.forEach(e => b.addEventListener(e, unlock, false))
events.forEach(e => b.addEventListener(e, unlock, false))

// await audioContext.resume()
// }
await audioContext.resume()
}

let iosWorkaroundPerformed = false
export async function initAudio(useIosMuteWorkaround = true): Promise<void> {
if (!audioContext) {
audioContext = new AudioContext()
}

if (!audioContext) {
throw new Error('The audio context does not exist yet! You must call `initAudio()` in response to a user interaction before performing this action.')
}

// only run this workaround code once
if (useIosMuteWorkaround && !iosWorkaroundPerformed) {
unmuteIosAudio(audioContext)
iosWorkaroundPerformed = true
}

// TODO: without this, synth note hangs on first press?
// await unlockAudioContext()
if (audioContext.state === 'suspended') {
await audioContext.resume()
}
await unlockAudioContext()
}

export async function getAudioContext(): Promise<AudioContext> {
Expand Down Expand Up @@ -106,11 +101,11 @@ export async function createBeatTrack(urls: string[], opts?: BeatTrackOptions):
export async function createSampler(urls: string[], opts?: SamplerOptions): Promise<Sampler> {
const sounds = await Promise.all(urls.map(async url => load(url, 'sound') as Promise<Sound>))
await initAudio()
throwIfContextNotExist()
return new Sampler(sounds, opts)
}

export async function createOscillator(options?: OscillatorOpts): Promise<Oscillator> {
await initAudio()
return new Oscillator(audioContext, options)
}

Expand All @@ -119,7 +114,6 @@ export async function createFont(url: string): Promise<Font> {
const text = await response.text()
const audioData = mungeSoundFont(text)
await initAudio()
throwIfContextNotExist()
const keyValuePairs = await extractDecodedKeyValuePairs(audioContext, audioData)
const notes = createNoteObjectsForFont(audioContext, keyValuePairs)
return new Font(notes)
Expand Down Expand Up @@ -187,7 +181,6 @@ async function load(src: string, type: 'sound' | 'track' | 'sampler'): Promise<S
responses.set(src, response)

await initAudio()
throwIfContextNotExist()

const buffer = await audioContext.decodeAudioData(await response.clone().arrayBuffer())

Expand Down Expand Up @@ -225,26 +218,23 @@ export function preventEventDefaults(key: HTMLElement): void {
events.forEach(event => key.addEventListener(event, prevent))
}

// TODO: Mashing on keys causes notes to skip b/c stop is called before play and etc..
// solve that... debounce or something? the commented out stuff doesn't work
export function useInteractionMethods(key: HTMLElement, player: Player): void {
// console.log(player.audioSourceNode.frequency.value)

function play(): void {
// console.log(player.audioSourceNode.frequency.value, 'play')
export async function useInteractionMethods(key: HTMLElement, player: Player): Promise<void> {
async function play(): Promise<void> {
await initAudio()
player.play()
}

function stop(): void {
// console.log(player.audioSourceNode.frequency.value, 'stop')
async function stop(): Promise<void> {
await initAudio()
player.stop()
}

key.addEventListener('touchstart', play)
key.addEventListener('touchend', stop)
key.addEventListener('touchcancel', stop)
// key.addEventListener('touchcancel', stop)
key.addEventListener('mousedown', play)
key.addEventListener('mouseup', stop)
key.addEventListener('mouseleave', stop)
}

export {
Expand Down

0 comments on commit aa14040

Please sign in to comment.