diff --git a/Makefile b/Makefile index 87b83d6..ab5ea6b 100644 --- a/Makefile +++ b/Makefile @@ -12,8 +12,8 @@ PKG_CONFIG ?= pkg-config SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 SDL2_image) SDL_LIBS := $(shell $(PKG_CONFIG) --libs sdl2 SDL2_image) -FFMPEG_CFLAGS := $(shell $(PKG_CONFIG) --cflags libavformat libavcodec libswscale libavutil) -FFMPEG_LIBS := $(shell $(PKG_CONFIG) --libs libavformat libavcodec libswscale libavutil) +FFMPEG_CFLAGS := $(shell $(PKG_CONFIG) --cflags libavformat libavcodec libswscale libswresample libavutil) +FFMPEG_LIBS := $(shell $(PKG_CONFIG) --libs libavformat libavcodec libswscale libswresample libavutil) ALL_CFLAGS := $(CFLAGS) $(SDL_CFLAGS) $(FFMPEG_CFLAGS) ALL_LIBS := $(LIBS) $(SDL_LIBS) $(FFMPEG_LIBS) -lm @@ -21,7 +21,7 @@ ALL_LIBS := $(LIBS) $(SDL_LIBS) $(FFMPEG_LIBS) -lm BUILD_DIR ?= build TARGET = $(BUILD_DIR)/gsplash DUMMY_TARGET = $(BUILD_DIR)/dummy_game -SRC = src/gsplash.c src/video.c +SRC = src/gsplash.c src/video.c src/audio.c DUMMY_SRC = src/dummy_game.c .PHONY: all clean install uninstall check diff --git a/src/audio.c b/src/audio.c new file mode 100644 index 0000000..ca8abad --- /dev/null +++ b/src/audio.c @@ -0,0 +1,208 @@ +#include "audio.h" + +#include +#include + +void audio_callback(void* userdata, uint8_t* stream, int len) { + AudioPlayer* player = (AudioPlayer*)userdata; + if (!player) { + memset(stream, 0, len); + return; + } + + int bytes_written = 0; + while (bytes_written < len) { + int remaining = len - bytes_written; + + if (player->audio_buffer_pos >= player->audio_buffer_filled) { + int bytes_decoded = decode_audio_frame(player, player->audio_buffer, + player->audio_buffer_size); + if (bytes_decoded <= 0) { + memset(stream + bytes_written, 0, remaining); + return; + } + player->audio_buffer_pos = 0; + player->audio_buffer_filled = bytes_decoded; + } + + int available = player->audio_buffer_filled - player->audio_buffer_pos; + int to_copy = (remaining < available) ? remaining : available; + + memcpy(stream + bytes_written, + player->audio_buffer + player->audio_buffer_pos, to_copy); + bytes_written += to_copy; + player->audio_buffer_pos += to_copy; + } +} + +bool init_audio_player(AudioPlayer* player, const char* path) { + memset(player, 0, sizeof(*player)); + + if (avformat_open_input(&player->format_ctx, path, NULL, NULL) != 0) { + goto error; + } + + if (avformat_find_stream_info(player->format_ctx, NULL) < 0) { + goto error; + } + + player->stream_index = -1; + for (unsigned int i = 0; i < player->format_ctx->nb_streams; ++i) { + if (player->format_ctx->streams[i]->codecpar->codec_type == + AVMEDIA_TYPE_AUDIO) { + player->stream_index = (int)i; + break; + } + } + + if (player->stream_index < 0) { + goto error; + } + + AVCodecParameters* codecpar = + player->format_ctx->streams[player->stream_index]->codecpar; + const AVCodec* decoder = avcodec_find_decoder(codecpar->codec_id); + if (!decoder) { + goto error; + } + + player->codec_ctx = avcodec_alloc_context3(decoder); + if (!player->codec_ctx) { + goto error; + } + + if (avcodec_parameters_to_context(player->codec_ctx, codecpar) < 0) { + goto error; + } + + if (avcodec_open2(player->codec_ctx, decoder, NULL) < 0) { + goto error; + } + + player->sample_rate = player->codec_ctx->sample_rate; + player->channels = player->codec_ctx->ch_layout.nb_channels; + + player->frame = av_frame_alloc(); + if (!player->frame) { + goto error; + } + + player->swr_ctx = swr_alloc(); + if (!player->swr_ctx) { + goto error; + } + + av_opt_set_chlayout(player->swr_ctx, "in_chlayout", + &player->codec_ctx->ch_layout, 0); + av_opt_set_chlayout(player->swr_ctx, "out_chlayout", + &(AVChannelLayout)AV_CHANNEL_LAYOUT_STEREO, 0); + av_opt_set_int(player->swr_ctx, "in_sample_rate", player->sample_rate, 0); + av_opt_set_int(player->swr_ctx, "out_sample_rate", player->sample_rate, 0); + av_opt_set_sample_fmt(player->swr_ctx, "in_sample_fmt", + player->codec_ctx->sample_fmt, 0); + av_opt_set_sample_fmt(player->swr_ctx, "out_sample_fmt", AV_SAMPLE_FMT_S16, + 0); + + if (swr_init(player->swr_ctx) < 0) { + goto error; + } + + player->audio_buffer_size = player->sample_rate * player->channels * 2 * + 2; + player->audio_buffer = (uint8_t*)av_malloc(player->audio_buffer_size); + if (!player->audio_buffer) { + goto error; + } + + SDL_zero(player->desired_spec); + player->desired_spec.freq = player->sample_rate; + player->desired_spec.format = AUDIO_S16; + player->desired_spec.channels = 2; + player->desired_spec.samples = 2048; + player->desired_spec.callback = audio_callback; + player->desired_spec.userdata = player; + + player->audio_device = SDL_OpenAudioDevice(NULL, 0, &player->desired_spec, + &player->obtained_spec, 0); + if (player->audio_device == 0) { + goto error; + } + + SDL_PauseAudioDevice(player->audio_device, 0); + return true; + +error: + cleanup_audio_player(player); + return false; +} + +void cleanup_audio_player(AudioPlayer* player) { + if (player->audio_device != 0) { + SDL_CloseAudioDevice(player->audio_device); + } + if (player->audio_buffer) { + av_free(player->audio_buffer); + } + if (player->swr_ctx) { + swr_free(&player->swr_ctx); + } + if (player->frame) { + av_frame_free(&player->frame); + } + if (player->codec_ctx) { + avcodec_free_context(&player->codec_ctx); + } + if (player->format_ctx) { + avformat_close_input(&player->format_ctx); + } + memset(player, 0, sizeof(*player)); +} + +int decode_audio_frame(AudioPlayer* player, uint8_t* out_buffer, + int out_size) { + AVPacket* packet = av_packet_alloc(); + if (!packet) { + return -1; + } + + while (av_read_frame(player->format_ctx, packet) >= 0) { + if (packet->stream_index != player->stream_index) { + av_packet_unref(packet); + continue; + } + + if (avcodec_send_packet(player->codec_ctx, packet) < 0) { + av_packet_unref(packet); + continue; + } + av_packet_unref(packet); + + while (true) { + int ret = avcodec_receive_frame(player->codec_ctx, player->frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + break; + } + if (ret < 0) { + av_packet_free(&packet); + return -1; + } + + uint8_t* out[] = {out_buffer}; + int samples = swr_convert(player->swr_ctx, out, out_size / 4, + (const uint8_t**)player->frame->data, + player->frame->nb_samples); + + if (samples > 0) { + int bytes_written = samples * 4; + av_packet_free(&packet); + return bytes_written; + } + } + } + + av_seek_frame(player->format_ctx, player->stream_index, 0, + AVSEEK_FLAG_BACKWARD); + avcodec_flush_buffers(player->codec_ctx); + av_packet_free(&packet); + return -1; +} diff --git a/src/audio.h b/src/audio.h new file mode 100644 index 0000000..9358fd9 --- /dev/null +++ b/src/audio.h @@ -0,0 +1,33 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#include +#include +#include +#include +#include +#include + +typedef struct AudioPlayer { + AVFormatContext* format_ctx; + AVCodecContext* codec_ctx; + SwrContext* swr_ctx; + AVFrame* frame; + int stream_index; + int sample_rate; + int channels; + SDL_AudioDeviceID audio_device; + SDL_AudioSpec desired_spec; + SDL_AudioSpec obtained_spec; + uint8_t* audio_buffer; + int audio_buffer_size; + int audio_buffer_pos; + int audio_buffer_filled; +} AudioPlayer; + +bool init_audio_player(AudioPlayer* player, const char* path); +void cleanup_audio_player(AudioPlayer* player); +int decode_audio_frame(AudioPlayer* player, uint8_t* out_buffer, int out_size); +void audio_callback(void* userdata, uint8_t* stream, int len); + +#endif // AUDIO_H diff --git a/src/gsplash.c b/src/gsplash.c index 45722f0..a635752 100644 --- a/src/gsplash.c +++ b/src/gsplash.c @@ -11,6 +11,7 @@ #include #include +#include "audio.h" #include "video.h" static void log_info(const char* fmt, ...) { @@ -146,8 +147,8 @@ int main(int argc, char* argv[]) { log_info("Starting splash: image='%s', game='%s', mode=%d", image_path, game_path, render_mode); - // Initialize SDL2 Video subsystems - if (SDL_Init(SDL_INIT_VIDEO) < 0) { + // Initialize SDL2 Video and Audio subsystems + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { log_error("SDL Init Failed: %s", SDL_GetError()); return 1; } @@ -176,9 +177,8 @@ int main(int argc, char* argv[]) { } log_info("Fullscreen borderless window created"); - SDL_ShowCursor(SDL_DISABLE); // Hide the mouse pointer + SDL_ShowCursor(SDL_DISABLE); - // Create a hardware-accelerated renderer SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED); if (renderer) { @@ -186,11 +186,22 @@ int main(int argc, char* argv[]) { } SDL_Texture* texture = NULL; VideoPlayer video_player; + AudioPlayer audio_player; bool video_active = false; + bool audio_active = false; if (has_video_extension(image_path)) { if (init_video_player(&video_player, renderer, image_path)) { video_active = true; + if (video_player.audio_stream_index >= 0) { + if (init_audio_player(&audio_player, image_path)) { + audio_active = true; + log_info("Audio stream loaded and initialized"); + } else { + log_info( + "Video has audio stream but failed to initialize audio player"); + } + } } else { log_error("Failed to open video '%s'; showing black screen", image_path); } @@ -215,13 +226,18 @@ int main(int argc, char* argv[]) { } if (!texture && init_video_player(&video_player, renderer, image_path)) { video_active = true; + if (video_player.audio_stream_index >= 0) { + if (init_audio_player(&audio_player, image_path)) { + audio_active = true; + log_info("Audio stream loaded and initialized"); + } + } } } if (!texture && !video_active) { log_error("Failed to load splash image '%s': %s; showing black screen", image_path, IMG_GetError()); - // Fallback: Clear to solid black if image file is broken SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); SDL_RenderPresent(renderer); @@ -263,7 +279,6 @@ int main(int argc, char* argv[]) { log_info("Launching game executable"); pid_t pid = fork(); if (pid == 0) { - // Inside Child Process: Hand over execution directly to the game binary log_info("Execing game: %s", game_path); execvp(game_path, &argv[arg_index + 1]); log_error("Failed to launch target game executable '%s': %s", game_path, @@ -273,14 +288,12 @@ int main(int argc, char* argv[]) { log_error("Failed to fork process: %s", strerror(errno)); } else { log_info("Game process started (pid=%d)", pid); - // Inside Parent Process: Manage splash screen lifecycle int running = 1; SDL_Event event; bool hide_scheduled = false; Uint32 hide_time = 0; while (running) { - // Non-blocking check: Has the game quit? int status; pid_t result = waitpid(pid, &status, WNOHANG); if (result > 0) { @@ -291,15 +304,14 @@ int main(int argc, char* argv[]) { } else { log_info("Game exited"); } - break; // Game closed, break the loop and close the script + break; } else if (result < 0) { log_error("waitpid failed: %s", strerror(errno)); - break; // Process error safetynet + break; } if (video_active && SDL_GetTicks() >= video_player.next_frame_tick) { if (!decode_next_frame(&video_player)) { - // If decoding fails or loops, try again on next tick } video_player.next_frame_tick += (Uint32)video_player.frame_delay_ms; Uint32 now = SDL_GetTicks(); @@ -318,22 +330,18 @@ int main(int argc, char* argv[]) { SDL_RenderPresent(renderer); } - // Check desktop server window events while (SDL_PollEvent(&event)) { if (event.type == SDL_QUIT) { log_info("Splash dismissed via window close"); running = 0; } if (event.type == SDL_WINDOWEVENT) { - // THE MOMENT THE GAME WINDOW STEALS FOCUS: if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST && !hide_scheduled) { log_info("Splash window hidden (focus lost), scheduling hide"); hide_scheduled = true; - hide_time = - SDL_GetTicks() + 100; // 100ms delay to prevent desktop flash + hide_time = SDL_GetTicks() + 100; } - // Re-render static image when compositor requests a redraw if (event.window.event == SDL_WINDOWEVENT_EXPOSED && texture && !video_active) { int out_w = 0, out_h = 0; @@ -354,7 +362,7 @@ int main(int argc, char* argv[]) { if (event.type == SDL_KEYDOWN) { if (event.key.keysym.sym == SDLK_ESCAPE) { log_info("Splash dismissed via escape key"); - running = 0; // Emergency escape key loop override + running = 0; } } } @@ -372,7 +380,7 @@ int main(int argc, char* argv[]) { SDL_Delay(delay > 33 ? 33 : delay); } } else { - SDL_Delay(33); // ~30 FPS polling loop to ensure near-zero CPU usage + SDL_Delay(33); } } } @@ -380,6 +388,7 @@ int main(int argc, char* argv[]) { // Clean up memory space if (texture) SDL_DestroyTexture(texture); if (video_active) cleanup_video_player(&video_player); + if (audio_active) cleanup_audio_player(&audio_player); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); IMG_Quit(); diff --git a/src/video.c b/src/video.c index 86689ca..b1d42a3 100644 --- a/src/video.c +++ b/src/video.c @@ -44,11 +44,15 @@ bool init_video_player(VideoPlayer* player, SDL_Renderer* renderer, } player->stream_index = -1; + player->audio_stream_index = -1; + for (unsigned int i = 0; i < player->format_ctx->nb_streams; ++i) { if (player->format_ctx->streams[i]->codecpar->codec_type == - AVMEDIA_TYPE_VIDEO) { + AVMEDIA_TYPE_VIDEO && player->stream_index < 0) { player->stream_index = (int)i; - break; + } else if (player->format_ctx->streams[i]->codecpar->codec_type == + AVMEDIA_TYPE_AUDIO && player->audio_stream_index < 0) { + player->audio_stream_index = (int)i; } } diff --git a/src/video.h b/src/video.h index b73f337..3f55a15 100644 --- a/src/video.h +++ b/src/video.h @@ -17,6 +17,7 @@ typedef struct VideoPlayer { uint8_t* rgba_buffer; int rgba_buffer_size; int stream_index; + int audio_stream_index; int width; int height; int frame_delay_ms; diff --git a/tests/assets/sample.mp4 b/tests/assets/sample.mp4 new file mode 100644 index 0000000..5a2c96c Binary files /dev/null and b/tests/assets/sample.mp4 differ