mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-10 02:54:15 +00:00
✨ Support for audio processor nodes
This commit is contained in:
parent
231d324e78
commit
f4f7e697e1
@ -1,39 +1,16 @@
|
||||
import React from "react"
|
||||
import Core from "evite/src/core"
|
||||
import { Observable } from "object-observer"
|
||||
import store from "store"
|
||||
import AudioPlayerStorage from "./storage"
|
||||
import { FastAverageColor } from "fast-average-color"
|
||||
|
||||
// import { createRealTimeBpmProcessor } from "realtime-bpm-analyzer"
|
||||
|
||||
import EmbbededMediaPlayer from "components/EmbbededMediaPlayer"
|
||||
import BackgroundMediaPlayer from "components/BackgroundMediaPlayer"
|
||||
|
||||
import { DOMWindow } from "components/RenderWindow"
|
||||
|
||||
class AudioPlayerStorage {
|
||||
static storeKey = "audioPlayer"
|
||||
|
||||
static get(key) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey)
|
||||
|
||||
if (data) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey) ?? {}
|
||||
|
||||
data[key] = value
|
||||
|
||||
store.set(AudioPlayerStorage.storeKey, data)
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
import GainProcessorNode from "./processors/gainNode"
|
||||
import CompressorProcessorNode from "./processors/compressorNode"
|
||||
|
||||
// TODO: Check if source playing is a stream. Also handle if it's a stream resuming after a pause will seek to the last position
|
||||
export default class Player extends Core {
|
||||
@ -52,7 +29,10 @@ export default class Player extends Core {
|
||||
|
||||
audioQueueHistory = []
|
||||
audioQueue = []
|
||||
audioProcessors = []
|
||||
audioProcessors = [
|
||||
new GainProcessorNode(this),
|
||||
new CompressorProcessorNode(this),
|
||||
]
|
||||
|
||||
currentAudioInstance = null
|
||||
|
||||
@ -83,6 +63,25 @@ export default class Player extends Core {
|
||||
volume: this.volume.bind(this),
|
||||
start: this.start.bind(this),
|
||||
startPlaylist: this.startPlaylist.bind(this),
|
||||
attachProcessor: function (name) {
|
||||
|
||||
}.bind(this),
|
||||
dettachProcessor: async function (name) {
|
||||
// find the processor by refName
|
||||
const processor = this.currentAudioInstance.attachedProcessors.find((_processor) => {
|
||||
return _processor.constructor.refName === name
|
||||
})
|
||||
|
||||
if (!processor) {
|
||||
throw new Error("Processor not found")
|
||||
}
|
||||
|
||||
if (typeof processor._detach !== "function") {
|
||||
throw new Error("Processor does not support detach")
|
||||
}
|
||||
|
||||
return this.currentAudioInstance = await processor._detach(this.currentAudioInstance)
|
||||
}.bind(this),
|
||||
playback: {
|
||||
mode: function (mode) {
|
||||
if (mode) {
|
||||
@ -157,6 +156,34 @@ export default class Player extends Core {
|
||||
}
|
||||
|
||||
async onInitialize() {
|
||||
// initialize all audio processors
|
||||
for await (const processor of this.audioProcessors) {
|
||||
console.log(`Initializing audio processor ${processor.constructor.name}`, processor)
|
||||
|
||||
if (typeof processor._init === "function") {
|
||||
try {
|
||||
await processor._init(this.audioContext)
|
||||
} catch (error) {
|
||||
console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// check if processor has exposed public methods
|
||||
if (processor.exposeToPublic) {
|
||||
Object.entries(processor.exposeToPublic).forEach(([key, value]) => {
|
||||
const refName = processor.constructor.refName
|
||||
|
||||
if (typeof this.public[refName] === "undefined") {
|
||||
// by default create a empty object
|
||||
this.public[refName] = {}
|
||||
}
|
||||
|
||||
this.public[refName][key] = value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Observable.observe(this.state, (changes) => {
|
||||
changes.forEach((change) => {
|
||||
if (change.type === "update") {
|
||||
@ -291,7 +318,7 @@ export default class Player extends Core {
|
||||
id: "mediaPlayer"
|
||||
})
|
||||
|
||||
this.currentDomWindow.render(<EmbbededMediaPlayer />)
|
||||
this.currentDomWindow.render(React.createElement(EmbbededMediaPlayer))
|
||||
}
|
||||
|
||||
detachPlayerComponent() {
|
||||
@ -413,7 +440,8 @@ export default class Player extends Core {
|
||||
track: null,
|
||||
gainNode: null,
|
||||
crossfadeInterval: null,
|
||||
crossfading: false
|
||||
crossfading: false,
|
||||
attachedProcessors: [],
|
||||
}
|
||||
|
||||
instanceObj.audioElement.signal = instanceObj.abortController.signal
|
||||
@ -540,37 +568,13 @@ export default class Player extends Core {
|
||||
|
||||
//this.enqueueLoadBuffer(instanceObj.audioElement)
|
||||
|
||||
//await this.instanciateRealtimeAnalyzerNode()
|
||||
|
||||
// create media element source as first node
|
||||
instanceObj.track = this.audioContext.createMediaElementSource(instanceObj.audioElement)
|
||||
|
||||
instanceObj.gainNode = this.audioContext.createGain()
|
||||
|
||||
instanceObj.gainNode.gain.value = this.state.audioVolume
|
||||
|
||||
const processorsList = [
|
||||
instanceObj.gainNode,
|
||||
...this.audioProcessors,
|
||||
]
|
||||
|
||||
let lastProcessor = null
|
||||
|
||||
processorsList.forEach((processor) => {
|
||||
if (lastProcessor) {
|
||||
lastProcessor.connect(processor)
|
||||
} else {
|
||||
instanceObj.track.connect(processor)
|
||||
}
|
||||
|
||||
lastProcessor = processor
|
||||
})
|
||||
|
||||
lastProcessor.connect(this.audioContext.destination)
|
||||
|
||||
return instanceObj
|
||||
}
|
||||
|
||||
play(instance, params = {}) {
|
||||
async play(instance, params = {}) {
|
||||
if (typeof instance === "number") {
|
||||
instance = this.audioQueue[instance]
|
||||
}
|
||||
@ -590,6 +594,16 @@ export default class Player extends Core {
|
||||
this.currentAudioInstance = instance
|
||||
this.state.currentAudioManifest = instance.manifest
|
||||
|
||||
for await (const [index, processor] of this.audioProcessors.entries()) {
|
||||
if (typeof processor._attach !== "function") {
|
||||
console.error(`Processor ${processor.constructor.refName} not support attach`)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
this.currentAudioInstance = await processor._attach(this.currentAudioInstance, index)
|
||||
}
|
||||
|
||||
// set time to 0
|
||||
this.currentAudioInstance.audioElement.currentTime = 0
|
||||
|
||||
@ -603,14 +617,6 @@ export default class Player extends Core {
|
||||
this.currentAudioInstance.gainNode.gain.value = this.state.audioVolume
|
||||
}
|
||||
|
||||
if (this.realtimeAnalyzerNode) {
|
||||
const filter = this.audioContext.createBiquadFilter()
|
||||
|
||||
filter.type = "lowpass"
|
||||
|
||||
this.currentAudioInstance.track.connect(filter).connect(this.realtimeAnalyzerNode)
|
||||
}
|
||||
|
||||
instance.audioElement.muted = this.state.audioMuted
|
||||
|
||||
instance.audioElement.load()
|
123
packages/app/src/cores/player/processorNode.js
Normal file
123
packages/app/src/cores/player/processorNode.js
Normal file
@ -0,0 +1,123 @@
|
||||
export default class ProcessorNode {
|
||||
constructor(PlayerCore) {
|
||||
if (!PlayerCore) {
|
||||
throw new Error("PlayerCore is required")
|
||||
}
|
||||
|
||||
this.PlayerCore = PlayerCore
|
||||
this.audioContext = PlayerCore.audioContext
|
||||
}
|
||||
|
||||
async _init() {
|
||||
// check if has init method
|
||||
if (typeof this.init === "function") {
|
||||
await this.init(this.audioContext)
|
||||
}
|
||||
|
||||
// check if has declared bus events
|
||||
if (typeof this.busEvents === "object") {
|
||||
Object.entries(this.busEvents).forEach((event, fn) => {
|
||||
app.eventBus.on(event, fn)
|
||||
})
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
_attach(instance, index) {
|
||||
if (typeof instance !== "object") {
|
||||
instance = this.PlayerCore.currentAudioInstance
|
||||
}
|
||||
|
||||
// check if has dependsOnSettings
|
||||
if (Array.isArray(this.constructor.dependsOnSettings)) {
|
||||
// check if the instance has the settings
|
||||
if (!this.constructor.dependsOnSettings.every((setting) => app.cores.settings.get(setting))) {
|
||||
console.warn(`Skipping attachment for [${this.constructor.refName ?? this.constructor.name}] node, cause is not passing the settings dependecy > ${this.constructor.dependsOnSettings.join(", ")}`)
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
// if index is not defined, attach to the last node
|
||||
if (!index) {
|
||||
index = instance.attachedProcessors.length
|
||||
}
|
||||
|
||||
const prevNode = instance.attachedProcessors[index - 1]
|
||||
const nextNode = instance.attachedProcessors[index + 1]
|
||||
|
||||
// first check if has prevNode and if is connected to the destination
|
||||
if (prevNode && prevNode.processor.numberOfOutputs > 0) {
|
||||
// if has outputs, disconnect from the next node
|
||||
prevNode.processor.disconnect()
|
||||
}
|
||||
|
||||
if (prevNode) {
|
||||
prevNode.processor.connect(this.processor)
|
||||
} else {
|
||||
// it means that this is the first node, so connect to the source
|
||||
instance.track.connect(this.processor)
|
||||
}
|
||||
|
||||
// now, connect the processor to the next node
|
||||
if (nextNode) {
|
||||
this.processor.connect(nextNode.processor)
|
||||
} else {
|
||||
// it means that this is the last node, so connect to the destination
|
||||
this.processor.connect(this.audioContext.destination)
|
||||
}
|
||||
|
||||
// add to the attachedProcessors
|
||||
instance.attachedProcessors.splice(index, 0, this)
|
||||
|
||||
if (typeof this.mutateInstance === "function") {
|
||||
instance = this.mutateInstance(instance)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
_detach(instance) {
|
||||
if (typeof instance !== "object") {
|
||||
instance = this.PlayerCore.currentAudioInstance
|
||||
}
|
||||
|
||||
// find index of the node within the attachedProcessors serching for matching refName
|
||||
const index = instance.attachedProcessors.findIndex((node) => {
|
||||
return node.constructor.refName === this.constructor.refName
|
||||
})
|
||||
|
||||
if (index === -1) {
|
||||
console.warn(`Node [${this.constructor.refName ?? this.constructor.name}] is not attached to the given instance`)
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// retrieve the previous and next nodes
|
||||
const prevNode = instance.attachedProcessors[index - 1]
|
||||
const nextNode = instance.attachedProcessors[index + 1]
|
||||
|
||||
// check if has previous node and if has outputs
|
||||
if (prevNode && prevNode.processor.numberOfOutputs > 0) {
|
||||
// if has outputs, disconnect from the previous node
|
||||
prevNode.processor.disconnect()
|
||||
}
|
||||
|
||||
// disconnect
|
||||
this.processor.disconnect()
|
||||
|
||||
// now, connect the previous node to the next node
|
||||
if (prevNode && nextNode) {
|
||||
prevNode.processor.connect(nextNode.processor)
|
||||
} else {
|
||||
// it means that this is the last node, so connect to the destination
|
||||
prevNode.processor.connect(this.audioContext.destination)
|
||||
}
|
||||
|
||||
// remove from the attachedProcessors
|
||||
instance.attachedProcessors.splice(index, 1)
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import AudioPlayerStorage from "../../storage"
|
||||
import ProcessorNode from "../../processorNode"
|
||||
|
||||
export default class CompressorProcessorNode extends ProcessorNode {
|
||||
static refName = "compressor"
|
||||
static dependsOnSettings = ["player.compressor"]
|
||||
static defaultCompressorValues = {
|
||||
threshold: -50,
|
||||
knee: 40,
|
||||
ratio: 12,
|
||||
attack: 0.003,
|
||||
release: 0.25,
|
||||
}
|
||||
|
||||
state = {
|
||||
compressorValues: AudioPlayerStorage.get("compressor") ?? CompressorProcessorNode.defaultCompressorValues,
|
||||
}
|
||||
|
||||
exposeToPublic = {
|
||||
modifyValues: function (values) {
|
||||
this.state.compressorValues = {
|
||||
...this.state.compressorValues,
|
||||
...values,
|
||||
}
|
||||
|
||||
AudioPlayerStorage.set("compressor", this.state.compressorValues)
|
||||
|
||||
this.applyValues()
|
||||
}.bind(this),
|
||||
resetDefaultValues: function () {
|
||||
this.exposeToPublic.modifyValues(CompressorProcessorNode.defaultCompressorValues)
|
||||
|
||||
return this.state.compressorValues
|
||||
}.bind(this),
|
||||
detach: this._detach.bind(this),
|
||||
attach: this._attach.bind(this),
|
||||
values: this.state.compressorValues,
|
||||
}
|
||||
|
||||
async init(AudioContext) {
|
||||
if (!AudioContext) {
|
||||
throw new Error("AudioContext is required")
|
||||
}
|
||||
|
||||
this.processor = AudioContext.createDynamicsCompressor()
|
||||
|
||||
this.applyValues()
|
||||
}
|
||||
|
||||
applyValues() {
|
||||
Object.keys(this.state.compressorValues).forEach((key) => {
|
||||
this.processor[key].value = this.state.compressorValues[key]
|
||||
})
|
||||
}
|
||||
}
|
37
packages/app/src/cores/player/processors/gainNode/index.js
Normal file
37
packages/app/src/cores/player/processors/gainNode/index.js
Normal file
@ -0,0 +1,37 @@
|
||||
import AudioPlayerStorage from "../../storage"
|
||||
import ProcessorNode from "../../processorNode"
|
||||
|
||||
export default class GainProcessorNode extends ProcessorNode {
|
||||
static refName = "gain"
|
||||
|
||||
static lock = true
|
||||
|
||||
static defaultValues = {
|
||||
volume: 0.3,
|
||||
}
|
||||
|
||||
state = {
|
||||
volume: AudioPlayerStorage.get("volume") ?? GainProcessorNode.defaultValues,
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.audioContext) {
|
||||
throw new Error("audioContext is required")
|
||||
}
|
||||
|
||||
this.processor = this.audioContext.createGain()
|
||||
|
||||
// set the default values
|
||||
this.processor.gain.value = parseFloat(this.state.volume)
|
||||
}
|
||||
|
||||
mutateInstance(instance) {
|
||||
if (!instance) {
|
||||
throw new Error("instance is required")
|
||||
}
|
||||
|
||||
instance.gainNode = this.processor
|
||||
|
||||
return instance
|
||||
}
|
||||
}
|
25
packages/app/src/cores/player/storage.js
Normal file
25
packages/app/src/cores/player/storage.js
Normal file
@ -0,0 +1,25 @@
|
||||
import store from "store"
|
||||
|
||||
export default class AudioPlayerStorage {
|
||||
static storeKey = "audioPlayer"
|
||||
|
||||
static get(key) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey)
|
||||
|
||||
if (data) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
static set(key, value) {
|
||||
const data = store.get(AudioPlayerStorage.storeKey) ?? {}
|
||||
|
||||
data[key] = value
|
||||
|
||||
store.set(AudioPlayerStorage.storeKey, data)
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user