|
|
2025/2/24(Mon) 14:04:15|NO.103115
自作システム上で動作するVtuberのモデルの口パクを「音声とシンクロする形」で制御したいと思いました。
要はそれは、wavプレイヤーのボリュームメーターと仕組みは同じなのではないかと思いました。
それで以下のような機能を実装したいと思ったのですが、以下の2点をどう実装したらいいのかよく分かりませんでした。
1.録音済みの音声のwavファイルの「音の大きさ」を、音声の再生と同期した形で取得したい
2.マイクから入力される音声の「音の大きさ」を、リアルタイムで取得したい
2の部分はWindows APIやCOM等で用意されていそうな気もするのですが、専門外過ぎてよく分かりませんでした。
動作させる対象のPC環境はWindows 10で、オーディオ環境はごく普通のWDMです。DirectXのDirectSound系でも構いません。
ご存じの方がいましたら、アドバイスお願い致します。(アドバイスは1番でも2番でも両方でも、分かる部分だけで構いません)
|
|
2025/2/24(Mon) 17:13:17|NO.103116
対象のPC環境をもう少し詳しく調べてみました。
動作可のもの
・WASAPI
・DirectX(DirectSound)
・WDM
この内、WDMが古い規格で、WASAPIが新しい規格のようです。
Windows 10で動作させるので、WASAPIの方がいいのかもしれません。
動作不可のもの
・ASIO
|
|
2025/2/24(Mon) 22:09:24|NO.103118
|
|
2025/2/25(Tue) 18:09:28|NO.103123
>GENKI さん
アドバイスありがとうございます!
GENKIさんの「音声の入力」のページは質問文を書き込んだ後に偶然発見して、大変参考にさせて貰ってます。現在研究中で、おそらくこれでリアルタイム口パクについては実装できそうです。
mod_riffについては、RIFFというものそのものを知らなかったので、こういう使い方をすればいいモジュールというのを初めて知りました。
これからmod_riffについても研究させて貰おうと思います。
Vtuberの口パクについてですが、自分もですが他のVtuberの子たちもなかなか思った通りの口パクにならず苦労しているようです。
|
|
2025/2/27(Thu) 00:53:34|NO.103136
こんにちわ
面白い事おこなってますね〜。
マイクなら標準搭載されてるwinmm使うのが簡単かと思います。
宜しかったら参考にどうぞ。
// 簡易マイク入力モジュール
// ----------------------------------------------
#module _WaveIn_
#uselib "WINMM.DLL"
#func waveInOpen "waveInOpen" var,int,var,int,int,int
#func waveInClose "waveInClose" int
#func waveInStart "waveInStart" int
#func waveInReset "waveInReset" int
#func waveInPrepareHeader "waveInPrepareHeader" int,var,int
#func waveInUnprepareHeader "waveInUnprepareHeader" int,var,int
#func waveInAddBuffer "waveInAddBuffer" int,var,int
#deffunc mic_open array _buff, int _rate
dup m_buff, _buff
// 入力のオープン(既定のマイクとフォーマットを設定)PCM: ? KHz 16bit mono
hwi = 0 : wfx = 1 | (1 << 16), _rate, _rate * 2, 2 | (16 << 16), 0
waveInOpen hwi, /*WAVE_MAPPER*/ 0xFFFFFFFF, wfx, hwnd, 0, /*CALLBACK_WINDOW*/ 0x10000
// マイク入力用バッファ作成(ダブル)
bufferLength = length(_buff) * 2
sdim buff1, bufferLength
sdim buff2, bufferLength
wh1 = varptr(buff1), varsize(buff1), 0,0,0,0,0,0
wh2 = varptr(buff2), varsize(buff2), 0,0,0,0,0,0
waveInPrepareHeader hwi, wh1, 32 : waveInAddBuffer hwi, wh1, 32
waveInPrepareHeader hwi, wh2, 32 : waveInAddBuffer hwi, wh2, 32
// 入力スタート
waveInStart hwi : if stat { dialog "ERROR: waveInStart("+stat+")" : end }
oncmd gosub *On_MM_WIM_DATA, /*MM_WIM_DATA*/0x03C0
return
*On_MM_WIM_DATA
dupptr wh, lparam, 32 : dupptr buff, wh.0, wh.1, 2
foreach m_buff : m_buff.cnt = double(wpeek(buff, cnt * 2) << 16 >> 16) / 32768.0 : loop
peak = 0.0 : foreach m_buff : peak = limitf(peak, absf(m_buff.cnt)) : loop
rms = 0.0 : foreach m_buff : rms += m_buff.cnt * m_buff.cnt : loop : rms = sqrt(rms / length(m_buff))
waveInAddBuffer wparam, wh, 32
return
#defcfunc mic_peak
return peak
#defcfunc mic_rms
return rms
#deffunc local exit onexit
waveInReset hwi : waveInUnprepareHeader hwi, wh, 32 : waveInClose hwi : return
#global
// サンプル
// ----------------------------------------------
#use a2d
// 画面とバッファ
screen 0, 320,512 : title "マイクで簡易口パク"
alCreateImageByFile 1, dir_tv+"up_tar.png" : alCreateImage 0, 512, 512 : alFont
// 設定値
gain = 10.0 // マイクの入力感度
dumping = 0.3 // 口の滑らかさ
vN = 52, 53, 70, 45, 52, 53, 30, 45 // 口(閉) 頂点データ 100*100
vO = 50, 30, 62, 45, 50, 58, 38, 45 // 口(お)
vE = 50, 35, 80, 43, 50, 55, 20, 43 // 口(え)
vA = 50, 30, 75, 47, 50, 65, 25, 47 // 口(あ)
// マイクオープン
ddim buff, 256 : mic_open buff, 8000 // ★追加した命令
*MAIN
// アップデート
// ----------------------------------------------
// 感度調整
stick pad : gain = limitf(gain + 0.5*(pad>>2&1)-0.5*(pad&1), 0, 100)
// まばたき更新
blink = limit(blink-1, 0, 2) : if rnd(120) = 0 { blink = 2 }
// 口の頂点ブレンド更新
blend = limitf(blend + (mic_rms() * gain - blend) * dumping, 0.0, 1.0) // ★入力の二乗平均を使う
repeat 1
if blend <= 0.25 { foreach vN : vC.cnt = vN.cnt : loop : break }
if blend <= 0.50 { foreach vN : vC.cnt = int((double(vO.cnt)-double(vN.cnt))*(blend-0.25)*4. + double(vN.cnt)) : loop : break }
if blend <= 0.75 { foreach vN : vC.cnt = int((double(vE.cnt)-double(vO.cnt))*(blend-0.50)*4. + double(vO.cnt)) : loop : break }
if blend <= 1.00 { foreach vN : vC.cnt = int((double(vA.cnt)-double(vE.cnt))*(blend-0.75)*4. + double(vE.cnt)) : loop : break }
loop
// ドロー
// ----------------------------------------------
alErase : alResetTransMode
// キャラ描画
alTransModeOffsetRotateZoom 0,0, 0, 0.5,0.5 // 頭体
alCopyImageToImage 1,0, 0,0, 640, 1024
alTransModeOffsetRotateZoom 64,165, 0, 0.5,0.5 // 眉
alCopyImageToImage 1,0, 0,0, 385, 128, 640, 0
alTransModeOffsetRotateZoom 65,217, 0, 0.5,0.5 // 目
alCopyImageToImage 1,0, 0,0, 385, 128, 640, 512+blink*128
alTransModeOffsetRotateZoom 123,271, 0, 0.7, 0.7 // 口描画
alColor 212,140,143 : alFillClosedCurve vC, 4, 0.8 // 口の塗
alColor : alDrawClosedCurve vC, 4, 0.8 // 口の線
// 入力波形描画
alTransModeOffsetRotateZoom ,32,,1.25, 0.3 : alColor 84,84,255
x2 = 0 : y2 = 100 : repeat length(buff) : x1 = x2 : y1 = y2 : x2 = cnt : y2 = -buff.cnt*100.0+100 : alDrawLine x1, y1, x2, y2 : loop
// 情報描画
alResetTransMode
alColor 255,122,0,64 : alFillRect 0,0, 320,16
alColor 255,122,0 : repeat 3,1 : alDrawLine 80*cnt, 0, 80*cnt, 16 : loop
txt = "閉じ", "お","え","あ" : repeat 4 : alDrawText txt.cnt, 80*cnt, 0, 80,16,1 :loop
alDrawText strf("口パク感度(← →): %3.1f", gain), 0,24
alColor 255,122,0,128 : alFillRect 0,4, blend*320,8
// 画面更新と待ち
alCopyImageToScreen 0, 0 : redraw : await 16 : redraw 0 : rgbcolor $FFFFFF : boxf
goto *MAIN
★余談★
音量は信号のピークを使うよりRMS使った方が好みです。
また、音量だと口が開きっぱなしになってしまいますので、
ある程度"音素"は判定した方が良いですね。

