Merge branch 'feature/jukebox' into 'main'

Feature/jukebox

See merge request nitro/nitro-renderer!24
This commit is contained in:
Bill 2022-11-08 22:44:30 +00:00
commit b9dab09c60
34 changed files with 1310 additions and 583 deletions

View File

@ -21,6 +21,7 @@
"postinstall": "node ./post-install.js"
},
"dependencies": {
"howler": "^2.2.3",
"@pixi/app": "~6.4.2",
"@pixi/basis": "~6.4.2",
"@pixi/canvas-display": "~6.4.2",
@ -58,6 +59,7 @@
},
"devDependencies": {
"@types/pako": "^1.0.3",
"@types/howler": "^2.2.7",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7",
"eslint": "^8.20.0",

View File

@ -0,0 +1,25 @@
import { IAdvancedMap } from '../../IAdvancedMap';
import { ISongInfo } from './common/ISongInfo';
import { IPlaylistController } from './IPlaylistController';
export interface IMusicController
{
getRoomItemPlaylist(_arg_1?:number):IPlaylistController;
getSongDiskInventorySize():number;
getSongDiskInventoryDiskId(_arg_1:number):number;
getSongDiskInventorySongId(_arg_1:number):number;
getSongInfo(_arg_1:number):ISongInfo;
getSongIdPlayingAtPriority(_arg_1:number):number;
playSong(songId:number, priority:number, startPos?:number, playLength?:number, fadeInSeconds?:number, fadeOutSeconds?:number):boolean;
stop(_arg_1:number):void;
addSongInfoRequest(_arg_1:number):void;
requestSongInfoWithoutSamples(_arg_1:number):void;
requestUserSongDisks():void;
onSongLoaded(_arg_1:number):void;
updateVolume(_arg_1:number):void;
samplesUnloaded(_arg_1:number[]):void;
get samplesIdsInUse():number[];
get songDiskInventory(): IAdvancedMap<number, number>
init(): void;
dispose():void;
}

View File

@ -1,6 +0,0 @@
import { INitroManager } from '../../common';
export interface IMusicManager extends INitroManager
{
playPosition: number;
}

View File

@ -0,0 +1,15 @@
import { ISongInfo } from './common/ISongInfo';
export interface IPlaylistController
{
get priority():number;
get length():number;
get playPosition():number;
get nowPlayingSongId():number;
get isPlaying():boolean;
get entries(): ISongInfo[];
getEntry(_arg_1: number):ISongInfo;
requestPlayList():void;
init(): void;
dispose():void;
}

View File

@ -1,7 +1,8 @@
import { INitroManager } from '../../common';
import { IMusicManager } from './IMusicManager';
import { IMusicController } from './IMusicController';
export interface ISoundManager extends INitroManager
{
musicManager: IMusicManager;
get musicController(): IMusicController;
get traxVolume(): number;
}

View File

@ -0,0 +1,12 @@
export interface ISongInfo
{
//get loaded():boolean;
get id():number;
get diskId():number;
set diskId(id: number);
get length():number;
get name():string;
get creator():string;
get songData():string;
//get soundObject():IHabboSound;
}

View File

@ -0,0 +1 @@
export * from './ISongInfo';

View File

@ -1,2 +1,4 @@
export * from './IMusicManager';
export * from './common';
export * from './IMusicController';
export * from './IPlaylistController';
export * from './ISoundManager';

View File

@ -1,13 +1,14 @@
import { IMessageDataWrapper, IMessageParser } from '../../../../../api';
import { IAdvancedMap, IMessageDataWrapper, IMessageParser } from '../../../../../api';
import { AdvancedMap } from '../../../../../core';
export class JukeboxSongDisksMessageParser implements IMessageParser
{
private _songDisks: Map<number, number> = new Map();
private _songDisks: IAdvancedMap<number, number> = new AdvancedMap();
private _maxLength: number;
flush(): boolean
{
this._songDisks.clear();
this._songDisks.reset();
this._maxLength = 0;
return true;
}
@ -19,13 +20,13 @@ export class JukeboxSongDisksMessageParser implements IMessageParser
for(let i = 0; i < count; i++)
{
this._songDisks.set(wrapper.readInt(), wrapper.readInt());
this._songDisks.add(wrapper.readInt(), wrapper.readInt());
}
return true;
}
public get songDisks(): Map<number, number>
public get songDisks(): IAdvancedMap<number, number>
{
return this._songDisks;
}

View File

@ -8,5 +8,6 @@ export * from './localization';
export * from './Nitro';
export * from './room';
export * from './session';
export * from './sound';
export * from './utils';
export * from './window';

View File

@ -1,8 +1,8 @@
import { IAdvancedMap, IMusicManager, INitroEvent, ISoundManager, NitroConfiguration } from '../../api';
import { IAdvancedMap, IMusicController, INitroEvent, ISoundManager, NitroConfiguration } from '../../api';
import { AdvancedMap, NitroManager } from '../../core';
import { NitroSettingsEvent, NitroSoundEvent, RoomEngineEvent, RoomEngineObjectEvent, RoomEngineSamplePlaybackEvent } from '../../events';
import { Nitro } from '../Nitro';
import { MusicManager } from './music';
import { MusicController } from './music/MusicController';
export class SoundManager extends NitroManager implements ISoundManager
{
@ -14,7 +14,7 @@ export class SoundManager extends NitroManager implements ISoundManager
private _furniSamples: IAdvancedMap<number, HTMLAudioElement>;
private _furnitureBeingPlayed: IAdvancedMap<number, number>;
private _musicManager: MusicManager;
private _musicController: IMusicController;
constructor()
{
@ -27,15 +27,14 @@ export class SoundManager extends NitroManager implements ISoundManager
this._internalSamples = new AdvancedMap();
this._furniSamples = new AdvancedMap();
this._furnitureBeingPlayed = new AdvancedMap();
this._musicManager = new MusicManager();
this._musicController = new MusicController();
this.onEvent = this.onEvent.bind(this);
}
public onInit(): void
{
this._musicManager.init();
this._musicController.init();
Nitro.instance.roomEngine.events.addEventListener(RoomEngineSamplePlaybackEvent.PLAY_SAMPLE, this.onEvent);
Nitro.instance.roomEngine.events.addEventListener(RoomEngineObjectEvent.REMOVED, this.onEvent);
@ -46,10 +45,10 @@ export class SoundManager extends NitroManager implements ISoundManager
public onDispose(): void
{
if(this._musicManager)
if(this._musicController)
{
this._musicManager.dispose();
this._musicManager = null;
this._musicController.dispose();
this._musicController = null;
}
Nitro.instance.roomEngine.events.removeEventListener(RoomEngineSamplePlaybackEvent.PLAY_SAMPLE, this.onEvent);
@ -86,12 +85,16 @@ export class SoundManager extends NitroManager implements ISoundManager
const castedEvent = (event as NitroSettingsEvent);
const volumeFurniUpdated = castedEvent.volumeFurni !== this._volumeFurni;
const volumeTraxUpdated = castedEvent.volumeTrax !== this._volumeTrax;
this._volumeSystem = (castedEvent.volumeSystem / 100);
this._volumeFurni = (castedEvent.volumeFurni / 100);
this._volumeTrax = (castedEvent.volumeTrax / 100);
if(volumeFurniUpdated) this.updateFurniSamplesVolume(this._volumeFurni);
if(volumeTraxUpdated) this._musicController?.updateVolume(this._volumeTrax);
return;
}
case NitroSoundEvent.PLAY_SOUND: {
@ -219,8 +222,13 @@ export class SoundManager extends NitroManager implements ISoundManager
});
}
public get musicManager(): IMusicManager
public get traxVolume(): number
{
return this._musicManager;
return this._volumeTrax;
}
public get musicController(): IMusicController
{
return this._musicController;
}
}

View File

