Added Hypseus extensions.
This commit is contained in:
parent
1aede80d34
commit
d024a50c8d
40 changed files with 9274 additions and 759 deletions
187
CHANGELOG
187
CHANGELOG
|
|
@ -32,6 +32,179 @@ API Changes
|
||||||
- Framework.singe applies the legacy sprite argument order once even when
|
- Framework.singe applies the legacy sprite argument order once even when
|
||||||
a game loads it more than once.
|
a game loads it more than once.
|
||||||
|
|
||||||
|
- Hypseus Singe extensions, so games written for it run unchanged:
|
||||||
|
spriteLoadFrames, spriteDrawFrame, spriteFrameWidth, spriteFrameHeight,
|
||||||
|
spriteResetColorKey, discAudioSuffix, singeSetQuitKeyEnabled (with the
|
||||||
|
Framework alias keyboardCatchQuit), vldpGetScale, vldpFocusArea,
|
||||||
|
vldpResetFocus, vldpSetMonochrome, overlaySetMonochrome,
|
||||||
|
vldpGetYUVPixel, controllerIsValid and controllerDoRumble. Sprite calls
|
||||||
|
take the handle first like the rest of 3.00; the LEGACY_SPRITE_ARGS
|
||||||
|
shim covers spriteDrawFrame and spriteResetColorKey.
|
||||||
|
|
||||||
|
- More Hypseus Singe names, almost all of them supplied by
|
||||||
|
Framework.singe over calls Singe already had: setOverlaySize and
|
||||||
|
setOverlayResolution over overlaySetResolution, rewriteStatus as a stub,
|
||||||
|
and the six music calls over the sound family. setOverlayLinearScale is
|
||||||
|
a real engine call: it smooths the overlay as the window scales it,
|
||||||
|
which is the default and what Singe has always done, or takes the
|
||||||
|
nearest pixel for crisp pixel art.
|
||||||
|
|
||||||
|
- The picture's place on screen is now one rectangle built from four
|
||||||
|
things in order: the window, --scalefactor (range widened from 50-100
|
||||||
|
to 25-100), --shiftx and --shifty (new; they move the picture inside the
|
||||||
|
room the scale left, -100 hard against one side and 100 against the
|
||||||
|
other, which is what Hypseus's own mean -- so at full scale there is no
|
||||||
|
room and a shift does nothing), and last the Sinden border, which is
|
||||||
|
therefore still a border whatever the shift and the scale did --
|
||||||
|
--scalefactor and --sindengun may now be used together. --rotate (new;
|
||||||
|
0, 90, 180 or 270 degrees)
|
||||||
|
turns the whole presentation, video, overlay, GUIs, 3D scene and
|
||||||
|
particles together, filling a portrait window on a quarter turn. Mouse
|
||||||
|
and light gun positions follow all of it, so a click lands where it is
|
||||||
|
drawn. Scripts move the scale and the rotation while the game runs with
|
||||||
|
the Hypseus extensions vldpSetScale, vldpSetRotate and vldpGetRotate.
|
||||||
|
|
||||||
|
- Bezels. --bezel names cabinet artwork in a bezels folder (--bezeldir
|
||||||
|
names another folder, --bezelflip draws the artwork in front of the
|
||||||
|
picture instead of behind it), and a Lua sidecar named after the
|
||||||
|
artwork declares the cutout the picture is drawn in, in the artwork's
|
||||||
|
own pixels: CUTOUT = { x = 200, y = 120, width = 800, height = 500 }.
|
||||||
|
With no cutout the picture keeps the window and --scalefactor,
|
||||||
|
--shiftx and --shifty place it, as before. A Sinden border goes to the
|
||||||
|
edge of the window when artwork is loaded, because the gun's camera
|
||||||
|
sees the whole screen; --sindenedge video|window overrides that.
|
||||||
|
Scripts ask mainBezelLoaded() which artwork they got and choose with
|
||||||
|
setOverlayOnTop whether the overlay sits above it.
|
||||||
|
|
||||||
|
- The arcade score panel behind the seven scoreBezel calls: credits, and
|
||||||
|
a score and a life count for two players, in a shipped RmlUi document
|
||||||
|
(Singe/scoreBezel.rml and Singe/scoreBezel.rcss) with a seven segment
|
||||||
|
look that a theme or a game may restyle. It is off until a game asks
|
||||||
|
for it with scoreBezelEnable, exactly as in Hypseus, and it sits in the
|
||||||
|
room a bezel's cutout leaves above or below the picture, or along the
|
||||||
|
bottom of the picture when there is none. Hypseus's second argument,
|
||||||
|
which chooses among its five scoreboards, is accepted and ignored.
|
||||||
|
guiDrawScreen puts a game's own GUIs out there beside it: it is
|
||||||
|
guiDraw with the rectangle measured across the whole window.
|
||||||
|
|
||||||
|
- A settings file. settings.cfg holds the options you would otherwise
|
||||||
|
type on every command line, in Lua like controls.cfg and found the same
|
||||||
|
four ways: the directory Singe was started in, above and inside the data
|
||||||
|
directory, and beside the game's script, with a later file winning key by
|
||||||
|
key. Its keys are the long option names without the dashes, so there is
|
||||||
|
no second vocabulary, and a switch takes true or false. A settings file
|
||||||
|
is a set of defaults: a games.dat entry for the game still overrides it
|
||||||
|
and the command line always does. Anything naming the game, naming a
|
||||||
|
directory, or acting before the file could be found (--framefile,
|
||||||
|
--gamedir, --datadir, --entry, --help, --apiversion, --noconsole, the
|
||||||
|
packing tools) belongs on the command line, and a key that is not an
|
||||||
|
option, is one of those, or carries a value the option will not take is
|
||||||
|
an error naming the file and the key. Singe/settings.cfg.example lists
|
||||||
|
every key with its default.
|
||||||
|
|
||||||
|
- Options a cabinet owner has been missing, none of them with a short
|
||||||
|
letter since the alphabet ran out: --screen picks the display (counting
|
||||||
|
from 1; a number too large lists the displays that are there),
|
||||||
|
--idleexit quits after so many seconds with nothing touched,
|
||||||
|
--startsilent starts muted until the first input, --monochrome starts
|
||||||
|
the disc picture in grey, --linearscale chooses the overlay's filter,
|
||||||
|
--nogamepad ignores the pads as --nomouse ignores the mice, --haptic
|
||||||
|
caps the rumble a game may ask for (0 turns it off), --keymapfile names
|
||||||
|
one controls.cfg instead of searching four places, --altaudio (and the
|
||||||
|
AUDIO_SUFFIX games.dat key) plays <base><suffix>.ogg beside the disc's
|
||||||
|
video for another language, --xratio and --yratio carry the gun
|
||||||
|
coordinate scales a Hypseus light gun game reads with ratioGetX and
|
||||||
|
ratioGetY, --fvalue carries one number to the game through getFValue,
|
||||||
|
and --apiversion prints one machine readable line for a front end and
|
||||||
|
exits. --nosound, which had been accepted and quietly ignored, now
|
||||||
|
actually mutes.
|
||||||
|
|
||||||
|
- Every controller Singe opens is now a gamepad, whatever it started life
|
||||||
|
as. SDL calls a device a gamepad only when it has a mapping for it, and
|
||||||
|
an arcade encoder board, a spinner, a wheel or a flight stick usually has
|
||||||
|
none, so such a device used to be invisible. Singe now writes a
|
||||||
|
straight-through mapping for it -- axes in order to the two sticks and
|
||||||
|
then the triggers, buttons in order to the standard names, the first hat
|
||||||
|
to the D-pad -- and opens it as an ordinary pad, so it reaches
|
||||||
|
controls.cfg, GAMEPAD_0 to GAMEPAD_3 and every controller call like any
|
||||||
|
other. A device with more controls than the gamepad model can name loses
|
||||||
|
the extras, and one whose buttons are not wired in that order will have
|
||||||
|
them in the wrong places: --program traces the mapping in SDL's own
|
||||||
|
format so it can be pasted into a gamecontrollerdb.txt and rearranged.
|
||||||
|
Singe now reads that file, from the four places it reads controls.cfg,
|
||||||
|
and it and SDL's own database both win over the mapping Singe would
|
||||||
|
write. --mapjoysticks=false turns the writing off.
|
||||||
|
|
||||||
|
- Input options for a cabinet: --gamepad_reorder says which physical pad
|
||||||
|
fills which slot (enumeration positions from 0, as Hypseus writes them),
|
||||||
|
--trigger_threshold says how far an analogue trigger must travel before
|
||||||
|
it counts as a button (a per cent of full travel, Hypseus's unit),
|
||||||
|
--joymouse lets the first pad's left stick drive the mouse cursor at
|
||||||
|
--js_range speed so a light gun game is playable on a stick-only
|
||||||
|
cabinet, --manymouse turns the many mouse mode on whatever the game asks
|
||||||
|
for so a two gun cabinet can be fixed from outside, and
|
||||||
|
--absolutes_only keeps only the devices that report an absolute
|
||||||
|
position, which is what a real gun does. controls.cfg gains DEAD_ZONES,
|
||||||
|
a dead zone for one axis at a time, and TRIGGER_THRESHOLD; both leave
|
||||||
|
every axis on DEAD_ZONE unless set, so nothing changes for anyone who
|
||||||
|
does not ask.
|
||||||
|
|
||||||
|
- SWITCH_MOUSE_DISCONNECT reaches onInputPressed when a mouse or light gun
|
||||||
|
is unplugged, so a gun game can say so instead of appearing to have
|
||||||
|
died, and joyMouseEnable, joyMouseIsEnabled, controllerGetName and
|
||||||
|
mouseGetName let a script drive and describe all of it.
|
||||||
|
|
||||||
|
- Alt-Enter switches between full screen and a window while the game runs,
|
||||||
|
as it does in Hypseus. Singe could not change the mode at all once it
|
||||||
|
had started.
|
||||||
|
|
||||||
|
- Subtitles. srtLoad reads a SubRip .srt and turns its timestamps into
|
||||||
|
disc frames at the disc's own frame rate, so a line is found again after
|
||||||
|
any search or skip; srtEnable shows them, srtPosition puts them where a
|
||||||
|
game wants (1 to 95 per cent down the picture) and srtClear takes one
|
||||||
|
off early. They are drawn through a shipped RmlUi document,
|
||||||
|
Singe/subtitle.rml with Singe/subtitle.rcss, both of which a game or a
|
||||||
|
theme may replace, over the picture and following it wherever the scale,
|
||||||
|
a shift, a bezel cutout or a rotation put it. A malformed file answers
|
||||||
|
false rather than dying. overlayBanner puts a message of its own up the
|
||||||
|
same way for about thirty frames.
|
||||||
|
|
||||||
|
- The disc picture gains three controls beside vldpSetMonochrome:
|
||||||
|
vldpFlash puts one white picture up the way a real player flashes when
|
||||||
|
it is searched, vldpSetBlend smooths the picture down its rows to take
|
||||||
|
the edges off an interlaced transfer, and vldpSetLuma brightens or
|
||||||
|
darkens it in eighths from half to one and a half without washing the
|
||||||
|
colour out. All three work on the decoded frame, cost nothing while
|
||||||
|
they are off, and follow a frame file from one segment to the next --
|
||||||
|
which vldpSetMonochrome now does too, and did not before.
|
||||||
|
|
||||||
|
- Sprites. spriteRotateFrame turns one frame of a sheet and
|
||||||
|
spriteDrawRotatedFrame draws it centred on a point, which is what
|
||||||
|
spriteDrawFrame will not do since it ignores rotation and scale by
|
||||||
|
design; spriteDrawGrid draws any rectangle of a sprite at its own size,
|
||||||
|
which Singe had no way to do at all; spriteGetFrames counts a sheet's
|
||||||
|
frames; and spriteLoadData and soundLoadData build a sprite or a sound
|
||||||
|
from bytes held in a Lua string, for a game that unpacks its own assets.
|
||||||
|
setOverlayOpacity fades the whole overlay and controllerHowMany counts
|
||||||
|
the open gamepads.
|
||||||
|
|
||||||
|
- Music has its own volume. The music family (musicLoad, musicPlay,
|
||||||
|
musicStop with a real fade, musicPause, musicResume, musicIsPlaying,
|
||||||
|
musicSetVolume, musicUnload) is now in the engine rather than shimmed
|
||||||
|
over the sound family, and carries a gain of its own on Hypseus's 0 to
|
||||||
|
128 scale: musicSetVolume moves no sound effect and soundSetVolume
|
||||||
|
moves no music. Music starts as loud as the sound effects, at whatever
|
||||||
|
--volume_nonvldp set. Ten more Hypseus names -- hypseusGetWidth,
|
||||||
|
hypseusGetHeight, takeScreenshot, mouseHowManyReal and the six
|
||||||
|
spriteAnim ones -- are aliases in Framework.singe.
|
||||||
|
|
||||||
|
- trace.txt now opens with a header a bug report can be built on: the
|
||||||
|
version, the whole command line, the operating system, the processor,
|
||||||
|
the renderer and GPU backend that were actually chosen, how video will
|
||||||
|
be decoded, which audio decoders the build carries, the SDL versions
|
||||||
|
built and linked against, the settings that were applied and the game. It sits between two rules so it can be
|
||||||
|
pasted whole.
|
||||||
|
|
||||||
- lfs.dir, lfs.attributes and lfs.symlinkattributes see inside a packed
|
- lfs.dir, lfs.attributes and lfs.symlinkattributes see inside a packed
|
||||||
game (the union of its loose directory, its data overlay and the
|
game (the union of its loose directory, its data overlay and the
|
||||||
database), and lfs.mkdir and lfs.rmdir act on its data overlay, so
|
database), and lfs.mkdir and lfs.rmdir act on its data overlay, so
|
||||||
|
|
@ -152,6 +325,20 @@ API Changes
|
||||||
layers, filter passes, mask writes, layer bytes, passes). Test scenes
|
layers, filter passes, mask writes, layer bytes, passes). Test scenes
|
||||||
scene40 to scene43 cover clip masks, layers, filters and gradients.
|
scene40 to scene43 cover clip masks, layers, filters and gradients.
|
||||||
|
|
||||||
|
- The Pi decodes video through its V4L2 memory-to-memory device (the
|
||||||
|
h264_v4l2m2m family of decoders) when the codec has one, falling back
|
||||||
|
to software when the device is missing, as on a Pi 5 for H.264. The
|
||||||
|
trace reports which decoder is in use and the format the device hands
|
||||||
|
back; --softwarevideo turns it off like the other hardware decoders.
|
||||||
|
|
||||||
|
- The Menu.sh and Menu.bat the engine writes now look beside themselves
|
||||||
|
for a file whose name starts with Singe and run the first one, the way
|
||||||
|
the launchers BatMaker writes already did, instead of naming the
|
||||||
|
executable. The executable's name carries its version, so a launcher
|
||||||
|
that named it stopped working at the next release, and the thing that
|
||||||
|
would have rewritten it was the launcher itself. With --gamedir the
|
||||||
|
executable is elsewhere and its full path is still written.
|
||||||
|
|
||||||
- Third-party libraries brought up to their current releases: ffmpeg
|
- Third-party libraries brought up to their current releases: ffmpeg
|
||||||
9.0.1 (from a 2023 development snapshot; libpostproc is gone with it),
|
9.0.1 (from a 2023 development snapshot; libpostproc is gone with it),
|
||||||
OpenSSL 3.5.8 LTS (from a 2023 development snapshot), Lua 5.4.9, zlib
|
OpenSSL 3.5.8 LTS (from a 2023 development snapshot), Lua 5.4.9, zlib
|
||||||
|
|
|
||||||
|
|
@ -187,11 +187,16 @@ add_custom_command(
|
||||||
# Support files extracted at first run.
|
# Support files extracted at first run.
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Framework.singe ${GENERATED_DIR}/Framework_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Framework.singe ${GENERATED_DIR}/Framework_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/controls.cfg ${GENERATED_DIR}/controls_cfg.h "")
|
||||||
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/settings.cfg ${GENERATED_DIR}/settings_cfg.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.singe ${GENERATED_DIR}/Menu_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuClassic.singe ${GENERATED_DIR}/MenuClassic_singe.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/MenuClassic.singe ${GENERATED_DIR}/MenuClassic_singe.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.rml ${GENERATED_DIR}/Menu_rml.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/Menu.rml ${GENERATED_DIR}/Menu_rml.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/menu.rcss ${GENERATED_DIR}/menu_rcss.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/menu.rcss ${GENERATED_DIR}/menu_rcss.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/gui.rcss ${GENERATED_DIR}/gui_rcss.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/gui.rcss ${GENERATED_DIR}/gui_rcss.h "")
|
||||||
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/scoreBezel.rml ${GENERATED_DIR}/scoreBezel_rml.h "")
|
||||||
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/scoreBezel.rcss ${GENERATED_DIR}/scoreBezel_rcss.h "")
|
||||||
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/subtitle.rml ${GENERATED_DIR}/subtitle_rml.h "")
|
||||||
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/subtitle.rcss ${GENERATED_DIR}/subtitle_rcss.h "")
|
||||||
singeEmbed(${CMAKE_SOURCE_DIR}/assets/FreeSansBold.ttf ${GENERATED_DIR}/FreeSansBold_ttf.h "")
|
singeEmbed(${CMAKE_SOURCE_DIR}/assets/FreeSansBold.ttf ${GENERATED_DIR}/FreeSansBold_ttf.h "")
|
||||||
|
|
||||||
# Menu background video: two clips cropped to 4:3, scaled to 720x480, keyframed every second so the
|
# Menu background video: two clips cropped to 4:3, scaled to 720x480, keyframed every second so the
|
||||||
|
|
@ -528,6 +533,8 @@ if(KANGAROO_OS STREQUAL "linux")
|
||||||
endif()
|
endif()
|
||||||
elseif(KANGAROO_OS STREQUAL "pi")
|
elseif(KANGAROO_OS STREQUAL "pi")
|
||||||
set(SYSTEM_LIBS -ldl)
|
set(SYSTEM_LIBS -ldl)
|
||||||
|
# The Pi decodes video through its V4L2 memory-to-memory device (videoPlayer.c).
|
||||||
|
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE SINGE_V4L2_DECODE)
|
||||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||||
list(APPEND SYSTEM_LIBS -s)
|
list(APPEND SYSTEM_LIBS -s)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -418,6 +418,61 @@ if singeSetPauseKeyEnabled ~= nil then
|
||||||
singeEnablePauseKey = function() singeSetPauseKeyEnabled(true) end
|
singeEnablePauseKey = function() singeSetPauseKeyEnabled(true) end
|
||||||
singeDisablePauseKey = function() singeSetPauseKeyEnabled(false) end
|
singeDisablePauseKey = function() singeSetPauseKeyEnabled(false) end
|
||||||
end
|
end
|
||||||
|
-- Hypseus Singe's name for the quit key setting, with the opposite sense: catching the
|
||||||
|
-- key means the engine stops quitting on it.
|
||||||
|
if singeSetQuitKeyEnabled ~= nil then
|
||||||
|
keyboardCatchQuit = function(catch) singeSetQuitKeyEnabled(not catch) end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Hypseus Singe Compatibility ------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
-- Names Hypseus Singe registers in its own engine and Singe answers from here.
|
||||||
|
-- Every one is guarded, so an engine that grows a real implementation wins.
|
||||||
|
|
||||||
|
|
||||||
|
-- Hypseus extension. Chooses an overlay resolution and hands it to
|
||||||
|
-- overlaySetResolution, which is all Hypseus's own call does: 1 is the disc's
|
||||||
|
-- video size, 3 is 360x240, 4 is the size given (ignored without one, as in
|
||||||
|
-- Hypseus) and anything else, 2 included, is half the disc on both axes. Singe's
|
||||||
|
-- discGetWidth and discGetHeight report the canvas size when there is no disc,
|
||||||
|
-- which is the space a disc-less game draws into, so no special case is needed.
|
||||||
|
local OVERLAY_PRESET_FULL = 1
|
||||||
|
local OVERLAY_PRESET_NONSQUARE = 3
|
||||||
|
local OVERLAY_PRESET_CUSTOM = 4
|
||||||
|
local OVERLAY_NONSQUARE_WIDTH = 360
|
||||||
|
local OVERLAY_NONSQUARE_HEIGHT = 240
|
||||||
|
local OVERLAY_HALF = 2
|
||||||
|
if setOverlaySize == nil and overlaySetResolution ~= nil then
|
||||||
|
function setOverlaySize(preset, width, height)
|
||||||
|
if preset == OVERLAY_PRESET_FULL then
|
||||||
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
||||||
|
elseif preset == OVERLAY_PRESET_NONSQUARE then
|
||||||
|
overlaySetResolution(OVERLAY_NONSQUARE_WIDTH, OVERLAY_NONSQUARE_HEIGHT)
|
||||||
|
elseif preset == OVERLAY_PRESET_CUSTOM then
|
||||||
|
if width ~= nil and height ~= nil then
|
||||||
|
overlaySetResolution(width, height)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
overlaySetResolution(discGetWidth() // OVERLAY_HALF, discGetHeight() // OVERLAY_HALF)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hypseus extension. Hypseus registers one C function under two names and Singe
|
||||||
|
-- already has the other one, so this is a pure alias of overlaySetResolution.
|
||||||
|
if setOverlayResolution == nil and overlaySetResolution ~= nil then
|
||||||
|
setOverlayResolution = overlaySetResolution
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hypseus extension. True in Hypseus while it is rewriting a game's asset paths
|
||||||
|
-- for an -espath install. Singe's virtual file system reaches a game's files
|
||||||
|
-- wherever they were installed, so there is nothing to rewrite and false is the
|
||||||
|
-- right answer: the games that ask keep the MYDIR they already have.
|
||||||
|
if rewriteStatus == nil then
|
||||||
|
function rewriteStatus() return false end
|
||||||
|
end
|
||||||
|
|
||||||
-- Singe 3.00 moved the sprite handle to the first argument. Games written for
|
-- Singe 3.00 moved the sprite handle to the first argument. Games written for
|
||||||
-- 2.10 can set SINGE_LEGACY_SPRITE_ARGS = true (or LEGACY_SPRITE_ARGS = true in
|
-- 2.10 can set SINGE_LEGACY_SPRITE_ARGS = true (or LEGACY_SPRITE_ARGS = true in
|
||||||
|
|
@ -451,6 +506,70 @@ if SINGE_LEGACY_SPRITE_ARGS and spriteDraw ~= nil and not SINGE_LEGACY_SPRITE_WR
|
||||||
end
|
end
|
||||||
local newSpriteSetFrame = spriteSetFrame
|
local newSpriteSetFrame = spriteSetFrame
|
||||||
spriteSetFrame = function(frame, id) return newSpriteSetFrame(id, frame) end
|
spriteSetFrame = function(frame, id) return newSpriteSetFrame(id, frame) end
|
||||||
|
-- Hypseus extensions ship in the old order too: (x, y, frame, spr),
|
||||||
|
-- (x, y, scale, frame, spr) or (x, y, scaleX, scaleY, frame, spr).
|
||||||
|
local newSpriteDrawFrame = spriteDrawFrame
|
||||||
|
spriteDrawFrame = function(...)
|
||||||
|
local args = { ... }
|
||||||
|
local id = table.remove(args)
|
||||||
|
local frame = table.remove(args)
|
||||||
|
return newSpriteDrawFrame(id, args[1], args[2], frame, table.unpack(args, 3, #args))
|
||||||
|
end
|
||||||
|
local newSpriteResetColorKey = spriteResetColorKey
|
||||||
|
spriteResetColorKey = function(enabled, id) return newSpriteResetColorKey(id, enabled) end
|
||||||
|
local newSpriteRotateFrame = spriteRotateFrame
|
||||||
|
spriteRotateFrame = function(degrees, frame, id) return newSpriteRotateFrame(id, degrees, frame) end
|
||||||
|
local newSpriteDrawRotatedFrame = spriteDrawRotatedFrame
|
||||||
|
spriteDrawRotatedFrame = function(...)
|
||||||
|
local args = { ... }
|
||||||
|
local id = table.remove(args)
|
||||||
|
return newSpriteDrawRotatedFrame(id, table.unpack(args))
|
||||||
|
end
|
||||||
|
-- spriteDrawGrid is the one Hypseus writes with the handle in the middle,
|
||||||
|
-- (x, y, spr, rectX, rectY, width, height), not on the end.
|
||||||
|
local newSpriteDrawGrid = spriteDrawGrid
|
||||||
|
spriteDrawGrid = function(x, y, id, sourceX, sourceY, width, height)
|
||||||
|
return newSpriteDrawGrid(id, x, y, sourceX, sourceY, width, height)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Hypseus names for calls Singe already has, and nothing more: each is a plain
|
||||||
|
-- alias, so it takes exactly the arguments the Singe call takes. They are set
|
||||||
|
-- after the legacy wrapping above, which means the six spriteAnim ones follow
|
||||||
|
-- SINGE_LEGACY_SPRITE_ARGS with the rest of the sprite family -- old order when
|
||||||
|
-- a game asked for it, handle first when it did not -- and a game never has two
|
||||||
|
-- argument orders in play at once.
|
||||||
|
if hypseusGetHeight == nil and singeGetHeight ~= nil then
|
||||||
|
hypseusGetHeight = singeGetHeight
|
||||||
|
end
|
||||||
|
if hypseusGetWidth == nil and singeGetWidth ~= nil then
|
||||||
|
hypseusGetWidth = singeGetWidth
|
||||||
|
end
|
||||||
|
if takeScreenshot == nil and singeScreenshot ~= nil then
|
||||||
|
takeScreenshot = singeScreenshot
|
||||||
|
end
|
||||||
|
-- Hypseus counts real mice apart from the ones it derives from a gamepad; Singe
|
||||||
|
-- has no derived mice, so mouseHowMany is already the real count.
|
||||||
|
if mouseHowManyReal == nil and mouseHowMany ~= nil then
|
||||||
|
mouseHowManyReal = mouseHowMany
|
||||||
|
end
|
||||||
|
if spriteAnimGetFrame == nil and spriteGetFrame ~= nil then
|
||||||
|
spriteAnimGetFrame = spriteGetFrame
|
||||||
|
end
|
||||||
|
if spriteAnimIsPlaying == nil and spriteIsPlaying ~= nil then
|
||||||
|
spriteAnimIsPlaying = spriteIsPlaying
|
||||||
|
end
|
||||||
|
if spriteAnimLoop == nil and spriteLoop ~= nil then
|
||||||
|
spriteAnimLoop = spriteLoop
|
||||||
|
end
|
||||||
|
if spriteAnimPause == nil and spritePause ~= nil then
|
||||||
|
spriteAnimPause = spritePause
|
||||||
|
end
|
||||||
|
if spriteAnimPlay == nil and spritePlay ~= nil then
|
||||||
|
spriteAnimPlay = spritePlay
|
||||||
|
end
|
||||||
|
if spriteAnimSetFrame == nil and spriteSetFrame ~= nil then
|
||||||
|
spriteAnimSetFrame = spriteSetFrame
|
||||||
end
|
end
|
||||||
|
|
||||||
if videoGetLanguageDescription ~= nil then
|
if videoGetLanguageDescription ~= nil then
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,25 @@
|
||||||
|
|
||||||
-- Default Mappings
|
-- Default Mappings
|
||||||
|
|
||||||
|
-- How far an axis must move before it counts as pressed, in raw axis units: SDL reports 32767 at
|
||||||
|
-- full deflection, so 15000 is a little under half way. It covers every axis of every gamepad,
|
||||||
|
-- the two analogue triggers included.
|
||||||
DEAD_ZONE = 15000
|
DEAD_ZONE = 15000
|
||||||
|
|
||||||
|
-- One axis at a time, for a stiff stick or a worn trigger, in the same raw units. Each entry is
|
||||||
|
-- { axis, dead zone }, where the axis is any GAMEPAD_N axis entry -- the axis itself or either of
|
||||||
|
-- its two directions, which all name the same axis. Anything not listed keeps DEAD_ZONE.
|
||||||
|
DEAD_ZONES = {
|
||||||
|
-- { GAMEPAD_0.AXIS_LEFT_X, 8000 },
|
||||||
|
-- { GAMEPAD_0.AXIS_LEFT_Y, 8000 },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- How far an analogue trigger must travel before it counts as a button, as a per cent of full
|
||||||
|
-- travel: Hypseus's unit, where its own default is 99.5. Zero leaves the triggers on DEAD_ZONE,
|
||||||
|
-- which is what Singe has always given them, and --trigger_threshold on the command line wins over
|
||||||
|
-- whatever is set here. 15000 raw units is 45.8 per cent, for comparison.
|
||||||
|
TRIGGER_THRESHOLD = 0
|
||||||
|
|
||||||
-- One table per switch, in the order the engine numbers them (SWITCH_UP = 0 ...).
|
-- One table per switch, in the order the engine numbers them (SWITCH_UP = 0 ...).
|
||||||
-- Each entry is a { name, value } pair from the SCANCODE, GAMEPAD_N, or MOUSE_N tables.
|
-- Each entry is a { name, value } pair from the SCANCODE, GAMEPAD_N, or MOUSE_N tables.
|
||||||
INPUT_UP = { SCANCODE.UP, SCANCODE.KP_8, GAMEPAD_0.AXIS_LEFT_Y_U, GAMEPAD_0.AXIS_RIGHT_Y_U, GAMEPAD_0.DPAD_UP }
|
INPUT_UP = { SCANCODE.UP, SCANCODE.KP_8, GAMEPAD_0.AXIS_LEFT_Y_U, GAMEPAD_0.AXIS_RIGHT_Y_U, GAMEPAD_0.DPAD_UP }
|
||||||
|
|
@ -52,3 +69,6 @@ INPUT_CONSOLE = { SCANCODE.GRAVE }
|
||||||
INPUT_ACTION_4 = { SCANCODE.RSHIFT, GAMEPAD_0.BUTTON_Y, MOUSE_0.BUTTON_X1 }
|
INPUT_ACTION_4 = { SCANCODE.RSHIFT, GAMEPAD_0.BUTTON_Y, MOUSE_0.BUTTON_X1 }
|
||||||
INPUT_TILT = { SCANCODE.T }
|
INPUT_TILT = { SCANCODE.T }
|
||||||
INPUT_GRAB = { SCANCODE.G }
|
INPUT_GRAB = { SCANCODE.G }
|
||||||
|
-- Not a control: the engine raises this one itself when a mouse or a light gun is unplugged, so a
|
||||||
|
-- gun game can say so on screen. Bind a key to it as well if you want to test what the game does.
|
||||||
|
INPUT_MOUSE_DISCONNECT = { }
|
||||||
|
|
|
||||||
96
assets/scoreBezel.rcss
Normal file
96
assets/scoreBezel.rcss
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
/*
|
||||||
|
* Singe 3
|
||||||
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
*
|
||||||
|
* The score panel's look: an arcade scoreboard's seven segment readouts, each a row of dim
|
||||||
|
* "all segments lit" digits with the live value glowing over them, so an unused digit still
|
||||||
|
* shows its ghost the way a real display does. A game or a theme may replace this file.
|
||||||
|
* Licensed under the GNU General Public License, version 3 or later.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: FreeSans;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff3018;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The cabinet the readouts are set into. */
|
||||||
|
#board {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 12dp 24dp;
|
||||||
|
background-color: #060606;
|
||||||
|
decorator: linear-gradient(180deg, #1a1a1c, #050505);
|
||||||
|
border: 3dp #3a3a40;
|
||||||
|
border-radius: 10dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 10dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An empty second player cell leaves the row to the other two. */
|
||||||
|
.cell:empty {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 20dp;
|
||||||
|
letter-spacing: 3dp;
|
||||||
|
color: #c8b070;
|
||||||
|
margin: 0 0 4dp 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A readout stacks the live digits exactly over the dim ones, so both are the same size and
|
||||||
|
the same shape whatever the value is. The slant is the LED look. */
|
||||||
|
.readout {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
font-size: 54dp;
|
||||||
|
letter-spacing: 6dp;
|
||||||
|
transform: skewX(-7deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.readout.small {
|
||||||
|
font-size: 34dp;
|
||||||
|
letter-spacing: 3dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
display: block;
|
||||||
|
color: #4d1108;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
color: #ff3018;
|
||||||
|
font-effect: glow(2dp 5dp #ff6a3cc0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The life count sits under the score, smaller, with its own label beside it. */
|
||||||
|
.lives {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 6dp 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lives .label {
|
||||||
|
font-size: 16dp;
|
||||||
|
margin: 0 8dp 0 0;
|
||||||
|
}
|
||||||
34
assets/scoreBezel.rml
Normal file
34
assets/scoreBezel.rml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<rml>
|
||||||
|
<!--
|
||||||
|
Singe 3
|
||||||
|
Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
|
||||||
|
The score panel: the document behind the scoreBezel calls, an arcade scoreboard with credits
|
||||||
|
and a score and a life count for each of two players. The engine writes the digits into the
|
||||||
|
.value elements and empties #player2 when the second score is switched off, so everything the
|
||||||
|
panel looks like lives in Singe/scoreBezel.rcss and everything it says comes from the game.
|
||||||
|
Licensed under the GNU General Public License, version 3 or later.
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<title>Score</title>
|
||||||
|
<link type="text/rcss" href="Singe/scoreBezel.rcss"/>
|
||||||
|
</head>
|
||||||
|
<body id="score">
|
||||||
|
<div id="board">
|
||||||
|
<div class="cell" id="player1">
|
||||||
|
<div class="label">1UP</div>
|
||||||
|
<div class="readout"><div class="ghost">888888</div><div class="value" id="score1">000000</div></div>
|
||||||
|
<div class="lives"><div class="label">LIVES</div><div class="readout small"><div class="ghost">8</div><div class="value" id="lives1">0</div></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="cell" id="middle">
|
||||||
|
<div class="label">CREDITS</div>
|
||||||
|
<div class="readout"><div class="ghost">88</div><div class="value" id="credits">00</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="cell" id="player2">
|
||||||
|
<div class="label">2UP</div>
|
||||||
|
<div class="readout"><div class="ghost">888888</div><div class="value" id="score2">000000</div></div>
|
||||||
|
<div class="lives"><div class="label">LIVES</div><div class="readout small"><div class="ghost">8</div><div class="value" id="lives2">0</div></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
124
assets/settings.cfg
Normal file
124
assets/settings.cfg
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
--[[
|
||||||
|
*
|
||||||
|
* Singe 3
|
||||||
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 3
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
* 02110-1301, USA.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
--]]
|
||||||
|
|
||||||
|
|
||||||
|
-- settings.cfg: the options you would otherwise type on every command line.
|
||||||
|
--
|
||||||
|
-- Copy this file to settings.cfg and uncomment what you want. Singe reads it
|
||||||
|
-- from the same four places it reads controls.cfg, in this order, and a later
|
||||||
|
-- one wins key by key:
|
||||||
|
--
|
||||||
|
-- 1. the directory Singe was started in
|
||||||
|
-- 2. the directory above the game's data directory
|
||||||
|
-- 3. the game's data directory
|
||||||
|
-- 4. the game's own script directory
|
||||||
|
--
|
||||||
|
-- Every key is the long name of a command line option without the leading
|
||||||
|
-- dashes, so there is no second vocabulary to learn, and the command line
|
||||||
|
-- always wins over anything set here. A games.dat entry for the game does
|
||||||
|
-- too, because what is here is a default and what is there describes the game.
|
||||||
|
-- A key that is not an option, or one that names the game or a path, is an
|
||||||
|
-- error naming this file and the key.
|
||||||
|
--
|
||||||
|
-- Options that take no value on the command line take true or false here.
|
||||||
|
-- Setting one to false is the same as leaving it out.
|
||||||
|
--
|
||||||
|
-- The value shown against each key is its default, or an example where the
|
||||||
|
-- default is worked out from your display or from the game.
|
||||||
|
|
||||||
|
|
||||||
|
-- The window ------------------------------------------------------------
|
||||||
|
|
||||||
|
-- screen = 1 -- which display, counting from 1; without it, the primary one
|
||||||
|
-- xresolution = 1280 -- window width in pixels; without it, one that fits the display
|
||||||
|
-- yresolution = 960 -- window height in pixels; without it, one that fits the width
|
||||||
|
-- aspect = "16:9" -- which window size table to pick from: 4:3, 16:9 or 16:10
|
||||||
|
-- fullscreen = false -- exclusive full screen at the desktop resolution
|
||||||
|
-- fullscreen_window = false -- borderless window covering the desktop
|
||||||
|
-- nologos = false -- skip the splash screens
|
||||||
|
-- canvas = "720x480" -- world size for a game with no disc
|
||||||
|
|
||||||
|
|
||||||
|
-- Where the picture sits -------------------------------------------------
|
||||||
|
|
||||||
|
-- stretch = false -- fill the window instead of keeping the video's shape
|
||||||
|
-- scalefactor = 100 -- per cent of the window the picture fills, 25 to 100
|
||||||
|
-- shiftx = 0 -- move it inside the room scalefactor left, -100 to 100
|
||||||
|
-- shifty = 0
|
||||||
|
-- rotate = 0 -- turn the whole presentation clockwise: 0, 90, 180 or 270
|
||||||
|
-- linearscale = true -- smooth the overlay as the window scales it, or take the nearest pixel
|
||||||
|
-- monochrome = false -- start with the disc picture in grey
|
||||||
|
|
||||||
|
|
||||||
|
-- Cabinet artwork --------------------------------------------------------
|
||||||
|
|
||||||
|
-- bezel = "cabinet.png" -- artwork in the bezels folder, drawn around the picture
|
||||||
|
-- bezeldir = "bezels" -- the folder holding it
|
||||||
|
-- bezelflip = false -- draw the artwork in front of the picture instead of behind it
|
||||||
|
-- sindenedge = "video" -- where a Sinden border sits: video, or window with artwork loaded
|
||||||
|
|
||||||
|
|
||||||
|
-- Sound ------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- nosound = false -- mute everything for the whole run
|
||||||
|
-- startsilent = false -- start muted until the first input of any kind
|
||||||
|
-- volume_vldp = 100 -- laserdisc volume in per cent
|
||||||
|
-- volume_nonvldp = 100 -- sound effect and extra video volume in per cent
|
||||||
|
-- audio = 0 -- which audio track of the disc video to play
|
||||||
|
-- altaudio = "" -- play <base><suffix>.ogg beside the disc video instead
|
||||||
|
-- audiodelay = 0 -- milliseconds the audio is heard late (negative if early)
|
||||||
|
|
||||||
|
|
||||||
|
-- Input ------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- nomouse = false -- ignore mice and light guns
|
||||||
|
-- nogamepad = false -- ignore gamepads
|
||||||
|
-- nocrosshair = false -- ask the game not to draw its own crosshair
|
||||||
|
-- haptic = 4 -- strongest rumble step a game may use; 0 turns rumble off
|
||||||
|
-- keymapfile = "controls.cfg" -- read the mappings from this file and make no search
|
||||||
|
-- sindengun = "10 5" -- the Sinden light gun border
|
||||||
|
-- xratio = 0 -- gun coordinate scale a game reads with ratioGetX()
|
||||||
|
-- yratio = 0 -- ... and ratioGetY()
|
||||||
|
|
||||||
|
-- mapjoysticks = true -- write a gamepad mapping for a device SDL does not recognise
|
||||||
|
-- gamepad_reorder = "3210" -- which pad fills which slot, as enumeration positions from 0
|
||||||
|
-- trigger_threshold = 0 -- per cent of full travel a trigger counts as pressed at; 0 uses DEAD_ZONE
|
||||||
|
-- joymouse = false -- let the first gamepad's left stick drive the mouse cursor
|
||||||
|
-- js_range = 5 -- how fast it drives it, 1 to 20
|
||||||
|
-- manymouse = false -- tell the mice apart whatever the game asks for
|
||||||
|
-- absolutes_only = false -- keep only the mice that report absolute positions, which light guns do
|
||||||
|
|
||||||
|
|
||||||
|
-- Cabinets and front ends ------------------------------------------------
|
||||||
|
|
||||||
|
-- idleexit = 0 -- quit after this many seconds with no input; 0 never does
|
||||||
|
-- fvalue = 0 -- one number handed to the game, which reads it with getFValue()
|
||||||
|
|
||||||
|
|
||||||
|
-- Working on a game ------------------------------------------------------
|
||||||
|
|
||||||
|
-- reload = false -- rerun the game when one of its loose script files changes
|
||||||
|
-- program = false -- trace engine activity to trace.txt
|
||||||
|
-- trace = false -- trace every Lua call to trace.txt
|
||||||
|
-- showcalculated = false -- print the frame ranges of every framefile segment
|
||||||
|
-- softwarevideo = false -- decode video in software even when the machine can do it in hardware
|
||||||
44
assets/subtitle.rcss
Normal file
44
assets/subtitle.rcss
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Singe 3
|
||||||
|
* Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
*
|
||||||
|
* What a subtitle looks like: centred lines of light text on a dark bar just wide enough to hold
|
||||||
|
* them, at whatever height the game asked for, over the picture. A game or a theme may replace
|
||||||
|
* this file; the engine only ever sets #slot's contents and the .cue's top.
|
||||||
|
* Licensed under the GNU General Public License, version 3 or later.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-family: FreeSans;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The document covers the picture, so a cue is placed down it as a per cent. */
|
||||||
|
#slot {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cue {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The bar hugs the text rather than spanning the picture, as a subtitle's does. */
|
||||||
|
.cue .text {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4dp 18dp;
|
||||||
|
font-size: 30dp;
|
||||||
|
line-height: 36dp;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #141414e0;
|
||||||
|
border-radius: 4dp;
|
||||||
|
font-effect: shadow(1dp 1dp #000000ff);
|
||||||
|
}
|
||||||
19
assets/subtitle.rml
Normal file
19
assets/subtitle.rml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<rml>
|
||||||
|
<!--
|
||||||
|
Singe 3
|
||||||
|
Copyright (C) 2006-2026 Scott Duensing <scott@kangaroopunch.com>
|
||||||
|
|
||||||
|
Subtitles: the document behind srtLoad, srtEnable, srtPosition and overlayBanner. The engine
|
||||||
|
writes one cue, or one banner, into #slot as a .cue whose inline style carries the height it was
|
||||||
|
asked for, so everything a subtitle looks like lives in Singe/subtitle.rcss and everything it
|
||||||
|
says comes from the game or from its .srt file.
|
||||||
|
Licensed under the GNU General Public License, version 3 or later.
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<title>Subtitles</title>
|
||||||
|
<link type="text/rcss" href="Singe/subtitle.rcss"/>
|
||||||
|
</head>
|
||||||
|
<body id="subtitle">
|
||||||
|
<div id="slot"></div>
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
2447
docs/Manual.adoc
2447
docs/Manual.adoc
File diff suppressed because it is too large
Load diff
|
|
@ -31,11 +31,16 @@
|
||||||
#include "generated/singeLogo.h"
|
#include "generated/singeLogo.h"
|
||||||
#include "generated/Framework_singe.h"
|
#include "generated/Framework_singe.h"
|
||||||
#include "generated/controls_cfg.h"
|
#include "generated/controls_cfg.h"
|
||||||
|
#include "generated/settings_cfg.h"
|
||||||
#include "generated/Menu_singe.h"
|
#include "generated/Menu_singe.h"
|
||||||
#include "generated/MenuClassic_singe.h"
|
#include "generated/MenuClassic_singe.h"
|
||||||
#include "generated/Menu_rml.h"
|
#include "generated/Menu_rml.h"
|
||||||
#include "generated/menu_rcss.h"
|
#include "generated/menu_rcss.h"
|
||||||
#include "generated/gui_rcss.h"
|
#include "generated/gui_rcss.h"
|
||||||
|
#include "generated/scoreBezel_rml.h"
|
||||||
|
#include "generated/scoreBezel_rcss.h"
|
||||||
|
#include "generated/subtitle_rml.h"
|
||||||
|
#include "generated/subtitle_rcss.h"
|
||||||
#include "generated/FreeSansBold_ttf.h"
|
#include "generated/FreeSansBold_ttf.h"
|
||||||
#include "generated/menuBackground_mkv.h"
|
#include "generated/menuBackground_mkv.h"
|
||||||
#include "generated/click_wav.h"
|
#include "generated/click_wav.h"
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@
|
||||||
#define REPORT_COLUMN_WIDTH 8
|
#define REPORT_COLUMN_WIDTH 8
|
||||||
|
|
||||||
|
|
||||||
|
#define LEGACY_VIDEO_EXTENSION "m2v" // The one container whose audio is always a sidecar
|
||||||
|
|
||||||
|
|
||||||
typedef struct FrameLineS {
|
typedef struct FrameLineS {
|
||||||
int32_t videoHandle;
|
int32_t videoHandle;
|
||||||
int64_t frame; // First laserdisc frame number in this segment
|
int64_t frame; // First laserdisc frame number in this segment
|
||||||
|
|
@ -85,6 +88,7 @@ static void _selectSegment(FrameFileT *f, int32_t index, int64_t frame, int32_t
|
||||||
// Transfer previous video's properties to this one
|
// Transfer previous video's properties to this one
|
||||||
videoGetVolume(oldHandle, &left, &right);
|
videoGetVolume(oldHandle, &left, &right);
|
||||||
videoSetVolume(newHandle, left, right);
|
videoSetVolume(newHandle, left, right);
|
||||||
|
videoSetMonochrome(newHandle, videoGetMonochrome(oldHandle));
|
||||||
track = videoGetAudioTrack(oldHandle);
|
track = videoGetAudioTrack(oldHandle);
|
||||||
if ((track >= 0) && (track < videoGetAudioTracks(newHandle))) {
|
if ((track >= 0) && (track < videoGetAudioTracks(newHandle))) {
|
||||||
videoSetAudioTrack(newHandle, track);
|
videoSetAudioTrack(newHandle, track);
|
||||||
|
|
@ -127,6 +131,31 @@ static void _showCalculated(const FrameFileT *f) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The sidecar audio file a video plays with: "<base><suffix>.ogg", or NULL for the audio inside
|
||||||
|
// the container. With no suffix only the old m2v/ogg pairs look for one, and only when it exists;
|
||||||
|
// the audio may be packed like the video, so the vfs is asked. With a suffix any container may
|
||||||
|
// have one, and its absence is reported as NULL too, so the caller checks the file itself.
|
||||||
|
char *frameFileAudioName(const char *filename, const char *suffix) {
|
||||||
|
const char *extension = utilGetFileExtension(filename);
|
||||||
|
int32_t baseLength = (int32_t)(extension - filename);
|
||||||
|
char *audio = NULL;
|
||||||
|
|
||||||
|
if ((suffix[0] == 0) && (utilStricmp(extension, LEGACY_VIDEO_EXTENSION) != 0)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if ((extension > filename) && (extension[-1] == '.')) {
|
||||||
|
baseLength--;
|
||||||
|
}
|
||||||
|
audio = utilCreateString("%.*s%s.ogg", baseLength, filename, suffix);
|
||||||
|
if (!vfsExists(audio)) {
|
||||||
|
free(audio);
|
||||||
|
audio = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return audio;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle) {
|
int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle) {
|
||||||
FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileGetFrame");
|
FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileGetFrame");
|
||||||
int32_t i = 0;
|
int32_t i = 0;
|
||||||
|
|
@ -221,15 +250,7 @@ int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer
|
||||||
files = newFiles;
|
files = newFiles;
|
||||||
files[count].frame = frame;
|
files[count].frame = frame;
|
||||||
files[count].filename = utilCreateString("%s%s", path, name);
|
files[count].filename = utilCreateString("%s%s", path, name);
|
||||||
// Is this an old m2v/ogg pair? The audio may be packed like the video, so ask the vfs.
|
audio = frameFileAudioName(files[count].filename, "");
|
||||||
audio = NULL;
|
|
||||||
if (utilStricmp(utilGetFileExtension(files[count].filename), "m2v") == 0) {
|
|
||||||
audio = utilCreateString("%.*s.ogg", (int32_t)(strlen(files[count].filename) - strlen("m2v") - 1), files[count].filename);
|
|
||||||
if (!vfsExists(audio)) {
|
|
||||||
free(audio);
|
|
||||||
audio = NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer, false);
|
files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer, false);
|
||||||
free(audio);
|
free(audio);
|
||||||
count++;
|
count++;
|
||||||
|
|
@ -301,6 +322,35 @@ void frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Points every segment at the "<base><suffix>.ogg" beside its video, or back at the default audio
|
||||||
|
// for an empty suffix, reopening each at its current frame. Nothing changes unless every segment
|
||||||
|
// has the file.
|
||||||
|
bool frameFileSetAudioSuffix(int32_t frameFileHandle, const char *suffix) {
|
||||||
|
FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileSetAudioSuffix");
|
||||||
|
int32_t i = 0;
|
||||||
|
char *audio = NULL;
|
||||||
|
|
||||||
|
if (suffix[0] != 0) {
|
||||||
|
for (i = 0; i < f->count; i++) {
|
||||||
|
audio = frameFileAudioName(f->files[i].filename, suffix);
|
||||||
|
if (audio == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
free(audio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (i = 0; i < f->count; i++) {
|
||||||
|
audio = frameFileAudioName(f->files[i].filename, suffix);
|
||||||
|
if (!videoReopenAudio(f->files[i].videoHandle, audio)) {
|
||||||
|
utilDie("Unable to open %s for audio.", audio);
|
||||||
|
}
|
||||||
|
free(audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void frameFileUnload(int32_t frameFileHandle) {
|
void frameFileUnload(int32_t frameFileHandle) {
|
||||||
FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileUnload");
|
FrameFileT *f = _getFrameFile(frameFileHandle, "frameFileUnload");
|
||||||
int32_t i = 0;
|
int32_t i = 0;
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,12 @@
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
|
|
||||||
|
char *frameFileAudioName(const char *filename, const char *suffix);
|
||||||
int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle);
|
int64_t frameFileGetFrame(int32_t frameFileHandle, int32_t videoHandle);
|
||||||
int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated);
|
int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer *renderer, bool showCalculated);
|
||||||
void frameFileQuit(void);
|
void frameFileQuit(void);
|
||||||
void frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame);
|
void frameFileSeek(int32_t frameFileHandle, int64_t seekFrame, int32_t *videoHandle, int64_t *actualFrame);
|
||||||
|
bool frameFileSetAudioSuffix(int32_t frameFileHandle, const char *suffix);
|
||||||
void frameFileUnload(int32_t frameFileHandle);
|
void frameFileUnload(int32_t frameFileHandle);
|
||||||
void frameFileUpdate(int32_t frameFileHandle, int32_t *videoHandle);
|
void frameFileUpdate(int32_t frameFileHandle, int32_t *videoHandle);
|
||||||
|
|
||||||
|
|
|
||||||
1397
src/main.c
1397
src/main.c
File diff suppressed because it is too large
Load diff
16
src/main.h
16
src/main.h
|
|
@ -30,8 +30,22 @@
|
||||||
|
|
||||||
#define VOLUME_MIN 0
|
#define VOLUME_MIN 0
|
||||||
#define VOLUME_MAX 100
|
#define VOLUME_MAX 100
|
||||||
#define SCALE_FACTOR_MIN 50
|
#define SCALE_FACTOR_MIN 25
|
||||||
#define SCALE_FACTOR_MAX 100
|
#define SCALE_FACTOR_MAX 100
|
||||||
|
#define SHIFT_MIN -100
|
||||||
|
#define SHIFT_MAX 100
|
||||||
|
#define ROTATE_MIN 0
|
||||||
|
#define ROTATE_MAX 270
|
||||||
|
#define ROTATE_STEP 90
|
||||||
|
#define RUMBLE_LEVEL_NONE 0 // --haptic 0: the pads never rumble
|
||||||
|
#define RUMBLE_LEVEL_MIN 1 // controllerDoRumble strength and duration steps
|
||||||
|
#define RUMBLE_LEVEL_MAX 4
|
||||||
|
#define GAMEPAD_ORDER_DIGITS 10 // --gamepad_reorder names enumeration positions 0 to 9
|
||||||
|
#define JOY_MOUSE_RANGE_MIN 1 // --js_range, in Hypseus's own steps
|
||||||
|
#define JOY_MOUSE_RANGE_MAX 20
|
||||||
|
#define JOY_MOUSE_RANGE_DEFAULT 5
|
||||||
|
#define TRIGGER_THRESHOLD_MIN 0.0 // --trigger_threshold: 0 leaves the triggers on DEAD_ZONE
|
||||||
|
#define TRIGGER_THRESHOLD_MAX 100.0 // Per cent of full travel, as Hypseus writes it
|
||||||
#define DIRECTORY_MODE 0777
|
#define DIRECTORY_MODE 0777
|
||||||
#define SCRIPT_MODE 0755 // Menu.sh: chmod ignores the umask, so no world write
|
#define SCRIPT_MODE 0755 // Menu.sh: chmod ignores the umask, so no world write
|
||||||
#define CANVAS_DEFAULT_WIDTH 720
|
#define CANVAS_DEFAULT_WIDTH 720
|
||||||
|
|
|
||||||
|
|
@ -6126,6 +6126,7 @@ void sceneUpdateTransforms(void) {
|
||||||
void sceneUpdateVideo(SceneVideoSourceFn source) {
|
void sceneUpdateVideo(SceneVideoSourceFn source) {
|
||||||
int32_t x;
|
int32_t x;
|
||||||
bool any = false;
|
bool any = false;
|
||||||
|
SDL_Texture *previous = SDL_GetRenderTarget(_scene.renderer);
|
||||||
SDL_Texture *frame;
|
SDL_Texture *frame;
|
||||||
float width;
|
float width;
|
||||||
float height;
|
float height;
|
||||||
|
|
@ -6154,7 +6155,8 @@ void sceneUpdateVideo(SceneVideoSourceFn source) {
|
||||||
any = true;
|
any = true;
|
||||||
}
|
}
|
||||||
if (any) {
|
if (any) {
|
||||||
SDL_SetRenderTarget(_scene.renderer, NULL);
|
// Back to whatever was being drawn into, which is a texture when --rotate is turning the frame.
|
||||||
|
SDL_SetRenderTarget(_scene.renderer, previous);
|
||||||
SDL_FlushRenderer(_scene.renderer);
|
SDL_FlushRenderer(_scene.renderer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3882
src/singe.c
3882
src/singe.c
File diff suppressed because it is too large
Load diff
49
src/singe.h
49
src/singe.h
|
|
@ -41,6 +41,14 @@ typedef enum ToolModeE {
|
||||||
TOOL_UNPACK
|
TOOL_UNPACK
|
||||||
} ToolModeE;
|
} ToolModeE;
|
||||||
|
|
||||||
|
// Where the Sinden light gun border sits. Auto puts it at the window's edge when a bezel is
|
||||||
|
// loaded (the gun's camera sees the whole screen) and around the picture when none is.
|
||||||
|
typedef enum SindenEdgeE {
|
||||||
|
SINDEN_EDGE_AUTO = 0,
|
||||||
|
SINDEN_EDGE_VIDEO,
|
||||||
|
SINDEN_EDGE_WINDOW
|
||||||
|
} SindenEdgeE;
|
||||||
|
|
||||||
// Number of --sindengun arguments selects the border style.
|
// Number of --sindengun arguments selects the border style.
|
||||||
typedef enum SindenModeE {
|
typedef enum SindenModeE {
|
||||||
SINDEN_WHITE = 1,
|
SINDEN_WHITE = 1,
|
||||||
|
|
@ -60,7 +68,8 @@ typedef enum GivenE {
|
||||||
GIVEN_SINDEN = 1 << 4,
|
GIVEN_SINDEN = 1 << 4,
|
||||||
GIVEN_AUDIO_TRACK = 1 << 5,
|
GIVEN_AUDIO_TRACK = 1 << 5,
|
||||||
GIVEN_AUDIO_DELAY = 1 << 6,
|
GIVEN_AUDIO_DELAY = 1 << 6,
|
||||||
GIVEN_CANVAS = 1 << 7
|
GIVEN_CANVAS = 1 << 7,
|
||||||
|
GIVEN_AUDIO_SUFFIX = 1 << 8
|
||||||
} GivenE;
|
} GivenE;
|
||||||
|
|
||||||
typedef struct ConfigS {
|
typedef struct ConfigS {
|
||||||
|
|
@ -72,8 +81,13 @@ typedef struct ConfigS {
|
||||||
ToolModeE toolMode;
|
ToolModeE toolMode;
|
||||||
uint32_t given; // GivenE bits
|
uint32_t given; // GivenE bits
|
||||||
char *gameDir; // -G: where the games and the Singe folder live; becomes the working directory
|
char *gameDir; // -G: where the games and the Singe folder live; becomes the working directory
|
||||||
|
char *bezelFile; // --bezel: the artwork image inside the bezels folder
|
||||||
|
char *bezelDir; // --bezeldir: the folder holding it, in place of "bezels"
|
||||||
char *dataDir;
|
char *dataDir;
|
||||||
char *dataDirBase;
|
char *dataDirBase;
|
||||||
|
char *keymapFile; // --keymapfile: the controls.cfg to read in place of the four place search
|
||||||
|
char *audioSuffix; // --altaudio: the disc audio suffix discAudioSuffix takes, applied at startup
|
||||||
|
char *gamepadOrder; // --gamepad_reorder: enumeration positions, one per gamepad slot, as Hypseus writes them
|
||||||
bool resolutionWasCalculated;
|
bool resolutionWasCalculated;
|
||||||
bool isFrameFile;
|
bool isFrameFile;
|
||||||
bool stretchVideo;
|
bool stretchVideo;
|
||||||
|
|
@ -89,25 +103,56 @@ typedef struct ConfigS {
|
||||||
bool reload; // --reload: watch the loose script files and rerun the game when one changes; F5 too
|
bool reload; // --reload: watch the loose script files and rerun the game when one changes; F5 too
|
||||||
bool scriptTracing;
|
bool scriptTracing;
|
||||||
bool legacySpriteArgs;
|
bool legacySpriteArgs;
|
||||||
|
bool bezelFlip; // --bezelflip: the artwork draws in front of the picture instead of behind it
|
||||||
bool disc; // Play a laserdisc video; otherwise the canvas is the world
|
bool disc; // Play a laserdisc video; otherwise the canvas is the world
|
||||||
bool softwareVideo; // Skip the platform's hardware decoder
|
bool softwareVideo; // Skip the platform's hardware decoder
|
||||||
|
bool noGamepad; // --nogamepad: ignore every gamepad, as --nomouse ignores the mice
|
||||||
|
bool mapJoysticks; // --mapjoysticks: write a gamepad mapping for a device SDL does not recognise
|
||||||
|
bool joyMouse; // --joymouse: the first gamepad's left stick drives the mouse cursor
|
||||||
|
bool manyMouse; // --manymouse: the user asks for one mouse device per player, whatever the game asks for
|
||||||
|
bool absolutesOnly; // --absolutes_only: keep only the mice that report absolute positions, which is what light guns do
|
||||||
|
bool startSilent; // --startsilent: muted until the first input of any kind
|
||||||
|
bool monochrome; // --monochrome: the disc picture starts in greyscale
|
||||||
|
bool linearScale; // --linearscale: the overlay starts filtered linearly, which is the default
|
||||||
int32_t bestRatioIndex;
|
int32_t bestRatioIndex;
|
||||||
int32_t volumeVldp;
|
int32_t volumeVldp;
|
||||||
int32_t volumeNonVldp;
|
int32_t volumeNonVldp;
|
||||||
int32_t scaleFactor;
|
int32_t scaleFactor; // --scalefactor: overscan compensation, per cent of the logical rectangle
|
||||||
|
int32_t shiftX; // --shiftx: per cent of half the logical width the picture moves right
|
||||||
|
int32_t shiftY; // --shifty: per cent of half the logical height the picture moves down
|
||||||
|
int32_t rotate; // --rotate: clockwise presentation rotation, 0, 90, 180 or 270 degrees
|
||||||
int32_t xResolution;
|
int32_t xResolution;
|
||||||
int32_t yResolution;
|
int32_t yResolution;
|
||||||
int32_t canvasWidth; // World size when there is no disc
|
int32_t canvasWidth; // World size when there is no disc
|
||||||
int32_t canvasHeight;
|
int32_t canvasHeight;
|
||||||
|
int32_t sindenEdge; // --sindenedge: SindenEdgeE, where the border rings are drawn
|
||||||
int32_t sindenArgc;
|
int32_t sindenArgc;
|
||||||
int32_t sindenArgv[SINDEN_ARG_MAX];
|
int32_t sindenArgv[SINDEN_ARG_MAX];
|
||||||
int32_t audioOutputTrack;
|
int32_t audioOutputTrack;
|
||||||
int32_t audioDelayMs; // Positive when the audio device is heard later than it reports
|
int32_t audioDelayMs; // Positive when the audio device is heard later than it reports
|
||||||
|
int32_t screen; // --screen: which display the window opens on, from 1; 0 is the primary
|
||||||
|
int32_t idleExitSeconds; // --idleexit: quit after this long with no input; 0 never quits
|
||||||
|
int32_t haptic; // --haptic: the strongest rumble step a script may ask for; 0 is no rumble
|
||||||
|
int32_t joyMouseRange; // --js_range: video pixels a frame the cursor moves at full stick deflection
|
||||||
|
double triggerThreshold; // --trigger_threshold: per cent of full travel a trigger counts as pressed at; 0 uses the dead zone
|
||||||
|
double ratioX; // --xratio: the gun coordinate scale a script reads with ratioGetX
|
||||||
|
double ratioY; // --yratio: the same for ratioGetY
|
||||||
|
double fValue; // --fvalue: the launcher's one number, read with getFValue
|
||||||
} ConfigT;
|
} ConfigT;
|
||||||
|
|
||||||
|
|
||||||
|
// One key of the settings file, and the file that set it, so an error can name both.
|
||||||
|
typedef struct SettingS {
|
||||||
|
char *key;
|
||||||
|
char *value;
|
||||||
|
char *source;
|
||||||
|
} SettingT;
|
||||||
|
|
||||||
|
|
||||||
ConfigT *confFromDatabase(const ConfigT *conf);
|
ConfigT *confFromDatabase(const ConfigT *conf);
|
||||||
ConfigT *confFromGamesDat(const ConfigT *conf);
|
ConfigT *confFromGamesDat(const ConfigT *conf);
|
||||||
|
void settingsFree(SettingT *settings, int32_t count);
|
||||||
|
SettingT *settingsLoad(const ConfigT *conf, int32_t *count);
|
||||||
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf);
|
void singe(SDL_Window *window, SDL_Renderer *renderer, SDL_GPUDevice *device, ConfigT *conf);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,12 +87,31 @@ typedef struct iso639_lang_t iso639_lang_t;
|
||||||
#define PERCENT_MAX 100
|
#define PERCENT_MAX 100
|
||||||
#define PERCENT_TO_SCALE 0.01f
|
#define PERCENT_TO_SCALE 0.01f
|
||||||
#define PLANE_COUNT 3 // Y, U, V
|
#define PLANE_COUNT 3 // Y, U, V
|
||||||
|
#define CHROMA_NEUTRAL 0x80 // U and V sample with no colour, for monochrome playback
|
||||||
|
#define FLASH_LUMA 0xd4 // videoFlash's white picture: 90 per cent luma, the value Hypseus flashes
|
||||||
|
#define LUMA_BLEND_TAPS 3 // videoSetBlend averages a luma sample with the rows above and below it
|
||||||
|
#define LUMA_LEVEL_SHIFT 3 // The distance from neutral is applied in eighths, as Hypseus applies it
|
||||||
|
#define LUMA_SAMPLE_MAX 255 // A luma sample is one byte
|
||||||
#define SCALER_PLANES 4 // libswscale reads four plane pointers and strides whatever the format
|
#define SCALER_PLANES 4 // libswscale reads four plane pointers and strides whatever the format
|
||||||
#define SEEK_RETRY_MAX 3 // Keyframes to back up when a seek lands past its target
|
#define SEEK_RETRY_MAX 3 // Keyframes to back up when a seek lands past its target
|
||||||
#define STRIDE_ALIGNMENT 64 // libswscale stores with aligned vector instructions; rows must start aligned
|
#define STRIDE_ALIGNMENT 64 // libswscale stores with aligned vector instructions; rows must start aligned
|
||||||
|
|
||||||
|
|
||||||
// What one decode request came to.
|
// What one decode request came to.
|
||||||
|
// The Pi has no VA-API or VDPAU; its decode hardware is a V4L2 memory-to-memory device driven by
|
||||||
|
// libavcodec's separate *_v4l2m2m decoders, which are chosen by name. Only the Pi build asks for
|
||||||
|
// them: on a desktop the probe would open every /dev/video* device.
|
||||||
|
#if defined(SINGE_V4L2_DECODE)
|
||||||
|
#define V4L2_DECODE true
|
||||||
|
#else
|
||||||
|
#define V4L2_DECODE false
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct V4l2DecoderS {
|
||||||
|
enum AVCodecID id;
|
||||||
|
const char *name;
|
||||||
|
} V4l2DecoderT;
|
||||||
|
|
||||||
typedef enum DecodeResultE {
|
typedef enum DecodeResultE {
|
||||||
DECODE_OK, // The wanted frame is in the back buffer
|
DECODE_OK, // The wanted frame is in the back buffer
|
||||||
DECODE_NONE, // Nothing decodable there: past the end, or a seek that could not be satisfied
|
DECODE_NONE, // Nothing decodable there: past the end, or a seek that could not be satisfied
|
||||||
|
|
@ -131,9 +150,17 @@ typedef struct AudioTrackS {
|
||||||
#pragma GCC diagnostic ignored "-Wpadded"
|
#pragma GCC diagnostic ignored "-Wpadded"
|
||||||
typedef struct VideoPlayerS {
|
typedef struct VideoPlayerS {
|
||||||
int32_t id;
|
int32_t id;
|
||||||
|
char *filename; // The video file, for reopening its own audio
|
||||||
bool playing;
|
bool playing;
|
||||||
bool resetTime;
|
bool resetTime;
|
||||||
bool rgb; // BGRA frames for scripts to read; otherwise YUV for the GPU
|
bool rgb; // BGRA frames for scripts to read; otherwise YUV for the GPU
|
||||||
|
bool monochrome; // Upload neutral chroma planes (YUV players only)
|
||||||
|
bool blend; // Smooth the luma plane down the picture (YUV players only)
|
||||||
|
bool flashed; // videoFlash put a white picture on the texture; the next update takes it off
|
||||||
|
int32_t luma; // Eighths added to every luma sample, -4 to 4; 0 leaves the picture alone
|
||||||
|
uint8_t *neutralChroma; // One chroma plane of CHROMA_NEUTRAL, allocated on first use
|
||||||
|
uint8_t *lumaPlane; // Scratch luma plane the blend, the luma level and the flash write into
|
||||||
|
size_t lumaBytes;
|
||||||
int32_t width;
|
int32_t width;
|
||||||
int32_t height;
|
int32_t height;
|
||||||
int32_t volumeLeft;
|
int32_t volumeLeft;
|
||||||
|
|
@ -160,6 +187,7 @@ typedef struct VideoPlayerS {
|
||||||
bool videoDrained;
|
bool videoDrained;
|
||||||
bool packetPending; // videoPacket holds data the decoder refused (EAGAIN)
|
bool packetPending; // videoPacket holds data the decoder refused (EAGAIN)
|
||||||
bool hwReported; // First hardware frame has been traced
|
bool hwReported; // First hardware frame has been traced
|
||||||
|
bool v4l2; // Decoding through a V4L2 memory-to-memory device
|
||||||
bool videoHasAudio; // The video file carries an audio stream of its own
|
bool videoHasAudio; // The video file carries an audio stream of its own
|
||||||
|
|
||||||
// Audio demuxer, decoder, and resampler. Owned by the main thread.
|
// Audio demuxer, decoder, and resampler. Owned by the main thread.
|
||||||
|
|
@ -211,11 +239,14 @@ typedef struct VideoPlayerS {
|
||||||
static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer);
|
static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer);
|
||||||
static void _alsaSetQuiet(bool quiet);
|
static void _alsaSetQuiet(bool quiet);
|
||||||
static int64_t _audioClock(VideoPlayerT *v, uint64_t now);
|
static int64_t _audioClock(VideoPlayerT *v, uint64_t now);
|
||||||
|
static void _audioClose(VideoPlayerT *v);
|
||||||
static void _audioCloseTrack(VideoPlayerT *v);
|
static void _audioCloseTrack(VideoPlayerT *v);
|
||||||
|
static bool _audioOpen(const char *filename, AVFormatContext **format, AudioTrackT **tracks, int32_t *count);
|
||||||
static void _audioQueueFrame(VideoPlayerT *v);
|
static void _audioQueueFrame(VideoPlayerT *v);
|
||||||
static void _audioSeek(VideoPlayerT *v, int64_t ms);
|
static void _audioSeek(VideoPlayerT *v, int64_t ms);
|
||||||
static void _audioSelectTrack(VideoPlayerT *v, int32_t track);
|
static void _audioSelectTrack(VideoPlayerT *v, int32_t track);
|
||||||
static void _audioSetupResampler(VideoPlayerT *v);
|
static void _audioSetupResampler(VideoPlayerT *v);
|
||||||
|
static void _audioStartTrack(VideoPlayerT *v, int32_t track);
|
||||||
static int _avioRead(void *opaque, uint8_t *buffer, int size);
|
static int _avioRead(void *opaque, uint8_t *buffer, int size);
|
||||||
static int64_t _avioSeek(void *opaque, int64_t offset, int whence);
|
static int64_t _avioSeek(void *opaque, int64_t offset, int whence);
|
||||||
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath);
|
static void _buildFrameTable(VideoPlayerT *v, const char *filename, const char *indexPath);
|
||||||
|
|
@ -230,10 +261,13 @@ static AVFormatContext *_formatOpen(const char *filename);
|
||||||
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 char *_indexFileName(const char *filename, const char *indexPath);
|
static char *_indexFileName(const char *filename, const char *indexPath);
|
||||||
static void _loadAudio(VideoPlayerT *v, const char *filename);
|
static uint8_t *_lumaPlane(VideoPlayerT *v);
|
||||||
static void _measureDeviceQueue(void *udata, MIX_Mixer *mixer, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
|
static void _measureDeviceQueue(void *udata, MIX_Mixer *mixer, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
|
||||||
|
static uint8_t *_neutralChroma(VideoPlayerT *v);
|
||||||
|
static bool _openDecoder(VideoPlayerT *v, const AVCodec *decoder, const AVStream *stream);
|
||||||
static void _openHardware(VideoPlayerT *v, const AVCodec *decoder);
|
static void _openHardware(VideoPlayerT *v, const AVCodec *decoder);
|
||||||
static void _openVideo(VideoPlayerT *v, const char *filename);
|
static void _openVideo(VideoPlayerT *v, const char *filename);
|
||||||
|
static const uint8_t *_pictureFilter(VideoPlayerT *v);
|
||||||
static bool _readIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *expected);
|
static bool _readIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *expected);
|
||||||
static void _reportKeyframes(VideoPlayerT *v, const char *filename);
|
static void _reportKeyframes(VideoPlayerT *v, const char *filename);
|
||||||
static void _requestFrame(VideoPlayerT *v);
|
static void _requestFrame(VideoPlayerT *v);
|
||||||
|
|
@ -244,6 +278,7 @@ static int64_t _streamTimeToMs(int64_t ts, AVRational timeBase);
|
||||||
static bool _takeDecodedFrame(VideoPlayerT *v);
|
static bool _takeDecodedFrame(VideoPlayerT *v);
|
||||||
static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
|
static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec, float *pcm, int32_t samples);
|
||||||
static void _uploadFrame(VideoPlayerT *v);
|
static void _uploadFrame(VideoPlayerT *v);
|
||||||
|
static const AVCodec *_v4l2Decoder(const AVCodec *decoder);
|
||||||
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header);
|
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header);
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -369,12 +404,82 @@ static int64_t _audioClock(VideoPlayerT *v, uint64_t now) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Releases the whole audio side: the mixer track, the decoder, the demuxer and the track table.
|
||||||
|
static void _audioClose(VideoPlayerT *v) {
|
||||||
|
int32_t x = 0;
|
||||||
|
|
||||||
|
if (v->audioSourceCount > 0) {
|
||||||
|
MIX_DestroyTrack(v->track);
|
||||||
|
SDL_DestroyAudioStream(v->audioStream);
|
||||||
|
_audioCloseTrack(v);
|
||||||
|
av_frame_free(&v->audioFrame);
|
||||||
|
av_packet_free(&v->audioPacket);
|
||||||
|
_formatClose(&v->audioFormat);
|
||||||
|
for (x = 0; x < v->audioSourceCount; x++) {
|
||||||
|
free(v->audio[x].language);
|
||||||
|
}
|
||||||
|
free(v->audio);
|
||||||
|
free(v->audioBuffer);
|
||||||
|
}
|
||||||
|
v->track = NULL;
|
||||||
|
v->audioStream = NULL;
|
||||||
|
v->audio = NULL;
|
||||||
|
v->audioBuffer = NULL;
|
||||||
|
v->audioBufferSamples = 0;
|
||||||
|
v->audioSourceCount = 0;
|
||||||
|
v->currentAudioTrack = -1;
|
||||||
|
v->audioEof = false;
|
||||||
|
v->audioClockValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void _audioCloseTrack(VideoPlayerT *v) {
|
static void _audioCloseTrack(VideoPlayerT *v) {
|
||||||
avcodec_free_context(&v->audioCodec);
|
avcodec_free_context(&v->audioCodec);
|
||||||
swr_free(&v->swr);
|
swr_free(&v->swr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Opens the audio side of a file (the video file, or a separate audio file) and lists its tracks.
|
||||||
|
// The track table is sized for every stream; only the audio ones fill it. A file with no audio
|
||||||
|
// stream leaves the demuxer closed and the count at zero. False when the file cannot be read.
|
||||||
|
static bool _audioOpen(const char *filename, AVFormatContext **format, AudioTrackT **tracks, int32_t *count) {
|
||||||
|
AVDictionaryEntry *tag = NULL;
|
||||||
|
int32_t x = 0;
|
||||||
|
|
||||||
|
*tracks = NULL;
|
||||||
|
*count = 0;
|
||||||
|
*format = _formatOpen(filename);
|
||||||
|
if (*format == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (avformat_find_stream_info(*format, NULL) < 0) {
|
||||||
|
_formatClose(format);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*tracks = (AudioTrackT *)calloc((size_t)(*format)->nb_streams + 1, sizeof(AudioTrackT));
|
||||||
|
if (!*tracks) {
|
||||||
|
utilDie("Unable to allocate audio tracks.");
|
||||||
|
}
|
||||||
|
for (x = 0; x < (int32_t)(*format)->nb_streams; x++) {
|
||||||
|
if ((*format)->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||||
|
(*tracks)[*count].streamIndex = x;
|
||||||
|
tag = av_dict_get((*format)->streams[x]->metadata, "language", NULL, 0);
|
||||||
|
if (tag != NULL) {
|
||||||
|
(*tracks)[*count].language = strdup(tag->value);
|
||||||
|
}
|
||||||
|
(*count)++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (*count == 0) {
|
||||||
|
free(*tracks);
|
||||||
|
*tracks = NULL;
|
||||||
|
_formatClose(format);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Resamples one decoded audio frame to the track's format and queues it, dropping whatever lies
|
// Resamples one decoded audio frame to the track's format and queues it, dropping whatever lies
|
||||||
// before the seek target.
|
// before the seek target.
|
||||||
static void _audioQueueFrame(VideoPlayerT *v) {
|
static void _audioQueueFrame(VideoPlayerT *v) {
|
||||||
|
|
@ -492,6 +597,37 @@ static void _audioSetupResampler(VideoPlayerT *v) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Starts the mixer track for an opened audio side on the given track: a track that never halts,
|
||||||
|
// fed from its own demuxer on the main thread, at the player's volume, paused unless the video plays.
|
||||||
|
static void _audioStartTrack(VideoPlayerT *v, int32_t track) {
|
||||||
|
SDL_PropertiesID playProps = 0;
|
||||||
|
|
||||||
|
v->audioPacket = av_packet_alloc();
|
||||||
|
v->audioFrame = av_frame_alloc();
|
||||||
|
if (!v->audioPacket || !v->audioFrame) {
|
||||||
|
utilDie("Unable to allocate audio decoding buffers.");
|
||||||
|
}
|
||||||
|
v->track = MIX_CreateTrack(_mixer);
|
||||||
|
if (!v->track) {
|
||||||
|
utilDie("%s", SDL_GetError());
|
||||||
|
}
|
||||||
|
_audioSelectTrack(v, track);
|
||||||
|
if (!MIX_SetTrackRawCallback(v->track, _trackMixed, v)) {
|
||||||
|
utilDie("%s", SDL_GetError());
|
||||||
|
}
|
||||||
|
playProps = SDL_CreateProperties();
|
||||||
|
SDL_SetBooleanProperty(playProps, MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN, false);
|
||||||
|
if (!MIX_PlayTrack(v->track, playProps)) {
|
||||||
|
utilDie("%s", SDL_GetError());
|
||||||
|
}
|
||||||
|
SDL_DestroyProperties(playProps);
|
||||||
|
videoSetVolume(v->id, v->volumeLeft, v->volumeRight);
|
||||||
|
if (!v->playing) {
|
||||||
|
MIX_PauseTrack(v->track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static int _avioRead(void *opaque, uint8_t *buffer, int size) {
|
static int _avioRead(void *opaque, uint8_t *buffer, int size) {
|
||||||
int64_t got = vfsStreamRead((VfsStreamT *)opaque, buffer, size);
|
int64_t got = vfsStreamRead((VfsStreamT *)opaque, buffer, size);
|
||||||
|
|
||||||
|
|
@ -686,6 +822,10 @@ static DecodeResultE _decodeFrame(VideoPlayerT *v, int64_t want) {
|
||||||
}
|
}
|
||||||
_convertFrame(v, &v->back, v->hwFrame);
|
_convertFrame(v, &v->back, v->hwFrame);
|
||||||
} else {
|
} else {
|
||||||
|
if (v->v4l2 && !v->hwReported) {
|
||||||
|
v->hwReported = true;
|
||||||
|
utilTrace("Video %d: first V4L2 frame arrived as %s %dx%d", v->id, av_get_pix_fmt_name((enum AVPixelFormat)v->videoFrame->format), v->videoFrame->width, v->videoFrame->height);
|
||||||
|
}
|
||||||
_convertFrame(v, &v->back, v->videoFrame);
|
_convertFrame(v, &v->back, v->videoFrame);
|
||||||
}
|
}
|
||||||
av_frame_unref(v->videoFrame);
|
av_frame_unref(v->videoFrame);
|
||||||
|
|
@ -930,45 +1070,18 @@ static char *_indexFileName(const char *filename, const char *indexPath) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Opens the audio side of a file (the video file, or the separate audio file of an old framefile) and
|
// The scratch luma plane the blend, the luma level and the flash write into, allocated the first
|
||||||
// lists its tracks. The track table is sized for every stream; only the audio ones fill it.
|
// time one of them is asked for. It is the size of a decoded luma plane, stride and all.
|
||||||
static void _loadAudio(VideoPlayerT *v, const char *filename) {
|
static uint8_t *_lumaPlane(VideoPlayerT *v) {
|
||||||
AVDictionaryEntry *tag = NULL;
|
if (v->lumaPlane == NULL) {
|
||||||
int32_t x = 0;
|
v->lumaBytes = (size_t)v->front.linesize[0] * (size_t)v->height;
|
||||||
|
v->lumaPlane = malloc(v->lumaBytes);
|
||||||
v->audioFormat = _formatOpen(filename);
|
if (!v->lumaPlane) {
|
||||||
if (v->audioFormat == NULL) {
|
utilDie("Unable to allocate the luma plane.");
|
||||||
utilDie("Unable to open %s for audio.", filename);
|
|
||||||
}
|
}
|
||||||
if (avformat_find_stream_info(v->audioFormat, NULL) < 0) {
|
|
||||||
utilDie("Unable to read stream information from %s.", filename);
|
|
||||||
}
|
|
||||||
v->audio = (AudioTrackT *)calloc((size_t)v->audioFormat->nb_streams + 1, sizeof(AudioTrackT));
|
|
||||||
if (!v->audio) {
|
|
||||||
utilDie("Unable to allocate audio tracks.");
|
|
||||||
}
|
|
||||||
for (x = 0; x < (int32_t)v->audioFormat->nb_streams; x++) {
|
|
||||||
if (v->audioFormat->streams[x]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
|
|
||||||
v->audio[v->audioSourceCount].streamIndex = x;
|
|
||||||
tag = av_dict_get(v->audioFormat->streams[x]->metadata, "language", NULL, 0);
|
|
||||||
if (tag != NULL) {
|
|
||||||
v->audio[v->audioSourceCount].language = strdup(tag->value);
|
|
||||||
}
|
|
||||||
v->audioSourceCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (v->audioSourceCount == 0) {
|
|
||||||
free(v->audio);
|
|
||||||
v->audio = NULL;
|
|
||||||
_formatClose(&v->audioFormat);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
v->audioPacket = av_packet_alloc();
|
return v->lumaPlane;
|
||||||
v->audioFrame = av_frame_alloc();
|
|
||||||
if (!v->audioPacket || !v->audioFrame) {
|
|
||||||
utilDie("Unable to allocate audio decoding buffers.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1000,6 +1113,41 @@ static void _measureDeviceQueue(void *udata, MIX_Mixer *mixer, const SDL_AudioSp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// One chroma plane of CHROMA_NEUTRAL, allocated the first time monochrome or the flash asks for
|
||||||
|
// it. Both chroma planes read from the one buffer, since they share a stride.
|
||||||
|
static uint8_t *_neutralChroma(VideoPlayerT *v) {
|
||||||
|
size_t planeBytes = (size_t)v->front.linesize[1] * (size_t)((v->height + 1) / 2);
|
||||||
|
|
||||||
|
if (v->neutralChroma == NULL) {
|
||||||
|
v->neutralChroma = malloc(planeBytes);
|
||||||
|
if (!v->neutralChroma) {
|
||||||
|
utilDie("Unable to allocate the neutral chroma plane.");
|
||||||
|
}
|
||||||
|
memset(v->neutralChroma, CHROMA_NEUTRAL, planeBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v->neutralChroma;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Allocates the codec context for the stream, attaches the platform's hardware decoder when there
|
||||||
|
// is one, and opens it. Returns false with the context freed when the decoder will not open.
|
||||||
|
static bool _openDecoder(VideoPlayerT *v, const AVCodec *decoder, const AVStream *stream) {
|
||||||
|
v->videoCodec = avcodec_alloc_context3(decoder);
|
||||||
|
if (!v->videoCodec || (avcodec_parameters_to_context(v->videoCodec, stream->codecpar) < 0)) {
|
||||||
|
utilDie("Unable to set up the video decoder.");
|
||||||
|
}
|
||||||
|
v->videoCodec->thread_count = 0; // Let libavcodec pick
|
||||||
|
_openHardware(v, decoder);
|
||||||
|
if (avcodec_open2(v->videoCodec, decoder, NULL) < 0) {
|
||||||
|
avcodec_free_context(&v->videoCodec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Attaches the platform's hardware decoder to the codec context when it offers one for this codec.
|
// Attaches the platform's hardware decoder to the codec context when it offers one for this codec.
|
||||||
// Failure of any step leaves the context decoding in software.
|
// Failure of any step leaves the context decoding in software.
|
||||||
static void _openHardware(VideoPlayerT *v, const AVCodec *decoder) {
|
static void _openHardware(VideoPlayerT *v, const AVCodec *decoder) {
|
||||||
|
|
@ -1050,6 +1198,7 @@ static void _openHardware(VideoPlayerT *v, const AVCodec *decoder) {
|
||||||
// Opens the demuxer and decoder the decoder thread will use.
|
// Opens the demuxer and decoder the decoder thread will use.
|
||||||
static void _openVideo(VideoPlayerT *v, const char *filename) {
|
static void _openVideo(VideoPlayerT *v, const char *filename) {
|
||||||
const AVCodec *decoder = NULL;
|
const AVCodec *decoder = NULL;
|
||||||
|
const AVCodec *v4l2 = NULL;
|
||||||
AVStream *stream = NULL;
|
AVStream *stream = NULL;
|
||||||
int32_t x = 0;
|
int32_t x = 0;
|
||||||
|
|
||||||
|
|
@ -1080,15 +1229,18 @@ static void _openVideo(VideoPlayerT *v, const char *filename) {
|
||||||
v->fps.den = DEFAULT_FPS_DENOMINATOR;
|
v->fps.den = DEFAULT_FPS_DENOMINATOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
v->videoCodec = avcodec_alloc_context3(decoder);
|
v4l2 = _v4l2Decoder(decoder);
|
||||||
if (!v->videoCodec || (avcodec_parameters_to_context(v->videoCodec, stream->codecpar) < 0)) {
|
if ((v4l2 != NULL) && _openDecoder(v, v4l2, stream)) {
|
||||||
utilDie("Unable to set up the video decoder for %s.", filename);
|
v->v4l2 = true;
|
||||||
|
utilTrace("Hardware decoding via %s.", v4l2->name);
|
||||||
|
} else {
|
||||||
|
if (v4l2 != NULL) {
|
||||||
|
utilTrace("Hardware decoding via %s is not available; decoding in software.", v4l2->name);
|
||||||
}
|
}
|
||||||
v->videoCodec->thread_count = 0; // Let libavcodec pick
|
if (!_openDecoder(v, decoder, stream)) {
|
||||||
_openHardware(v, decoder);
|
|
||||||
if (avcodec_open2(v->videoCodec, decoder, NULL) < 0) {
|
|
||||||
utilDie("Unable to open the video decoder for %s.", filename);
|
utilDie("Unable to open the video decoder for %s.", filename);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
v->videoPacket = av_packet_alloc();
|
v->videoPacket = av_packet_alloc();
|
||||||
v->videoFrame = av_frame_alloc();
|
v->videoFrame = av_frame_alloc();
|
||||||
if (!v->videoPacket || !v->videoFrame) {
|
if (!v->videoPacket || !v->videoFrame) {
|
||||||
|
|
@ -1099,6 +1251,39 @@ static void _openVideo(VideoPlayerT *v, const char *filename) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The disc picture controls, on the decoded luma plane, in the order a viewer expects: the blend
|
||||||
|
// smooths each sample against the rows above and below it (Hypseus's own three tap average, which
|
||||||
|
// takes the hard edges off an interlaced transfer), and the luma level then brightens or darkens
|
||||||
|
// what that left, in eighths from half to one and a half. The chroma planes are untouched, so a
|
||||||
|
// change of level moves the brightness without draining the colour. Returns the plane to upload.
|
||||||
|
static const uint8_t *_pictureFilter(VideoPlayerT *v) {
|
||||||
|
const uint8_t *source = v->front.data[0];
|
||||||
|
uint8_t *plane = _lumaPlane(v);
|
||||||
|
int32_t pitch = v->front.linesize[0];
|
||||||
|
int32_t value = 0;
|
||||||
|
int32_t x = 0;
|
||||||
|
int32_t y = 0;
|
||||||
|
|
||||||
|
for (y = 0; y < v->height; y++) {
|
||||||
|
const uint8_t *above = source + ((y > 0) ? (y - 1) : y) * pitch;
|
||||||
|
const uint8_t *here = source + y * pitch;
|
||||||
|
const uint8_t *below = source + ((y < (v->height - 1)) ? (y + 1) : y) * pitch;
|
||||||
|
uint8_t *row = plane + y * pitch;
|
||||||
|
|
||||||
|
for (x = 0; x < v->width; x++) {
|
||||||
|
value = v->blend ? ((above[x] + here[x] + below[x]) / LUMA_BLEND_TAPS) : here[x];
|
||||||
|
if (v->luma != 0) {
|
||||||
|
value += (value * v->luma) >> LUMA_LEVEL_SHIFT;
|
||||||
|
value = SDL_clamp(value, 0, LUMA_SAMPLE_MAX);
|
||||||
|
}
|
||||||
|
row[x] = (uint8_t)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return plane;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Loads a cached frame table if it matches this file. Returns false when the table must be rebuilt.
|
// Loads a cached frame table if it matches this file. Returns false when the table must be rebuilt.
|
||||||
static bool _readIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *expected) {
|
static bool _readIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *expected) {
|
||||||
size_t bytes = 0;
|
size_t bytes = 0;
|
||||||
|
|
@ -1271,17 +1456,60 @@ static void _trackMixed(void *udata, MIX_Track *track, const SDL_AudioSpec *spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Pushes the front buffer to the texture. YUV players convert on the GPU.
|
// Pushes the front buffer to the texture. YUV players convert on the GPU, and the picture
|
||||||
|
// controls change the planes on the way past.
|
||||||
static void _uploadFrame(VideoPlayerT *v) {
|
static void _uploadFrame(VideoPlayerT *v) {
|
||||||
|
const uint8_t *luma = v->front.data[0];
|
||||||
|
const uint8_t *cb = v->front.data[1];
|
||||||
|
const uint8_t *cr = v->front.data[2];
|
||||||
|
|
||||||
if (v->rgb) {
|
if (v->rgb) {
|
||||||
SDL_UpdateTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0]);
|
SDL_UpdateTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0]);
|
||||||
} else {
|
} 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]);
|
if (v->blend || (v->luma != 0)) {
|
||||||
|
luma = _pictureFilter(v);
|
||||||
}
|
}
|
||||||
|
if (v->monochrome) {
|
||||||
|
// Both chroma planes read from one neutral plane; the buffers share a stride.
|
||||||
|
cb = _neutralChroma(v);
|
||||||
|
cr = cb;
|
||||||
|
}
|
||||||
|
SDL_UpdateYUVTexture(v->videoTexture, NULL, luma, v->front.linesize[0], cb, v->front.linesize[1], cr, v->front.linesize[2]);
|
||||||
|
}
|
||||||
|
v->flashed = false;
|
||||||
v->uploadedFrame = v->front.frame;
|
v->uploadedFrame = v->front.frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// The V4L2 memory-to-memory decoder for the stream's codec on a Pi build, NULL when there is none or
|
||||||
|
// hardware decoding is off. Whether the device exists is found out when the decoder opens.
|
||||||
|
static const AVCodec *_v4l2Decoder(const AVCodec *decoder) {
|
||||||
|
static const V4l2DecoderT table[] = {
|
||||||
|
{ AV_CODEC_ID_H263, "h263_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_H264, "h264_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_HEVC, "hevc_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_MPEG1VIDEO, "mpeg1_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_MPEG2VIDEO, "mpeg2_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_MPEG4, "mpeg4_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_VC1, "vc1_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_VP8, "vp8_v4l2m2m" },
|
||||||
|
{ AV_CODEC_ID_VP9, "vp9_v4l2m2m" }
|
||||||
|
};
|
||||||
|
size_t x = 0;
|
||||||
|
|
||||||
|
if (!V4L2_DECODE || !_hardwareDecoding) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (x = 0; x < SDL_arraysize(table); x++) {
|
||||||
|
if (table[x].id == decoder->id) {
|
||||||
|
return avcodec_find_decoder_by_name(table[x].name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header) {
|
static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const IndexHeaderT *header) {
|
||||||
FILE *out = fopen(indexName, "wb");
|
FILE *out = fopen(indexName, "wb");
|
||||||
|
|
||||||
|
|
@ -1299,6 +1527,25 @@ static void _writeIndexCache(VideoPlayerT *v, const char *indexName, const Index
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// One white picture on the disc, the way a laserdisc player flashes when it is searched. It goes
|
||||||
|
// up at once and the next decoded frame takes it off again; a player showing colour for a script to
|
||||||
|
// read is left alone.
|
||||||
|
void videoFlash(int32_t playerHandle) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoFlash");
|
||||||
|
uint8_t *luma = NULL;
|
||||||
|
uint8_t *chroma = NULL;
|
||||||
|
|
||||||
|
if (v->rgb || (v->front.frame < 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
luma = _lumaPlane(v);
|
||||||
|
chroma = _neutralChroma(v);
|
||||||
|
memset(luma, FLASH_LUMA, v->lumaBytes);
|
||||||
|
SDL_UpdateYUVTexture(v->videoTexture, NULL, luma, v->front.linesize[0], chroma, v->front.linesize[1], chroma, v->front.linesize[2]);
|
||||||
|
v->flashed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int32_t videoGetAudioCalibration(void) {
|
int32_t videoGetAudioCalibration(void) {
|
||||||
return _audioCalibrationMs;
|
return _audioCalibrationMs;
|
||||||
}
|
}
|
||||||
|
|
@ -1325,6 +1572,15 @@ int32_t videoGetAudioTracks(int32_t playerHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Frames per second of the loaded video, which is what a subtitle's timestamps are turned into
|
||||||
|
// frame numbers with.
|
||||||
|
double videoGetFps(int32_t playerHandle) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetFps");
|
||||||
|
|
||||||
|
return (double)v->fps.num / (double)v->fps.den;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int64_t videoGetFrame(int32_t playerHandle) {
|
int64_t videoGetFrame(int32_t playerHandle) {
|
||||||
return _getPlayer(playerHandle, "videoGetFrame")->frame;
|
return _getPlayer(playerHandle, "videoGetFrame")->frame;
|
||||||
}
|
}
|
||||||
|
|
@ -1377,6 +1633,31 @@ MIX_Mixer *videoGetMixer(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// What the decoder will be, for the trace header: the platform's hardware decoders in the order
|
||||||
|
// they are tried, or software when there are none or --softwarevideo ruled them out. Which one a
|
||||||
|
// given video actually got is traced as that video opens, since a codec may offer neither.
|
||||||
|
const char *videoGetDecoderDescription(void) {
|
||||||
|
if (!_hardwareDecoding) {
|
||||||
|
return "software only (--softwarevideo)";
|
||||||
|
}
|
||||||
|
if (V4L2_DECODE) {
|
||||||
|
return "hardware v4l2m2m where the codec allows, software otherwise";
|
||||||
|
}
|
||||||
|
#if defined(_WIN32)
|
||||||
|
return "hardware d3d11va or dxva2 where the codec allows, software otherwise";
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
return "hardware videotoolbox where the codec allows, software otherwise";
|
||||||
|
#else
|
||||||
|
return "hardware vaapi or vdpau where the codec allows, software otherwise";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool videoGetMonochrome(int32_t playerHandle) {
|
||||||
|
return _getPlayer(playerHandle, "videoGetMonochrome")->monochrome;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Reads one pixel of the frame being shown. 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");
|
||||||
|
|
@ -1442,6 +1723,22 @@ int32_t videoGetWidth(int32_t playerHandle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Reads the raw 4:2:0 samples of one pixel of the frame being shown. False for rgb players, before
|
||||||
|
// the first frame, or outside the frame.
|
||||||
|
bool videoGetYUVPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *luma, uint8_t *cb, uint8_t *cr) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetYUVPixel");
|
||||||
|
|
||||||
|
if (v->rgb || (v->front.frame < 0) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*luma = v->front.data[0][y * v->front.linesize[0] + x];
|
||||||
|
*cb = v->front.data[1][(y / 2) * v->front.linesize[1] + (x / 2)];
|
||||||
|
*cr = v->front.data[2][(y / 2) * v->front.linesize[2] + (x / 2)];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void videoInit(MIX_Mixer *mixer) {
|
void videoInit(MIX_Mixer *mixer) {
|
||||||
uint64_t started = 0;
|
uint64_t started = 0;
|
||||||
|
|
||||||
|
|
@ -1488,13 +1785,17 @@ bool videoIsPlaying(int32_t playerHandle) {
|
||||||
// scripts can read the pixels; everything else stays YUV and is converted by the GPU.
|
// 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 videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb) {
|
||||||
VideoPlayerT *v = NULL;
|
VideoPlayerT *v = NULL;
|
||||||
SDL_PropertiesID playProps = 0;
|
const char *name = audioFilename ? audioFilename : videoFilename;
|
||||||
|
|
||||||
// Create new videoPlayer
|
// Create new videoPlayer
|
||||||
v = calloc(1, sizeof(VideoPlayerT));
|
v = calloc(1, sizeof(VideoPlayerT));
|
||||||
if (!v) {
|
if (!v) {
|
||||||
utilDie("Unable to allocate new video player.");
|
utilDie("Unable to allocate new video player.");
|
||||||
}
|
}
|
||||||
|
v->filename = strdup(videoFilename);
|
||||||
|
if (!v->filename) {
|
||||||
|
utilDie("Unable to allocate new video player.");
|
||||||
|
}
|
||||||
|
|
||||||
// Set some starting values (everything else is zero from calloc)
|
// Set some starting values (everything else is zero from calloc)
|
||||||
v->currentAudioTrack = -1;
|
v->currentAudioTrack = -1;
|
||||||
|
|
@ -1534,34 +1835,21 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
|
||||||
utilDie("%s", SDL_GetError());
|
utilDie("%s", SDL_GetError());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio: a mixer track that never halts, fed from its own demuxer on the main thread. A silent
|
|
||||||
// video file is not demuxed a second time to find that out.
|
|
||||||
if ((audioFilename != NULL) || v->videoHasAudio) {
|
|
||||||
_loadAudio(v, audioFilename ? audioFilename : videoFilename);
|
|
||||||
}
|
|
||||||
if (v->audioSourceCount > 0) {
|
|
||||||
v->track = MIX_CreateTrack(_mixer);
|
|
||||||
if (!v->track) {
|
|
||||||
utilDie("%s", SDL_GetError());
|
|
||||||
}
|
|
||||||
_audioSelectTrack(v, 0);
|
|
||||||
if (!MIX_SetTrackRawCallback(v->track, _trackMixed, v)) {
|
|
||||||
utilDie("%s", SDL_GetError());
|
|
||||||
}
|
|
||||||
playProps = SDL_CreateProperties();
|
|
||||||
SDL_SetBooleanProperty(playProps, MIX_PROP_PLAY_HALT_WHEN_EXHAUSTED_BOOLEAN, false);
|
|
||||||
if (!MIX_PlayTrack(v->track, playProps)) {
|
|
||||||
utilDie("%s", SDL_GetError());
|
|
||||||
}
|
|
||||||
SDL_DestroyProperties(playProps);
|
|
||||||
// Paused until the video plays.
|
|
||||||
MIX_PauseTrack(v->track);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to player hash
|
// Add to player hash
|
||||||
v->id = _nextId++;
|
v->id = _nextId++;
|
||||||
HASH_ADD_INT(_videoPlayerHash, id, v);
|
HASH_ADD_INT(_videoPlayerHash, id, v);
|
||||||
|
|
||||||
|
// Audio: a mixer track that never halts, fed from its own demuxer on the main thread. A silent
|
||||||
|
// video file is not demuxed a second time to find that out.
|
||||||
|
if ((audioFilename != NULL) || v->videoHasAudio) {
|
||||||
|
if (!_audioOpen(name, &v->audioFormat, &v->audio, &v->audioSourceCount)) {
|
||||||
|
utilDie("Unable to open %s for audio.", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (v->audioSourceCount > 0) {
|
||||||
|
_audioStartTrack(v, 0);
|
||||||
|
}
|
||||||
|
|
||||||
return v->id;
|
return v->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1604,6 +1892,38 @@ void videoQuit(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Replaces the audio side with another file (NULL: the video's own audio, or silence when it has
|
||||||
|
// none) and realigns it at the current frame, keeping the volume, the play state and the track
|
||||||
|
// index when the new file has it. False, with the old audio untouched, when the file cannot be read.
|
||||||
|
bool videoReopenAudio(int32_t playerHandle, const char *audioFilename) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoReopenAudio");
|
||||||
|
AVFormatContext *format = NULL;
|
||||||
|
AudioTrackT *tracks = NULL;
|
||||||
|
int32_t count = 0;
|
||||||
|
int32_t track = v->currentAudioTrack;
|
||||||
|
const char *name = audioFilename;
|
||||||
|
|
||||||
|
if ((name == NULL) && v->videoHasAudio) {
|
||||||
|
name = v->filename;
|
||||||
|
}
|
||||||
|
if ((name != NULL) && !_audioOpen(name, &format, &tracks, &count)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_audioClose(v);
|
||||||
|
v->audioFormat = format;
|
||||||
|
v->audio = tracks;
|
||||||
|
v->audioSourceCount = count;
|
||||||
|
if (count > 0) {
|
||||||
|
_audioStartTrack(v, ((track >= 0) && (track < count)) ? track : 0);
|
||||||
|
}
|
||||||
|
utilTrace("Video %d: audio reopened from %s (%d track(s))", v->id, name ? name : "nothing", count);
|
||||||
|
// Drop the queued audio from the old file and realign.
|
||||||
|
v->resetTime = true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void videoSeek(int32_t playerHandle, int64_t seekFrame) {
|
void videoSeek(int32_t playerHandle, int64_t seekFrame) {
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoSeek");
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoSeek");
|
||||||
int64_t count = v->frameCount;
|
int64_t count = v->frameCount;
|
||||||
|
|
@ -1644,12 +1964,60 @@ void videoSetAudioTrack(int32_t playerHandle, int32_t track) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Smooths the picture down its rows, the way Hypseus's blend filter does: each luma sample becomes
|
||||||
|
// the average of itself and the samples above and below it. A player showing colour for a script to
|
||||||
|
// read is left alone. The change lands on the next decoded frame.
|
||||||
|
void videoSetBlend(int32_t playerHandle, bool enabled) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetBlend");
|
||||||
|
|
||||||
|
if (v->rgb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
v->blend = enabled;
|
||||||
|
if (v->front.frame >= 0) {
|
||||||
|
_uploadFrame(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Chosen before any video loads; existing players keep whatever they opened with.
|
// Chosen before any video loads; existing players keep whatever they opened with.
|
||||||
void videoSetHardwareDecoding(bool enabled) {
|
void videoSetHardwareDecoding(bool enabled) {
|
||||||
_hardwareDecoding = enabled;
|
_hardwareDecoding = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Brightens or darkens the picture. The level runs 0 to LUMA_LEVEL_MAX with LUMA_LEVEL_NEUTRAL
|
||||||
|
// changing nothing, and the distance from neutral is applied to every luma sample in eighths, so 0
|
||||||
|
// halves the brightness and 8 raises it by half. Turning it off, or asking for the neutral level,
|
||||||
|
// puts the picture back.
|
||||||
|
void videoSetLuma(int32_t playerHandle, bool enabled, int32_t level) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetLuma");
|
||||||
|
|
||||||
|
if (v->rgb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
v->luma = enabled ? SDL_clamp(level, 0, LUMA_LEVEL_MAX) - LUMA_LEVEL_NEUTRAL : 0;
|
||||||
|
if (v->front.frame >= 0) {
|
||||||
|
_uploadFrame(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Shows a YUV player in luma only. The frame on the texture is re-uploaded so a paused video
|
||||||
|
// changes at once. Pixel reads keep returning colour; rgb players are unaffected.
|
||||||
|
void videoSetMonochrome(int32_t playerHandle, bool enabled) {
|
||||||
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetMonochrome");
|
||||||
|
|
||||||
|
if (v->rgb || (enabled == v->monochrome)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
v->monochrome = enabled;
|
||||||
|
if (v->front.frame >= 0) {
|
||||||
|
_uploadFrame(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent) {
|
void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent) {
|
||||||
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetVolume");
|
VideoPlayerT *v = _getPlayer(playerHandle, "videoSetVolume");
|
||||||
MIX_StereoGains gains;
|
MIX_StereoGains gains;
|
||||||
|
|
@ -1688,21 +2056,11 @@ void videoUnload(int32_t playerHandle) {
|
||||||
av_buffer_unref(&v->hwDevice);
|
av_buffer_unref(&v->hwDevice);
|
||||||
_formatClose(&v->videoFormat);
|
_formatClose(&v->videoFormat);
|
||||||
free(v->frames);
|
free(v->frames);
|
||||||
|
free(v->filename);
|
||||||
|
free(v->neutralChroma);
|
||||||
|
free(v->lumaPlane);
|
||||||
SDL_DestroyTexture(v->videoTexture);
|
SDL_DestroyTexture(v->videoTexture);
|
||||||
|
_audioClose(v);
|
||||||
if (v->audioSourceCount > 0) {
|
|
||||||
MIX_DestroyTrack(v->track);
|
|
||||||
SDL_DestroyAudioStream(v->audioStream);
|
|
||||||
_audioCloseTrack(v);
|
|
||||||
av_frame_free(&v->audioFrame);
|
|
||||||
av_packet_free(&v->audioPacket);
|
|
||||||
_formatClose(&v->audioFormat);
|
|
||||||
for (x = 0; x < v->audioSourceCount; x++) {
|
|
||||||
free(v->audio[x].language);
|
|
||||||
}
|
|
||||||
free(v->audio);
|
|
||||||
free(v->audioBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma GCC diagnostic push
|
#pragma GCC diagnostic push
|
||||||
#pragma GCC diagnostic ignored "-Wcast-align"
|
#pragma GCC diagnostic ignored "-Wcast-align"
|
||||||
|
|
@ -1753,9 +2111,10 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hand the decoder the frame we want, and show whatever it has finished.
|
// Hand the decoder the frame we want, and show whatever it has finished. A flash left on the
|
||||||
|
// texture comes off here even when nothing new was decoded, so it cannot stick on a still disc.
|
||||||
_requestFrame(v);
|
_requestFrame(v);
|
||||||
if (_takeDecodedFrame(v)) {
|
if (_takeDecodedFrame(v) || v->flashed) {
|
||||||
_uploadFrame(v);
|
_uploadFrame(v);
|
||||||
}
|
}
|
||||||
*texture = v->videoTexture;
|
*texture = v->videoTexture;
|
||||||
|
|
|
||||||
|
|
@ -33,23 +33,30 @@
|
||||||
|
|
||||||
#define VIDEO_VOLUME_MAX 100
|
#define VIDEO_VOLUME_MAX 100
|
||||||
#define VIDEO_AUDIO_DELAY_MAX 1000 // Milliseconds either way
|
#define VIDEO_AUDIO_DELAY_MAX 1000 // Milliseconds either way
|
||||||
|
#define LUMA_LEVEL_MAX 8 // videoSetLuma takes 0 to 8 ...
|
||||||
|
#define LUMA_LEVEL_NEUTRAL 4 // ... where this level changes nothing, 0 halves the luma and 8 raises it by half
|
||||||
|
|
||||||
|
|
||||||
|
void videoFlash(int32_t playerHandle);
|
||||||
int32_t videoGetAudioCalibration(void);
|
int32_t videoGetAudioCalibration(void);
|
||||||
int32_t videoGetAudioDelay(void);
|
int32_t videoGetAudioDelay(void);
|
||||||
int32_t videoGetAudioLatency(void);
|
int32_t videoGetAudioLatency(void);
|
||||||
int32_t videoGetAudioTrack(int32_t playerHandle);
|
int32_t videoGetAudioTrack(int32_t playerHandle);
|
||||||
int32_t videoGetAudioTracks(int32_t playerHandle);
|
int32_t videoGetAudioTracks(int32_t playerHandle);
|
||||||
|
double videoGetFps(int32_t playerHandle);
|
||||||
int64_t videoGetFrame(int32_t playerHandle);
|
int64_t videoGetFrame(int32_t playerHandle);
|
||||||
int64_t videoGetFrameCount(int32_t playerHandle);
|
int64_t videoGetFrameCount(int32_t playerHandle);
|
||||||
int32_t videoGetHeight(int32_t playerHandle);
|
int32_t videoGetHeight(int32_t playerHandle);
|
||||||
const char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack);
|
const char *videoGetLanguage(int32_t playerHandle, int32_t audioTrack);
|
||||||
const char *videoGetLanguageDescription(const char *languageCode);
|
const char *videoGetLanguageDescription(const char *languageCode);
|
||||||
MIX_Mixer *videoGetMixer(void);
|
MIX_Mixer *videoGetMixer(void);
|
||||||
|
const char *videoGetDecoderDescription(void);
|
||||||
|
bool videoGetMonochrome(int32_t playerHandle);
|
||||||
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);
|
||||||
bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch);
|
bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch);
|
||||||
void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent);
|
void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *rightPercent);
|
||||||
int32_t videoGetWidth(int32_t playerHandle);
|
int32_t videoGetWidth(int32_t playerHandle);
|
||||||
|
bool videoGetYUVPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *luma, uint8_t *cb, uint8_t *cr);
|
||||||
void videoInit(MIX_Mixer *mixer);
|
void videoInit(MIX_Mixer *mixer);
|
||||||
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, bool rgb);
|
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb);
|
||||||
|
|
@ -57,11 +64,15 @@ void videoLockAudio(void);
|
||||||
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);
|
||||||
|
bool videoReopenAudio(int32_t playerHandle, const char *audioFilename);
|
||||||
void videoSeek(int32_t playerHandle, int64_t seekFrame);
|
void videoSeek(int32_t playerHandle, int64_t seekFrame);
|
||||||
void videoSetAudioCalibration(int32_t milliseconds);
|
void videoSetAudioCalibration(int32_t milliseconds);
|
||||||
void videoSetAudioDelay(int32_t milliseconds);
|
void videoSetAudioDelay(int32_t milliseconds);
|
||||||
void videoSetAudioTrack(int32_t playerHandle, int32_t track);
|
void videoSetAudioTrack(int32_t playerHandle, int32_t track);
|
||||||
|
void videoSetBlend(int32_t playerHandle, bool enabled);
|
||||||
void videoSetHardwareDecoding(bool enabled);
|
void videoSetHardwareDecoding(bool enabled);
|
||||||
|
void videoSetLuma(int32_t playerHandle, bool enabled, int32_t level);
|
||||||
|
void videoSetMonochrome(int32_t playerHandle, bool enabled);
|
||||||
void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent);
|
void videoSetVolume(int32_t playerHandle, int32_t leftPercent, int32_t rightPercent);
|
||||||
void videoUnload(int32_t playerHandle);
|
void videoUnload(int32_t playerHandle);
|
||||||
void videoUnlockAudio(void);
|
void videoUnlockAudio(void);
|
||||||
|
|
|
||||||
|
|
@ -60,3 +60,11 @@ screenshot or two and quits by itself; the results are in screenshots/.
|
||||||
| scene41.singe | 41 | 46.2 | GUI layers: scene41.rml over the disc with two overlapping opaque squares under filter: opacity(0.5) (the overlap must stay as light as the rest) beside the same pair under the opacity property (it darkens), a panel cut out by mask-image: image(maskFade.png), a group inside a group, and a box-shadow without blur that RmlUi renders through a layer saved as a texture; the script drops the group to opacity(0.25) between the two screenshots |
|
| scene41.singe | 41 | 46.2 | GUI layers: scene41.rml over the disc with two overlapping opaque squares under filter: opacity(0.5) (the overlap must stay as light as the rest) beside the same pair under the opacity property (it darkens), a panel cut out by mask-image: image(maskFade.png), a group inside a group, and a box-shadow without blur that RmlUi renders through a layer saved as a texture; the script drops the group to opacity(0.25) between the two screenshots |
|
||||||
| scene42.singe | 42 | 46.3 | GUI filters: scene42.rml over the disc with a panel under filter: blur(6dp), text under drop-shadow, a row of squares through grayscale, sepia, invert, hue-rotate(90deg) and saturate(3) beside an unfiltered one, a panel with a soft box-shadow, and a panel under blur(40dp) (the downscaled path); guiGetStats() printed at each screenshot, and both blurs narrowed through RmlUi's Lua API between the two |
|
| scene42.singe | 42 | 46.3 | GUI filters: scene42.rml over the disc with a panel under filter: blur(6dp), text under drop-shadow, a row of squares through grayscale, sepia, invert, hue-rotate(90deg) and saturate(3) beside an unfiltered one, a panel with a soft box-shadow, and a panel under blur(40dp) (the downscaled path); guiGetStats() printed at each screenshot, and both blurs narrowed through RmlUi's Lua API between the two |
|
||||||
| scene43.singe | 43 | 46.4 | GUI gradients: scene43.rml over the disc with a three-stop linear-gradient panel, a radial-gradient disc, a conic-gradient wheel (its starting angle turned through RmlUi's Lua API between the two screenshots), a repeating-linear-gradient stripe, a twenty-stop gradient that trips the sixteen-stop warning once, and the shipped theme's gradient panel, buttons and progress bar |
|
| scene43.singe | 43 | 46.4 | GUI gradients: scene43.rml over the disc with a three-stop linear-gradient panel, a radial-gradient disc, a conic-gradient wheel (its starting angle turned through RmlUi's Lua API between the two screenshots), a repeating-linear-gradient stripe, a twenty-stop gradient that trips the sixteen-stop warning once, and the shipped theme's gradient panel, buttons and progress bar |
|
||||||
|
| scene44.singe | 44 | 47 | Hypseus extensions over the disc: strip.png (five 32x32 frames) through spriteLoadFrames and the three spriteDrawFrame forms side by side (an out of range frame draws frame 1, a still ignores the frame), the sheet as an ordinary animation under spriteSetFrame and spriteDraw, vldpFocusArea on the middle of the frame (frame 60) then off (80), vldpSetMonochrome and overlaySetMonochrome on (90) then off (110), and a status line with vldpGetScale, spriteFrameWidth/Height, vldpGetYUVPixel beside vldpGetPixel and controllerIsValid(0); screenshots at 30, 70, 100 and 120 |
|
||||||
|
| scene45.singe | 45 | 48.1 | Hypseus parity shims: setOverlaySize preset 1 (the disc, 720x480), 2 (half, 360x240), 4 with a size (640x400), 3 (360x240), 4 with another size (320x240), 4 with none (ignored, still 320x240) and an unrecognised 9 (half again), then setOverlayResolution as the alias of overlaySetResolution (480x320), each redrawn and shot at its new size; setOverlayLinearScale true (170) then false (190) over checker.png, which the window scales four times up; musicLoad/musicPlay/musicStop/musicIsPlaying/musicSetVolume/musicUnload over tone.ogg (a 440 Hz sine); and a status line with scoreBezelGetState (the score panel is left off here; scene47 drives it), rewriteStatus, ratioGetX/Y and musicIsPlaying; screenshots at 20, 40, 60, 80, 100, 120, 140, 160, 180, 200 and 245 |
|
||||||
|
| scene46.singe | 46 | 48.2 | The video rectangle over the disc inside a Sinden border (`-g '10 5'`): a frame around the very edge of the picture, magenta corner ticks and a cyan crosshair a quarter of the way in, stepped through vldpSetScale 50, 25 and 100 and vldpSetRotate 90, 180, 270 and 0 with a screenshot at each; run three times, plain and with `--shiftx -40 --shifty -40` and `--shiftx 40 --shifty 40`, for the shifted stops, which show only once the scale factor has left room to move in |
|
||||||
|
| scene47.singe | 47 | 48.3 | Bezels over the disc: cabinet artwork (testScripts/bezels/cabinet.png, 1280x800, with a deliberately off centre 800x500 cutout at 200,120 declared in cabinet.cfg and a yellow ring painted just outside it) with the picture landing exactly in the cutout, a GUI (scene47.rml) drawn on the artwork's left pillar with guiDrawScreen, and the score panel stepped through scoreBezelEnable, credits, two scores, two lives, setOverlayOnTop, scoreBezelClear and a value that lights it again; run five times, plain, with `--bezelflip` (the artwork's corner brackets then cover the picture, and setOverlayOnTop puts the overlay back over them), with `-g '10 5'` (the Sinden border goes to the window's edge), with `-g '10 5' --sindenedge video` (and back around the picture), and with no `--bezel` at all |
|
||||||
|
| scene48.singe | 48 | 48.4 | User options and the settings file over the disc: a status page of everything a script can see of the new configuration -- `getFValue()`, `ratioGetX()`/`ratioGetY()`, `soundGetVolume()`, `vldpGetScale()`, `controllerIsValid(0)`, `mouseHowMany()` and `singeWantsCrosshairs()` -- with a six colour ramp in the overlay beside the disc, which stays in colour while `--monochrome` greys the disc, and one `controllerDoRumble` so the trace records the step `--haptic` left of it; run plain, with a `settings.cfg` setting several options and the command line overriding one of them, and with `--monochrome`, `--startsilent`, `--altaudio`, `--idleexit`, `--nogamepad` and `--screen 1` |
|
||||||
|
| scene49.singe | 49 | 48.5 | Input over the disc: every device the engine can see on one page -- the four gamepad slots with the name, the six axis values and the fifteen button states of whatever is in each, the mice ManyMouse found with their names, the last mouse position and how many times `SWITCH_MOUSE_DISCONNECT` has fired, the keyboard mode, and the switches the mappings are currently letting through -- with `DEAD_ZONE`, `TRIGGER_THRESHOLD` (raw and as a per cent of full travel) and `joyMouseIsEnabled()` across the top. The phases are timed on the wall clock (shots at 2, 6, 10 and 16 seconds, quitting at 18) so a device can be attached, pressed and unplugged while it runs; the LD_PRELOAD harness in the scratchpad drives an SDL virtual joystick through exactly that. Run plain, with `--joymouse --js_range 20`, with `--gamepad_reorder 10`, with `--trigger_threshold 99.5`, with `--mapjoysticks=false`, with `--manymouse --absolutes_only`, with a `gamecontrollerdb.txt` beside it, and with a `controls.cfg` setting `DEAD_ZONES` |
|
||||||
|
| scene50.singe | 50 | 48.6 | Subtitles and the disc picture over the disc: `testScripts/subtitles.srt` (four cues, one of them two lines and one carrying a `<bracket>` and an `&` that must reach the screen as text) parsed into disc frames at the disc's own frame rate and shown through the shipped `Singe/subtitle.rml`, with `testScripts/broken.srt` refused (`srtLoad` answers false and loads nothing); the disc searched to a known frame before every shot so a cue is read off the disc's frame number and not the wall clock; `srtPosition` at 15 and 80 per cent; `overlayBanner` taking the cue's place for thirty drawn frames and then giving it back; `srtEnable` off and on; `srtClear`; `vldpSetLuma` at 0, 8, 4 and off; `vldpSetBlend` on, off and together with the luma; and `vldpFlash`, asked for and shot in the same tick because it lasts one drawn frame. 23 screenshots |
|
||||||
|
| scene51.singe | 51 | 48.6 | Sprites, in-memory assets and music over the disc: `spriteRotateFrame` turning frames 1 to 4 of `testScripts/arrows.png` (a four frame strip whose arrows have a white notch, so a rotation is readable) at 0, 45, 135 and 225 degrees, drawn by `spriteDrawRotatedFrame` plain, at 1.5 and at 2 by 0.75, beside the same frame through `spriteDrawFrame` which ignores both; `spriteScale` before a rotation, so the pair honours the sprite's own scale; an out of range frame falling back to frame 1; `spriteDrawGrid` taking three rectangles out of the same file loaded as one wide still and a fourth out of a sprite `spriteLoadData` built from the file's bytes; `spriteGetFrames` on all three; `controllerHowMany`; `setOverlayOpacity` at 96 and back to 255; and the music family over `testScripts/tune.mp3` (six seconds of 330 Hz, so the built-in mp3 decoder is exercised) -- play looping, `musicSetVolume` at 128 and 16, `soundSetVolume` at 8 with a `soundLoadData` sound played through it to show the two volumes do not move each other, pause, resume, a 250 ms fade out, a play once and an unload. 16 screenshots |
|
||||||
|
|
|
||||||
BIN
testScripts/arrows.png
(Stored with Git LFS)
Normal file
BIN
testScripts/arrows.png
(Stored with Git LFS)
Normal file
Binary file not shown.
2
testScripts/bezels/cabinet.cfg
Normal file
2
testScripts/bezels/cabinet.cfg
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- The hole in cabinet.png the picture is drawn in, in the artwork's own pixels.
|
||||||
|
CUTOUT = { x = 200, y = 120, width = 800, height = 500 }
|
||||||
BIN
testScripts/bezels/cabinet.png
(Stored with Git LFS)
Normal file
BIN
testScripts/bezels/cabinet.png
(Stored with Git LFS)
Normal file
Binary file not shown.
3
testScripts/broken.srt
Normal file
3
testScripts/broken.srt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
This file is not SubRip at all.
|
||||||
|
It has no timestamps and no cues,
|
||||||
|
so srtLoad must answer false.
|
||||||
BIN
testScripts/checker.png
(Stored with Git LFS)
Normal file
BIN
testScripts/checker.png
(Stored with Git LFS)
Normal file
Binary file not shown.
|
|
@ -453,4 +453,92 @@ GAMES = {
|
||||||
DEVELOPER = "Test",
|
DEVELOPER = "Test",
|
||||||
PUBLISHER = "Test",
|
PUBLISHER = "Test",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Hypseus Extensions",
|
||||||
|
SCRIPT = "testScripts/scene44.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "Hypseus extensions: a sprite sheet drawn by frame in the three spriteDrawFrame forms, the disc focus area on and off, both monochrome modes on and off, vldpGetScale, vldpGetYUVPixel beside vldpGetPixel, and the controller checks.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Hypseus Parity",
|
||||||
|
SCRIPT = "testScripts/scene45.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "Hypseus parity shims: every setOverlaySize preset and setOverlayResolution redrawn at the new size, setOverlayLinearScale on and off over a checkerboard, the music wrappers over a generated tone, and a status line with scoreBezelGetState (the panel stays off here) and the rewriteStatus and ratio stubs.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Video Rectangle",
|
||||||
|
SCRIPT = "testScripts/scene46.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "The video rectangle: a crosshair and an edge frame over the disc inside a Sinden border, stepped through vldpSetScale at 50 and 25 and vldpSetRotate at 90, 180 and 270; run again with --shiftx and --shifty, which move the picture in the room the scale left and so show nothing until it shrinks.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Bezels",
|
||||||
|
SCRIPT = "testScripts/scene47.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "Bezels: cabinet artwork from testScripts/bezels with an off centre cutout the picture lands in, a GUI on the artwork outside the picture with guiDrawScreen, and the score panel stepped through credits, two scores, two lives, setOverlayOnTop, scoreBezelClear and back. Run with --bezeldir testScripts/bezels --bezel cabinet.png, again with --bezelflip, twice more with -g '10 5' and with --sindenedge video, and once with no bezel at all.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "User Options",
|
||||||
|
SCRIPT = "testScripts/scene48.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "User options and the settings file: getFValue, ratioGetX and ratioGetY beside soundGetVolume, vldpGetScale, controllerIsValid and mouseHowMany over the disc, with a colour ramp in the overlay that stays in colour while --monochrome greys the disc. Run plain, with a settings.cfg the command line then overrides, and with --monochrome, --startsilent, --altaudio, --idleexit, --nogamepad and --screen 1.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Input",
|
||||||
|
SCRIPT = "testScripts/scene49.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "Input: every device the engine can see, with its kind, slot and identity, the live state of every axis and button of each, the dead zone and trigger threshold in force, the switches those are letting through, and whether a stick is driving the mouse. Timed on the wall clock so a device can be attached, pressed and taken away while it runs. Run plain, with --joymouse, --gamepad_reorder, --trigger_threshold, --mapjoysticks=false, --manymouse and --absolutes_only, and with a controls.cfg setting DEAD_ZONES.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Subtitles and the Disc Picture",
|
||||||
|
SCRIPT = "testScripts/scene50.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "Subtitles, the banner and the disc picture controls: a .srt parsed into disc frames and shown over the picture through an RmlUi document, at two heights, with a malformed file refused; overlayBanner over a cue; srtClear; and vldpSetLuma, vldpSetBlend and vldpFlash on the picture itself. The disc is searched to a known frame before each shot, so every cue is read off the disc's own frame number.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
TITLE = "Sprites, Data and Music",
|
||||||
|
SCRIPT = "testScripts/scene51.singe",
|
||||||
|
VIDEO = "Singe/menuBackground.mkv",
|
||||||
|
DESCRIPTION = "Sprite sheet rotation (spriteRotateFrame and spriteDrawRotatedFrame, which unlike spriteDrawFrame honour a rotation and a scale), the source rectangle blit spriteDrawGrid, spriteGetFrames, controllerHowMany, setOverlayOpacity, the in-memory loaders spriteLoadData and soundLoadData, and the whole music family over an mp3, with music and sound effects proved to carry independent volumes.",
|
||||||
|
YEAR = 2026,
|
||||||
|
GENRE = "Test",
|
||||||
|
PLATFORM = "Singe",
|
||||||
|
DEVELOPER = "Test",
|
||||||
|
PUBLISHER = "Test",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
testScripts/scene44.singe
Normal file
61
testScripts/scene44.singe
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
-- Hypseus extensions over the disc: a sprite sheet drawn by frame in the three spriteDrawFrame
|
||||||
|
-- forms, the disc focus area, both monochrome modes, the scale, YUV and RGB pixel reads and the
|
||||||
|
-- controller checks. discAudioSuffix needs a sidecar file and is exercised by hand. The
|
||||||
|
-- framework supplies keyboardCatchQuit, the Hypseus name for the quit key setting.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 18)
|
||||||
|
local strip = spriteLoadFrames(5, "testScripts/strip.png")
|
||||||
|
local crate = spriteLoad("testScripts/crate.png")
|
||||||
|
local frames = 0
|
||||||
|
local width, height = overlayGetWidth(), overlayGetHeight()
|
||||||
|
local discW, discH = discGetWidth(), discGetHeight()
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
spriteResetColorKey(strip, true) -- Accepted; nothing to re-key in Singe.
|
||||||
|
singeSetQuitKeyEnabled(false) -- The quit switch would reach the script now ...
|
||||||
|
singeSetQuitKeyEnabled(true) -- ... and quits the engine again.
|
||||||
|
keyboardCatchQuit(false) -- The Hypseus alias, same setting.
|
||||||
|
controllerDoRumble(1, 1) -- No controller headless: a silent no-op in both forms.
|
||||||
|
if controllerIsValid(0) then
|
||||||
|
controllerDoRumble(0, 2, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
local frame = (math.floor(frames / 10) % 5) + 1
|
||||||
|
overlayClear()
|
||||||
|
-- The three forms side by side, an out of range frame (draws frame 1) and a still through the same call.
|
||||||
|
spriteDrawFrame(strip, 20, 40, frame)
|
||||||
|
spriteDrawFrame(strip, 70, 40, frame, 2)
|
||||||
|
spriteDrawFrame(strip, 150, 40, frame, 3, 1.5)
|
||||||
|
spriteDrawFrame(strip, 260, 40, 9)
|
||||||
|
spriteDrawFrame(crate, 300, 40, 4, 0.5)
|
||||||
|
-- spriteDraw shows the sheet as an ordinary animation parked on a frame, 0-based through spriteSetFrame.
|
||||||
|
spriteSetFrame(strip, 2)
|
||||||
|
spriteDraw(strip, 20, 100)
|
||||||
|
-- The effects, on and off again, with a screenshot of each state.
|
||||||
|
if frames == 60 then
|
||||||
|
vldpFocusArea(discW * 0.25, discH * 0.25, discW * 0.5, discH * 0.5)
|
||||||
|
elseif frames == 80 then
|
||||||
|
vldpFocusArea()
|
||||||
|
elseif frames == 90 then
|
||||||
|
vldpSetMonochrome(true)
|
||||||
|
overlaySetMonochrome(true)
|
||||||
|
elseif frames == 110 then
|
||||||
|
vldpSetMonochrome(false)
|
||||||
|
overlaySetMonochrome(false)
|
||||||
|
end
|
||||||
|
local y1, u1, v1 = vldpGetYUVPixel(width / 2, height / 2)
|
||||||
|
local r, g, b = vldpGetPixel(width / 2, height / 2)
|
||||||
|
fontPrint(10, 130, string.format("frame %d disc frame %d scale %d", frames, discGetFrame(), vldpGetScale()))
|
||||||
|
fontPrint(10, 155, string.format("yuv %d %d %d rgb %d %d %d pad0 %s", y1, u1, v1, r, g, b, tostring(controllerIsValid(0))))
|
||||||
|
fontPrint(10, 180, string.format("frame size %dx%d crate %dx%d", spriteFrameWidth(strip), spriteFrameHeight(strip), spriteFrameWidth(crate), spriteFrameHeight(crate)))
|
||||||
|
if frames == 30 or frames == 70 or frames == 100 or frames == 120 then
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if frames == 130 then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
85
testScripts/scene45.singe
Normal file
85
testScripts/scene45.singe
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
-- Hypseus parity, stage 1: the framework's setOverlaySize presets and setOverlayResolution
|
||||||
|
-- (every call is followed by a redraw and a screenshot, since the resize empties the overlay),
|
||||||
|
-- the engine's setOverlayLinearScale over a checkerboard the window scales up, the music
|
||||||
|
-- wrappers over a generated tone, and a status line carrying scoreBezelGetState (false until a
|
||||||
|
-- game asks for the score panel, which scene47 does), rewriteStatus, ratioGetX, ratioGetY and
|
||||||
|
-- musicIsPlaying.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 12)
|
||||||
|
local checker = spriteLoad("testScripts/checker.png")
|
||||||
|
local tone = musicLoad("testScripts/tone.ogg")
|
||||||
|
local frames = 0
|
||||||
|
local note = "start"
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
scoreBezelEnable(false, 0) -- Accepted with a type Singe ignores; the panel stays off, as Hypseus ships it.
|
||||||
|
scoreBezelCredits(3)
|
||||||
|
scoreBezelScore(1, 12345)
|
||||||
|
scoreBezelLives(1, 3)
|
||||||
|
scoreBezelTwinScoreOn(true)
|
||||||
|
musicSetVolume(soundGetVolume() * 2) -- The round trip every Hypseus game makes.
|
||||||
|
musicPlay(tone, -1)
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
overlayClear()
|
||||||
|
overlayBox(1, 1, overlayGetWidth() - 2, overlayGetHeight() - 2)
|
||||||
|
spriteDraw(checker, 10, 10)
|
||||||
|
spriteDraw(checker, overlayGetWidth() - 74, 10)
|
||||||
|
fontPrint(10, 90, string.format("frame %d overlay %dx%d disc %dx%d", frames, overlayGetWidth(), overlayGetHeight(), discGetWidth(), discGetHeight()))
|
||||||
|
fontPrint(10, 108, string.format("bezel %s rewrite %s ratio %d %d music %s", tostring(scoreBezelGetState()), tostring(rewriteStatus()), ratioGetX(), ratioGetY(), tostring(musicIsPlaying())))
|
||||||
|
fontPrint(10, 126, note)
|
||||||
|
-- Each overlay size in turn, every one a visible change from the one before it.
|
||||||
|
if frames == 10 then
|
||||||
|
setOverlaySize(1) -- The disc's own size.
|
||||||
|
note = "setOverlaySize(1)"
|
||||||
|
elseif frames == 30 then
|
||||||
|
setOverlaySize(2) -- Half the disc, both axes.
|
||||||
|
note = "setOverlaySize(2)"
|
||||||
|
elseif frames == 50 then
|
||||||
|
setOverlaySize(4, 640, 400) -- The size given.
|
||||||
|
note = "setOverlaySize(4, 640, 400)"
|
||||||
|
elseif frames == 70 then
|
||||||
|
setOverlaySize(3) -- 360x240, whatever the disc is.
|
||||||
|
note = "setOverlaySize(3)"
|
||||||
|
elseif frames == 90 then
|
||||||
|
setOverlaySize(4, 320, 240)
|
||||||
|
note = "setOverlaySize(4, 320, 240)"
|
||||||
|
elseif frames == 110 then
|
||||||
|
setOverlaySize(4) -- Ignored without a size, as in Hypseus: still 320x240.
|
||||||
|
note = "setOverlaySize(4) ignored"
|
||||||
|
elseif frames == 130 then
|
||||||
|
setOverlaySize(9) -- Unrecognised: half the disc, as preset 2 does.
|
||||||
|
note = "setOverlaySize(9) is half"
|
||||||
|
elseif frames == 150 then
|
||||||
|
setOverlayResolution(480, 320) -- The alias of overlaySetResolution.
|
||||||
|
note = "setOverlayResolution(480, 320)"
|
||||||
|
elseif frames == 170 then
|
||||||
|
setOverlayLinearScale(true) -- The checkerboard softens as the window scales it.
|
||||||
|
note = "setOverlayLinearScale(true)"
|
||||||
|
elseif frames == 190 then
|
||||||
|
setOverlayLinearScale(false) -- ... and blocks up again.
|
||||||
|
note = "setOverlayLinearScale(false)"
|
||||||
|
elseif frames == 210 then
|
||||||
|
musicStop(tone)
|
||||||
|
note = string.format("musicStop(tone): playing %s", tostring(musicIsPlaying(tone)))
|
||||||
|
elseif frames == 220 then
|
||||||
|
musicPlay(tone) -- Once through, no loop count.
|
||||||
|
musicSetVolume(128) -- The top of Hypseus's own 0 to 128 scale.
|
||||||
|
note = string.format("musicPlay(tone): playing %s", tostring(musicIsPlaying(tone)))
|
||||||
|
elseif frames == 230 then
|
||||||
|
musicStop() -- Every piece of music, with no fade.
|
||||||
|
note = string.format("musicStop(): playing %s", tostring(musicIsPlaying()))
|
||||||
|
elseif frames == 240 then
|
||||||
|
musicUnload(tone)
|
||||||
|
note = "musicUnload(tone)"
|
||||||
|
end
|
||||||
|
if frames == 20 or frames == 40 or frames == 60 or frames == 80 or frames == 100 or frames == 120 or frames == 140 or frames == 160 or frames == 180 or frames == 200 or frames == 245 then
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if frames == 250 then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
76
testScripts/scene46.singe
Normal file
76
testScripts/scene46.singe
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
-- The video rectangle: the picture shifted, scaled and turned inside a Sinden border, with a
|
||||||
|
-- crosshair at a fixed overlay coordinate that must stay on the same point of the picture. The
|
||||||
|
-- shift has no runtime setter, so it comes from --shiftx and --shifty and the script is run once
|
||||||
|
-- for each of the three shifts; the scale and the rotation step here.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 14)
|
||||||
|
local width, height = overlayGetWidth(), overlayGetHeight()
|
||||||
|
local crossX = math.floor(width / 4)
|
||||||
|
local crossY = math.floor(height / 4)
|
||||||
|
local frames = 0
|
||||||
|
local shot = 0
|
||||||
|
local steps = {
|
||||||
|
{ "as given", nil },
|
||||||
|
{ "scale 50", function() return vldpSetScale(50) end },
|
||||||
|
{ "scale 25", function() return vldpSetScale(25) end },
|
||||||
|
{ "scale 100", function() return vldpSetScale(100) end },
|
||||||
|
{ "rotate 90", function() return vldpSetRotate(90) end },
|
||||||
|
{ "rotate 180", function() return vldpSetRotate(180) end },
|
||||||
|
{ "rotate 270", function() return vldpSetRotate(270) end },
|
||||||
|
{ "rotate 0", function() return vldpSetRotate(0) end },
|
||||||
|
}
|
||||||
|
local label = steps[1][1]
|
||||||
|
local applied = true
|
||||||
|
local step = 1
|
||||||
|
|
||||||
|
local STEP_FRAMES = 20
|
||||||
|
local SHOT_DELAY = 10
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
overlayClear()
|
||||||
|
|
||||||
|
-- A frame around the very edge of the picture, so the rectangle's position and size are visible.
|
||||||
|
colorForeground(255, 255, 0)
|
||||||
|
overlayBox(0, 0, width - 1, height - 1)
|
||||||
|
overlayBox(3, 3, width - 4, height - 4)
|
||||||
|
|
||||||
|
-- Corner ticks tell a turned picture from a shifted one: the long arm is the top left corner.
|
||||||
|
colorForeground(255, 0, 255)
|
||||||
|
overlayLine(0, 0, 60, 0)
|
||||||
|
overlayLine(0, 0, 0, 40)
|
||||||
|
|
||||||
|
-- The crosshair sits a quarter of the way in and must stay on the same point of the picture.
|
||||||
|
colorForeground(0, 255, 255)
|
||||||
|
overlayCircle(crossX, crossY, 14)
|
||||||
|
overlayLine(crossX - 24, crossY, crossX + 24, crossY)
|
||||||
|
overlayLine(crossX, crossY - 24, crossX, crossY + 24)
|
||||||
|
|
||||||
|
colorForeground(255, 255, 255)
|
||||||
|
fontPrint(10, height - 46, string.format("%s scale %d rotate %d applied %s", label, vldpGetScale(), vldpGetRotate(), tostring(applied)))
|
||||||
|
fontPrint(10, height - 26, string.format("overlay %dx%d crosshair %d,%d disc frame %d", width, height, crossX, crossY, discGetFrame()))
|
||||||
|
|
||||||
|
-- One step every STEP_FRAMES frames, shot SHOT_DELAY frames later so the change is on screen.
|
||||||
|
if (frames % STEP_FRAMES) == 0 then
|
||||||
|
step = (frames / STEP_FRAMES) + 1
|
||||||
|
if steps[step] ~= nil then
|
||||||
|
label = steps[step][1]
|
||||||
|
if steps[step][2] ~= nil then
|
||||||
|
applied = steps[step][2]()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if ((frames % STEP_FRAMES) == SHOT_DELAY) and (shot < #steps) then
|
||||||
|
shot = shot + 1
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if shot >= #steps and (frames % STEP_FRAMES) == SHOT_DELAY + 2 then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
24
testScripts/scene47.rml
Normal file
24
testScripts/scene47.rml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<rml>
|
||||||
|
<head>
|
||||||
|
<title>scene47</title>
|
||||||
|
<link type="text/rcss" href="Singe/gui.rcss"/>
|
||||||
|
<style>
|
||||||
|
body { width: 100%; height: 100%; font-size: 15dp; }
|
||||||
|
.panel { width: 100%; height: 100%; box-sizing: border-box; padding: 10dp; text-align: center; }
|
||||||
|
h2 { font-size: 17dp; margin: 0 0 8dp 0; }
|
||||||
|
.line { display: block; margin: 0 0 8dp 0; word-break: break-word; }
|
||||||
|
.hot { color: #7ce07c; }
|
||||||
|
.small { font-size: 11dp; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body id="scene47">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>ON THE<br/>ARTWORK</h2>
|
||||||
|
<div class="line">guiDrawScreen<br/>draws me on the<br/>artwork.</div>
|
||||||
|
<div class="line">step</div>
|
||||||
|
<div class="line hot" id="step">1</div>
|
||||||
|
<div class="line">bezel</div>
|
||||||
|
<div class="line hot small" id="bezel">no</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</rml>
|
||||||
110
testScripts/scene47.singe
Normal file
110
testScripts/scene47.singe
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
-- Bezels: the cabinet artwork in testScripts/bezels (cabinet.png and its cabinet.cfg cutout), the
|
||||||
|
-- picture landing in the cutout, a GUI drawn on the artwork outside the picture with guiDrawScreen,
|
||||||
|
-- and the score panel behind the seven scoreBezel calls. A green frame runs along the very edge of
|
||||||
|
-- the picture, so the yellow ring painted just outside the cutout in the artwork must sit right
|
||||||
|
-- against it with no gap; magenta corner marks show what artwork drawn in front of the picture
|
||||||
|
-- (--bezelflip) covers, and setOverlayOnTop puts them back on top of it.
|
||||||
|
-- Run five times: plain, --bezelflip, with a Sinden border, with a Sinden border and
|
||||||
|
-- --sindenedge=video, and with no --bezel at all.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 12)
|
||||||
|
local width, height = overlayGetWidth(), overlayGetHeight()
|
||||||
|
local loaded, bezel = mainBezelLoaded()
|
||||||
|
local gui = guiNew(128, 384)
|
||||||
|
local document = guiLoad(gui, "testScripts/scene47.rml")
|
||||||
|
local frames = 0
|
||||||
|
local shot = 0
|
||||||
|
local note = "scoreBezelGetState " .. tostring(scoreBezelGetState())
|
||||||
|
|
||||||
|
local STEP_FRAMES = 25
|
||||||
|
local SHOT_DELAY = 14
|
||||||
|
|
||||||
|
-- Every step leaves the panel showing something different, and the last two prove that a cleared
|
||||||
|
-- panel comes back as soon as a value arrives.
|
||||||
|
local steps = {
|
||||||
|
{ "panel off", nil },
|
||||||
|
{ "panel on", function()
|
||||||
|
scoreBezelEnable(true, 0) -- The type is Hypseus's backend choice; Singe has one and ignores it.
|
||||||
|
scoreBezelCredits(5)
|
||||||
|
scoreBezelScore(1, 1234)
|
||||||
|
scoreBezelLives(1, 3)
|
||||||
|
scoreBezelScore(2, 999999) -- Ignored: the second score is not switched on yet.
|
||||||
|
note = "scoreBezelGetState " .. tostring(scoreBezelGetState())
|
||||||
|
end },
|
||||||
|
{ "twin on", function()
|
||||||
|
scoreBezelTwinScoreOn(true)
|
||||||
|
scoreBezelScore(2, 777)
|
||||||
|
scoreBezelLives(2, 2)
|
||||||
|
note = "second player on"
|
||||||
|
end },
|
||||||
|
{ "values", function()
|
||||||
|
scoreBezelCredits(12)
|
||||||
|
scoreBezelScore(1, 987654)
|
||||||
|
scoreBezelLives(1, 9)
|
||||||
|
scoreBezelScore(2, 42)
|
||||||
|
scoreBezelLives(2, 1)
|
||||||
|
note = "credits 12 1UP 987654 2UP 42"
|
||||||
|
end },
|
||||||
|
{ "overlay on top", function()
|
||||||
|
local ok, id = setOverlayOnTop(true)
|
||||||
|
note = string.format("setOverlayOnTop(true) -> %s %s", tostring(ok), id)
|
||||||
|
end },
|
||||||
|
{ "cleared", function()
|
||||||
|
setOverlayOnTop(false)
|
||||||
|
scoreBezelClear()
|
||||||
|
note = "scoreBezelClear()"
|
||||||
|
end },
|
||||||
|
{ "restored", function()
|
||||||
|
scoreBezelCredits(1)
|
||||||
|
note = "one credit brings the panel back"
|
||||||
|
end },
|
||||||
|
}
|
||||||
|
local label = steps[1][1]
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
guiSetValue(gui, document, "bezel", loaded and bezel or "no")
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
overlayClear()
|
||||||
|
|
||||||
|
-- The very edge of the picture: the artwork's yellow ring must be right against this.
|
||||||
|
colorForeground(0, 255, 0)
|
||||||
|
overlayBox(0, 0, width - 1, height - 1)
|
||||||
|
|
||||||
|
-- Corner marks inside the picture, where the artwork's brackets reach when it draws in front.
|
||||||
|
colorForeground(255, 0, 255)
|
||||||
|
overlayBox(4, 4, 30, 30)
|
||||||
|
overlayBox(width - 31, 4, width - 5, 30)
|
||||||
|
overlayBox(4, height - 31, 30, height - 5)
|
||||||
|
overlayBox(width - 31, height - 31, width - 5, height - 5)
|
||||||
|
|
||||||
|
colorForeground(255, 255, 255)
|
||||||
|
fontPrint(40, 20, string.format("scene47 %s", label))
|
||||||
|
fontPrint(40, 38, string.format("bezel %s id %s", tostring(loaded), bezel))
|
||||||
|
fontPrint(40, 56, note)
|
||||||
|
|
||||||
|
guiDrawScreen(gui, 8, 44, 38, 114)
|
||||||
|
|
||||||
|
if (frames % STEP_FRAMES) == 0 then
|
||||||
|
local step = (frames / STEP_FRAMES) + 1
|
||||||
|
if steps[step] ~= nil then
|
||||||
|
label = steps[step][1]
|
||||||
|
if steps[step][2] ~= nil then
|
||||||
|
steps[step][2]()
|
||||||
|
end
|
||||||
|
guiSetValue(gui, document, "step", string.format("%d", step))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if ((frames % STEP_FRAMES) == SHOT_DELAY) and (shot < #steps) then
|
||||||
|
shot = shot + 1
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if (shot >= #steps) and ((frames % STEP_FRAMES) == (SHOT_DELAY + 2)) then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
71
testScripts/scene48.singe
Normal file
71
testScripts/scene48.singe
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-- User options and the settings file: everything a script can see of the new configuration.
|
||||||
|
-- getFValue, ratioGetX and ratioGetY come straight from --fvalue, --xratio and --yratio (or from
|
||||||
|
-- settings.cfg), soundGetVolume follows --volume_nonvldp, controllerIsValid(0) and mouseHowMany
|
||||||
|
-- follow --nogamepad and --nomouse, and controllerDoRumble is called once so the trace records the
|
||||||
|
-- step --haptic left of it. The disc fills most of the picture, so --monochrome shows in a
|
||||||
|
-- screenshot; --startsilent, --altaudio, --idleexit and --screen are checked from the trace.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 12)
|
||||||
|
local width, height = overlayGetWidth(), overlayGetHeight()
|
||||||
|
local frames = 0
|
||||||
|
local shot = 0
|
||||||
|
|
||||||
|
local STEP_FRAMES = 30
|
||||||
|
local SHOT_DELAY = 18
|
||||||
|
local SHOTS = 3
|
||||||
|
local SWATCHES = 6
|
||||||
|
local SWATCH_HEIGHT = 15
|
||||||
|
|
||||||
|
-- A colour ramp in the overlay beside the disc: the overlay keeps its colour under --monochrome,
|
||||||
|
-- which greys the disc alone, so the two together show exactly what the option did.
|
||||||
|
local swatch = {
|
||||||
|
{ 255, 0, 0 },
|
||||||
|
{ 0, 255, 0 },
|
||||||
|
{ 0, 0, 255 },
|
||||||
|
{ 255, 255, 0 },
|
||||||
|
{ 0, 255, 255 },
|
||||||
|
{ 255, 0, 255 },
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
|
||||||
|
-- Once, so --haptic shows in the trace whether a pad is plugged in or not.
|
||||||
|
controllerDoRumble(0, 4, 1)
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
local x = 0
|
||||||
|
local y = 0
|
||||||
|
|
||||||
|
frames = frames + 1
|
||||||
|
overlayClear()
|
||||||
|
|
||||||
|
colorForeground(0, 255, 0)
|
||||||
|
overlayBox(0, 0, width - 1, height - 1)
|
||||||
|
|
||||||
|
for x = 1, SWATCHES do
|
||||||
|
colorForeground(swatch[x][1], swatch[x][2], swatch[x][3])
|
||||||
|
for y = 0, SWATCH_HEIGHT do
|
||||||
|
overlayLine(width - 30, 8 + (x - 1) * 18 + y, width - 8, 8 + (x - 1) * 18 + y)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
colorForeground(255, 255, 255)
|
||||||
|
fontPrint(20, 20, "scene48 user options")
|
||||||
|
fontPrint(20, 38, string.format("getFValue %s", tostring(getFValue())))
|
||||||
|
fontPrint(20, 56, string.format("ratioGetX %s ratioGetY %s", tostring(ratioGetX()), tostring(ratioGetY())))
|
||||||
|
fontPrint(20, 74, string.format("soundGetVolume %d vldpGetScale %d", soundGetVolume(), vldpGetScale()))
|
||||||
|
fontPrint(20, 92, string.format("controllerIsValid(0) %s mouseHowMany %d", tostring(controllerIsValid(0)), mouseHowMany()))
|
||||||
|
fontPrint(20, 110, string.format("singeWantsCrosshairs %s", tostring(singeWantsCrosshairs())))
|
||||||
|
|
||||||
|
if ((frames % STEP_FRAMES) == SHOT_DELAY) and (shot < SHOTS) then
|
||||||
|
shot = shot + 1
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if (shot >= SHOTS) and ((frames % STEP_FRAMES) == (SHOT_DELAY + 2)) then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
164
testScripts/scene49.singe
Normal file
164
testScripts/scene49.singe
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
-- Input: every device the engine can see, its kind, slot and identity, the live state of every
|
||||||
|
-- axis and button of each, the dead zone and trigger threshold in force, and whether the stick is
|
||||||
|
-- driving the mouse. Every device Singe opens is a gamepad, an unrecognised one through a mapping
|
||||||
|
-- Singe writes for it, so there is one family to report and not two.
|
||||||
|
--
|
||||||
|
-- The phases are timed on the wall clock so a test harness can attach a device, press something on
|
||||||
|
-- it and take it away again while the scene is running.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 11)
|
||||||
|
local width, height = 0, 0
|
||||||
|
local shot = 0
|
||||||
|
local started = os.clock()
|
||||||
|
local mouseX = -1
|
||||||
|
local mouseY = -1
|
||||||
|
local mouseMoves = 0
|
||||||
|
local disconnects = 0
|
||||||
|
|
||||||
|
local LINE = 14
|
||||||
|
local AXIS_FULL = 32767 -- What SDL reports at full deflection, and what DEAD_ZONE counts in
|
||||||
|
local PANEL_HEIGHT = 350
|
||||||
|
local SHOT_TIMES = { 2, 6, 10, 16 }
|
||||||
|
local QUIT_TIME = 18
|
||||||
|
local PADS = { GAMEPAD_0, GAMEPAD_1, GAMEPAD_2, GAMEPAD_3 }
|
||||||
|
local AXES = 6
|
||||||
|
local BUTTONS = {
|
||||||
|
"BUTTON_A", "BUTTON_B", "BUTTON_X", "BUTTON_Y", "BUTTON_BACK", "BUTTON_GUIDE", "BUTTON_START",
|
||||||
|
"BUTTON_LEFT_STICK", "BUTTON_RIGHT_STICK", "BUTTON_LEFT_BUMPER", "BUTTON_RIGHT_BUMPER",
|
||||||
|
"DPAD_UP", "DPAD_DOWN", "DPAD_LEFT", "DPAD_RIGHT"
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Every mapped switch by name, so what a dead zone let through can be read off the screen.
|
||||||
|
local SWITCHES = {
|
||||||
|
[SWITCH_UP] = "UP", [SWITCH_LEFT] = "LEFT", [SWITCH_DOWN] = "DOWN", [SWITCH_RIGHT] = "RIGHT",
|
||||||
|
[SWITCH_START1] = "START1", [SWITCH_START2] = "START2", [SWITCH_BUTTON1] = "BUTTON1",
|
||||||
|
[SWITCH_BUTTON2] = "BUTTON2", [SWITCH_BUTTON3] = "BUTTON3", [SWITCH_BUTTON4] = "BUTTON4",
|
||||||
|
[SWITCH_COIN1] = "COIN1", [SWITCH_COIN2] = "COIN2", [SWITCH_SKILL1] = "SKILL1",
|
||||||
|
[SWITCH_SKILL2] = "SKILL2", [SWITCH_SKILL3] = "SKILL3", [SWITCH_SERVICE] = "SERVICE",
|
||||||
|
[SWITCH_TEST] = "TEST", [SWITCH_RESET] = "RESET", [SWITCH_SCREENSHOT] = "SCREENSHOT",
|
||||||
|
[SWITCH_QUIT] = "QUIT", [SWITCH_PAUSE] = "PAUSE", [SWITCH_CONSOLE] = "CONSOLE",
|
||||||
|
[SWITCH_TILT] = "TILT", [SWITCH_GRAB] = "GRAB", [SWITCH_MOUSE_DISCONNECT] = "MOUSE_DISCONNECT"
|
||||||
|
}
|
||||||
|
local held = {}
|
||||||
|
|
||||||
|
overlaySetResolution(720, 480)
|
||||||
|
width, height = overlayGetWidth(), overlayGetHeight()
|
||||||
|
fontSelect(font)
|
||||||
|
discPlay()
|
||||||
|
|
||||||
|
|
||||||
|
-- Every button of one pad as a string of 0s and 1s, in SDL's own button order.
|
||||||
|
function buttonBits(slot)
|
||||||
|
local bits = ""
|
||||||
|
local b = 0
|
||||||
|
|
||||||
|
for b = 1, #BUTTONS do
|
||||||
|
bits = bits .. (controllerGetButton(slot, PADS[slot + 1][BUTTONS[b]].value) and "1" or "0")
|
||||||
|
end
|
||||||
|
|
||||||
|
return bits
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onMouseMoved(x, y, xr, yr, mouse)
|
||||||
|
mouseX = x
|
||||||
|
mouseY = y
|
||||||
|
mouseMoves = mouseMoves + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onInputPressed(switch)
|
||||||
|
held[switch] = true
|
||||||
|
if switch == SWITCH_MOUSE_DISCONNECT then
|
||||||
|
disconnects = disconnects + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onInputReleased(switch)
|
||||||
|
held[switch] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
local elapsed = os.clock() - started
|
||||||
|
local line = 0
|
||||||
|
local slot = 0
|
||||||
|
local axis = 0
|
||||||
|
local text = ""
|
||||||
|
|
||||||
|
overlayClear()
|
||||||
|
|
||||||
|
-- A dark panel behind the text, so every line reads over whatever the disc is showing.
|
||||||
|
colorForeground(0, 0, 40)
|
||||||
|
for line = 0, PANEL_HEIGHT do
|
||||||
|
overlayLine(4, 4 + line, width - 5, 4 + line)
|
||||||
|
end
|
||||||
|
colorForeground(0, 255, 0)
|
||||||
|
overlayBox(4, 4, width - 5, 4 + PANEL_HEIGHT)
|
||||||
|
colorForeground(255, 255, 255)
|
||||||
|
|
||||||
|
line = 16
|
||||||
|
fontPrint(12, line, string.format("scene49 input t=%.1f", elapsed))
|
||||||
|
line = line + LINE * 2
|
||||||
|
|
||||||
|
fontPrint(12, line, string.format("DEAD_ZONE %d TRIGGER_THRESHOLD %d raw (%.1f%% of full travel) joyMouseIsEnabled %s",
|
||||||
|
SINGE_DEAD_ZONE, SINGE_TRIGGER_THRESHOLD, SINGE_TRIGGER_THRESHOLD * 100.0 / AXIS_FULL, tostring(joyMouseIsEnabled())))
|
||||||
|
line = line + LINE * 2
|
||||||
|
|
||||||
|
-- Gamepads. Every device Singe opens is one of these, whatever it started life as.
|
||||||
|
for slot = 0, SINGE_MAX_CONTROLLERS - 1 do
|
||||||
|
if controllerIsValid(slot) then
|
||||||
|
fontPrint(12, line, string.format("gamepad slot %d \"%s\"", slot, tostring(controllerGetName(slot))))
|
||||||
|
line = line + LINE
|
||||||
|
text = ""
|
||||||
|
for axis = 0, AXES - 1 do
|
||||||
|
text = text .. string.format(" a%d=%-7d", axis, controllerGetAxis(slot, axis))
|
||||||
|
end
|
||||||
|
fontPrint(28, line, "axes " .. text)
|
||||||
|
line = line + LINE
|
||||||
|
fontPrint(28, line, "buttons " .. buttonBits(slot) .. " (A B X Y Back Guide Start LS RS LB RB DpU DpD DpL DpR)")
|
||||||
|
line = line + LINE
|
||||||
|
else
|
||||||
|
fontPrint(12, line, string.format("gamepad slot %d empty", slot))
|
||||||
|
line = line + LINE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
line = line + LINE
|
||||||
|
|
||||||
|
-- Mice, which are their own family: ManyMouse names them, and mouseHowMany counts them.
|
||||||
|
fontPrint(12, line, string.format("mice %d", mouseHowMany()))
|
||||||
|
line = line + LINE
|
||||||
|
for slot = 0, SINGE_MAX_MICE - 1 do
|
||||||
|
if slot < mouseHowMany() then
|
||||||
|
fontPrint(28, line, string.format("mouse %d \"%s\"", slot, tostring(mouseGetName(slot))))
|
||||||
|
line = line + LINE
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fontPrint(28, line, string.format("last position %d,%d after %d movements", mouseX, mouseY, mouseMoves))
|
||||||
|
line = line + LINE
|
||||||
|
fontPrint(28, line, string.format("SWITCH_MOUSE_DISCONNECT seen %d times", disconnects))
|
||||||
|
line = line + LINE * 2
|
||||||
|
|
||||||
|
fontPrint(12, line, string.format("keyboard mode %d last down %d", keyboardGetMode(), keyboardGetLastDown()))
|
||||||
|
line = line + LINE
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
for slot = 0, #SWITCHES do
|
||||||
|
if held[slot] then
|
||||||
|
text = text .. " " .. SWITCHES[slot]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fontPrint(12, line, "switches held " .. ((text == "") and "(none)" or text))
|
||||||
|
|
||||||
|
if (shot < #SHOT_TIMES) and (elapsed >= SHOT_TIMES[shot + 1]) then
|
||||||
|
shot = shot + 1
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if elapsed >= QUIT_TIME then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
77
testScripts/scene50.singe
Normal file
77
testScripts/scene50.singe
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
-- Hypseus parity, stage 6: subtitles, the banner and the disc picture controls. The disc is
|
||||||
|
-- searched to a known frame before each subtitle shot, so a cue's timing is read off the disc's
|
||||||
|
-- own frame number and not off the wall clock; the .srt's timestamps become disc frames at the
|
||||||
|
-- disc's frame rate when it is loaded.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 14)
|
||||||
|
local frames = 0
|
||||||
|
local step = 0
|
||||||
|
local note = "start"
|
||||||
|
local broken = srtLoad("testScripts/broken.srt") -- No cues in it: false, and nothing loaded.
|
||||||
|
local good = srtLoad("testScripts/subtitles.srt") -- Four cues over the twelve second disc.
|
||||||
|
|
||||||
|
-- Each step: a name, what to do when it is reached, and the frame to leave the disc on.
|
||||||
|
local steps = {
|
||||||
|
{ "cue 1 (frames 0 to 45)", 15, function() srtEnable(true) end },
|
||||||
|
{ "cue 2, two lines", 90, function() end },
|
||||||
|
{ "cue 2 at srtPosition(15)", 90, function() srtPosition(15) end },
|
||||||
|
{ "cue 2 at srtPosition(80)", 90, function() srtPosition(80) end },
|
||||||
|
{ "cue 3, escaped < and &", 210, function() end },
|
||||||
|
{ "the gap: no cue", 150, function() end },
|
||||||
|
{ "cue 4, the last", 300, function() end },
|
||||||
|
{ "overlayBanner over the cue", 300, function() overlayBanner("A banner, sixty characters or fewer, over the picture.", 30) end },
|
||||||
|
{ "the banner has timed out", 300, function() end },
|
||||||
|
{ "srtEnable(false)", 300, function() srtEnable(false) end },
|
||||||
|
{ "srtEnable(true) again", 300, function() srtEnable(true) end },
|
||||||
|
{ "srtClear()", 300, function() srtClear() end },
|
||||||
|
{ "vldpSetLuma(true, 0): half", 300, function() srtEnable(false) vldpSetLuma(true, 0) end },
|
||||||
|
{ "vldpSetLuma(true, 8): brighter", 300, function() vldpSetLuma(true, 8) end },
|
||||||
|
{ "vldpSetLuma(true, 4): neutral", 300, function() vldpSetLuma(true, 4) end },
|
||||||
|
{ "vldpSetLuma(false)", 300, function() vldpSetLuma(false) end },
|
||||||
|
{ "vldpSetBlend(true)", 300, function() vldpSetBlend(true) end },
|
||||||
|
{ "vldpSetBlend(false)", 300, function() vldpSetBlend(false) end },
|
||||||
|
{ "vldpSetLuma(true, 1) and blend", 300, function() vldpSetBlend(true) vldpSetLuma(true, 1) end },
|
||||||
|
{ "back to the plain picture", 300, function() vldpSetBlend(false) vldpSetLuma(false) end },
|
||||||
|
-- The blend is a one row smoothing, so it is easiest to see magnified: the same sliver of the
|
||||||
|
-- picture through vldpFocusArea, without it and then with it.
|
||||||
|
{ "a magnified sliver, no blend", 300, function() vldpFocusArea(discGetWidth() * 0.42, discGetHeight() * 0.42, 96, 64) end },
|
||||||
|
{ "the same sliver, blended", 300, function() vldpSetBlend(true) end },
|
||||||
|
{ "the sliver, blend off again", 300, function() vldpSetBlend(false) end },
|
||||||
|
{ "vldpResetFocus()", 300, function() vldpResetFocus() end },
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
discSearch(1)
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
if frames == ((#steps + 1) * 20) + 1 then
|
||||||
|
note = "vldpFlash()"
|
||||||
|
vldpFlash()
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
overlayClear()
|
||||||
|
overlayBox(1, 1, overlayGetWidth() - 2, overlayGetHeight() - 2)
|
||||||
|
fontPrint(8, 6, string.format("srtLoad broken=%s good=%s disc frame %d", tostring(broken), tostring(good), discGetFrame()))
|
||||||
|
fontPrint(8, 24, string.format("step %d/%d %s", step, #steps, note))
|
||||||
|
-- One step every twenty script frames: act on the first, shoot on the tenth, so a banner
|
||||||
|
-- (thirty drawn frames) is up for one shot and gone by the next.
|
||||||
|
if (frames % 20) == 1 and step < #steps then
|
||||||
|
step = step + 1
|
||||||
|
note = steps[step][1]
|
||||||
|
discSearch(steps[step][2])
|
||||||
|
steps[step][3]()
|
||||||
|
elseif (frames % 20) == 11 then
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
-- The flash lasts one drawn frame, so it is asked for and shot in the same tick. The note is
|
||||||
|
-- set before the text is drawn, so the shot names what it is showing.
|
||||||
|
if frames == ((#steps + 1) * 20) + 3 then
|
||||||
|
note = "the frame after vldpFlash()"
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if frames == ((#steps + 1) * 20) + 20 then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
71
testScripts/scene51.singe
Normal file
71
testScripts/scene51.singe
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-- Hypseus parity, stage 6: the sprite sheet rotation pair, the source rectangle blit, the frame
|
||||||
|
-- count getter, the two in-memory loaders, the overlay opacity, controllerHowMany and the music
|
||||||
|
-- family with its own gain, over an mp3 so the built-in decoder is exercised.
|
||||||
|
dofile("Singe/Framework.singe")
|
||||||
|
local font = fontLoad("Singe/FreeSansBold.ttf", 14)
|
||||||
|
local arrows = spriteLoadFrames(4, "testScripts/arrows.png")
|
||||||
|
local sheet = spriteLoad("testScripts/arrows.png") -- The same file as one wide still.
|
||||||
|
local bytes = io.open("testScripts/arrows.png", "rb"):read("a")
|
||||||
|
local copied = spriteLoadData(bytes) -- The same image, from a Lua string.
|
||||||
|
local wave = io.open("testScripts/tone.ogg", "rb"):read("a")
|
||||||
|
local effect = soundLoadData(wave) -- A sound from a Lua string.
|
||||||
|
local tune = musicLoad("testScripts/tune.mp3") -- 6 seconds of 330 Hz, mp3.
|
||||||
|
local frames = 0
|
||||||
|
local step = 0
|
||||||
|
local note = "start"
|
||||||
|
local said = ""
|
||||||
|
|
||||||
|
local steps = {
|
||||||
|
{ "spriteRotateFrame(arrows, 0, 1)", function() spriteRotateFrame(arrows, 0, 1) end },
|
||||||
|
{ "spriteRotateFrame(arrows, 45, 2)", function() spriteRotateFrame(arrows, 45, 2) end },
|
||||||
|
{ "spriteRotateFrame(arrows, 135, 3)", function() spriteRotateFrame(arrows, 135, 3) end },
|
||||||
|
{ "spriteScale(arrows, 2) then 225, 4", function() spriteScale(arrows, 2) spriteRotateFrame(arrows, 225, 4) end },
|
||||||
|
{ "out of range frame draws frame 1", function() spriteScale(arrows, 1) spriteRotateFrame(arrows, 90, 99) end },
|
||||||
|
{ "setOverlayOpacity(96)", function() setOverlayOpacity(96) end },
|
||||||
|
{ "setOverlayOpacity(255)", function() setOverlayOpacity(255) end },
|
||||||
|
{ "musicPlay(tune, -1) at volume 128", function() said = "musicSetVolume returned " .. musicSetVolume(128) musicPlay(tune, -1) end },
|
||||||
|
{ "musicSetVolume(16), sound unmoved", function() said = "musicSetVolume returned " .. musicSetVolume(16) end },
|
||||||
|
{ "soundSetVolume(8), music unmoved", function() soundSetVolume(8) soundPlay(effect) end },
|
||||||
|
{ "musicPause()", function() musicPause() end },
|
||||||
|
{ "musicResume()", function() musicResume() end },
|
||||||
|
{ "musicStop(tune, 250) with a fade", function() musicStop(tune, 250) end },
|
||||||
|
{ "musicPlay(tune) once through", function() musicPlay(tune) end },
|
||||||
|
{ "musicStop() and musicUnload(tune)", function() musicStop() musicUnload(tune) end },
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSelect(font)
|
||||||
|
overlaySetResolution(640, 480) -- Room for the rotated frames at twice their size.
|
||||||
|
discPlay()
|
||||||
|
soundSetVolume(40)
|
||||||
|
spriteRotateFrame(arrows, 0, 1) -- There is a frame to draw before the first step runs.
|
||||||
|
|
||||||
|
function onOverlayUpdate()
|
||||||
|
frames = frames + 1
|
||||||
|
overlayClear()
|
||||||
|
overlayBox(1, 1, overlayGetWidth() - 2, overlayGetHeight() - 2)
|
||||||
|
-- The rotated frame, centred on its point, beside the same frame drawn flat by spriteDrawFrame.
|
||||||
|
spriteDrawRotatedFrame(arrows, 90, 200)
|
||||||
|
spriteDrawRotatedFrame(arrows, 240, 200, 1.5)
|
||||||
|
spriteDrawRotatedFrame(arrows, 400, 200, 2, 0.75)
|
||||||
|
spriteDrawFrame(arrows, 520, 176, 1)
|
||||||
|
-- spriteDrawGrid takes a rectangle out of the wide sheet: frames 3 and 2, and a quarter of one.
|
||||||
|
spriteDrawGrid(sheet, 40, 330, 96, 0, 48, 48)
|
||||||
|
spriteDrawGrid(sheet, 120, 330, 48, 0, 48, 48)
|
||||||
|
spriteDrawGrid(sheet, 200, 330, 12, 12, 24, 24)
|
||||||
|
-- The sprite built from bytes draws exactly as the one built from the file.
|
||||||
|
spriteDrawGrid(copied, 280, 330, 0, 0, 48, 48)
|
||||||
|
fontPrint(8, 6, string.format("frames sheet=%d still=%d data=%d controllers %d", spriteGetFrames(arrows), spriteGetFrames(sheet), spriteGetFrames(copied), controllerHowMany()))
|
||||||
|
fontPrint(8, 24, string.format("sound %d music playing %s %s", soundGetVolume(), tostring(musicIsPlaying()), said))
|
||||||
|
fontPrint(8, 42, string.format("step %d/%d %s", step, #steps, note))
|
||||||
|
if (frames % 20) == 1 and step < #steps then
|
||||||
|
step = step + 1
|
||||||
|
note = steps[step][1]
|
||||||
|
steps[step][2]()
|
||||||
|
elseif (frames % 20) == 11 then
|
||||||
|
singeScreenshot()
|
||||||
|
end
|
||||||
|
if frames == ((#steps + 1) * 20) then
|
||||||
|
singeQuit()
|
||||||
|
end
|
||||||
|
return OVERLAY_UPDATED
|
||||||
|
end
|
||||||
BIN
testScripts/strip.png
(Stored with Git LFS)
Normal file
BIN
testScripts/strip.png
(Stored with Git LFS)
Normal file
Binary file not shown.
17
testScripts/subtitles.srt
Normal file
17
testScripts/subtitles.srt
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
1
|
||||||
|
00:00:00,000 --> 00:00:01,500
|
||||||
|
The first cue, at the start of the disc.
|
||||||
|
|
||||||
|
2
|
||||||
|
00:00:02,000 --> 00:00:04,000
|
||||||
|
A second cue with two lines,
|
||||||
|
so the parser has to join them.
|
||||||
|
|
||||||
|
3
|
||||||
|
00:00:06,000 --> 00:00:08,000
|
||||||
|
A third cue after a gap,
|
||||||
|
with a <bracket> & an ampersand.
|
||||||
|
|
||||||
|
4
|
||||||
|
00:00:09,500 --> 00:00:11,500
|
||||||
|
The fourth and last cue.
|
||||||
BIN
testScripts/tone.ogg
(Stored with Git LFS)
Normal file
BIN
testScripts/tone.ogg
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
testScripts/tune.mp3
(Stored with Git LFS)
Normal file
BIN
testScripts/tune.mp3
(Stored with Git LFS)
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue