WIP: new GUI

This commit is contained in:
wataru 2023-06-09 19:57:08 +09:00
parent bc5fd76cc2
commit 8c21d4e5ff
34 changed files with 1682 additions and 286 deletions

View File

@ -16,70 +16,8 @@
"options": {}
}
],
"serverControl": [
{
"name": "startButton",
"options": {}
},
{
"name": "performance",
"options": {}
},
{
"name": "modelSwitch",
"options": {}
},
{
"name": "serverOperation",
"options": {
"showDownload": true,
"showExportOnnx": true,
"showReload": true
}
}
],
"modelSetting": [
{
"name": "modelUploaderv2",
"options": {}
},
{
"name": "modelSlotRow2",
"options": {}
},
{
"name": "commonFileSelect",
"options": {
"title": "Model(.onnx, .pt, pth)",
"acceptExtentions": ["onnx", "pth", "pt"],
"fileKind": "rvcModel"
}
},
{
"name": "commonFileSelect",
"options": {
"title": "index(.index)",
"acceptExtentions": ["index"],
"fileKind": "rvcIndex"
}
},
{
"name": "sampleModelSelect",
"options": {}
},
{
"name": "sampleDownloadControlRow",
"options": {}
},
{
"name": "defaultTuneRow2",
"options": {}
},
{
"name": "modelUploadButtonRow2",
"options": {}
}
],
"serverControl": [],
"modelSetting": [],
"lab": [
{
"name": "mergeLab",
@ -87,72 +25,10 @@
}
],
"deviceSetting": [
{
"name": "audioDeviceMode",
"options": {}
},
{
"name": "audioInput",
"options": {}
},
{
"name": "audioOutput",
"options": {}
}
],
"qualityControl": [
{
"name": "noiseControl",
"options": {}
},
{
"name": "gainControl",
"options": {}
},
{
"name": "f0Detector",
"options": {
"detectors": ["dio", "harvest", "crepe"]
}
},
{
"name": "divider",
"options": {}
},
{
"name": "analyzer",
"options": {}
}
],
"speakerSetting": [
{
"name": "tune",
"options": {}
},
{
"name": "indexRatio",
"options": {}
},
{
"name": "silentThreshold",
"options": {}
}
],
"converterSetting": [
{
"name": "inputChunkNum",
"options": {}
},
{
"name": "extraDataLength",
"options": {}
},
{
"name": "gpu",
"options": {}
}
],
"deviceSetting": [],
"qualityControl": [],
"speakerSetting": [],
"converterSetting": [],
"advancedSetting": [
{
"name": "protocol",
@ -182,6 +58,23 @@
"name": "protect",
"options": {}
}
],
"modelSlotControl": [
{
"name": "modelSlotArea",
"options": {}
},
{
"name": "characterArea",
"options": {}
},
{
"name": "configArea",
"options": {
"detectors": ["dio", "harvest", "crepe"],
"inputChunkNums": [8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 2048]
}
}
]
},

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-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>

After

Width:  |  Height:  |  Size: 311 B

BIN
client/demo/dist/assets/icons/human.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

View File

