SDK for Web:モーション開始に音声再生を紐づける方法

皆様、大変お世話になっております。

SDK for Webで、モーション開始に音声再生を紐づける方法はないでしょうか?
使用しているSDKは4.0です。

ループするモーションに音声を付けたいのですが、
実装時にモーションのループと音声のループを別々の処理にしてしまうと、
モーションの長さと音声の長さが完全に同じでないと、ループごとに音ズレしていってしまいます。

しかしモーション開始に音声再生が紐づけられれば、
音声ファイルの長さに気を使う必要がなくなります。

Cubism2.1の頃は、SDKに音声再生機構が実装されていたので
とても簡単に紐づけできたのですが、Cubism4.0はその機構がない為、困っております。

本件について、旧コミュニティにて関連する質問を発見しました。

こちらに対するLive2Dスタッフ様のご回答について、

Cubism 4 SDK for Web のSample上では特定のモーション実行時に
音声再生するような実装はなく、
ユーザー様自身でカスタマイズしていただく必要がございます。
・SDK側で効果音を後からモーションに紐付けるようにする実装が必要
→任意の音声ファイルを何らかのライブラリAPIなどで取り込み、
特定のモーションを再生させる際に音声再生させる

との事でしたが、当方知識が不足しておりこれだけでは理解できず問題解決に至りませんでした。
「ユーザー自身で行うカスタマイズ」の、具体的な方法をご教示頂けないでしょうか。

何卒、よろしくお願いいたします。

こんばんは。
SDK for Webではモデルをタップするとモーションを再生する関数が走り、モーションが再生されます。
なので、その関数内でTypeScriptの音声再生メソッドを呼び出せばいいのかなと思うのですが、いかがでしょうか?
私はTypeScriptの知識がありませんのでGPTに聞いてみました。

TypeScriptで音声を再生させる方法はいくつかありますが、一般的にはJavaScriptのAudioオブジェクトを使用します。このオブジェクトは、音楽ファイルの再生や停止、音量調整などの機能を提供していますSource 4

まず、Audioオブジェクトを作成し、そのsrcプロパティに音声ファイルのURLを設定します。次に、playメソッドを呼び出すことで音声を再生します。以下にその基本的なコード例を示します。

let audio = new Audio('audio_file.mp3');
audio.play();

また、pauseメソッドを使用すると音声を一時停止できます。loopプロパティをtrueに設定すると、音声が終了したときに自動的に再生を開始しますSource 4

audio.pause();
audio.loop = true;

また、ローカルの音声ファイルを再生する場合は、File APIを使用します。File APIを使うと、ユーザーが選択した音声ファイルのデータを取得できます。その後、FileReaderオブジェクトを使用して、取得したデータをAudioオブジェクトのsrcプロパティに設定しますSource 5

let fileInput = document.getElementById('file1') as HTMLInputElement;
let audio = new Audio();

fileInput.addEventListener('change', (event) => {
 let file = event.target.files[0];
 if (!file.type.match('audio.*')) {
   alert("音声ファイルを選択してください。");
   return;
 }
 let reader = new FileReader();
 reader.onload = () => {
   audio.src = reader.result as string;
 };
 reader.readAsDataURL(file);
});

function play() {
 audio.play();
}

このコードでは、ユーザーがファイルを選択すると、そのファイルのデータを取得し、そのデータをAudioオブジェクトのsrcプロパティに設定しています。そして、play関数を呼び出すと、選択した音声ファイルが再生されます。

これらの方法を使用することで、TypeScriptで音声を再生することができます。


そのまま貼り付けましたが、実際にAudioは存在し、呼び出せるっぽいです。
お試しください。

「いいね!」 1

ご回答ありがとうございます。

モーションを再生させる命令(関数?)が記述されている箇所に、
音声再生の命令を追加する・・・というような事でしょうか?

すみません。その「モーションを再生する関数」はどこに記述されているのでしょうか・・・?
当方、プログラミング的な知識が無いため判別できず・・・申し訳ありません。

SDKから音声を再生する為に、

☆【2.1】
Viewerで音声ファイルを設定するだけでOK
→知識不要、誰でも使えるレベル。

だったものが・・・

☆【3.0以降】
自分でSDKをカスタムしてね(引用の通り、公式回答)
→突如、専門知識が要求される。

・・・となってしまい、
「唐突なレベル差についていけず取り残された」
というユーザーです。故に知識がなく・・・重ね重ね申し訳ありません。

なるほど、2.1ではViewerで設定するだけで簡単に紐付けられたのですね。
プログラミング知識がなかったら確かに難しいかと思います。

では以下のようにコーディングしてみてください。
Samples/TypeScript/Demo/src の中にlapplive2dmanager.tsというファイルがあります。
その中にonTap()という関数があるはずです。
その中の処理を以下のように変えてみてください。
追加したコードは2行だけです。

