WIP: refactor, merge v13

This commit is contained in:
wataru 2023-03-08 02:26:17 +09:00
parent 3ad682dba2
commit cdb3234111
126 changed files with 23590 additions and 1 deletions

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 473 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 395 B

View File

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 403 B

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

10
client/demo_v13/dist/index.html vendored Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8" />
<title>Voice Changer Client Demo</title>
<script defer src="index.js"></script></head>
<body style="width: 100%; height: 100%; margin: 0px">
<div id="app" style="width: 100%; height: 100%"></div>
</body>
</html>

934
client/demo_v13/dist/index.js vendored Normal file

File diff suppressed because one or more lines are too long

19452
client/demo_v13/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": ".eslintrc.js",
"scripts": {
"clean": "rimraf dist/*",
"webpack:prod": "npx webpack --config webpack.prod.js",
"webpack:dev": "npx webpack --config webpack.dev.js",
"build:prod": "npm-run-all clean webpack:prod",
"build:dev": "npm-run-all clean webpack:dev",
"start": "webpack-dev-server --config webpack.dev.js",
"build:mod": "cd ../lib && npm run build:dev && cd - && cp -r ../lib/dist/* node_modules/@dannadori/voice-changer-client-js/dist/",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"voice conversion"
],
"author": "wataru.okada@flect.co.jp",
"license": "ISC",
"devDependencies": {
"@babel/plugin-transform-runtime": "^7.21.0",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@types/node": "^18.14.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"eslint": "^8.35.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-webpack-plugin": "^4.0.0",
"html-loader": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"npm-run-all": "^4.1.5",
"postcss-loader": "^7.0.2",
"postcss-nested": "^6.0.1",
"prettier": "^2.8.4",
"rimraf": "^4.1.3",
"style-loader": "^3.3.1",
"ts-loader": "^9.4.2",
"tsconfig-paths": "^4.1.2",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"@dannadori/voice-changer-client-js": "^1.0.86",
"@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-brands-svg-icons": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 122 KiB

View File

Before

Width:  |  Height:  |  Size: 473 B

After

Width:  |  Height:  |  Size: 473 B

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

View File

Before

Width:  |  Height:  |  Size: 365 B

After

Width:  |  Height:  |  Size: 365 B

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 395 B

View File

Before

Width:  |  Height:  |  Size: 403 B

After

Width:  |  Height:  |  Size: 403 B

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,172 @@
import * as React from "react";
import { createRoot } from "react-dom/client";
import "./css/App.css"
import { ErrorInfo, useMemo, useState, } from "react";
import { useMicrophoneOptions } from "./100_options_microphone";
import { AppStateProvider, useAppState } from "./001_provider/001_AppStateProvider";
import localForage from "localforage";
import { library } from "@fortawesome/fontawesome-svg-core";
import { fas } from "@fortawesome/free-solid-svg-icons";
import { far } from "@fortawesome/free-regular-svg-icons";
import { fab } from "@fortawesome/free-brands-svg-icons";
import { AppRootProvider } from "./001_provider/001_AppRootProvider";
import ErrorBoundary from "./001_provider/900_ErrorBoundary";
import { INDEXEDDB_KEY_CLIENT, INDEXEDDB_KEY_MODEL_DATA, INDEXEDDB_KEY_SERVER, INDEXEDDB_KEY_WORKLET, INDEXEDDB_KEY_WORKLETNODE, useIndexedDB } from "@dannadori/voice-changer-client-js";
import { INDEXEDDB_KEY_AUDIO_OUTPUT } from "./const";
library.add(fas, far, fab);
const container = document.getElementById("app")!;
const root = createRoot(container);
const App = () => {
const appState = useAppState()
const { removeItem } = useIndexedDB()
const { voiceChangerSetting } = useMicrophoneOptions()
const titleRow = useMemo(() => {
return (
<div className="top-title">
<span className="title">Voice Changer Setting</span>
<span className="top-title-version">for v.1.3.x</span>
<span className="belongings">
<a className="link" href="https://github.com/w-okada/voice-changer" target="_blank" rel="noopener noreferrer">
<img src="./assets/icons/github.svg" />
<span>github</span>
</a>
<a className="link" href="https://zenn.dev/wok/books/0002_vc-helper-v_1_3" target="_blank" rel="noopener noreferrer">
<img src="./assets/icons/help-circle.svg" />
<span>manual</span>
</a>
<a className="link" href="https://www.buymeacoffee.com/wokad" target="_blank" rel="noopener noreferrer">
<img className="donate-img" src="./assets/buymeacoffee.png" />
<span></span>
</a>
</span>
<span className="belongings">
</span>
</div>
)
}, [])
const clearRow = useMemo(() => {
const onClearSettingClicked = async () => {
await appState.clearSetting()
await removeItem(INDEXEDDB_KEY_AUDIO_OUTPUT)
location.reload()
}
return (
<>
<div className="body-row split-3-3-4 left-padding-1">
<div className="body-button-container">
<div className="body-button" onClick={onClearSettingClicked}>clear setting</div>
</div>
<div className="body-item-text"></div>
<div className="body-item-text"></div>
</div>
</>
)
}, [])
const mainSetting = useMemo(() => {
return (
<>
<div className="main-body">
{titleRow}
{clearRow}
{voiceChangerSetting}
</div>
</>
)
}, [voiceChangerSetting])
return (
<>
{mainSetting}
</>
)
}
const AppStateWrapper = () => {
// エラーバウンダリー設定
const [error, setError] = useState<{ error: Error, errorInfo: ErrorInfo }>()
const { removeItem } = useIndexedDB()
// localForage.config({
// driver: localForage.INDEXEDDB,
// name: INDEXEDDB_DB_APP_NAME,
// version: 1.0,
// storeName: INDEXEDDB_DB_NAME,
// description: 'appStorage'
// })
const errorComponent = useMemo(() => {
const errorName = error?.error.name || "no error name"
const errorMessage = error?.error.message || "no error message"
const errorInfos = (error?.errorInfo.componentStack || "no error stack").split("\n")
const onClearCacheClicked = async () => {
const indexedDBKeys = [
INDEXEDDB_KEY_CLIENT,
INDEXEDDB_KEY_SERVER,
INDEXEDDB_KEY_WORKLETNODE,
INDEXEDDB_KEY_MODEL_DATA,
INDEXEDDB_KEY_WORKLET,
INDEXEDDB_KEY_AUDIO_OUTPUT
]
for (const k of indexedDBKeys) {
await removeItem(k)
}
location.reload();
}
return (
<div className="error-container">
<div className="top-error-message">
</div>
<div className="top-error-description">
<p></p>
<p></p>
<p><button onClick={onClearCacheClicked}></button></p>
</div>
<div className="error-detail">
<div className="error-name">
{errorName}
</div>
<div className="error-message">
{errorMessage}
</div>
<div className="error-info-container">
{errorInfos.map(x => {
return <div className="error-info-line" key={x}>{x}</div>
})}
</div>
</div>
</div>
)
}, [error])
const updateError = (error: Error, errorInfo: React.ErrorInfo) => {
console.log("error compo", error, errorInfo)
setError({ error, errorInfo })
}
return (
<ErrorBoundary fallback={errorComponent} onError={updateError}>
<AppStateProvider>
<App></App>
</AppStateProvider>
</ErrorBoundary>
)
}
root.render(
<AppRootProvider>
<AppStateWrapper></AppStateWrapper>
</AppRootProvider>
);

View File

@ -0,0 +1,75 @@
import { ClientState } from "@dannadori/voice-changer-client-js";
import React, { useContext, useEffect, useRef } from "react";
import { ReactNode } from "react";
import { useVCClient, VCClientState } from "../001_globalHooks/001_useVCClient";
import { FrontendManagerStateAndMethod, useFrontendManager } from "../001_globalHooks/010_useFrontendManager";
import { useAppRoot } from "./001_AppRootProvider";
type Props = {
children: ReactNode;
};
type AppStateValue = ClientState & {
audioContext: AudioContext
frontendManagerState: FrontendManagerStateAndMethod;
initializedRef: React.MutableRefObject<boolean>
}
const AppStateContext = React.createContext<AppStateValue | null>(null);
export const useAppState = (): AppStateValue => {
const state = useContext(AppStateContext);
if (!state) {
throw new Error("useAppState must be used within AppStateProvider");
}
return state;
};
export const AppStateProvider = ({ children }: Props) => {
const appRoot = useAppRoot()
const clientState = useVCClient({ audioContext: appRoot.audioContextState.audioContext })
const frontendManagerState = useFrontendManager();
const initializedRef = useRef<boolean>(false)
useEffect(() => {
if (clientState.clientState.initialized) {
initializedRef.current = true
clientState.clientState.clientSetting.updateClientSetting({
...clientState.clientState.clientSetting.clientSetting, speakers: [
{
"id": 107,
"name": "user"
},
{
"id": 100,
"name": "ずんだもん"
},
{
"id": 101,
"name": "そら"
},
{
"id": 102,
"name": "めたん"
},
{
"id": 103,
"name": "つむぎ"
}
]
})
}
}, [clientState.clientState.initialized])
const providerValue: AppStateValue = {
audioContext: appRoot.audioContextState.audioContext!,
...clientState.clientState,
frontendManagerState,
initializedRef
};
return <AppStateContext.Provider value={providerValue}>{children}</AppStateContext.Provider>;
};

View File

@ -0,0 +1,293 @@
import { OnnxExecutionProvider, Framework, fileSelector, Correspondence } from "@dannadori/voice-changer-client-js"
import React, { useState } from "react"
import { useMemo } from "react"
import { useAppState } from "./001_provider/001_AppStateProvider";
import { AnimationTypes, HeaderButton, HeaderButtonProps } from "./components/101_HeaderButton";
export type ServerSettingState = {
modelSetting: JSX.Element;
}
export const useModelSettingArea = (): ServerSettingState => {
const appState = useAppState()
const [showPyTorch, setShowPyTorch] = useState<boolean>(false)
const accodionButton = useMemo(() => {
const accodionButtonProps: HeaderButtonProps = {
stateControlCheckbox: appState.frontendManagerState.stateControls.openModelSettingCheckbox,
tooltip: "Open/Close",
onIcon: ["fas", "caret-up"],
offIcon: ["fas", "caret-up"],
animation: AnimationTypes.spinner,
tooltipClass: "tooltip-right",
};
return <HeaderButton {...accodionButtonProps}></HeaderButton>;
}, []);
const uploadeModelRow = useMemo(() => {
const onPyTorchFileLoadClicked = async () => {
const file = await fileSelector("")
if (file.name.endsWith(".pth") == false) {
alert("モデルファイルの拡張子はpthである必要があります。")
return
}
appState.serverSetting.setFileUploadSetting({
...appState.serverSetting.fileUploadSetting,
pyTorchModel: {
file: file
}
})
}
const onPyTorchFileClearClicked = () => {
appState.serverSetting.setFileUploadSetting({
...appState.serverSetting.fileUploadSetting,
pyTorchModel: null
})
}
const onConfigFileLoadClicked = async () => {
const file = await fileSelector("")
if (file.name.endsWith(".json") == false) {
alert("モデルファイルの拡張子はjsonである必要があります。")
return
}
appState.serverSetting.setFileUploadSetting({
...appState.serverSetting.fileUploadSetting,
configFile: {
file: file
}
})
}
const onConfigFileClearClicked = () => {
appState.serverSetting.setFileUploadSetting({
...appState.serverSetting.fileUploadSetting,
configFile: null
})
}
const onOnnxFileLoadClicked = async () => {
const file = await fileSelector("")
if (file.name.endsWith(".onnx") == false) {
alert("モデルファイルの拡張子はonnxである必要があります。")
return
}
appState.serverSetting.setFileUploadSetting({
...appState.serverSetting.fileUploadSetting,
onnxModel: {
file: file
}
})
}
const onOnnxFileClearClicked = () => {
appState.serverSetting.setFileUploadSetting({
...appState.serverSetting.fileUploadSetting,
onnxModel: null
})
}
const onCorrespondenceFileLoadClicked = async () => {
const file = await fileSelector("")
const correspondenceText = await file.text()
const cors = correspondenceText.split("\n").map(line => {
const items = line.split("|")
if (items.length != 3) {
console.warn("Invalid Correspondence Line:", line)
return null
} else {
const cor: Correspondence = {
sid: Number(items[0]),
correspondence: Number(items[1]),
dirname: items[2]
}
return cor
}
}).filter(x => { return x != null }) as Correspondence[]
console.log(cors)
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, correspondences: cors })
}
const onCorrespondenceFileClearClicked = () => {
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, correspondences: [] })
}
const onModelUploadClicked = async () => {
appState.serverSetting.loadModel()
}
const uploadButtonClassName = appState.serverSetting.isUploading ? "body-button-disabled" : "body-button"
const uploadButtonAction = appState.serverSetting.isUploading ? () => { } : onModelUploadClicked
const uploadButtonLabel = appState.serverSetting.isUploading ? "wait..." : "upload"
const configFilenameText = appState.serverSetting.fileUploadSetting.configFile?.filename || appState.serverSetting.fileUploadSetting.configFile?.file?.name || ""
const onnxModelFilenameText = appState.serverSetting.fileUploadSetting.onnxModel?.filename || appState.serverSetting.fileUploadSetting.onnxModel?.file?.name || ""
const pyTorchFilenameText = appState.serverSetting.fileUploadSetting.pyTorchModel?.filename || appState.serverSetting.fileUploadSetting.pyTorchModel?.file?.name || ""
const correspondenceFileText = appState.clientSetting.clientSetting.correspondences ? JSON.stringify(appState.clientSetting.clientSetting.correspondences.map(x => { return x.dirname })) : ""
const uploadingStatus = appState.serverSetting.isUploading ?
appState.serverSetting.uploadProgress == 0 ? `loading model...(wait about 20sec)` : `uploading.... ${appState.serverSetting.uploadProgress}%` : ""
return (
<>
<div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title left-padding-1">Model Uploader</div>
<div className="body-item-text">
<div></div>
</div>
<div className="body-item-text">
<div>
<input type="checkbox" checked={showPyTorch} onChange={(e) => {
setShowPyTorch(e.target.checked)
}} /> enable PyTorch
</div>
</div>
</div>
<div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title left-padding-2">Config(.json)</div>
<div className="body-item-text">
<div>{configFilenameText}</div>
</div>
<div className="body-button-container">
<div className="body-button" onClick={onConfigFileLoadClicked}>select</div>
<div className="body-button left-margin-1" onClick={onConfigFileClearClicked}>clear</div>
</div>
</div>
{/* <div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title left-padding-2">Correspondence</div>
<div className="body-item-text">
<div>{correspondenceFileText}</div>
</div>
<div className="body-button-container">
<div className="body-button" onClick={onCorrespondenceFileLoadClicked}>select</div>
<div className="body-button left-margin-1" onClick={onCorrespondenceFileClearClicked}>clear</div>
</div>
</div>
*/}
<div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title left-padding-2">Onnx(.onnx)</div>
<div className="body-item-text">
<div>{onnxModelFilenameText}</div>
</div>
<div className="body-button-container">
<div className="body-button" onClick={onOnnxFileLoadClicked}>select</div>
<div className="body-button left-margin-1" onClick={onOnnxFileClearClicked}>clear</div>
</div>
</div>
{showPyTorch ?
(
<div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title left-padding-2">PyTorch(.pth)</div>
<div className="body-item-text">
<div>{pyTorchFilenameText}</div>
</div>
<div className="body-button-container">
<div className="body-button" onClick={onPyTorchFileLoadClicked}>select</div>
<div className="body-button left-margin-1" onClick={onPyTorchFileClearClicked}>clear</div>
</div>
</div>
)
:
(
<></>
)
}
<div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title left-padding-2"></div>
<div className="body-item-text">
{uploadingStatus}
</div>
<div className="body-button-container">
<div className={uploadButtonClassName} onClick={uploadButtonAction}>{uploadButtonLabel}</div>
</div>
</div>
</>
)
}, [
appState.serverSetting.fileUploadSetting,
appState.serverSetting.loadModel,
appState.serverSetting.isUploading,
appState.serverSetting.uploadProgress,
appState.clientSetting.clientSetting.correspondences,
appState.serverSetting.updateServerSettings,
appState.serverSetting.setFileUploadSetting,
showPyTorch])
const frameworkRow = useMemo(() => {
const onFrameworkChanged = async (val: Framework) => {
appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, framework: val })
}
return (
<div className="body-row split-3-7 left-padding-1 guided">
<div className="body-item-title left-padding-1">Framework</div>
<div className="body-select-container">
<select className="body-select" value={appState.serverSetting.serverSetting.framework} onChange={(e) => {
onFrameworkChanged(e.target.value as
Framework)
}}>
{
Object.values(Framework).map(x => {
return <option key={x} value={x}>{x}</option>
})
}
</select>
</div>
</div>
)
}, [appState.serverSetting.serverSetting.framework, appState.serverSetting.updateServerSettings])
const onnxExecutionProviderRow = useMemo(() => {
if (appState.serverSetting.serverSetting.framework != "ONNX") {
return
}
const onOnnxExecutionProviderChanged = async (val: OnnxExecutionProvider) => {
appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, onnxExecutionProvider: val })
}
console.log("setting", appState.serverSetting.serverSetting)
return (
<div className="body-row split-3-7 left-padding-1">
<div className="body-item-title left-padding-2">OnnxExecutionProvider</div>
<div className="body-select-container">
<select className="body-select" value={appState.serverSetting.serverSetting.onnxExecutionProvider} onChange={(e) => {
onOnnxExecutionProviderChanged(e.target.value as
OnnxExecutionProvider)
}}>
{
Object.values(OnnxExecutionProvider).map(x => {
return <option key={x} value={x}>{x}</option>
})
}
</select>
</div>
</div>
)
}, [appState.serverSetting.serverSetting.framework, appState.serverSetting.serverSetting.onnxExecutionProvider, appState.serverSetting.updateServerSettings])
const modelSetting = useMemo(() => {
return (
<>
{appState.frontendManagerState.stateControls.openModelSettingCheckbox.trigger}
<div className="partition">
<div className="partition-header">
<span className="caret">
{accodionButton}
</span>
<span className="title" onClick={() => { appState.frontendManagerState.stateControls.openModelSettingCheckbox.updateState(!appState.frontendManagerState.stateControls.openModelSettingCheckbox.checked()) }}>
Model Setting
</span>
<span></span>
</div>
<div className="partition-content">
{uploadeModelRow}
{frameworkRow}
{onnxExecutionProviderRow}
</div>
</div>
</>
)
}, [uploadeModelRow, frameworkRow, onnxExecutionProviderRow])
return {
modelSetting,
}
}

