休憩に収まるプレイリストを作る

C++Beginner
オンラインで実践に進む

はじめに

休憩には時間の上限があります。音楽ライブラリには、好みの優先度、秒単位の長さ、固定のトラック ID を持つ完全な録音が用意されています。公開された順序で曲を調べ、時間内に収まる録音を丸ごと選び、実際に再生できるリストを保存する、再利用可能な C++ プログラムを作ります。

基本的なターミナル操作と、C++ の文字列、クラス、vector、ファイルストリーム、例外の知識が必要です。この実習で使う const 参照、比較関数、相対パスは説明します。パーサー、オリジナルの合成音源、ブラウザプレーヤーは準備済みです。作成するのは選択関数と出力関数であり、音声変換ソフトや最適な詰め込みを求めるアルゴリズムではありません。

コマンドには Terminal タブ、試聴には Player タブを使います。どちらも同じ環境を利用します。編集するのは selection.cpp と export.cpp だけとし、プレイリストは outputs/ 直下に作成してください。曲目表、提供されたプログラムの枠組み、outputs/audio/ の元ファイルは変更しません。これらのオリジナル合成楽曲には、外部の音声サンプルを使用していません。

完全な曲を休憩時間に収める

このステップでは、まず正常な例を聴き、その後、残り時間を数えて完全な録音を選びます。Player から始めます。example-120.m3u8 を選び、Load playlist、続いて Play 1 をクリックします。リストには Morning Light(50 秒)と Window Seat(70 秒)が入っています。音声コントロールで一時停止や再生位置の移動を試してください。次に example-90.m3u8 を選び、読み込んで唯一の曲を再生します。短い休憩では最初の録音を丸ごと残し、2 曲目を途中で切りません。

Terminal に切り替えます。プロンプトに不要な文字や未完了のコマンドがあれば、Ctrl+C でその入力を取り消します。cd はディレクトリを移動するコマンドです。次の絶対パスは、通常の作業ディレクトリの間違いから戻る際にも使えます。

cd /home/labex/project/short-break

cat はテキストファイルを表示します。sample.tsv は 2 曲の曲目表です。TSV はタブで区切られた値を意味します。列は ID、タイトル、整数の秒数、優先度、音声パスの順です。優先度が高いものから調べます。ID は録音を識別するもので、タイトルは重複してもかまいません。

cat sample.tsv

playlist.hpp は Track オブジェクトと 2 つの関数のインターフェースを宣言しています。各 Track は 5 つのフィールドをまとめます。vector<Track> は複数のオブジェクトを順番に保持します。この短い定義を読んでください。

cat playlist.hpp

用意された初期プログラムをビルドします。g++ は C++ をコンパイルし、-std=c++17 は C++17 を指定します。-Wall -Wextra は有用な警告を有効にし、-o playlist は実行ファイル名を指定します。3 つの .cpp ファイルをまとめてコンパイルします。初期プログラムでは予算の引数が未使用であるという警告が出ますが、これは想定どおりです。

g++ -std=c++17 -Wall -Wextra main.cpp selection.cpp export.cpp -o playlist

./playlist は現在のディレクトリの実行ファイルを起動します。引数は曲目表、1 から 86400 まで(両端を含む)の整数秒の予算、出力パスです。準備済みパーサーは、曲目表の長さと優先度にも同じ範囲を適用します。パーサーを実装する必要はありません。不完全な初期プログラムで 90 秒の例を実行します。

./playlist sample.tsv 90 outputs/sample-90.m3u8

両方の録音と Total: 120s / 90s が表示されます。これが修正する問題です。初期出力はまだタイトルだけのリストなので、今 Player で読み込むと #EXTM3U ヘッダーがないというエラーになります。これは想定された形式エラーで、音声ファイルがない場合とは異なります。出力形式はステップ 3 で学びます。

次に chooseTracks で残りの予算を使います。tracks を値で渡すとローカルのコピーが作られるので、後で並べ替えても曲目表は変わりません。const auto& track は既存のオブジェクトをコピーも変更もせずに読みます。push_back は選んだオブジェクトを末尾に追加します。<= により、ちょうど収まる曲も含めます。追加後、-= で長さを引きます。収まらない曲は飛ばし、ループは続けます。

短いファイルを、次の完全な関数で置き換えます。> はこのソースファイルを上書きします。ヒアドキュメント <<'CPP' は、最後の CPP までの行を cat に渡します。引用符付きの区切り文字により C++ の文字がそのまま保存されます。最後に単独の行で置かれた区切り文字も含め、ブロック全体を貼り付けてください。