@ -16,70 +16,8 @@
"options": {}
}
],
"serverControl": [
{
"name": "startButton",
"options": {}
},
{
"name": "performance",
"options": {}
},
{
"name": "modelSwitch",
"options": {}
},
{
"name": "serverOperation",
"options": {
"showDownload": true,
"showExportOnnx": true,
"showReload": true
}
}
],
"modelSetting": [
{
"name": "modelUploaderv2",
"options": {}
},
{
"name": "modelSlotRow2",
"options": {}
},
{
"name": "commonFileSelect",
"options": {
"title": "Model(.onnx, .pt, pth)",
"acceptExtentions": ["onnx", "pth", "pt"],
"fileKind": "rvcModel"
}
},
{
"name": "commonFileSelect",
"options": {
"title": "index(.index)",
"acceptExtentions": ["index"],
"fileKind": "rvcIndex"
}
},
{
"name": "sampleModelSelect",
"options": {}
},
{
"name": "sampleDownloadControlRow",
"options": {}
},
{
"name": "defaultTuneRow2",
"options": {}
},
{
"name": "modelUploadButtonRow2",
"options": {}
}
],
"serverControl": [],
"modelSetting": [],
"lab": [
{
"name": "mergeLab",
@ -87,72 +25,10 @@
}
],
"deviceSetting": [
{
"name": "audioDeviceMode",
"options": {}
},
{
"name": "audioInput",
"options": {}
},
{
"name": "audioOutput",
"options": {}
}
],
"qualityControl": [
{
"name": "noiseControl",
"options": {}
},
{
"name": "gainControl",
"options": {}
},
{
"name": "f0Detector",
"options": {
"detectors": ["dio", "harvest", "crepe"]
}
},
{
"name": "divider",
"options": {}
},
{
"name": "analyzer",
"options": {}
}
],
"speakerSetting": [
{
"name": "tune",
"options": {}
},
{
"name": "indexRatio",
"options": {}
},
{
"name": "silentThreshold",
"options": {}
}
],
"converterSetting": [
{
"name": "inputChunkNum",
"options": {}
},
{
"name": "extraDataLength",
"options": {}
},
{
"name": "gpu",
"options": {}
}
],
"deviceSetting": [],
"qualityControl": [],
"speakerSetting": [],
"converterSetting": [],
"advancedSetting": [
{
"name": "protocol",
@ -182,6 +58,23 @@
"name": "protect",
"options": {}
}
],
"modelSlotControl": [
{
"name": "modelSlotArea",
"options": {}
},
{
"name": "characterArea",
"options": {}
},
{
"name": "configArea",
"options": {
"detectors": ["dio", "harvest", "crepe"],
"inputChunkNums": [8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 2048]
}
}
]
},

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-folder"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path></svg>

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -16,6 +16,7 @@ export type AppGuiDemoSetting = {
"converterSetting": GuiComponentSetting[],
"advancedSetting": GuiComponentSetting[],
"lab": GuiComponentSetting[],
"modelSlotControl": GuiComponentSetting[],
},
dialogs: {
"license": { title: string, auther: string, contact: string, url: string, license: string }[]
@ -54,7 +55,8 @@ const InitialAppGuiDemoSetting: AppGuiDemoSetting = {
"speakerSetting": [],
"converterSetting": [],
"advancedSetting": [],
"lab": []
"lab": [],
"modelSlotControl": []
},
dialogs: {
"license": [{ title: "", auther: "", contact: "", url: "", license: "MIT" }]

View File

@ -57,6 +57,9 @@ import { SampleModelSelectRow, SampleModelSelectRowProps } from "./components/30
import { SampleDownloadControlRow, SampleDownloadControlRowProps } from "./components/301-k_SampleDownloadControl"
import { IndexRatioRow, IndexRatioRowProps } from "./components/609_IndexRatioRow copy"
import { ProtectRow, ProtectRowProps } from "./components/610_ProtectRow"
import { ModelSlotArea, ModelSlotAreaProps } from "./components2/100_ModelSlotArea"
import { CharacterArea, CharacterAreaProps } from "./components2/101_CharacterArea"
import { ConfigArea, ConfigAreaProps } from "./components2/102_ConfigArea"
export const catalog: { [key: string]: (props: any) => JSX.Element } = {}
@ -152,6 +155,10 @@ const initialize = () => {
addToCatalog("mergeLab", (props: MergeLabRowProps) => { return <MergeLabRow {...props} /> })
addToCatalog("modelSlotArea", (props: ModelSlotAreaProps) => { return <ModelSlotArea {...props} /> })
addToCatalog("characterArea", (props: CharacterAreaProps) => { return <CharacterArea {...props} /> })
addToCatalog("configArea", (props: ConfigAreaProps) => { return <ConfigArea {...props} /> })
}

View File

@ -1,4 +1,4 @@
import React, { useEffect } from "react"
import React from "react"
import { GuiStateProvider } from "./001_GuiStateProvider";
import { Dialogs } from "./900_Dialogs";
import { TitleArea } from "./100_TitleArea";
@ -10,6 +10,7 @@ import { SpeakerSetting } from "./600_SpeakerSetting";
import { ConverterSetting } from "./700_ConverterSetting";
import { AdvancedSetting } from "./800_AdvancedSetting";
import { Lab } from "./a00_Lab";
import { ModelSlotControl } from "./b00_ModelSlotControl";
export const Demo = () => {
return (
@ -17,6 +18,7 @@ export const Demo = () => {
<div className="main-body">
<Dialogs />
<TitleArea />
<ModelSlotControl></ModelSlotControl>
<ServerControl />
<ModelSetting />
<SpeakerSetting />

View File

@ -23,6 +23,9 @@ export const ServerControl = () => {
const serverControl = useMemo(() => {
if (componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>

View File

@ -22,6 +22,9 @@ export const ModelSetting = () => {
}, []);
const modelSetting = useMemo(() => {
if (componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>

View File

@ -22,6 +22,9 @@ export const DeviceSetting = () => {
}, []);
const deviceSetting = useMemo(() => {
if (componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>

View File

@ -22,6 +22,9 @@ export const QualityControl = () => {
}, []);
const deviceSetting = useMemo(() => {
if (componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>

View File

@ -22,6 +22,9 @@ export const SpeakerSetting = () => {
}, []);
const deviceSetting = useMemo(() => {
if (componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>

View File

@ -21,7 +21,12 @@ export const ConverterSetting = () => {
return <HeaderButton {...accodionButtonProps}></HeaderButton>;
}, []);
const deviceSetting = useMemo(() => {
if (componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>

View File

@ -21,6 +21,9 @@ export const AdvancedSetting = () => {
}, []);
const deviceSetting = useMemo(() => {
if (componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>

View File

@ -1,8 +1,5 @@
import React, { useMemo, useState } from "react";
import { useGuiState } from "./001_GuiStateProvider";
import { getMessage } from "./messages/MessageBuilder";
import { isDesktopApp } from "../../const";
import { useAppRoot } from "../../001_provider/001_AppRootProvider";
import { useAppState } from "../../001_provider/001_AppStateProvider";
import { InitialFileUploadSetting, fileSelector } from "@dannadori/voice-changer-client-js";
@ -26,7 +23,6 @@ export const ModelSlotManagerDialog = () => {
const [mode, setMode] = useState<Mode>("localFile")
const [fromNetTargetIndex, setFromNetTargetIndex] = useState<number>(0)
const [lang, setLang] = useState<string>("All")
const [sampleId, setSampleId] = useState<string>("")
/////////////////////////////////////////
@ -36,6 +32,9 @@ export const ModelSlotManagerDialog = () => {
if (mode != "localFile") {
return <></>
}
if (!serverSetting.serverSetting.modelSlots) {
return <></>
}
const checkExtention = (filename: string, acceptExtentions: string[]) => {
const ext = filename.split('.').pop();
@ -154,13 +153,13 @@ export const ModelSlotManagerDialog = () => {
} : async (_index: number) => { }
const fileValueClass = (uploadData?.slot == index) ? "model-slot-detail-row-value-edit" : isRegisterd ? "model-slot-detail-row-value-download" : "model-slot-detail-row-value"
const fileValueAction = (uploadData?.slot == index) ? (url: string) => {
} : (url: string) => {
const fileValueAction = (uploadData?.slot == index) ? (_url: string) => {
} : isRegisterd ? (url: string) => {
const link = document.createElement("a")
link.href = url
link.download = url.replace(/^.*[\\\/]/, '')
link.click()
}
} : (_url: string) => { }
const iconUrl = x.modelFile && x.modelFile.length > 0 ? (x.iconFile && x.iconFile.length > 0 ? x.iconFile : "/assets/icons/noimage.png") : "/assets/icons/blank.png"

View File

@ -0,0 +1,29 @@
import React, { useMemo } from "react"
import { useAppRoot } from "../../001_provider/001_AppRootProvider"
import { generateComponent } from "./002_ComponentGenerator"
export const ModelSlotControl = () => {
const { appGuiSettingState } = useAppRoot()
const componentSettings = appGuiSettingState.appGuiSetting.front.modelSlotControl
const deviceSetting = useMemo(() => {
if (!componentSettings || componentSettings.length == 0) {
return <></>
}
const components = componentSettings.map((x, index) => {
const c = generateComponent(x.name, x.options)
return <div key={`${x.name}_${index}`}>{c}</div>
})
return (
<>
<div className="partition">
<div className="partition-content">
{components}
</div>
</div>
</>
)
}, [])
return deviceSetting
}

View File

@ -0,0 +1,68 @@
import React, { useMemo } from "react"
import { useAppState } from "../../../001_provider/001_AppStateProvider"
import { useGuiState } from "../001_GuiStateProvider"
export type ModelSlotAreaProps = {
}
export const ModelSlotArea = (_props: ModelSlotAreaProps) => {
const { serverSetting, getInfo } = useAppState()
const guiState = useGuiState()
const modelTiles = useMemo(() => {
if (!serverSetting.serverSetting.modelSlots) {
return []
}
return serverSetting.serverSetting.modelSlots.map((x, index) => {
if (x.modelFile.length == 0) {
return null
}
const tileContainerClass = index == serverSetting.serverSetting.modelSlotIndex ? "model-slot-tile-container-selected" : "model-slot-tile-container"
const name = x.name.length > 8 ? x.name.substring(0, 7) + "..." : x.name
const iconElem = x.iconFile.length > 0 ?
<img className="model-slot-tile-icon" src={x.iconFile} alt={x.name} /> :
<div className="model-slot-tile-icon-no-entry">no entry.</div>
const clickAction = async () => {
const dummyModelSlotIndex = (Math.floor(Date.now() / 1000)) * 1000 + index
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, modelSlotIndex: dummyModelSlotIndex })
setTimeout(() => { // quick hack
getInfo()
}, 1000 * 2)
}
return (
<div key={index} className={tileContainerClass} onClick={clickAction}>
<div className="model-slot-tile-icon-div">
{iconElem}
</div>
<div className="model-slot-tile-dscription">
{name}
</div>
</div >
)
}).filter(x => x != null)
}, [serverSetting.serverSetting.modelSlots, serverSetting.serverSetting.modelSlotIndex])
const modelSlotArea = useMemo(() => {
const onModelSlotEditClicked = () => {
guiState.stateControls.showModelSlotManagerCheckbox.updateState(true)
}
return (
<div className="model-slot-area">
<div className="model-slot-panel">
<div className="model-slot-tiles-container">{modelTiles}</div>
<div className="model-slot-buttons">
<div className="model-slot-button" onClick={onModelSlotEditClicked}>
edit
</div>
</div>
</div>
</div>
)
}, [modelTiles])
return modelSlotArea
}

View File

@ -0,0 +1,289 @@
import React, { useEffect, useMemo, useState } from "react"
import { useAppState } from "../../../001_provider/001_AppStateProvider"
import { useGuiState } from "../001_GuiStateProvider"
import { OnnxExporterInfo } from "@dannadori/voice-changer-client-js"
export type CharacterAreaProps = {
}
export const CharacterArea = (_props: CharacterAreaProps) => {
const { serverSetting, clientSetting, initializedRef, volume, bufferingTime, performance } = useAppState()
const guiState = useGuiState()
const selected = useMemo(() => {
if (serverSetting.serverSetting.modelSlotIndex == undefined) {
return
}
return serverSetting.serverSetting.modelSlots[serverSetting.serverSetting.modelSlotIndex]
}, [serverSetting.serverSetting.modelSlotIndex, serverSetting.serverSetting.modelSlots])
useEffect(() => {
const vol = document.getElementById("status-vol") as HTMLSpanElement
const buf = document.getElementById("status-buf") as HTMLSpanElement
const res = document.getElementById("status-res") as HTMLSpanElement
if (!vol || !buf || !res) {
return
}
vol.innerText = volume.toFixed(4)
buf.innerText = bufferingTime.toString()
res.innerText = performance.responseTime.toString()
}, [volume, bufferingTime, performance])
const portrait = useMemo(() => {
if (!selected) {
return <></>
}
const icon = selected.iconFile.length > 0 ? selected.iconFile : "./assets/icons/human.png"
const selectedTermOfUseUrlLink = selected.termsOfUseUrl ? <a href={selected.termsOfUseUrl} target="_blank" rel="noopener noreferrer" className="portrait-area-terms-of-use-link">[terms of use]</a> : <></>
return (
<div className="portrait-area">
<div className="portrait-container">
<img className="portrait" src={icon} alt={selected.name} />
<div className="portrait-area-status">
<p>vol: <span id="status-vol">0</span></p>
<p>buf: <span id="status-buf">0</span> ms</p>
<p>res: <span id="status-res">0</span> ms</p>
</div>
<div className="portrait-area-terms-of-use">
{selectedTermOfUseUrlLink}
</div>
</div>
</div>
)
}, [selected])
const [startWithAudioContextCreate, setStartWithAudioContextCreate] = useState<boolean>(false)
useEffect(() => {
if (!startWithAudioContextCreate) {
return
}
guiState.setIsConverting(true)
clientSetting.start()
}, [startWithAudioContextCreate])
const startControl = useMemo(() => {
const onStartClicked = async () => {
if (serverSetting.serverSetting.enableServerAudio == 0) {
if (!initializedRef.current) {
while (true) {
await new Promise<void>((resolve) => {
setTimeout(resolve, 500)
})
if (initializedRef.current) {
break
}
}
setStartWithAudioContextCreate(true)
} else {
guiState.setIsConverting(true)
await clientSetting.start()
}
} else {
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, serverAudioStated: 1 })
guiState.setIsConverting(true)
}
}
const onStopClicked = async () => {
if (serverSetting.serverSetting.enableServerAudio == 0) {
guiState.setIsConverting(false)
await clientSetting.stop()
} else {
guiState.setIsConverting(false)
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, serverAudioStated: 0 })
}
}
const startClassName = guiState.isConverting ? "character-area-control-button-active" : "character-area-control-button-stanby"
const stopClassName = guiState.isConverting ? "character-area-control-button-stanby" : "character-area-control-button-active"
return (
<div className="character-area-control">
<div className="character-area-control-buttons">
<div onClick={onStartClicked} className={startClassName}>start</div>
<div onClick={onStopClicked} className={stopClassName}>stop</div>
</div>
</div>
)
}, [
guiState.isConverting,
clientSetting.start,
clientSetting.stop,
serverSetting.serverSetting,
serverSetting.updateServerSettings
])
const gainControl = useMemo(() => {
const currentInputGain = serverSetting.serverSetting.enableServerAudio == 0 ? clientSetting.clientSetting.inputGain : serverSetting.serverSetting.serverInputAudioGain
const inputValueUpdatedAction = serverSetting.serverSetting.enableServerAudio == 0 ?
async (val: number) => {
await clientSetting.updateClientSetting({ ...clientSetting.clientSetting, inputGain: val })
} :
async (val: number) => {
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, serverInputAudioGain: val })
}
const currentOutputGain = serverSetting.serverSetting.enableServerAudio == 0 ? clientSetting.clientSetting.outputGain : serverSetting.serverSetting.serverOutputAudioGain
const outputValueUpdatedAction = serverSetting.serverSetting.enableServerAudio == 0 ?
async (val: number) => {
await clientSetting.updateClientSetting({ ...clientSetting.clientSetting, outputGain: val })
} :
async (val: number) => {
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, serverOutputAudioGain: val })
}
return (
<div className="character-area-control">
<div className="character-area-control-title">
GAIN:
</div>
<div className="character-area-control-field">
<div className="character-area-slider-control">
<span className="character-area-slider-control-kind">in</span>
<span className="character-area-slider-control-slider">
<input type="range" min="0.1" max="10.0" step="0.1" value={currentInputGain} onChange={(e) => {
inputValueUpdatedAction(Number(e.target.value))
}}></input>
</span>
<span className="character-area-slider-control-val">{currentInputGain}</span>
</div>
<div className="character-area-slider-control">
<span className="character-area-slider-control-kind">out</span>
<span className="character-area-slider-control-slider">
<input type="range" min="0.1" max="10.0" step="0.1" value={currentOutputGain} onChange={(e) => {
outputValueUpdatedAction(Number(e.target.value))
}}></input>
</span>
<span className="character-area-slider-control-val">{currentOutputGain}</span>
</div>
</div>
</div>
)
}, [serverSetting.serverSetting, clientSetting.clientSetting, clientSetting.updateClientSetting, serverSetting.updateServerSettings])
const tuningCotrol = useMemo(() => {
const currentTuning = serverSetting.serverSetting.tran
const tranValueUpdatedAction = async (val: number) => {
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, tran: val })
}
return (
<div className="character-area-control">
<div className="character-area-control-title">
TUNE:
</div>
<div className="character-area-control-field">
<div className="character-area-slider-control">
<span className="character-area-slider-control-kind"></span>
<span className="character-area-slider-control-slider">
<input type="range" min="-50" max="50" step="1" value={currentTuning} onChange={(e) => {
tranValueUpdatedAction(Number(e.target.value))
}}></input>
</span>
<span className="character-area-slider-control-val">{currentTuning}</span>
</div>
</div>
</div>
)
}, [serverSetting.serverSetting, clientSetting.updateClientSetting])
const indexCotrol = useMemo(() => {
const currentIndexRatio = serverSetting.serverSetting.indexRatio
const indexRatioValueUpdatedAction = async (val: number) => {
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, indexRatio: val })
}
return (
<div className="character-area-control">
<div className="character-area-control-title">
INDEX:
</div>
<div className="character-area-control-field">
<div className="character-area-slider-control">
<span className="character-area-slider-control-kind"></span>
<span className="character-area-slider-control-slider">
<input type="range" min="0" max="1" step="0.1" value={currentIndexRatio} onChange={(e) => {
indexRatioValueUpdatedAction(Number(e.target.value))
}}></input>
</span>
<span className="character-area-slider-control-val">{currentIndexRatio}</span>
</div>
</div>
</div>
)
}, [serverSetting.serverSetting, clientSetting.updateClientSetting])
const modelSlotControl = useMemo(() => {
if (!selected) {
return <></>
}
const onUpdateDefaultClicked = async () => {
await serverSetting.updateModelDefault()
}
const onnxExportButtonAction = async () => {
if (guiState.isConverting) {
alert("cannot export onnx when voice conversion is enabled")
return
}
document.getElementById("dialog")?.classList.add("dialog-container-show")
guiState.stateControls.showWaitingCheckbox.updateState(true)
const res = await serverSetting.getOnnx() as OnnxExporterInfo
const a = document.createElement("a")
a.href = res.path
a.download = res.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
guiState.stateControls.showWaitingCheckbox.updateState(false)
}
const exportOnnx = selected.modelFile.endsWith("pth") ? (
<div className="character-area-buutton" onClick={onnxExportButtonAction}>export onnx</div>
) : <></>
return (
<div className="character-area-control">
<div className="character-area-control-title">
</div>
<div className="character-area-control-field">
<div className="character-area-buuttons">
<div className="character-area-buutton" onClick={onUpdateDefaultClicked}>save default</div>
{exportOnnx}
</div>
</div>
</div>
)
}, [selected, serverSetting.getOnnx, serverSetting.updateModelDefault])
const characterArea = useMemo(() => {
return (
<div className="character-area">
{portrait}
<div className="character-area-control-area">
{startControl}
{gainControl}
{tuningCotrol}
{indexCotrol}
{modelSlotControl}
</div>
</div>
)
}, [portrait, startControl, gainControl, tuningCotrol, modelSlotControl])
return characterArea
}

View File

@ -0,0 +1,77 @@
import React, { useMemo } from "react"
import { useAppState } from "../../../001_provider/001_AppStateProvider"
import { F0Detector, } from "@dannadori/voice-changer-client-js"
export type QualityAreaProps = {
detectors: string[]
}
export const QualityArea = (props: QualityAreaProps) => {
const { clientSetting, serverSetting } = useAppState()
const qualityArea = useMemo(() => {
if (!serverSetting.updateServerSettings || !clientSetting.updateClientSetting || !serverSetting.serverSetting || !clientSetting.clientSetting) {
return <></>
}
return (
<div className="config-sub-area">
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">NOISE:</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-noise-container">
<div className="config-sub-area-noise-checkbox-container">
<input type="checkbox" disabled={serverSetting.serverSetting.enableServerAudio != 0} checked={clientSetting.clientSetting.echoCancel} onChange={(e) => {
clientSetting.updateClientSetting({ ...clientSetting.clientSetting, echoCancel: e.target.checked })
}} /> <span>Echo</span>
</div>
<div className="config-sub-area-noise-checkbox-container">
<input type="checkbox" disabled={serverSetting.serverSetting.enableServerAudio != 0} checked={clientSetting.clientSetting.noiseSuppression} onChange={(e) => {
clientSetting.updateClientSetting({ ...clientSetting.clientSetting, noiseSuppression: e.target.checked })
}} /> <span>Sup1</span>
</div>
<div className="config-sub-area-noise-checkbox-container">
<input type="checkbox" disabled={serverSetting.serverSetting.enableServerAudio != 0} checked={clientSetting.clientSetting.noiseSuppression2} onChange={(e) => {
clientSetting.updateClientSetting({ ...clientSetting.clientSetting, noiseSuppression2: e.target.checked })
}} /> <span>Sup2</span>
</div>
</div>
</div>
</div>
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">F0 Det.:</div>
<div className="config-sub-area-control-field">
<select className="body-select" value={serverSetting.serverSetting.f0Detector} onChange={(e) => {
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, f0Detector: e.target.value as F0Detector })
}}>
{
Object.values(props.detectors).map(x => {
//@ts-ignore
return <option key={x} value={x}>{x}</option>
})
}
</select>
</div>
</div>
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">S.Thresh.:</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-slider-control">
<span className="config-sub-area-slider-control-kind"></span>
<span className="config-sub-area-slider-control-slider">
<input type="range" className="config-sub-area-slider-control-slider" min="0.00000" max="0.001" step="0.00001" value={serverSetting.serverSetting.silentThreshold || 0} onChange={(e) => {
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, silentThreshold: Number(e.target.value) })
}}></input>
</span>
<span className="config-sub-area-slider-control-val">{serverSetting.serverSetting.silentThreshold}</span>
</div>
</div>
</div>
</div>
)
}, [serverSetting.serverSetting, clientSetting.clientSetting, serverSetting.updateServerSettings, clientSetting.updateClientSetting])
return qualityArea
}

View File

@ -0,0 +1,81 @@
import React, { useMemo } from "react"
import { useAppState } from "../../../001_provider/001_AppStateProvider"
export type ConvertProps = {
inputChunkNums: number[]
}
export const ConvertArea = (props: ConvertProps) => {
const { clientSetting, serverSetting, workletNodeSetting } = useAppState()
const convertArea = useMemo(() => {
let nums: number[]
if (!props.inputChunkNums) {
nums = [8, 16, 24, 32, 40, 48, 64, 80, 96, 112, 128, 192, 256, 320, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024, 2048]
} else {
nums = props.inputChunkNums
}
const gpusEntry = [...serverSetting.serverSetting.gpus]
gpusEntry.push({
id: -1,
name: "cpu",
memory: 0
})
return (
<div className="config-sub-area">
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">CHUNK:</div>
<div className="config-sub-area-control-field">
<select className="body-select" value={workletNodeSetting.workletNodeSetting.inputChunkNum} onChange={(e) => {
workletNodeSetting.updateWorkletNodeSetting({ ...workletNodeSetting.workletNodeSetting, inputChunkNum: Number(e.target.value) })
workletNodeSetting.trancateBuffer()
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, serverReadChunkSize: Number(e.target.value) })
}}>
{
nums.map(x => {
return <option key={x} value={x}>{x} ({(x * 128 * 1000 / 48000).toFixed(1)} ms, {x * 128})</option>
})
}
</select>
</div>
</div>
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">EXTRA:</div>
<div className="config-sub-area-control-field">
<select className="body-select" value={serverSetting.serverSetting.extraConvertSize} onChange={(e) => {
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, extraConvertSize: Number(e.target.value) })
workletNodeSetting.trancateBuffer()
}}>
{
[1024 * 4, 1024 * 8, 1024 * 16, 1024 * 32, 1024 * 64, 1024 * 128].map(x => {
return <option key={x} value={x}>{x}</option>
})
}
</select>
</div>
</div>
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">GPU:</div>
<div className="config-sub-area-control-field">
<select className="body-select" value={serverSetting.serverSetting.gpu} onChange={(e) => {
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, gpu: Number(e.target.value) })
}}>
{
gpusEntry.map(x => {
return <option key={x.id} value={x.id}>{x.name}({(x.memory / 1024 / 1024 / 1024).toFixed(0)}GB) </option>
})
}
</select>
</div>
</div>
</div>
)
}, [serverSetting.serverSetting, clientSetting.clientSetting, workletNodeSetting.workletNodeSetting, serverSetting.updateServerSettings, clientSetting.updateClientSetting, workletNodeSetting.updateWorkletNodeSetting])
return convertArea
}