View File

@ -0,0 +1,390 @@
import { fileSelectorAsDataURL, useIndexedDB } from "@dannadori/voice-changer-client-js"
import React, { useEffect, useMemo, useRef, useState } from "react"
import { AUDIO_ELEMENT_FOR_PLAY_RESULT, AUDIO_ELEMENT_FOR_TEST_CONVERTED, AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK, AUDIO_ELEMENT_FOR_TEST_ORIGINAL, INDEXEDDB_KEY_AUDIO_OUTPUT } from "./const"
import { useAppState } from "./001_provider/001_AppStateProvider";
import { AnimationTypes, HeaderButton, HeaderButtonProps } from "./components/101_HeaderButton";
const reloadDevices = async () => {
try {
const ms = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
ms.getTracks().forEach(x => { x.stop() })
} catch (e) {
console.warn("Enumerate device error::", e)
}
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
const audioInputs = mediaDeviceInfos.filter(x => { return x.kind == "audioinput" })
audioInputs.push({
deviceId: "none",
groupId: "none",
kind: "audioinput",
label: "none",
toJSON: () => { }
})
audioInputs.push({
deviceId: "file",
groupId: "file",
kind: "audioinput",
label: "file",
toJSON: () => { }
})
const audioOutputs = mediaDeviceInfos.filter(x => { return x.kind == "audiooutput" })
audioOutputs.push({
deviceId: "none",
groupId: "none",
kind: "audiooutput",
label: "none",
toJSON: () => { }
})
// audioOutputs.push({
// deviceId: "record",
// groupId: "record",
// kind: "audiooutput",
// label: "record",
// toJSON: () => { }
// })
return [audioInputs, audioOutputs]
}
export type DeviceSettingState = {
deviceSetting: JSX.Element;
}
export const useDeviceSetting = (): DeviceSettingState => {
const appState = useAppState()
const accodionButton = useMemo(() => {
const accodionButtonProps: HeaderButtonProps = {
stateControlCheckbox: appState.frontendManagerState.stateControls.openDeviceSettingCheckbox,
tooltip: "Open/Close",
onIcon: ["fas", "caret-up"],
offIcon: ["fas", "caret-up"],
animation: AnimationTypes.spinner,
tooltipClass: "tooltip-right",
};
return <HeaderButton {...accodionButtonProps}></HeaderButton>;
}, []);
const [inputAudioDeviceInfo, setInputAudioDeviceInfo] = useState<MediaDeviceInfo[]>([])
const [outputAudioDeviceInfo, setOutputAudioDeviceInfo] = useState<MediaDeviceInfo[]>([])
const [audioInputForGUI, setAudioInputForGUI] = useState<string>("none")
const [audioOutputForGUI, setAudioOutputForGUI] = useState<string>("none")
const [fileInputEchoback, setFileInputEchoback] = useState<boolean>()//最初のmuteが有効になるように。undefined
const { getItem, setItem } = useIndexedDB()
const audioSrcNode = useRef<MediaElementAudioSourceNode>()
const [outputRecordingStarted, setOutputRecordingStarted] = useState<boolean>(false)
const [useServerMicrophone, setUseServerMicrophone] = useState<boolean>(false)
// リスト内の
useEffect(() => {
const initialize = async () => {
const audioInfo = await reloadDevices()
setInputAudioDeviceInfo(audioInfo[0])
setOutputAudioDeviceInfo(audioInfo[1])
// if (useServerMicrophone) {
// try {
// const serverDevices = await appState.serverSetting.getServerDevices()
// setServerInputAudioDeviceInfo(serverDevices.audio_input_devices)
// } catch (e) {
// console.warn(e)
// }
// }
}
initialize()
}, [useServerMicrophone])
// キャッシュの設定は反映(たぶん、設定操作の時も起動していしまう。が問題は起こらないはず)
useEffect(() => {
if (typeof appState.clientSetting.clientSetting.audioInput == "string") {
if (inputAudioDeviceInfo.find(x => {
// console.log("COMPARE:", x.deviceId, appState.clientSetting.setting.audioInput)
return x.deviceId == appState.clientSetting.clientSetting.audioInput
})) {
setAudioInputForGUI(appState.clientSetting.clientSetting.audioInput)
}
}
}, [inputAudioDeviceInfo, appState.clientSetting.clientSetting.audioInput])
const audioInputRow = useMemo(() => {
if (useServerMicrophone) {
return <></>
}
return (
<div className="body-row split-3-7 left-padding-1 guided">
<div className="body-item-title left-padding-1">AudioInput</div>
<div className="body-select-container">
<select className="body-select" value={audioInputForGUI} onChange={(e) => {
setAudioInputForGUI(e.target.value)
}}>
{
inputAudioDeviceInfo.map(x => {
return <option key={x.deviceId} value={x.deviceId}>{x.label}</option>
})
}
</select>
</div>
</div>
)
}, [inputAudioDeviceInfo, audioInputForGUI, useServerMicrophone])
useEffect(() => {
if (audioInputForGUI == "file") {
// file selector (audioMediaInputRow)
} else {
if (!useServerMicrophone) {
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, audioInput: audioInputForGUI })
} else {
console.log("server mic")
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, audioInput: null })
}
}
}, [appState.audioContext, audioInputForGUI, appState.clientSetting.updateClientSetting])
const audioMediaInputRow = useMemo(() => {
if (audioInputForGUI != "file") {
return <></>
}
const onFileLoadClicked = async () => {
const url = await fileSelectorAsDataURL("")
// input stream for client.
const audio = document.getElementById(AUDIO_ELEMENT_FOR_TEST_CONVERTED) as HTMLAudioElement
audio.pause()
audio.srcObject = null
audio.src = url
await audio.play()
if (!audioSrcNode.current) {
audioSrcNode.current = appState.audioContext!.createMediaElementSource(audio);
}
if (audioSrcNode.current.mediaElement != audio) {
audioSrcNode.current = appState.audioContext!.createMediaElementSource(audio);
}
const dst = appState.audioContext.createMediaStreamDestination()
audioSrcNode.current.connect(dst)
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, audioInput: dst.stream })
const audio_echo = document.getElementById(AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK) as HTMLAudioElement
audio_echo.srcObject = dst.stream
audio_echo.play()
audio_echo.volume = 0
setFileInputEchoback(false)
// original stream to play.
const audio_org = document.getElementById(AUDIO_ELEMENT_FOR_TEST_ORIGINAL) as HTMLAudioElement
audio_org.src = url
audio_org.pause()
// audio_org.onplay = () => {
// console.log(audioOutputRef.current)
// // @ts-ignore
// audio_org.setSinkId(audioOutputRef.current)
// }
}
return (
<div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title"></div>
<div className="body-item-text">
<div style={{ display: "none" }}>
org:<audio id={AUDIO_ELEMENT_FOR_TEST_ORIGINAL} controls></audio>
</div>
<div>
<audio id={AUDIO_ELEMENT_FOR_TEST_CONVERTED} controls></audio>
<audio id={AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK} controls hidden></audio>
</div>
</div>
<div className="body-button-container">
<div className="body-button" onClick={onFileLoadClicked}>load</div>
<input type="checkbox" checked={fileInputEchoback} onChange={(e) => { setFileInputEchoback(e.target.checked) }} /> echoback
</div>
</div>
)
}, [audioInputForGUI, appState.clientSetting.updateClientSetting, fileInputEchoback])
const audioOutputRow = useMemo(() => {
return (
<div className="body-row split-3-7 left-padding-1 guided">
<div className="body-item-title left-padding-1">AudioOutput</div>
<div className="body-select-container">
<select className="body-select" value={audioOutputForGUI} onChange={(e) => {
setAudioOutputForGUI(e.target.value)
setItem(INDEXEDDB_KEY_AUDIO_OUTPUT, e.target.value)
}}>
{
outputAudioDeviceInfo.map(x => {
return <option key={x.deviceId} value={x.deviceId}>{x.label}</option>
})
}
</select>
<audio hidden id={AUDIO_ELEMENT_FOR_PLAY_RESULT}></audio>
</div>
</div>
)
}, [outputAudioDeviceInfo, audioOutputForGUI])
const audioOutputRecordingRow = useMemo(() => {
// if (audioOutputForGUI != "record") {
// return <></>
// }
const onOutputRecordStartClicked = async () => {
setOutputRecordingStarted(true)
await appState.workletNodeSetting.startOutputRecording()
}
const onOutputRecordStopClicked = async () => {
setOutputRecordingStarted(false)
const record = await appState.workletNodeSetting.stopOutputRecording()
downloadRecord(record)
}
const startClassName = outputRecordingStarted ? "body-button-active" : "body-button-stanby"
const stopClassName = outputRecordingStarted ? "body-button-stanby" : "body-button-active"
return (
<div className="body-row split-3-3-4 left-padding-1 guided">
<div className="body-item-title left-padding-2">output record</div>
<div className="body-button-container">
<div onClick={onOutputRecordStartClicked} className={startClassName}>start</div>
<div onClick={onOutputRecordStopClicked} className={stopClassName}>stop</div>
</div>
<div className="body-input-container">
</div>
</div>
)
}, [audioOutputForGUI, outputRecordingStarted, appState.workletNodeSetting.startOutputRecording, appState.workletNodeSetting.stopOutputRecording])
useEffect(() => {
[AUDIO_ELEMENT_FOR_PLAY_RESULT, AUDIO_ELEMENT_FOR_TEST_ORIGINAL, AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK].forEach(x => {
const audio = document.getElementById(x) as HTMLAudioElement
if (audio) {
if (audioOutputForGUI == "none") {
// @ts-ignore
audio.setSinkId("")
if (x == AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK) {
audio.volume = 0
} else {
audio.volume = 0
}
} else {
// @ts-ignore
audio.setSinkId(audioOutputForGUI)
if (x == AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK) {
audio.volume = fileInputEchoback ? 1 : 0
} else {
audio.volume = 1
}
}
}
})
}, [audioOutputForGUI])
useEffect(() => {
const loadCache = async () => {
const key = await getItem(INDEXEDDB_KEY_AUDIO_OUTPUT)
if (key) {
setAudioOutputForGUI(key as string)
}
}
loadCache()
}, [])
useEffect(() => {
[AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK].forEach(x => {
const audio = document.getElementById(x) as HTMLAudioElement
if (audio) {
audio.volume = fileInputEchoback ? 1 : 0
}
})
}, [fileInputEchoback])
const deviceSetting = useMemo(() => {
return (
<>
{appState.frontendManagerState.stateControls.openDeviceSettingCheckbox.trigger}
<div className="partition">
<div className="partition-header">
<span className="caret">
{accodionButton}
</span>
<span className="title" onClick={() => { appState.frontendManagerState.stateControls.openDeviceSettingCheckbox.updateState(!appState.frontendManagerState.stateControls.openDeviceSettingCheckbox.checked()) }}>
Device Setting
</span>
<span className="belongings">
{/* <input className="belongings-checkbox" type="checkbox" checked={useServerMicrophone} onChange={(e) => {
setUseServerMicrophone(e.target.checked)
}} /> use server mic (Experimental) */}
</span>
</div>
<div className="partition-content">
{audioInputRow}
{audioMediaInputRow}
{audioOutputRow}
{audioOutputRecordingRow}
</div>
</div>
</>
)
}, [audioInputRow, audioMediaInputRow, audioOutputRow, audioOutputRecordingRow, useServerMicrophone])
const downloadRecord = (data: Float32Array) => {
const writeString = (view: DataView, offset: number, string: string) => {
for (var i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
const floatTo16BitPCM = (output: DataView, offset: number, input: Float32Array) => {
for (var i = 0; i < input.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
};
const buffer = new ArrayBuffer(44 + data.length * 2);
const view = new DataView(buffer);
// https://www.youfit.co.jp/archives/1418
writeString(view, 0, 'RIFF'); // RIFFヘッダ
view.setUint32(4, 32 + data.length * 2, true); // これ以降のファイルサイズ
writeString(view, 8, 'WAVE'); // WAVEヘッダ
writeString(view, 12, 'fmt '); // fmtチャンク
view.setUint32(16, 16, true); // fmtチャンクのバイト数
view.setUint16(20, 1, true); // フォーマットID
view.setUint16(22, 1, true); // チャンネル数
view.setUint32(24, 48000, true); // サンプリングレート
view.setUint32(28, 48000 * 2, true); // データ速度
view.setUint16(32, 2, true); // ブロックサイズ
view.setUint16(34, 16, true); // サンプルあたりのビット数
writeString(view, 36, 'data'); // dataチャンク
view.setUint32(40, data.length * 2, true); // 波形データのバイト数
floatTo16BitPCM(view, 44, data); // 波形データ
const audioBlob = new Blob([view], { type: 'audio/wav' });
const url = URL.createObjectURL(audioBlob);
const a = document.createElement("a");
a.href = url;
a.download = `output.wav`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return {
deviceSetting,
}
}

View File

@ -0,0 +1,312 @@
import { F0Detector } from "@dannadori/voice-changer-client-js"
import React, { useEffect, useMemo, useState } from "react"
import { useAppState } from "./001_provider/001_AppStateProvider";
import { AnimationTypes, HeaderButton, HeaderButtonProps } from "./components/101_HeaderButton";
export type QualityControlState = {
qualityControl: JSX.Element;
}
const reloadDevices = async () => {
try {
const ms = await navigator.mediaDevices.getUserMedia({ video: false, audio: true });
ms.getTracks().forEach(x => { x.stop() })
} catch (e) {
console.warn("Enumerate device error::", e)
}
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
const audioOutputs = mediaDeviceInfos.filter(x => { return x.kind == "audiooutput" })
return audioOutputs
}
export const useQualityControl = (): QualityControlState => {
const appState = useAppState()
const accodionButton = useMemo(() => {
const accodionButtonProps: HeaderButtonProps = {
stateControlCheckbox: appState.frontendManagerState.stateControls.openQualityControlCheckbox,
tooltip: "Open/Close",
onIcon: ["fas", "caret-up"],
offIcon: ["fas", "caret-up"],
animation: AnimationTypes.spinner,
tooltipClass: "tooltip-right",
};
return <HeaderButton {...accodionButtonProps}></HeaderButton>;
}, []);
const [recording, setRecording] = useState<boolean>(false)
const [outputAudioDeviceInfo, setOutputAudioDeviceInfo] = useState<MediaDeviceInfo[]>([])
const [audioOutputForGUI, setAudioOutputForGUI] = useState<string>("default")
useEffect(() => {
const initialize = async () => {
const audioInfo = await reloadDevices()
setOutputAudioDeviceInfo(audioInfo)
}
initialize()
}, [])
const noiseControlRow = useMemo(() => {
return (
<div className="body-row split-3-2-2-2-1 left-padding-1 guided">
<div className="body-item-title left-padding-1 ">Noise Suppression</div>
<div>
<input type="checkbox" checked={appState.clientSetting.clientSetting.echoCancel} onChange={(e) => {
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, echoCancel: e.target.checked })
}} /> echo cancel
</div>
<div>
<input type="checkbox" checked={appState.clientSetting.clientSetting.noiseSuppression} onChange={(e) => {
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, noiseSuppression: e.target.checked })
}} /> suppression1
</div>
<div>
<input type="checkbox" checked={appState.clientSetting.clientSetting.noiseSuppression2} onChange={(e) => {
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, noiseSuppression2: e.target.checked })
}} /> suppression2
</div>
<div className="body-button-container">
</div>
</div>
)
}, [
appState.clientSetting.clientSetting.echoCancel,
appState.clientSetting.clientSetting.noiseSuppression,
appState.clientSetting.clientSetting.noiseSuppression2,
appState.clientSetting.updateClientSetting
])
const gainControlRow = useMemo(() => {
return (
<div className="body-row split-3-2-2-3 left-padding-1 guided">
<div className="body-item-title left-padding-1 ">Gain Control</div>
<div>
<span className="body-item-input-slider-label">in</span>
<input type="range" className="body-item-input-slider" min="0.0" max="1.0" step="0.1" value={appState.clientSetting.clientSetting.inputGain} onChange={(e) => {
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, inputGain: Number(e.target.value) })
}}></input>
<span className="body-item-input-slider-val">{appState.clientSetting.clientSetting.inputGain}</span>
</div>
<div>
<span className="body-item-input-slider-label">out</span>
<input type="range" className="body-item-input-slider" min="0.0" max="1.0" step="0.1" value={appState.clientSetting.clientSetting.outputGain} onChange={(e) => {
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, outputGain: Number(e.target.value) })
}}></input>
<span className="body-item-input-slider-val">{appState.clientSetting.clientSetting.outputGain}</span>
</div>
<div className="body-button-container">
</div>
</div>
)
}, [
appState.clientSetting.clientSetting.inputGain,
appState.clientSetting.clientSetting.outputGain,
appState.clientSetting.updateClientSetting
])
// const f0DetectorRow = useMemo(() => {
// const desc = { "harvest": "High Quality", "dio": "Light Weight" }
// return (
// <div className="body-row split-3-7 left-padding-1 guided">
// <div className="body-item-title left-padding-1 ">F0 Detector</div>
// <div className="body-select-container">
// <select className="body-select" value={appState.serverSetting.serverSetting.f0Detector} onChange={(e) => {
// appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, f0Detector: e.target.value as F0Detector })
// }}>
// {
// Object.values(F0Detector).map(x => {
// //@ts-ignore
// return <option key={x} value={x}>{x}({desc[x]})</option>
// })
// }
// </select>
// </div>
// </div>
// )
// }, [appState.serverSetting.serverSetting.f0Detector, appState.serverSetting.updateServerSettings])
const recordIORow = useMemo(() => {
const onRecordStartClicked = async () => {
setRecording(true)
await appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, recordIO: 1 })
}
const onRecordStopClicked = async () => {
setRecording(false)
await appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, recordIO: 0 })
// set wav (input)
const wavInput = document.getElementById("body-wav-container-wav-input") as HTMLAudioElement
wavInput.src = "/tmp/in.wav?" + new Date().getTime()
wavInput.controls = true
// @ts-ignore
wavInput.setSinkId(audioOutputForGUI)
// set wav (output)
const wavOutput = document.getElementById("body-wav-container-wav-output") as HTMLAudioElement
wavOutput.src = "/tmp/out.wav?" + new Date().getTime()
wavOutput.controls = true
// @ts-ignore
wavOutput.setSinkId(audioOutputForGUI)
}
const onRecordAnalizeClicked = async () => {
if (appState.frontendManagerState.isConverting) {
alert("please stop voice conversion. 解析処理と音声変換を同時に行うことはできません。音声変化をストップしてください。")
return
}
appState.frontendManagerState.setIsAnalyzing(true)
await appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, recordIO: 2 })
// set spectrogram (dio)
const imageDio = document.getElementById("body-image-container-img-dio") as HTMLImageElement
imageDio.src = "/tmp/analyze-dio.png?" + new Date().getTime()
imageDio.style.width = "100%"
// set spectrogram (harvest)
const imageHarvest = document.getElementById("body-image-container-img-harvest") as HTMLImageElement
imageHarvest.src = "/tmp/analyze-harvest.png?" + new Date().getTime()
imageHarvest.style.width = "100%"
appState.frontendManagerState.setIsAnalyzing(false)
}
const startClassName = recording ? "body-button-active" : "body-button-stanby"
const stopClassName = recording ? "body-button-stanby" : "body-button-active"
const analyzeClassName = appState.frontendManagerState.isAnalyzing ? "body-button-active" : "body-button-stanby"
const analyzeLabel = appState.frontendManagerState.isAnalyzing ? "wait..." : "Analyze"
return (
<>
<div className="body-row split-3-7 left-padding-1 guided">
<div className="body-item-title left-padding-1 ">Analyzer(Experimental)</div>
<div className="body-button-container">
</div>
</div>
<div className="body-row split-3-7 left-padding-1 guided">
<div className="body-item-title left-padding-2 ">
Sampling
</div>
<div className="body-button-container">
<div onClick={onRecordStartClicked} className={startClassName}>Start</div>
<div onClick={onRecordStopClicked} className={stopClassName}>Stop</div>
<div onClick={onRecordAnalizeClicked} className={analyzeClassName}>{analyzeLabel}</div>
</div>
</div>
<div className="body-row split-3-2-2-3 left-padding-1 guided">
<div className="body-item-title left-padding-2 ">
<div>
Play
</div>
<select className="body-select-50 left-margin-2" value={audioOutputForGUI} onChange={(e) => {
setAudioOutputForGUI(e.target.value)
const wavInput = document.getElementById("body-wav-container-wav-input") as HTMLAudioElement
const wavOutput = document.getElementById("body-wav-container-wav-output") as HTMLAudioElement
//@ts-ignore
wavInput.setSinkId(e.target.value)
//@ts-ignore
wavOutput.setSinkId(e.target.value)
}}>
{
outputAudioDeviceInfo.map(x => {
return <option key={x.deviceId} value={x.deviceId}>{x.label}</option>
})
}
</select>
</div>
{/* <div>
<div className="body-wav-container">
<div className="body-wav-container-title">Input</div>
<div className="body-wav-container-title">Output</div>
</div>
<div className="body-wav-container">
<div className="body-wav-container-wav">
<audio src="" id="body-wav-container-wav-input"></audio>
</div>
<div className="body-wav-container-wav" >
<audio src="" id="body-wav-container-wav-output"></audio>
</div>
</div>
</div> */}
<div>
<div className="body-wav-container-title">Input</div>
<div className="body-wav-container-wav">
<audio src="" id="body-wav-container-wav-input"></audio>
</div>
</div>
<div >
<div className="body-wav-container-title">Output</div>
<div className="body-wav-container-wav" >
<audio src="" id="body-wav-container-wav-output"></audio>
</div>
</div>
<div></div>
</div>
<div className="body-row split-3-7 left-padding-1 guided">
<div className="body-item-title left-padding-2 ">
Spectrogram
</div>
<div>
<div className="body-image-container">
<div className="body-image-container-title">PyWorld Dio</div>
<div className="body-image-container-title">PyWorld Harvest</div>
</div>
<div className="body-image-container">
<div className="body-image-container-img" >
<img src="" alt="" id="body-image-container-img-dio" />
</div>
<div className="body-image-container-img">
<img src="" alt="" id="body-image-container-img-harvest" />
</div>
</div>
</div>
</div>
</>
)
}, [appState.serverSetting.serverSetting.recordIO, appState.serverSetting.updateServerSettings, outputAudioDeviceInfo, audioOutputForGUI, appState.frontendManagerState.isAnalyzing, appState.frontendManagerState.isConverting])
const QualityControlContent = useMemo(() => {
return (
<>
{noiseControlRow}
{gainControlRow}
<div className="body-row divider"></div>
{recordIORow}
</>
)
}, [gainControlRow, noiseControlRow, recordIORow])
const qualityControl = useMemo(() => {
return (
<>
{appState.frontendManagerState.stateControls.openQualityControlCheckbox.trigger}
<div className="partition">
<div className="partition-header">
<span className="caret">
{accodionButton}
</span>
<span className="title" onClick={() => { appState.frontendManagerState.stateControls.openQualityControlCheckbox.updateState(!appState.frontendManagerState.stateControls.openQualityControlCheckbox.checked()) }}>
Quality Control
</span>
</div>
<div className="partition-content">
{QualityControlContent}
</div>
</div>
</>
)
}, [QualityControlContent])
return {
qualityControl,
}
}