@ -0,0 +1,54 @@
import { ISongInfo } from '../../../api';
import { PlayListEntry } from '../../communication/messages/parser/sound/PlayListEntry';
export class SongDataEntry extends PlayListEntry implements ISongInfo
{
private _songData:string;
private _jukeboxDiskId:number = -1;
constructor(id:number, length:number, name:string, creator:string, songData: string = '')
{
super(id, length, name, creator);
this._songData = songData;
}
public override get id():number
{
return this._id;
}
public override get length():number
{
return this._length;
}
public override get name():string
{
return this._name;
}
public override get creator():string
{
return this._creator;
}
public get songData():string
{
return this._songData;
}
public set songData(k:string)
{
this._songData = k;
}
public get diskId():number
{
return this._jukeboxDiskId;
}
public set diskId(k:number)
{
this._jukeboxDiskId = k;
}
}

View File

@ -1,4 +1,3 @@
import { GetTickerTime } from '../../../pixi-proxy';
export class SongStartRequestData
{
@ -9,14 +8,14 @@ export class SongStartRequestData
private _fadeInSeconds: number;
private _fadeOutSeconds: number;
constructor(songId: number, startPos: number, playLength: number, playRequestTime: number, fadeInSeconds: number, fadeOutSeconds: number)
constructor(songId: number, startPos: number, playLength: number, fadeInSeconds: number, fadeOutSeconds: number)
{
this._songId = songId;
this._startPos = startPos;
this._playLength = playLength;
this._playRequestTime = playRequestTime;
this._fadeInSeconds = fadeInSeconds;
this._fadeOutSeconds = fadeOutSeconds;
this._playRequestTime = Date.now();
}
public get songId(): number
@ -28,7 +27,7 @@ export class SongStartRequestData
{
if(this._startPos < 0) return 0;
return this._startPos + ((GetTickerTime() - this._playRequestTime) / 1000);
return this._startPos + ((Date.now() - this._playRequestTime) / 1000);
}
public get playLength(): number

View File

@ -1,194 +0,0 @@
import { BinaryReader, BinaryWriter } from '../../../core';
export class TraxSample
{
public static readonly SAMPLE_FREQUENCY_44KHZ: string = 'sample_44khz';
public static readonly SAMPLE_FREQUENCY_22KHZ: string = 'sample_22khz';
public static readonly SAMPLE_FREQUENCY_11KHZ: string = 'sample_11khz';
public static readonly SAMPLE_SCALE_16BIT: string = 'sample_16bit';
public static readonly SAMPLE_SCALE_8BIT: string = 'sample_8bit';
public static readonly _Str_11575: number = (1 / 0x8000);
private static readonly _Str_14308: number = 32;
private static readonly MASK_8BIT: number = 0xFF;
private static readonly MASK_16BIT: number = 0xFFFF;
private static readonly OFFSET_8BIT: number = 127;
private static readonly OFFSET_16BIT: number = 32767;
private _sampleData: number[];
private _id: number;
private _samplesPerValue: number;
private _sampleRepeats: number;
private _usageList: any[];
private _usageTimestamp: number;
constructor(k: BinaryWriter, sampleId: number, sampleFrequency: string = TraxSample.SAMPLE_FREQUENCY_44KHZ, sampleScale: string = TraxSample.SAMPLE_SCALE_16BIT)
{
this._id = sampleId;
this._samplesPerValue = 2;
this._sampleRepeats = 1;
let local5 = 65536;
switch(sampleFrequency)
{
case TraxSample.SAMPLE_FREQUENCY_22KHZ:
this._sampleRepeats = 2;
break;
case TraxSample.SAMPLE_FREQUENCY_11KHZ:
this._sampleRepeats = 4;
break;
default:
this._sampleRepeats = 1;
}
if(sampleScale === TraxSample.SAMPLE_SCALE_8BIT)
{
this._samplesPerValue = 4;
local5 = 0x0100;
}
const local6: number = this._samplesPerValue * this._sampleRepeats;
const local7: number = (Math.trunc(k.getBuffer().byteLength / 8) / local6) * local6;
this._sampleData = new Array(local7 / local6);
const local8: number = 1 / (local5 / 2);
k.position = 0;
const reader: BinaryReader = new BinaryReader(k.getBuffer());
let local9: number;
const local10: number = (local7 / this._sampleRepeats);
let local12: number;
let local15: number;
for(let i = 0; i < local10; i++)
{
local12 = reader.readFloat();
reader.readFloat();
for(let j = 2; j <= this._sampleRepeats; j++)
{
local15 = reader.readFloat();
reader.readFloat();
local12 = (((j * (j - 1)) / j) + (local15 / j));
}
if(i >= ((local10 - 1) - TraxSample._Str_14308)) local12 = (local12 * (((local10 - 1) - 1) / TraxSample._Str_14308));
let local14 = ((local12 + 1) / local8);
if(local14 < 0)
{
local14 = 0;
}
else if(local14 >= local5)
{
local14 = local5 - 1;
}
local9 = (local9 * local5) + local14;
if((i % this._samplesPerValue) === this._samplesPerValue - 1)
{
this._sampleData[Math.trunc(i / this._samplesPerValue)] = local9;
}
}
}
public setSample(k: number[], arg2: number, arg3: number, arg4: number): number
{
let local8: number;
let local9: number;
let local10: number;
if(k === null || this._sampleData === null) return arg4;
const local5 = this._samplesPerValue * this._sampleRepeats;
arg4 = arg4 / local5;
if(arg2 < 0) arg3 = arg3 + arg2;
if(arg3 > k.length - arg2) arg3 = k.length - arg2;
let local6 = arg3 / local5;
let local7: number;
if(local6 > this._sampleData.length - arg4)
{
local7 = (local6 - this._sampleData.length - arg4) * local5;
local6 = this._sampleData.length - arg4;
if(local7 > (k.length - arg2)) local7 = k.length - arg2;
}
if(this._sampleRepeats === 1)
{
if(this._samplesPerValue === 2)
{
while(local6-- > 0)
{
local8 = this._sampleData[arg4++];
k[arg2++] = (((local8 >> 16) & TraxSample.MASK_16BIT) - TraxSample.OFFSET_16BIT);
k[arg2++] = ((local8 & TraxSample.MASK_16BIT) - TraxSample.OFFSET_16BIT);
}
}
else if(this._samplesPerValue === 4)
{
while(local6-- > 0)
{
local8 = this._sampleData[arg4++];
k[arg2++] = ((((local8 >> 24) & TraxSample.MASK_8BIT) - TraxSample.OFFSET_8BIT) << 8);
k[arg2++] = ((((local8 >> 16) & TraxSample.MASK_8BIT) - TraxSample.OFFSET_8BIT) << 8);
k[arg2++] = ((((local8 >> 8) & TraxSample.MASK_8BIT) - TraxSample.OFFSET_8BIT) << 8);
k[arg2++] = (((local8 & TraxSample.MASK_8BIT) - TraxSample.OFFSET_8BIT) << 8);
}
}
}
else if(this._sampleRepeats >= 2)
{
local9 = 0;
local10 = 0;
if(this._samplesPerValue === 2)
{
while(local6-- > 0)
{
local8 = this._sampleData[arg4++];
local10 = (((local8 >> 16) & TraxSample.MASK_16BIT) - TraxSample.OFFSET_16BIT);
local9 = this._sampleRepeats;
while(local9 > 0)
// eslint-disable-next-line no-empty
{
}
}
}
}
}
public get id(): number
{
return this._id;
}
public get length(): number
{
return this._sampleData.length * this._samplesPerValue * this._sampleRepeats;
}
public get usageCount(): number
{
return this._usageList.length;
}
public get usageTimestamp(): number
{
return this._usageTimestamp;
}
}

View File

@ -1,166 +0,0 @@
import { TraxData } from './TraxData';
export class TraxSequencer
{
private static readonly SAMPLES_PER_SECOND: number = 44100;
private static readonly BUFFER_LENGTH: number = 0x2000;
private static readonly SAMPLES_BAR_LENGTH: number = 88000;
private static readonly BAR_LENGTH: number = 88000;
private static readonly MIXING_BUFFER: number[] = new Array(this.BUFFER_LENGTH);
private static readonly INTERPOLATION_BUFFER: number[] = new Array(this.BUFFER_LENGTH);
private _volume: number;
//sound
//soundChannel
private _traxData: TraxData;
private _samples: Map<number, any>;
private _ready: boolean;
private _songId: number;
private _playLengthSamples: number = 0;
private _playHead: number;
//sequence array
private _prepared: boolean;
private _finished: boolean = true;
private _lengthSamples: number;
private _latencyMs: number;
private _fadeInActive: boolean;
private _fadeOutActive: boolean;
private _fadeInLengthSamples: number;
private _fadeOutLengthSamples: number;
private _fadeInSampleCounter: number;
private _fadeOutSampleCounter: number;
//fadeOutStopTimer
//stopTimer
private _useCutMode: boolean;
private _expectedStreamPosition: number = 0;
private _bufferUnderRunCount: number = 0;
constructor(songId: number, traxData: TraxData, samples: any)
{
this._latencyMs = 30;
//set events
this._songId = songId;
this._volume = 1;
//set sound new Sound()
//set soundchannel null
this._samples = new Map();
this._traxData = traxData;
this._ready = true;
this._playHead = 0;
//set sequence []
this._prepared = false;
this._lengthSamples = 0;
this._finished = false;
this._fadeInActive = false;
this._fadeOutActive = false;
this._fadeInLengthSamples = 0;
this._fadeOutLengthSamples = 0;
this._fadeInSampleCounter = 0;
this._fadeOutSampleCounter = 0;
}
public prepare(): boolean
{
if(!this._ready) return false;
if(this._prepared) return true;
if(this._traxData != null)
{
this._useCutMode = false;
if(this._traxData.hasMetaData)
{
this._useCutMode = this._traxData.metaCutMode;
}
if(this._useCutMode)
// eslint-disable-next-line no-empty
{
}
}
}
public prepareSequence(): void
{
for(const channel of this._traxData.channels)
{
for(const item of channel.items)
{
const sample = this._samples.get(item.id);
}
}
}
public get position(): number
{
return this._playHead / TraxSequencer.SAMPLES_PER_SECOND;
}
private set position(value: number)
{
this._playHead = value * TraxSequencer.SAMPLES_PER_SECOND;
}
public get volume(): number
{
return this._volume;
}
public set volume(value: number)
{
this._volume = value;
//soundTransform for channel
}
public get ready(): boolean
{
return this._ready;
}
public set ready(value: boolean)
{
this._ready = value;
}
public get finished(): boolean
{
return this._finished;
}
public set finished(value: boolean)
{
this._finished = value;
}
public get fadeOutSeconds(): number
{
return this._fadeOutLengthSamples / TraxSequencer.SAMPLES_PER_SECOND;
}
public set fadeOutSeconds(value: number)
{
this._fadeOutLengthSamples = value * TraxSequencer.SAMPLES_PER_SECOND;
}
public get fadeInSetconds(): number
{
return this._fadeInLengthSamples / TraxSequencer.SAMPLES_PER_SECOND;
}
public set fadeInSeconds(value: number)
{
this._fadeInLengthSamples = value * TraxSequencer.SAMPLES_PER_SECOND;
}
public get traxData(): TraxData
{
return this._traxData;
}
public get length(): number
{
return this._lengthSamples / TraxSequencer.SAMPLES_PER_SECOND;
}
}

View File

@ -1,6 +1,2 @@
export * from './SongDataEntry';
export * from './SongStartRequestData';
export * from './TraxChannel';
export * from './TraxChannelItem';
export * from './TraxData';
export * from './TraxSample';
export * from './TraxSequencer';

View File

@ -0,0 +1,27 @@
import { NitroEvent } from '../../../events';
export class NotifyPlayedSongEvent extends NitroEvent
{
public static readonly NOTIFY_PLAYED_SONG = 'UIEW_NOTIFY_PLAYED_SONG';
private _name: string;
private _creator: string;
constructor(name:string, creator:string)
{
super(NotifyPlayedSongEvent.NOTIFY_PLAYED_SONG);
this._name = name;
this._creator = creator;
}
public get name(): string
{
return this._name;
}
public get creator(): string
{
return this._creator;
}
}

View File

@ -0,0 +1,35 @@
import { NitroEvent } from '../../../events';
export class NowPlayingEvent extends NitroEvent
{
public static readonly NPE_USER_PLAY_SONG = 'NPE_USER_PLAY_SONG';
public static readonly NPW_USER_STOP_SONG = 'NPW_USER_STOP_SONG';
public static readonly NPE_SONG_CHANGED = 'NPE_SONG_CHANGED';
private _id:number;
private _position:number;
private _priority:number;
constructor(k:string, priority:number, id:number, position:number)
{
super(k);
this._id = id;
this._position = position;
this._priority = priority;
}
public get id():number
{
return this._id;
}
public get position():number
{
return this._position;
}
public get priority():number
{
return this._priority;
}
}

View File

@ -0,0 +1,12 @@
import { NitroEvent } from '../../../events';
export class PlayListStatusEvent extends NitroEvent
{
public static readonly PLUE_PLAY_LIST_UPDATED = 'PLUE_PLAY_LIST_UPDATED';
public static readonly PLUE_PLAY_LIST_FULL = 'PLUE_PLAY_LIST_FULL';
constructor(k:string)
{
super(k);
}
}

View File

@ -0,0 +1,11 @@
import { NitroEvent } from '../../../events';
export class SongDiskInventoryReceivedEvent extends NitroEvent
{
public static readonly SDIR_SONG_DISK_INVENTORY_RECEIVENT_EVENT = 'SDIR_SONG_DISK_INVENTORY_RECEIVENT_EVENT';
constructor(k:string)
{
super(k);
}
}

View File

@ -0,0 +1,19 @@
import { NitroEvent } from '../../../events';
export class SongInfoReceivedEvent extends NitroEvent
{
public static readonly SIR_TRAX_SONG_INFO_RECEIVED = 'SIR_TRAX_SONG_INFO_RECEIVED';
private _id:number;
constructor(k:string, _arg_2:number)
{
super(k);
this._id = _arg_2;
}
public get id():number
{
return this._id;
}
}

View File

@ -4,8 +4,16 @@ export class SoundManagerEvent extends NitroEvent
{
public static TRAX_SONG_COMPLETE: string = 'SME_TRAX_SONG_COMPLETE';
constructor(type: string)
private _id: number;
constructor(type: string, id: number)
{
super(type);
this._id = id;
}
public get id(): number
{
return this._id;
}
}

View File

@ -1 +1,6 @@
export * from './NotifyPlayedSongEvent';
export * from './NowPlayingEvent';
export * from './PlayListStatusEvent';
export * from './SongDiskInventoryReceivedEvent';
export * from './SongInfoReceivedEvent';
export * from './SoundManagerEvent';

View File

@ -2,3 +2,4 @@ export * from './common';
export * from './events';
export * from './music';
export * from './SoundManager';
export * from './trax';

View File

@ -0,0 +1,198 @@
import { IMessageEvent, IPlaylistController, ISongInfo } from '../../../api';
import { GetJukeboxPlayListMessageComposer, JukeboxPlayListFullMessageEvent, JukeboxSongDisksMessageEvent, NowPlayingMessageEvent } from '../../communication';
import { Nitro } from '../../Nitro';
import { SongDataEntry } from '../common/SongDataEntry';
import { NowPlayingEvent, PlayListStatusEvent, SongInfoReceivedEvent } from '../events';
import { MusicPriorities } from './MusicPriorities';
export class JukeboxPlaylistController implements IPlaylistController
{
private _isPlaying = false;
private _entries:ISongInfo[];
private _currentEntryId: number;
private _missingSongInfo:number[];
private _playPosition: number;
private _disposed: boolean = false;
private _messageEvents: IMessageEvent[];
constructor()
{
this._entries = [];
this._missingSongInfo = [];
this._messageEvents = [
new NowPlayingMessageEvent(this.onNowPlayingMessageEvent.bind(this)),
new JukeboxSongDisksMessageEvent(this.onJukeboxSongDisksMessageEvent.bind(this)),
new JukeboxPlayListFullMessageEvent(this.onJukeboxPlayListFullMessageEvent.bind(this))
];
this.onSongInfoReceivedEvent = this.onSongInfoReceivedEvent.bind(this);
}
public init(): void
{
this._messageEvents.forEach(event => Nitro.instance.communication.connection.addMessageEvent(event));
//this._events.addEventListener(SoundManagerEvent.TRAX_SONG_COMPLETE, this.onSongFinishedPlayingEvent);
Nitro.instance.soundManager.events.addEventListener(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, this.onSongInfoReceivedEvent);
}
public get priority(): number
{
return MusicPriorities.PRIORITY_ROOM_PLAYLIST;
}
private onNowPlayingMessageEvent(event: NowPlayingMessageEvent): void
{
const parser = event.getParser();
console.log(((((('Received Now Playing message with: ' + parser.currentSongId) + ', ') + parser.nextSongId) + ', ') + parser.syncCount));
this._isPlaying = (parser.currentSongId !== -1);
if(parser.currentSongId >= 0)
{
Nitro.instance.soundManager.musicController.playSong(parser.currentSongId, MusicPriorities.PRIORITY_ROOM_PLAYLIST, (parser.syncCount / 1000), 0, 1, 1);
this._currentEntryId = parser.currentSongId;
}
else
{
this.stopPlaying();
}
if(parser.nextSongId >= 0) Nitro.instance.soundManager.musicController.addSongInfoRequest(parser.nextSongId);
this._playPosition = parser.currentPosition;
//Dispatch local event NowPlayingEvent
Nitro.instance.soundManager.events.dispatchEvent(new NowPlayingEvent(NowPlayingEvent.NPE_SONG_CHANGED, MusicPriorities.PRIORITY_ROOM_PLAYLIST, parser.currentSongId, parser.currentPosition));
}
private onJukeboxSongDisksMessageEvent(event: JukeboxSongDisksMessageEvent): void
{
const parser = event.getParser();
console.log(('Received Jukebox song disks (=playlist) message, length of playlist: ' + parser.songDisks.length));
this._entries = [];
for(let i = 0; i < parser.songDisks.length; i++)
{
const songId = parser.songDisks.getWithIndex(i);
const diskId = parser.songDisks.getKey(i);
let songInfo = (Nitro.instance.soundManager.musicController.getSongInfo(songId) as SongDataEntry);
if(songInfo == null)
{
songInfo = new SongDataEntry(songId, -1, null, null, null);
if(this._missingSongInfo.indexOf(songId) < 0)
{
this._missingSongInfo.push(songId);
Nitro.instance.soundManager.musicController.requestSongInfoWithoutSamples(songId);
}
}
songInfo.diskId = diskId;
this._entries.push(songInfo);
}
if(this._missingSongInfo.length == 0)
{
Nitro.instance.soundManager.events.dispatchEvent(new PlayListStatusEvent(PlayListStatusEvent.PLUE_PLAY_LIST_UPDATED));
}
}
private onJukeboxPlayListFullMessageEvent(event: JukeboxPlayListFullMessageEvent): void
{
console.log('Received jukebox playlist full message.');
Nitro.instance.soundManager.events.dispatchEvent(new PlayListStatusEvent(PlayListStatusEvent.PLUE_PLAY_LIST_FULL));
}
private onSongInfoReceivedEvent(songInfoEvent:SongInfoReceivedEvent):void
{
for(let i = 0; i < this.length; i++)
{
const songData = this._entries[i];
if(songData.id == songInfoEvent.id)
{
const diskId = songData.diskId;
const updatedSongData = Nitro.instance.soundManager.musicController.getSongInfo(songInfoEvent.id);
if(updatedSongData != null)
{
updatedSongData.diskId = diskId;
this._entries[i] = updatedSongData;
}
break;
}
}
const _local_3 = this._missingSongInfo.indexOf(songInfoEvent.id);
if(_local_3 >= 0)
{
this._missingSongInfo.splice(_local_3, 1);
}
if(this._missingSongInfo.length == 0)
{
Nitro.instance.soundManager.events.dispatchEvent(new PlayListStatusEvent(PlayListStatusEvent.PLUE_PLAY_LIST_UPDATED));
}
}
public stopPlaying():void
{
Nitro.instance.soundManager.musicController.stop(this.priority);
this._currentEntryId = -1;
this._playPosition = -1;
this._isPlaying = false;
}
public get length(): number
{
if(!this._entries)
{
return 0;
}
return this._entries.length;
}
public get playPosition(): number
{
return this._playPosition;
}
public get nowPlayingSongId(): number
{
return this._currentEntryId;
}
public get isPlaying(): boolean
{
return this._isPlaying;
}
public get entries(): ISongInfo[]
{
return this._entries;
}
public getEntry(k: number): ISongInfo
{
if(((k < 0) || (k >= this._entries.length)))
{
return null;
}
return this._entries[k];
}
public requestPlayList(): void
{
Nitro.instance.communication.connection.send(new GetJukeboxPlayListMessageComposer());
}
public dispose(): void
{
if(!this._disposed)
{
this._disposed = true;
this.stopPlaying();
if(Nitro.instance.soundManager.events)
{
Nitro.instance.soundManager.events.removeEventListener(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, this.onSongInfoReceivedEvent);
}
this._messageEvents.forEach(event => Nitro.instance.communication.connection.removeMessageEvent(event));
this._messageEvents = null;
//this._events.removeEventListener(SoundControllerEvent.TRAX_SONG_COMPLETE, this.onSongFinishedPlayingEvent);
}
}
}

View File

@ -0,0 +1,599 @@
import { IAdvancedMap, IMessageEvent, IMusicController, IPlaylistController } from '../../../api';
import { ISongInfo } from '../../../api/nitro/sound/common/ISongInfo';
import { AdvancedMap } from '../../../core';
import { RoomObjectSoundMachineEvent } from '../../../events';
import { GetNowPlayingMessageComposer, GetSongInfoMessageComposer, GetUserSongDisksMessageComposer, TraxSongInfoMessageEvent, UserSongDisksInventoryMessageEvent } from '../../communication';
import { Nitro } from '../../Nitro';
import { SongDataEntry } from '../common/SongDataEntry';
import { SongStartRequestData } from '../common/SongStartRequestData';
import { NotifyPlayedSongEvent, NowPlayingEvent, SongDiskInventoryReceivedEvent, SongInfoReceivedEvent, SoundManagerEvent } from '../events';
import { TraxData } from '../trax/TraxData';
import { JukeboxPlaylistController } from './JukeboxPlaylistController';
import { MusicPlayer } from './MusicPlayer';
import { MusicPriorities } from './MusicPriorities';
export class MusicController implements IMusicController
{
public static readonly SKIP_POSITION_SET: number = -1;
private static readonly MAXIMUM_NOTIFY_PRIORITY: number = MusicPriorities.PRIORITY_ROOM_PLAYLIST;
private _timerInstance: number;
private _songRequestList: number[];
private _requestedSongs: Map<number, boolean>;
private _availableSongs: Map<number, SongDataEntry>;
private _songRequestsPerPriority: SongStartRequestData[];
private _songRequestCountsPerPriority:number[];
private _diskInventoryMissingData: number[];
private _songDiskInventory: IAdvancedMap<number, number>;
private _priorityPlaying:number = -1;
private _requestNumberPlaying: number = -1;
private _messageEvents: IMessageEvent[];
private _roomItemPlaylist: IPlaylistController;
private _musicPlayer: MusicPlayer;
private _songIdPlaying: number;
private _previousNotifiedSongId: number;
private _previousNotificationTime: number = -1;
constructor()
{
this._timerInstance = -1;
this._songRequestList = [];
this._requestedSongs = new Map<number, boolean>();
this._availableSongs = new Map<number, SongDataEntry>();
this._songDiskInventory = new AdvancedMap<number, number>();
this._songRequestsPerPriority = [];
this._songRequestCountsPerPriority = [];
this._diskInventoryMissingData = [];
this._songIdPlaying = -1;
this._previousNotifiedSongId = -1;
this._messageEvents = [
new TraxSongInfoMessageEvent(this.onTraxSongInfoMessageEvent.bind(this)),
new UserSongDisksInventoryMessageEvent(this.onSongDiskInventoryMessage.bind(this))
];
this.onJukeboxInit = this.onJukeboxInit.bind(this);
this.onJukeboxDispose = this.onJukeboxDispose.bind(this);
this.onSoundMachineInit = this.onSoundMachineInit.bind(this);
this.onSoundMachineDispose = this.onSoundMachineDispose.bind(this);
this.onTraxSongComplete = this.onTraxSongComplete.bind(this);
}
public init(): void
{
this._timerInstance = window.setInterval(this.onTick.bind(this), 1000);
this._musicPlayer = new MusicPlayer(Nitro.instance.getConfiguration<string>('external.samples.url'));
this._messageEvents.forEach(event => Nitro.instance.communication.connection.addMessageEvent(event));
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.JUKEBOX_INIT, this.onJukeboxInit);
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.JUKEBOX_DISPOSE, this.onJukeboxDispose);
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_INIT, this.onSoundMachineInit);
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_DISPOSE, this.onSoundMachineDispose);
Nitro.instance.soundManager.events.addEventListener(SoundManagerEvent.TRAX_SONG_COMPLETE, this.onTraxSongComplete);
}
public getRoomItemPlaylist(_arg_1?: number): IPlaylistController
{
return this._roomItemPlaylist;
}
public get songDiskInventory(): IAdvancedMap<number, number>
{
return this._songDiskInventory;
}
public getSongDiskInventorySize(): number
{
return this._songDiskInventory.length;
}
public getSongDiskInventoryDiskId(k: number): number
{
if(((k >= 0) && (k < this._songDiskInventory.length)))
{
return this._songDiskInventory.getKey(k);
}
return -1;
}
public getSongDiskInventorySongId(k: number): number
{
if(((k >= 0) && (k < this._songDiskInventory.length)))
{
return this._songDiskInventory.getWithIndex(k);
}
return -1;
}
public getSongInfo(k: number): ISongInfo
{
const _local_2:SongDataEntry = this.getSongDataEntry(k);
if(_local_2 == null)
{
this.requestSongInfoWithoutSamples(k);
}
return _local_2;
}
public getSongIdPlayingAtPriority(k: number): number
{
if(k != this._priorityPlaying)
{
return -1;
}
return this._songIdPlaying;
}
public stop(priority: number): void
{
const isCurrentPlayingPriority = (priority === this._priorityPlaying);
const isTopRequestPriority = (this.getTopRequestPriority() === priority);
if(isCurrentPlayingPriority)
{
this.resetSongStartRequest(priority);
this.stopSongAtPriority(priority);
}
else
{
this.resetSongStartRequest(priority);
if(isTopRequestPriority)
{
this.reRequestSongAtPriority(this._priorityPlaying);
}
}
}
public addSongInfoRequest(k: number): void
{
this.requestSong(k, true);
}
public requestSongInfoWithoutSamples(k: number): void
{
this.requestSong(k, false);
}
public requestUserSongDisks(): void
{
Nitro.instance.communication.connection.send(new GetUserSongDisksMessageComposer());
}
public updateVolume(_arg_1: number): void
{
this._musicPlayer.setVolume(_arg_1);
}
public dispose(): void
{
if(this._timerInstance)
{
clearInterval(this._timerInstance);
this._timerInstance = null;
}
this._messageEvents.forEach(event => Nitro.instance.communication.connection.removeMessageEvent(event));
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.JUKEBOX_INIT, this.onJukeboxInit);
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.JUKEBOX_DISPOSE, this.onJukeboxDispose);
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_INIT, this.onSoundMachineInit);
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_DISPOSE, this.onSoundMachineDispose);
Nitro.instance.soundManager.events.removeEventListener(SoundManagerEvent.TRAX_SONG_COMPLETE, this.onTraxSongComplete);
}
public get samplesIdsInUse(): number[]
{
let _local_3:SongStartRequestData;
let _local_4:SongDataEntry;
let k = [];
for(let i = 0; i < this._songRequestsPerPriority.length; i++)
{
if(this._songRequestsPerPriority[i] != null)
{
_local_3 = this._songRequestsPerPriority[i];
_local_4 = this._availableSongs.get(_local_3.songId);
if(_local_4)
{
const songData = _local_4.songData;
if(songData.length > 0)
{
const traxData = new TraxData(songData);
k = k.concat(traxData.getSampleIds());
}
}
}
}
return k;
}
public onSongLoaded(songId: number): void
{
console.log(('Song loaded : ' + songId));
const priority = this.getTopRequestPriority();
if(priority >= 0)
{
const songIdAtTopPriority = this.getSongIdRequestedAtPriority(priority);
if(songId === songIdAtTopPriority)
{
this.playSongObject(priority, songId);
}
}
}
public samplesUnloaded(_arg_1: number[]): void
{
throw new Error('Method not implemented.');
}
protected onTraxSongComplete(k:SoundManagerEvent):void
{
console.log((('Song ' + k.id) + ' finished playing'));
if(this.getSongIdPlayingAtPriority(this._priorityPlaying) == k.id)
{
if(((this.getTopRequestPriority() == this._priorityPlaying) && (this.getSongRequestCountAtPriority(this._priorityPlaying) == this._requestNumberPlaying)))
{
this.resetSongStartRequest(this._priorityPlaying);
}
const priorityPlaying = this._priorityPlaying;
this.playSongWithHighestPriority();
if(priorityPlaying >= MusicPriorities.PRIORITY_SONG_PLAY)
{
Nitro.instance.soundManager.events.dispatchEvent(new NowPlayingEvent(NowPlayingEvent.NPW_USER_STOP_SONG, priorityPlaying, k.id, -1));
}
}
}
private onTraxSongInfoMessageEvent(event: TraxSongInfoMessageEvent): void
{
const parser = event.getParser();
for(const song of parser.songs)
{
const songAvailable = !!this.getSongDataEntry(song.id);
const areSamplesRequested = !!this.areSamplesRequested(song.id);
if(!songAvailable)
{
if(!areSamplesRequested)
{
//_local_9 = this._soundManager.loadTraxSong(_local_6.id, _local_6.data);
}
const songInfoEntry: SongDataEntry = new SongDataEntry(song.id, song.length, song.name, song.creator, song.data);
this._availableSongs.set(song.id, songInfoEntry);
const topRequestPriotityIndex: number = this.getTopRequestPriority();
const songId: number = this.getSongIdRequestedAtPriority(topRequestPriotityIndex);
if(song.id === songId)
{
this._musicPlayer.preloadSamplesForSong(song.data).then(() => this.onSongLoaded(song.id));
//this.playSongObject(topRequestPriotityIndex, songId);
}
Nitro.instance.soundManager.events.dispatchEvent(new SongInfoReceivedEvent(SongInfoReceivedEvent.SIR_TRAX_SONG_INFO_RECEIVED, song.id));
while(this._diskInventoryMissingData.indexOf(song.id) != -1)
{
this._diskInventoryMissingData.splice(this._diskInventoryMissingData.indexOf(song.id), 1);
if(this._diskInventoryMissingData.length === 0)
{
Nitro.instance.soundManager.events.dispatchEvent(new SongDiskInventoryReceivedEvent(SongDiskInventoryReceivedEvent.SDIR_SONG_DISK_INVENTORY_RECEIVENT_EVENT));
}
}
console.log(('Received song info : ' + song.id));
}
}
}
private onSongDiskInventoryMessage(event: UserSongDisksInventoryMessageEvent): void
{
const parser = event.getParser();
this._songDiskInventory.reset();
for(let i = 0; i < parser.songDiskCount; i++)
{
const diskId = parser.getDiskId(i);
const songId = parser.getSongId(i);
this._songDiskInventory.add(diskId, songId);
if(!this._availableSongs.get(songId))
{
this._diskInventoryMissingData.push(songId);
this.requestSongInfoWithoutSamples(songId);
}
}
if(this._diskInventoryMissingData.length === 0)
{
Nitro.instance.soundManager.events.dispatchEvent(new SongDiskInventoryReceivedEvent(SongDiskInventoryReceivedEvent.SDIR_SONG_DISK_INVENTORY_RECEIVENT_EVENT));
}
}
private onTick(): void
{
if(this._songRequestList.length === 0) return;
Nitro.instance.communication.connection.send(new GetSongInfoMessageComposer(...this._songRequestList));
console.log(('Requested song info\'s : ' + this._songRequestList));
this._songRequestList = [];
}
private requestSong(songId: number, arg2: boolean): void
{
if(!this._requestedSongs.get(songId))
{
this._requestedSongs.set(songId, arg2);
this._songRequestList.push(songId);
}
}
private areSamplesRequested(k:number):boolean
{
if(!this._requestedSongs.get(k))
{
return false;
}
return this._requestedSongs.get(k);
}
private processSongEntryForPlaying(k:number, _arg_2:boolean=true):boolean
{
const songData:SongDataEntry = this.getSongDataEntry(k);
if(!songData)
{
this.addSongInfoRequest(k);
return false;
}
/* if(_local_3.soundObject == null)
{
_local_3.soundObject = this._soundManager.loadTraxSong(_local_3.id, _local_3.songData);
}
const _local_4:IHabboSound = _local_3.soundObject;
if(!_local_4.ready)
{
return false;
} */
return true;
}
public playSong(songId: number, priority: number, startPos: number = 0, playLength: number = 0, fadeInSeconds: number = 0.5, fadeOutSeconds: number = 0.5): boolean
{
console.log(('Requesting ' + songId + ' for playing'));
if(!this.addSongStartRequest(priority, songId, startPos, playLength, fadeInSeconds, fadeOutSeconds))
{
return false;
}
if(!this.processSongEntryForPlaying(songId))
{
return false;
}
if(priority >= this._priorityPlaying)
{
this.playSongObject(priority, songId);
}
else
{
console.log(((('Higher priority song blocked playing. Stored song ' + songId) + ' for priority ') + priority));
}
return true;
}
private playSongObject(priority:number, songId:number):boolean
{
if((((songId == -1) || (priority < 0)) || (priority >= MusicPriorities.PRIORITY_COUNT)))
{
return false;
}
const _local_3 = false;
// if(this.stopSongAtPriority(this._priorityPlaying))
// {
// _local_3 = true;
// }
const songData:SongDataEntry = this.getSongDataEntry(songId);
if(!songData)
{
console.log((('WARNING: Unable to find song entry id ' + songId) + ' that was supposed to be loaded.'));
return false;
}
if(_local_3)
{
console.log(('Waiting previous song to stop before playing song ' + songId));
return true;
}
this._musicPlayer.setVolume(Nitro.instance.soundManager.traxVolume);
let startPos = MusicController.SKIP_POSITION_SET;
let playLength = 0;
let fadeInSeconds = 2;
let fadeOutSeconds = 1;
const songRequestData:SongStartRequestData = this.getSongStartRequest(priority);
console.log(songRequestData);
if(songRequestData)
{
startPos = songRequestData.startPos;
playLength = songRequestData.playLength;
fadeInSeconds = songRequestData.fadeInSeconds;
fadeOutSeconds = songRequestData.fadeOutSeconds;
}
if(startPos >= (songData.length / 1000))
{
console.log('start position is too far');
return false;
}
if(startPos <= MusicController.SKIP_POSITION_SET)
{
startPos = 0;
}
startPos = Math.trunc(startPos);
/*
_local_5.fadeInSeconds = _local_8;
_local_5.fadeOutSeconds = _local_9;
_local_5.position = _local_6;
_local_5.play(_local_7);
*/
this._priorityPlaying = priority;
this._requestNumberPlaying = this.getSongRequestCountAtPriority(priority);
this._songIdPlaying = songId;
if(this._priorityPlaying <= MusicController.MAXIMUM_NOTIFY_PRIORITY)
{
this.notifySongPlaying(songData);
}
this._musicPlayer.play(songData.songData, startPos, playLength);
if(priority > MusicPriorities.PRIORITY_ROOM_PLAYLIST)
{
Nitro.instance.soundManager.events.dispatchEvent(new NowPlayingEvent(NowPlayingEvent.NPE_USER_PLAY_SONG, priority, songData.id, -1));
}
console.log(((((((((('Started playing song ' + songId) + ' at position ') + startPos) + ' for ') + playLength) + ' seconds (length ') + (songData.length / 1000)) + ') with priority ') + priority));
return true;
}
private notifySongPlaying(k:SongDataEntry):void
{
const _local_2 = 8000;
const timeNow = Date.now();
if(((k.length >= _local_2) && ((!(this._previousNotifiedSongId == k.id)) || (timeNow > (this._previousNotificationTime + _local_2)))))
{
Nitro.instance.soundManager.events.dispatchEvent(new NotifyPlayedSongEvent(k.name, k.creator));
this._previousNotifiedSongId = k.id;
this._previousNotificationTime = timeNow;
}
}
private addSongStartRequest(priority:number, songId:number, startPos:number, playLength:number, fadeInSeconds:number, fadeOutSeconds:number):boolean
{
if(((priority < 0) || (priority >= MusicPriorities.PRIORITY_COUNT)))
{
return false;
}
const songStartRequest = new SongStartRequestData(songId, startPos, playLength, fadeInSeconds, fadeOutSeconds);
this._songRequestsPerPriority[priority] = songStartRequest;
this._songRequestCountsPerPriority[priority] = (this._songRequestCountsPerPriority[priority] + 1);
return true;
}
private getSongDataEntry(k:number):SongDataEntry
{
let entry:SongDataEntry;
if(this._availableSongs != null)
{
entry = (this._availableSongs.get(k));
}
return entry;
}
private getSongStartRequest(k:number):SongStartRequestData
{
return this._songRequestsPerPriority[k];
}
private getTopRequestPriority(): number
{
return this._songRequestsPerPriority.length - 1;
}
private getSongIdRequestedAtPriority(priorityIndex: number): number
{
if(priorityIndex < 0 || priorityIndex >= MusicPriorities.PRIORITY_COUNT) return -1;
if(!this._songRequestsPerPriority[priorityIndex]) return -1;
return this._songRequestsPerPriority[priorityIndex].songId;
}
private getSongRequestCountAtPriority(k:number):number
{
if(((k < 0) || (k >= MusicPriorities.PRIORITY_COUNT)))
{
return -1;
}
return this._songRequestCountsPerPriority[k];
}
private playSongWithHighestPriority():void
{
let _local_3:number;
this._priorityPlaying = -1;
this._songIdPlaying = -1;
this._requestNumberPlaying = -1;
const k = this.getTopRequestPriority();
let _local_2 = k;
while(_local_2 >= 0)
{
_local_3 = this.getSongIdRequestedAtPriority(_local_2);
if(((_local_3 >= 0) && (this.playSongObject(_local_2, _local_3))))
{
return;
}
_local_2--;
}
}
private resetSongStartRequest(k:number):void
{
if(((k >= 0) && (k < MusicPriorities.PRIORITY_COUNT)))
{
this._songRequestsPerPriority[k] = null;
}
}
private reRequestSongAtPriority(k:number):void
{
this._songRequestCountsPerPriority[k] = (this._songRequestCountsPerPriority[k] + 1);
}
private stopSongAtPriority(priority:number):boolean
{
if(((priority == this._priorityPlaying) && (this._priorityPlaying >= 0)))
{
const songIdAtPriority = this.getSongIdPlayingAtPriority(priority);
if(songIdAtPriority >= 0)
{
const songData = this.getSongDataEntry(songIdAtPriority);
//this.stopSongDataEntry(_local_3);
console.log('stopping song ' + songIdAtPriority);
this._musicPlayer.stop();
return true;
}
}
return false;
}
private onSoundMachineInit(k:Event):void
{
this.disposeRoomPlaylist();
//this._roomItemPlaylist = (new SoundMachinePlayListController(this._soundManager, this, this._events) as IPlaylistController);
}
private onSoundMachineDispose(k:Event):void
{
this.disposeRoomPlaylist();
}
private onJukeboxInit(k:Event):void
{
this.disposeRoomPlaylist();
this._roomItemPlaylist = (new JukeboxPlaylistController() as IPlaylistController);
this._roomItemPlaylist.init();
Nitro.instance.communication.connection.send(new GetNowPlayingMessageComposer());
}
private onJukeboxDispose(k:Event):void
{
this.disposeRoomPlaylist();
}
private disposeRoomPlaylist():void
{
if(this._roomItemPlaylist != null)
{
this._roomItemPlaylist.dispose();
this._roomItemPlaylist = null;
}
}
}

View File

@ -1,183 +0,0 @@
import { IMusicManager, INitroEvent } from '../../../api';
import { NitroManager } from '../../../core';
import { RoomObjectSoundMachineEvent } from '../../../events';
import { GetSongInfoMessageComposer, JukeboxPlayListFullMessageEvent, JukeboxSongDisksMessageEvent, NowPlayingMessageEvent, SongInfoEntry, TraxSongInfoMessageEvent, UserSongDisksInventoryMessageEvent } from '../../communication';
import { Nitro } from '../../Nitro';
import { SongStartRequestData } from '../common';
import { SoundManagerEvent } from '../events';
import { MusicPriorities } from './MusicPriorities';
export class MusicManager extends NitroManager implements IMusicManager
{
public static readonly SKIP_POSITION_SET: number = -1;
private static readonly MAXIMUM_NOTIFY_PRIORITY: number = MusicPriorities.PRIORITY_ROOM_PLAYLIST;
private _timerInstance: any;
private _songRequestList: number[];
private _requestedSongs: Map<number, boolean>;
private _availableSongs: Map<number, SongInfoEntry>;
private _songRequestsPerPriority: SongStartRequestData[];
private _currentEntryId: number;
private _playPosition: number;
private _isPlaying: boolean;
constructor()
{
super();
this._timerInstance = null;
this._songRequestList = [];
this._requestedSongs = new Map();
this._availableSongs = new Map();
this._songRequestsPerPriority = [];
this._currentEntryId = -1;
this._playPosition = -1;
this._isPlaying = false;
this.onEvent = this.onEvent.bind(this);
this._timerInstance = setInterval(this.onTick.bind(this), 1000);
}
public onInit(): void
{
Nitro.instance.communication.connection.addMessageEvent(new TraxSongInfoMessageEvent(this.onEvent));
Nitro.instance.communication.connection.addMessageEvent(new UserSongDisksInventoryMessageEvent(this.onEvent));
Nitro.instance.communication.connection.addMessageEvent(new NowPlayingMessageEvent(this.onNowPlayingMessageEvent.bind(this)));
Nitro.instance.communication.connection.addMessageEvent(new JukeboxSongDisksMessageEvent(this.onEvent));
Nitro.instance.communication.connection.addMessageEvent(new JukeboxPlayListFullMessageEvent(this.onEvent));
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.JUKEBOX_INIT, this.onEvent);
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.JUKEBOX_DISPOSE, this.onEvent);
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_INIT, this.onEvent);
Nitro.instance.roomEngine.events.addEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_DISPOSE, this.onEvent);
this.events.addEventListener(SoundManagerEvent.TRAX_SONG_COMPLETE, this.onEvent);
}
public onDispose(): void
{
if(this._timerInstance)
{
clearInterval(this._timerInstance);
this._timerInstance = null;
}
Nitro.instance.communication.connection.removeMessageEvent(new TraxSongInfoMessageEvent(this.onEvent));
Nitro.instance.communication.connection.removeMessageEvent(new UserSongDisksInventoryMessageEvent(this.onEvent));
Nitro.instance.communication.connection.removeMessageEvent(new NowPlayingMessageEvent(this.onNowPlayingMessageEvent.bind(this)));
Nitro.instance.communication.connection.removeMessageEvent(new JukeboxSongDisksMessageEvent(this.onEvent));
Nitro.instance.communication.connection.removeMessageEvent(new JukeboxPlayListFullMessageEvent(this.onEvent));
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.JUKEBOX_INIT, this.onEvent);
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.JUKEBOX_DISPOSE, this.onEvent);
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_INIT, this.onEvent);
Nitro.instance.roomEngine.events.removeEventListener(RoomObjectSoundMachineEvent.SOUND_MACHINE_DISPOSE, this.onEvent);
this.events.removeEventListener(SoundManagerEvent.TRAX_SONG_COMPLETE, this.onEvent);
}
private onEvent(event: INitroEvent): void
{
}
private onTraxSongInfoMessageEvent(event: TraxSongInfoMessageEvent): void
{
const parser = event.getParser();
for(const song of parser.songs)
{
const songAvailable: boolean = (this._availableSongs.get(song.id) !== null);
const areSamplesRequested: boolean = (this._requestedSongs.get(song.id) !== null);
if(!songAvailable)
{
if(areSamplesRequested)
{
//LoadTraxSong
}
const songInfoEntry: SongInfoEntry = new SongInfoEntry(song.id, song.length, song.name, song.creator, song.data);
this._availableSongs.set(song.id, songInfoEntry);
const topRequestPriotityIndex: number = this.getTopRequestPriority();
const songId: number = this.getSongIdRequestedAtPriority(topRequestPriotityIndex);
}
}
}
private onNowPlayingMessageEvent(event: NowPlayingMessageEvent): void
{
const parser = event.getParser();
this._isPlaying = (parser.currentSongId !== -1);
if(parser.currentSongId >= 0)
{
this.playSong(parser.currentSongId, MusicPriorities.PRIORITY_ROOM_PLAYLIST, (parser.syncCount / 1000), 0, 1, 1);
}
else
{
this.stopPlaying();
}
if(parser.nextSongId >= 0) this.requestSong(parser.nextSongId, true);
this._playPosition = parser.currentPosition;
//Dispatch local event NowPlayingEvent
}
private onTick(): void
{
if(this._songRequestList.length === 0) return;
Nitro.instance.communication.connection.send(new GetSongInfoMessageComposer(...this._songRequestList));
this._songRequestList = [];
}
private requestSong(songId: number, arg2: boolean): void
{
if(this._requestedSongs.get(songId) === null)
{
this._requestedSongs.set(songId, arg2);
this._songRequestList.push(songId);
}
}
private playSong(songId: number, priority: number, startPos: number = 0, playLength: number = 0, fadeInSeconds: number = 0.5, fadeOutSeconds: number = 0.5)
{
}
private stopPlaying(): void
{
this._currentEntryId = -1;
this._playPosition = -1;
this._isPlaying = false;
}
private getTopRequestPriority(): number
{
return this._songRequestsPerPriority.length - 1;
}
private getSongIdRequestedAtPriority(priorityIndex: number): number
{
if(priorityIndex < 0 || priorityIndex >= MusicPriorities.PRIORITY_COUNT) return -1;
if(!this._songRequestsPerPriority[priorityIndex]) return -1;
return this._songRequestsPerPriority[priorityIndex].songId;
}
public get playPosition(): number
{
return this._playPosition;
}
public set playPosition(value: number)
{
this._playPosition = value;
}
}

