commit
c41d69c6c5
|
@ -45,6 +45,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dom-mediacapture-record": "^1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
"@types/react-beautiful-dnd": "^13.1.1",
|
"@types/react-beautiful-dnd": "^13.1.1",
|
||||||
|
"@types/wicg-mediasession": "^1.1.3",
|
||||||
"async-mutex": "^0.2.6",
|
"async-mutex": "^0.2.6",
|
||||||
"gh-pages": "^2.2.0"
|
"gh-pages": "^2.2.0"
|
||||||
}
|
}
|
||||||
|
@ -2308,6 +2309,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.5.tgz",
|
||||||
"integrity": "sha512-dYolx2XWesl1TMu+1BjtjU6eC6c2zZ2VDKhjU4f/mtR3+UBfMW6h1tPCQt7leY5Y8JBg0Fe/mMnoDMkPPNX9sw=="
|
"integrity": "sha512-dYolx2XWesl1TMu+1BjtjU6eC6c2zZ2VDKhjU4f/mtR3+UBfMW6h1tPCQt7leY5Y8JBg0Fe/mMnoDMkPPNX9sw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/wicg-mediasession": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/wicg-mediasession/-/wicg-mediasession-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-lzoszzJJfW9vcaIxf6tDx3lCJq/4oaD+mplA7sCV7W21PGdR6yUPwErN047ziIcwFx61w8WMURIwUyj1V7KJIQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "13.0.8",
|
"version": "13.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",
|
||||||
|
@ -21217,6 +21224,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.5.tgz",
|
||||||
"integrity": "sha512-dYolx2XWesl1TMu+1BjtjU6eC6c2zZ2VDKhjU4f/mtR3+UBfMW6h1tPCQt7leY5Y8JBg0Fe/mMnoDMkPPNX9sw=="
|
"integrity": "sha512-dYolx2XWesl1TMu+1BjtjU6eC6c2zZ2VDKhjU4f/mtR3+UBfMW6h1tPCQt7leY5Y8JBg0Fe/mMnoDMkPPNX9sw=="
|
||||||
},
|
},
|
||||||
|
"@types/wicg-mediasession": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/wicg-mediasession/-/wicg-mediasession-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-lzoszzJJfW9vcaIxf6tDx3lCJq/4oaD+mplA7sCV7W21PGdR6yUPwErN047ziIcwFx61w8WMURIwUyj1V7KJIQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/yargs": {
|
"@types/yargs": {
|
||||||
"version": "13.0.8",
|
"version": "13.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.8.tgz",
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dom-mediacapture-record": "^1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
"@types/react-beautiful-dnd": "^13.1.1",
|
"@types/react-beautiful-dnd": "^13.1.1",
|
||||||
|
"@types/wicg-mediasession": "^1.1.3",
|
||||||
"async-mutex": "^0.2.6",
|
"async-mutex": "^0.2.6",
|
||||||
"gh-pages": "^2.2.0"
|
"gh-pages": "^2.2.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,8 @@
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script>
|
||||||
try {
|
try {
|
||||||
if (location.protocol !== 'https:' && !location.host.match(/^localhost/gm)) {
|
if (location.protocol !== 'file:' && location.protocol !== 'https:' && !location.host.match(/^localhost/gm)) {
|
||||||
// Make sure we're on https, or WebUSB won't work.
|
// Make sure we're on https/file, or WebUSB won't work.
|
||||||
location.replace(`https:${location.href.substring(location.protocol.length)}`);
|
location.replace(`https:${location.href.substring(location.protocol.length)}`);
|
||||||
console.log('Redirecting to https....');
|
console.log('Redirecting to https....');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback, useRef, useEffect, useState } from 'react';
|
import React, { useCallback, useRef, useEffect, useState, useMemo } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
|
||||||
|
@ -134,7 +134,7 @@ export const Controls = () => {
|
||||||
let deviceState = deviceStatus?.state ?? null;
|
let deviceState = deviceStatus?.state ?? null;
|
||||||
let discPresent = deviceStatus?.discPresent ?? false;
|
let discPresent = deviceStatus?.discPresent ?? false;
|
||||||
let paused = deviceStatus?.state === 'paused';
|
let paused = deviceStatus?.state === 'paused';
|
||||||
const tracks = getSortedTracks(disc);
|
const tracks = useMemo(() => getSortedTracks(disc), [disc]);
|
||||||
if (!discPresent) {
|
if (!discPresent) {
|
||||||
message = ``;
|
message = ``;
|
||||||
} else if (deviceState === 'readingTOC') {
|
} else if (deviceState === 'readingTOC') {
|
||||||
|
@ -143,9 +143,7 @@ export const Controls = () => {
|
||||||
message = `BLANKDISC`;
|
message = `BLANKDISC`;
|
||||||
} else if (deviceStatus && deviceStatus.track !== null && tracks[deviceStatus.track]) {
|
} else if (deviceStatus && deviceStatus.track !== null && tracks[deviceStatus.track]) {
|
||||||
let title = tracks[deviceStatus.track].fullWidthTitle || tracks[deviceStatus.track].title;
|
let title = tracks[deviceStatus.track].fullWidthTitle || tracks[deviceStatus.track].title;
|
||||||
message =
|
message = (deviceStatus.track + 1).toString().padStart(3, '0') + (title ? ' - ' + title : '');
|
||||||
(deviceStatus.track + 1).toString().padStart(3, '0') +
|
|
||||||
(title ? ' - ' + title : '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lcdScroll, setLcdScroll] = useState(0);
|
const [lcdScroll, setLcdScroll] = useState(0);
|
||||||
|
|
|
@ -328,11 +328,11 @@ export const Main = (props: {}) => {
|
||||||
}
|
}
|
||||||
if (deviceStatus.track !== track) {
|
if (deviceStatus.track !== track) {
|
||||||
dispatch(control('goto', track));
|
dispatch(control('goto', track));
|
||||||
if (deviceStatus.state !== 'playing') {
|
|
||||||
dispatch(control('play'));
|
dispatch(control('play'));
|
||||||
}
|
|
||||||
} else if (deviceStatus.state === 'playing') {
|
} else if (deviceStatus.state === 'playing') {
|
||||||
dispatch(control('pause'));
|
dispatch(control('pause'));
|
||||||
|
} else {
|
||||||
|
dispatch(control('play'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, deviceStatus]
|
[dispatch, deviceStatus]
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
export function createEmptyWave(time: number, sampleRate = 22050) {
|
||||||
|
const numOfChan = 1;
|
||||||
|
const depthInBytes = 2;
|
||||||
|
const length = time * sampleRate * numOfChan * depthInBytes + 44;
|
||||||
|
const buffer = new ArrayBuffer(length);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
|
||||||
|
let pos = 0;
|
||||||
|
function setUint16(data: number) {
|
||||||
|
view.setUint16(pos, data, true);
|
||||||
|
pos += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setUint32(data: number) {
|
||||||
|
view.setUint32(pos, data, true);
|
||||||
|
pos += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write WAVE header
|
||||||
|
setUint32(0x46464952);
|
||||||
|
setUint32(length - 8);
|
||||||
|
setUint32(0x45564157);
|
||||||
|
|
||||||
|
setUint32(0x20746d66);
|
||||||
|
setUint32(16);
|
||||||
|
setUint16(1);
|
||||||
|
setUint16(numOfChan);
|
||||||
|
setUint32(sampleRate);
|
||||||
|
setUint32(sampleRate * numOfChan * depthInBytes);
|
||||||
|
setUint16(numOfChan * depthInBytes);
|
||||||
|
setUint16(depthInBytes * 8);
|
||||||
|
|
||||||
|
setUint32(0x61746164);
|
||||||
|
setUint32(length - pos - 4);
|
||||||
|
|
||||||
|
return new Blob([buffer], { type: 'audio/wav' });
|
||||||
|
}
|
|
@ -18,11 +18,13 @@ import './fonts/fonts.css';
|
||||||
|
|
||||||
import { FFMpegAudioExportService } from './services/audio-export';
|
import { FFMpegAudioExportService } from './services/audio-export';
|
||||||
import { MediaRecorderService } from './services/mediarecorder';
|
import { MediaRecorderService } from './services/mediarecorder';
|
||||||
|
import { BrowserMediaSessionService } from './services/media-session';
|
||||||
|
|
||||||
serviceRegistry.netmdService = new NetMDUSBService({ debug: true });
|
serviceRegistry.netmdService = (window as any).native?.interface || new NetMDUSBService({ debug: true });
|
||||||
// serviceRegistry.netmdService = new NetMDMockService(); // Uncomment to work without a device attached
|
// serviceRegistry.netmdService = new NetMDMockService(); // Uncomment to work without a device attached
|
||||||
serviceRegistry.audioExportService = new FFMpegAudioExportService();
|
serviceRegistry.audioExportService = new FFMpegAudioExportService();
|
||||||
serviceRegistry.mediaRecorderService = new MediaRecorderService();
|
serviceRegistry.mediaRecorderService = new MediaRecorderService();
|
||||||
|
serviceRegistry.mediaSessionService = new BrowserMediaSessionService(store);
|
||||||
|
|
||||||
(function setupEventHandlers() {
|
(function setupEventHandlers() {
|
||||||
window.addEventListener('beforeunload', ev => {
|
window.addEventListener('beforeunload', ev => {
|
||||||
|
|
|
@ -17,13 +17,14 @@ import {
|
||||||
askNotificationPermission,
|
askNotificationPermission,
|
||||||
getGroupedTracks,
|
getGroupedTracks,
|
||||||
getHalfWidthTitleLength,
|
getHalfWidthTitleLength,
|
||||||
|
timeToSeekArgs,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import * as mm from 'music-metadata-browser';
|
import * as mm from 'music-metadata-browser';
|
||||||
import { TitleFormatType, UploadFormat } from './convert-dialog-feature';
|
import { TitleFormatType, UploadFormat } from './convert-dialog-feature';
|
||||||
import NotificationCompleteIconUrl from '../images/record-complete-notification-icon.png';
|
import NotificationCompleteIconUrl from '../images/record-complete-notification-icon.png';
|
||||||
import { assertNumber } from 'netmd-js/dist/utils';
|
import { assertNumber } from 'netmd-js/dist/utils';
|
||||||
|
|
||||||
export function control(action: 'play' | 'stop' | 'next' | 'prev' | 'goto' | 'pause', params?: unknown) {
|
export function control(action: 'play' | 'stop' | 'next' | 'prev' | 'goto' | 'pause' | 'seek', params?: unknown) {
|
||||||
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'play':
|
case 'play':
|
||||||
|
@ -41,11 +42,23 @@ export function control(action: 'play' | 'stop' | 'next' | 'prev' | 'goto' | 'pa
|
||||||
case 'pause':
|
case 'pause':
|
||||||
await serviceRegistry.netmdService!.pause();
|
await serviceRegistry.netmdService!.pause();
|
||||||
break;
|
break;
|
||||||
case 'goto':
|
case 'goto': {
|
||||||
const trackNumber = assertNumber(params, 'Invalid track number for "goto" command');
|
const trackNumber = assertNumber(params, 'Invalid track number for "goto" command');
|
||||||
await serviceRegistry.netmdService!.gotoTrack(trackNumber);
|
await serviceRegistry.netmdService!.gotoTrack(trackNumber);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'seek': {
|
||||||
|
if (!(params instanceof Object)) {
|
||||||
|
throw new Error('"seek" command has wrong params');
|
||||||
|
}
|
||||||
|
const typedParams: { trackNumber: number; time: number } = params as any;
|
||||||
|
const trackNumber = assertNumber(typedParams.trackNumber, 'Invalid track number for "seek" command');
|
||||||
|
const time = assertNumber(typedParams.time, 'Invalid time for "seek" command');
|
||||||
|
const timeArgs = timeToSeekArgs(time);
|
||||||
|
await serviceRegistry.netmdService!.gotoTime(trackNumber, timeArgs[0], timeArgs[1], timeArgs[2], timeArgs[3]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
// CAVEAT: change-track might take a up to a few seconds to complete.
|
// CAVEAT: change-track might take a up to a few seconds to complete.
|
||||||
// We wait 500ms and let the monitor do further updates
|
// We wait 500ms and let the monitor do further updates
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
@ -184,6 +197,7 @@ export function pair() {
|
||||||
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
return async function(dispatch: AppDispatch, getState: () => RootState) {
|
||||||
dispatch(appStateActions.setPairingFailed(false));
|
dispatch(appStateActions.setPairingFailed(false));
|
||||||
|
|
||||||
|
serviceRegistry.mediaSessionService?.init(); // no need to await
|
||||||
await serviceRegistry.audioExportService!.init();
|
await serviceRegistry.audioExportService!.init();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -50,5 +50,8 @@ export const store = configureStore({
|
||||||
|
|
||||||
const initialState = Object.freeze(store.getState());
|
const initialState = Object.freeze(store.getState());
|
||||||
|
|
||||||
|
export type AppStore = typeof store;
|
||||||
|
export type AppSubscribe = typeof store.subscribe;
|
||||||
|
export type AppGetState = typeof store.getState;
|
||||||
export type RootState = ReturnType<typeof store.getState>;
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
export type AppDispatch = typeof store.dispatch;
|
export type AppDispatch = typeof store.dispatch;
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
import { debounce, DisplayTrack, getSortedTracks, sleep } from '../utils';
|
||||||
|
import { createEmptyWave } from '../create-empty-wave';
|
||||||
|
import { control } from '../redux/actions';
|
||||||
|
import { AppStore, AppSubscribe, AppGetState } from '../redux/store';
|
||||||
|
import { Dispatch } from '@reduxjs/toolkit';
|
||||||
|
import { Disc } from 'netmd-js';
|
||||||
|
|
||||||
|
export interface MediaSessionService {
|
||||||
|
init(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BrowserMediaSessionService {
|
||||||
|
private initialized = false;
|
||||||
|
private audioEl?: HTMLAudioElement;
|
||||||
|
|
||||||
|
private dispatch: Dispatch<any>; // CAVEAT: AppDispatch type doesn't have an overload for redux thunk actions
|
||||||
|
private subscribe: AppSubscribe;
|
||||||
|
private getState: AppGetState;
|
||||||
|
|
||||||
|
constructor(appStore: AppStore) {
|
||||||
|
this.dispatch = appStore.dispatch.bind(appStore) as Dispatch<any>;
|
||||||
|
this.subscribe = appStore.subscribe.bind(appStore);
|
||||||
|
this.getState = appStore.getState.bind(appStore);
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (this.initialized || !navigator.mediaSession) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
// Audio el
|
||||||
|
const audioEl = document.createElement('audio');
|
||||||
|
audioEl.id = 'browser-media-session-helper';
|
||||||
|
document.body.appendChild(audioEl);
|
||||||
|
|
||||||
|
audioEl.setAttribute('loop', 'true');
|
||||||
|
audioEl.src = URL.createObjectURL(createEmptyWave(6));
|
||||||
|
audioEl.volume = 0;
|
||||||
|
|
||||||
|
this.audioEl = audioEl;
|
||||||
|
|
||||||
|
// Blocks media session events during initialization
|
||||||
|
navigator.mediaSession.setActionHandler('play', null);
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', null);
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', null);
|
||||||
|
navigator.mediaSession.setActionHandler('pause', null);
|
||||||
|
navigator.mediaSession.setActionHandler('seekto', null);
|
||||||
|
navigator.mediaSession.metadata = null;
|
||||||
|
|
||||||
|
audioEl.play();
|
||||||
|
await sleep(5000); // CAVEAT: 5secs is the minimum playing time for media info to show up
|
||||||
|
audioEl.pause();
|
||||||
|
|
||||||
|
if (this.getState().main.deviceStatus?.state === 'playing') {
|
||||||
|
// restore current state
|
||||||
|
audioEl.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('MediaSession ready');
|
||||||
|
|
||||||
|
// Set mediaSession event handlers
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||||
|
this.dispatch(control('prev'));
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||||
|
this.dispatch(control('next'));
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler('pause', () => {
|
||||||
|
audioEl.pause();
|
||||||
|
this.dispatch(control('pause'));
|
||||||
|
});
|
||||||
|
navigator.mediaSession.setActionHandler('play', () => {
|
||||||
|
audioEl.play();
|
||||||
|
this.dispatch(control('play'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const debouncedSeek = debounce((time: number, trackNumber: number) => {
|
||||||
|
this.dispatch(control('seek', { time, trackNumber }));
|
||||||
|
audioEl.currentTime = time;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('seekto', details => {
|
||||||
|
const trackNumber = this.getState().main.deviceStatus?.track ?? -1;
|
||||||
|
if (trackNumber === -1 || details.seekTime === null) {
|
||||||
|
return; // can't seek without knowing the track number or the seek time
|
||||||
|
}
|
||||||
|
debouncedSeek(details.seekTime, trackNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.subscribe(() => {
|
||||||
|
this.syncState();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will save cpu cycles when just playing music. Might replace in the future with some memoization library
|
||||||
|
private sortedTracks: DisplayTrack[] = [];
|
||||||
|
private sortedTracksDisc: Disc | null = null;
|
||||||
|
getSortedTracksWithCache(disc: Disc | null) {
|
||||||
|
if (disc !== this.sortedTracksDisc) {
|
||||||
|
this.sortedTracks = getSortedTracks(disc);
|
||||||
|
this.sortedTracksDisc = disc;
|
||||||
|
}
|
||||||
|
return this.sortedTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncState() {
|
||||||
|
if (!this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioEl = this.audioEl!;
|
||||||
|
const {
|
||||||
|
main: { deviceStatus, disc },
|
||||||
|
} = this.getState();
|
||||||
|
|
||||||
|
const isPlaying = deviceStatus?.state === 'playing';
|
||||||
|
const currentDiscTitle = disc?.title;
|
||||||
|
const currentTrackIndex = deviceStatus?.track ?? -1;
|
||||||
|
const allTracks = this.getSortedTracksWithCache(disc);
|
||||||
|
const currentTrack: DisplayTrack | undefined = allTracks[currentTrackIndex];
|
||||||
|
const currentTrackTitle = currentTrack ? currentTrack.fullWidthTitle || currentTrack?.title : '';
|
||||||
|
const currentTrackDurationInSecs = Math.round(currentTrack?.durationInSecs ?? -1);
|
||||||
|
|
||||||
|
const oldTrackTitle = navigator.mediaSession.metadata?.title;
|
||||||
|
const oldDiscTitle = navigator.mediaSession.metadata?.album;
|
||||||
|
|
||||||
|
// Sync MmediaSession
|
||||||
|
if (isPlaying && navigator.mediaSession.playbackState !== 'playing') {
|
||||||
|
navigator.mediaSession.playbackState = 'playing';
|
||||||
|
} else if (!isPlaying && navigator.mediaSession.playbackState !== 'paused') {
|
||||||
|
navigator.mediaSession.playbackState = 'paused';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync MediaMetadata
|
||||||
|
if (oldTrackTitle !== currentTrackTitle || oldDiscTitle !== currentDiscTitle) {
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
title: currentTrackTitle,
|
||||||
|
album: currentDiscTitle,
|
||||||
|
artwork: [
|
||||||
|
{ src: window.location.pathname + 'MiniDisc192.png', sizes: '192x192', type: 'image/png' },
|
||||||
|
{ src: window.location.pathname + 'MiniDisc512.png', sizes: '512x512', type: 'image/png' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync audio duration.
|
||||||
|
// CAVEAT: replacing the src may change the audioEl paused state.
|
||||||
|
if (audioEl && audioEl.duration !== currentTrackDurationInSecs && isPlaying) {
|
||||||
|
URL.revokeObjectURL(audioEl.src ?? '');
|
||||||
|
audioEl.src = URL.createObjectURL(createEmptyWave(Math.max(currentTrackDurationInSecs, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync <audio> state
|
||||||
|
if (isPlaying && audioEl.paused) {
|
||||||
|
audioEl.play();
|
||||||
|
} else if (!isPlaying && !audioEl.paused) {
|
||||||
|
audioEl.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ class NetMDMockService implements NetMDService {
|
||||||
public _discCapacity: number = 80 * 60 * 512;
|
public _discCapacity: number = 80 * 60 * 512;
|
||||||
public _tracks: Track[] = [
|
public _tracks: Track[] = [
|
||||||
{
|
{
|
||||||
duration: 5 * 60 * 512,
|
duration: 3 * 60 * 512,
|
||||||
encoding: Encoding.sp,
|
encoding: Encoding.sp,
|
||||||
index: 0,
|
index: 0,
|
||||||
channel: Channels.stereo,
|
channel: Channels.stereo,
|
||||||
|
@ -360,6 +360,11 @@ class NetMDMockService implements NetMDService {
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async gotoTime(index: number, hour = 0, minute = 0, second = 0, frame = 0) {
|
||||||
|
this._status.track = index;
|
||||||
|
await sleep(500);
|
||||||
|
}
|
||||||
|
|
||||||
async getPosition() {
|
async getPosition() {
|
||||||
if (this._status.track === null || this._status.time === null) {
|
if (this._status.track === null || this._status.time === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -58,6 +58,7 @@ export interface NetMDService {
|
||||||
next(): Promise<void>;
|
next(): Promise<void>;
|
||||||
prev(): Promise<void>;
|
prev(): Promise<void>;
|
||||||
gotoTrack(index: number): Promise<void>;
|
gotoTrack(index: number): Promise<void>;
|
||||||
|
gotoTime(index: number, hour: number, minute: number, second: number, frame: number): Promise<void>;
|
||||||
getPosition(): Promise<number[] | null>;
|
getPosition(): Promise<number[] | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -384,6 +385,11 @@ export class NetMDUSBService implements NetMDService {
|
||||||
await this.netmdInterface!.gotoTrack(index);
|
await this.netmdInterface!.gotoTrack(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@asyncMutex
|
||||||
|
async gotoTime(index: number, h: number, m: number, s: number, f: number) {
|
||||||
|
await this.netmdInterface!.gotoTime(index, h, m, s, f);
|
||||||
|
}
|
||||||
|
|
||||||
@asyncMutex
|
@asyncMutex
|
||||||
async getPosition() {
|
async getPosition() {
|
||||||
return await this.netmdInterface!.getPosition();
|
return await this.netmdInterface!.getPosition();
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { NetMDService } from './netmd';
|
import { NetMDService } from './netmd';
|
||||||
import { AudioExportService } from './audio-export';
|
import { AudioExportService } from './audio-export';
|
||||||
import { MediaRecorderService } from './mediarecorder';
|
import { MediaRecorderService } from './mediarecorder';
|
||||||
|
import { MediaSessionService } from './media-session';
|
||||||
|
|
||||||
interface ServiceRegistry {
|
interface ServiceRegistry {
|
||||||
netmdService?: NetMDService;
|
netmdService?: NetMDService;
|
||||||
audioExportService?: AudioExportService;
|
audioExportService?: AudioExportService;
|
||||||
mediaRecorderService?: MediaRecorderService;
|
mediaRecorderService?: MediaRecorderService;
|
||||||
|
mediaSessionService?: MediaSessionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServiceRegistry: ServiceRegistry = {};
|
const ServiceRegistry: ServiceRegistry = {};
|
||||||
|
|
85
src/utils.ts
85
src/utils.ts
|
@ -12,6 +12,17 @@ export function sleep(ms: number) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
export async function sleepWithProgressCallback(ms: number, cb: (perc: number) => void) {
|
||||||
let elapsedSecs = 1;
|
let elapsedSecs = 1;
|
||||||
let interval = setInterval(() => {
|
let interval = setInterval(() => {
|
||||||
|
@ -84,6 +95,20 @@ export function framesToSec(frames: number) {
|
||||||
return frames / 512;
|
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) {
|
export function sanitizeTitle(title: string) {
|
||||||
return title.normalize('NFD').replace(/[^\x00-\x7F]/g, '');
|
return title.normalize('NFD').replace(/[^\x00-\x7F]/g, '');
|
||||||
}
|
}
|
||||||
|
@ -91,6 +116,7 @@ export function sanitizeTitle(title: string) {
|
||||||
export function getHalfWidthTitleLength(title: string) {
|
export function getHalfWidthTitleLength(title: string) {
|
||||||
// Some characters are written as 2 bytes
|
// Some characters are written as 2 bytes
|
||||||
// prettier-ignore
|
// 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 };
|
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 (
|
return (
|
||||||
title.length +
|
title.length +
|
||||||
|
@ -102,8 +128,59 @@ export function getHalfWidthTitleLength(title: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeHalfWidthTitle(title: string) {
|
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
|
// prettier-ignore
|
||||||
const mappings: { [key: string]: string } = { 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、', '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', ''': "'", '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\': '\\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', '\u3000': ' ', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ぁ': 'ァ', 'あ': 'ア', 'ぃ': 'ィ', 'い': 'イ', 'ぅ': 'ゥ', 'う': 'ウ', 'ぇ': 'ェ', 'え': 'エ', 'ぉ': 'ォ', 'お': 'オ', 'か': 'カ', 'が': 'ガ', 'き': 'キ', 'ぎ': 'ギ', 'く': 'ク', 'ぐ': 'グ', 'け': 'ケ', 'げ': 'ゲ', 'こ': 'コ', 'ご': 'ゴ', 'さ': 'サ', 'ざ': 'ザ', 'し': 'シ', 'じ': 'ジ', 'す': 'ス', 'ず': 'ズ', 'せ': 'セ', 'ぜ': 'ゼ', 'そ': 'ソ', 'ぞ': 'ゾ', 'た': 'タ', 'だ': 'ダ', 'ち': 'チ', 'ぢ': 'ヂ', 'っ': 'ッ', 'つ': 'ツ', 'づ': 'ヅ', 'て': 'テ', 'で': 'デ', 'と': 'ト', 'ど': 'ド', 'な': 'ナ', 'に': 'ニ', 'ぬ': 'ヌ', 'ね': 'ネ', 'の': 'ノ', 'は': 'ハ', 'ば': 'バ', 'ぱ': 'パ', 'ひ': 'ヒ', 'び': 'ビ', 'ぴ': 'ピ', 'ふ': 'フ', 'ぶ': 'ブ', 'ぷ': 'プ', 'へ': 'ヘ', 'べ': 'ベ', 'ぺ': 'ペ', 'ほ': 'ホ', 'ぼ': 'ボ', 'ぽ': 'ポ', 'ま': 'マ', 'み': 'ミ', 'む': 'ム', 'め': 'メ', 'も': 'モ', 'ゃ': 'ャ', 'や': 'ヤ', 'ゅ': 'ュ', 'ゆ': 'ユ', 'ょ': 'ョ', 'よ': 'ヨ', 'ら': 'ラ', 'り': 'リ', 'る': 'ル', 'れ': 'レ', 'ろ': 'ロ', 'わ': 'ワ', 'を': 'ヲ', 'ん': 'ン', 'ゎ': 'ヮ', 'ゐ': 'ヰ', 'ゑ': 'ヱ', 'ゕ': 'ヵ', 'ゖ': 'ヶ', 'ゔ': 'ヴ', 'ゝ': 'ヽ', 'ゞ': 'ヾ' };
|
const mappings: { [key: string]: string } = { '-': '-', 'ー': '-', 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': '-', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、', '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', ''': "'", '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\': '\\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', '\u3000': ' ', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ぁ': 'ァ', 'あ': 'ア', 'ぃ': 'ィ', 'い': 'イ', 'ぅ': 'ゥ', 'う': 'ウ', 'ぇ': 'ェ', 'え': 'エ', 'ぉ': 'ォ', 'お': 'オ', 'か': 'カ', 'が': 'ガ', 'き': 'キ', 'ぎ': 'ギ', 'く': 'ク', 'ぐ': 'グ', 'け': 'ケ', 'げ': 'ゲ', 'こ': 'コ', 'ご': 'ゴ', 'さ': 'サ', 'ざ': 'ザ', 'し': 'シ', 'じ': 'ジ', 'す': 'ス', 'ず': 'ズ', 'せ': 'セ', 'ぜ': 'ゼ', 'そ': 'ソ', 'ぞ': 'ゾ', 'た': 'タ', 'だ': 'ダ', 'ち': 'チ', 'ぢ': 'ヂ', 'っ': 'ッ', 'つ': 'ツ', 'づ': 'ヅ', 'て': 'テ', 'で': 'デ', 'と': 'ト', 'ど': 'ド', 'な': 'ナ', 'に': 'ニ', 'ぬ': 'ヌ', 'ね': 'ネ', 'の': 'ノ', 'は': 'ハ', 'ば': 'バ', 'ぱ': 'パ', 'ひ': 'ヒ', 'び': 'ビ', 'ぴ': 'ピ', 'ふ': 'フ', 'ぶ': 'ブ', 'ぷ': 'プ', 'へ': 'ヘ', 'べ': 'ベ', 'ぺ': 'ペ', 'ほ': 'ホ', 'ぼ': 'ボ', 'ぽ': 'ポ', 'ま': 'マ', 'み': 'ミ', 'む': 'ム', 'め': 'メ', 'も': 'モ', 'ゃ': 'ャ', 'や': 'ヤ', 'ゅ': 'ュ', 'ゆ': 'ユ', 'ょ': 'ョ', 'よ': 'ヨ', 'ら': 'ラ', 'り': 'リ', 'る': 'ル', 'れ': 'レ', 'ろ': 'ロ', 'わ': 'ワ', 'を': 'ヲ', 'ん': 'ン', 'ゎ': 'ヮ', 'ゐ': 'ヰ', 'ゑ': 'ヱ', 'ゕ': 'ヵ', 'ゖ': 'ヶ', 'ゔ': 'ヴ', 'ゝ': 'ヽ', 'ゞ': 'ヾ' };
|
||||||
const allowedHalfWidthKana: string[] = Object.values(mappings);
|
const allowedHalfWidthKana: string[] = Object.values(mappings);
|
||||||
|
|
||||||
const newTitle = title
|
const newTitle = title
|
||||||
|
@ -120,7 +197,7 @@ export function sanitizeHalfWidthTitle(title: string) {
|
||||||
return newTitle;
|
return newTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeFullWidthTitle(title: string) {
|
export function sanitizeFullWidthTitle(title: string, justRemap: boolean = false) {
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
const mappings: { [key: string]: string } = { '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', "'": ''', '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\\': '\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', ' ': '\u3000', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、' };
|
const mappings: { [key: string]: string } = { '!': '!', '"': '"', '#': '#', '$': '$', '%': '%', '&': '&', "'": ''', '(': '(', ')': ')', '*': '*', '+': '+', ',': ',', '-': '-', '.': '.', '/': '/', ':': ':', ';': ';', '<': '<', '=': '=', '>': '>', '?': '?', '@': '@', 'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E', 'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O', 'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z', '[': '[', '\\': '\', ']': ']', '^': '^', '_': '_', '`': '`', 'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e', 'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j', 'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o', 'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't', 'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z', '{': '{', '|': '|', '}': '}', '~': '~', ' ': '\u3000', '0': '0', '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', 'ァ': 'ァ', 'ア': 'ア', 'ィ': 'ィ', 'イ': 'イ', 'ゥ': 'ゥ', 'ウ': 'ウ', 'ェ': 'ェ', 'エ': 'エ', 'ォ': 'ォ', 'オ': 'オ', 'カ': 'カ', 'ガ': 'ガ', 'キ': 'キ', 'ギ': 'ギ', 'ク': 'ク', 'グ': 'グ', 'ケ': 'ケ', 'ゲ': 'ゲ', 'コ': 'コ', 'ゴ': 'ゴ', 'サ': 'サ', 'ザ': 'ザ', 'シ': 'シ', 'ジ': 'ジ', 'ス': 'ス', 'ズ': 'ズ', 'セ': 'セ', 'ゼ': 'ゼ', 'ソ': 'ソ', 'ゾ': 'ゾ', 'タ': 'タ', 'ダ': 'ダ', 'チ': 'チ', 'ヂ': 'ヂ', 'ッ': 'ッ', 'ツ': 'ツ', 'ヅ': 'ヅ', 'テ': 'テ', 'デ': 'デ', 'ト': 'ト', 'ド': 'ド', 'ナ': 'ナ', 'ニ': 'ニ', 'ヌ': 'ヌ', 'ネ': 'ネ', 'ノ': 'ノ', 'ハ': 'ハ', 'バ': 'バ', 'パ': 'パ', 'ヒ': 'ヒ', 'ビ': 'ビ', 'ピ': 'ピ', 'フ': 'フ', 'ブ': 'ブ', 'プ': 'プ', 'ヘ': 'ヘ', 'ベ': 'ベ', 'ペ': 'ペ', 'ホ': 'ホ', 'ボ': 'ボ', 'ポ': 'ポ', 'マ': 'マ', 'ミ': 'ミ', 'ム': 'ム', 'メ': 'メ', 'モ': 'モ', 'ャ': 'ャ', 'ヤ': 'ヤ', 'ュ': 'ュ', 'ユ': 'ユ', 'ョ': 'ョ', 'ヨ': 'ヨ', 'ラ': 'ラ', 'リ': 'リ', 'ル': 'ル', 'レ': 'レ', 'ロ': 'ロ', 'ワ': 'ワ', 'ヲ': 'ヲ', 'ン': 'ン', 'ー': 'ー', 'ヮ': 'ヮ', 'ヰ': 'ヰ', 'ヱ': 'ヱ', 'ヵ': 'ヵ', 'ヶ': 'ヶ', 'ヴ': 'ヴ', 'ヽ': 'ヽ', 'ヾ': 'ヾ', '・': '・', '「': '「', '」': '」', '。': '。', '、': '、' };
|
||||||
|
|
||||||
|
@ -129,6 +206,8 @@ export function sanitizeFullWidthTitle(title: string) {
|
||||||
.map(n => mappings[n] ?? n)
|
.map(n => mappings[n] ?? n)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
|
if (justRemap) return newTitle;
|
||||||
|
|
||||||
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
|
const sjisEncoded = jconv.encode(newTitle, 'SJIS');
|
||||||
if (jconv.decode(sjisEncoded, 'SJIS') !== newTitle) return sanitizeTitle(title); // Fallback
|
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)
|
if (sjisEncoded.length !== title.length * 2) return sanitizeTitle(title); // Fallback (every character in the full-width title is 2 bytes)
|
||||||
|
@ -148,6 +227,7 @@ export type DisplayTrack = {
|
||||||
group: string | null;
|
group: string | null;
|
||||||
duration: string;
|
duration: string;
|
||||||
encoding: string;
|
encoding: string;
|
||||||
|
durationInSecs: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getSortedTracks(disc: Disc | null) {
|
export function getSortedTracks(disc: Disc | null) {
|
||||||
|
@ -162,6 +242,7 @@ export function getSortedTracks(disc: Disc | null) {
|
||||||
group: group.title ?? null,
|
group: group.title ?? null,
|
||||||
encoding: EncodingName[track.encoding],
|
encoding: EncodingName[track.encoding],
|
||||||
duration: formatTimeFromFrames(track.duration, false),
|
duration: formatTimeFromFrames(track.duration, false),
|
||||||
|
durationInSecs: track.duration / 512, // CAVEAT: 1s = 512 frames
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1682,6 +1682,11 @@
|
||||||
"resolved" "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.5.tgz"
|
"resolved" "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.5.tgz"
|
||||||
"version" "1.0.5"
|
"version" "1.0.5"
|
||||||
|
|
||||||
|
"@types/wicg-mediasession@^1.1.3":
|
||||||
|
"integrity" "sha512-lzoszzJJfW9vcaIxf6tDx3lCJq/4oaD+mplA7sCV7W21PGdR6yUPwErN047ziIcwFx61w8WMURIwUyj1V7KJIQ=="
|
||||||
|
"resolved" "https://registry.npmjs.org/@types/wicg-mediasession/-/wicg-mediasession-1.1.3.tgz"
|
||||||
|
"version" "1.1.3"
|
||||||
|
|
||||||
"@types/yargs-parser@*":
|
"@types/yargs-parser@*":
|
||||||
"integrity" "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
|
"integrity" "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
|
||||||
"resolved" "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz"
|
"resolved" "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz"
|
||||||
|
|
Loading…
Reference in New Issue