View File

@ -0,0 +1,416 @@
import React, { useEffect, useMemo, useRef, useState } from "react"
import { useAppState } from "../../../001_provider/001_AppStateProvider"
import { fileSelectorAsDataURL, useIndexedDB, } from "@dannadori/voice-changer-client-js"
import { useGuiState } from "../001_GuiStateProvider"
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 { useAppRoot } from "../../../001_provider/001_AppRootProvider"
export type DeviceAreaProps = {
}
export const DeviceArea = (_props: DeviceAreaProps) => {
const { clientSetting, serverSetting, workletNodeSetting, audioContext, setAudioOutputElementId, initializedRef } = useAppState()
const { isConverting, audioInputForGUI, inputAudioDeviceInfo, setAudioInputForGUI, fileInputEchoback, setFileInputEchoback, setAudioOutputForGUI, audioOutputForGUI, outputAudioDeviceInfo } = useGuiState()
const [hostApi, setHostApi] = useState<string>("ALL")
const audioSrcNode = useRef<MediaElementAudioSourceNode>()
const { appGuiSettingState } = useAppRoot()
const clientType = appGuiSettingState.appGuiSetting.id
const { getItem, setItem } = useIndexedDB({ clientType: clientType })
const [outputRecordingStarted, setOutputRecordingStarted] = useState<boolean>(false)
// (1) Audio Mode
const deviceModeRow = useMemo(() => {
const enableServerAudio = serverSetting.serverSetting.enableServerAudio
const clientChecked = enableServerAudio == 1 ? false : true
const serverChecked = enableServerAudio == 1 ? true : false
const onDeviceModeChanged = (val: number) => {
if (isConverting) {
alert("cannot change mode when voice conversion is enabled")
return
}
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, enableServerAudio: val })
}
return (
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">AUDIO:</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-noise-container">
<div className="config-sub-area-noise-checkbox-container">
<input type="radio" id="client-device" name="device-mode" checked={clientChecked} onChange={() => { onDeviceModeChanged(0) }} /> <label htmlFor="client-device">client</label>
</div>
<div className="config-sub-area-noise-checkbox-container">
<input className="left-padding-1" type="radio" id="server-device" name="device-mode" checked={serverChecked} onChange={() => { onDeviceModeChanged(1) }} />
<label htmlFor="server-device">server</label>
</div>
</div>
</div>
</div>
)
}, [serverSetting.serverSetting, serverSetting.updateServerSettings])
// (2) Audio Input
// キャッシュの設定は反映(たぶん、設定操作の時も起動していしまう。が問題は起こらないはず)
useEffect(() => {
if (typeof clientSetting.clientSetting.audioInput == "string") {
if (inputAudioDeviceInfo.find(x => {
// console.log("COMPARE:", x.deviceId, appState.clientSetting.setting.audioInput)
return x.deviceId == clientSetting.clientSetting.audioInput
})) {
setAudioInputForGUI(clientSetting.clientSetting.audioInput)
}
}
}, [inputAudioDeviceInfo, clientSetting.clientSetting.audioInput])
// (2-1) クライアント
const clientAudioInputRow = useMemo(() => {
if (serverSetting.serverSetting.enableServerAudio == 1) {
return <></>
}
return (
<div className="config-sub-area-control">
<div className="config-sub-area-control-title left-padding-1">input</div>
<div className="config-sub-area-control-field">
<select className="body-select" value={audioInputForGUI} onChange={(e) => {
setAudioInputForGUI(e.target.value)
if (audioInputForGUI != "file") {
clientSetting.updateClientSetting({ ...clientSetting.clientSetting, audioInput: e.target.value })
}
}}>
{
inputAudioDeviceInfo.map(x => {
return <option key={x.deviceId} value={x.deviceId}>{x.label}</option>
})
}
</select>
</div>
</div>
)
}, [clientSetting.updateClientSetting, clientSetting.clientSetting, inputAudioDeviceInfo, audioInputForGUI, serverSetting.serverSetting.enableServerAudio])
// (2-2) サーバ
const serverAudioInputRow = useMemo(() => {
if (serverSetting.serverSetting.enableServerAudio == 0) {
return <></>
}
const devices = serverSetting.serverSetting.serverAudioInputDevices
const hostAPIs = new Set(devices.map(x => { return x.hostAPI }))
const hostAPIOptions = Array.from(hostAPIs).map((x, index) => { return <option value={x} key={index} >{x}</option> })
const filteredDevice = devices.map((x, index) => {
if (hostApi != "ALL" && x.hostAPI != hostApi) {
return null
}
return <option value={x.index} key={index}>[{x.hostAPI}]{x.name}</option>
}).filter(x => x != null)
return (
<div className="config-sub-area-control">
<div className="config-sub-area-control-title left-padding-1">input</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-control-field-auido-io">
<select className="config-sub-area-control-field-auido-io-filter" name="kinds" id="kinds" value={hostApi} onChange={(e) => { setHostApi(e.target.value) }}>
{hostAPIOptions}
</select>
<select className="config-sub-area-control-field-auido-io-select" value={serverSetting.serverSetting.serverInputDeviceId} onChange={(e) => {
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, serverInputDeviceId: Number(e.target.value) })
}}>
{filteredDevice}
</select>
</div>
</div>
</div>
)
}, [clientSetting.updateClientSetting, clientSetting.clientSetting, inputAudioDeviceInfo, audioInputForGUI, serverSetting.serverSetting.enableServerAudio])
// (2-3) File
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 audioInputMediaRow = useMemo(() => {
if (audioInputForGUI != "file" || serverSetting.serverSetting.enableServerAudio == 1) {
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 = audioContext!.createMediaElementSource(audio);
}
if (audioSrcNode.current.mediaElement != audio) {
audioSrcNode.current = audioContext!.createMediaElementSource(audio);
}
const dst = audioContext.createMediaStreamDestination()
audioSrcNode.current.connect(dst)
clientSetting.updateClientSetting({ ...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()
}
const echobackClass = fileInputEchoback ? "config-sub-area-control-field-wav-file-echoback-button-active" : "config-sub-area-control-field-wav-file-echoback-button"
return (
<div className="config-sub-area-control">
<div className="config-sub-area-control-field">
<div className="config-sub-area-control-field-wav-file left-padding-1">
<div className="config-sub-area-control-field-wav-file-audio-container">
<audio id={AUDIO_ELEMENT_FOR_TEST_ORIGINAL} controls hidden></audio>
<audio className="config-sub-area-control-field-wav-file-audio" id={AUDIO_ELEMENT_FOR_TEST_CONVERTED} controls controlsList="nodownload noplaybackrate"></audio>
<audio id={AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK} controls hidden></audio>
</div>
<div>
<img className="config-sub-area-control-field-wav-file-folder" src="./assets/icons/folder.svg" onClick={onFileLoadClicked} />
</div>
<div className={echobackClass} onClick={() => { setFileInputEchoback(!fileInputEchoback) }}>
echo{fileInputEchoback}
</div>
</div>
</div>
</div>
)
}, [audioInputForGUI, fileInputEchoback, serverSetting.serverSetting])
// (3) Audio Output
useEffect(() => {
const loadCache = async () => {
const key = await getItem(INDEXEDDB_KEY_AUDIO_OUTPUT)
if (key) {
setAudioOutputForGUI(key as string)
}
}
loadCache()
}, [])
useEffect(() => {
const setAudioOutput = async () => {
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
[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 (serverSetting.serverSetting.enableServerAudio == 1) {
// Server Audio を使う場合はElementから音は出さない。
audio.volume = 0
} else if (audioOutputForGUI == "none") {
// @ts-ignore
audio.setSinkId("")
if (x == AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK) {
audio.volume = 0
} else {
audio.volume = 0
}
} else {
const audioOutputs = mediaDeviceInfos.filter(x => { return x.kind == "audiooutput" })
const found = audioOutputs.some(x => { return x.deviceId == audioOutputForGUI })
if (found) {
// @ts-ignore // 例外キャッチできないので事前にIDチェックが必要らしい。
audio.setSinkId(audioOutputForGUI)
} else {
console.warn("No audio output device. use default")
}
if (x == AUDIO_ELEMENT_FOR_TEST_CONVERTED_ECHOBACK) {
audio.volume = fileInputEchoback ? 1 : 0
} else {
audio.volume = 1
}
}
}
})
}
setAudioOutput()
}, [audioOutputForGUI, fileInputEchoback, serverSetting.serverSetting.enableServerAudio])
// (3-1) クライアント
const clientAudioOutputRow = useMemo(() => {
if (serverSetting.serverSetting.enableServerAudio == 1) {
return <></>
}
return (
<div className="config-sub-area-control">
<div className="config-sub-area-control-title left-padding-1">output</div>
<div className="config-sub-area-control-field">
<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>
</div>
</div>
)
}, [serverSetting.serverSetting.enableServerAudio, outputAudioDeviceInfo, audioOutputForGUI])
useEffect(() => {
console.log("initializedRef.current", initializedRef.current)
setAudioOutputElementId(AUDIO_ELEMENT_FOR_PLAY_RESULT)
}, [initializedRef.current])
// (3-2) サーバ
const serverAudioOutputRow = useMemo(() => {
if (serverSetting.serverSetting.enableServerAudio == 0) {
return <></>
}
const devices = serverSetting.serverSetting.serverAudioOutputDevices
const hostAPIs = new Set(devices.map(x => { return x.hostAPI }))
const hostAPIOptions = Array.from(hostAPIs).map((x, index) => { return <option value={x} key={index} >{x}</option> })
// const filteredDevice = devices.filter(x => { return x.hostAPI == hostApi || hostApi == "" }).map((x, index) => { return <option value={x.index} key={index}>{x.name}</option> })
const filteredDevice = devices.map((x, index) => {
const className = (x.hostAPI == hostApi || hostApi == "") ? "select-option-red" : ""
return <option className={className} value={x.index} key={index}>[{x.hostAPI}]{x.name}</option>
})
return (
<div className="config-sub-area-control">
<div className="config-sub-area-control-title left-padding-1">output</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-control-field-auido-io">
<select className="config-sub-area-control-field-auido-io-filter" name="kinds" id="kinds" value={hostApi} onChange={(e) => { setHostApi(e.target.value) }}>
{hostAPIOptions}
</select>
<select className="config-sub-area-control-field-auido-io-select" value={serverSetting.serverSetting.serverOutputDeviceId} onChange={(e) => {
serverSetting.updateServerSettings({ ...serverSetting.serverSetting, serverOutputDeviceId: Number(e.target.value) })
}}>
{filteredDevice}
</select>
</div>
</div>
</div>
)
}, [hostApi, serverSetting.serverSetting, serverSetting.updateServerSettings])
// (4) レコーダー
const outputRecorderRow = useMemo(() => {
const onOutputRecordStartClicked = async () => {
setOutputRecordingStarted(true)
await workletNodeSetting.startOutputRecording()
}
const onOutputRecordStopClicked = async () => {
setOutputRecordingStarted(false)
const record = await workletNodeSetting.stopOutputRecording()
downloadRecord(record)
}
const startClassName = outputRecordingStarted ? "config-sub-area-buutton-active" : "config-sub-area-buutton"
const stopClassName = outputRecordingStarted ? "config-sub-area-buutton" : "config-sub-area-buutton-active"
return (
<div className="config-sub-area-control">
<div className="config-sub-area-control-title">REC.</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-buuttons">
<div onClick={onOutputRecordStartClicked} className={startClassName}>start</div>
<div onClick={onOutputRecordStopClicked} className={stopClassName}>stop</div>
</div>
</div>
</div>
)
}, [outputRecordingStarted, workletNodeSetting])
return (
<div className="config-sub-area">
{deviceModeRow}
{clientAudioInputRow}
{serverAudioInputRow}
{audioInputMediaRow}
{clientAudioOutputRow}
{serverAudioOutputRow}
{outputRecorderRow}
<audio hidden id={AUDIO_ELEMENT_FOR_PLAY_RESULT}></audio>
</div>
)
}
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);
}