View File

@ -0,0 +1,226 @@
import { Howl } from 'howler';
import { Nitro } from '../../Nitro';
import { SoundManagerEvent } from '../events';
import { TraxData } from '../trax/TraxData';
export class MusicPlayer
{
private _currentSong: TraxData | undefined;
private _startPos: number;
private _playLength: number;
private _isPlaying: boolean;
private _currentPos: number;
private _cache: Map<number, Howl>;
private _sampleUrl: string;
private _tickerInterval: number | undefined;
private _sequence: ISequenceEntry[][];
constructor(sampleUrl: string)
{
this._sampleUrl = sampleUrl;
this._isPlaying = false;
this._startPos = 0;
this._currentPos = 0;
this._playLength = 0;
this._sequence = [];
this._cache = new Map<number, Howl>();
}
public async play(song: string, startPos: number = 0, playLength: number = -1): Promise<void>
{
this.reset();
this._currentSong = new TraxData(song);
this._startPos = Math.trunc(startPos);
this._playLength = playLength;
this._currentPos = this._startPos;
//this.emit('loading');
await this.preload();
this._isPlaying = true;
//this.emit('playing', this._currentPos, this._playLength - 1);
this.tick(); // to evade initial 1 sec delay
this._tickerInterval = window.setInterval(() => this.tick(), 1000);
}
public pause(): void
{
this._isPlaying = false;
//this.emit('paused', this._currentPos);
Howler.stop();
}
public resume(): void
{
this._isPlaying = true;
//this.emit('playing', this._currentPos, this._playLength - 1 );
}
public stop(): void
{
this.reset();
//this.emit('stopped');
}
private reset(): void
{
this._isPlaying = false;
clearInterval(this._tickerInterval);
Howler.stop();
this._currentSong = undefined;
this._startPos = 0;
this._playLength = 0;
this._sequence = [];
this._currentPos = 0;
}
/**
* Sets global howler volume for all sounds
* @param volume value from 0.0 to 1.0
*/
public setVolume(volume: number): void
{
Howler.volume(volume);
}
/**
* Gets global howler volume for all sounds
* @returns value from 0.0 to 1.0
*/
public getVolume(): number
{
return Howler.volume();
}
/**
* Gets sample from cache or loads it if not in cache
* @param id sample id
* @returns howl sound object
*/
public async getSample(id: number): Promise<Howl>
{
let sample = this._cache.get(id);
if(!sample) sample = await this.loadSong(id);
return Promise.resolve(sample);
}
private async preload(): Promise<void>
{
this._sequence = [];
if(!this._currentSong) return;
for(const channel of this._currentSong.channels)
{
const sequenceEntryArray: ISequenceEntry[] = [];
for(const sample of channel.items)
{
const sampleSound = await this.getSample(sample.id);
const sampleCount = Math.ceil( (sample.length * 2) / Math.ceil(sampleSound.duration()));
for(let i = 0; i < sampleCount; i++)
{
for(let j = 0; j < Math.ceil(sampleSound.duration()); j++)
{
sequenceEntryArray.push({ sampleId: sample.id, offset: j });
}
}
}
this._sequence.push(sequenceEntryArray);
}
if(this._playLength <= 0) this._playLength = Math.max(...this._sequence.map( (value: ISequenceEntry[] ) => value.length));
}
public async preloadSamplesForSong(song: string): Promise<void>
{
const traxData = new TraxData(song);
await Promise.all(traxData.getSampleIds().map(id => this.getSample(id)));
}
private async loadSong(songId: number): Promise<Howl>
{
return new Promise <Howl>((resolve, reject) =>
{
const sample = new Howl({
src: [this._sampleUrl.replace('%sample%', songId.toString())],
preload: true,
});
sample.once('load', () =>
{
this._cache.set(songId, sample);
resolve(sample);
});
sample.once('loaderror', () =>
{
console.log('failed to load sample ' + songId);
reject('failed to load sample ' + songId);
});
});
}
private tick(): void
{
if(this._currentPos > this._playLength - 1)
{
Nitro.instance.soundManager.events.dispatchEvent(new SoundManagerEvent(SoundManagerEvent.TRAX_SONG_COMPLETE, Nitro.instance.soundManager.musicController.getRoomItemPlaylist().nowPlayingSongId));
this.stop();
}
if(this._isPlaying)
{
if(this._currentSong)
{
//this.emit('time', this._currentPos);
this.playPosition(this._currentPos);
}
this._currentPos++;
}
}
private playPosition(pos: number): void
{
if(!this._currentSong || !this._sequence) return;
for(const sequencyEntry of this._sequence)
{
const entry = sequencyEntry[pos];
if(!entry) continue;
// sample -1 is play none
// sample 0 is 1 second of empty noise
if(entry.sampleId === -1 || entry.sampleId === 0) continue;
const sampleAudio = this._cache.get(entry.sampleId);
if(!sampleAudio) continue;
if(entry.offset === 0)
{
sampleAudio.play();
}
else if(!sampleAudio.playing())
{
sampleAudio.seek(entry.offset);
sampleAudio.play();
}
}
}
}
interface ISequenceEntry
{
sampleId: number;
offset: number;
}

