TextAlive App API × Three.js Introductory Course

TextAlive

Three.jsと音楽に合わせてタイミングよく歌詞が動くAPI「TextAlive App Api」を使ったアプリケーションの作り方を紹介いたします。

Demo】https://misora.main.jp/ea4azmyb/voice/

Demoのような単語が宙に舞う作例を作れるように、基礎を中心に解説をしようと思います。DemoデータをGitHubをアップしております。解説が不要な方は、以下を参照ください。

GitHub - MisoraRyo/CharVoice-Samp-TA: Voiceアニメーション。TextAlive App APIを使った、歌詞が宙に舞うようにアニメーションするものです。
Voiceアニメーション。TextAlive App APIを使った、歌詞が宙に舞うようにアニメーションするものです。 - MisoraRyo/CharVoice-Samp-TA

TextAlive App API をはじめる前に

ライセンスについて

TextAlive App APIは、規約に則ったアプリの利用料は無償商用利用する場合は事前に相談のこと。

使用する楽曲は、アプリ内で利用できる楽曲や歌詞などは、開発者が適切な権利を有していたり、権利者から許諾を受けていたりする「利用可能コンテンツ」に限られます。とのことなので、事前に楽曲管理者の承諾が別途必要です。ご注意ください。

利用規約 | developer.textalive.jp
https://developer.textalive.jp/terms/

プログラミング・コンテスト

APIを使っていろいろ制作してみたい方!

毎年4月あたりから、初音ミクのマジカルミライで「プログラミング・コンテスト」が行われています。グランプリ曲を使ったWebアプリケーション(リリックアプリ)を募集しております。ぜひチャレンジしてみてはいかがでしょうか?

デモサイトは、コンテスト用の楽曲を使用させていただきました。

初音ミク「マジカルミライ 2024」プログラミング・コンテスト | developer.textalive.jp
初音ミク「マジカルミライ 2024」で、TextAlive App APIを使ってテーマ曲にあわせて魅力的に動く歌詞などの映像演出をプログラミングするコンテストが開催されます。このイベント紹介ページでは、コンテストの概要や応募に役立つ技術情...
https://developer.textalive.jp/events/magicalmirai2024/

TextAlive App APIとは

TextAlive App API は、音楽に合わせてタイミングよく歌詞が動くWebアプリケーション(リリックアプリ)を開発できるJavaScript用のライブラリです。 

1. 開発の始め方 | TextAlive App API | developer.textalive.jp
https://developer.textalive.jp/app/

Web上で公開されている楽曲と歌詞を利用しています。楽曲と歌詞はいずれもURLが TextAlive サービスサイトに登録されている必要があります。コンテストでは、楽曲のURLが登録済みなので、特に気にすることなく利用できます。

TextAlive App APIを使用するには、開発者向けのTokenを取得する必要があります以下のサイトからログインして、開発者登録、新規取得で完了します。コンテストの動画(3:54)に、詳しく解説がありますので、ぜひ参照ください。

開発者情報 | developer.textalive.jp

TextAlive App APIの基礎

TextAlive App Apiの開発者向けTokenを取得できましたでしょうか? 取得しない場合、歌詞情報などを取得できないので、ぜひ取得してください。

まずは、以下の画像のように、console.log上に、歌詞の情報を出力させたものを作成したいと思います。(数値はPosition値、文字は歌詞(文字・単語・フレーズ)になります。)

npm

npm パッケージのインストールしてください

npm install textalive-app-api
HTML

再生・停止ボタンを作ります。

<!--Play&Stop Button-->
<div id="btn-area">
 <button id="Play-Btn">PLAY</button>
 <button id="Stop-Btn">STOP</button>
</div>
Javascript

ここからTextAlive App Apiです。(★★★★★に取得したTokenを入力ください)

