UNPKG

@seamless-medley/medley

Version:

Audio engine for Node.js, with built-in "radio like" gapless/seamless playback

123 lines (122 loc) 4.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.audioFormats = exports.Queue = exports.Medley = void 0; exports.createMedley = createMedley; 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; }; function createMedley(options) { const queue = new exports.Queue(); const medley = new exports.Medley(queue, options); return { medley, queue }; }