cat > selection.cpp <<'CPP'
#include "playlist.hpp"
#include <algorithm>
std::vector<Track> chooseTracks(std::vector<Track> tracks, int budget) {
    std::vector<Track> selected;
    for (const auto& track : tracks) {
        if (track.seconds <= budget) {
            selected.push_back(track);
            budget -= track.seconds;
        }
    }
    return selected;
}
CPP

ソースを編集したら再ビルドし、同じ例をもう一度実行します。

g++ -std=c++17 -Wall -Wextra main.cpp selection.cpp export.cpp -o playlist
./playlist sample.tsv 90 outputs/sample-90.m3u8

今度は t01 | Morning Light | 50s と Total: 50s / 90s だけが表示されます。残りは 40 秒なので、70 秒の録音は入りません。この版は入力の順序を使っています。次は、曲目表の行順に依存しない順序を実現します。

コンパイルエラーが行を示した場合は、エディターで selection.cpp を開いて修正するか、置き換えブロック全体をもう一度実行し、再ビルドしてください。ビルド失敗後に古い実行ファイルを動かしても、変更を検証したことにはなりません。

決まった優先順位を適用する

このステップでは、曲を選ぶ前にライブラリ全体を並べ替えます。Terminal の /home/labex/project/short-break から始めます。曲目表全体を確認してください。

cat catalog.tsv

行は意図的に混ぜてあります。優先度の高い順に調べ、同じ値ならトラック ID をアルファベットの昇順にします。そのため、長さにかかわらず t02 は t03 より先です。これは単純な好みの判断であり、合計時間を最大にする組み合わせ探索ではありません。

90 秒の予算では、t01 が 50 秒を使います。残り 40 秒には t02(95 秒)も t03(70 秒)も入りません。続けて t04(30 秒)を選ぶと残りは 10 秒です。t05(20 秒)は入りません。結果は t01、t04、合計 80 秒です。最初の長すぎる曲で止まると、後にある適切な選択肢を逃します。

std::sort は begin() から end() までの半開区間を使います。比較関数は、a を b より先にすべき場合に true を返します。[] はラムダと呼ぶ小さな無名関数を導入し、2 つの const Track& 引数は比較対象を読み取ります。!= の判定で主となる比較を選び、最後の return が同点を ID で決着させます。selection.cpp を完全版で置き換えてください。

cat > selection.cpp <<'CPP'
#include "playlist.hpp"
#include <algorithm>
std::vector<Track> chooseTracks(std::vector<Track> tracks, int budget) {
    std::sort(tracks.begin(), tracks.end(),
        [](const Track& a, const Track& b) {
            if (a.preference != b.preference)
                return a.preference > b.preference;
            return a.id < b.id;
        });
    std::vector<Track> selected;
    for (const auto& track : tracks) {
        if (track.seconds <= budget) {
            selected.push_back(track);
            budget -= track.seconds;
        }
    }
    return selected;
}
CPP

再ビルドし、両方の予算で実行します。

g++ -std=c++17 -Wall -Wextra main.cpp selection.cpp export.cpp -o playlist
./playlist catalog.tsv 90 outputs/break-90.m3u8
./playlist catalog.tsv 120 outputs/break-120.m3u8

90 秒の結果は t01、t04 で、合計 80 秒です。120 秒では t01、t03 となり、ちょうど 120 秒です。t02 を飛ばした後、70 秒の曲が残り 70 秒にぴったり入ります。どちらも完全な録音を使用します。次のステップまでは、出力はまだタイトルのリストです。

再生できるプレイリストを出力する

このステップでは、選んだオブジェクトを UTF-8 の拡張 M3U として保存します。Terminal から始めます。.m3u8 はテキストのプレイリストであり、音声そのものではありません。先頭行は #EXTM3U です。選んだ曲ごとに #EXTINF:seconds,title のメタデータ行を書き、次の行に音声パスを書きます。

相対パスの基準はプレイリストのディレクトリであり、曲目表の場所やプロセスの現在のディレクトリではありません。生成するリストは outputs/ 直下に保存し、元音源は outputs/audio/ に置いてあります。そのため audio/Morning Light.wav のような項目は、リストの隣にある録音を参照できます。ダウンロードや共有時もこのフォルダー関係を保ってください。空白と Unicode は使用できます。用意されたパスを保ち、改行、先頭の #、絶対パス、親ディレクトリへの移動、追加のプレイリスト命令は使いません。

std::ofstream は出力ファイルを開きます。<< はテキストや数値を書き、\n は改行を書きます。関数は選択した vector と出力名を const 参照で受け取るので、コピーも変更もしません。ファイル操作の失敗は例外となり、用意された main プログラムが報告します。入力パーサーは、この関数が出力を開く前に正の整数の予算、曲目表、参照される全音源を検証するため、それらの入力エラーでは既存の出力が保持されます。