import { Player } from "textalive-app-api";
//TextAlive_APi Init
const player = new Player({
    //
    app: { 
      token: "★★★★★",//Token ★★★★★取得したトークンを追加ください!!!★★★★
      parameters: [
      ]
    },
    mediaElement: document.querySelector("#media"),
    vocalAmplitudeEnabled : true,/*歌声の声量*/
    valenceArousalEnabled : true,/*感情値*/

    //fontFamilies: ["kokoro"], // null <= すべてのフォントを読み込む
    //lyricsFetchTimeout:1000, //
    //throttleInterval:10, //アップデート関数の発行間隔をしていする。ミリセカンド。
    //mediaBannerPosition:"top", //音源メディアの情報を表示する位置を指定する。座標指定ではない。
});
//テキストのグローバル変数
let nowChar = "";
let nowWord = "";
let nowPhrase = "";
//曲の長さ&終了処理をする
let endTime = 0;
let voiceEndTime = 0;
//最大声量
let MaxVocal = 0;
let SongVocal = 0; //0~1の値

// リスナの登録 / Register listeners
player.addListener({

    onAppReady: (app) => {
      if (!app.managed) {
        
        // Jsonデータ内にパラメータを格納しております。
        // player.createFromSongUrl( Songs[0].url, Songs[0].data);

        // SUPERHERO / めろくる
        player.createFromSongUrl("https://piapro.jp/t/hZ35/20240130103028", {
          video: {
            // 音楽地図訂正履歴
            beatId: 4592293,
            chordId: 2727635,
            repetitiveSegmentId: 2824326,
            // 歌詞タイミング訂正履歴: https://textalive.jp/lyrics/piapro.jp%2Ft%2FhZ35%2F20240130103028
            lyricId: 59415,
            lyricDiffId: 13962
          }
        });

        
      } else {
        console.log("No app.managed"); 
      }

      if (!app.managed) {}
    },

    onAppMediaChange: (mediaUrl) => {
      console.log("新しい再生楽曲が指定されました:", mediaUrl);
    },

    onVolumeUpdate: (e)=>{
      console.log("Volume", e);
    },
  
    onFontsLoad: (e) =>{/* フォントが読み込めたら呼ばれる */
      console.log("font", e);
    },
  
    onTextLoad: (body) => {/* 楽曲のテキスト情報が取得されたら */
      console.log("onTextLoad",body);
    },
  
    onVideoReady: (video)=> {/* 楽曲情報が取れたら呼ばれる */

      if (!player.app.managed) {

        MaxVocal = player.getMaxVocalAmplitude();
        console.log("最大声量:" + MaxVocal)
        //終了処理のために取得するグローバル変数
        voiceEndTime = player.video.data.endTime;
        endTime = player.video.data.duration;
        console.log("終了時間 VoiceEndTime:" + voiceEndTime);
        console.log("終了時間 duration:" + endTime);
        console.log("FPS:" + player.fps);

      }//END if (!player.app.managed)
  
    },
  
    onTimerReady() {/* 再生コントロールができるようになったら呼ばれる */
      //loadingのテキストの書き換え
      console.log("再生準備ができました");
      
      //再生ボタンのスイッチング
      document.getElementById("Play-Btn").addEventListener("click", () => function(p){  
        if (p.isPlaying){ 
            //再生中
        }else{
            //再生してない
            p.requestPlay();
        }
      }(player));

      //停止ボタンのスイッチング
      document.getElementById("Stop-Btn").addEventListener("click", () => function(p){ 
        if (p.isPlaying){
          //再生中なら
            p.requestStop();
        }else{ 
          //再生してない   
        }
      }(player));

    },
  
    onPlay: () => {/* 再生時に呼ばれる */
      console.log("player.onPlay");
    },
  
    onPause: () => {
      console.log("player.onPause");
      //★初期起動時にpostion値が入るバグ回避
      player.requestStop();//onStopを呼ぶ 
    },
  
    onSeek: () => {
      console.log("player.onSeek");
    },
  
    onStop: () => {
      console.log("player.onStop");
      
      //初期化
      nowChar = "";
      nowWord = "";
      nowPhrase = "";
    },
  
    // Play Loop 
    onTimeUpdate: (position) =>{
      console.log(position);

      /* 歌詞&フレーズ */
      let Char = player.video.findChar(position - 100, { loose: true });
      let Word = player.video.findWord( position - 100, { loose: true });
      let Phrase = player.video.findPhrase( position - 100, { loose: true });
      
      //文字を取得する
      if(nowChar != Char.text){
            nowChar = Char.text;
            console.log(nowChar);
      }//End if(char)

      //単語を取得する
      if(Word){
        if(nowWord != Word.text){
            nowWord = Word.text;
            console.log(nowWord);
        }
      }//End if(Word)
      
      //フレーズを取得する
      if(Phrase) {
        if(nowPhrase != Phrase.text){
            nowPhrase = Phrase.text
            console.log(nowPhrase);
        }
      }//End if(phrase)
      
      //ボーカルの声量を取得する
      //SongVocal = player.getVocalAmplitude(position)/ MaxVocal;
      //console.log(SongVocal);

      //楽曲の長さを100%で表示する
      //positionbarElement.style.width = Math.floor( position ) / endTime * 100 + "%";

    }// End onTimeUpdate
  

});//END player.addListener