View File

@ -0,0 +1,129 @@
import React, { useMemo, useState } from "react"
import { useAppState } from "../../../001_provider/001_AppStateProvider"
import { useGuiState } from "../001_GuiStateProvider"
import { AUDIO_ELEMENT_FOR_SAMPLING_INPUT, AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT } from "../../../const"
export type RecorderAreaProps = {
}
export const RecorderArea = (_props: RecorderAreaProps) => {
const { serverSetting, workletNodeSetting } = useAppState()
const { audioOutputForAnalyzer, setAudioOutputForAnalyzer, outputAudioDeviceInfo } = useGuiState()
const [serverIORecording, setServerIORecording] = useState<boolean>(false)
// const recorderRow = useMemo(() => {
// return (
// <div className="config-sub-area-control">
// <div className="config-sub-area-control-title">RECORD:</div>
// <div className="config-sub-area-control-field">
// </div>
// </div>
// )
// }, [serverSetting.serverSetting, serverSetting.updateServerSettings])
const serverIORecorderRow = useMemo(() => {
const onServerIORecordStartClicked = async () => {
setServerIORecording(true)
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, recordIO: 1 })
}
const onServerIORecordStopClicked = async () => {
setServerIORecording(false)
await serverSetting.updateServerSettings({ ...serverSetting.serverSetting, recordIO: 0 })
// set wav (input)
const wavInput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_INPUT) as HTMLAudioElement
wavInput.src = "/tmp/in.wav?" + new Date().getTime()
wavInput.controls = true
// @ts-ignore
wavInput.setSinkId(audioOutputForAnalyzer)
// set wav (output)
const wavOutput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT) as HTMLAudioElement
wavOutput.src = "/tmp/out.wav?" + new Date().getTime()
wavOutput.controls = true
// @ts-ignore
wavOutput.setSinkId(audioOutputForAnalyzer)
}
const startClassName = serverIORecording ? "config-sub-area-buutton-active" : "config-sub-area-buutton"
const stopClassName = serverIORecording ? "config-sub-area-buutton" : "config-sub-area-buutton-active"
return (
<>
<div className="config-sub-area-control">
<div className="config-sub-area-control-title-long">ServerIO Analyzer</div>
</div>
<div className="config-sub-area-control left-padding-1">
<div className="config-sub-area-control-title">SIO rec.</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-buuttons">
<div onClick={onServerIORecordStartClicked} className={startClassName}>start</div>
<div onClick={onServerIORecordStopClicked} className={stopClassName}>stop</div>
</div>
</div>
</div>
<div className="config-sub-area-control left-padding-1">
<div className="config-sub-area-control-title">dev</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-control-field-auido-io">
<select className="body-select" value={audioOutputForAnalyzer} onChange={(e) => {
setAudioOutputForAnalyzer(e.target.value)
const wavInput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_INPUT) as HTMLAudioElement
const wavOutput = document.getElementById(AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT) as HTMLAudioElement
//@ts-ignore
wavInput.setSinkId(e.target.value)
//@ts-ignore
wavOutput.setSinkId(e.target.value)
}}>
{
outputAudioDeviceInfo.map(x => {
if (x.deviceId == "none") {
return null
}
return <option key={x.deviceId} value={x.deviceId}>{x.label}</option>
}).filter(x => { return x != null })
}
</select>
</div>
</div>
</div>
<div className="config-sub-area-control left-padding-1">
<div className="config-sub-area-control-title">in</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-control-field-wav-file">
<div className="config-sub-area-control-field-wav-file-audio-container">
<audio className="config-sub-area-control-field-wav-file-audio" id={AUDIO_ELEMENT_FOR_SAMPLING_INPUT} controls></audio>
</div>
</div>
</div>
</div>
<div className="config-sub-area-control left-padding-1">
<div className="config-sub-area-control-title">out</div>
<div className="config-sub-area-control-field">
<div className="config-sub-area-control-field-wav-file">
<div className="config-sub-area-control-field-wav-file-audio-container">
<audio className="config-sub-area-control-field-wav-file-audio" id={AUDIO_ELEMENT_FOR_SAMPLING_OUTPUT} controls></audio>
</div>
</div>
</div>
</div>
</>
)
}, [serverIORecording, workletNodeSetting])
return (
<div className="config-sub-area">
{serverIORecorderRow}
</div>
)
}

