diff --git a/Makefile b/Makefile index 47295b1..5a2e6f6 100644 --- a/Makefile +++ b/Makefile @@ -83,8 +83,8 @@ endif CFLAGS += -Werror -Wall -Wno-unused-result -Wno-strict-aliasing -Wno-unknown-warning -Wno-unknown-warning-option -Wno-multichar -Wno-int-in-bool-context -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES SDL_LDFLAGS := -lSDL2 -lGL -GTK3_CFLAGS := `pkg-config --cflags gio-2.0 gtk+-3.0 epoxy` -DGTK_DISABLE_DEPRECATED=1 -DG_DISABLE_DEPRECATED=1 -DRESOURCE_PREFIX=\"/io/github/sameboy/\" -DAPP_ID=\"io.github.sameboy\" -GTK3_LDFLAGS := `pkg-config --libs gio-2.0 gtk+-3.0 epoxy` +GTK3_CFLAGS := `pkg-config --cflags gio-2.0 gtk+-3.0 epoxy openal` -DGTK_DISABLE_DEPRECATED=1 -DG_DISABLE_DEPRECATED=1 -DRESOURCE_PREFIX=\"/io/github/sameboy/\" -DAPP_ID=\"io.github.sameboy\" +GTK3_LDFLAGS := `pkg-config --libs gio-2.0 gtk+-3.0 epoxy openal` # TODO: REMOVE DISABLE UNUSED WARNINGS GTK3_CFLAGS += -Wno-unused diff --git a/gtk3/main.c b/gtk3/main.c index d5c203d..47e85bf 100644 --- a/gtk3/main.c +++ b/gtk3/main.c @@ -1,5 +1,11 @@ #include "main.h" +static ALCdevice *al_device; +static ALCcontext *al_context; +static ALuint al_source; +static ALuint al_buffer[96000*2]; +static ALuint next_buffer = 0; + static GtkApplication *main_application; static GtkBuilder *builder; static GtkGLArea *gl_area; @@ -199,6 +205,69 @@ void gl_check_realize(GtkWidget *w, gpointer user_data_gptr) { } } +static void setup_audio() { + #define RETURN_IF_AL_ERROR(msg) { \ + GError *error = openal_error(msg); \ + if (error) { \ + g_printerr("OpenAL: (%d) %s\n", error->code, error->message); \ + g_clear_error(&error); \ + return; \ + } \ + } + + al_device = alcOpenDevice(NULL); + + if (!al_device) RETURN_IF_AL_ERROR("Failed to open device") + + al_context = alcCreateContext(al_device, NULL); + + if (!alcMakeContextCurrent(al_context)) RETURN_IF_AL_ERROR("Failed to create context") + + alGenSources((ALuint)1, &al_source); + RETURN_IF_AL_ERROR(NULL) + + alSourcef(al_source, AL_PITCH, 1); + RETURN_IF_AL_ERROR(NULL) + + alSourcef(al_source, AL_GAIN, 1); + RETURN_IF_AL_ERROR(NULL) + + alSource3f(al_source, AL_POSITION, 0, 0, 0); + RETURN_IF_AL_ERROR(NULL) + + alSource3f(al_source, AL_VELOCITY, 0, 0, 0); + RETURN_IF_AL_ERROR(NULL) + + alSourcei(al_source, AL_LOOPING, AL_FALSE); + RETURN_IF_AL_ERROR(NULL) + + alSourcei(al_source, AL_SOURCE_TYPE, AL_STREAMING); + RETURN_IF_AL_ERROR(NULL) + + alGenBuffers((ALuint)96000*2, al_buffer); + RETURN_IF_AL_ERROR("Failed to create audio buffer") +} + +static void gb_audio_callback(GB_gameboy_t *gb, GB_sample_t *sample) { + alBufferData(al_buffer[next_buffer], AL_FORMAT_STEREO16, sample, 4, 96000); + + GError *error = openal_error("alBufferData"); + if (error) { + g_printerr("OpenAL: (%d) %s\n", error->code, error->message); + g_clear_error(&error); + } + + alSourceQueueBuffers(al_source, 1, &al_buffer[next_buffer]); + + error = openal_error("alSourceQueueBuffers"); + if (error) { + g_printerr("OpenAL: (%d) %s\n", error->code, error->message); + g_clear_error(&error); + } + + next_buffer = (next_buffer + 1) % (96000*2); +} + // Returns a `GApplication`s `GMenuModel` by ID // GApplication menus are loaded from `gtk/menus.ui`, `gtk/menus-traditional.ui` and `gtk/menus-common.ui`. static GMenuModel *get_menu_model(GApplication *app, const char *id) { @@ -499,6 +568,8 @@ static void activate(GApplication *app, gpointer user_data_gptr) { gtk_application_add_window(GTK_APPLICATION(app), GTK_WINDOW(main_window)); gtk_widget_show_all(GTK_WIDGET(main_window)); + setup_audio(); + g_thread_new("CoreLoop", run, user_data); } @@ -508,6 +579,13 @@ static void shutdown(GApplication *app, GFile **files, gint n_files, const gchar save_settings(); free_settings(); + + alDeleteSources(1, &al_source); + alDeleteBuffers(96000*2, al_buffer); + al_device = alcGetContextsDevice(al_context); + alcMakeContextCurrent(NULL); + alcDestroyContext(al_context); + alcCloseDevice(al_device); } // This function gets called when there are files to open. @@ -1347,6 +1425,37 @@ static void vblank(GB_gameboy_t *gb) { g_idle_add((GSourceFunc) on_vblank, NULL); } +static gpointer openal_run(gpointer user_data_gptr) { + GMutex audio_mutex; + GCond audio_cond; + g_cond_init(&audio_cond); + g_mutex_init(&audio_mutex); + g_mutex_lock(&audio_mutex); + + gboolean play_audio = true; + + while (play_audio) { + ALint processed; + alGetSourcei(al_source, AL_BUFFERS_PROCESSED, &processed); + + ALint state; + alGetSourcei(al_source, AL_SOURCE_STATE, &state); + if (state != AL_PLAYING) { + alSourcePlay(al_source); + } + + ALuint x = 0; + while (processed--) { + alSourceUnqueueBuffers(al_source, 1, &x); + } + + gint64 end_time = g_get_monotonic_time() + 1 * G_TIME_SPAN_MILLISECOND; + g_cond_wait_until(&audio_cond, &audio_mutex, end_time); + } + + return FALSE; +} + static gpointer run(gpointer user_data_gptr) { UserData *user_data = user_data_gptr; @@ -1372,12 +1481,12 @@ static gpointer run(gpointer user_data_gptr) { GB_set_vblank_callback(&gb, (GB_vblank_callback_t) vblank); GB_set_pixels_output(&gb, get_current_buffer()); GB_set_rgb_encode_callback(&gb, rgb_encode); - // GB_set_sample_rate(&gb, have_aspec.freq); + GB_set_sample_rate(&gb, 96000); GB_set_color_correction_mode(&gb, get_color_correction_mode()); GB_set_highpass_filter_mode(&gb, get_highpass_mode()); GB_set_rewind_length(&gb, config.rewind_duration); GB_set_update_input_hint_callback(&gb, handle_events); - // GB_apu_set_sample_callback(&gb, gb_audio_callback); + GB_apu_set_sample_callback(&gb, gb_audio_callback); } GError *error; @@ -1446,6 +1555,10 @@ static gpointer run(gpointer user_data_gptr) { } if (user_data->file != NULL && GB_load_rom(&gb, g_file_get_path(user_data->file)) == 0) { + alSourcePlay(al_source); + + g_thread_new("OpenAL", openal_run, NULL); + /* Run emulation */ while (running) { if (paused || rewind_paused) { diff --git a/gtk3/main.h b/gtk3/main.h index 09217d7..0edc126 100644 --- a/gtk3/main.h +++ b/gtk3/main.h @@ -1,6 +1,8 @@ #ifndef main_h #define main_h +#include +#include #include #include #include @@ -13,6 +15,47 @@ #include "settings.h" #include "shader.h" +#define OPENAL_ERROR openal_error_quark() + +GQuark openal_error_quark (void) { + return g_quark_from_static_string("openal-error-quark"); +} + +GError *openal_error(const gchar *user_msg) { + ALCenum error = alGetError(); + gchar *description; + gchar *msg; + + switch (error) { + case AL_NO_ERROR: + return NULL; + case AL_INVALID_NAME: + description = "A bad name (ID) was passed to an OpenAL function"; + break; + case AL_INVALID_ENUM: + description = "An invalid enum value was passed to an OpenAL function"; + break; + case AL_INVALID_VALUE: + description = "An invalid value was passed to an OpenAL function"; + break; + case AL_INVALID_OPERATION: + description = "The requested operation is not valid"; + break; + case AL_OUT_OF_MEMORY: + description = "The requested operation resulted in OpenAL running out of memory"; + break; + } + + if (user_msg != NULL) { + msg = g_strdup_printf("%s: %s", user_msg, description); + } + else { + msg = description; + } + + return g_error_new_literal(OPENAL_ERROR, error, msg); +} + typedef struct UserData { bool fullscreen; GFile *file; @@ -43,6 +86,8 @@ static gint handle_local_options(GApplication *app, GVariantDict *options, gpoin void gl_check_realize(GtkWidget *w, gpointer user_data_gptr); gboolean test_gl_support(void); +static void setup_audio(); + static GMenuModel *get_menu_model(GApplication *app, const char *id); static void create_fallback_canvas(void); static void setup_menu(GApplication *app);