Added sound oszilator for search display
Some checks failed
Build, Test & Push Frontend / docker (pull_request) Has been cancelled
Build, Test & Push Frontend / quality-check (pull_request) Has been cancelled

This commit is contained in:
2026-03-07 17:19:53 +01:00
parent 2ab1d2dd85
commit 66643d8e18
5 changed files with 109 additions and 1 deletions

View File

@@ -0,0 +1,80 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class SortingAudioService {
private audioContext: AudioContext | null = null;
private ensureContext(): AudioContext {
this.audioContext ??= new AudioContext();
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
return this.audioContext;
}
// Call this on the user gesture (button click) so the AudioContext can be created/resumed.
initOnUserGesture(): void {
this.ensureContext();
}
playTone(value: number, maxValue: number, animationSpeedMs: number): void {
const ctx = this.ensureContext();
const frequency = this.valueToFrequency(value, maxValue);
// Keep tone duration slightly shorter than the animation frame to avoid overlap
const duration = Math.min(0.1, (animationSpeedMs * 0.75) / 1000);
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.type = 'sawtooth';
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + duration);
}
playSortedSweep(sortedValues: number[], maxValue: number): void {
const ctx = this.ensureContext();
// Play a quick ascending sweep through all sorted bar values
const step = Math.ceil(sortedValues.length / 40);
sortedValues.forEach((value, i) => {
if (i % step !== 0) {
return;
}
const delayMs = (i / step) * 18;
setTimeout(() => {
const frequency = this.valueToFrequency(value, maxValue);
const oscillator = ctx.createOscillator();
const gainNode = ctx.createGain();
oscillator.connect(gainNode);
gainNode.connect(ctx.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(frequency, ctx.currentTime);
gainNode.gain.setValueAtTime(0.15, ctx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.06);
oscillator.start(ctx.currentTime);
oscillator.stop(ctx.currentTime + 0.06);
}, delayMs);
});
}
// Maps a bar value linearly to the frequency range 1801100 Hz (roughly 3 octaves)
private valueToFrequency(value: number, maxValue: number): number {
const minFreq = 400;
const maxFreq = 800;
return minFreq + (value / maxValue) * (maxFreq - minFreq);
}
}

View File

@@ -37,6 +37,10 @@
<button mat-raised-button (click)="generateNewArray()"> <button mat-raised-button (click)="generateNewArray()">
<mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }} <mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }}
</button> </button>
<button mat-raised-button (click)="toggleSound()">
<mat-icon>{{ isSoundEnabled ? 'volume_up' : 'volume_off' }}</mat-icon>
{{ isSoundEnabled ? ('SORTING.SOUND_OFF' | translate) : ('SORTING.SOUND_ON' | translate) }}
</button>
</div> </div>
<div class="controls-panel"> <div class="controls-panel">

View File

@@ -7,6 +7,7 @@ import {MatButtonModule} from "@angular/material/button";
import {MatIconModule} from "@angular/material/icon"; import {MatIconModule} from "@angular/material/icon";
import {TranslateModule} from "@ngx-translate/core"; import {TranslateModule} from "@ngx-translate/core";
import { SortingService } from './service/sorting.service'; import { SortingService } from './service/sorting.service';
import { SortingAudioService } from './service/sorting-audio.service';
import {SortData, SortSnapshot} from './sorting.models'; import {SortData, SortSnapshot} from './sorting.models';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import {MatInput} from '@angular/material/input'; import {MatInput} from '@angular/material/input';
@@ -22,6 +23,7 @@ import {Information} from '../information/information';
export class SortingComponent implements OnInit { export class SortingComponent implements OnInit {
private readonly sortingService: SortingService = inject(SortingService); private readonly sortingService: SortingService = inject(SortingService);
private readonly audioService: SortingAudioService = inject(SortingAudioService);
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef); private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
readonly MAX_ARRAY_SIZE: number = 200; readonly MAX_ARRAY_SIZE: number = 200;
@@ -70,9 +72,10 @@ export class SortingComponent implements OnInit {
unsortedArrayCopy: SortData[] = []; unsortedArrayCopy: SortData[] = [];
arraySize = 50; arraySize = 50;
maxArrayValue = 100; maxArrayValue = 100;
animationSpeed = 50; // Milliseconds per step animationSpeed = 100; // Milliseconds per step
selectedAlgorithm: string = this.algoInformation.entries[0].name; selectedAlgorithm: string = this.algoInformation.entries[0].name;
executionTime = 0; executionTime = 0;
isSoundEnabled = false;
ngOnInit(): void { ngOnInit(): void {
this.generateNewArray(); this.generateNewArray();
@@ -117,8 +120,14 @@ export class SortingComponent implements OnInit {
} }
} }
toggleSound(): void {
this.isSoundEnabled = !this.isSoundEnabled;
}
async startSorting(): Promise<void> { async startSorting(): Promise<void> {
this.resetSorting(); this.resetSorting();
// Init the AudioContext on this user gesture so the browser allows sound
this.audioService.initOnUserGesture();
const startTime = performance.now(); const startTime = performance.now();
let snapshots: SortSnapshot[] = []; let snapshots: SortSnapshot[] = [];
@@ -156,11 +165,22 @@ export class SortingComponent implements OnInit {
} }
} }
if (this.isSoundEnabled) {
// Play a tone for each comparing bar (max 2 at once to avoid noise)
snapshot.array
.filter(item => item.state === 'comparing')
.slice(0, 2)
.forEach(item => this.audioService.playTone(item.value, this.maxArrayValue, this.animationSpeed));
}
this.cdr.detectChanges(); this.cdr.detectChanges();
if (index === snapshots.length - 1) { if (index === snapshots.length - 1) {
this.sortArray.forEach(item => item.state = 'sorted'); this.sortArray.forEach(item => item.state = 'sorted');
this.cdr.detectChanges(); this.cdr.detectChanges();
if (this.isSoundEnabled) {
this.audioService.playSortedSweep(this.sortArray.map(item => item.value), this.maxArrayValue);
}
} }
}, index * this.animationSpeed); }, index * this.animationSpeed);

View File

@@ -364,6 +364,8 @@
"START": "Sortierung starten", "START": "Sortierung starten",
"RESET": "Zurücksetzen", "RESET": "Zurücksetzen",
"GENERATE_NEW_ARRAY": "Neues Array generieren", "GENERATE_NEW_ARRAY": "Neues Array generieren",
"SOUND_ON": "Ton an",
"SOUND_OFF": "Ton aus",
"EXECUTION_TIME": "Ausführungszeit", "EXECUTION_TIME": "Ausführungszeit",
"ARRAY_SIZE": "Anzahl der Balken", "ARRAY_SIZE": "Anzahl der Balken",
"EXPLANATION": { "EXPLANATION": {

View File

@@ -364,6 +364,8 @@
"START": "Start Sorting", "START": "Start Sorting",
"RESET": "Reset", "RESET": "Reset",
"GENERATE_NEW_ARRAY": "Generate New Array", "GENERATE_NEW_ARRAY": "Generate New Array",
"SOUND_ON": "Sound On",
"SOUND_OFF": "Sound Off",
"EXECUTION_TIME": "Execution Time", "EXECUTION_TIME": "Execution Time",
"ARRAY_SIZE": "Number of Bars", "ARRAY_SIZE": "Number of Bars",
"EXPLANATION": { "EXPLANATION": {