View File

@ -0,0 +1,165 @@
import React, { useEffect, useMemo, useState } from "react"
import { useAppState } from "./001_provider/001_AppStateProvider";
import { AnimationTypes, HeaderButton, HeaderButtonProps } from "./components/101_HeaderButton";
export const useSpeakerSetting = () => {
const appState = useAppState()
const [editSpeakerTargetId, setEditSpeakerTargetId] = useState<number>(0)
const [editSpeakerTargetName, setEditSpeakerTargetName] = useState<string>("")
const accodionButton = useMemo(() => {
const accodionButtonProps: HeaderButtonProps = {
stateControlCheckbox: appState.frontendManagerState.stateControls.openSpeakerSettingCheckbox,
tooltip: "Open/Close",
onIcon: ["fas", "caret-up"],
offIcon: ["fas", "caret-up"],
animation: AnimationTypes.spinner,
tooltipClass: "tooltip-right",
};
return <HeaderButton {...accodionButtonProps}></HeaderButton>;
}, []);
// const calcDefaultF0Factor = (srcId: number, dstId: number) => {
// const src = appState.clientSetting.clientSetting.correspondences?.find(x => {
// return x.sid == srcId
// })
// const dst = appState.clientSetting.clientSetting.correspondences?.find(x => {
// return x.sid == dstId
// })
// const recommendedF0Factor = dst && src ? dst.correspondence / src.correspondence : 0
// return recommendedF0Factor
// }
// useEffect(() => {
// const recF0 = calcDefaultF0Factor(appState.serverSetting.serverSetting.srcId, appState.serverSetting.serverSetting.dstId)
// appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, f0Factor: recF0 })
// }, [appState.clientSetting.clientSetting.correspondences])
const srcIdRow = useMemo(() => {
return (
<div className="body-row split-3-2-1-4 left-padding-1 guided">
<div className="body-item-title left-padding-1">Source Speaker Id</div>
<div className="body-select-container">
<select className="body-select" value={appState.serverSetting.serverSetting.srcId} onChange={(e) => {
appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, srcId: Number(e.target.value) })
}}>
{
appState.clientSetting.clientSetting.speakers.map(x => {
return <option key={x.id} value={x.id}>{x.name}({x.id})</option>
})
}
</select>
</div>
<div className="body-item-text">
</div>
<div className="body-item-text"></div>
</div>
)
}, [appState.serverSetting.serverSetting.srcId, appState.serverSetting.serverSetting.dstId, appState.clientSetting.clientSetting.speakers, appState.serverSetting.updateServerSettings])
const dstIdRow = useMemo(() => {
return (
<div className="body-row split-3-2-1-4 left-padding-1 guided">
<div className="body-item-title left-padding-1">Destination Speaker Id</div>
<div className="body-select-container">
<select className="body-select" value={appState.serverSetting.serverSetting.dstId} onChange={(e) => {
appState.serverSetting.updateServerSettings({ ...appState.serverSetting.serverSetting, dstId: Number(e.target.value) })
}}>
{
appState.clientSetting.clientSetting.speakers?.map(x => {
return <option key={x.id} value={x.id}>{x.name}({x.id})</option>
})
}
</select>
</div>
<div className="body-item-text">
</div>
<div className="body-item-text"></div>
</div>
)
}, [appState.serverSetting.serverSetting.srcId, appState.serverSetting.serverSetting.dstId, appState.clientSetting.clientSetting.speakers, appState.serverSetting.updateServerSettings])
const editSpeakerIdMappingRow = useMemo(() => {
const onSetSpeakerMappingClicked = async () => {
const targetId = editSpeakerTargetId
const targetName = editSpeakerTargetName
const targetSpeaker = appState.clientSetting.clientSetting.speakers.find(x => { return x.id == targetId })
if (targetSpeaker) {
if (targetName.length == 0) { // Delete
const newSpeakers = appState.clientSetting.clientSetting.speakers.filter(x => { return x.id != targetId })
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, speakers: newSpeakers })
} else { // Update
targetSpeaker.name = targetName
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, speakers: [...appState.clientSetting.clientSetting.speakers] })
}
} else {
if (targetName.length == 0) { // Noop
} else {// add
appState.clientSetting.clientSetting.speakers.push({
id: targetId,
name: targetName
})
appState.clientSetting.updateClientSetting({ ...appState.clientSetting.clientSetting, speakers: [...appState.clientSetting.clientSetting.speakers] })
}
}
}
return (
<div className="body-row split-3-1-2-4 left-padding-1 guided">
<div className="body-item-title left-padding-1">Edit Speaker Mapping</div>
<div className="body-input-container">
<input type="number" min={1} max={256} step={1} value={editSpeakerTargetId} onChange={(e) => {
const id = Number(e.target.value)
setEditSpeakerTargetId(id)
setEditSpeakerTargetName(appState.clientSetting.clientSetting.speakers.find(x => { return x.id == id })?.name || "")
}} />
</div>
<div className="body-input-container">
<input type="text" value={editSpeakerTargetName} onChange={(e) => {
setEditSpeakerTargetName(e.target.value)
}} />
</div>
<div className="body-button-container">
<div className="body-button" onClick={onSetSpeakerMappingClicked}>set</div>
</div>
</div>
)
}, [appState.clientSetting.clientSetting.speakers, editSpeakerTargetId, editSpeakerTargetName])
const speakerSetting = useMemo(() => {
return (
<>
{appState.frontendManagerState.stateControls.openSpeakerSettingCheckbox.trigger}
<div className="partition">
<div className="partition-header">
<span className="caret">
{accodionButton}
</span>
<span className="title" onClick={() => { appState.frontendManagerState.stateControls.openSpeakerSettingCheckbox.updateState(!appState.frontendManagerState.stateControls.openSpeakerSettingCheckbox.checked()) }}>
Speaker Setting
</span>
</div>
<div className="partition-content">
{srcIdRow}
{dstIdRow}
{editSpeakerIdMappingRow}
</div>
</div>
</>
)
}, [srcIdRow, dstIdRow, editSpeakerIdMappingRow])
return {
speakerSetting,
}
}

View File

@ -0,0 +1,18 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true,
},
ecmaVersion: 13,
sourceType: "module",
},
plugins: ["react", "@typescript-eslint"],
rules: {},
};

View File

@ -0,0 +1,6 @@
{
"tabWidth": 4,
"useTabs": false,
"semi": true,
"printWidth": 360
}

8
client/demo_v15/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"files.associations": {
"*.css": "postcss"
},
"workbench.colorCustomizations": {
"tab.activeBackground": "#65952acc"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
client/demo_v15/dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
autoprefixer: {},
"postcss-nested": {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>

After

Width:  |  Height:  |  Size: 473 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-github"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path></svg>

After

Width:  |  Height:  |  Size: 522 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-help-circle"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-twitter"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg>

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html style="width: 100%; height: 100%; overflow: hidden">
<head>
<meta charset="utf-8" />
<title>Voice Changer Client Demo</title>
</head>
<body style="width: 100%; height: 100%; margin: 0px">
<div id="app" style="width: 100%; height: 100%"></div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show More