155 lines
4.3 KiB
TypeScript
155 lines
4.3 KiB
TypeScript
import Pitchfinder from "pitchfinder";
|
||
import { IPA_CONSONANTS, IPA_MAPPINGS, IPA_VOWELS } from "./constants";
|
||
|
||
export const extractFrequencies = (props: {
|
||
peaks: Float32Array;
|
||
sampleRate: number;
|
||
}): number[] => {
|
||
const { peaks, sampleRate } = props;
|
||
|
||
const detectPitch = Pitchfinder.AMDF({
|
||
sampleRate,
|
||
sensitivity: 0.05,
|
||
});
|
||
const duration = peaks.length / sampleRate;
|
||
const bpm = peaks.length / duration / 60;
|
||
|
||
const frequencies = Pitchfinder.frequencies(detectPitch, peaks, {
|
||
tempo: bpm,
|
||
quantization: bpm,
|
||
});
|
||
|
||
const cleanedFrequencies = removeNoise(frequencies);
|
||
|
||
return cleanedFrequencies;
|
||
};
|
||
|
||
export const removeNoise = (
|
||
numbers: number[],
|
||
threshold: number = 0.2
|
||
): number[] => {
|
||
numbers.forEach((num, i) => {
|
||
if (i === 0) return;
|
||
if (typeof num !== "number") return;
|
||
|
||
const prevNum = numbers[i - 1] || num;
|
||
const nextNum = numbers[i + 1] || num;
|
||
const avgNeighbor = (prevNum + nextNum) / 2.0;
|
||
const deviation = Math.abs(num - avgNeighbor);
|
||
|
||
if (deviation > threshold * avgNeighbor) {
|
||
numbers[i] = null;
|
||
}
|
||
});
|
||
|
||
return numbers;
|
||
};
|
||
|
||
export function milisecondsToTimestamp(ms: number) {
|
||
const hours = Math.floor(ms / 3600000).toString();
|
||
const minutes = Math.floor((ms % 3600000) / 60000).toString();
|
||
const seconds = Math.floor(((ms % 360000) % 60000) / 1000).toString();
|
||
const milliseconds = Math.floor(ms % 1000).toString();
|
||
return `${hours.padStart(2, "0")}:${minutes.padStart(
|
||
2,
|
||
"0"
|
||
)}:${seconds.padStart(2, "0")},${milliseconds}`;
|
||
}
|
||
|
||
export const convertWordIpaToNormal = (
|
||
ipas: string[],
|
||
options?: { mappings?: any }
|
||
): string[] => {
|
||
const { mappings = IPA_MAPPINGS } = options || {};
|
||
const consonants = Object.keys(IPA_CONSONANTS)
|
||
.map((key) => IPA_CONSONANTS[key])
|
||
.reduce((acc, val) => acc.concat(val), []);
|
||
const consonantsRegex = new RegExp(`^(\ˈ|ˌ)?` + consonants.join("|"));
|
||
const vowels = Object.keys(IPA_VOWELS)
|
||
.map((key) => IPA_VOWELS[key])
|
||
.reduce((acc, val) => acc.concat(val), []);
|
||
const vowelsRegex = new RegExp(`^(\ˈ|ˌ)?` + vowels.join("|"));
|
||
|
||
const converted: string[] = [];
|
||
|
||
// convert each ipa to normal
|
||
// if ipa is a vowel and marked, check if the previous ipa is a consonant,
|
||
// if so, mark the consonant instead
|
||
for (let i = 0; i < ipas.length; i++) {
|
||
const ipa = ipas[i];
|
||
converted.push(convertIpaToNormal(ipa, { mappings, marked: false }));
|
||
|
||
const isVowel = vowelsRegex.test(ipa);
|
||
const mark = ipa.match(/(\ˈ|ˌ)/);
|
||
|
||
let j = i - 1;
|
||
for (; j > 0 && j > i - 2; j--) {
|
||
if (
|
||
consonantsRegex.test(converted[j]) &&
|
||
!IPA_CONSONANTS.trill.includes(converted[j]) &&
|
||
!IPA_CONSONANTS.approximant.includes(converted[j]) &&
|
||
!IPA_CONSONANTS.lateralApproximant.includes(converted[j])
|
||
)
|
||
break;
|
||
if (
|
||
consonantsRegex.test(converted[j]) &&
|
||
!consonantsRegex.test(converted[j - 1])
|
||
) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (isVowel && mark) {
|
||
if (converted[j] && consonantsRegex.test(converted[j])) {
|
||
converted[j] = mark[0] + converted[j];
|
||
} else {
|
||
converted[i] = mark[0] + converted[i];
|
||
}
|
||
}
|
||
}
|
||
|
||
return converted;
|
||
};
|
||
|
||
export const convertIpaToNormal = (
|
||
ipa: string,
|
||
options?: { mappings?: any; marked?: boolean }
|
||
): string => {
|
||
const { mappings = IPA_MAPPINGS, marked = false } = options || {};
|
||
|
||
const mark = ipa.match(/(\ˈ|ˌ)/);
|
||
const cleanIpa = ipa.replace(mark ? mark[0] : "", "");
|
||
|
||
const converted = mappings[cleanIpa] || cleanIpa;
|
||
if (mark && marked) {
|
||
return `${mark[0]}${converted}`;
|
||
} else {
|
||
return converted;
|
||
}
|
||
};
|
||
|
||
// make size of bytes human readable
|
||
export const humanFileSize = (bytes: number, si: boolean = false) => {
|
||
const thresh = si ? 1000 : 1024;
|
||
if (Math.abs(bytes) < thresh) {
|
||
return bytes + " B";
|
||
}
|
||
const units = si
|
||
? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
||
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
|
||
let u = -1;
|
||
const r = 10;
|
||
do {
|
||
bytes /= thresh;
|
||
++u;
|
||
} while (
|
||
Math.round(Math.abs(bytes) * r) / r >= thresh &&
|
||
u < units.length - 1
|
||
);
|
||
return bytes.toFixed(1) + " " + units[u];
|
||
};
|
||
|
||
export function getExtension(filename: string, defaultExt: string) {
|
||
return /(?:\.([^.]+))?$/.exec(filename)[1] || defaultExt;
|
||
}
|