Add audio system foundation with player architecture refactor

This commit is contained in:
SrGooglo 2025-04-24 06:04:45 +00:00
parent 29ae54fe03
commit 369803534b
11 changed files with 712 additions and 746 deletions

View File

@ -0,0 +1,123 @@
import { MediaPlayer } from "dashjs"
import PlayerProcessors from "./PlayerProcessors"
import AudioPlayerStorage from "../player.storage"
export default class AudioBase {
constructor(player) {
this.player = player
}
audio = new Audio()
context = null
demuxer = null
elementSource = null
processorsManager = new PlayerProcessors(this)
processors = {}
waitUpdateTimeout = null
initialize = async () => {
// create a audio context
this.context = new AudioContext({
sampleRate:
AudioPlayerStorage.get("sample_rate") ??
this.player.constructor.defaultSampleRate,
latencyHint: "playback",
})
// configure some settings for audio
this.audio.crossOrigin = "anonymous"
this.audio.preload = "metadata"
// listen all events
for (const [key, value] of Object.entries(this.audioEvents)) {
this.audio.addEventListener(key, value)
}
// setup demuxer for mpd
this.createDemuxer()
// create element source
this.elementSource = this.context.createMediaElementSource(this.audio)
// initialize audio processors
await this.processorsManager.initialize()
await this.processorsManager.attachAllNodes()
}
createDemuxer() {
this.demuxer = MediaPlayer().create()
this.demuxer.updateSettings({
streaming: {
buffer: {
resetSourceBuffersForTrackSwitch: true,
},
},
})
this.demuxer.initialize(this.audio, null, false)
}
flush() {
this.audio.pause()
this.audio.src = null
this.audio.currentTime = 0
this.demuxer.destroy()
this.createDemuxer()
}
audioEvents = {
ended: () => {
this.player.next()
},
loadeddata: () => {
this.player.state.loading = false
},
loadedmetadata: () => {
if (this.audio.duration === Infinity) {
this.player.state.live = true
} else {
this.player.state.live = false
}
},
play: () => {
this.player.state.playback_status = "playing"
},
playing: () => {
this.player.state.loading = false
this.player.state.playback_status = "playing"
if (typeof this.waitUpdateTimeout !== "undefined") {
clearTimeout(this.waitUpdateTimeout)
this.waitUpdateTimeout = null
}
},
pause: () => {
this.player.state.playback_status = "paused"
},
durationchange: () => {
this.player.eventBus.emit(
`player.durationchange`,
this.audio.duration,
)
},
waiting: () => {
if (this.waitUpdateTimeout) {
clearTimeout(this.waitUpdateTimeout)
this.waitUpdateTimeout = null
}
// if takes more than 150ms to load, update loading state
this.waitUpdateTimeout = setTimeout(() => {
this.player.state.loading = true
}, 150)
},
seeked: () => {
this.player.eventBus.emit(`player.seeked`, this.audio.currentTime)
},
}
}

View File

@ -0,0 +1,56 @@
export default class MediaSession {
constructor(player) {
this.player = player
}
async initialize() {
for (const [action, handler] of this.handlers) {
navigator.mediaSession.setActionHandler(action, handler)
}
}
handlers = [
[
"play",
() => {
console.log("media session play event", "play")
this.player.resumePlayback()
},
],
[
"pause",
() => {
console.log("media session pause event", "pause")
this.player.pausePlayback()
},
],
[
"seekto",
(seek) => {
console.log("media session seek event", seek)
this.player.seek(seek.seekTime)
},
],
]
update = (manifest) => {
navigator.mediaSession.metadata = new MediaMetadata({
title: manifest.title,
artist: manifest.artist,
album: manifest.album,
artwork: [
{
src: manifest.cover,
},
],
})
}
flush = () => {
navigator.mediaSession.metadata = null
}
updateIsPlaying = (isPlaying) => {
navigator.mediaSession.playbackState = isPlaying ? "playing" : "paused"
}
}

View File

