all repos — mgba @ 888b64f8b5b5cee593550ebce09df600e38b2422

mGBA Game Boy Advance Emulator

Video: GIF encoder using ImageMagick
Jeffrey Pfau jeffrey@endrift.com
Wed, 19 Nov 2014 03:19:35 -0800
commit

888b64f8b5b5cee593550ebce09df600e38b2422

parent

0308f136c7e195bd2e245dd3676d559b97595ceb

M CMakeLists.txtCMakeLists.txt

@@ -7,6 +7,7 @@ set(USE_GDB_STUB ON CACHE BOOL "Whether or not to enable the GDB stub ARM debugger")

set(USE_FFMPEG ON CACHE BOOL "Whether or not to enable FFmpeg support") set(USE_PNG ON CACHE BOOL "Whether or not to enable PNG support") set(USE_LIBZIP ON CACHE BOOL "Whether or not to enable ZIP support") +set(USE_MAGICK ON CACHE BOOL "Whether or not to enable ImageMagick support") set(BUILD_QT ON CACHE BOOL "Build Qt frontend") set(BUILD_SDL ON CACHE BOOL "Build SDL frontend") set(BUILD_PERF OFF CACHE BOOL "Build performance profiling tool")

@@ -86,6 +87,7 @@ find_feature(USE_CLI_DEBUGGER "libedit")

find_feature(USE_FFMPEG "libavcodec;libavformat;libavresample;libavutil;libswscale") find_feature(USE_PNG "ZLIB;PNG") find_feature(USE_LIBZIP "libzip") +find_feature(USE_MAGICK "MagickWand") include(CheckFunctionExists) check_function_exists(strndup HAVE_STRNDUP)

@@ -142,6 +144,14 @@ list(APPEND UTIL_SRC "${CMAKE_SOURCE_DIR}/src/platform/ffmpeg/ffmpeg-encoder.c")

list(APPEND DEPENDENCY_LIB ${LIBAVCODEC_LIBRARIES} ${LIBAVFORMAT_LIBRARIES} ${LIBAVRESAMPLE_LIBRARIES} ${LIBAVUTIL_LIBRARIES} ${LIBSWSCALE_LIBRARIES}) endif() +if(USE_MAGICK) + add_definitions(-DUSE_MAGICK) + include_directories(AFTER ${MAGICKWAND_INCLUDE_DIRS}) + link_directories(${MAGICKWAND_LIBRARY_DIRS}) + list(APPEND UTIL_SRC "${CMAKE_SOURCE_DIR}/src/platform/imagemagick/imagemagick-gif-encoder.c") + list(APPEND DEPENDENCY_LIB ${MAGICKWAND_LIBRARIES}) +endif() + if(USE_PNG) add_definitions(-DUSE_PNG) include_directories(AFTER ${PNG_INCLUDE_DIRS})

@@ -195,6 +205,7 @@ message(STATUS "Feature summary:")

message(STATUS " CLI debugger: ${USE_CLI_DEBUGGER}") message(STATUS " GDB stub: ${USE_GDB_STUB}") message(STATUS " Video recording: ${USE_FFMPEG}") +message(STATUS " GIF recording: ${USE_MAGICK}") message(STATUS " Screenshot/advanced savestate support: ${USE_PNG}") message(STATUS " ZIP support: ${USE_LIBZIP}") message(STATUS "Frontend summary:")
A src/platform/imagemagick/imagemagick-gif-encoder.c

@@ -0,0 +1,73 @@

+#include "imagemagick-gif-encoder.h" + +#include "gba-video.h" + +static void _magickPostVideoFrame(struct GBAAVStream*, struct GBAVideoRenderer* renderer); +static void _magickPostAudioFrame(struct GBAAVStream*, int32_t left, int32_t right); + +void ImageMagickGIFEncoderInit(struct ImageMagickGIFEncoder* encoder) { + encoder->wand = 0; + + encoder->d.postVideoFrame = _magickPostVideoFrame; + encoder->d.postAudioFrame = _magickPostAudioFrame; + + encoder->frameskip = 2; +} + +bool ImageMagickGIFEncoderOpen(struct ImageMagickGIFEncoder* encoder, const char* outfile) { + MagickWandGenesis(); + encoder->wand = NewMagickWand(); + encoder->outfile = strdup(outfile); + encoder->frame = malloc(VIDEO_HORIZONTAL_PIXELS * VIDEO_VERTICAL_PIXELS * 4); + encoder->currentFrame = 0; + return true; +} +void ImageMagickGIFEncoderClose(struct ImageMagickGIFEncoder* encoder) { + if (!encoder->wand) { + return; + } + MagickWriteImages(encoder->wand, encoder->outfile, MagickTrue); + free(encoder->outfile); + free(encoder->frame); + DestroyMagickWand(encoder->wand); + encoder->wand = 0; + MagickWandTerminus(); +} + +bool ImageMagickGIFEncoderIsOpen(struct ImageMagickGIFEncoder* encoder) { + return !!encoder->wand; +} + +static void _magickPostVideoFrame(struct GBAAVStream* stream, struct GBAVideoRenderer* renderer) { + struct ImageMagickGIFEncoder* encoder = (struct ImageMagickGIFEncoder*) stream; + + if (encoder->currentFrame % (encoder->frameskip + 1)) { + ++encoder->currentFrame; + return; + } + + uint8_t* pixels; + unsigned stride; + renderer->getPixels(renderer, &stride, (void**) &pixels); + size_t row; + for (row = 0; row < VIDEO_VERTICAL_PIXELS; ++row) { + memcpy(&encoder->frame[row * VIDEO_HORIZONTAL_PIXELS], &pixels[row * 4 *stride], VIDEO_HORIZONTAL_PIXELS * 4); + } + + MagickConstituteImage(encoder->wand, VIDEO_HORIZONTAL_PIXELS, VIDEO_VERTICAL_PIXELS, "RGBP", CharPixel, encoder->frame); + uint64_t ts = encoder->currentFrame; + uint64_t nts = encoder->currentFrame + encoder->frameskip + 1; + ts *= VIDEO_TOTAL_LENGTH * 100; + nts *= VIDEO_TOTAL_LENGTH * 100; + ts /= GBA_ARM7TDMI_FREQUENCY; + nts /= GBA_ARM7TDMI_FREQUENCY; + MagickSetImageDelay(encoder->wand, nts - ts); + ++encoder->currentFrame; +} + +static void _magickPostAudioFrame(struct GBAAVStream* stream, int32_t left, int32_t right) { + UNUSED(stream); + UNUSED(left); + UNUSED(right); + // This is a video-only format... +}
A src/platform/imagemagick/imagemagick-gif-encoder.h

@@ -0,0 +1,23 @@

+#ifndef IMAGEMAGICK_GIF_ENCODER +#define IMAGEMAGICK_GIF_ENCODER + +#include "gba-thread.h" + +#include <wand/MagickWand.h> + +struct ImageMagickGIFEncoder { + struct GBAAVStream d; + MagickWand* wand; + char* outfile; + uint32_t* frame; + + unsigned currentFrame; + int frameskip; +}; + +void ImageMagickGIFEncoderInit(struct ImageMagickGIFEncoder*); +bool ImageMagickGIFEncoderOpen(struct ImageMagickGIFEncoder*, const char* outfile); +void ImageMagickGIFEncoderClose(struct ImageMagickGIFEncoder*); +bool ImageMagickGIFEncoderIsOpen(struct ImageMagickGIFEncoder*); + +#endif
M src/platform/qt/CMakeLists.txtsrc/platform/qt/CMakeLists.txt

@@ -35,6 +35,7 @@ ConfigController.cpp

Display.cpp GBAApp.cpp GBAKeyEditor.cpp + GIFView.cpp GameController.cpp InputController.cpp KeyEditor.cpp

@@ -46,6 +47,7 @@ VFileDevice.cpp

VideoView.cpp) qt5_wrap_ui(UI_FILES + GIFView.ui LoadSaveState.ui LogView.ui VideoView.ui)
A src/platform/qt/GIFVIew.ui

@@ -0,0 +1,100 @@