View File

@ -0,0 +1,31 @@
import React, { useMemo } from "react"
import { QualityArea } from "./102-1_QualityArea"
import { ConvertArea } from "./102-2_ConvertArea"
import { DeviceArea } from "./102-3_DeviceArea"
import { RecorderArea } from "./102-4_RecorderArea"
export type ConfigAreaProps = {
detectors: string[]
inputChunkNums: number[]
}
export const ConfigArea = (props: ConfigAreaProps) => {
const configArea = useMemo(() => {
return (
<>
<div className="config-area">
<QualityArea detectors={props.detectors}></QualityArea>
<ConvertArea inputChunkNums={props.inputChunkNums}></ConvertArea>
</div>
<div className="config-area">
<DeviceArea></DeviceArea>
<RecorderArea></RecorderArea>
</div>
</>
)
}, [])
return configArea
}

View File

@ -960,3 +960,341 @@ body {
}
}
}
.model-slot-area {
display: inline-block;
background: var(--company-color2);
border-radius: 10px;
padding: 20px;
.model-slot-panel {
display: flex;
flex-direction: row;
gap: 5px;
.model-slot-tiles-container {
display: flex;
flex-direction: row;
gap: 2px;
flex-wrap: wrap;
/* width: calc(30rem + 40px + 10px); */
}
.model-slot-buttons {
display: flex;
flex-direction: column-reverse;
.model-slot-button {
border: solid 2px #999;
color: white;
font-size: 0.8rem;
border-radius: 2px;
background: #333;
cursor: pointer;
padding: 5px;
}
.model-slot-button:hover {
border: solid 2px #faa;
}
}
}
}
.model-slot-tile-container,
.model-slot-tile-container-selected {
width: 6rem;
height: 6rem;
border-radius: 2px;
display: flex;
flex-direction: column;
align-items: center;
}
.model-slot-tile-container-selected {
background: #43030c;
}
.model-slot-tile-container:hover {
background: #43030c;
}
.model-slot-tile-icon-div {
width: 5rem;
height: 5rem;
padding-top: 4px;
}
.model-slot-tile-icon {
width: 5rem;
height: 5rem;
object-fit: contain;
border-radius: 10px;
}
.model-slot-tile-icon-no-entry {
color: gray;
}
.model-slot-tile-dscription {
font-size: 0.7rem;
font-weight: 700;
color: navajowhite;
padding-top: 4px;
}
.character-area {
display: flex;
gap: 5px;
padding: 20px;
.portrait-area {
width: 20rem;
height: 20rem;
.portrait-container {
position: relative;
width: 20rem;
height: 20rem;
.portrait {
width: 20rem;
height: 20rem;
object-fit: contain;
border-radius: 10px;
position: absolute;
}
.portrait-area-status {
width: 5rem;
background: rgba(100, 100, 100, 0.5);
color: white;
position: absolute;
paddig: 2px;
font-size: 0.7rem;
left: 5px;
top: 5px;
border-radius: 2px;
}
.portrait-area-terms-of-use {
width: 5rem;
background: rgba(100, 100, 100, 0.5);
color: white;
position: absolute;
paddig: 2px;
font-size: 0.7rem;
right: 5px;
bottom: 5px;
.portrait-area-terms-of-use-link {
color: white;
}
}
}
}
.character-area-control-area {
display: flex;
flex-direction: column;
gap: 10px;
.character-area-control {
display: flex;
gap: 3px;
.character-area-control-buttons {
display: flex;
flex-direction: row;
gap: 10px;
.character-area-control-button-active {
width: 5rem;
border: solid 1px #333;
border-radius: 2px;
background: #ada;
font-weight: 700;
text-align: center;
}
.character-area-control-button-stanby {
width: 5rem;
border: solid 1px #999;
border-radius: 2px;
background: #aba;
cursor: pointer;
font-weight: 700;
text-align: center;
&:hover {
border: solid 1px #000;
}
}
}
.character-area-control-title {
width: 3rem;
font-weight: 700;
}
.character-area-control-field {
/* width: 20rem; */
display: flex;
flex-direction: column;
.character-area-slider-control {
display: flex;
flex-direction: row;
.character-area-slider-control-kind {
width: 2rem;
}
.character-area-slider-control-slider {
width: 10rem;
}
.character-area-slider-control-val {
width: 3rem;
}
}
.character-area-buuttons {
display: flex;
flex-direction: row;
gap: 5px;
.character-area-buutton {
border: solid 2px #999;
color: white;
font-size: 0.8rem;
border-radius: 2px;
background: #666;
cursor: pointer;
padding: 5px;
}
.character-area-buutton:hover {
border: solid 2px #faa;
}
}
}
}
}
}
/* audio::-webkit-media-controls-play-button,
audio::-webkit-media-controls-panel {
background-color: #ff0;
height: 1rem;
}
audio::-webkit-media-controls-enclosure {
max-height: 1rem;
}
audio::-webkit-media-controls {
justify-content: start;
}
audio::-webkit-media-controls-overlay-enclosure{
height: 1rem;
} */
.config-area {
display: flex;
gap: 5px;
padding: 20px;
.config-sub-area {
display: flex;
flex-direction: column;
gap: 3px;
.config-sub-area-control {
display: flex;
.config-sub-area-control-title {
width: 5rem;
font-weight: 700;
}
.config-sub-area-control-title-long {
width: 20rem;
font-weight: 700;
}
.config-sub-area-control-field {
width: 15rem;
display: flex;
flex-direction: column;
justify-content: center;
.config-sub-area-noise-container {
display: flex;
gap: 10px;
.config-sub-area-noise-checkbox-container {
display: flex;
gap: 5px;
}
}
.config-sub-area-slider-control {
display: flex;
flex-direction: row;
.config-sub-area-slider-control-kind {
width: 1rem;
}
.config-sub-area-slider-control-slider {
width: 10rem;
}
.config-sub-area-slider-control-val {
width: 3rem;
}
}
.config-sub-area-buuttons {
display: flex;
flex-direction: row;
gap: 5px;
align-items: center;
.config-sub-area-buutton {
border: solid 2px #999;
color: white;
background: #666;
cursor: pointer;
font-size: 0.8rem;
border-radius: 5px;
height: 1.2rem;
padding-left: 2px;
padding-right: 2px;
}
.config-sub-area-buutton:hover {
border: solid 2px #faa;
}
.config-sub-area-buutton-active {
border: solid 2px #999;
color: white;
background: #844;
cursor: pointer;
font-size: 0.8rem;
border-radius: 5px;
height: 1.2rem;
padding-left: 2px;
padding-right: 2px;
}
}
.config-sub-area-control-field-auido-io {
display: flex;
flex-direction: row;
.config-sub-area-control-field-auido-io-filter {
max-width: 30%;
}
.config-sub-area-control-field-auido-io-select {
max-width: 70%;
}
}
.config-sub-area-control-field-wav-file {
display: flex;
flex-direction: row;
gap: 5px;
.config-sub-area-control-field-wav-file-audio-container {
height: 1rem;
.config-sub-area-control-field-wav-file-audio {
height: 1rem;
width: 15rem;
}
}
.config-sub-area-control-field-wav-file-folder {
height: 1rem;
width: 1rem;
cursor: pointer;
}
.config-sub-area-control-field-wav-file-echoback-button {
border: solid 1px #333;
background: #fff;
font-size: 0.8rem;
border-radius: 5px;
height: 1.2rem;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}
.config-sub-area-control-field-wav-file-echoback-button-active {
font-size: 0.8rem;
border: solid 1px #333;
border-radius: 5px;
background: #ada;
height: 1.2rem;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}
}
}
}
}
}

