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?

プライベート用フルアルバム制作パイプラインを組んでみた

Last updated at Posted at 2025-12-24

この記事は Applibot Advent Calendar 2025 25日目の記事です。

お久しぶりです、ナカムロ.です。メリークリスマス。荷が重いです。

今回は酒のアテになる個人の趣味開発話なので、特に有益なクリスマスプレゼントはないかもです。技術エッセイみたい感じなので、気軽にお願いします。

はじめに:アーティストへの憧れと、脳内フルアルバム

昔からリズムアクションゲームの特に楽曲が好きです。最初はユーザーとして遊ぶだけでしたが、あるときから開発に携わったりもしてきました。ここでは詳細は省きますが、どうしても楽曲制作だけは不可侵領域でした。その過程で、たくさんの音楽クリエイターの方々と関わってきました。彼らが生み出す楽曲には、いつも圧倒されます。そして同時に、「いつか自分も作ってみたい」という憧れがずっとありました。DTMソフトを購入して曲を作ってみたこともあります。頭の中には「こういう曲があったらいいな」というイメージが常にありました。UK Garage×Japanese Neo-Soul、Deep House×Japanese City Pop、個人的な趣味の日本酒の温度帯をコンセプトにしたアルバム——。

でも、いざ作ろうとすると手が止まってしまいます。

イメージを形にする技術が追いつかない。メロディは浮かんでも、アレンジができない。歌詞は書けても、ボーカルがいない。結局、「いつか作りたいアルバム」は頭の中にだけ存在したまま、長い時間が経ちました。

このプロジェクトについて

これは、そんな「脳内にだけ存在していたアルバム」を、現代のツールを使って形にしてみた個人的な試みの記録です。

最初にお伝えしておきたいこと

  • これは商業利用を目的としたものではありません
  • 「AIで誰でもプロになれる!」という話でもありません
  • あくまで、身内で楽しむための趣味のパイプラインです

なお、使用している各種サービス(SUNO AI等)はProアカウントで課金しており、生成した楽曲の権利は私が保有できる状態(2025/12/25記事執筆時点)です。つまり商用利用も権利的にはクリアしています。ただ、売る気は全くなく、あくまで個人で楽しむためのものです。

リズムゲーム開発などを通じて音楽クリエイターの方々と仕事をしてきた立場として、彼らには最大限のリスペクトを持っています。このプロジェクトはあくまで「音楽を作れない人間が、頭の中のアイディアを少しだけ形にして体験してみる」ための個人的なものです。

この記事では、その技術的な構成を記録しておきます。同じような境遇の方が、同じように「脳内にだけ存在する音楽」を形にしたいと思ったときの参考になれば嬉しいです。

システム構成

コンセプト設計(人間)
    ↓
プロンプト構築(Claude Code + 人間の対話)
    ├── 日本語歌詞(ふりがな付き)
    └── ローマ字歌詞(SUNO AI用)
    ↓
楽曲生成(SUNO AI)→ 人間による選定
    ↓
画像生成(Nano Banana Pro)
    ↓
メタデータ抽出(Bashスクリプト)
    ↓
動画生成(After Effects + ExtendScript)
    ↓
ローカル保存 or 限定公開

使用技術

レイヤー ツール 役割
プロンプト設計 Claude Code(Opus 4.5) 対話しながら歌詞・スタイルを構築
楽曲生成 SUNO AI(v5) プロンプトから楽曲生成
画像生成 Gemini(Nano Banana Pro) 楽曲コンセプトに合った背景画像
メタデータ抽出 Bash + ffprobe MP3から長さ等を抽出してJSON化
動画自動生成 After Effects + ExtendScript アルバム動画の自動レイアウト

ディレクトリ構成

Music/
├── albums/
│   ├── album_01/          # 温度帯 -ONDOTAI-
│   │   ├── 01_yukibie.mp3
│   │   ├── 01_yukibie.md  # プロンプト
│   │   ├── 01.png
│   │   ├── 02_hanabie.mp3
│   │   ├── 02_hanabie.md
│   │   ├── 02.png
│   │   ├── ...
│   │   ├── album_info.md
│   │   └── tracks.json    # 自動生成
│   └── album_02/
├── scripts/
│   ├── analyze_album.sh   # メタデータ抽出
│   └── create_album_video.jsx  # AEスクリプト
├── templates/
│   └── song_template.md
└── docs/
    ├── suno_prompt_guide.md
    └── album_production_flow.md

音源はMP3で管理しています。もちろん音質にこだわるならWAVでも良いのですが、軽量でとにかく出来上がりまでの時間を優先しました。趣味なのでストレージも節約します。

templates/song_template.md(クリックで展開)
# 曲タイトル