HTMLのPlyaボタンを押すと楽曲の曲とTextAliveが開始されます。ブラウザのdeveloerToolを開いて、console.logの値が表示されているればOKです。曲が終了すると自動でLoopが停止します。

コードの中身を簡単に解説すると、

再生が開始されると onTimeUpdate: (position) の関数がLOOPして、positionの値が更新されていきます。

基本的に、ここの箇所から、positionの値を参照して、player.video.findChar()player.video.findWord()player.video.findPhrase()を使い、歌詞の文字・単語・文章を取得して、タイミングよく歌詞を表示させていきます。

そのほかにも、position値からビートを取得するplayer.findBeat()ボーカルの声量取得するplayer.getVocalAmplitude()、などで楽曲のいろいろなパラメータをonTimeUpdate内で取得できます。

よく使う関数

歌詞(フレーズ)を取得:player.video.findPhrase(position, { loose: true })

歌詞(単語)を取得:player.video.findWord(position, { loose: true })

歌詞(文字)を取得:player.video.findChar(position, { loose: true })

歌声の声量を取得:player.getVocalAmplitude(position)

ビートの情報を取得:player.findBeat(position)  or  player.findBeat(position).progress(position)

サビ区間の情報を取得:player.findChorus(player.timer.position)

以上が、TextAlive App APIの基本的な要素になります。

さらに詳しい解説は、公式サイトを参考ください。

1. 開発の始め方 | TextAlive App API | developer.textalive.jp

Three.jsと組み合わせる

Three.jsを使った方法で、歌詞情報を取得させていきたいと思います。

Three.jsを使うので、TextAliveのonTimeUpdate: (position)を使わずに、Three.jsのrenderLoop{requestAnimationFrame(renderLoop)}内で、positionを更新して、TextAliveと連携するように変更したいと思います。

Javascript

TextAlive のonTimeUpdate()は使わないので、削除するもしくはコメントアウトしてください。

// 上記のコードと同じで、
// onTimeUpdate()内のコードを削除する、またはコメントアウトする
//

