diff --git a/packages/app/src/cores/player/classes/AudioBase.js b/packages/app/src/cores/player/classes/AudioBase.js new file mode 100644 index 00000000..1818bfcf --- /dev/null +++ b/packages/app/src/cores/player/classes/AudioBase.js @@ -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) + }, + } +} diff --git a/packages/app/src/cores/player/classes/MediaSession.js b/packages/app/src/cores/player/classes/MediaSession.js new file mode 100644 index 00000000..cd34fad7 --- /dev/null +++ b/packages/app/src/cores/player/classes/MediaSession.js @@ -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" + } +} diff --git a/packages/app/src/cores/player/classes/PlayerProcessors.js b/packages/app/src/cores/player/classes/PlayerProcessors.js index f59db0aa..fb982159 100644 --- a/packages/app/src/cores/player/classes/PlayerProcessors.js +++ b/packages/app/src/cores/player/classes/PlayerProcessors.js @@ -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 - } -} \ No newline at end of file + detachAllNodes = async () => { + for (const [index, node] of this.attached.entries()) { + await node._detach() + } + } +} diff --git a/packages/app/src/cores/player/classes/TrackInstance.js b/packages/app/src/cores/player/classes/TrackInstance.js index f88278c8..cae48993 100644 --- a/packages/app/src/cores/player/classes/TrackInstance.js +++ b/packages/app/src/cores/player/classes/TrackInstance.js @@ -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 + // } } diff --git a/packages/app/src/cores/player/classes/TrackManifest.js b/packages/app/src/cores/player/classes/TrackManifest.js index 1a8e404a..01433c59 100644 --- a/packages/app/src/cores/player/classes/TrackManifest.js +++ b/packages/app/src/cores/player/classes/TrackManifest.js @@ -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, diff --git a/packages/app/src/cores/player/player.core.js b/packages/app/src/cores/player/player.core.js index 435a466b..2d0c260f 100755 --- a/packages/app/src/cores/player/player.core.js +++ b/packages/app/src/cores/player/player.core.js @@ -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() { diff --git a/packages/app/src/cores/player/processors/compressorNode/index.js b/packages/app/src/cores/player/processors/compressorNode/index.js index 8c372892..446e5e15 100755 --- a/packages/app/src/cores/player/processors/compressorNode/index.js +++ b/packages/app/src/cores/player/processors/compressorNode/index.js @@ -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] - }) - } -} \ No newline at end of file + applyValues() { + Object.keys(this.presets.currentPresetValues).forEach((key) => { + this.processor[key].value = this.presets.currentPresetValues[key] + }) + } +} diff --git a/packages/app/src/cores/player/processors/eqNode/index.js b/packages/app/src/cores/player/processors/eqNode/index.js index 09103971..5525a6d6 100755 --- a/packages/app/src/cores/player/processors/eqNode/index.js +++ b/packages/app/src/cores/player/processors/eqNode/index.js @@ -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) - } -} \ No newline at end of file + // set last processor for processor node can properly connect to the next node + this.processor._last = this.processor.eqNodes.at(-1) + } +} diff --git a/packages/app/src/cores/player/processors/gainNode/index.js b/packages/app/src/cores/player/processors/gainNode/index.js index dc00fa59..0ed6c33f 100755 --- a/packages/app/src/cores/player/processors/gainNode/index.js +++ b/packages/app/src/cores/player/processors/gainNode/index.js @@ -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 - } -} \ No newline at end of file + this.processor = this.audioContext.createGain() + this.processor.gain.value = this.player.state.volume + } +} diff --git a/packages/app/src/cores/player/processors/index.js b/packages/app/src/cores/player/processors/index.js index c266c920..4a6a87c0 100755 --- a/packages/app/src/cores/player/processors/index.js +++ b/packages/app/src/cores/player/processors/index.js @@ -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, -] \ No newline at end of file + //BPMProcessorNode, + EqProcessorNode, + GainProcessorNode, + CompressorProcessorNode, + //SpatialNode, +] diff --git a/packages/app/src/cores/player/processors/node.js b/packages/app/src/cores/player/processors/node.js index 84a6a865..ddcd1826 100755 --- a/packages/app/src/cores/player/processors/node.js +++ b/packages/app/src/cores/player/processors/node.js @@ -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 - } -} \ No newline at end of file + return index + } +}