@ -1,83 +1,96 @@
import defaultAudioProccessors from "../processors"
export default class PlayerProcessors {
constructor(player) {
this.player = player
}
constructor(base) {
this.base = base
}
processors = []
nodes = []
attached = []
public = {}
public = {}
async initialize() {
// if already exists audio processors, destroy all before create new
if (this.processors.length > 0) {
this.player.console.log("Destroying audio processors")
async initialize() {
// if already exists audio processors, destroy all before create new
if (this.nodes.length > 0) {
this.base.player.console.log("Destroying audio processors")
this.processors.forEach((processor) => {
this.player.console.log(`Destroying audio processor ${processor.constructor.name}`, processor)
processor._destroy()
})
this.nodes.forEach((node) => {
this.base.player.console.log(
`Destroying audio processor node ${node.constructor.name}`,
node,
)
node._destroy()
})
this.processors = []
}
this.nodes = []
}
// instanciate default audio processors
for await (const defaultProccessor of defaultAudioProccessors) {
this.processors.push(new defaultProccessor(this.player))
}
// instanciate default audio processors
for await (const defaultProccessor of defaultAudioProccessors) {
this.nodes.push(new defaultProccessor(this))
}
// initialize audio processors
for await (const processor of this.processors) {
if (typeof processor._init === "function") {
try {
await processor._init(this.player.audioContext)
} catch (error) {
this.player.console.error(`Failed to initialize audio processor ${processor.constructor.name} >`, error)
continue
}
}
// initialize audio processors
for await (const node of this.nodes) {
if (typeof node._init === "function") {
try {
await node._init()
} catch (error) {
this.base.player.console.error(
`Failed to initialize audio processor node ${node.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
// check if processor has exposed public methods
if (node.exposeToPublic) {
Object.entries(node.exposeToPublic).forEach(([key, value]) => {
const refName = node.constructor.refName
if (typeof this.player.public[refName] === "undefined") {
// by default create a empty object
this.player.public[refName] = {}
}
if (typeof this.base.processors[refName] === "undefined") {
// by default create a empty object
this.base.processors[refName] = {}
}
this.player.public[refName][key] = value
})
}
}
}
this.base.processors[refName][key] = value
})
}
}
}
async attachProcessorsToInstance(instance) {
for await (const [index, processor] of this.processors.entries()) {
if (processor.constructor.node_bypass === true) {
instance.contextElement.connect(processor.processor)
attachAllNodes = async () => {
for await (const [index, node] of this.nodes.entries()) {
if (node.constructor.node_bypass === true) {
this.base.context.elementSource.connect(node.processor)
processor.processor.connect(this.player.audioContext.destination)
node.processor.connect(this.base.context.destination)
continue
}
continue
}
if (typeof processor._attach !== "function") {
this.player.console.error(`Processor ${processor.constructor.refName} not support attach`)
if (typeof node._attach !== "function") {
this.base.console.error(
`Processor ${node.constructor.refName} not support attach`,
)
continue
}
continue
}
instance = await processor._attach(instance, index)
}
await node._attach(index)
}
const lastProcessor = instance.attachedProcessors[instance.attachedProcessors.length - 1].processor
const lastProcessor = this.attached[this.attached.length - 1].processor
// now attach to destination
lastProcessor.connect(this.player.audioContext.destination)
// now attach to destination
lastProcessor.connect(this.base.context.destination)
}
return instance
}
detachAllNodes = async () => {
for (const [index, node] of this.attached.entries()) {
await node._detach()
}
}
}

View File

@ -1,206 +1,131 @@
import TrackManifest from "./TrackManifest"
import { MediaPlayer } from "dashjs"
export default class TrackInstance {
constructor(player, manifest) {
constructor(manifest, player) {
if (typeof manifest === "undefined") {
throw new Error("Manifest is required")
}
if (!player) {
throw new Error("Player core is required")
}
if (typeof manifest === "undefined") {
throw new Error("Manifest is required")
if (!(manifest instanceof TrackManifest)) {
manifest = new TrackManifest(manifest, player)
}
if (!manifest.source) {
throw new Error("Manifest must have a source")
}
this.player = player
this.manifest = manifest
this.id = this.manifest.id ?? this.manifest._id
return this
}
_initialized = false
play = async (params = {}) => {
const startTime = performance.now()
audio = null
contextElement = null
abortController = new AbortController()
attachedProcessors = []
waitUpdateTimeout = null
mediaEvents = {
ended: () => {
this.player.next()
},
loadeddata: () => {
this.player.state.loading = false
},
loadedmetadata: () => {
if (this.audio.duration === Infinity) {
this.player.state.live = true
} else {
this.player.state.live = false
}
},
play: () => {
this.player.state.playback_status = "playing"
},
playing: () => {
this.player.state.loading = false
this.player.state.playback_status = "playing"
if (typeof this.waitUpdateTimeout !== "undefined") {
clearTimeout(this.waitUpdateTimeout)
this.waitUpdateTimeout = null
}
},
pause: () => {
this.player.state.playback_status = "paused"
},
durationchange: () => {
this.player.eventBus.emit(
`player.durationchange`,
this.audio.duration,
)
},
waiting: () => {
if (this.waitUpdateTimeout) {
clearTimeout(this.waitUpdateTimeout)
this.waitUpdateTimeout = null
}
// if takes more than 150ms to load, update loading state
this.waitUpdateTimeout = setTimeout(() => {
this.player.state.loading = true
}, 150)
},
seeked: () => {
this.player.eventBus.emit(`player.seeked`, this.audio.currentTime)
},
}
initialize = async () => {
this.manifest = await this.resolveManifest()
this.audio = new Audio()
this.audio.signal = this.abortController.signal
this.audio.crossOrigin = "anonymous"
this.audio.preload = "metadata"
// support for dash audio streaming
if (this.manifest.source.endsWith(".mpd")) {
this.muxerPlayer = MediaPlayer().create()
this.muxerPlayer.updateSettings({
streaming: {
buffer: {
resetSourceBuffersForTrackSwitch: true,
useChangeTypeForTrackSwitch: false,
},
},
})
this.muxerPlayer.initialize(this.audio, null, false)
this.muxerPlayer.attachSource(this.manifest.source)
if (!this.manifest.source.endsWith(".mpd")) {
this.player.base.demuxer.destroy()
this.player.base.audio.src = this.manifest.source
} else {
this.audio.src = this.manifest.source
}
for (const [key, value] of Object.entries(this.mediaEvents)) {
this.audio.addEventListener(key, value)
}
this.contextElement = this.player.audioContext.createMediaElementSource(
this.audio,
)
this._initialized = true
return this
}
stop = () => {
if (this.audio) {
this.audio.pause()
}
if (this.muxerPlayer) {
this.muxerPlayer.destroy()
}
const lastProcessor =
this.attachedProcessors[this.attachedProcessors.length - 1]
if (lastProcessor) {
this.attachedProcessors[
this.attachedProcessors.length - 1
]._destroy(this)
}
this.attachedProcessors = []
}
resolveManifest = async () => {
if (typeof this.manifest === "string") {
this.manifest = {
src: this.manifest,
}
}
this.manifest = new TrackManifest(this.manifest, {
serviceProviders: this.player.serviceProviders,
})
if (this.manifest.service) {
if (!this.player.serviceProviders.has(this.manifest.service)) {
throw new Error(
`Service ${this.manifest.service} is not supported`,
)
if (!this.player.base.demuxer) {
this.player.base.createDemuxer()
}
// try to resolve source file
if (!this.manifest.source) {
console.log("Resolving manifest cause no source defined")
this.manifest = await this.player.serviceProviders.resolve(
this.manifest.service,
this.manifest,
)
console.log("Manifest resolved", this.manifest)
}
}
if (!this.manifest.source) {
throw new Error("Manifest `source` is required")
}
// set empty metadata if not provided
if (!this.manifest.metadata) {
this.manifest.metadata = {}
}
// auto name if a title is not provided
if (!this.manifest.metadata.title) {
this.manifest.metadata.title = this.manifest.source.split("/").pop()
}
// process overrides
const override = await this.manifest.serviceOperations.fetchOverride()
if (override) {
console.log(
`Override found for track ${this.manifest._id}`,
override,
await this.player.base.demuxer.attachSource(
`${this.manifest.source}?t=${Date.now()}`,
)
this.manifest.overrides = override
}
return this.manifest
this.player.base.audio.currentTime = params.time ?? 0
if (this.player.base.audio.paused) {
await this.player.base.audio.play()
}
// reset audio volume and gain
this.player.base.audio.volume = 1
this.player.base.processors.gain.set(this.player.state.volume)
const endTime = performance.now()
this._loadMs = endTime - startTime
console.log(`[INSTANCE] Playing >`, this)
}
pause = async () => {
console.log("[INSTANCE] Pausing >", this)
this.player.base.audio.pause()
}
resume = async () => {
console.log("[INSTANCE] Resuming >", this)
this.player.base.audio.play()
}
// resolveManifest = async () => {
// if (typeof this.manifest === "string") {
// this.manifest = {
// src: this.manifest,
// }
// }
// this.manifest = new TrackManifest(this.manifest, {
// serviceProviders: this.player.serviceProviders,
// })
// if (this.manifest.service) {
// if (!this.player.serviceProviders.has(this.manifest.service)) {
// throw new Error(
// `Service ${this.manifest.service} is not supported`,
// )
// }
// // try to resolve source file
// if (!this.manifest.source) {
// console.log("Resolving manifest cause no source defined")
// this.manifest = await this.player.serviceProviders.resolve(
// this.manifest.service,
// this.manifest,
// )
// console.log("Manifest resolved", this.manifest)
// }
// }
// if (!this.manifest.source) {
// throw new Error("Manifest `source` is required")
// }
// // set empty metadata if not provided
// if (!this.manifest.metadata) {
// this.manifest.metadata = {}
// }
// // auto name if a title is not provided
// if (!this.manifest.metadata.title) {
// this.manifest.metadata.title = this.manifest.source.split("/").pop()
// }
// // process overrides
// const override = await this.manifest.serviceOperations.fetchOverride()
// if (override) {
// console.log(
// `Override found for track ${this.manifest._id}`,
// override,
// )
// this.manifest.overrides = override
// }
// return this.manifest
// }
}

View File

@ -1,4 +1,4 @@
import jsmediatags from "jsmediatags/dist/jsmediatags.min.js"
import { parseBlob } from "music-metadata"
import { FastAverageColor } from "fast-average-color"
export default class TrackManifest {
@ -33,13 +33,6 @@ export default class TrackManifest {
this.artist = params.artist
}
if (
typeof params.artists !== "undefined" ||
Array.isArray(params.artists)
) {
this.artistStr = params.artists.join(", ")
}
if (typeof params.source !== "undefined") {
this.source = params.source
}
@ -48,8 +41,8 @@ export default class TrackManifest {
this.metadata = params.metadata
}
if (typeof params.lyrics_enabled !== "undefined") {
this.lyrics_enabled = params.lyrics_enabled
if (typeof params.liked !== "undefined") {
this.liked = params.liked
}
return this
@ -64,59 +57,54 @@ export default class TrackManifest {
album = "Unknown"
artist = "Unknown"
source = null
metadata = null
metadata = {}
// set default service to default
service = "default"
// Extended from db
lyrics_enabled = false
liked = null
async initialize() {
if (this.params.file) {
this.metadata = await this.analyzeMetadata(
this.params.file.originFileObj,
)
this.metadata.format = this.metadata.type.toUpperCase()
if (this.metadata.tags) {
if (this.metadata.tags.title) {
this.title = this.metadata.tags.title
}
if (this.metadata.tags.artist) {
this.artist = this.metadata.tags.artist
}
if (this.metadata.tags.album) {
this.album = this.metadata.tags.album
}
if (this.metadata.tags.picture) {
this.cover = app.cores.remoteStorage.binaryArrayToFile(
this.metadata.tags.picture,
"cover",
)
const coverUpload =
await app.cores.remoteStorage.uploadFile(this.cover)
this.cover = coverUpload.url
delete this.metadata.tags.picture
}
this.handleChanges({
cover: this.cover,
title: this.title,
artist: this.artist,
album: this.album,
})
}
if (!this.params.file) {
return this
}
const analyzedMetadata = await parseBlob(
this.params.file.originFileObj,
{
skipPostHeaders: true,
},
).catch(() => ({}))
this.metadata.format = analyzedMetadata.format.codec
if (analyzedMetadata.common) {
this.title = analyzedMetadata.common.title ?? this.title
this.artist = analyzedMetadata.common.artist ?? this.artist
this.album = analyzedMetadata.common.album ?? this.album
}
if (analyzedMetadata.common.picture) {
const cover = analyzedMetadata.common.picture[0]
const coverFile = new File([cover.data], "cover", {
type: cover.format,
})
const coverUpload =
await app.cores.remoteStorage.uploadFile(coverFile)
this.cover = coverUpload.url
}
this.handleChanges({
cover: this.cover,
title: this.title,
artist: this.artist,
album: this.album,
})
return this
}
@ -126,19 +114,6 @@ export default class TrackManifest {
}
}
analyzeMetadata = async (file) => {
return new Promise((resolve, reject) => {
jsmediatags.read(file, {
onSuccess: (data) => {
return resolve(data)
},
onError: (error) => {
return reject(error)
},
})
})
}
analyzeCoverColor = async () => {
const fac = new FastAverageColor()
@ -169,8 +144,6 @@ export default class TrackManifest {
this,
)
console.log(this.overrides)
if (this.overrides) {
return {
...result,
@ -210,6 +183,7 @@ export default class TrackManifest {
return {
_id: this._id,
uid: this.uid,
cover: this.cover,
title: this.title,
album: this.album,
artist: this.artist,

View File

@ -3,11 +3,11 @@ import { Core } from "@ragestudio/vessel"
import ActivityEvent from "@classes/ActivityEvent"
import QueueManager from "@classes/QueueManager"
import TrackInstance from "./classes/TrackInstance"
//import MediaSession from "./classes/MediaSession"
import MediaSession from "./classes/MediaSession"
import ServiceProviders from "./classes/Services"
import PlayerState from "./classes/PlayerState"
import PlayerUI from "./classes/PlayerUI"
import PlayerProcessors from "./classes/PlayerProcessors"
import AudioBase from "./classes/AudioBase"
import setSampleRate from "./helpers/setSampleRate"
@ -22,27 +22,18 @@ export default class Player extends Core {
// player config
static defaultSampleRate = 48000
static gradualFadeMs = 150
static maxManifestPrecompute = 3
state = new PlayerState(this)
ui = new PlayerUI(this)
serviceProviders = new ServiceProviders()
//nativeControls = new MediaSession()
audioContext = new AudioContext({
sampleRate:
AudioPlayerStorage.get("sample_rate") ?? Player.defaultSampleRate,
latencyHint: "playback",
})
nativeControls = new MediaSession(this)
audioProcessors = new PlayerProcessors(this)
base = new AudioBase(this)
queue = new QueueManager({
loadFunction: this.createInstance,
})
currentTrackInstance = null
public = {
start: this.start,
close: this.close,
@ -74,10 +65,11 @@ export default class Player extends Core {
eventBus: () => {
return this.eventBus
},
base: () => {
return this.base
},
state: this.state,
ui: this.ui.public,
audioContext: this.audioContext,
gradualFadeMs: Player.gradualFadeMs,
}
async afterInitialize() {
@ -85,8 +77,8 @@ export default class Player extends Core {
this.state.volume = 1
}
//await this.nativeControls.initialize()
await this.audioProcessors.initialize()
await this.nativeControls.initialize()
await this.base.initialize()
}
//
@ -100,10 +92,6 @@ export default class Player extends Core {
}
}
async createInstance(manifest) {
return new TrackInstance(this, manifest)
}
//
// Playback methods
//
@ -112,46 +100,21 @@ export default class Player extends Core {
throw new Error("Audio instance is required")
}
this.console.log("Initializing instance", instance)
// resume audio context if needed
if (this.audioContext.state === "suspended") {
this.audioContext.resume()
if (this.base.context.state === "suspended") {
this.base.context.resume()
}
// initialize instance if is not
if (this.queue.currentItem._initialized === false) {
this.queue.currentItem = await instance.initialize()
}
this.console.log("Instance", this.queue.currentItem)
// update manifest
this.state.track_manifest = this.queue.currentItem.manifest
// attach processors
this.queue.currentItem =
await this.audioProcessors.attachProcessorsToInstance(
this.queue.currentItem,
)
// set audio properties
this.queue.currentItem.audio.currentTime = params.time ?? 0
this.queue.currentItem.audio.muted = this.state.muted
this.queue.currentItem.audio.loop =
this.state.playback_mode === "repeat"
this.queue.currentItem.gainNode.gain.value = Math.pow(
this.state.volume,
2,
)
this.state.track_manifest =
this.queue.currentItem.manifest.toSeriableObject()
// play
await this.queue.currentItem.audio.play()
this.console.log(`Playing track >`, this.queue.currentItem)
//await this.queue.currentItem.audio.play()
await this.queue.currentItem.play(params)
// update native controls
//this.nativeControls.update(this.queue.currentItem.manifest)
this.nativeControls.update(this.queue.currentItem.manifest)
return this.queue.currentItem
}
@ -160,10 +123,10 @@ export default class Player extends Core {
this.ui.attachPlayerComponent()
if (this.queue.currentItem) {
await this.queue.currentItem.stop()
await this.queue.currentItem.pause()
}
await this.abortPreloads()
//await this.abortPreloads()
await this.queue.flush()
this.state.loading = true
@ -187,8 +150,8 @@ export default class Player extends Core {
playlist = await this.serviceProviders.resolveMany(playlist)
}
for await (const [index, _manifest] of playlist.entries()) {
let instance = await this.createInstance(_manifest)
for await (let [index, _manifest] of playlist.entries()) {
let instance = new TrackInstance(_manifest, this)
this.queue.add(instance)
}
@ -229,10 +192,6 @@ export default class Player extends Core {
}
next() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
//const isRandom = this.state.playback_mode === "shuffle"
const item = this.queue.next()
@ -244,10 +203,6 @@ export default class Player extends Core {
}
previous() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
const item = this.queue.previous()
return this.play(item)
@ -275,18 +230,14 @@ export default class Player extends Core {
return null
}
// set gain exponentially
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
0.0001,
this.audioContext.currentTime + Player.gradualFadeMs / 1000,
)
this.base.processors.gain.fade(0)
setTimeout(() => {
this.queue.currentItem.audio.pause()
this.queue.currentItem.pause()
resolve()
}, Player.gradualFadeMs)
//this.nativeControls.updateIsPlaying(false)
this.nativeControls.updateIsPlaying(false)
})
}
@ -302,19 +253,12 @@ export default class Player extends Core {
}
// ensure audio elemeto starts from 0 volume
this.queue.currentItem.gainNode.gain.value = 0.0001
this.queue.currentItem.audio.play().then(() => {
this.queue.currentItem.resume().then(() => {
resolve()
})
this.base.processors.gain.fade(this.state.volume)
// set gain exponentially
this.queue.currentItem.gainNode.gain.linearRampToValueAtTime(
Math.pow(this.state.volume, 2),
this.audioContext.currentTime + Player.gradualFadeMs / 1000,
)
//this.nativeControls.updateIsPlaying(true)
this.nativeControls.updateIsPlaying(true)
})
}
@ -325,10 +269,7 @@ export default class Player extends Core {
this.state.playback_mode = mode
if (this.queue.currentItem) {
this.queue.currentItem.audio.loop =
this.state.playback_mode === "repeat"
}
this.base.audio.loop = this.state.playback_mode === "repeat"
AudioPlayerStorage.set("mode", mode)
@ -336,22 +277,15 @@ export default class Player extends Core {
}
stopPlayback() {
if (this.queue.currentItem) {
this.queue.currentItem.stop()
}
this.base.flush()
this.queue.flush()
this.abortPreloads()
this.state.playback_status = "stopped"
this.state.track_manifest = null
this.queue.currentItem = null
this.track_next_instances = []
this.track_prev_instances = []
//this.nativeControls.destroy()
//this.abortPreloads()
this.nativeControls.flush()
}
//
@ -369,7 +303,7 @@ export default class Player extends Core {
if (typeof to === "boolean") {
this.state.muted = to
this.queue.currentItem.audio.muted = to
this.base.audio.muted = to
}
return this.state.muted
@ -395,65 +329,42 @@ export default class Player extends Core {
volume = 0
}
this.state.volume = volume
AudioPlayerStorage.set("volume", volume)
if (this.queue.currentItem) {
if (this.queue.currentItem.gainNode) {
this.queue.currentItem.gainNode.gain.value = Math.pow(
this.state.volume,
2,
)
}
}
this.state.volume = volume
this.base.processors.gain.set(volume)
return this.state.volume
}
seek(time) {
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
if (!this.base.audio) {
return false
}
// if time not provided, return current time
if (typeof time === "undefined") {
return this.queue.currentItem.audio.currentTime
return this.base.audio.currentTime
}
// if time is provided, seek to that time
if (typeof time === "number") {
this.console.log(
`Seeking to ${time} | Duration: ${this.queue.currentItem.audio.duration}`,
`Seeking to ${time} | Duration: ${this.base.audio.duration}`,
)
this.queue.currentItem.audio.currentTime = time
this.base.audio.currentTime = time
return time
}
}
duration() {
if (!this.queue.currentItem || !this.queue.currentItem.audio) {
if (!this.base.audio) {
return false
}
return this.queue.currentItem.audio.duration
}
loop(to) {
if (typeof to !== "boolean") {
this.console.warn("Loop must be a boolean")
return false
}
this.state.loop = to ?? !this.state.loop
if (this.queue.currentItem.audio) {
this.queue.currentItem.audio.loop = this.state.loop
}
return this.state.loop
return this.base.audio.duration
}
close() {

View File

@ -2,44 +2,40 @@ import ProcessorNode from "../node"
import Presets from "../../classes/Presets"
export default class CompressorProcessorNode extends ProcessorNode {
constructor(props) {
super(props)
constructor(props) {
super(props)
this.presets = new Presets({
storage_key: "compressor",
defaultPresetValue: {
threshold: -50,
knee: 40,
ratio: 12,
attack: 0.003,
release: 0.25,
},
onApplyValues: this.applyValues.bind(this),
})
this.presets = new Presets({
storage_key: "compressor",
defaultPresetValue: {
threshold: -50,
knee: 40,
ratio: 12,
attack: 0.003,
release: 0.25,
},
onApplyValues: this.applyValues.bind(this),
})
this.exposeToPublic = {
presets: this.presets,
detach: this._detach,
attach: this._attach,
}
}
this.exposeToPublic = {
presets: this.presets,
detach: this._detach,
attach: this._attach,
}
}
static refName = "compressor"
static dependsOnSettings = ["player.compressor"]
static refName = "compressor"
static dependsOnSettings = ["player.compressor"]
async init(AudioContext) {
if (!AudioContext) {
throw new Error("AudioContext is required")
}
async init() {
this.processor = this.audioContext.createDynamicsCompressor()
this.processor = AudioContext.createDynamicsCompressor()
this.applyValues()
}
this.applyValues()
}
applyValues() {
Object.keys(this.presets.currentPresetValues).forEach((key) => {
this.processor[key].value = this.presets.currentPresetValues[key]
})
}
applyValues() {
Object.keys(this.presets.currentPresetValues).forEach((key) => {
this.processor[key].value = this.presets.currentPresetValues[key]
})
}
}

View File

@ -2,93 +2,98 @@ import ProcessorNode from "../node"
import Presets from "../../classes/Presets"
export default class EqProcessorNode extends ProcessorNode {
constructor(props) {
super(props)
constructor(props) {
super(props)
this.presets = new Presets({
storage_key: "eq",
defaultPresetValue: {
32: 0,
64: 0,
125: 0,
250: 0,
500: 0,
1000: 0,
2000: 0,
4000: 0,
8000: 0,
16000: 0,
},
onApplyValues: this.applyValues.bind(this),
})
this.presets = new Presets({
storage_key: "eq",
defaultPresetValue: {
32: 0,
64: 0,
125: 0,
250: 0,
500: 0,
1000: 0,
2000: 0,
4000: 0,
8000: 0,
16000: 0,
},
onApplyValues: this.applyValues.bind(this),
})
this.exposeToPublic = {
presets: this.presets,
}
}
this.exposeToPublic = {
presets: this.presets,
}
}
static refName = "eq"
static lock = true
static refName = "eq"
applyValues() {
// apply to current instance
this.processor.eqNodes.forEach((processor) => {
const gainValue = this.presets.currentPresetValues[processor.frequency.value]
applyValues() {
// apply to current instance
this.processor.eqNodes.forEach((processor) => {
const gainValue =
this.presets.currentPresetValues[processor.frequency.value]
if (processor.gain.value !== gainValue) {
console.debug(`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`)
processor.gain.value = gainValue
}
})
}
if (processor.gain.value !== gainValue) {
console.debug(
`[EQ] Applying values to ${processor.frequency.value} Hz frequency with gain ${gainValue}`,
)
processor.gain.value = gainValue
}
})
}
async init() {
if (!this.audioContext) {
throw new Error("audioContext is required")
}
async init() {
if (!this.audioContext) {
throw new Error("audioContext is required")
}
this.processor = this.audioContext.createGain()
this.processor = this.audioContext.createGain()
this.processor.gain.value = 1
this.processor.gain.value = 1
this.processor.eqNodes = []
this.processor.eqNodes = []
const values = Object.entries(this.presets.currentPresetValues).map((entry) => {
return {
freq: parseFloat(entry[0]),
gain: parseFloat(entry[1]),
}
})
const values = Object.entries(this.presets.currentPresetValues).map(
(entry) => {
return {
freq: parseFloat(entry[0]),
gain: parseFloat(entry[1]),
}
},
)
values.forEach((eqValue, index) => {
// chekc if freq and gain is valid
if (isNaN(eqValue.freq)) {
eqValue.freq = 0
}
if (isNaN(eqValue.gain)) {
eqValue.gain = 0
}
values.forEach((eqValue, index) => {
// chekc if freq and gain is valid
if (isNaN(eqValue.freq)) {
eqValue.freq = 0
}
if (isNaN(eqValue.gain)) {
eqValue.gain = 0
}
this.processor.eqNodes[index] = this.audioContext.createBiquadFilter()
this.processor.eqNodes[index].type = "peaking"
this.processor.eqNodes[index].frequency.value = eqValue.freq
this.processor.eqNodes[index].gain.value = eqValue.gain
})
this.processor.eqNodes[index] =
this.audioContext.createBiquadFilter()
this.processor.eqNodes[index].type = "peaking"
this.processor.eqNodes[index].frequency.value = eqValue.freq
this.processor.eqNodes[index].gain.value = eqValue.gain
})
// connect nodes
for await (let [index, eqNode] of this.processor.eqNodes.entries()) {
const nextNode = this.processor.eqNodes[index + 1]
// connect nodes
for await (let [index, eqNode] of this.processor.eqNodes.entries()) {
const nextNode = this.processor.eqNodes[index + 1]
if (index === 0) {
this.processor.connect(eqNode)
}
if (index === 0) {
this.processor.connect(eqNode)
}
if (nextNode) {
eqNode.connect(nextNode)
}
}
if (nextNode) {
eqNode.connect(nextNode)
}
}
// set last processor for processor node can properly connect to the next node
this.processor._last = this.processor.eqNodes.at(-1)
}
// set last processor for processor node can properly connect to the next node
this.processor._last = this.processor.eqNodes.at(-1)
}
}

View File

@ -1,60 +1,49 @@
import AudioPlayerStorage from "../../player.storage"
import ProcessorNode from "../node"
export default class GainProcessorNode extends ProcessorNode {
static refName = "gain"
static refName = "gain"
static gradualFadeMs = 150
static lock = true
exposeToPublic = {
set: this.setGain.bind(this),
linearRampToValueAtTime: this.linearRampToValueAtTime.bind(this),
fade: this.fade.bind(this),
}
static defaultValues = {
gain: 1,
}
setGain(gain) {
gain = this.processGainValue(gain)
state = {
gain: AudioPlayerStorage.get("gain") ?? GainProcessorNode.defaultValues.gain,
}
return (this.processor.gain.value = gain)
}
exposeToPublic = {
modifyValues: function (values) {
this.state = {
...this.state,
...values,
}
linearRampToValueAtTime(gain, time) {
gain = this.processGainValue(gain)
return this.processor.gain.linearRampToValueAtTime(gain, time)
}
AudioPlayerStorage.set("gain", this.state.gain)
fade(gain) {
if (gain <= 0) {
gain = 0.0001
} else {
gain = this.processGainValue(gain)
}
this.applyValues()
}.bind(this),
resetDefaultValues: function () {
this.exposeToPublic.modifyValues(GainProcessorNode.defaultValues)
const currentTime = this.audioContext.currentTime
const fadeTime = currentTime + this.constructor.gradualFadeMs / 1000
return this.state
}.bind(this),
values: () => this.state,
}
this.processor.gain.linearRampToValueAtTime(gain, fadeTime)
}
applyValues() {
// apply to current instance
this.processor.gain.value = app.cores.player.state.volume * this.state.gain
}
processGainValue(gain) {
return Math.pow(gain, 2)
}
async init() {
if (!this.audioContext) {
throw new Error("audioContext is required")
}
async init() {
if (!this.audioContext) {
throw new Error("audioContext is required")
}
this.processor = this.audioContext.createGain()
this.applyValues()
}
mutateInstance(instance) {
if (!instance) {
throw new Error("instance is required")
}
instance.gainNode = this.processor
return instance
}
this.processor = this.audioContext.createGain()
this.processor.gain.value = this.player.state.volume
}
}

View File

@ -2,13 +2,12 @@ import EqProcessorNode from "./eqNode"
import GainProcessorNode from "./gainNode"
import CompressorProcessorNode from "./compressorNode"
//import BPMProcessorNode from "./bpmNode"
import SpatialNode from "./spatialNode"
//import SpatialNode from "./spatialNode"
export default [
//BPMProcessorNode,
EqProcessorNode,
GainProcessorNode,
CompressorProcessorNode,
SpatialNode,
//BPMProcessorNode,
EqProcessorNode,
GainProcessorNode,
CompressorProcessorNode,
//SpatialNode,
]

View File

@ -1,172 +1,147 @@
export default class ProcessorNode {
constructor(PlayerCore) {
if (!PlayerCore) {
throw new Error("PlayerCore is required")
}
constructor(manager) {
if (!manager) {
throw new Error("processorManager is required")
}
this.PlayerCore = PlayerCore
this.audioContext = PlayerCore.audioContext
}
this.manager = manager
this.audioContext = manager.base.context
this.elementSource = manager.base.elementSource
this.player = manager.base.player
}
async _init() {
// check if has init method
if (typeof this.init === "function") {
await this.init(this.audioContext)
}
async _init() {
// check if has init method
if (typeof this.init === "function") {
await this.init()
}
// check if has declared bus events
if (typeof this.busEvents === "object") {
Object.entries(this.busEvents).forEach((event, fn) => {
app.eventBus.on(event, fn)
})
}
// check if has declared bus events
if (typeof this.busEvents === "object") {
Object.entries(this.busEvents).forEach((event, fn) => {
app.eventBus.on(event, fn)
})
}
if (typeof this.processor._last === "undefined") {
this.processor._last = this.processor
}
if (typeof this.processor._last === "undefined") {
this.processor._last = this.processor
}
return this
}
return this
}
_attach(instance, index) {
if (typeof instance !== "object") {
instance = this.PlayerCore.currentAudioInstance
}
_attach(index) {
// 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(", ")}`,
)
// 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 null
}
}
return instance
}
}
// if index is not defined, attach to the last node
if (!index) {
index = this.manager.attached.length
}
// if index is not defined, attach to the last node
if (!index) {
index = instance.attachedProcessors.length
}
const prevNode = this.manager.attached[index - 1]
const nextNode = this.manager.attached[index + 1]
const prevNode = instance.attachedProcessors[index - 1]
const nextNode = instance.attachedProcessors[index + 1]
const currentIndex = this._findIndex()
const currentIndex = this._findIndex(instance)
// check if is already attached
if (currentIndex !== false) {
console.warn(
`[${this.constructor.refName ?? this.constructor.name}] node is already attached`,
)
// check if is already attached
if (currentIndex !== false) {
console.warn(`[${this.constructor.refName ?? this.constructor.name}] node is already attached`)
return null
}
return instance
}
// first check if has prevNode and if is connected to something
// if has, disconnect it
// if it not has, its means that is the first node, so connect to the media source
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is already attached to the previous node, disconnecting...`)
// if has outputs, disconnect from the next node
prevNode.processor._last.disconnect()
// first check if has prevNode and if is connected to something
// if has, disconnect it
// if it not has, its means that is the first node, so connect to the media source
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is already attached to the previous node, disconnecting...`)
// if has outputs, disconnect from the next node
prevNode.processor._last.disconnect()
// now, connect to the processor
prevNode.processor._last.connect(this.processor)
} else {
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
this.elementSource.connect(this.processor)
}
// now, connect to the processor
prevNode.processor._last.connect(this.processor)
} else {
//console.log(`[${this.constructor.refName ?? this.constructor.name}] node is the first node, connecting to the media source...`)
instance.contextElement.connect(this.processor)
}
// now, check if it has a next node
// if has, connect to it
// if not, connect to the destination
if (nextNode) {
this.processor.connect(nextNode.processor)
}
// now, check if it has a next node
// if has, connect to it
// if not, connect to the destination
if (nextNode) {
this.processor.connect(nextNode.processor)
}
// add to the attachedProcessors
this.manager.attached.splice(index, 0, this)
// add to the attachedProcessors
instance.attachedProcessors.splice(index, 0, this)
// // handle instance mutation
// if (typeof this.mutateInstance === "function") {
// instance = this.mutateInstance(instance)
// }
// handle instance mutation
if (typeof this.mutateInstance === "function") {
instance = this.mutateInstance(instance)
}
return this
}
return instance
}
_detach() {
// find index of the node within the attachedProcessors serching for matching refName
const index = this._findIndex()
_detach(instance) {
if (typeof instance !== "object") {
instance = this.PlayerCore.currentAudioInstance
}
if (!index) {
return null
}
// find index of the node within the attachedProcessors serching for matching refName
const index = this._findIndex(instance)
// retrieve the previous and next nodes
const prevNode = this.manager.attached[index - 1]
const nextNode = this.manager.attached[index + 1]
if (!index) {
return instance
}
// check if has previous node and if has outputs
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
// if has outputs, disconnect from the previous node
prevNode.processor._last.disconnect()
}
// retrieve the previous and next nodes
const prevNode = instance.attachedProcessors[index - 1]
const nextNode = instance.attachedProcessors[index + 1]
// disconnect
this.processor.disconnect()
this.manager.attached.splice(index, 1)
// check if has previous node and if has outputs
if (prevNode && prevNode.processor._last.numberOfOutputs > 0) {
// if has outputs, disconnect from the previous node
prevNode.processor._last.disconnect()
}
// now, connect the previous node to the next node
if (prevNode && nextNode) {
prevNode.processor._last.connect(nextNode.processor)
} else {
// it means that this is the last node, so connect to the destination
prevNode.processor._last.connect(this.audioContext.destination)
}
// disconnect
instance = this._destroy(instance)
return this
}
// now, connect the previous node to the next node
if (prevNode && nextNode) {
prevNode.processor._last.connect(nextNode.processor)
} else {
// it means that this is the last node, so connect to the destination
prevNode.processor._last.connect(this.audioContext.destination)
}
_findIndex() {
// find index of the node within the attachedProcessors serching for matching refName
const index = this.manager.attached.findIndex((node) => {
return node.constructor.refName === this.constructor.refName
})
return instance
}
if (index === -1) {
return false
}
_destroy(instance) {
if (typeof instance !== "object") {
instance = this.PlayerCore.currentAudioInstance
}
const index = this._findIndex(instance)
if (!index) {
return instance
}
this.processor.disconnect()
instance.attachedProcessors.splice(index, 1)
return instance
}
_findIndex(instance) {
if (!instance) {
instance = this.PlayerCore.currentAudioInstance
}
if (!instance) {
console.warn(`Instance is not defined`)
return false
}
// 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) {
return false
}
return index
}
return index
}
}