// リスナの登録 / Register listeners
player.addListener({

    onAppReady: (app) => {
      if (!app.managed) {
        
        // Jsonデータ内にパラメータを格納しております。
        // player.createFromSongUrl( Songs[0].url, Songs[0].data);

        // SUPERHERO / めろくる
        player.createFromSongUrl("https://piapro.jp/t/hZ35/20240130103028", {
          video: {
            // 音楽地図訂正履歴
            beatId: 4592293,
            chordId: 2727635,
            repetitiveSegmentId: 2824326,
            // 歌詞タイミング訂正履歴: https://textalive.jp/lyrics/piapro.jp%2Ft%2FhZ35%2F20240130103028
            lyricId: 59415,
            lyricDiffId: 13962
          }
        });

        
      } else {
        console.log("No app.managed"); 
      }

      if (!app.managed) {}
    },

    onAppMediaChange: (mediaUrl) => {
      console.log("新しい再生楽曲が指定されました:", mediaUrl);
    },


    /*省略*/


    onPlay: () => {/* 再生時に呼ばれる */
      console.log("player.onPlay");
    },
  
    onPause: () => {
      console.log("player.onPause");
      //★初期起動時にpostion値が入るバグ回避
      player.requestStop();//onStopを呼ぶ 
    },
  
    onSeek: () => {
      console.log("player.onSeek");
    },
  
    onStop: () => {
      console.log("player.onStop");
      
      //初期化
      nowChar = "";
      nowWord = "";
      nowPhrase = "";
    },
  
    //再生時に回転する 再生位置の情報が更新されたら呼ばれる */
    // onTimeUpdate: (position) =>{
    //   console.log(position);

    //   /* 歌詞&フレーズ */
    //   let Char = player.video.findChar(position - 100, { loose: true });
    //   let Word = player.video.findWord( position - 100, { loose: true });
    //   let Phrase = player.video.findPhrase( position - 100, { loose: true });
      
    //   //文字を取得する
    //   if(nowChar != Char.text){
    //         nowChar = Char.text;
    //         console.log(nowChar);
    //   }//End if(char)

    //   //単語を取得する
    //   if(Word){
    //     if(nowWord != Word.text){
    //         nowWord = Word.text;
    //         console.log(nowWord);
    //     }
    //   }//End if(Word)
      
    //   //フレーズを取得する
    //   if(Phrase) {
    //     if(nowPhrase != Phrase.text){
    //         nowPhrase = Phrase.text
    //         console.log(nowPhrase);
    //     }
    //   }//End if(phrase)
      
    //   //ボーカルの声量を取得する
    //   //SongVocal = player.getVocalAmplitude(position)/ MaxVocal;
    //   //console.log(SongVocal);

    //   //声量を100%で表示する
    //   //positionbarElement.style.width = Math.floor( position ) / endTime * 100 + "%";

    // }// End onTimeUpdate
  

});//END player.addListener

Three.jsで、必要最低限のSceneとRender、Cameraをセットします。

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
/////////////////////////////////////////////////////////////////////////
///// SCENE CREATION

const scene = new THREE.Scene()
scene.background = new THREE.Color('#eee');

/////////////////////////////////////////////////////////////////////////
///// RENDERER CONFIG

let PixelRation = 1; //PixelRatio
PixelRation = Math.min(window.devicePixelRatio, 2.0);

const renderer = new THREE.WebGLRenderer({
  canvas:document.getElementById("MyCanvas"),
  alpha:true,
  antialias:true,
});
renderer.setPixelRatio(PixelRation) //Set PixelRatio
renderer.setSize(window.innerWidth, window.innerHeight) // Make it FullScreen

/////////////////////////////////////////////////////////////////////////
///// CAMERAS CONFIG

const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 1000)
camera.position.set(0.0, 0.0, 180.0);
scene.add(camera)

/////////////////////////////////////////////////////////////////////////
///// CREATE ORBIT CONTROLS

const controls = new OrbitControls(camera, renderer.domElement)

Three.jsで同様に、console.log上に、歌詞の情報を出力してみようと思います。

/////////////////////////////////////////////////////////////////////////
//// RENDER LOOP FUNCTION
const clock = new THREE.Clock();
//const positionbarElement = document.getElementById("nav-bar");

