휴식 시간에 맞는 재생 목록 만들기

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를 선택하여 불러오고 하나뿐인 트랙을 재생합니다. 짧은 휴식은 첫 녹음을 온전히 유지하며, 두 번째 녹음을 자르지 않습니다.

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은 선택한 객체를 결과 뒤에 추가합니다. <=는 정확히 들어맞는 트랙도 포함합니다. 추가한 뒤 -=로 길이를 뺍니다. 들어가지 않는 트랙은 건너뛰고 반복은 계속합니다.

짧은 파일을 아래의 완전한 함수로 바꾸세요. >는 이 소스 파일을 덮어씁니다. 히어 도큐먼트 <<'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를 반환합니다. []는 람다라는 작은 익명 함수를 시작하며, 두 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 참조로 받아서 복사하거나 변경하지 않습니다. 파일 실패는 예외를 던지고, 준비된 메인 프로그램이 이를 보고합니다. 입력 파서는 이 함수가 출력을 열기 전에 양의 정수 예산, 카탈로그, 참조된 모든 음원을 검사하므로 이런 입력 오류에서는 기존 출력이 보존됩니다.

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에서 시작하세요. 모든 트랙보다 짧은 양의 예산도 유효하지만 완전한 녹음은 하나도 들어가지 않습니다. 1초 목록을 생성합니다.

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

Total: 0s / 1s를 보고하며 파일에는 #EXTM3U만 들어 있습니다. Player에서 불러오면 트랙 수가 0이어야 합니다. 이는 올바른 빈 결과이며 연결이나 파싱 실패가 아닙니다.

CLI는 정수 초를 나타내는 숫자를 받으므로 90s 같은 예산은 잘못된 입력입니다. 다음 명령은 의도적으로 상태 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 파일을 작성했습니다. 실제 출력을 재생하면서 상대 경로를 이해하고, 올바른 빈 목록과 잘못된 입력도 구별했습니다. 다음 챌린지는 같은 규칙을 유지하면서 라이브러리를 바꿉니다.