public onTap(x: number, y: number): void {
    if (LAppDefine.DebugLogEnable) {
      LAppPal.printMessage(
        `[APP]tap point: {x: ${x.toFixed(2)} y: ${y.toFixed(2)}}`
      );
    }

    for (let i = 0; i < this._models.getSize(); i++) {
      if (this._models.at(i).hitTest(LAppDefine.HitAreaNameHead, x, y)) {
        if (LAppDefine.DebugLogEnable) {
          LAppPal.printMessage(
            `[APP]hit area: [${LAppDefine.HitAreaNameHead}]`
          );
        }
        this._models.at(i).setRandomExpression();
      } else if (this._models.at(i).hitTest(LAppDefine.HitAreaNameBody, x, y)) {
        if (LAppDefine.DebugLogEnable) {
          LAppPal.printMessage(
            `[APP]hit area: [${LAppDefine.HitAreaNameBody}]`
          );
        }
        this._models
          .at(i)
          .startRandomMotion(
            LAppDefine.MotionGroupTapBody,
            LAppDefine.PriorityNormal,
            this._finishedMotion
          );

          // 追加コード
          let audio = new Audio("../../Resources/Haru/sounds/haru_Info_14.wav");
          audio.play();
      }
    }
  }

SDK for Webが持つサンプルモデル「ハル」は音声を持つのでそれを再生するようにしてみました。
お持ちの音声を再生したい場合は、適宜追加コードを書き換えてみてください。

このコードではモデルの胴体をタップしたときにランダムにモーションが選ばれて再生されるのですが、モーションの再生と同時に音声が流れるようになっています。

重ね重ね申し上げますが当方TypeScript, JavaScriptの知識はほぼ皆無ですので、微妙なコードかもしれません。
以上、よろしくお願いいたします。

「いいね!」 2

具体的なコード(というのでしょうか?)を記載して頂き、有難う御座います。
ご返信を参考にし、SDKから実際に音声を流すことができる事を理解し、
音声を流す為の命令(関数?)について知る事が出来ました。

ただ、私のやりたかった事について、すみません、
いま見直すと私の最初の書き込みに情報が不足しており、分かりづらかったと思います。

以下、詳細です。

例えば自作モデル「A.moc3」に対して、モーションを複数用意します。

A001.motion3
A002.motion3
A003.motion3


そして、モーション1つにつき音声も1つずつ用意します。

A001.ogg
A002.ogg
A003.ogg


これを、

A001.motion3ーA001.ogg
A002.motion3ーA002.ogg
A003.motion3ーA003.ogg


という様に紐づけて、
『各モーションの開始』に合わせて、対応した音声を再生したいのです。
ループするモーションの場合は、ループする時にも音声を再度再生したいです。

こうなってくると、プログラム的には結構複雑でしょうか・・・?

3.0以降のViewerにも「音声」項目があり、音声ファイルを登録する事自体は出来ます。
model3.json内にも、

