@seamless-medley/medley
Version:
Audio engine for Node.js, with built-in "radio like" gapless/seamless playback
117 lines (116 loc) • 4.33 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.audioFormats = exports.Queue = exports.Medley = void 0;
const node_events_1 = require("node:events");
const node_path_1 = require("node:path");
const node_stream_1 = require("node:stream");
const nodeGypBuild = require('node-gyp-build');
const module_id = process.env.MEDLEY_DEV ? (0, node_path_1.dirname)(__dirname) : __dirname;
const medley = nodeGypBuild(module_id);
exports.Medley = medley.Medley;
exports.Queue = medley.Queue;
Object.setPrototypeOf(exports.Medley.prototype, node_events_1.EventEmitter.prototype);
exports.Medley.getInfo = function () {
const { runtime = {}, ...rest } = exports.Medley.$getInfo();
return {
runtime: {
...nodeGypBuild.parseTags((0, node_path_1.basename)(nodeGypBuild.resolve(module_id))),
...runtime,
},
...rest
};
};
exports.audioFormats = ['Int16LE', 'Int16BE', 'FloatLE', 'FloatBE'];
const formatToBytesPerSample = (format) => {
switch (format) {
case 'FloatBE':
case 'FloatLE':
return 4;
case 'Int16BE':
case 'Int16LE':
return 2;
default:
return 0;
}
};
const audioStreamResults = new Map();
exports.Medley.prototype.requestAudioStream = async function (options = { format: 'FloatLE' }) {
const result = this['*$reqAudio'](options);
const streamId = result.id;
const sampleRate = Number(options.sampleRate ?? 44100);
const defaultBuffering = sampleRate * 0.01;
const defaultBufferSize = sampleRate * 0.25;
let buffering = Number(options.buffering ?? defaultBuffering);
const bufferSize = Number(options.bufferSize ?? defaultBufferSize);
if (buffering < 1) {
throw new Error('buffering cannot be less than 1');
}
if (bufferSize <= buffering) {
throw new Error('bufferSize is too small');
}
const bytesPerSample = formatToBytesPerSample(options.format);
const getSamplesReady = () => (this['*$reqAudio$getSamplesReady'](streamId) ?? 0);
const waitForBuffer = (sampleSize) => new Promise((resolve) => {
const check = () => {
if (getSamplesReady() >= sampleSize) {
resolve();
return;
}
setTimeout(check, 10);
};
check();
});
const consume = async (size) => {
return await this['*$reqAudio$consume'](streamId, Math.max(size, buffering * bytesPerSample * 2));
};
const stream = new node_stream_1.Readable({
highWaterMark: bufferSize * 1.5 * bytesPerSample * 2,
objectMode: false,
read: async (size) => {
await waitForBuffer(buffering);
stream.push(await consume(size));
}
});
stream.on('close', async () => {
stream.emit('closed');
});
stream.on('finish', async () => {
stream.emit('finished');
});
const streamResult = {
stream,
...result,
update: (newOptions) => {
if (newOptions.buffering) {
const newBuffering = Number(newOptions.buffering ?? defaultBuffering);
if (newBuffering < 1) {
throw new Error('buffering cannot be less than 1');
}
if (bufferSize <= newBuffering) {
throw new Error('bufferSize is too small');
}
buffering = newBuffering;
}
return this.updateAudioStream(streamId, newOptions);
},
getLatency: () => {
const r = buffering + (stream.readableLength / bytesPerSample / 2);
const bufferDelay = (r / sampleRate * 1000);
return bufferDelay + this['*$reqAudio$getLatency'](streamId);
},
getFx: type => this['*$reqAudio$getFx'](streamId, type),
setFx: (type, params) => this['*$reqAudio$setFx'](streamId, type, params)
};
audioStreamResults.set(streamId, streamResult);
return streamResult;
};
exports.Medley.prototype.deleteAudioStream = function (id) {
const request = audioStreamResults.get(id);
if (!request) {
return;
}
const result = this['*$reqAudio$dispose'](id);
request.stream.destroy();
audioStreamResults.delete(id);
return result;
};