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()">
|
<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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user