From 6f91e947c35b361c45d2449035577bd37d868093 Mon Sep 17 00:00:00 2001 From: alvinwong Date: Mon, 17 Feb 2014 04:02:42 +0000 Subject: [PATCH] Linux (gtk): Add preliminary AV recording with x264 and flac. --- desmume/src/gtk/Makefile.am | 4 + desmume/src/gtk/avout.h | 32 +++++ desmume/src/gtk/avout_flac.cpp | 47 ++++++++ desmume/src/gtk/avout_flac.h | 34 ++++++ desmume/src/gtk/avout_pipe_base.cpp | 102 ++++++++++++++++ desmume/src/gtk/avout_pipe_base.h | 39 +++++++ desmume/src/gtk/avout_x264.cpp | 47 ++++++++ desmume/src/gtk/avout_x264.h | 34 ++++++ desmume/src/gtk/main.cpp | 173 +++++++++++++++++++++++++++- 9 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 desmume/src/gtk/avout.h create mode 100644 desmume/src/gtk/avout_flac.cpp create mode 100644 desmume/src/gtk/avout_flac.h create mode 100644 desmume/src/gtk/avout_pipe_base.cpp create mode 100644 desmume/src/gtk/avout_pipe_base.h create mode 100644 desmume/src/gtk/avout_x264.cpp create mode 100644 desmume/src/gtk/avout_x264.h diff --git a/desmume/src/gtk/Makefile.am b/desmume/src/gtk/Makefile.am index 4bfda77d7..09a8cead0 100644 --- a/desmume/src/gtk/Makefile.am +++ b/desmume/src/gtk/Makefile.am @@ -10,6 +10,10 @@ pixmap_DATA = DeSmuME.xpm EXTRA_DIST = DeSmuME.xpm desmume.desktop bin_PROGRAMS = desmume desmume_SOURCES = \ + avout.h \ + avout_pipe_base.cpp avout_pipe_base.h \ + avout_x264.cpp avout_x264.h \ + avout_flac.cpp avout_flac.h \ desmume.h desmume.cpp \ dTool.h dToolsList.cpp \ tools/ioregsView.cpp tools/ioregsView.h \ diff --git a/desmume/src/gtk/avout.h b/desmume/src/gtk/avout.h new file mode 100644 index 000000000..8966856ea --- /dev/null +++ b/desmume/src/gtk/avout.h @@ -0,0 +1,32 @@ +/* + Copyright (C) 2014 DeSmuME team + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the this software. If not, see . +*/ + +#ifndef _AVOUT_H_ +#define _AVOUT_H_ + +#include "types.h" + +class AVOut { +public: + virtual bool begin(const char* fname) { return false; } + virtual void end() {} + virtual bool isRecording() { return false; } + virtual void updateAudio(void* soundData, int soundLen) {} + virtual void updateVideo(const u16* buffer) {} +}; + +#endif diff --git a/desmume/src/gtk/avout_flac.cpp b/desmume/src/gtk/avout_flac.cpp new file mode 100644 index 000000000..eb32fdbbc --- /dev/null +++ b/desmume/src/gtk/avout_flac.cpp @@ -0,0 +1,47 @@ +/* + Copyright (C) 2014 DeSmuME team + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the this software. If not, see . +*/ + +#include +#include + +#include "avout_flac.h" + +AVOutFlac::AVOutFlac() { + const char* const args[] = { + "flac", + "-f", + "-o", this->filename, + "--endian=little", + "--channels=2", + "--bps=16", + "--sample-rate=44100", + "--sign=signed", + "--force-raw-format", + "-", + NULL + }; + memcpy(this->args, args, sizeof(args)); +} + +const char* const* AVOutFlac::getArgv(const char* fname) { + if (strlen(fname) >= sizeof(this->filename)) { + return NULL; + } + strncpy(this->filename, fname, sizeof(this->filename)); + return this->args; +} + diff --git a/desmume/src/gtk/avout_flac.h b/desmume/src/gtk/avout_flac.h new file mode 100644 index 000000000..818d81665 --- /dev/null +++ b/desmume/src/gtk/avout_flac.h @@ -0,0 +1,34 @@ +/* + Copyright (C) 2014 DeSmuME team + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the this software. If not, see . +*/ + +#ifndef _AVOUT_FLAC_H_ +#define _AVOUT_FLAC_H_ + +#include "avout_pipe_base.h" + +class AVOutFlac : public AVOutPipeBase { +public: + AVOutFlac(); +protected: + Type type() { return TYPE_AUDIO; } + const char* const* getArgv(const char* fname); +private: + char filename[1024]; + const char* args[12]; +}; + +#endif diff --git a/desmume/src/gtk/avout_pipe_base.cpp b/desmume/src/gtk/avout_pipe_base.cpp new file mode 100644 index 000000000..fa4f5d431 --- /dev/null +++ b/desmume/src/gtk/avout_pipe_base.cpp @@ -0,0 +1,102 @@ +/* + Copyright (C) 2014 DeSmuME team + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the this software. If not, see . +*/ + +#include +#include + +#include "types.h" +#include "SPU.h" + +#include "avout_pipe_base.h" + +static inline int writeAll(int fd, const void* buf, size_t count) { + ssize_t written = 0, writtenTotal = 0; + do { + written = write(fd, ((u8*)buf) + writtenTotal, count - writtenTotal); + } while (written >= 0 && (writtenTotal += written) < count); + return written; +} + +bool AVOutPipeBase::begin(const char* fname) { + if (this->recording) { + return false; + } + const char* const* args = this->getArgv(fname); + if (args == NULL) { + return false; + } + int pipefd[2]; + if (pipe(pipefd) < 0) { + fprintf(stderr, "Fail to open pipe\n"); + return false; + } + pid_t pid = fork(); + if (pid == 0) { + close(pipefd[1]); + dup2(pipefd[0], STDIN_FILENO); + execvp(args[0], (char* const*)args); + fprintf(stderr, "Fail to start %s: %d %s\n", args[0], errno, strerror(errno)); + _exit(1); + } + close(pipefd[0]); + this->pipe_fd = pipefd[1]; + this->recording = true; + return true; +} + +void AVOutPipeBase::end() { + if (this->recording) { + close(this->pipe_fd); + this->recording = false; + } +} + +bool AVOutPipeBase::isRecording() { + return this->recording; +} + +void AVOutPipeBase::updateAudio(void* soundData, int soundLen) { + if(!this->recording || this->type() != TYPE_AUDIO) { + return; + } + if (writeAll(this->pipe_fd, soundData, soundLen * 2 * 2) == -1) { + fprintf(stderr, "Error on writing audio: %d %s\n", errno, strerror(errno)); + this->end(); + } +} + +void AVOutPipeBase::updateVideo(const u16* buffer) { + if(!this->recording || this->type() != TYPE_VIDEO) { + return; + } + u8 rgb[256 * 384 * 3]; + u8* cur = rgb; + for (int i = 0; i < 256 * 384; i++) { + u16 gpu_pixel = buffer[i]; + *cur = ((gpu_pixel >> 0) & 0x1f) << 3; + cur++; + *cur = ((gpu_pixel >> 5) & 0x1f) << 3; + cur++; + *cur = ((gpu_pixel >> 10) & 0x1f) << 3; + cur++; + } + if (write(this->pipe_fd, rgb, 256 * 384 * 3) == -1) { + fprintf(stderr, "Error on writing video: %d %s\n", errno, strerror(errno)); + this->end(); + } +} + diff --git a/desmume/src/gtk/avout_pipe_base.h b/desmume/src/gtk/avout_pipe_base.h new file mode 100644 index 000000000..3f1c0edd8 --- /dev/null +++ b/desmume/src/gtk/avout_pipe_base.h @@ -0,0 +1,39 @@ +/* + Copyright (C) 2014 DeSmuME team + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the this software. If not, see . +*/ + +#ifndef _AVOUT_PIPE_BASE_H_ +#define _AVOUT_PIPE_BASE_H_ + +#include "avout.h" + +class AVOutPipeBase : public AVOut { +public: + bool begin(const char* fname); + void end(); + bool isRecording(); + void updateAudio(void* soundData, int soundLen); + void updateVideo(const u16* buffer); +protected: + enum Type { TYPE_AUDIO, TYPE_VIDEO }; + virtual Type type() = 0; + virtual const char* const* getArgv(const char* fname) = 0; +private: + bool recording; + int pipe_fd; +}; + +#endif diff --git a/desmume/src/gtk/avout_x264.cpp b/desmume/src/gtk/avout_x264.cpp new file mode 100644 index 000000000..1996c0f8a --- /dev/null +++ b/desmume/src/gtk/avout_x264.cpp @@ -0,0 +1,47 @@ +/* + Copyright (C) 2014 DeSmuME team + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the this software. If not, see . +*/ + +#include +#include + +#include "avout_x264.h" + +AVOutX264::AVOutX264() { + const char* const args[] = { + "x264", + "--qp", "0", + "--demuxer", "raw", + "--input-csp", "rgb", + "--input-depth", "8", + "--input-res", "256x384", + "--fps", "60", + "--output-csp", "i444", + "-o", this->filename, + "-", + NULL + }; + memcpy(this->args, args, sizeof(args)); +} + +const char* const* AVOutX264::getArgv(const char* fname) { + if (strlen(fname) >= sizeof(this->filename)) { + return NULL; + } + strncpy(this->filename, fname, sizeof(this->filename)); + return this->args; +} + diff --git a/desmume/src/gtk/avout_x264.h b/desmume/src/gtk/avout_x264.h new file mode 100644 index 000000000..24125a0f9 --- /dev/null +++ b/desmume/src/gtk/avout_x264.h @@ -0,0 +1,34 @@ +/* + Copyright (C) 2014 DeSmuME team + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This file is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with the this software. If not, see . +*/ + +#ifndef _AVOUT_X264_H_ +#define _AVOUT_X264_H_ + +#include "avout_pipe_base.h" + +class AVOutX264 : public AVOutPipeBase { +public: + AVOutX264(); +protected: + Type type() { return TYPE_VIDEO; } + const char* const* getArgv(const char* fname); +private: + char filename[1024]; + const char* args[19]; +}; + +#endif diff --git a/desmume/src/gtk/main.cpp b/desmume/src/gtk/main.cpp index 1b921febf..b5634d443 100644 --- a/desmume/src/gtk/main.cpp +++ b/desmume/src/gtk/main.cpp @@ -51,6 +51,9 @@ #include "cheatsGTK.h" #include "GPU_osd.h" +#include "avout_x264.h" +#include "avout_flac.h" + #include "commandline.h" #include "slot2.h" @@ -106,6 +109,12 @@ enum { gboolean EmuLoop(gpointer data); +static AVOutX264 avout_x264; +static AVOutFlac avout_flac; +static void RecordAV_x264(); +static void RecordAV_flac(); +static void RecordAV_stop(); + static void DoQuit(); static void RecordMovieDialog(); static void PlayMovieDialog(); @@ -186,6 +195,12 @@ static const char *ui_description = " " " " " " +" " +" " +" " +" " +" " +" " " " " " " " @@ -341,6 +356,10 @@ static const GtkActionEntry action_entries[] = { { "recordmovie", NULL, "Record movie _to ...", NULL, NULL, RecordMovieDialog }, { "playmovie", NULL, "Play movie _from ...", NULL, NULL, PlayMovieDialog }, { "stopmovie", NULL, "Stop movie", NULL, NULL, StopMovie }, + { "RecordAVMenu", NULL, "Record _video/audio" }, + { "record_x264", "gtk-media-record", "Record lossless H._264 (video only)...", NULL, NULL, RecordAV_x264 }, + { "record_flac", "gtk-media-record", "Record _flac (audio only)...", NULL, NULL, RecordAV_flac }, + { "record_stop", "gtk-media-stop", "_Stop recording", NULL, NULL, RecordAV_stop }, { "SavestateMenu", NULL, "_Save state" }, { "LoadstateMenu", NULL, "_Load state" }, #ifdef DESMUME_GTK_FIRMWARE_BROKEN @@ -1063,6 +1082,138 @@ static void SaveStateDialog() gtk_widget_destroy(pFileSelection); } +static void RecordAV_x264() +{ + GtkFileFilter *pFilter_mkv, *pFilter_mp4, *pFilter_any; + GtkWidget *pFileSelection; + GtkWidget *pParent; + gchar *sPath; + + if (desmume_running()) + Pause(); + + pParent = GTK_WIDGET(pWindow); + + pFilter_mkv = gtk_file_filter_new(); + gtk_file_filter_add_pattern(pFilter_mkv, "*.mkv"); + gtk_file_filter_set_name(pFilter_mkv, "Matroska (.mkv)"); + + pFilter_mp4 = gtk_file_filter_new(); + gtk_file_filter_add_pattern(pFilter_mp4, "*.mp4"); + gtk_file_filter_set_name(pFilter_mp4, "MP4 (.mp4)"); + + pFilter_any = gtk_file_filter_new(); + gtk_file_filter_add_pattern(pFilter_any, "*"); + gtk_file_filter_set_name(pFilter_any, "All files"); + + /* Creating the selection window */ + pFileSelection = gtk_file_chooser_dialog_new("Save video...", + GTK_WINDOW(pParent), + GTK_FILE_CHOOSER_ACTION_SAVE, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_OK, + NULL); + gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (pFileSelection), TRUE); + + /* Only the dialog window is accepting events: */ + gtk_window_set_modal(GTK_WINDOW(pFileSelection), TRUE); + + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(pFileSelection), pFilter_mkv); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(pFileSelection), pFilter_mp4); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(pFileSelection), pFilter_any); + + /* Showing the window */ + switch(gtk_dialog_run(GTK_DIALOG(pFileSelection))) { + case GTK_RESPONSE_OK: + sPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(pFileSelection)); + + if(avout_x264.begin(sPath)) { + gtk_action_set_sensitive(gtk_action_group_get_action(action_group, "record_x264"), FALSE); + } else { + GtkWidget *pDialog = gtk_message_dialog_new(GTK_WINDOW(pFileSelection), + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + "Unable to record video to:\n%s", sPath); + gtk_dialog_run(GTK_DIALOG(pDialog)); + gtk_widget_destroy(pDialog); + } + + g_free(sPath); + break; + default: + break; + } + gtk_widget_destroy(pFileSelection); +} + +static void RecordAV_flac() +{ + GtkFileFilter *pFilter_flac, *pFilter_any; + GtkWidget *pFileSelection; + GtkWidget *pParent; + gchar *sPath; + + if (desmume_running()) + Pause(); + + pParent = GTK_WIDGET(pWindow); + + pFilter_flac = gtk_file_filter_new(); + gtk_file_filter_add_pattern(pFilter_flac, "*.flac"); + gtk_file_filter_set_name(pFilter_flac, "FLAC file (.flac)"); + + pFilter_any = gtk_file_filter_new(); + gtk_file_filter_add_pattern(pFilter_any, "*"); + gtk_file_filter_set_name(pFilter_any, "All files"); + + /* Creating the selection window */ + pFileSelection = gtk_file_chooser_dialog_new("Save audio...", + GTK_WINDOW(pParent), + GTK_FILE_CHOOSER_ACTION_SAVE, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_SAVE, GTK_RESPONSE_OK, + NULL); + gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (pFileSelection), TRUE); + + /* Only the dialog window is accepting events: */ + gtk_window_set_modal(GTK_WINDOW(pFileSelection), TRUE); + + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(pFileSelection), pFilter_flac); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(pFileSelection), pFilter_any); + + /* Showing the window */ + switch(gtk_dialog_run(GTK_DIALOG(pFileSelection))) { + case GTK_RESPONSE_OK: + sPath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(pFileSelection)); + + if(avout_flac.begin(sPath)) { + gtk_action_set_sensitive(gtk_action_group_get_action(action_group, "record_flac"), FALSE); + } else { + GtkWidget *pDialog = gtk_message_dialog_new(GTK_WINDOW(pFileSelection), + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + "Unable to record audio to:\n%s", sPath); + gtk_dialog_run(GTK_DIALOG(pDialog)); + gtk_widget_destroy(pDialog); + } + + g_free(sPath); + break; + default: + break; + } + gtk_widget_destroy(pFileSelection); +} + +static void RecordAV_stop() { + avout_x264.end(); + avout_flac.end(); + gtk_action_set_sensitive(gtk_action_group_get_action(action_group, "record_x264"), TRUE); + gtk_action_set_sensitive(gtk_action_group_get_action(action_group, "record_flac"), TRUE); +} + static void OpenNdsDialog() { GtkFileFilter *pFilter_nds, *pFilter_dsgba, *pFilter_any; @@ -1181,8 +1332,12 @@ static void OpenRecent(GtkRecentChooser *chooser, gpointer user_data) static void Reset() { + bool shouldBeRunning = desmume_running(); + Pause(); NDS_Reset(); - Launch(); + if (shouldBeRunning) { + Launch(); + } } @@ -2035,6 +2190,18 @@ public: while (gtk_events_pending()) gtk_main_iteration(); } + + // HUD uses this to show pause state + virtual bool EMU_IsEmulationPaused() { return !desmume_running(); } + + virtual bool AVI_IsRecording() + { + return avout_x264.isRecording() || avout_flac.isRecording(); + } + + virtual void AVI_SoundUpdate(void* soundData, int soundLen) { + avout_flac.updateAudio(soundData, soundLen); + } }; static void DoQuit() @@ -2138,6 +2305,7 @@ gboolean EmuLoop(gpointer data) _updateDTools(); gtk_widget_queue_draw( pDrawingArea ); osd->clear(); + avout_x264.updateVideo((u16*)GPU_screen); if (gtk_fps_limiter_disabled || keys_latch & KEYMASK_(KEY_BOOST - 1)) { if (autoframeskip) { @@ -2747,6 +2915,9 @@ common_gtk_main( class configured_features *my_config) /* Main loop */ gtk_main(); + avout_x264.end(); + avout_flac.end(); + desmume_free(); #if defined(HAVE_LIBOSMESA)