function renderLoop() {
    //stats.begin();//STATS計測

    const delta = clock.getDelta();//animation programs
    const elapsedTime = clock.getElapsedTime();

    ////////////////////////////////////////
    // TextAlive 

    if(player.isPlaying){
      const position = player.timer.position;

      let Char = player.video.findChar(position - 100, { loose: true });
      let Word = player.video.findWord( position - 100, { loose: true });
      let Phrase = player.video.findPhrase( position - 100, { loose: true });
      
      //文字を取得する
      if(nowChar != Char.text){
            nowChar = Char.text;
            console.log(nowChar);
      }//End if(char)

      //単語を取得する
      if(Word){
        if(nowWord != Word.text){
            nowWord = Word.text;
            console.log(nowWord);
        }
      }//End if(Word)
      
      //フレーズを取得する
      if(Phrase) {
        if(nowPhrase != Phrase.text){
            nowPhrase = Phrase.text
            console.log(nowPhrase);
        }
      }//End if(phrase)

      //再生バーの更新
      //positionbarElement.style.width = Math.floor( position ) / endTime * 100 + "%"; 
    }

    // End TextAlive
    ////////////////////////////////////////

    renderer.render(scene, camera) // render the scene using the camera
    requestAnimationFrame(renderLoop) //loop the render function
    
    //stats.end();//stats計測
}

renderLoop() //start rendering

画像のように、console.logの内容が表示されれば完了です。

再生ボタンを押すと、renderLoop()内のif(player.isPlaying)がTrueになり、中の関数が回りはじめます。これで、TextAliveのonTimeUpdate: (position)と同じような挙動を実現させます。

Three.jsで歌詞オブジェクトを作成して、デモのように表示させる

今回は、歌詞の単語を取得して、その情報から単語の3Dオブジェクトを出力させて、一定時間後に削除するものを作成します。下の画像のようなものになります。

FontのJsonデータからshapeを生成させます。生成方法などの詳細はこちらの記事をを参照ください。

下記のコードを追加してください。(ZenOldMincho_Regular_min.jsonはGithubのものを使用してください)

Javascript
// sample:https://github.com/mrdoob/three.js/blob/master/examples/webgl_geometry_text_shapes.html

import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import Typeface from '../static/ZenOldMincho_Regular_min.json';

// Three.js でテキストを生成するために必要なフォントデータ
const fontLoader = new FontLoader();
const Ffont  = fontLoader.parse(Typeface);
/////////////////////////////////////////////////////////////////////////
///// 文字を表示させる関数
/////

function DisplayWord(string, wordcount, starttime, endtime, PhraseData){

  const String = string;
  const StringNum = wordcount; //未使用
  const StartTime = starttime; 
  const EndTime = endtime;

  //動詞・名詞
  // if( PhraseData._data.pos == "V" || PhraseData._data.pos == "N" ){
  //   radius =  getRandomNum(1, 50)
  // }else{
  // }

  //
  // テキストの形成
  //
  const TEXT = String;
  const shapes = Ffont.generateShapes( TEXT, 6 );//文字サイズ
  const TextGeometry = new THREE.ShapeGeometry( shapes, 4 );
  TextGeometry.computeBoundingBox();
  TextGeometry.center();//Center the geometry based on the bounding box.

  const TextMaterial = new THREE.MeshBasicMaterial({
    color: 0x222222,
    side: THREE.DoubleSide,
    transparent:true,
    opacity: 1.0,
    //wireframe: true,
  });
  const Geotext = new THREE.Mesh( TextGeometry, TextMaterial );
  // 表示させる
  scene.add(Geotext);

  // 一定時間後に削除する
  setTimeout(() => {
    Geotext.geometry.dispose();
    Geotext.material.dispose();
    scene.remove(Geotext);
    console.log("remove");
  }, EndTime -StartTime )


}// end DisplayPhrase
/////////////////////////////////////////////////////////////////////////
//// RENDER LOOP FUNCTION
const clock = new THREE.Clock();
//const positionbarElement = document.getElementById("nav-bar");

