Remotionでポッドキャストオーディオグラムを自動生成する完全ガイド
Remotionでポッドキャストオーディオグラムを自動生成する完全ガイド
ポッドキャストの認知拡大において、最も費用対効果が高いコンテンツのひとつがオーディオグラムだ。オーディオグラムとは、60秒程度のエピソードクリップに波形アニメーション・エピソードタイトル・ゲスト名・番組アートワーク・再生進行バーを組み合わせた正方形の動画クリップで、Instagram、Twitter/X、LinkedIn、YouTube Shortsでの配信を想定して制作される。
なぜオーディオグラムが有効なのか。SpotifyやApple Podcastsはすでにそのポッドキャストを知っている人が使うプラットフォームだ。新規リスナーの獲得にはソーシャルメディアへの露出が必要で、そこで効果的なのが「無音スクロールの指を止める動く映像」だ。テキスト投稿や静止画よりも、動画コンテンツはフィードアルゴリズムからも優遇される傾向がある。
課題は制作ボリュームだ。週1回のポッドキャストで1エピソードあたり3本のクリップを作れば、年間156本のオーディオグラムが必要になる。動画編集ソフトで手作業することは現実的でない。Remotionはこの問題を解決する。コンポーネントとプロップスで設計し、Node.js APIでRSSフィードからデータを取得し、MP4ファイルを人手なしで書き出せる。
この記事では、<Audio>コンポーネント、@remotion/media-utilsの波形ビジュアライザーAPI、進行バー、メタデータオーバーレイ、RSSからのバッチ生成パイプラインをすべてコードを交えて解説する。
オーディオグラムとは何か
オーディオグラムを構成する要素は通常4つだ。
- 波形ビジュアライザー:音声の周波数に応じてリアルタイムで動くバーグラフ。最も視覚的なインパクトを持つ要素。
- エピソード情報オーバーレイ:番組アートワーク、エピソードタイトル、ゲスト名。
- 再生進行バー:現在の再生位置を示す細いバー。動画の長さに対して今どの位置にいるかを視覚化する。
- 音声トラック:実際のポッドキャストのクリップ。視聴者はこれを聴くために動画を止める。
典型的なフォーマットは60秒の正方形動画(1080×1080)で、Instagram投稿、Twitter/Xへの添付、LinkedInビデオとして使う。縦型(1080×1920)にすればInstagram ReelsとYouTube Shortsにも対応できる。
@remotion/media-utilsの概要
@remotion/media-utilsはRemotionの公式パッケージで、音声・動画データ処理のユーティリティを提供する。オーディオグラム制作で使う主要な関数は2つだ。
getAudioData(src: string): Promise<AudioData> — 音声ファイルをデコードし、サンプルレート、チャンネル数、デコードされた音声サンプル、総時間を含むAudioDataオブジェクトを返す。
visualizeAudio({ audioData, frame, fps, numberOfSamples }): Float32Array — デコード済みのAudioData、現在のフレーム番号、fps、周波数バケット数を受け取り、現時点の周波数ごとの強度(0〜1に正規化)をFloat32Arrayで返す。この値がそのまま波形バーの高さになる。
Reactコンポーネント内では、getAudioDataの代わりにフック版のuseAudioDataを使う。これはローディング状態を管理し、データが取得できるまでnullを返す。
インストール:
npm install @remotion/media-utils
コンポジションの設定
// Root.tsx
import { Composition } from "remotion";
import { AudiogramComposition } from "./Audiogram";
export const RemotionRoot: React.FC = () => {
return (
<Composition
id="Audiogram"
component={AudiogramComposition}
width={1080}
height={1080}
fps={30}
durationInFrames={1800} // 30fps × 60秒 = 1800フレーム
defaultProps={{
audioSrc: "./tmp/audio/episode-clip.mp3",
episodeTitle: "AIが変えるプロダクト開発の現場",
guestName: "田中 裕子 / プロダクトディレクター",
artworkSrc: "./assets/artwork.jpg",
accentColor: "#6366f1",
numberOfBars: 80,
}}
/>
);
};
<Audio>コンポーネント
Remotionの組み込み<Audio>コンポーネントは、音声再生をコンポジションのタイムラインと同期する。Remotion Studioのブラウザプレビューではスクラブに合わせてリアルタイムで再生し、ヘッドレスレンダリング時には出力MP4に音声トラックとして書き込む。
import { Audio } from "remotion";
// コンポジションコンポーネント内で:
<Audio src={audioSrc} />
<Audio>が受け付ける主なプロップス:
src:音声ファイルのURL、またはインポートしたstaticアセットのパスstartFrom:再生を開始するフレーム番号(デフォルト0)endAt:再生を停止するフレーム番号volume:0〜1の数値、または(frame: number) => numberの動的ボリューム関数
オーディオグラムでは、frame 0から開始してコンポジション全体にわたって再生する最もシンプルな使い方で十分だ。
波形ビジュアライザーの実装
波形ビジュアライザーはオーディオグラムの視覚的な核心だ。完全な実装を以下に示す。
import { useCurrentFrame, useVideoConfig } from "remotion";
import { visualizeAudio, useAudioData } from "@remotion/media-utils";
interface WaveformProps {
audioSrc: string;
numberOfBars?: number;
barColor?: string;
barWidth?: number;
gap?: number;
maxHeight?: number;
}
export const Waveform: React.FC<WaveformProps> = ({
audioSrc,
numberOfBars = 80,
barColor = "#6366f1",
barWidth = 8,
gap = 4,
maxHeight = 160,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const audioData = useAudioData(audioSrc);
// データ取得中はプレースホルダーを表示
if (!audioData) {
return (
<div
style={{
display: "flex",
alignItems: "center",
gap,
height: maxHeight,
}}
>
{Array.from({ length: numberOfBars }).map((_, i) => (
<div
key={i}
style={{
width: barWidth,
height: maxHeight * 0.08,
background: barColor,
borderRadius: barWidth / 2,
opacity: 0.2,
}}
/>
))}
</div>
);
}
const frequencyData = visualizeAudio({
fps,
frame,
audioData,
numberOfSamples: numberOfBars,
});
const totalWidth = numberOfBars * barWidth + (numberOfBars - 1) * gap;
return (
<div
style={{
display: "flex",
alignItems: "center",
gap,
height: maxHeight,
width: totalWidth,
}}
>
{frequencyData.map((value, index) => {
const barHeight = Math.max(4, value * maxHeight);
return (
<div
key={index}
style={{
width: barWidth,
height: barHeight,
background: barColor,
borderRadius: barWidth / 2,
flexShrink: 0,
}}
/>
);
})}
</div>
);
};
visualizeAudioは短時間フーリエ変換(STFT)を使って現在タイムスタンプの周波数情報を抽出する。numberOfSamplesに指定した数の要素を持つFloat32Arrayが返り、各要素はその周波数バケットの強度(0〜1に正規化)を表す。この値を各バーの高さに直接マッピングすることで、音声に連動した波形アニメーションが完成する。
再生進行バー
import { useCurrentFrame, useVideoConfig, interpolate } from "remotion";
interface ProgressBarProps {
color?: string;
height?: number;
backgroundColor?: string;
}
export const ProgressBar: React.FC<ProgressBarProps> = ({
color = "#6366f1",
height = 6,
backgroundColor = "rgba(255,255,255,0.2)",
}) => {
const frame = useCurrentFrame();
const { durationInFrames } = useVideoConfig();
const progress = interpolate(
frame,
[0, durationInFrames - 1],
[0, 100],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}
);
return (
<div
style={{
width: "100%",
height,
background: backgroundColor,
borderRadius: height / 2,
overflow: "hidden",
}}
>
<div
style={{
width: `${progress}%`,
height: "100%",
background: color,
borderRadius: height / 2,
}}
/>
</div>
);
};
エピソードメタデータオーバーレイ
import { Img, interpolate, useCurrentFrame } from "remotion";
interface MetadataOverlayProps {
episodeTitle: string;
guestName: string;
artworkSrc: string;
accentColor: string;
}
export const MetadataOverlay: React.FC<MetadataOverlayProps> = ({
episodeTitle,
guestName,
artworkSrc,
accentColor,
}) => {
const frame = useCurrentFrame();
// 最初の30フレームで下からスライドイン
const translateY = interpolate(frame, [0, 30], [40, 0], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const opacity = interpolate(frame, [0, 30], [0, 1], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
return (
<div
style={{
opacity,
transform: `translateY(${translateY}px)`,
display: "flex",
alignItems: "center",
gap: 24,
fontFamily: '"Yu Gothic", "Hiragino Kaku Gothic ProN", "Noto Sans JP", sans-serif',
}}
>
{/* 番組アートワーク */}
<Img
src={artworkSrc}
style={{
width: 120,
height: 120,
borderRadius: 16,
objectFit: "cover",
flexShrink: 0,
border: `3px solid ${accentColor}`,
}}
/>
{/* テキスト情報 */}
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 12,
fontWeight: 700,
color: accentColor,
letterSpacing: "0.1em",
marginBottom: 8,
}}
>
PODCAST
</div>
<div
style={{
fontSize: 24,
fontWeight: 700,
color: "#ffffff",
lineHeight: 1.4,
marginBottom: 8,
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{episodeTitle}
</div>
<div
style={{
fontSize: 15,
color: "rgba(255,255,255,0.65)",
}}
>
{guestName}
</div>
</div>
</div>
);
};
<Img>コンポーネント(素の<img>タグではなく)を使うことが重要だ。RemotionはフレームをレンダリングするときにすべてのRemotionコンポーネントが参照するリソースを事前に読み込む仕組みを持っており、<Img>はそのシステムに統合されている。素の<img>を使うとレンダリング初期フレームで画像が空白になる可能性がある。
フルコンポジションの組み立て
import { AbsoluteFill, Audio } from "remotion";
interface AudiogramProps {
audioSrc: string;
episodeTitle: string;
guestName: string;
artworkSrc: string;
accentColor: string;
numberOfBars: number;
}
export const AudiogramComposition: React.FC<AudiogramProps> = ({
audioSrc,
episodeTitle,
guestName,
artworkSrc,
accentColor,
numberOfBars,
}) => {
return (
<AbsoluteFill
style={{
background: "#0f0f1a",
padding: 60,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
{/* 音声をタイムラインにバインド */}
<Audio src={audioSrc} />
{/* 上部:波形ビジュアライザー */}
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Waveform
audioSrc={audioSrc}
numberOfBars={numberOfBars}
barColor={accentColor}
barWidth={8}
gap={4}
maxHeight={200}
/>
</div>
{/* 中部:エピソード情報 */}
<MetadataOverlay
episodeTitle={episodeTitle}
guestName={guestName}
artworkSrc={artworkSrc}
accentColor={accentColor}
/>
{/* 下部:進行バー */}
<div style={{ marginTop: 32 }}>
<ProgressBar color={accentColor} height={6} />
</div>
</AbsoluteFill>
);
};
シミュレーション波形(フォールバック)
レイアウト確認やサムネイル生成など、実音声データなしで波形表示を確認したい場合は、数式ベースのシミュレーション波形が便利だ。
import { useCurrentFrame } from "remotion";
const useSimulatedWaveform = (numberOfBars: number): number[] => {
const frame = useCurrentFrame();
return Array.from({ length: numberOfBars }, (_, i) => {
const base = Math.sin((frame / 30) * Math.PI + i * 0.3) * 0.4 + 0.5;
const noise = Math.sin(i * 137.5 + frame * 0.7) * 0.15;
return Math.max(0.05, Math.min(1, base + noise));
});
};
これはオーガニックな波形の動きをシミュレートする。本番用にはvisualizeAudioによる実音声データを使うべきだが、テンプレートプレビューや開発中のレイアウト確認には十分だ。
RSSフィードからのバッチ自動生成
Remotionをポッドキャスト制作に組み込む最大の価値は自動化にある。以下は、RSSフィードから最新エピソードを取得し、エピソードごとにオーディオグラムを自動生成する完全なパイプラインだ。
import { bundle } from "@remotion/bundler";
import { renderMedia, selectComposition } from "@remotion/renderer";
import { XMLParser } from "fast-xml-parser";
import { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";
// RSSフィードの解析
const fetchEpisodes = async (rssUrl: string) => {
const res = await fetch(rssUrl);
const xml = await res.text();
const parser = new XMLParser({ ignoreAttributes: false });
const result = parser.parse(xml);
const items = result.rss.channel.item;
return Array.isArray(items) ? items : [items];
};
// ffmpegで最初の60秒を切り出す
const downloadClip = (audioUrl: string, outputPath: string): void => {
execSync(
`ffmpeg -i "${audioUrl}" -t 60 -c:a libmp3lame -q:a 2 "${outputPath}" -y`,
{ stdio: "pipe" }
);
};
const generateAudiograms = async () => {
const RSS_URL = process.env.PODCAST_RSS_URL ?? "";
const ARTWORK_URL = process.env.PODCAST_ARTWORK_URL ?? "";
const ACCENT_COLOR = process.env.ACCENT_COLOR ?? "#6366f1";
const OUTPUT_DIR = "./out/audiograms";
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.mkdirSync("./tmp/audio", { recursive: true });
const bundleLocation = await bundle({ entryPoint: "./src/index.ts" });
const episodes = await fetchEpisodes(RSS_URL);
// 直近5エピソードを処理
for (const episode of episodes.slice(0, 5)) {
const title = episode.title as string;
const audioUrl = episode.enclosure?.["@_url"] as string;
const safeTitle = title
.replace(/[^a-z0-9]/gi, "-")
.toLowerCase()
.slice(0, 40);
const clipPath = path.resolve(`./tmp/audio/${safeTitle}.mp3`);
const outputPath = path.resolve(`${OUTPUT_DIR}/${safeTitle}.mp4`);
if (fs.existsSync(outputPath)) {
console.log(`スキップ(既存): ${title}`);
continue;
}
console.log(`音声クリップをダウンロード中: ${title}`);
downloadClip(audioUrl, clipPath);
const inputProps = {
audioSrc: clipPath,
episodeTitle: title,
guestName: (episode["itunes:author"] as string) ?? "Your Podcast",
artworkSrc: ARTWORK_URL,
accentColor: ACCENT_COLOR,
numberOfBars: 80,
};
const composition = await selectComposition({
serveUrl: bundleLocation,
id: "Audiogram",
inputProps,
});
console.log(`レンダリング中: ${title}`);
await renderMedia({
composition,
serveUrl: bundleLocation,
codec: "mp4",
outputLocation: outputPath,
inputProps,
});
console.log(`完了: ${outputPath}`);
}
};
generateAudiograms().catch(console.error);
このスクリプトをエピソードリリース後にcronジョブとして実行すれば、プロモーション用クリップの制作が完全に自動化される。
プラットフォーム別の設定
Instagram・TikTok・YouTube Shorts(縦型 9:16、1080×1920)
<Composition
id="AudiogramPortrait"
component={AudiogramComposition}
width={1080}
height={1920}
fps={30}
durationInFrames={1800}
defaultProps={...}
/>
縦型レイアウトでは、波形を画面中央付近に配置し、アートワークとメタデータを上部3分の1に置く構成が見やすい。
Twitter/X・LinkedIn(正方形 1:1、1080×1080)
正方形フォーマットが最も汎用性が高い。Twitter/XはGIF的な挙動で自動再生し、LinkedInは正方形動画にフィード上でより多くのスペースを割り当てる傾向がある。
RenderCompのオーディオグラムテンプレート
ゼロから洗練されたオーディオグラムコンポジションを構築するには数時間の開発時間が必要だ。RenderCompでは、Remotion用のプロダクション品質オーディオグラムテンプレートを複数のスタイル(バー波形、ミラー波形、サークル)とダーク・ライトテーマで提供している。inputPropsでエピソードタイトル、ゲスト名、アートワーク、アクセントカラーを差し替えるだけですぐに使える設計になっており、正方形(1:1)と縦型(9:16)の両コンポジションが含まれる。
rendercomp.comで詳細を確認してほしい。
よくある質問
Q: @remotion/media-utilsのインストールは必須ですか?波形なしでオーディオグラムは作れますか?
A: 波形ビジュアライザーを使わないのであれば@remotion/media-utilsは不要です。<Audio>コンポーネントはRemotionコアに含まれており、追加パッケージなしで音声の同期再生が可能です。波形を数式ベースのシミュレーション波形で代替すれば、完全にパッケージなしで動作するオーディオグラムを作れます。
Q: useAudioDataがnullを返し続けます。なぜですか?
A: CORSの問題が最も一般的な原因です。Remotion Studioでは音声ファイルが同一オリジンから提供されるか、適切なCORSヘッダーが必要です。本番レンダリング(ヘッドレスChromium)では、音声ファイルをローカルパスとして渡すことでこの問題を回避できます。リモートURLを参照する場合は、事前にローカルにダウンロードすることを推奨します。
Q: 特定の1分間(例:12分30秒から13分30秒)を切り出してオーディオグラムにできますか?
A: はい。ffmpegで目的のセグメントを抽出してからRemotionに渡します:ffmpeg -i input.mp3 -ss 00:12:30 -t 60 -c:a copy clip.mp3。Remotionコンポジションはセグメントの開始位置を知る必要はなく、渡された音声ファイルの先頭から再生します。
Q: 音声のトランスクリプトを字幕として表示できますか?
A: はい。Whisperなどの音声認識ツールから単語レベルのタイムスタンプ付きトランスクリプトを取得し、現在フレームの時間に対応する単語を表示するオーバーレイを実装できます。const currentTime = frame / fpsで現在の秒数を求め、transcript.filter(w => w.start <= currentTime && w.end > currentTime)で表示する単語を絞り込みます。
Q: Instagram向けとTwitter/X向けを同じ音声から同時に生成できますか?
A: はい。正方形(AudiogramSquare)と縦型(AudiogramPortrait)の2つのコンポジションを登録し、バッチスクリプトで各エピソードに対して2回renderMedia()を呼び出します。同じ音声ファイルと同じメタデータで、異なるアスペクト比の2本の動画が生成されます。
Q: visualizeAudioは何の音声フォーマットに対応していますか?
A: MP3、WAV、AAC、OGGに対応しています。デコードはRemotionが起動するChromiumインスタンス内のWeb Audio APIが担当しているため、Chromiumが対応している音声フォーマットはすべてサポートされます。
Q: 長いエピソード(1時間以上)の音声をuseAudioDataに渡すとどうなりますか?
A: useAudioDataは音声ファイル全体をPCMサンプルとしてメモリに展開します。1時間以上の音声ファイルは数GB単位のRAMを消費する可能性があります。オーディオグラムの用途(60秒クリップ)では問題になりません。長尺の音声を使う場合は事前にffmpegで必要な部分だけを切り出してから渡してください。