View File

@ -1,2 +1,3 @@
export * from './MusicManager';
export * from './JukeboxPlaylistController';
export * from './MusicController';
export * from './MusicPriorities';

View File

@ -1,4 +1,5 @@
import { TraxChannelItem } from './TraxChannelItem';
export class TraxChannel
{
private _id: number;

View File

@ -1,5 +1,6 @@
import { TraxChannel } from './TraxChannel';
import { TraxChannelItem } from './TraxChannelItem';
export class TraxData
{
private _channels: TraxChannel[];
@ -8,6 +9,8 @@ export class TraxData
constructor(data: string)
{
this._channels = [];
this._metaData = new Map<string, string>();
let channelLines: string[] = [];
const lines: string[] = data.split(':');
@ -75,17 +78,17 @@ export class TraxData
public get hasMetaData(): boolean
{
return this._metaData.get('meta') !== null;
return this._metaData.has('meta');
}
public get metaCutMode(): boolean
{
return this._metaData.get('c') !== null;
return this._metaData.has('c');
}
public get metaTempo(): number
public get metaTempo(): number | null
{
const tempo: string = this._metaData.get('t');
const tempo = this._metaData.get('t');
if(!tempo) return null;

View File

@ -0,0 +1,3 @@
export * from './TraxChannel';
export * from './TraxChannelItem';
export * from './TraxData';

View File

@ -229,6 +229,11 @@
resolved "https://registry.yarnpkg.com/@types/earcut/-/earcut-2.1.1.tgz#573a0af609f17005c751f6f4ffec49cfe358ea51"
integrity sha512-w8oigUCDjElRHRRrMvn/spybSMyX8MTkKA5Dv+tS1IE/TgmNZPqUYtvYBXGY8cieSE66gm+szeK+bnbxC2xHTQ==
"@types/howler@^2.2.7":
version "2.2.7"
resolved "https://registry.yarnpkg.com/@types/howler/-/howler-2.2.7.tgz#5acfbed57f9e1d99b8dabe1b824729e1c1ea1fae"
integrity sha512-PEZldwZqJJw1PWRTpupyC7ajVTZA8aHd8nB/Y0n6zRZi5u8ktYDntsHj13ltEiBRqWwF06pASxBEvCTxniG8eA==
"@types/json-schema@^7.0.9":
version "7.0.9"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz"
@ -712,6 +717,11 @@ has-flag@^4.0.0:
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
howler@^2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.3.tgz#a2eff9b08b586798e7a2ee17a602a90df28715da"
integrity sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==
ignore@^5.2.0:
version "5.2.0"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz"