From 4f0afcc7cd33f0ce82ef73bcd5604e6116ead7f8 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Tue, 24 Sep 2024 17:13:41 -0400 Subject: [PATCH 01/29] Enable OpenAI service --- src/ServiceManager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ServiceManager.ts b/src/ServiceManager.ts index 6cf63e2..02c4873 100644 --- a/src/ServiceManager.ts +++ b/src/ServiceManager.ts @@ -2,6 +2,7 @@ import {TTSService} from "./services/TTSService"; import TTSPlugin from "./main"; import {SpeechSynthesis} from "./services/SpeechSynthesis"; import {Notice} from "obsidian"; +import { OpenAI } from "./services/OpenAI"; export class ServiceManager { private readonly plugin: TTSPlugin; @@ -10,7 +11,7 @@ export class ServiceManager { constructor(plugin: TTSPlugin) { this.plugin = plugin; this.services.push(new SpeechSynthesis(this.plugin)); - //this.services.push(new OpenAI(this)); + this.services.push(new OpenAI(this.plugin)); } public getServices(): TTSService[] { From 140fa6840e5aa85614b758e1942455980715514c Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Tue, 24 Sep 2024 17:24:52 -0400 Subject: [PATCH 02/29] Enable OpenAI service settings --- src/settings.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index e266695..db1a444 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,6 +2,7 @@ import {ButtonComponent, PluginSettingTab, Setting} from "obsidian"; import {TextInputPrompt} from "./TextInputPrompt"; import TTSPlugin from "./main"; import {LanguageVoiceModal} from "./LanguageVoiceModal"; +import { ServiceConfigurationModal } from "./ServiceConfigurationModal"; export interface LanguageVoiceMap { id: string; @@ -84,7 +85,7 @@ export class TTSSettingsTab extends PluginSettingTab { } for (const voice of voices) { - //dropdown.addOption(`${voice.serviceId}-${voice.id}`, `${voice.serviceName}: ${voice.name}`); + dropdown.addOption(`${voice.serviceId}-${voice.id}`, `${voice.serviceName}: ${voice.name}`); dropdown.addOption(`${voice.serviceId}-${voice.id}`, `${voice.name}`); } dropdown @@ -108,7 +109,7 @@ export class TTSSettingsTab extends PluginSettingTab { }); }); - /*new Setting(containerEl) + new Setting(containerEl) .setName("Services") .setHeading(); @@ -122,7 +123,7 @@ export class TTSSettingsTab extends PluginSettingTab { .onClick(() => { new ServiceConfigurationModal(this.plugin).open(); }); - });*/ + }); new Setting(containerEl) .setName("Language specific voices") From f59c16bd46e5e9f6f841829073c5ca6850572792 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 28 Sep 2024 00:11:54 -0400 Subject: [PATCH 03/29] v0.5.5 Implemented OpenAI and Azure voice services --- manifest.json => obsidian-tts/manifest.json | 20 +- package.json | 14 +- src/ServiceConfigurationModal.ts | 112 +++++++-- src/ServiceManager.ts | 2 + src/services/Azure.ts | 241 ++++++++++++++++++++ src/services/OpenAI.ts | 9 +- src/services/constants.ts | 98 ++++++++ src/settings.ts | 70 +++++- versions.json | 3 +- 9 files changed, 530 insertions(+), 39 deletions(-) rename manifest.json => obsidian-tts/manifest.json (87%) create mode 100644 src/services/Azure.ts create mode 100644 src/services/constants.ts diff --git a/manifest.json b/obsidian-tts/manifest.json similarity index 87% rename from manifest.json rename to obsidian-tts/manifest.json index f5b0c21..25e811c 100644 --- a/manifest.json +++ b/obsidian-tts/manifest.json @@ -1,10 +1,10 @@ -{ - "id": "obsidian-tts", - "name": "Text to Speech", - "version": "0.5.4", - "minAppVersion": "1.4.0", - "description": "Hear your notes.", - "author": "Johannes Theiner", - "authorUrl": "https://github.com/joethei", - "isDesktopOnly": false -} +{ + "id": "obsidian-tts", + "name": "Text to Speech", + "version": "0.5.5", + "minAppVersion": "1.4.0", + "description": "Hear your notes.", + "author": "Johannes Theiner", + "authorUrl": "https://github.com/joethei", + "isDesktopOnly": false +} diff --git a/package.json b/package.json index 23a2ee8..3c8143f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-tts", - "version": "0.5.3", + "version": "0.5.5", "description": "Text to speech for Obsidian. Hear your notes.", "main": "main.js", "scripts": { @@ -15,23 +15,25 @@ "dependencies": { "@cospired/i18n-iso-languages": "^3.1.1", "@types/node": "^16.11.6", + "@vanakat/plugin-api": "0.1.0", "builtin-modules": "^3.2.0", + "microsoft-cognitiveservices-speech-sdk": "^1.40.0", "obsidian": "1.4.11", - "tslib": "2.3.1", - "typescript": "4.4.4", + "soundtouchjs": "^0.1.30", "tinyld": "1.2.3", - "@vanakat/plugin-api": "0.1.0" + "tslib": "2.3.1", + "typescript": "4.4.4" }, "devDependencies": { + "@microsoft/tsdoc": "0.14.1", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "esbuild": "0.13.12", "eslint": "7.32.0", - "@microsoft/tsdoc": "0.14.1", "eslint-plugin-tsdoc": "0.2.16", "typedoc": "0.22.18" }, "overrides": { - "obsidian" : "$obsidian" + "obsidian": "$obsidian" } } diff --git a/src/ServiceConfigurationModal.ts b/src/ServiceConfigurationModal.ts index f0aa862..faef0aa 100644 --- a/src/ServiceConfigurationModal.ts +++ b/src/ServiceConfigurationModal.ts @@ -1,12 +1,16 @@ import {Modal, Setting} from "obsidian"; import TTSPlugin from "./main"; +import {DEFAULT_SETTINGS} from "./settings"; +import {STYLE_OPTIONS, ROLE_OPTIONS, SERVICE_OPTIONS} from "./services/constants"; export class ServiceConfigurationModal extends Modal { plugin: TTSPlugin; + service: string; - constructor(plugin: TTSPlugin) { + constructor(plugin: TTSPlugin, service?: string) { super(plugin.app); this.plugin = plugin; + this.service = service || ''; } display(service?: string): void { @@ -15,19 +19,22 @@ export class ServiceConfigurationModal extends Modal { contentEl.empty(); - new Setting(contentEl) - .setName('Service') - .setDesc('test') - .addDropdown((dropdown) => { - dropdown.addOption('openai', 'OpenAI'); - dropdown.addOption('microsoft', 'Microsoft Azure'); + if (!service) { + new Setting(contentEl) + .setName('Service') + .setDesc('Add a remote voice service') + .addDropdown((dropdown) => { + Object.entries(SERVICE_OPTIONS).forEach(([key, value]) => { + dropdown.addOption(key, value); + }); - dropdown.setValue(service); + dropdown.setValue(service); - dropdown.onChange(async(value) => { - this.display(value); - }) - }); + dropdown.onChange(async(value) => { + this.display(value); + }) + }); + } if (service === 'openai') { new Setting(contentEl) @@ -40,19 +47,88 @@ export class ServiceConfigurationModal extends Modal { this.plugin.settings.services.openai.key = value; await this.plugin.saveSettings(); }); - }) - ; + }); + } + + if (service === 'azure') { + new Setting(contentEl) + .setName('API Key') + .setDesc('Azure speech services API key') + .addText(async text => { + text + .setValue(this.plugin.settings.services.azure.key) + .onChange(async value => { + this.plugin.settings.services.azure.key = value; + await this.plugin.saveSettings(); + }); + }); + new Setting(contentEl) + .setName('Speech Region') + .setDesc('Azure speech services region') + .addText(async text => { + text + .setValue(this.plugin.settings.services.azure.region) + .onChange(async value => { + this.plugin.settings.services.azure.region = value; + await this.plugin.saveSettings(); + }); + }); + new Setting(contentEl) + .setName('Role') + .addDropdown((dropdown) => { + Object.entries(ROLE_OPTIONS).forEach(([key, value]) => { + dropdown.addOption(key, value); + }); + dropdown.setValue(this.plugin.settings.services.azure.role); + dropdown.onChange(async(value) => { + this.plugin.settings.services.azure.role = value; + await this.plugin.saveSettings(); + }) + }); + new Setting(contentEl) + .setName('Style') + .addDropdown((dropdown) => { + Object.entries(STYLE_OPTIONS).forEach(([key, value]) => { + dropdown.addOption(key, value); + }); + dropdown.setValue(this.plugin.settings.services.azure.style); + dropdown.onChange(async(value) => { + this.plugin.settings.services.azure.style = value; + await this.plugin.saveSettings(); + }) + }); + new Setting(contentEl) + .setName("Intensity") + .addSlider(async (slider) => { + slider + .setValue(this.plugin.settings.services.azure.intensity * 100) + .setDynamicTooltip() + .setLimits(0, 200, 1) + .onChange(async (value: number) => { + this.plugin.settings.services.azure.intensity = value / 100; + await this.plugin.saveSettings(); + }); + }).addExtraButton((button) => { + button + .setIcon('reset') + .setTooltip('restore default') + .onClick(async () => { + this.plugin.settings.services.azure.intensity = DEFAULT_SETTINGS.services.azure.intensity; + await this.plugin.saveSettings(); + this.display(); + }); + }); } - } onOpen(): void { //@ts-ignore - this.setTitle('Add new service'); - this.display(); - + if (this.service) this.setTitle(`Configure ${SERVICE_OPTIONS[this.service]} service`); + //@ts-ignore + else this.setTitle('Add new service'); + this.display(this.service); } diff --git a/src/ServiceManager.ts b/src/ServiceManager.ts index 02c4873..7eca98f 100644 --- a/src/ServiceManager.ts +++ b/src/ServiceManager.ts @@ -3,6 +3,7 @@ import TTSPlugin from "./main"; import {SpeechSynthesis} from "./services/SpeechSynthesis"; import {Notice} from "obsidian"; import { OpenAI } from "./services/OpenAI"; +import { Azure } from "./services/Azure"; export class ServiceManager { private readonly plugin: TTSPlugin; @@ -12,6 +13,7 @@ export class ServiceManager { this.plugin = plugin; this.services.push(new SpeechSynthesis(this.plugin)); this.services.push(new OpenAI(this.plugin)); + this.services.push(new Azure(this.plugin)); } public getServices(): TTSService[] { diff --git a/src/services/Azure.ts b/src/services/Azure.ts new file mode 100644 index 0000000..8fb4a16 --- /dev/null +++ b/src/services/Azure.ts @@ -0,0 +1,241 @@ +import {TTSService} from "./TTSService"; +import TTSPlugin from "../main"; +import {requestUrl} from "obsidian"; +import sdk, { + SpeechSynthesisOutputFormat, +} from "microsoft-cognitiveservices-speech-sdk"; + +export class Azure implements TTSService { + plugin: TTSPlugin; + id = "azure"; + name = "Azure"; + + source: AudioBufferSourceNode; + currentTime = 0; + + constructor(plugin: TTSPlugin) { + this.plugin = plugin; + } + + languages: []; + + async getVoices(): Promise<{ id: string; name: string; languages: string[] }[]> { + return [ + { + id: "alloy", + name: "Alloy", + languages: this.languages, + }, + { + id: "en-CA-ClaraNeural", + name: "ClaraNeural", + languages: this.languages, + }, + { + id: "en-CA-LiamNeural", + name: "LiamNeural", + languages: this.languages, + }, + { + id: "en-US-AvaNeural", + name: "AvaNeural", + languages: this.languages, + }, + { + id: "en-US-AndrewNeural", + name: "AndrewNeural", + languages: this.languages, + }, + { + id: "en-US-EmmaNeural", + name: "EmmaNeural", + languages: this.languages, + }, + { + id: "en-US-BrianNeural", + name: "BrianNeural", + languages: this.languages, + }, + { + id: "en-US-JennyNeural", + name: "JennyNeural", + languages: this.languages, + }, + { + id: "en-US-GuyNeural", + name: "GuyNeural", + languages: this.languages, + }, + { + id: "en-US-AriaNeural", + name: "AriaNeural", + languages: this.languages, + }, + { + id: "en-US-DavisNeural", + name: "DavisNeural", + languages: this.languages, + }, + { + id: "en-US-JaneNeural", + name: "JaneNeural", + languages: this.languages, + }, + { + id: "en-US-JasonNeural", + name: "JasonNeural", + languages: this.languages, + }, + { + id: "en-US-SaraNeural", + name: "SaraNeural", + languages: this.languages, + }, + { + id: "en-US-TonyNeural", + name: "TonyNeural", + languages: this.languages, + }, + { + id: "en-US-NancyNeural", + name: "NancyNeural", + languages: this.languages, + }, + { + id: "en-US-AmberNeural", + name: "AmberNeural", + languages: this.languages, + }, + { + id: "en-US-AnaNeural", + name: "AnaNeural", + languages: this.languages, + }, + { + id: "en-US-AshleyNeural", + name: "AshleyNeural", + languages: this.languages, + }, + { + id: "en-US-BrandonNeural", + name: "BrandonNeural", + languages: this.languages, + }, + { + id: "en-US-ChristopherNeural", + name: "ChristopherNeural", + languages: this.languages, + }, + { + id: "en-US-CoraNeural", + name: "CoraNeural", + languages: this.languages, + }, + { + id: "en-US-ElizabethNeural", + name: "ElizabethNeural", + languages: this.languages, + }, + { + id: "en-US-EricNeural", + name: "EricNeural", + languages: this.languages, + }, + { + id: "en-US-JacobNeural", + name: "JacobNeural", + languages: this.languages, + }, + ]; + } + + isConfigured(): boolean { + return this.plugin.settings.services.azure.key.length > 0 && + this.plugin.settings.services.azure.region.length > 0; + } + + isPaused(): boolean { + if(!this.source) return true; + return this.source.context.state === "suspended"; + } + + isSpeaking(): boolean { + if(!this.source) return false; + return this.source.context.state === "running"; + } + + isValid(): boolean { + return this.plugin.settings.services.azure.key.length > 0 && + this.plugin.settings.services.azure.region.length > 0; + } + + pause(): void { + this.currentTime = this.source.context.currentTime; + this.source.stop(); + } + + resume(): void { + this.source.start(this.currentTime); + } + + async sayWithVoice(text: string, voice: string) : Promise { + const speechConfig = sdk.SpeechConfig.fromSubscription( + this.plugin.settings.services.azure.key, + this.plugin.settings.services.azure.region + ); + speechConfig.speechSynthesisVoiceName = voice; + // speechConfig.speechSynthesisOutputFormat = + // audioFormat as unknown as SpeechSynthesisOutputFormat; + // const audioConfig = sdk.AudioConfig.fromDefaultSpeakerOutput(); + const synthesizer = new sdk.SpeechSynthesizer(speechConfig); + + const regionCode = voice.split('-').slice(0, 2).join('-'); + const {role, style, intensity} = this.plugin.settings.services.azure; + + // SSML content + const ssmlContent = text + ? ` + + + + ${text || ""} + + + + ` + : ""; + + // Start the synthesizer and wait for a result. + ssmlContent && + synthesizer.speakSsmlAsync( + ssmlContent, + result => { + if (result) + synthesizer.close(); + if ( + result.reason === + sdk.ResultReason.SynthesizingAudioCompleted + ) { + const audio = result.audioData; + const context = new AudioContext(); + context.decodeAudioData(audio, buffer => { + this.source = context.createBufferSource(); + this.source.buffer = buffer; + this.source.connect(context.destination); + this.source.start(); + }); + return audio; + } + }, + function (e) { + console.log(e); + synthesizer.close(); + } + ); + } + + stop(): void { + this.source.stop(); + } + +} diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index 3846a05..268be2b 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -1,6 +1,7 @@ import {TTSService} from "./TTSService"; import TTSPlugin from "../main"; import {requestUrl} from "obsidian"; +import { PitchShifter } from 'soundtouchjs'; export class OpenAI implements TTSService { plugin: TTSPlugin; @@ -97,8 +98,12 @@ export class OpenAI implements TTSService { const context = new AudioContext(); const buffer = await context.decodeAudioData(audioFile.arrayBuffer); - this.source = context.createBufferSource(); - this.source.buffer = buffer; + + const shifter = new PitchShifter(context, buffer, 1024); + shifter.tempo = this.plugin.settings.rate; + shifter.pitch = this.plugin.settings.pitch; + + this.source = shifter; this.source.connect(context.destination); this.source.start(); } diff --git a/src/services/constants.ts b/src/services/constants.ts new file mode 100644 index 0000000..8175113 --- /dev/null +++ b/src/services/constants.ts @@ -0,0 +1,98 @@ +import { SpeechSynthesisOutputFormat } from "microsoft-cognitiveservices-speech-sdk"; + +export const VOICE_FORMAT_NAMES = [ + "raw-8khz-8bit-mono-mulaw(.wav)", + "riff-16khz-16kbps-mono-siren(.wav)", + "audio-16khz-16kbps-mono-siren(.wav)", + "audio-16khz-32kbitrate-mono-mp3(.mp3)", + "audio-16khz-128kbitrate-mono-mp3(.mp3)", + "audio-16khz-64kbitrate-mono-mp3(.mp3)", + "raw-16khz-16bit-mono-truesilk(.wav)", + "riff-16khz-16bit-mono-pcm(.wav)", + "riff-24khz-16bit-mono-pcm(.wav)", + "riff-8khz-8bit-mono-mulaw(.wav)", + "raw-16khz-16bit-mono-pcm(.wav)", + "raw-24khz-16bit-mono-pcm(.wav)", + "raw-8khz-16bit-mono-pcm(.wav)", + "ogg-16khz-16bit-mono-opus(.wav)", + "raw-48khz-16bit-mono-pcm(.wav)", + "ogg-48khz-16bit-mono-opus(.wav)", + "webm-16khz-16bit-mono-opus(.wav)", + "webm-24khz-16bit-mono-opus(.wav)", + "raw-24khz-16bit-mono-truesilk(.wav)", + "raw-8khz-8bit-mono-alaw(.wav)", + "riff-8khz-8bit-mono-alaw(.wav)", + "webm-24khz-16bit-24kbps-mono-opus(.wav)", + "audio-16khz-16bit-32kbps-mono-opus(.wav)", + "audio-24khz-16bit-48kbps-mono-opus(.wav)", + "audio-24khz-16bit-24kbps-mono-opus(.wav)", + "raw-22050hz-16bit-mono-pcm(.wav)", + "riff-22050hz-16bit-mono-pcm(.wav)", + "raw-44100hz-16bit-mono-pcm(.wav)", + "riff-44100hz-16bit-mono-pcm(.wav)", +]; + +const VOICE_FORMAT_VASL = Object.keys(SpeechSynthesisOutputFormat).filter( + (val) => isNaN(Number(val)) +); + +export const SERVICE_OPTIONS = { + "openai": "OpenAI", + "azure": "Azure", +}; + +export const STYLE_OPTIONS = { + advertisement_upbeat: "advertisement upbeat", + affectionate: "affectionate", + angry: "angry", + assistant: "assistant", + calm: "calm", + chat: "chat", + cheerful: "cheerful", + customerservice: "customerservice", + depressed: "depressed", + disgruntled: "disgruntled", + "documentary-narration": "documentary narration", + embarrassed: "embarrassed", + empathetic: "empathetic", + envious: "envious", + excited: "excited", + fearful: "fearful", + friendly: "friendly", + gentle: "gentle", + hopeful: "hopeful", + lyrical: "lyrical", + "narration-professional": "narration professional", + "narration-relaxed": "narration relaxed", + newscast: "newscast", + "newscast-casual": "newscast casual", + "newscast-formal": "newscast formal", + "poetry-reading": "poetry reading", + sad: "sad", + serious: "serious", + shouting: "shouting", + sports_commentary: "sports commentary", + sports_commentary_excited: "sports commentary excited", + whispering: "whispering", + terrified: "terrified", + unfriendly: "unfriendly", +}; + +export const ROLE_OPTIONS = { + Girl: "Girl", + Boy: "Boy", + YoungAdultFemale: "YoungAdultFemale", + YoungAdultMale: "YoungAdultMale", + OlderAdultFemale: "OlderAdultFemale", + OlderAdultMale: "OlderAdultMale", + SeniorFemale: "SeniorFemale", + SeniorMale: "SeniorMale", +}; + +export const VOICE_FORMAT_MAP = VOICE_FORMAT_NAMES.reduce( + (res, key, index) => ({ + ...res, + [key]: VOICE_FORMAT_VASL[index], + }), + {} +); diff --git a/src/settings.ts b/src/settings.ts index db1a444..e4a19ad 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -10,8 +10,14 @@ export interface LanguageVoiceMap { voice: string; } +export interface TTSDefaultVoices { + [service: string]: string; +} + export interface TTSSettings { defaultVoice: string, + defaultVoices: TTSDefaultVoices; + defaultService: string, pitch: number; rate: number; volume: number; @@ -27,12 +33,21 @@ export interface TTSSettings { services: { openai: { key: string; + }, + azure: { + key: string; + region: string; + role: string; + style: string; + intensity: number; } } } export const DEFAULT_SETTINGS: TTSSettings = { defaultVoice: "", + defaultVoices: {}, + defaultService: "speechSynthesis", pitch: 1, rate: 1, volume: 1, @@ -48,6 +63,13 @@ export const DEFAULT_SETTINGS: TTSSettings = { services: { openai: { key: '', + }, + azure: { + key: '', + region: '', + role: 'OlderAdultFemale', + style: 'chat', + intensity: 1 } } } @@ -85,12 +107,15 @@ export class TTSSettingsTab extends PluginSettingTab { } for (const voice of voices) { - dropdown.addOption(`${voice.serviceId}-${voice.id}`, `${voice.serviceName}: ${voice.name}`); - dropdown.addOption(`${voice.serviceId}-${voice.id}`, `${voice.name}`); + if (voice.serviceId === this.plugin.settings.defaultService) { + dropdown.addOption(`${voice.serviceId}-${voice.id}`, `${voice.name}`); + } } dropdown .setValue(this.plugin.settings.defaultVoice) .onChange(async (value) => { + const serviceKey = `${value.split('-')[0]}Voice`; + this.plugin.settings.defaultVoices[serviceKey] = value; this.plugin.settings.defaultVoice = value; await this.plugin.saveSettings(); }); @@ -124,6 +149,47 @@ export class TTSSettingsTab extends PluginSettingTab { new ServiceConfigurationModal(this.plugin).open(); }); }); + + const servicesEl = containerEl.createDiv("settings-banner", (banner) => { + banner.createEl("h4", { + cls: "setting-item-name", + text: "Configured services", + }); + }); + + for (const service of this.plugin.serviceManager.getServices()) { + if (service.isConfigured() && service.isValid() && service.id !== "speechSynthesis") { + const setting = new Setting(servicesEl); + setting.setName(service.name); + setting.addExtraButton((b) => { + b.setIcon("pencil") + .setTooltip("Edit") + .onClick(() => { + new ServiceConfigurationModal(this.plugin, service.id).open(); + }); + }); + } + } + + new Setting(containerEl) + .setName("Active service") + .setDesc("Select voice service to use") + .addDropdown(async (dropdown) => { + const services = this.plugin.serviceManager.getServices(); + for (const service of services) { + if (service.isConfigured() && service.isValid()) { + dropdown.addOption(service.id, service.name); + } + } + dropdown + .setValue(this.plugin.settings.defaultService) + .onChange(async (value) => { + this.plugin.settings.defaultService = value; + this.plugin.settings.defaultVoice = this.plugin.settings.defaultVoices[`${value}Voice`]; + await this.plugin.saveSettings(); + this.display(); + }); + }); new Setting(containerEl) .setName("Language specific voices") diff --git a/versions.json b/versions.json index f52c457..1f6b47e 100644 --- a/versions.json +++ b/versions.json @@ -10,5 +10,6 @@ "0.5.1": "0.12.0", "0.5.2": "0.12.0", "0.5.3": "1.4.0", - "0.5.4": "1.4.0" + "0.5.4": "1.4.0", + "0.5.5": "1.4.0" } From 3290240c04a9ebc1b444a8c549353f32491ba72c Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 28 Sep 2024 00:17:49 -0400 Subject: [PATCH 04/29] Small build fixes --- .github/workflows/publish.yml | 2 +- src/ServiceManager.ts | 8 +++++++- src/services/Azure.ts | 5 +---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1f88dca..125e310 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,7 +18,7 @@ jobs: - name: Use Node.js uses: actions/setup-node@v1 with: - node-version: "14.x" # You might need to adjust this value to your own version + node-version: "16.x" # You might need to adjust this value to your own version - name: Build id: build run: | diff --git a/src/ServiceManager.ts b/src/ServiceManager.ts index 7eca98f..ff8c560 100644 --- a/src/ServiceManager.ts +++ b/src/ServiceManager.ts @@ -5,6 +5,12 @@ import {Notice} from "obsidian"; import { OpenAI } from "./services/OpenAI"; import { Azure } from "./services/Azure"; +export interface Voice { + service: string; + id: string; + name: string; + languages: string[]; +} export class ServiceManager { private readonly plugin: TTSPlugin; private services: TTSService[] = []; @@ -64,7 +70,7 @@ export class ServiceManager { } - async getVoices() { + async getVoices(): Promise { const voices = []; for (const service of this.services) { for (const voice of await service.getVoices()) { diff --git a/src/services/Azure.ts b/src/services/Azure.ts index 8fb4a16..d8143bd 100644 --- a/src/services/Azure.ts +++ b/src/services/Azure.ts @@ -1,9 +1,6 @@ import {TTSService} from "./TTSService"; import TTSPlugin from "../main"; -import {requestUrl} from "obsidian"; -import sdk, { - SpeechSynthesisOutputFormat, -} from "microsoft-cognitiveservices-speech-sdk"; +import sdk from "microsoft-cognitiveservices-speech-sdk"; export class Azure implements TTSService { plugin: TTSPlugin; From 43509b8c8552180e1e86c488b2c04bad82b50241 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 28 Sep 2024 00:20:44 -0400 Subject: [PATCH 05/29] remove accidental commit of manifest --- obsidian-tts/manifest.json | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 obsidian-tts/manifest.json diff --git a/obsidian-tts/manifest.json b/obsidian-tts/manifest.json deleted file mode 100644 index 25e811c..0000000 --- a/obsidian-tts/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "obsidian-tts", - "name": "Text to Speech", - "version": "0.5.5", - "minAppVersion": "1.4.0", - "description": "Hear your notes.", - "author": "Johannes Theiner", - "authorUrl": "https://github.com/joethei", - "isDesktopOnly": false -} From 610485d1a77531301e6df6175abf3dcc032a594b Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 28 Sep 2024 00:24:36 -0400 Subject: [PATCH 06/29] Put the manifest back where it belongs --- manifest.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 manifest.json diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..25e811c --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "obsidian-tts", + "name": "Text to Speech", + "version": "0.5.5", + "minAppVersion": "1.4.0", + "description": "Hear your notes.", + "author": "Johannes Theiner", + "authorUrl": "https://github.com/joethei", + "isDesktopOnly": false +} From 9eb402412a78d112fc68053ab7948fbae80c33ba Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 10:42:29 -0400 Subject: [PATCH 07/29] Change to rollup from esbuild for worklet support --- esbuild.config.mjs | 29 ------ package.json | 20 +++-- rollup.config.build.mjs | 31 +++++++ rollup.config.dev.mjs | 35 ++++++++ src/ServiceManager.ts | 35 ++++---- src/main.ts | 151 ++++++++++++++++++++++---------- src/services/Azure.ts | 72 +++++++++------ src/services/SpeechSynthesis.ts | 9 ++ src/utils.ts | 3 + 9 files changed, 259 insertions(+), 126 deletions(-) delete mode 100644 esbuild.config.mjs create mode 100644 rollup.config.build.mjs create mode 100644 rollup.config.dev.mjs create mode 100644 src/utils.ts diff --git a/esbuild.config.mjs b/esbuild.config.mjs deleted file mode 100644 index 69ca0f4..0000000 --- a/esbuild.config.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import esbuild from "esbuild"; -import process from "process"; -import builtins from 'builtin-modules' - -const banner = - `/* -THIS IS A GENERATED/BUNDLED FILE BY ESBUILD -if you want to view the source, please visit the github repository of this plugin -https://github.com/joethei/obsidian-tts -*/ -`; - -const prod = (process.argv[2] === 'production'); - -esbuild.build({ - banner: { - js: banner, - }, - entryPoints: ['src/main.ts'], - bundle: true, - external: ['obsidian', 'electron', ...builtins], - format: 'cjs', - watch: !prod, - target: 'es2016', - logLevel: "info", - sourcemap: prod ? false : 'inline', - treeShaking: true, - outfile: 'main.js', -}).catch(() => process.exit(1)); diff --git a/package.json b/package.json index 3c8143f..82156ed 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "Text to speech for Obsidian. Hear your notes.", "main": "main.js", "scripts": { - "dev": "node esbuild.config.mjs", - "build": "node esbuild.config.mjs production", + "dev": "rollup --config rollup.config.dev.mjs -w", + "build": "rollup --config rollup.config.build.mjs", "lint": "eslint . --ext .ts", "docs": "typedoc" }, @@ -13,7 +13,8 @@ "author": "joethei", "license": "GPL-3.0", "dependencies": { - "@cospired/i18n-iso-languages": "^3.1.1", + "@cospired/i18n-iso-languages": "^4.2.0", + "@soundtouchjs/audio-worklet": "^0.1.17", "@types/node": "^16.11.6", "@vanakat/plugin-api": "0.1.0", "builtin-modules": "^3.2.0", @@ -25,15 +26,24 @@ "typescript": "4.4.4" }, "devDependencies": { + "@colingm/rollup-plugin-web-worker-loader": "^1.6.1", "@microsoft/tsdoc": "0.14.1", + "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.0", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", - "esbuild": "0.13.12", "eslint": "7.32.0", "eslint-plugin-tsdoc": "0.2.16", + "rollup": "^4.24.0", "typedoc": "0.22.18" }, "overrides": { - "obsidian": "$obsidian" + "obsidian": "$obsidian", + "@colingm/rollup-plugin-web-worker-loader": { + "rollup": "$rollup" + } } } diff --git a/rollup.config.build.mjs b/rollup.config.build.mjs new file mode 100644 index 0000000..1079d53 --- /dev/null +++ b/rollup.config.build.mjs @@ -0,0 +1,31 @@ +import typescript from '@rollup/plugin-typescript'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import terser from "@rollup/plugin-terser"; +import json from '@rollup/plugin-json'; + +export default { + input: 'src/main.ts', + output: { + dir: '.', + sourcemap: 'inline', + format: 'cjs', + exports: 'default' + }, + external: ['obsidian', 'electron'], + plugins: [ + typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.d.ts'] }), + nodeResolve({browser: true}), + commonjs(), + terser(), + json() + ], + onwarn: function(warning, warner) { + if (warning.code === 'CIRCULAR_DEPENDENCY'){ + if(warning.importer && warning.importer.startsWith('node_modules')){ + return; + } + } + warner(warning); + } +}; diff --git a/rollup.config.dev.mjs b/rollup.config.dev.mjs new file mode 100644 index 0000000..7c5e287 --- /dev/null +++ b/rollup.config.dev.mjs @@ -0,0 +1,35 @@ +import typescript from '@rollup/plugin-typescript'; +import {nodeResolve} from '@rollup/plugin-node-resolve'; +import json from '@rollup/plugin-json'; +import commonjs from '@rollup/plugin-commonjs'; +import copy from 'rollup-plugin-copy' + +export default { + input: 'src/main.ts', + output: { + dir: 'examples/.obsidian/plugins/obsidian-tracker', + sourcemap: 'inline', + format: 'cjs', + exports: 'default' + }, + external: ['obsidian'], + plugins: [ + typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.d.ts'] }), + nodeResolve({browser: true}), + commonjs(), + json(), + copy({ + targets: [ + { src: ['styles.css', 'manifest.json'], dest: 'examples/.obsidian/plugins/obsidian-tracker' } + ] + }) + ], + onwarn: function(warning, warner){ + if (warning.code === 'CIRCULAR_DEPENDENCY'){ + if(warning.importer && warning.importer.startsWith('node_modules')){ + return; + } + } + warner(warning); + } +}; diff --git a/src/ServiceManager.ts b/src/ServiceManager.ts index ff8c560..0ceb940 100644 --- a/src/ServiceManager.ts +++ b/src/ServiceManager.ts @@ -4,6 +4,7 @@ import {SpeechSynthesis} from "./services/SpeechSynthesis"; import {Notice} from "obsidian"; import { OpenAI } from "./services/OpenAI"; import { Azure } from "./services/Azure"; +import { cleanText } from "./utils"; export interface Voice { service: string; @@ -14,12 +15,14 @@ export interface Voice { export class ServiceManager { private readonly plugin: TTSPlugin; private services: TTSService[] = []; + private activeService: TTSService; constructor(plugin: TTSPlugin) { this.plugin = plugin; this.services.push(new SpeechSynthesis(this.plugin)); this.services.push(new OpenAI(this.plugin)); this.services.push(new Azure(this.plugin)); + this.activeService = this.services.find(service => this.plugin.settings.defaultService === service.id); } public getServices(): TTSService[] { @@ -27,35 +30,23 @@ export class ServiceManager { } public isSpeaking(): boolean { - return this.services.some(service => service.isSpeaking()); + return this.activeService.isSpeaking(); } public isPaused(): boolean { - return this.services.every(service => service.isPaused()); + return this.activeService.isPaused(); } stop() : void { - for (const service of this.services) { - if(service.isSpeaking() || service.isPaused()) { - service.stop(); - } - } + this.activeService.stop(); } pause() : void { - for (const service of this.services) { - if(service.isSpeaking()) { - service.pause(); - } - } + this.activeService.pause(); } resume(): void { - for (const service of this.services) { - if(service.isPaused()) { - service.resume(); - } - } + this.activeService.resume(); } async sayWithVoice(text: string, voice: string): Promise { @@ -66,8 +57,7 @@ export class ServiceManager { if(!service) { new Notice("No service found for voice" + voice); } - await service.sayWithVoice(text, voice); - + await service.sayWithVoice(cleanText(text), voice); } async getVoices(): Promise { @@ -85,5 +75,12 @@ export class ServiceManager { return voices; } + get progress(): number { + return this.activeService.progress; + } + + seek(progress: number): void { + this.activeService.seek(progress); + } } diff --git a/src/main.ts b/src/main.ts index 42fe540..8256ace 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,14 +7,25 @@ import {DEFAULT_SETTINGS, LanguageVoiceMap, TTSSettings, TTSSettingsTab} from ". import {registerAPI} from "@vanakat/plugin-api"; import {detect} from "tinyld"; import {ServiceManager} from "./ServiceManager"; +import { cleanText } from './utils'; export default class TTSPlugin extends Plugin { settings: TTSSettings; statusbar: HTMLElement; + menuVisible = false; + statusbarIntervalId: number; + seekbar: HTMLElement; + playButton: HTMLElement; + pauseButton: HTMLElement; + stopButton: HTMLElement; + serviceManager: ServiceManager; + get isPaused(): boolean { + return this.serviceManager.isPaused(); + } async onload(): Promise { // from https://github.com/phosphor-icons/core @@ -120,8 +131,8 @@ export default class TTSPlugin extends Plugin { } }, 1000 * 10)); - this.addRibbonIcon("audio-file", "Text to Speech", async (event) => { - await this.createMenu(event); + this.addRibbonIcon("audio-file", "Text to Speech", async () => { + await this.createMenu(); }); this.registerEvent(this.app.workspace.on('editor-menu', ((menu, _, markdownView) => { @@ -161,26 +172,65 @@ export default class TTSPlugin extends Plugin { this.statusbar.classList.add("mod-clickable"); this.statusbar.setAttribute("aria-label", "Text to Speech"); this.statusbar.setAttribute("aria-label-position", "bottom"); - this.statusbar.onClickEvent(async (event) => { - await this.createMenu(event); + this.statusbar.onClickEvent(async () => { + if (this.menuVisible) { + this.removeMenu(); + return; + } + + this.menuVisible = true; + await this.createMenu(); }); registerAPI("tts", this.serviceManager, this); } - async createMenu(event: MouseEvent): Promise { - const menu = new Menu(); + removeMenu(): void { + this.menuVisible = false; + clearInterval(this.statusbarIntervalId); + this.seekbar.remove(); + this.playButton.remove(); + this.pauseButton.remove(); + this.stopButton.remove(); + } + + async createMenu(): Promise { + const getSeekbarBackgroundStyle = (value: number): string => { + return 'linear-gradient(to right, var(--interactive-accent) 0%, var(--interactive-accent) ' + value + '%, #fff ' + value + '%, white 100%)'; + } + const updateSeekbar = () => { + const value = this.serviceManager.progress; + this.seekbar.querySelector('input').value = value.toString(); + this.seekbar.querySelector('input').style.background = getSeekbarBackgroundStyle(value); + }; + const createPlayButton = () => { + this.playButton = this.addStatusBarItem(); + setIcon(this.playButton, 'play-audio-glyph'); + this.playButton.onClickEvent(() => { + this.serviceManager.resume(); + this.statusbarIntervalId = this.registerInterval(window.setInterval(updateSeekbar, 1000)); + this.playButton.remove(); + createPauseButton(); + }); + } + const createPauseButton = () => { + this.pauseButton = this.addStatusBarItem(); + setIcon(this.pauseButton, 'pause'); + this.pauseButton.onClickEvent(() => { + clearInterval(this.statusbarIntervalId); + this.serviceManager.pause(); + this.pauseButton.remove(); + createPlayButton(); + }); + } const markdownView = this.app.workspace.activeEditor; if (markdownView) { - if (window.speechSynthesis.speaking) { - menu.addItem((item) => { - item - .setIcon("play-audio-glyph") - .setTitle("Add to playback queue") - .onClick((async () => { - await this.play(markdownView); - })); + if (this.serviceManager.isSpeaking()) { + this.playButton = this.addStatusBarItem() + setIcon(this.playButton, 'play-audio-glyph'); + this.playButton.onClickEvent(async () => { + await this.play(markdownView); }); } else { await this.play(markdownView); @@ -188,40 +238,51 @@ export default class TTSPlugin extends Plugin { } } - if (window.speechSynthesis.speaking) { - menu.addItem((item) => { - item - .setIcon("stop-audio-glyph") - .setTitle("Stop") - .onClick(async () => { - this.serviceManager.stop(); - }); - }); + if (this.serviceManager.isSpeaking()) { + // Seekbar + const curProgress = this.serviceManager.progress ? this.serviceManager.progress : 0; + this.seekbar = this.addStatusBarItem(); + const slider = this.seekbar.createEl('input', {type: 'range', attr: {min: '0', max: '100', value: curProgress.toString(), step: '1'}}); + slider.style.background = getSeekbarBackgroundStyle(curProgress); + slider.style.border = "solid 1px var(--interactive-accent)"; + slider.style.borderRadius = "8px"; + slider.style.height = "7px"; + slider.style.width = "200px"; + slider.style.outline = "none"; + slider.style.transition = "background 450ms ease-in"; + slider.oninput = function(this: HTMLInputElement) { + const value = parseInt(this.value); + this.style.background = getSeekbarBackgroundStyle(value); + }; + + this.statusbarIntervalId = this.registerInterval(window.setInterval(updateSeekbar, 1000)); + + this.seekbar.querySelector('input').onmousedown = () => { + if (!this.isPaused) { + clearInterval(this.statusbarIntervalId); + } + }; + this.seekbar.querySelector('input').onchange = (e) => { + this.serviceManager.seek(parseInt((e.target as HTMLInputElement).value)); + if (!this.isPaused) { + this.statusbarIntervalId = this.registerInterval(window.setInterval(updateSeekbar, 1000)); + } + }; + // Stop button + this.stopButton = this.addStatusBarItem(); + setIcon(this.stopButton, 'stop-audio-glyph'); + this.stopButton.onClickEvent(() => { + this.serviceManager.stop(); + this.removeMenu(); + }); - if (window.speechSynthesis.paused) { - menu.addItem((item) => { - item - .setIcon("play-audio-glyph") - .setTitle("Resume") - .onClick(async () => { - this.serviceManager.resume(); - }); - }); + if (this.serviceManager.isPaused()) { + createPlayButton(); } else { - menu.addItem((item) => { - item - .setIcon("paused") - .setTitle("Pause") - .onClick(async () => { - this.serviceManager.pause(); - }); - }); + createPauseButton(); } } - - - menu.showAtPosition({x: event.x, y: event.y}); } async onunload(): Promise { @@ -279,11 +340,11 @@ export default class TTSPlugin extends Plugin { if (service === undefined) { new Notice("TTS: Could not use configured language, please check your settings.\nUsing default voice"); - await this.serviceManager.sayWithVoice(text, this.settings.defaultVoice); + await this.serviceManager.sayWithVoice(cleanText(text), this.settings.defaultVoice); return; } - await service.sayWithVoice(text, split[1]); + await service.sayWithVoice(cleanText(text), split[1]); } prepareText(title: string, text: string): string { diff --git a/src/services/Azure.ts b/src/services/Azure.ts index d8143bd..215f7a1 100644 --- a/src/services/Azure.ts +++ b/src/services/Azure.ts @@ -1,14 +1,21 @@ import {TTSService} from "./TTSService"; import TTSPlugin from "../main"; -import sdk from "microsoft-cognitiveservices-speech-sdk"; +import { + SpeakerAudioDestination, + SpeechConfig, + AudioConfig, + SpeechSynthesizer, + ResultReason +} from "microsoft-cognitiveservices-speech-sdk"; export class Azure implements TTSService { plugin: TTSPlugin; id = "azure"; name = "Azure"; + _isPlaying = false; + _duration = 0; - source: AudioBufferSourceNode; - currentTime = 0; + source: SpeakerAudioDestination | null = null; constructor(plugin: TTSPlugin) { this.plugin = plugin; @@ -152,13 +159,11 @@ export class Azure implements TTSService { } isPaused(): boolean { - if(!this.source) return true; - return this.source.context.state === "suspended"; + return this.source && !this._isPlaying; } isSpeaking(): boolean { - if(!this.source) return false; - return this.source.context.state === "running"; + return this.source ? true : false; } isValid(): boolean { @@ -167,24 +172,25 @@ export class Azure implements TTSService { } pause(): void { - this.currentTime = this.source.context.currentTime; - this.source.stop(); + this._isPlaying = false; + this.source.pause(); } resume(): void { - this.source.start(this.currentTime); + this._isPlaying = true; + this.source.resume(); } async sayWithVoice(text: string, voice: string) : Promise { - const speechConfig = sdk.SpeechConfig.fromSubscription( + const speechConfig = SpeechConfig.fromSubscription( this.plugin.settings.services.azure.key, this.plugin.settings.services.azure.region ); speechConfig.speechSynthesisVoiceName = voice; - // speechConfig.speechSynthesisOutputFormat = - // audioFormat as unknown as SpeechSynthesisOutputFormat; - // const audioConfig = sdk.AudioConfig.fromDefaultSpeakerOutput(); - const synthesizer = new sdk.SpeechSynthesizer(speechConfig); + this.source = new SpeakerAudioDestination(); + // this.source = new AzurePlayer(); + const audioConfig = AudioConfig.fromSpeakerOutput(this.source); + const synthesizer = new SpeechSynthesizer(speechConfig, audioConfig); const regionCode = voice.split('-').slice(0, 2).join('-'); const {role, style, intensity} = this.plugin.settings.services.azure; @@ -207,21 +213,16 @@ export class Azure implements TTSService { synthesizer.speakSsmlAsync( ssmlContent, result => { - if (result) - synthesizer.close(); if ( result.reason === - sdk.ResultReason.SynthesizingAudioCompleted + ResultReason.SynthesizingAudioCompleted ) { - const audio = result.audioData; - const context = new AudioContext(); - context.decodeAudioData(audio, buffer => { - this.source = context.createBufferSource(); - this.source.buffer = buffer; - this.source.connect(context.destination); - this.source.start(); - }); - return audio; + synthesizer.close(); + this._duration = result.audioDuration / 10000000; + this._isPlaying = true; + this.source.onAudioEnd = () => { + this._isPlaying = false; + }; } }, function (e) { @@ -232,7 +233,22 @@ export class Azure implements TTSService { } stop(): void { - this.source.stop(); + this._isPlaying = false; + this.source.pause(); + this.source.close(); + this.source = null; } + get progress(): number { + if (!this.source) return 0; + const currentTime = this.source.internalAudio.currentTime; + const progress = currentTime / this._duration; + return Math.max(0, Math.min(100, progress * 100)); + } + + seek(progress: number): void { + if (!this.source) return; + const currentTime = this._duration * (progress / 100); + this.source.internalAudio.currentTime = currentTime; + } } diff --git a/src/services/SpeechSynthesis.ts b/src/services/SpeechSynthesis.ts index 32bb1f4..23fdd04 100644 --- a/src/services/SpeechSynthesis.ts +++ b/src/services/SpeechSynthesis.ts @@ -6,11 +6,20 @@ export class SpeechSynthesis implements TTSService { plugin: TTSPlugin; id = 'speechSynthesis'; name = 'Speech Synthesis'; + currentTime = 0; constructor(plugin: TTSPlugin) { this.plugin = plugin; } + get progress(): number { + return this.currentTime; + } + + seek(time: number): void { + this.currentTime = time; + } + stop(): void { if (!this.isSpeaking()) return; window.speechSynthesis.cancel(); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..594be86 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,3 @@ +export const cleanText = (text: string): string => { + return text; // TODO: Implement +} From 278ae13e05593095676a9233e7f46df59c0aa68f Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 12:21:13 -0400 Subject: [PATCH 08/29] Audio worklet working with OpenAI service --- esbuild.config.mjs | 29 + package.json | 1 + rollup.config.build.mjs | 6 +- src/main.ts | 1 + src/services/OpenAI.ts | 31 +- src/soundtouch/soundtouch-audio-node.js | 536 +++++++ src/soundtouch/soundtouch-worklet.js | 1898 +++++++++++++++++++++++ tsconfig.json | 5 +- 8 files changed, 2494 insertions(+), 13 deletions(-) create mode 100644 esbuild.config.mjs create mode 100644 src/soundtouch/soundtouch-audio-node.js create mode 100644 src/soundtouch/soundtouch-worklet.js diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..69ca0f4 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,29 @@ +import esbuild from "esbuild"; +import process from "process"; +import builtins from 'builtin-modules' + +const banner = + `/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +https://github.com/joethei/obsidian-tts +*/ +`; + +const prod = (process.argv[2] === 'production'); + +esbuild.build({ + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: ['obsidian', 'electron', ...builtins], + format: 'cjs', + watch: !prod, + target: 'es2016', + logLevel: "info", + sourcemap: prod ? false : 'inline', + treeShaking: true, + outfile: 'main.js', +}).catch(() => process.exit(1)); diff --git a/package.json b/package.json index 82156ed..03d2abb 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "builtin-modules": "^3.2.0", "microsoft-cognitiveservices-speech-sdk": "^1.40.0", "obsidian": "1.4.11", + "regenerator-runtime": "^0.14.1", "soundtouchjs": "^0.1.30", "tinyld": "1.2.3", "tslib": "2.3.1", diff --git a/rollup.config.build.mjs b/rollup.config.build.mjs index 1079d53..5f0279b 100644 --- a/rollup.config.build.mjs +++ b/rollup.config.build.mjs @@ -3,6 +3,7 @@ import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import terser from "@rollup/plugin-terser"; import json from '@rollup/plugin-json'; +import webWorkerLoader from '@colingm/rollup-plugin-web-worker-loader'; export default { input: 'src/main.ts', @@ -17,8 +18,9 @@ export default { typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.d.ts'] }), nodeResolve({browser: true}), commonjs(), - terser(), - json() + // terser(), + json(), + webWorkerLoader({ targetPlatform: 'browser' }) ], onwarn: function(warning, warner) { if (warning.code === 'CIRCULAR_DEPENDENCY'){ diff --git a/src/main.ts b/src/main.ts index 8256ace..277affe 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import {registerAPI} from "@vanakat/plugin-api"; import {detect} from "tinyld"; import {ServiceManager} from "./ServiceManager"; import { cleanText } from './utils'; +import 'regenerator-runtime/runtime'; export default class TTSPlugin extends Plugin { diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index 268be2b..93c0531 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -1,14 +1,16 @@ import {TTSService} from "./TTSService"; import TTSPlugin from "../main"; import {requestUrl} from "obsidian"; -import { PitchShifter } from 'soundtouchjs'; +// @ts-ignore +import registerSoundtouchWorklet from "audio-worklet:../soundtouch/soundtouch-worklet"; +import createSoundTouchNode from '@soundtouchjs/audio-worklet'; export class OpenAI implements TTSService { plugin: TTSPlugin; id = "openai"; name = "OpenAI"; - source: AudioBufferSourceNode; + source: AudioBufferSourceNode | null = null; currentTime = 0; constructor(plugin: TTSPlugin) { @@ -97,15 +99,26 @@ export class OpenAI implements TTSService { const context = new AudioContext(); - const buffer = await context.decodeAudioData(audioFile.arrayBuffer); + // const buffer = await context.decodeAudioData(audioFile.arrayBuffer); - const shifter = new PitchShifter(context, buffer, 1024); - shifter.tempo = this.plugin.settings.rate; - shifter.pitch = this.plugin.settings.pitch; + await registerSoundtouchWorklet(context); + + // const soundtouchWorkletNode = new AudioWorkletNode(context, 'soundtouch-worklet'); - this.source = shifter; - this.source.connect(context.destination); - this.source.start(); + const soundtouch = createSoundTouchNode(context, AudioWorkletNode, audioFile.arrayBuffer); + + soundtouch.on('initialized', () => { + console.log('SoundTouch initialized'); + soundtouch.tempo = this.plugin.settings.rate; + soundtouch.pitch = this.plugin.settings.pitch; + + const bufferNode = soundtouch.connectToBuffer(); // AudioBuffer goes to SoundTouchNode + const gainNode = context.createGain(); + soundtouch.connect(gainNode); // SoundTouch goes to the GainNode + gainNode.connect(context.destination); // GainNode goes to the AudioDestinationNode + + soundtouch.play(); + }); } stop(): void { diff --git a/src/soundtouch/soundtouch-audio-node.js b/src/soundtouch/soundtouch-audio-node.js new file mode 100644 index 0000000..9bc1a30 --- /dev/null +++ b/src/soundtouch/soundtouch-audio-node.js @@ -0,0 +1,536 @@ +/* +* SoundTouch Audio Worklet v0.1.17 AudioWorklet using the +* SoundTouch audio processing library +* +* Copyright (c) Olli Parviainen +* Copyright (c) Ryan Berdeen +* Copyright (c) Jakub Fiala +* Copyright (c) Steve 'Cutter' Blades +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + + if (info.done) { + resolve(value); + } else { + Promise.resolve(value).then(_next, _throw); + } +} + +function _asyncToGenerator(fn) { + return function () { + var self = this, + args = arguments; + return new Promise(function (resolve, reject) { + var gen = fn.apply(self, args); + + function _next(value) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); + } + + function _throw(err) { + asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); + } + + _next(undefined); + }); + }; +} + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", { + writable: false + }); + return Constructor; +} + +function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + Object.defineProperty(subClass, "prototype", { + writable: false + }); + if (superClass) _setPrototypeOf(subClass, superClass); +} + +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); +} + +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); +} + +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); + return true; + } catch (e) { + return false; + } +} + +function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; +} + +function _possibleConstructorReturn(self, call) { + if (call && (typeof call === "object" || typeof call === "function")) { + return call; + } else if (call !== void 0) { + throw new TypeError("Derived constructors may only return object or undefined"); + } + + return _assertThisInitialized(self); +} + +function _createSuper(Derived) { + var hasNativeReflectConstruct = _isNativeReflectConstruct(); + + return function _createSuperInternal() { + var Super = _getPrototypeOf(Derived), + result; + + if (hasNativeReflectConstruct) { + var NewTarget = _getPrototypeOf(this).constructor; + + result = Reflect.construct(Super, arguments, NewTarget); + } else { + result = Super.apply(this, arguments); + } + + return _possibleConstructorReturn(this, result); + }; +} + +var pad = function pad(n, width, z) { + z = z || '0'; + n = n + ''; + return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; +}; +var minsSecs = function minsSecs(secs) { + var mins = Math.floor(secs / 60); + var seconds = secs - mins * 60; + return "".concat(mins, ":").concat(pad(parseInt(seconds), 2)); +}; +var diffSecs = function diffSecs(ms1, ms2) { + return (ms2 - ms1) / 1000; +}; + +var createSoundTouchNode = function createSoundTouchNode(audioCtx, AudioWorkletNode, arrayBuffer, options) { + var SoundTouchNode = function (_AudioWorkletNode) { + _inherits(SoundTouchNode, _AudioWorkletNode); + var _super = _createSuper(SoundTouchNode); + function SoundTouchNode(context, arrayBuffer, options) { + var _this; + _classCallCheck(this, SoundTouchNode); + _this = _super.call(this, context, 'soundtouch-worklet', options); + _this._arrayBuffer = arrayBuffer.slice(0); + _this.listeners = []; + _this.port.onmessage = _this._messageProcessor.bind(_assertThisInitialized(_this)); + _this.sourcePosition = 0; + _this.timePlayed = 0; + _this._startTime = 0; + _this._pauseTime = 0; + _this._playHead = 0; + _this._playing = false; + _this._ready = false; + _this._initialPlay = true; + return _this; + } + _createClass(SoundTouchNode, [{ + key: "formattedDuration", + get: function get() { + return minsSecs(this.duration); + } + }, { + key: "formattedTimePlayed", + get: function get() { + return minsSecs(this.timePlayed); + } + }, { + key: "percentagePlayed", + get: function get() { + return 100 * this.sourcePosition / (this.duration * this.sampleRate); + } + , + set: function set(percentage) { + var duration = this.duration, + sampleRate = this.sampleRate; + this.sourcePosition = parseInt(duration * sampleRate * (percentage / 100)); + this._updateFilterProp('sourcePosition', this.sourcePosition); + this.currentTime = this.duration * percentage / 100; + } + }, { + key: "currentTime", + get: function get() { + if (!this.playing) { + return this._playHead; + } + return this._playHead + diffSecs(this._startTime, new Date().getTime()); + } + , + set: function set(val) { + this._playHead = val; + } + }, { + key: "playing", + get: function get() { + return this._playing; + } + , + set: function set(val) { + this._playing = Boolean(val); + } + }, { + key: "ready", + get: function get() { + return this._ready; + } + , + set: function set(val) { + this._ready = Boolean(val); + } + }, { + key: "sampleRate", + get: function get() { + if (this.audioBuffer) { + return this.audioBuffer.sampleRate; + } + return undefined; + } + }, { + key: "duration", + get: function get() { + if (this.audioBuffer) { + return this.audioBuffer.duration; + } + return undefined; + } + }, { + key: "bufferLength", + get: function get() { + if (this.audioBuffer) { + return this.audioBuffer.length; + } + return undefined; + } + }, { + key: "numberOfChannels", + get: function get() { + if (this.audioBuffer) { + return this.audioBuffer.numberOfChannels; + } + return undefined; + } + }, { + key: "pitch", + set: function set(pitch) { + this._updatePipeProp('pitch', pitch); + } + }, { + key: "pitchSemitones", + set: function set(semitone) { + this._updatePipeProp('pitchSemitones', semitone); + } + }, { + key: "rate", + set: function set(rate) { + this._updatePipeProp('rate', rate); + } + }, { + key: "tempo", + set: function set(tempo) { + this._updatePipeProp('tempo', tempo); + } + }, { + key: "connectToBuffer", + value: function connectToBuffer() { + this.bufferNode = this.context.createBufferSource(); + this.bufferNode.buffer = this.audioBuffer; + this.bufferNode.onended = function () { + return console.log('song ended'); + }; + this.bufferNode.connect(this); + return this.bufferNode; + } + }, { + key: "disconnectFromBuffer", + value: function disconnectFromBuffer() { + this.bufferNode.disconnect(); + } + }, { + key: "handleAudioData", + value: function handleAudioData(audioBuffer) { + this.audioBuffer = audioBuffer; + this.port.postMessage({ + message: 'INITIALIZE_PROCESSOR', + detail: this.createBaseArray(audioBuffer) + }); + } + }, { + key: "createBaseArray", + value: function createBaseArray(audioBuffer) { + return [{ + sampleRate: this.sampleRate, + duration: this.duration, + bufferLength: this.bufferLength, + numberOfChannels: this.numberOfChannels + }, audioBuffer.getChannelData(0), this.numberOfChannels > 1 ? audioBuffer.getChannelData(1) : audioBuffer.getChannelData(0)]; + } + }, { + key: "play", + value: function () { + var _play = _asyncToGenerator( regeneratorRuntime.mark(function _callee() { + return regeneratorRuntime.wrap(function _callee$(_context) { + while (1) { + switch (_context.prev = _context.next) { + case 0: + if (this.ready) { + _context.next = 2; + break; + } + throw new Error('Your processor is not ready yet'); + case 2: + if (this.playing) { + this.stop(true); + } + if (this._initialPlay) { + if (this._playHead === 0) { + this.percentagePlayed = 0; + } + this._initialPlay = false; + } + _context.next = 6; + return this.context.resume(); + case 6: + this._startTime = new Date().getTime(); + this.playing = true; + case 8: + case "end": + return _context.stop(); + } + } + }, _callee, this); + })); + function play() { + return _play.apply(this, arguments); + } + return play; + }() + }, { + key: "pause", + value: function pause() { + var currTime = this.currentTime; + this.stop(); + this.currentTime = currTime; + } + }, { + key: "stop", + value: function () { + var _stop = _asyncToGenerator( regeneratorRuntime.mark(function _callee2() { + return regeneratorRuntime.wrap(function _callee2$(_context2) { + while (1) { + switch (_context2.prev = _context2.next) { + case 0: + _context2.next = 2; + return this.context.suspend(); + case 2: + this.currentTime = 0; + this._startTime = new Date().getTime(); + this.playing = false; + case 5: + case "end": + return _context2.stop(); + } + } + }, _callee2, this); + })); + function stop() { + return _stop.apply(this, arguments); + } + return stop; + }() + }, { + key: "on", + value: function on(eventName, cb) { + this.listeners.push({ + name: eventName, + cb: cb + }); + this.addEventListener(eventName, function (event) { + return cb(event.detail); + }); + } + }, { + key: "off", + value: function off() { + var _this2 = this; + var eventName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var listeners = this.listeners; + if (eventName) { + listeners = listeners.filter(function (e) { + return e.name === eventName; + }); + } + listeners.forEach(function (e) { + _this2.removeEventListener(e.name, function (event) { + return e.cb(event.detail); + }); + }); + } + }, { + key: "onprocessorerror", + value: function onprocessorerror(err) { + throw err; + } + }, { + key: "_updatePipeProp", + value: function _updatePipeProp(name, value) { + this.port.postMessage({ + message: 'SET_PIPE_PROP', + detail: { + name: name, + value: value + } + }); + } + }, { + key: "_updateFilterProp", + value: function _updateFilterProp(name, value) { + this.port.postMessage({ + message: 'SET_FILTER_PROP', + detail: { + name: name, + value: value + } + }); + } + }, { + key: "_messageProcessor", + value: function _messageProcessor(eventFromWorker) { + var _this3 = this; + var _eventFromWorker$data = eventFromWorker.data, + message = _eventFromWorker$data.message, + detail = _eventFromWorker$data.detail; + var sampleRate = this.sampleRate, + currentTime = this.timePlayed; + if (message === 'SOURCEPOSITION') { + this.sourcePosition = detail; + var timePlayed = detail / sampleRate; + if (currentTime !== timePlayed) { + this.timePlayed = timePlayed; + var timeEvent = new CustomEvent('play', { + detail: { + timePlayed: this.timePlayed, + formattedTimePlayed: this.formattedTimePlayed, + percentagePlayed: this.percentagePlayed + } + }); + this.dispatchEvent(timeEvent); + } + } + if (message === 'PROCESSOR_CONSTRUCTOR') { + this.context.decodeAudioData(this._arrayBuffer, function (audioData) { + return _this3.handleAudioData(audioData); + }, function (err) { + return console.log('[decodeAudioData ERROR] ', err); + }); + return; + } + if (message === 'PROCESSOR_READY') { + this.ready = true; + if (typeof this.onInitialized === 'function') { + this.onInitialized(detail); + return; + } + var init = new CustomEvent('initialized', detail); + this.dispatchEvent(init); + return; + } + if (message === 'PROCESSOR_END') { + this.stop(); + this.percentagePlayed = 0; + var endOfPlay = new CustomEvent('end', { + detail: { + timePlayed: this.currentTime, + formattedTimePlayed: this.formattedTimePlayed, + percentagePlayed: this.percentagePlayed + } + }); + this.dispatchEvent(endOfPlay); + return; + } + } + }]); + return SoundTouchNode; + }(AudioWorkletNode); + return new SoundTouchNode(audioCtx, arrayBuffer, options); +}; + +export { createSoundTouchNode as default }; +//# sourceMappingURL=soundtouch-audio-node.js.map diff --git a/src/soundtouch/soundtouch-worklet.js b/src/soundtouch/soundtouch-worklet.js new file mode 100644 index 0000000..70c4147 --- /dev/null +++ b/src/soundtouch/soundtouch-worklet.js @@ -0,0 +1,1898 @@ +/* +* SoundTouch Audio Worklet v0.1.17 AudioWorklet using the +* SoundTouch audio processing library +* +* Copyright (c) Olli Parviainen +* Copyright (c) Ryan Berdeen +* Copyright (c) Jakub Fiala +* Copyright (c) Steve 'Cutter' Blades +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +*/ + +'use strict'; + +function _typeof(obj) { + "@babel/helpers - typeof"; + + return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { + return typeof obj; + } : function (obj) { + return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }, _typeof(obj); +} + +function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +} + +function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } +} + +function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + Object.defineProperty(Constructor, "prototype", { + writable: false + }); + return Constructor; +} + +function _inherits(subClass, superClass) { + if (typeof superClass !== "function" && superClass !== null) { + throw new TypeError("Super expression must either be null or a function"); + } + + subClass.prototype = Object.create(superClass && superClass.prototype, { + constructor: { + value: subClass, + writable: true, + configurable: true + } + }); + Object.defineProperty(subClass, "prototype", { + writable: false + }); + if (superClass) _setPrototypeOf(subClass, superClass); +} + +function _getPrototypeOf(o) { + _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { + return o.__proto__ || Object.getPrototypeOf(o); + }; + return _getPrototypeOf(o); +} + +function _setPrototypeOf(o, p) { + _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { + o.__proto__ = p; + return o; + }; + + return _setPrototypeOf(o, p); +} + +function _isNativeReflectConstruct() { + if (typeof Reflect === "undefined" || !Reflect.construct) return false; + if (Reflect.construct.sham) return false; + if (typeof Proxy === "function") return true; + + try { + Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); + return true; + } catch (e) { + return false; + } +} + +function _construct(Parent, args, Class) { + if (_isNativeReflectConstruct()) { + _construct = Reflect.construct; + } else { + _construct = function _construct(Parent, args, Class) { + var a = [null]; + a.push.apply(a, args); + var Constructor = Function.bind.apply(Parent, a); + var instance = new Constructor(); + if (Class) _setPrototypeOf(instance, Class.prototype); + return instance; + }; + } + + return _construct.apply(null, arguments); +} + +function _isNativeFunction(fn) { + return Function.toString.call(fn).indexOf("[native code]") !== -1; +} + +function _wrapNativeSuper(Class) { + var _cache = typeof Map === "function" ? new Map() : undefined; + + _wrapNativeSuper = function _wrapNativeSuper(Class) { + if (Class === null || !_isNativeFunction(Class)) return Class; + + if (typeof Class !== "function") { + throw new TypeError("Super expression must either be null or a function"); + } + + if (typeof _cache !== "undefined") { + if (_cache.has(Class)) return _cache.get(Class); + + _cache.set(Class, Wrapper); + } + + function Wrapper() { + return _construct(Class, arguments, _getPrototypeOf(this).constructor); + } + + Wrapper.prototype = Object.create(Class.prototype, { + constructor: { + value: Wrapper, + enumerable: false, + writable: true, + configurable: true + } + }); + return _setPrototypeOf(Wrapper, Class); + }; + + return _wrapNativeSuper(Class); +} + +function _assertThisInitialized(self) { + if (self === void 0) { + throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); + } + + return self; +} + +function _possibleConstructorReturn(self, call) { + if (call && (typeof call === "object" || typeof call === "function")) { + return call; + } else if (call !== void 0) { + throw new TypeError("Derived constructors may only return object or undefined"); + } + + return _assertThisInitialized(self); +} + +function _createSuper(Derived) { + var hasNativeReflectConstruct = _isNativeReflectConstruct(); + + return function _createSuperInternal() { + var Super = _getPrototypeOf(Derived), + result; + + if (hasNativeReflectConstruct) { + var NewTarget = _getPrototypeOf(this).constructor; + + result = Reflect.construct(Super, arguments, NewTarget); + } else { + result = Super.apply(this, arguments); + } + + return _possibleConstructorReturn(this, result); + }; +} + +function _superPropBase(object, property) { + while (!Object.prototype.hasOwnProperty.call(object, property)) { + object = _getPrototypeOf(object); + if (object === null) break; + } + + return object; +} + +function _get() { + if (typeof Reflect !== "undefined" && Reflect.get) { + _get = Reflect.get; + } else { + _get = function _get(target, property, receiver) { + var base = _superPropBase(target, property); + + if (!base) return; + var desc = Object.getOwnPropertyDescriptor(base, property); + + if (desc.get) { + return desc.get.call(arguments.length < 3 ? target : receiver); + } + + return desc.value; + }; + } + + return _get.apply(this, arguments); +} + +function _slicedToArray(arr, i) { + return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); +} + +function _arrayWithHoles(arr) { + if (Array.isArray(arr)) return arr; +} + +function _iterableToArrayLimit(arr, i) { + var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; + + if (_i == null) return; + var _arr = []; + var _n = true; + var _d = false; + + var _s, _e; + + try { + for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { + _arr.push(_s.value); + + if (i && _arr.length === i) break; + } + } catch (err) { + _d = true; + _e = err; + } finally { + try { + if (!_n && _i["return"] != null) _i["return"](); + } finally { + if (_d) throw _e; + } + } + + return _arr; +} + +function _unsupportedIterableToArray(o, minLen) { + if (!o) return; + if (typeof o === "string") return _arrayLikeToArray(o, minLen); + var n = Object.prototype.toString.call(o).slice(8, -1); + if (n === "Object" && o.constructor) n = o.constructor.name; + if (n === "Map" || n === "Set") return Array.from(o); + if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); +} + +function _arrayLikeToArray(arr, len) { + if (len == null || len > arr.length) len = arr.length; + + for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; + + return arr2; +} + +function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); +} + +var runtime = function (exports) { + var Op = Object.prototype; + var hasOwn = Op.hasOwnProperty; + var undefined$1; + var $Symbol = typeof Symbol === "function" ? Symbol : {}; + var iteratorSymbol = $Symbol.iterator || "@@iterator"; + var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; + var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; + function define(obj, key, value) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + return obj[key]; + } + try { + define({}, ""); + } catch (err) { + define = function define(obj, key, value) { + return obj[key] = value; + }; + } + function wrap(innerFn, outerFn, self, tryLocsList) { + var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; + var generator = Object.create(protoGenerator.prototype); + var context = new Context(tryLocsList || []); + generator._invoke = makeInvokeMethod(innerFn, self, context); + return generator; + } + exports.wrap = wrap; + function tryCatch(fn, obj, arg) { + try { + return { + type: "normal", + arg: fn.call(obj, arg) + }; + } catch (err) { + return { + type: "throw", + arg: err + }; + } + } + var GenStateSuspendedStart = "suspendedStart"; + var GenStateSuspendedYield = "suspendedYield"; + var GenStateExecuting = "executing"; + var GenStateCompleted = "completed"; + var ContinueSentinel = {}; + function Generator() {} + function GeneratorFunction() {} + function GeneratorFunctionPrototype() {} + var IteratorPrototype = {}; + define(IteratorPrototype, iteratorSymbol, function () { + return this; + }); + var getProto = Object.getPrototypeOf; + var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); + if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { + IteratorPrototype = NativeIteratorPrototype; + } + var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); + GeneratorFunction.prototype = GeneratorFunctionPrototype; + define(Gp, "constructor", GeneratorFunctionPrototype); + define(GeneratorFunctionPrototype, "constructor", GeneratorFunction); + GeneratorFunction.displayName = define(GeneratorFunctionPrototype, toStringTagSymbol, "GeneratorFunction"); + function defineIteratorMethods(prototype) { + ["next", "throw", "return"].forEach(function (method) { + define(prototype, method, function (arg) { + return this._invoke(method, arg); + }); + }); + } + exports.isGeneratorFunction = function (genFun) { + var ctor = typeof genFun === "function" && genFun.constructor; + return ctor ? ctor === GeneratorFunction || + (ctor.displayName || ctor.name) === "GeneratorFunction" : false; + }; + exports.mark = function (genFun) { + if (Object.setPrototypeOf) { + Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); + } else { + genFun.__proto__ = GeneratorFunctionPrototype; + define(genFun, toStringTagSymbol, "GeneratorFunction"); + } + genFun.prototype = Object.create(Gp); + return genFun; + }; + exports.awrap = function (arg) { + return { + __await: arg + }; + }; + function AsyncIterator(generator, PromiseImpl) { + function invoke(method, arg, resolve, reject) { + var record = tryCatch(generator[method], generator, arg); + if (record.type === "throw") { + reject(record.arg); + } else { + var result = record.arg; + var value = result.value; + if (value && _typeof(value) === "object" && hasOwn.call(value, "__await")) { + return PromiseImpl.resolve(value.__await).then(function (value) { + invoke("next", value, resolve, reject); + }, function (err) { + invoke("throw", err, resolve, reject); + }); + } + return PromiseImpl.resolve(value).then(function (unwrapped) { + result.value = unwrapped; + resolve(result); + }, function (error) { + return invoke("throw", error, resolve, reject); + }); + } + } + var previousPromise; + function enqueue(method, arg) { + function callInvokeWithMethodAndArg() { + return new PromiseImpl(function (resolve, reject) { + invoke(method, arg, resolve, reject); + }); + } + return previousPromise = + previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, + callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); + } + this._invoke = enqueue; + } + defineIteratorMethods(AsyncIterator.prototype); + define(AsyncIterator.prototype, asyncIteratorSymbol, function () { + return this; + }); + exports.AsyncIterator = AsyncIterator; + exports.async = function (innerFn, outerFn, self, tryLocsList, PromiseImpl) { + if (PromiseImpl === void 0) PromiseImpl = Promise; + var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList), PromiseImpl); + return exports.isGeneratorFunction(outerFn) ? iter + : iter.next().then(function (result) { + return result.done ? result.value : iter.next(); + }); + }; + function makeInvokeMethod(innerFn, self, context) { + var state = GenStateSuspendedStart; + return function invoke(method, arg) { + if (state === GenStateExecuting) { + throw new Error("Generator is already running"); + } + if (state === GenStateCompleted) { + if (method === "throw") { + throw arg; + } + return doneResult(); + } + context.method = method; + context.arg = arg; + while (true) { + var delegate = context.delegate; + if (delegate) { + var delegateResult = maybeInvokeDelegate(delegate, context); + if (delegateResult) { + if (delegateResult === ContinueSentinel) continue; + return delegateResult; + } + } + if (context.method === "next") { + context.sent = context._sent = context.arg; + } else if (context.method === "throw") { + if (state === GenStateSuspendedStart) { + state = GenStateCompleted; + throw context.arg; + } + context.dispatchException(context.arg); + } else if (context.method === "return") { + context.abrupt("return", context.arg); + } + state = GenStateExecuting; + var record = tryCatch(innerFn, self, context); + if (record.type === "normal") { + state = context.done ? GenStateCompleted : GenStateSuspendedYield; + if (record.arg === ContinueSentinel) { + continue; + } + return { + value: record.arg, + done: context.done + }; + } else if (record.type === "throw") { + state = GenStateCompleted; + context.method = "throw"; + context.arg = record.arg; + } + } + }; + } + function maybeInvokeDelegate(delegate, context) { + var method = delegate.iterator[context.method]; + if (method === undefined$1) { + context.delegate = null; + if (context.method === "throw") { + if (delegate.iterator["return"]) { + context.method = "return"; + context.arg = undefined$1; + maybeInvokeDelegate(delegate, context); + if (context.method === "throw") { + return ContinueSentinel; + } + } + context.method = "throw"; + context.arg = new TypeError("The iterator does not provide a 'throw' method"); + } + return ContinueSentinel; + } + var record = tryCatch(method, delegate.iterator, context.arg); + if (record.type === "throw") { + context.method = "throw"; + context.arg = record.arg; + context.delegate = null; + return ContinueSentinel; + } + var info = record.arg; + if (!info) { + context.method = "throw"; + context.arg = new TypeError("iterator result is not an object"); + context.delegate = null; + return ContinueSentinel; + } + if (info.done) { + context[delegate.resultName] = info.value; + context.next = delegate.nextLoc; + if (context.method !== "return") { + context.method = "next"; + context.arg = undefined$1; + } + } else { + return info; + } + context.delegate = null; + return ContinueSentinel; + } + defineIteratorMethods(Gp); + define(Gp, toStringTagSymbol, "Generator"); + define(Gp, iteratorSymbol, function () { + return this; + }); + define(Gp, "toString", function () { + return "[object Generator]"; + }); + function pushTryEntry(locs) { + var entry = { + tryLoc: locs[0] + }; + if (1 in locs) { + entry.catchLoc = locs[1]; + } + if (2 in locs) { + entry.finallyLoc = locs[2]; + entry.afterLoc = locs[3]; + } + this.tryEntries.push(entry); + } + function resetTryEntry(entry) { + var record = entry.completion || {}; + record.type = "normal"; + delete record.arg; + entry.completion = record; + } + function Context(tryLocsList) { + this.tryEntries = [{ + tryLoc: "root" + }]; + tryLocsList.forEach(pushTryEntry, this); + this.reset(true); + } + exports.keys = function (object) { + var keys = []; + for (var key in object) { + keys.push(key); + } + keys.reverse(); + return function next() { + while (keys.length) { + var key = keys.pop(); + if (key in object) { + next.value = key; + next.done = false; + return next; + } + } + next.done = true; + return next; + }; + }; + function values(iterable) { + if (iterable) { + var iteratorMethod = iterable[iteratorSymbol]; + if (iteratorMethod) { + return iteratorMethod.call(iterable); + } + if (typeof iterable.next === "function") { + return iterable; + } + if (!isNaN(iterable.length)) { + var i = -1, + next = function next() { + while (++i < iterable.length) { + if (hasOwn.call(iterable, i)) { + next.value = iterable[i]; + next.done = false; + return next; + } + } + next.value = undefined$1; + next.done = true; + return next; + }; + return next.next = next; + } + } + return { + next: doneResult + }; + } + exports.values = values; + function doneResult() { + return { + value: undefined$1, + done: true + }; + } + Context.prototype = { + constructor: Context, + reset: function reset(skipTempReset) { + this.prev = 0; + this.next = 0; + this.sent = this._sent = undefined$1; + this.done = false; + this.delegate = null; + this.method = "next"; + this.arg = undefined$1; + this.tryEntries.forEach(resetTryEntry); + if (!skipTempReset) { + for (var name in this) { + if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { + this[name] = undefined$1; + } + } + } + }, + stop: function stop() { + this.done = true; + var rootEntry = this.tryEntries[0]; + var rootRecord = rootEntry.completion; + if (rootRecord.type === "throw") { + throw rootRecord.arg; + } + return this.rval; + }, + dispatchException: function dispatchException(exception) { + if (this.done) { + throw exception; + } + var context = this; + function handle(loc, caught) { + record.type = "throw"; + record.arg = exception; + context.next = loc; + if (caught) { + context.method = "next"; + context.arg = undefined$1; + } + return !!caught; + } + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + var record = entry.completion; + if (entry.tryLoc === "root") { + return handle("end"); + } + if (entry.tryLoc <= this.prev) { + var hasCatch = hasOwn.call(entry, "catchLoc"); + var hasFinally = hasOwn.call(entry, "finallyLoc"); + if (hasCatch && hasFinally) { + if (this.prev < entry.catchLoc) { + return handle(entry.catchLoc, true); + } else if (this.prev < entry.finallyLoc) { + return handle(entry.finallyLoc); + } + } else if (hasCatch) { + if (this.prev < entry.catchLoc) { + return handle(entry.catchLoc, true); + } + } else if (hasFinally) { + if (this.prev < entry.finallyLoc) { + return handle(entry.finallyLoc); + } + } else { + throw new Error("try statement without catch or finally"); + } + } + } + }, + abrupt: function abrupt(type, arg) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { + var finallyEntry = entry; + break; + } + } + if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { + finallyEntry = null; + } + var record = finallyEntry ? finallyEntry.completion : {}; + record.type = type; + record.arg = arg; + if (finallyEntry) { + this.method = "next"; + this.next = finallyEntry.finallyLoc; + return ContinueSentinel; + } + return this.complete(record); + }, + complete: function complete(record, afterLoc) { + if (record.type === "throw") { + throw record.arg; + } + if (record.type === "break" || record.type === "continue") { + this.next = record.arg; + } else if (record.type === "return") { + this.rval = this.arg = record.arg; + this.method = "return"; + this.next = "end"; + } else if (record.type === "normal" && afterLoc) { + this.next = afterLoc; + } + return ContinueSentinel; + }, + finish: function finish(finallyLoc) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.finallyLoc === finallyLoc) { + this.complete(entry.completion, entry.afterLoc); + resetTryEntry(entry); + return ContinueSentinel; + } + } + }, + "catch": function _catch(tryLoc) { + for (var i = this.tryEntries.length - 1; i >= 0; --i) { + var entry = this.tryEntries[i]; + if (entry.tryLoc === tryLoc) { + var record = entry.completion; + if (record.type === "throw") { + var thrown = record.arg; + resetTryEntry(entry); + } + return thrown; + } + } + throw new Error("illegal catch attempt"); + }, + delegateYield: function delegateYield(iterable, resultName, nextLoc) { + this.delegate = { + iterator: values(iterable), + resultName: resultName, + nextLoc: nextLoc + }; + if (this.method === "next") { + this.arg = undefined$1; + } + return ContinueSentinel; + } + }; + return exports; +}( +(typeof module === "undefined" ? "undefined" : _typeof(module)) === "object" ? module.exports : {}); +try { + regeneratorRuntime = runtime; +} catch (accidentalStrictMode) { + if ((typeof globalThis === "undefined" ? "undefined" : _typeof(globalThis)) === "object") { + globalThis.regeneratorRuntime = runtime; + } else { + Function("r", "regeneratorRuntime = r")(runtime); + } +} + +var FifoSampleBuffer = function () { + function FifoSampleBuffer() { + _classCallCheck(this, FifoSampleBuffer); + this._vector = new Float32Array(); + this._position = 0; + this._frameCount = 0; + } + _createClass(FifoSampleBuffer, [{ + key: "vector", + get: function get() { + return this._vector; + } + }, { + key: "position", + get: function get() { + return this._position; + } + }, { + key: "startIndex", + get: function get() { + return this._position * 2; + } + }, { + key: "frameCount", + get: function get() { + return this._frameCount; + } + }, { + key: "endIndex", + get: function get() { + return (this._position + this._frameCount) * 2; + } + }, { + key: "clear", + value: function clear() { + this.receive(this._frameCount); + this.rewind(); + } + }, { + key: "put", + value: function put(numFrames) { + this._frameCount += numFrames; + } + }, { + key: "putSamples", + value: function putSamples(samples, position) { + var numFrames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + position = position || 0; + var sourceOffset = position * 2; + if (!(numFrames >= 0)) { + numFrames = (samples.length - sourceOffset) / 2; + } + var numSamples = numFrames * 2; + this.ensureCapacity(numFrames + this._frameCount); + var destOffset = this.endIndex; + this.vector.set(samples.subarray(sourceOffset, sourceOffset + numSamples), destOffset); + this._frameCount += numFrames; + } + }, { + key: "putBuffer", + value: function putBuffer(buffer, position) { + var numFrames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + position = position || 0; + if (!(numFrames >= 0)) { + numFrames = buffer.frameCount - position; + } + this.putSamples(buffer.vector, buffer.position + position, numFrames); + } + }, { + key: "receive", + value: function receive(numFrames) { + if (!(numFrames >= 0) || numFrames > this._frameCount) { + numFrames = this.frameCount; + } + this._frameCount -= numFrames; + this._position += numFrames; + } + }, { + key: "receiveSamples", + value: function receiveSamples(output) { + var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + var numSamples = numFrames * 2; + var sourceOffset = this.startIndex; + output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples)); + this.receive(numFrames); + } + }, { + key: "extract", + value: function extract(output) { + var position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + var numFrames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + var sourceOffset = this.startIndex + position * 2; + var numSamples = numFrames * 2; + output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples)); + } + }, { + key: "ensureCapacity", + value: function ensureCapacity() { + var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + var minLength = parseInt(numFrames * 2); + if (this._vector.length < minLength) { + var newVector = new Float32Array(minLength); + newVector.set(this._vector.subarray(this.startIndex, this.endIndex)); + this._vector = newVector; + this._position = 0; + } else { + this.rewind(); + } + } + }, { + key: "ensureAdditionalCapacity", + value: function ensureAdditionalCapacity() { + var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + this.ensureCapacity(this._frameCount + numFrames); + } + }, { + key: "rewind", + value: function rewind() { + if (this._position > 0) { + this._vector.set(this._vector.subarray(this.startIndex, this.endIndex)); + this._position = 0; + } + } + }]); + return FifoSampleBuffer; +}(); +var AbstractFifoSamplePipe = function () { + function AbstractFifoSamplePipe(createBuffers) { + _classCallCheck(this, AbstractFifoSamplePipe); + if (createBuffers) { + this._inputBuffer = new FifoSampleBuffer(); + this._outputBuffer = new FifoSampleBuffer(); + } else { + this._inputBuffer = this._outputBuffer = null; + } + } + _createClass(AbstractFifoSamplePipe, [{ + key: "inputBuffer", + get: function get() { + return this._inputBuffer; + }, + set: function set(inputBuffer) { + this._inputBuffer = inputBuffer; + } + }, { + key: "outputBuffer", + get: function get() { + return this._outputBuffer; + }, + set: function set(outputBuffer) { + this._outputBuffer = outputBuffer; + } + }, { + key: "clear", + value: function clear() { + this._inputBuffer.clear(); + this._outputBuffer.clear(); + } + }]); + return AbstractFifoSamplePipe; +}(); +var RateTransposer = function (_AbstractFifoSamplePi) { + _inherits(RateTransposer, _AbstractFifoSamplePi); + var _super = _createSuper(RateTransposer); + function RateTransposer(createBuffers) { + var _this; + _classCallCheck(this, RateTransposer); + _this = _super.call(this, createBuffers); + _this.reset(); + _this._rate = 1; + return _this; + } + _createClass(RateTransposer, [{ + key: "rate", + set: function set(rate) { + this._rate = rate; + } + }, { + key: "reset", + value: function reset() { + this.slopeCount = 0; + this.prevSampleL = 0; + this.prevSampleR = 0; + } + }, { + key: "clone", + value: function clone() { + var result = new RateTransposer(); + result.rate = this._rate; + return result; + } + }, { + key: "process", + value: function process() { + var numFrames = this._inputBuffer.frameCount; + this._outputBuffer.ensureAdditionalCapacity(numFrames / this._rate + 1); + var numFramesOutput = this.transpose(numFrames); + this._inputBuffer.receive(); + this._outputBuffer.put(numFramesOutput); + } + }, { + key: "transpose", + value: function transpose() { + var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + if (numFrames === 0) { + return 0; + } + var src = this._inputBuffer.vector; + var srcOffset = this._inputBuffer.startIndex; + var dest = this._outputBuffer.vector; + var destOffset = this._outputBuffer.endIndex; + var used = 0; + var i = 0; + while (this.slopeCount < 1.0) { + dest[destOffset + 2 * i] = (1.0 - this.slopeCount) * this.prevSampleL + this.slopeCount * src[srcOffset]; + dest[destOffset + 2 * i + 1] = (1.0 - this.slopeCount) * this.prevSampleR + this.slopeCount * src[srcOffset + 1]; + i = i + 1; + this.slopeCount += this._rate; + } + this.slopeCount -= 1.0; + if (numFrames !== 1) { + out: while (true) { + while (this.slopeCount > 1.0) { + this.slopeCount -= 1.0; + used = used + 1; + if (used >= numFrames - 1) { + break out; + } + } + var srcIndex = srcOffset + 2 * used; + dest[destOffset + 2 * i] = (1.0 - this.slopeCount) * src[srcIndex] + this.slopeCount * src[srcIndex + 2]; + dest[destOffset + 2 * i + 1] = (1.0 - this.slopeCount) * src[srcIndex + 1] + this.slopeCount * src[srcIndex + 3]; + i = i + 1; + this.slopeCount += this._rate; + } + } + this.prevSampleL = src[srcOffset + 2 * numFrames - 2]; + this.prevSampleR = src[srcOffset + 2 * numFrames - 1]; + return i; + } + }]); + return RateTransposer; +}(AbstractFifoSamplePipe); +var FilterSupport = function () { + function FilterSupport(pipe) { + _classCallCheck(this, FilterSupport); + this._pipe = pipe; + } + _createClass(FilterSupport, [{ + key: "pipe", + get: function get() { + return this._pipe; + } + }, { + key: "inputBuffer", + get: function get() { + return this._pipe.inputBuffer; + } + }, { + key: "outputBuffer", + get: function get() { + return this._pipe.outputBuffer; + } + }, { + key: "fillInputBuffer", + value: function fillInputBuffer() { + throw new Error('fillInputBuffer() not overridden'); + } + }, { + key: "fillOutputBuffer", + value: function fillOutputBuffer() { + var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + while (this.outputBuffer.frameCount < numFrames) { + var numInputFrames = 8192 * 2 - this.inputBuffer.frameCount; + this.fillInputBuffer(numInputFrames); + if (this.inputBuffer.frameCount < 8192 * 2) { + break; + } + this._pipe.process(); + } + } + }, { + key: "clear", + value: function clear() { + this._pipe.clear(); + } + }]); + return FilterSupport; +}(); +var noop = function noop() { + return; +}; +var SimpleFilter = function (_FilterSupport) { + _inherits(SimpleFilter, _FilterSupport); + var _super2 = _createSuper(SimpleFilter); + function SimpleFilter(sourceSound, pipe) { + var _this2; + var callback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop; + _classCallCheck(this, SimpleFilter); + _this2 = _super2.call(this, pipe); + _this2.callback = callback; + _this2.sourceSound = sourceSound; + _this2.historyBufferSize = 22050; + _this2._sourcePosition = 0; + _this2.outputBufferPosition = 0; + _this2._position = 0; + return _this2; + } + _createClass(SimpleFilter, [{ + key: "position", + get: function get() { + return this._position; + }, + set: function set(position) { + if (position > this._position) { + throw new RangeError('New position may not be greater than current position'); + } + var newOutputBufferPosition = this.outputBufferPosition - (this._position - position); + if (newOutputBufferPosition < 0) { + throw new RangeError('New position falls outside of history buffer'); + } + this.outputBufferPosition = newOutputBufferPosition; + this._position = position; + } + }, { + key: "sourcePosition", + get: function get() { + return this._sourcePosition; + }, + set: function set(sourcePosition) { + this.clear(); + this._sourcePosition = sourcePosition; + } + }, { + key: "onEnd", + value: function onEnd() { + this.callback(); + } + }, { + key: "fillInputBuffer", + value: function fillInputBuffer() { + var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + var samples = new Float32Array(numFrames * 2); + var numFramesExtracted = this.sourceSound.extract(samples, numFrames, this._sourcePosition); + this._sourcePosition += numFramesExtracted; + this.inputBuffer.putSamples(samples, 0, numFramesExtracted); + } + }, { + key: "extract", + value: function extract(target) { + var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + this.fillOutputBuffer(this.outputBufferPosition + numFrames); + var numFramesExtracted = Math.min(numFrames, this.outputBuffer.frameCount - this.outputBufferPosition); + this.outputBuffer.extract(target, this.outputBufferPosition, numFramesExtracted); + var currentFrames = this.outputBufferPosition + numFramesExtracted; + this.outputBufferPosition = Math.min(this.historyBufferSize, currentFrames); + this.outputBuffer.receive(Math.max(currentFrames - this.historyBufferSize, 0)); + this._position += numFramesExtracted; + return numFramesExtracted; + } + }, { + key: "handleSampleData", + value: function handleSampleData(event) { + this.extract(event.data, 4096); + } + }, { + key: "clear", + value: function clear() { + _get(_getPrototypeOf(SimpleFilter.prototype), "clear", this).call(this); + this.outputBufferPosition = 0; + } + }]); + return SimpleFilter; +}(FilterSupport); +var USE_AUTO_SEQUENCE_LEN = 0; +var DEFAULT_SEQUENCE_MS = USE_AUTO_SEQUENCE_LEN; +var USE_AUTO_SEEKWINDOW_LEN = 0; +var DEFAULT_SEEKWINDOW_MS = USE_AUTO_SEEKWINDOW_LEN; +var DEFAULT_OVERLAP_MS = 8; +var _SCAN_OFFSETS = [[124, 186, 248, 310, 372, 434, 496, 558, 620, 682, 744, 806, 868, 930, 992, 1054, 1116, 1178, 1240, 1302, 1364, 1426, 1488, 0], [-100, -75, -50, -25, 25, 50, 75, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [-20, -15, -10, -5, 5, 10, 15, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [-4, -3, -2, -1, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]; +var AUTOSEQ_TEMPO_LOW = 0.5; +var AUTOSEQ_TEMPO_TOP = 2.0; +var AUTOSEQ_AT_MIN = 125.0; +var AUTOSEQ_AT_MAX = 50.0; +var AUTOSEQ_K = (AUTOSEQ_AT_MAX - AUTOSEQ_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW); +var AUTOSEQ_C = AUTOSEQ_AT_MIN - AUTOSEQ_K * AUTOSEQ_TEMPO_LOW; +var AUTOSEEK_AT_MIN = 25.0; +var AUTOSEEK_AT_MAX = 15.0; +var AUTOSEEK_K = (AUTOSEEK_AT_MAX - AUTOSEEK_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW); +var AUTOSEEK_C = AUTOSEEK_AT_MIN - AUTOSEEK_K * AUTOSEQ_TEMPO_LOW; +var Stretch = function (_AbstractFifoSamplePi2) { + _inherits(Stretch, _AbstractFifoSamplePi2); + var _super3 = _createSuper(Stretch); + function Stretch(createBuffers) { + var _this3; + _classCallCheck(this, Stretch); + _this3 = _super3.call(this, createBuffers); + _this3._quickSeek = true; + _this3.midBufferDirty = false; + _this3.midBuffer = null; + _this3.overlapLength = 0; + _this3.autoSeqSetting = true; + _this3.autoSeekSetting = true; + _this3._tempo = 1; + _this3.setParameters(44100, DEFAULT_SEQUENCE_MS, DEFAULT_SEEKWINDOW_MS, DEFAULT_OVERLAP_MS); + return _this3; + } + _createClass(Stretch, [{ + key: "clear", + value: function clear() { + _get(_getPrototypeOf(Stretch.prototype), "clear", this).call(this); + this.clearMidBuffer(); + } + }, { + key: "clearMidBuffer", + value: function clearMidBuffer() { + if (this.midBufferDirty) { + this.midBufferDirty = false; + this.midBuffer = null; + } + } + }, { + key: "setParameters", + value: function setParameters(sampleRate, sequenceMs, seekWindowMs, overlapMs) { + if (sampleRate > 0) { + this.sampleRate = sampleRate; + } + if (overlapMs > 0) { + this.overlapMs = overlapMs; + } + if (sequenceMs > 0) { + this.sequenceMs = sequenceMs; + this.autoSeqSetting = false; + } else { + this.autoSeqSetting = true; + } + if (seekWindowMs > 0) { + this.seekWindowMs = seekWindowMs; + this.autoSeekSetting = false; + } else { + this.autoSeekSetting = true; + } + this.calculateSequenceParameters(); + this.calculateOverlapLength(this.overlapMs); + this.tempo = this._tempo; + } + }, { + key: "tempo", + get: function get() { + return this._tempo; + }, + set: function set(newTempo) { + var intskip; + this._tempo = newTempo; + this.calculateSequenceParameters(); + this.nominalSkip = this._tempo * (this.seekWindowLength - this.overlapLength); + this.skipFract = 0; + intskip = Math.floor(this.nominalSkip + 0.5); + this.sampleReq = Math.max(intskip + this.overlapLength, this.seekWindowLength) + this.seekLength; + } + }, { + key: "inputChunkSize", + get: function get() { + return this.sampleReq; + } + }, { + key: "outputChunkSize", + get: function get() { + return this.overlapLength + Math.max(0, this.seekWindowLength - 2 * this.overlapLength); + } + }, { + key: "calculateOverlapLength", + value: function calculateOverlapLength() { + var overlapInMsec = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; + var newOvl; + newOvl = this.sampleRate * overlapInMsec / 1000; + newOvl = newOvl < 16 ? 16 : newOvl; + newOvl -= newOvl % 8; + this.overlapLength = newOvl; + this.refMidBuffer = new Float32Array(this.overlapLength * 2); + this.midBuffer = new Float32Array(this.overlapLength * 2); + } + }, { + key: "checkLimits", + value: function checkLimits(x, mi, ma) { + return x < mi ? mi : x > ma ? ma : x; + } + }, { + key: "calculateSequenceParameters", + value: function calculateSequenceParameters() { + var seq; + var seek; + if (this.autoSeqSetting) { + seq = AUTOSEQ_C + AUTOSEQ_K * this._tempo; + seq = this.checkLimits(seq, AUTOSEQ_AT_MAX, AUTOSEQ_AT_MIN); + this.sequenceMs = Math.floor(seq + 0.5); + } + if (this.autoSeekSetting) { + seek = AUTOSEEK_C + AUTOSEEK_K * this._tempo; + seek = this.checkLimits(seek, AUTOSEEK_AT_MAX, AUTOSEEK_AT_MIN); + this.seekWindowMs = Math.floor(seek + 0.5); + } + this.seekWindowLength = Math.floor(this.sampleRate * this.sequenceMs / 1000); + this.seekLength = Math.floor(this.sampleRate * this.seekWindowMs / 1000); + } + }, { + key: "quickSeek", + set: function set(enable) { + this._quickSeek = enable; + } + }, { + key: "clone", + value: function clone() { + var result = new Stretch(); + result.tempo = this._tempo; + result.setParameters(this.sampleRate, this.sequenceMs, this.seekWindowMs, this.overlapMs); + return result; + } + }, { + key: "seekBestOverlapPosition", + value: function seekBestOverlapPosition() { + return this._quickSeek ? this.seekBestOverlapPositionStereoQuick() : this.seekBestOverlapPositionStereo(); + } + }, { + key: "seekBestOverlapPositionStereo", + value: function seekBestOverlapPositionStereo() { + var bestOffset; + var bestCorrelation; + var correlation; + var i = 0; + this.preCalculateCorrelationReferenceStereo(); + bestOffset = 0; + bestCorrelation = Number.MIN_VALUE; + for (; i < this.seekLength; i = i + 1) { + correlation = this.calculateCrossCorrelationStereo(2 * i, this.refMidBuffer); + if (correlation > bestCorrelation) { + bestCorrelation = correlation; + bestOffset = i; + } + } + return bestOffset; + } + }, { + key: "seekBestOverlapPositionStereoQuick", + value: function seekBestOverlapPositionStereoQuick() { + var bestOffset; + var bestCorrelation; + var correlation; + var scanCount = 0; + var correlationOffset; + var tempOffset; + this.preCalculateCorrelationReferenceStereo(); + bestCorrelation = Number.MIN_VALUE; + bestOffset = 0; + correlationOffset = 0; + tempOffset = 0; + for (; scanCount < 4; scanCount = scanCount + 1) { + var j = 0; + while (_SCAN_OFFSETS[scanCount][j]) { + tempOffset = correlationOffset + _SCAN_OFFSETS[scanCount][j]; + if (tempOffset >= this.seekLength) { + break; + } + correlation = this.calculateCrossCorrelationStereo(2 * tempOffset, this.refMidBuffer); + if (correlation > bestCorrelation) { + bestCorrelation = correlation; + bestOffset = tempOffset; + } + j = j + 1; + } + correlationOffset = bestOffset; + } + return bestOffset; + } + }, { + key: "preCalculateCorrelationReferenceStereo", + value: function preCalculateCorrelationReferenceStereo() { + var i = 0; + var context; + var temp; + for (; i < this.overlapLength; i = i + 1) { + temp = i * (this.overlapLength - i); + context = i * 2; + this.refMidBuffer[context] = this.midBuffer[context] * temp; + this.refMidBuffer[context + 1] = this.midBuffer[context + 1] * temp; + } + } + }, { + key: "calculateCrossCorrelationStereo", + value: function calculateCrossCorrelationStereo(mixingPosition, compare) { + var mixing = this._inputBuffer.vector; + mixingPosition += this._inputBuffer.startIndex; + var correlation = 0; + var i = 2; + var calcLength = 2 * this.overlapLength; + var mixingOffset; + for (; i < calcLength; i = i + 2) { + mixingOffset = i + mixingPosition; + correlation += mixing[mixingOffset] * compare[i] + mixing[mixingOffset + 1] * compare[i + 1]; + } + return correlation; + } + }, { + key: "overlap", + value: function overlap(overlapPosition) { + this.overlapStereo(2 * overlapPosition); + } + }, { + key: "overlapStereo", + value: function overlapStereo(inputPosition) { + var input = this._inputBuffer.vector; + inputPosition += this._inputBuffer.startIndex; + var output = this._outputBuffer.vector; + var outputPosition = this._outputBuffer.endIndex; + var i = 0; + var context; + var tempFrame; + var frameScale = 1 / this.overlapLength; + var fi; + var inputOffset; + var outputOffset; + for (; i < this.overlapLength; i = i + 1) { + tempFrame = (this.overlapLength - i) * frameScale; + fi = i * frameScale; + context = 2 * i; + inputOffset = context + inputPosition; + outputOffset = context + outputPosition; + output[outputOffset + 0] = input[inputOffset + 0] * fi + this.midBuffer[context + 0] * tempFrame; + output[outputOffset + 1] = input[inputOffset + 1] * fi + this.midBuffer[context + 1] * tempFrame; + } + } + }, { + key: "process", + value: function process() { + var offset; + var temp; + var overlapSkip; + if (this.midBuffer === null) { + if (this._inputBuffer.frameCount < this.overlapLength) { + return; + } + this.midBuffer = new Float32Array(this.overlapLength * 2); + this._inputBuffer.receiveSamples(this.midBuffer, this.overlapLength); + } + while (this._inputBuffer.frameCount >= this.sampleReq) { + offset = this.seekBestOverlapPosition(); + this._outputBuffer.ensureAdditionalCapacity(this.overlapLength); + this.overlap(Math.floor(offset)); + this._outputBuffer.put(this.overlapLength); + temp = this.seekWindowLength - 2 * this.overlapLength; + if (temp > 0) { + this._outputBuffer.putBuffer(this._inputBuffer, offset + this.overlapLength, temp); + } + var start = this._inputBuffer.startIndex + 2 * (offset + this.seekWindowLength - this.overlapLength); + this.midBuffer.set(this._inputBuffer.vector.subarray(start, start + 2 * this.overlapLength)); + this.skipFract += this.nominalSkip; + overlapSkip = Math.floor(this.skipFract); + this.skipFract -= overlapSkip; + this._inputBuffer.receive(overlapSkip); + } + } + }]); + return Stretch; +}(AbstractFifoSamplePipe); +var testFloatEqual = function testFloatEqual(a, b) { + return (a > b ? a - b : b - a) > 1e-10; +}; +var SoundTouch = function () { + function SoundTouch() { + _classCallCheck(this, SoundTouch); + this.transposer = new RateTransposer(false); + this.stretch = new Stretch(false); + this._inputBuffer = new FifoSampleBuffer(); + this._intermediateBuffer = new FifoSampleBuffer(); + this._outputBuffer = new FifoSampleBuffer(); + this._rate = 0; + this._tempo = 0; + this.virtualPitch = 1.0; + this.virtualRate = 1.0; + this.virtualTempo = 1.0; + this.calculateEffectiveRateAndTempo(); + } + _createClass(SoundTouch, [{ + key: "clear", + value: function clear() { + this.transposer.clear(); + this.stretch.clear(); + } + }, { + key: "clone", + value: function clone() { + var result = new SoundTouch(); + result.rate = this.rate; + result.tempo = this.tempo; + return result; + } + }, { + key: "rate", + get: function get() { + return this._rate; + }, + set: function set(rate) { + this.virtualRate = rate; + this.calculateEffectiveRateAndTempo(); + } + }, { + key: "rateChange", + set: function set(rateChange) { + this._rate = 1.0 + 0.01 * rateChange; + } + }, { + key: "tempo", + get: function get() { + return this._tempo; + }, + set: function set(tempo) { + this.virtualTempo = tempo; + this.calculateEffectiveRateAndTempo(); + } + }, { + key: "tempoChange", + set: function set(tempoChange) { + this.tempo = 1.0 + 0.01 * tempoChange; + } + }, { + key: "pitch", + set: function set(pitch) { + this.virtualPitch = pitch; + this.calculateEffectiveRateAndTempo(); + } + }, { + key: "pitchOctaves", + set: function set(pitchOctaves) { + this.pitch = Math.exp(0.69314718056 * pitchOctaves); + this.calculateEffectiveRateAndTempo(); + } + }, { + key: "pitchSemitones", + set: function set(pitchSemitones) { + this.pitchOctaves = pitchSemitones / 12.0; + } + }, { + key: "inputBuffer", + get: function get() { + return this._inputBuffer; + } + }, { + key: "outputBuffer", + get: function get() { + return this._outputBuffer; + } + }, { + key: "calculateEffectiveRateAndTempo", + value: function calculateEffectiveRateAndTempo() { + var previousTempo = this._tempo; + var previousRate = this._rate; + this._tempo = this.virtualTempo / this.virtualPitch; + this._rate = this.virtualRate * this.virtualPitch; + if (testFloatEqual(this._tempo, previousTempo)) { + this.stretch.tempo = this._tempo; + } + if (testFloatEqual(this._rate, previousRate)) { + this.transposer.rate = this._rate; + } + if (this._rate > 1.0) { + if (this._outputBuffer != this.transposer.outputBuffer) { + this.stretch.inputBuffer = this._inputBuffer; + this.stretch.outputBuffer = this._intermediateBuffer; + this.transposer.inputBuffer = this._intermediateBuffer; + this.transposer.outputBuffer = this._outputBuffer; + } + } else { + if (this._outputBuffer != this.stretch.outputBuffer) { + this.transposer.inputBuffer = this._inputBuffer; + this.transposer.outputBuffer = this._intermediateBuffer; + this.stretch.inputBuffer = this._intermediateBuffer; + this.stretch.outputBuffer = this._outputBuffer; + } + } + } + }, { + key: "process", + value: function process() { + if (this._rate > 1.0) { + this.stretch.process(); + this.transposer.process(); + } else { + this.transposer.process(); + this.stretch.process(); + } + } + }]); + return SoundTouch; +}(); +var WebAudioBufferSource = function () { + function WebAudioBufferSource(buffer) { + _classCallCheck(this, WebAudioBufferSource); + this.buffer = buffer; + this._position = 0; + } + _createClass(WebAudioBufferSource, [{ + key: "dualChannel", + get: function get() { + return this.buffer.numberOfChannels > 1; + } + }, { + key: "position", + get: function get() { + return this._position; + }, + set: function set(value) { + this._position = value; + } + }, { + key: "extract", + value: function extract(target) { + var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + var position = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + this.position = position; + var left = this.buffer.getChannelData(0); + var right = this.dualChannel ? this.buffer.getChannelData(1) : this.buffer.getChannelData(0); + var i = 0; + for (; i < numFrames; i++) { + target[i * 2] = left[i + position]; + target[i * 2 + 1] = right[i + position]; + } + return Math.min(numFrames, left.length - position); + } + }]); + return WebAudioBufferSource; +}(); +var getWebAudioNode = function getWebAudioNode(context, filter) { + var sourcePositionCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop; + var bufferSize = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 4096; + var node = context.createScriptProcessor(bufferSize, 2, 2); + var samples = new Float32Array(bufferSize * 2); + node.onaudioprocess = function (event) { + var left = event.outputBuffer.getChannelData(0); + var right = event.outputBuffer.getChannelData(1); + var framesExtracted = filter.extract(samples, bufferSize); + sourcePositionCallback(filter.sourcePosition); + if (framesExtracted === 0) { + filter.onEnd(); + } + var i = 0; + for (; i < framesExtracted; i++) { + left[i] = samples[i * 2]; + right[i] = samples[i * 2 + 1]; + } + }; + return node; +}; +var pad = function pad(n, width, z) { + z = z || '0'; + n = n + ''; + return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; +}; +var minsSecs = function minsSecs(secs) { + var mins = Math.floor(secs / 60); + var seconds = secs - mins * 60; + return "".concat(mins, ":").concat(pad(parseInt(seconds), 2)); +}; +var onUpdate = function onUpdate(sourcePosition) { + var currentTimePlayed = this.timePlayed; + var sampleRate = this.sampleRate; + this.sourcePosition = sourcePosition; + this.timePlayed = sourcePosition / sampleRate; + if (currentTimePlayed !== this.timePlayed) { + var timePlayed = new CustomEvent('play', { + detail: { + timePlayed: this.timePlayed, + formattedTimePlayed: this.formattedTimePlayed, + percentagePlayed: this.percentagePlayed + } + }); + this._node.dispatchEvent(timePlayed); + } +}; +(function () { + function PitchShifter(context, buffer, bufferSize) { + var _this4 = this; + var onEnd = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : noop; + _classCallCheck(this, PitchShifter); + this._soundtouch = new SoundTouch(); + var source = new WebAudioBufferSource(buffer); + this.timePlayed = 0; + this.sourcePosition = 0; + this._filter = new SimpleFilter(source, this._soundtouch, onEnd); + this._node = getWebAudioNode(context, this._filter, function (sourcePostion) { + return onUpdate.call(_this4, sourcePostion); + }, bufferSize); + this.tempo = 1; + this.rate = 1; + this.duration = buffer.duration; + this.sampleRate = context.sampleRate; + this.listeners = []; + } + _createClass(PitchShifter, [{ + key: "formattedDuration", + get: function get() { + return minsSecs(this.duration); + } + }, { + key: "formattedTimePlayed", + get: function get() { + return minsSecs(this.timePlayed); + } + }, { + key: "percentagePlayed", + get: function get() { + return 100 * this._filter.sourcePosition / (this.duration * this.sampleRate); + }, + set: function set(perc) { + this._filter.sourcePosition = parseInt(perc * this.duration * this.sampleRate); + this.sourcePosition = this._filter.sourcePosition; + this.timePlayed = this.sourcePosition / this.sampleRate; + } + }, { + key: "node", + get: function get() { + return this._node; + } + }, { + key: "pitch", + set: function set(pitch) { + this._soundtouch.pitch = pitch; + } + }, { + key: "pitchSemitones", + set: function set(semitone) { + this._soundtouch.pitchSemitones = semitone; + } + }, { + key: "rate", + set: function set(rate) { + this._soundtouch.rate = rate; + } + }, { + key: "tempo", + set: function set(tempo) { + this._soundtouch.tempo = tempo; + } + }, { + key: "connect", + value: function connect(toNode) { + this._node.connect(toNode); + } + }, { + key: "disconnect", + value: function disconnect() { + this._node.disconnect(); + } + }, { + key: "on", + value: function on(eventName, cb) { + this.listeners.push({ + name: eventName, + cb: cb + }); + this._node.addEventListener(eventName, function (event) { + return cb(event.detail); + }); + } + }, { + key: "off", + value: function off() { + var _this5 = this; + var eventName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var listeners = this.listeners; + if (eventName) { + listeners = listeners.filter(function (e) { + return e.name === eventName; + }); + } + listeners.forEach(function (e) { + _this5._node.removeEventListener(e.name, function (event) { + return e.cb(event.detail); + }); + }); + } + }]); + return PitchShifter; +})(); + +var ProcessAudioBufferSource = function () { + function ProcessAudioBufferSource(bufferProps, leftChannel, rightChannel) { + _classCallCheck(this, ProcessAudioBufferSource); + Object.assign(this, bufferProps); + this.leftChannel = leftChannel; + this.rightChannel = rightChannel; + this._position = 0; + } + _createClass(ProcessAudioBufferSource, [{ + key: "position", + get: function get() { + return this._position; + }, + set: function set(value) { + this._position = value; + } + }, { + key: "extract", + value: function extract(target) { + var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; + var position = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; + this.position = position; + var i = 0; + for (; i < numFrames; i++) { + target[i * 2] = this.leftChannel[i + position]; + target[i * 2 + 1] = this.rightChannel[i + position]; + } + return numFrames; + } + }]); + return ProcessAudioBufferSource; +}(); + +var SoundTouchWorklet = function (_AudioWorkletProcesso) { + _inherits(SoundTouchWorklet, _AudioWorkletProcesso); + var _super = _createSuper(SoundTouchWorklet); + function SoundTouchWorklet(nodeOptions) { + var _this; + _classCallCheck(this, SoundTouchWorklet); + _this = _super.call(this); + _this._initialized = false; + _this.bufferSize = 128; + _this.port.onmessage = _this._messageProcessor.bind(_assertThisInitialized(_this)); + _this.port.postMessage({ + message: 'PROCESSOR_CONSTRUCTOR', + detail: nodeOptions + }); + return _this; + } + _createClass(SoundTouchWorklet, [{ + key: "_messageProcessor", + value: function _messageProcessor(eventFromWorker) { + var _eventFromWorker$data = eventFromWorker.data, + message = _eventFromWorker$data.message, + detail = _eventFromWorker$data.detail; + if (message === 'INITIALIZE_PROCESSOR') { + var _detail = _slicedToArray(detail, 3), + bufferProps = _detail[0], + leftChannel = _detail[1], + rightChannel = _detail[2]; + this.bufferSource = new ProcessAudioBufferSource(bufferProps, leftChannel, rightChannel); + this._samples = new Float32Array(this.bufferSize * 2); + this._pipe = new SoundTouch(); + this._filter = new SimpleFilter(this.bufferSource, this._pipe); + this.port.postMessage({ + message: 'PROCESSOR_READY' + }); + this._initialized = true; + return true; + } + if (message === 'SET_PIPE_PROP' && detail) { + var name = detail.name, + value = detail.value; + this._pipe[name] = value; + this.port.postMessage({ + message: 'PIPE_PROP_CHANGED', + detail: "Updated ".concat(name, " to ").concat(this._pipe[name], "\ntypeof ").concat(_typeof(value)) + }); + return; + } + if (message === 'SET_FILTER_PROP' && detail) { + var _name = detail.name, + _value = detail.value; + this._filter[_name] = _value; + this.port.postMessage({ + message: 'FILTER_PROP_CHANGED', + detail: "Updated ".concat(_name, " to ").concat(this._filter[_name], "\ntypeof ").concat(_typeof(_value)) + }); + return; + } + console.log('[PitchShifterWorkletProcessor] Unknown message: ', eventFromWorker); + } + }, { + key: "_sendMessage", + value: function _sendMessage(message) { + var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; + if (!message) { + return; + } + this.port.postMessage({ + message: message, + detail: detail + }); + } + }, { + key: "process", + value: function process(inputs, outputs) { + if (!this._initialized || !inputs[0].length) { + return true; + } + var left = outputs[0][0]; + var right = outputs[0].length > 1 ? outputs[0][1] : outputs[0][0]; + var samples = this._samples; + if (!left || left && !left.length) { + return false; + } + var framesExtracted = this._filter.extract(samples, inputs[0][0].length); + if (isNaN(samples[0]) || !framesExtracted) { + this._sendMessage('PROCESSOR_END'); + return false; + } + this._sendMessage('SOURCEPOSITION', this._filter.sourcePosition); + var i = 0; + for (; i < framesExtracted; i = i + 1) { + left[i] = samples[i * 2]; + right[i] = samples[i * 2 + 1]; + if (isNaN(left[i]) || isNaN(right[i])) { + left[i] = 0; + right[i] = 0; + } + } + return true; + } + }]); + return SoundTouchWorklet; +}( _wrapNativeSuper(AudioWorkletProcessor)); +registerProcessor('soundtouch-worklet', SoundTouchWorklet); diff --git a/tsconfig.json b/tsconfig.json index e5c5eeb..09511d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES6", + "target": "ES2018", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", @@ -13,7 +13,8 @@ "DOM", "ES5", "ES6", - "ES7" + "ES7", + "ES2018" ], "allowSyntheticDefaultImports": true }, From 56d9450fc3430b17b7b4306acdea64dca66dfdaf Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 15:18:45 -0400 Subject: [PATCH 09/29] Dynamically include soundtouch-worklet at build time from node_modules --- package.json | 2 + rollup.config.build.mjs | 15 +- src/services/OpenAI.ts | 2 +- src/soundtouch/soundtouch-audio-node.js | 536 ------- src/soundtouch/soundtouch-worklet.js | 1898 ----------------------- 5 files changed, 17 insertions(+), 2436 deletions(-) delete mode 100644 src/soundtouch/soundtouch-audio-node.js delete mode 100644 src/soundtouch/soundtouch-worklet.js diff --git a/package.json b/package.json index 03d2abb..78fc989 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "eslint": "7.32.0", "eslint-plugin-tsdoc": "0.2.16", "rollup": "^4.24.0", + "rollup-plugin-copy": "^3.5.0", + "rollup-plugin-delete": "^2.1.0", "typedoc": "0.22.18" }, "overrides": { diff --git a/rollup.config.build.mjs b/rollup.config.build.mjs index 5f0279b..b873d78 100644 --- a/rollup.config.build.mjs +++ b/rollup.config.build.mjs @@ -4,6 +4,8 @@ import commonjs from '@rollup/plugin-commonjs'; import terser from "@rollup/plugin-terser"; import json from '@rollup/plugin-json'; import webWorkerLoader from '@colingm/rollup-plugin-web-worker-loader'; +import del from 'rollup-plugin-delete'; +import copy from 'rollup-plugin-copy'; export default { input: 'src/main.ts', @@ -20,7 +22,18 @@ export default { commonjs(), // terser(), json(), - webWorkerLoader({ targetPlatform: 'browser' }) + webWorkerLoader({ targetPlatform: 'browser' }), + copy({ + targets: [{ + src: 'node_modules/@soundtouchjs/audio-worklet/dist/soundtouch-worklet.js', + dest: 'src' + }], + hook: 'buildStart' + }), + del({ + targets: 'src/soundtouch-worklet.js', + hook: 'buildEnd' + }) ], onwarn: function(warning, warner) { if (warning.code === 'CIRCULAR_DEPENDENCY'){ diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index 93c0531..64bd576 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -2,7 +2,7 @@ import {TTSService} from "./TTSService"; import TTSPlugin from "../main"; import {requestUrl} from "obsidian"; // @ts-ignore -import registerSoundtouchWorklet from "audio-worklet:../soundtouch/soundtouch-worklet"; +import registerSoundtouchWorklet from "audio-worklet:../soundtouch-worklet"; import createSoundTouchNode from '@soundtouchjs/audio-worklet'; export class OpenAI implements TTSService { diff --git a/src/soundtouch/soundtouch-audio-node.js b/src/soundtouch/soundtouch-audio-node.js deleted file mode 100644 index 9bc1a30..0000000 --- a/src/soundtouch/soundtouch-audio-node.js +++ /dev/null @@ -1,536 +0,0 @@ -/* -* SoundTouch Audio Worklet v0.1.17 AudioWorklet using the -* SoundTouch audio processing library -* -* Copyright (c) Olli Parviainen -* Copyright (c) Ryan Berdeen -* Copyright (c) Jakub Fiala -* Copyright (c) Steve 'Cutter' Blades -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { - try { - var info = gen[key](arg); - var value = info.value; - } catch (error) { - reject(error); - return; - } - - if (info.done) { - resolve(value); - } else { - Promise.resolve(value).then(_next, _throw); - } -} - -function _asyncToGenerator(fn) { - return function () { - var self = this, - args = arguments; - return new Promise(function (resolve, reject) { - var gen = fn.apply(self, args); - - function _next(value) { - asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); - } - - function _throw(err) { - asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); - } - - _next(undefined); - }); - }; -} - -function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -} - -function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } -} - -function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - Object.defineProperty(Constructor, "prototype", { - writable: false - }); - return Constructor; -} - -function _inherits(subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function"); - } - - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - writable: true, - configurable: true - } - }); - Object.defineProperty(subClass, "prototype", { - writable: false - }); - if (superClass) _setPrototypeOf(subClass, superClass); -} - -function _getPrototypeOf(o) { - _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { - return o.__proto__ || Object.getPrototypeOf(o); - }; - return _getPrototypeOf(o); -} - -function _setPrototypeOf(o, p) { - _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { - o.__proto__ = p; - return o; - }; - - return _setPrototypeOf(o, p); -} - -function _isNativeReflectConstruct() { - if (typeof Reflect === "undefined" || !Reflect.construct) return false; - if (Reflect.construct.sham) return false; - if (typeof Proxy === "function") return true; - - try { - Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); - return true; - } catch (e) { - return false; - } -} - -function _assertThisInitialized(self) { - if (self === void 0) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return self; -} - -function _possibleConstructorReturn(self, call) { - if (call && (typeof call === "object" || typeof call === "function")) { - return call; - } else if (call !== void 0) { - throw new TypeError("Derived constructors may only return object or undefined"); - } - - return _assertThisInitialized(self); -} - -function _createSuper(Derived) { - var hasNativeReflectConstruct = _isNativeReflectConstruct(); - - return function _createSuperInternal() { - var Super = _getPrototypeOf(Derived), - result; - - if (hasNativeReflectConstruct) { - var NewTarget = _getPrototypeOf(this).constructor; - - result = Reflect.construct(Super, arguments, NewTarget); - } else { - result = Super.apply(this, arguments); - } - - return _possibleConstructorReturn(this, result); - }; -} - -var pad = function pad(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; -}; -var minsSecs = function minsSecs(secs) { - var mins = Math.floor(secs / 60); - var seconds = secs - mins * 60; - return "".concat(mins, ":").concat(pad(parseInt(seconds), 2)); -}; -var diffSecs = function diffSecs(ms1, ms2) { - return (ms2 - ms1) / 1000; -}; - -var createSoundTouchNode = function createSoundTouchNode(audioCtx, AudioWorkletNode, arrayBuffer, options) { - var SoundTouchNode = function (_AudioWorkletNode) { - _inherits(SoundTouchNode, _AudioWorkletNode); - var _super = _createSuper(SoundTouchNode); - function SoundTouchNode(context, arrayBuffer, options) { - var _this; - _classCallCheck(this, SoundTouchNode); - _this = _super.call(this, context, 'soundtouch-worklet', options); - _this._arrayBuffer = arrayBuffer.slice(0); - _this.listeners = []; - _this.port.onmessage = _this._messageProcessor.bind(_assertThisInitialized(_this)); - _this.sourcePosition = 0; - _this.timePlayed = 0; - _this._startTime = 0; - _this._pauseTime = 0; - _this._playHead = 0; - _this._playing = false; - _this._ready = false; - _this._initialPlay = true; - return _this; - } - _createClass(SoundTouchNode, [{ - key: "formattedDuration", - get: function get() { - return minsSecs(this.duration); - } - }, { - key: "formattedTimePlayed", - get: function get() { - return minsSecs(this.timePlayed); - } - }, { - key: "percentagePlayed", - get: function get() { - return 100 * this.sourcePosition / (this.duration * this.sampleRate); - } - , - set: function set(percentage) { - var duration = this.duration, - sampleRate = this.sampleRate; - this.sourcePosition = parseInt(duration * sampleRate * (percentage / 100)); - this._updateFilterProp('sourcePosition', this.sourcePosition); - this.currentTime = this.duration * percentage / 100; - } - }, { - key: "currentTime", - get: function get() { - if (!this.playing) { - return this._playHead; - } - return this._playHead + diffSecs(this._startTime, new Date().getTime()); - } - , - set: function set(val) { - this._playHead = val; - } - }, { - key: "playing", - get: function get() { - return this._playing; - } - , - set: function set(val) { - this._playing = Boolean(val); - } - }, { - key: "ready", - get: function get() { - return this._ready; - } - , - set: function set(val) { - this._ready = Boolean(val); - } - }, { - key: "sampleRate", - get: function get() { - if (this.audioBuffer) { - return this.audioBuffer.sampleRate; - } - return undefined; - } - }, { - key: "duration", - get: function get() { - if (this.audioBuffer) { - return this.audioBuffer.duration; - } - return undefined; - } - }, { - key: "bufferLength", - get: function get() { - if (this.audioBuffer) { - return this.audioBuffer.length; - } - return undefined; - } - }, { - key: "numberOfChannels", - get: function get() { - if (this.audioBuffer) { - return this.audioBuffer.numberOfChannels; - } - return undefined; - } - }, { - key: "pitch", - set: function set(pitch) { - this._updatePipeProp('pitch', pitch); - } - }, { - key: "pitchSemitones", - set: function set(semitone) { - this._updatePipeProp('pitchSemitones', semitone); - } - }, { - key: "rate", - set: function set(rate) { - this._updatePipeProp('rate', rate); - } - }, { - key: "tempo", - set: function set(tempo) { - this._updatePipeProp('tempo', tempo); - } - }, { - key: "connectToBuffer", - value: function connectToBuffer() { - this.bufferNode = this.context.createBufferSource(); - this.bufferNode.buffer = this.audioBuffer; - this.bufferNode.onended = function () { - return console.log('song ended'); - }; - this.bufferNode.connect(this); - return this.bufferNode; - } - }, { - key: "disconnectFromBuffer", - value: function disconnectFromBuffer() { - this.bufferNode.disconnect(); - } - }, { - key: "handleAudioData", - value: function handleAudioData(audioBuffer) { - this.audioBuffer = audioBuffer; - this.port.postMessage({ - message: 'INITIALIZE_PROCESSOR', - detail: this.createBaseArray(audioBuffer) - }); - } - }, { - key: "createBaseArray", - value: function createBaseArray(audioBuffer) { - return [{ - sampleRate: this.sampleRate, - duration: this.duration, - bufferLength: this.bufferLength, - numberOfChannels: this.numberOfChannels - }, audioBuffer.getChannelData(0), this.numberOfChannels > 1 ? audioBuffer.getChannelData(1) : audioBuffer.getChannelData(0)]; - } - }, { - key: "play", - value: function () { - var _play = _asyncToGenerator( regeneratorRuntime.mark(function _callee() { - return regeneratorRuntime.wrap(function _callee$(_context) { - while (1) { - switch (_context.prev = _context.next) { - case 0: - if (this.ready) { - _context.next = 2; - break; - } - throw new Error('Your processor is not ready yet'); - case 2: - if (this.playing) { - this.stop(true); - } - if (this._initialPlay) { - if (this._playHead === 0) { - this.percentagePlayed = 0; - } - this._initialPlay = false; - } - _context.next = 6; - return this.context.resume(); - case 6: - this._startTime = new Date().getTime(); - this.playing = true; - case 8: - case "end": - return _context.stop(); - } - } - }, _callee, this); - })); - function play() { - return _play.apply(this, arguments); - } - return play; - }() - }, { - key: "pause", - value: function pause() { - var currTime = this.currentTime; - this.stop(); - this.currentTime = currTime; - } - }, { - key: "stop", - value: function () { - var _stop = _asyncToGenerator( regeneratorRuntime.mark(function _callee2() { - return regeneratorRuntime.wrap(function _callee2$(_context2) { - while (1) { - switch (_context2.prev = _context2.next) { - case 0: - _context2.next = 2; - return this.context.suspend(); - case 2: - this.currentTime = 0; - this._startTime = new Date().getTime(); - this.playing = false; - case 5: - case "end": - return _context2.stop(); - } - } - }, _callee2, this); - })); - function stop() { - return _stop.apply(this, arguments); - } - return stop; - }() - }, { - key: "on", - value: function on(eventName, cb) { - this.listeners.push({ - name: eventName, - cb: cb - }); - this.addEventListener(eventName, function (event) { - return cb(event.detail); - }); - } - }, { - key: "off", - value: function off() { - var _this2 = this; - var eventName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var listeners = this.listeners; - if (eventName) { - listeners = listeners.filter(function (e) { - return e.name === eventName; - }); - } - listeners.forEach(function (e) { - _this2.removeEventListener(e.name, function (event) { - return e.cb(event.detail); - }); - }); - } - }, { - key: "onprocessorerror", - value: function onprocessorerror(err) { - throw err; - } - }, { - key: "_updatePipeProp", - value: function _updatePipeProp(name, value) { - this.port.postMessage({ - message: 'SET_PIPE_PROP', - detail: { - name: name, - value: value - } - }); - } - }, { - key: "_updateFilterProp", - value: function _updateFilterProp(name, value) { - this.port.postMessage({ - message: 'SET_FILTER_PROP', - detail: { - name: name, - value: value - } - }); - } - }, { - key: "_messageProcessor", - value: function _messageProcessor(eventFromWorker) { - var _this3 = this; - var _eventFromWorker$data = eventFromWorker.data, - message = _eventFromWorker$data.message, - detail = _eventFromWorker$data.detail; - var sampleRate = this.sampleRate, - currentTime = this.timePlayed; - if (message === 'SOURCEPOSITION') { - this.sourcePosition = detail; - var timePlayed = detail / sampleRate; - if (currentTime !== timePlayed) { - this.timePlayed = timePlayed; - var timeEvent = new CustomEvent('play', { - detail: { - timePlayed: this.timePlayed, - formattedTimePlayed: this.formattedTimePlayed, - percentagePlayed: this.percentagePlayed - } - }); - this.dispatchEvent(timeEvent); - } - } - if (message === 'PROCESSOR_CONSTRUCTOR') { - this.context.decodeAudioData(this._arrayBuffer, function (audioData) { - return _this3.handleAudioData(audioData); - }, function (err) { - return console.log('[decodeAudioData ERROR] ', err); - }); - return; - } - if (message === 'PROCESSOR_READY') { - this.ready = true; - if (typeof this.onInitialized === 'function') { - this.onInitialized(detail); - return; - } - var init = new CustomEvent('initialized', detail); - this.dispatchEvent(init); - return; - } - if (message === 'PROCESSOR_END') { - this.stop(); - this.percentagePlayed = 0; - var endOfPlay = new CustomEvent('end', { - detail: { - timePlayed: this.currentTime, - formattedTimePlayed: this.formattedTimePlayed, - percentagePlayed: this.percentagePlayed - } - }); - this.dispatchEvent(endOfPlay); - return; - } - } - }]); - return SoundTouchNode; - }(AudioWorkletNode); - return new SoundTouchNode(audioCtx, arrayBuffer, options); -}; - -export { createSoundTouchNode as default }; -//# sourceMappingURL=soundtouch-audio-node.js.map diff --git a/src/soundtouch/soundtouch-worklet.js b/src/soundtouch/soundtouch-worklet.js deleted file mode 100644 index 70c4147..0000000 --- a/src/soundtouch/soundtouch-worklet.js +++ /dev/null @@ -1,1898 +0,0 @@ -/* -* SoundTouch Audio Worklet v0.1.17 AudioWorklet using the -* SoundTouch audio processing library -* -* Copyright (c) Olli Parviainen -* Copyright (c) Ryan Berdeen -* Copyright (c) Jakub Fiala -* Copyright (c) Steve 'Cutter' Blades -* -* This library is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 2.1 of the License, or (at your option) any later version. -* -* This library is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this library; if not, write to the Free Software -* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -'use strict'; - -function _typeof(obj) { - "@babel/helpers - typeof"; - - return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { - return typeof obj; - } : function (obj) { - return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }, _typeof(obj); -} - -function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -} - -function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } -} - -function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps); - if (staticProps) _defineProperties(Constructor, staticProps); - Object.defineProperty(Constructor, "prototype", { - writable: false - }); - return Constructor; -} - -function _inherits(subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function"); - } - - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - writable: true, - configurable: true - } - }); - Object.defineProperty(subClass, "prototype", { - writable: false - }); - if (superClass) _setPrototypeOf(subClass, superClass); -} - -function _getPrototypeOf(o) { - _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { - return o.__proto__ || Object.getPrototypeOf(o); - }; - return _getPrototypeOf(o); -} - -function _setPrototypeOf(o, p) { - _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { - o.__proto__ = p; - return o; - }; - - return _setPrototypeOf(o, p); -} - -function _isNativeReflectConstruct() { - if (typeof Reflect === "undefined" || !Reflect.construct) return false; - if (Reflect.construct.sham) return false; - if (typeof Proxy === "function") return true; - - try { - Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); - return true; - } catch (e) { - return false; - } -} - -function _construct(Parent, args, Class) { - if (_isNativeReflectConstruct()) { - _construct = Reflect.construct; - } else { - _construct = function _construct(Parent, args, Class) { - var a = [null]; - a.push.apply(a, args); - var Constructor = Function.bind.apply(Parent, a); - var instance = new Constructor(); - if (Class) _setPrototypeOf(instance, Class.prototype); - return instance; - }; - } - - return _construct.apply(null, arguments); -} - -function _isNativeFunction(fn) { - return Function.toString.call(fn).indexOf("[native code]") !== -1; -} - -function _wrapNativeSuper(Class) { - var _cache = typeof Map === "function" ? new Map() : undefined; - - _wrapNativeSuper = function _wrapNativeSuper(Class) { - if (Class === null || !_isNativeFunction(Class)) return Class; - - if (typeof Class !== "function") { - throw new TypeError("Super expression must either be null or a function"); - } - - if (typeof _cache !== "undefined") { - if (_cache.has(Class)) return _cache.get(Class); - - _cache.set(Class, Wrapper); - } - - function Wrapper() { - return _construct(Class, arguments, _getPrototypeOf(this).constructor); - } - - Wrapper.prototype = Object.create(Class.prototype, { - constructor: { - value: Wrapper, - enumerable: false, - writable: true, - configurable: true - } - }); - return _setPrototypeOf(Wrapper, Class); - }; - - return _wrapNativeSuper(Class); -} - -function _assertThisInitialized(self) { - if (self === void 0) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return self; -} - -function _possibleConstructorReturn(self, call) { - if (call && (typeof call === "object" || typeof call === "function")) { - return call; - } else if (call !== void 0) { - throw new TypeError("Derived constructors may only return object or undefined"); - } - - return _assertThisInitialized(self); -} - -function _createSuper(Derived) { - var hasNativeReflectConstruct = _isNativeReflectConstruct(); - - return function _createSuperInternal() { - var Super = _getPrototypeOf(Derived), - result; - - if (hasNativeReflectConstruct) { - var NewTarget = _getPrototypeOf(this).constructor; - - result = Reflect.construct(Super, arguments, NewTarget); - } else { - result = Super.apply(this, arguments); - } - - return _possibleConstructorReturn(this, result); - }; -} - -function _superPropBase(object, property) { - while (!Object.prototype.hasOwnProperty.call(object, property)) { - object = _getPrototypeOf(object); - if (object === null) break; - } - - return object; -} - -function _get() { - if (typeof Reflect !== "undefined" && Reflect.get) { - _get = Reflect.get; - } else { - _get = function _get(target, property, receiver) { - var base = _superPropBase(target, property); - - if (!base) return; - var desc = Object.getOwnPropertyDescriptor(base, property); - - if (desc.get) { - return desc.get.call(arguments.length < 3 ? target : receiver); - } - - return desc.value; - }; - } - - return _get.apply(this, arguments); -} - -function _slicedToArray(arr, i) { - return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); -} - -function _arrayWithHoles(arr) { - if (Array.isArray(arr)) return arr; -} - -function _iterableToArrayLimit(arr, i) { - var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; - - if (_i == null) return; - var _arr = []; - var _n = true; - var _d = false; - - var _s, _e; - - try { - for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { - _arr.push(_s.value); - - if (i && _arr.length === i) break; - } - } catch (err) { - _d = true; - _e = err; - } finally { - try { - if (!_n && _i["return"] != null) _i["return"](); - } finally { - if (_d) throw _e; - } - } - - return _arr; -} - -function _unsupportedIterableToArray(o, minLen) { - if (!o) return; - if (typeof o === "string") return _arrayLikeToArray(o, minLen); - var n = Object.prototype.toString.call(o).slice(8, -1); - if (n === "Object" && o.constructor) n = o.constructor.name; - if (n === "Map" || n === "Set") return Array.from(o); - if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); -} - -function _arrayLikeToArray(arr, len) { - if (len == null || len > arr.length) len = arr.length; - - for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; - - return arr2; -} - -function _nonIterableRest() { - throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); -} - -var runtime = function (exports) { - var Op = Object.prototype; - var hasOwn = Op.hasOwnProperty; - var undefined$1; - var $Symbol = typeof Symbol === "function" ? Symbol : {}; - var iteratorSymbol = $Symbol.iterator || "@@iterator"; - var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; - var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; - function define(obj, key, value) { - Object.defineProperty(obj, key, { - value: value, - enumerable: true, - configurable: true, - writable: true - }); - return obj[key]; - } - try { - define({}, ""); - } catch (err) { - define = function define(obj, key, value) { - return obj[key] = value; - }; - } - function wrap(innerFn, outerFn, self, tryLocsList) { - var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; - var generator = Object.create(protoGenerator.prototype); - var context = new Context(tryLocsList || []); - generator._invoke = makeInvokeMethod(innerFn, self, context); - return generator; - } - exports.wrap = wrap; - function tryCatch(fn, obj, arg) { - try { - return { - type: "normal", - arg: fn.call(obj, arg) - }; - } catch (err) { - return { - type: "throw", - arg: err - }; - } - } - var GenStateSuspendedStart = "suspendedStart"; - var GenStateSuspendedYield = "suspendedYield"; - var GenStateExecuting = "executing"; - var GenStateCompleted = "completed"; - var ContinueSentinel = {}; - function Generator() {} - function GeneratorFunction() {} - function GeneratorFunctionPrototype() {} - var IteratorPrototype = {}; - define(IteratorPrototype, iteratorSymbol, function () { - return this; - }); - var getProto = Object.getPrototypeOf; - var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); - if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { - IteratorPrototype = NativeIteratorPrototype; - } - var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); - GeneratorFunction.prototype = GeneratorFunctionPrototype; - define(Gp, "constructor", GeneratorFunctionPrototype); - define(GeneratorFunctionPrototype, "constructor", GeneratorFunction); - GeneratorFunction.displayName = define(GeneratorFunctionPrototype, toStringTagSymbol, "GeneratorFunction"); - function defineIteratorMethods(prototype) { - ["next", "throw", "return"].forEach(function (method) { - define(prototype, method, function (arg) { - return this._invoke(method, arg); - }); - }); - } - exports.isGeneratorFunction = function (genFun) { - var ctor = typeof genFun === "function" && genFun.constructor; - return ctor ? ctor === GeneratorFunction || - (ctor.displayName || ctor.name) === "GeneratorFunction" : false; - }; - exports.mark = function (genFun) { - if (Object.setPrototypeOf) { - Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); - } else { - genFun.__proto__ = GeneratorFunctionPrototype; - define(genFun, toStringTagSymbol, "GeneratorFunction"); - } - genFun.prototype = Object.create(Gp); - return genFun; - }; - exports.awrap = function (arg) { - return { - __await: arg - }; - }; - function AsyncIterator(generator, PromiseImpl) { - function invoke(method, arg, resolve, reject) { - var record = tryCatch(generator[method], generator, arg); - if (record.type === "throw") { - reject(record.arg); - } else { - var result = record.arg; - var value = result.value; - if (value && _typeof(value) === "object" && hasOwn.call(value, "__await")) { - return PromiseImpl.resolve(value.__await).then(function (value) { - invoke("next", value, resolve, reject); - }, function (err) { - invoke("throw", err, resolve, reject); - }); - } - return PromiseImpl.resolve(value).then(function (unwrapped) { - result.value = unwrapped; - resolve(result); - }, function (error) { - return invoke("throw", error, resolve, reject); - }); - } - } - var previousPromise; - function enqueue(method, arg) { - function callInvokeWithMethodAndArg() { - return new PromiseImpl(function (resolve, reject) { - invoke(method, arg, resolve, reject); - }); - } - return previousPromise = - previousPromise ? previousPromise.then(callInvokeWithMethodAndArg, - callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); - } - this._invoke = enqueue; - } - defineIteratorMethods(AsyncIterator.prototype); - define(AsyncIterator.prototype, asyncIteratorSymbol, function () { - return this; - }); - exports.AsyncIterator = AsyncIterator; - exports.async = function (innerFn, outerFn, self, tryLocsList, PromiseImpl) { - if (PromiseImpl === void 0) PromiseImpl = Promise; - var iter = new AsyncIterator(wrap(innerFn, outerFn, self, tryLocsList), PromiseImpl); - return exports.isGeneratorFunction(outerFn) ? iter - : iter.next().then(function (result) { - return result.done ? result.value : iter.next(); - }); - }; - function makeInvokeMethod(innerFn, self, context) { - var state = GenStateSuspendedStart; - return function invoke(method, arg) { - if (state === GenStateExecuting) { - throw new Error("Generator is already running"); - } - if (state === GenStateCompleted) { - if (method === "throw") { - throw arg; - } - return doneResult(); - } - context.method = method; - context.arg = arg; - while (true) { - var delegate = context.delegate; - if (delegate) { - var delegateResult = maybeInvokeDelegate(delegate, context); - if (delegateResult) { - if (delegateResult === ContinueSentinel) continue; - return delegateResult; - } - } - if (context.method === "next") { - context.sent = context._sent = context.arg; - } else if (context.method === "throw") { - if (state === GenStateSuspendedStart) { - state = GenStateCompleted; - throw context.arg; - } - context.dispatchException(context.arg); - } else if (context.method === "return") { - context.abrupt("return", context.arg); - } - state = GenStateExecuting; - var record = tryCatch(innerFn, self, context); - if (record.type === "normal") { - state = context.done ? GenStateCompleted : GenStateSuspendedYield; - if (record.arg === ContinueSentinel) { - continue; - } - return { - value: record.arg, - done: context.done - }; - } else if (record.type === "throw") { - state = GenStateCompleted; - context.method = "throw"; - context.arg = record.arg; - } - } - }; - } - function maybeInvokeDelegate(delegate, context) { - var method = delegate.iterator[context.method]; - if (method === undefined$1) { - context.delegate = null; - if (context.method === "throw") { - if (delegate.iterator["return"]) { - context.method = "return"; - context.arg = undefined$1; - maybeInvokeDelegate(delegate, context); - if (context.method === "throw") { - return ContinueSentinel; - } - } - context.method = "throw"; - context.arg = new TypeError("The iterator does not provide a 'throw' method"); - } - return ContinueSentinel; - } - var record = tryCatch(method, delegate.iterator, context.arg); - if (record.type === "throw") { - context.method = "throw"; - context.arg = record.arg; - context.delegate = null; - return ContinueSentinel; - } - var info = record.arg; - if (!info) { - context.method = "throw"; - context.arg = new TypeError("iterator result is not an object"); - context.delegate = null; - return ContinueSentinel; - } - if (info.done) { - context[delegate.resultName] = info.value; - context.next = delegate.nextLoc; - if (context.method !== "return") { - context.method = "next"; - context.arg = undefined$1; - } - } else { - return info; - } - context.delegate = null; - return ContinueSentinel; - } - defineIteratorMethods(Gp); - define(Gp, toStringTagSymbol, "Generator"); - define(Gp, iteratorSymbol, function () { - return this; - }); - define(Gp, "toString", function () { - return "[object Generator]"; - }); - function pushTryEntry(locs) { - var entry = { - tryLoc: locs[0] - }; - if (1 in locs) { - entry.catchLoc = locs[1]; - } - if (2 in locs) { - entry.finallyLoc = locs[2]; - entry.afterLoc = locs[3]; - } - this.tryEntries.push(entry); - } - function resetTryEntry(entry) { - var record = entry.completion || {}; - record.type = "normal"; - delete record.arg; - entry.completion = record; - } - function Context(tryLocsList) { - this.tryEntries = [{ - tryLoc: "root" - }]; - tryLocsList.forEach(pushTryEntry, this); - this.reset(true); - } - exports.keys = function (object) { - var keys = []; - for (var key in object) { - keys.push(key); - } - keys.reverse(); - return function next() { - while (keys.length) { - var key = keys.pop(); - if (key in object) { - next.value = key; - next.done = false; - return next; - } - } - next.done = true; - return next; - }; - }; - function values(iterable) { - if (iterable) { - var iteratorMethod = iterable[iteratorSymbol]; - if (iteratorMethod) { - return iteratorMethod.call(iterable); - } - if (typeof iterable.next === "function") { - return iterable; - } - if (!isNaN(iterable.length)) { - var i = -1, - next = function next() { - while (++i < iterable.length) { - if (hasOwn.call(iterable, i)) { - next.value = iterable[i]; - next.done = false; - return next; - } - } - next.value = undefined$1; - next.done = true; - return next; - }; - return next.next = next; - } - } - return { - next: doneResult - }; - } - exports.values = values; - function doneResult() { - return { - value: undefined$1, - done: true - }; - } - Context.prototype = { - constructor: Context, - reset: function reset(skipTempReset) { - this.prev = 0; - this.next = 0; - this.sent = this._sent = undefined$1; - this.done = false; - this.delegate = null; - this.method = "next"; - this.arg = undefined$1; - this.tryEntries.forEach(resetTryEntry); - if (!skipTempReset) { - for (var name in this) { - if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { - this[name] = undefined$1; - } - } - } - }, - stop: function stop() { - this.done = true; - var rootEntry = this.tryEntries[0]; - var rootRecord = rootEntry.completion; - if (rootRecord.type === "throw") { - throw rootRecord.arg; - } - return this.rval; - }, - dispatchException: function dispatchException(exception) { - if (this.done) { - throw exception; - } - var context = this; - function handle(loc, caught) { - record.type = "throw"; - record.arg = exception; - context.next = loc; - if (caught) { - context.method = "next"; - context.arg = undefined$1; - } - return !!caught; - } - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - var record = entry.completion; - if (entry.tryLoc === "root") { - return handle("end"); - } - if (entry.tryLoc <= this.prev) { - var hasCatch = hasOwn.call(entry, "catchLoc"); - var hasFinally = hasOwn.call(entry, "finallyLoc"); - if (hasCatch && hasFinally) { - if (this.prev < entry.catchLoc) { - return handle(entry.catchLoc, true); - } else if (this.prev < entry.finallyLoc) { - return handle(entry.finallyLoc); - } - } else if (hasCatch) { - if (this.prev < entry.catchLoc) { - return handle(entry.catchLoc, true); - } - } else if (hasFinally) { - if (this.prev < entry.finallyLoc) { - return handle(entry.finallyLoc); - } - } else { - throw new Error("try statement without catch or finally"); - } - } - } - }, - abrupt: function abrupt(type, arg) { - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { - var finallyEntry = entry; - break; - } - } - if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { - finallyEntry = null; - } - var record = finallyEntry ? finallyEntry.completion : {}; - record.type = type; - record.arg = arg; - if (finallyEntry) { - this.method = "next"; - this.next = finallyEntry.finallyLoc; - return ContinueSentinel; - } - return this.complete(record); - }, - complete: function complete(record, afterLoc) { - if (record.type === "throw") { - throw record.arg; - } - if (record.type === "break" || record.type === "continue") { - this.next = record.arg; - } else if (record.type === "return") { - this.rval = this.arg = record.arg; - this.method = "return"; - this.next = "end"; - } else if (record.type === "normal" && afterLoc) { - this.next = afterLoc; - } - return ContinueSentinel; - }, - finish: function finish(finallyLoc) { - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - if (entry.finallyLoc === finallyLoc) { - this.complete(entry.completion, entry.afterLoc); - resetTryEntry(entry); - return ContinueSentinel; - } - } - }, - "catch": function _catch(tryLoc) { - for (var i = this.tryEntries.length - 1; i >= 0; --i) { - var entry = this.tryEntries[i]; - if (entry.tryLoc === tryLoc) { - var record = entry.completion; - if (record.type === "throw") { - var thrown = record.arg; - resetTryEntry(entry); - } - return thrown; - } - } - throw new Error("illegal catch attempt"); - }, - delegateYield: function delegateYield(iterable, resultName, nextLoc) { - this.delegate = { - iterator: values(iterable), - resultName: resultName, - nextLoc: nextLoc - }; - if (this.method === "next") { - this.arg = undefined$1; - } - return ContinueSentinel; - } - }; - return exports; -}( -(typeof module === "undefined" ? "undefined" : _typeof(module)) === "object" ? module.exports : {}); -try { - regeneratorRuntime = runtime; -} catch (accidentalStrictMode) { - if ((typeof globalThis === "undefined" ? "undefined" : _typeof(globalThis)) === "object") { - globalThis.regeneratorRuntime = runtime; - } else { - Function("r", "regeneratorRuntime = r")(runtime); - } -} - -var FifoSampleBuffer = function () { - function FifoSampleBuffer() { - _classCallCheck(this, FifoSampleBuffer); - this._vector = new Float32Array(); - this._position = 0; - this._frameCount = 0; - } - _createClass(FifoSampleBuffer, [{ - key: "vector", - get: function get() { - return this._vector; - } - }, { - key: "position", - get: function get() { - return this._position; - } - }, { - key: "startIndex", - get: function get() { - return this._position * 2; - } - }, { - key: "frameCount", - get: function get() { - return this._frameCount; - } - }, { - key: "endIndex", - get: function get() { - return (this._position + this._frameCount) * 2; - } - }, { - key: "clear", - value: function clear() { - this.receive(this._frameCount); - this.rewind(); - } - }, { - key: "put", - value: function put(numFrames) { - this._frameCount += numFrames; - } - }, { - key: "putSamples", - value: function putSamples(samples, position) { - var numFrames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - position = position || 0; - var sourceOffset = position * 2; - if (!(numFrames >= 0)) { - numFrames = (samples.length - sourceOffset) / 2; - } - var numSamples = numFrames * 2; - this.ensureCapacity(numFrames + this._frameCount); - var destOffset = this.endIndex; - this.vector.set(samples.subarray(sourceOffset, sourceOffset + numSamples), destOffset); - this._frameCount += numFrames; - } - }, { - key: "putBuffer", - value: function putBuffer(buffer, position) { - var numFrames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - position = position || 0; - if (!(numFrames >= 0)) { - numFrames = buffer.frameCount - position; - } - this.putSamples(buffer.vector, buffer.position + position, numFrames); - } - }, { - key: "receive", - value: function receive(numFrames) { - if (!(numFrames >= 0) || numFrames > this._frameCount) { - numFrames = this.frameCount; - } - this._frameCount -= numFrames; - this._position += numFrames; - } - }, { - key: "receiveSamples", - value: function receiveSamples(output) { - var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - var numSamples = numFrames * 2; - var sourceOffset = this.startIndex; - output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples)); - this.receive(numFrames); - } - }, { - key: "extract", - value: function extract(output) { - var position = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - var numFrames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - var sourceOffset = this.startIndex + position * 2; - var numSamples = numFrames * 2; - output.set(this._vector.subarray(sourceOffset, sourceOffset + numSamples)); - } - }, { - key: "ensureCapacity", - value: function ensureCapacity() { - var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var minLength = parseInt(numFrames * 2); - if (this._vector.length < minLength) { - var newVector = new Float32Array(minLength); - newVector.set(this._vector.subarray(this.startIndex, this.endIndex)); - this._vector = newVector; - this._position = 0; - } else { - this.rewind(); - } - } - }, { - key: "ensureAdditionalCapacity", - value: function ensureAdditionalCapacity() { - var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - this.ensureCapacity(this._frameCount + numFrames); - } - }, { - key: "rewind", - value: function rewind() { - if (this._position > 0) { - this._vector.set(this._vector.subarray(this.startIndex, this.endIndex)); - this._position = 0; - } - } - }]); - return FifoSampleBuffer; -}(); -var AbstractFifoSamplePipe = function () { - function AbstractFifoSamplePipe(createBuffers) { - _classCallCheck(this, AbstractFifoSamplePipe); - if (createBuffers) { - this._inputBuffer = new FifoSampleBuffer(); - this._outputBuffer = new FifoSampleBuffer(); - } else { - this._inputBuffer = this._outputBuffer = null; - } - } - _createClass(AbstractFifoSamplePipe, [{ - key: "inputBuffer", - get: function get() { - return this._inputBuffer; - }, - set: function set(inputBuffer) { - this._inputBuffer = inputBuffer; - } - }, { - key: "outputBuffer", - get: function get() { - return this._outputBuffer; - }, - set: function set(outputBuffer) { - this._outputBuffer = outputBuffer; - } - }, { - key: "clear", - value: function clear() { - this._inputBuffer.clear(); - this._outputBuffer.clear(); - } - }]); - return AbstractFifoSamplePipe; -}(); -var RateTransposer = function (_AbstractFifoSamplePi) { - _inherits(RateTransposer, _AbstractFifoSamplePi); - var _super = _createSuper(RateTransposer); - function RateTransposer(createBuffers) { - var _this; - _classCallCheck(this, RateTransposer); - _this = _super.call(this, createBuffers); - _this.reset(); - _this._rate = 1; - return _this; - } - _createClass(RateTransposer, [{ - key: "rate", - set: function set(rate) { - this._rate = rate; - } - }, { - key: "reset", - value: function reset() { - this.slopeCount = 0; - this.prevSampleL = 0; - this.prevSampleR = 0; - } - }, { - key: "clone", - value: function clone() { - var result = new RateTransposer(); - result.rate = this._rate; - return result; - } - }, { - key: "process", - value: function process() { - var numFrames = this._inputBuffer.frameCount; - this._outputBuffer.ensureAdditionalCapacity(numFrames / this._rate + 1); - var numFramesOutput = this.transpose(numFrames); - this._inputBuffer.receive(); - this._outputBuffer.put(numFramesOutput); - } - }, { - key: "transpose", - value: function transpose() { - var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - if (numFrames === 0) { - return 0; - } - var src = this._inputBuffer.vector; - var srcOffset = this._inputBuffer.startIndex; - var dest = this._outputBuffer.vector; - var destOffset = this._outputBuffer.endIndex; - var used = 0; - var i = 0; - while (this.slopeCount < 1.0) { - dest[destOffset + 2 * i] = (1.0 - this.slopeCount) * this.prevSampleL + this.slopeCount * src[srcOffset]; - dest[destOffset + 2 * i + 1] = (1.0 - this.slopeCount) * this.prevSampleR + this.slopeCount * src[srcOffset + 1]; - i = i + 1; - this.slopeCount += this._rate; - } - this.slopeCount -= 1.0; - if (numFrames !== 1) { - out: while (true) { - while (this.slopeCount > 1.0) { - this.slopeCount -= 1.0; - used = used + 1; - if (used >= numFrames - 1) { - break out; - } - } - var srcIndex = srcOffset + 2 * used; - dest[destOffset + 2 * i] = (1.0 - this.slopeCount) * src[srcIndex] + this.slopeCount * src[srcIndex + 2]; - dest[destOffset + 2 * i + 1] = (1.0 - this.slopeCount) * src[srcIndex + 1] + this.slopeCount * src[srcIndex + 3]; - i = i + 1; - this.slopeCount += this._rate; - } - } - this.prevSampleL = src[srcOffset + 2 * numFrames - 2]; - this.prevSampleR = src[srcOffset + 2 * numFrames - 1]; - return i; - } - }]); - return RateTransposer; -}(AbstractFifoSamplePipe); -var FilterSupport = function () { - function FilterSupport(pipe) { - _classCallCheck(this, FilterSupport); - this._pipe = pipe; - } - _createClass(FilterSupport, [{ - key: "pipe", - get: function get() { - return this._pipe; - } - }, { - key: "inputBuffer", - get: function get() { - return this._pipe.inputBuffer; - } - }, { - key: "outputBuffer", - get: function get() { - return this._pipe.outputBuffer; - } - }, { - key: "fillInputBuffer", - value: function fillInputBuffer() { - throw new Error('fillInputBuffer() not overridden'); - } - }, { - key: "fillOutputBuffer", - value: function fillOutputBuffer() { - var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - while (this.outputBuffer.frameCount < numFrames) { - var numInputFrames = 8192 * 2 - this.inputBuffer.frameCount; - this.fillInputBuffer(numInputFrames); - if (this.inputBuffer.frameCount < 8192 * 2) { - break; - } - this._pipe.process(); - } - } - }, { - key: "clear", - value: function clear() { - this._pipe.clear(); - } - }]); - return FilterSupport; -}(); -var noop = function noop() { - return; -}; -var SimpleFilter = function (_FilterSupport) { - _inherits(SimpleFilter, _FilterSupport); - var _super2 = _createSuper(SimpleFilter); - function SimpleFilter(sourceSound, pipe) { - var _this2; - var callback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop; - _classCallCheck(this, SimpleFilter); - _this2 = _super2.call(this, pipe); - _this2.callback = callback; - _this2.sourceSound = sourceSound; - _this2.historyBufferSize = 22050; - _this2._sourcePosition = 0; - _this2.outputBufferPosition = 0; - _this2._position = 0; - return _this2; - } - _createClass(SimpleFilter, [{ - key: "position", - get: function get() { - return this._position; - }, - set: function set(position) { - if (position > this._position) { - throw new RangeError('New position may not be greater than current position'); - } - var newOutputBufferPosition = this.outputBufferPosition - (this._position - position); - if (newOutputBufferPosition < 0) { - throw new RangeError('New position falls outside of history buffer'); - } - this.outputBufferPosition = newOutputBufferPosition; - this._position = position; - } - }, { - key: "sourcePosition", - get: function get() { - return this._sourcePosition; - }, - set: function set(sourcePosition) { - this.clear(); - this._sourcePosition = sourcePosition; - } - }, { - key: "onEnd", - value: function onEnd() { - this.callback(); - } - }, { - key: "fillInputBuffer", - value: function fillInputBuffer() { - var numFrames = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var samples = new Float32Array(numFrames * 2); - var numFramesExtracted = this.sourceSound.extract(samples, numFrames, this._sourcePosition); - this._sourcePosition += numFramesExtracted; - this.inputBuffer.putSamples(samples, 0, numFramesExtracted); - } - }, { - key: "extract", - value: function extract(target) { - var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - this.fillOutputBuffer(this.outputBufferPosition + numFrames); - var numFramesExtracted = Math.min(numFrames, this.outputBuffer.frameCount - this.outputBufferPosition); - this.outputBuffer.extract(target, this.outputBufferPosition, numFramesExtracted); - var currentFrames = this.outputBufferPosition + numFramesExtracted; - this.outputBufferPosition = Math.min(this.historyBufferSize, currentFrames); - this.outputBuffer.receive(Math.max(currentFrames - this.historyBufferSize, 0)); - this._position += numFramesExtracted; - return numFramesExtracted; - } - }, { - key: "handleSampleData", - value: function handleSampleData(event) { - this.extract(event.data, 4096); - } - }, { - key: "clear", - value: function clear() { - _get(_getPrototypeOf(SimpleFilter.prototype), "clear", this).call(this); - this.outputBufferPosition = 0; - } - }]); - return SimpleFilter; -}(FilterSupport); -var USE_AUTO_SEQUENCE_LEN = 0; -var DEFAULT_SEQUENCE_MS = USE_AUTO_SEQUENCE_LEN; -var USE_AUTO_SEEKWINDOW_LEN = 0; -var DEFAULT_SEEKWINDOW_MS = USE_AUTO_SEEKWINDOW_LEN; -var DEFAULT_OVERLAP_MS = 8; -var _SCAN_OFFSETS = [[124, 186, 248, 310, 372, 434, 496, 558, 620, 682, 744, 806, 868, 930, 992, 1054, 1116, 1178, 1240, 1302, 1364, 1426, 1488, 0], [-100, -75, -50, -25, 25, 50, 75, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [-20, -15, -10, -5, 5, 10, 15, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [-4, -3, -2, -1, 1, 2, 3, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]; -var AUTOSEQ_TEMPO_LOW = 0.5; -var AUTOSEQ_TEMPO_TOP = 2.0; -var AUTOSEQ_AT_MIN = 125.0; -var AUTOSEQ_AT_MAX = 50.0; -var AUTOSEQ_K = (AUTOSEQ_AT_MAX - AUTOSEQ_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW); -var AUTOSEQ_C = AUTOSEQ_AT_MIN - AUTOSEQ_K * AUTOSEQ_TEMPO_LOW; -var AUTOSEEK_AT_MIN = 25.0; -var AUTOSEEK_AT_MAX = 15.0; -var AUTOSEEK_K = (AUTOSEEK_AT_MAX - AUTOSEEK_AT_MIN) / (AUTOSEQ_TEMPO_TOP - AUTOSEQ_TEMPO_LOW); -var AUTOSEEK_C = AUTOSEEK_AT_MIN - AUTOSEEK_K * AUTOSEQ_TEMPO_LOW; -var Stretch = function (_AbstractFifoSamplePi2) { - _inherits(Stretch, _AbstractFifoSamplePi2); - var _super3 = _createSuper(Stretch); - function Stretch(createBuffers) { - var _this3; - _classCallCheck(this, Stretch); - _this3 = _super3.call(this, createBuffers); - _this3._quickSeek = true; - _this3.midBufferDirty = false; - _this3.midBuffer = null; - _this3.overlapLength = 0; - _this3.autoSeqSetting = true; - _this3.autoSeekSetting = true; - _this3._tempo = 1; - _this3.setParameters(44100, DEFAULT_SEQUENCE_MS, DEFAULT_SEEKWINDOW_MS, DEFAULT_OVERLAP_MS); - return _this3; - } - _createClass(Stretch, [{ - key: "clear", - value: function clear() { - _get(_getPrototypeOf(Stretch.prototype), "clear", this).call(this); - this.clearMidBuffer(); - } - }, { - key: "clearMidBuffer", - value: function clearMidBuffer() { - if (this.midBufferDirty) { - this.midBufferDirty = false; - this.midBuffer = null; - } - } - }, { - key: "setParameters", - value: function setParameters(sampleRate, sequenceMs, seekWindowMs, overlapMs) { - if (sampleRate > 0) { - this.sampleRate = sampleRate; - } - if (overlapMs > 0) { - this.overlapMs = overlapMs; - } - if (sequenceMs > 0) { - this.sequenceMs = sequenceMs; - this.autoSeqSetting = false; - } else { - this.autoSeqSetting = true; - } - if (seekWindowMs > 0) { - this.seekWindowMs = seekWindowMs; - this.autoSeekSetting = false; - } else { - this.autoSeekSetting = true; - } - this.calculateSequenceParameters(); - this.calculateOverlapLength(this.overlapMs); - this.tempo = this._tempo; - } - }, { - key: "tempo", - get: function get() { - return this._tempo; - }, - set: function set(newTempo) { - var intskip; - this._tempo = newTempo; - this.calculateSequenceParameters(); - this.nominalSkip = this._tempo * (this.seekWindowLength - this.overlapLength); - this.skipFract = 0; - intskip = Math.floor(this.nominalSkip + 0.5); - this.sampleReq = Math.max(intskip + this.overlapLength, this.seekWindowLength) + this.seekLength; - } - }, { - key: "inputChunkSize", - get: function get() { - return this.sampleReq; - } - }, { - key: "outputChunkSize", - get: function get() { - return this.overlapLength + Math.max(0, this.seekWindowLength - 2 * this.overlapLength); - } - }, { - key: "calculateOverlapLength", - value: function calculateOverlapLength() { - var overlapInMsec = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; - var newOvl; - newOvl = this.sampleRate * overlapInMsec / 1000; - newOvl = newOvl < 16 ? 16 : newOvl; - newOvl -= newOvl % 8; - this.overlapLength = newOvl; - this.refMidBuffer = new Float32Array(this.overlapLength * 2); - this.midBuffer = new Float32Array(this.overlapLength * 2); - } - }, { - key: "checkLimits", - value: function checkLimits(x, mi, ma) { - return x < mi ? mi : x > ma ? ma : x; - } - }, { - key: "calculateSequenceParameters", - value: function calculateSequenceParameters() { - var seq; - var seek; - if (this.autoSeqSetting) { - seq = AUTOSEQ_C + AUTOSEQ_K * this._tempo; - seq = this.checkLimits(seq, AUTOSEQ_AT_MAX, AUTOSEQ_AT_MIN); - this.sequenceMs = Math.floor(seq + 0.5); - } - if (this.autoSeekSetting) { - seek = AUTOSEEK_C + AUTOSEEK_K * this._tempo; - seek = this.checkLimits(seek, AUTOSEEK_AT_MAX, AUTOSEEK_AT_MIN); - this.seekWindowMs = Math.floor(seek + 0.5); - } - this.seekWindowLength = Math.floor(this.sampleRate * this.sequenceMs / 1000); - this.seekLength = Math.floor(this.sampleRate * this.seekWindowMs / 1000); - } - }, { - key: "quickSeek", - set: function set(enable) { - this._quickSeek = enable; - } - }, { - key: "clone", - value: function clone() { - var result = new Stretch(); - result.tempo = this._tempo; - result.setParameters(this.sampleRate, this.sequenceMs, this.seekWindowMs, this.overlapMs); - return result; - } - }, { - key: "seekBestOverlapPosition", - value: function seekBestOverlapPosition() { - return this._quickSeek ? this.seekBestOverlapPositionStereoQuick() : this.seekBestOverlapPositionStereo(); - } - }, { - key: "seekBestOverlapPositionStereo", - value: function seekBestOverlapPositionStereo() { - var bestOffset; - var bestCorrelation; - var correlation; - var i = 0; - this.preCalculateCorrelationReferenceStereo(); - bestOffset = 0; - bestCorrelation = Number.MIN_VALUE; - for (; i < this.seekLength; i = i + 1) { - correlation = this.calculateCrossCorrelationStereo(2 * i, this.refMidBuffer); - if (correlation > bestCorrelation) { - bestCorrelation = correlation; - bestOffset = i; - } - } - return bestOffset; - } - }, { - key: "seekBestOverlapPositionStereoQuick", - value: function seekBestOverlapPositionStereoQuick() { - var bestOffset; - var bestCorrelation; - var correlation; - var scanCount = 0; - var correlationOffset; - var tempOffset; - this.preCalculateCorrelationReferenceStereo(); - bestCorrelation = Number.MIN_VALUE; - bestOffset = 0; - correlationOffset = 0; - tempOffset = 0; - for (; scanCount < 4; scanCount = scanCount + 1) { - var j = 0; - while (_SCAN_OFFSETS[scanCount][j]) { - tempOffset = correlationOffset + _SCAN_OFFSETS[scanCount][j]; - if (tempOffset >= this.seekLength) { - break; - } - correlation = this.calculateCrossCorrelationStereo(2 * tempOffset, this.refMidBuffer); - if (correlation > bestCorrelation) { - bestCorrelation = correlation; - bestOffset = tempOffset; - } - j = j + 1; - } - correlationOffset = bestOffset; - } - return bestOffset; - } - }, { - key: "preCalculateCorrelationReferenceStereo", - value: function preCalculateCorrelationReferenceStereo() { - var i = 0; - var context; - var temp; - for (; i < this.overlapLength; i = i + 1) { - temp = i * (this.overlapLength - i); - context = i * 2; - this.refMidBuffer[context] = this.midBuffer[context] * temp; - this.refMidBuffer[context + 1] = this.midBuffer[context + 1] * temp; - } - } - }, { - key: "calculateCrossCorrelationStereo", - value: function calculateCrossCorrelationStereo(mixingPosition, compare) { - var mixing = this._inputBuffer.vector; - mixingPosition += this._inputBuffer.startIndex; - var correlation = 0; - var i = 2; - var calcLength = 2 * this.overlapLength; - var mixingOffset; - for (; i < calcLength; i = i + 2) { - mixingOffset = i + mixingPosition; - correlation += mixing[mixingOffset] * compare[i] + mixing[mixingOffset + 1] * compare[i + 1]; - } - return correlation; - } - }, { - key: "overlap", - value: function overlap(overlapPosition) { - this.overlapStereo(2 * overlapPosition); - } - }, { - key: "overlapStereo", - value: function overlapStereo(inputPosition) { - var input = this._inputBuffer.vector; - inputPosition += this._inputBuffer.startIndex; - var output = this._outputBuffer.vector; - var outputPosition = this._outputBuffer.endIndex; - var i = 0; - var context; - var tempFrame; - var frameScale = 1 / this.overlapLength; - var fi; - var inputOffset; - var outputOffset; - for (; i < this.overlapLength; i = i + 1) { - tempFrame = (this.overlapLength - i) * frameScale; - fi = i * frameScale; - context = 2 * i; - inputOffset = context + inputPosition; - outputOffset = context + outputPosition; - output[outputOffset + 0] = input[inputOffset + 0] * fi + this.midBuffer[context + 0] * tempFrame; - output[outputOffset + 1] = input[inputOffset + 1] * fi + this.midBuffer[context + 1] * tempFrame; - } - } - }, { - key: "process", - value: function process() { - var offset; - var temp; - var overlapSkip; - if (this.midBuffer === null) { - if (this._inputBuffer.frameCount < this.overlapLength) { - return; - } - this.midBuffer = new Float32Array(this.overlapLength * 2); - this._inputBuffer.receiveSamples(this.midBuffer, this.overlapLength); - } - while (this._inputBuffer.frameCount >= this.sampleReq) { - offset = this.seekBestOverlapPosition(); - this._outputBuffer.ensureAdditionalCapacity(this.overlapLength); - this.overlap(Math.floor(offset)); - this._outputBuffer.put(this.overlapLength); - temp = this.seekWindowLength - 2 * this.overlapLength; - if (temp > 0) { - this._outputBuffer.putBuffer(this._inputBuffer, offset + this.overlapLength, temp); - } - var start = this._inputBuffer.startIndex + 2 * (offset + this.seekWindowLength - this.overlapLength); - this.midBuffer.set(this._inputBuffer.vector.subarray(start, start + 2 * this.overlapLength)); - this.skipFract += this.nominalSkip; - overlapSkip = Math.floor(this.skipFract); - this.skipFract -= overlapSkip; - this._inputBuffer.receive(overlapSkip); - } - } - }]); - return Stretch; -}(AbstractFifoSamplePipe); -var testFloatEqual = function testFloatEqual(a, b) { - return (a > b ? a - b : b - a) > 1e-10; -}; -var SoundTouch = function () { - function SoundTouch() { - _classCallCheck(this, SoundTouch); - this.transposer = new RateTransposer(false); - this.stretch = new Stretch(false); - this._inputBuffer = new FifoSampleBuffer(); - this._intermediateBuffer = new FifoSampleBuffer(); - this._outputBuffer = new FifoSampleBuffer(); - this._rate = 0; - this._tempo = 0; - this.virtualPitch = 1.0; - this.virtualRate = 1.0; - this.virtualTempo = 1.0; - this.calculateEffectiveRateAndTempo(); - } - _createClass(SoundTouch, [{ - key: "clear", - value: function clear() { - this.transposer.clear(); - this.stretch.clear(); - } - }, { - key: "clone", - value: function clone() { - var result = new SoundTouch(); - result.rate = this.rate; - result.tempo = this.tempo; - return result; - } - }, { - key: "rate", - get: function get() { - return this._rate; - }, - set: function set(rate) { - this.virtualRate = rate; - this.calculateEffectiveRateAndTempo(); - } - }, { - key: "rateChange", - set: function set(rateChange) { - this._rate = 1.0 + 0.01 * rateChange; - } - }, { - key: "tempo", - get: function get() { - return this._tempo; - }, - set: function set(tempo) { - this.virtualTempo = tempo; - this.calculateEffectiveRateAndTempo(); - } - }, { - key: "tempoChange", - set: function set(tempoChange) { - this.tempo = 1.0 + 0.01 * tempoChange; - } - }, { - key: "pitch", - set: function set(pitch) { - this.virtualPitch = pitch; - this.calculateEffectiveRateAndTempo(); - } - }, { - key: "pitchOctaves", - set: function set(pitchOctaves) { - this.pitch = Math.exp(0.69314718056 * pitchOctaves); - this.calculateEffectiveRateAndTempo(); - } - }, { - key: "pitchSemitones", - set: function set(pitchSemitones) { - this.pitchOctaves = pitchSemitones / 12.0; - } - }, { - key: "inputBuffer", - get: function get() { - return this._inputBuffer; - } - }, { - key: "outputBuffer", - get: function get() { - return this._outputBuffer; - } - }, { - key: "calculateEffectiveRateAndTempo", - value: function calculateEffectiveRateAndTempo() { - var previousTempo = this._tempo; - var previousRate = this._rate; - this._tempo = this.virtualTempo / this.virtualPitch; - this._rate = this.virtualRate * this.virtualPitch; - if (testFloatEqual(this._tempo, previousTempo)) { - this.stretch.tempo = this._tempo; - } - if (testFloatEqual(this._rate, previousRate)) { - this.transposer.rate = this._rate; - } - if (this._rate > 1.0) { - if (this._outputBuffer != this.transposer.outputBuffer) { - this.stretch.inputBuffer = this._inputBuffer; - this.stretch.outputBuffer = this._intermediateBuffer; - this.transposer.inputBuffer = this._intermediateBuffer; - this.transposer.outputBuffer = this._outputBuffer; - } - } else { - if (this._outputBuffer != this.stretch.outputBuffer) { - this.transposer.inputBuffer = this._inputBuffer; - this.transposer.outputBuffer = this._intermediateBuffer; - this.stretch.inputBuffer = this._intermediateBuffer; - this.stretch.outputBuffer = this._outputBuffer; - } - } - } - }, { - key: "process", - value: function process() { - if (this._rate > 1.0) { - this.stretch.process(); - this.transposer.process(); - } else { - this.transposer.process(); - this.stretch.process(); - } - } - }]); - return SoundTouch; -}(); -var WebAudioBufferSource = function () { - function WebAudioBufferSource(buffer) { - _classCallCheck(this, WebAudioBufferSource); - this.buffer = buffer; - this._position = 0; - } - _createClass(WebAudioBufferSource, [{ - key: "dualChannel", - get: function get() { - return this.buffer.numberOfChannels > 1; - } - }, { - key: "position", - get: function get() { - return this._position; - }, - set: function set(value) { - this._position = value; - } - }, { - key: "extract", - value: function extract(target) { - var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - var position = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - this.position = position; - var left = this.buffer.getChannelData(0); - var right = this.dualChannel ? this.buffer.getChannelData(1) : this.buffer.getChannelData(0); - var i = 0; - for (; i < numFrames; i++) { - target[i * 2] = left[i + position]; - target[i * 2 + 1] = right[i + position]; - } - return Math.min(numFrames, left.length - position); - } - }]); - return WebAudioBufferSource; -}(); -var getWebAudioNode = function getWebAudioNode(context, filter) { - var sourcePositionCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : noop; - var bufferSize = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 4096; - var node = context.createScriptProcessor(bufferSize, 2, 2); - var samples = new Float32Array(bufferSize * 2); - node.onaudioprocess = function (event) { - var left = event.outputBuffer.getChannelData(0); - var right = event.outputBuffer.getChannelData(1); - var framesExtracted = filter.extract(samples, bufferSize); - sourcePositionCallback(filter.sourcePosition); - if (framesExtracted === 0) { - filter.onEnd(); - } - var i = 0; - for (; i < framesExtracted; i++) { - left[i] = samples[i * 2]; - right[i] = samples[i * 2 + 1]; - } - }; - return node; -}; -var pad = function pad(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; -}; -var minsSecs = function minsSecs(secs) { - var mins = Math.floor(secs / 60); - var seconds = secs - mins * 60; - return "".concat(mins, ":").concat(pad(parseInt(seconds), 2)); -}; -var onUpdate = function onUpdate(sourcePosition) { - var currentTimePlayed = this.timePlayed; - var sampleRate = this.sampleRate; - this.sourcePosition = sourcePosition; - this.timePlayed = sourcePosition / sampleRate; - if (currentTimePlayed !== this.timePlayed) { - var timePlayed = new CustomEvent('play', { - detail: { - timePlayed: this.timePlayed, - formattedTimePlayed: this.formattedTimePlayed, - percentagePlayed: this.percentagePlayed - } - }); - this._node.dispatchEvent(timePlayed); - } -}; -(function () { - function PitchShifter(context, buffer, bufferSize) { - var _this4 = this; - var onEnd = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : noop; - _classCallCheck(this, PitchShifter); - this._soundtouch = new SoundTouch(); - var source = new WebAudioBufferSource(buffer); - this.timePlayed = 0; - this.sourcePosition = 0; - this._filter = new SimpleFilter(source, this._soundtouch, onEnd); - this._node = getWebAudioNode(context, this._filter, function (sourcePostion) { - return onUpdate.call(_this4, sourcePostion); - }, bufferSize); - this.tempo = 1; - this.rate = 1; - this.duration = buffer.duration; - this.sampleRate = context.sampleRate; - this.listeners = []; - } - _createClass(PitchShifter, [{ - key: "formattedDuration", - get: function get() { - return minsSecs(this.duration); - } - }, { - key: "formattedTimePlayed", - get: function get() { - return minsSecs(this.timePlayed); - } - }, { - key: "percentagePlayed", - get: function get() { - return 100 * this._filter.sourcePosition / (this.duration * this.sampleRate); - }, - set: function set(perc) { - this._filter.sourcePosition = parseInt(perc * this.duration * this.sampleRate); - this.sourcePosition = this._filter.sourcePosition; - this.timePlayed = this.sourcePosition / this.sampleRate; - } - }, { - key: "node", - get: function get() { - return this._node; - } - }, { - key: "pitch", - set: function set(pitch) { - this._soundtouch.pitch = pitch; - } - }, { - key: "pitchSemitones", - set: function set(semitone) { - this._soundtouch.pitchSemitones = semitone; - } - }, { - key: "rate", - set: function set(rate) { - this._soundtouch.rate = rate; - } - }, { - key: "tempo", - set: function set(tempo) { - this._soundtouch.tempo = tempo; - } - }, { - key: "connect", - value: function connect(toNode) { - this._node.connect(toNode); - } - }, { - key: "disconnect", - value: function disconnect() { - this._node.disconnect(); - } - }, { - key: "on", - value: function on(eventName, cb) { - this.listeners.push({ - name: eventName, - cb: cb - }); - this._node.addEventListener(eventName, function (event) { - return cb(event.detail); - }); - } - }, { - key: "off", - value: function off() { - var _this5 = this; - var eventName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var listeners = this.listeners; - if (eventName) { - listeners = listeners.filter(function (e) { - return e.name === eventName; - }); - } - listeners.forEach(function (e) { - _this5._node.removeEventListener(e.name, function (event) { - return e.cb(event.detail); - }); - }); - } - }]); - return PitchShifter; -})(); - -var ProcessAudioBufferSource = function () { - function ProcessAudioBufferSource(bufferProps, leftChannel, rightChannel) { - _classCallCheck(this, ProcessAudioBufferSource); - Object.assign(this, bufferProps); - this.leftChannel = leftChannel; - this.rightChannel = rightChannel; - this._position = 0; - } - _createClass(ProcessAudioBufferSource, [{ - key: "position", - get: function get() { - return this._position; - }, - set: function set(value) { - this._position = value; - } - }, { - key: "extract", - value: function extract(target) { - var numFrames = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; - var position = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - this.position = position; - var i = 0; - for (; i < numFrames; i++) { - target[i * 2] = this.leftChannel[i + position]; - target[i * 2 + 1] = this.rightChannel[i + position]; - } - return numFrames; - } - }]); - return ProcessAudioBufferSource; -}(); - -var SoundTouchWorklet = function (_AudioWorkletProcesso) { - _inherits(SoundTouchWorklet, _AudioWorkletProcesso); - var _super = _createSuper(SoundTouchWorklet); - function SoundTouchWorklet(nodeOptions) { - var _this; - _classCallCheck(this, SoundTouchWorklet); - _this = _super.call(this); - _this._initialized = false; - _this.bufferSize = 128; - _this.port.onmessage = _this._messageProcessor.bind(_assertThisInitialized(_this)); - _this.port.postMessage({ - message: 'PROCESSOR_CONSTRUCTOR', - detail: nodeOptions - }); - return _this; - } - _createClass(SoundTouchWorklet, [{ - key: "_messageProcessor", - value: function _messageProcessor(eventFromWorker) { - var _eventFromWorker$data = eventFromWorker.data, - message = _eventFromWorker$data.message, - detail = _eventFromWorker$data.detail; - if (message === 'INITIALIZE_PROCESSOR') { - var _detail = _slicedToArray(detail, 3), - bufferProps = _detail[0], - leftChannel = _detail[1], - rightChannel = _detail[2]; - this.bufferSource = new ProcessAudioBufferSource(bufferProps, leftChannel, rightChannel); - this._samples = new Float32Array(this.bufferSize * 2); - this._pipe = new SoundTouch(); - this._filter = new SimpleFilter(this.bufferSource, this._pipe); - this.port.postMessage({ - message: 'PROCESSOR_READY' - }); - this._initialized = true; - return true; - } - if (message === 'SET_PIPE_PROP' && detail) { - var name = detail.name, - value = detail.value; - this._pipe[name] = value; - this.port.postMessage({ - message: 'PIPE_PROP_CHANGED', - detail: "Updated ".concat(name, " to ").concat(this._pipe[name], "\ntypeof ").concat(_typeof(value)) - }); - return; - } - if (message === 'SET_FILTER_PROP' && detail) { - var _name = detail.name, - _value = detail.value; - this._filter[_name] = _value; - this.port.postMessage({ - message: 'FILTER_PROP_CHANGED', - detail: "Updated ".concat(_name, " to ").concat(this._filter[_name], "\ntypeof ").concat(_typeof(_value)) - }); - return; - } - console.log('[PitchShifterWorkletProcessor] Unknown message: ', eventFromWorker); - } - }, { - key: "_sendMessage", - value: function _sendMessage(message) { - var detail = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - if (!message) { - return; - } - this.port.postMessage({ - message: message, - detail: detail - }); - } - }, { - key: "process", - value: function process(inputs, outputs) { - if (!this._initialized || !inputs[0].length) { - return true; - } - var left = outputs[0][0]; - var right = outputs[0].length > 1 ? outputs[0][1] : outputs[0][0]; - var samples = this._samples; - if (!left || left && !left.length) { - return false; - } - var framesExtracted = this._filter.extract(samples, inputs[0][0].length); - if (isNaN(samples[0]) || !framesExtracted) { - this._sendMessage('PROCESSOR_END'); - return false; - } - this._sendMessage('SOURCEPOSITION', this._filter.sourcePosition); - var i = 0; - for (; i < framesExtracted; i = i + 1) { - left[i] = samples[i * 2]; - right[i] = samples[i * 2 + 1]; - if (isNaN(left[i]) || isNaN(right[i])) { - left[i] = 0; - right[i] = 0; - } - } - return true; - } - }]); - return SoundTouchWorklet; -}( _wrapNativeSuper(AudioWorkletProcessor)); -registerProcessor('soundtouch-worklet', SoundTouchWorklet); From 9124d3ac878f40253ff34520391935a0407cf2ae Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 15:18:56 -0400 Subject: [PATCH 10/29] Remove unused esbuild config --- esbuild.config.mjs | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 esbuild.config.mjs diff --git a/esbuild.config.mjs b/esbuild.config.mjs deleted file mode 100644 index 69ca0f4..0000000 --- a/esbuild.config.mjs +++ /dev/null @@ -1,29 +0,0 @@ -import esbuild from "esbuild"; -import process from "process"; -import builtins from 'builtin-modules' - -const banner = - `/* -THIS IS A GENERATED/BUNDLED FILE BY ESBUILD -if you want to view the source, please visit the github repository of this plugin -https://github.com/joethei/obsidian-tts -*/ -`; - -const prod = (process.argv[2] === 'production'); - -esbuild.build({ - banner: { - js: banner, - }, - entryPoints: ['src/main.ts'], - bundle: true, - external: ['obsidian', 'electron', ...builtins], - format: 'cjs', - watch: !prod, - target: 'es2016', - logLevel: "info", - sourcemap: prod ? false : 'inline', - treeShaking: true, - outfile: 'main.js', -}).catch(() => process.exit(1)); From 48dc258e669f2c542fe6ed49fe05be87da3fa4ee Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 15:40:30 -0400 Subject: [PATCH 11/29] Add banner and suppress warnings --- rollup.config.build.mjs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/rollup.config.build.mjs b/rollup.config.build.mjs index b873d78..c526f75 100644 --- a/rollup.config.build.mjs +++ b/rollup.config.build.mjs @@ -7,10 +7,19 @@ import webWorkerLoader from '@colingm/rollup-plugin-web-worker-loader'; import del from 'rollup-plugin-delete'; import copy from 'rollup-plugin-copy'; +const banner = + `/* +THIS IS A GENERATED/BUNDLED FILE CREATED USING ROLLUP +If you want to view the source, please visit the github repository of this plugin +https://github.com/joethei/obsidian-tts +*/ +`; + export default { input: 'src/main.ts', output: { dir: '.', + banner: banner, sourcemap: 'inline', format: 'cjs', exports: 'default' @@ -36,11 +45,10 @@ export default { }) ], onwarn: function(warning, warner) { - if (warning.code === 'CIRCULAR_DEPENDENCY'){ - if(warning.importer && warning.importer.startsWith('node_modules')){ - return; - } - } - warner(warning); + if (warning.id && /node_modules/.test(warning.id)) return; + if (warning.ids && warning.ids.every((id) => /node_modules/.test(id))) + return; + + warner(warning); } }; From 5eee0ba7b9ea3b4d84b5d5cbc1b5b4fa55775a95 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 16:01:28 -0400 Subject: [PATCH 12/29] Speed up build with esbuild --- package.json | 3 +-- rollup.config.build.mjs | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 78fc989..147d09a 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,6 @@ "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-terser": "^0.4.4", - "@rollup/plugin-typescript": "^12.1.0", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "7.32.0", @@ -41,6 +39,7 @@ "rollup": "^4.24.0", "rollup-plugin-copy": "^3.5.0", "rollup-plugin-delete": "^2.1.0", + "rollup-plugin-esbuild": "^6.1.1", "typedoc": "0.22.18" }, "overrides": { diff --git a/rollup.config.build.mjs b/rollup.config.build.mjs index c526f75..aec6b3a 100644 --- a/rollup.config.build.mjs +++ b/rollup.config.build.mjs @@ -1,11 +1,10 @@ -import typescript from '@rollup/plugin-typescript'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; -import terser from "@rollup/plugin-terser"; import json from '@rollup/plugin-json'; import webWorkerLoader from '@colingm/rollup-plugin-web-worker-loader'; import del from 'rollup-plugin-delete'; import copy from 'rollup-plugin-copy'; +import esbuild from 'rollup-plugin-esbuild' const banner = `/* @@ -26,10 +25,9 @@ export default { }, external: ['obsidian', 'electron'], plugins: [ - typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.d.ts'] }), nodeResolve({browser: true}), commonjs(), - // terser(), + esbuild({ target: 'es2018', minify: true }), json(), webWorkerLoader({ targetPlatform: 'browser' }), copy({ From c3c1e90a16dd79696659b842481bb9252822e472 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 18:22:15 -0400 Subject: [PATCH 13/29] Remove circular dependency --- src/LanguageVoiceModal.ts | 2 +- src/ServiceConfigurationModal.ts | 3 +- src/{services => }/constants.ts | 70 ++++++++++++++++++++++++++++++++ src/main.ts | 5 ++- src/settings.ts | 70 +------------------------------- 5 files changed, 76 insertions(+), 74 deletions(-) rename src/{services => }/constants.ts (68%) diff --git a/src/LanguageVoiceModal.ts b/src/LanguageVoiceModal.ts index c4b8060..eae45af 100644 --- a/src/LanguageVoiceModal.ts +++ b/src/LanguageVoiceModal.ts @@ -1,5 +1,5 @@ import {Modal, Setting} from "obsidian"; -import {LanguageVoiceMap} from "./settings"; +import { LanguageVoiceMap } from "./DEFAULT_SETTINGS"; import {TextInputPrompt} from "./TextInputPrompt"; import TTSPlugin from "./main"; import languages from "@cospired/i18n-iso-languages"; diff --git a/src/ServiceConfigurationModal.ts b/src/ServiceConfigurationModal.ts index faef0aa..a4e296e 100644 --- a/src/ServiceConfigurationModal.ts +++ b/src/ServiceConfigurationModal.ts @@ -1,7 +1,6 @@ import {Modal, Setting} from "obsidian"; import TTSPlugin from "./main"; -import {DEFAULT_SETTINGS} from "./settings"; -import {STYLE_OPTIONS, ROLE_OPTIONS, SERVICE_OPTIONS} from "./services/constants"; +import {DEFAULT_SETTINGS, STYLE_OPTIONS, ROLE_OPTIONS, SERVICE_OPTIONS} from "./constants"; export class ServiceConfigurationModal extends Modal { plugin: TTSPlugin; diff --git a/src/services/constants.ts b/src/constants.ts similarity index 68% rename from src/services/constants.ts rename to src/constants.ts index 8175113..56d8a18 100644 --- a/src/services/constants.ts +++ b/src/constants.ts @@ -1,5 +1,75 @@ import { SpeechSynthesisOutputFormat } from "microsoft-cognitiveservices-speech-sdk"; +export interface LanguageVoiceMap { + id: string; + language: string; + voice: string; +} + +export interface TTSDefaultVoices { + [service: string]: string; +} + +export interface TTSSettings { + defaultVoice: string, + defaultVoices: TTSDefaultVoices; + defaultService: string, + pitch: number; + rate: number; + volume: number; + speakLinks: boolean; + speakFrontmatter: boolean; + speakSyntax: boolean; + speakCodeblocks: boolean; + speakTitle: boolean; + speakEmoji: boolean; + speakComments: boolean; + languageVoices: LanguageVoiceMap[]; + stopPlaybackWhenNoteChanges: boolean; + services: { + openai: { + key: string; + }, + azure: { + key: string; + region: string; + role: string; + style: string; + intensity: number; + } + } +} + +export const DEFAULT_SETTINGS: TTSSettings = { + defaultVoice: "", + defaultVoices: {}, + defaultService: "speechSynthesis", + pitch: 1, + rate: 1, + volume: 1, + speakLinks: false, + speakFrontmatter: false, + speakSyntax: false, + speakTitle: true, + speakCodeblocks: false, + speakEmoji: false, + speakComments: false, + languageVoices: [], + stopPlaybackWhenNoteChanges: false, + services: { + openai: { + key: '', + }, + azure: { + key: '', + region: '', + role: 'OlderAdultFemale', + style: 'chat', + intensity: 1 + } + } +} + export const VOICE_FORMAT_NAMES = [ "raw-8khz-8bit-mono-mulaw(.wav)", "riff-16khz-16kbps-mono-siren(.wav)", diff --git a/src/main.ts b/src/main.ts index 277affe..135c4f4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,10 @@ import { addIcon, MarkdownFileInfo, - MarkdownView, Menu, Notice, Platform, + MarkdownView, Notice, Platform, Plugin, setIcon, TFile } from 'obsidian'; -import {DEFAULT_SETTINGS, LanguageVoiceMap, TTSSettings, TTSSettingsTab} from "./settings"; +import {TTSSettingsTab} from "./settings"; +import { DEFAULT_SETTINGS, LanguageVoiceMap, TTSSettings } from "./constants"; import {registerAPI} from "@vanakat/plugin-api"; import {detect} from "tinyld"; import {ServiceManager} from "./ServiceManager"; diff --git a/src/settings.ts b/src/settings.ts index e4a19ad..d155f5b 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -3,76 +3,8 @@ import {TextInputPrompt} from "./TextInputPrompt"; import TTSPlugin from "./main"; import {LanguageVoiceModal} from "./LanguageVoiceModal"; import { ServiceConfigurationModal } from "./ServiceConfigurationModal"; +import {DEFAULT_SETTINGS} from "./constants"; -export interface LanguageVoiceMap { - id: string; - language: string; - voice: string; -} - -export interface TTSDefaultVoices { - [service: string]: string; -} - -export interface TTSSettings { - defaultVoice: string, - defaultVoices: TTSDefaultVoices; - defaultService: string, - pitch: number; - rate: number; - volume: number; - speakLinks: boolean; - speakFrontmatter: boolean; - speakSyntax: boolean; - speakCodeblocks: boolean; - speakTitle: boolean; - speakEmoji: boolean; - speakComments: boolean; - languageVoices: LanguageVoiceMap[]; - stopPlaybackWhenNoteChanges: boolean; - services: { - openai: { - key: string; - }, - azure: { - key: string; - region: string; - role: string; - style: string; - intensity: number; - } - } -} - -export const DEFAULT_SETTINGS: TTSSettings = { - defaultVoice: "", - defaultVoices: {}, - defaultService: "speechSynthesis", - pitch: 1, - rate: 1, - volume: 1, - speakLinks: false, - speakFrontmatter: false, - speakSyntax: false, - speakTitle: true, - speakCodeblocks: false, - speakEmoji: false, - speakComments: false, - languageVoices: [], - stopPlaybackWhenNoteChanges: false, - services: { - openai: { - key: '', - }, - azure: { - key: '', - region: '', - role: 'OlderAdultFemale', - style: 'chat', - intensity: 1 - } - } -} export class TTSSettingsTab extends PluginSettingTab { plugin: TTSPlugin; From 5aad919de835664ec7bc8b552f920430a5d1b276 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 19:46:48 -0400 Subject: [PATCH 14/29] WIP refactoring out AudioPlayer --- src/services/AudioPlayer.ts | 110 ++++++++++++++++++++++++++++++++++++ src/services/OpenAI.ts | 50 +++++----------- src/services/TTSService.ts | 1 + 3 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 src/services/AudioPlayer.ts diff --git a/src/services/AudioPlayer.ts b/src/services/AudioPlayer.ts new file mode 100644 index 0000000..4089faa --- /dev/null +++ b/src/services/AudioPlayer.ts @@ -0,0 +1,110 @@ +// @ts-ignore +import registerSoundtouchWorklet from "audio-worklet:../soundtouch-worklet"; +import createSoundTouchNode from '@soundtouchjs/audio-worklet'; +import TTSPlugin from "src/main"; + +interface PlayEventDetail { + timePlayed: number, // float representing the current 'playHead' position in seconds + formattedTimePlayed: string, // the 'timePlayed' in 'mm:ss' format + percentagePlayed: number // int representing the percentage of what's been played based on the 'timePlayed' and audio duration +} + +interface Soundtouch { + rate: number; + tempo: number; + pitch: number; + percentagePlayed: number; + connectToBuffer(): AudioBufferSourceNode; + disconnectFromBuffer(): void; + connect(node: AudioNode): void; + disconnect(): void; + play(): void; + stop(): void; + pause(): void; + off(): void; + on(event: string, callback: (detail?: PlayEventDetail) => void): void; +} + +export default class AudioPlayer { + plugin: TTSPlugin; + audioCtx: AudioContext; + gainNode: GainNode; + soundtouch: Soundtouch; + buffer: ArrayBuffer; + bufferNode: AudioBufferSourceNode; + updateProgress: (detail: PlayEventDetail) => void; + isReady = false; + isPlaying = false; + paused = false; + + constructor(plugin: TTSPlugin) { + this.plugin = plugin; + this.createContext(); + } + + private async createContext() { + this.audioCtx = new AudioContext(); + await registerSoundtouchWorklet(this.audioCtx); + } + + private onEnd = (detail: PlayEventDetail) => { + // this.updateProgress(detail); + this.soundtouch.percentagePlayed = 0; + } + + // protected set setUpdateProgressCallback(updateProgress: (detail: PlayEventDetail) => void) { + // this.updateProgress = updateProgress; + // } + + private onInitialized() { + this.isReady = true; + // this.soundtouch.on('play', this.updateProgress); + this.soundtouch.on('end', () => this.onEnd); + this.soundtouch.tempo = this.plugin.settings.rate; + this.soundtouch.pitch = this.plugin.settings.pitch; + this.play(); + } + + protected setupSoundtouch(buffer: ArrayBuffer): void { + if (this.soundtouch) { + this.soundtouch.stop(); + this.soundtouch.off(); + } + this.buffer = buffer; + this.soundtouch = createSoundTouchNode(this.audioCtx, AudioWorkletNode, this.buffer); + this.soundtouch.on('initialized', () => this.onInitialized()); + } + + protected play(): void { + if (!this.isReady) return; + this.bufferNode = this.soundtouch.connectToBuffer(); // AudioBuffer goes to SoundTouchNode + this.gainNode = this.audioCtx.createGain(); + this.soundtouch.connect(this.gainNode); // SoundTouch goes to the GainNode + this.gainNode.connect(this.audioCtx.destination); // GainNode goes to the AudioDestinationNode + + this.soundtouch.play(); + + this.isPlaying = true; + } + + private disconnect(): boolean { + if (!this.bufferNode) return false; + this.gainNode.disconnect(); // disconnect the DestinationNode + this.soundtouch.disconnect(); // disconnect the AudioGainNode + this.soundtouch.disconnectFromBuffer(); // disconnect the SoundTouchNode + } + + protected stop(): void { + if (!this.disconnect()) return; + this.soundtouch.percentagePlayed = 0; + this.soundtouch.pause(); + this.paused = true; + } + + protected pause(): void { + if (!this.disconnect()) return; + this.soundtouch.pause(); + this.paused = true; + } + +} diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index 64bd576..7e07300 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -1,19 +1,15 @@ import {TTSService} from "./TTSService"; import TTSPlugin from "../main"; import {requestUrl} from "obsidian"; -// @ts-ignore -import registerSoundtouchWorklet from "audio-worklet:../soundtouch-worklet"; -import createSoundTouchNode from '@soundtouchjs/audio-worklet'; +import AudioPlayer from "./AudioPlayer"; -export class OpenAI implements TTSService { +export class OpenAI extends AudioPlayer implements TTSService { plugin: TTSPlugin; id = "openai"; name = "OpenAI"; - source: AudioBufferSourceNode | null = null; - currentTime = 0; - constructor(plugin: TTSPlugin) { + super(plugin); this.plugin = plugin; } @@ -59,13 +55,11 @@ export class OpenAI implements TTSService { } isPaused(): boolean { - if(!this.source) return true; - return this.source.context.state === "suspended"; + return this.paused; } isSpeaking(): boolean { - if(!this.source) return false; - return this.source.context.state === "running"; + return this.isPlaying; } isValid(): boolean { @@ -73,12 +67,11 @@ export class OpenAI implements TTSService { } pause(): void { - this.currentTime = this.source.context.currentTime; - this.source.stop(); + this.pause(); } resume(): void { - this.source.start(this.currentTime); + this.play(); } async sayWithVoice(text: string, voice: string) : Promise { @@ -97,32 +90,15 @@ export class OpenAI implements TTSService { }) }); - - const context = new AudioContext(); - // const buffer = await context.decodeAudioData(audioFile.arrayBuffer); - - await registerSoundtouchWorklet(context); - - // const soundtouchWorkletNode = new AudioWorkletNode(context, 'soundtouch-worklet'); - - const soundtouch = createSoundTouchNode(context, AudioWorkletNode, audioFile.arrayBuffer); - - soundtouch.on('initialized', () => { - console.log('SoundTouch initialized'); - soundtouch.tempo = this.plugin.settings.rate; - soundtouch.pitch = this.plugin.settings.pitch; - - const bufferNode = soundtouch.connectToBuffer(); // AudioBuffer goes to SoundTouchNode - const gainNode = context.createGain(); - soundtouch.connect(gainNode); // SoundTouch goes to the GainNode - gainNode.connect(context.destination); // GainNode goes to the AudioDestinationNode - - soundtouch.play(); - }); + this.setupSoundtouch(audioFile.arrayBuffer); } stop(): void { - this.source.stop(); + this.stop(); + } + + get progress(): number { + return this.soundtouch.percentagePlayed; } } diff --git a/src/services/TTSService.ts b/src/services/TTSService.ts index d41f5c1..4a80fe9 100644 --- a/src/services/TTSService.ts +++ b/src/services/TTSService.ts @@ -8,6 +8,7 @@ export interface TTSService { id: string; name: string; + progress: number; /** * @public From 2e298e9c763c80c897592d71cf68bb511d5a27f2 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Fri, 4 Oct 2024 23:32:30 -0400 Subject: [PATCH 15/29] Allow seeking SpeechSynthesis Working on OpenAI seek --- src/services/OpenAI.ts | 4 ++++ src/services/SpeechSynthesis.ts | 33 ++++++++++++++++++++++++++------- src/services/TTSService.ts | 2 ++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index 7e07300..c5fd243 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -101,4 +101,8 @@ export class OpenAI extends AudioPlayer implements TTSService { return this.soundtouch.percentagePlayed; } + seek(position: number): void { + this.soundtouch.percentagePlayed = position; + } + } diff --git a/src/services/SpeechSynthesis.ts b/src/services/SpeechSynthesis.ts index 23fdd04..fe187a6 100644 --- a/src/services/SpeechSynthesis.ts +++ b/src/services/SpeechSynthesis.ts @@ -6,18 +6,24 @@ export class SpeechSynthesis implements TTSService { plugin: TTSPlugin; id = 'speechSynthesis'; name = 'Speech Synthesis'; - currentTime = 0; + words: string[] = []; + wordCounter = 0; + voice = ''; + text = ''; constructor(plugin: TTSPlugin) { this.plugin = plugin; } get progress(): number { - return this.currentTime; + return this.wordCounter / this.words.length * 100; } - seek(time: number): void { - this.currentTime = time; + seek(percent: number): void { + const wordIndex = Math.floor(this.words.length * percent / 100); + const fragment = this.words.slice(wordIndex).join(' '); + this.wordCounter = wordIndex; + this.speak(fragment); } stop(): void { @@ -62,15 +68,28 @@ export class SpeechSynthesis implements TTSService { }) } - async sayWithVoice(text: string, voice: string): Promise { + private speak(text: string): void { const msg = new SpeechSynthesisUtterance(); msg.text = text; msg.volume = this.plugin.settings.volume; msg.rate = this.plugin.settings.rate; msg.pitch = this.plugin.settings.pitch; - msg.voice = window.speechSynthesis.getVoices().filter(otherVoice => otherVoice.name === voice)[0]; + msg.voice = window.speechSynthesis.getVoices().filter(otherVoice => otherVoice.name === this.voice)[0]; + msg.onboundary = (event) => { + if (event.name === "word") { + this.wordCounter++; + } + }; + window.speechSynthesis.cancel(); window.speechSynthesis.speak(msg); - this.plugin.statusbar.createSpan({text: 'Speaking'}); + } + + async sayWithVoice(text: string, voice: string): Promise { + this.text = text; + this.words = text.split(' '); + this.voice = voice; + this.speak(text); + // this.plugin.statusbar.createSpan({text: 'Speaking'}); } } diff --git a/src/services/TTSService.ts b/src/services/TTSService.ts index 4a80fe9..dfd3e7a 100644 --- a/src/services/TTSService.ts +++ b/src/services/TTSService.ts @@ -41,6 +41,8 @@ export interface TTSService { getVoices() : Promise; + seek(position: number): void; + /** * @internal * This may not be used, depending on user settings From 836d08b628c6fb6439e2369a6e241220edb46e85 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 03:32:54 -0400 Subject: [PATCH 16/29] Controls working --- src/main.ts | 11 ++++++----- src/services/AudioPlayer.ts | 28 +++++++++------------------- src/services/OpenAI.ts | 10 ++++++---- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/src/main.ts b/src/main.ts index 135c4f4..6caaaa3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -180,7 +180,6 @@ export default class TTSPlugin extends Plugin { return; } - this.menuVisible = true; await this.createMenu(); }); @@ -190,10 +189,10 @@ export default class TTSPlugin extends Plugin { removeMenu(): void { this.menuVisible = false; clearInterval(this.statusbarIntervalId); - this.seekbar.remove(); - this.playButton.remove(); - this.pauseButton.remove(); - this.stopButton.remove(); + this.seekbar?.remove(); + this.playButton?.remove(); + this.pauseButton?.remove(); + this.stopButton?.remove(); } async createMenu(): Promise { @@ -228,6 +227,7 @@ export default class TTSPlugin extends Plugin { const markdownView = this.app.workspace.activeEditor; if (markdownView) { + this.menuVisible = true; if (this.serviceManager.isSpeaking()) { this.playButton = this.addStatusBarItem() setIcon(this.playButton, 'play-audio-glyph'); @@ -241,6 +241,7 @@ export default class TTSPlugin extends Plugin { } if (this.serviceManager.isSpeaking()) { + this.menuVisible = true; // Seekbar const curProgress = this.serviceManager.progress ? this.serviceManager.progress : 0; this.seekbar = this.addStatusBarItem(); diff --git a/src/services/AudioPlayer.ts b/src/services/AudioPlayer.ts index 4089faa..e24a046 100644 --- a/src/services/AudioPlayer.ts +++ b/src/services/AudioPlayer.ts @@ -14,12 +14,14 @@ interface Soundtouch { tempo: number; pitch: number; percentagePlayed: number; + playing: boolean; + ready: boolean; connectToBuffer(): AudioBufferSourceNode; disconnectFromBuffer(): void; connect(node: AudioNode): void; disconnect(): void; play(): void; - stop(): void; + stop(): Promise; pause(): void; off(): void; on(event: string, callback: (detail?: PlayEventDetail) => void): void; @@ -32,9 +34,6 @@ export default class AudioPlayer { soundtouch: Soundtouch; buffer: ArrayBuffer; bufferNode: AudioBufferSourceNode; - updateProgress: (detail: PlayEventDetail) => void; - isReady = false; - isPlaying = false; paused = false; constructor(plugin: TTSPlugin) { @@ -47,18 +46,11 @@ export default class AudioPlayer { await registerSoundtouchWorklet(this.audioCtx); } - private onEnd = (detail: PlayEventDetail) => { - // this.updateProgress(detail); - this.soundtouch.percentagePlayed = 0; + private onEnd = () => { + this.stop(); } - // protected set setUpdateProgressCallback(updateProgress: (detail: PlayEventDetail) => void) { - // this.updateProgress = updateProgress; - // } - private onInitialized() { - this.isReady = true; - // this.soundtouch.on('play', this.updateProgress); this.soundtouch.on('end', () => this.onEnd); this.soundtouch.tempo = this.plugin.settings.rate; this.soundtouch.pitch = this.plugin.settings.pitch; @@ -76,15 +68,13 @@ export default class AudioPlayer { } protected play(): void { - if (!this.isReady) return; + if (!this.soundtouch.ready) return; this.bufferNode = this.soundtouch.connectToBuffer(); // AudioBuffer goes to SoundTouchNode this.gainNode = this.audioCtx.createGain(); this.soundtouch.connect(this.gainNode); // SoundTouch goes to the GainNode this.gainNode.connect(this.audioCtx.destination); // GainNode goes to the AudioDestinationNode this.soundtouch.play(); - - this.isPlaying = true; } private disconnect(): boolean { @@ -92,13 +82,13 @@ export default class AudioPlayer { this.gainNode.disconnect(); // disconnect the DestinationNode this.soundtouch.disconnect(); // disconnect the AudioGainNode this.soundtouch.disconnectFromBuffer(); // disconnect the SoundTouchNode + return true; } protected stop(): void { if (!this.disconnect()) return; - this.soundtouch.percentagePlayed = 0; - this.soundtouch.pause(); - this.paused = true; + this.soundtouch.off(); + this.soundtouch.stop(); } protected pause(): void { diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index c5fd243..3cb2645 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -55,11 +55,13 @@ export class OpenAI extends AudioPlayer implements TTSService { } isPaused(): boolean { + if (!this.soundtouch) return false; return this.paused; } isSpeaking(): boolean { - return this.isPlaying; + if (!this.soundtouch) return false; + return this.soundtouch.playing; } isValid(): boolean { @@ -67,11 +69,11 @@ export class OpenAI extends AudioPlayer implements TTSService { } pause(): void { - this.pause(); + super.pause(); } resume(): void { - this.play(); + super.play(); } async sayWithVoice(text: string, voice: string) : Promise { @@ -94,7 +96,7 @@ export class OpenAI extends AudioPlayer implements TTSService { } stop(): void { - this.stop(); + super.stop(); } get progress(): number { From c5237c858e8d7cc3ef3d10c7f110eaba9cc30f68 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 03:44:16 -0400 Subject: [PATCH 17/29] Add Speaking indicator to statusbar --- src/services/Azure.ts | 1 + src/services/OpenAI.ts | 1 + src/services/SpeechSynthesis.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/Azure.ts b/src/services/Azure.ts index 215f7a1..1f8cc58 100644 --- a/src/services/Azure.ts +++ b/src/services/Azure.ts @@ -223,6 +223,7 @@ export class Azure implements TTSService { this.source.onAudioEnd = () => { this._isPlaying = false; }; + this.plugin.statusbar.createSpan({text: 'Speaking'}); } }, function (e) { diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index 3cb2645..17f78a0 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -93,6 +93,7 @@ export class OpenAI extends AudioPlayer implements TTSService { }); this.setupSoundtouch(audioFile.arrayBuffer); + this.plugin.statusbar.createSpan({text: 'Speaking'}); } stop(): void { diff --git a/src/services/SpeechSynthesis.ts b/src/services/SpeechSynthesis.ts index fe187a6..7b9b2b9 100644 --- a/src/services/SpeechSynthesis.ts +++ b/src/services/SpeechSynthesis.ts @@ -89,7 +89,7 @@ export class SpeechSynthesis implements TTSService { this.words = text.split(' '); this.voice = voice; this.speak(text); - // this.plugin.statusbar.createSpan({text: 'Speaking'}); + this.plugin.statusbar.createSpan({text: 'Speaking'}); } } From aafa9f81e38ecf8fb03890b1fc77851755c1a5c4 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 03:53:53 -0400 Subject: [PATCH 18/29] Update settings UI --- src/ServiceConfigurationModal.ts | 4 ++-- src/settings.ts | 9 ++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ServiceConfigurationModal.ts b/src/ServiceConfigurationModal.ts index a4e296e..7f44da2 100644 --- a/src/ServiceConfigurationModal.ts +++ b/src/ServiceConfigurationModal.ts @@ -51,7 +51,7 @@ export class ServiceConfigurationModal extends Modal { if (service === 'azure') { new Setting(contentEl) - .setName('API Key') + .setName('Api key') .setDesc('Azure speech services API key') .addText(async text => { text @@ -62,7 +62,7 @@ export class ServiceConfigurationModal extends Modal { }); }); new Setting(contentEl) - .setName('Speech Region') + .setName('Speech region') .setDesc('Azure speech services region') .addText(async text => { text diff --git a/src/settings.ts b/src/settings.ts index d155f5b..cafa93f 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -82,16 +82,11 @@ export class TTSSettingsTab extends PluginSettingTab { }); }); - const servicesEl = containerEl.createDiv("settings-banner", (banner) => { - banner.createEl("h4", { - cls: "setting-item-name", - text: "Configured services", - }); - }); + new Setting(containerEl).setName('Configured services').setHeading(); for (const service of this.plugin.serviceManager.getServices()) { if (service.isConfigured() && service.isValid() && service.id !== "speechSynthesis") { - const setting = new Setting(servicesEl); + const setting = new Setting(containerEl); setting.setName(service.name); setting.addExtraButton((b) => { b.setIcon("pencil") From 57091b0feb593bee251992c3679af2f85b6c096a Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 04:49:04 -0400 Subject: [PATCH 19/29] Added RegExp settings --- src/LanguageVoiceModal.ts | 2 +- src/ServiceManager.ts | 2 +- src/TextInputPrompt.ts | 19 ++++++++++++---- src/constants.ts | 2 ++ src/main.ts | 4 ++-- src/settings.ts | 47 +++++++++++++++++++++++++++++++++++++-- src/utils.ts | 9 ++++++-- 7 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/LanguageVoiceModal.ts b/src/LanguageVoiceModal.ts index eae45af..83f0eac 100644 --- a/src/LanguageVoiceModal.ts +++ b/src/LanguageVoiceModal.ts @@ -73,7 +73,7 @@ export class LanguageVoiceModal extends Modal { .setIcon("play-audio-glyph") .setTooltip("Test voice") .onClick(async() => { - const input = new TextInputPrompt(this.app, "What do you want to hear?", "", "Hello world this is Text to speech running in obsidian", "Hello world this is Text to speech running in obsidian"); + const input = new TextInputPrompt(this.app, "What do you want to hear?", "", "Hello world this is Text to speech running in obsidian", "Hello world this is Text to speech running in obsidian", "Play", false); await input.openAndGetValue((async value => { if (value.getValue().length === 0) return; await this.plugin.serviceManager.sayWithVoice(value.getValue(), this.id); diff --git a/src/ServiceManager.ts b/src/ServiceManager.ts index 0ceb940..b6ef109 100644 --- a/src/ServiceManager.ts +++ b/src/ServiceManager.ts @@ -57,7 +57,7 @@ export class ServiceManager { if(!service) { new Notice("No service found for voice" + voice); } - await service.sayWithVoice(cleanText(text), voice); + await service.sayWithVoice(cleanText(text, this.plugin.settings.regexPatternsToIgnore), voice); } async getVoices(): Promise { diff --git a/src/TextInputPrompt.ts b/src/TextInputPrompt.ts index 6c67dd6..2f5705a 100644 --- a/src/TextInputPrompt.ts +++ b/src/TextInputPrompt.ts @@ -5,9 +5,17 @@ export class TextInputPrompt extends Modal { private resolve: (value: TextComponent) => void; private textComponent: TextComponent; - constructor(app: App, private promptText: string, private hint: string, private defaultValue: string, private placeholder: string) { - super(app); - } + constructor( + app: App, + private promptText: string, + private hint: string, + private defaultValue: string, + private placeholder: string, + private buttonText: string, + private autoClose: boolean = true + ) { + super(app); + } onOpen(): void { this.titleEl.setText(this.promptText); @@ -32,9 +40,12 @@ export class TextInputPrompt extends Modal { new Setting(div).addButton((b) => { b - .setButtonText("Play") + .setButtonText(this.buttonText) .onClick(async () => { this.resolve(this.textComponent); + if (this.autoClose) { + this.close(); + } }); return b; }); diff --git a/src/constants.ts b/src/constants.ts index 56d8a18..9ea3964 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,6 +26,7 @@ export interface TTSSettings { speakComments: boolean; languageVoices: LanguageVoiceMap[]; stopPlaybackWhenNoteChanges: boolean; + regexPatternsToIgnore: string[]; services: { openai: { key: string; @@ -56,6 +57,7 @@ export const DEFAULT_SETTINGS: TTSSettings = { speakComments: false, languageVoices: [], stopPlaybackWhenNoteChanges: false, + regexPatternsToIgnore: [], services: { openai: { key: '', diff --git a/src/main.ts b/src/main.ts index 6caaaa3..3b20c5e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -343,11 +343,11 @@ export default class TTSPlugin extends Plugin { if (service === undefined) { new Notice("TTS: Could not use configured language, please check your settings.\nUsing default voice"); - await this.serviceManager.sayWithVoice(cleanText(text), this.settings.defaultVoice); + await this.serviceManager.sayWithVoice(cleanText(text, this.settings.regexPatternsToIgnore), this.settings.defaultVoice); return; } - await service.sayWithVoice(cleanText(text), split[1]); + await service.sayWithVoice(cleanText(text, this.settings.regexPatternsToIgnore), split[1]); } prepareText(title: string, text: string): string { diff --git a/src/settings.ts b/src/settings.ts index cafa93f..0c81fca 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,4 +1,4 @@ -import {ButtonComponent, PluginSettingTab, Setting} from "obsidian"; +import {ButtonComponent, Notice, PluginSettingTab, Setting} from "obsidian"; import {TextInputPrompt} from "./TextInputPrompt"; import TTSPlugin from "./main"; import {LanguageVoiceModal} from "./LanguageVoiceModal"; @@ -56,7 +56,7 @@ export class TTSSettingsTab extends PluginSettingTab { .setIcon("play-audio-glyph") .setTooltip("Test voice") .onClick(async () => { - const input = new TextInputPrompt(this.app, "What do you want to hear?", "", "Hello world this is Text to speech running in obsidian", "Hello world this is Text to speech running in obsidian"); + const input = new TextInputPrompt(this.app, "What do you want to hear?", "", "Hello world this is Text to speech running in obsidian", "Hello world this is Text to speech running in obsidian", "Play", false); await input.openAndGetValue((async value => { if (value.getValue().length === 0) return; await this.plugin.serviceManager.sayWithVoice(value.getValue(), this.plugin.settings.defaultVoice); @@ -360,5 +360,48 @@ export class TTSSettingsTab extends PluginSettingTab { await this.plugin.saveSettings(); }); }); + + new Setting(containerEl) + .setName("Advanced") + .setHeading(); + new Setting(containerEl) + .setName("Regex patterns to ignore when speaking") + .addButton((button: ButtonComponent): ButtonComponent => { + return button + .setTooltip("Edit regex patterns to ignore") + .setIcon("pencil") + .onClick(() => { + new TextInputPrompt(this.app, "Regex patterns to ignore", "This pattern is passed to the RegExp constructor", "", "\\[[\d\s,-]*\\]", "Submit").openAndGetValue(async (value) => { + // check if the value is a valid regex + try { + new RegExp(value.getValue()); + } catch (e) { + new Notice("Invalid regex pattern"); + return; + } + // check if the value is already in the list + if (this.plugin.settings.regexPatternsToIgnore.includes(value.getValue())) { + new Notice("This pattern is already in the list"); + return; + } + this.plugin.settings.regexPatternsToIgnore.push(value.getValue()); + await this.plugin.saveSettings(); + this.display(); + }); + }); + }); + for (const pattern of this.plugin.settings.regexPatternsToIgnore) { + const setting = new Setting(containerEl); + setting.setName(pattern); + setting.addExtraButton((b) => { + b.setIcon("trash") + .setTooltip("Delete") + .onClick(() => { + this.plugin.settings.regexPatternsToIgnore = this.plugin.settings.regexPatternsToIgnore.filter(value => value !== pattern); + this.plugin.saveSettings(); + this.display(); + }); + }); + } } } diff --git a/src/utils.ts b/src/utils.ts index 594be86..1467477 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,8 @@ -export const cleanText = (text: string): string => { - return text; // TODO: Implement +export const cleanText = (text: string, patterns: string[]): string => { + if (!patterns || patterns.length === 0) return text; + let res = text; + for (const pattern of patterns) { + res = res.replace(new RegExp(pattern, "g"), ""); + } + return res; } From a2467f5dcc76eb842c5cba9a9b01465815cc4ebc Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 11:03:36 -0400 Subject: [PATCH 20/29] SpeechSynthesis statusbar bugfixes --- src/main.ts | 6 +++--- src/services/SpeechSynthesis.ts | 5 ++++- src/utils.ts | 7 +++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 3b20c5e..63a65c1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,7 @@ import { DEFAULT_SETTINGS, LanguageVoiceMap, TTSSettings } from "./constants"; import {registerAPI} from "@vanakat/plugin-api"; import {detect} from "tinyld"; import {ServiceManager} from "./ServiceManager"; -import { cleanText } from './utils'; +import { cleanText, resetStatusbar } from './utils'; import 'regenerator-runtime/runtime'; @@ -128,8 +128,7 @@ export default class TTSPlugin extends Plugin { //clear statusbar text if not speaking this.registerInterval(window.setInterval(() => { if (!this.serviceManager.isSpeaking()) { - this.statusbar.empty(); - setIcon(this.statusbar, 'audio-file'); + resetStatusbar(this.statusbar); } }, 1000 * 10)); @@ -277,6 +276,7 @@ export default class TTSPlugin extends Plugin { setIcon(this.stopButton, 'stop-audio-glyph'); this.stopButton.onClickEvent(() => { this.serviceManager.stop(); + resetStatusbar(this.statusbar); this.removeMenu(); }); diff --git a/src/services/SpeechSynthesis.ts b/src/services/SpeechSynthesis.ts index 7b9b2b9..af32671 100644 --- a/src/services/SpeechSynthesis.ts +++ b/src/services/SpeechSynthesis.ts @@ -1,6 +1,7 @@ import {Platform} from "obsidian"; import TTSPlugin from "../main"; import {TTSService} from "./TTSService"; +import { resetStatusbar } from "../utils"; export class SpeechSynthesis implements TTSService { plugin: TTSPlugin; @@ -81,6 +82,8 @@ export class SpeechSynthesis implements TTSService { } }; window.speechSynthesis.cancel(); + resetStatusbar(this.plugin.statusbar); + this.plugin.statusbar.createSpan({text: 'Speaking'}); window.speechSynthesis.speak(msg); } @@ -88,8 +91,8 @@ export class SpeechSynthesis implements TTSService { this.text = text; this.words = text.split(' '); this.voice = voice; + this.wordCounter = 0; this.speak(text); - this.plugin.statusbar.createSpan({text: 'Speaking'}); } } diff --git a/src/utils.ts b/src/utils.ts index 1467477..6ed9cf1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import { setIcon } from "obsidian"; + export const cleanText = (text: string, patterns: string[]): string => { if (!patterns || patterns.length === 0) return text; let res = text; @@ -6,3 +8,8 @@ export const cleanText = (text: string, patterns: string[]): string => { } return res; } + +export const resetStatusbar = (statusbar: HTMLElement): void => { + statusbar.empty(); + setIcon(statusbar, 'audio-file'); +} From 5938d93bf071f6d4b945be1bee6a62393833669e Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 11:08:33 -0400 Subject: [PATCH 21/29] Update rollup dev config --- rollup.config.dev.mjs | 44 ++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/rollup.config.dev.mjs b/rollup.config.dev.mjs index 7c5e287..26f4855 100644 --- a/rollup.config.dev.mjs +++ b/rollup.config.dev.mjs @@ -1,35 +1,45 @@ -import typescript from '@rollup/plugin-typescript'; import {nodeResolve} from '@rollup/plugin-node-resolve'; import json from '@rollup/plugin-json'; import commonjs from '@rollup/plugin-commonjs'; -import copy from 'rollup-plugin-copy' +import webWorkerLoader from '@colingm/rollup-plugin-web-worker-loader'; +import del from 'rollup-plugin-delete'; +import copy from 'rollup-plugin-copy'; +import esbuild from 'rollup-plugin-esbuild' export default { input: 'src/main.ts', output: { - dir: 'examples/.obsidian/plugins/obsidian-tracker', + dir: './dist/obsidian-tts', sourcemap: 'inline', format: 'cjs', exports: 'default' }, - external: ['obsidian'], + external: ['obsidian', 'electron'], plugins: [ - typescript({ tsconfig: './tsconfig.json', exclude: ['**/*.d.ts'] }), nodeResolve({browser: true}), commonjs(), + esbuild({ target: 'es2018', minify: true }), json(), - copy({ - targets: [ - { src: ['styles.css', 'manifest.json'], dest: 'examples/.obsidian/plugins/obsidian-tracker' } - ] - }) + webWorkerLoader({ targetPlatform: 'browser' }), + copy({ + targets: [{ + src: 'node_modules/@soundtouchjs/audio-worklet/dist/soundtouch-worklet.js', + dest: 'src' + }, + { src: ['manifest.json'], dest: './dist/obsidian-tts' } + ], + hook: 'buildStart' + }), + del({ + targets: 'src/soundtouch-worklet.js', + hook: 'buildEnd' + }) ], - onwarn: function(warning, warner){ - if (warning.code === 'CIRCULAR_DEPENDENCY'){ - if(warning.importer && warning.importer.startsWith('node_modules')){ - return; - } - } - warner(warning); + onwarn: function(warning, warner) { + if (warning.id && /node_modules/.test(warning.id)) return; + if (warning.ids && warning.ids.every((id) => /node_modules/.test(id))) + return; + + warner(warning); } }; From c0838ba0eef7c845193fa01962780033979bd096 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 12:01:42 -0400 Subject: [PATCH 22/29] OpenAI statusbar bugfixes --- rollup.config.dev.mjs | 2 +- src/services/AudioPlayer.ts | 15 +++++++-------- src/services/OpenAI.ts | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/rollup.config.dev.mjs b/rollup.config.dev.mjs index 26f4855..93a4b50 100644 --- a/rollup.config.dev.mjs +++ b/rollup.config.dev.mjs @@ -18,7 +18,7 @@ export default { plugins: [ nodeResolve({browser: true}), commonjs(), - esbuild({ target: 'es2018', minify: true }), + esbuild({ target: 'es2018', minify: false }), json(), webWorkerLoader({ targetPlatform: 'browser' }), copy({ diff --git a/src/services/AudioPlayer.ts b/src/services/AudioPlayer.ts index e24a046..de76787 100644 --- a/src/services/AudioPlayer.ts +++ b/src/services/AudioPlayer.ts @@ -2,6 +2,7 @@ import registerSoundtouchWorklet from "audio-worklet:../soundtouch-worklet"; import createSoundTouchNode from '@soundtouchjs/audio-worklet'; import TTSPlugin from "src/main"; +import { resetStatusbar } from "../utils"; interface PlayEventDetail { timePlayed: number, // float representing the current 'playHead' position in seconds @@ -32,7 +33,6 @@ export default class AudioPlayer { audioCtx: AudioContext; gainNode: GainNode; soundtouch: Soundtouch; - buffer: ArrayBuffer; bufferNode: AudioBufferSourceNode; paused = false; @@ -57,13 +57,11 @@ export default class AudioPlayer { this.play(); } - protected setupSoundtouch(buffer: ArrayBuffer): void { + protected async setupSoundtouch(buffer: ArrayBuffer): Promise { if (this.soundtouch) { - this.soundtouch.stop(); - this.soundtouch.off(); + await this.stop(); } - this.buffer = buffer; - this.soundtouch = createSoundTouchNode(this.audioCtx, AudioWorkletNode, this.buffer); + this.soundtouch = createSoundTouchNode(this.audioCtx, AudioWorkletNode, buffer); this.soundtouch.on('initialized', () => this.onInitialized()); } @@ -85,10 +83,11 @@ export default class AudioPlayer { return true; } - protected stop(): void { + protected async stop(): Promise { if (!this.disconnect()) return; this.soundtouch.off(); - this.soundtouch.stop(); + await this.soundtouch.stop(); + resetStatusbar(this.plugin.statusbar); } protected pause(): void { diff --git a/src/services/OpenAI.ts b/src/services/OpenAI.ts index 17f78a0..246d9b6 100644 --- a/src/services/OpenAI.ts +++ b/src/services/OpenAI.ts @@ -92,12 +92,12 @@ export class OpenAI extends AudioPlayer implements TTSService { }) }); - this.setupSoundtouch(audioFile.arrayBuffer); + await this.setupSoundtouch(audioFile.arrayBuffer); this.plugin.statusbar.createSpan({text: 'Speaking'}); } - stop(): void { - super.stop(); + stop(): Promise { + return super.stop(); } get progress(): number { From eb0f8fa206c8484135741a9b44ac4fc618c6e2a1 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 12:05:40 -0400 Subject: [PATCH 23/29] Azure statusbar bugfixes --- src/services/Azure.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/services/Azure.ts b/src/services/Azure.ts index 1f8cc58..b1fc6bc 100644 --- a/src/services/Azure.ts +++ b/src/services/Azure.ts @@ -7,6 +7,7 @@ import { SpeechSynthesizer, ResultReason } from "microsoft-cognitiveservices-speech-sdk"; +import { resetStatusbar } from "../utils"; export class Azure implements TTSService { plugin: TTSPlugin; @@ -182,6 +183,10 @@ export class Azure implements TTSService { } async sayWithVoice(text: string, voice: string) : Promise { + if (this.source) { + this.stop(); + resetStatusbar(this.plugin.statusbar); + } const speechConfig = SpeechConfig.fromSubscription( this.plugin.settings.services.azure.key, this.plugin.settings.services.azure.region From a8a2b9549204b60ec020103547488df31509baeb Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 12:19:44 -0400 Subject: [PATCH 24/29] ES6 compatibility and language fixes --- rollup.config.build.mjs | 2 +- rollup.config.dev.mjs | 2 +- src/ServiceConfigurationModal.ts | 2 +- src/services/Azure.ts | 4 ++-- tsconfig.json | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rollup.config.build.mjs b/rollup.config.build.mjs index aec6b3a..b8e5229 100644 --- a/rollup.config.build.mjs +++ b/rollup.config.build.mjs @@ -27,7 +27,7 @@ export default { plugins: [ nodeResolve({browser: true}), commonjs(), - esbuild({ target: 'es2018', minify: true }), + esbuild({ target: 'es2015', minify: true }), json(), webWorkerLoader({ targetPlatform: 'browser' }), copy({ diff --git a/rollup.config.dev.mjs b/rollup.config.dev.mjs index 93a4b50..38542b4 100644 --- a/rollup.config.dev.mjs +++ b/rollup.config.dev.mjs @@ -18,7 +18,7 @@ export default { plugins: [ nodeResolve({browser: true}), commonjs(), - esbuild({ target: 'es2018', minify: false }), + esbuild({ target: 'es2015', minify: false }), json(), webWorkerLoader({ targetPlatform: 'browser' }), copy({ diff --git a/src/ServiceConfigurationModal.ts b/src/ServiceConfigurationModal.ts index 7f44da2..3793d04 100644 --- a/src/ServiceConfigurationModal.ts +++ b/src/ServiceConfigurationModal.ts @@ -51,7 +51,7 @@ export class ServiceConfigurationModal extends Modal { if (service === 'azure') { new Setting(contentEl) - .setName('Api key') + .setName('API key') .setDesc('Azure speech services API key') .addText(async text => { text diff --git a/src/services/Azure.ts b/src/services/Azure.ts index b1fc6bc..636b92a 100644 --- a/src/services/Azure.ts +++ b/src/services/Azure.ts @@ -231,8 +231,8 @@ export class Azure implements TTSService { this.plugin.statusbar.createSpan({text: 'Speaking'}); } }, - function (e) { - console.log(e); + function (error: string) { + console.error('[Azure]', error); synthesizer.close(); } ); diff --git a/tsconfig.json b/tsconfig.json index 09511d2..3a68827 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "ES2018", + "target": "ES6", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", From c72d71309aaf461b6f917df726a084ad6524ffd6 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 12:26:24 -0400 Subject: [PATCH 25/29] Moved seekbar styles to styles.css --- src/main.ts | 7 +------ src/styles.css | 8 ++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 src/styles.css diff --git a/src/main.ts b/src/main.ts index 63a65c1..2b3a565 100644 --- a/src/main.ts +++ b/src/main.ts @@ -245,13 +245,8 @@ export default class TTSPlugin extends Plugin { const curProgress = this.serviceManager.progress ? this.serviceManager.progress : 0; this.seekbar = this.addStatusBarItem(); const slider = this.seekbar.createEl('input', {type: 'range', attr: {min: '0', max: '100', value: curProgress.toString(), step: '1'}}); + slider.classList.add('seekbar'); slider.style.background = getSeekbarBackgroundStyle(curProgress); - slider.style.border = "solid 1px var(--interactive-accent)"; - slider.style.borderRadius = "8px"; - slider.style.height = "7px"; - slider.style.width = "200px"; - slider.style.outline = "none"; - slider.style.transition = "background 450ms ease-in"; slider.oninput = function(this: HTMLInputElement) { const value = parseInt(this.value); this.style.background = getSeekbarBackgroundStyle(value); diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..f85304b --- /dev/null +++ b/src/styles.css @@ -0,0 +1,8 @@ +.seekbar { + border: "solid 1px var(--interactive-accent)"; + border-radius: "8px"; + height: "7px"; + width: "200px"; + outline: "none"; + transition: "background 450ms ease-in"; +} From a0a98fc7ac3b21cadb5be598ba68b28516c370c2 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 12:30:30 -0400 Subject: [PATCH 26/29] Added styles.css to publish workflow --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 125e310..900c7e5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,7 +26,7 @@ jobs: yarn yarn run build mkdir ${{ env.PLUGIN_NAME }} - cp main.js manifest.json ${{ env.PLUGIN_NAME }} + cp main.js manifest.json styles.css ${{ env.PLUGIN_NAME }} zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} ls echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" From d3a7358bfc561a31b1ade949160d89f17e97d3f8 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 13:53:43 -0400 Subject: [PATCH 27/29] Change pause icon --- src/main.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main.ts b/src/main.ts index 2b3a565..3bee4fb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,8 +33,6 @@ export default class TTSPlugin extends Plugin { // from https://github.com/phosphor-icons/core addIcon('tts-play-pause', ''); - - console.log("loading tts plugin"); //https://bugs.chromium.org/p/chromium/issues/detail?id=487255 @@ -77,7 +75,7 @@ export default class TTSPlugin extends Plugin { this.addCommand({ id: 'pause-tts-playback', name: 'pause playback', - icon: 'pause', + icon: 'lucide-pause-circle', checkCallback: (checking: boolean) => { if (!checking) { this.serviceManager.pause(); @@ -215,7 +213,7 @@ export default class TTSPlugin extends Plugin { } const createPauseButton = () => { this.pauseButton = this.addStatusBarItem(); - setIcon(this.pauseButton, 'pause'); + setIcon(this.pauseButton, 'lucide-pause-circle'); this.pauseButton.onClickEvent(() => { clearInterval(this.statusbarIntervalId); this.serviceManager.pause(); @@ -333,6 +331,7 @@ export default class TTSPlugin extends Plugin { new Notice("TTS: could not find voice for language " + languageCode + ". Using default voice."); } } + // @ts-ignore const split = usedVoice.split(/-(.*)/s); const service = this.serviceManager.getServices().filter(service => service.id === split[0] && service.isConfigured() && service.isValid()).first(); From 05d62beecb22b6b6b521214c318b1c5b9d3f73c8 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 14:05:26 -0400 Subject: [PATCH 28/29] Fix controls on markdownview --- src/main.ts | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/src/main.ts b/src/main.ts index 3bee4fb..eff5f8b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -221,31 +221,15 @@ export default class TTSPlugin extends Plugin { createPlayButton(); }); } - - const markdownView = this.app.workspace.activeEditor; - if (markdownView) { - this.menuVisible = true; - if (this.serviceManager.isSpeaking()) { - this.playButton = this.addStatusBarItem() - setIcon(this.playButton, 'play-audio-glyph'); - this.playButton.onClickEvent(async () => { - await this.play(markdownView); - }); - } else { - await this.play(markdownView); - return; - } - } - - if (this.serviceManager.isSpeaking()) { + const showControls = () => { this.menuVisible = true; // Seekbar const curProgress = this.serviceManager.progress ? this.serviceManager.progress : 0; this.seekbar = this.addStatusBarItem(); - const slider = this.seekbar.createEl('input', {type: 'range', attr: {min: '0', max: '100', value: curProgress.toString(), step: '1'}}); + const slider = this.seekbar.createEl('input', { type: 'range', attr: { min: '0', max: '100', value: curProgress.toString(), step: '1' } }); slider.classList.add('seekbar'); slider.style.background = getSeekbarBackgroundStyle(curProgress); - slider.oninput = function(this: HTMLInputElement) { + slider.oninput = function (this: HTMLInputElement) { const value = parseInt(this.value); this.style.background = getSeekbarBackgroundStyle(value); }; @@ -279,6 +263,14 @@ export default class TTSPlugin extends Plugin { createPauseButton(); } } + + const markdownView = this.app.workspace.activeEditor; + if (markdownView) { + await this.play(markdownView); + showControls(); + } else if (this.serviceManager.isSpeaking()) { + showControls(); + } } async onunload(): Promise { From 99482b0cc2f37d5b117c5d475ecf9dd8005e93a8 Mon Sep 17 00:00:00 2001 From: Pavel Yarmak Date: Sat, 5 Oct 2024 14:16:06 -0400 Subject: [PATCH 29/29] Enable plugin, excluding SpeechSythesis service, on Android --- src/ServiceManager.ts | 8 ++++++-- src/main.ts | 8 +------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/ServiceManager.ts b/src/ServiceManager.ts index b6ef109..80f856e 100644 --- a/src/ServiceManager.ts +++ b/src/ServiceManager.ts @@ -1,7 +1,7 @@ import {TTSService} from "./services/TTSService"; import TTSPlugin from "./main"; import {SpeechSynthesis} from "./services/SpeechSynthesis"; -import {Notice} from "obsidian"; +import {Notice, Platform} from "obsidian"; import { OpenAI } from "./services/OpenAI"; import { Azure } from "./services/Azure"; import { cleanText } from "./utils"; @@ -19,7 +19,11 @@ export class ServiceManager { constructor(plugin: TTSPlugin) { this.plugin = plugin; - this.services.push(new SpeechSynthesis(this.plugin)); + // Due to a bug in android SpeechSynthesis does not work on this platform + // https://bugs.chromium.org/p/chromium/issues/detail?id=487255 + if (!Platform.isAndroidApp) { + this.services.push(new SpeechSynthesis(this.plugin)); + } this.services.push(new OpenAI(this.plugin)); this.services.push(new Azure(this.plugin)); this.activeService = this.services.find(service => this.plugin.settings.defaultService === service.id); diff --git a/src/main.ts b/src/main.ts index eff5f8b..4d110a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { addIcon, MarkdownFileInfo, - MarkdownView, Notice, Platform, + MarkdownView, Notice, Plugin, setIcon, TFile } from 'obsidian'; import {TTSSettingsTab} from "./settings"; @@ -35,12 +35,6 @@ export default class TTSPlugin extends Plugin { console.log("loading tts plugin"); - //https://bugs.chromium.org/p/chromium/issues/detail?id=487255 - if (Platform.isAndroidApp) { - new Notice("TTS: due to a bug in android this plugin does not work on this platform"); - this.unload(); - } - await this.loadSettings(); this.serviceManager = new ServiceManager(this);