mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
Add audio system foundation with player architecture refactor
This commit is contained in:
parent
29ae54fe03
commit
369803534b
123
packages/app/src/cores/player/classes/AudioBase.js
Normal file
123
packages/app/src/cores/player/classes/AudioBase.js
Normal 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)
|
||||
},
|
||||
}
|
||||
}
|
56
packages/app/src/cores/player/classes/MediaSession.js
Normal file
56
packages/app/src/cores/player/classes/MediaSession.js
Normal 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"
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
// }
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
]
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user