diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts index 315ee1856ea..d66333e1aec 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts @@ -142,9 +142,9 @@ class ViewModel extends Disposable { /** @description play audio-cue for diff */ const currentViewItem = this.currentElement.read(reader); if (currentViewItem?.type === LineType.Deleted) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'accessibleDiffViewer.currentElementChanged' }); } else if (currentViewItem?.type === LineType.Added) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'accessibleDiffViewer.currentElementChanged' }); } })); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts index e2612415c32..ed02f28c188 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts @@ -255,11 +255,11 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { if (e?.reason === CursorChangeReason.Explicit) { const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modifiedRange.contains(e.position.lineNumber)); if (diff?.lineRangeMapping.modifiedRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); } else if (diff?.lineRangeMapping.originalRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); } else if (diff) { - this._audioCueService.playAudioCue(AudioCue.diffLineModified); + this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); } } })); @@ -460,11 +460,11 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._goTo(diff); if (diff.lineRangeMapping.modifiedRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.goToDiff' }); } else if (diff.lineRangeMapping.originalRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.goToDiff' }); } else if (diff) { - this._audioCueService.playAudioCue(AudioCue.diffLineModified); + this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.goToDiff' }); } } diff --git a/src/vs/editor/browser/widget/diffNavigator.ts b/src/vs/editor/browser/widget/diffNavigator.ts index 0b221d7c7a4..0f34a1e5960 100644 --- a/src/vs/editor/browser/widget/diffNavigator.ts +++ b/src/vs/editor/browser/widget/diffNavigator.ts @@ -224,11 +224,11 @@ export class DiffNavigator extends Disposable implements IDiffNavigator { } const insertedOrModified = modifiedEditor.getLineDecorations(lineNumber).find(l => l.options.className === 'line-insert'); if (insertedOrModified) { - this._audioCueService.playAudioCue(AudioCue.diffLineModified, true); + this._audioCueService.playAudioCue(AudioCue.diffLineModified, { allowManyInParallel: true }); } else if (jumpToChange) { // The modified editor does not include deleted lines, but when // we are moved to the area where lines were deleted, play this cue - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true }); } else { return; } diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 66ec0c678e1..ea813d85aa5 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -325,9 +325,9 @@ export class DiffReview extends Disposable { } const element = !type ? current : type === 'next' ? current?.nextElementSibling : current?.previousElementSibling; if (element?.classList.contains(DiffEditorLineClasses.Insert)) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted, true); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { allowManyInParallel: true }); } else if (element?.classList.contains(DiffEditorLineClasses.Delete)) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true }); } this.scrollbar.scanDomNode(); } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index f026f61f400..e7297361013 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -1039,7 +1039,7 @@ class StandaloneContextMenuService extends ContextMenuService { class StandaloneAudioService implements IAudioCueService { _serviceBrand: undefined; - async playAudioCue(cue: AudioCue, allowManyInParallel?: boolean | undefined): Promise { + async playAudioCue(cue: AudioCue, options: {}): Promise { } async playAudioCues(cues: AudioCue[]): Promise { diff --git a/src/vs/platform/audioCues/browser/audioCueService.ts b/src/vs/platform/audioCues/browser/audioCueService.ts index a7b6d7507fd..bbf03de5613 100644 --- a/src/vs/platform/audioCues/browser/audioCueService.ts +++ b/src/vs/platform/audioCues/browser/audioCueService.ts @@ -11,13 +11,14 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { observableFromEvent, derived } from 'vs/base/common/observable'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAudioCueService = createDecorator('audioCue'); export interface IAudioCueService { readonly _serviceBrand: undefined; - playAudioCue(cue: AudioCue, allowManyInParallel?: boolean): Promise; - playAudioCues(cues: AudioCue[]): Promise; + playAudioCue(cue: AudioCue, options?: IAudioCueOptions): Promise; + playAudioCues(cues: (AudioCue | { cue: AudioCue; source: string })[]): Promise; isEnabled(cue: AudioCue): boolean; onEnabledChanged(cue: AudioCue): Event; @@ -25,33 +26,72 @@ export interface IAudioCueService { playAudioCueLoop(cue: AudioCue, milliseconds: number): IDisposable; } +export interface IAudioCueOptions { + allowManyInParallel?: boolean; + source?: string; +} + export class AudioCueService extends Disposable implements IAudioCueService { readonly _serviceBrand: undefined; - sounds: Map = new Map(); + private readonly sounds: Map = new Map(); private readonly screenReaderAttached = observableFromEvent( this.accessibilityService.onDidChangeScreenReaderOptimized, () => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized() ); + private readonly sentTelemetry = new Set(); constructor( @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); } - public async playAudioCue(cue: AudioCue, allowManyInParallel = false): Promise { + public async playAudioCue(cue: AudioCue, options: IAudioCueOptions = {}): Promise { if (this.isEnabled(cue)) { - await this.playSound(cue.sound.getSound(), allowManyInParallel); + this.sendAudioCueTelemetry(cue, options.source); + await this.playSound(cue.sound.getSound(), options.allowManyInParallel); } } - public async playAudioCues(cues: AudioCue[]): Promise { + public async playAudioCues(cues: (AudioCue | { cue: AudioCue; source: string })[]): Promise { + for (const cue of cues) { + this.sendAudioCueTelemetry('cue' in cue ? cue.cue : cue, 'source' in cue ? cue.source : undefined); + } + // Some audio cues might reuse sounds. Don't play the same sound twice. - const sounds = new Set(cues.filter(cue => this.isEnabled(cue)).map(cue => cue.sound.getSound())); + const sounds = new Set(cues.map(c => 'cue' in c ? c.cue : c).filter(cue => this.isEnabled(cue)).map(cue => cue.sound.getSound())); await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true))); } + private sendAudioCueTelemetry(cue: AudioCue, source: string | undefined): void { + const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized(); + const key = cue.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); + // Only send once per user session + if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) { + return; + } + this.sentTelemetry.add(key); + + this.telemetryService.publicLog2<{ + audioCue: string; + source: string; + isScreenReaderOptimized: boolean; + }, { + owner: 'hediet'; + + audioCue: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The audio cue that was played.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source that triggered the audio cue (e.g. "diffEditorNavigation").' }; + isScreenReaderOptimized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is using a screen reader' }; + + comment: 'This data is collected to understand how audio cues are used and if more audio cues should be added.'; + }>('audioCue.played', { + audioCue: cue.name, + source: source ?? '', + isScreenReaderOptimized, + }); + } private getVolumeInPercent(): number { const volume = this.configurationService.getValue('audioCues.volume'); @@ -92,7 +132,7 @@ export class AudioCueService extends Disposable implements IAudioCueService { let playing = true; const playSound = () => { if (playing) { - this.playAudioCue(cue, true).finally(() => { + this.playAudioCue(cue, { allowManyInParallel: true }).finally(() => { setTimeout(() => { if (playing) { playSound(); diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index 026abbda6f0..71a9a649c0c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -30,7 +30,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS)); } acceptRequest(): void { - this._audioCueService.playAudioCue(AudioCue.chatRequestSent, true); + this._audioCueService.playAudioCue(AudioCue.chatRequestSent, { allowManyInParallel: true }); this._runOnceScheduler.schedule(); } acceptResponse(response?: IChatResponseViewModel | string): void { @@ -42,7 +42,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi if (this._lastResponse === responseContent) { return; } - this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, true); + this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, { allowManyInParallel: true }); this._hasReceivedRequest = false; if (!response) { return; diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 09798922953..3b274ee4c2a 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -662,13 +662,13 @@ async function playAudioCueForChange(change: IChange, audioCueService: IAudioCue const changeType = getChangeType(change); switch (changeType) { case ChangeType.Add: - audioCueService.playAudioCue(AudioCue.diffLineInserted, true); + audioCueService.playAudioCue(AudioCue.diffLineInserted, { allowManyInParallel: true, source: 'dirtyDiffDecoration' }); break; case ChangeType.Delete: - audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); + audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true, source: 'dirtyDiffDecoration' }); break; case ChangeType.Modify: - audioCueService.playAudioCue(AudioCue.diffLineModified, true); + audioCueService.playAudioCue(AudioCue.diffLineModified, { allowManyInParallel: true, source: 'dirtyDiffDecoration' }); break; } } diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts index 27ffd2db8cb..18a05b903cf 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts @@ -147,7 +147,7 @@ export class AccessibleBufferWidget extends TerminalAccessibleWidget { return; } if (activeItem.exitCode) { - this._audioCueService.playAudioCue(AudioCue.error, true); + this._audioCueService.playAudioCue(AudioCue.error, { allowManyInParallel: true, source: 'accessibleBufferWidget' }); } this.editorWidget.revealLine(activeItem.lineNumber, 0); });