View File

@ -237,6 +237,11 @@ export type ServerInfo = VoiceChangerServerSetting & {
serverAudioInputDevices: ServerAudioDevice[]
serverAudioOutputDevices: ServerAudioDevice[]
sampleModels: RVCSampleModel[]
gpus: {
id: number,
name: string,
memory: number,
}[]
}
@ -308,6 +313,7 @@ export const DefaultServerSetting: ServerInfo = {
silenceFront: 1,
modelSlotIndex: 0,
sampleModels: [],
gpus: [],
useEnhancer: 0,
useDiff: 1,

View File

@ -159,8 +159,10 @@ export const useClient = (props: UseClientProps): ClientState => {
return
}
const audio = document.getElementById(elemId) as HTMLAudioElement
audio.srcObject = voiceChangerClientRef.current.stream
audio.play()
if (audio.paused) {
audio.srcObject = voiceChangerClientRef.current.stream
audio.play()
}
}
// (2-2) 情報リロード

View File

@ -76,6 +76,7 @@ class MMVC_Rest_Fileuploader:
self, key: str = Form(...), val: Union[int, str, float] = Form(...)
):
# print("[Voice Changer] update configuration:", key, val)
print("post_update_settings", key, type(val))
info = self.voiceChangerManager.update_settings(key, val)
json_compatible_item_data = jsonable_encoder(info)
return JSONResponse(content=json_compatible_item_data)

