Skip to content

Commit

Permalink
base sound class sound and oscillator extend from
Browse files Browse the repository at this point in the history
  • Loading branch information
sethbrasile committed Sep 23, 2024
1 parent 4392bc4 commit 1c5b5d5
Show file tree
Hide file tree
Showing 7 changed files with 291 additions and 347 deletions.
9 changes: 8 additions & 1 deletion src/app/pages/synthesis/drum-kit.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { codeBlock, htmlBlock } from '@app/utils'
import PermissionBanner from '../components/permission-banner'
import { setupSnareButton, setupSnareCrackButton, setupSnareMeatButton } from './snare-button'
import { setupHihatButton } from './hihat-button'
import { setupBassDropButton } from './bass-drop-button'
import { setupKickButton } from './kick-button'
import nav from './nav'
import { initAudio } from '@/index'

const playKick = `
import { initAudio, createOscillator, createWhiteNoise, LayeredSound } from 'ez-web-audio'
Expand Down Expand Up @@ -233,7 +235,9 @@ export function setupHihatButton(element: HTMLButtonElement) {
`

const Content = {
setup() {
async setup() {
PermissionBanner.setup()
await initAudio()
setupKickButton(document.querySelector<HTMLButtonElement>('#play_kick')!)
setupSnareButton(document.querySelector<HTMLButtonElement>('#play_snare')!)
setupHihatButton(document.querySelector<HTMLButtonElement>('#play_hihat')!)
Expand All @@ -244,6 +248,9 @@ const Content = {
html: `
${nav}
<h1>Synthesis</h1>
${PermissionBanner}
<div class="beat-pad">
<span role="label">Play Kick</span>
<span class="pad" role="button" id="play_kick"></span>
Expand Down
48 changes: 34 additions & 14 deletions src/app/pages/synthesis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ const Content = {
// We need to map each note to an <li>
const keys = new Map<Note, HTMLLIElement>()

await initAudio()

// for each note create an <li> and map it to
notes.forEach((note) => {
// create an li
Expand All @@ -32,22 +30,44 @@ const Content = {

// put the key/note pair into the keys Map
keys.set(note, key)

// add the setup listener so that each key can trigger audio context init
key.addEventListener('mousedown', setup)
key.addEventListener('touchstart', setup)
})

keys.forEach(async (key, note) => {
// Create the oscillator for this key and set its frequency from the corresponding note
const osc = await createOscillator({
frequency: note.frequency,
type: 'square',
// oscillators are pretty loud so turn it down
gain: 0.2,
async function setup(e: MouseEvent | TouchEvent): Promise<void> {
// First key is pressed...
// AudioContext setup
await initAudio()

keys.forEach(async (key, note) => {
// Create the oscillator for this key and set its frequency from the corresponding note
const osc = await createOscillator({
frequency: note.frequency,
type: 'square',
// oscillators are pretty loud so turn it down
gain: 0.2,
})

key.addEventListener('touchstart', () => osc.play())
key.addEventListener('touchend', () => osc.stop())
key.addEventListener('mousedown', () => osc.play())
key.addEventListener('mouseup', () => osc.stop())

key.removeEventListener('mousedown', setup)
key.removeEventListener('touchstart', setup)

// If this iteration corresponds to the actual key that was pressed
if (e.target === key) {
// then start playing the
osc.play()
}
})
}

key.addEventListener('touchstart', () => osc.play())
key.addEventListener('touchend', () => osc.stop())
key.addEventListener('mousedown', () => osc.play())
key.addEventListener('mouseup', () => osc.stop())
})
element.addEventListener('mousedown', setup)
element.addEventListener('touchstart', setup)
},
html: `
Expand Down
219 changes: 219 additions & 0 deletions src/base-sound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import type { TimeObject } from '@utils/create-time-object'
import type { Playable } from '@interfaces/playable'
import type { Connectable, Connection } from '@interfaces/connectable'
import type { ControlType, ParamController, RampType, RatioType } from '@controllers/base-param-controller'
import audioContextAwareTimeout from '@utils/timeout'

export abstract class BaseSound implements Connectable, Playable {
protected _isPlaying = false
protected gainNode: GainNode
protected pannerNode: StereoPannerNode
protected setTimeout: (fn: () => void, delayMillis: number) => number
protected startedPlayingAt: number = 0

public connections: Connection[] = []
public startOffset: number = 0

protected abstract controller: ParamController
protected abstract wireConnections(): void
protected abstract setup(): void

public abstract audioSourceNode: OscillatorNode | AudioBufferSourceNode
public abstract duration: TimeObject

constructor(protected audioContext: AudioContext, opts?: any) {
const gainNode = audioContext.createGain()
const pannerNode = audioContext.createStereoPanner()

this.gainNode = gainNode
this.pannerNode = pannerNode

if (opts?.setTimeout) {
this.setTimeout = opts.setTimeout
}
else {
this.setTimeout = audioContextAwareTimeout(audioContext).setTimeout
}
}

public addConnection(connection: Connection): void {
this.connections.push(connection)
this.wireConnections()
}

public removeConnection(name: string): void {
const connection = this.getConnection(name)
if (connection) {
const index = this.connections.indexOf(connection)
if (index > -1) {
this.connections.splice(index, 1)
this.wireConnections()
}
}
}

// Allows you to get any user created connection in the connections array
public getConnection(name: string): Connection | undefined {
return this.connections.find(c => c.name === name)
}

// Allows you to get node from any user created connection in the connections array
public getNodeFrom<T extends AudioNode | StereoPannerNode>(connectionName: string): T | undefined {
return this.getConnection(connectionName)?.audioNode as T | undefined
}

public update(type: ControlType): {
to: (value: number) => {
from: (method: RatioType) => void
}
} {
return this.controller.update(type)
}

public changePanTo(value: number): void {
this.controller.update('pan').to(value).from('ratio')
}

public changeGainTo(value: number): {
from: (method: RatioType) => void
} {
return this.controller.update('gain').to(value)
}

public onPlaySet(type: ControlType): {
to: (value: number) => {
at: (time: number) => void
endingAt: (time: number, rampType?: RampType) => void
}
} {
return this.controller.onPlaySet(type)
}

public onPlayRamp(type: ControlType, rampType?: RampType): {
from: (startValue: number) => {
to: (endValue: number) => {
in: (endTime: number) => void
}
}
} {
return this.controller.onPlayRamp(type, rampType)
}

public play(): void {
this.playAt(this.audioContext.currentTime)
}

public playIn(when: number): void {
this.playAt(this.audioContext.currentTime + when)
}

public playFor(duration: number): void {
this.playAt(this.audioContext.currentTime)
this.setTimeout(() => this.stop(), duration * 1000)
}

/**
* Starts playing the audio source after `playIn` seconds have elapsed, then
* stops the audio source `stopAfter` seconds after it started playing.
*
* @public
* @method playInAndStopAfter
*
* @param {number} playIn Number of seconds from "now" that the audio source
* should play.
*
* @param {number} stopAfter Number of seconds from when the audio source
* started playing that the audio source should be stopped.
*/
public playInAndStopAfter(playIn: number, stopAfter: number): void {
this.playIn(playIn)
this.stopIn(playIn + stopAfter)
}

/**
* The underlying method that backs all of the `play` methods. Plays the audio source at
* the specified moment in time. A "moment in time" is measured in seconds from the moment
* that the {{#crossLink "AudioContext"}}{{/crossLink}} was instantiated.
*
* @param {number} time The moment in time (in seconds, relative to the
* {{#crossLink "AudioContext"}}AudioContext's{{/crossLink}} "beginning of
* time") when the audio source should be played.
*
* @method playAt
*/
public async playAt(time: number): Promise<void> {
const { audioContext } = this
const { currentTime } = audioContext

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.duration.pojo.seconds * 1000)

if (time <= currentTime) {
this._isPlaying = true
}
else {
this.setTimeout(() => {
this._isPlaying = true
}, (time - currentTime) * 1000)
}
}

/**
* Stops the audio source after specified seconds have elapsed.
*
* @public
* @method stopIn
*
* @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)
}

/**
* The underlying method that backs all of the `stop` methods. Stops sound and
* set `isPlaying` to false at specified time.
*
* Functionally equivalent to the `stopAt` method.
*
* @method stopAt
*
* @param {number} stopAt The moment in time (in seconds, relative to the
* {{#crossLink "AudioContext"}}AudioContext's{{/crossLink}} "beginning of
* time") when the audio source should be stopped.
*/
public stopAt(stopAt: number): void {
const node = this.audioSourceNode
const currentTime = this.audioContext.currentTime

if (node) {
node.stop(stopAt)
}

if (stopAt === currentTime) {
this._isPlaying = false
}
else {
this.setTimeout(() => this._isPlaying = false, (stopAt - currentTime) * 1000)
}
}

public stop(): void {
this.audioSourceNode.stop()
this._isPlaying = false
}

public get isPlaying(): boolean {
return this._isPlaying
}

public get percentGain(): number {
return this.controller.gain * 100
}
}
2 changes: 1 addition & 1 deletion src/interfaces/connectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { ControlType } from '@controllers/base-param-controller'
export interface Connection { audioNode: AudioNode, name: string }
export interface Connectable {
connections: Connection[]
audioSourceNode: AudioNode
percentGain: number
audioSourceNode: AudioNode
update: (type: ControlType, value: number) => void
getNodeFrom: (name: string) => AudioNode | undefined
addConnection: (connection: Connection, name: string) => void
Expand Down
Loading

0 comments on commit 1c5b5d5

Please sign in to comment.