Audio sync rewritten. Pause fixed. Video decoding performance improvements. Ability to have disc-less games.

This commit is contained in:
Scott Duensing 2026-09-02 18:13:06 -05:00
parent f6d63b6eb0
commit 34d61d957a
11 changed files with 486 additions and 108 deletions

View file

@ -15,6 +15,15 @@ API Changes
LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from
Singe 2.10" in the manual. Singe 2.10" in the manual.
- Singe is no longer laserdisc only. A game declares a disc with
DISC = true in games.dat (or --disc / --framefile on the command line);
otherwise it runs without one, on a canvas sized by CANVAS_X/CANVAS_Y or
--canvas=WxH (default 720x480), the disc functions become harmless, and
discGetState() returns the new DISC_EJECTED.
Every existing games.dat entry needs DISC = true added; the menu skips
an entry with VIDEO but no DISC = true and says why, and the engine
refuses one if launched directly. Scripts can test SINGE_DISC.
- mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old - mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old
mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are
now aliases defined in Framework.singe. now aliases defined in Framework.singe.
@ -44,6 +53,13 @@ API Changes
pause, quit, screenshot, or grab now work in MODE_FULL too; keyboard pause, quit, screenshot, or grab now work in MODE_FULL too; keyboard
mappings stay raw there. mappings stay raw there.
- Video decoding runs on its own thread per video, so a discSearch() far
from a keyframe no longer stalls the game loop; the previous frame stays
on screen until the new one is ready. The laserdisc and framefile videos
are handed to the GPU as YUV and converted there instead of on the CPU,
and each video reports its keyframe spacing when loaded, with a warning
when seeks may be slow.
- Constants that used to be duplicated in Framework.singe (SWITCH_*, - Constants that used to be duplicated in Framework.singe (SWITCH_*,
FONT_QUALITY_*, MODE_*, MOUSE_*, OVERLAY_*, RENDER_*, SOUND_ERROR_*) are FONT_QUALITY_*, MODE_*, MOUSE_*, OVERLAY_*, RENDER_*, SOUND_ERROR_*) are
now defined by the engine. New: DISC_STOPPED/DISC_PLAYING/DISC_PAUSED now defined by the engine. New: DISC_STOPPED/DISC_PLAYING/DISC_PAUSED

View file

@ -301,11 +301,21 @@ for dir in lfs.dir(".") do
for file in lfs.dir(dir .. "/.") do for file in lfs.dir(dir .. "/.") do
if file == "games.dat" then if file == "games.dat" then
-- Load games.dat -- Load games.dat
GAMES = {}
dofile(dir .. "/games.dat") dofile(dir .. "/games.dat")
for key,value in pairs(GAMES) do for _, value in pairs(GAMES or {}) do
-- Since 2.20 a laserdisc game must say DISC = true; refuse the ambiguous cases here
-- with a message rather than letting the engine stop the menu when it is picked.
local title = tostring(value.TITLE or value.SCRIPT or "?")
if value.VIDEO and not value.DISC then
debugPrint(dir .. "/games.dat: \"" .. title .. "\" names a VIDEO without DISC = true; add DISC = true to the entry. Skipped.")
elseif value.DISC and not value.VIDEO then
debugPrint(dir .. "/games.dat: \"" .. title .. "\" says DISC = true but has no VIDEO. Skipped.")
else
table.insert(GAME_LIST, value) table.insert(GAME_LIST, value)
GAME_COUNT = GAME_COUNT + 1 GAME_COUNT = GAME_COUNT + 1
end end
end
GAMES = {} GAMES = {}
end end
end end

View file

@ -107,15 +107,17 @@ Singe [OPTIONS] scriptName{.singe}
The script name is the only required argument. It may be a `.singe` file, or The script name is the only required argument. It may be a `.singe` file, or
a directory containing a script of the same name (`ActionMax` finds a directory containing a script of the same name (`ActionMax` finds
`ActionMax/ActionMax.singe`). When no `--framefile` is given, Singe looks for `ActionMax/ActionMax.singe`). For a laserdisc game (`--disc`) with no
a video next to the script with the same base name and any extension FFmpeg `--framefile`, Singe looks for a video next to the script with the same base
can demux, then for a `.txt` framefile. name and any extension FFmpeg can demux, then for a `.txt` framefile.
[cols="1,2",options="header"] [cols="1,2",options="header"]
|=== |===
| Option | Purpose | Option | Purpose
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`). | `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging. | `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
| `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc.
| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored.
| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it. Defaults to the game's own directory. | `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it. Defaults to the game's own directory.
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`. | `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution. | `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
@ -533,6 +535,7 @@ GAMES = {
{ {
TITLE = ".38 Ambush Alley", TITLE = ".38 Ambush Alley",
SCRIPT = "ActionMax/38AmbushAlley.singe", SCRIPT = "ActionMax/38AmbushAlley.singe",
DISC = true,
VIDEO = "ActionMax/frame_38AmbushAlley.txt", VIDEO = "ActionMax/frame_38AmbushAlley.txt",
DATA = "ActionMax", DATA = "ActionMax",
STRETCH = false, STRETCH = false,
@ -558,14 +561,38 @@ GAMES = {
} }
---- ----
The keys `SCRIPT`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`, The keys `SCRIPT`, `DISC`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`,
`RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and `LEGACY_SPRITE_ARGS` are `NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and
read by the engine when the menu (or your own script, through `LEGACY_SPRITE_ARGS` are read by the engine when the menu (or your own
`scriptExecute` / `scriptPush`) launches the entry; they override the script, through `scriptExecute` / `scriptPush`) launches the entry; they
command line. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10 override the command line. A laserdisc game must say `DISC = true` and name
its `VIDEO`; an entry with a `VIDEO` but no `DISC = true` is refused with a
message, as is `DISC = true` without a `VIDEO`. A game without a disc leaves
both out and may set `CANVAS_X` / `CANVAS_Y` (default 720x480); see
<<withoutadisc,Games Without a Disc>>. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10
with the old sprite argument order (see <<migrating,Migrating from Singe with the old sprite argument order (see <<migrating,Migrating from Singe
2.10>>). The remaining keys are read by the menu for display. 2.10>>). The remaining keys are read by the menu for display.
[#withoutadisc]
=== Games Without a Disc
Singe no longer assumes a laserdisc. A game is a laserdisc game only when it
says so: `DISC = true` in its `games.dat` entry, or `--disc` (or a
`--framefile`) on the command line. Everything else runs without a disc, and
the world is a *canvas* instead of a video frame: `CANVAS_X` / `CANVAS_Y` in
`games.dat` or `--canvas=WxH` on the command line, 720x480 by default so a
discless game looks like a standard definition laserdisc game unless it
asks otherwise.
Without a disc the engine draws on black, the overlay defaults to half the
canvas as usual, and the `disc*` and `vldp*` functions become harmless:
`discGetState` reports `DISC_EJECTED`, `discGetFrame` returns `0`,
`discGetWidth` / `discGetHeight` return the canvas, `vldpGetPixel` returns
black, and the transport calls trace and do nothing. Everything else -- the
overlay, sprites, fonts, sounds, extra videos through `videoLoad`, input,
and both programming models -- is identical. The global `SINGE_DISC` tells a
script which kind of game it is running as.
[#migrating] [#migrating]
=== Migrating from Singe 2.10 === Migrating from Singe 2.10
@ -608,6 +635,11 @@ changed shape, but a few behaviors did:
* `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` / * `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` /
`singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and `singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and
`singeSetPauseKeyEnabled`. `singeSetPauseKeyEnabled`.
* A laserdisc game must now declare itself: add `DISC = true` next to
`VIDEO` in every `games.dat` entry, and pass `--disc` (or `--framefile`)
on the command line. Without it the game runs without a disc, and a
`games.dat` entry that names a `VIDEO` without `DISC = true` is refused
with a message naming the fix. See <<withoutadisc,Games Without a Disc>>.
* The pause key now pauses the whole game, not just the media: the script * The pause key now pauses the whole game, not just the media: the script
is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered
to the script only when the key has been disabled. It acts on the key to the script only when the key has been disabled. It acts on the key
@ -627,11 +659,12 @@ available to `controls.cfg` and to `Framework.singe` alike:
| `MOUSE_SINGLE`, `MOUSE_MANY` (also `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments for `mouseSetMode`. | `MOUSE_SINGLE`, `MOUSE_MANY` (also `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments for `mouseSetMode`.
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Return values for `onOverlayUpdate`. | `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Return values for `onOverlayUpdate`.
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`. | `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`.
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED` | Return values of `discGetState`. | `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Return values of `discGetState`.
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free. | `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free.
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v2.20"`), and as the number `singeVersion()` returns. | `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v2.20"`), and as the number `singeVersion()` returns.
| `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`. | `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`.
| `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order. | `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order.
| `SINGE_DISC` | True when the game has a laserdisc; false when the canvas is the world.
| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE` | Layout of the controller and mouse input codes; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them. | `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE` | Layout of the controller and mouse input codes; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them.
|=== |===
@ -671,7 +704,12 @@ format, channel count, and rate.
The first time a video is opened, Singe indexes it and stores the index next The first time a video is opened, Singe indexes it and stores the index next
to the game's other data (`<name>.index`). Indexing takes a while for large to the game's other data (`<name>.index`). Indexing takes a while for large
files and happens again if the video changes. files and happens again if the video changes. When a video is loaded, Singe
reports its keyframe spacing in the program trace and prints a warning if
keyframes are more than two seconds apart, because a seek has to decode
forward from the previous keyframe. Decoding happens on a separate thread,
so a slow seek shows the previous frame a little longer instead of stalling
the game.
For laserdisc footage the constraints are frame accuracy and seek speed, not For laserdisc footage the constraints are frame accuracy and seek speed, not
compression. H.264 in MP4 or MKV with a short keyframe interval (one or two compression. H.264 in MP4 or MKV with a short keyframe interval (one or two
@ -1045,6 +1083,7 @@ Returns an integer describing the playback state of the disc.
| `DISC_STOPPED` | `2` | Stopped (`discStop` was called) | `DISC_STOPPED` | `2` | Stopped (`discStop` was called)
| `DISC_PLAYING` | `3` | Playing | `DISC_PLAYING` | `3` | Playing
| `DISC_PAUSED` | `4` | Paused | `DISC_PAUSED` | `4` | Paused
| `DISC_EJECTED` | `5` | The game has no disc (see <<withoutadisc,Games Without a Disc>>)
|=== |===
Singe's lightweight player has no distinct searching state; a disc that is seeking reports `DISC_PAUSED`. Singe's lightweight player has no distinct searching state; a disc that is seeking reports `DISC_PAUSED`.

View file

@ -13,6 +13,14 @@ ActionMax/Emulator.singe
lengthIntro, lengthGame, lengthMenu, highThreshold, lowThreshold, lengthIntro, lengthGame, lengthMenu, highThreshold, lowThreshold,
sensorX, sensorY, sensorLeft, and sensorTop before loading it. sensorX, sensorY, sensorLeft, and sensorTop before loading it.
The ActionMax games.dat is not part of this repository. Since Singe
2.20 every laserdisc entry in it needs one added line, next to VIDEO:
DISC = true,
Entries without it are skipped by the menu with a message, and an entry
that names a VIDEO without DISC = true is refused by the engine.
daitarn_3_singe/Script/toolbox.singe daitarn_3_singe/Script/toolbox.singe
Helper library from "Daitarn 3" (Karis, 2020). The calling script must Helper library from "Daitarn 3" (Karis, 2020). The calling script must
define OVLW, OVLH, and bPause. define OVLW, OVLH, and bPause.

View file

@ -216,7 +216,7 @@ int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer
audio = NULL; audio = NULL;
} }
} }
files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer); files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer, false);
free(audio); free(audio);
count++; count++;
} }

View file