View File

@ -38,7 +38,7 @@
"modelUrl": "https://huggingface.co/wok000/vcclient_model/resolve/main/rvc_v2_alpha/tokina_shigure/tokina_shigure_v2_40k_e100_simple.onnx",
"indexUrl": "https://huggingface.co/wok000/vcclient_model/resolve/main/rvc_v2_alpha/tokina_shigure/added_IVF2736_Flat_nprobe_1_v2.index.bin",
"termsOfUseUrl": "https://huggingface.co/wok000/vcclient_model/raw/main/rvc_v2_alpha/tokina_shigure/terms_of_use.txt",
"icon": "https://huggingface.co/wok000/vcclient_model/resolve/main/rvc_v2_alpha/tokina_shigure/dummy.png",
"icon": "https://huggingface.co/wok000/vcclient_model/resolve/main/rvc_v2_alpha/tokina_shigure/tokina_shigure.png",
"credit": "刻鳴時雨",
"description": "",
"sampleRate": 40000,

View File

@ -28,6 +28,10 @@ class MMVC_SocketIOApp:
"filename": f"{getFrontendPath()}/assets/icons/tool.svg",
"content_type": "image/svg+xml",
},
"/assets/icons/folder.svg": {
"filename": f"{getFrontendPath()}/assets/icons/folder.svg",
"content_type": "image/svg+xml",
},
"/buymeacoffee.png": {
"filename": f"{getFrontendPath()}/assets/buymeacoffee.png",
"content_type": "image/png",

View File

@ -197,9 +197,13 @@ class RVC:
self.settings.modelSlots = modelSlots
def update_settings(self, key: str, val: int | float | str):
print("update", key, val)
if key in self.settings.intData:
# 設定前処理
print("update(int)1", type(val), type(self.settings.tran))
val = cast(int, val)
print("update(int)2", type(val), type(self.settings.tran))
print("update(int)3", key, val)
if key == "modelSlotIndex":
if val < 0:
return True
@ -213,7 +217,9 @@ class RVC:
self.prepareModel(val)
# 設定
print("update(int)4", key, val)
setattr(self.settings, key, val)
print("update(int)5", type(val), type(self.settings.tran))
if key == "gpu":
self.deviceManager.setForceTensor(False)
@ -222,6 +228,7 @@ class RVC:
elif key in self.settings.floatData:
setattr(self.settings, key, float(val))
elif key in self.settings.strData:
print("update(str)", key, val)
setattr(self.settings, key, str(val))
if key == "f0Detector" and self.pipeline is not None:
pitchExtractor = PitchExtractorManager.getPitchExtractor(
@ -513,7 +520,7 @@ class RVC:
try:
shutil.move(uploadPath, storePath)
params = json.load(open(storeJson, "r", encoding="utf-8"))
params[paramsDict["name"]] = storePath
params[paramsDict["name"]] = storePath # type:ignore
json.dump(params, open(storeJson, "w"))
except Exception as e:
print("Exception::::", e)

View File

@ -341,6 +341,15 @@ class VoiceChanger:
data = asdict(self.settings)
if self.voiceChanger is not None:
data.update(self.voiceChanger.get_info())
devCount = torch.cuda.device_count()
gpus = []
for id in range(devCount):
name = torch.cuda.get_device_name(id)
memory = torch.cuda.get_device_properties(id).total_memory
gpu = {"id": id, "name": name, "memory": memory}
gpus.append(gpu)
data["gpus"] = gpus
return data
def get_performance(self):