0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

lrcファイルの内容に沿ったページを作ってみよう

Posted at

概要

直近でOpenAI Whisperlrcファイルについて勉強したことにより
セリフや歌詞の文字起こしをしつつ、そのシーンに合わせたセリフを画面に表示できるのでないかというイメージが湧いてきました

ということでVueを使ってページに起こせないかということをやってみます
(Vueの環境構築などについては、説明を省略します)

本題

さっそくVueでWebページを作ってみました
スクリーンショット 2025-07-26 11.24.56.png

白い枠にlrcファイルの内容を表示しております
白枠のしたの再生ボタンを押すと、音声ファイルが再生されて同時に上の歌詞も同期して動きます

App.vue
<template>
  <header>
    <h1>再生再生プレイヤー</h1>
  </header>
  <main>
    <!-- audioSrcが音声ファイル、lyricsSrcがlrcファイル -->
    <LyricPlayer audioSrc="<音声ファイル>" lyricsSrc="<歌詞ファイル>" />
  </main>
</template>

<script setup>
import LyricPlayer from './components/LyricPlayer.vue';
</script>

<style scoped>
header {
  text-align: center;
  margin-bottom: 20px;
}
main {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px;
}
</style>
LyricPlayer.vue
<template>
  <div class="lyrics-container">
    <p
      v-for="(lyric, index) in lyricsData"
      :key="index"
      :class="{ 'active': index === currentLyricIndex }"
      :ref="el => setLyricLineRef(el, index)"
      class="lyric-line"
    >
      {{ lyric.text }}
    </p>
  </div>
  <audio id="audio-player" :src="audioSrc" controls ref="audioPlayer"></audio>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';

// プロパティとして音声ファイルと歌詞ファイルのパスを受け取る
const props = defineProps({
  audioSrc: String,
  lyricsSrc: String
});

// リアクティブなデータ(状態)を定義
const lyricsData = ref([]); // パースされた歌詞データ
const currentLyricIndex = ref(-1); // 現在アクティブな歌詞のインデックス

// テンプレート参照(DOM要素への参照)
const audioPlayer = ref(null); // <audio>要素への参照
const lyricLineRefs = ref([]); // 各歌詞行の <p> 要素への参照

// 各歌詞行の参照をセットするヘルパー関数
const setLyricLineRef = (el, index) => {
  if (el) {
    lyricLineRefs.value[index] = el;
  }
};

// 歌詞ファイルを読み込み、パースする関数
const loadLyrics = async () => {
  try {
    const response = await fetch(props.lyricsSrc);
    const lrcText = await response.text();

    const parsedLyrics = lrcText.split('\n').map(line => {
      const match = line.match(/\[(\d{2}):(\d{2}).(\d{2,3})\](.*)/);
      if (match) {
        const minutes = parseInt(match[1]);
        const seconds = parseInt(match[2]);
        // ミリ秒は2桁または3桁の場合があるので、padEndで3桁に揃える
        const milliseconds = parseInt(match[3].padEnd(3, '0'));
        const time = minutes * 60 + seconds + milliseconds / 1000;
        const text = match[4].trim();
        return { time, text };
      }
      return null;
    }).filter(Boolean); // nullを除外

    lyricsData.value = parsedLyrics;
  } catch (error) {
    console.error("歌詞の読み込みまたはパースに失敗しました:", error);
  }
};

// 音声の再生時間を監視し、歌詞を更新するイベントハンドラ
const handleTimeUpdate = () => {
  const currentTime = audioPlayer.value.currentTime;
  let nextIndex = -1;

  // 現在の再生時間に対応する歌詞のインデックスを見つける
  for (let i = 0; i < lyricsData.value.length; i++) {
    if (currentTime >= lyricsData.value[i].time) {
      nextIndex = i;
    } else {
      // 時間順に並んでいるので、これ以上は探す必要がない
      break;
    }
  }

  // インデックスが変わった場合のみ更新
  if (nextIndex !== currentLyricIndex.value) {
    currentLyricIndex.value = nextIndex;

    // 現在の歌詞行をスクロールして表示
    if (nextIndex !== -1 && lyricLineRefs.value[nextIndex]) {
      lyricLineRefs.value[nextIndex].scrollIntoView({ behavior: 'smooth', block: 'center' });
    }
  }
};

onMounted(() => {
  loadLyrics();
  if (audioPlayer.value) {
    audioPlayer.value.addEventListener('timeupdate', handleTimeUpdate);
  }
});

onBeforeUnmount(() => {
  if (audioPlayer.value) {
    audioPlayer.value.removeEventListener('timeupdate', handleTimeUpdate);
  }
});
</script>

<style scoped>
.lyrics-container {
  height: 300px;
  overflow-y: scroll;
  margin: 20px auto;
  padding: 20px;
  border: 1px solid #ccc;
  background-color: #fff;
  width: 80%;
  max-width: 600px;
  text-align: center;
  line-height: 1.5;
}

.lyric-line {
  font-size: 1.5em;
  color: #888;
  transition: color 0.3s ease, font-size 0.3s ease;
}

.lyric-line.active {
  font-size: 2em;
  font-weight: bold;
  color: #333;
}

#audio-player {
  margin-top: 20px;
  width: 80%;
  max-width: 600px;
}
</style>

いろいろコメントが残っていますが、上記がデフォルトのVueプロジェクトから追加した2ファイルになります

まだまだ改良できるところはありますが、改良については何かいい案が思いついたら更新していければと思います!

終わりに

これまで2記事書いてきたおかげでほぼ素材は揃っていたので、ベーシックな画面を作るのは非常に簡単でした
現状だと歌詞ファイル・音声ファイルを手動で作成してそれをVue側に反映させてなど手作業の部分があるので、リアルタイムっぽくするには工夫できることはありそうです

Vueのデザインの学習も兼ねて継続して触っていければと思います!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?