| |
|
2025/2/28(Fri) 18:15:36|NO.103155
> usagi さん
usagiさん、アドバイスありがとうございます!
これは凄い! これは凄いですね!
これは画像をオリジナルのものに差し替えれば、そのままVtuberとしてデビューできてしまうほどだと思います。
凄く重要な技術だと思うので、このスクリプトに関する技術動画を撮ったのですが、これを動画配信で流していいでしょうか?
⚫研究中に気づいたこと「口の開き方によってキャラクターを差別化できる」
usagiさんとGENKIさんのスクリプトを研究していて気づいたのですが、usagiさんの以下の部分は特に重要だと思いました。
// 口の頂点ブレンド更新
blend = limitf(blend + (mic_rms() * gain - blend) * dumping, 0.0, 1.0) // ★入力の二乗平均を使う
例えば上記の部分を以下のように、
// 口の頂点ブレンド更新
blend = limitf(blend + (mic_peak() * gain - blend) * dumping, 0.0, 1.0) // ★入力のピークを使う
変更した場合、当然、口が開きっぱなしになるわけですが、キャラクターの表現として「アホの娘っぽさ」が生まれ、
この部分の「口の開き方補正によって、キャラクター性に変化を持たせることができる」ことが分かりました。
つまり、mic_rms()とmic_peak()の線形補完によって「キャラクター性」の具合がパラメータとして調整できるわけです。
これはかなり大きな発見でした。
この部分の更なる調整によって「口の開き方によるキャクラター性の差別化」が可能となるかもしれません。
そこでGENKIさんのスクリプトのフィルタリング部分も組み合わせれば、かなり自由な調整ができそうです。
お二人のアドバイスは異なるアプローチ(のように見える)でのアドバイスのはずなのに、こういう形で繋がるのはかなり不思議な感じです。
今回の研究は思った以上に大きな発見が多いです。
|
|
2025/3/3(Mon) 08:16:30|NO.103192
返信遅れてすみません。私が掲示板に書き込んだものはご自由にお使い下さいませ。
※参考がある場合は必ずコメント記載します。
blendの部分は滑らかにするために、線形補完にバネ組み合わせたイメージです。
何か参考になれば幸いです。
|
|
2025/7/17(Thu) 20:03:53|NO.103684
Vtuberデビューされたのですね。おめでとうございます。
古い未解決スレッドを上げるのもどうかと悩んだのですが、
透過のお話しが出たので参考になればと思い投稿しました。
a2dは透明情報保持できるのでレイヤードウインドウ機能を使えばいろいろできますよ。
よろしかったらどうぞ。
※前とあまり変わってませんが追加した所★つけました。
#use a2d
// 簡易マイク入力モジュール
// ----------------------------------------------
#module _WaveIn_
#uselib "WINMM.DLL"
#func waveInOpen "waveInOpen" var,int,var,int,int,int
#func waveInClose "waveInClose" int
#func waveInStart "waveInStart" int
#func waveInReset "waveInReset" int
#func waveInPrepareHeader "waveInPrepareHeader" int,var,int
#func waveInUnprepareHeader "waveInUnprepareHeader" int,var,int
#func waveInAddBuffer "waveInAddBuffer" int,var,int
#deffunc mic_open array _buff, int _rate
dup m_buff, _buff
// 入力のオープン(既定のマイクとフォーマットを設定)PCM: ? KHz 16bit mono
hwi = 0 : wfx = 1 | (1 << 16), _rate, _rate * 2, 2 | (16 << 16), 0
waveInOpen hwi, /*WAVE_MAPPER*/ 0xFFFFFFFF, wfx, hwnd, 0, /*CALLBACK_WINDOW*/ 0x10000
// マイク入力用バッファ作成(ダブル)
bufferLength = length(_buff) * 2
sdim buff1, bufferLength
sdim buff2, bufferLength
wh1 = varptr(buff1), varsize(buff1), 0,0,0,0,0,0
wh2 = varptr(buff2), varsize(buff2), 0,0,0,0,0,0
waveInPrepareHeader hwi, wh1, 32 : waveInAddBuffer hwi, wh1, 32
waveInPrepareHeader hwi, wh2, 32 : waveInAddBuffer hwi, wh2, 32
// 入力スタート
waveInStart hwi : if stat { dialog "ERROR: waveInStart("+stat+")" : end }
oncmd gosub *On_MM_WIM_DATA, /*MM_WIM_DATA*/0x03C0
return
*On_MM_WIM_DATA
dupptr wh, lparam, 32 : dupptr buff, wh.0, wh.1, 2
foreach m_buff : m_buff.cnt = double(wpeek(buff, cnt * 2) << 16 >> 16) / 32768.0 : loop
peak = 0.0 : foreach m_buff : peak = limitf(peak, absf(m_buff.cnt)) : loop
rms = 0.0 : foreach m_buff : rms += m_buff.cnt * m_buff.cnt : loop : rms = sqrt(rms / length(m_buff))
waveInAddBuffer wparam, wh, 32
return
#defcfunc mic_peak
return peak
#defcfunc mic_rms
return rms
#deffunc local exit onexit
waveInReset hwi : waveInUnprepareHeader hwi, wh, 32 : waveInClose hwi : return
#global
// ★簡易透過ウインドウモジュール(a2d依存)
// ----------------------------------------------
#module LAYERED_WINDOW
; gdi+ ----------------------
#const PixelFormat32bppPARGB $E200B
; gdi32 ---------------------
#uselib "gdi32.dll"
#func CreateDIBSection "CreateDIBSection" sptr,sptr,sptr,sptr,sptr,sptr
#cfunc CreateCompatibleDC "CreateCompatibleDC" sptr
#func DeleteDC "DeleteDC" sptr
#cfunc CreateCompatibleBitmap "CreateCompatibleBitmap" sptr,sptr,sptr
#func SelectObject "SelectObject" sptr,sptr
#func DeleteObject "DeleteObject" sptr
#func StretchDIBits "StretchDIBits" sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr
#define SRCCOPY 0x00CC0020
#define CAPTUREBLT 0x40000000
; user32 --------------------
#uselib "user32.dll"
#cfunc GetWindowLongA "GetWindowLongA" sptr,sptr
#func SetWindowLongA "SetWindowLongA" sptr,sptr,sptr
#func UpdateLayeredWindow "UpdateLayeredWindow" sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr,sptr
#cfunc GetDC "GetDC" sptr
#func ReleaseDC "ReleaseDC" sptr,sptr
#define GWL_STYLE -16
#define GWL_EXSTYLE -20
#define WS_EX_LAYERED 0x00080000
#define ULW_COLORKEY 0x00000001
#define ULW_ALPHA 0x00000002
#define ctype BLEND_FUNCTION(%1=255) (0x01000000|((%1&0xFF)<<16))
; Function ------------------
#define global alLayeredWindow(%1,%2=255,%3=ginfo_wx1,%4=ginfo_wy1,%5=ginfo_winx,%6=ginfo_winy) _alLayeredWindow %1,%2,%3,%4,%5,%6
#deffunc _alLayeredWindow int id, int alpha, int wx, int wy, int ww, int wh
alSelectImage id
lwAlpha alpha, wx, wy, ww, wh
return
#deffunc lwAlpha int alpha, int wx, int wy, int ww, int wh
; a2dのカレントからLockBits
if imgValid@a2d == 0 { return -1 }
rect@a2d = 0, 0, ww, wh
dim BitmapData@a2d, 8
GdipBitmapLockBits@a2d imgImage@a2d, varptr(rect@a2d), 1, PixelFormat32bppPARGB, varptr(BitmapData@a2d)
dupptr bmp, BitmapData@a2d(4), BitmapData@a2d(0) * BitmapData@a2d(1) * 4
; スクリーンと互換性のあるデバイスコンテキスト、ビットマップ作成
hdcDst = GetDC(0)
hsrcdc = CreateCompatibleDC(hdcDst)
hbitmap = CreateCompatibleBitmap(hdcDst, ww, wh)
; ビットマップコピー
SelectObject hsrcdc, hbitmap
bmpinfo = 40, ww, wh, 1|(32<<16), 0 ,0,0,0,0,0
StretchDIBits hsrcdc, 0,0,ww,wh, 0,wh,ww,-wh, varptr(bmp), varptr(bmpinfo), 0, SRCCOPY|CAPTUREBLT
; レイヤードウインドウ
SetWindowLongA hwnd, GWL_EXSTYLE, GetWindowLongA(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED
ptDst = wx, wy : size = ww, wh
ptSrc = 0, 0 : blend = BLEND_FUNCTION(alpha)
crKey = 0 : dwFlags = ULW_ALPHA
UpdateLayeredWindow hwnd, hdcDst, varptr(ptDst), varptr(size), hsrcdc, varptr(ptSrc), crKey, varptr(blend), dwFlags
; 解放
ReleaseDC 0, hdcDst : DeleteObject hbitmap : DeleteDC hsrcdc
GdipBitmapUnlockBits@a2d imgImage@a2d, varptr(BitmapData@a2d)
return
#global
#module ; ★a2d拡張 (これは別にいらないけどオマケ)
#deffunc alExTransModeOffsetRotateZoomAt int dx, int dy, double pa, double sx, double sy, int px, int py
if imgValid@a2d {
GdipCreateMatrix@a2d varptr(tmpMatrix@a2d)
GdipTranslateMatrix@a2d tmpMatrix@a2d, dx, dy, 0
GdipTranslateMatrix@a2d tmpMatrix@a2d, px, py, 0
GdipRotateMatrix@a2d tmpMatrix@a2d, pa, 0
GdipScaleMatrix@a2d tmpMatrix@a2d, sx, sy, 0
GdipTranslateMatrix@a2d tmpMatrix@a2d, -px, -py, 0
GdipSetWorldTransform@a2d imgGraphics@a2d, tmpMatrix@a2d
GdipDeleteMatrix@a2d tmpMatrix@a2d
tmpMatrix@a2d = 0
}
return
#global
// サンプル
// ----------------------------------------------
// 画面とバッファ ★HSPとa2dのメインスクリーンは同じ大きさにしてください。
screen 0, 320,512 : title "マイクで簡易口パク+背景透過"
alCreateImageByFile 2, dir_tv+"up_tar.png" ; 素材
alCreateImage 1, 320, 512 : alFont ; ★バックイメージ
alCreateImage 0, 320, 512 : alFont ; ★メインイメージ
// 設定値
opacity = 255 // ★キャラの透明度
reverse = 1 // ★キャラの向き
gain = 10.0 // マイクの入力感度
dumping = 0.3 // 口の滑らかさ
vN = 52, 53, 70, 45, 52, 53, 30, 45 // 口(閉) 頂点データ 100*100
vO = 50, 30, 62, 45, 50, 58, 38, 45 // 口(お)
vE = 50, 35, 80, 43, 50, 55, 20, 43 // 口(え)
vA = 50, 30, 75, 47, 50, 65, 25, 47 // 口(あ)
// マイクオープン
ddim buff, 256 : mic_open buff, 8000 // ★追加した命令
*MAIN
// アップデート
// ----------------------------------------------
// マイク感度調整 (十字キー←→で調整)
stick pad, 256 : gain = limitf(gain + 0.5*(pad>>2&1)-0.5*(pad&1), 0, 100)
// ★透明度調整 (十字キー↑↓で調整)
opacity = limit(opacity+ 10*(pad>>1&1)-10*(pad>>3&1), 32, 255)
// ★A D キーで左右反転
if pad&$04000 { reverse = -1 } : if pad&$10000 { reverse = 1 }
// ★左クリックでキャラ移動
if pad&256 { sendmsg hwnd , 0x00a1/*WM_NCLBUTTONDOWN*/, 0x02/*HTCAPTION*/, lparam }
// ★右クリックで終了
if pad&512 { end }
// まばたき更新
blink = limit(blink-1, 0, 2) : if rnd(120) = 0 { blink = 2 }
// 口の頂点ブレンド更新
blend = limitf(blend + (mic_rms() * gain - blend) * dumping, 0.0, 1.0)
repeat 1
if blend <= 0.25 { foreach vN : vC.cnt = vN.cnt : loop : break }
if blend <= 0.50 { foreach vN : vC.cnt = int((double(vO.cnt)-double(vN.cnt))*(blend-0.25)*4. + double(vN.cnt)) : loop : break }
if blend <= 0.75 { foreach vN : vC.cnt = int((double(vE.cnt)-double(vO.cnt))*(blend-0.50)*4. + double(vO.cnt)) : loop : break }
if blend <= 1.00 { foreach vN : vC.cnt = int((double(vA.cnt)-double(vE.cnt))*(blend-0.75)*4. + double(vE.cnt)) : loop : break }
loop
// ドロー
// ----------------------------------------------
alSelectImage 1 : alErase : alResetTransMode
// キャラ描画
alTransModeOffsetRotateZoom 0,0, 0, 0.5,0.5 // 頭体
alCopyImageToImage 2,1, 0,0, 640, 1024
alTransModeOffsetRotateZoom 64,165, 0, 0.5,0.5 // 眉
alCopyImageToImage 2,1, 0,0, 385, 128, 640, 0
alTransModeOffsetRotateZoom 65,217, 0, 0.5,0.5 // 目
alCopyImageToImage 2,1, 0,0, 385, 128, 640, 512+blink*128
alTransModeOffsetRotateZoom 123,271, 0, 0.7, 0.7 // 口描画
alColor 212,140,143 : alFillClosedCurve vC, 4, 0.8 // 口の塗
alColor : alDrawClosedCurve vC, 4, 0.8 // 口の線
// ★オマケ:ちょっとゆらゆらと呼吸(背景のリアルタイム透過が分かり易い様に)
alSelectImage 0 : alErase
angle = 0.05*cos(0.1*tick) : scale = 0.98 + 0.02*sin(0.025*tick)
alExTransModeOffsetRotateZoomAt 0,0, angle, scale*reverse,scale, 160, 512
alCopyImageToImage 1,0, 0,0, 320, 512
// ★画面更新と待ち
alLayeredWindow 0, opacity, ginfo_wx1, ginfo_wy1, 320, 512 ; ★追加命令: Image ID, 透明度, スクリーンX, Y, ウインドウW, H
tick++ : await 16
goto *MAIN

| |
|
2025/7/19(Sat) 12:50:42|NO.103713
> usagi さん
ウィンドウにレイヤードウィンドウ型というものがあるということを初めて知りました!
あんまり見ない珍しい形式のウィンドウだなぁと思いまして、
そういえば、ミュージックプレイヤーのツールに特殊な形状のウィンドウがあることを思い出しまして、
こうやって透明度が入ったウィンドウが実装されていたんだなぁと思いました。
あと、a2dモジュールから直接データを取得してきて
別形式のウィンドウに対応させるという発想は自分には足りなくて、
ビットマップとしてコピーしてしまえば、できるものなんだなぁと勉強になりました。
できないと思い込むのは良くないなと思いました。
実はこの透明度の入ったウィンドウはやりたいと思っていたのですが、
a2dの性質上、できないんだろうなと思い込んでいて、結果的に質問しなかったのです。
こういう拡張のアプローチもあるんだなぁと、とても勉強になりました! ありがとうございます!(^v^)
|
|