+<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>GIFView</class> + <widget class="QWidget" name="GIFView"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>342</width> + <height>124</height> + </rect> + </property> + <property name="windowTitle"> + <string>Record GIF</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QGridLayout" name="gridLayout"> + <item row="1" column="0"> + <widget class="QPushButton" name="start"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Start</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="stop"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Stop</string> + </property> + </widget> + </item> + <item row="1" column="3"> + <widget class="QPushButton" name="selectFile"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Select File</string> + </property> + </widget> + </item> + <item row="0" column="0" colspan="4"> + <widget class="QLineEdit" name="filename"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>1</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + </widget> + </item> + <item row="1" column="2"> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + <item> + <widget class="QDialogButtonBox" name="buttonBox"> + <property name="standardButtons"> + <set>QDialogButtonBox::Close</set> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
A src/platform/qt/GIFView.cpp

@@ -0,0 +1,59 @@

+#include "GIFView.h" + +#ifdef USE_MAGICK + +#include <QFileDialog> +#include <QMap> + +using namespace QGBA; + +GIFView::GIFView(QWidget* parent) + : QWidget(parent) +{ + m_ui.setupUi(this); + + connect(m_ui.buttonBox, SIGNAL(rejected()), this, SLOT(close())); + connect(m_ui.start, SIGNAL(clicked()), this, SLOT(startRecording())); + connect(m_ui.stop, SIGNAL(clicked()), this, SLOT(stopRecording())); + + connect(m_ui.selectFile, SIGNAL(clicked()), this, SLOT(selectFile())); + connect(m_ui.filename, SIGNAL(textChanged(const QString&)), this, SLOT(setFilename(const QString&))); + + ImageMagickGIFEncoderInit(&m_encoder); +} + +GIFView::~GIFView() { + stopRecording(); +} + +void GIFView::startRecording() { + if (!ImageMagickGIFEncoderOpen(&m_encoder, m_filename.toLocal8Bit().constData())) { + return; + } + m_ui.start->setEnabled(false); + m_ui.stop->setEnabled(true); + emit recordingStarted(&m_encoder.d); +} + +void GIFView::stopRecording() { + emit recordingStopped(); + ImageMagickGIFEncoderClose(&m_encoder); + m_ui.stop->setEnabled(false); + m_ui.start->setEnabled(true); +} + +void GIFView::selectFile() { + QString filename = QFileDialog::getSaveFileName(this, tr("Select output file")); + if (!filename.isEmpty()) { + m_ui.filename->setText(filename); + if (!ImageMagickGIFEncoderIsOpen(&m_encoder)) { + m_ui.start->setEnabled(true); + } + } +} + +void GIFView::setFilename(const QString& fname) { + m_filename = fname; +} + +#endif
A src/platform/qt/GIFView.h

@@ -0,0 +1,49 @@

+#ifndef QGBA_GIF_VIEW +#define QGBA_GIF_VIEW + +#ifdef USE_MAGICK + +#include <QWidget> + +#include "ui_GIFView.h" + +extern "C" { +#include "platform/imagemagick/imagemagick-gif-encoder.h" +} + +namespace QGBA { + +class GIFView : public QWidget { +Q_OBJECT + +public: + GIFView(QWidget* parent = nullptr); + virtual ~GIFView(); + + GBAAVStream* getStream() { return &m_encoder.d; } + +public slots: + void startRecording(); + void stopRecording(); + +signals: + void recordingStarted(GBAAVStream*); + void recordingStopped(); + +private slots: + void selectFile(); + void setFilename(const QString&); + +private: + Ui::GIFView m_ui; + + ImageMagickGIFEncoder m_encoder; + + QString m_filename; +}; + +} + +#endif + +#endif
M src/platform/qt/VideoView.cppsrc/platform/qt/VideoView.cpp

@@ -167,16 +167,6 @@ .width = 240,

.height = 160, }); - addPreset(m_ui.presetGIF, (Preset) { - .container = "GIF", - .vcodec = "GIF", - .acodec = "None", - .vbr = 0, - .abr = 0, - .width = 240, - .height = 160, - }); - setAudioCodec(m_ui.audio->currentText()); setVideoCodec(m_ui.video->currentText()); setAudioBitrate(m_ui.abr->value());
M src/platform/qt/VideoView.uisrc/platform/qt/VideoView.ui

