创建适合休息时长的播放列表

C++Beginner
立即练习

介绍

休息有时长限制。你的音乐库为每段完整录音提供偏好分数、秒数和稳定的音轨 ID。你将编写一个可重复使用的 C++ 程序,按照公开规则依次考虑音轨,保留放得下的完整录音,并保存成真正可以播放的列表。

你需要掌握基本终端导航,以及 C++ 字符串、类、vector、文件流和异常。本实验会介绍用到的 const 引用、比较函数和相对路径。解析器、原创合成音频和浏览器播放器已经准备好。你负责编写选择与导出函数,不需要制作音频转换器或最优装箱算法。

在 Terminal Tab 中运行命令,在 Player Tab 中试听。二者使用同一个环境。只编辑 selection.cpp 和 export.cpp,将播放列表直接创建在 outputs/ 中。保持曲目表文件、提供的框架和 outputs/audio/ 中的原始文件不变。这些原创合成乐曲没有使用外部音频采样。

将完整音轨放进休息时间

在这一步中,你将先试听正常样例,再用剩余时间计数器选择完整录音。先打开 Player。选择 example-120.m3u8,点击 Load playlist,再点击 Play 1。列表包含 Morning Light(50 秒)和 Window Seat(70 秒)。用音频控件暂停或调整播放位置。接着选择 example-90.m3u8,加载并播放其中唯一的音轨。较短的休息保留第一段完整录音,而不是截短第二段。

切换到 Terminal。如果提示符处出现杂乱字符或未完成的命令,按 Ctrl+C 取消当前输入。cd 用于切换目录;下面的绝对路径也能帮助你从普通的目录错误中恢复:

cd /home/labex/project/short-break

cat 显示文本文件。sample.tsv 是只有两段音轨的曲目表;TSV 表示制表符分隔的值。各列依次是 ID、标题、整数秒数、偏好和音频路径。偏好越高,越先考虑。ID 标识一段录音;标题可以重复。

cat sample.tsv

playlist.hpp 声明了 Track 对象和两个函数的接口。每个 Track 将五个字段放在一起,vector<Track> 则按顺序保存多个对象。阅读这个简短定义:

cat playlist.hpp

编译提供的起始程序。g++ 编译 C++;-std=c++17 选择 C++17,-Wall -Wextra 启用有用的警告,-o playlist 指定可执行文件名。三个 .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 将选中的对象追加到结果中。<= 会包含恰好放得下的音轨;加入后,-= 减去它的时长。放不下的音轨被跳过,循环继续。

用下面的完整函数替换这个短文件。> 会替换该源文件。here-document <<'CPP' 把后面的行传给 cat,直到单独一行的 CPP 为止;带引号的分隔符会原样保留 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。[] 引入一个小型匿名函数,称为 lambda;两个 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 写入换行。函数通过 const 引用接收选中的 vector 和输出名称,因此不会复制或修改它们。文件错误会抛出异常,由准备好的主程序报告。输入解析器会在这个函数打开输出前,验证正整数预算、曲目表以及所有引用的音频;因此这些输入失败不会破坏已有输出。

替换 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。应显示两段音轨和声明的总时长 80 秒。点击 Play 1,用音频控件调整位置并暂停,然后点击 Play 2 试听第二段。浏览器读取的是你保存的 M3U8 及其中准确的音频路径。来源缺失会显示 Cannot play,不会自动补上一段替代音轨。通过 Download playlist 和 Download current track 保存真实文件;列表本身不包含音频数据。

试听空间中已保存的休息播放列表

注意两行音轨及其时长,以及下方的音频控件。检查播放后,切换回 Terminal。

复用程序并读懂拒绝信息

在这一步中,你将区分有效的短结果和无效输入。从 Terminal 开始。比所有音轨都短的正预算仍然有效,只是没有完整录音放得下。生成一个一秒预算的列表:

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

程序报告 Total: 0s / 1s,文件只包含 #EXTM3U。在 Player 中加载时应显示零段音轨。这是正确的空结果,不是连接或解析失败。

90s 这样的预算无效,因为 CLI 要求用数字表示整数秒。下一条命令会故意以状态 1 退出,而且不能替换你已有的 90 秒列表。echo $? 会立即显示上一条命令的退出状态(0 表示成功)。请连续执行这两条命令:

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

错误说明预算和时长必须是正整数秒,随后显示状态 1。不需要重置环境:重试时去掉后缀,使用 90。如果输入路径拼错,Cannot open catalog 会指向曲目表参数。用第 1 步的绝对 cd 路径和真实文件名 catalog.tsv 恢复。Missing audio 表示曲目表中的某个路径无法找到文件;保留准备好的原始文件,并在重试前检查路径。

确认原来的正确文件仍然包含两组有效的元数据和路径:

cat outputs/break-90.m3u8

现在可以用同一个程序处理不同的合法正预算,始终直接写入 outputs/。程序按偏好和 ID 排序,跳过过长的候选,绝不截短录音。本练习不验证实体扬声器输出,也不保证所有第三方播放器的行为;提供的浏览器会解码这些真实 WAV 文件。

总结

你使用 C++ 对象和 vector 对录音排序,在剩余预算内选择完整音轨,并写出 UTF-8 M3U8 文件。你播放了真实输出,理解了相对路径,也区分了有效空列表与无效输入。接下来的挑战将改变音乐库,但保留这些规则。