export.cpp を置き換えます。

cat > export.cpp <<'CPP'
#include "playlist.hpp"
#include <fstream>
#include <stdexcept>
void writePlaylist(const std::vector<Track>& tracks, const std::string& output) {
    std::ofstream file(output);
    if (!file) throw std::runtime_error("Cannot write playlist.");
    file << "#EXTM3U\n";
    for (const auto& track : tracks) {
        file << "#EXTINF:" << track.seconds << ',' << track.title << '\n';
        file << track.path << '\n';
    }
    if (!file) throw std::runtime_error("Playlist write failed.");
}
CPP

再ビルドし、更新した出力関数で両方のリストを作り直します。

g++ -std=c++17 -Wall -Wextra main.cpp selection.cpp export.cpp -o playlist
./playlist catalog.tsv 90 outputs/break-90.m3u8
./playlist catalog.tsv 120 outputs/break-120.m3u8

保存した 90 秒のファイルを確認します。

cat outputs/break-90.m3u8

保存された UTF-8 ファイルの内容は次のとおりです。

#EXTM3U
#EXTINF:50,Morning Light
audio/Morning Light.wav
#EXTINF:30,夜の散歩
audio/夜の散歩.wav

日本語のタイトルとファイル名も UTF-8 テキストのままです。一部のターミナルでは、保存されたバイトは正常でも表示が代替文字になることがあります。表示を直すために元ファイルを改名しないでください。Player では元のタイトルとパスを確認できます。長さの行は元ファイル全体を表し、ここの数値を書き換えても音声は短くなりません。

Player に切り替えます。Refresh files をクリックし、break-90.m3u8 を選んで Load playlist をクリックします。2 曲、宣言された合計 80 秒が表示されるはずです。Play 1 をクリックし、音声コントロールで位置を動かして一時停止した後、Play 2 で 2 曲目を聴いてください。ブラウザは保存した M3U8 と、そこに書かれた音声パスだけを読みます。音源がなければ Cannot play と表示され、代わりの曲を補うことはありません。Download playlist と Download current track で実ファイルを保存できます。リストだけでは音声は含まれません。

試聴画面に表示された保存済みの休憩用リスト

2 行の曲と長さ、その下の音声コントロールに注目してください。再生を確かめたら Terminal に戻ります。

再利用と拒否メッセージを確かめる

このステップでは、正しい短い結果と不正な入力を区別します。Terminal から始めます。すべての曲より短い正の予算も有効ですが、完全な録音は 1 曲も入りません。1 秒のリストを生成します。

./playlist catalog.tsv 1 outputs/tiny.m3u8

Total: 0s / 1s と表示され、ファイルには #EXTM3U だけが入ります。Player に読み込むと 0 曲になるはずです。これは正しい空の結果であり、接続や解析の失敗ではありません。

CLI は整数秒を数字で受け取るため、90s のような予算は不正です。次のコマンドは意図的にステータス 1 で終了し、既存の 90 秒のリストを置き換えてはいけません。echo $? は直前のコマンドの終了ステータスを表示します(0 は成功)。次の 2 つを続けて実行してください。

./playlist catalog.tsv 90s outputs/break-90.m3u8
echo $?

予算と長さは正の整数秒でなければならないというエラーの後に、ステータス 1 が表示されます。環境をリセットせず、末尾の文字を除いて 90 で再試行します。入力パスの誤記なら Cannot open catalog が曲目表の引数を示します。ステップ 1 の絶対パスの cd と、正しい名前 catalog.tsv で戻れます。Missing audio は曲目表のパスがファイルに届かないことを示します。元音源は保ち、再試行前にパスを確認してください。

既存の正しいファイルに、2 組の正しいメタデータとパスが残っていることを確認します。

cat outputs/break-90.m3u8

これで同じプログラムを異なる有効な正の予算で再利用できます。出力先は常に outputs/ 直下です。優先度と ID で並べ、長すぎる候補を飛ばし、録音を切り詰めることはありません。実際のスピーカーからの音や、あらゆる外部プレーヤーの動作はこの実習の対象外です。提供されたブラウザはこれらの実際の WAV ファイルをデコードします。

まとめ

C++ のオブジェクトと vector で録音を並べ、残りの予算内で完全な曲を選び、UTF-8 M3U8 ファイルを書きました。実際の出力を再生して相対パスを理解し、有効な空リストと不正な入力も区別しました。次のチャレンジではライブラリが変わりますが、規則は同じです。