webminidisc/src/utils.ts

551 lines
24 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Disc, formatTimeFromFrames, Encoding, getTracks, Group } from 'netmd-js';
import { useSelector, shallowEqual } from 'react-redux';
import { RootState } from './redux/store';
import { Mutex } from 'async-mutex';
import { Theme } from '@material-ui/core';
import jconv from 'jconv';
import { halfWidthToFullWidthRange } from 'netmd-js/dist/utils';
export function sleep(ms: number) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
export function debounce<T extends Function>(func: T, timeout = 300): T {
let timer: ReturnType<typeof setTimeout>;
const debouncedFn = (...args: any) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, timeout);
};
return (debouncedFn as any) as T;
}
export async function sleepWithProgressCallback(ms: number, cb: (perc: number) => void) {
let elapsedSecs = 1;
let interval = setInterval(() => {
elapsedSecs++;
cb(Math.min(100, ((elapsedSecs * 1000) / ms) * 100));
}, 1000);
await sleep(ms);
window.clearInterval(interval);
}
export function useShallowEqualSelector<TState = RootState, TSelected = unknown>(selector: (state: TState) => TSelected): TSelected {
return useSelector(selector, shallowEqual);
}
export function debugEnabled() {
return process.env.NODE_ENV === 'development';
}
export function getPublicPathFor(script: string) {
return `${process.env.PUBLIC_URL}/${script}`;
}
export function savePreference(key: string, value: unknown) {
localStorage.setItem(key, JSON.stringify(value));
}
export function loadPreference<T>(key: string, defaultValue: T): T {
let res = localStorage.getItem(key);
if (res === null) {
return defaultValue;
} else {
try {
return JSON.parse(res) as T;
} catch (e) {
return defaultValue;
}
}
}
export function getAvailableCharsForTitle(disc: Disc, includeGroups?: boolean) {
const cellLimit = 255;
// see https://www.minidisc.org/md_toc.html
const fixLength = (len: number) => Math.ceil(len / 7);
let groups = disc.groups.filter(n => n.title !== null);
// Assume worst-case scenario
let fwTitle = disc.fullWidthTitle + `0;//`;
let hwTitle = disc.title + `0;//`;
if (includeGroups || includeGroups === undefined)
for (let group of groups) {
let range = `${group.tracks[0].index + 1}${group.tracks.length - 1 !== 0 &&
`-${group.tracks[group.tracks.length - 1].index + 1}`}//`;
// The order of these characters doesn't matter. It's for length only
fwTitle += group.fullWidthTitle + range;
hwTitle += group.title + range;
}
let usedCells = 0;
usedCells += fixLength(fwTitle.length * 2);
usedCells += fixLength(getHalfWidthTitleLength(hwTitle));
for (let trk of getTracks(disc)) {
usedCells += fixLength((trk.fullWidthTitle?.length ?? 0) * 2);
usedCells += fixLength(getHalfWidthTitleLength(trk.title ?? ''));
}
return Math.max(cellLimit - usedCells, 0) * 7;
}
export function framesToSec(frames: number) {
return frames / 512;
}
export function timeToSeekArgs(timeInSecs: number): number[] {
let value = Math.round(timeInSecs); // ignore frames
let s = value % 60;
value = (value - s) / 60; // min
let m = value % 60;
value = (value - m) / 60; // hour
let h = value;
return [h, m, s, 0];
}
export function sanitizeTitle(title: string) {
return title.normalize('NFD').replace(/[^\x00-\x7F]/g, '');
}
export function getHalfWidthTitleLength(title: string) {
// Some characters are written as 2 bytes
// prettier-ignore
// '\u309C': -1, '\uFF9F': -1, '\u309B': -1, '\uFF9E': -1 but when they become part of a multi byte character, it will sum up to 0
const multiByteChars: { [key: string]: number } = { "ガ": 1, "ギ": 1, "グ": 1, "ゲ": 1, "ゴ": 1, "ザ": 1, "ジ": 1, "ズ": 1, "ゼ": 1, "ゾ": 1, "ダ": 1, "ヂ": 1, "ヅ": 1, "デ": 1, "ド": 1, "バ": 1, "パ": 1, "ビ": 1, "ピ": 1, "ブ": 1, "プ": 1, "ベ": 1, "ペ": 1, "ボ": 1, "ポ": 1, "ヮ": 1, "ヰ": 1, "ヱ": 1, "ヵ": 1, "ヶ": 1, "ヴ": 1, "ヽ": 1, "ヾ": 1, "が": 1, "ぎ": 1, "ぐ": 1, "げ": 1, "ご": 1, "ざ": 1, "じ": 1, "ず": 1, "ぜ": 1, "ぞ": 1, "だ": 1, "ぢ": 1, "づ": 1, "で": 1, "ど": 1, "ば": 1, "ぱ": 1, "び": 1, "ぴ": 1, "ぶ": 1, "ぷ": 1, "べ": 1, "ぺ": 1, "ぼ": 1, "ぽ": 1, "ゎ": 1, "ゐ": 1, "ゑ": 1, "ゕ": 1, "ゖ": 1, "ゔ": 1, "ゝ": 1, "ゞ": 1 };
return (
title.length +
title
.split('')
.map(n => multiByteChars[n] ?? 0)
.reduce((a, b) => a + b, 0)
);
}
export function sanitizeHalfWidthTitle(title: string) {
enum CharType {
normal,
dakuten,
handakuten,
}
const handakutenPossible = 'はひふへほハヒフヘホ'.split('');
const dakutenPossible = 'かきくけこさしすせそたちつてとカキクケコサシスセソタチツテト'.split('').concat(handakutenPossible);
//'Flatten' all the characters followed by the (han)dakuten character into one
let dakutenFix = [];
let type = CharType.normal;
for (const char of sanitizeFullWidthTitle(title, true)
.split('')
.reverse()) {
//This only works for full-width kana. It will get converted to half-width later anyway...
switch (type) {
case CharType.dakuten:
if (dakutenPossible.includes(char)) {
dakutenFix.push(String.fromCharCode(char.charCodeAt(0) + 1));
type = CharType.normal;
break;
} //Else fall through
case CharType.handakuten:
if (handakutenPossible.includes(char)) {
dakutenFix.push(String.fromCharCode(char.charCodeAt(0) + 2));
type = CharType.normal;
break;
} //Else fall through
case CharType.normal:
switch (char) {
case '\u309B':
case '\u3099':
case '\uFF9E':
type = CharType.dakuten;
break;
case '\u309C':
case '\u309A':
case '\uFF9F':
type = CharType.handakuten;
break;
default:
type = CharType.normal;
dakutenFix.push(char);
break;
}
break;
}
}
title = dakutenFix.reverse().join('');
// prettier-ignore
const mappings: { [key: string]: string } = { '': '-', 'ー': '-', 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', '': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': '-', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、', '': '!', '': '"', '': '#', '': '$', '': '%', '': '&', '': "'", '': '(', '': ')', '': '*', '': '+', '': ',', '': '.', '': '/', '': ':', '': ';', '': '<', '': '=', '': '>', '': '?', '': '@', '': 'A', '': 'B', '': 'C', '': 'D', '': 'E', '': 'F', '': 'G', '': 'H', '': 'I', '': 'J', '': 'K', '': 'L', '': 'M', '': 'N', '': 'O', '': 'P', '': 'Q', '': 'R', '': 'S', '': 'T', '': 'U', '': 'V', '': 'W', '': 'X', '': 'Y', '': 'Z', '': '[', '': '\\', '': ']', '': '^', '_': '_', '': '`', '': 'a', '': 'b', '': 'c', '': 'd', '': 'e', '': 'f', '': 'g', '': 'h', '': 'i', '': 'j', '': 'k', '': 'l', '': 'm', '': 'n', '': 'o', '': 'p', '': 'q', '': 'r', '': 's', '': 't', '': 'u', '': 'v', '': 'w', '': 'x', '': 'y', '': 'z', '': '{', '': '|', '': '}', '': '~', '\u3000': ' ', '': '0', '': '1', '': '2', '': '3', '': '4', '': '5', '': '6', '': '7', '': '8', '': '9', 'ぁ': 'ァ', 'あ': 'ア', 'ぃ': 'ィ', 'い': 'イ', 'ぅ': 'ゥ', 'う': 'ウ', 'ぇ': 'ェ', 'え': 'エ', 'ぉ': 'ォ', 'お': 'オ', 'か': 'カ', 'が': 'ガ', 'き': 'キ', 'ぎ': 'ギ', 'く': 'ク', 'ぐ': 'グ', 'け': 'ケ', 'げ': 'ゲ', 'こ': 'コ', 'ご': 'ゴ', 'さ': 'サ', 'ざ': 'ザ', 'し': 'シ', 'じ': 'ジ', 'す': 'ス', 'ず': 'ズ', 'せ': 'セ', 'ぜ': 'ゼ', 'そ': 'ソ', 'ぞ': 'ゾ', 'た': 'タ', 'だ': 'ダ', 'ち': 'チ', 'ぢ': 'ヂ', 'っ': 'ッ', 'つ': 'ツ', 'づ': 'ヅ', 'て': 'テ', 'で': 'デ', 'と': 'ト', 'ど': 'ド', 'な': 'ナ', 'に': 'ニ', 'ぬ': 'ヌ', 'ね': 'ネ', 'の': 'ノ', 'は': 'ハ', 'ば': 'バ', 'ぱ': 'パ', 'ひ': 'ヒ', 'び': 'ビ', 'ぴ': 'ピ', 'ふ': 'フ', 'ぶ': 'ブ', 'ぷ': 'プ', 'へ': 'ヘ', 'べ': 'ベ', 'ぺ': 'ペ', 'ほ': 'ホ', 'ぼ': 'ボ', 'ぽ': 'ポ', 'ま': 'マ', 'み': 'ミ', 'む': 'ム', 'め': 'メ', 'も': 'モ', 'ゃ': 'ャ', 'や': 'ヤ', 'ゅ': 'ュ', 'ゆ': 'ユ', 'ょ': 'ョ', 'よ': 'ヨ', 'ら': 'ラ', 'り': 'リ', 'る': 'ル', 'れ': 'レ', 'ろ': 'ロ', 'わ': 'ワ', 'を': 'ヲ', 'ん': 'ン', 'ゎ': 'ヮ', 'ゐ': 'ヰ', 'ゑ': 'ヱ', 'ゕ': 'ヵ', 'ゖ': 'ヶ', 'ゔ': 'ヴ', 'ゝ': 'ヽ', 'ゞ': 'ヾ' };
const allowedHalfWidthKana: string[] = Object.values(mappings);
const newTitle = title
.split('')
.map(n => {
if (mappings[n]) return mappings[n];
if (n.charCodeAt(0) < 0x7f || allowedHalfWidthKana.includes(n)) return n;
return ' ';
})
.join('');
// Check if the amount of characters is the same as the amount of encoded bytes (when accounting for dakuten). Otherwise the disc might end up corrupted
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
if (sjisEncoded.length !== getHalfWidthTitleLength(title)) return sanitizeTitle(title); //Fallback
return newTitle;
}
export function sanitizeFullWidthTitle(title: string, justRemap: boolean = false) {
// prettier-ignore
const mappings: { [key: string]: string } = { '!': '', '"': '', '#': '', '$': '', '%': '', '&': '', "'": '', '(': '', ')': '', '*': '', '+': '', ',': '', '-': '', '.': '', '/': '', ':': '', ';': '', '<': '', '=': '', '>': '', '?': '', '@': '', 'A': '', 'B': '', 'C': '', 'D': '', 'E': '', 'F': '', 'G': '', 'H': '', 'I': '', 'J': '', 'K': '', 'L': '', 'M': '', 'N': '', 'O': '', 'P': '', 'Q': '', 'R': '', 'S': '', 'T': '', 'U': '', 'V': '', 'W': '', 'X': '', 'Y': '', 'Z': '', '[': '', '\\': '', ']': '', '^': '', '_': '_', '`': '', 'a': '', 'b': '', 'c': '', 'd': '', 'e': '', 'f': '', 'g': '', 'h': '', 'i': '', 'j': '', 'k': '', 'l': '', 'm': '', 'n': '', 'o': '', 'p': '', 'q': '', 'r': '', 's': '', 't': '', 'u': '', 'v': '', 'w': '', 'x': '', 'y': '', 'z': '', '{': '', '|': '', '}': '', '~': '', ' ': '\u3000', '0': '', '1': '', '2': '', '3': '', '4': '', '5': '', '6': '', '7': '', '8': '', '9': '', 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': '', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、' };
const newTitle = title
.split('')
.map(n => mappings[n] ?? n)
.join('');
if (justRemap) return newTitle;
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
if (jconv.decode(sjisEncoded, 'SJIS') !== newTitle) return sanitizeTitle(title); // Fallback
if (sjisEncoded.length !== title.length * 2) return sanitizeTitle(title); // Fallback (every character in the full-width title is 2 bytes)
return newTitle;
}
export const EncodingName: { [k: number]: string } = {
[Encoding.sp]: 'SP',
[Encoding.lp2]: 'LP2',
[Encoding.lp4]: 'LP4',
};
export type DisplayTrack = {
index: number;
title: string;
fullWidthTitle: string;
group: string | null;
duration: string;
encoding: string;
durationInSecs: number;
};
export function getSortedTracks(disc: Disc | null) {
let tracks: DisplayTrack[] = [];
if (disc !== null) {
for (let group of disc.groups) {
for (let track of group.tracks) {
tracks.push({
index: track.index,
title: track.title ?? `Unknown Title`,
fullWidthTitle: track.fullWidthTitle ?? ``,
group: group.title ?? null,
encoding: EncodingName[track.encoding],
duration: formatTimeFromFrames(track.duration, false),
durationInSecs: track.duration / 512, // CAVEAT: 1s = 512 frames
});
}
}
}
tracks.sort((l, r) => l.index - r.index);
return tracks;
}
export function getGroupedTracks(disc: Disc | null) {
if (!disc) {
return [];
}
let groupedList: Group[] = [];
let ungroupedTracks = [...(disc.groups.find(n => n.title === null)?.tracks ?? [])];
let lastIndex = 0;
for (let group of disc.groups) {
if (group.title === null) {
continue; // Ungrouped tracks
}
let toCopy = group.tracks[0].index - lastIndex;
groupedList.push({
index: -1,
title: null,
fullWidthTitle: null,
tracks: toCopy === 0 ? [] : ungroupedTracks.splice(0, toCopy),
});
lastIndex = group.tracks[group.tracks.length - 1].index + 1;
groupedList.push(group);
}
groupedList.push({
index: -1,
title: null,
fullWidthTitle: null,
tracks: ungroupedTracks,
});
return groupedList;
}
export function recomputeGroupsAfterTrackMove(disc: Disc, trackIndex: number, targetIndex: number) {
// Used for moving tracks in netmd-mock and deleting
let offset = trackIndex > targetIndex ? 1 : -1;
let deleteMode = targetIndex === -1;
if (deleteMode) {
offset = -1;
targetIndex = disc.trackCount;
}
let boundsStart = Math.min(trackIndex, targetIndex);
let boundsEnd = Math.max(trackIndex, targetIndex);
let allTracks = disc.groups
.map(n => n.tracks)
.reduce((a, b) => a.concat(b), [])
.sort((a, b) => a.index - b.index)
.filter(n => !deleteMode || n.index !== trackIndex);
let groupBoundaries: {
name: string | null;
fullWidthName: string | null;
start: number;
end: number;
}[] = disc.groups
.filter(n => n.title !== null)
.map(group => ({
name: group.title,
fullWidthName: group.fullWidthTitle,
start: group.tracks[0].index,
end: group.tracks[0].index + group.tracks.length - 1,
})); // Convert to a format better for shifting
let anyChanges = false;
for (let group of groupBoundaries) {
if (group.start > boundsStart && group.start <= boundsEnd) {
group.start += offset;
anyChanges = true;
}
if (group.end >= boundsStart && group.end < boundsEnd) {
group.end += offset;
anyChanges = true;
}
}
if (!anyChanges) return disc;
let newDisc: Disc = { ...disc };
// Convert back
newDisc.groups = groupBoundaries
.map(n => ({
title: n.name,
fullWidthTitle: n.fullWidthName,
index: n.start,
tracks: allTracks.slice(n.start, n.end + 1),
}))
.filter(n => n.tracks.length > 0);
// Convert ungrouped tracks
let allGrouped = newDisc.groups.map(n => n.tracks).reduce((a, b) => a.concat(b), []);
let ungrouped = allTracks.filter(n => !allGrouped.includes(n));
// Fix all the track indexes
if (deleteMode) {
for (let i = 0; i < allTracks.length; i++) {
allTracks[i].index = i;
}
}
if (ungrouped.length) newDisc.groups.unshift({ title: null, fullWidthTitle: null, index: 0, tracks: ungrouped });
return newDisc;
}
export function compileDiscTitles(disc: Disc) {
let availableCharactersForTitle = getAvailableCharsForTitle(
{
...disc,
title: '',
fullWidthTitle: '',
},
false
);
// If the disc or any of the groups, or any track has a full-width title, provide support for them
const useFullWidth =
disc.fullWidthTitle ||
disc.groups.filter(n => !!n.fullWidthTitle).length > 0 ||
disc.groups
.map(n => n.tracks)
.reduce((a, b) => a.concat(b), [])
.filter(n => !!n.fullWidthTitle).length > 0;
const fixLength = (l: number) => Math.ceil(l / 7) * 7;
let newRawTitle = '',
newRawFullWidthTitle = '';
if (disc.title) newRawTitle = `0;${disc.title}//`;
if (useFullWidth) newRawFullWidthTitle = `${disc.fullWidthTitle}`;
for (let n of disc.groups) {
if (n.title === null || n.tracks.length === 0) continue;
let range = `${n.tracks[0].index + 1}`;
if (n.tracks.length !== 1) {
// Special case
range += `-${n.tracks[0].index + n.tracks.length}`;
}
let newRawTitleAfterGroup = newRawTitle + `${range};${n.title}//`,
newRawFullWidthTitleAfterGroup = newRawFullWidthTitle + halfWidthToFullWidthRange(range) + `${n.fullWidthTitle ?? ''}`;
let titlesLengthInTOC = fixLength(getHalfWidthTitleLength(newRawTitleAfterGroup));
if (useFullWidth) titlesLengthInTOC += fixLength(newRawFullWidthTitleAfterGroup.length * 2);
if (availableCharactersForTitle - titlesLengthInTOC < 0) break;
newRawTitle = newRawTitleAfterGroup;
newRawFullWidthTitle = newRawFullWidthTitleAfterGroup;
}
let titlesLengthInTOC = fixLength(getHalfWidthTitleLength(newRawTitle));
if (useFullWidth) titlesLengthInTOC += fixLength(newRawFullWidthTitle.length * 2); // If this check fails the titles without the groups already take too much space, don't change anything
if (availableCharactersForTitle - titlesLengthInTOC < 0) {
return null;
}
return {
newRawTitle,
newRawFullWidthTitle: useFullWidth ? newRawFullWidthTitle : '',
};
}
export function isSequential(numbers: number[]) {
if (numbers.length === 0) return true;
let last = numbers[0];
for (let num of numbers) {
if (num === last) {
++last;
} else return false;
}
return true;
}
export function asyncMutex(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// This is meant to be used only with classes having a "mutex" instance property
const oldValue = descriptor.value;
descriptor.value = async function(...args: any) {
const mutex = (this as any).mutex as Mutex;
const release = await mutex.acquire();
try {
return await oldValue.apply(this, args);
} finally {
release();
}
};
return descriptor;
}
export function forAnyDesktop(theme: Theme) {
return theme.breakpoints.up(600 + theme.spacing(2) * 2);
}
export function belowDesktop(theme: Theme) {
return theme.breakpoints.down(600 + theme.spacing(2) * 2);
}
export function forWideDesktop(theme: Theme) {
return theme.breakpoints.up(700 + theme.spacing(2) * 2) + ` and (min-height: 750px)`;
}
export function askNotificationPermission(): Promise<NotificationPermission> {
// Adapted from: https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
function checkNotificationPromise() {
try {
Notification.requestPermission().then();
} catch (e) {
return false;
}
return true;
}
if (checkNotificationPromise()) {
return Notification.requestPermission();
} else {
return new Promise(resolve => Notification.requestPermission(resolve));
}
}
export async function getAtrac3Info(file: File) {
// see: http://soundfile.sapp.org/doc/WaveFormat/
// and: https://www.fatalerrors.org/a/detailed-explanation-of-wav-file-format.html
const fileData = await file.arrayBuffer();
if (fileData.byteLength < 44) {
return null;
}
const riffDescriptor = new Uint32Array(fileData.slice(0, 12));
if (riffDescriptor[0] !== 0x46464952 || riffDescriptor[2] !== 0x45564157) {
// 'RIFF' && 'WAVE'
return null;
}
// WAVE format
const waveDescriptor = new Uint32Array(fileData.slice(12, 20));
if (waveDescriptor[0] !== 0x20746d66) {
return false;
}
const audioFormatAndChanneld = new Uint16Array(fileData.slice(20, 24));
if (audioFormatAndChanneld[0] !== 0x270 || audioFormatAndChanneld[1] !== 2) {
// 'atrac3' && 2 channels
return null;
}
const sampleRateAndByteRate = new Uint32Array(fileData.slice(24, 32));
if (sampleRateAndByteRate[0] !== 44100) {
// Sample rate
return null;
}
const byteRate = sampleRateAndByteRate[1];
let mode: 'LP2' | 'LP105' | 'LP4' | null = null;
if (byteRate > 16000) {
mode = 'LP2';
} else if (byteRate > 13000) {
mode = 'LP105';
} else if (byteRate > 8000) {
mode = 'LP4';
} else {
mode = null;
}
if (mode === null) {
return null;
}
const waveBlockEndOffset = new Uint16Array(fileData.slice(36, 38));
let dataOffset = -1;
const nextBlockStartOffset = waveBlockEndOffset[0] + 38;
const nextBlockEndOffset = nextBlockStartOffset + 8;
const nextBlock = new Uint32Array(fileData.slice(nextBlockStartOffset, nextBlockEndOffset));
if (nextBlock[0] === 0x61746164) {
// data
dataOffset = nextBlockEndOffset;
} else if (nextBlock[0] === 0x74636166) {
// fact
const dataBlockLength = 8;
dataOffset = nextBlockEndOffset + nextBlock[1] + dataBlockLength;
}
if (dataOffset === -1) {
return null;
}
return {
mode,
dataOffset,
};
}
declare let process: any;