@seamless-medley/medley
Version:
Audio engine for Node.js, with built-in "radio like" gapless/seamless playback
449 lines (355 loc) • 10.8 kB
TypeScript
/// <reference types="node" />
import type { Readable } from 'node:stream';
export interface TrackInfo {
/**
* Path to the physical file.
*/
readonly path: string;
/**
* Start position of the track
*
* Setting this property will also disable lead-in
*/
readonly cueInPosition?: number;
/**
* Stop position of the track
*/
readonly cueOutPosition?: number;
/**
* Disable lead-in of the next track, useful for transiting from jingle/sweeper
*
* The lead-in is the position where it is considered as the start singing point,
* usually presented in a track which has smooth beginning.
*/
readonly disableNextLeadIn?: boolean;
}
export type TrackDescriptor<T extends TrackInfo> = string | T;
export declare class Queue<T extends TrackInfo = TrackInfo> {
constructor(tracks?: TrackDescriptor<T> | TrackDescriptor<T>[]);
get length(): number;
add(track: TrackDescriptor<T> | TrackDescriptor<T>[]): void;
/**
* Insert track(s) at position specified by the `index` parameter.
* @param index
* @param track
*/
insert(index: number, track: TrackDescriptor<T> | TrackDescriptor<T>[]): void;
/**
* Delete a track at `index`
* @param index
*/
delete(index: number): void;
/**
* Delete number of tracks specified by `count` starting from `from`
* @param from
* @param count
*/
delete(from: number, count: number): void;
swap(index1: number, index2: number): void;
move(currentIndex: number, newIndex: number): void;
get(index: number): T;
set(index: number, track: TrackDescriptor<T>): void;
/**
* Convert tracks list into an array
*
* @remarks
* The returned array will not link with the internal listing of this `Queue`.
*
* @returns Array
*/
toArray(): T[];
}
export interface AudioLevel {
magnitude: number;
peak: number;
}
export interface AudioLevels {
/**
* Audio level for left channel
*/
left: AudioLevel;
/**
* Audio level for right channel
*/
right: AudioLevel;
}
type NormalEvent = 'audioDeviceChanged';
type DeckEvent = 'loaded' | 'unloaded' | 'started' | 'finished' | 'mainDeckChanged';
export declare enum DeckIndex {
A = 0,
B = 1,
C = 2
}
export type TrackPlay<T extends TrackInfo> = {
uuid: string;
track: T;
duration: number;
}
export type Listener<T = void> = () => T;
export type DeckListener<T extends TrackInfo> = (deckIndex: DeckIndex, trackPlay: TrackPlay<T>) => void;
export type EnqueueCallback = (result: boolean) => void;
export type EnqueueListener = (done: EnqueueCallback) => void;
export type LogListener = (level: number, name: string, msg: string) => void;
export type MedleyOptions = {
logging?: boolean;
skipDeviceScanning?: boolean;
}
export declare class Medley<T extends TrackInfo = TrackInfo> {
constructor(queue: Queue<T>, options?: MedleyOptions);
on(event: DeckEvent, listener: DeckListener<T>): this;
once(event: DeckEvent, listener: DeckListener<T>): this;
off(event: DeckEvent, listener: DeckListener<T>): this;
on(event: 'enqueueNext', listener: EnqueueListener): this;
once(event: 'enqueueNext', listener: EnqueueListener): this;
off(event: 'enqueueNext', listener: EnqueueListener): this;
on(event: NormalEvent, listener: Listener): this;
once(event: NormalEvent, listener: Listener): this;
off(event: NormalEvent, listener: Listener): this;
on(event: 'log', listener: LogListener): this;
once(event: 'log', listener: LogListener): this;
off(event: 'log', listener: LogListener): this;
/**
* @returns `AudioLevels`
*/
get level(): AudioLevels;
/**
* Reduction level in dB
*/
get reduction(): number;
/**
* @returns `true` if the engine is running, `false` otherwise.
*
* @remarks This is not affected by `paused` property.
*/
get playing(): boolean;
get paused(): boolean;
/**
* Audio volume in linear scale, `0` = silent, `1` = 0dBFS
* This only affact main output
*/
get volume(): number;
set volume(value: number);
/**
* S-Curve for fading in/out, range from `0` to `100`
*/
get fadingCurve(): number;
set fadingCurve(value: number);
/**
* The maximum duration in seconds for the fade-out transition between tracks.
*/
get maximumFadeOutDuration(): number;
set maximumFadeOutDuration(value: number);
/**
* The duration in seconds at the beginning of a track to be considered as having a long intro.
*
* A track with a long intro will cause a fading-in to occur during transition.
*/
get minimumLeadingToFade(): number;
set minimumLeadingToFade(value: number);
/**
* Gain (in dB) to boost for tracks having ReplayGain metadata embeded, default to 9.0dB
* If the a has no ReplayGain metadata, this value is ignored.
*
* @default 9.0
*/
get replayGainBoost(): number;
set replayGainBoost(decibels: number);
/**
* Start the engine, also clear the `paused` state.
*/
play(shouldFade: boolean = true): boolean;
/**
* Stop the engine and unload track(s).
*/
stop(shouldFade: boolean = true): void;
togglePause(fade: boolean = true): void;
/**
* Force fading out and unloading of the current track.
*/
fadeOut(): boolean;
/**
* Seek the main deck
* @param time in seconds
* @param index Deck index, omit to seek on the main deck
*/
seek(time: number, index?: DeckIndex): void;
/**
* Seek
* @param fraction
* @param index Deck index, omit to seek on the main deck
*
* @example
* seek(0) - Seek to the beginning.
* seek(0.5) - Seek to the middle of a track.
*/
seekFractional(fraction: number, index?: DeckIndex): void;
getAvailableDevices(): AudioDeviceTypeInfo[];
setAudioDevice(descriptor: Partial<AudioDeviceDescriptor>): boolean;
getAudioDevice(): AudioDeviceDescriptor | undefined;
getDeckMetadata(index: DeckIndex): Metadata | undefined;
getDeckPositions(index: DeckIndex): DeckPositions;
async requestAudioStream(options?: RequestAudioOptions): Promise<RequestAudioStreamResult>;
updateAudioStream(id: RequestAudioResult['id'], options: UpdateAudioStreamOptions): boolean;
deleteAudioStream(id: number): boolean;
getFx(type: 'karaoke'): KaraokeParams;
getFx(type: any): never;
setFx(type: 'karaoke', params: KaraokeUpdateParams): boolean;
setFx(type: any, params: never): false;
static getMetadata(path: string): Metadata | undefined;
static getAudioProperties(path: string, readMode?: AudioPropertiesReadMode): AudioProperties;
static getCoverAndLyrics(path: string): CoverAndLyrics;
static isTrackLoadable(track: TrackDescriptor<any>): boolean;
static getInfo(): MedleyInfo;
}
export type MedleyInfo = {
runtime: {
file: string;
specificity: number;
runtime?: 'node' | 'electron';
napi?: boolean;
libc?: string;
},
juce: {
version: Record<'major' | 'minor' | 'build', number>;
cpu: Partial<Record<'intel' | 'arm' | 'arm64' | 'aarch64' | 'sse' | 'neon' | 'vdsp', true>>;
};
version: Record<'major' | 'minor' | 'patch', number> & { prerelease?: string };
versionString: string;
}
declare const audioFormats = ['Int16LE', 'Int16BE', 'FloatLE', 'FloatBE'] as const;
export type AudioFormat = typeof audioFormats[number];
export type RequestAudioOptions = {
sampleRate?: number;
/**
* Maximun frames the internal buffer can hold, increase this value helps reduce stuttering in some situations
*
* @default 250ms (deviceSampleRate * 0.25)
*/
bufferSize?: number;
/**
* Number of frames to buffer before returning the buffered frames back to Node.js stream
*
* Reducing this value will cause the stream to pump faster
*
* Setting this value to 0 may cause the underlying stream to return empty buffers
* which cause Node.js to utilize more CPU cycles while waiting for data
*
* @default 10ms (deviceSampleRate * 0.01)
*/
buffering?: number;
/**
* Audio sample format, possible values are:
* - `Int16LE` - 16 bit signed integer, little endian
* - `Int16BE` - 16 bit signed integer, big endian
* - `FloatLE` - 32 bit floating point, little endian
* - `FloatBE` - 32 bit floating point, big endian
*
* @default FloatLE
*/
format: AudioFormat;
/**
* Output gain, a floating point number range from 0-1
*
* @default 1.0 (0dBFS)
*/
gain?: number;
fx?: {
karaoke?: KaraokeUpdateParams;
}
}
export type UpdateAudioStreamOptions = Partial<Pick<RequestAudioOptions, 'buffering' | 'gain' | 'fx'>>;
export type RequestAudioResult = {
readonly id: number;
readonly channels: number;
readonly originalSampleRate: number;
readonly sampleRate: number;
readonly bitPerSample: number;
}
export type RequestAudioStreamResult = RequestAudioResult & {
readonly stream: Readable;
update(options: UpdateAudioStreamOptions): boolean;
/**
* Get audio pipeline latency in millisecond
*/
getLatency(): number;
getFx(type: 'karaoke'): KaraokeParams | undefined;
getFx(type: any): never;
setFx(type: 'karaoke', params: KaraokeUpdateParams): boolean;
setFx(type: any, params: never): false;
}
export type NullAudioDeviceDescriptor = {
type: 'Null';
device: 'Null Device';
}
export type AudioDeviceDescriptor = NullAudioDeviceDescriptor | {
type: string;
device: string;
}
export type AudioDeviceTypeInfo = {
type: string;
isCurrent: boolean;
devices: string[];
defaultDevice: string;
currentDevice?: string;
}
export type Metadata = {
title?: string;
artist?: string;
album?: string;
isrc?: string;
albumArtist?: string;
originalArtist?: string;
trackGain?: number;
bpm?: number;
comments: [string, string][];
}
export type AudioPropertiesReadMode = 'fast' | 'average' | 'accurate';
export type AudioProperties = {
channels?: number;
bitrate?: number;
sampleRate?: number;
duration?: number;
}
export type MetadataFields = keyof Metadata;
export type CoverAndLyrics = {
cover: Buffer;
coverMimeType: string;
lyrics: string;
}
export type DeckPositions = {
current?: number;
duration?: number;
/**
* First audible position
*/
first?: number;
/**
* Last audible position
*/
last?: number;
/**
* Leading duration
*/
leading?: number;
/**
* Leading duration
*/
trailing?: number;
/**
* The cue point/position for the next deck
*/
cuePoint?: number;
transitionStart?: number;
transitionEnd?: number;
}
export type KaraokeParams = {
enabled: boolean;
mix: number;
lowpassCutoff: number;
lowpassQ: number;
highpassCutoff: number;
highpassQ: number;
}
export type KaraokeUpdateParams = Partial<KaraokeParams & {
dontTransit?: boolean;
}>;