From 2a88a897f6e284ed84b1907468858dff2633d884 Mon Sep 17 00:00:00 2001 From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com> Date: Wed, 15 Feb 2023 13:38:21 +0100 Subject: [PATCH] feat: Notifications through AudioClip fixed: #67 (really old issue) --- docs/_config.yml | 2 +- docs/sonos-device/notifications-and-tts.md | 41 ++++++ examples/play-notification-audioclip.js | 51 ++++++++ package-lock.json | 142 +++++++++++++-------- package.json | 4 +- src/sonos-device.ts | 80 ++++++++++++ 6 files changed, 265 insertions(+), 55 deletions(-) create mode 100644 examples/play-notification-audioclip.js diff --git a/docs/_config.yml b/docs/_config.yml index 041c54e..d3c9ff9 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -55,7 +55,7 @@ aux_links: - "//github.com/sponsors/svrooij/" # Footer content appears at the bottom of every page's main content -footer_content: "Copyright © 2022 Stephan van Rooij. Distributed by an MIT license." +footer_content: "Copyright © 2023 Stephan van Rooij. Distributed by an MIT license." toc: max_levels: 2 diff --git a/docs/sonos-device/notifications-and-tts.md b/docs/sonos-device/notifications-and-tts.md index 0f7f8ab..0fc14da 100644 --- a/docs/sonos-device/notifications-and-tts.md +++ b/docs/sonos-device/notifications-and-tts.md @@ -32,6 +32,27 @@ sonos.PlayNotification({ }) ``` +## Notifications with AudioClip + +Sonos has a [native](https://developer.sonos.com/reference/control-api/audioclip/) way to play an audio clip through the music, this used to be cloud only functionality. Until [Thomas discovered](https://github.com/bencevans/node-sonos/issues/530#issuecomment-1430039043) it was also available on the local network. We now have experimental support for this audio clip endpoint, which only works on speakers that are compatible with the S2 app, and we are probably going to replace the custom build version with the native version. + +The native AudioClip does not pause the music, the clip is played over it, not having to revert back to the original music because it is not replaced. Sample code available in [examples](https://github.com/svrooij/node-sonos-ts/tree/master/examples) + +```js +const SonosDevice = require('@svrooij/sonos').SonosDevice +const sonos = new SonosDevice(process.env.SONOS_HOST || '192.168.96.56') + +sonos.PlayNotificationAudioClip({ + trackUri: 'https://cdn.smartersoft-group.com/various/pull-bell-short.mp3', // Can be any uri sonos understands + // trackUri: 'https://cdn.smartersoft-group.com/various/someone-at-the-door.mp3', // Cached text-to-speech file. + onlyWhenPlaying: false, // make sure that it only plays when you're listening to music. So it won't play when you're sleeping. + volume: 15, // Set the volume for the notification (and revert back afterwards) + }) + .then(queued => { + console.log('Queued notification %o', queued) + }) +``` + ## Text to speech A lot of people want to send text to sonos to use for notifications (or a welcome message in your B&B). @@ -55,6 +76,25 @@ We have support for **neural** language engines. Pick a voice that supports it a The server I've build is based on Amazon Polly, but I invite everybody to build their own if you want to support another tts service. +```js +const SonosDevice = require('@svrooij/sonos').SonosDevice +const sonos = new SonosDevice(process.env.SONOS_HOST || '192.168.96.56') +sonos.PlayTTSAudioClip({ + text: 'Someone at the front-door', + lang: 'en-US', + gender: 'male', + volume: 50, + endpoint: 'https://your.tts.endpoint/api/generate' + }) + .then(queued => { + console.log('Queued notification %o', queued) + }) +``` + +### TTS AudioClip + +Text to speech but with the AudioClip endpoint, see [Notifications with Audio Clip](#notifications-with-audioclip) for differences. + ```js const SonosDevice = require('@svrooij/sonos').SonosDevice const sonos = new SonosDevice(process.env.SONOS_HOST || '192.168.96.56') @@ -74,6 +114,7 @@ sonos.PlayTTS({ }) ``` + ## Notifications on all speakers If you use the **SonosManager** (the recommended way to use this library), you can also play a notification on all groups by sending the same command to the SonosManager instead of the individual speaker. diff --git a/examples/play-notification-audioclip.js b/examples/play-notification-audioclip.js new file mode 100644 index 0000000..e55d61d --- /dev/null +++ b/examples/play-notification-audioclip.js @@ -0,0 +1,51 @@ +const SonosDevice = require('../lib').SonosDevice + +const sonos = new SonosDevice(process.env.SONOS_HOST || '192.168.96.56') + +// Pre-start listening for events for more efficient handling. +sonos.Events.on('currentTrack', (track) => { + console.log('TrackChanged %o', track); +}); + + +// setTimeout(async () => { +// // Add a second notification (by some other event) +// await sonos.PlayNotificationAudioClip({ +// trackUri: 'https://cdn.smartersoft-group.com/various/someone-at-the-door.mp3', // Cached text-to-speech file. +// onlyWhenPlaying: false, // make sure that it only plays when you're listening to music. So it won't play when you're sleeping. +// volume: 20, // Set the volume for the notification (and revert back afterwards) +// }); +// await sonos.PlayNotificationAudioClip({ +// trackUri: 'https://cdn.smartersoft-group.com/various/someone-at-the-door.mp3', // Cached text-to-speech file. +// onlyWhenPlaying: false, // make sure that it only plays when you're listening to music. So it won't play when you're sleeping. +// volume: 20, // Set the volume for the notification (and revert back afterwards) +// }); +// }, 500) + +sonos.PlayNotificationAudioClip({ + trackUri: 'https://cdn.smartersoft-group.com/various/pull-bell-short.mp3', // Can be any uri sonos understands + onlyWhenPlaying: false, // make sure that it only plays when you're listening to music. So it won't play when you're sleeping. + volume: 80, // Set the volume for the notification (and revert back afterwards) +}) + .then(queued => { + console.log('Queued notification %o', queued) + + }) + .catch((err) => { + console.error(err); + }) + .finally(() => { + sonos.CancelEvents(); + }); + +// If you have a TTS endpoint, you can do text-to-speech +// PlayTTS() will just call a server to generate the TTS mp3 file and then call PlayNotification(). + +// sonos.PlayTTS({ text: 'Someone at the front-door', lang: 'en-US', gender: 'male', volume: 50 }) +// .then(played => { +// console.log('Played notification %o', played) +// setTimeout(() => { +// process.exit(0) +// }, 2000) +// }) +// .catch(console.error) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 11f3432..231cec9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "guid-typescript": "^1.0.9", "html-entities": "^2.3.2", "node-fetch": "^2.6.1", - "typed-emitter": "^1.3.1" + "typed-emitter": "^1.3.1", + "ws": "^8.12.1" }, "devDependencies": { "@types/chai": "^4.2.16", @@ -22,6 +23,7 @@ "@types/jest": "^26.0.15", "@types/node": "^16.11.7", "@types/node-fetch": "^2.5.10", + "@types/ws": "^8.5.4", "@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/parser": "^3.9.0", "chai": "^4.3.4", @@ -103,13 +105,10 @@ } }, "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, "bin": { "json5": "lib/cli.js" }, @@ -1880,6 +1879,15 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "15.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", @@ -3115,9 +3123,9 @@ "dev": true }, "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, "engines": { "node": ">=0.10" @@ -8526,6 +8534,27 @@ "node": ">=0.4.0" } }, + "node_modules/jsdom/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8563,9 +8592,9 @@ "dev": true }, "node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -10657,8 +10686,9 @@ } }, "node_modules/table/node_modules/ansi-regex": { - "version": "4.1.0", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, "engines": { "node": ">=6" @@ -10868,13 +10898,10 @@ } }, "node_modules/ts-jest/node_modules/json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, "bin": { "json5": "lib/cli.js" }, @@ -11403,16 +11430,15 @@ } }, "node_modules/ws": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", - "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==", - "dev": true, + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -11622,13 +11648,10 @@ } }, "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "semver": { "version": "6.3.0", @@ -13067,6 +13090,15 @@ "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, + "@types/ws": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", + "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "15.0.9", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.9.tgz", @@ -13984,9 +14016,9 @@ "dev": true }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, "deep-eql": { @@ -18058,6 +18090,13 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", "dev": true + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, + "requires": {} } } }, @@ -18092,9 +18131,9 @@ "dev": true }, "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" @@ -19752,8 +19791,9 @@ }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true }, "emoji-regex": { @@ -19915,13 +19955,10 @@ }, "dependencies": { "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true }, "mkdirp": { "version": "1.0.4", @@ -20332,10 +20369,9 @@ } }, "ws": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", - "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==", - "dev": true, + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.12.1.tgz", + "integrity": "sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==", "requires": {} }, "xml-name-validator": { diff --git a/package.json b/package.json index 1848324..498648a 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@types/jest": "^26.0.15", "@types/node": "^16.11.7", "@types/node-fetch": "^2.5.10", + "@types/ws": "^8.5.4", "@typescript-eslint/eslint-plugin": "^3.9.0", "@typescript-eslint/parser": "^3.9.0", "chai": "^4.3.4", @@ -57,7 +58,8 @@ "guid-typescript": "^1.0.9", "html-entities": "^2.3.2", "node-fetch": "^2.6.1", - "typed-emitter": "^1.3.1" + "typed-emitter": "^1.3.1", + "ws": "^8.12.1" }, "files": [ "README.md", diff --git a/src/sonos-device.ts b/src/sonos-device.ts index 2aba1e3..4c47b5c 100644 --- a/src/sonos-device.ts +++ b/src/sonos-device.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'events'; import TypedEmitter from 'typed-emitter'; import fetch from 'node-fetch'; import { parse } from 'fast-xml-parser'; +import WebSocket from 'ws'; import SonosDeviceBase from './sonos-device-base'; import { GetZoneInfoResponse, GetZoneAttributesResponse, AddURIToQueueResponse, AVTransportServiceEvent, RenderingControlServiceEvent, MusicService, AccountData, @@ -617,6 +618,85 @@ export default class SonosDevice extends SonosDeviceBase { // #endregion + // #region Notification AudioClip + /** + * Play an audio clip through the native (local) AudioClip command + * + * @param {PlayNotificationOptions} options The options + * @param {string} [options.trackUri] The uri of the sound to play as notification, can be every supported sonos uri. + * @param {boolean} [options.onlyWhenPlaying] Only play a notification if currently playing music. You don't have to check if the user is home ;) + * @param {number} [options.volume] Change the volume for the notification and revert afterwards. + * @returns {Promise} Returns true when the AudioClip is send to the speaker, false when stopped and onlyWhenPlaying === true + * @remarks This is only supported on S2 speakers, see: https://developer.sonos.com/reference/control-api/audioclip/ + * @experimental This is experimental, do not depend on this. + * @memberof SonosDevice + */ + public async PlayNotificationAudioClip(options: PlayNotificationOptions): Promise { + this.debug('PlayNotificationAudioClip(%o)', options); + if (options.onlyWhenPlaying === true && this.CurrentTransportStateSimple === TransportState.Stopped) { + return false; + } + + if (options.volume !== undefined && (options.volume < 1 || options.volume > 100)) { + throw new Error('Volume needs to be between 1 and 100'); + } + + if (!this.uuid.startsWith('RINCON')) { + await this.LoadUuid(true); + } + + // Have no idea if this should be public + // https://github.com/bencevans/node-sonos/issues/530#issuecomment-1430039043 + const apiKey = '123e4567-e89b-12d3-a456-426655440000'; + return new Promise((resolve, reject) => { + const ws = new WebSocket(`wss://${this.host}:1443/websocket/api`, 'v1.api.smartspeaker.audio', { + headers: { + 'X-Sonos-Api-Key': apiKey, + }, + // Ignore certificate errors + rejectUnauthorized: false, + }); + ws.on('error', (err) => { + reject(err); + }); + // On socket opened send a message, and close the socket + ws.on('open', () => { + ws.send(`[{"namespace":"audioClip:1","command":"loadAudioClip","playerId":"${this.uuid}","sessionId":null,"cmdId":null},{"name": "Sonos TS Notification", "appId": "io.svrooij.sonos-ts", "streamUrl": "${options.trackUri}", "volume": ${options.volume ?? this.volume ?? 25} }]`, (err) => { + ws.close(); + if (err) { + reject(err); + return; + } + resolve(true); + }); + }); + }); + } + + /** + * PlayTTS, but with (experimental) local AudioClip method + * + * @param {PlayTtsOptions} options + * @param {string} options.text Text to request a TTS file for. + * @param {string} options.lang Language to request tts file for. + * @param {string} [options.endpoint] TTS endpoint, see documentation, can also be set by environment variable 'SONOS_TTS_ENDPOINT' + * @param {string} [options.gender] Supply gender, some languages support both genders. + * @param {string} [options.name] Supply voice name, some services support several voices with different names. + * @param {boolean} [options.onlyWhenPlaying] Only play a notification if currently playing music. You don't have to check if the user is home ;) + * @param {number} [options.volume] Change the volume for the notification and revert afterwards. + * @returns {Promise} Returns when added to queue or (for the first) when all notifications have played. + * @experimental This is experimental, do not depend on this. + * @memberof SonosDevice + */ + public async PlayTTSAudioClip(options: PlayTtsOptions): Promise { + this.debug('PlayTTSAudioClip(%o)', options); + + const notificationOptions = await TtsHelper.TtsOptionsToNotification(options); + + return await this.PlayNotificationAudioClip(notificationOptions); + } + + // #endregion /** * Switch the playback to this url. *