canti primo tag

This commit is contained in:
David Frassi
2026-05-16 17:24:59 +02:00
commit 0c33fc6fcf
106 changed files with 28210 additions and 0 deletions
+187
View File
@@ -0,0 +1,187 @@
import { Injectable, signal } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AudioEngineService {
public isListening = signal<boolean>(false);
public wordsDetected = signal<number>(0);
public linesDetected = signal<number>(0);
public energyLevel = signal<number>(0); // For visual feedback
public searchTranscript = signal<string>('');
public isSearching = signal<boolean>(false);
public sensitivity = signal<number>(75); // Default sensitivity (50-100)
public clearSearchTranscript() {
this.searchTranscript.set('');
}
private audioContext: AudioContext | null = null;
private analyser: AnalyserNode | null = null;
private stream: MediaStream | null = null;
private animationFrame: number | null = null;
private recognition: any = null;
// Detection logic state
private isSpeaking: boolean = false;
private lastSilenceTime: number = Date.now();
private lastWordTime: number = 0;
// Constants for tuning - Optimized for close proximity (singer/guitarist)
private readonly SILENCE_GAP = 100; // ms
private readonly LINE_SILENCE_GAP = 600; // ms
private readonly COOLDOWN = 1000; // ms
private peakEnergy: number = 0;
private lineStarted: boolean = false;
constructor() {
this.initSpeechRecognition();
}
private initSpeechRecognition() {
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
if (SpeechRecognition) {
this.recognition = new SpeechRecognition();
this.recognition.continuous = false; // Stop after one phrase for search
this.recognition.interimResults = true;
this.recognition.lang = 'it-IT';
this.recognition.onresult = (event: any) => {
const text = event.results[0][0].transcript;
this.searchTranscript.set(text);
};
this.recognition.onend = () => {
this.isSearching.set(false);
};
this.recognition.onerror = (err: any) => {
console.error('Search recognition error:', err);
this.isSearching.set(false);
};
}
}
startSearchRecognition() {
if (!this.recognition) {
alert('Il riconoscimento vocale non è supportato in questo browser.');
return;
}
this.searchTranscript.set('');
this.isSearching.set(true);
try {
this.recognition.start();
} catch (e) {
console.warn('Recognition already started', e);
}
}
stopSearchRecognition() {
this.recognition?.stop();
this.isSearching.set(false);
}
async startListening() {
if (this.isListening()) return;
try {
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: false // Prevents boosting background noise during silence
}
});
this.audioContext = new AudioContext();
const source = this.audioContext.createMediaStreamSource(this.stream);
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 512;
source.connect(this.analyser);
this.isListening.set(true);
this.processAudio();
} catch (err) {
console.error('Error accessing microphone', err);
alert('Errore microfono: assicurati di usare HTTPS e di aver dato i permessi.');
}
}
stopListening() {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
this.stream?.getTracks().forEach(track => track.stop());
this.audioContext?.close();
this.isListening.set(false);
this.energyLevel.set(0);
}
private processAudio() {
if (!this.analyser) return;
const bufferLength = this.analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
const analyze = () => {
this.analyser!.getByteFrequencyData(dataArray);
// Calculate average energy (volume)
let sum = 0;
for (let i = 0; i < bufferLength; i++) {
sum += dataArray[i];
}
const avgEnergy = sum / bufferLength;
this.energyLevel.set(avgEnergy);
const now = Date.now();
// Balanced mapping: 0% -> 220 (very quiet), 100% -> 20 (very sensitive)
const currentThreshold = 220 - (this.sensitivity() * 2.0);
if (avgEnergy > currentThreshold) {
if (avgEnergy > this.peakEnergy) {
this.peakEnergy = avgEnergy;
}
if (!this.isSpeaking) {
this.isSpeaking = true;
this.peakEnergy = avgEnergy;
this.lastSilenceTime = now;
}
// Se siamo in "speaking" e sentiamo un calo significativo rispetto al picco recente (almeno 30% di calo)
// Questo permette di avanzare anche se c'è rumore di fondo sopra la soglia base.
const dropRatio = (this.peakEnergy - avgEnergy) / this.peakEnergy;
if (this.isSpeaking && dropRatio > 0.35 && this.peakEnergy > currentThreshold * 1.2) {
if (now - this.lastWordTime > this.COOLDOWN) {
this.linesDetected.update(v => v + 1);
this.lastWordTime = now;
this.peakEnergy = avgEnergy; // Reset peak
this.isSpeaking = false;
console.log('Line advanced - relative drop detected', { dropRatio, avgEnergy, peak: this.peakEnergy });
}
}
this.lastSilenceTime = now;
} else {
// Sotto soglia (silenzio per il sistema)
if (this.isSpeaking && (now - this.lastSilenceTime > 200)) {
if (now - this.lastWordTime > this.COOLDOWN) {
this.linesDetected.update(v => v + 1);
this.lastWordTime = now;
console.log('Line advanced - silence detected');
}
this.isSpeaking = false;
this.peakEnergy = 0;
}
}
this.animationFrame = requestAnimationFrame(analyze);
};
analyze();
}
resetWordCount() {
this.wordsDetected.set(0);
this.linesDetected.set(0);
this.lineStarted = false;
}
}