@@ -146,16 +146,6 @@ <string notr="true">presets</string>

</attribute> </widget> </item> - <item> - <widget class="QRadioButton" name="presetGIF"> - <property name="text"> - <string>GIF</string> - </property> - <attribute name="buttonGroup"> - <string notr="true">presets</string> - </attribute> - </widget> - </item> </layout> </item> <item>
M src/platform/qt/Window.cppsrc/platform/qt/Window.cpp

@@ -11,6 +11,7 @@ #include "GameController.h"

#include "GBAKeyEditor.h" #include "GDBController.h" #include "GDBWindow.h" +#include "GIFView.h" #include "LoadSaveState.h" #include "LogView.h" #include "VideoView.h"

@@ -30,6 +31,9 @@ , m_logo(":/res/mgba-1024.png")

, m_config(config) #ifdef USE_FFMPEG , m_videoView(nullptr) +#endif +#ifdef USE_MAGICK + , m_gifView(nullptr) #endif #ifdef USE_GDB_STUB , m_gdbController(nullptr)

@@ -70,6 +74,10 @@ delete m_logView;

#ifdef USE_FFMPEG delete m_videoView; +#endif + +#ifdef USE_MAGICK + delete m_gifView; #endif }

@@ -160,6 +168,20 @@ connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_videoView, SLOT(close()));

connect(this, SIGNAL(shutdown()), m_videoView, SLOT(close())); } m_videoView->show(); +} +#endif + +#ifdef USE_MAGICK +void Window::openGIFWindow() { + if (!m_gifView) { + m_gifView = new GIFView(); + connect(m_gifView, SIGNAL(recordingStarted(GBAAVStream*)), m_controller, SLOT(setAVStream(GBAAVStream*))); + connect(m_gifView, SIGNAL(recordingStopped()), m_controller, SLOT(clearAVStream()), Qt::DirectConnection); + connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_gifView, SLOT(stopRecording())); + connect(m_controller, SIGNAL(gameStopped(GBAThread*)), m_gifView, SLOT(close())); + connect(this, SIGNAL(shutdown()), m_gifView, SLOT(close())); + } + m_gifView->show(); } #endif

@@ -456,7 +478,7 @@ fpsTargetOption->addValue(tr("120"), 120, target);

fpsTargetOption->addValue(tr("240"), 240, target); m_config->updateOption("fpsTarget"); -#if defined(USE_PNG) || defined(USE_FFMPEG) +#if defined(USE_PNG) || defined(USE_FFMPEG) || defined(USE_MAGICK) avMenu->addSeparator(); #endif

@@ -475,6 +497,14 @@ recordOutput->setShortcut(tr("F11"));

connect(recordOutput, SIGNAL(triggered()), this, SLOT(openVideoWindow())); addAction(recordOutput); avMenu->addAction(recordOutput); +#endif + +#ifdef USE_MAGICK + QAction* recordGIF = new QAction(tr("Record GIF..."), avMenu); + recordGIF->setShortcut(tr("Shift+F11")); + connect(recordGIF, SIGNAL(triggered()), this, SLOT(openGIFWindow())); + addAction(recordGIF); + avMenu->addAction(recordGIF); #endif QMenu* debuggingMenu = menubar->addMenu(tr("&Debugging"));
M src/platform/qt/Window.hsrc/platform/qt/Window.h

@@ -20,6 +20,7 @@ namespace QGBA {

class ConfigController; class GameController; +class GIFView; class LogView; class VideoView; class WindowBackground;

@@ -54,6 +55,10 @@ void openKeymapWindow();

#ifdef USE_FFMPEG void openVideoWindow(); +#endif + +#ifdef USE_MAGICK + void openGIFWindow(); #endif #ifdef USE_GDB_STUB

@@ -90,6 +95,10 @@ InputController m_inputController;

#ifdef USE_FFMPEG VideoView* m_videoView; +#endif + +#ifdef USE_MAGICK + GIFView* m_gifView; #endif #ifdef USE_GDB_STUB