Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

给网页上的 Live2D 添加语音口型同步 #7

Open
itorr opened this issue Nov 26, 2022 · 10 comments
Open

给网页上的 Live2D 添加语音口型同步 #7

itorr opened this issue Nov 26, 2022 · 10 comments
Labels
enhancement New feature or request

Comments

@itorr
Copy link
Owner

itorr commented Nov 26, 2022

最近给你画我猜的小狐狸添加了跟着语音对口型的功能,点击尾巴、呆毛等地方,小狐狸会凭心情回应你一句话。

预览 https://enazo.cn

目标

现在小狐狸的台词数量特别多,几乎不可能针对每一条台词做 Live2D 的动画设计,而且也希望一个动作可以用在多句台词上。

那么就想让嘴型从动画上解耦,能靠频谱自动响应嘴型那就最好了。

寻找方案

我现在在用 @guansss/pixi-live2d-display 这个库,翻了下源码好像并没有实现官方文档里的语音同步

翻官方的 Web SDK 里提供的 TS 代码,是对 wav 进行解码,实现的嘴型分析
具体没有细看、不是很理解为什么要这么做

小狐狸是小女孩的声音,码率控制在 24kbps 应该是极限了,同码率下 和 ogg 差别不大,最后选择兼容性更好的 AAC

由于我的小狐狸展示尺寸其实只有 120px x 120px x 二倍、仅只给网页用,所以嘴型这里并没有做成 5x5,目前相当于只实现了 2x3 (・ェ・。) 所以在嘴型这里也不是必须 aiueo 对口

而且嘴型同步这里看起来官方的代码里也没实现 aiueo 的对口,甚至音频格式只支持 wav 这怎么想都不适合落地,于是回到舒适区给之前的 webAudio 播放提示音部分 加上波形获取,再找找现在用的库有没有实现设置参数的接口

瞬时频谱通过 Analyser 获取

我们对于频率其实不需要太详细,getByteFrequencyData 肯定就够用

https://developer.mozilla.org/zh-CN/docs/Web/API/AnalyserNode/getByteFrequencyData

const audioCtx = new AudioContext();

// 新建分析仪
const analyser = audioCtx.createAnalyser();

// 根据 频率分辨率建立个 Uint8Array 数组备用
const frequencyData = new Uint8Array(analyser.frequencyBinCount);

// 取音频文件成 arraybuffer
const request = new XMLHttpRequest();
request.open('GET', 'antabaka.m4a', true);
request.responseType = 'arraybuffer';
request.onload = ()=>{
    const audioData = request.response;

    audioCtx.decodeAudioData(audioData, function(buffer) {
        // 新建 Buffer 源
        const source = audioCtx.createBufferSource();
        source.buffer = buffer;

        // 连接到 audioCtx
        source.connect(audioCtx.destination);

        // 连接到 音频分析器
        source.connect(analyser);

        // 开始播放
        source.start(0);
    });
};
request.send();

// 需要用到频谱的时候 从分析仪获取到 之前备用的 frequencyData 里
const getByteFrequencyData = ()=>{
	analyser.getByteFrequencyData(frequencyData);
	return frequencyData;
};

这样频谱就获取到了

接下来选择一段适合的频段、取个能让嘴巴顺畅动起来的响度区间

按照网页上 Live2D 的 30fps 运行速度预览一下频谱

const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
canvas.width = 1200;
canvas.height = 400;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000';

const w = 20;
const o = 80;
const run = ()=>{
    const frequencyData = getByteFrequencyData();
    ctx.clearRect(0,0,1200,400)
    for (var i = 0; i < 1200; i += o) {
        const h = frequencyData[i];
        ctx.fillRect(
            (i/o) * w,
            0,
            w,
            h * 2,
        )
    }

    setTimeout(run,1000/30);
};

横轴为频率、纵轴为响度,我打算从中选择一段变化丰富的片段,按情况安排一条曲线

