|
|
|
export const FEATURE_VECTOR_SIZE = 32; |
|
|
|
interface AdvancedFeatures { |
|
spectralCentroid: number; |
|
spectralRolloff: number; |
|
spectralFlux: number; |
|
zeroCrossingRate: number; |
|
rms: number; |
|
peak: number; |
|
crest: number; |
|
spectralSpread: number; |
|
spectralFlatness: number; |
|
spectralSlope: number; |
|
harmonicRatio: number; |
|
noiseRatio: number; |
|
tonalPower: number; |
|
spectralContrast: number[]; |
|
spectralBandEnergy: number[]; |
|
temporalFeatures: number[]; |
|
} |
|
|
|
|
|
export const extractMLFeatures = ( |
|
magnitudes: number[], |
|
rawData: Uint8Array, |
|
previousAmplitudes: number[], |
|
sampleRate: number |
|
): number[] => { |
|
const features: number[] = new Array(FEATURE_VECTOR_SIZE).fill(0); |
|
|
|
try { |
|
|
|
const amplitudes = convertRawToAmplitudes(rawData); |
|
|
|
|
|
const advancedFeatures = extractAdvancedFeatures(magnitudes, amplitudes, previousAmplitudes, sampleRate); |
|
|
|
|
|
const featureVector = mapToFeatureVector(advancedFeatures); |
|
|
|
|
|
for (let i = 0; i < Math.min(FEATURE_VECTOR_SIZE, featureVector.length); i++) { |
|
features[i] = featureVector[i]; |
|
} |
|
|
|
return features; |
|
} catch (error) { |
|
console.warn('Error extracting ML features:', error); |
|
|
|
return extractBasicFeatures(magnitudes, rawData, sampleRate); |
|
} |
|
}; |
|
|
|
|
|
function convertRawToAmplitudes(rawData: Uint8Array): number[] { |
|
const amplitudes: number[] = []; |
|
|
|
|
|
for (let i = 0; i < rawData.length - 1; i += 2) { |
|
|
|
const sample = (rawData[i + 1] << 8) | rawData[i]; |
|
const signed = sample > 32767 ? sample - 65536 : sample; |
|
amplitudes.push(signed / 32768.0); |
|
} |
|
|
|
return amplitudes; |
|
} |
|
|
|
|
|
function extractAdvancedFeatures( |
|
magnitudes: number[], |
|
amplitudes: number[], |
|
previousAmplitudes: number[], |
|
sampleRate: number |
|
): AdvancedFeatures { |
|
|
|
const N = magnitudes.length; |
|
const nyquist = sampleRate / 2; |
|
|
|
|
|
const spectralCentroid = calculateSpectralCentroid(magnitudes, nyquist); |
|
|
|
|
|
const spectralRolloff = calculateSpectralRolloff(magnitudes, nyquist, 0.85); |
|
|
|
|
|
const spectralFlux = calculateSpectralFlux(magnitudes, previousAmplitudes); |
|
|
|
|
|
const zeroCrossingRate = calculateZeroCrossingRate(amplitudes); |
|
|
|
|
|
const rms = calculateRMS(amplitudes); |
|
|
|
|
|
const peak = Math.max(...amplitudes.map(Math.abs)); |
|
|
|
|
|
const crest = rms > 0 ? peak / rms : 0; |
|
|
|
|
|
const spectralSpread = calculateSpectralSpread(magnitudes, spectralCentroid, nyquist); |
|
|
|
|
|
const spectralFlatness = calculateSpectralFlatness(magnitudes); |
|
|
|
|
|
const spectralSlope = calculateSpectralSlope(magnitudes, nyquist); |
|
|
|
|
|
const { harmonicRatio, noiseRatio } = calculateHarmonicNoiseRatio(magnitudes); |
|
|
|
|
|
const tonalPower = calculateTonalPower(magnitudes); |
|
|
|
|
|
const spectralContrast = calculateSpectralContrast(magnitudes, 7); |
|
|
|
|
|
const spectralBandEnergy = calculateBandEnergy(magnitudes, 8); |
|
|
|
|
|
const temporalFeatures = calculateTemporalFeatures(amplitudes, previousAmplitudes); |
|
|
|
return { |
|
spectralCentroid, |
|
spectralRolloff, |
|
spectralFlux, |
|
zeroCrossingRate, |
|
rms, |
|
peak, |
|
crest, |
|
spectralSpread, |
|
spectralFlatness, |
|
spectralSlope, |
|
harmonicRatio, |
|
noiseRatio, |
|
tonalPower, |
|
spectralContrast, |
|
spectralBandEnergy, |
|
temporalFeatures |
|
}; |
|
} |
|
|
|
|
|
function mapToFeatureVector(features: AdvancedFeatures): number[] { |
|
const vector: number[] = []; |
|
|
|
|
|
vector.push( |
|
features.spectralCentroid, |
|
features.spectralRolloff, |
|
features.spectralFlux, |
|
features.zeroCrossingRate, |
|
features.rms, |
|
features.peak, |
|
features.crest, |
|
features.spectralSpread, |
|
features.spectralFlatness, |
|
features.spectralSlope, |
|
features.harmonicRatio, |
|
features.noiseRatio, |
|
features.tonalPower |
|
); |
|
|
|
|
|
vector.push(...features.spectralContrast); |
|
|
|
|
|
vector.push(...features.spectralBandEnergy); |
|
|
|
|
|
vector.push(...features.temporalFeatures); |
|
|
|
return vector.slice(0, 32); |
|
} |
|
|
|
|
|
|
|
function calculateSpectralCentroid(magnitudes: number[], nyquist: number): number { |
|
let weightedSum = 0; |
|
let magnitudeSum = 0; |
|
|
|
for (let i = 0; i < magnitudes.length; i++) { |
|
const freq = (i * nyquist) / magnitudes.length; |
|
weightedSum += freq * magnitudes[i]; |
|
magnitudeSum += magnitudes[i]; |
|
} |
|
|
|
return magnitudeSum > 0 ? weightedSum / magnitudeSum : 0; |
|
} |
|
|
|
function calculateSpectralRolloff(magnitudes: number[], nyquist: number, threshold: number): number { |
|
const totalEnergy = magnitudes.reduce((sum, mag) => sum + mag * mag, 0); |
|
const targetEnergy = totalEnergy * threshold; |
|
|
|
let cumulativeEnergy = 0; |
|
for (let i = 0; i < magnitudes.length; i++) { |
|
cumulativeEnergy += magnitudes[i] * magnitudes[i]; |
|
if (cumulativeEnergy >= targetEnergy) { |
|
return (i * nyquist) / magnitudes.length; |
|
} |
|
} |
|
|
|
return nyquist; |
|
} |
|
|
|
function calculateSpectralFlux(current: number[], previous: number[]): number { |
|
if (previous.length === 0) return 0; |
|
|
|
let flux = 0; |
|
const minLength = Math.min(current.length, previous.length); |
|
|
|
for (let i = 0; i < minLength; i++) { |
|
const diff = current[i] - previous[i]; |
|
if (diff > 0) flux += diff * diff; |
|
} |
|
|
|
return Math.sqrt(flux / minLength); |
|
} |
|
|
|
function calculateZeroCrossingRate(amplitudes: number[]): number { |
|
let crossings = 0; |
|
|
|
for (let i = 1; i < amplitudes.length; i++) { |
|
if ((amplitudes[i] >= 0) !== (amplitudes[i-1] >= 0)) { |
|
crossings++; |
|
} |
|
} |
|
|
|
return crossings / (amplitudes.length - 1); |
|
} |
|
|
|
function calculateRMS(amplitudes: number[]): number { |
|
const sumSquares = amplitudes.reduce((sum, amp) => sum + amp * amp, 0); |
|
return Math.sqrt(sumSquares / amplitudes.length); |
|
} |
|
|
|
function calculateSpectralSpread(magnitudes: number[], centroid: number, nyquist: number): number { |
|
let weightedVariance = 0; |
|
let magnitudeSum = 0; |
|
|
|
for (let i = 0; i < magnitudes.length; i++) { |
|
const freq = (i * nyquist) / magnitudes.length; |
|
const deviation = freq - centroid; |
|
weightedVariance += deviation * deviation * magnitudes[i]; |
|
magnitudeSum += magnitudes[i]; |
|
} |
|
|
|
return magnitudeSum > 0 ? Math.sqrt(weightedVariance / magnitudeSum) : 0; |
|
} |
|
|
|
function calculateSpectralFlatness(magnitudes: number[]): number { |
|
let geometricMean = 1; |
|
let arithmeticMean = 0; |
|
let count = 0; |
|
|
|
for (const mag of magnitudes) { |
|
if (mag > 0) { |
|
geometricMean *= Math.pow(mag, 1 / magnitudes.length); |
|
arithmeticMean += mag; |
|
count++; |
|
} |
|
} |
|
|
|
arithmeticMean /= count; |
|
return arithmeticMean > 0 ? geometricMean / arithmeticMean : 0; |
|
} |
|
|
|
function calculateSpectralSlope(magnitudes: number[], nyquist: number): number { |
|
let sumXY = 0, sumX = 0, sumY = 0, sumX2 = 0; |
|
const n = magnitudes.length; |
|
|
|
for (let i = 0; i < n; i++) { |
|
const x = (i * nyquist) / n; |
|
const y = magnitudes[i]; |
|
|
|
sumXY += x * y; |
|
sumX += x; |
|
sumY += y; |
|
sumX2 += x * x; |
|
} |
|
|
|
const denominator = n * sumX2 - sumX * sumX; |
|
return denominator !== 0 ? (n * sumXY - sumX * sumY) / denominator : 0; |
|
} |
|
|
|
function calculateHarmonicNoiseRatio(magnitudes: number[]): { harmonicRatio: number, noiseRatio: number } { |
|
|
|
const sortedMags = [...magnitudes].sort((a, b) => b - a); |
|
const peakEnergy = sortedMags.slice(0, Math.floor(sortedMags.length * 0.1)).reduce((a, b) => a + b, 0); |
|
const totalEnergy = magnitudes.reduce((a, b) => a + b, 0); |
|
|
|
const harmonicRatio = totalEnergy > 0 ? peakEnergy / totalEnergy : 0; |
|
const noiseRatio = 1 - harmonicRatio; |
|
|
|
return { harmonicRatio, noiseRatio }; |
|
} |
|
|
|
function calculateTonalPower(magnitudes: number[]): number { |
|
|
|
let tonalPower = 0; |
|
const threshold = Math.max(...magnitudes) * 0.1; |
|
|
|
for (const mag of magnitudes) { |
|
if (mag > threshold) { |
|
tonalPower += mag * mag; |
|
} |
|
} |
|
|
|
const totalPower = magnitudes.reduce((sum, mag) => sum + mag * mag, 0); |
|
return totalPower > 0 ? tonalPower / totalPower : 0; |
|
} |
|
|
|
function calculateSpectralContrast(magnitudes: number[], numBands: number): number[] { |
|
const bandSize = Math.floor(magnitudes.length / numBands); |
|
const contrasts: number[] = []; |
|
|
|
for (let band = 0; band < numBands; band++) { |
|
const start = band * bandSize; |
|
const end = Math.min(start + bandSize, magnitudes.length); |
|
const bandMags = magnitudes.slice(start, end); |
|
|
|
if (bandMags.length > 0) { |
|
const sortedBand = [...bandMags].sort((a, b) => b - a); |
|
const peakMean = sortedBand.slice(0, Math.max(1, Math.floor(sortedBand.length * 0.2))) |
|
.reduce((a, b) => a + b, 0) / Math.max(1, Math.floor(sortedBand.length * 0.2)); |
|
const valleyMean = sortedBand.slice(Math.floor(sortedBand.length * 0.8)) |
|
.reduce((a, b) => a + b, 0) / Math.max(1, sortedBand.length - Math.floor(sortedBand.length * 0.8)); |
|
|
|
contrasts.push(valleyMean > 0 ? Math.log(peakMean / valleyMean) : 0); |
|
} else { |
|
contrasts.push(0); |
|
} |
|
} |
|
|
|
return contrasts; |
|
} |
|
|
|
function calculateBandEnergy(magnitudes: number[], numBands: number): number[] { |
|
const bandSize = Math.floor(magnitudes.length / numBands); |
|
const energies: number[] = []; |
|
|
|
for (let band = 0; band < numBands; band++) { |
|
const start = band * bandSize; |
|
const end = Math.min(start + bandSize, magnitudes.length); |
|
|
|
let energy = 0; |
|
for (let i = start; i < end; i++) { |
|
energy += magnitudes[i] * magnitudes[i]; |
|
} |
|
|
|
energies.push(energy / (end - start)); |
|
} |
|
|
|
return energies; |
|
} |
|
|
|
function calculateTemporalFeatures(current: number[], previous: number[]): number[] { |
|
const features: number[] = []; |
|
|
|
|
|
const currentEnergy = current.reduce((sum, amp) => sum + amp * amp, 0); |
|
const previousEnergy = previous.length > 0 ? previous.reduce((sum, amp) => sum + amp * amp, 0) : currentEnergy; |
|
const energyChange = previousEnergy > 0 ? (currentEnergy - previousEnergy) / previousEnergy : 0; |
|
features.push(energyChange); |
|
|
|
|
|
let autocorr = 0; |
|
if (current.length > 1) { |
|
for (let i = 1; i < current.length; i++) { |
|
autocorr += current[i] * current[i-1]; |
|
} |
|
autocorr /= (current.length - 1); |
|
} |
|
features.push(autocorr); |
|
|
|
|
|
const mean = current.reduce((a, b) => a + b, 0) / current.length; |
|
const variance = current.reduce((sum, amp) => sum + (amp - mean) * (amp - mean), 0) / current.length; |
|
features.push(variance); |
|
|
|
|
|
const std = Math.sqrt(variance); |
|
let skewness = 0; |
|
if (std > 0) { |
|
skewness = current.reduce((sum, amp) => sum + Math.pow((amp - mean) / std, 3), 0) / current.length; |
|
} |
|
features.push(skewness); |
|
|
|
return features; |
|
} |
|
|
|
|
|
function extractBasicFeatures(magnitudes: number[], rawData: Uint8Array, sampleRate: number): number[] { |
|
const features: number[] = new Array(FEATURE_VECTOR_SIZE).fill(0); |
|
|
|
|
|
for (let i = 0; i < Math.min(FEATURE_VECTOR_SIZE, magnitudes.length); i++) { |
|
features[i] = magnitudes[i]; |
|
} |
|
|
|
return features; |
|
} |