music: add music directory backend

This adds support to play both CDAudio single MP3 as well as loading
individual tracks directly from a music directory.
This commit is contained in:
Marcin Kurczewski 2024-05-01 10:44:37 +02:00
parent 0c827cb3b0
commit 475ca7c255
10 changed files with 516 additions and 286 deletions

View File

@ -90,7 +90,9 @@ dll_sources = [
'src/game/math.c',
'src/game/math_misc.c',
'src/game/matrix.c',
'src/game/music.c',
'src/game/music/music_main.c',
'src/game/music/music_backend_files.c',
'src/game/music/music_backend_cdaudio.c',
'src/game/objects/creatures/bird.c',
'src/game/output.c',
'src/game/overlay.c',

View File

@ -1,283 +0,0 @@
#include "game/music.h"
#include "global/const.h"
#include "global/funcs.h"
#include "global/types.h"
#include "global/vars.h"
#include <libtrx/engine/audio.h>
#include <libtrx/filesystem.h>
#include <libtrx/log.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdio.h>
#define MAX_CD_TRACKS 60
static MUSIC_TRACK_ID m_TrackCurrent = MX_INACTIVE;
static MUSIC_TRACK_ID m_TrackLooped = MX_INACTIVE;
typedef struct {
uint64_t from;
uint64_t to;
bool active;
} CDAUDIO_TRACK;
typedef struct {
char *path;
char *audio_type;
} CDAUDIO_SPEC;
static const int32_t m_CDAudioSpecCount = 2;
static CDAUDIO_SPEC m_CDAudioSpecs[] = {
{
.path = "audio/cdaudio.wav",
.audio_type = "waveaudio",
},
{
.path = "audio/cdaudio.mp3",
.audio_type = "mpegvideo",
},
};
static const CDAUDIO_SPEC *m_ChosenSpec = NULL;
static CDAUDIO_TRACK m_Tracks[MAX_CD_TRACKS];
static bool m_Initialized = false;
static float m_MusicVolume = 0.0f;
static int m_AudioStreamID = -1;
static const CDAUDIO_SPEC *Music_FindSpec(void);
static bool Music_ParseCDAudio(void);
static void Music_StreamFinished(int stream_id, void *user_data);
static void Music_StreamFinished(const int stream_id, void *const user_data)
{
// When a stream finishes, play the remembered background BGM.
if (stream_id == m_AudioStreamID) {
m_AudioStreamID = -1;
if (m_TrackLooped >= 0) {
Music_Play(m_TrackLooped, true);
}
}
}
static const CDAUDIO_SPEC *Music_FindSpec(void)
{
const CDAUDIO_SPEC *spec = NULL;
for (int32_t i = 0; i < m_CDAudioSpecCount; i++) {
MYFILE *const fp = File_Open(m_CDAudioSpecs[i].path, FILE_OPEN_READ);
if (fp != NULL) {
spec = &m_CDAudioSpecs[i];
File_Close(fp);
break;
}
}
if (spec == NULL) {
LOG_WARNING("Cannot find any CDAudio data files");
return NULL;
}
return spec;
}
static bool Music_ParseCDAudio(void)
{
char *track_content = NULL;
size_t track_content_size;
if (!File_Load("audio/cdaudio.dat", &track_content, &track_content_size)) {
LOG_WARNING("Cannot find CDAudio control file");
return false;
}
memset(m_Tracks, 0, sizeof(m_Tracks));
size_t offset = 0;
while (offset < track_content_size) {
while (track_content[offset] == '\n' || track_content[offset] == '\r') {
if (++offset >= track_content_size) {
goto parse_end;
}
}
uint64_t track_num;
uint64_t from;
uint64_t to;
int32_t result = sscanf(
&track_content[offset], "%" PRIu64 " %" PRIu64 " %" PRIu64,
&track_num, &from, &to);
if (result == 3 && track_num > 0 && track_num <= MAX_CD_TRACKS) {
int32_t track_idx = track_num - 1;
m_Tracks[track_idx].active = true;
m_Tracks[track_idx].from = from;
m_Tracks[track_idx].to = to;
}
while (track_content[offset] != '\n' && track_content[offset] != '\r') {
if (++offset >= track_content_size) {
goto parse_end;
}
}
}
parse_end:
free(track_content);
// reindex wrong track boundaries
for (int32_t i = 0; i < MAX_CD_TRACKS; i++) {
if (!m_Tracks[i].active) {
continue;
}
if (i < MAX_CD_TRACKS - 1 && m_Tracks[i].from >= m_Tracks[i].to) {
for (int32_t j = i + 1; j < MAX_CD_TRACKS; j++) {
if (m_Tracks[j].active) {
m_Tracks[i].to = m_Tracks[j].from;
break;
}
}
}
if (m_Tracks[i].from >= m_Tracks[i].to && i > 0) {
for (int32_t j = i - 1; j >= 0; j--) {
if (m_Tracks[j].active) {
m_Tracks[i].from = m_Tracks[j].to;
break;
}
}
}
}
return true;
}
bool __cdecl Music_Init(void)
{
// TODO: remove this guard once Music_Init can be called in a proper place
if (m_Initialized) {
return true;
}
if (!Audio_Init()) {
LOG_ERROR("Failed to initialize libtrx sound system");
return false;
}
m_ChosenSpec = Music_FindSpec();
if (m_ChosenSpec == NULL) {
LOG_ERROR("Failed to find CDAudio data");
return false;
}
if (!Music_ParseCDAudio()) {
LOG_ERROR("Failed to parse CDAudio data");
return false;
}
m_Initialized = true;
m_TrackCurrent = MX_INACTIVE;
m_TrackLooped = MX_INACTIVE;
Music_SetVolume(25 * g_OptionMusicVolume + 5);
return true;
}
void __cdecl Music_Shutdown(void)
{
if (m_AudioStreamID < 0) {
return;
}
// We are only interested in calling Music_StreamFinished if a stream
// finished by itself. In cases where we end the streams early by hand,
// we clear the finish callback in order to avoid resuming the BGM playback
// just after we stop it.
Audio_Stream_SetFinishCallback(m_AudioStreamID, NULL, NULL);
Audio_Stream_Close(m_AudioStreamID);
}
void __cdecl Music_Play(int16_t track_id, bool is_looped)
{
if (track_id == m_TrackCurrent) {
return;
}
// TODO: this should be called in shell instead, once per game launch
Music_Init();
Audio_Stream_Close(m_AudioStreamID);
if (g_OptionMusicVolume == 0) {
LOG_DEBUG("Not playing track %d because the game is silent", track_id);
return;
}
const int32_t track_idx = Music_GetRealTrack(track_id) - 1;
if (track_idx < 0 || track_idx >= MAX_CD_TRACKS) {
LOG_ERROR("Invalid track: %d", track_id);
return;
}
const CDAUDIO_TRACK *track = &m_Tracks[track_idx];
if (!track->active) {
LOG_ERROR("Invalid track: %d", track_id);
return;
}
LOG_DEBUG(
"Playing track %d (real: %d), looped: %d", track_id, track_idx + 1,
is_looped);
m_AudioStreamID = Audio_Stream_CreateFromFile(m_ChosenSpec->path);
if (m_AudioStreamID < 0) {
LOG_ERROR("Failed to create music stream for track %d", track_id);
return;
}
g_CD_TrackID = track_id;
m_TrackCurrent = track_id;
if (is_looped) {
m_TrackLooped = track_id;
}
Audio_Stream_SetVolume(m_AudioStreamID, m_MusicVolume);
Audio_Stream_SetFinishCallback(m_AudioStreamID, Music_StreamFinished, NULL);
Audio_Stream_SetStartTimestamp(m_AudioStreamID, track->from / 1000.0);
Audio_Stream_SetStopTimestamp(m_AudioStreamID, track->to / 1000.0);
Audio_Stream_SeekTimestamp(m_AudioStreamID, track->from / 1000.0);
Audio_Stream_SetIsLooped(m_AudioStreamID, is_looped);
}
void __cdecl Music_Stop(void)
{
if (m_AudioStreamID < 0) {
return;
}
m_TrackCurrent = MX_INACTIVE;
m_TrackLooped = MX_INACTIVE;
Audio_Stream_Close(m_AudioStreamID);
}
bool __cdecl Music_PlaySynced(int16_t track_id)
{
Music_Play(track_id, false);
return true;
}
uint32_t __cdecl Music_GetFrames(void)
{
if (m_AudioStreamID < 0) {
return 0;
}
return Audio_Stream_GetTimestamp(m_AudioStreamID) * FRAMES_PER_SECOND
* TICKS_PER_FRAME / 1000.0;
}
void __cdecl Music_SetVolume(int32_t volume)
{
m_MusicVolume = volume ? volume / 255.0f : 0.0f;
if (m_AudioStreamID >= 0) {
Audio_Stream_SetVolume(m_AudioStreamID, m_MusicVolume);
}
}

View File

@ -0,0 +1,11 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
typedef struct MUSIC_BACKEND {
bool (*init)(struct MUSIC_BACKEND *backend);
const char *(*describe)(const struct MUSIC_BACKEND *backend);
int32_t (*play)(const struct MUSIC_BACKEND *backend, int32_t track_id);
void *data;
} MUSIC_BACKEND;

View File

@ -0,0 +1,197 @@
#include "game/music/music_backend_cdaudio.h"
#include <libtrx/engine/audio.h>
#include <libtrx/filesystem.h>
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <assert.h>
#include <inttypes.h>
#include <stdarg.h>
#include <stdio.h>
#define MAX_CD_TRACKS 60
typedef struct {
uint64_t from;
uint64_t to;
bool active;
} CDAUDIO_TRACK;
typedef struct {
const char *path;
const char *description;
CDAUDIO_TRACK *tracks;
} BACKEND_DATA;
static bool Music_Backend_CDAudio_Parse(BACKEND_DATA *const data);
static bool Music_Backend_CDAudio_Init(MUSIC_BACKEND *const backend);
static const char *Music_Backend_CDAudio_Describe(
const MUSIC_BACKEND *const backend);
static int32_t Music_Backend_CDAudio_Play(
const MUSIC_BACKEND *const backend, int32_t track_id);
static bool Music_Backend_CDAudio_Parse(BACKEND_DATA *const data)
{
assert(data != NULL);
char *track_content = NULL;
size_t track_content_size;
if (!File_Load("audio/cdaudio.dat", &track_content, &track_content_size)) {
LOG_WARNING("Cannot find CDAudio control file");
return false;
}
data->tracks = Memory_Alloc(sizeof(CDAUDIO_TRACK) * MAX_CD_TRACKS);
size_t offset = 0;
while (offset < track_content_size) {
while (track_content[offset] == '\n' || track_content[offset] == '\r') {
if (++offset >= track_content_size) {
goto parse_end;
}
}
uint64_t track_num;
uint64_t from;
uint64_t to;
int32_t result = sscanf(
&track_content[offset], "%" PRIu64 " %" PRIu64 " %" PRIu64,
&track_num, &from, &to);
if (result == 3 && track_num > 0 && track_num <= MAX_CD_TRACKS) {
int32_t track_idx = track_num - 1;
data->tracks[track_idx].active = true;
data->tracks[track_idx].from = from;
data->tracks[track_idx].to = to;
}
while (track_content[offset] != '\n' && track_content[offset] != '\r') {
if (++offset >= track_content_size) {
goto parse_end;
}
}
}
parse_end:
Memory_Free(track_content);
// reindex wrong track boundaries
for (int32_t i = 0; i < MAX_CD_TRACKS; i++) {
if (!data->tracks[i].active) {
continue;
}
if (i < MAX_CD_TRACKS - 1
&& data->tracks[i].from >= data->tracks[i].to) {
for (int32_t j = i + 1; j < MAX_CD_TRACKS; j++) {
if (data->tracks[j].active) {
data->tracks[i].to = data->tracks[j].from;
break;
}
}
}
if (data->tracks[i].from >= data->tracks[i].to && i > 0) {
for (int32_t j = i - 1; j >= 0; j--) {
if (data->tracks[j].active) {
data->tracks[i].from = data->tracks[j].to;
break;
}
}
}
}
return true;
}
static bool Music_Backend_CDAudio_Init(MUSIC_BACKEND *const backend)
{
assert(backend != NULL);
BACKEND_DATA *data = backend->data;
assert(data != NULL);
MYFILE *const fp = File_Open(data->path, FILE_OPEN_READ);
if (fp == NULL) {
return false;
}
if (!Music_Backend_CDAudio_Parse(data)) {
LOG_ERROR("Failed to parse CDAudio data");
return false;
}
return true;
}
static const char *Music_Backend_CDAudio_Describe(
const MUSIC_BACKEND *const backend)
{
assert(backend != NULL);
const BACKEND_DATA *const data = backend->data;
assert(data != NULL);
return data->description;
}
static int32_t Music_Backend_CDAudio_Play(
const MUSIC_BACKEND *const backend, int32_t track_id)
{
assert(backend != NULL);
const BACKEND_DATA *const data = backend->data;
assert(data != NULL);
const int32_t track_idx = track_id - 1;
const CDAUDIO_TRACK *track = &data->tracks[track_idx];
if (track_idx < 0 || track_idx >= MAX_CD_TRACKS) {
LOG_ERROR("Invalid track: %d", track_id);
return -1;
}
if (!track->active) {
LOG_ERROR("Invalid track: %d", track_id);
return -1;
}
int32_t audio_stream_id = Audio_Stream_CreateFromFile(data->path);
Audio_Stream_SetStartTimestamp(audio_stream_id, track->from / 1000.0);
Audio_Stream_SetStopTimestamp(audio_stream_id, track->to / 1000.0);
Audio_Stream_SeekTimestamp(audio_stream_id, track->from / 1000.0);
return audio_stream_id;
}
MUSIC_BACKEND *Music_Backend_CDAudio_Factory(const char *path)
{
assert(path != NULL);
const char *description_fmt = "CDAudio (path: %s)";
const size_t description_size = snprintf(NULL, 0, description_fmt, path);
char *description = Memory_Alloc(description_size + 1);
sprintf(description, description_fmt, path);
BACKEND_DATA *data = Memory_Alloc(sizeof(BACKEND_DATA));
data->path = Memory_DupStr(path);
data->description = description;
MUSIC_BACKEND *backend = Memory_Alloc(sizeof(MUSIC_BACKEND));
backend->data = data;
backend->init = Music_Backend_CDAudio_Init;
backend->describe = Music_Backend_CDAudio_Describe;
backend->play = Music_Backend_CDAudio_Play;
return backend;
}
void Music_Backend_CDAudio_Destroy(MUSIC_BACKEND *backend)
{
if (backend == NULL) {
return;
}
if (backend->data != NULL) {
BACKEND_DATA *const data = backend->data;
Memory_FreePointer(&data->path);
Memory_FreePointer(&data->description);
Memory_FreePointer(&data->tracks);
}
Memory_FreePointer(&backend->data);
Memory_FreePointer(&backend);
}

View File

@ -0,0 +1,6 @@
#pragma once
#include "game/music/music_backend.h"
MUSIC_BACKEND *Music_Backend_CDAudio_Factory(const char *path);
void Music_Backend_CDAudio_Destroy(MUSIC_BACKEND *backend);

View File

@ -0,0 +1,107 @@
#include "game/music/music_backend_files.h"
#include <libtrx/engine/audio.h>
#include <libtrx/filesystem.h>
#include <libtrx/log.h>
#include <libtrx/memory.h>
#include <assert.h>
typedef struct {
const char *dir;
const char *description;
} BACKEND_DATA;
static const char *m_ExtensionsToTry[] = { ".flac", ".ogg", ".mp3", ".wav",
NULL };
static char *Music_Backend_Files_GetTrackFileName(
const char *base_dir, int32_t track);
static bool Music_Backend_Files_Init(MUSIC_BACKEND *const backend);
static int32_t Music_Backend_Files_Play(
const MUSIC_BACKEND *const backend, int32_t track_id);
static char *Music_Backend_Files_GetTrackFileName(
const char *base_dir, int32_t track)
{
char file_path[64];
sprintf(file_path, "%s/track%02d.flac", base_dir, track);
char *result = File_GuessExtension(file_path, m_ExtensionsToTry);
if (!File_Exists(file_path)) {
Memory_FreePointer(&result);
sprintf(file_path, "%s/%d.flac", base_dir, track);
result = File_GuessExtension(file_path, m_ExtensionsToTry);
}
return result;
}
static bool Music_Backend_Files_Init(MUSIC_BACKEND *const backend)
{
assert(backend != NULL);
const BACKEND_DATA *data = backend->data;
assert(data->dir != NULL);
return File_DirExists(data->dir);
}
static const char *Music_Backend_Files_Describe(
const MUSIC_BACKEND *const backend)
{
assert(backend != NULL);
const BACKEND_DATA *const data = backend->data;
assert(data != NULL);
return data->description;
}
static int32_t Music_Backend_Files_Play(
const MUSIC_BACKEND *const backend, int32_t track_id)
{
assert(backend != NULL);
const BACKEND_DATA *const data = backend->data;
assert(data != NULL);
char *file_path = Music_Backend_Files_GetTrackFileName(data->dir, track_id);
if (file_path == NULL) {
LOG_ERROR("Invalid track: %d", track_id);
return -1;
}
return Audio_Stream_CreateFromFile(file_path);
}
MUSIC_BACKEND *Music_Backend_Files_Factory(const char *path)
{
assert(path != NULL);
const char *description_fmt = "Directory (directory: %s)";
const size_t description_size = snprintf(NULL, 0, description_fmt, path);
char *description = Memory_Alloc(description_size + 1);
sprintf(description, description_fmt, path);
BACKEND_DATA *data = Memory_Alloc(sizeof(BACKEND_DATA));
data->dir = Memory_DupStr(path);
data->description = description;
MUSIC_BACKEND *backend = Memory_Alloc(sizeof(MUSIC_BACKEND));
backend->data = data;
backend->init = Music_Backend_Files_Init;
backend->describe = Music_Backend_Files_Describe;
backend->play = Music_Backend_Files_Play;
return backend;
}
void Music_Backend_Files_Destroy(MUSIC_BACKEND *backend)
{
if (backend == NULL) {
return;
}
if (backend->data != NULL) {
BACKEND_DATA *const data = backend->data;
Memory_FreePointer(&data->dir);
Memory_FreePointer(&data->description);
}
Memory_FreePointer(&backend->data);
Memory_FreePointer(&backend);
}

View File

@ -0,0 +1,6 @@
#pragma once
#include "game/music/music_backend.h"
MUSIC_BACKEND *Music_Backend_Files_Factory(const char *path);
void Music_Backend_Files_Destroy(MUSIC_BACKEND *backend);

182
src/game/music/music_main.c Normal file
View File

@ -0,0 +1,182 @@
#include "game/music.h"
#include "game/music/music_backend.h"
#include "game/music/music_backend_cdaudio.h"
#include "game/music/music_backend_files.h"
#include "global/const.h"
#include "global/funcs.h"
#include "global/types.h"
#include "global/vars.h"
#include <libtrx/engine/audio.h>
#include <libtrx/filesystem.h>
#include <libtrx/log.h>
#include <assert.h>
static MUSIC_TRACK_ID m_TrackCurrent = MX_INACTIVE;
static MUSIC_TRACK_ID m_TrackLooped = MX_INACTIVE;
static bool m_Initialized = false;
static float m_MusicVolume = 0.0f;
static int m_AudioStreamID = -1;
static const MUSIC_BACKEND *m_Backend = NULL;
static const MUSIC_BACKEND *Music_FindBackend(void);
static void Music_StreamFinished(int stream_id, void *user_data);
static const MUSIC_BACKEND *Music_FindBackend(void)
{
MUSIC_BACKEND *all_backends[] = {
Music_Backend_Files_Factory("music"),
Music_Backend_CDAudio_Factory("audio/cdaudio.wav"),
Music_Backend_CDAudio_Factory("audio/cdaudio.mp3"),
NULL,
};
MUSIC_BACKEND **backend_ptr = all_backends;
while (true) {
MUSIC_BACKEND *backend = *backend_ptr;
if (backend == NULL) {
break;
}
if (backend->init(backend)) {
return backend;
}
backend_ptr++;
}
return NULL;
}
static void Music_StreamFinished(const int stream_id, void *const user_data)
{
// When a stream finishes, play the remembered background BGM.
if (stream_id == m_AudioStreamID) {
m_AudioStreamID = -1;
if (m_TrackLooped >= 0) {
Music_Play(m_TrackLooped, true);
}
}
}
bool __cdecl Music_Init(void)
{
bool result = false;
// TODO: remove this guard once Music_Init can be called in a proper place
if (m_Initialized) {
return true;
}
if (!Audio_Init()) {
LOG_ERROR("Failed to initialize libtrx sound system");
goto finish;
}
m_Backend = Music_FindBackend();
if (m_Backend == NULL) {
LOG_ERROR("No music backend is available");
goto finish;
}
LOG_ERROR("Chosen music backend: %s", m_Backend->describe(m_Backend));
result = true;
Music_SetVolume(25 * g_OptionMusicVolume + 5);
finish:
m_TrackCurrent = MX_INACTIVE;
m_TrackLooped = MX_INACTIVE;
m_Initialized = true;
return result;
}
void __cdecl Music_Shutdown(void)
{
if (m_AudioStreamID < 0) {
return;
}
// We are only interested in calling Music_StreamFinished if a stream
// finished by itself. In cases where we end the streams early by hand,
// we clear the finish callback in order to avoid resuming the BGM playback
// just after we stop it.
Audio_Stream_SetFinishCallback(m_AudioStreamID, NULL, NULL);
Audio_Stream_Close(m_AudioStreamID);
}
void __cdecl Music_Play(int16_t track_id, bool is_looped)
{
if (track_id == m_TrackCurrent) {
return;
}
// TODO: this should be called in shell instead, once per game launch
Music_Init();
Audio_Stream_Close(m_AudioStreamID);
if (g_OptionMusicVolume == 0) {
LOG_DEBUG("Not playing track %d because the game is silent", track_id);
goto finish;
}
if (m_Backend == NULL) {
LOG_DEBUG(
"Not playing track %d because no backend is available", track_id);
goto finish;
}
const int32_t real_track_id = Music_GetRealTrack(track_id);
LOG_DEBUG(
"Playing track %d (real: %d), looped: %d", track_id, real_track_id,
is_looped);
m_AudioStreamID = m_Backend->play(m_Backend, real_track_id);
if (m_AudioStreamID < 0) {
LOG_ERROR("Failed to create music stream for track %d", track_id);
goto finish;
}
Audio_Stream_SetIsLooped(m_AudioStreamID, is_looped);
Audio_Stream_SetVolume(m_AudioStreamID, m_MusicVolume);
Audio_Stream_SetFinishCallback(m_AudioStreamID, Music_StreamFinished, NULL);
finish:
g_CD_TrackID = track_id;
m_TrackCurrent = track_id;
if (is_looped) {
m_TrackLooped = track_id;
}
}
void __cdecl Music_Stop(void)
{
if (m_AudioStreamID < 0) {
return;
}
m_TrackCurrent = MX_INACTIVE;
m_TrackLooped = MX_INACTIVE;
Audio_Stream_Close(m_AudioStreamID);
}
bool __cdecl Music_PlaySynced(int16_t track_id)
{
Music_Play(track_id, false);
return true;
}
uint32_t __cdecl Music_GetFrames(void)
{
if (m_AudioStreamID < 0) {
return 0;
}
return Audio_Stream_GetTimestamp(m_AudioStreamID) * FRAMES_PER_SECOND
* TICKS_PER_FRAME / 1000.0;
}
void __cdecl Music_SetVolume(int32_t volume)
{
m_MusicVolume = volume ? volume / 255.0f : 0.0f;
if (m_AudioStreamID >= 0) {
Audio_Stream_SetVolume(m_AudioStreamID, m_MusicVolume);
}
}

@ -1 +1 @@
Subproject commit 67f6f1110768381596724250bd420a1e4497dfa0
Subproject commit 0f6d84dcd5e437a9b24989b9cf84fa163df33a6a

View File

@ -10,7 +10,9 @@ run_script(
TR2X_REPO_DIR / "build/windows",
],
system_include_dirs=[TR2X_REPO_DIR / "subprojects/libtrx/include"],
own_include_map={},
own_include_map={
"game/music/music_main.c": "game/music.h",
},
fix_map={},
forced_order=["<ddrawi.h>", "<d3dhal.h>"],
)