feat: refactor and optimize frontend with Next.js App Router, TypeScript, and Tailwind CSS

- Complete refactoring of old frontend into Next.js App Router workspace
- Redesigned sidebar collapsing animation with absolute toggle positioning
- Resolved visual canvas bleed transitions between light/dark themes
- Added custom dark theme variant for toggle switch buttons
- Implemented full localization across Indonesian, English, Spanish, Japanese, and Chinese
- Synchronized HTML document themes to apply dark mode styles to portals/overlays
This commit is contained in:
akukanara
2026-05-31 16:46:57 +07:00
Unverified
parent 19e5138525
commit 9d876de930
81 changed files with 10554 additions and 1583 deletions
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
1:"$Sreact.fragment"
2:I[47257,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ClientPageRoot"]
3:I[52683,["/_next/static/chunks/0dbhjjzl8qfwv.js","/_next/static/chunks/01yjdu1pd4sew.js"],"default"]
6:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"OutletBoundary"]
7:"$Sreact.suspense"
0:{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/_next/static/chunks/01yjdu1pd4sew.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
4:{}
5:"$0:rsc:props:children:0:props:serverProvidedParams:params"
8:null
+20
View File
@@ -0,0 +1,20 @@
1:"$Sreact.fragment"
2:I[39756,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
3:I[37457,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
4:I[47257,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ClientPageRoot"]
5:I[52683,["/_next/static/chunks/0dbhjjzl8qfwv.js","/_next/static/chunks/01yjdu1pd4sew.js"],"default"]
8:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"OutletBoundary"]
9:"$Sreact.suspense"
b:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ViewportBoundary"]
d:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"MetadataBoundary"]
f:I[68027,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default",1]
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
:HL["/_next/static/media/797e433ab948586e-s.p.09zddjkbdep5a.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
:HL["/_next/static/media/caa3a2e1cccd8315-s.p.09~u27dqhyhd6.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
0:{"P":null,"c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/0dbhjjzl8qfwv.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased","children":["$","body",null,{"className":"min-h-full flex flex-col","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@6","$@7"]}}],[["$","script","script-0",{"src":"/_next/static/chunks/01yjdu1pd4sew.js","async":true,"nonce":"$undefined"}]],["$","$L8",null,{"children":["$","$9",null,{"name":"Next.MetadataOutlet","children":"$@a"}]}]]}],{},null,false,null]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$9",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"ncF6Dp_0OCMhTp1KUVnS4"}
6:{}
7:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
c:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
10:I[27201,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"IconMark"]
a:null
e:[["$","title","0",{"children":"🎙️ ONNX VC - Real-Time AI Voice Changer"}],["$","meta","1",{"name":"description","content":"ONNX VC - Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan ONNX Runtime."}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0x3dzn~oxb6tn.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L10","3",{}]]
+6
View File
@@ -0,0 +1,6 @@
1:"$Sreact.fragment"
2:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ViewportBoundary"]
3:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"MetadataBoundary"]
4:"$Sreact.suspense"
5:I[27201,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"IconMark"]
0:{"rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"🎙️ ONNX VC - Real-Time AI Voice Changer"}],["$","meta","1",{"name":"description","content":"ONNX VC - Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan ONNX Runtime."}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0x3dzn~oxb6tn.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L5","3",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
+5
View File
@@ -0,0 +1,5 @@
1:"$Sreact.fragment"
2:I[39756,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
3:I[37457,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
0:{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next"}],["$","script","script-0",{"src":"/_next/static/chunks/0dbhjjzl8qfwv.js","async":true}]],["$","html",null,{"lang":"en","className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased","children":["$","body",null,{"className":"min-h-full flex flex-col","children":["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
+4
View File
@@ -0,0 +1,4 @@
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
:HL["/_next/static/media/797e433ab948586e-s.p.09zddjkbdep5a.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
:HL["/_next/static/media/caa3a2e1cccd8315-s.p.09~u27dqhyhd6.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
0:{"tree":{"name":"","param":null,"prefetchHints":16,"slots":{"children":{"name":"__PAGE__","param":null,"prefetchHints":0,"slots":null}}},"staleTime":300,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,11 @@
self.__BUILD_MANIFEST = {
"__rewrites": {
"afterFiles": [],
"beforeFiles": [],
"fallback": []
},
"sortedPages": [
"/_app",
"/_error"
]
};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()
@@ -0,0 +1 @@
self.__MIDDLEWARE_MATCHERS = [];self.__MIDDLEWARE_MATCHERS_CB && self.__MIDDLEWARE_MATCHERS_CB()
@@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
File diff suppressed because one or more lines are too long
+16
View File
@@ -0,0 +1,16 @@
1:"$Sreact.fragment"
2:I[39756,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
3:I[37457,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
4:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"OutletBoundary"]
5:"$Sreact.suspense"
8:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ViewportBoundary"]
a:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"MetadataBoundary"]
c:I[68027,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default",1]
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
0:{"P":null,"c":["","_not-found"],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/0dbhjjzl8qfwv.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased","children":["$","body",null,{"className":"min-h-full flex flex-col","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L4",null,{"children":["$","$5",null,{"name":"Next.MetadataOutlet","children":"$@6"}]}]]}],{},null,false,null]},null,false,"$@7"]},null,false,null],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$c",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"ncF6Dp_0OCMhTp1KUVnS4"}
d:[]
7:"$Wd"
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
e:I[27201,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"IconMark"]
6:null
b:[["$","title","0",{"children":"🎙️ ONNX VC - Real-Time AI Voice Changer"}],["$","meta","1",{"name":"description","content":"ONNX VC - Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan ONNX Runtime."}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0x3dzn~oxb6tn.ico","sizes":"256x256","type":"image/x-icon"}],["$","$Le","3",{}]]
+16
View File
@@ -0,0 +1,16 @@
1:"$Sreact.fragment"
2:I[39756,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
3:I[37457,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
4:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"OutletBoundary"]
5:"$Sreact.suspense"
8:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ViewportBoundary"]
a:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"MetadataBoundary"]
c:I[68027,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default",1]
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
0:{"P":null,"c":["","_not-found"],"q":"","i":false,"f":[[["",{"children":["/_not-found",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/0dbhjjzl8qfwv.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased","children":["$","body",null,{"className":"min-h-full flex flex-col","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","forbidden":"$undefined","unauthorized":"$undefined"}]]}],{"children":[["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:style","children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:1:props:style","children":404}],["$","div",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:style","children":["$","h2",null,{"style":"$0:f:0:1:0:props:children:1:props:children:props:children:props:notFound:0:1:props:children:props:children:2:props:children:props:style","children":"This page could not be found."}]}]]}]}]],null,["$","$L4",null,{"children":["$","$5",null,{"name":"Next.MetadataOutlet","children":"$@6"}]}]]}],{},null,false,null]},null,false,"$@7"]},null,false,null],["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L8",null,{"children":"$L9"}],["$","div",null,{"hidden":true,"children":["$","$La",null,{"children":["$","$5",null,{"name":"Next.Metadata","children":"$Lb"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$c",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"ncF6Dp_0OCMhTp1KUVnS4"}
d:[]
7:"$Wd"
9:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
e:I[27201,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"IconMark"]
6:null
b:[["$","title","0",{"children":"🎙️ ONNX VC - Real-Time AI Voice Changer"}],["$","meta","1",{"name":"description","content":"ONNX VC - Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan ONNX Runtime."}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0x3dzn~oxb6tn.ico","sizes":"256x256","type":"image/x-icon"}],["$","$Le","3",{}]]
+6
View File
@@ -0,0 +1,6 @@
1:"$Sreact.fragment"
2:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ViewportBoundary"]
3:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"MetadataBoundary"]
4:"$Sreact.suspense"
5:I[27201,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"IconMark"]
0:{"rsc":["$","$1","h",{"children":[["$","meta",null,{"name":"robots","content":"noindex"}],["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"🎙️ ONNX VC - Real-Time AI Voice Changer"}],["$","meta","1",{"name":"description","content":"ONNX VC - Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan ONNX Runtime."}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0x3dzn~oxb6tn.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L5","3",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
+5
View File
@@ -0,0 +1,5 @@
1:"$Sreact.fragment"
2:I[39756,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
3:I[37457,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
0:{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next"}],["$","script","script-0",{"src":"/_next/static/chunks/0dbhjjzl8qfwv.js","async":true}]],["$","html",null,{"lang":"en","className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased","children":["$","body",null,{"className":"min-h-full flex flex-col","children":["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
@@ -0,0 +1,5 @@
1:"$Sreact.fragment"
2:I[39756,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
3:I[37457,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
4:[]
0:{"rsc":["$","$1","c",{"children":[null,["$","$L2",null,{"parallelRouterKey":"children","template":["$","$L3",null,{}]}]]}],"isPartial":false,"staleTime":300,"varyParams":"$W4","buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
@@ -0,0 +1,5 @@
1:"$Sreact.fragment"
2:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"OutletBoundary"]
3:"$Sreact.suspense"
0:{"rsc":["$","$1","c",{"children":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],null,["$","$L2",null,{"children":["$","$3",null,{"name":"Next.MetadataOutlet","children":"$@4"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
4:null
+2
View File
@@ -0,0 +1,2 @@
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
0:{"tree":{"name":"","param":null,"prefetchHints":16,"slots":{"children":{"name":"/_not-found","param":null,"prefetchHints":0,"slots":{"children":{"name":"__PAGE__","param":null,"prefetchHints":0,"slots":null}}}}},"staleTime":300,"buildId":"ncF6Dp_0OCMhTp1KUVnS4"}
-744
View File
@@ -1,744 +0,0 @@
/**
* Omni Real-Time Voice Changer - Client App
* High-performance browser-based mic streaming and RVC playback.
*/
// UI Elements
const wsUrlInput = document.getElementById('ws_url');
const connectionStatus = document.getElementById('connection_status');
const connectBtn = document.getElementById('connect_btn');
const streamBtn = document.getElementById('stream_btn');
const playToggleBtn = document.getElementById('play_toggle_btn');
const modelSelect = document.getElementById('model_select');
const deviceSelect = document.getElementById('device_select');
const transposeSlider = document.getElementById('transpose_slider');
const transposeVal = document.getElementById('transpose_val');
const gateSlider = document.getElementById('gate_slider');
const gateVal = document.getElementById('gate_val');
const inputGainSlider = document.getElementById('input_gain_slider');
const inputGainVal = document.getElementById('input_gain_val');
const outputGainSlider = document.getElementById('output_gain_slider');
const outputGainVal = document.getElementById('output_gain_val');
const chunkSelect = document.getElementById('chunk_select');
const noiseCancelCheckbox = document.getElementById('noise_cancel_checkbox');
const routingModeSelect = document.getElementById('routing_mode_select');
const hardwareDevicesPanel = document.getElementById('hardware_devices_panel');
const serverInputSelect = document.getElementById('server_input_select');
const serverOutputSelect = document.getElementById('server_output_select');
const browserNoiseCancelGroup = document.getElementById('browser_noise_cancel_group');
const presetLatencyBtn = document.getElementById('preset_latency_btn');
const presetQualityBtn = document.getElementById('preset_quality_btn');
const inputCanvas = document.getElementById('input_canvas');
const outputCanvas = document.getElementById('output_canvas');
const hudLatency = document.getElementById('hud_latency');
const hudTime = document.getElementById('hud_time');
const hudGateStatus = document.getElementById('hud_gate_status');
const hudSr = document.getElementById('hud_sr');
// Audio Visualizer Contexts
const inputCtx = inputCanvas.getContext('2d');
const outputCtx = outputCanvas.getContext('2d');
// Web Audio State
let audioContext = null;
let micStream = null;
let micSourceNode = null;
let scriptProcessorNode = null;
let micAccumulator = new Float32Array(0); // Accumulates audio for large/custom chunk sizes
// WebSocket State
let socket = null;
let isStreaming = false;
let playOutput = true;
let targetSampleRate = 40000; // RVC Model default, updated dynamically
// Playback Sync State
let nextPlaybackTime = 0;
const safetyDelay = 0.10; // 100ms buffer to absorb network/websocket jitter (increased for perfect smoothness!)
// Latency Tracking Queues
let sentTimestamps = [];
const maxSentLogs = 50;
// --- SMOOTH VISUALIZER (Rolling Display Buffers + RAF loop) ---
// Fixed display buffer size: ~85ms window looks great at all chunk sizes.
const VIS_DISPLAY_SIZE = 4096;
let inputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE); // rolling input (updated ~85ms)
let outputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE); // fallback for hardware mode
let rafHandle = null;
// Time-synced output queue: each entry = { data: Float32Array, startTime: number (audioCtx seconds) }
let outputChunkQueue = [];
function pushToDisplayBuf(displayBuf, newSamples) {
if (newSamples.length >= VIS_DISPLAY_SIZE) {
displayBuf.set(newSamples.slice(newSamples.length - VIS_DISPLAY_SIZE));
} else {
displayBuf.copyWithin(0, newSamples.length);
displayBuf.set(newSamples, VIS_DISPLAY_SIZE - newSamples.length);
}
}
// Build a VIS_DISPLAY_SIZE window of output samples ending at audioContext.currentTime
function buildTimeSyncedOutputBuf() {
if (!audioContext || outputChunkQueue.length === 0) return outputDisplayBuf;
const now = audioContext.currentTime;
const windowDuration = VIS_DISPLAY_SIZE / targetSampleRate;
const windowStart = now - windowDuration;
// Drop chunks that ended before our window start
while (outputChunkQueue.length > 0) {
const c = outputChunkQueue[0];
if (c.startTime + c.data.length / targetSampleRate < windowStart) {
outputChunkQueue.shift();
} else break;
}
const out = new Float32Array(VIS_DISPLAY_SIZE);
for (const chunk of outputChunkQueue) {
const chunkEnd = chunk.startTime + chunk.data.length / targetSampleRate;
// Overlap between [windowStart, now] and [chunk.startTime, chunkEnd]
const overlapStart = Math.max(windowStart, chunk.startTime);
const overlapEnd = Math.min(now, chunkEnd);
if (overlapStart >= overlapEnd) continue;
const srcOffset = Math.floor((overlapStart - chunk.startTime) * targetSampleRate);
const destOffset = Math.floor((overlapStart - windowStart) * targetSampleRate);
const count = Math.floor((overlapEnd - overlapStart) * targetSampleRate);
const safeCount = Math.min(count,
chunk.data.length - srcOffset,
VIS_DISPLAY_SIZE - destOffset);
if (safeCount > 0) out.set(chunk.data.subarray(srcOffset, srcOffset + safeCount), destOffset);
}
return out;
}
function startVisualizerLoop() {
if (rafHandle) return;
function frame() {
drawWaveform(inputDisplayBuf, inputCanvas, '#a3e635');
// Time-synced output: scrub through queued chunks using audioContext clock
drawWaveform(buildTimeSyncedOutputBuf(), outputCanvas, '#22d3ee');
rafHandle = requestAnimationFrame(frame);
}
rafHandle = requestAnimationFrame(frame);
}
function stopVisualizerLoop() {
if (rafHandle) {
cancelAnimationFrame(rafHandle);
rafHandle = null;
}
outputChunkQueue = [];
}
// Setup Canvas Sizes dynamically
function resizeCanvases() {
inputCanvas.width = inputCanvas.clientWidth * window.devicePixelRatio;
inputCanvas.height = inputCanvas.clientHeight * window.devicePixelRatio;
outputCanvas.width = outputCanvas.clientWidth * window.devicePixelRatio;
outputCanvas.height = outputCanvas.clientHeight * window.devicePixelRatio;
}
resizeCanvases();
window.addEventListener('resize', resizeCanvases);
// Connect / Disconnect WebSocket
connectBtn.addEventListener('click', () => {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
disconnectServer();
} else {
connectServer();
}
});
function connectServer() {
const url = wsUrlInput.value.trim();
updateConnectionStatus('connecting');
try {
socket = new WebSocket(url);
socket.binaryType = 'arraybuffer';
socket.onopen = () => {
console.log('Connected to RVC Server');
updateConnectionStatus('connected');
sendConfigToServer(); // Send initial configurations
streamBtn.disabled = false;
playToggleBtn.disabled = false;
};
socket.onclose = () => {
console.log('WebSocket Connection Closed');
disconnectServer();
};
socket.onerror = (err) => {
console.error('WebSocket Error:', err);
disconnectServer();
};
socket.onmessage = (event) => {
if (typeof event.data === 'string') {
// Config or control response
try {
const response = JSON.parse(event.data);
if (response.type === 'config_success') {
targetSampleRate = response.target_sr;
console.log('Server configuration synced successfully:', response);
} else if (response.type === 'init_devices') {
populateServerDevices(response.devices, response.default_input, response.default_output);
} else if (response.type === 'visualizer') {
// Feed rolling display buffers — RAF loop handles drawing at 60fps
pushToDisplayBuf(inputDisplayBuf, new Float32Array(response.input));
pushToDisplayBuf(outputDisplayBuf, new Float32Array(response.output));
if (!rafHandle) startVisualizerLoop();
} else if (response.type === 'error') {
alert('Server Error: ' + response.message);
}
} catch (e) {
console.error('Error parsing text message:', e);
}
} else if (event.data instanceof ArrayBuffer) {
// Binary processed PCM audio chunk returned from server (Browser Mode only)
handleServerAudioChunk(event.data);
}
};
} catch (e) {
console.error('Connection failed:', e);
disconnectServer();
}
}
function disconnectServer() {
if (isStreaming) {
stopStreaming();
}
if (socket) {
try {
socket.close();
} catch (e) {}
socket = null;
}
updateConnectionStatus('disconnected');
streamBtn.disabled = true;
playToggleBtn.disabled = true;
}
function updateConnectionStatus(status) {
connectionStatus.className = 'status-badge ' + status;
if (status === 'connected') {
connectionStatus.textContent = 'Terhubung';
connectBtn.textContent = 'Putuskan Server';
connectBtn.className = 'btn btn-primary';
} else if (status === 'connecting') {
connectionStatus.textContent = 'Menghubungkan';
connectBtn.textContent = 'Batal';
} else {
connectionStatus.textContent = 'Terputus';
connectBtn.textContent = 'Hubungkan Server';
connectBtn.className = 'btn btn-primary';
}
}
// Config synchronization
function sendConfigToServer() {
if (!socket || socket.readyState !== WebSocket.OPEN) return;
const activeF0 = document.querySelector('input[name="f0_method"]:checked').value;
const config = {
type: 'config',
model_name: modelSelect.value,
device: deviceSelect.value,
f0_method: activeF0,
f0_up_key: parseInt(transposeSlider.value),
noise_gate: parseFloat(gateSlider.value),
input_gain: parseFloat(inputGainSlider.value),
output_gain: parseFloat(outputGainSlider.value),
input_sr: audioContext ? audioContext.sampleRate : 44100,
routing_mode: routingModeSelect.value,
input_device: serverInputSelect.value ? parseInt(serverInputSelect.value) : null,
output_device: serverOutputSelect.value ? parseInt(serverOutputSelect.value) : null,
chunk_size: parseInt(chunkSelect.value)
};
socket.send(jsonEncode(config));
console.log('Sent configuration change:', config);
}
// Populate Server Audio Devices dropdowns
function populateServerDevices(devices, defaultInput, defaultOutput) {
serverInputSelect.innerHTML = '';
serverOutputSelect.innerHTML = '';
if (devices.length === 0) {
const optIn = document.createElement('option');
optIn.textContent = 'Tidak ada mic terdeteksi di server';
serverInputSelect.appendChild(optIn);
const optOut = document.createElement('option');
optOut.textContent = 'Tidak ada output terdeteksi di server';
serverOutputSelect.appendChild(optOut);
return;
}
devices.forEach(device => {
if (device.max_input_channels > 0) {
const opt = document.createElement('option');
opt.value = device.id;
opt.textContent = `[ID ${device.id}] ${device.name}`;
if (device.id === defaultInput) opt.selected = true;
serverInputSelect.appendChild(opt);
}
if (device.max_output_channels > 0) {
const opt = document.createElement('option');
opt.value = device.id;
opt.textContent = `[ID ${device.id}] ${device.name}`;
if (device.id === defaultOutput) opt.selected = true;
serverOutputSelect.appendChild(opt);
}
});
console.log('Successfully populated server hardware devices in UI.');
}
// UI Event Listeners to trigger instant sync
modelSelect.addEventListener('change', sendConfigToServer);
deviceSelect.addEventListener('change', sendConfigToServer);
document.querySelectorAll('input[name="f0_method"]').forEach(radio => {
radio.addEventListener('change', sendConfigToServer);
});
transposeSlider.addEventListener('input', () => {
transposeVal.textContent = (transposeSlider.value >= 0 ? '+' : '') + transposeSlider.value + ' semitone';
});
transposeSlider.addEventListener('change', sendConfigToServer);
gateSlider.addEventListener('input', () => {
gateVal.textContent = gateSlider.value + ' dB';
});
gateSlider.addEventListener('change', sendConfigToServer);
inputGainSlider.addEventListener('input', () => {
inputGainVal.textContent = parseFloat(inputGainSlider.value).toFixed(1) + 'x';
});
inputGainSlider.addEventListener('change', sendConfigToServer);
outputGainSlider.addEventListener('input', () => {
outputGainVal.textContent = parseFloat(outputGainSlider.value).toFixed(1) + 'x';
});
outputGainSlider.addEventListener('change', sendConfigToServer);
chunkSelect.addEventListener('change', () => {
// Reinitialize stream if buffer size is changed during active streaming
if (isStreaming) {
stopStreaming();
startStreaming();
}
});
noiseCancelCheckbox.addEventListener('change', () => {
// Reinitialize microphone with new noise cancellation constraints if streaming
if (isStreaming) {
stopStreaming();
startStreaming();
}
});
// Helper to dynamically adjust UI layout based on Routing Mode
function applyAudioRoutingUI() {
if (routingModeSelect.value === 'hardware') {
hardwareDevicesPanel.style.display = 'block';
playToggleBtn.style.display = 'none'; // Hide browser-only "Mendengarkan" button
browserNoiseCancelGroup.style.display = 'none'; // Hide browser-only Noise Cancel checkbox
} else {
hardwareDevicesPanel.style.display = 'none';
playToggleBtn.style.display = 'inline-block'; // Show browser-only "Mendengarkan" button
browserNoiseCancelGroup.style.display = 'block'; // Show browser-only Noise Cancel checkbox
}
}
// Routing Mode Event Listeners
routingModeSelect.addEventListener('change', () => {
applyAudioRoutingUI();
sendConfigToServer();
if (isStreaming) {
stopStreaming();
startStreaming();
}
});
serverInputSelect.addEventListener('change', sendConfigToServer);
serverOutputSelect.addEventListener('change', sendConfigToServer);
// Quick Presets Event Listeners
presetLatencyBtn.addEventListener('click', () => {
const radioPM = document.querySelector('input[name="f0_method"][value="pm"]');
if (radioPM) radioPM.checked = true;
chunkSelect.value = "8192";
console.log("Preset loaded: Latency (PM + 8192)");
sendConfigToServer();
if (isStreaming) {
stopStreaming();
startStreaming();
}
});
presetQualityBtn.addEventListener('click', () => {
const radioRMVPE = document.querySelector('input[name="f0_method"][value="rmvpe"]');
if (radioRMVPE) radioRMVPE.checked = true;
chunkSelect.value = "16384";
console.log("Preset loaded: Quality (RMVPE + 16384)");
sendConfigToServer();
if (isStreaming) {
stopStreaming();
startStreaming();
}
});
// Helper functions for UI JSON safely
function jsonEncode(obj) {
return JSON.stringify(obj);
}
playToggleBtn.addEventListener('click', () => {
playOutput = !playOutput;
if (playOutput) {
playToggleBtn.textContent = '🔊 Mendengarkan: AKTIF';
playToggleBtn.className = 'btn btn-primary';
} else {
playToggleBtn.textContent = '🔇 Mendengarkan: SENYAP';
playToggleBtn.className = 'btn btn-accent';
}
});
// Stream Toggle
streamBtn.addEventListener('click', () => {
if (isStreaming) {
stopStreaming();
} else {
startStreaming();
}
});
async function startStreaming() {
isStreaming = true;
streamBtn.textContent = 'Hentikan Pengubah Suara';
streamBtn.className = 'btn btn-primary';
const isHardwareMode = (routingModeSelect.value === 'hardware');
if (isHardwareMode) {
// --- SERVER HARDWARE ROUTING MODE ---
inputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
outputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
startVisualizerLoop();
sendConfigToServer(); // Sends config with routing_mode: 'hardware' which triggers stream start on server
console.log('Server Hardware Mode initialized.');
return;
}
// --- CLIENT BROWSER MODE ---
// 1. Create AudioContext if not active
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)({
latencyHint: 'interactive'
});
}
if (audioContext.state === 'suspended') {
await audioContext.resume();
}
hudSr.textContent = audioContext.sampleRate + ' Hz';
sendConfigToServer(); // sync actual input sample rate
// 2. Request user microphone with high-fidelity, lowest possible latency constraints
try {
const useNoiseCancel = noiseCancelCheckbox.checked;
micStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: useNoiseCancel,
noiseSuppression: useNoiseCancel,
autoGainControl: useNoiseCancel
}
});
micSourceNode = audioContext.createMediaStreamSource(micStream);
// 3. Create Audio Processing Loop Node (ScriptProcessorNode)
// BaseAudioContext's createScriptProcessor buffer size MUST be a power of two between 256 and 16384.
// We use a fixed, highly supported buffer size of 4096 for recording, and accumulate samples in-memory
// to support ANY arbitrary or extremely large chunk size (like 12288, 24576, 32768) selected by the user!
const recordBufferSize = 4096;
scriptProcessorNode = audioContext.createScriptProcessor(recordBufferSize, 1, 1);
scriptProcessorNode.onaudioprocess = (event) => {
if (!isStreaming) return;
const inputBuffer = event.inputBuffer;
const inputData = inputBuffer.getChannelData(0); // 4096 samples
// Push latest mic samples into the rolling display buffer every callback (~85ms)
pushToDisplayBuf(inputDisplayBuf, inputData);
// Append incoming recorded samples to our accumulator
const temp = new Float32Array(micAccumulator.length + inputData.length);
temp.set(micAccumulator);
temp.set(inputData, micAccumulator.length);
micAccumulator = temp;
const targetChunkSize = parseInt(chunkSelect.value);
// Process and send chunks of the user's selected target size
while (micAccumulator.length >= targetChunkSize) {
const chunkToSend = micAccumulator.slice(0, targetChunkSize);
micAccumulator = micAccumulator.slice(targetChunkSize); // Keep remainder
// Voice Activity Detection for gate status badge
let maxVal = 0;
for (let i = 0; i < chunkToSend.length; i++) maxVal = Math.max(maxVal, Math.abs(chunkToSend[i]));
if (maxVal > 0.005) {
hudGateStatus.textContent = 'Bicara';
hudGateStatus.className = 'hud-value active-badge';
} else {
hudGateStatus.textContent = 'Berdiam';
hudGateStatus.className = 'hud-value text-muted';
}
// Send binary PCM Float32 audio chunk of target size to Python Server
if (socket && socket.readyState === WebSocket.OPEN) {
const packetTime = performance.now();
sentTimestamps.push({ id: packetTime, sent: packetTime });
if (sentTimestamps.length > maxSentLogs) {
sentTimestamps.shift();
}
socket.send(chunkToSend.buffer); // Send direct array buffer
}
}
};
micSourceNode.connect(scriptProcessorNode);
scriptProcessorNode.connect(audioContext.destination); // Required to trigger onaudioprocess
// Reset playback sync clock
nextPlaybackTime = 0;
micAccumulator = new Float32Array(0); // Reset accumulator
inputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
outputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
startVisualizerLoop();
console.log('Browser Streaming active. Recording buffer size: 4096 | Target chunk size:', chunkSelect.value);
} catch (e) {
console.error('Failed to access microphone:', e);
alert('Gagal mengakses mikrofon Anda: ' + e.message);
stopStreaming();
}
}
function stopStreaming() {
isStreaming = false;
streamBtn.textContent = 'Mulai Mengubah Suara';
streamBtn.className = 'btn btn-accent';
playOutput = true;
playToggleBtn.textContent = '🔊 Mendengarkan: AKTIF';
playToggleBtn.className = 'btn btn-primary';
const isHardwareMode = (routingModeSelect.value === 'hardware');
if (isHardwareMode) {
// --- SERVER HARDWARE ROUTING MODE ---
if (socket && socket.readyState === WebSocket.OPEN) {
const config = {
type: 'config',
routing_mode: 'browser' // Tells server to stop local hardware stream
};
socket.send(jsonEncode(config));
}
console.log('Server Hardware Mode stopped.');
hudGateStatus.textContent = 'Berdiam';
hudGateStatus.className = 'hud-value text-muted';
hudLatency.textContent = '-- ms';
hudTime.textContent = '-- ms';
stopVisualizerLoop();
inputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
outputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
clearCanvas(inputCanvas);
clearCanvas(outputCanvas);
return;
}
// --- CLIENT BROWSER MODE ---
// Stop microphone stream tracks
if (micStream) {
micStream.getTracks().forEach(track => track.stop());
micStream = null;
}
// Disconnect Web Audio nodes
if (micSourceNode) {
micSourceNode.disconnect();
micSourceNode = null;
}
if (scriptProcessorNode) {
scriptProcessorNode.disconnect();
scriptProcessorNode = null;
}
micAccumulator = new Float32Array(0); // Reset accumulator
stopVisualizerLoop();
inputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
outputDisplayBuf = new Float32Array(VIS_DISPLAY_SIZE);
hudGateStatus.textContent = 'Berdiam';
hudGateStatus.className = 'hud-value text-muted';
hudLatency.textContent = '-- ms';
hudTime.textContent = '-- ms';
clearCanvas(inputCanvas);
clearCanvas(outputCanvas);
}
// Seamless Audio Playback Scheduler (Absorbs WebSocket & processing jitter)
function handleServerAudioChunk(arrayBuffer) {
if (!isStreaming) return;
// 1. Measure Round-Trip Time Latency (RTT)
const now = performance.now();
let rtt = 0;
if (sentTimestamps.length > 0) {
const oldestSent = sentTimestamps.shift();
rtt = now - oldestSent.sent;
hudLatency.textContent = Math.round(rtt) + ' ms';
}
// Convert arrayBuffer to Float32 samples
const payload = new Float32Array(arrayBuffer);
const processingTime = payload[0]; // first float32 is the server processing time in ms
const pcmData = payload.subarray(1); // the rest is the audio
// 2. Schedule chunk smoothly inside the AudioContext timeline
const audioBuf = audioContext.createBuffer(1, pcmData.length, targetSampleRate);
audioBuf.getChannelData(0).set(pcmData);
const source = audioContext.createBufferSource();
source.buffer = audioBuf;
if (playOutput) {
source.connect(audioContext.destination);
}
// Calculate precise playback clock scheduling
const currentTime = audioContext.currentTime;
const chunkDuration = audioBuf.duration; // actual chunk duration in seconds
// Adaptive buffer: enough headroom so next chunk always arrives before this one ends.
// 2.5× chunk or 500ms cap — absorbs even 300ms+ processing spikes.
const adaptiveBuf = Math.min(chunkDuration * 2.5, 0.50);
if (nextPlaybackTime < currentTime) {
// Clock behind — first chunk or dropout recovery.
// Use full adaptiveBuf on BOTH cases so recovery fully rebuilds headroom.
// (0.5× recovery was causing cascading dropouts: one late chunk → the next also late)
nextPlaybackTime = currentTime + adaptiveBuf;
} else if (nextPlaybackTime > currentTime + chunkDuration * 5.0) {
// --- ADAPTIVE LATENCY BUSTER ---
// Only snap when queue is >5 chunk-durations ahead (genuine backlog, not normal look-ahead).
// At 8192 (170ms): threshold = 850ms
// At 65536 (1.6s): threshold = 8s
const snapTarget = currentTime + adaptiveBuf;
console.log(`Latency Buster: ${Math.round((nextPlaybackTime-currentTime)*1000)}ms → ${Math.round(adaptiveBuf*1000)}ms`);
nextPlaybackTime = snapTarget;
}
// Record schedule start time BEFORE advancing the clock (for time-synced visualizer)
const scheduleStartTime = nextPlaybackTime;
// Schedule play
source.start(nextPlaybackTime);
hudTime.textContent = Math.max(0, Math.round(processingTime)) + ' ms';
// Advance playback sync clock
nextPlaybackTime += audioBuf.duration;
// Push to time-synced output queue for visualizer (keyed by when audio actually plays)
outputChunkQueue.push({ data: pcmData, startTime: scheduleStartTime });
// Keep queue bounded to ~10 seconds of audio max
while (outputChunkQueue.length > 0) {
const c = outputChunkQueue[0];
if (c.startTime + c.data.length / targetSampleRate < audioContext.currentTime - 2.0) {
outputChunkQueue.shift();
} else break;
}
}
// --- VISUALIZATION / DRAWING ROUTINES ---
function drawWaveform(dataArray, canvas, strokeColor) {
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Dark transparent redraw for trace/motion-blur effect
ctx.fillStyle = 'rgba(5, 7, 4, 0.4)';
ctx.fillRect(0, 0, width, height);
ctx.lineWidth = 2 * window.devicePixelRatio;
ctx.strokeStyle = strokeColor;
ctx.beginPath();
const sliceWidth = width / dataArray.length;
let x = 0;
for (let i = 0; i < dataArray.length; i++) {
// Center the wave around half-height and scale scale amplitude
const v = dataArray[i] * 1.5;
const y = (v * (height / 2)) + (height / 2);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.lineTo(width, height / 2);
ctx.stroke();
// Draw a subtle baseline center glowing path
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
}
function clearCanvas(canvas) {
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#050704';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Apply initial UI layout on startup
applyAudioRoutingUI();
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -243
View File
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
1:"$Sreact.fragment"
2:I[39756,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
3:I[37457,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default"]
4:I[47257,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ClientPageRoot"]
5:I[52683,["/_next/static/chunks/0dbhjjzl8qfwv.js","/_next/static/chunks/01yjdu1pd4sew.js"],"default"]
8:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"OutletBoundary"]
9:"$Sreact.suspense"
b:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"ViewportBoundary"]
d:I[97367,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"MetadataBoundary"]
f:I[68027,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"default",1]
:HL["/_next/static/chunks/0~n~ndg8g34g9.css","style"]
:HL["/_next/static/media/797e433ab948586e-s.p.09zddjkbdep5a.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
:HL["/_next/static/media/caa3a2e1cccd8315-s.p.09~u27dqhyhd6.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
0:{"P":null,"c":["",""],"q":"","i":false,"f":[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",16],[["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}],["$","script","script-0",{"src":"/_next/static/chunks/0dbhjjzl8qfwv.js","async":true,"nonce":"$undefined"}]],["$","html",null,{"lang":"en","className":"geist_a71539c9-module__T19VSG__variable geist_mono_8d43a2aa-module__8Li5zG__variable h-full antialiased","children":["$","body",null,{"className":"min-h-full flex flex-col","children":["$","$L2",null,{"parallelRouterKey":"children","error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L3",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]],"forbidden":"$undefined","unauthorized":"$undefined"}]}]}]]}],{"children":[["$","$1","c",{"children":[["$","$L4",null,{"Component":"$5","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@6","$@7"]}}],[["$","script","script-0",{"src":"/_next/static/chunks/01yjdu1pd4sew.js","async":true,"nonce":"$undefined"}]],["$","$L8",null,{"children":["$","$9",null,{"name":"Next.MetadataOutlet","children":"$@a"}]}]]}],{},null,false,null]},null,false,null],["$","$1","h",{"children":[null,["$","$Lb",null,{"children":"$Lc"}],["$","div",null,{"hidden":true,"children":["$","$Ld",null,{"children":["$","$9",null,{"name":"Next.Metadata","children":"$Le"}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],false]],"m":"$undefined","G":["$f",[["$","link","0",{"rel":"stylesheet","href":"/_next/static/chunks/0~n~ndg8g34g9.css","precedence":"next","crossOrigin":"$undefined","nonce":"$undefined"}]]],"S":true,"h":null,"s":"$undefined","l":"$undefined","p":"$undefined","d":"$undefined","b":"ncF6Dp_0OCMhTp1KUVnS4"}
6:{}
7:"$0:f:0:1:1:children:0:props:children:0:props:serverProvidedParams:params"
c:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
10:I[27201,["/_next/static/chunks/0dbhjjzl8qfwv.js"],"IconMark"]
a:null
e:[["$","title","0",{"children":"🎙️ ONNX VC - Real-Time AI Voice Changer"}],["$","meta","1",{"name":"description","content":"ONNX VC - Pengubah suara real-time berbasis AI berlatensi ultra-rendah dengan ONNX Runtime."}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.0x3dzn~oxb6tn.ico","sizes":"256x256","type":"image/x-icon"}],["$","$L10","3",{}]]
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

-596
View File
@@ -1,596 +0,0 @@
/* ==========================================================================
CSS GLOBAL TOKENS & RESET (LIME LIGHT THEME)
========================================================================== */
:root {
--bg-dark: #0a0d08;
--bg-card: rgba(15, 20, 13, 0.7);
--border-color: rgba(163, 230, 53, 0.18);
--primary: #a3e635;
--primary-glow: rgba(163, 230, 53, 0.4);
--accent: #22d3ee;
--accent-glow: rgba(34, 211, 238, 0.45);
--emerald: #10b981;
--rose: #f43f5e;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--font-header: 'Outfit', 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--transition-smooth: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-dark);
color: var(--text-main);
font-family: var(--font-body);
min-height: 100vh;
overflow-x: hidden;
position: relative;
padding: 2rem 1.5rem;
}
/* ==========================================================================
DYNAMIC GLOWING BACKGROUND
========================================================================== */
.glow-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
background:
radial-gradient(circle at 10% 20%, rgba(163, 230, 53, 0.08) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(34, 211, 238, 0.09) 0%, transparent 45%);
pointer-events: none;
}
/* ==========================================================================
LAYOUT CONTAINER & CARDS
========================================================================== */
.dashboard-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.glassmorphism {
background: var(--bg-card);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--border-color);
border-radius: 16px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
transition: var(--transition-smooth);
}
.glassmorphism:hover {
border-color: rgba(163, 230, 53, 0.3);
box-shadow: 0 10px 40px 0 rgba(163, 230, 53, 0.1);
}
.card {
padding: 1.75rem;
}
.card-title {
font-family: var(--font-header);
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1.25rem;
background: linear-gradient(135deg, #fff 0%, var(--text-muted) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
padding-bottom: 0.75rem;
}
/* ==========================================================================
APP HEADER
========================================================================== */
.app-header {
text-align: center;
margin-bottom: 1rem;
}
.logo-area {
display: inline-flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.logo-area h1 {
font-family: var(--font-header);
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.5px;
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 40px rgba(163, 230, 53, 0.2);
}
.pulse-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--rose);
box-shadow: 0 0 10px var(--rose);
}
.pulse-indicator.active {
background-color: var(--emerald);
box-shadow: 0 0 10px var(--emerald);
animation: pulse 1.8s infinite;
}
.tagline {
color: var(--text-muted);
font-size: 0.95rem;
font-weight: 400;
max-width: 600px;
margin: 0 auto;
}
/* ==========================================================================
DASHBOARD GRID LAYOUT
========================================================================== */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
.col-span-2 {
grid-column: span 1 !important;
}
}
.col-span-2 {
grid-column: span 2;
}
/* ==========================================================================
INPUTS & CONTROLS
========================================================================== */
.control-group {
margin-bottom: 1.25rem;
}
.control-group:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 0.85rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.custom-select {
width: 100%;
padding: 0.8rem 1rem;
background-color: rgba(14, 20, 13, 0.8);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-main);
font-size: 0.9rem;
font-family: var(--font-body);
outline: none;
transition: var(--transition-smooth);
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1.2rem;
}
.custom-select:focus {
border-color: var(--primary);
box-shadow: 0 0 8px var(--primary-glow);
}
.input-group input {
background-color: rgba(14, 20, 13, 0.8);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-main);
padding: 0.8rem 1rem;
width: 100%;
font-family: var(--font-body);
font-size: 0.9rem;
outline: none;
transition: var(--transition-smooth);
}
.input-group input:focus {
border-color: var(--primary);
box-shadow: 0 0 8px var(--primary-glow);
}
/* ==========================================================================
SLIDERS STYLING
========================================================================== */
.slider-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.25rem;
}
.slider-value {
font-family: var(--font-header);
font-weight: 600;
color: var(--accent);
text-shadow: 0 0 8px var(--accent-glow);
font-size: 0.95rem;
}
.custom-slider {
-webkit-appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
background: rgba(163, 230, 53, 0.15);
outline: none;
margin: 0.75rem 0;
}
.custom-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 100%);
cursor: pointer;
box-shadow: 0 0 10px var(--primary-glow);
transition: transform 0.1s ease;
}
.custom-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.slider-ticks {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--text-muted);
}
/* ==========================================================================
BUTTONS
========================================================================== */
.btn {
padding: 0.8rem 1.5rem;
border-radius: 8px;
font-family: var(--font-header);
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
border: none;
outline: none;
transition: var(--transition-smooth);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, #65a30d 100%);
color: #0c0f0a;
box-shadow: 0 4px 14px 0 var(--primary-glow);
}
.btn-primary:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px 0 rgba(163, 230, 53, 0.6);
}
.btn-accent {
background: linear-gradient(135deg, var(--accent) 0%, #0891b2 100%);
color: #0c0f0a;
box-shadow: 0 4px 14px 0 var(--accent-glow);
}
.btn-accent:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 20px 0 rgba(34, 211, 238, 0.65);
}
.btn:active:not(:disabled) {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
box-shadow: none;
color: var(--text-muted);
}
/* ==========================================================================
CONNECTION BAR
========================================================================== */
.connection-bar {
padding: 1rem 1.5rem !important;
}
.form-row {
display: flex;
align-items: flex-end;
gap: 1.5rem;
flex-wrap: wrap;
}
.form-row .input-group {
flex: 1;
min-width: 250px;
}
.connection-status-container {
display: flex;
align-items: center;
height: 48px;
}
.status-badge {
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.status-badge::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
}
.status-badge.connected {
background-color: rgba(16, 185, 129, 0.15);
color: var(--emerald);
border: 1px solid rgba(16, 185, 129, 0.3);
}
.status-badge.connected::before {
background-color: var(--emerald);
box-shadow: 0 0 6px var(--emerald);
}
.status-badge.disconnected {
background-color: rgba(254, 63, 94, 0.15);
color: var(--rose);
border: 1px solid rgba(254, 63, 94, 0.3);
}
.status-badge.disconnected::before {
background-color: var(--rose);
box-shadow: 0 0 6px var(--rose);
}
.status-badge.connecting {
background-color: rgba(34, 211, 238, 0.15);
color: var(--accent);
border: 1px solid rgba(34, 211, 238, 0.3);
}
.status-badge.connecting::before {
background-color: var(--accent);
box-shadow: 0 0 6px var(--accent);
animation: blink 1s infinite;
}
.btn-group-row {
display: flex;
gap: 0.75rem;
height: 48px;
}
/* ==========================================================================
MODERN RADIO TILES
========================================================================== */
.radio-group-modern {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.radio-tile {
position: relative;
cursor: pointer;
margin: 0;
}
.radio-tile input {
position: absolute;
opacity: 0;
}
.tile-label {
display: block;
padding: 0.6rem;
background-color: rgba(15, 20, 13, 0.5);
border: 1px solid var(--border-color);
border-radius: 8px;
text-align: center;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-muted);
transition: var(--transition-smooth);
}
.radio-tile input:checked + .tile-label {
background-color: rgba(163, 230, 53, 0.12);
border-color: var(--primary);
color: var(--text-main);
box-shadow: 0 0 10px rgba(163, 230, 53, 0.2);
}
.radio-tile:hover .tile-label {
border-color: rgba(163, 230, 53, 0.4);
}
/* ==========================================================================
OSCILLOSCOPE WAVEFORM CANVASES
========================================================================== */
.visualizer-row {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
.visualizer-container {
flex: 1;
min-width: 280px;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.vis-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
font-weight: 500;
color: var(--text-muted);
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.input-dot {
background-color: var(--primary);
box-shadow: 0 0 6px var(--primary);
}
.output-dot {
background-color: var(--accent);
box-shadow: 0 0 6px var(--accent);
}
.waveform-canvas {
width: 100%;
height: 150px;
background-color: #050704;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.03);
}
/* ==========================================================================
PERFORMANCE HUD
========================================================================== */
.performance-hud {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.85rem 1.75rem !important;
}
.hud-item {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.hud-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted);
font-weight: 500;
}
.hud-value {
font-family: var(--font-header);
font-size: 1.1rem;
font-weight: 700;
color: white;
}
.hud-separator {
width: 1px;
height: 30px;
background-color: rgba(255, 255, 255, 0.08);
}
.hud-value.text-accent {
color: var(--accent);
text-shadow: 0 0 8px var(--accent-glow);
}
.active-badge {
color: var(--emerald);
text-shadow: 0 0 6px rgba(16, 185, 129, 0.4);
}
@media (max-width: 600px) {
.performance-hud {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.hud-separator {
display: none;
}
}
/* ==========================================================================
KEYFRAME ANIMATIONS
========================================================================== */
@keyframes pulse {
0% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
}
70% {
transform: scale(1.1);
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
}
100% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
}
}
@keyframes blink {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

+1
View File
@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B