/* * tbzmusic.c: the game's music over libmodplug, through Mix_HookMusic (see tbzmusic.h). * Copyright (C) 2026 FredyGames. GNU GPL version 3 or later, like TecnoballZ. */ #define TBZ_MUSIC_IMPLEMENTATION #include "tbzmusic.h" #include #include #include typedef struct { ModPlugFile *file; MODULE module; } tbz_music; static tbz_music *playing = NULL; static int paused = 0; static int volume = MIX_MAX_VOLUME; /* number of orders of a ProTracker module (31 samples: song length at offset 950) */ static int module_orders (const Uint8 * data, size_t size) { if (size > 950 && data[950] > 0 && data[950] <= 128) return data[950]; return 1; } Mix_Music * tbz_LoadMUS (const char *path) { SDL_RWops *rw = SDL_RWFromFile (path, "rb"); Sint64 size; Uint8 *data; tbz_music *music; ModPlug_Settings settings; int frequency = 44100, channels = 2; Uint16 format; if (rw == NULL) return NULL; size = SDL_RWsize (rw); data = (Uint8 *) malloc (size > 0 ? (size_t) size : 1); if (data == NULL || size <= 0 || SDL_RWread (rw, data, 1, (size_t) size) != (size_t) size) { SDL_RWclose (rw); free (data); SDL_SetError ("cannot read %s", path); return NULL; } SDL_RWclose (rw); /* the module plays at the mixer's rate, in 16-bit samples */ Mix_QuerySpec (&frequency, &format, &channels); ModPlug_GetSettings (&settings); settings.mFrequency = frequency; settings.mChannels = channels >= 2 ? 2 : 1; settings.mBits = 16; settings.mResamplingMode = MODPLUG_RESAMPLE_LINEAR; settings.mLoopCount = -1; ModPlug_SetSettings (&settings); music = (tbz_music *) calloc (1, sizeof (tbz_music)); music->file = ModPlug_Load (data, (int) size); music->module.numpos = module_orders (data, (size_t) size); free (data); if (music->file == NULL) { free (music); SDL_SetError ("cannot load the module %s", path); return NULL; } return (Mix_Music *) music; } static void SDLCALL mix_music (void *udata, Uint8 * stream, int len) { tbz_music *music = (tbz_music *) udata; int done = 0; memset (stream, 0, len); if (music == NULL || music->file == NULL || paused) return; while (done < len) { int n = ModPlug_Read (music->file, stream + done, len - done); if (n <= 0) { /* end of the song: start again, as MikMod does with loops = -1 */ ModPlug_SeekOrder (music->file, 0); break; } done += n; } if (volume < MIX_MAX_VOLUME) { Sint16 *s = (Sint16 *) stream; int i; for (i = 0; i < len / 2; i++) s[i] = (Sint16) ((s[i] * volume) / MIX_MAX_VOLUME); } music->module.sngpos = ModPlug_GetCurrentOrder (music->file); music->module.patpos = ModPlug_GetCurrentRow (music->file); } int tbz_PlayMusic (Mix_Music * m, int loops) { tbz_music *music = (tbz_music *) m; (void) loops; if (music == NULL) return -1; Mix_HookMusic (NULL, NULL); ModPlug_SeekOrder (music->file, 0); music->module.sngpos = 0; music->module.patpos = 0; playing = music; paused = 0; Mix_HookMusic (mix_music, music); return 0; } int tbz_HaltMusic (void) { Mix_HookMusic (NULL, NULL); playing = NULL; return 0; } void tbz_FreeMusic (Mix_Music * m) { tbz_music *music = (tbz_music *) m; if (music == NULL) return; if (music == playing) tbz_HaltMusic (); ModPlug_Unload (music->file); free (music); } int tbz_SetMusicPosition (double position) { if (playing == NULL) return -1; SDL_LockAudio (); ModPlug_SeekOrder (playing->file, (int) position); playing->module.sngpos = (int) position; playing->module.patpos = 0; SDL_UnlockAudio (); return 0; } int tbz_VolumeMusic (int v) { int previous = volume; if (v >= 0) volume = v > MIX_MAX_VOLUME ? MIX_MAX_VOLUME : v; return previous; } MODULE * tbz_music_module (Mix_Music * m) { return m == NULL ? NULL : &((tbz_music *) m)->module; } int Player_Paused (void) { return paused; } void Player_TogglePause (void) { paused = !paused; } void Player_Stop (void) { /* the game calls it right after starting a level's music; with SDL_mixer's MikMod backend the * music keeps playing, so there is nothing to do here */ }