“Motions”: {
“A001”: [
{
“File”: “motions/A001.motion3.json”,
“Sound”: “sounds/A001.ogg”
}
}

のように記録もされます。
しかし、Viewer上では再生できますが、SDKの利用先では再生されません。

このmodel3.jsonの"Sound"の記録を参照して、各音声を各モーション開始に合わせて
再生するような方法があればいいのですが・・・(2.1はまさにそのような方式だったのだと思います)

こちらこそ意図を読みきれずにすみません。
確かにViewer上で紐づけたときはmodel3.jsonに記録されますね。
SDKには紐付けられた音声ファイルの名前を取ってくる機能があります。これを使えばいけるはずですね。
以下にとりあえず書いてみたコードを示します。(参考までに、「コード」というのは「ソースコード」の省略形です。)

先程、lapplive2dmanager.tsというファイル内にコードを書いてもらったと思いますが削除してください。
そして、以下のコードの処理を、lappmodel.tsの中にある、startRandomMotion()という関数の処理と差し替えてください。
もしくは、以下に示したコード内の新しく追加した部分だけコピペしてみてください。
(追加コード、追加コードここまで、で挟まれているコードをコピペする)

public startRandomMotion(
    group: string,
    priority: number,
    onFinishedMotionHandler?: FinishedMotionCallback
  ): CubismMotionQueueEntryHandle {
    if (this._modelSetting.getMotionCount(group) == 0) {
      return InvalidMotionQueueEntryHandleValue;
    }

    const no: number = Math.floor(
      Math.random() * this._modelSetting.getMotionCount(group)
    );

    // 追加コード
    const voice = this._modelSetting.getMotionSoundFileName(group, no);
    if (voice.localeCompare('') != 0) {
      let path = voice;
      path = this._modelHomeDir + path;

      let audio = new Audio(path);
      audio.play();
    }
    // 追加コードここまで

    return this.startMotion(group, no, priority, onFinishedMotionHandler);
  }

これで正しく紐づけされたモーションが再生されるかなーと思います。
ただ、ループモーションの場合はどうなるか分かりません。

これでいかがでしょうか…?

「いいね!」 1

ご回答いただき、ありがとうございます。

>「コード」というのは「ソースコード」の省略形です。
こういった事を、親切に教えて頂けるのは知識のない者として大変助かります。
解説サイト等を見ても「分かってる」前提で進むことが多いので・・・ありがとうございます。

ご教示頂いたコードを追加した所、
実際にsoundの記録を参照して音声が再生される事が確認できました。
こちらもありがとうございます。

自分一人では、そもそもsoud参照が可能なのか自体が分からなかったので、
ご回答頂けて大変参考になりました。

またも初歩的な質問で申し訳ないのですが、
もし可能であれば、追加コードの行ごとにどのような処理を行っているのか
簡単な内容でも大丈夫ですので、ご教示頂く事はできますでしょうか。

ご検討いただければ幸いで御座います。

「いいね!」 1

上手くいったようでよかったです!

>こういった事を、親切に教えて頂けるのは知識のない者として大変助かります。
いえいえ。最初はみんなゼロからですからね。大丈夫です。

>またも初歩的な質問で申し訳ないのですが、
>もし可能であれば、追加コードの行ごとにどのような処理を行っているのか
>簡単な内容でも大丈夫ですので、ご教示頂く事はできますでしょうか。
はい、もちろん大丈夫です!
以下、1行ずつ説明します。

const voice = this._modelSetting.getMotionSoundFileName(group, no);

=の右側の処理は、model3.jsonからSoundに設定されたファイルパス(ファイルの置き場所)を取ってくる処理です。
group, no と2つの引数(値)を入れて情報を取ってきます。例えばサンプルモデルのHaru.model3.jsonの場合は

"Motions": {
			"Idle": [
				{
					"File": "motions/haru_g_idle.motion3.json",
					"FadeInTime": 0.5,
					"FadeOutTime": 0.5
				},
				{
					"File": "motions/haru_g_m15.motion3.json",
					"FadeInTime": 0.5,
					"FadeOutTime": 0.5
				}
			],
			"TapBody": [
				{
					"File": "motions/haru_g_m26.motion3.json",
					"FadeInTime": 0.5,
					"FadeOutTime": 0.5,
					"Sound": "sounds/haru_talk_13.wav"
				},
				{
					"File": "motions/haru_g_m06.motion3.json",
					"FadeInTime": 0.5,
					"FadeOutTime": 0.5,
					"Sound": "sounds/haru_Info_14.wav"
				},
				{
					"File": "motions/haru_g_m20.motion3.json",
					"FadeInTime": 0.5,
					"FadeOutTime": 0.5,
					"Sound": "sounds/haru_normal_6.wav"
				},
				{
					"File": "motions/haru_g_m09.motion3.json",
					"FadeInTime": 0.5,
					"FadeOutTime": 0.5,
					"Sound": "sounds/haru_Info_04.wav"
				}
			]
		},

というmodel3.jsonになっていますが、この場合はgroupに"TapBody", noに0と入れることで、"sound/haru_talk_13.wav"というファイルパスが取ってこれます。
=の左側const voiceはvoiceという名前で変数を定義しています。簡単にいうと値(この場合は音声のファイルパス)に名前を付けているということです。

if (voice.localeCompare(‘’) != 0) {

これは取ってきた音声のファイルパスが空でなかった場合に音声を再生する処理を行うということを表しています。つまり、モーションと音声の紐づけがされていなかった場合はスルーするということです。

let path = voice;
path = this._modelHomeDir + path;

ファイルパスに変換しています。
voice変数に入れられたファイルパスはあくまでもmodel3.jsonから見た相対的なファイルパスです。なので、プログラム上で認識できるようなファイルパスに変換しています。
(難しかったらすみません。言ってください。)

let audio = new Audio(path);
audio.play()

JavaScriptはAudioという型を使って音声を再生する仕組みを持つようです。
ファイルパスを引数として与えてAudioのインスタンス(説明が難しいですが、Audioという型から具体的なものを作って、それをインスタンスなどと言います。)を作成できます。
そのAudioはplay()という音声を再生させる関数(機能)を持つので、それをaudio.play()で呼び出している、というわけです。

できるだけ初心者向けということを意識して書いてみましたが、それでも分からないところはあるかもしれません。ググったり、もしくはそれでも分からなければ聞いていただければと思います。

「いいね!」 1

とても丁寧なご回答を頂き有難う御座います。

ご回答、初心者に対しても大変分かりやすい内容でした。
例えば「.localeCompare」も、検索すればリファレンスが出てきますが、
ある程度慣れている人向けという内容が多く、見てもいまいち理解できませんでした。
しかし、ご回答と合わせて見て見れば「そう言う事が書いてあったのか」と納得できました。
分かりやすい解説をありがとうございます。

現状のコードは以下の問題があります。

sampleのharuは、モーション完了まで他のモーションが割り込めない様になっていますが、
現状だとタップするごとに、モーションは変わってないのに音声だけ流れてしまいます。
また、音声が流れている所に別の音声が流れると、音声が重なって再生されてしまいます。

こちらの問題に対しても、スクリプトの知識自体が無いため、
やはり自分ひとりでは解決できそうにありません。

しかし、自分が知識を持っていないからと、
こうして一つ一つ全部聞いていては大変に迷惑を掛けてしまうし、
また、キリがないかもしれないとも感じています。

続けてご回答いただければ大変嬉しいです。
しかしこれ以上のご回答が難しいならば、その旨伝えて頂いても大丈夫で御座います。

宜しくお願い致します。

理解できたということでよかったです!

そうですね、例示してらっしゃるlocaleCompare()の公式リファレンスなどはやはり難しいと思います。
たとえプログラミングに慣れていても読みやすいものではないかもですね。

確かに試してみたらそのように再生されてしまいますね。不完全なコードでした。
口パクをモーションに合わせて実行している箇所があったのでそちらに音声再生のコードを持ってくることで解決できそうです。

以下の行(605行目)のすぐ下にこのコードを入れてみてください。

// 音声再生のコードを追加する。
let audio = new Audio(path);
audio.play();

私の手元では連続クリックしても再生されないようになりました。

このくらいなら自分でもすぐ分かるので全然聞いていただいて構いません!

ymさんはデザイナー、モデラーでしょうか?プログラマーでなければ自分でやらないと思うので大変ですね。
答えが分かったらそれでいいという人の方が多いと思うので一行ずつ意味を知りたいというのはなかなかないかなと思います。知識欲がお有りだと思いますので、JavaScriptやTypeScriptを初心者用の本で軽くやってみるといいかもしれません。
今は全く意味の分からない英単語が羅列されているだけに見えているでしょうが、少し学んでみると見え方が変わると思いますよ。お時間なかなか無いと思いますが気が向いたらぜひやってみてください!

「いいね!」 1

引き続きご返信いただき、ありがとうございます。

新たなコードを頂き、こちらもありがとうございます。
現在、テストする時間が取れずまだ試せておりませんが、
いずれ必ず試させて頂きたいと思います。

ご推察の通り、当方は原画・デザイン・モデリングがメインです。
プログラムは専門外の為、今回質問させて頂きました。

モーションと同時に音を出す以外に特にやりたい事は無いので、
それだけの為に時間を使って学ぶのは・・・と思っておりましたが、
SDKをカスタムするのであれば、例え音を出すだけだとしても知識は必要だと
今回のやり取りで実感いたしました。

お勧め頂いたように、TypeScriptの書籍で学習する事も検討いたします。
(いつ時間が取れるか・・・とは思いますが)

ただ、SDK2.1の頃の様に、知識が無い人でも
簡単に音声を扱えるようにして欲しい、という思いもやはりあります。
こちらは「要望」という形で、アンケート等で伝えていきたいと思います。

前述の通り、折角ご回答頂いたのにコードを試せていない現状ですので、
一旦、質問を終了させて頂きたいと思います。

この度は、大変丁寧なご回答を頂き誠に有り難う御座いました。
今後とも宜しくお願い申し上げます。

いえいえ、お時間あるときにお試しいただければと思います。

なるほど、やはりクリエイターの方でしたか。
確かにモーションと同時に音を出す以外にやりたいことがないのであれば書籍で学ぶまでは必要ないと思うのは当然だと思います。
(私は技術者ですがLive2Dモデルを一から作るために絵を描くところから学ぶ、となるとやはり躊躇しますので)

書籍を購入するのにもお金がかかりますし時間も取ってしまいますから、無料で学べるWebサイトなどでさらっと見てもいいかもしれませんね。
ProgateやPaizaというサービスが有名かと思います。気が向いたらどうぞ。
本職があると思いますので本当に気が向いたらで結構です。

私はSDK 2.1の時代を知りませんがViewer上でボタンを押すだけで設定できるのであればそれに越したことはないですね…
ただその仕様だとカスタマイズ性が少なくなるなどの弊害があって今の形になっているのかもしれませんね。そのあたりの背景は分かりませんが。

いえいえ!助けになったようでよかったです。
返信できるとは限りませんが、また何かあればご投稿ください。

「いいね!」 1