diff --git a/classes/Audiostream.js b/classes/Audiostream.js index ba696b6..e34c116 100644 --- a/classes/Audiostream.js +++ b/classes/Audiostream.js @@ -25,7 +25,6 @@ class Audiostream { this.host = config?.server?.host || "127.0.0.1"; this.port = data.port; this.clientId = data.clientId; - this.size = data.size; this.threshold = data.threshold; this.#handleSocket(net.connect({ host: this.getHost(), @@ -40,7 +39,8 @@ class Audiostream { }); this.eventParser.on('audio:play', (data) => { logger.debug('handling event \'audio:play\'...'); - global.player.play(data?.position); + // global.player.play(data?.position); + global.player.speak(); }); this.eventParser.on('audio:pause', (data) => { logger.debug('handling event \'audio:pause\'...'); @@ -62,7 +62,7 @@ class Audiostream { #handleSocket(socket) { socket.on('connect', async () => { logger.debug('connected to audio server \'' + this.getTag() + '\'...'); - await global.player.prepare(this.size, this.threshold); + await global.player.prepare(this.threshold, socket); new Message('audio:register', { clientId: this.clientId, port: socket.localPort }).send(); }); socket.on('error', (error) => { @@ -71,9 +71,9 @@ class Audiostream { socket.on('timeout', () => { logger.warn('connection to audio server \'' + this.getTag() + '\' timed out'); }); - socket.on('data', (data) => { - global.player.feed(data); - }); + // socket.on('data', (data) => { + // global.player.speaker.feed(data); + // }); socket.on('end', () => { logger.info('connection to audio server \'' + this.getTag() + '\' ended'); }); @@ -81,6 +81,7 @@ class Audiostream { logger.info('connection to audio server \'' + this.getTag() + '\' closed'); global.player.stopFeed(); }); + // global.player.speaker.feed(socket); } } diff --git a/classes/Player.js b/classes/Player.js index 42d0ed8..7d21801 100644 --- a/classes/Player.js +++ b/classes/Player.js @@ -1,3 +1,4 @@ +const Speaker = require('./Speaker.js'); const EventEmitter = require('events'); const { spawn } = require('child_process'); const createWriteStream = require('fs').createWriteStream; @@ -14,30 +15,75 @@ class Player extends EventEmitter { this.tmp = { file: resolve(global.config?.tmp || '/tmp/kannon.tmp') }; + this.buffer = { + size: 0, + elements: [] + }; } - async prepare(size, threshold) { + async prepare(threshold, stream) { logger.debug('preparing audio player...'); await this.#reset(); await this.#removeTemporaryFile(); - this.size = size; this.threshold = threshold; - this.tmp.stream = createWriteStream(this.tmp.file); + this.stream = stream; + // this.tmp.stream = createWriteStream(this.tmp.file); + // this.speaker = new Speaker(speakeroptions.channel, speakeroptions.bitDepth, speakeroptions.sampleRate); + this.speaker = new Speaker(2, 16, 44100); + this.buffer.limit = config?.buffer?.limit; + if (isNaN(this.buffer.limit) || this.buffer.limit < this.threshold) { + this.buffer.limit = this.threshold; + } + this.#fillBuffer(); } - async feed(buffer) { - this.tmp.stream.write(buffer); - if (this.tmp.announced === undefined && this.tmp.stream.bytesWritten >= this.threshold) { - this.tmp.announced = true; - this.#setState(constants.STATE_READY); - logger.debug('threshold of ' + this.threshold + ' bytes reached after ' + (Date.now() - this.timestamp) + 'ms'); + #fillBuffer() { + this.stream.on('data', (data) => { + this.buffer.size += data.length; + this.buffer.elements.push(data); + if (this.buffer.announced === undefined && this.buffer.size >= this.threshold) { + this.buffer.announced = true; + this.#setState(constants.STATE_READY); + logger.debug('threshold of ' + this.threshold + ' bytes reached after ' + (Date.now() - this.timestamp) + 'ms'); + } + if (this.buffer.size >= this.buffer.limit) { + this.stream.pause(); + logger.warn('BUFFER LIMIT REACHED - PAUSING STREAM'); + } + this.speak(true); + }); + } + + speak(checkState) { + if (checkState === true && this.isPlaying() !== true) { + return; + } + this.#setState(constants.STATE_PLAYING); + while (this.buffer.elements.length > 0) { + const tmp = this.buffer.elements[0]; + this.buffer.elements.shift(); + this.speaker.pipe(tmp); + this.buffer.size -= tmp.length; + if (this.buffer.size < this.buffer.limit) { + this.stream.resume(); + logger.warn('RESUMING STREAM - BUFFER NOT FILLED'); + } } } + // async feed(buffer) { + // this.tmp.stream.write(buffer); + // if (this.tmp.announced === undefined && this.tmp.stream.bytesWritten >= this.threshold) { + // this.tmp.announced = true; + // this.#setState(constants.STATE_READY); + // logger.debug('threshold of ' + this.threshold + ' bytes reached after ' + (Date.now() - this.timestamp) + 'ms'); + // } + // } + stopFeed() { - logger.debug('finished writing of ' + this.tmp.stream.bytesWritten + ' bytes after ' + (Date.now() - this.timestamp) + 'ms'); - this.tmp.stream.end(); - this.tmp.stream.close(); + // logger.debug('finished writing of ' + this.tmp.stream.bytesWritten + ' bytes after ' + (Date.now() - this.timestamp) + 'ms'); + // this.tmp.stream.end(); + // this.tmp.stream.close(); } async play(position) { diff --git a/classes/Speaker.js b/classes/Speaker.js new file mode 100644 index 0000000..cdd4408 --- /dev/null +++ b/classes/Speaker.js @@ -0,0 +1,82 @@ +const NodeSpeaker = require('speaker'); +const createReadStream = require('fs').createReadStream; + +class Speaker { + + constructor(channels, bitDepth, sampleRate) { + this.#handlePlayer(channels, bitDepth, sampleRate); + this.playback = { + played: 0, + tmp: 0 + }; + } + + pipe(data) { + return this.speaker.write(data); + } + + // pipe(buffer, position) { + // if (buffer === undefined) { + // return; + // } + // this.playback.stream = createReadStream(file); + // if (isNaN(position) || position < 0) { + // position = 0; + // } + // position = 65537* 100; + // this.playback.stream.on('data', (data) => { + // if (position > 0 && this.playback.played <= position) { + // const offset = position - (this.playback.played + data.length); + // if (offset >= 0) { + // this.playback.played += data.length; + // return; + // } + // data = data.subarray(data.length + offset); + // } + // if (this.speaker.write(data) === true) { + // this.playback.played += data.length; + // } else { + // this.playback.tmp = data.length; + // this.playback.stream.pause(); + // } + // }); + // this.playback.stream.on('end', () => { + // logger.debug('read stream ended'); + // }); + // this.playback.stream.on('close', () => { + // logger.debug('read stream closed'); + // }); + // this.playback.stream.on('drain', () => { + // logger.debug('read stream drained'); + // }); + // this.playback.stream.on('error', (error) => { + // logger.debug('read stream encountered an error: ' + error); + // }); + // } + + #handlePlayer(channels, bitDepth, sampleRate) { + this.speaker = new NodeSpeaker({ + channels: channels, + bitDepth: bitDepth, + sampleRate: sampleRate + }); + this.speaker.on('open', () => { + logger.debug('speaker opened...'); + }); + this.speaker.on('flush', () => { + logger.debug('speaker flushed...'); + }); + this.speaker.on('close', () => { + logger.debug('speaker closed...'); + }); + this.speaker.on('drain', () => { + // handle backpressure + // this.playback.played += this.tmp; + // this.playback.tmp = 0; + // this.playback.stream.resume(); + }); + } + +} + +module.exports = Speaker; \ No newline at end of file diff --git a/example_config.json b/example_config.json index 1c59319..71b5513 100644 --- a/example_config.json +++ b/example_config.json @@ -12,5 +12,8 @@ "limit": 0, "delay": 1000 }, + "buffer": { + "limit": 10 + }, "tmp": "/tmp/kannon.tmp" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5c4acfc..94c50b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,58 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "moment": "^2.29.1" + "moment": "^2.29.1", + "speaker": "^0.5.4" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/moment": { "version": "2.29.2", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", @@ -19,13 +68,87 @@ "engines": { "node": "*" } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/speaker": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/speaker/-/speaker-0.5.4.tgz", + "integrity": "sha512-0I35CJGgqU1rd/a3qVysR5gLlG+8QlzJcPAEnYvT0BLfuLdJ7JNdlQHwbh7ETNcXDXbzm2O148GEAoAER54Dvw==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.3.0", + "buffer-alloc": "^1.1.0", + "debug": "^4.0.0" + }, + "engines": { + "node": ">=8.6" + } } }, "dependencies": { + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "requires": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "moment": { "version": "2.29.2", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "speaker": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/speaker/-/speaker-0.5.4.tgz", + "integrity": "sha512-0I35CJGgqU1rd/a3qVysR5gLlG+8QlzJcPAEnYvT0BLfuLdJ7JNdlQHwbh7ETNcXDXbzm2O148GEAoAER54Dvw==", + "requires": { + "bindings": "^1.3.0", + "buffer-alloc": "^1.1.0", + "debug": "^4.0.0" + } } } } diff --git a/package.json b/package.json index 307ebd3..0c84844 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "url": "https://git.velvettear.de/velvettear/kannon-client.git" }, "dependencies": { - "moment": "^2.29.1" + "moment": "^2.29.1", + "speaker": "^0.5.4" } }