function renderLoop() {
    stats.begin();//STATS計測

    const delta = clock.getDelta();//animation programs
    const elapsedTime = clock.getElapsedTime();

    ////////////////////////////////////////
    // TextAlive 
    if(player.isPlaying){
      const position = player.timer.position;

      let Char = player.video.findChar(position - 100, { loose: true });
      let Word = player.video.findWord( position - 100, { loose: true });
      let Phrase = player.video.findPhrase( position - 100, { loose: true });
      
      //文字を取得する
      if(nowChar != Char.text){
            nowChar = Char.text;
            console.log(nowChar);
      }//End if(char)

      //単語を取得する
      if(Word) {
        if(nowWord != Word.text){

          nowWord = Word.text;
          const StartTime = Word.startTime - position - 100;
          
          //単語を表示させる関数
          setTimeout(() => {
            DisplayWord(nowWord, nowWord.length, Word.startTime, Word.endTime, Word);
          }, StartTime);
        }
      }

      //フレーズを取得する
      if(Phrase) {
        if(nowPhrase != Phrase.text){
            nowPhrase = Phrase.text
            console.log(nowPhrase);
        }
      }//End if(phrase)


      //再生バーの更新
      //positionbarElement.style.width = Math.floor( position ) / endTime * 100 + "%"; 
    }

    // End TextAlive
    ////////////////////////////////////////
    renderer.render(scene, camera) // render the scene using the camera
    requestAnimationFrame(renderLoop) //loop the render function
    
    //stats.end();//stats計測
}

renderLoop() //start rendering

新しい歌詞の単語だったら判定式より、DisplayWord()関数を実行して、文字の3Dオブジェクトを出力させています。

これが、基本的なThree.jsとTextAlive App Apiを組み合わせたベースになります。

デモデータのようなものを作成するには、GSAPを使って、表示と同時にアニメーションをセットすることで実現できます。詳しくは、GitHubのファイルを参照ください。

GitHub - MisoraRyo/CharVoice-Samp-TA: Voiceアニメーション。TextAlive App APIを使った、歌詞が宙に舞うようにアニメーションするものです。
Voiceアニメーション。TextAlive App APIを使った、歌詞が宙に舞うようにアニメーションするものです。 - MisoraRyo/CharVoice-Samp-TA

最後に

いかがでしたでしょうか?

TextAlive App API × Three.js の基礎的な内容で、歌詞を表示させるだけですが、慣れないと難しいところだと思い、解説させていただきました。これをベースにいろいろとアレンジしていくことで、リリックアプリを作成することができます。

現在、TextAlive App API を使ったプログラミングコンテストも開催しております。サンプルで使用した楽曲をベースに、作成した作品で応募することができます。ぜひ、作品を投稿してみてはいかがでしょうか?

初音ミク「マジカルミライ 2024」プログラミング・コンテスト | developer.textalive.jp
初音ミク「マジカルミライ 2024」で、TextAlive App APIを使ってテーマ曲にあわせて魅力的に動く歌詞などの映像演出をプログラミングするコンテストが開催されます。このイベント紹介ページでは、コンテストの概要や応募に役立つ技術情...
textalive.jp

また、TextAlive App Apiの指南書「ボカロ曲の歌詞をあやつるリリックアプリを作ってみよう!」が、技術書典にて、販売中です。私の過去の作品で「Text-Aquarium(テキストアクアリウム)」というものがあります。こちらの作品紹介をベースにThree.jsのテクニックを掲載させていただきました。ぜひ、興味のある方は、読んでみてください!

ボカロ曲の歌詞をあやつるリリックアプリを作ってみよう!:リリックアプリ開発者コミュニティ
TextAlive App APIを使用したリリックアプリに関して記載した本です。 マジカルミライプログラミングコンテスト受賞者による各アプリの解説やはじめてリリックアプリを知る人もリリックアプリ作成を開始できるような内容になります。これか...
技術書典
エントリー作品|初音ミク「マジカルミライ 2023」 プログラミング・コンテスト
プログラミングの力で創作文化に参加できる!初音ミク「マジカルミライ 2023」プログラミング・コンテストを実施
magicalmirai

以上、記事が良かったらXのフォロー、また、もう一つ私の記事をご覧ください。

タイトルとURLをコピーしました