// musicPlayer.cpp // // miniaudio-backed playback of mp3 / wav / flac files. See musicPlayer.h. // // MINIAUDIO_IMPLEMENTATION is defined in this one translation unit so the // large miniaudio implementation is compiled exactly once per binary. #define MINIAUDIO_IMPLEMENTATION #include "miniaudio.h" #include "musicPlayer.h" #include #include #include // When built with ALSA support, silence ALSA's own error printing: with no // sound card present it dumps a lot of "cannot find card '0'" chatter to // stderr, which would corrupt the TUI during playback. #ifdef HMS_HAS_ALSA #include #endif namespace mp { namespace { #ifdef HMS_HAS_ALSA void alsaErrorHandler(const char*, int, const char*, int, const char*, ...) { // swallow ALSA's verbose configuration/device errors } #endif struct PlaybackUser { ma_decoder* decoder; const std::atomic* stop; std::atomic ended{false}; }; void dataCallback(ma_device* device, void* out, const void* /*in*/, ma_uint32 frameCount) { auto* user = static_cast(device->pUserData); ma_decoder* decoder = user->decoder; const size_t bytesPerFrame = ma_get_bytes_per_frame(decoder->outputFormat, decoder->outputChannels); if (user->stop && user->stop->load()) { // Stop requested: feed silence and let the main loop tear the device // down. memset(out, 0, static_cast(frameCount) * bytesPerFrame); user->ended.store(true); return; } ma_uint64 framesRead = 0; const ma_result result = ma_decoder_read_pcm_frames(decoder, out, frameCount, &framesRead); if (framesRead > 0 && framesRead < frameCount) { // Underrun / near EOF: zero-pad the rest of the buffer. memset(static_cast(out) + framesRead * bytesPerFrame, 0, static_cast(frameCount - framesRead) * bytesPerFrame); } else if (framesRead == 0) { memset(out, 0, static_cast(frameCount) * bytesPerFrame); } // A short read means the file is exhausted (MA_AT_END); any other // non-success result is a decode error. Both end the playback loop. if (framesRead < frameCount || (result != MA_SUCCESS && result != MA_AT_END)) { user->ended.store(true); } } } // namespace int play(const std::string& path, Timeline* timeline, std::atomic* stop) { if (path.empty()) return 1; // Audio is the show's clock: anchor the timeline the moment playback is // about to begin (a no-op if the caller already anchored it). if (timeline && !timeline->started()) timeline->start(); ma_decoder_config decoderConfig = ma_decoder_config_init_default(); ma_decoder decoder; if (ma_decoder_init_file(path.c_str(), &decoderConfig, &decoder) != MA_SUCCESS) { return 2; // cannot open / no decoder for the format } #ifdef HMS_HAS_ALSA snd_lib_error_set_handler(alsaErrorHandler); #endif ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback); deviceConfig.playback.format = decoder.outputFormat; deviceConfig.playback.channels = decoder.outputChannels; deviceConfig.sampleRate = decoder.outputSampleRate; deviceConfig.dataCallback = dataCallback; PlaybackUser user{&decoder, stop}; deviceConfig.pUserData = &user; ma_device device; if (ma_device_init(nullptr, &deviceConfig, &device) != MA_SUCCESS) { ma_decoder_uninit(&decoder); return 3; // no audio backend / device available } ma_device_start(&device); while (!user.ended.load() && !(stop && stop->load())) { ma_sleep(50); } ma_device_uninit(&device); ma_decoder_uninit(&decoder); return 0; } } // namespace mp