diff --git a/CHANGELOG.md b/CHANGELOG.md index 75873e1..512a8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [4-r.3-beta.1] - 2021-05-13 + +### Added + +* Add the sample to manipulate the lip-sync from a waveform on the audio file(.wav). +* Add sample voices to `Haru`. + + ## [4-r.2] - 2021-03-09 ### Added @@ -73,7 +81,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Fix issue with reloading model images in WebKit. - +[4-r.3-beta.1]: https://github.com/Live2D/CubismWebSamples/compare/4-r.2...4-r.3-beta.1 [4-r.2]: https://github.com/Live2D/CubismWebSamples/compare/4-r.1...4-r.2 [4-r.1]: https://github.com/Live2D/CubismWebSamples/compare/4-beta.2...4-r.1 [4-beta.2]: https://github.com/Live2D/CubismWebSamples/compare/4-beta.1...4-beta.2 diff --git a/Core/CHANGELOG.md b/Core/CHANGELOG.md index 0faaa1c..18f3f33 100644 --- a/Core/CHANGELOG.md +++ b/Core/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## 2021-03-09 + +### Added +* Add funtcions for Viewer. + + * `csmGetParameterKeyCounts` + * `csmGetParameterKeyValues` + +### Changed + +* Update Core version to `04.01.0000`. + + ## 2020-01-30 ### Added diff --git a/Framework b/Framework index 4b2130b..8ac72d4 160000 --- a/Framework +++ b/Framework @@ -1 +1 @@ -Subproject commit 4b2130bca984e1a5c3714fbe1fae201bc4cff452 +Subproject commit 8ac72d40bc0a22eb06742c7062614457589bc822 diff --git a/LICENSE.md b/LICENSE.md index 8e34488..ffb3ffe 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -6,6 +6,7 @@ Cubism Web Samples is included in Live2D Cubism Components. Cubism Web Samples は Live2D Cubism Components に含まれます。 +Cubism Web Samples 包括在 Live2D Cubism Components 中。 ## Cubism SDK Release License @@ -17,6 +18,9 @@ Cubism Web Samples は Live2D Cubism Components に含まれます。 * [Cubism SDK リリースライセンス](https://www.live2d.com/ja/download/cubism-sdk/release-license/) +如果您的企业在最近一个会计年度的销售额达到或超过1000万日元,您必须得到Cubism SDK的出版授权许可(出版许可协议)。 + +* [Cubism SDK发行许可证](https://www.live2d.com/zh-CHS/download/cubism-sdk/release-license/) ## Live2D Open Software License @@ -24,6 +28,7 @@ Live2D Cubism Components is available under Live2D Open Software License. * [Live2D Open Software License](https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html) * [Live2D Open Software 使用許諾契約書](https://www.live2d.com/eula/live2d-open-software-license-agreement_jp.html) +* [Live2D Open Software 使用授权协议](https://www.live2d.com/eula/live2d-open-software-license-agreement_cn.html) ## Live2D Proprietary Software License @@ -32,6 +37,7 @@ Live2D Cubism Core is available under Live2D Proprietary Software License. * [Live2D Proprietary Software License Agreement](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html) * [Live2D Proprietary Software 使用許諾契約書](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_jp.html) +* [Live2D Proprietary Software 使用授权协议](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_cn.html) ## Free Material License @@ -40,6 +46,7 @@ Live2D models listed below are available under Free Material License. * [Free Material License Agreement](https://www.live2d.com/eula/live2d-free-material-license-agreement_en.html) * [無償提供マテリアルの使用許諾契約書](https://www.live2d.com/eula/live2d-free-material-license-agreement_jp.html) +* [无偿提供素材使用授权协议](https://www.live2d.com/eula/live2d-free-material-license-agreement_cn.html) ``` Samples/Resources/Haru diff --git a/README.md b/README.md index 50a5a5f..38a82ff 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ -# Cubism Web Samples +# \[Beta Version\] Cubism Web Samples Live2D Cubism 4 Editor で出力したモデルを表示するアプリケーションのサンプル実装です。 Cubism Web Framework および Live2D Cubism Core と組み合わせて使用します。 +**本 SDK は、 Beta バージョンとなります。先行して新機能を取り込んでいるため、不安定な挙動を示す場合がございます。安定した製品をお求めの方は、公式サイトから配布されている正式版のパッケージ又は `develop` `master` ブランチをご利用ください。** + +**[Beta Version] の SDK の不具合、各種ご意見等に関しましては、 Live2D コミュニティ にてご連絡ください。直接のコードに対する指摘、修正等は、直接 Pull requests としてご投稿ください。** + ## ライセンス @@ -80,10 +84,10 @@ NOTE: デバック用の設定は、`.vscode/launch.json` に記述していま ### Node.js -* 15.11.0 -* 14.16.0 -* 12.21.0 -* 10.24.0 +* 15.14.0 +* 14.16.1 +* 12.22.1 +* 10.24.1 ## 動作確認環境 diff --git a/Samples/Resources/Haru/Haru.model3.json b/Samples/Resources/Haru/Haru.model3.json index 55bbe5a..00b148f 100644 --- a/Samples/Resources/Haru/Haru.model3.json +++ b/Samples/Resources/Haru/Haru.model3.json @@ -23,10 +23,14 @@ {"File":"motions/haru_g_m15.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5} ], "TapBody": [ - {"File":"motions/haru_g_m06.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5}, - {"File":"motions/haru_g_m09.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5}, - {"File":"motions/haru_g_m20.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5}, - {"File":"motions/haru_g_m26.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5} + {"File":"motions/haru_g_m06.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5 + ,"Sound": "sounds/haru_normal_01.wav"}, + {"File":"motions/haru_g_m09.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5 + ,"Sound": "sounds/haru_normal_02.wav"}, + {"File":"motions/haru_g_m20.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5 + ,"Sound": "sounds/haru_normal_03.wav"}, + {"File":"motions/haru_g_m26.motion3.json" ,"FadeInTime":0.5, "FadeOutTime":0.5 + ,"Sound": "sounds/haru_normal_04.wav"} ] }, "UserData": "Haru.userdata3.json" @@ -52,4 +56,4 @@ {"Name":"Head", "Id":"HitArea"}, {"Name":"Body", "Id":"HitArea2"} ] -} \ No newline at end of file +} diff --git a/Samples/Resources/Haru/sounds/haru_normal_01.wav b/Samples/Resources/Haru/sounds/haru_normal_01.wav new file mode 100644 index 0000000..13cb456 Binary files /dev/null and b/Samples/Resources/Haru/sounds/haru_normal_01.wav differ diff --git a/Samples/Resources/Haru/sounds/haru_normal_02.wav b/Samples/Resources/Haru/sounds/haru_normal_02.wav new file mode 100644 index 0000000..cbd3db5 Binary files /dev/null and b/Samples/Resources/Haru/sounds/haru_normal_02.wav differ diff --git a/Samples/Resources/Haru/sounds/haru_normal_03.wav b/Samples/Resources/Haru/sounds/haru_normal_03.wav new file mode 100644 index 0000000..cd72bc8 Binary files /dev/null and b/Samples/Resources/Haru/sounds/haru_normal_03.wav differ diff --git a/Samples/Resources/Haru/sounds/haru_normal_04.wav b/Samples/Resources/Haru/sounds/haru_normal_04.wav new file mode 100644 index 0000000..7d0d617 Binary files /dev/null and b/Samples/Resources/Haru/sounds/haru_normal_04.wav differ diff --git a/Samples/TypeScript/Demo/src/lappmodel.ts b/Samples/TypeScript/Demo/src/lappmodel.ts index 6112d3d..aee725a 100644 --- a/Samples/TypeScript/Demo/src/lappmodel.ts +++ b/Samples/TypeScript/Demo/src/lappmodel.ts @@ -38,6 +38,7 @@ import * as LAppDefine from './lappdefine'; import { canvas, frameBuffer, gl, LAppDelegate } from './lappdelegate'; import { LAppPal } from './lapppal'; import { TextureInfo } from './lapptexturemanager'; +import { LAppWavFileHandler } from './lappwavfilehandler'; enum LoadStep { LoadAssets, @@ -507,7 +508,10 @@ export class LAppModel extends CubismUserModel { // リップシンクの設定 if (this._lipsync) { - const value = 0; // リアルタイムでリップシンクを行う場合、システムから音量を取得して、0~1の範囲で値を入力します。 + let value = 0.0; // リアルタイムでリップシンクを行う場合、システムから音量を取得して、0~1の範囲で値を入力します。 + + this._wavFileHandler.update(deltaTimeSeconds); + value = this._wavFileHandler.getRms(); for (let i = 0; i < this._lipSyncIds.getSize(); ++i) { this._model.addParameterValueById(this._lipSyncIds.at(i), value, 0.8); @@ -583,6 +587,14 @@ export class LAppModel extends CubismUserModel { motion.setFinishedMotionHandler(onFinishedMotionHandler); } + //voice + const voice = this._modelSetting.getMotionSoundFileName(group, no); + if (voice.localeCompare('') != 0) { + let path = voice; + path = this._modelHomeDir + path; + this._wavFileHandler.start(path); + } + if (this._debugMode) { LAppPal.printMessage(`[APP]start motion: [${group}_${no}`); } @@ -843,6 +855,7 @@ export class LAppModel extends CubismUserModel { this._textureCount = 0; this._motionCount = 0; this._allMotionCount = 0; + this._wavFileHandler = new LAppWavFileHandler(); } _modelSetting: ICubismModelSetting; // モデルセッティング情報 @@ -870,4 +883,5 @@ export class LAppModel extends CubismUserModel { _textureCount: number; // テクスチャカウント _motionCount: number; // モーションデータカウント _allMotionCount: number; // モーション総数 + _wavFileHandler: LAppWavFileHandler; //wavファイルハンドラ } diff --git a/Samples/TypeScript/Demo/src/lappwavfilehandler.ts b/Samples/TypeScript/Demo/src/lappwavfilehandler.ts new file mode 100644 index 0000000..fcca346 --- /dev/null +++ b/Samples/TypeScript/Demo/src/lappwavfilehandler.ts @@ -0,0 +1,388 @@ +/** + * Copyright(c) Live2D Inc. All rights reserved. + * + * Use of this source code is governed by the Live2D Open Software license + * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. + */ + +import { LAppPal } from './lapppal'; + +export let s_instance: LAppWavFileHandler = null; + +export class LAppWavFileHandler { + /** + * クラスのインスタンス(シングルトン)を返す。 + * インスタンスが生成されていない場合は内部でインスタンスを生成する。 + * + * @return クラスのインスタンス + */ + public static getInstance(): LAppWavFileHandler { + if (s_instance == null) { + s_instance = new LAppWavFileHandler(); + } + + return s_instance; + } + + /** + * クラスのインスタンス(シングルトン)を解放する。 + */ + public static releaseInstance(): void { + if (s_instance != null) { + s_instance = void 0; + } + + s_instance = null; + } + + public update(deltaTimeSeconds: number) { + let goalOffset: number; + let rms: number; + + // データロード前/ファイル末尾に達した場合は更新しない + if ( + this._pcmData == null || + this._sampleOffset >= this._wavFileInfo._samplesPerChannel + ) { + this._lastRms = 0.0; + return false; + } + + // 経過時間後の状態を保持 + this._userTimeSeconds += deltaTimeSeconds; + goalOffset = Math.floor( + this._userTimeSeconds * this._wavFileInfo._samplingRate + ); + if (goalOffset > this._wavFileInfo._samplesPerChannel) { + goalOffset = this._wavFileInfo._samplesPerChannel; + } + + // RMS計測 + rms = 0.0; + for ( + let channelCount = 0; + channelCount < this._wavFileInfo._numberOfChannels; + channelCount++ + ) { + for ( + let sampleCount = this._sampleOffset; + sampleCount < goalOffset; + sampleCount++ + ) { + const pcm = this._pcmData[channelCount][sampleCount]; + rms += pcm * pcm; + } + } + rms = Math.sqrt( + rms / + (this._wavFileInfo._numberOfChannels * + (goalOffset - this._sampleOffset)) + ); + + this._lastRms = rms; + this._sampleOffset = goalOffset; + return true; + } + + public start(filePath: string): void { + // サンプル位参照位置を初期化 + this._sampleOffset = 0; + this._userTimeSeconds = 0.0; + + // RMS値をリセット + this._lastRms = 0.0; + + if (!this.loadWavFile(filePath)) { + return; + } + } + + public getRms(): number { + return this._lastRms; + } + + public loadWavFile(filePath: string): boolean { + let ret = false; + + if (this._pcmData != null) { + this.releasePcmData(); + } + + // ファイルロード + const asyncFileLoad = async () => { + return fetch(filePath).then(responce => { + return responce.arrayBuffer(); + }); + }; + + const asyncWavFileManager = (async () => { + this._byteReader._fileByte = await asyncFileLoad(); + this._byteReader._fileDataView = new DataView(this._byteReader._fileByte); + this._byteReader._fileSize = this._byteReader._fileByte.byteLength; + this._byteReader._readOffset = 0; + + // ファイルロードに失敗しているか、先頭のシグネチャ"RIFF"を入れるサイズもない場合は失敗 + if ( + this._byteReader._fileByte == null || + this._byteReader._fileSize < 4 + ) { + return false; + } + + // ファイル名 + this._wavFileInfo._fileName = filePath; + + try { + // シグネチャ "RIFF" + if (!this._byteReader.getCheckSignature('RIFF')) { + ret = false; + throw new Error('Cannot find Signeture "RIFF".'); + } + // ファイルサイズ-8(読み飛ばし) + this._byteReader.get32LittleEndian(); + // シグネチャ "WAVE" + if (!this._byteReader.getCheckSignature('WAVE')) { + ret = false; + throw new Error('Cannot find Signeture "WAVE".'); + } + // シグネチャ "fmt " + if (!this._byteReader.getCheckSignature('fmt ')) { + ret = false; + throw new Error('Cannot find Signeture "fmt".'); + } + // fmtチャンクサイズ + const fmtChunkSize = this._byteReader.get32LittleEndian(); + // フォーマットIDは1(リニアPCM)以外受け付けない + if (this._byteReader.get16LittleEndian() != 1) { + ret = false; + throw new Error('File is not linear PCM.'); + } + // チャンネル数 + this._wavFileInfo._numberOfChannels = this._byteReader.get16LittleEndian(); + // サンプリングレート + this._wavFileInfo._samplingRate = this._byteReader.get32LittleEndian(); + // データ速度[byte/sec](読み飛ばし) + this._byteReader.get32LittleEndian(); + // ブロックサイズ(読み飛ばし) + this._byteReader.get16LittleEndian(); + // 量子化ビット数 + this._wavFileInfo._bitsPerSample = this._byteReader.get16LittleEndian(); + // fmtチャンクの拡張部分の読み飛ばし + if (fmtChunkSize > 16) { + this._byteReader._readOffset += fmtChunkSize - 16; + } + // "data"チャンクが出現するまで読み飛ばし + while ( + !this._byteReader.getCheckSignature('data') && + this._byteReader._readOffset < this._byteReader._fileSize + ) { + this._byteReader._readOffset += + this._byteReader.get32LittleEndian() + 4; + } + // ファイル内に"data"チャンクが出現しなかった + if (this._byteReader._readOffset >= this._byteReader._fileSize) { + ret = false; + throw new Error('Cannot find "data" Chunk.'); + } + // サンプル数 + { + const dataChunkSize = this._byteReader.get32LittleEndian(); + this._wavFileInfo._samplesPerChannel = + (dataChunkSize * 8) / + (this._wavFileInfo._bitsPerSample * + this._wavFileInfo._numberOfChannels); + } + // 領域確保 + this._pcmData = new Array(this._wavFileInfo._numberOfChannels); + for ( + let channelCount = 0; + channelCount < this._wavFileInfo._numberOfChannels; + channelCount++ + ) { + this._pcmData[channelCount] = new Float32Array( + this._wavFileInfo._samplesPerChannel + ); + } + // 波形データ取得 + for ( + let sampleCount = 0; + sampleCount < this._wavFileInfo._samplesPerChannel; + sampleCount++ + ) { + for ( + let channelCount = 0; + channelCount < this._wavFileInfo._numberOfChannels; + channelCount++ + ) { + this._pcmData[channelCount][sampleCount] = this.getPcmSample(); + } + } + + ret = true; + } catch (e) { + console.log(e); + } + })(); + + return ret; + } + + public getPcmSample(): number { + let pcm32; + + // 32ビット幅に拡張してから-1~1の範囲に丸める + switch (this._wavFileInfo._bitsPerSample) { + case 8: + pcm32 = this._byteReader.get8() - 128; + pcm32 <<= 24; + break; + case 16: + pcm32 = this._byteReader.get16LittleEndian() << 16; + break; + case 24: + pcm32 = this._byteReader.get24LittleEndian() << 8; + break; + default: + // 対応していないビット幅 + pcm32 = 0; + break; + } + + return pcm32 / 2147483647; //Number.MAX_VALUE; + } + + public releasePcmData(): void { + for ( + let channelCount = 0; + channelCount < this._wavFileInfo._numberOfChannels; + channelCount++ + ) { + delete this._pcmData[channelCount]; + } + delete this._pcmData; + this._pcmData = null; + } + + constructor() { + this._pcmData = null; + this._userTimeSeconds = 0.0; + this._lastRms = 0.0; + this._sampleOffset = 0.0; + this._wavFileInfo = new WavFileInfo(); + this._byteReader = new ByteReader(); + } + + _pcmData: Array; + _userTimeSeconds: number; + _lastRms: number; + _sampleOffset: number; + _wavFileInfo: WavFileInfo; + _byteReader: ByteReader; + _loadFiletoBytes = (arrayBuffer: ArrayBuffer, length: number): void => { + this._byteReader._fileByte = arrayBuffer; + this._byteReader._fileDataView = new DataView(this._byteReader._fileByte); + this._byteReader._fileSize = length; + }; +} + +export class WavFileInfo { + constructor() { + this._fileName = ''; + this._numberOfChannels = 0; + this._bitsPerSample = 0; + this._samplingRate = 0; + this._samplesPerChannel = 0; + } + + _fileName: string; ///< ファイル名 + _numberOfChannels: number; ///< チャンネル数 + _bitsPerSample: number; ///< サンプルあたりビット数 + _samplingRate: number; ///< サンプリングレート + _samplesPerChannel: number; ///< 1チャンネルあたり総サンプル数 +} + +export class ByteReader { + constructor() { + this._fileByte = null; + this._fileDataView = null; + this._fileSize = 0; + this._readOffset = 0; + } + + /** + * @brief 8ビット読み込み + * @return Csm::csmUint8 読み取った8ビット値 + */ + public get8(): number { + const ret = this._fileDataView.getUint8(this._readOffset); + this._readOffset++; + return ret; + } + + /** + * @brief 16ビット読み込み(リトルエンディアン) + * @return Csm::csmUint16 読み取った16ビット値 + */ + public get16LittleEndian(): number { + const ret = + (this._fileDataView.getUint8(this._readOffset + 1) << 8) | + this._fileDataView.getUint8(this._readOffset); + this._readOffset += 2; + return ret; + } + + /** + * @brief 24ビット読み込み(リトルエンディアン) + * @return Csm::csmUint32 読み取った24ビット値(下位24ビットに設定) + */ + public get24LittleEndian(): number { + const ret = + (this._fileDataView.getUint8(this._readOffset + 2) << 16) | + (this._fileDataView.getUint8(this._readOffset + 1) << 8) | + this._fileDataView.getUint8(this._readOffset); + this._readOffset += 3; + return ret; + } + + /** + * @brief 32ビット読み込み(リトルエンディアン) + * @return Csm::csmUint32 読み取った32ビット値 + */ + public get32LittleEndian(): number { + const ret = + (this._fileDataView.getUint8(this._readOffset + 3) << 24) | + (this._fileDataView.getUint8(this._readOffset + 2) << 16) | + (this._fileDataView.getUint8(this._readOffset + 1) << 8) | + this._fileDataView.getUint8(this._readOffset); + this._readOffset += 4; + return ret; + } + + /** + * @brief シグネチャの取得と参照文字列との一致チェック + * @param[in] reference 検査対象のシグネチャ文字列 + * @retval true 一致している + * @retval false 一致していない + */ + public getCheckSignature(reference: string): boolean { + const getSignature: Uint8Array = new Uint8Array(4); + const referenceString: Uint8Array = new TextEncoder().encode(reference); + if (reference.length != 4) { + return false; + } + for (let signatureOffset = 0; signatureOffset < 4; signatureOffset++) { + getSignature[signatureOffset] = this.get8(); + } + return ( + getSignature[0] == referenceString[0] && + getSignature[1] == referenceString[1] && + getSignature[2] == referenceString[2] && + getSignature[3] == referenceString[3] + ); + } + + _fileByte: ArrayBuffer; ///< ロードしたファイルのバイト列 + _fileDataView: DataView; + _fileSize: number; ///< ファイルサイズ + _readOffset: number; ///< ファイル参照位置 +}