@@ -15,30 +15,111 @@ export function useVRMLipSync(audioNode: Ref<AudioBufferSourceNode | undefined,
1515 const { state : lipSyncNode , isReady } = useAsyncState ( createWLipSyncNode ( audioContext , profile as Profile ) , undefined )
1616
1717 // https://github.com/mrxz/wLipSync/blob/c3bc4b321dc7e1ca333d75f7aa1e9e746cbbb23a/example/index.js#L50-L66
18- const lipSyncMap = {
18+ const RAW_KEYS = [ 'A' , 'E' , 'I' , 'O' , 'U' , 'S' ] as const
19+ type LipKey = 'A' | 'E' | 'I' | 'O' | 'U'
20+ const LIP_KEYS : LipKey [ ] = [ 'A' , 'E' , 'I' , 'O' , 'U' ]
21+ const BLENDSHAPE_MAP : Record < LipKey , string > = {
1922 A : 'aa' ,
2023 E : 'ee' ,
2124 I : 'ih' ,
2225 O : 'oh' ,
2326 U : 'ou' ,
2427 }
28+ const RAW_TO_LIP : Record < typeof RAW_KEYS [ number ] , LipKey > = {
29+ A : 'A' ,
30+ E : 'E' ,
31+ I : 'I' ,
32+ O : 'O' ,
33+ U : 'U' ,
34+ S : 'I' ,
35+ }
2536
26- watch ( [ isReady , audioNode ] , ( [ ready , newAudioNode ] , [ , oldAudioNode ] ) => {
27- if ( oldAudioNode )
28- oldAudioNode . disconnect ( )
37+ const smoothState : Record < LipKey , number > = { A : 0 , E : 0 , I : 0 , O : 0 , U : 0 }
38+ const ATTACK = 50 // the speed moving to the next mouth shape animation
39+ const RELEASE = 30 // the speed ending the current mouth shape animation
40+ const CAP = 0.7
41+ const SILENCE_VOL = 0.04
42+ const SILENCE_GAIN = 0.05
43+ const IDLE_MS = 160
44+ let lastActiveAt = 0
2945
30- if ( ready && newAudioNode )
31- newAudioNode . connect ( lipSyncNode . value ! )
46+ watch ( [ isReady , audioNode ] , ( [ ready , newAudioNode ] , [ , oldAudioNode ] ) => {
47+ if ( oldAudioNode && oldAudioNode !== newAudioNode ) {
48+ try {
49+ oldAudioNode . disconnect ( )
50+ }
51+ catch { }
52+ }
53+ if ( ! ready || ! newAudioNode || ! lipSyncNode . value )
54+ return
55+ try {
56+ newAudioNode . connect ( lipSyncNode . value )
57+ }
58+ catch { }
3259 } , { immediate : true } )
3360 onUnmounted ( ( ) => audioNode . value ?. disconnect ( ) )
3461
35- function update ( vrm ?: VRMCore ) {
36- if ( ! vrm ?. expressionManager || ! lipSyncNode . value )
62+ function update ( vrm ?: VRMCore , delta = 0.016 ) {
63+ const node = lipSyncNode . value
64+ if ( ! vrm ?. expressionManager || ! node )
3765 return
3866
39- for ( const key of Object . keys ( lipSyncNode . value . weights ) ) {
40- const weight = lipSyncNode . value . weights [ key ] * lipSyncNode . value . volume
41- vrm . expressionManager ?. setValue ( lipSyncMap [ key as keyof typeof lipSyncMap ] , weight )
67+ const vol = node . volume ?? 0
68+ const amp = Math . min ( vol * 0.9 , 1 ) ** 0.7
69+
70+ // Remapping wLipSync output AEIOUS to AEIOU
71+ const projected : Record < LipKey , number > = { A : 0 , E : 0 , I : 0 , O : 0 , U : 0 }
72+ for ( const raw of RAW_KEYS ) {
73+ const lip = RAW_TO_LIP [ raw ]
74+ const rawVal = node . weights [ raw ] ?? 0
75+ projected [ lip ] = Math . max ( projected [ lip ] , rawVal * amp )
76+ }
77+
78+ // winner + runner
79+ // Original code: all AEIOU mouth shape blended together. Because the A mouth shape has the largest deformation, mixing A-E-I-O-U based on their raw weights causes the combined result to be biased heavily toward A in most cases.
80+ // Improved code: Only the 2 mouth shapes with the largest weights will be blended.
81+ let winner : LipKey = 'I'
82+ let runner : LipKey = 'E'
83+ let winnerVal = - Infinity
84+ let runnerVal = - Infinity
85+ for ( const key of LIP_KEYS ) {
86+ const val = projected [ key ]
87+ if ( val > winnerVal ) {
88+ runnerVal = winnerVal
89+ runner = winner
90+ winnerVal = val
91+ winner = key
92+ }
93+ else if ( val > runnerVal ) {
94+ runnerVal = val
95+ runner = key
96+ }
97+ }
98+
99+ // Detect pause or keep silence
100+ const now = performance . now ( )
101+ let silent = amp < SILENCE_VOL || winnerVal < SILENCE_GAIN
102+ if ( ! silent )
103+ lastActiveAt = now
104+ if ( now - lastActiveAt > IDLE_MS )
105+ silent = true
106+
107+ // winner + runner weights
108+ const target : Record < LipKey , number > = { A : 0 , E : 0 , I : 0 , O : 0 , U : 0 }
109+ if ( ! silent ) {
110+ target [ winner ] = Math . min ( CAP , winnerVal )
111+ target [ runner ] = Math . min ( CAP * 0.5 , runnerVal * 0.6 )
112+ }
113+
114+ // smoothness and expression generation
115+ for ( const key of LIP_KEYS ) {
116+ const from = smoothState [ key ]
117+ const to = target [ key ]
118+ // lerp
119+ const rate = 1 - Math . exp ( - ( to > from ? ATTACK : RELEASE ) * delta )
120+ smoothState [ key ] = from + ( to - from ) * rate
121+ const weight = ( smoothState [ key ] <= 0.01 ? 0 : smoothState [ key ] ) * 0.7
122+ vrm . expressionManager . setValue ( BLENDSHAPE_MAP [ key ] , weight )
42123 }
43124 }
44125
0 commit comments