@ -124,6 +124,8 @@ static const OptionT _options[] = {
{ 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false }, { 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false },
{ 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", true }, { 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", true },
{ 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false }, { 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false },
{ 'C', "canvas", ap_yes, "WxH", "world size for games without a disc (default 720x480)", false },
{ 'D', "disc", ap_no, NULL, "play a laserdisc video (implied by --framefile)", false },
{ 'd', "datadir", ap_yes, "PATHNAME", "alternate location for written files", false }, { 'd', "datadir", ap_yes, "PATHNAME", "alternate location for written files", false },
{ 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false }, { 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false },
{ 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false }, { 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false },
@ -540,6 +542,7 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
int32_t aspectDom = -1; int32_t aspectDom = -1;
int32_t *target = NULL; int32_t *target = NULL;
char *aspectString = NULL; char *aspectString = NULL;
char *canvasString = NULL;
char *sindenString = NULL; char *sindenString = NULL;
char *temp = NULL; char *temp = NULL;
const char *arg = NULL; const char *arg = NULL;
@ -574,6 +577,8 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
conf->volumeNonVldp = VOLUME_MAX; conf->volumeNonVldp = VOLUME_MAX;
conf->scaleFactor = SCALE_FACTOR_MAX; conf->scaleFactor = SCALE_FACTOR_MAX;
conf->resolutionWasCalculated = true; conf->resolutionWasCalculated = true;
conf->canvasWidth = CANVAS_DEFAULT_WIDTH;
conf->canvasHeight = CANVAS_DEFAULT_HEIGHT;
// Parse command line // Parse command line
for (argIndex = 0; argIndex < ap_arguments(&parser); argIndex++) { for (argIndex = 0; argIndex < ap_arguments(&parser); argIndex++) {
@ -608,6 +613,17 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
conf->showCalculated = true; conf->showCalculated = true;
break; break;
// Canvas size
case 'C':
free(canvasString);
canvasString = strdup(arg);
break;
// Laserdisc
case 'D':
conf->disc = true;
break;
// Data Dir // Data Dir
case 'd': case 'd':
free(conf->dataDir); free(conf->dataDir);
@ -680,10 +696,11 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
conf->stretchVideo = true; conf->stretchVideo = true;
break; break;
// Video File // Video File - a video means a disc.
case 'v': case 'v':
free(conf->videoFile); free(conf->videoFile);
conf->videoFile = strdup(arg); conf->videoFile = strdup(arg);
conf->disc = true;
break; break;
// Full Screen Windowed // Full Screen Windowed
@ -778,6 +795,19 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
free(aspectString); free(aspectString);
} }
// Did they specify a canvas size?
if (canvasString) {
temp = strchr(canvasString, 'x');
if (temp == NULL) {
_showUsage(exeName, "Canvas size must be WIDTHxHEIGHT, for example 640x480.");
}
*temp = 0;
if (!_parseInteger(canvasString, &conf->canvasWidth) || !_parseInteger(temp + 1, &conf->canvasHeight) || (conf->canvasWidth <= 0) || (conf->canvasHeight <= 0)) {
_showUsage(exeName, "Canvas size must be WIDTHxHEIGHT, for example 640x480.");
}
free(canvasString);
}
return conf; return conf;
} }
@ -849,11 +879,17 @@ static void _resolveFiles(const char *exeName, ConfigT *conf) {
} }
} }
free(temp); free(temp);
// A game is only a laserdisc game when it says so.
if (!conf->disc && conf->videoFile) {
utilSay("Note: %s found but --disc was not given; running without a disc.", conf->videoFile);
free(conf->videoFile);
conf->videoFile = NULL;
} }
if (!conf->videoFile) { }
if (conf->disc && !conf->videoFile) {
_showUsage(exeName, "Unable to locate video."); _showUsage(exeName, "Unable to locate video.");
} }
conf->isFrameFile = isFrameFileName(conf->videoFile); conf->isFrameFile = conf->disc && isFrameFileName(conf->videoFile);
if (conf->dataDir) { if (conf->dataDir) {
// They provided a data directory. Append the game name. // They provided a data directory. Append the game name.

View file

@ -33,6 +33,8 @@
#define SCALE_FACTOR_MIN 50 #define SCALE_FACTOR_MIN 50
#define SCALE_FACTOR_MAX 100 #define SCALE_FACTOR_MAX 100
#define DIRECTORY_MODE 0777 #define DIRECTORY_MODE 0777
#define CANVAS_DEFAULT_WIDTH 720
#define CANVAS_DEFAULT_HEIGHT 480
ConfigT *cloneConf(const ConfigT *conf); ConfigT *cloneConf(const ConfigT *conf);

View file

@ -139,7 +139,8 @@ typedef enum MouseModeE {
typedef enum DiscStateE { typedef enum DiscStateE {
DISC_STOPPED = 2, DISC_STOPPED = 2,
DISC_PLAYING = 3, DISC_PLAYING = 3,
DISC_PAUSED = 4 DISC_PAUSED = 4,
DISC_EJECTED = 5 // The game has no disc at all
} DiscStateE; } DiscStateE;
typedef enum FontQualityE { typedef enum FontQualityE {
@ -290,7 +291,9 @@ typedef struct GlobalS {
int32_t keyboardLastDown; int32_t keyboardLastDown;
int32_t keyboardLastUp; int32_t keyboardLastUp;
int32_t frameFileHandle; int32_t frameFileHandle;
int32_t videoHandle; int32_t videoHandle; // -1 when the game has no disc
int32_t canvasWidth; // World size: the disc's, or the configured canvas
int32_t canvasHeight;
FontQualityE fontQuality; FontQualityE fontQuality;
MouseModeE mouseMode; MouseModeE mouseMode;
int32_t mouseCount; int32_t mouseCount;
@ -718,8 +721,12 @@ static ConfigT *_buildConfFromTable(lua_State *L) {
int64_t valueNumber = 0; int64_t valueNumber = 0;
ConfigT *c = NULL; ConfigT *c = NULL;
// Start with current config. // Start with current config, but every entry declares its own disc.
c = cloneConf(_global.conf); c = cloneConf(_global.conf);
c->disc = false;
c->isFrameFile = false;
free(c->videoFile);
c->videoFile = NULL;
// Update with data in the table on the top of the Lua stack. // Update with data in the table on the top of the Lua stack.
lua_pushnil(L); lua_pushnil(L);
@ -784,12 +791,29 @@ static ConfigT *_buildConfFromTable(lua_State *L) {
c->audioOutputTrack = (int32_t)valueNumber; c->audioOutputTrack = (int32_t)valueNumber;
} else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) { } else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) {
c->legacySpriteArgs = valueBoolean; c->legacySpriteArgs = valueBoolean;
} else if (strcmp(confKey, "DISC") == 0) {
c->disc = valueBoolean;
} else if (strcmp(confKey, "CANVAS_X") == 0) {
c->canvasWidth = (int32_t)valueNumber;
} else if (strcmp(confKey, "CANVAS_Y") == 0) {
c->canvasHeight = (int32_t)valueNumber;
} }
// Clean up for next pair // Clean up for next pair
lua_pop(L, 1); lua_pop(L, 1);
} }
// A laserdisc game says DISC = true and names its VIDEO; anything else is a mistake worth stopping for.
if (c->disc && (c->videoFile == NULL)) {
utilDie("%s: DISC = true but no VIDEO given.", c->scriptFile);
}
if (!c->disc && (c->videoFile != NULL)) {
utilDie("%s: VIDEO given without DISC = true. Add DISC = true to the games.dat entry for a laserdisc game.", c->scriptFile);
}
if ((c->canvasWidth <= 0) || (c->canvasHeight <= 0)) {
utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile);
}
// Create new data dir location based on script location. // Create new data dir location based on script location.
free(c->dataDir); free(c->dataDir);
c->dataDir = createDataDir(c->dataDirBase, c->scriptFile); c->dataDir = createDataDir(c->dataDirBase, c->scriptFile);
@ -1591,6 +1615,8 @@ static void _pushConstants(lua_State *L) {
lua_setglobal(L, "DISC_PLAYING"); lua_setglobal(L, "DISC_PLAYING");
lua_pushinteger(L, DISC_PAUSED); lua_pushinteger(L, DISC_PAUSED);
lua_setglobal(L, "DISC_PAUSED"); lua_setglobal(L, "DISC_PAUSED");
lua_pushinteger(L, DISC_EJECTED);
lua_setglobal(L, "DISC_EJECTED");
lua_pushinteger(L, -1); lua_pushinteger(L, -1);
lua_setglobal(L, "SOUND_ERROR_INVALID"); lua_setglobal(L, "SOUND_ERROR_INVALID");
@ -1628,6 +1654,8 @@ static void _pushConstants(lua_State *L) {
lua_setglobal(L, "SINGE_DEAD_ZONE"); lua_setglobal(L, "SINGE_DEAD_ZONE");
lua_pushboolean(L, _global.conf->legacySpriteArgs); lua_pushboolean(L, _global.conf->legacySpriteArgs);
lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS"); lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS");
lua_pushboolean(L, _global.conf->disc);
lua_setglobal(L, "SINGE_DISC");
} }
@ -1918,7 +1946,7 @@ static void _takeScreenshot(void) {
static void _updatePauseState(void) { static void _updatePauseState(void) {
if (_global.pauseState) { if (_global.pauseState) {
// Pause laserdisc // Pause laserdisc
if (!_global.discStopped && videoIsPlaying(_global.videoHandle)) { if ((_global.videoHandle >= 0) && !_global.discStopped && videoIsPlaying(_global.videoHandle)) {
_global.wasPlayingBeforePause = true; _global.wasPlayingBeforePause = true;
videoPause(_global.videoHandle); videoPause(_global.videoHandle);
} }
@ -1926,7 +1954,7 @@ static void _updatePauseState(void) {
Mix_Pause(-1); Mix_Pause(-1);
} else { } else {
// Resume laserdisc // Resume laserdisc
if (!_global.discStopped && _global.wasPlayingBeforePause) { if ((_global.videoHandle >= 0) && !_global.discStopped && _global.wasPlayingBeforePause) {
_global.wasPlayingBeforePause = false; _global.wasPlayingBeforePause = false;
videoPlay(_global.videoHandle); videoPlay(_global.videoHandle);
} }
@ -2110,9 +2138,7 @@ static int32_t apiDiscGetFrame(lua_State *L) {
static int32_t apiDiscGetHeight(lua_State *L) { static int32_t apiDiscGetHeight(lua_State *L) {
int32_t height = 0; int32_t height = 0;
if (_global.videoHandle >= 0) { height = _global.canvasHeight;
height = videoGetHeight(_global.videoHandle);
}
_luaTrace(L, "discGetHeight", "%d", height); _luaTrace(L, "discGetHeight", "%d", height);
lua_pushinteger(L, height); lua_pushinteger(L, height);
@ -2140,11 +2166,13 @@ static int32_t apiDiscGetLanguage(lua_State *L) {
} }
// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED. // state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED, or DISC_EJECTED (no disc).
static int32_t apiDiscGetState(lua_State *L) { static int32_t apiDiscGetState(lua_State *L) {
DiscStateE state = DISC_PAUSED; DiscStateE state = DISC_PAUSED;
if (_global.discStopped) { if (_global.videoHandle < 0) {
state = DISC_EJECTED;
} else if (_global.discStopped) {
state = DISC_STOPPED; state = DISC_STOPPED;
} else { } else {
if ((_global.videoHandle >= 0) && videoIsPlaying(_global.videoHandle)) { if ((_global.videoHandle >= 0) && videoIsPlaying(_global.videoHandle)) {
@ -2162,9 +2190,7 @@ static int32_t apiDiscGetState(lua_State *L) {
static int32_t apiDiscGetWidth(lua_State *L) { static int32_t apiDiscGetWidth(lua_State *L) {
int32_t width = 0; int32_t width = 0;
if (_global.videoHandle >= 0) { width = _global.canvasWidth;
width = videoGetWidth(_global.videoHandle);
}
_luaTrace(L, "discGetWidth", "%d", width); _luaTrace(L, "discGetWidth", "%d", width);
lua_pushinteger(L, width); lua_pushinteger(L, width);
@ -2897,10 +2923,8 @@ static int32_t apiOverlaySetResolution(lua_State *L) {
utilDie("%s", SDL_GetError()); utilDie("%s", SDL_GetError());
} }
SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND); SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND);
if (_global.videoHandle >= 0) { _global.overlayScaleX = (double)width / (double)_global.canvasWidth;
_global.overlayScaleX = (double)width / (double)videoGetWidth(_global.videoHandle); _global.overlayScaleY = (double)height / (double)_global.canvasHeight;
_global.overlayScaleY = (double)height / (double)videoGetHeight(_global.videoHandle);
}
_overlayTouched(); _overlayTouched();
_luaTrace(L, "overlaySetResolution", "%d %d", width, height); _luaTrace(L, "overlaySetResolution", "%d %d", width, height);
@ -3827,7 +3851,7 @@ static int32_t apiVideoLoad(lua_State *L) {
if (!video) { if (!video) {
_luaDie(L, "videoLoad", "Unable to allocate new video."); _luaDie(L, "videoLoad", "Unable to allocate new video.");
} }
video->handle = videoLoad(name, NULL, dataDir, _global.renderer); video->handle = videoLoad(name, NULL, dataDir, _global.renderer, true);
video->id = _global.nextVideoId++; video->id = _global.nextVideoId++;
video->lastFrame = -1; video->lastFrame = -1;
video->scaleX = 1.0; video->scaleX = 1.0;
@ -4327,7 +4351,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
lua_register(_global.luaContext, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth. lua_register(_global.luaContext, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth.
lua_register(_global.luaContext, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx lua_register(_global.luaContext, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx
// Open main video file // Open main video file, if this is a laserdisc game. Otherwise the canvas is the world.
if (_global.conf->disc) {
_progTrace("Opening main video file"); _progTrace("Opening main video file");
_doIndexDisplay(INDEX_DISPLAY_START); _doIndexDisplay(INDEX_DISPLAY_START);
videoSetIndexCallback(_doIndexDisplay); videoSetIndexCallback(_doIndexDisplay);
@ -4335,13 +4360,20 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
_global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated); _global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated);
frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle
} else { } else {
_global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer); _global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer, false);
} }
videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp); videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp);
videoSetIndexCallback(NULL); videoSetIndexCallback(NULL);
_doIndexDisplay(INDEX_DISPLAY_STOP); _doIndexDisplay(INDEX_DISPLAY_STOP);
videoWidth = videoGetWidth(_global.videoHandle); _global.canvasWidth = videoGetWidth(_global.videoHandle);
videoHeight = videoGetHeight(_global.videoHandle); _global.canvasHeight = videoGetHeight(_global.videoHandle);
} else {
_progTrace("No disc; canvas is %dx%d", _global.conf->canvasWidth, _global.conf->canvasHeight);
_global.canvasWidth = _global.conf->canvasWidth;
_global.canvasHeight = _global.conf->canvasHeight;
}
videoWidth = _global.canvasWidth;
videoHeight = _global.canvasHeight;
// Should we resize the window to the video's shape? // Should we resize the window to the video's shape?
if (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) { if (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) {
@ -4507,11 +4539,13 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
SDL_SetColorKey(_global.consoleFontSurface, SDL_TRUE, _global.consoleFontSurface->format->Rmask | _global.consoleFontSurface->format->Bmask); SDL_SetColorKey(_global.consoleFontSurface, SDL_TRUE, _global.consoleFontSurface->format->Rmask | _global.consoleFontSurface->format->Bmask);
// The disc always starts parked on frame 1, paused, like discSearch(1). // The disc always starts parked on frame 1, paused, like discSearch(1).
if (_global.videoHandle >= 0) {
_progTrace("Parking laserdisc on frame 1"); _progTrace("Parking laserdisc on frame 1");
_discSeek(1); _discSeek(1);
videoPause(_global.videoHandle); videoPause(_global.videoHandle);
_global.discStopped = false; _global.discStopped = false;
_selectDefaultAudioTrack(_global.videoHandle); _selectDefaultAudioTrack(_global.videoHandle);
}
// Start script // Start script
_progTrace("Running %s", _global.conf->scriptFile); _progTrace("Running %s", _global.conf->scriptFile);
@ -4696,7 +4730,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
} }
} }
// Update video // Update the disc video
if (_global.videoHandle >= 0) {
thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture); thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture);
if (_global.conf->isFrameFile) { if (_global.conf->isFrameFile) {
frameFileUpdate(_global.frameFileHandle, &_global.videoHandle); frameFileUpdate(_global.frameFileHandle, &_global.videoHandle);
@ -4707,6 +4742,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
frameClock = 0; frameClock = 0;
_global.refreshDisplay = true; _global.refreshDisplay = true;
} }
}
// Call game code, unless the engine has it paused. // Call game code, unless the engine has it paused.
if (!_global.frozen && (SDL_GetTicks() > frameClock)) { if (!_global.frozen && (SDL_GetTicks() > frameClock)) {
@ -4735,7 +4771,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a); SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a);
SDL_RenderFillRect(_global.renderer, &sindenWhite); SDL_RenderFillRect(_global.renderer, &sindenWhite);
} }
// Laserdisc Video // Laserdisc Video. Games without a disc draw on black.
if (_global.videoHandle >= 0) {
if (_global.discStopped) { if (_global.discStopped) {
// Stopped discs display blue like the good old days // Stopped discs display blue like the good old days
SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE); SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE);
@ -4743,6 +4780,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
} else { } else {
SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget); SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget);
} }
}
// Overlay // Overlay
if (_global.overlayDirty) { if (_global.overlayDirty) {
SDL_UpdateTexture(_global.overlayTexture, NULL, _global.overlay->pixels, _global.overlay->pitch); SDL_UpdateTexture(_global.overlayTexture, NULL, _global.overlay->pixels, _global.overlay->pitch);
@ -4806,12 +4844,15 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
} }
// Unload background video // Unload background video
_progTrace("Unloading main video file");
if (_global.conf->isFrameFile) { if (_global.conf->isFrameFile) {
_progTrace("Unloading framefile");
frameFileUnload(_global.frameFileHandle); frameFileUnload(_global.frameFileHandle);
} else { } else {
if (_global.videoHandle >= 0) {
_progTrace("Unloading main video file");
videoUnload(_global.videoHandle); videoUnload(_global.videoHandle);
} }
}
// Stop controllers // Stop controllers
_progTrace("Stopping controllers"); _progTrace("Stopping controllers");

View file

@ -63,12 +63,15 @@ typedef struct ConfigS {
bool programTracing; bool programTracing;
bool scriptTracing; bool scriptTracing;
bool legacySpriteArgs; bool legacySpriteArgs;
bool disc; // Play a laserdisc video; otherwise the canvas is the world
int32_t bestRatioIndex; int32_t bestRatioIndex;
int32_t volumeVldp; int32_t volumeVldp;
int32_t volumeNonVldp; int32_t volumeNonVldp;
int32_t scaleFactor; int32_t scaleFactor;
int32_t xResolution; int32_t xResolution;
int32_t yResolution; int32_t yResolution;
int32_t canvasWidth; // World size when there is no disc
int32_t canvasHeight;
int32_t sindenArgc; int32_t sindenArgc;
int32_t sindenArgv[SINDEN_ARG_MAX]; int32_t sindenArgv[SINDEN_ARG_MAX];
int32_t audioOutputTrack; int32_t audioOutputTrack;

View file

@ -55,8 +55,17 @@ typedef struct iso639_lang_t iso639_lang_t;
#define MS_PER_SECOND 1000.0 #define MS_PER_SECOND 1000.0
#define PERCENT_TO_SCALE 0.01f #define PERCENT_TO_SCALE 0.01f
#define PERCENT_MAX 100 #define PERCENT_MAX 100
#define PLANE_COUNT 3 // Y, U, V
#define KEYFRAME_WARN_SECONDS 2.0 // Seeks decode forward from the previous keyframe
// One decoded frame owned by the player (FFMS2 reuses its own buffer, so we copy).
typedef struct FrameBufferS {
uint8_t *data[PLANE_COUNT];
int32_t linesize[PLANE_COUNT];
int64_t frame; // -1 until something has been decoded into it
} FrameBufferT;
typedef struct AudioStreamS { typedef struct AudioStreamS {
FFMS_AudioSource *audioSource; FFMS_AudioSource *audioSource;
const FFMS_AudioProperties *audioProps; const FFMS_AudioProperties *audioProps;
@ -69,7 +78,6 @@ typedef struct VideoPlayerS {
int32_t id; int32_t id;
bool playing; bool playing;
bool resetTime; bool resetTime;
bool frameDirty;
uint8_t *audioBuffer; uint8_t *audioBuffer;
uint8_t *audioSilenceRaw; uint8_t *audioSilenceRaw;
uint8_t audioSampleBytes; uint8_t audioSampleBytes;
@ -102,21 +110,40 @@ typedef struct VideoPlayerS {
FFMS_Track *videoTrackHandle; FFMS_Track *videoTrackHandle;
const FFMS_VideoProperties *videoProps; const FFMS_VideoProperties *videoProps;
const FFMS_TrackTimeBase *videoTimeBase; const FFMS_TrackTimeBase *videoTimeBase;
const FFMS_Frame *frameData; bool rgb; // BGRA frames for scripts to read; otherwise YUV for the GPU
FrameBufferT front; // What the texture and the script see
FrameBufferT back; // What the decoder thread fills
bool backReady;
bool threadError;
bool quitThread;
int64_t requestedFrame; // Waiting for the decoder, -1 when none
int64_t pendingFrame; // Last frame asked for, so it is not asked for twice
int64_t uploadedFrame; // Frame currently on the texture
SDL_Thread *thread;
SDL_mutex *lock;
SDL_cond *wake;
FFMS_ErrorInfo threadErrInfo;
char threadErrMsg[ERROR_BUFFER_SIZE];
UT_hash_handle hh; UT_hash_handle hh;
} VideoPlayerT; } VideoPlayerT;
#pragma GCC diagnostic pop #pragma GCC diagnostic pop
static int64_t _audioClock(VideoPlayerT *v, uint32_t now); static int64_t _audioClock(VideoPlayerT *v, uint32_t now);
static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer);
static void _copyFrame(VideoPlayerT *v, FrameBufferT *buffer, const FFMS_Frame *frame);
static FFMS_Index *_createIndex(const char *filename, const char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v); static FFMS_Index *_createIndex(const char *filename, const char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v);
static int _decoderThread(void *data); // SDL thread entry. Not changing int.
static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata); // Callback. Not changing ints. static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata); // Callback. Not changing ints.
static void _feedAudio(VideoPlayerT *v); static void _feedAudio(VideoPlayerT *v);
static int64_t _frameTime(VideoPlayerT *v, int64_t frame); static int64_t _frameTime(VideoPlayerT *v, int64_t frame);
static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller); static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller);
static int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *icPrivate); // Callback. Not changing int. static int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *icPrivate); // Callback. Not changing int.
static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index); static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index);
static void _loadFrame(VideoPlayerT *v); static void _reportKeyframes(VideoPlayerT *v, const char *filename);
static void _requestFrame(VideoPlayerT *v);
static bool _takeDecodedFrame(VideoPlayerT *v);
static void _uploadFrame(VideoPlayerT *v);
static int64_t _msToSamples(VideoPlayerT *v, int64_t ms); static int64_t _msToSamples(VideoPlayerT *v, int64_t ms);
static void _resetClock(VideoPlayerT *v, uint32_t now); static void _resetClock(VideoPlayerT *v, uint32_t now);
@ -131,6 +158,49 @@ static int64_t _mixLatencyMs = 0; // Time between handing
static SDL_AudioFormat _mixFormat = 0; static SDL_AudioFormat _mixFormat = 0;
static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer) {
int32_t chromaWidth = (v->width + 1) / 2;
int32_t chromaHeight = (v->height + 1) / 2;
buffer->frame = -1;
if (v->rgb) {
buffer->linesize[0] = v->width * BYTES_PER_PIXEL;
buffer->data[0] = malloc((size_t)buffer->linesize[0] * (size_t)v->height);
if (!buffer->data[0]) {
utilDie("Unable to allocate frame buffer.");
}
} else {
buffer->linesize[0] = v->width;
buffer->linesize[1] = chromaWidth;
buffer->linesize[2] = chromaWidth;
buffer->data[0] = malloc((size_t)v->width * (size_t)v->height);
buffer->data[1] = malloc((size_t)chromaWidth * (size_t)chromaHeight);
buffer->data[2] = malloc((size_t)chromaWidth * (size_t)chromaHeight);
if (!buffer->data[0] || !buffer->data[1] || !buffer->data[2]) {
utilDie("Unable to allocate frame buffer.");
}
}
}
// Copies FFMS2's frame into our buffer, one plane at a time (line sizes may differ).
static void _copyFrame(VideoPlayerT *v, FrameBufferT *buffer, const FFMS_Frame *frame) {
int32_t plane = 0;
int32_t planes = v->rgb ? 1 : PLANE_COUNT;
int32_t rows = 0;
int32_t width = 0;
int32_t y = 0;
for (plane = 0; plane < planes; plane++) {
rows = (plane == 0) ? v->height : (v->height + 1) / 2;
width = buffer->linesize[plane];
for (y = 0; y < rows; y++) {
memcpy(buffer->data[plane] + y * buffer->linesize[plane], frame->Data[plane] + y * frame->Linesize[plane], (size_t)width);
}
}
}
// Presentation time (ms) the listener is hearing right now. Audio is the master clock: // Presentation time (ms) the listener is hearing right now. Audio is the master clock:
// the picture is fitted to what the device has actually consumed, so the two cannot drift. // the picture is fitted to what the device has actually consumed, so the two cannot drift.
static int64_t _audioClock(VideoPlayerT *v, uint32_t now) { static int64_t _audioClock(VideoPlayerT *v, uint32_t now) {
@ -200,6 +270,43 @@ static FFMS_Index *_createIndex(const char *filename, const char *indexPath, boo
} }
// Decodes whatever frame was last requested into the back buffer, then waits for the next request.
// FFMS2 seeks by decoding forward from a keyframe, so this keeps long seeks off the main loop.
static int _decoderThread(void *data) {
VideoPlayerT *v = (VideoPlayerT *)data;
const FFMS_Frame *frame = NULL;
int64_t want = -1;
SDL_LockMutex(v->lock);
while (!v->quitThread) {
// Only one frame is in flight and the main thread must take it before the next one.
if ((v->requestedFrame < 0) || v->backReady) {
SDL_CondWait(v->wake, v->lock);
continue;
}
want = v->requestedFrame;
v->requestedFrame = -1;
SDL_UnlockMutex(v->lock);
frame = FFMS_GetFrame(v->videoSource, (int)want, &v->threadErrInfo);
if (frame != NULL) {
_copyFrame(v, &v->back, frame);
}
SDL_LockMutex(v->lock);
if (frame == NULL) {
v->threadError = true;
} else {
v->back.frame = want;
v->backReady = true;
}
}
SDL_UnlockMutex(v->lock);
return 0;
}
// Runs on the SDL_mixer audio thread. Everything it touches is guarded by SDL_LockAudio on the main thread. // Runs on the SDL_mixer audio thread. Everything it touches is guarded by SDL_LockAudio on the main thread.
static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata) { static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata) {
VideoPlayerT *v = (VideoPlayerT *)udata; VideoPlayerT *v = (VideoPlayerT *)udata;
@ -375,27 +482,57 @@ static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index)
} }
// Decode the current frame and push it to the texture.
static void _loadFrame(VideoPlayerT *v) {
v->frameData = FFMS_GetFrame(v->videoSource, (int)v->frame, &v->errInfo);
if (v->frameData == NULL) {
utilDie("%s", v->errInfo.Buffer);
}
SDL_UpdateTexture(v->videoTexture, NULL, v->frameData->Data[0], v->frameData->Linesize[0]);
v->frameDirty = false;
}
static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) { static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) {
return (int64_t)((double)ms / MS_PER_SECOND * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate); return (int64_t)((double)ms / MS_PER_SECOND * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate);
} }
// Tells the author how far a seek may have to decode. Keyframe spacing is a property of the file.
static void _reportKeyframes(VideoPlayerT *v, const char *filename) {
int32_t frame = 0;
int32_t lastKeyframe = 0;
int32_t keyframes = 0;
int32_t longestGap = 0;
double seconds = 0.0;
for (frame = 0; frame < v->videoProps->NumFrames; frame++) {
if (FFMS_GetFrameInfo(v->videoTrackHandle, frame)->KeyFrame) {
if ((keyframes > 0) && (frame - lastKeyframe > longestGap)) {
longestGap = frame - lastKeyframe;
}
lastKeyframe = frame;
keyframes++;
}
}
if (v->videoProps->NumFrames - lastKeyframe > longestGap) {
longestGap = v->videoProps->NumFrames - lastKeyframe;
}
if (v->videoProps->FPSNumerator > 0) {
seconds = (double)longestGap * (double)v->videoProps->FPSDenominator / (double)v->videoProps->FPSNumerator;
}
utilTrace("%s: %d frames, %d keyframes, longest gap %d frames (%.1f seconds)", filename, v->videoProps->NumFrames, keyframes, longestGap, seconds);
if (seconds > KEYFRAME_WARN_SECONDS) {
utilSay("Warning: %s has keyframes up to %.1f seconds apart; seeking into that video may stall. Re-encode with a keyframe interval of two seconds or less.", filename, seconds);
}
}
// Asks the decoder thread for the frame the clock says we should be showing, if it is not already coming.
static void _requestFrame(VideoPlayerT *v) {
SDL_LockMutex(v->lock);
if ((v->frame != v->front.frame) && (v->frame != v->pendingFrame)) {
v->requestedFrame = v->frame;
v->pendingFrame = v->frame;
SDL_CondSignal(v->wake);
}
SDL_UnlockMutex(v->lock);
}
// Restart the presentation clock at the current frame and realign audio to it. // Restart the presentation clock at the current frame and realign audio to it.
static void _resetClock(VideoPlayerT *v, uint32_t now) { static void _resetClock(VideoPlayerT *v, uint32_t now) {
v->startTicks = now; v->startTicks = now;
v->startTime = _frameTime(v, v->frame); v->startTime = _frameTime(v, v->frame);
v->frameDirty = true;
v->resetTime = false; v->resetTime = false;
if (v->audioSourceCount > 0) { if (v->audioSourceCount > 0) {
SDL_LockAudio(); SDL_LockAudio();
@ -409,6 +546,40 @@ static void _resetClock(VideoPlayerT *v, uint32_t now) {
} }
// Swaps in a finished frame from the decoder thread. Returns true when there is a new one.
static bool _takeDecodedFrame(VideoPlayerT *v) {
FrameBufferT temp;
bool taken = false;
SDL_LockMutex(v->lock);
if (v->threadError) {
utilDie("%s", v->threadErrInfo.Buffer);
}
if (v->backReady) {
temp = v->front;
v->front = v->back;
v->back = temp;
v->backReady = false;
taken = true;
SDL_CondSignal(v->wake);
}
SDL_UnlockMutex(v->lock);
return taken;
}
// Pushes the front buffer to the texture. YUV players convert on the GPU.
static void _uploadFrame(VideoPlayerT *v) {
if (v->rgb) {
SDL_UpdateTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0]);
} else {
SDL_UpdateYUVTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0], v->front.data[1], v->front.linesize[1], v->front.data[2], v->front.linesize[2]);
}
v->uploadedFrame = v->front.frame;
}
int32_t videoGetAudioTrack(int32_t playerHandle) { int32_t videoGetAudioTrack(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetAudioTrack")->currentAudioTrack; return _getPlayer(playerHandle, "videoGetAudioTrack")->currentAudioTrack;
} }
@ -465,33 +636,49 @@ const char *videoGetLanguageDescription(const char *languageCode) {
} }
// Reads one pixel of the most recently decoded frame. Returns false if there is no frame yet. // Reads one pixel of the frame being shown. Returns false if there is no frame yet.
bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) { bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel"); VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel");
const uint8_t *pixel = NULL; const uint8_t *pixel = NULL;
int32_t c = 0;
int32_t d = 0;
int32_t e = 0;
int32_t value = 0;
if ((v->frameData == NULL) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) { if ((v->front.frame < 0) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) {
return false; return false;
} }
// Frames are decoded as BGRA. if (v->rgb) {
pixel = v->frameData->Data[0] + (y * v->frameData->Linesize[0]) + (x * BYTES_PER_PIXEL); pixel = v->front.data[0] + (y * v->front.linesize[0]) + (x * BYTES_PER_PIXEL);
*b = pixel[0]; *b = pixel[0];
*g = pixel[1]; *g = pixel[1];
*r = pixel[2]; *r = pixel[2];
} else {
// BT.601 limited range, the same conversion SDL applies on the GPU for SD video.
c = v->front.data[0][y * v->front.linesize[0] + x] - 16;
d = v->front.data[1][(y / 2) * v->front.linesize[1] + (x / 2)] - 128;
e = v->front.data[2][(y / 2) * v->front.linesize[2] + (x / 2)] - 128;
value = (298 * c + 409 * e + 128) >> 8;
*r = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value));
value = (298 * c - 100 * d - 208 * e + 128) >> 8;
*g = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value));
value = (298 * c + 516 * d + 128) >> 8;
*b = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value));
}
return true; return true;
} }
// Exposes the most recently decoded BGRA frame. Valid until the next videoUpdate of this player. // Exposes the BGRA frame being shown (players loaded with rgb). Valid until the next videoUpdate of this player.
bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch) { bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixels"); VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixels");
if (v->frameData == NULL) { if (!v->rgb || (v->front.frame < 0)) {
return false; return false;
} }
*pixels = v->frameData->Data[0]; *pixels = v->front.data[0];
*pitch = v->frameData->Linesize[0]; *pitch = v->front.linesize[0];
return true; return true;
} }
@ -543,8 +730,9 @@ bool videoIsPlaying(int32_t playerHandle) {
} }
// audioFilename may be NULL when the audio lives in the video file. // audioFilename may be NULL when the audio lives in the video file. rgb players decode to BGRA so
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer) { // scripts can read the pixels; everything else stays YUV and is converted by the GPU.
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb) {
int32_t pixelFormats[2]; int32_t pixelFormats[2];
FFMS_Index *vIndex = NULL; FFMS_Index *vIndex = NULL;
FFMS_Index *aIndex = NULL; FFMS_Index *aIndex = NULL;
@ -568,6 +756,12 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
v->errInfo.BufferSize = sizeof(v->errMsg); v->errInfo.BufferSize = sizeof(v->errMsg);
v->errInfo.ErrorType = FFMS_ERROR_SUCCESS; v->errInfo.ErrorType = FFMS_ERROR_SUCCESS;
v->errInfo.SubType = FFMS_ERROR_SUCCESS; v->errInfo.SubType = FFMS_ERROR_SUCCESS;
v->threadErrInfo = v->errInfo;
v->threadErrInfo.Buffer = v->threadErrMsg;
v->rgb = rgb;
v->requestedFrame = -1;
v->pendingFrame = -1;
v->uploadedFrame = -1;
if (audioFilename) { if (audioFilename) {
vIndex = _createIndex(videoFilename, indexPath, true, false, v); vIndex = _createIndex(videoFilename, indexPath, true, false, v);
@ -599,12 +793,13 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
v->width = frame->EncodedWidth; v->width = frame->EncodedWidth;
v->height = frame->EncodedHeight; v->height = frame->EncodedHeight;
// Set up output video format // Set up output video format. YUV 4:2:0 is what the decoder produces, so no conversion runs.
pixelFormats[0] = FFMS_GetPixFmt("bgra"); pixelFormats[0] = FFMS_GetPixFmt(rgb ? "bgra" : "yuv420p");
pixelFormats[1] = -1; pixelFormats[1] = -1;
if (FFMS_SetOutputFormatV2(v->videoSource, pixelFormats, v->width, v->height, FFMS_RESIZER_BICUBIC, &v->errInfo)) { if (FFMS_SetOutputFormatV2(v->videoSource, pixelFormats, v->width, v->height, FFMS_RESIZER_BICUBIC, &v->errInfo)) {
utilDie("%s", v->errInfo.Buffer); utilDie("%s", v->errInfo.Buffer);
} }
_reportKeyframes(v, videoFilename);
// Find audio track(s) // Find audio track(s)
_loadAudio(v, audioFilename, aIndex); _loadAudio(v, audioFilename, aIndex);
@ -616,11 +811,24 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
FFMS_DestroyIndex(vIndex); FFMS_DestroyIndex(vIndex);
// Create video texture // Create video texture
v->videoTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, v->width, v->height); v->videoTexture = SDL_CreateTexture(renderer, rgb ? SDL_PIXELFORMAT_BGRA32 : SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, v->width, v->height);
if (v->videoTexture == NULL) { if (v->videoTexture == NULL) {
utilDie("%s", SDL_GetError()); utilDie("%s", SDL_GetError());
} }
// Frame buffers and the decoder thread that fills them
_allocateFrameBuffer(v, &v->front);
_allocateFrameBuffer(v, &v->back);
v->lock = SDL_CreateMutex();
v->wake = SDL_CreateCond();
if (!v->lock || !v->wake) {
utilDie("%s", SDL_GetError());
}
v->thread = SDL_CreateThread(_decoderThread, "singeDecoder", v);
if (v->thread == NULL) {
utilDie("%s", SDL_GetError());
}
// Do we have audio? // Do we have audio?
if (v->audioSourceCount > 0) { if (v->audioSourceCount > 0) {
// Determine audio format // Determine audio format
@ -800,6 +1008,19 @@ void videoUnload(int32_t playerHandle) {
free(v->audio); free(v->audio);
} }
// Stop the decoder before touching anything it uses.
SDL_LockMutex(v->lock);
v->quitThread = true;
SDL_CondSignal(v->wake);
SDL_UnlockMutex(v->lock);
SDL_WaitThread(v->thread, NULL);
SDL_DestroyCond(v->wake);
SDL_DestroyMutex(v->lock);
for (x = 0; x < PLANE_COUNT; x++) {
free(v->front.data[x]);
free(v->back.data[x]);
}
FFMS_DestroyVideoSource(v->videoSource); FFMS_DestroyVideoSource(v->videoSource);
SDL_DestroyTexture(v->videoTexture); SDL_DestroyTexture(v->videoTexture);
@ -834,7 +1055,6 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
next = v->frame + 1; next = v->frame + 1;
while ((next < count) && (_frameTime(v, next) <= elapsed)) { while ((next < count) && (_frameTime(v, next) <= elapsed)) {
v->frame = next; v->frame = next;
v->frameDirty = true;
next++; next++;
} }
// Past the end of the last frame? Loop. // Past the end of the last frame? Loop.
@ -848,8 +1068,10 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
} }
} }
if (v->frameDirty) { // Hand the decoder the frame we want, and show whatever it has finished.
_loadFrame(v); _requestFrame(v);
if (_takeDecodedFrame(v)) {
_uploadFrame(v);
} }
*texture = v->videoTexture; *texture = v->videoTexture;
@ -858,5 +1080,6 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
_feedAudio(v); _feedAudio(v);
} }
return v->frame; // Report what is on screen, so the caller redraws when it changes.
return v->uploadedFrame;
} }

View file

@ -49,7 +49,7 @@ void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *
int32_t videoGetWidth(int32_t playerHandle); int32_t videoGetWidth(int32_t playerHandle);
void videoInit(int32_t mixerChunkFrames); void videoInit(int32_t mixerChunkFrames);
bool videoIsPlaying(int32_t playerHandle); bool videoIsPlaying(int32_t playerHandle);
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer); int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb);
void videoPause(int32_t playerHandle); void videoPause(int32_t playerHandle);
void videoPlay(int32_t playerHandle); void videoPlay(int32_t playerHandle);
void videoQuit(void); void videoQuit(void);