## スタイル
\`\`\`
[ジャンル], [サブジャンル], [ボーカルタイプ], [雰囲気], [楽器], BPM: [数値], [コード進行]
\`\`\`

## 歌詞(日本語)

人間が読みやすい形式。ふりがな付き。

\`\`\`
[Intro]
[楽器指示など]

[Verse 1]
歌詞(よみがな)をここに
漢字(かんじ)にはふりがなを

[Chorus]
サビの歌詞(かし)

[Verse 2]
2番(にばん)の歌詞(かし)

[Bridge]
ブリッジパート

[Outro]
[演奏指示など]
\`\`\`

## 歌詞(ローマ字 / SUNO AI用)

SUNO AIへの入力用。発音精度向上のためローマ字で記述。

\`\`\`
[Intro]
[Instrument instructions]

[Verse 1]
Kashi wo koko ni
Kanji ni wa furigana wo

[Chorus]
Sabi no kashi

[Verse 2]
Niban no kashi

[Bridge]
Bridge part

[Outro]
[Performance instructions]
\`\`\`

## SUNO AI用コピー

スタイルをそのままコピー:
\`\`\`
[上記スタイルをここにコピー]
\`\`\`

## オプション設定

- **BPM**: [テンポ]
- **キー**: [調性 例: C Major, A Minor]
- **楽器編成**: [使用楽器]
- **その他の指示**: [特別な要望]

## メモ

[制作に関するメモや参考情報]

---

## 参考: メタタグ一覧

### 構成タグ
| タグ | 用途 |
|------|------|
| `[Intro]` | イントロ |
| `[Verse]` | Aメロ |
| `[Pre-Chorus]` | Bメロ |
| `[Chorus]` | サビ |
| `[Bridge]` | ブリッジ |
| `[Interlude]` | 間奏 |
| `[Drop]` | ドロップ |
| `[Outro]` | アウトロ |
| `[End]` | 終了 |

### ボーカルタイプ
- Female Vocals / Male Vocals / Duet / Choir / Whisper Vocals
docs/suno_prompt_guide.md(クリックで展開)
# SUNO AI プロンプトガイド

## 概要

SUNO AIで高品質な日本語楽曲を生成するためのプロンプト構築ガイドです。

## 歌詞の二重形式

SUNO AIに日本語歌詞を入力する際、**ローマ字変換版**を使用すると発音精度と品質が向上します。

### 形式

\`\`\`markdown
## 歌詞(日本語)
読みやすさ重視。ふりがな付きで人間が確認・編集しやすい形式。

## 歌詞(ローマ字 / SUNO AI用)
SUNO AIへの実際の入力用。発音精度向上のためローマ字で記述。
\`\`\`

### 例

**日本語版(確認用):**
\`\`\`
[Verse 1]
凍(い)てつく朝(あさ)に 息(いき)が白(しろ)く
グラスの中(なか)で 眠(ねむ)る雫(しずく)
\`\`\`

**ローマ字版(SUNO AI入力用):**
\`\`\`
[Verse 1]
Itetsuku asa ni iki ga shiroku
Gurasu no naka de nemuru shizuku
\`\`\`

## スタイル記述

### 基本構成

\`\`\`
[ジャンル], [サブジャンル], [ボーカルタイプ], [雰囲気], [楽器], [BPM], [コード進行]
\`\`\`

### ジャンル例

| カテゴリ | 例 |
|---------|-----|
| 電子音楽 | Minimal Techno, House, Ambient, Synthwave |
| ポップ | J-Pop, City Pop, Kayokyoku, Shibuya-kei |
| ロック | J-Rock, Alternative, Indie Rock |
| その他 | Jazz, R&B, Folk, Enka |

### ボーカルタイプ

- `Female Vocals` - 女性ボーカル
- `Male Vocals` - 男性ボーカル
- `Duet` - デュエット
- `Choir` - コーラス
- `Whisper Vocals` - ウィスパーボイス

### 雰囲気・質感

| 温度感 | キーワード |
|--------|-----------|
| 冷たい | Cold, Icy, Crystalline, Ethereal |
| 涼しい | Cool, Fresh, Breezy, Light |
| 暖かい | Warm, Cozy, Mellow, Soft |
| 熱い | Hot, Passionate, Intense, Fiery |

### 楽器指定

\`\`\`
[楽器カテゴリ] [楽器名], [エフェクト]
\`\`\`

例:
- `Icy Pad, Digital Lead, TR-909 Kick`
- `Acoustic Guitar, Warm Bass, Brush Drums`
- `Koto, Shamisen, Taiko` (和楽器)

### BPMガイド

| テンポ | BPM範囲 | 用途 |
|--------|---------|------|
| Slow | 60-80 | バラード、アンビエント |
| Medium | 80-110 | ポップ、R&B |
| Uptempo | 110-130 | ダンス、ロック |
| Fast | 130+ | テクノ、EDM |

## メタタグ

### 構成タグ

歌詞内で使用する構成指示タグ:

| タグ | 用途 |
|------|------|
| `[Intro]` | イントロ |
| `[Verse]` / `[Verse 1]` | Aメロ |
| `[Pre-Chorus]` | Bメロ |
| `[Chorus]` | サビ |
| `[Bridge]` | ブリッジ |
| `[Interlude]` | 間奏 |
| `[Drop]` | ドロップ(EDM) |
| `[Outro]` | アウトロ |
| `[End]` | 終了指示 |

### 演奏指示タグ

タグ内に演奏指示を追加:

\`\`\`
[Intro]
[Icy synth pad] [Minimal kick drum]

[Bridge]
[Ambient breakdown]

[Outro]
[Fading icy textures]
\`\`\`

## プロンプト品質チェックリスト

- [ ] スタイルは具体的か(曖昧な表現を避ける)
- [ ] BPMは指定されているか
- [ ] ボーカルタイプは明記されているか
- [ ] 歌詞のローマ字版を用意したか
- [ ] 構成タグは適切に配置されているか
- [ ] 楽器指定は具体的か

## ローマ字変換ルール

### 基本ルール

1. **ヘボン式ローマ字**を基本とする
2. **長音**は母音を重ねる(おう→ou、ああ→aa)
3. **促音(っ)**は子音を重ねる(きっと→kitto)
4. **撥音(ん)**は n で表記(ただし母音・y の前は n')
5. **単語間**はスペースで区切る

### 例外・注意点

| 日本語 | ローマ字 | 備考 |
|--------|----------|------|
| を | wo | 助詞の「を」 |
| は | wa | 助詞の「は」 |
| へ | e | 助詞の「へ」 |
| づ | zu | 「ず」と同じ |
| ぢ | ji | 「じ」と同じ |

### 変換例

\`\`\`
凍てつく朝に → Itetsuku asa ni
息が白く → iki ga shiroku
心地いい → kokochi ii
澄み切った → sumi kitta
\`\`\`

## ワークフロー

1. **コンセプト設計** - テーマ、雰囲気、ターゲットを決定
2. **スタイル構築** - ジャンル、楽器、BPM等を決定
3. **日本語歌詞作成** - ふりがな付きで作成
4. **ローマ字変換** - SUNO AI入力用に変換
5. **SUNO AI生成** - 複数パターン生成
6. **人間による選定** - 試聴して最良のものを選択
7. **ファイル保存** - albums/album_XX/に配置

楽曲プロンプトの設計

SUNO AIに渡すプロンプトは、以下の形式で構築しています。

スタイル指定(1行)

Minimal Techno, Japanese Kayokyoku, Female Vocals, Cold, Digital, BPM: 100, Am - F - C - G

ジャンル、サブジャンル、ボーカルタイプ、雰囲気、楽器編成、BPM、コード進行を1行にまとめます。

歌詞:二重管理方式

SUNO AIは日本語のカナ表記よりローマ字の方が発音精度は高いです。そのため、各楽曲で2つのバージョンを管理しています。(v5で日本語歌詞も改善はされましたが、両方試して個人的にはまだローマ字のほうが精度は高いと思います)

日本語版(確認・編集用)

[Verse 1]
凍(い)てつく朝(あさ)に 息(いき)が白(しろ)く
グラスの中(なか)で 眠(ねむ)る雫(しずく)

ローマ字版(SUNO AI入力用)

[Verse 1]
Itetsuku asa ni iki ga shiroku
Gurasu no naka de nemuru shizuku

ローマ字変換ルール

パターン 変換例
長音 おう → ou, ああ → aa
促音 きっと → kitto
助詞「は」 は → wa
助詞「を」 を → wo
「ん」 ん → n(母音前はn')

プロンプト構築の工夫

試行錯誤する中で見つけた、いくつかの工夫があります。

1. サビでタイトルを繰り返す

キャッチーさを出すために、サビでタイトル(曲名)を繰り返すようにしています。

[Chorus]
雪冷(ゆきび)え 雪冷(ゆきび)え
冷(つめ)たさが心地(ここち)いい
雪冷(ゆきび)え 雪冷(ゆきび)え
澄(す)み切(き)った この感覚(かんかく)

J-POPや歌謡曲でよくあるパターンですが、これがあるとグッと「曲らしく」なります。

2. 演奏指示タグを活用する

歌詞の中に [Brass fanfare][Techno beat intensifies] といった演奏指示を入れると、その通りにアレンジしてくれることがあります。

[Intro]
[Icy synth pad] [Minimal kick drum]

[Drop]
[Techno beat intensifies]

[Bridge]
[Strings swell]

必ず反映されるわけではないですが、雰囲気のコントロールに役立ちます。

3. 具体的な機材名・アーティスト風を指定

「TR-909 Kick」「Linn Drum」のような具体的な機材名や、「80年代日本のガールズシティポップ」のようなリファレンスを入れると、サウンドの方向性が定まりやすくなります。

4. 温度感・質感のキーワードを複数入れる

雰囲気を伝えるキーワードは1つだけでなく、複数入れた方が精度が上がる印象です。

Cold, Digital, Crystalline, Ethereal, Icy atmosphere

人間のフィードバックを入れるポイント

AIに丸投げではなく、いくつかの段階で人間の意図を反映させています。

1. テーマ選定とニュアンス調整

アルバムのコンセプトを決める段階で、「どういう雰囲気にしたいか」「どんなワードを入れたいか」を最初に決めておきます。

例えば「温度帯」アルバムは、私の趣味である日本酒から着想を得ました。いろんなお酒をいろんな温度帯で飲みながら「この温度が一番美味しい」を探すのが好きで、その体験を楽曲にできないかと考えたのがきっかけです。

具体的には:

  • 温度が上がるにつれてBPMも上げる
  • 各温度帯の「体感」を歌詞に反映させる(冷たさ→温もり→熱さ)
  • 日本酒の専門用語(雪冷え、花冷え、ぬる燗など)をそのままタイトルに

といった方針を決めてから、10曲分の設計に入ります。

2. 歌詞の確認と調整

生成された歌詞案を確認しながら、「ここはこういうニュアンスにしたい」「このワードは入れたい」といったフィードバックを入れています。基本的な路線は大きく変えませんが、細かい表現の調整は人間の感覚で行います。

また、実際にSUNO AIで生成してみると聞き取りにくいワードが出てくることがあります。その場合は手動でローマ字を調整して、発音が自然になるように微調整しています。

3. 生成結果の選定

SUNO AIは同じプロンプトでも毎回違う結果を出すので、1曲につき10パターンくらい生成して、1曲ずつ聴いて選定しています。良いと思った曲をピックアップしていき、最終的には2〜3曲の中から「これだ」というものを選ぶ流れです。楽曲は基本全部聴くため、実時間でいうと一番時間が掛かる部分かもしれません。

ただ、この選定作業自体がなかなか楽しい時間でした。お酒を飲みながら「こっちの方がサビのメロディがいいな」「いや、こっちのアレンジも捨てがたい」と悩む時間は、良い酒のアテになります。その温度の燗酒を飲みながら選定するルールにしていましたが、なかなか楽しい時間です。

実際の商用楽曲オーディションになると当然ですが、想いの宿った「作品」との向き合いになるため真剣に聴くことになります。妥協は許されません。その一方で、今回の選定については気楽なものになり、個人的に楽しめる趣味の時間が生まれたのも、良い発見でした。

4. フィーリング重視の最終判断

プロンプトでBPMやコード進行を指定していますが、正直なところ完全に希望通りにはなりません。ランダム性が大きいので、最終的には「聴いてみて良いかどうか」というフィーリングを重視しています。

結果として、当初想定していたBPMとは違う曲が採用されることもあります。それも含めて「こういう解釈もあるのか」と楽しんでいます。決して酔いによる妥協はありません。

完成例:雪冷え

参考までに、アルバム1曲目「雪冷え」の完成プロンプトを掲載します。

スタイル指定

Minimal Techno, Japanese Kayokyoku, Female Vocals, Cold, Digital, Crystalline, BPM: 100, Ethereal, Icy atmosphere, Icy Pad, Digital Lead, TR-909 Kick, Minimal Hi-hats, Reverb, Delay, Filter, Am - F - C - G

歌詞(ローマ字 / SUNO AI入力用)

[Intro]
[Icy synth pad] [Minimal kick drum]

[Verse 1]
Itetsuku asa ni iki ga shiroku
Gurasu no naka de nemuru shizuku
Godo no sekai toumei na yume
Furereba kieru yuki no kesshou

[Chorus]
Yukibie Yukibie
Tsumetasa ga kokochi ii
Yukibie Yukibie
Sumi kitta kono kankaku

[Drop]
[Techno beat intensifies]

[Verse 2]
Seijaku no naka hibiku kodou
Shitasaki ni nokoru kasuka na amami
Kyokugen made hiyashita omoi
Junsui na mama hozon sareru

[Chorus]
Yukibie Yukibie
Tsumetasa ga kokochi ii
Yukibie Yukibie
Sumi kitta kono kankaku

[Bridge]
[Ambient breakdown]
Koori no sekai de
Yukkuri to mezameru
Atarashii asa ga kuru

[Outro]
[Fading icy textures]

このプロンプトをSUNO AIに入力すると、ミニマルテクノと歌謡曲を融合した「雪冷え」が生成されます。当然ですが、私の雪冷えとは違う顔になるでしょう。

自動化スクリプト

メタデータ抽出(Bash)

scripts/analyze_album.sh は、アルバムフォルダ内のMP3ファイルを解析して tracks.json を生成します。

analyze_album.sh(クリックで展開)
#!/bin/bash
#
# analyze_album.sh
# アルバムフォルダ内のmp3ファイルを解析し、楽曲データをJSONで出力
#
# 使い方:
#   ./analyze_album.sh /path/to/album_folder
#
# 出力:
#   album_folder/tracks.json

set -e

# 引数チェック
if [ -z "$1" ]; then
    echo "使い方: $0 <アルバムフォルダのパス>"
    echo "例: $0 ./albums/album_01"
    exit 1
fi

ALBUM_DIR="$1"

# フォルダ存在チェック
if [ ! -d "$ALBUM_DIR" ]; then
    echo "エラー: フォルダが見つかりません: $ALBUM_DIR"
    exit 1
fi

# ffprobe存在チェック
if ! command -v ffprobe &> /dev/null; then
    echo "エラー: ffprobeがインストールされていません"
    echo "インストール: brew install ffmpeg"
    exit 1
fi

# 出力ファイル
OUTPUT_FILE="$ALBUM_DIR/tracks.json"

echo "アルバム解析中: $ALBUM_DIR"
echo ""

# JSON開始
echo '{' > "$OUTPUT_FILE"
echo '  "tracks": [' >> "$OUTPUT_FILE"

# mp3ファイルを番号順にソートして処理
FIRST=true
TOTAL_DURATION=0

for mp3file in $(ls "$ALBUM_DIR"/*.mp3 2>/dev/null | sort); do
    if [ -f "$mp3file" ]; then
        # ファイル名から情報を抽出
        FILENAME=$(basename "$mp3file")

        # トラック番号を抽出(先頭の数字)
        TRACK_NUM=$(echo "$FILENAME" | sed 's/^\([0-9]*\).*/\1/')

        # タイトルを抽出(番号と拡張子を除去)
        TITLE=$(echo "$FILENAME" | sed 's/^[0-9]*_//' | sed 's/\.mp3$//')

        # 長さを取得(秒)
        DURATION=$(ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$mp3file" 2>/dev/null)

        # 小数点以下2桁に丸める
        DURATION=$(printf "%.2f" "$DURATION")

        # 合計時間を加算
        TOTAL_DURATION=$(echo "$TOTAL_DURATION + $DURATION" | bc)

        # 対応するpng画像があるかチェック
        PNG_FILE="$ALBUM_DIR/$TRACK_NUM.png"
        HAS_IMAGE="false"
        if [ -f "$PNG_FILE" ]; then
            HAS_IMAGE="true"
        fi

        # JSON出力
        if [ "$FIRST" = true ]; then
            FIRST=false
        else
            echo ',' >> "$OUTPUT_FILE"
        fi

        echo -n "    {\"num\": $TRACK_NUM, \"file\": \"$FILENAME\", \"title\": \"$TITLE\", \"duration\": $DURATION, \"hasImage\": $HAS_IMAGE}" >> "$OUTPUT_FILE"

        # コンソール出力
        printf "%02d. %-20s %7.2f秒  画像:%s\n" "$TRACK_NUM" "$TITLE" "$DURATION" "$HAS_IMAGE"
    fi
done

echo '' >> "$OUTPUT_FILE"
echo '  ],' >> "$OUTPUT_FILE"

# 合計時間を追加
TOTAL_MIN=$(echo "$TOTAL_DURATION / 60" | bc)
TOTAL_SEC=$(echo "$TOTAL_DURATION % 60" | bc)
echo "  \"totalDuration\": $TOTAL_DURATION," >> "$OUTPUT_FILE"
echo "  \"totalFormatted\": \"${TOTAL_MIN}${TOTAL_SEC}\"" >> "$OUTPUT_FILE"
echo '}' >> "$OUTPUT_FILE"

echo ""
echo "----------------------------------------"
echo "合計時間: ${TOTAL_MIN}${TOTAL_SEC}秒 ($TOTAL_DURATION秒)"
echo ""
echo "出力: $OUTPUT_FILE"
echo "完了!"

出力例:

{
  "album_name": "温度帯 -ONDOTAI-",
  "total_duration": 2145.5,
  "tracks": [
    {
      "number": 1,
      "title": "雪冷え",
      "filename": "01_yukibie.mp3",
      "duration": 214.5,
      "image": "01.png"
    }
  ]
}

After Effects自動化(ExtendScript)

scripts/create_album_video.jsx は、tracks.json を読み込んでAfter Effectsのコンポジションを自動生成します。

各トラックに対して:

  1. 背景画像レイヤー(ガウスぼかし適用)
  2. オーディオスペクトラム
  3. タイトルテキスト
  4. フェードイン/アウト

を自動配置します。手動でやると1アルバム30分以上かかる作業が、数秒で完了します。

create_album_video.jsx(クリックで展開)
/**
 * After Effects Script: Album Video Creator
 * 汎用アルバム動画作成スクリプト
 *
 * 【使い方】
 * 1. ターミナルで analyze_album.sh を実行して tracks.json を生成
 *    ./scripts/analyze_album.sh ./albums/album_01
 * 2. After Effectsを開く
 * 3. ファイル > スクリプト > スクリプトファイルを実行...
 * 4. このファイル (create_album_video.jsx) を選択
 * 5. フォルダ選択ダイアログでアルバムフォルダを選択
 */

(function() {
    // === 設定 ===
    var config = {
        width: 1920,
        height: 1080,
        frameRate: 30,
        backgroundColor: [0, 0, 0],  // 黒
        titleFadeIn: 0.5,  // フェードイン時間
        titleFadeOut: 0.5, // フェードアウト時間
        fontName: "NotoSansJP-Regular",  // Noto Sans JP
        fontSize: 80,
        subFontSize: 40,
        fontColor: [1, 1, 1],  // 白
        blurAmount: 100  // 背景ぼかし量
    };

    // === JSONパーサー(ExtendScript用簡易版) ===
    function parseJSON(jsonString) {
        // eval を使用(ExtendScriptにはJSON.parseがない)
        return eval('(' + jsonString + ')');
    }

    // === ファイル読み込み ===
    function readFile(filePath) {
        var file = new File(filePath);
        if (!file.exists) {
            return null;
        }
        file.open('r');
        var content = file.read();
        file.close();
        return content;
    }

    // === メイン処理 ===
    function main() {
        // プロジェクトが開いているか確認
        if (!app.project) {
            app.newProject();
        }

        // フォルダ選択ダイアログを表示
        alert("アルバムフォルダを選択してください。\n(tracks.json と mp3ファイルが入っているフォルダ)");
        var albumFolder = Folder.selectDialog("アルバムフォルダを選択");

        if (!albumFolder) {
            alert("キャンセルされました。");
            return;
        }

        // tracks.json を読み込み
        var jsonPath = albumFolder.fsName + "/tracks.json";
        var jsonContent = readFile(jsonPath);

        if (!jsonContent) {
            alert("tracks.json が見つかりません。\n\n先にターミナルで以下を実行してください:\n./scripts/analyze_album.sh " + albumFolder.fsName);
            return;
        }

        var albumData;
        try {
            albumData = parseJSON(jsonContent);
        } catch (e) {
            alert("tracks.json の解析に失敗しました。\n" + e.toString());
            return;
        }

        var tracks = albumData.tracks;
        var totalDuration = albumData.totalDuration;

        // アルバム名をフォルダ名から取得
        var albumName = albumFolder.name;

        // メインコンポジションを作成
        var comp = app.project.items.addComp(
            albumName + "_Video",
            config.width,
            config.height,
            1,  // ピクセルアスペクト比
            totalDuration,
            config.frameRate
        );

        // 背景用ソリッドを作成
        var bgSolid = comp.layers.addSolid(
            config.backgroundColor,
            "Background",
            config.width,
            config.height,
            1
        );
        bgSolid.startTime = 0;
        bgSolid.outPoint = totalDuration;

        // 各トラックを配置
        var currentTime = 0;

        for (var i = 0; i < tracks.length; i++) {
            var track = tracks[i];
            var trackNum = track.num.toString();
            if (trackNum.length < 2) trackNum = "0" + trackNum;

            // 音声ファイルを検索
            var audioFile = new File(albumFolder.fsName + "/" + track.file);

            var audioLayer = null;
            if (audioFile.exists) {
                // 音声をインポート
                var importOptions = new ImportOptions(audioFile);
                var audioItem = app.project.importFile(importOptions);

                // コンポジションに追加
                audioLayer = comp.layers.add(audioItem);
                audioLayer.startTime = currentTime;
                audioLayer.name = track.title;

                // 実際のファイルから長さを更新
                track.duration = audioLayer.outPoint - audioLayer.startTime;

                // オーディオスペクトラム用ソリッドを作成
                var spectrumLayer = comp.layers.addSolid(
                    [0, 0, 0],
                    "Spectrum_" + trackNum,
                    config.width,
                    config.height,
                    1
                );
                spectrumLayer.startTime = currentTime;
                spectrumLayer.outPoint = currentTime + track.duration;

                // Audio Spectrumエフェクトを適用
                var spectrumEffect = spectrumLayer.property("Effects").addProperty("ADBE AudSpect");

                // エフェクト設定
                spectrumEffect.property("ADBE AudSpect-0001").setValue(audioLayer.index);
                spectrumEffect.property("ADBE AudSpect-0002").setValue([50, config.height - 120]);
                spectrumEffect.property("ADBE AudSpect-0003").setValue([config.width - 50, config.height - 120]);
                spectrumEffect.property("ADBE AudSpect-0006").setValue(20);
                spectrumEffect.property("ADBE AudSpect-0007").setValue(3000);
                spectrumEffect.property("ADBE AudSpect-0008").setValue(128);
                spectrumEffect.property("ADBE AudSpect-0009").setValue(400);
                spectrumEffect.property("ADBE AudSpect-0010").setValue(50);
                spectrumEffect.property("ADBE AudSpect-0012").setValue(10);
                spectrumEffect.property("ADBE AudSpect-0013").setValue(0);
                spectrumEffect.property("ADBE AudSpect-0014").setValue([1, 1, 1]);
                spectrumEffect.property("ADBE AudSpect-0015").setValue([0.5, 0.5, 0.5]);
                spectrumEffect.property("ADBE AudSpect-0020").setValue(2);
                spectrumEffect.property("ADBE AudSpect-0021").setValue(2);

                // フェード効果
                var specOpacity = spectrumLayer.property("Opacity");
                specOpacity.setValueAtTime(currentTime, 0);
                specOpacity.setValueAtTime(currentTime + config.titleFadeIn, 100);
                specOpacity.setValueAtTime(currentTime + track.duration - config.titleFadeOut, 100);
                specOpacity.setValueAtTime(currentTime + track.duration, 0);

            } else {
                alert("ファイルが見つかりません: " + track.file + "\nスキップします。");
            }

            // 背景画像を読み込み(XX.png)
            var bgImageFile = new File(albumFolder.fsName + "/" + trackNum + ".png");

            if (bgImageFile.exists) {
                // 画像をインポート
                var bgImportOptions = new ImportOptions(bgImageFile);
                var bgImageItem = app.project.importFile(bgImportOptions);

                // コンポジションに追加
                var bgImageLayer = comp.layers.add(bgImageItem);
                bgImageLayer.startTime = currentTime;
                bgImageLayer.outPoint = currentTime + track.duration;
                bgImageLayer.name = "BG_" + trackNum;

                // 画面サイズにフィット
                var imgWidth = bgImageItem.width;
                var imgHeight = bgImageItem.height;
                var scaleX = (config.width / imgWidth) * 100;
                var scaleY = (config.height / imgHeight) * 100;
                var scale = Math.max(scaleX, scaleY);
                bgImageLayer.property("Scale").setValue([scale, scale]);

                // 中央に配置
                bgImageLayer.property("Position").setValue([config.width / 2, config.height / 2]);

                // ガウスブラーを適用
                var blurEffect = bgImageLayer.property("Effects").addProperty("ADBE Gaussian Blur 2");
                blurEffect.property(1).setValue(config.blurAmount);

                // フェード効果
                var bgOpacity = bgImageLayer.property("Opacity");
                bgOpacity.setValueAtTime(currentTime, 0);
                bgOpacity.setValueAtTime(currentTime + config.titleFadeIn, 100);
                bgOpacity.setValueAtTime(currentTime + track.duration - config.titleFadeOut, 100);
                bgOpacity.setValueAtTime(currentTime + track.duration, 0);

                // 最背面に移動
                bgImageLayer.moveAfter(comp.layer(comp.numLayers));
            }

            // タイトルテキストレイヤーを作成
            var titleLayer = comp.layers.addText(track.title);
            var titleTextProp = titleLayer.property("Source Text");
            var titleTextDoc = titleTextProp.value;

            titleTextDoc.font = config.fontName;
            titleTextDoc.fontSize = config.fontSize;
            titleTextDoc.fillColor = config.fontColor;
            titleTextDoc.justification = ParagraphJustification.CENTER_JUSTIFY;
            titleTextProp.setValue(titleTextDoc);

            titleLayer.property("Position").setValue([config.width / 2, config.height / 2 - 50]);
            titleLayer.name = "Title_" + trackNum;

            titleLayer.startTime = currentTime;
            titleLayer.outPoint = currentTime + track.duration;

            var opacity = titleLayer.property("Opacity");
            opacity.setValueAtTime(currentTime, 0);
            opacity.setValueAtTime(currentTime + config.titleFadeIn, 100);
            opacity.setValueAtTime(currentTime + track.duration - config.titleFadeOut, 100);
            opacity.setValueAtTime(currentTime + track.duration, 0);

            // トラック番号を作成
            var numLayer = comp.layers.addText(track.num.toString() + " / " + tracks.length.toString());
            var numTextProp = numLayer.property("Source Text");
            var numTextDoc = numTextProp.value;

            numTextDoc.font = config.fontName;
            numTextDoc.fontSize = 30;
            numTextDoc.fillColor = config.fontColor;
            numTextDoc.justification = ParagraphJustification.CENTER_JUSTIFY;
            numTextProp.setValue(numTextDoc);

            numLayer.property("Position").setValue([config.width / 2, config.height - 80]);
            numLayer.name = "Num_" + trackNum;

            numLayer.startTime = currentTime;
            numLayer.outPoint = currentTime + track.duration;

            var numOpacity = numLayer.property("Opacity");
            numOpacity.setValueAtTime(currentTime, 0);
            numOpacity.setValueAtTime(currentTime + config.titleFadeIn, 100);
            numOpacity.setValueAtTime(currentTime + track.duration - config.titleFadeOut, 100);
            numOpacity.setValueAtTime(currentTime + track.duration, 0);

            // 次のトラックへ
            currentTime += track.duration;
        }

        // コンポジションを開く
        comp.openInViewer();

        alert("コンポジションを作成しました!\n\nアルバム: " + albumName + "\n合計時間: " + albumData.totalFormatted + "\nトラック数: " + tracks.length);
    }

    // 実行
    app.beginUndoGroup("Create Album Video");
    try {
        main();
    } catch (e) {
        alert("エラー: " + e.toString());
    }
    app.endUndoGroup();

})();

実際に作ったアルバム

Album 01: 温度帯 -ONDOTAI-

日本酒の提供温度(5℃〜55℃)をコンセプトにしたファーストアルバムです。

参考:YouTube(あくまでプロトタイプのため限定公開)

温度帯 楽曲名 ジャンル BPM
雪冷え(5℃) 雪冷え Minimal Techno × Japanese Kayokyoku 100
花冷え(10℃) 花冷え Deep Tech House × Japanese Vocal House 122
涼冷え(15℃) 涼冷え Deep House × Japanese City Pop 110
冷や(常温) 冷や Club Enka × Techno Kayokyoku 118
日向燗(30℃) 日向燗 Deep House × Japanese Tropical Bass 125
人肌燗(35℃) 人肌燗 UK Garage × Japanese Neo-Soul 130
ぬる燗(40℃) ぬる燗 Tech House × Japanese Electronic 128
上燗(45℃) 上燗 Big Room House × Japanese Electronic 132
熱燗(50℃) 熱燗 Hardstyle × Japanese Hardcore 150
飛び切り燗(55℃〜) 飛び切り燗 Uptempo Hardcore × Japanese Gabber 180

温度が上がるにつれてBPMも上昇していき、最後の「飛び切り燗」はアルバムのフィナーレとして一気に180BPMまで駆け上がる構成です。

次回作は、酒米(日本酒の原料となる米の品種)をテーマにしたアルバムを作ろうと考えています。

所感

良かった点

  • 頭の中のイメージを「とりあえず形にできる」
    完璧ではなくても、「こういう雰囲気」を音として聴けるのは嬉しいです

  • プロンプト設計自体が楽しい
    「このジャンルとこのジャンルを混ぜたらどうなる?」という実験ができます

  • 自動化で繰り返し作業が減る
    動画化の部分はスクリプトで完全自動化できました

限界

  • 細かいアレンジのコントロールは難しい
    「ここのギターをもう少し控えめに」といった調整はできません

  • 毎回同じ結果にならない
    同じプロンプトでも生成のたびに違う曲になります

  • 転調の指定が難しい
    サビで転調させたい、といった指定は今のところ上手く実現できていません

おわりに

このプロジェクトは、「音楽を作れない人間が、脳内アイディアを少しだけ音楽でアウトプットしてみる」試みでした。「こういう曲があったらいいな」というアイディアを、とりあえず音として聴ける形にできたのは、個人的にはとても意味がありました。誰に聴かせるわけでもなく、自分だけで「ああ、こういうのが聴きたかったんだ」と楽しむ——そういう使い方です。

使い道としては、例えば:

  • 今日の飲み会だけの特別なオリジナルBGM
  • 身内のイベントや結婚式二次会で流すBGM
  • 作業用BGMとして自分だけで聴く
  • 開発日記や制作風景を楽曲にして、個人用の記録として残す

といった、ごく私的な場面での活用が考えられます。「今日だけのBGM」を、プロに頼むほどではないけど既存曲でもない形で用意できる——そんな選択肢が増えたのは、個人的には嬉しいことでした。

同じように「頭の中にだけアルバムがある」という方の参考になれば幸いです。

身内に送ってフィードバックをもらったときに、「花冷えが好みでした」「冷やですね」「飛び切り燗が耳から離れません」といった感想を頂けて、なんだか嬉しかったです。音楽プロデューサーの疑似体験ができた気分でした。

あくまで私の脳内に浮かんだプロトタイプとしてのフルアルバム「温度帯」ですが、これがいつか本当にアーティストの方によって楽曲制作され、プロのボーカリストによって歌唱され、どこかでリリースされる日が来たら面白いな、とも思っています。

今後の展望

やってみたいこととして、以下のようなものがあります。

  • 歌詞のSRTファイル化 - 歌詞データから字幕ファイルを生成して、動画に歌詞字幕を入れる
  • オフボーカルのステムを活用 - イベントの主役に実際に歌ってもらう、なんてことも面白いかもしれません

まだまだ改善の余地はありますが、趣味の範囲でゆっくり拡張していければと思います。引き続き、晩酌がちょっと楽しくなるコンテンツを模索していきます。

まとめ

この記事が、同じように 「脳内にだけフルアルバムがある」 という方にとって、ささやかなクリスマスプレゼントになることを祈り、この記事およびApplibot Advent Calendar 2025 を締めたいと思います。

ありがとうございました。メリークリスマス。

参考リンク

プロモーション

株式会社アプリボットでは、 エンジニアをはじめ全職種で積極採用中 です。
(完全趣味話でしたが)少しでも興味を持たれた方は是非お気軽にご連絡ください!

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?