Added sound oszilator for search display
This commit is contained in:
@@ -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 180–1100 Hz (roughly 3 octaves)
|
||||
private valueToFrequency(value: number, maxValue: number): number {
|
||||
const minFreq = 400;
|
||||
const maxFreq = 800;
|
||||
return minFreq + (value / maxValue) * (maxFreq - minFreq);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@
|
||||
<button mat-raised-button (click)="generateNewArray()">
|
||||
<mat-icon>add_box</mat-icon> {{ 'SORTING.GENERATE_NEW_ARRAY' | translate }}
|
||||
</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 class="controls-panel">
|
||||
|
||||
@@ -7,6 +7,7 @@ import {MatButtonModule} from "@angular/material/button";
|
||||
import {MatIconModule} from "@angular/material/icon";
|
||||
import {TranslateModule} from "@ngx-translate/core";
|
||||
import { SortingService } from './service/sorting.service';
|
||||
import { SortingAudioService } from './service/sorting-audio.service';
|
||||
import {SortData, SortSnapshot} from './sorting.models';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {MatInput} from '@angular/material/input';
|
||||
@@ -22,6 +23,7 @@ import {Information} from '../information/information';
|
||||
export class SortingComponent implements OnInit {
|
||||
|
||||
private readonly sortingService: SortingService = inject(SortingService);
|
||||
private readonly audioService: SortingAudioService = inject(SortingAudioService);
|
||||
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
||||
|
||||
readonly MAX_ARRAY_SIZE: number = 200;
|
||||
@@ -70,9 +72,10 @@ export class SortingComponent implements OnInit {
|
||||
unsortedArrayCopy: SortData[] = [];
|
||||
arraySize = 50;
|
||||
maxArrayValue = 100;
|
||||
animationSpeed = 50; // Milliseconds per step
|
||||
animationSpeed = 100; // Milliseconds per step
|
||||
selectedAlgorithm: string = this.algoInformation.entries[0].name;
|
||||
executionTime = 0;
|
||||
isSoundEnabled = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.generateNewArray();
|
||||
@@ -117,8 +120,14 @@ export class SortingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
toggleSound(): void {
|
||||
this.isSoundEnabled = !this.isSoundEnabled;
|
||||
}
|
||||
|
||||
async startSorting(): Promise<void> {
|
||||
this.resetSorting();
|
||||
// Init the AudioContext on this user gesture so the browser allows sound
|
||||
this.audioService.initOnUserGesture();
|
||||
const startTime = performance.now();
|
||||
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();
|
||||
|
||||
if (index === snapshots.length - 1) {
|
||||
this.sortArray.forEach(item => item.state = 'sorted');
|
||||
this.cdr.detectChanges();
|
||||
if (this.isSoundEnabled) {
|
||||
this.audioService.playSortedSweep(this.sortArray.map(item => item.value), this.maxArrayValue);
|
||||
}
|
||||
}
|
||||
}, index * this.animationSpeed);
|
||||
|
||||
|
||||
@@ -364,6 +364,8 @@
|
||||
"START": "Sortierung starten",
|
||||
"RESET": "Zurücksetzen",
|
||||
"GENERATE_NEW_ARRAY": "Neues Array generieren",
|
||||
"SOUND_ON": "Ton an",
|
||||
"SOUND_OFF": "Ton aus",
|
||||
"EXECUTION_TIME": "Ausführungszeit",
|
||||
"ARRAY_SIZE": "Anzahl der Balken",
|
||||
"EXPLANATION": {
|
||||
|
||||
@@ -364,6 +364,8 @@
|
||||
"START": "Start Sorting",
|
||||
"RESET": "Reset",
|
||||
"GENERATE_NEW_ARRAY": "Generate New Array",
|
||||
"SOUND_ON": "Sound On",
|
||||
"SOUND_OFF": "Sound Off",
|
||||
"EXECUTION_TIME": "Execution Time",
|
||||
"ARRAY_SIZE": "Number of Bars",
|
||||
"EXPLANATION": {
|
||||
|
||||
Reference in New Issue
Block a user