mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2026-04-10 12:17:20 +00:00
Merge remote-tracking branch 'origin/master' into special-stage-magician
This commit is contained in:
commit
17af9463ac
73 changed files with 4142 additions and 165 deletions
|
|
@ -20,7 +20,7 @@ AlwaysBreakTemplateDeclarations: Yes
|
|||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBraces: Allman # Always break before braces, to match existing SRB2 code
|
||||
BreakConstructorInitializers: BeforeComma
|
||||
BreakConstructorInitializers: AfterColon
|
||||
CompactNamespaces: true
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
Cpp11BracedListStyle: true
|
||||
|
|
@ -55,3 +55,4 @@ SpacesInConditionalStatement: false
|
|||
SpacesInContainerLiterals: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
PenaltyReturnTypeOnItsOwnLine: 1000
|
||||
|
|
|
|||
|
|
@ -132,6 +132,9 @@ if("${SRB2_CONFIG_SYSTEM_LIBRARIES}")
|
|||
find_package(SDL2 REQUIRED)
|
||||
find_package(CURL REQUIRED)
|
||||
find_package(GME REQUIRED)
|
||||
find_package(VPX REQUIRED)
|
||||
find_package(Vorbis REQUIRED)
|
||||
find_package(VorbisEnc REQUIRED)
|
||||
endif()
|
||||
|
||||
if(${PROJECT_SOURCE_DIR} MATCHES ${PROJECT_BINARY_DIR})
|
||||
|
|
|
|||
33
cmake/Modules/FindVPX.cmake
Normal file
33
cmake/Modules/FindVPX.cmake
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
include(LibFindMacros)
|
||||
|
||||
libfind_pkg_check_modules(VPX_PKGCONF VPX)
|
||||
|
||||
find_path(VPX_INCLUDE_DIR
|
||||
NAMES vpx/vp8.h
|
||||
PATHS
|
||||
${VPX_PKGCONF_INCLUDE_DIRS}
|
||||
"/usr/include"
|
||||
"/usr/local/include"
|
||||
)
|
||||
|
||||
find_library(VPX_LIBRARY
|
||||
NAMES vpx
|
||||
PATHS
|
||||
${VPX_PKGCONF_LIBRARY_DIRS}
|
||||
"/usr/lib"
|
||||
"/usr/local/lib"
|
||||
)
|
||||
|
||||
set(VPX_PROCESS_INCLUDES VPX_INCLUDE_DIR)
|
||||
set(VPX_PROCESS_LIBS VPX_LIBRARY)
|
||||
libfind_process(VPX)
|
||||
|
||||
if(VPX_FOUND AND NOT TARGET webm::libvpx)
|
||||
add_library(webm::libvpx UNKNOWN IMPORTED)
|
||||
set_target_properties(
|
||||
webm::libvpx
|
||||
PROPERTIES
|
||||
IMPORTED_LOCATION "${VPX_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${VPX_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
33
cmake/Modules/FindVorbis.cmake
Normal file
33
cmake/Modules/FindVorbis.cmake
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
include(LibFindMacros)
|
||||
|
||||
libfind_pkg_check_modules(Vorbis_PKGCONF Vorbis)
|
||||
|
||||
find_path(Vorbis_INCLUDE_DIR
|
||||
NAMES vorbis/codec.h
|
||||
PATHS
|
||||
${Vorbis_PKGCONF_INCLUDE_DIRS}
|
||||
"/usr/include"
|
||||
"/usr/local/include"
|
||||
)
|
||||
|
||||
find_library(Vorbis_LIBRARY
|
||||
NAMES vorbis
|
||||
PATHS
|
||||
${Vorbis_PKGCONF_LIBRARY_DIRS}
|
||||
"/usr/lib"
|
||||
"/usr/local/lib"
|
||||
)
|
||||
|
||||
set(Vorbis_PROCESS_INCLUDES Vorbis_INCLUDE_DIR)
|
||||
set(Vorbis_PROCESS_LIBS Vorbis_LIBRARY)
|
||||
libfind_process(Vorbis)
|
||||
|
||||
if(Vorbis_FOUND AND NOT TARGET Vorbis::vorbis)
|
||||
add_library(Vorbis::vorbis UNKNOWN IMPORTED)
|
||||
set_target_properties(
|
||||
Vorbis::vorbis
|
||||
PROPERTIES
|
||||
IMPORTED_LOCATION "${Vorbis_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${Vorbis_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
33
cmake/Modules/FindVorbisEnc.cmake
Normal file
33
cmake/Modules/FindVorbisEnc.cmake
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
include(LibFindMacros)
|
||||
|
||||
libfind_pkg_check_modules(VorbisEnc_PKGCONF VorbisEnc)
|
||||
|
||||
find_path(VorbisEnc_INCLUDE_DIR
|
||||
NAMES vorbis/vorbisenc.h
|
||||
PATHS
|
||||
${VorbisEnc_PKGCONF_INCLUDE_DIRS}
|
||||
"/usr/include"
|
||||
"/usr/local/include"
|
||||
)
|
||||
|
||||
find_library(VorbisEnc_LIBRARY
|
||||
NAMES vorbisenc
|
||||
PATHS
|
||||
${VorbisEnc_PKGCONF_LIBRARY_DIRS}
|
||||
"/usr/lib"
|
||||
"/usr/local/lib"
|
||||
)
|
||||
|
||||
set(VorbisEnc_PROCESS_INCLUDES VorbisEnc_INCLUDE_DIR)
|
||||
set(VorbisEnc_PROCESS_LIBS VorbisEnc_LIBRARY)
|
||||
libfind_process(VorbisEnc)
|
||||
|
||||
if(VorbisEnc_FOUND AND NOT TARGET Vorbis::vorbisenc)
|
||||
add_library(Vorbis::vorbisenc UNKNOWN IMPORTED)
|
||||
set_target_properties(
|
||||
Vorbis::vorbisenc
|
||||
PROPERTIES
|
||||
IMPORTED_LOCATION "${VorbisEnc_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${VorbisEnc_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
|
|
@ -32,6 +32,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32
|
|||
m_aatree.c
|
||||
m_anigif.c
|
||||
m_argv.c
|
||||
m_avrecorder.cpp
|
||||
m_bbox.c
|
||||
m_cheat.c
|
||||
m_cond.c
|
||||
|
|
@ -229,6 +230,9 @@ target_link_libraries(SRB2SDL2 PRIVATE xmp-lite::xmp-lite)
|
|||
target_link_libraries(SRB2SDL2 PRIVATE glad::glad)
|
||||
target_link_libraries(SRB2SDL2 PRIVATE fmt)
|
||||
target_link_libraries(SRB2SDL2 PRIVATE imgui::imgui)
|
||||
target_link_libraries(SRB2SDL2 PRIVATE webm::libwebm webm::libvpx)
|
||||
target_link_libraries(SRB2SDL2 PRIVATE libyuv::libyuv)
|
||||
target_link_libraries(SRB2SDL2 PRIVATE Vorbis::vorbis Vorbis::vorbisenc)
|
||||
|
||||
target_link_libraries(SRB2SDL2 PRIVATE acsvm)
|
||||
|
||||
|
|
@ -546,6 +550,7 @@ if(SRB2_CONFIG_ENABLE_TESTS)
|
|||
add_subdirectory(tests)
|
||||
endif()
|
||||
add_subdirectory(menus)
|
||||
add_subdirectory(media)
|
||||
|
||||
# strip debug symbols into separate file when using gcc.
|
||||
# to be consistent with Makefile, don't generate for OS X.
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@
|
|||
#include "deh_tables.h"
|
||||
#include "m_perfstats.h"
|
||||
#include "k_specialstage.h"
|
||||
#include "m_avrecorder.h"
|
||||
|
||||
#ifdef HAVE_DISCORDRPC
|
||||
#include "discord.h"
|
||||
|
|
@ -903,6 +904,7 @@ void D_RegisterClientCommands(void)
|
|||
CV_RegisterVar(&cv_moviemode);
|
||||
CV_RegisterVar(&cv_movie_option);
|
||||
CV_RegisterVar(&cv_movie_folder);
|
||||
M_AVRecorder_AddCommands();
|
||||
// PNG variables
|
||||
CV_RegisterVar(&cv_zlib_level);
|
||||
CV_RegisterVar(&cv_zlib_memory);
|
||||
|
|
@ -4942,23 +4944,30 @@ static void PointLimit_OnChange(void)
|
|||
return;
|
||||
}
|
||||
|
||||
// Don't allow pointlimit in non-pointlimited gametypes!
|
||||
if (server && Playing() && !(gametyperules & GTR_POINTLIMIT))
|
||||
if (gamestate == GS_LEVEL && leveltime < starttime)
|
||||
{
|
||||
if (cv_pointlimit.value)
|
||||
CV_StealthSetValue(&cv_pointlimit, 0);
|
||||
return;
|
||||
}
|
||||
{
|
||||
CONS_Printf(M_GetText("Point limit has been set to %d.\n"), cv_pointlimit.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CONS_Printf(M_GetText("Point limit has been disabled.\n"));
|
||||
}
|
||||
|
||||
if (cv_pointlimit.value)
|
||||
{
|
||||
CONS_Printf(M_GetText("Levels will end after %s scores %d point%s.\n"),
|
||||
G_GametypeHasTeams() ? M_GetText("a team") : M_GetText("someone"),
|
||||
cv_pointlimit.value,
|
||||
cv_pointlimit.value > 1 ? "s" : "");
|
||||
g_pointlimit = cv_pointlimit.value;
|
||||
}
|
||||
else
|
||||
CONS_Printf(M_GetText("Point limit disabled\n"));
|
||||
{
|
||||
if (cv_pointlimit.value)
|
||||
{
|
||||
CONS_Printf(M_GetText("Point limit will be %d next round.\n"), cv_pointlimit.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CONS_Printf(M_GetText("Point limit will be disabled next round.\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void NetTimeout_OnChange(void)
|
||||
|
|
@ -4983,6 +4992,8 @@ UINT32 timelimitintics = 0;
|
|||
UINT32 extratimeintics = 0;
|
||||
UINT32 secretextratime = 0;
|
||||
|
||||
UINT32 g_pointlimit = 0;
|
||||
|
||||
/** Deals with a timelimit change by printing the change to the console.
|
||||
* If the gametype is single player, cooperative, or race, the timelimit is
|
||||
* silently disabled again.
|
||||
|
|
@ -6463,7 +6474,7 @@ static void Command_ShowScores_f(void)
|
|||
// FIXME: %lu? what's wrong with %u? ~Callum (produces warnings...)
|
||||
CONS_Printf(M_GetText("%s's score is %u\n"), player_names[i], players[i].score);
|
||||
}
|
||||
CONS_Printf(M_GetText("The pointlimit is %d\n"), cv_pointlimit.value);
|
||||
CONS_Printf(M_GetText("The pointlimit is %d\n"), g_pointlimit);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ extern consvar_t cv_pointlimit;
|
|||
extern consvar_t cv_timelimit;
|
||||
extern consvar_t cv_numlaps;
|
||||
extern UINT32 timelimitintics, extratimeintics, secretextratime;
|
||||
extern UINT32 g_pointlimit;
|
||||
extern consvar_t cv_allowexitlevel;
|
||||
|
||||
extern consvar_t cv_autobalance;
|
||||
|
|
|
|||
|
|
@ -661,6 +661,8 @@ struct player_t
|
|||
|
||||
UINT8 eggmanTransferDelay;
|
||||
|
||||
UINT8 tripwireReboundDelay; // When failing Tripwire, brieftly lock out speed-based tripwire pass (anti-cheese)
|
||||
|
||||
mobj_t *stumbleIndicator;
|
||||
|
||||
#ifdef HWRENDER
|
||||
|
|
|
|||
42
src/g_game.c
42
src/g_game.c
|
|
@ -4359,7 +4359,7 @@ void G_LoadGameSettings(void)
|
|||
}
|
||||
|
||||
#define GD_VERSIONCHECK 0xBA5ED123 // Change every major version, as usual
|
||||
#define GD_VERSIONMINOR 0 // Change every format update
|
||||
#define GD_VERSIONMINOR 1 // Change every format update
|
||||
|
||||
// G_LoadGameData
|
||||
// Loads the main data file, which stores information such as emblems found, etc.
|
||||
|
|
@ -4369,6 +4369,7 @@ void G_LoadGameData(void)
|
|||
UINT32 versionID;
|
||||
UINT8 versionMinor;
|
||||
UINT8 rtemp;
|
||||
boolean gridunusable = false;
|
||||
savebuffer_t save = {0};
|
||||
|
||||
//For records
|
||||
|
|
@ -4423,6 +4424,10 @@ void G_LoadGameData(void)
|
|||
P_SaveBufferFree(&save);
|
||||
I_Error("Game data is from the future! (expected %d, got %d)", GD_VERSIONMINOR, versionMinor);
|
||||
}
|
||||
if (versionMinor == 0)
|
||||
{
|
||||
gridunusable = true;
|
||||
}
|
||||
|
||||
gamedata->totalplaytime = READUINT32(save.p);
|
||||
gamedata->matchesplayed = READUINT32(save.p);
|
||||
|
|
@ -4469,21 +4474,34 @@ void G_LoadGameData(void)
|
|||
i += j;
|
||||
}
|
||||
|
||||
gamedata->challengegridwidth = READUINT16(save.p);
|
||||
Z_Free(gamedata->challengegrid);
|
||||
if (gamedata->challengegridwidth)
|
||||
if (gridunusable)
|
||||
{
|
||||
gamedata->challengegrid = Z_Malloc(
|
||||
(gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)),
|
||||
PU_STATIC, NULL);
|
||||
for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++)
|
||||
{
|
||||
gamedata->challengegrid[i] = READUINT8(save.p);
|
||||
}
|
||||
UINT16 burn = READUINT16(save.p); // Previous challengegridwidth
|
||||
UINT8 height = (versionMinor > 0) ? CHALLENGEGRIDHEIGHT : 5;
|
||||
save.p += (burn * height * sizeof(UINT8)); // Step over previous grid data
|
||||
|
||||
gamedata->challengegridwidth = 0;
|
||||
Z_Free(gamedata->challengegrid);
|
||||
gamedata->challengegrid = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
gamedata->challengegrid = NULL;
|
||||
gamedata->challengegridwidth = READUINT16(save.p);
|
||||
Z_Free(gamedata->challengegrid);
|
||||
if (gamedata->challengegridwidth)
|
||||
{
|
||||
gamedata->challengegrid = Z_Malloc(
|
||||
(gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)),
|
||||
PU_STATIC, NULL);
|
||||
for (i = 0; i < (gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT); i++)
|
||||
{
|
||||
gamedata->challengegrid[i] = READUINT8(save.p);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
gamedata->challengegrid = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
gamedata->timesBeaten = READUINT32(save.p);
|
||||
|
|
|
|||
|
|
@ -2453,10 +2453,10 @@ static void HU_DrawRankings(void)
|
|||
|
||||
timedone = true;
|
||||
}
|
||||
else if ((gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0)
|
||||
else if ((gametyperules & GTR_POINTLIMIT) && g_pointlimit > 0)
|
||||
{
|
||||
V_DrawCenteredString(64, 8, 0, "POINT LIMIT");
|
||||
V_DrawCenteredString(64, 16, hilicol, va("%d", cv_pointlimit.value));
|
||||
V_DrawCenteredString(64, 16, hilicol, va("%d", g_pointlimit));
|
||||
pointsdone = true;
|
||||
}
|
||||
else if (gametyperules & GTR_CIRCUIT)
|
||||
|
|
@ -2494,10 +2494,10 @@ static void HU_DrawRankings(void)
|
|||
V_DrawCenteredString(256, 16, hilicol, "OVERTIME");
|
||||
}
|
||||
}
|
||||
else if (!pointsdone && (gametyperules & GTR_POINTLIMIT) && cv_pointlimit.value > 0)
|
||||
else if (!pointsdone && (gametyperules & GTR_POINTLIMIT) && g_pointlimit > 0)
|
||||
{
|
||||
V_DrawCenteredString(256, 8, 0, "POINT LIMIT");
|
||||
V_DrawCenteredString(256, 16, hilicol, va("%d", cv_pointlimit.value));
|
||||
V_DrawCenteredString(256, 16, hilicol, va("%d", g_pointlimit));
|
||||
}
|
||||
else if (gametyperules & GTR_CIRCUIT)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
#include "../discord.h"
|
||||
#endif
|
||||
#include "../doomstat.h"
|
||||
#include "../m_avrecorder.h"
|
||||
#include "../st_stuff.h"
|
||||
#include "../s_sound.h"
|
||||
#include "../v_video.h"
|
||||
|
|
@ -121,6 +122,8 @@ static void temp_legacy_finishupdate_draws()
|
|||
}
|
||||
if (cv_mindelay.value && consoleplayer == serverplayer && Playing())
|
||||
SCR_DisplayLocalPing();
|
||||
|
||||
M_AVRecorder_DrawFrameRate();
|
||||
}
|
||||
|
||||
if (marathonmode)
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ void I_StartupSound(void);
|
|||
*/
|
||||
void I_ShutdownSound(void);
|
||||
|
||||
/** \brief Update instance of AVRecorder for audio capture.
|
||||
*/
|
||||
void I_UpdateAudioRecorder(void);
|
||||
|
||||
/// ------------------------
|
||||
/// SFX I/O
|
||||
/// ------------------------
|
||||
|
|
|
|||
|
|
@ -8037,7 +8037,7 @@ mobjinfo_t mobjinfo[NUMMOBJTYPES] =
|
|||
S_NULL, // missilestate
|
||||
S_SPRK1, // deathstate
|
||||
S_NULL, // xdeathstate
|
||||
sfx_ncitem, // deathsound
|
||||
sfx_None, // deathsound
|
||||
1, // speed
|
||||
16*FRACUNIT, // radius
|
||||
30*FRACUNIT, // height
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ void K_DrawMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, UINT16 map,
|
|||
void K_DrawLikeMapThumbnail(INT32 x, INT32 y, INT32 width, UINT32 flags, patch_t *patch, UINT8 *colormap);
|
||||
void K_drawTargetHUD(const vector3_t *origin, player_t *player);
|
||||
|
||||
extern patch_t *kp_facehighlight[8];
|
||||
extern patch_t *kp_capsuletarget_arrow[2][2];
|
||||
extern patch_t *kp_capsuletarget_icon[2];
|
||||
extern patch_t *kp_capsuletarget_far[2];
|
||||
|
|
|
|||
18
src/k_kart.c
18
src/k_kart.c
|
|
@ -100,6 +100,7 @@ void K_TimerReset(void)
|
|||
numbulbs = 1;
|
||||
inDuel = rainbowstartavailable = false;
|
||||
timelimitintics = extratimeintics = secretextratime = 0;
|
||||
g_pointlimit = 0;
|
||||
}
|
||||
|
||||
void K_TimerInit(void)
|
||||
|
|
@ -206,6 +207,11 @@ void K_TimerInit(void)
|
|||
}
|
||||
}
|
||||
|
||||
if (gametyperules & GTR_POINTLIMIT)
|
||||
{
|
||||
g_pointlimit = cv_pointlimit.value;
|
||||
}
|
||||
|
||||
if (inDuel == true)
|
||||
{
|
||||
K_SpawnDuelOnlyItems();
|
||||
|
|
@ -2683,7 +2689,7 @@ tripwirepass_t K_TripwirePassConditions(player_t *player)
|
|||
|
||||
if (
|
||||
player->flamedash ||
|
||||
player->speed > 2 * K_GetKartSpeed(player, false, false)
|
||||
(player->speed > 2 * K_GetKartSpeed(player, false, false) && player->tripwireReboundDelay == 0)
|
||||
)
|
||||
return TRIPWIRE_BOOST;
|
||||
|
||||
|
|
@ -4080,10 +4086,17 @@ void K_TumbleInterrupt(player_t *player)
|
|||
|
||||
void K_ApplyTripWire(player_t *player, tripwirestate_t state)
|
||||
{
|
||||
// We are either softlocked or wildly misbehaving. Stop that!
|
||||
if (state == TRIPSTATE_BLOCKED && player->tripwireReboundDelay && (player->speed > 5 * K_GetKartSpeed(player, false, false)))
|
||||
K_TumblePlayer(player, NULL, NULL);
|
||||
|
||||
if (state == TRIPSTATE_PASSED)
|
||||
S_StartSound(player->mo, sfx_ssa015);
|
||||
else if (state == TRIPSTATE_BLOCKED)
|
||||
{
|
||||
S_StartSound(player->mo, sfx_kc40);
|
||||
player->tripwireReboundDelay = 60;
|
||||
}
|
||||
|
||||
player->tripwireState = state;
|
||||
K_AddHitLag(player->mo, 10, false);
|
||||
|
|
@ -7715,6 +7728,9 @@ void K_KartPlayerThink(player_t *player, ticcmd_t *cmd)
|
|||
if (player->eggmanTransferDelay)
|
||||
player->eggmanTransferDelay--;
|
||||
|
||||
if (player->tripwireReboundDelay)
|
||||
player->tripwireReboundDelay--;
|
||||
|
||||
if (player->ringdelay)
|
||||
player->ringdelay--;
|
||||
|
||||
|
|
|
|||
|
|
@ -1137,6 +1137,8 @@ void M_DrawAddons(void);
|
|||
#define CC_ANIM 3
|
||||
#define CC_MAX 4
|
||||
|
||||
#define TILEFLIP_MAX 16
|
||||
|
||||
// Keep track of some pause menu data for visual goodness.
|
||||
extern struct challengesmenu_s {
|
||||
|
||||
|
|
@ -1151,7 +1153,7 @@ extern struct challengesmenu_s {
|
|||
SINT8 row, hilix, focusx;
|
||||
UINT8 col, hiliy;
|
||||
|
||||
UINT8 *extradata;
|
||||
challengegridextradata_t *extradata;
|
||||
|
||||
boolean pending;
|
||||
boolean requestnew;
|
||||
|
|
|
|||
344
src/k_menudraw.c
344
src/k_menudraw.c
|
|
@ -1479,7 +1479,7 @@ static void M_DrawCharSelectPreview(UINT8 num)
|
|||
static void M_DrawCharSelectExplosions(boolean charsel, INT16 basex, INT16 basey)
|
||||
{
|
||||
UINT8 i;
|
||||
INT16 quadx = 0, quady = 0;
|
||||
INT16 quadx = 2, quady = 2, mul = 22;
|
||||
|
||||
for (i = 0; i < CSEXPLOSIONS; i++)
|
||||
{
|
||||
|
|
@ -1495,13 +1495,14 @@ static void M_DrawCharSelectExplosions(boolean charsel, INT16 basex, INT16 basey
|
|||
{
|
||||
quadx = 4 * (setup_explosions[i].x / 3);
|
||||
quady = 4 * (setup_explosions[i].y / 3);
|
||||
mul = 16;
|
||||
}
|
||||
|
||||
colormap = R_GetTranslationColormap(TC_DEFAULT, setup_explosions[i].color, GTC_MENUCACHE);
|
||||
|
||||
V_DrawMappedPatch(
|
||||
basex + (setup_explosions[i].x*16) + quadx - 6,
|
||||
basey + (setup_explosions[i].y*16) + quady - 6,
|
||||
basex + (setup_explosions[i].x*mul) + quadx - 6,
|
||||
basey + (setup_explosions[i].y*mul) + quady - 6,
|
||||
0, W_CachePatchName(va("CHCNFRM%d", frame), PU_CACHE),
|
||||
colormap
|
||||
);
|
||||
|
|
@ -4516,10 +4517,11 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili
|
|||
{
|
||||
unlockable_t *ref = NULL;
|
||||
patch_t *pat = missingpat;
|
||||
UINT8 *colormap = NULL;
|
||||
fixed_t siz;
|
||||
UINT8 *colormap = NULL, *bgmap = NULL;
|
||||
fixed_t siz, accordion;
|
||||
UINT8 id, num;
|
||||
UINT32 edgelength;
|
||||
boolean unlockedyet;
|
||||
boolean categoryside;
|
||||
|
||||
id = (i * CHALLENGEGRIDHEIGHT) + j;
|
||||
num = gamedata->challengegrid[id];
|
||||
|
|
@ -4533,18 +4535,116 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili
|
|||
// Okay, this is what we want to draw.
|
||||
ref = &unlockables[num];
|
||||
|
||||
edgelength = (ref->majorunlock ? 30 : 14);
|
||||
unlockedyet = !((gamedata->unlocked[num] == false)
|
||||
|| (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME));
|
||||
|
||||
// ...unless we simply aren't unlocked yet.
|
||||
if ((gamedata->unlocked[num] == false)
|
||||
|| (challengesmenu.pending && num == challengesmenu.currentunlock && challengesmenu.unlockanim <= UNLOCKTIME))
|
||||
// If we aren't unlocked yet, return early.
|
||||
if (!unlockedyet)
|
||||
{
|
||||
V_DrawFill(x+1, y+1, edgelength, edgelength,
|
||||
((challengesmenu.extradata[id] == CHE_HINT) ? 132 : 11));
|
||||
UINT32 flags = 0;
|
||||
|
||||
if (challengesmenu.extradata[id].flags != CHE_HINT)
|
||||
{
|
||||
colormap = R_GetTranslationColormap(TC_BLINK, SKINCOLOR_BLACK, GTC_CACHE);
|
||||
flags = V_SUBTRACT|V_90TRANS;
|
||||
}
|
||||
|
||||
pat = W_CachePatchName(
|
||||
va("UN_HNT%c%c",
|
||||
(hili && !colormap) ? '1' : '2',
|
||||
ref->majorunlock ? 'B' : 'A'
|
||||
),
|
||||
PU_CACHE);
|
||||
|
||||
V_DrawFixedPatch(
|
||||
x*FRACUNIT, y*FRACUNIT,
|
||||
FRACUNIT,
|
||||
flags, pat,
|
||||
colormap
|
||||
);
|
||||
|
||||
pat = missingpat;
|
||||
colormap = NULL;
|
||||
|
||||
goto drawborder;
|
||||
}
|
||||
|
||||
if (ref->icon != NULL && ref->icon[0])
|
||||
accordion = FRACUNIT;
|
||||
|
||||
if (challengesmenu.extradata[id].flip != 0
|
||||
&& challengesmenu.extradata[id].flip != (TILEFLIP_MAX/2))
|
||||
{
|
||||
angle_t bad = (FixedAngle((fixed_t)(challengesmenu.extradata[id].flip) * (360*FRACUNIT/TILEFLIP_MAX)) >> ANGLETOFINESHIFT) & FINEMASK;
|
||||
accordion = FINECOSINE(bad);
|
||||
if (accordion < 0)
|
||||
accordion = -accordion;
|
||||
}
|
||||
|
||||
pat = W_CachePatchName(
|
||||
(ref->majorunlock ? "UN_BORDB" : "UN_BORDA"),
|
||||
PU_CACHE);
|
||||
|
||||
bgmap = R_GetTranslationColormap(TC_DEFAULT, SKINCOLOR_SILVER, GTC_MENUCACHE);
|
||||
|
||||
V_DrawStretchyFixedPatch(
|
||||
(x*FRACUNIT) + (SHORT(pat->width)*(FRACUNIT-accordion)/2), y*FRACUNIT,
|
||||
accordion,
|
||||
FRACUNIT,
|
||||
0, pat,
|
||||
bgmap
|
||||
);
|
||||
|
||||
pat = missingpat;
|
||||
|
||||
categoryside = (challengesmenu.extradata[id].flip <= TILEFLIP_MAX/4
|
||||
|| challengesmenu.extradata[id].flip > (3*TILEFLIP_MAX)/4);
|
||||
|
||||
if (categoryside)
|
||||
{
|
||||
char categoryid = '8';
|
||||
colormap = bgmap;
|
||||
switch (ref->type)
|
||||
{
|
||||
case SECRET_SKIN:
|
||||
categoryid = '1';
|
||||
break;
|
||||
case SECRET_FOLLOWER:
|
||||
categoryid = '2';
|
||||
break;
|
||||
/*case SECRET_COLOR:
|
||||
categoryid = '3';
|
||||
break;*/
|
||||
case SECRET_CUP:
|
||||
categoryid = '4';
|
||||
break;
|
||||
//case SECRET_MASTERBOTS:
|
||||
case SECRET_HARDSPEED:
|
||||
case SECRET_ENCORE:
|
||||
categoryid = '5';
|
||||
break;
|
||||
case SECRET_ALTTITLE:
|
||||
case SECRET_SOUNDTEST:
|
||||
categoryid = '6';
|
||||
break;
|
||||
case SECRET_TIMEATTACK:
|
||||
case SECRET_BREAKTHECAPSULES:
|
||||
case SECRET_SPECIALATTACK:
|
||||
categoryid = '7';
|
||||
break;
|
||||
}
|
||||
pat = W_CachePatchName(va("UN_RR0%c%c",
|
||||
categoryid,
|
||||
(ref->majorunlock) ? 'B' : 'A'),
|
||||
PU_CACHE);
|
||||
if (pat == missingpat)
|
||||
{
|
||||
pat = W_CachePatchName(va("UN_RR0%c%c",
|
||||
categoryid,
|
||||
(ref->majorunlock) ? 'A' : 'B'),
|
||||
PU_CACHE);
|
||||
}
|
||||
}
|
||||
else if (ref->icon != NULL && ref->icon[0])
|
||||
{
|
||||
pat = W_CachePatchName(ref->icon, PU_CACHE);
|
||||
if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors)
|
||||
|
|
@ -4552,58 +4652,110 @@ static void M_DrawChallengeTile(INT16 i, INT16 j, INT32 x, INT32 y, boolean hili
|
|||
colormap = R_GetTranslationColormap(TC_DEFAULT, ref->color, GTC_MENUCACHE);
|
||||
}
|
||||
}
|
||||
else switch (ref->type)
|
||||
else
|
||||
{
|
||||
case SECRET_SKIN:
|
||||
UINT8 iconid = 0;
|
||||
switch (ref->type)
|
||||
{
|
||||
INT32 skin = M_UnlockableSkinNum(ref);
|
||||
if (skin != -1)
|
||||
case SECRET_SKIN:
|
||||
{
|
||||
colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE);
|
||||
pat = faceprefix[skin][(ref->majorunlock) ? FACE_WANTED : FACE_RANK];
|
||||
INT32 skin = M_UnlockableSkinNum(ref);
|
||||
if (skin != -1)
|
||||
{
|
||||
colormap = R_GetTranslationColormap(skin, skins[skin].prefcolor, GTC_MENUCACHE);
|
||||
pat = faceprefix[skin][(ref->majorunlock) ? FACE_WANTED : FACE_RANK];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SECRET_FOLLOWER:
|
||||
{
|
||||
INT32 skin = M_UnlockableFollowerNum(ref);
|
||||
if (skin != -1)
|
||||
{
|
||||
UINT16 col = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value);
|
||||
colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE);
|
||||
pat = W_CachePatchName(followers[skin].icon, PU_CACHE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/*case SECRET_MASTERBOTS:
|
||||
iconid = 4;
|
||||
break;*/
|
||||
case SECRET_HARDSPEED:
|
||||
iconid = 3;
|
||||
break;
|
||||
case SECRET_ENCORE:
|
||||
iconid = 5;
|
||||
break;
|
||||
|
||||
case SECRET_ALTTITLE:
|
||||
iconid = 6;
|
||||
break;
|
||||
case SECRET_SOUNDTEST:
|
||||
iconid = 1;
|
||||
break;
|
||||
|
||||
case SECRET_TIMEATTACK:
|
||||
iconid = 7;
|
||||
break;
|
||||
case SECRET_BREAKTHECAPSULES:
|
||||
iconid = 8;
|
||||
break;
|
||||
case SECRET_SPECIALATTACK:
|
||||
iconid = 9;
|
||||
break;
|
||||
|
||||
default:
|
||||
{
|
||||
if (!colormap && ref->color != SKINCOLOR_NONE && ref->color < numskincolors)
|
||||
{
|
||||
colormap = R_GetTranslationColormap(TC_RAINBOW, ref->color, GTC_MENUCACHE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SECRET_FOLLOWER:
|
||||
|
||||
if (pat == missingpat)
|
||||
{
|
||||
INT32 skin = M_UnlockableFollowerNum(ref);
|
||||
if (skin != -1)
|
||||
pat = W_CachePatchName(va("UN_IC%02u%c",
|
||||
iconid,
|
||||
ref->majorunlock ? 'B' : 'A'),
|
||||
PU_CACHE);
|
||||
if (pat == missingpat)
|
||||
{
|
||||
UINT16 col = K_GetEffectiveFollowerColor(followers[skin].defaultcolor, cv_playercolor[0].value);
|
||||
colormap = R_GetTranslationColormap(TC_DEFAULT, col, GTC_MENUCACHE);
|
||||
pat = W_CachePatchName(followers[skin].icon, PU_CACHE);
|
||||
pat = W_CachePatchName(va("UN_IC%02u%c",
|
||||
iconid,
|
||||
ref->majorunlock ? 'A' : 'B'),
|
||||
PU_CACHE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
pat = W_CachePatchName(va("UN_RR00%c", ref->majorunlock ? 'B' : 'A'), PU_CACHE);
|
||||
if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors)
|
||||
{
|
||||
//CONS_Printf(" color for %d is %s\n", num, skincolors[unlockables[num].color].name);
|
||||
colormap = R_GetTranslationColormap(TC_RAINBOW, ref->color, GTC_MENUCACHE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
siz = (SHORT(pat->width) << FRACBITS);
|
||||
siz = FixedDiv(((ref->majorunlock) ? 32 : 16) << FRACBITS, siz);
|
||||
|
||||
V_SetClipRect(
|
||||
(x+1) << FRACBITS, (y+1) << FRACBITS,
|
||||
edgelength << FRACBITS, edgelength << FRACBITS,
|
||||
0
|
||||
);
|
||||
|
||||
V_DrawFixedPatch(
|
||||
x*FRACUNIT, y*FRACUNIT,
|
||||
siz,
|
||||
0, pat,
|
||||
colormap
|
||||
);
|
||||
|
||||
V_ClearClipRect();
|
||||
if (!siz)
|
||||
; // prevent div/0
|
||||
else if (ref->majorunlock)
|
||||
{
|
||||
V_DrawStretchyFixedPatch(
|
||||
((x + 5)*FRACUNIT) + (32*(FRACUNIT-accordion)/2), (y + 5)*FRACUNIT,
|
||||
FixedDiv(32*accordion, siz),
|
||||
FixedDiv(32 << FRACBITS, siz),
|
||||
0, pat,
|
||||
colormap
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
V_DrawStretchyFixedPatch(
|
||||
((x + 2)*FRACUNIT) + (16*(FRACUNIT-accordion)/2), (y + 2)*FRACUNIT,
|
||||
FixedDiv(16*accordion, siz),
|
||||
FixedDiv(16 << FRACBITS, siz),
|
||||
0, pat,
|
||||
colormap
|
||||
);
|
||||
}
|
||||
|
||||
drawborder:
|
||||
if (!hili)
|
||||
|
|
@ -4611,12 +4763,23 @@ drawborder:
|
|||
return;
|
||||
}
|
||||
|
||||
V_DrawFixedPatch(
|
||||
x*FRACUNIT, y*FRACUNIT,
|
||||
((ref != NULL && ref->majorunlock) ? FRACUNIT*2 : FRACUNIT),
|
||||
0, kp_facehighlight[(challengesmenu.ticker / 4) % 8],
|
||||
NULL
|
||||
);
|
||||
{
|
||||
boolean maj = (ref != NULL && ref->majorunlock);
|
||||
char buffer[9];
|
||||
sprintf(buffer, "UN_RETA1");
|
||||
buffer[6] = maj ? 'B' : 'A';
|
||||
buffer[7] = (skullAnimCounter/5) ? '2' : '1';
|
||||
pat = W_CachePatchName(buffer, PU_CACHE);
|
||||
|
||||
colormap = R_GetTranslationColormap(TC_DEFAULT, cv_playercolor[0].value, GTC_MENUCACHE);
|
||||
|
||||
V_DrawFixedPatch(
|
||||
x*FRACUNIT, y*FRACUNIT,
|
||||
FRACUNIT,
|
||||
0, pat,
|
||||
colormap
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_DrawChallengePreview(INT32 x, INT32 y)
|
||||
|
|
@ -4819,6 +4982,9 @@ static void M_DrawChallengePreview(INT32 x, INT32 y)
|
|||
}
|
||||
}
|
||||
|
||||
#define challengetransparentstrength 8
|
||||
#define challengesgridstep 22
|
||||
|
||||
void M_DrawChallenges(void)
|
||||
{
|
||||
INT32 x = currentMenu->x, explodex, selectx;
|
||||
|
|
@ -4828,8 +4994,25 @@ void M_DrawChallenges(void)
|
|||
INT16 offset;
|
||||
|
||||
{
|
||||
patch_t *bg = W_CachePatchName("BGUNLCK2", PU_CACHE);
|
||||
#define questionslow 4 // slows down the scroll by this factor
|
||||
#define questionloop (questionslow*100) // modulo
|
||||
INT32 questionoffset = (challengesmenu.ticker % questionloop);
|
||||
patch_t *bg = W_CachePatchName("BGUNLCKG", PU_CACHE);
|
||||
patch_t *qm = W_CachePatchName("BGUNLSC", PU_CACHE);
|
||||
|
||||
// Background gradient
|
||||
V_DrawFixedPatch(0, 0, FRACUNIT, 0, bg, NULL);
|
||||
|
||||
// Scrolling question mark overlay
|
||||
V_DrawFixedPatch(
|
||||
-((160 + questionoffset)*FRACUNIT)/questionslow,
|
||||
-(4*FRACUNIT) - (245*(FixedDiv((questionloop - questionoffset)*FRACUNIT, questionloop*FRACUNIT))),
|
||||
FRACUNIT,
|
||||
V_MODULATE,
|
||||
qm,
|
||||
NULL);
|
||||
#undef questionslow
|
||||
#undef questionloop
|
||||
}
|
||||
|
||||
if (gamedata->challengegrid == NULL || challengesmenu.extradata == NULL)
|
||||
|
|
@ -4838,43 +5021,45 @@ void M_DrawChallenges(void)
|
|||
goto challengedesc;
|
||||
}
|
||||
|
||||
x -= 16;
|
||||
V_DrawFadeFill(0, y-2, BASEVIDWIDTH, 90, 0, 31, challengetransparentstrength);
|
||||
|
||||
x -= (challengesgridstep-1);
|
||||
|
||||
x += challengesmenu.offset;
|
||||
|
||||
if (challengegridloops)
|
||||
{
|
||||
if (!challengesmenu.col && challengesmenu.hilix)
|
||||
x -= gamedata->challengegridwidth*16;
|
||||
x -= gamedata->challengegridwidth*challengesgridstep;
|
||||
i = challengesmenu.col + challengesmenu.focusx;
|
||||
explodex = x - (i*16);
|
||||
explodex = x - (i*challengesgridstep);
|
||||
|
||||
while (x < BASEVIDWIDTH-16)
|
||||
while (x < BASEVIDWIDTH-challengesgridstep)
|
||||
{
|
||||
i = (i + 1) % gamedata->challengegridwidth;
|
||||
x += 16;
|
||||
x += challengesgridstep;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gamedata->challengegridwidth & 1)
|
||||
x += 8;
|
||||
x += (challengesgridstep/2);
|
||||
|
||||
i = gamedata->challengegridwidth-1;
|
||||
explodex = x - (i*16)/2;
|
||||
x += (i*16)/2;
|
||||
explodex = x - (i*challengesgridstep)/2;
|
||||
x += (i*challengesgridstep)/2;
|
||||
}
|
||||
|
||||
selectx = explodex + (challengesmenu.hilix*16);
|
||||
selectx = explodex + (challengesmenu.hilix*challengesgridstep);
|
||||
|
||||
while (i >= 0 && x >= -32)
|
||||
while (i >= 0 && x >= -(challengesgridstep*2))
|
||||
{
|
||||
y = currentMenu->y-16;
|
||||
y = currentMenu->y-challengesgridstep;
|
||||
for (j = 0; j < CHALLENGEGRIDHEIGHT; j++)
|
||||
{
|
||||
y += 16;
|
||||
y += challengesgridstep;
|
||||
|
||||
if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j] & CHE_DONTDRAW)
|
||||
if (challengesmenu.extradata[(i * CHALLENGEGRIDHEIGHT) + j].flags & CHE_DONTDRAW)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -4887,7 +5072,7 @@ void M_DrawChallenges(void)
|
|||
M_DrawChallengeTile(i, j, x, y, false);
|
||||
}
|
||||
|
||||
x -= 16;
|
||||
x -= challengesgridstep;
|
||||
i--;
|
||||
if (challengegridloops && i < 0)
|
||||
{
|
||||
|
|
@ -4903,7 +5088,7 @@ void M_DrawChallenges(void)
|
|||
challengesmenu.hilix,
|
||||
challengesmenu.hiliy,
|
||||
selectx,
|
||||
currentMenu->y + (challengesmenu.hiliy*16),
|
||||
currentMenu->y + (challengesmenu.hiliy*challengesgridstep),
|
||||
true);
|
||||
M_DrawCharSelectExplosions(false, explodex, currentMenu->y);
|
||||
|
||||
|
|
@ -4922,6 +5107,12 @@ challengedesc:
|
|||
{
|
||||
y = 120;
|
||||
|
||||
V_DrawScaledPatch(0, y,
|
||||
(10-challengetransparentstrength)<<V_ALPHASHIFT,
|
||||
W_CachePatchName("MENUHINT", PU_CACHE));
|
||||
|
||||
V_DrawFadeFill(0, y+27, BASEVIDWIDTH, BASEVIDHEIGHT - (y+27), 0, 31, challengetransparentstrength);
|
||||
|
||||
if (challengesmenu.currentunlock < MAXUNLOCKABLES)
|
||||
{
|
||||
str = unlockables[challengesmenu.currentunlock].name;
|
||||
|
|
@ -4953,7 +5144,7 @@ challengedesc:
|
|||
&& challengesmenu.currentunlock < MAXUNLOCKABLES
|
||||
&& ((gamedata->unlocked[challengesmenu.currentunlock] == true)
|
||||
|| ((challengesmenu.extradata != NULL)
|
||||
&& (challengesmenu.extradata[i] & CHE_HINT))
|
||||
&& (challengesmenu.extradata[i].flags & CHE_HINT))
|
||||
)
|
||||
)
|
||||
{
|
||||
|
|
@ -4961,6 +5152,9 @@ challengedesc:
|
|||
}
|
||||
}
|
||||
|
||||
#undef challengetransparentstrength
|
||||
#undef challengesgridstep
|
||||
|
||||
// Statistics menu
|
||||
|
||||
#define STATSSTEP 10
|
||||
|
|
|
|||
|
|
@ -306,6 +306,8 @@ static int player_get(lua_State *L)
|
|||
lua_pushinteger(L, plr->tripwirePass);
|
||||
else if (fastcmp(field,"tripwireLeniency"))
|
||||
lua_pushinteger(L, plr->tripwireLeniency);
|
||||
else if (fastcmp(field,"tripwireReboundDelay"))
|
||||
lua_pushinteger(L, plr->tripwireReboundDelay);
|
||||
/*
|
||||
else if (fastcmp(field,"itemroulette"))
|
||||
lua_pushinteger(L, plr->itemroulette);
|
||||
|
|
@ -684,6 +686,8 @@ static int player_set(lua_State *L)
|
|||
plr->tripwirePass = luaL_checkinteger(L, 3);
|
||||
else if (fastcmp(field,"tripwireLeniency"))
|
||||
plr->tripwireLeniency = luaL_checkinteger(L, 3);
|
||||
else if (fastcmp(field,"tripwireReboundDelay"))
|
||||
plr->tripwireReboundDelay = luaL_checkinteger(L, 3);
|
||||
/*
|
||||
else if (fastcmp(field,"itemroulette"))
|
||||
plr->itemroulette = luaL_checkinteger(L, 3);
|
||||
|
|
|
|||
|
|
@ -205,7 +205,7 @@ int LUA_PushGlobals(lua_State *L, const char *word)
|
|||
lua_pushinteger(L, timelimitintics);
|
||||
return 1;
|
||||
} else if (fastcmp(word,"pointlimit")) {
|
||||
lua_pushinteger(L, cv_pointlimit.value);
|
||||
lua_pushinteger(L, g_pointlimit);
|
||||
return 1;
|
||||
// begin map vars
|
||||
} else if (fastcmp(word,"titlemap")) {
|
||||
|
|
|
|||
252
src/m_avrecorder.cpp
Normal file
252
src/m_avrecorder.cpp
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
#include "cxxutil.hpp"
|
||||
#include "m_avrecorder.hpp"
|
||||
#include "media/options.hpp"
|
||||
|
||||
#include "command.h"
|
||||
#include "i_sound.h"
|
||||
#include "m_avrecorder.h"
|
||||
#include "m_fixed.h"
|
||||
#include "screen.h" // vid global
|
||||
#include "st_stuff.h" // st_palette
|
||||
#include "v_video.h" // pLocalPalette
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
namespace Res
|
||||
{
|
||||
|
||||
// Using an unscoped enum here so it can implicitly cast to
|
||||
// int (in CV_PossibleValue_t). Wrap this in a namespace so
|
||||
// access is still scoped. E.g. Res::kGame
|
||||
|
||||
enum : int32_t
|
||||
{
|
||||
kGame, // user chosen resolution, vid.width
|
||||
kBase, // smallest version maintaining aspect ratio, vid.width / vid.dupx
|
||||
kBase2x,
|
||||
kBase4x,
|
||||
kWindow, // window size (monitor in fullscreen), vid.realwidth
|
||||
kCustom, // movie_custom_resolution
|
||||
};
|
||||
|
||||
}; // namespace Res
|
||||
|
||||
CV_PossibleValue_t movie_resolution_cons_t[] = {
|
||||
{Res::kGame, "Native"},
|
||||
{Res::kBase, "Small"},
|
||||
{Res::kBase2x, "Medium"},
|
||||
{Res::kBase4x, "Large"},
|
||||
{Res::kWindow, "Window"},
|
||||
{Res::kCustom, "Custom"},
|
||||
{0, NULL}};
|
||||
|
||||
CV_PossibleValue_t movie_limit_cons_t[] = {{1, "MIN"}, {INT32_MAX, "MAX"}, {0, "Unlimited"}, {0, NULL}};
|
||||
|
||||
}; // namespace
|
||||
|
||||
consvar_t cv_movie_resolution = CVAR_INIT("movie_resolution", "Medium", CV_SAVE, movie_resolution_cons_t, NULL);
|
||||
consvar_t cv_movie_custom_resolution = CVAR_INIT("movie_custom_resolution", "640x400", CV_SAVE, NULL, NULL);
|
||||
|
||||
consvar_t cv_movie_fps = CVAR_INIT("movie_fps", "60", CV_SAVE, CV_Natural, NULL);
|
||||
consvar_t cv_movie_showfps = CVAR_INIT("movie_showfps", "Yes", CV_SAVE, CV_YesNo, NULL);
|
||||
|
||||
consvar_t cv_movie_sound = CVAR_INIT("movie_sound", "On", CV_SAVE, CV_OnOff, NULL);
|
||||
|
||||
consvar_t cv_movie_duration = CVAR_INIT("movie_duration", "Unlimited", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL);
|
||||
consvar_t cv_movie_size = CVAR_INIT("movie_size", "8.0", CV_SAVE | CV_FLOAT, movie_limit_cons_t, NULL);
|
||||
|
||||
std::shared_ptr<AVRecorder> g_av_recorder;
|
||||
|
||||
void M_AVRecorder_AddCommands(void)
|
||||
{
|
||||
CV_RegisterVar(&cv_movie_custom_resolution);
|
||||
CV_RegisterVar(&cv_movie_duration);
|
||||
CV_RegisterVar(&cv_movie_fps);
|
||||
CV_RegisterVar(&cv_movie_resolution);
|
||||
CV_RegisterVar(&cv_movie_showfps);
|
||||
CV_RegisterVar(&cv_movie_size);
|
||||
CV_RegisterVar(&cv_movie_sound);
|
||||
|
||||
srb2::media::Options::register_all();
|
||||
}
|
||||
|
||||
static AVRecorder::Config configure()
|
||||
{
|
||||
AVRecorder::Config cfg {};
|
||||
|
||||
if (cv_movie_duration.value > 0)
|
||||
{
|
||||
cfg.max_duration = std::chrono::duration<float>(FixedToFloat(cv_movie_duration.value));
|
||||
}
|
||||
|
||||
if (cv_movie_size.value > 0)
|
||||
{
|
||||
cfg.max_size = FixedToFloat(cv_movie_size.value) * 1024 * 1024;
|
||||
}
|
||||
|
||||
if (sound_started && cv_movie_sound.value)
|
||||
{
|
||||
cfg.audio = {
|
||||
.sample_rate = 44100,
|
||||
};
|
||||
}
|
||||
|
||||
cfg.video = {
|
||||
.frame_rate = cv_movie_fps.value,
|
||||
};
|
||||
|
||||
AVRecorder::Config::Video& v = *cfg.video;
|
||||
|
||||
auto basex = [&v](int scale)
|
||||
{
|
||||
v.width = vid.width / vid.dupx * scale;
|
||||
v.height = vid.height / vid.dupy * scale;
|
||||
};
|
||||
|
||||
switch (cv_movie_resolution.value)
|
||||
{
|
||||
case Res::kGame:
|
||||
v.width = vid.width;
|
||||
v.height = vid.height;
|
||||
break;
|
||||
|
||||
case Res::kBase:
|
||||
basex(1);
|
||||
break;
|
||||
|
||||
case Res::kBase2x:
|
||||
basex(2);
|
||||
break;
|
||||
|
||||
case Res::kBase4x:
|
||||
basex(4);
|
||||
break;
|
||||
|
||||
case Res::kWindow:
|
||||
v.width = vid.realwidth;
|
||||
v.height = vid.realheight;
|
||||
break;
|
||||
|
||||
case Res::kCustom:
|
||||
if (sscanf(cv_movie_custom_resolution.string, "%dx%d", &v.width, &v.height) != 2)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format(
|
||||
"Bad movie_custom_resolution '{}', should be <width>x<height> (e.g. 640x400)",
|
||||
cv_movie_custom_resolution.string
|
||||
));
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
SRB2_ASSERT(false);
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
boolean M_AVRecorder_Open(const char* filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
AVRecorder::Config cfg = configure();
|
||||
|
||||
cfg.file_name = filename;
|
||||
|
||||
g_av_recorder = std::make_shared<AVRecorder>(cfg);
|
||||
|
||||
I_UpdateAudioRecorder();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, "Exception starting video recorder: %s\n", ex.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void M_AVRecorder_Close(void)
|
||||
{
|
||||
g_av_recorder.reset();
|
||||
|
||||
I_UpdateAudioRecorder();
|
||||
}
|
||||
|
||||
const char* M_AVRecorder_GetFileExtension(void)
|
||||
{
|
||||
return AVRecorder::file_extension();
|
||||
}
|
||||
|
||||
const char* M_AVRecorder_GetCurrentFormat(void)
|
||||
{
|
||||
SRB2_ASSERT(g_av_recorder != nullptr);
|
||||
|
||||
return g_av_recorder->format_name();
|
||||
}
|
||||
|
||||
void M_AVRecorder_PrintCurrentConfiguration(void)
|
||||
{
|
||||
SRB2_ASSERT(g_av_recorder != nullptr);
|
||||
|
||||
g_av_recorder->print_configuration();
|
||||
}
|
||||
|
||||
boolean M_AVRecorder_IsExpired(void)
|
||||
{
|
||||
SRB2_ASSERT(g_av_recorder != nullptr);
|
||||
|
||||
return g_av_recorder->invalid();
|
||||
}
|
||||
|
||||
void M_AVRecorder_DrawFrameRate(void)
|
||||
{
|
||||
if (!cv_movie_showfps.value || !g_av_recorder)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
g_av_recorder->draw_statistics();
|
||||
}
|
||||
|
||||
// TODO: remove once hwr2 twodee is finished
|
||||
void M_AVRecorder_CopySoftwareScreen(void)
|
||||
{
|
||||
SRB2_ASSERT(g_av_recorder != nullptr);
|
||||
|
||||
auto frame = g_av_recorder->new_indexed_video_frame(vid.width, vid.height);
|
||||
|
||||
if (!frame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tcb::span<RGBA_t> pal(&pLocalPalette[std::max(st_palette, 0) * 256], 256);
|
||||
tcb::span<uint8_t> scr(screens[0], vid.width * vid.height);
|
||||
|
||||
std::copy(pal.begin(), pal.end(), frame->palette.begin());
|
||||
std::copy(scr.begin(), scr.end(), frame->screen.begin());
|
||||
|
||||
g_av_recorder->push_indexed_video_frame(std::move(frame));
|
||||
}
|
||||
53
src/m_avrecorder.h
Normal file
53
src/m_avrecorder.h
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef M_AVRECORDER_H
|
||||
#define M_AVRECORDER_H
|
||||
|
||||
#include "typedef.h" // consvar_t
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void M_AVRecorder_AddCommands(void);
|
||||
|
||||
const char *M_AVRecorder_GetFileExtension(void);
|
||||
|
||||
// True if successully opened.
|
||||
boolean M_AVRecorder_Open(const char *filename);
|
||||
|
||||
void M_AVRecorder_Close(void);
|
||||
|
||||
// Check whether AVRecorder is still valid. Call M_AVRecorder_Close if expired.
|
||||
boolean M_AVRecorder_IsExpired(void);
|
||||
|
||||
const char *M_AVRecorder_GetCurrentFormat(void);
|
||||
|
||||
void M_AVRecorder_PrintCurrentConfiguration(void);
|
||||
|
||||
void M_AVRecorder_DrawFrameRate(void);
|
||||
|
||||
// TODO: remove once hwr2 twodee is finished
|
||||
void M_AVRecorder_CopySoftwareScreen(void);
|
||||
|
||||
extern consvar_t
|
||||
cv_movie_custom_resolution,
|
||||
cv_movie_duration,
|
||||
cv_movie_fps,
|
||||
cv_movie_resolution,
|
||||
cv_movie_showfps,
|
||||
cv_movie_size,
|
||||
cv_movie_sound;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}; // extern "C"
|
||||
#endif
|
||||
|
||||
#endif/*M_AVRECORDER_H*/
|
||||
19
src/m_avrecorder.hpp
Normal file
19
src/m_avrecorder.hpp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __M_AVRECORDER_HPP__
|
||||
#define __M_AVRECORDER_HPP__
|
||||
|
||||
#include <memory> // shared_ptr
|
||||
|
||||
#include "media/avrecorder.hpp"
|
||||
|
||||
extern std::shared_ptr<srb2::media::AVRecorder> g_av_recorder;
|
||||
|
||||
#endif // __M_AVRECORDER_HPP__
|
||||
57
src/m_cond.c
57
src/m_cond.c
|
|
@ -1,5 +1,6 @@
|
|||
// SONIC ROBO BLAST 2
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2022-2023 by Vivian "toaster" Grannell.
|
||||
// Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
|
||||
// Copyright (C) 2012-2020 by Sonic Team Junior.
|
||||
//
|
||||
|
|
@ -259,28 +260,34 @@ quickcheckagain:
|
|||
}
|
||||
}
|
||||
|
||||
UINT8 *M_ChallengeGridExtraData(void)
|
||||
void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata)
|
||||
{
|
||||
UINT8 i, j, num, id, tempid, work;
|
||||
UINT8 *extradata;
|
||||
boolean idchange;
|
||||
|
||||
if (!gamedata->challengegrid)
|
||||
if (gamedata->challengegrid == NULL)
|
||||
{
|
||||
return NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
extradata = Z_Malloc(
|
||||
(gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(UINT8)),
|
||||
PU_STATIC, NULL);
|
||||
|
||||
if (!extradata)
|
||||
if (extradata == NULL)
|
||||
{
|
||||
I_Error("M_ChallengeGridExtraData: was not able to allocate extradata");
|
||||
return;
|
||||
}
|
||||
|
||||
//CONS_Printf(" --- \n");
|
||||
|
||||
// Pre-wipe flags.
|
||||
for (i = 0; i < gamedata->challengegridwidth; i++)
|
||||
{
|
||||
for (j = 0; j < CHALLENGEGRIDHEIGHT; j++)
|
||||
{
|
||||
id = (i * CHALLENGEGRIDHEIGHT) + j;
|
||||
extradata[id].flags = CHE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate extra data.
|
||||
for (i = 0; i < gamedata->challengegridwidth; i++)
|
||||
{
|
||||
for (j = 0; j < CHALLENGEGRIDHEIGHT; j++)
|
||||
|
|
@ -289,8 +296,6 @@ UINT8 *M_ChallengeGridExtraData(void)
|
|||
num = gamedata->challengegrid[id];
|
||||
idchange = false;
|
||||
|
||||
extradata[id] = CHE_NONE;
|
||||
|
||||
// Empty spots in the grid are always unconnected.
|
||||
if (num >= MAXUNLOCKABLES)
|
||||
{
|
||||
|
|
@ -304,13 +309,13 @@ UINT8 *M_ChallengeGridExtraData(void)
|
|||
work = gamedata->challengegrid[tempid];
|
||||
if (work == num)
|
||||
{
|
||||
extradata[id] = CHE_CONNECTEDUP;
|
||||
extradata[id].flags = CHE_CONNECTEDUP;
|
||||
|
||||
// Get the id to write extra hint data to.
|
||||
// This check is safe because extradata's order of population
|
||||
if (extradata[tempid] & CHE_CONNECTEDLEFT)
|
||||
if (extradata[tempid].flags & CHE_CONNECTEDLEFT)
|
||||
{
|
||||
extradata[id] |= CHE_CONNECTEDLEFT;
|
||||
extradata[id].flags |= CHE_CONNECTEDLEFT;
|
||||
//CONS_Printf(" %d - %d above %d is invalid, check to left\n", num, tempid, id);
|
||||
if (i > 0)
|
||||
{
|
||||
|
|
@ -327,14 +332,14 @@ UINT8 *M_ChallengeGridExtraData(void)
|
|||
id = tempid;
|
||||
idchange = true;
|
||||
|
||||
if (extradata[id] == CHE_HINT)
|
||||
if (extradata[id].flags == CHE_HINT)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (work < MAXUNLOCKABLES && gamedata->unlocked[work])
|
||||
{
|
||||
extradata[id] = CHE_HINT;
|
||||
extradata[id].flags = CHE_HINT;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -356,11 +361,11 @@ UINT8 *M_ChallengeGridExtraData(void)
|
|||
{
|
||||
//CONS_Printf(" %d - %d to left of %d is valid\n", work, tempid, id);
|
||||
// If we haven't already updated our id, it's the one to our left.
|
||||
if (extradata[id] == CHE_HINT)
|
||||
if (extradata[id].flags == CHE_HINT)
|
||||
{
|
||||
extradata[tempid] = CHE_HINT;
|
||||
extradata[tempid].flags = CHE_HINT;
|
||||
}
|
||||
extradata[id] = CHE_CONNECTEDLEFT;
|
||||
extradata[id].flags = CHE_CONNECTEDLEFT;
|
||||
id = tempid;
|
||||
}
|
||||
/*else
|
||||
|
|
@ -368,13 +373,13 @@ UINT8 *M_ChallengeGridExtraData(void)
|
|||
}
|
||||
else if (work < MAXUNLOCKABLES && gamedata->unlocked[work])
|
||||
{
|
||||
extradata[id] = CHE_HINT;
|
||||
extradata[id].flags = CHE_HINT;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Since we're not modifying id past this point, the conditions become much simpler.
|
||||
if (extradata[id] == CHE_HINT)
|
||||
if ((extradata[id].flags & (CHE_HINT|CHE_DONTDRAW)) == CHE_HINT)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -391,7 +396,7 @@ UINT8 *M_ChallengeGridExtraData(void)
|
|||
}
|
||||
else if (work < MAXUNLOCKABLES && gamedata->unlocked[work])
|
||||
{
|
||||
extradata[id] = CHE_HINT;
|
||||
extradata[id].flags = CHE_HINT;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -414,14 +419,12 @@ UINT8 *M_ChallengeGridExtraData(void)
|
|||
}
|
||||
else if (work < MAXUNLOCKABLES && gamedata->unlocked[work])
|
||||
{
|
||||
extradata[id] = CHE_HINT;
|
||||
extradata[id].flags = CHE_HINT;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extradata;
|
||||
}
|
||||
|
||||
void M_AddRawCondition(UINT8 set, UINT8 id, conditiontype_t c, INT32 r, INT16 x1, INT16 x2)
|
||||
|
|
@ -909,7 +912,7 @@ boolean M_UpdateUnlockablesAndExtraEmblems(boolean loud)
|
|||
{
|
||||
if (loud)
|
||||
{
|
||||
S_StartSound(NULL, sfx_ncitem);
|
||||
S_StartSound(NULL, sfx_achiev);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
14
src/m_cond.h
14
src/m_cond.h
|
|
@ -1,5 +1,6 @@
|
|||
// SONIC ROBO BLAST 2
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2022-2023 by Vivian "toaster" Grannell.
|
||||
// Copyright (C) 2012-2016 by Matthew "Kaito Sinclaire" Walsh.
|
||||
// Copyright (C) 2012-2020 by Sonic Team Junior.
|
||||
//
|
||||
|
|
@ -138,7 +139,7 @@ typedef enum
|
|||
#define MAXEMBLEMS 512
|
||||
#define MAXUNLOCKABLES MAXCONDITIONSETS
|
||||
|
||||
#define CHALLENGEGRIDHEIGHT 5
|
||||
#define CHALLENGEGRIDHEIGHT 4
|
||||
#ifdef DEVELOP
|
||||
#define CHALLENGEGRIDLOOPWIDTH 3
|
||||
#else
|
||||
|
|
@ -192,12 +193,21 @@ void M_NewGameDataStruct(void);
|
|||
|
||||
// Challenges menu stuff
|
||||
void M_PopulateChallengeGrid(void);
|
||||
UINT8 *M_ChallengeGridExtraData(void);
|
||||
|
||||
struct challengegridextradata_t
|
||||
{
|
||||
UINT8 flags;
|
||||
UINT8 flip;
|
||||
};
|
||||
|
||||
void M_UpdateChallengeGridExtraData(challengegridextradata_t *extradata);
|
||||
|
||||
#define CHE_NONE 0
|
||||
#define CHE_HINT 1
|
||||
#define CHE_CONNECTEDLEFT (1<<1)
|
||||
#define CHE_CONNECTEDUP (1<<2)
|
||||
#define CHE_DONTDRAW (CHE_CONNECTEDLEFT|CHE_CONNECTEDUP)
|
||||
|
||||
char *M_BuildConditionSetString(UINT8 unlockid);
|
||||
#define DESCRIPTIONWIDTH 170
|
||||
|
||||
|
|
|
|||
51
src/m_misc.c
51
src/m_misc.c
|
|
@ -44,6 +44,7 @@
|
|||
#include "command.h" // cv_execversion
|
||||
|
||||
#include "m_anigif.h"
|
||||
#include "m_avrecorder.h"
|
||||
|
||||
// So that the screenshot menu auto-updates...
|
||||
#include "k_menu.h"
|
||||
|
|
@ -113,8 +114,8 @@ consvar_t cv_screenshot_folder = CVAR_INIT ("screenshot_folder", "", CV_SAVE, NU
|
|||
|
||||
consvar_t cv_screenshot_colorprofile = CVAR_INIT ("screenshot_colorprofile", "Yes", CV_SAVE, CV_YesNo, NULL);
|
||||
|
||||
static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {0, NULL}};
|
||||
consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "GIF", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange);
|
||||
static CV_PossibleValue_t moviemode_cons_t[] = {{MM_GIF, "GIF"}, {MM_APNG, "aPNG"}, {MM_SCREENSHOT, "Screenshots"}, {MM_AVRECORDER, "WebM"}, {0, NULL}};
|
||||
consvar_t cv_moviemode = CVAR_INIT ("moviemode_mode", "WebM", CV_SAVE|CV_CALL, moviemode_cons_t, Moviemode_mode_Onchange);
|
||||
|
||||
consvar_t cv_movie_option = CVAR_INIT ("movie_option", "Default", CV_SAVE|CV_CALL, screenshot_cons_t, Moviemode_option_Onchange);
|
||||
consvar_t cv_movie_folder = CVAR_INIT ("movie_folder", "", CV_SAVE, NULL, NULL);
|
||||
|
|
@ -1295,6 +1296,25 @@ static inline moviemode_t M_StartMovieGIF(const char *pathname)
|
|||
}
|
||||
#endif
|
||||
|
||||
static inline moviemode_t M_StartMovieAVRecorder(const char *pathname)
|
||||
{
|
||||
const char *ext = M_AVRecorder_GetFileExtension();
|
||||
const char *freename;
|
||||
|
||||
if (!(freename = Newsnapshotfile(pathname, ext)))
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, "Couldn't create %s file: no slots open in %s\n", ext, pathname);
|
||||
return MM_OFF;
|
||||
}
|
||||
|
||||
if (!M_AVRecorder_Open(va(pandf,pathname,freename)))
|
||||
{
|
||||
return MM_OFF;
|
||||
}
|
||||
|
||||
return MM_AVRECORDER;
|
||||
}
|
||||
|
||||
void M_StartMovie(void)
|
||||
{
|
||||
#if NUMSCREENS > 2
|
||||
|
|
@ -1332,6 +1352,9 @@ void M_StartMovie(void)
|
|||
case MM_SCREENSHOT:
|
||||
moviemode = MM_SCREENSHOT;
|
||||
break;
|
||||
case MM_AVRECORDER:
|
||||
moviemode = M_StartMovieAVRecorder(pathname);
|
||||
break;
|
||||
default: //???
|
||||
return;
|
||||
}
|
||||
|
|
@ -1342,6 +1365,11 @@ void M_StartMovie(void)
|
|||
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "GIF");
|
||||
else if (moviemode == MM_SCREENSHOT)
|
||||
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), "screenshots");
|
||||
else if (moviemode == MM_AVRECORDER)
|
||||
{
|
||||
CONS_Printf(M_GetText("Movie mode enabled (%s).\n"), M_AVRecorder_GetCurrentFormat());
|
||||
M_AVRecorder_PrintCurrentConfiguration();
|
||||
}
|
||||
|
||||
//singletics = (moviemode != MM_OFF);
|
||||
#endif
|
||||
|
|
@ -1353,6 +1381,22 @@ void M_SaveFrame(void)
|
|||
// paranoia: should be unnecessary without singletics
|
||||
static tic_t oldtic = 0;
|
||||
|
||||
if (moviemode == MM_AVRECORDER)
|
||||
{
|
||||
// TODO: replace once hwr2 twodee is finished
|
||||
if (rendermode == render_soft)
|
||||
{
|
||||
M_AVRecorder_CopySoftwareScreen();
|
||||
}
|
||||
|
||||
if (M_AVRecorder_IsExpired())
|
||||
{
|
||||
M_StopMovie();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// skip interpolated frames for other modes
|
||||
if (oldtic == I_GetTime())
|
||||
return;
|
||||
else
|
||||
|
|
@ -1440,6 +1484,9 @@ void M_StopMovie(void)
|
|||
#endif
|
||||
case MM_SCREENSHOT:
|
||||
break;
|
||||
case MM_AVRECORDER:
|
||||
M_AVRecorder_Close();
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,8 @@ typedef enum {
|
|||
MM_OFF = 0,
|
||||
MM_APNG,
|
||||
MM_GIF,
|
||||
MM_SCREENSHOT
|
||||
MM_SCREENSHOT,
|
||||
MM_AVRECORDER,
|
||||
} moviemode_t;
|
||||
extern moviemode_t moviemode;
|
||||
|
||||
|
|
|
|||
34
src/media/CMakeLists.txt
Normal file
34
src/media/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
target_sources(SRB2SDL2 PRIVATE
|
||||
audio_encoder.hpp
|
||||
avrecorder.cpp
|
||||
avrecorder.hpp
|
||||
avrecorder_feedback.cpp
|
||||
avrecorder_impl.hpp
|
||||
avrecorder_indexed.cpp
|
||||
avrecorder_queue.cpp
|
||||
cfile.cpp
|
||||
cfile.hpp
|
||||
container.hpp
|
||||
encoder.hpp
|
||||
options.cpp
|
||||
options.hpp
|
||||
options_values.cpp
|
||||
video_encoder.hpp
|
||||
video_frame.hpp
|
||||
vorbis.cpp
|
||||
vorbis.hpp
|
||||
vorbis_error.hpp
|
||||
vp8.cpp
|
||||
vp8.hpp
|
||||
vpx_error.hpp
|
||||
webm.hpp
|
||||
webm_encoder.hpp
|
||||
webm_container.cpp
|
||||
webm_container.hpp
|
||||
webm_vorbis.hpp
|
||||
webm_vorbis_lace.cpp
|
||||
webm_vp8.hpp
|
||||
webm_writer.hpp
|
||||
yuv420p.cpp
|
||||
yuv420p.hpp
|
||||
)
|
||||
39
src/media/audio_encoder.hpp
Normal file
39
src/media/audio_encoder.hpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_AUDIO_ENCODER_HPP__
|
||||
#define __SRB2_MEDIA_AUDIO_ENCODER_HPP__
|
||||
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
#include "encoder.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class AudioEncoder : virtual public MediaEncoder
|
||||
{
|
||||
public:
|
||||
using sample_buffer_t = tcb::span<const float>;
|
||||
|
||||
struct Config
|
||||
{
|
||||
int channels;
|
||||
int sample_rate;
|
||||
};
|
||||
|
||||
virtual void encode(sample_buffer_t samples) = 0;
|
||||
|
||||
virtual int channels() const = 0;
|
||||
virtual int sample_rate() const = 0;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_AUDIO_ENCODER_HPP__
|
||||
214
src/media/avrecorder.cpp
Normal file
214
src/media/avrecorder.cpp
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "../i_time.h"
|
||||
#include "../m_fixed.h"
|
||||
#include "avrecorder_impl.hpp"
|
||||
#include "webm_container.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
using Impl = AVRecorder::Impl;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr auto kBufferMethod = VideoFrame::BufferMethod::kEncoderAllocatedRGBA8888;
|
||||
|
||||
}; // namespace
|
||||
|
||||
Impl::Impl(Config cfg) :
|
||||
max_size_(cfg.max_size),
|
||||
max_duration_(cfg.max_duration),
|
||||
|
||||
container_(std::make_unique<WebmContainer>(MediaContainer::Config {
|
||||
cfg.file_name,
|
||||
[this](const MediaContainer& container) { container_dtor_handler(container); },
|
||||
})),
|
||||
|
||||
audio_encoder_(make_audio_encoder(cfg)),
|
||||
video_encoder_(make_video_encoder(cfg)),
|
||||
|
||||
epoch_(I_GetTime()),
|
||||
|
||||
thread_([this] { worker(); })
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioEncoder> Impl::make_audio_encoder(const Config cfg) const
|
||||
{
|
||||
if (!cfg.audio)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Config::Audio& a = *cfg.audio;
|
||||
|
||||
return container_->make_audio_encoder({2, a.sample_rate});
|
||||
}
|
||||
|
||||
std::unique_ptr<VideoEncoder> Impl::make_video_encoder(const Config cfg) const
|
||||
{
|
||||
if (!cfg.video)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Config::Video& v = *cfg.video;
|
||||
|
||||
return container_->make_video_encoder({v.width, v.height, v.frame_rate, kBufferMethod});
|
||||
}
|
||||
|
||||
Impl::~Impl()
|
||||
{
|
||||
valid_ = false;
|
||||
wake_up_worker();
|
||||
thread_.join();
|
||||
|
||||
try
|
||||
{
|
||||
// Finally flush encoders, unless queues were finished
|
||||
// already due to time or size constraints.
|
||||
|
||||
if (!audio_queue_.finished())
|
||||
{
|
||||
audio_encoder_->flush();
|
||||
}
|
||||
|
||||
if (!video_queue_.finished())
|
||||
{
|
||||
video_encoder_->flush();
|
||||
}
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, "AVRecorder::Impl::~Impl: %s\n", ex.what());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> Impl::advance_video_pts()
|
||||
{
|
||||
auto _ = queue_guard();
|
||||
|
||||
// Don't let this queue grow out of hand. It's normal
|
||||
// for encoding time to vary by a small margin and
|
||||
// spend longer than one frame rate on a single
|
||||
// frame. It should normalize though.
|
||||
|
||||
if (video_queue_.vec_.size() >= 3)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
SRB2_ASSERT(video_encoder_ != nullptr);
|
||||
|
||||
const float tic_pts = video_encoder_->frame_rate() / static_cast<float>(TICRATE);
|
||||
const int pts = ((I_GetTime() - epoch_) + FixedToFloat(g_time.timefrac)) * tic_pts;
|
||||
|
||||
if (!video_queue_.advance(pts, 1))
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return pts;
|
||||
}
|
||||
|
||||
void Impl::worker()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
QueueState qs;
|
||||
|
||||
try
|
||||
{
|
||||
while ((qs = encode_queues()) == QueueState::kFlushed)
|
||||
;
|
||||
}
|
||||
catch (const std::exception& ex)
|
||||
{
|
||||
CONS_Alert(CONS_ERROR, "AVRecorder::Impl::worker: %s\n", ex.what());
|
||||
break;
|
||||
}
|
||||
|
||||
if (qs != QueueState::kFinished && valid_)
|
||||
{
|
||||
std::unique_lock lock(queue_mutex_);
|
||||
|
||||
queue_cond_.wait(lock);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Breaking out of the loop ensures invalidation!
|
||||
valid_ = false;
|
||||
}
|
||||
|
||||
const char* AVRecorder::file_extension()
|
||||
{
|
||||
return "webm";
|
||||
}
|
||||
|
||||
AVRecorder::AVRecorder(const Config config) : impl_(std::make_unique<Impl>(config))
|
||||
{
|
||||
}
|
||||
|
||||
AVRecorder::~AVRecorder()
|
||||
{
|
||||
// impl_ is destroyed in a background thread so it doesn't
|
||||
// block the thread AVRecorder was destroyed in.
|
||||
//
|
||||
// TODO: Save into a thread pool instead of detaching so
|
||||
// the thread could be joined at program exit and
|
||||
// not possibly terminate before fully destroyed?
|
||||
|
||||
std::thread([_ = std::move(impl_)] {}).detach();
|
||||
}
|
||||
|
||||
const char* AVRecorder::format_name() const
|
||||
{
|
||||
return impl_->container_->name();
|
||||
}
|
||||
|
||||
void AVRecorder::push_audio_samples(audio_buffer_t buffer)
|
||||
{
|
||||
const auto _ = impl_->queue_guard();
|
||||
|
||||
auto& q = impl_->audio_queue_;
|
||||
|
||||
if (!q.advance(q.pts(), buffer.size()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using T = const float;
|
||||
tcb::span<T> p(reinterpret_cast<T*>(buffer.data()), buffer.size() * 2); // 2 channels
|
||||
|
||||
std::copy(p.begin(), p.end(), std::back_inserter(q.vec_));
|
||||
|
||||
impl_->wake_up_worker();
|
||||
}
|
||||
|
||||
bool AVRecorder::invalid() const
|
||||
{
|
||||
return !impl_->valid_;
|
||||
}
|
||||
108
src/media/avrecorder.hpp
Normal file
108
src/media/avrecorder.hpp
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_AVRECORDER_HPP__
|
||||
#define __SRB2_MEDIA_AVRECORDER_HPP__
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
#include "../audio/sample.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class AVRecorder
|
||||
{
|
||||
public:
|
||||
using audio_sample_t = srb2::audio::Sample<2>;
|
||||
using audio_buffer_t = tcb::span<const audio_sample_t>;
|
||||
|
||||
class Impl;
|
||||
|
||||
struct Config
|
||||
{
|
||||
struct Audio
|
||||
{
|
||||
int sample_rate;
|
||||
};
|
||||
|
||||
struct Video
|
||||
{
|
||||
int width;
|
||||
int height;
|
||||
int frame_rate;
|
||||
};
|
||||
|
||||
std::string file_name;
|
||||
|
||||
std::optional<std::size_t> max_size; // file size limit
|
||||
std::optional<std::chrono::duration<float>> max_duration;
|
||||
|
||||
std::optional<Audio> audio;
|
||||
std::optional<Video> video;
|
||||
};
|
||||
|
||||
// TODO: remove once hwr2 twodee is finished
|
||||
struct IndexedVideoFrame
|
||||
{
|
||||
using instance_t = std::unique_ptr<IndexedVideoFrame>;
|
||||
|
||||
std::array<RGBA_t, 256> palette;
|
||||
std::vector<uint8_t> screen;
|
||||
uint32_t width, height;
|
||||
int pts;
|
||||
|
||||
IndexedVideoFrame(uint32_t width_, uint32_t height_, int pts_) :
|
||||
screen(width_ * height_), width(width_), height(height_), pts(pts_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// Returns the canonical file extension minus the dot.
|
||||
// E.g. "webm" (not ".webm").
|
||||
static const char* file_extension();
|
||||
|
||||
AVRecorder(Config config);
|
||||
~AVRecorder();
|
||||
|
||||
void print_configuration() const;
|
||||
void draw_statistics() const;
|
||||
|
||||
void push_audio_samples(audio_buffer_t buffer);
|
||||
|
||||
// May return nullptr in case called between units of
|
||||
// Config::frame_rate
|
||||
IndexedVideoFrame::instance_t new_indexed_video_frame(uint32_t width, uint32_t height);
|
||||
|
||||
void push_indexed_video_frame(IndexedVideoFrame::instance_t frame);
|
||||
|
||||
// Proper name of the container format.
|
||||
const char* format_name() const;
|
||||
|
||||
// True if this instance has terminated. Continuing to use
|
||||
// this interface is useless and the object should be
|
||||
// destructed immediately.
|
||||
bool invalid() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_AVRECORDER_HPP__
|
||||
149
src/media/avrecorder_feedback.cpp
Normal file
149
src/media/avrecorder_feedback.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <filesystem>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "avrecorder_impl.hpp"
|
||||
|
||||
#include "../v_video.h"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
using Impl = AVRecorder::Impl;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr float kMb = 1024.f * 1024.f;
|
||||
|
||||
}; // namespace
|
||||
|
||||
void Impl::container_dtor_handler(const MediaContainer& container) const
|
||||
{
|
||||
// Note that because this method is called from
|
||||
// container_'s destructor, any member variables declared
|
||||
// after Impl::container_ should not be accessed by now
|
||||
// (since they would have already destructed).
|
||||
|
||||
if (max_size_ && container.size() > *max_size_)
|
||||
{
|
||||
const std::string line = fmt::format(
|
||||
"Video size has exceeded limit {} > {} ({}%)."
|
||||
" This should not happen, please report this bug.\n",
|
||||
container.size(),
|
||||
*max_size_,
|
||||
100.f * (*max_size_ / static_cast<float>(container.size()))
|
||||
);
|
||||
|
||||
CONS_Alert(CONS_WARNING, "%s\n", line.c_str());
|
||||
}
|
||||
|
||||
std::ostringstream msg;
|
||||
|
||||
msg << "Video saved: " << std::filesystem::path(container.file_name()).filename().string()
|
||||
<< fmt::format(" ({:.2f}", container.size() / kMb);
|
||||
|
||||
if (max_size_)
|
||||
{
|
||||
msg << fmt::format("/{:.2f}", *max_size_ / kMb);
|
||||
}
|
||||
|
||||
msg << fmt::format(" MB, {:.1f}", container.duration().count());
|
||||
|
||||
if (max_duration_config_)
|
||||
{
|
||||
msg << fmt::format("/{:.1f}", max_duration_config_->count());
|
||||
}
|
||||
|
||||
msg << " seconds)";
|
||||
|
||||
CONS_Printf("%s\n", msg.str().c_str());
|
||||
}
|
||||
|
||||
void AVRecorder::print_configuration() const
|
||||
{
|
||||
if (impl_->audio_encoder_)
|
||||
{
|
||||
const auto& a = *impl_->audio_encoder_;
|
||||
|
||||
CONS_Printf("Audio: %s %dch %d Hz\n", a.name(), a.channels(), a.sample_rate());
|
||||
}
|
||||
|
||||
if (impl_->video_encoder_)
|
||||
{
|
||||
const auto& v = *impl_->video_encoder_;
|
||||
|
||||
CONS_Printf(
|
||||
"Video: %s %dx%d %d fps %d threads\n",
|
||||
v.name(),
|
||||
v.width(),
|
||||
v.height(),
|
||||
v.frame_rate(),
|
||||
v.thread_count()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void AVRecorder::draw_statistics() const
|
||||
{
|
||||
SRB2_ASSERT(impl_->video_encoder_ != nullptr);
|
||||
|
||||
auto draw = [](int x, std::string text, int32_t flags = 0)
|
||||
{
|
||||
V_DrawThinString(
|
||||
x,
|
||||
190,
|
||||
(V_6WIDTHSPACE | V_ALLOWLOWERCASE | V_SNAPTOBOTTOM | V_SNAPTORIGHT) | flags,
|
||||
text.c_str()
|
||||
);
|
||||
};
|
||||
|
||||
const float fps = impl_->video_frame_rate_avg_;
|
||||
const float size = impl_->container_->size();
|
||||
|
||||
const int32_t fps_color = [&]
|
||||
{
|
||||
const int cap = impl_->video_encoder_->frame_rate();
|
||||
|
||||
// red when dropped below 60% of the target
|
||||
if (fps > 0.f && fps < (0.6f * cap))
|
||||
{
|
||||
return V_REDMAP;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}();
|
||||
|
||||
const int32_t mb_color = [&]
|
||||
{
|
||||
if (!impl_->max_size_)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
const std::size_t cap = *impl_->max_size_;
|
||||
|
||||
// yellow when within 1 MB of the limit
|
||||
if (size >= (cap - kMb))
|
||||
{
|
||||
return V_YELLOWMAP;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}();
|
||||
|
||||
draw(200, fmt::format("{:.0f}", fps), fps_color);
|
||||
draw(230, fmt::format("{:.1f}s", impl_->container_->duration().count()));
|
||||
draw(260, fmt::format("{:.1f} MB", size / kMb), mb_color);
|
||||
}
|
||||
175
src/media/avrecorder_impl.hpp
Normal file
175
src/media/avrecorder_impl.hpp
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_AVRECORDER_IMPL_HPP__
|
||||
#define __SRB2_MEDIA_AVRECORDER_IMPL_HPP__
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "../i_time.h"
|
||||
#include "avrecorder.hpp"
|
||||
#include "container.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class AVRecorder::Impl
|
||||
{
|
||||
public:
|
||||
template <typename T>
|
||||
class Queue
|
||||
{
|
||||
public:
|
||||
// The use of typename = void is a GCC bug.
|
||||
// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=85282
|
||||
// Explicit specialization inside of a class still
|
||||
// does not work as of 12.2.1.
|
||||
|
||||
template <typename, typename = void>
|
||||
struct Traits
|
||||
{
|
||||
};
|
||||
|
||||
template <typename _>
|
||||
struct Traits<AudioEncoder, _>
|
||||
{
|
||||
using frame_type = float;
|
||||
};
|
||||
|
||||
template <typename _>
|
||||
struct Traits<VideoEncoder, _>
|
||||
{
|
||||
using frame_type = IndexedVideoFrame::instance_t;
|
||||
};
|
||||
|
||||
std::vector<typename Traits<T>::frame_type> vec_;
|
||||
|
||||
// This number only decrements once a frame has
|
||||
// actually been written to container.
|
||||
std::size_t queued_frames_ = 0;
|
||||
|
||||
Queue(const std::unique_ptr<T>& encoder, Impl& impl) : encoder_(encoder.get()), impl_(&impl) {}
|
||||
|
||||
// This method handles validation of the queue,
|
||||
// finishing the queue and advancing PTS. Returns true
|
||||
// if PTS was advanced.
|
||||
bool advance(int pts, int duration);
|
||||
|
||||
// True if no more data may be queued.
|
||||
bool finished() const { return finished_; }
|
||||
|
||||
// Presentation Time Stamp; one frame for video
|
||||
// encoders, one sample for audio encoders.
|
||||
int pts() const { return pts_; }
|
||||
|
||||
private:
|
||||
using time_unit_t = std::chrono::duration<float>;
|
||||
|
||||
T* const encoder_;
|
||||
Impl* const impl_;
|
||||
|
||||
bool finished_ = (encoder_ == nullptr);
|
||||
int pts_ = -1; // valid pts starts at 0
|
||||
|
||||
// Actual duration of PTS unit.
|
||||
time_unit_t time_scale() const;
|
||||
};
|
||||
|
||||
const std::optional<std::size_t> max_size_;
|
||||
std::optional<std::chrono::duration<float>> max_duration_;
|
||||
|
||||
// max_duration_ may be readjusted in case a queue
|
||||
// finishes early for any reason. max_duration_config_ is
|
||||
// the original, unmodified value.
|
||||
const decltype(max_duration_) max_duration_config_ = max_duration_;
|
||||
|
||||
std::unique_ptr<MediaContainer> container_;
|
||||
std::unique_ptr<AudioEncoder> audio_encoder_;
|
||||
std::unique_ptr<VideoEncoder> video_encoder_;
|
||||
|
||||
Queue<AudioEncoder> audio_queue_ {audio_encoder_, *this};
|
||||
Queue<VideoEncoder> video_queue_ {video_encoder_, *this};
|
||||
|
||||
// This class becomes invalid if:
|
||||
//
|
||||
// 1) an exception occurred
|
||||
// 2) the object has begun destructing
|
||||
std::atomic<bool> valid_ = true;
|
||||
|
||||
// Average number of frames actually encoded per second.
|
||||
std::atomic<float> video_frame_rate_avg_ = 0.f;
|
||||
|
||||
Impl(Config config);
|
||||
~Impl();
|
||||
|
||||
// Returns valid PTS if enough time has passed.
|
||||
std::optional<int> advance_video_pts();
|
||||
|
||||
// Use before accessing audio_queue_ or video_queue_.
|
||||
auto queue_guard() { return std::lock_guard(queue_mutex_); }
|
||||
|
||||
// Use to notify worker thread if queues were modified.
|
||||
void wake_up_worker() { queue_cond_.notify_one(); }
|
||||
|
||||
private:
|
||||
enum class QueueState
|
||||
{
|
||||
kEmpty, // all queues are empty
|
||||
kFlushed, // a queue was flushed but more data may be waiting
|
||||
kFinished, // all queues are finished -- no more data may be queued
|
||||
};
|
||||
|
||||
const tic_t epoch_;
|
||||
|
||||
VideoEncoder::FrameCount video_frame_count_reference_ = {};
|
||||
|
||||
std::thread thread_;
|
||||
mutable std::recursive_mutex queue_mutex_; // guards audio and video queues
|
||||
std::condition_variable_any queue_cond_;
|
||||
|
||||
std::unique_ptr<AudioEncoder> make_audio_encoder(const Config cfg) const;
|
||||
std::unique_ptr<VideoEncoder> make_video_encoder(const Config cfg) const;
|
||||
|
||||
QueueState encode_queues();
|
||||
void update_video_frame_rate_avg();
|
||||
|
||||
void worker();
|
||||
|
||||
void container_dtor_handler(const MediaContainer& container) const;
|
||||
|
||||
// TODO: remove once hwr2 twodee is finished
|
||||
VideoFrame::instance_t convert_indexed_video_frame(const IndexedVideoFrame& indexed);
|
||||
};
|
||||
|
||||
template <>
|
||||
inline AVRecorder::Impl::Queue<AudioEncoder>::time_unit_t AVRecorder::Impl::Queue<AudioEncoder>::time_scale() const
|
||||
{
|
||||
return time_unit_t(1.f / encoder_->sample_rate());
|
||||
}
|
||||
|
||||
template <>
|
||||
inline AVRecorder::Impl::Queue<VideoEncoder>::time_unit_t AVRecorder::Impl::Queue<VideoEncoder>::time_scale() const
|
||||
{
|
||||
return time_unit_t(1.f / encoder_->frame_rate());
|
||||
}
|
||||
|
||||
extern template class AVRecorder::Impl::Queue<AudioEncoder>;
|
||||
extern template class AVRecorder::Impl::Queue<VideoEncoder>;
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_AVRECORDER_IMPL_HPP__
|
||||
69
src/media/avrecorder_indexed.cpp
Normal file
69
src/media/avrecorder_indexed.cpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
// TODO: remove this file once hwr2 twodee is finished
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "avrecorder_impl.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
using Impl = AVRecorder::Impl;
|
||||
|
||||
VideoFrame::instance_t Impl::convert_indexed_video_frame(const IndexedVideoFrame& indexed)
|
||||
{
|
||||
VideoFrame::instance_t frame = video_encoder_->new_frame(indexed.width, indexed.height, indexed.pts);
|
||||
|
||||
SRB2_ASSERT(frame != nullptr);
|
||||
|
||||
const VideoFrame::Buffer& buffer = frame->rgba_buffer();
|
||||
|
||||
const uint8_t* s = indexed.screen.data();
|
||||
uint8_t* p = buffer.plane.data();
|
||||
|
||||
for (int y = 0; y < frame->height(); ++y)
|
||||
{
|
||||
for (int x = 0; x < frame->width(); ++x)
|
||||
{
|
||||
const RGBA_t& c = indexed.palette[s[x]];
|
||||
|
||||
reinterpret_cast<uint32_t*>(p)[x] = c.rgba;
|
||||
}
|
||||
|
||||
s += indexed.width;
|
||||
p += buffer.row_stride;
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
AVRecorder::IndexedVideoFrame::instance_t AVRecorder::new_indexed_video_frame(uint32_t width, uint32_t height)
|
||||
{
|
||||
std::optional<int> pts = impl_->advance_video_pts();
|
||||
|
||||
if (!pts)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::make_unique<IndexedVideoFrame>(width, height, *pts);
|
||||
}
|
||||
|
||||
void AVRecorder::push_indexed_video_frame(IndexedVideoFrame::instance_t frame)
|
||||
{
|
||||
auto _ = impl_->queue_guard();
|
||||
|
||||
impl_->video_queue_.vec_.emplace_back(std::move(frame));
|
||||
impl_->wake_up_worker();
|
||||
}
|
||||
169
src/media/avrecorder_queue.cpp
Normal file
169
src/media/avrecorder_queue.cpp
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
#include "avrecorder_impl.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
using Impl = AVRecorder::Impl;
|
||||
|
||||
template <typename T>
|
||||
bool Impl::Queue<T>::advance(int new_pts, int duration)
|
||||
{
|
||||
if (!impl_->valid_ || finished())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
new_pts += duration;
|
||||
|
||||
// PTS must only advance.
|
||||
if (new_pts <= pts())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto finish = [this]
|
||||
{
|
||||
finished_ = true;
|
||||
|
||||
const auto t = impl_->container_->duration();
|
||||
|
||||
// Tracks are ultimately cut to the shortest among
|
||||
// them, therefore it would be pointless for another
|
||||
// queue to continue beyond this point.
|
||||
//
|
||||
// This is relevant if finishing due to size
|
||||
// constraint; in that case, another queue might be
|
||||
// far behind this one in terms of size and would
|
||||
// continue in vain.
|
||||
|
||||
if (!impl_->max_duration_ || t < impl_->max_duration_)
|
||||
{
|
||||
impl_->max_duration_ = t;
|
||||
}
|
||||
|
||||
impl_->wake_up_worker();
|
||||
};
|
||||
|
||||
if (impl_->max_duration_)
|
||||
{
|
||||
const int final_pts = *impl_->max_duration_ / time_scale();
|
||||
|
||||
if (new_pts > final_pts)
|
||||
{
|
||||
return finish(), false;
|
||||
}
|
||||
}
|
||||
|
||||
if (impl_->max_size_)
|
||||
{
|
||||
constexpr float kError = 0.99f; // 1% muxing overhead
|
||||
|
||||
const MediaEncoder::BitRate est = encoder_->estimated_bit_rate();
|
||||
|
||||
const float br = est.bits / 8.f;
|
||||
|
||||
// count size of already queued frames too
|
||||
const float t = ((duration + queued_frames_) * time_scale()) / est.period;
|
||||
|
||||
if ((impl_->container_->size() + (t * br)) > (*impl_->max_size_ * kError))
|
||||
{
|
||||
return finish(), false;
|
||||
}
|
||||
}
|
||||
|
||||
pts_ = new_pts;
|
||||
queued_frames_ += duration;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Impl::QueueState Impl::encode_queues()
|
||||
{
|
||||
bool remain = false;
|
||||
bool flushed = false;
|
||||
|
||||
auto check = [&, this](auto& q, auto encode)
|
||||
{
|
||||
std::unique_lock lock(queue_mutex_);
|
||||
|
||||
if (!q.finished())
|
||||
{
|
||||
remain = true;
|
||||
}
|
||||
|
||||
if (!q.vec_.empty())
|
||||
{
|
||||
const std::size_t n = q.queued_frames_;
|
||||
|
||||
auto copy = std::move(q.vec_);
|
||||
|
||||
lock.unlock();
|
||||
encode(std::move(copy));
|
||||
lock.lock();
|
||||
|
||||
q.queued_frames_ -= n;
|
||||
|
||||
flushed = true;
|
||||
}
|
||||
};
|
||||
|
||||
auto encode_audio = [this](auto copy) { audio_encoder_->encode(copy); };
|
||||
auto encode_video = [this](auto copy)
|
||||
{
|
||||
for (auto& p : copy)
|
||||
{
|
||||
auto frame = convert_indexed_video_frame(*p);
|
||||
|
||||
video_encoder_->encode(std::move(frame));
|
||||
}
|
||||
|
||||
update_video_frame_rate_avg();
|
||||
};
|
||||
|
||||
check(audio_queue_, encode_audio);
|
||||
check(video_queue_, encode_video);
|
||||
|
||||
if (flushed)
|
||||
{
|
||||
return QueueState::kFlushed;
|
||||
}
|
||||
else if (remain)
|
||||
{
|
||||
return QueueState::kEmpty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return QueueState::kFinished;
|
||||
}
|
||||
}
|
||||
|
||||
void Impl::update_video_frame_rate_avg()
|
||||
{
|
||||
constexpr auto period = std::chrono::duration<float>(1.f);
|
||||
|
||||
auto& ref = video_frame_count_reference_;
|
||||
const auto count = video_encoder_->frame_count();
|
||||
const auto t = (count.duration - ref.duration);
|
||||
|
||||
if (t >= period)
|
||||
{
|
||||
video_frame_rate_avg_ = (count.frames - ref.frames) * (period / t);
|
||||
ref = count;
|
||||
}
|
||||
}
|
||||
|
||||
template class Impl::Queue<AudioEncoder>;
|
||||
template class Impl::Queue<VideoEncoder>;
|
||||
34
src/media/cfile.cpp
Normal file
34
src/media/cfile.cpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "cfile.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
CFile::CFile(const std::string file_name) : name_(file_name)
|
||||
{
|
||||
file_ = std::fopen(name(), "wb");
|
||||
|
||||
if (file_ == nullptr)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format("{}: {}", name(), std::strerror(errno)));
|
||||
}
|
||||
}
|
||||
|
||||
CFile::~CFile()
|
||||
{
|
||||
std::fclose(file_);
|
||||
}
|
||||
36
src/media/cfile.hpp
Normal file
36
src/media/cfile.hpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_CFILE_HPP__
|
||||
#define __SRB2_MEDIA_CFILE_HPP__
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class CFile
|
||||
{
|
||||
public:
|
||||
CFile(const std::string file_name);
|
||||
~CFile();
|
||||
|
||||
operator std::FILE*() const { return file_; }
|
||||
|
||||
const char* name() const { return name_.c_str(); }
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
std::FILE* file_;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_CFILE_HPP__
|
||||
53
src/media/container.hpp
Normal file
53
src/media/container.hpp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_CONTAINER_HPP__
|
||||
#define __SRB2_MEDIA_CONTAINER_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "audio_encoder.hpp"
|
||||
#include "video_encoder.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class MediaContainer
|
||||
{
|
||||
public:
|
||||
using dtor_cb_t = std::function<void(const MediaContainer&)>;
|
||||
using time_unit_t = std::chrono::duration<float>;
|
||||
|
||||
struct Config
|
||||
{
|
||||
std::string file_name;
|
||||
dtor_cb_t destructor_callback;
|
||||
};
|
||||
|
||||
virtual ~MediaContainer() = default;
|
||||
|
||||
virtual std::unique_ptr<AudioEncoder> make_audio_encoder(AudioEncoder::Config config) = 0;
|
||||
virtual std::unique_ptr<VideoEncoder> make_video_encoder(VideoEncoder::Config config) = 0;
|
||||
|
||||
virtual const char* name() const = 0;
|
||||
virtual const char* file_name() const = 0;
|
||||
|
||||
// These are normally estimates. However, when called from
|
||||
// Config::destructor_callback, these are the exact final
|
||||
// values.
|
||||
virtual time_unit_t duration() const = 0;
|
||||
virtual std::size_t size() const = 0;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_CONTAINER_HPP__
|
||||
51
src/media/encoder.hpp
Normal file
51
src/media/encoder.hpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_ENCODER_HPP__
|
||||
#define __SRB2_MEDIA_ENCODER_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class MediaEncoder
|
||||
{
|
||||
public:
|
||||
using time_unit_t = std::chrono::duration<float>;
|
||||
|
||||
struct BitRate
|
||||
{
|
||||
std::size_t bits; // 8 bits = 1 byte :)
|
||||
time_unit_t period;
|
||||
};
|
||||
|
||||
virtual ~MediaEncoder() = default;
|
||||
|
||||
// Should be called finally but it's optional.
|
||||
virtual void flush() = 0;
|
||||
|
||||
virtual const char* name() const = 0;
|
||||
|
||||
// Returns an average bit rate over a constant period of
|
||||
// time, assuming no frames drops.
|
||||
virtual BitRate estimated_bit_rate() const = 0;
|
||||
|
||||
protected:
|
||||
using frame_buffer_t = tcb::span<const std::byte>;
|
||||
|
||||
virtual void write_frame(frame_buffer_t frame, time_unit_t timestamp, bool is_key_frame) = 0;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_ENCODER_HPP__
|
||||
117
src/media/options.cpp
Normal file
117
src/media/options.cpp
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "../m_fixed.h"
|
||||
#include "options.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
Options::Options(const char* prefix, map_t map) : prefix_(prefix), map_(map)
|
||||
{
|
||||
for (auto& [suffix, cvar] : map_)
|
||||
{
|
||||
cvar.name = strdup(fmt::format("{}_{}", prefix_, suffix).c_str());
|
||||
cvars_.emplace_back(&cvar);
|
||||
}
|
||||
}
|
||||
|
||||
const consvar_t& Options::cvar(const char* option) const
|
||||
{
|
||||
const consvar_t& cvar = map_.at(option);
|
||||
|
||||
SRB2_ASSERT(cvar.string != nullptr);
|
||||
|
||||
return cvar;
|
||||
}
|
||||
|
||||
template <>
|
||||
int Options::get<int>(const char* option) const
|
||||
{
|
||||
return cvar(option).value;
|
||||
}
|
||||
|
||||
template <>
|
||||
float Options::get<float>(const char* option) const
|
||||
{
|
||||
return FixedToFloat(cvar(option).value);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
consvar_t Options::values(const char* default_value, const Range<T> range, std::map<std::string_view, T> list)
|
||||
{
|
||||
constexpr bool is_float = std::is_floating_point_v<T>;
|
||||
|
||||
const std::size_t min_max_size = (range.min || range.max) ? 2 : 0;
|
||||
auto* arr = new CV_PossibleValue_t[list.size() + min_max_size + 1];
|
||||
|
||||
auto cast = [is_float](T n)
|
||||
{
|
||||
if constexpr (is_float)
|
||||
{
|
||||
return FloatToFixed(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
return n;
|
||||
}
|
||||
};
|
||||
|
||||
if (min_max_size)
|
||||
{
|
||||
// Order is very important, MIN then MAX.
|
||||
arr[0] = {range.min ? cast(*range.min) : INT32_MIN, "MIN"};
|
||||
arr[1] = {range.max ? cast(*range.max) : INT32_MAX, "MAX"};
|
||||
}
|
||||
|
||||
{
|
||||
std::size_t i = min_max_size;
|
||||
|
||||
for (const auto& [k, v] : list)
|
||||
{
|
||||
arr[i].value = cast(v);
|
||||
arr[i].strvalue = k.data();
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
arr[i].value = 0;
|
||||
arr[i].strvalue = nullptr;
|
||||
}
|
||||
|
||||
int32_t flags = CV_SAVE;
|
||||
|
||||
if constexpr (is_float)
|
||||
{
|
||||
flags |= CV_FLOAT;
|
||||
}
|
||||
|
||||
return CVAR_INIT(nullptr, default_value, flags, arr, nullptr);
|
||||
}
|
||||
|
||||
void Options::register_all()
|
||||
{
|
||||
for (auto cvar : cvars_)
|
||||
{
|
||||
CV_RegisterVar(cvar);
|
||||
}
|
||||
|
||||
cvars_ = {};
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
template consvar_t Options::values(const char* default_value, const Range<int> range, std::map<std::string_view, int> list);
|
||||
template consvar_t Options::values(const char* default_value, const Range<float> range, std::map<std::string_view, float> list);
|
||||
// clang-format on
|
||||
58
src/media/options.hpp
Normal file
58
src/media/options.hpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_OPTIONS_HPP__
|
||||
#define __SRB2_MEDIA_OPTIONS_HPP__
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "../command.h"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class Options
|
||||
{
|
||||
public:
|
||||
using map_t = std::unordered_map<std::string, consvar_t>;
|
||||
|
||||
template <typename T>
|
||||
struct Range
|
||||
{
|
||||
std::optional<T> min, max;
|
||||
};
|
||||
|
||||
// Registers all options as cvars.
|
||||
static void register_all();
|
||||
|
||||
Options(const char* prefix, map_t map);
|
||||
|
||||
template <typename T>
|
||||
T get(const char* option) const;
|
||||
|
||||
template <typename T>
|
||||
static consvar_t values(const char* default_value, const Range<T> range, std::map<std::string_view, T> list = {});
|
||||
|
||||
private:
|
||||
static std::vector<consvar_t*> cvars_;
|
||||
|
||||
const char* prefix_;
|
||||
map_t map_;
|
||||
|
||||
const consvar_t& cvar(const char* option) const;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_OPTIONS_HPP__
|
||||
58
src/media/options_values.cpp
Normal file
58
src/media/options_values.cpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <vpx/vpx_encoder.h>
|
||||
|
||||
#include "options.hpp"
|
||||
#include "vorbis.hpp"
|
||||
#include "vp8.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
// NOTE: Options::cvars_ MUST be initialized before any
|
||||
// Options instances construct. For static objects, they have
|
||||
// to be defined in the same translation unit as
|
||||
// Options::cvars_ to guarantee initialization order.
|
||||
|
||||
std::vector<consvar_t*> Options::cvars_;
|
||||
|
||||
// clang-format off
|
||||
const Options VorbisEncoder::options_("vorbis", {
|
||||
{"quality", Options::values<float>("0", {-0.1f, 1.f})},
|
||||
{"max_bitrate", Options::values<int>("-1", {-1})},
|
||||
{"nominal_bitrate", Options::values<int>("-1", {-1})},
|
||||
{"min_bitrate", Options::values<int>("-1", {-1})},
|
||||
});
|
||||
|
||||
const Options VP8Encoder::options_("vp8", {
|
||||
{"quality_mode", Options::values<int>("q", {}, {
|
||||
{"vbr", VPX_VBR},
|
||||
{"cbr", VPX_CBR},
|
||||
{"cq", VPX_CQ},
|
||||
{"q", VPX_Q},
|
||||
})},
|
||||
{"target_bitrate", Options::values<int>("800", {1})},
|
||||
{"min_q", Options::values<int>("4", {4, 63})},
|
||||
{"max_q", Options::values<int>("55", {4, 63})},
|
||||
{"kf_min", Options::values<int>("0", {0})},
|
||||
{"kf_max", Options::values<int>("auto", {0}, {
|
||||
{"auto", static_cast<int>(KeyFrameOption::kAuto)},
|
||||
})},
|
||||
{"cpu_used", Options::values<int>("0", {-16, 16})},
|
||||
{"cq_level", Options::values<int>("10", {0, 63})},
|
||||
{"deadline", Options::values<int>("10", {1}, {
|
||||
{"infinite", static_cast<int>(DeadlineOption::kInfinite)},
|
||||
})},
|
||||
{"sharpness", Options::values<int>("7", {0, 7})},
|
||||
{"token_parts", Options::values<int>("0", {0, 3})},
|
||||
{"threads", Options::values<int>("1", {1})},
|
||||
});
|
||||
// clang-format on
|
||||
58
src/media/video_encoder.hpp
Normal file
58
src/media/video_encoder.hpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_VIDEO_ENCODER_HPP__
|
||||
#define __SRB2_MEDIA_VIDEO_ENCODER_HPP__
|
||||
|
||||
#include "encoder.hpp"
|
||||
#include "video_frame.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class VideoEncoder : virtual public MediaEncoder
|
||||
{
|
||||
public:
|
||||
struct Config
|
||||
{
|
||||
int width;
|
||||
int height;
|
||||
int frame_rate;
|
||||
VideoFrame::BufferMethod buffer_method;
|
||||
};
|
||||
|
||||
struct FrameCount
|
||||
{
|
||||
// Number of real frames, not counting frame skips.
|
||||
int frames;
|
||||
|
||||
time_unit_t duration;
|
||||
};
|
||||
|
||||
// VideoFrame::width() and VideoFrame::height() should be
|
||||
// used on the returned frame.
|
||||
virtual VideoFrame::instance_t new_frame(int width, int height, int pts) = 0;
|
||||
|
||||
virtual void encode(VideoFrame::instance_t frame) = 0;
|
||||
|
||||
virtual int width() const = 0;
|
||||
virtual int height() const = 0;
|
||||
virtual int frame_rate() const = 0;
|
||||
|
||||
// Reports the number of threads used, if the encoder is
|
||||
// multithreaded.
|
||||
virtual int thread_count() const = 0;
|
||||
|
||||
// Number of frames fully encoded so far.
|
||||
virtual FrameCount frame_count() const = 0;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_VIDEO_ENCODER_HPP__
|
||||
63
src/media/video_frame.hpp
Normal file
63
src/media/video_frame.hpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_VIDEO_FRAME_HPP__
|
||||
#define __SRB2_MEDIA_VIDEO_FRAME_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class VideoFrame
|
||||
{
|
||||
public:
|
||||
using instance_t = std::unique_ptr<VideoFrame>;
|
||||
|
||||
enum class BufferMethod
|
||||
{
|
||||
// Returns an already allocated buffer for each
|
||||
// frame. See VideoFrame::rgba_buffer(). The encoder
|
||||
// completely manages allocating this buffer.
|
||||
kEncoderAllocatedRGBA8888,
|
||||
};
|
||||
|
||||
struct Buffer
|
||||
{
|
||||
tcb::span<uint8_t> plane;
|
||||
std::size_t row_stride; // size of each row
|
||||
};
|
||||
|
||||
virtual int width() const = 0;
|
||||
virtual int height() const = 0;
|
||||
|
||||
int pts() const { return pts_; }
|
||||
|
||||
// Returns a buffer that should be
|
||||
// filled with RGBA pixels.
|
||||
//
|
||||
// This method may only be used if
|
||||
// the encoder was configured with
|
||||
// BufferMethod::kEncoderAllocatedRGBA8888.
|
||||
virtual const Buffer& rgba_buffer() const = 0;
|
||||
|
||||
protected:
|
||||
VideoFrame(int pts) : pts_(pts) {}
|
||||
|
||||
private:
|
||||
int pts_;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_VIDEO_FRAME_HPP__
|
||||
142
src/media/vorbis.cpp
Normal file
142
src/media/vorbis.cpp
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <vorbis/vorbisenc.h>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "vorbis.hpp"
|
||||
#include "vorbis_error.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
void runtime_assert(VorbisError error, const char *what)
|
||||
{
|
||||
if (error != 0)
|
||||
{
|
||||
throw std::runtime_error(fmt::format("{}: {}", what, error));
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace
|
||||
|
||||
VorbisEncoder::VorbisEncoder(Config cfg)
|
||||
{
|
||||
const long max_bitrate = options_.get<int>("max_bitrate");
|
||||
const long nominal_bitrate = options_.get<int>("nominal_bitrate");
|
||||
const long min_bitrate = options_.get<int>("min_bitrate");
|
||||
|
||||
vorbis_info_init(&vi_);
|
||||
|
||||
if (max_bitrate != -1 || nominal_bitrate != -1 || min_bitrate != -1)
|
||||
{
|
||||
// managed bitrate mode
|
||||
VorbisError error =
|
||||
vorbis_encode_init(&vi_, cfg.channels, cfg.sample_rate, max_bitrate, nominal_bitrate, min_bitrate);
|
||||
|
||||
if (error != 0)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format(
|
||||
"vorbis_encode_init: {}, max_bitrate={}, nominal_bitrate={}, min_bitrate={}",
|
||||
error,
|
||||
max_bitrate,
|
||||
nominal_bitrate,
|
||||
min_bitrate
|
||||
));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// variable bitrate mode
|
||||
const float quality = options_.get<float>("quality");
|
||||
|
||||
VorbisError error = vorbis_encode_init_vbr(&vi_, cfg.channels, cfg.sample_rate, quality);
|
||||
|
||||
if (error != 0)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format("vorbis_encode_init: {}, quality={}", error, quality));
|
||||
}
|
||||
}
|
||||
|
||||
runtime_assert(vorbis_analysis_init(&vd_, &vi_), "vorbis_analysis_init");
|
||||
runtime_assert(vorbis_block_init(&vd_, &vb_), "vorbis_block_init");
|
||||
}
|
||||
|
||||
VorbisEncoder::~VorbisEncoder()
|
||||
{
|
||||
vorbis_block_clear(&vb_);
|
||||
vorbis_dsp_clear(&vd_);
|
||||
vorbis_info_clear(&vi_);
|
||||
}
|
||||
|
||||
VorbisEncoder::headers_t VorbisEncoder::generate_headers()
|
||||
{
|
||||
headers_t op;
|
||||
|
||||
vorbis_comment vc;
|
||||
vorbis_comment_init(&vc);
|
||||
|
||||
VorbisError error = vorbis_analysis_headerout(&vd_, &vc, &op[0], &op[1], &op[2]);
|
||||
|
||||
if (error != 0)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format("vorbis_analysis_headerout: {}", error));
|
||||
}
|
||||
|
||||
vorbis_comment_clear(&vc);
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
void VorbisEncoder::analyse(sample_buffer_t in)
|
||||
{
|
||||
const int ch = channels();
|
||||
|
||||
const std::size_t n = in.size() / ch;
|
||||
float** fv = vorbis_analysis_buffer(&vd_, n);
|
||||
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
{
|
||||
auto s = in.subspan(i * ch, ch);
|
||||
|
||||
fv[0][i] = s[0];
|
||||
fv[1][i] = s[1];
|
||||
}
|
||||
|
||||
// automatically handles end of stream if n = 0
|
||||
runtime_assert(vorbis_analysis_wrote(&vd_, n), "vorbis_analysis_wrote");
|
||||
|
||||
while (vorbis_analysis_blockout(&vd_, &vb_) > 0)
|
||||
{
|
||||
runtime_assert(vorbis_analysis(&vb_, nullptr), "vorbis_analysis");
|
||||
runtime_assert(vorbis_bitrate_addblock(&vb_), "vorbis_bitrate_addblock");
|
||||
|
||||
ogg_packet op;
|
||||
|
||||
while (vorbis_bitrate_flushpacket(&vd_, &op) > 0)
|
||||
{
|
||||
write_packet(&op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VorbisEncoder::write_packet(ogg_packet* op)
|
||||
{
|
||||
using T = const std::byte;
|
||||
tcb::span<T> p(reinterpret_cast<T*>(op->packet), static_cast<std::size_t>(op->bytes));
|
||||
|
||||
write_frame(p, std::chrono::duration<float>(vorbis_granule_time(&vd_, op->granulepos)), true);
|
||||
}
|
||||
54
src/media/vorbis.hpp
Normal file
54
src/media/vorbis.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_VORBIS_HPP__
|
||||
#define __SRB2_MEDIA_VORBIS_HPP__
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <vorbis/codec.h>
|
||||
|
||||
#include "audio_encoder.hpp"
|
||||
#include "options.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class VorbisEncoder : public AudioEncoder
|
||||
{
|
||||
public:
|
||||
static const Options options_;
|
||||
|
||||
VorbisEncoder(Config config);
|
||||
~VorbisEncoder();
|
||||
|
||||
virtual void encode(sample_buffer_t samples) override final { analyse(samples); }
|
||||
virtual void flush() override final { analyse(); }
|
||||
|
||||
virtual const char* name() const override final { return "Vorbis"; }
|
||||
virtual int channels() const override final { return vi_.channels; }
|
||||
virtual int sample_rate() const override final { return vi_.rate; }
|
||||
|
||||
protected:
|
||||
using headers_t = std::array<ogg_packet, 3>;
|
||||
|
||||
headers_t generate_headers();
|
||||
|
||||
private:
|
||||
vorbis_info vi_;
|
||||
vorbis_dsp_state vd_;
|
||||
vorbis_block vb_;
|
||||
|
||||
void analyse(sample_buffer_t samples = {});
|
||||
void write_packet(ogg_packet* op);
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_VORBIS_HPP__
|
||||
54
src/media/vorbis_error.hpp
Normal file
54
src/media/vorbis_error.hpp
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_VORBIS_ERROR_HPP__
|
||||
#define __SRB2_MEDIA_VORBIS_ERROR_HPP__
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <vorbis/codec.h>
|
||||
|
||||
class VorbisError
|
||||
{
|
||||
public:
|
||||
VorbisError(int error) : error_(error) {}
|
||||
|
||||
operator int() const { return error_; }
|
||||
|
||||
std::string name() const
|
||||
{
|
||||
switch (error_)
|
||||
{
|
||||
case OV_EFAULT:
|
||||
return "Internal error (OV_EFAULT)";
|
||||
case OV_EINVAL:
|
||||
return "Invalid settings (OV_EINVAL)";
|
||||
case OV_EIMPL:
|
||||
return "Invalid settings (OV_EIMPL)";
|
||||
default:
|
||||
return fmt::format("error {}", error_);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
int error_;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct fmt::formatter<VorbisError> : formatter<std::string>
|
||||
{
|
||||
template <typename FormatContext>
|
||||
auto format(const VorbisError& error, FormatContext& ctx) const
|
||||
{
|
||||
return formatter<std::string>::format(error.name(), ctx);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __SRB2_MEDIA_VORBIS_ERROR_HPP__
|
||||
241
src/media/vp8.cpp
Normal file
241
src/media/vp8.cpp
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "vp8.hpp"
|
||||
#include "vpx_error.hpp"
|
||||
#include "yuv420p.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
vpx_codec_iface_t* VP8Encoder::kCodec = vpx_codec_vp8_cx();
|
||||
|
||||
const vpx_codec_enc_cfg_t VP8Encoder::configure(const Config user)
|
||||
{
|
||||
vpx_codec_enc_cfg_t cfg;
|
||||
vpx_codec_enc_config_default(kCodec, &cfg, 0);
|
||||
|
||||
cfg.g_threads = options_.get<int>("threads");
|
||||
|
||||
cfg.g_w = user.width;
|
||||
cfg.g_h = user.height;
|
||||
|
||||
cfg.g_bit_depth = VPX_BITS_8;
|
||||
cfg.g_input_bit_depth = 8;
|
||||
|
||||
cfg.g_timebase.num = 1;
|
||||
cfg.g_timebase.den = user.frame_rate;
|
||||
|
||||
cfg.g_pass = VPX_RC_ONE_PASS;
|
||||
cfg.rc_end_usage = static_cast<vpx_rc_mode>(options_.get<int>("quality_mode"));
|
||||
cfg.kf_mode = VPX_KF_AUTO;
|
||||
|
||||
cfg.rc_target_bitrate = options_.get<int>("target_bitrate");
|
||||
cfg.rc_min_quantizer = options_.get<int>("min_q");
|
||||
cfg.rc_max_quantizer = options_.get<int>("max_q");
|
||||
|
||||
// Keyframe spacing, in number of frames.
|
||||
// kf_max_dist should be low enough to allow scrubbing.
|
||||
|
||||
int kf_max = options_.get<int>("kf_max");
|
||||
|
||||
if (kf_max == static_cast<int>(KeyFrameOption::kAuto))
|
||||
{
|
||||
// Automatically pick a good rate
|
||||
kf_max = (user.frame_rate / 2); // every .5s
|
||||
}
|
||||
|
||||
cfg.kf_min_dist = options_.get<int>("kf_min");
|
||||
cfg.kf_max_dist = kf_max;
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
VP8Encoder::VP8Encoder(Config config) : ctx_(config), img_(config.width, config.height), frame_rate_(config.frame_rate)
|
||||
{
|
||||
SRB2_ASSERT(config.buffer_method == VideoFrame::BufferMethod::kEncoderAllocatedRGBA8888);
|
||||
|
||||
control<int>(VP8E_SET_CPUUSED, "cpu_used");
|
||||
control<int>(VP8E_SET_CQ_LEVEL, "cq_level");
|
||||
control<int>(VP8E_SET_SHARPNESS, "sharpness");
|
||||
control<int>(VP8E_SET_TOKEN_PARTITIONS, "token_parts");
|
||||
|
||||
auto plane = [this](int k, int ycs = 0)
|
||||
{
|
||||
using T = uint8_t;
|
||||
auto view = tcb::span<T>(reinterpret_cast<T*>(img_->planes[k]), img_->stride[k] * (img_->h >> ycs));
|
||||
|
||||
return VideoFrame::Buffer {view, static_cast<std::size_t>(img_->stride[k])};
|
||||
};
|
||||
|
||||
frame_ = std::make_unique<YUV420pFrame>(
|
||||
0,
|
||||
plane(VPX_PLANE_Y),
|
||||
plane(VPX_PLANE_U, img_->y_chroma_shift),
|
||||
plane(VPX_PLANE_V, img_->y_chroma_shift),
|
||||
rgba_buffer_
|
||||
);
|
||||
}
|
||||
|
||||
VP8Encoder::CtxWrapper::CtxWrapper(const Config user)
|
||||
{
|
||||
const vpx_codec_enc_cfg_t cfg = configure(user);
|
||||
|
||||
if (vpx_codec_enc_init(&ctx_, kCodec, &cfg, 0) != VPX_CODEC_OK)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format("vpx_codec_enc_init: {}", VpxError(ctx_)));
|
||||
}
|
||||
}
|
||||
|
||||
VP8Encoder::CtxWrapper::~CtxWrapper()
|
||||
{
|
||||
vpx_codec_destroy(&ctx_);
|
||||
}
|
||||
|
||||
VP8Encoder::ImgWrapper::ImgWrapper(int width, int height)
|
||||
{
|
||||
if (vpx_img_alloc(&img_, VPX_IMG_FMT_I420, width, height, YUV420pFrame::kAlignment) == nullptr)
|
||||
{
|
||||
throw std::runtime_error("vpx_img_alloc");
|
||||
}
|
||||
}
|
||||
|
||||
VP8Encoder::ImgWrapper::~ImgWrapper()
|
||||
{
|
||||
vpx_img_free(&img_);
|
||||
}
|
||||
|
||||
VideoFrame::instance_t VP8Encoder::new_frame(int width, int height, int pts)
|
||||
{
|
||||
SRB2_ASSERT(frame_ != nullptr);
|
||||
|
||||
if (rgba_buffer_.resize(width, height))
|
||||
{
|
||||
// If there was a resize, the aspect ratio may not
|
||||
// match. When the frame is scaled later, it will be
|
||||
// "fit" into the target aspect ratio, leaving some
|
||||
// empty space around the scaled image. (See
|
||||
// VP8Encoder::encode)
|
||||
//
|
||||
// Set whole scaled buffer to black now so the empty
|
||||
// space appears as "black bars".
|
||||
rgba_scaled_buffer_.erase();
|
||||
}
|
||||
|
||||
frame_->reset(pts, rgba_buffer_);
|
||||
|
||||
return std::move(frame_);
|
||||
}
|
||||
|
||||
void VP8Encoder::encode(VideoFrame::instance_t frame)
|
||||
{
|
||||
{
|
||||
using T = YUV420pFrame;
|
||||
|
||||
SRB2_ASSERT(frame_ == nullptr);
|
||||
SRB2_ASSERT(dynamic_cast<T*>(frame.get()) != nullptr);
|
||||
|
||||
frame_ = std::unique_ptr<T>(static_cast<T*>(frame.release()));
|
||||
}
|
||||
|
||||
// This frame must be scaled to match encoder configuration
|
||||
if (frame_->width() != width() || frame_->height() != height())
|
||||
{
|
||||
rgba_scaled_buffer_.resize(width(), height());
|
||||
frame_->scale(rgba_scaled_buffer_);
|
||||
}
|
||||
else
|
||||
{
|
||||
rgba_scaled_buffer_.release();
|
||||
}
|
||||
|
||||
frame_->convert();
|
||||
|
||||
if (vpx_codec_encode(ctx_, img_, frame_->pts(), 1, 0, deadline_) != VPX_CODEC_OK)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format("VP8Encoder::encode: vpx_codec_encode: {}", VpxError(ctx_)));
|
||||
}
|
||||
|
||||
process();
|
||||
}
|
||||
|
||||
void VP8Encoder::flush()
|
||||
{
|
||||
do
|
||||
{
|
||||
if (vpx_codec_encode(ctx_, nullptr, 0, 0, 0, 0) != VPX_CODEC_OK)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format("VP8Encoder::flush: vpx_codec_encode: {}", VpxError(ctx_)));
|
||||
}
|
||||
} while (process());
|
||||
}
|
||||
|
||||
bool VP8Encoder::process()
|
||||
{
|
||||
bool output = false;
|
||||
|
||||
vpx_codec_iter_t iter = NULL;
|
||||
const vpx_codec_cx_pkt_t* pkt;
|
||||
|
||||
while ((pkt = vpx_codec_get_cx_data(ctx_, &iter)))
|
||||
{
|
||||
output = true;
|
||||
|
||||
if (pkt->kind != VPX_CODEC_CX_FRAME_PKT)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& frame = pkt->data.frame;
|
||||
|
||||
{
|
||||
const std::lock_guard _(frame_count_mutex_);
|
||||
|
||||
duration_ = frame.pts + frame.duration;
|
||||
frame_count_++;
|
||||
}
|
||||
|
||||
const float ts = frame.pts / static_cast<float>(frame_rate());
|
||||
|
||||
using T = const std::byte;
|
||||
tcb::span<T> p(reinterpret_cast<T*>(frame.buf), frame.sz);
|
||||
|
||||
write_frame(p, std::chrono::duration<float>(ts), (frame.flags & VPX_FRAME_IS_KEY));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void VP8Encoder::control(vp8e_enc_control_id id, const char* option)
|
||||
{
|
||||
auto value = options_.get<T>(option);
|
||||
|
||||
if (vpx_codec_control_(ctx_, id, value) != VPX_CODEC_OK)
|
||||
{
|
||||
throw std::invalid_argument(fmt::format("vpx_codec_control: {}, {}={}", VpxError(ctx_), option, value));
|
||||
}
|
||||
}
|
||||
|
||||
VideoEncoder::FrameCount VP8Encoder::frame_count() const
|
||||
{
|
||||
const std::lock_guard _(frame_count_mutex_);
|
||||
|
||||
return {frame_count_, std::chrono::duration<float>(duration_ / static_cast<float>(frame_rate()))};
|
||||
}
|
||||
112
src/media/vp8.hpp
Normal file
112
src/media/vp8.hpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_VP8_HPP__
|
||||
#define __SRB2_MEDIA_VP8_HPP__
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <vpx/vp8cx.h>
|
||||
|
||||
#include "options.hpp"
|
||||
#include "video_encoder.hpp"
|
||||
#include "yuv420p.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class VP8Encoder : public VideoEncoder
|
||||
{
|
||||
public:
|
||||
static const Options options_;
|
||||
|
||||
VP8Encoder(VideoEncoder::Config config);
|
||||
|
||||
virtual VideoFrame::instance_t new_frame(int width, int height, int pts) override final;
|
||||
|
||||
virtual void encode(VideoFrame::instance_t frame) override final;
|
||||
virtual void flush() override final;
|
||||
|
||||
virtual const char* name() const override final { return "VP8"; }
|
||||
virtual int width() const override final { return img_->w; }
|
||||
virtual int height() const override final { return img_->h; }
|
||||
virtual int frame_rate() const override final { return frame_rate_; }
|
||||
virtual int thread_count() const override final { return thread_count_; }
|
||||
|
||||
virtual FrameCount frame_count() const override final;
|
||||
|
||||
private:
|
||||
class CtxWrapper
|
||||
{
|
||||
public:
|
||||
CtxWrapper(const Config config);
|
||||
~CtxWrapper();
|
||||
|
||||
operator vpx_codec_ctx_t*() { return &ctx_; }
|
||||
operator vpx_codec_ctx_t&() { return ctx_; }
|
||||
|
||||
private:
|
||||
vpx_codec_ctx_t ctx_;
|
||||
};
|
||||
|
||||
class ImgWrapper
|
||||
{
|
||||
public:
|
||||
ImgWrapper(int width, int height);
|
||||
~ImgWrapper();
|
||||
|
||||
operator vpx_image_t*() { return &img_; }
|
||||
vpx_image_t* operator->() { return &img_; }
|
||||
const vpx_image_t* operator->() const { return &img_; }
|
||||
|
||||
private:
|
||||
vpx_image_t img_;
|
||||
};
|
||||
|
||||
enum class KeyFrameOption : int
|
||||
{
|
||||
kAuto = -1,
|
||||
};
|
||||
|
||||
enum class DeadlineOption : int
|
||||
{
|
||||
kInfinite = 0,
|
||||
};
|
||||
|
||||
static vpx_codec_iface_t* kCodec;
|
||||
|
||||
static const vpx_codec_enc_cfg_t configure(const Config config);
|
||||
|
||||
CtxWrapper ctx_;
|
||||
ImgWrapper img_;
|
||||
|
||||
const int frame_rate_;
|
||||
const int thread_count_ = options_.get<int>("threads");
|
||||
const int deadline_ = options_.get<int>("deadline");
|
||||
|
||||
mutable std::recursive_mutex frame_count_mutex_;
|
||||
|
||||
int duration_ = 0;
|
||||
int frame_count_ = 0;
|
||||
|
||||
YUV420pFrame::BufferRGBA //
|
||||
rgba_buffer_,
|
||||
rgba_scaled_buffer_; // only allocated if input NEEDS scaling
|
||||
|
||||
std::unique_ptr<YUV420pFrame> frame_;
|
||||
|
||||
bool process();
|
||||
|
||||
template <typename T> // T = option type
|
||||
void control(vp8e_enc_control_id id, const char* option);
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_VP8_HPP__
|
||||
45
src/media/vpx_error.hpp
Normal file
45
src/media/vpx_error.hpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_VPX_ERROR_HPP__
|
||||
#define __SRB2_MEDIA_VPX_ERROR_HPP__
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <vpx/vpx_codec.h>
|
||||
|
||||
class VpxError
|
||||
{
|
||||
public:
|
||||
VpxError(vpx_codec_ctx_t& ctx) : ctx_(&ctx) {}
|
||||
|
||||
std::string description() const
|
||||
{
|
||||
const char* error = vpx_codec_error(ctx_);
|
||||
const char* detail = vpx_codec_error_detail(ctx_);
|
||||
|
||||
return detail ? fmt::format("{}: {}", error, detail) : error;
|
||||
}
|
||||
|
||||
private:
|
||||
vpx_codec_ctx_t* ctx_;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct fmt::formatter<VpxError> : formatter<std::string>
|
||||
{
|
||||
template <typename FormatContext>
|
||||
auto format(const VpxError& error, FormatContext& ctx) const
|
||||
{
|
||||
return formatter<std::string>::format(error.description(), ctx);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // __SRB2_MEDIA_VPX_ERROR_HPP__
|
||||
26
src/media/webm.hpp
Normal file
26
src/media/webm.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_WEBM_HPP__
|
||||
#define __SRB2_MEDIA_WEBM_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <ratio>
|
||||
|
||||
namespace srb2::media::webm
|
||||
{
|
||||
|
||||
using track = uint64_t;
|
||||
using timestamp = uint64_t;
|
||||
using duration = std::chrono::duration<timestamp, std::nano>;
|
||||
|
||||
}; // namespace srb2::media::webm
|
||||
|
||||
#endif // __SRB2_MEDIA_WEBM_HPP__
|
||||
241
src/media/webm_container.cpp
Normal file
241
src/media/webm_container.cpp
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "webm_vorbis.hpp"
|
||||
#include "webm_vp8.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
using time_unit_t = MediaEncoder::time_unit_t;
|
||||
|
||||
WebmContainer::WebmContainer(const Config cfg) : writer_(cfg.file_name), dtor_cb_(cfg.destructor_callback)
|
||||
{
|
||||
if (!segment_.Init(&writer_))
|
||||
{
|
||||
throw std::runtime_error("mkvmuxer::Segment::Init");
|
||||
}
|
||||
}
|
||||
|
||||
WebmContainer::~WebmContainer()
|
||||
{
|
||||
flush_queue();
|
||||
|
||||
if (!segment_.Finalize())
|
||||
{
|
||||
CONS_Alert(CONS_WARNING, "mkvmuxer::Segment::Finalize has failed\n");
|
||||
}
|
||||
|
||||
finalized_ = true;
|
||||
|
||||
if (dtor_cb_)
|
||||
{
|
||||
dtor_cb_(*this);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioEncoder> WebmContainer::make_audio_encoder(AudioEncoder::Config cfg)
|
||||
{
|
||||
const uint64_t tid = segment_.AddAudioTrack(cfg.sample_rate, cfg.channels, 0);
|
||||
|
||||
return std::make_unique<WebmVorbisEncoder>(*this, tid, cfg);
|
||||
}
|
||||
|
||||
std::unique_ptr<VideoEncoder> WebmContainer::make_video_encoder(VideoEncoder::Config cfg)
|
||||
{
|
||||
const uint64_t tid = segment_.AddVideoTrack(cfg.width, cfg.height, 0);
|
||||
|
||||
return std::make_unique<WebmVP8Encoder>(*this, tid, cfg);
|
||||
}
|
||||
|
||||
time_unit_t WebmContainer::duration() const
|
||||
{
|
||||
if (finalized_)
|
||||
{
|
||||
const auto& si = *segment_.segment_info();
|
||||
|
||||
return webm::duration(static_cast<uint64_t>(si.duration() * si.timecode_scale()));
|
||||
}
|
||||
|
||||
auto _ = queue_guard();
|
||||
|
||||
return webm::duration(latest_timestamp_);
|
||||
}
|
||||
|
||||
std::size_t WebmContainer::size() const
|
||||
{
|
||||
if (finalized_)
|
||||
{
|
||||
return writer_.Position();
|
||||
}
|
||||
|
||||
auto _ = queue_guard();
|
||||
|
||||
return writer_.Position() + queue_size_;
|
||||
}
|
||||
|
||||
std::size_t WebmContainer::track_size(webm::track trackid) const
|
||||
{
|
||||
auto _ = queue_guard();
|
||||
|
||||
return queue_.at(trackid).data_size;
|
||||
}
|
||||
|
||||
time_unit_t WebmContainer::track_duration(webm::track trackid) const
|
||||
{
|
||||
auto _ = queue_guard();
|
||||
|
||||
return webm::duration(queue_.at(trackid).flushed_timestamp);
|
||||
}
|
||||
|
||||
void WebmContainer::write_frame(
|
||||
tcb::span<const std::byte> buffer,
|
||||
webm::track trackid,
|
||||
webm::timestamp timestamp,
|
||||
bool is_key_frame
|
||||
)
|
||||
{
|
||||
if (!segment_.AddFrame(
|
||||
reinterpret_cast<const uint8_t*>(buffer.data()),
|
||||
buffer.size_bytes(),
|
||||
trackid,
|
||||
timestamp,
|
||||
is_key_frame
|
||||
))
|
||||
{
|
||||
throw std::runtime_error(fmt::format(
|
||||
"mkvmuxer::Segment::AddFrame, size={}, track={}, ts={}, key={}",
|
||||
buffer.size_bytes(),
|
||||
trackid,
|
||||
timestamp,
|
||||
is_key_frame
|
||||
));
|
||||
}
|
||||
|
||||
queue_[trackid].data_size += buffer.size_bytes();
|
||||
}
|
||||
|
||||
void WebmContainer::queue_frame(
|
||||
tcb::span<const std::byte> buffer,
|
||||
webm::track trackid,
|
||||
webm::timestamp timestamp,
|
||||
bool is_key_frame
|
||||
)
|
||||
{
|
||||
auto _ = queue_guard();
|
||||
|
||||
auto& q = queue_.at(trackid);
|
||||
|
||||
// If another track is behind this one, queue this
|
||||
// frame until the other track catches up.
|
||||
|
||||
if (flush_queue() < timestamp)
|
||||
{
|
||||
q.frames.emplace_back(buffer, timestamp, is_key_frame);
|
||||
queue_size_ += buffer.size_bytes();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing is waiting; this frame can be written
|
||||
// immediately.
|
||||
|
||||
write_frame(buffer, trackid, timestamp, is_key_frame);
|
||||
q.flushed_timestamp = timestamp;
|
||||
}
|
||||
|
||||
q.queued_timestamp = timestamp;
|
||||
latest_timestamp_ = timestamp;
|
||||
}
|
||||
|
||||
webm::timestamp WebmContainer::flush_queue()
|
||||
{
|
||||
webm::timestamp goal = latest_timestamp_;
|
||||
|
||||
// Flush all tracks' queues, not beyond the end of the
|
||||
// shortest track.
|
||||
|
||||
for (const auto& [_, q] : queue_)
|
||||
{
|
||||
if (q.queued_timestamp < goal)
|
||||
{
|
||||
goal = q.queued_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
webm::timestamp shortest;
|
||||
|
||||
do
|
||||
{
|
||||
shortest = goal;
|
||||
|
||||
for (const auto& [tid, q] : queue_)
|
||||
{
|
||||
const webm::timestamp flushed = flush_single_queue(tid, q.queued_timestamp);
|
||||
|
||||
if (flushed < shortest)
|
||||
{
|
||||
shortest = flushed;
|
||||
}
|
||||
}
|
||||
} while (shortest < goal);
|
||||
|
||||
return shortest;
|
||||
}
|
||||
|
||||
webm::timestamp WebmContainer::flush_single_queue(webm::track trackid, webm::timestamp flushed_timestamp)
|
||||
{
|
||||
webm::timestamp goal = flushed_timestamp;
|
||||
|
||||
// Find the lowest timestamp yet flushed from all other
|
||||
// tracks. We cannot write a frame beyond this timestamp
|
||||
// because PTS must only increase.
|
||||
|
||||
for (const auto& [tid, other] : queue_)
|
||||
{
|
||||
if (tid != trackid && other.flushed_timestamp < goal)
|
||||
{
|
||||
goal = other.flushed_timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
auto& q = queue_.at(trackid);
|
||||
auto it = q.frames.cbegin();
|
||||
|
||||
// Flush previously queued frames in this track.
|
||||
|
||||
for (; it != q.frames.cend(); ++it)
|
||||
{
|
||||
const auto& frame = *it;
|
||||
|
||||
if (frame.timestamp > goal)
|
||||
{
|
||||
q.flushed_timestamp = frame.timestamp;
|
||||
break;
|
||||
}
|
||||
|
||||
write_frame(frame.buffer, trackid, frame.timestamp, frame.is_key_frame);
|
||||
|
||||
queue_size_ -= frame.buffer.size();
|
||||
}
|
||||
|
||||
q.frames.erase(q.frames.cbegin(), it);
|
||||
|
||||
if (q.frames.empty())
|
||||
{
|
||||
q.flushed_timestamp = flushed_timestamp;
|
||||
}
|
||||
|
||||
return goal;
|
||||
}
|
||||
112
src/media/webm_container.hpp
Normal file
112
src/media/webm_container.hpp
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_WEBM_CONTAINER_HPP__
|
||||
#define __SRB2_MEDIA_WEBM_CONTAINER_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <mkvmuxer/mkvmuxer.h>
|
||||
|
||||
#include "container.hpp"
|
||||
#include "webm.hpp"
|
||||
#include "webm_writer.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class WebmContainer : virtual public MediaContainer
|
||||
{
|
||||
public:
|
||||
WebmContainer(Config cfg);
|
||||
~WebmContainer();
|
||||
|
||||
virtual std::unique_ptr<AudioEncoder> make_audio_encoder(AudioEncoder::Config config) override final;
|
||||
virtual std::unique_ptr<VideoEncoder> make_video_encoder(VideoEncoder::Config config) override final;
|
||||
|
||||
virtual const char* name() const override final { return "WebM"; }
|
||||
virtual const char* file_name() const override final { return writer_.name(); }
|
||||
|
||||
virtual time_unit_t duration() const override final;
|
||||
virtual std::size_t size() const override final;
|
||||
|
||||
std::size_t track_size(webm::track trackid) const;
|
||||
time_unit_t track_duration(webm::track trackid) const;
|
||||
|
||||
template <typename T = mkvmuxer::Track>
|
||||
T* get_track(webm::track trackid) const
|
||||
{
|
||||
return reinterpret_cast<T*>(segment_.GetTrackByNumber(trackid));
|
||||
}
|
||||
|
||||
void init_queue(webm::track trackid) { queue_.try_emplace(trackid); }
|
||||
|
||||
// init_queue MUST be called before using this function.
|
||||
void queue_frame(
|
||||
tcb::span<const std::byte> buffer,
|
||||
webm::track trackid,
|
||||
webm::timestamp timestamp,
|
||||
bool is_key_frame
|
||||
);
|
||||
|
||||
auto queue_guard() const { return std::lock_guard(queue_mutex_); }
|
||||
|
||||
private:
|
||||
struct FrameQueue
|
||||
{
|
||||
struct Frame
|
||||
{
|
||||
std::vector<std::byte> buffer;
|
||||
webm::timestamp timestamp;
|
||||
bool is_key_frame;
|
||||
|
||||
Frame(tcb::span<const std::byte> buffer_, webm::timestamp timestamp_, bool is_key_frame_) :
|
||||
buffer(buffer_.begin(), buffer_.end()), timestamp(timestamp_), is_key_frame(is_key_frame_)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<Frame> frames;
|
||||
std::size_t data_size = 0;
|
||||
|
||||
webm::timestamp flushed_timestamp = 0;
|
||||
webm::timestamp queued_timestamp = 0;
|
||||
};
|
||||
|
||||
mkvmuxer::Segment segment_;
|
||||
WebmWriter writer_;
|
||||
|
||||
mutable std::recursive_mutex queue_mutex_;
|
||||
|
||||
std::unordered_map<webm::track, FrameQueue> queue_;
|
||||
|
||||
webm::timestamp latest_timestamp_ = 0;
|
||||
std::size_t queue_size_ = 0;
|
||||
|
||||
bool finalized_ = false;
|
||||
const dtor_cb_t dtor_cb_;
|
||||
|
||||
void write_frame(
|
||||
tcb::span<const std::byte> buffer,
|
||||
webm::track trackid,
|
||||
webm::timestamp timestamp,
|
||||
bool is_key_frame
|
||||
);
|
||||
|
||||
// Returns the largest timestamp that can be written.
|
||||
webm::timestamp flush_queue();
|
||||
webm::timestamp flush_single_queue(webm::track trackid, webm::timestamp flushed_timestamp);
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_WEBM_CONTAINER_HPP__
|
||||
51
src/media/webm_encoder.hpp
Normal file
51
src/media/webm_encoder.hpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_WEBM_ENCODER_HPP__
|
||||
#define __SRB2_MEDIA_WEBM_ENCODER_HPP__
|
||||
|
||||
#include <mkvmuxer/mkvmuxer.h>
|
||||
|
||||
#include "encoder.hpp"
|
||||
#include "webm_container.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
template <typename T = mkvmuxer::Track>
|
||||
class WebmEncoder : virtual public MediaEncoder
|
||||
{
|
||||
public:
|
||||
WebmEncoder(WebmContainer& container, webm::track trackid) : container_(container), trackid_(trackid)
|
||||
{
|
||||
container_.init_queue(trackid_);
|
||||
}
|
||||
|
||||
protected:
|
||||
WebmContainer& container_;
|
||||
webm::track trackid_;
|
||||
|
||||
std::size_t size() const { return container_.track_size(trackid_); }
|
||||
time_unit_t duration() const { return container_.track_duration(trackid_); }
|
||||
|
||||
static T* get_track(const WebmContainer& container, webm::track trackid) { return container.get_track<T>(trackid); }
|
||||
|
||||
T* track() const { return get_track(container_, trackid_); }
|
||||
|
||||
virtual void write_frame(frame_buffer_t p, time_unit_t ts, bool is_key_frame) override final
|
||||
{
|
||||
const auto ts_nano = std::chrono::duration_cast<webm::duration>(ts);
|
||||
|
||||
container_.queue_frame(p, trackid_, ts_nano.count(), is_key_frame);
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_WEBM_ENCODER_HPP__
|
||||
65
src/media/webm_vorbis.hpp
Normal file
65
src/media/webm_vorbis.hpp
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_WEBM_VORBIS_HPP__
|
||||
#define __SRB2_MEDIA_WEBM_VORBIS_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "vorbis.hpp"
|
||||
#include "webm_encoder.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class WebmVorbisEncoder : public WebmEncoder<mkvmuxer::AudioTrack>, public VorbisEncoder
|
||||
{
|
||||
public:
|
||||
WebmVorbisEncoder(WebmContainer& container, webm::track trackid, AudioEncoder::Config cfg) :
|
||||
WebmEncoder(container, trackid), VorbisEncoder(cfg)
|
||||
{
|
||||
// write Vorbis extra data
|
||||
|
||||
const auto p = make_vorbis_private_data();
|
||||
|
||||
if (!track()->SetCodecPrivate(reinterpret_cast<const uint8_t*>(p.data()), p.size()))
|
||||
{
|
||||
throw std::runtime_error(fmt::format("mkvmuxer::AudioTrack::SetCodecPrivate, size={}", p.size()));
|
||||
}
|
||||
}
|
||||
|
||||
virtual BitRate estimated_bit_rate() const override final
|
||||
{
|
||||
auto _ = container_.queue_guard();
|
||||
|
||||
const std::chrono::duration<float> t = duration();
|
||||
|
||||
if (t <= t.zero())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
return {static_cast<std::size_t>((size() * 8) / t.count()), 1s};
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::byte> make_vorbis_private_data();
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_WEBM_VORBIS_HPP__
|
||||
79
src/media/webm_vorbis_lace.cpp
Normal file
79
src/media/webm_vorbis_lace.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#include <vector>
|
||||
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
#include "webm_vorbis.hpp"
|
||||
|
||||
// https://www.matroska.org/technical/notes.html#xiph-lacing
|
||||
// https://www.matroska.org/technical/codec_specs.html#a_vorbis
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
static std::size_t lace_length(const ogg_packet& op)
|
||||
{
|
||||
return (op.bytes / 255) + 1;
|
||||
}
|
||||
|
||||
static void lace(std::vector<std::byte>& v, const ogg_packet& op)
|
||||
{
|
||||
// The lacing size is encoded in at least one byte. If
|
||||
// the value is 255, add the value of the next byte in
|
||||
// sequence. This ends with a byte that is less than 255.
|
||||
|
||||
std::fill_n(std::back_inserter(v), lace_length(op) - 1, std::byte {255});
|
||||
|
||||
const unsigned char n = (op.bytes % 255);
|
||||
v.emplace_back(std::byte {n});
|
||||
}
|
||||
|
||||
std::vector<std::byte> WebmVorbisEncoder::make_vorbis_private_data()
|
||||
{
|
||||
const headers_t packets = generate_headers();
|
||||
|
||||
std::vector<std::byte> v;
|
||||
|
||||
// There are three Vorbis header packets. The lacing for
|
||||
// these packets in Matroska does not count the final
|
||||
// packet.
|
||||
|
||||
// clang-format off
|
||||
v.reserve(
|
||||
1
|
||||
+ lace_length(packets[0])
|
||||
+ lace_length(packets[1])
|
||||
+ packets[0].bytes
|
||||
+ packets[1].bytes
|
||||
+ packets[2].bytes);
|
||||
// clang-format on
|
||||
|
||||
// The first byte is the number of packets. Once again,
|
||||
// the last packet is not counted.
|
||||
v.emplace_back(std::byte {2});
|
||||
|
||||
// Then the laced sizes for each packet.
|
||||
lace(v, packets[0]);
|
||||
lace(v, packets[1]);
|
||||
|
||||
// Then each packet's data. The last packet's data
|
||||
// actually is written here.
|
||||
for (auto op : packets)
|
||||
{
|
||||
tcb::span<const std::byte> p(reinterpret_cast<const std::byte*>(op.packet), op.bytes);
|
||||
|
||||
std::copy(p.begin(), p.end(), std::back_inserter(v));
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
44
src/media/webm_vp8.hpp
Normal file
44
src/media/webm_vp8.hpp
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_WEBM_VP8_HPP__
|
||||
#define __SRB2_MEDIA_WEBM_VP8_HPP__
|
||||
|
||||
#include "vp8.hpp"
|
||||
#include "webm_encoder.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class WebmVP8Encoder : public WebmEncoder<mkvmuxer::VideoTrack>, public VP8Encoder
|
||||
{
|
||||
public:
|
||||
WebmVP8Encoder(WebmContainer& container, webm::track trackid, VideoEncoder::Config cfg) :
|
||||
WebmEncoder(container, trackid), VP8Encoder(cfg)
|
||||
{
|
||||
}
|
||||
|
||||
virtual BitRate estimated_bit_rate() const override final
|
||||
{
|
||||
auto _ = container_.queue_guard();
|
||||
|
||||
const int frames = frame_count().frames;
|
||||
|
||||
if (frames <= 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
return {(size() * 8) / frames, std::chrono::duration<float>(1.f / frame_rate())};
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_WEBM_VP8_HPP__
|
||||
32
src/media/webm_writer.hpp
Normal file
32
src/media/webm_writer.hpp
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_WEBM_WRITER_HPP__
|
||||
#define __SRB2_MEDIA_WEBM_WRITER_HPP__
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include <mkvmuxer/mkvwriter.h>
|
||||
|
||||
#include "cfile.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class WebmWriter : public CFile, public mkvmuxer::MkvWriter
|
||||
{
|
||||
public:
|
||||
WebmWriter(const std::string file_name) : CFile(file_name), MkvWriter(static_cast<std::FILE*>(*this)) {}
|
||||
~WebmWriter() { MkvWriter::Close(); }
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_WEBM_WRITER_HPP__
|
||||
125
src/media/yuv420p.cpp
Normal file
125
src/media/yuv420p.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include <libyuv/convert.h>
|
||||
#include <libyuv/scale_argb.h>
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
#include "../cxxutil.hpp"
|
||||
#include "yuv420p.hpp"
|
||||
|
||||
using namespace srb2::media;
|
||||
|
||||
bool YUV420pFrame::BufferRGBA::resize(int width, int height)
|
||||
{
|
||||
if (width == width_ && height == height_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
|
||||
row_stride = width * 4;
|
||||
|
||||
const std::size_t new_size = row_stride * height;
|
||||
|
||||
// Overallocate since the vector's alignment can't be
|
||||
// easily controlled. This is not a significant waste.
|
||||
vec_.resize(new_size + (kAlignment - 1));
|
||||
|
||||
void* p = vec_.data();
|
||||
std::size_t n = vec_.size();
|
||||
|
||||
p = std::align(kAlignment, 1, p, n);
|
||||
SRB2_ASSERT(p != nullptr);
|
||||
|
||||
plane = tcb::span<uint8_t>(reinterpret_cast<uint8_t*>(p), new_size);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void YUV420pFrame::BufferRGBA::erase()
|
||||
{
|
||||
std::fill(vec_.begin(), vec_.end(), 0);
|
||||
}
|
||||
|
||||
void YUV420pFrame::BufferRGBA::release()
|
||||
{
|
||||
if (!vec_.empty())
|
||||
{
|
||||
*this = {};
|
||||
}
|
||||
}
|
||||
|
||||
const VideoFrame::Buffer& YUV420pFrame::rgba_buffer() const
|
||||
{
|
||||
return *rgba_;
|
||||
}
|
||||
|
||||
void YUV420pFrame::convert() const
|
||||
{
|
||||
// ABGR = RGBA in memory
|
||||
libyuv::ABGRToI420(
|
||||
rgba_->plane.data(),
|
||||
rgba_->row_stride,
|
||||
y_.plane.data(),
|
||||
y_.row_stride,
|
||||
u_.plane.data(),
|
||||
u_.row_stride,
|
||||
v_.plane.data(),
|
||||
v_.row_stride,
|
||||
width(),
|
||||
height()
|
||||
);
|
||||
}
|
||||
|
||||
void YUV420pFrame::scale(const BufferRGBA& scaled_rgba)
|
||||
{
|
||||
int vw = scaled_rgba.width();
|
||||
int vh = scaled_rgba.height();
|
||||
|
||||
uint8_t* p = scaled_rgba.plane.data();
|
||||
|
||||
const float ru = width() / static_cast<float>(height());
|
||||
const float rs = vw / static_cast<float>(vh);
|
||||
|
||||
// Maintain aspect ratio of unscaled. Fit inside scaled
|
||||
// aspect by centering image.
|
||||
|
||||
if (rs > ru) // scaled is wider
|
||||
{
|
||||
vw = vh * ru;
|
||||
p += (scaled_rgba.width() - vw) / 2 * 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
vh = vw / ru;
|
||||
p += (scaled_rgba.height() - vh) / 2 * scaled_rgba.row_stride;
|
||||
}
|
||||
|
||||
// Curiously, this function doesn't care about channel order.
|
||||
libyuv::ARGBScale(
|
||||
rgba_->plane.data(),
|
||||
rgba_->row_stride,
|
||||
width(),
|
||||
height(),
|
||||
p,
|
||||
scaled_rgba.row_stride,
|
||||
vw,
|
||||
vh,
|
||||
libyuv::FilterMode::kFilterNone
|
||||
);
|
||||
|
||||
rgba_ = &scaled_rgba;
|
||||
}
|
||||
74
src/media/yuv420p.hpp
Normal file
74
src/media/yuv420p.hpp
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
// RING RACERS
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (C) 2023 by James Robert Roman
|
||||
//
|
||||
// This program is free software distributed under the
|
||||
// terms of the GNU General Public License, version 2.
|
||||
// See the 'LICENSE' file for more details.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef __SRB2_MEDIA_YUV420P_HPP__
|
||||
#define __SRB2_MEDIA_YUV420P_HPP__
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "video_frame.hpp"
|
||||
|
||||
namespace srb2::media
|
||||
{
|
||||
|
||||
class YUV420pFrame : public VideoFrame
|
||||
{
|
||||
public:
|
||||
// 32-byte aligned for AVX optimizations (see libyuv)
|
||||
static constexpr int kAlignment = 32;
|
||||
|
||||
class BufferRGBA : public VideoFrame::Buffer
|
||||
{
|
||||
public:
|
||||
bool resize(int width, int height); // true if resized
|
||||
|
||||
void erase(); // fills with black
|
||||
void release();
|
||||
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
|
||||
private:
|
||||
int width_, height_;
|
||||
|
||||
std::vector<uint8_t> vec_;
|
||||
};
|
||||
|
||||
YUV420pFrame(int pts, Buffer y, Buffer u, Buffer v, const BufferRGBA& rgba) :
|
||||
VideoFrame(pts), y_(y), u_(u), v_(v), rgba_(&rgba)
|
||||
{
|
||||
}
|
||||
|
||||
~YUV420pFrame() = default;
|
||||
|
||||
// Simply resets PTS and RGBA buffer while keeping YUV
|
||||
// buffers intact.
|
||||
void reset(int pts, const BufferRGBA& rgba) { *this = YUV420pFrame(pts, y_, u_, v_, rgba); }
|
||||
|
||||
// Converts RGBA buffer to YUV planes.
|
||||
void convert() const;
|
||||
|
||||
// Scales the existing buffer into a new one. This new
|
||||
// buffer replaces the existing one.
|
||||
void scale(const BufferRGBA& rgba);
|
||||
|
||||
virtual int width() const override { return rgba_->width(); }
|
||||
virtual int height() const override { return rgba_->height(); }
|
||||
|
||||
virtual const Buffer& rgba_buffer() const override;
|
||||
|
||||
private:
|
||||
Buffer y_, u_, v_;
|
||||
const BufferRGBA* rgba_;
|
||||
};
|
||||
|
||||
}; // namespace srb2::media
|
||||
|
||||
#endif // __SRB2_MEDIA_YUV420P_HPP__
|
||||
|
|
@ -72,7 +72,7 @@ static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh)
|
|||
continue;
|
||||
}
|
||||
|
||||
if (challengesmenu.extradata[i] & CHE_CONNECTEDLEFT)
|
||||
if (challengesmenu.extradata[i].flags & CHE_CONNECTEDLEFT)
|
||||
{
|
||||
// no need to check for CHE_CONNECTEDUP in linear iteration
|
||||
continue;
|
||||
|
|
@ -84,6 +84,16 @@ static void M_ChallengesAutoFocus(UINT8 unlockid, boolean fresh)
|
|||
challengesmenu.col = challengesmenu.hilix = i/CHALLENGEGRIDHEIGHT;
|
||||
challengesmenu.row = challengesmenu.hiliy = i%CHALLENGEGRIDHEIGHT;
|
||||
|
||||
// Begin animation
|
||||
if (challengesmenu.extradata[i].flip == 0)
|
||||
{
|
||||
challengesmenu.extradata[i].flip =
|
||||
(challengesmenu.pending
|
||||
? (TILEFLIP_MAX/2)
|
||||
: 1
|
||||
);
|
||||
}
|
||||
|
||||
if (fresh)
|
||||
{
|
||||
// We're just entering the menu. Immediately jump to the desired position...
|
||||
|
|
@ -174,7 +184,12 @@ menu_t *M_InterruptMenuWithChallenges(menu_t *desiredmenu)
|
|||
|
||||
M_PopulateChallengeGrid();
|
||||
if (gamedata->challengegrid)
|
||||
challengesmenu.extradata = M_ChallengeGridExtraData();
|
||||
{
|
||||
challengesmenu.extradata = Z_Calloc(
|
||||
(gamedata->challengegridwidth * CHALLENGEGRIDHEIGHT * sizeof(challengegridextradata_t)),
|
||||
PU_STATIC, NULL);
|
||||
M_UpdateChallengeGridExtraData(challengesmenu.extradata);
|
||||
}
|
||||
|
||||
memset(setup_explosions, 0, sizeof(setup_explosions));
|
||||
memset(&challengesmenu.unlockcount, 0, sizeof(challengesmenu.unlockcount));
|
||||
|
|
@ -256,7 +271,8 @@ void M_Challenges(INT32 choice)
|
|||
void M_ChallengesTick(void)
|
||||
{
|
||||
const UINT8 pid = 0;
|
||||
UINT8 i, newunlock = MAXUNLOCKABLES;
|
||||
UINT16 i;
|
||||
UINT8 newunlock = MAXUNLOCKABLES;
|
||||
|
||||
// Ticking
|
||||
challengesmenu.ticker++;
|
||||
|
|
@ -270,6 +286,29 @@ void M_ChallengesTick(void)
|
|||
challengesmenu.unlockcount[CC_ANIM]--;
|
||||
M_CupSelectTick();
|
||||
|
||||
// Update tile flip state.
|
||||
if (challengesmenu.extradata != NULL)
|
||||
{
|
||||
UINT16 id = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy;
|
||||
boolean seeeveryone = M_MenuButtonHeld(pid, MBT_R);
|
||||
boolean allthewaythrough;
|
||||
UINT8 maxflip;
|
||||
for (i = 0; i < (CHALLENGEGRIDHEIGHT * gamedata->challengegridwidth); i++)
|
||||
{
|
||||
allthewaythrough = (!seeeveryone && !challengesmenu.pending && i != id);
|
||||
maxflip = ((seeeveryone || !allthewaythrough) ? (TILEFLIP_MAX/2) : TILEFLIP_MAX);
|
||||
if ((seeeveryone || (challengesmenu.extradata[i].flip > 0))
|
||||
&& (challengesmenu.extradata[i].flip != maxflip))
|
||||
{
|
||||
challengesmenu.extradata[i].flip++;
|
||||
if (challengesmenu.extradata[i].flip >= TILEFLIP_MAX)
|
||||
{
|
||||
challengesmenu.extradata[i].flip = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (challengesmenu.pending)
|
||||
{
|
||||
// Pending mode.
|
||||
|
|
@ -318,11 +357,15 @@ void M_ChallengesTick(void)
|
|||
challengesmenu.unlockcount[CC_TALLY]++;
|
||||
challengesmenu.unlockcount[CC_ANIM]++;
|
||||
|
||||
Z_Free(challengesmenu.extradata);
|
||||
if ((challengesmenu.extradata = M_ChallengeGridExtraData()))
|
||||
if (challengesmenu.extradata)
|
||||
{
|
||||
unlockable_t *ref = &unlockables[challengesmenu.currentunlock];
|
||||
UINT16 bombcolor = SKINCOLOR_NONE;
|
||||
unlockable_t *ref;
|
||||
UINT16 bombcolor;
|
||||
|
||||
M_UpdateChallengeGridExtraData(challengesmenu.extradata);
|
||||
|
||||
ref = &unlockables[challengesmenu.currentunlock];
|
||||
bombcolor = SKINCOLOR_NONE;
|
||||
|
||||
if (ref->color != SKINCOLOR_NONE && ref->color < numskincolors)
|
||||
{
|
||||
|
|
@ -413,8 +456,7 @@ boolean M_ChallengesInputs(INT32 ch)
|
|||
gamedata->challengegrid = NULL;
|
||||
gamedata->challengegridwidth = 0;
|
||||
M_PopulateChallengeGrid();
|
||||
Z_Free(challengesmenu.extradata);
|
||||
challengesmenu.extradata = M_ChallengeGridExtraData();
|
||||
M_UpdateChallengeGridExtraData(challengesmenu.extradata);
|
||||
|
||||
M_ChallengesAutoFocus(challengesmenu.currentunlock, true);
|
||||
|
||||
|
|
@ -461,8 +503,8 @@ boolean M_ChallengesInputs(INT32 ch)
|
|||
}
|
||||
if (!(challengesmenu.extradata[
|
||||
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
|
||||
+ challengesmenu.row]
|
||||
& CHE_CONNECTEDUP))
|
||||
+ challengesmenu.row
|
||||
].flags & CHE_CONNECTEDUP))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
@ -475,8 +517,8 @@ boolean M_ChallengesInputs(INT32 ch)
|
|||
{
|
||||
i = (challengesmenu.extradata[
|
||||
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
|
||||
+ challengesmenu.row]
|
||||
& CHE_CONNECTEDUP) ? 2 : 1;
|
||||
+ challengesmenu.row
|
||||
].flags & CHE_CONNECTEDUP) ? 2 : 1;
|
||||
while (i > 0)
|
||||
{
|
||||
if (challengesmenu.row > 0)
|
||||
|
|
@ -516,8 +558,8 @@ boolean M_ChallengesInputs(INT32 ch)
|
|||
|
||||
if (!(challengesmenu.extradata[
|
||||
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
|
||||
+ challengesmenu.row]
|
||||
& CHE_CONNECTEDLEFT))
|
||||
+ challengesmenu.row
|
||||
].flags & CHE_CONNECTEDLEFT))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
|
@ -531,8 +573,8 @@ boolean M_ChallengesInputs(INT32 ch)
|
|||
{
|
||||
i = (challengesmenu.extradata[
|
||||
(challengesmenu.col * CHALLENGEGRIDHEIGHT)
|
||||
+ challengesmenu.row]
|
||||
& CHE_CONNECTEDLEFT) ? 2 : 1;
|
||||
+ challengesmenu.row
|
||||
].flags & CHE_CONNECTEDLEFT) ? 2 : 1;
|
||||
while (i > 0)
|
||||
{
|
||||
// Slide the focus counter to movement, if we can.
|
||||
|
|
@ -570,12 +612,12 @@ boolean M_ChallengesInputs(INT32 ch)
|
|||
{
|
||||
// Adjust highlight coordinates up/to the left for large tiles.
|
||||
|
||||
if (challengesmenu.hiliy > 0 && (challengesmenu.extradata[i] & CHE_CONNECTEDUP))
|
||||
if (challengesmenu.hiliy > 0 && (challengesmenu.extradata[i].flags & CHE_CONNECTEDUP))
|
||||
{
|
||||
challengesmenu.hiliy--;
|
||||
}
|
||||
|
||||
if ((challengesmenu.extradata[i] & CHE_CONNECTEDLEFT))
|
||||
if ((challengesmenu.extradata[i].flags & CHE_CONNECTEDLEFT))
|
||||
{
|
||||
if (challengesmenu.hilix > 0)
|
||||
{
|
||||
|
|
@ -586,8 +628,14 @@ boolean M_ChallengesInputs(INT32 ch)
|
|||
challengesmenu.hilix = gamedata->challengegridwidth-1;
|
||||
}
|
||||
}
|
||||
|
||||
i = (challengesmenu.hilix * CHALLENGEGRIDHEIGHT) + challengesmenu.hiliy;
|
||||
}
|
||||
|
||||
// Begin animation
|
||||
if (challengesmenu.extradata[i].flip == 0)
|
||||
challengesmenu.extradata[i].flip++;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -552,7 +552,8 @@ void P_TouchSpecialThing(mobj_t *special, mobj_t *toucher, boolean heightcheck)
|
|||
if (P_IsLocalPlayer(player) && !gamedata->collected[special->health-1])
|
||||
{
|
||||
gamedata->collected[special->health-1] = gotcollected = true;
|
||||
M_UpdateUnlockablesAndExtraEmblems(true);
|
||||
if (!M_UpdateUnlockablesAndExtraEmblems(true))
|
||||
S_StartSound(NULL, sfx_ncitem);
|
||||
G_SaveGameData();
|
||||
}
|
||||
|
||||
|
|
@ -797,7 +798,7 @@ void P_CheckPointLimit(void)
|
|||
if (!K_CanChangeRules(true))
|
||||
return;
|
||||
|
||||
if (!cv_pointlimit.value)
|
||||
if (!g_pointlimit)
|
||||
return;
|
||||
|
||||
if (!(gametyperules & GTR_POINTLIMIT))
|
||||
|
|
@ -810,7 +811,7 @@ void P_CheckPointLimit(void)
|
|||
if (G_GametypeHasTeams())
|
||||
{
|
||||
// Just check both teams
|
||||
if ((UINT32)cv_pointlimit.value <= redscore || (UINT32)cv_pointlimit.value <= bluescore)
|
||||
if (g_pointlimit <= redscore || g_pointlimit <= bluescore)
|
||||
{
|
||||
if (server)
|
||||
SendNetXCmd(XD_EXITLEVEL, NULL, 0);
|
||||
|
|
@ -823,7 +824,7 @@ void P_CheckPointLimit(void)
|
|||
if (!playeringame[i] || players[i].spectator)
|
||||
continue;
|
||||
|
||||
if ((UINT32)cv_pointlimit.value <= players[i].roundscore)
|
||||
if (g_pointlimit <= players[i].roundscore)
|
||||
{
|
||||
for (i = 0; i < MAXPLAYERS; i++) // AAAAA nested loop using the same iteration variable ;;
|
||||
{
|
||||
|
|
|
|||
|
|
@ -395,6 +395,8 @@ static void P_NetArchivePlayers(savebuffer_t *save)
|
|||
|
||||
WRITEUINT8(save->p, players[i].eggmanTransferDelay);
|
||||
|
||||
WRITEUINT8(save->p, players[i].tripwireReboundDelay);
|
||||
|
||||
// respawnvars_t
|
||||
WRITEUINT8(save->p, players[i].respawn.state);
|
||||
WRITEUINT32(save->p, K_GetWaypointHeapIndex(players[i].respawn.wp));
|
||||
|
|
@ -750,6 +752,8 @@ static void P_NetUnArchivePlayers(savebuffer_t *save)
|
|||
|
||||
players[i].eggmanTransferDelay = READUINT8(save->p);
|
||||
|
||||
players[i].tripwireReboundDelay = READUINT8(save->p);
|
||||
|
||||
// respawnvars_t
|
||||
players[i].respawn.state = READUINT8(save->p);
|
||||
players[i].respawn.wp = (waypoint_t *)(size_t)READUINT32(save->p);
|
||||
|
|
@ -4984,6 +4988,8 @@ static void P_NetArchiveMisc(savebuffer_t *save, boolean resending)
|
|||
WRITEUINT32(save->p, extratimeintics);
|
||||
WRITEUINT32(save->p, secretextratime);
|
||||
|
||||
WRITEUINT32(save->p, g_pointlimit);
|
||||
|
||||
// Is it paused?
|
||||
if (paused)
|
||||
WRITEUINT8(save->p, 0x2f);
|
||||
|
|
@ -5154,6 +5160,8 @@ static inline boolean P_NetUnArchiveMisc(savebuffer_t *save, boolean reloading)
|
|||
extratimeintics = READUINT32(save->p);
|
||||
secretextratime = READUINT32(save->p);
|
||||
|
||||
g_pointlimit = READUINT32(save->p);
|
||||
|
||||
// Is it paused?
|
||||
if (READUINT8(save->p) == 0x2f)
|
||||
paused = true;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
#include "../audio/sound_effect_player.hpp"
|
||||
#include "../cxxutil.hpp"
|
||||
#include "../io/streams.hpp"
|
||||
#include "../m_avrecorder.hpp"
|
||||
|
||||
#include "../doomdef.h"
|
||||
#include "../i_sound.h"
|
||||
|
|
@ -57,6 +58,8 @@ static shared_ptr<Gain<2>> gain_music;
|
|||
|
||||
static vector<shared_ptr<SoundEffectPlayer>> sound_effect_channels;
|
||||
|
||||
static shared_ptr<srb2::media::AVRecorder> av_recorder;
|
||||
|
||||
static void (*music_fade_callback)();
|
||||
|
||||
void* I_GetSfx(sfxinfo_t* sfx)
|
||||
|
|
@ -135,6 +138,9 @@ void audio_callback(void* userdata, Uint8* buffer, int len)
|
|||
std::clamp(float_buffer[i].amplitudes[1], -1.f, 1.f),
|
||||
};
|
||||
}
|
||||
|
||||
if (av_recorder)
|
||||
av_recorder->push_audio_samples(tcb::span {float_buffer, float_len});
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
|
|
@ -749,3 +755,11 @@ boolean I_FadeInPlaySong(UINT32 ms, boolean looping)
|
|||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
void I_UpdateAudioRecorder(void)
|
||||
{
|
||||
// must be locked since av_recorder is used by audio_callback
|
||||
SdlAudioLockHandle _;
|
||||
|
||||
av_recorder = g_av_recorder;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1101,6 +1101,7 @@ sfxinfo_t S_sfx[NUMSFX] =
|
|||
{"typri1", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SA2 boss typewriting 1
|
||||
{"typri2", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // SA2 final boss-type typewriting
|
||||
{"eggspr", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // Sonic Unleashed Trap Spring
|
||||
{"achiev", false, 204, 0, -1, NULL, 0, -1, -1, LUMPERROR, "Achievement"},
|
||||
|
||||
// SRB2Kart - Drop target sounds
|
||||
{"kdtrg1", false, 64, 16, -1, NULL, 0, -1, -1, LUMPERROR, ""}, // Low energy, SF_X8AWAYSOUND
|
||||
|
|
|
|||
|
|
@ -1168,6 +1168,7 @@ typedef enum
|
|||
sfx_typri1,
|
||||
sfx_typri2,
|
||||
sfx_eggspr,
|
||||
sfx_achiev,
|
||||
|
||||
// SRB2Kart - Drop target sounds
|
||||
sfx_kdtrg1,
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ TYPEDEF (conditionset_t);
|
|||
TYPEDEF (emblem_t);
|
||||
TYPEDEF (unlockable_t);
|
||||
TYPEDEF (gamedata_t);
|
||||
TYPEDEF (challengegridextradata_t);
|
||||
|
||||
// m_dllist.h
|
||||
TYPEDEF (mdllistitem_t);
|
||||
|
|
|
|||
5
thirdparty/CMakeLists.txt
vendored
5
thirdparty/CMakeLists.txt
vendored
|
|
@ -15,6 +15,9 @@ include("cpm-sdl2.cmake")
|
|||
include("cpm-png.cmake")
|
||||
include("cpm-curl.cmake")
|
||||
include("cpm-libgme.cmake")
|
||||
include("cpm-libvpx.cmake")
|
||||
include("cpm-ogg.cmake") # libvorbis depends
|
||||
include("cpm-libvorbis.cmake")
|
||||
endif()
|
||||
|
||||
include("cpm-rapidjson.cmake")
|
||||
|
|
@ -23,6 +26,8 @@ include("cpm-xmp-lite.cmake")
|
|||
include("cpm-fmt.cmake")
|
||||
include("cpm-imgui.cmake")
|
||||
include("cpm-acsvm.cmake")
|
||||
include("cpm-libwebm.cmake")
|
||||
include("cpm-libyuv.cmake")
|
||||
|
||||
add_subdirectory(tcbrindle_span)
|
||||
add_subdirectory(stb_vorbis)
|
||||
|
|
|
|||
11
thirdparty/cpm-libvorbis.cmake
vendored
Normal file
11
thirdparty/cpm-libvorbis.cmake
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
CPMAddPackage(
|
||||
NAME vorbis
|
||||
VERSION 1.3.7
|
||||
URL "https://github.com/xiph/vorbis/releases/download/v1.3.7/libvorbis-1.3.7.zip"
|
||||
EXCLUDE_FROM_ALL ON
|
||||
)
|
||||
|
||||
if(vorbis_ADDED)
|
||||
add_library(Vorbis::vorbis ALIAS vorbis)
|
||||
add_library(Vorbis::vorbisenc ALIAS vorbisenc)
|
||||
endif()
|
||||
37
thirdparty/cpm-libvpx.cmake
vendored
Normal file
37
thirdparty/cpm-libvpx.cmake
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
CPMAddPackage(
|
||||
NAME libvpx
|
||||
VERSION 1.12.0
|
||||
URL "https://chromium.googlesource.com/webm/libvpx/+archive/03265cd42b3783532de72f2ded5436652e6f5ce3.tar.gz"
|
||||
EXCLUDE_FROM_ALL ON
|
||||
DOWNLOAD_ONLY YES
|
||||
)
|
||||
|
||||
if(libvpx_ADDED)
|
||||
include(ExternalProject)
|
||||
|
||||
# libvpx configure script does CPU detection. So lets just
|
||||
# call it instead of trying to do all that in CMake.
|
||||
ExternalProject_Add(libvpx
|
||||
PREFIX "${libvpx_BINARY_DIR}"
|
||||
SOURCE_DIR "${libvpx_SOURCE_DIR}"
|
||||
BINARY_DIR "${libvpx_BINARY_DIR}"
|
||||
CONFIGURE_COMMAND sh "${libvpx_SOURCE_DIR}/configure"
|
||||
--enable-vp8 --disable-vp9 --disable-vp8-decoder
|
||||
--disable-examples --disable-tools --disable-docs
|
||||
--disable-webm-io --disable-libyuv --disable-unit-tests
|
||||
BUILD_COMMAND "make"
|
||||
BUILD_BYPRODUCTS "${libvpx_BINARY_DIR}/libvpx.a"
|
||||
INSTALL_COMMAND ""
|
||||
USES_TERMINAL_CONFIGURE ON
|
||||
USES_TERMINAL_BUILD ON
|
||||
)
|
||||
|
||||
add_library(webm::libvpx STATIC IMPORTED GLOBAL)
|
||||
add_dependencies(webm::libvpx libvpx)
|
||||
set_target_properties(
|
||||
webm::libvpx
|
||||
PROPERTIES
|
||||
IMPORTED_LOCATION "${libvpx_BINARY_DIR}/libvpx.a"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${libvpx_SOURCE_DIR}"
|
||||
)
|
||||
endif()
|
||||
31
thirdparty/cpm-libwebm.cmake
vendored
Normal file
31
thirdparty/cpm-libwebm.cmake
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
CPMAddPackage(
|
||||
NAME libwebm
|
||||
VERSION 1.0.0.29
|
||||
URL "https://chromium.googlesource.com/webm/libwebm/+archive/2f9fc054ab9547ca06071ec68dab9d54960abb2e.tar.gz"
|
||||
EXCLUDE_FROM_ALL ON
|
||||
DOWNLOAD_ONLY YES
|
||||
)
|
||||
|
||||
if(libwebm_ADDED)
|
||||
set(libwebm_SOURCES
|
||||
|
||||
common/file_util.cc
|
||||
common/file_util.h
|
||||
common/hdr_util.cc
|
||||
common/hdr_util.h
|
||||
common/webmids.h
|
||||
|
||||
mkvmuxer/mkvmuxer.cc
|
||||
mkvmuxer/mkvmuxer.h
|
||||
mkvmuxer/mkvmuxertypes.h
|
||||
mkvmuxer/mkvmuxerutil.cc
|
||||
mkvmuxer/mkvmuxerutil.h
|
||||
mkvmuxer/mkvwriter.cc
|
||||
mkvmuxer/mkvwriter.h
|
||||
)
|
||||
list(TRANSFORM libwebm_SOURCES PREPEND "${libwebm_SOURCE_DIR}/")
|
||||
add_library(webm STATIC ${libwebm_SOURCES})
|
||||
target_include_directories(webm PUBLIC "${libwebm_SOURCE_DIR}")
|
||||
target_compile_features(webm PRIVATE cxx_std_11)
|
||||
add_library(webm::libwebm ALIAS webm)
|
||||
endif()
|
||||
77
thirdparty/cpm-libyuv.cmake
vendored
Normal file
77
thirdparty/cpm-libyuv.cmake
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
CPMAddPackage(
|
||||
NAME libyuv
|
||||
VERSION 0
|
||||
URL "https://chromium.googlesource.com/libyuv/libyuv/+archive/b2528b0be934de1918e20c85fc170d809eeb49ab.tar.gz"
|
||||
EXCLUDE_FROM_ALL ON
|
||||
DOWNLOAD_ONLY YES
|
||||
)
|
||||
|
||||
if(libyuv_ADDED)
|
||||
set(libyuv_SOURCES
|
||||
|
||||
# Headers
|
||||
include/libyuv.h
|
||||
include/libyuv/basic_types.h
|
||||
include/libyuv/compare.h
|
||||
include/libyuv/convert.h
|
||||
include/libyuv/convert_argb.h
|
||||
include/libyuv/convert_from.h
|
||||
include/libyuv/convert_from_argb.h
|
||||
include/libyuv/cpu_id.h
|
||||
include/libyuv/mjpeg_decoder.h
|
||||
include/libyuv/planar_functions.h
|
||||
include/libyuv/rotate.h
|
||||
include/libyuv/rotate_argb.h
|
||||
include/libyuv/rotate_row.h
|
||||
include/libyuv/row.h
|
||||
include/libyuv/scale.h
|
||||
include/libyuv/scale_argb.h
|
||||
include/libyuv/scale_rgb.h
|
||||
include/libyuv/scale_row.h
|
||||
include/libyuv/scale_uv.h
|
||||
include/libyuv/version.h
|
||||
include/libyuv/video_common.h
|
||||
|
||||
# Source Files
|
||||
source/compare.cc
|
||||
source/compare_common.cc
|
||||
source/compare_gcc.cc
|
||||
source/compare_win.cc
|
||||
source/convert.cc
|
||||
source/convert_argb.cc
|
||||
source/convert_from.cc
|
||||
source/convert_from_argb.cc
|
||||
source/convert_jpeg.cc
|
||||
source/convert_to_argb.cc
|
||||
source/convert_to_i420.cc
|
||||
source/cpu_id.cc
|
||||
source/mjpeg_decoder.cc
|
||||
source/mjpeg_validate.cc
|
||||
source/planar_functions.cc
|
||||
source/rotate.cc
|
||||
source/rotate_any.cc
|
||||
source/rotate_argb.cc
|
||||
source/rotate_common.cc
|
||||
source/rotate_gcc.cc
|
||||
source/rotate_win.cc
|
||||
source/row_any.cc
|
||||
source/row_common.cc
|
||||
source/row_gcc.cc
|
||||
source/row_win.cc
|
||||
source/scale.cc
|
||||
source/scale_any.cc
|
||||
source/scale_argb.cc
|
||||
source/scale_common.cc
|
||||
source/scale_gcc.cc
|
||||
source/scale_rgb.cc
|
||||
source/scale_uv.cc
|
||||
source/scale_win.cc
|
||||
source/video_common.cc
|
||||
)
|
||||
list(TRANSFORM libyuv_SOURCES PREPEND "${libyuv_SOURCE_DIR}/")
|
||||
add_library(yuv STATIC ${libyuv_SOURCES})
|
||||
|
||||
target_include_directories(yuv PUBLIC "${libyuv_SOURCE_DIR}/include")
|
||||
|
||||
add_library(libyuv::libyuv ALIAS yuv)
|
||||
endif()
|
||||
13
thirdparty/cpm-ogg.cmake
vendored
Normal file
13
thirdparty/cpm-ogg.cmake
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
CPMAddPackage(
|
||||
NAME ogg
|
||||
VERSION 1.3.5
|
||||
URL "https://github.com/xiph/ogg/releases/download/v1.3.5/libogg-1.3.5.zip"
|
||||
EXCLUDE_FROM_ALL ON
|
||||
)
|
||||
|
||||
if(ogg_ADDED)
|
||||
# Fixes bug with find_package not being able to find
|
||||
# ogg when cross-building.
|
||||
set(OGG_INCLUDE_DIR "${ogg_SOURCE_DIR}/include")
|
||||
set(OGG_LIBRARY Ogg:ogg)
|
||||
endif()
|
||||
Loading…
Add table
Reference in a new issue