这个根据角色声音特征不同,位置应该也会有区别。我试了半天总结的规律是频率变化在响度面前不值一提,而我的小狐狸口型只有 2x3,其实也没必要做那么细的变化。
最后是选了 0-700 的频率范围、响度范围 0.2-0.8 ,只做了一维度的开合,和官方一样(

学了两天 Live2D 的我好像也搞明白了大概概念,Live2D 通过参数抽象的动作程度,再通过关键帧曲线控制参数实现了动画。
我这里需要做的就是在动画里删除控制嘴巴的部分,留给网页自己控制嘴巴的参数。

于是打开 pixi-live2d-display 的文档

完了,好像不是一点点复杂。

不知道为啥这么熟练的我,肯定是直接按照 Live2D 界面上的关键字直接搜索 Parameter 看看,ψ(`∇´)ψ

呃,无数个结果。因为这个文档里每个函数的参数 用的 Parameters 标题,也被索引进了文档搜索里,离谱。
最后只能自己一点一点翻,没找到、陷入僵局

最后翻源码在 coreModel 里翻到一堆 setParameter 相关的方法,看名字盲猜 setParameterValueById 最好用,找到我需要改的参数 ParamMouthOpenY 数值范围 0-1 备用

我的小狐狸实例

import { Live2DModel } from 'pixi-live2d-display';
const modelPath = `你的模型.model3.json`;
const model = await Live2DModel.from(modelPath);

设置嘴巴开合高度函数,注意限制下传入参数范围(写代码之前直接调试发现超过范围值会有bug

const setMouthOpenY = v=>{
    v = Math.max(0,Math.min(1,v));
    model.internalModel.coreModel.setParameterValueById('ParamMouthOpenY',v);
}

在小狐狸说话的期间,不停的获取瞬时频谱 按照 0-700 频率、0.2 ~ 0.8 响度区间 整理成 嘴巴开合程度。

const o = 80;
const arrayAdd = a=>a.reduce((i,a)=>i+a,0);
let playing = true;
const run =()=>{
    if(!playing) return;
    const frequencyData = getByteFrequencyData();
    const arr = [];
    // 频率范围还是太广了,跳采!
    for (var i = 0; i < 700; i += o) {
        arr.push(frequencyData[i]);
    }
    setMouthOpenY((arrayAdd(arr)/arr.length - 20)/60);
    setTimeout(run,1000/30);
}

在音频播放结束时停下来

source.onended = ()={
    // 停止播放
    playing = false;
}

再来看看俺的小狐狸

于是嘴型同步就做好啦~
再来炫耀下小狐狸🤗
👉🏻 https://enazo.cn

相关

翻到 pixi-live2d-display 的 issue#78 里有口型同步相关讨论
希望能看到更理想的解决方案🤔 订阅先

参考

@itorr itorr added the enhancement New feature or request label Nov 26, 2022
@itorr
Copy link
Owner Author

itorr commented Nov 26, 2022

需要搞定的问题 尾巴和眼睛绑定 导致动画结束时固定会连冠不上

@organics2016
Copy link

牛逼,我先插个眼,一会试试

@organics2016
Copy link

model.internalModel.coreModel.setParameterValueById('ParamMouthOpenY',v)
这句是有效的,但是我每次执行完之后会被 PIXI 强行按照 model.json 里面的值渲染回去
后来我把 ParamMouthOpenY 相关 Segments 初始数据都删了,但是 PIXI 会把空 Segments 视为0
效果就是嘴快张开了,马上有闭住了,特别鬼畜。。这怎么搞啊大佬? 我用的是 autoUpdate:true 难不成要自己更新状态?

@organics2016
Copy link

搞定了,原来把整个 Segments 数组删了就行了,啊啊啊啊,不足就是sdk飘红,不过不影响使用,感谢大佬!

@itorr
Copy link
Owner Author

itorr commented Mar 12, 2023

model.internalModel.coreModel.setParameterValueById('ParamMouthOpenY',v) 这句是有效的,但是我每次执行完之后会被 PIXI 强行按照 model.json 里面的值渲染回去 后来我把 ParamMouthOpenY 相关 Segments 初始数据都删了,但是 PIXI 会把空 Segments 视为0 效果就是嘴快张开了,马上有闭住了,特别鬼畜。。这怎么搞啊大佬? 我用的是 autoUpdate:true 难不成要自己更新状态?

可以把模型里原本 动作、物理 控制 ParamMouthOpenY 属性的部分删掉,或者调小在 动作、物理 里面对 ParamMouthOpenY 的影响比例,然后新建一个自定的属性 通过设定影响比例联合控制 ParamMouthOpenY

@zhao896632126
Copy link

我在不断执行run方法的时候,model.internalModel.coreModel.setParameterValueById('ParamMouthOpenY',v)这句话执行起来没有效果呢,口型偶尔会动几下,是下原因呀?查看了v的值是很正常的

@zhao896632126
Copy link

搞定了,原来把整个 Segments 数组删了就行了,啊啊啊啊,不足就是sdk飘红,不过不影响使用,感谢大佬!

我在idle.motion3.json默认动作的motion3文件下把ParamMouthOpenY整个 Segments 数组删掉了,动作就加载不出来了呢,F12报TypeError: this._json.Curves[curveIndex].Segments is undefined,咋解决呀

@organics2016
Copy link

搞定了,原来把整个 Segments 数组删了就行了,啊啊啊啊,不足就是sdk飘红,不过不影响使用,感谢大佬!

我在idle.motion3.json默认动作的motion3文件下把ParamMouthOpenY整个 Segments 数组删掉了,动作就加载不出来了呢,F12报TypeError: this._json.Curves[curveIndex].Segments is undefined,咋解决呀

我表达不准确,,不是把Segments 数组删了,,是清空,, Segments :[]为空数组

@lqh1008
Copy link

lqh1008 commented Jun 1, 2024

请问下,这个库@guansss/pixi-live2d-display可以通过语音控制嘴唇变动吗

@itorr
Copy link
Owner Author

itorr commented Jun 2, 2024

请问下,这个库@guansss/pixi-live2d-display可以通过语音控制嘴唇变动吗

可以关注下 对应库的这个 issue guansss/pixi-live2d-display#78

我是直接操作 coreModel.setParameterValueById 方法设置的 ParamMouthOpenY 参数,可以更自由的控制嘴唇

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants