canti primo tag
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user