SoundToScreen shows where sounds come from. It may be useful for people who play without sound, with mono sound, or people with hearing impairment.
- Three types of sound: noise (harmless), level (doors, elevators), danger (threats).
- Compatible with old saves.
- Options to configure looks.
- SoundToScreen won’t work if UZDoom is launched with
-nosound.
- Toby Accessibility Mod by Alando1.
- Making Games Better for the Deaf and Hard of Hearing by Game Maker’s Toolkit.
SPDX-FileCopyrightText: © 2022 Alexander Kromm <mmaulwurff@gmail.com>
SPDX-License-Identifier: GPL-3.0-only
OptionMenu st_Menu
{
st_PlainTranslator
Title "SoundToScreen"
StaticText "Position"
Slider "Horizontal", st_x_position, 0.0, 1.0, 0.01, 2
Slider "Vertical", st_y_position, 0.0, 1.0, 0.01, 2
StaticText ""
Slider "Width", st_x_radius, 20, 50, 1
Slider "Height", st_y_radius, 20, 50, 1
Slider "Dot size", st_dot_size, 1, 9, 2
StaticText ""
Option "On automap", st_on_automap, OnOff
StaticText ""
StaticText "Colors"
ColorPicker "Base", st_color_base
ColorPicker "Noise", st_color_noise
ColorPicker "Level", st_color_geometry
ColorPicker "Danger", st_color_danger
}
AddOptionMenu OptionsMenu { Submenu "$ST_MENU_NAME", st_Menu }
AddOptionMenu OptionsMenuSimple { Submenu "$ST_MENU_NAME", st_Menu }[enu default]
ST_MENU_NAME = "SoundToScreen \cj⚞";
[ru]
ST_DANGER = "Опасность";user float st_x_position = 0.5;
user float st_y_position = 0.3;
user float st_x_radius = 30;
user float st_y_radius = 30;
user int st_dot_size = 3;
user bool st_on_automap = false;
user color st_color_base = "000000"; // black
user color st_color_noise = "FFFFFF"; // white
user color st_color_geometry = "00FF00"; // green
user color st_color_danger = "FF0000"; // redGameInfo { EventHandlers = "st_EventHandler" }version 4.14.3
#include "zscript/st_PlainTranslator.zs"
class st_EventHandler : StaticEventHandler
{
// Collects sounds by searching for actors that make sounds and doors/elevators.
override void worldTick()
{
if (!mIsInitialized)
{
if (level.mapName ~== "TITLEMAP") return;
initialize();
}
if (players[consolePlayer].mo == NULL) return;
mIterator.reinit();
for (int type = Noise; type < SoundTypesCount; ++type)
mColors[type] = mColorCvars[type].getString();
for (int type = Noise; type < SoundTypesCount; ++type)
{
for (int i = 0; i < DOTS_COUNT; ++i)
mDots[type][i] = 0;
}
let player = players[consolePlayer].mo;
Thinker aThinker;
while (aThinker = mIterator.next())
{
let anActor = Actor(aThinker);
if (anActor != NULL)
{
if (anActor == player) continue;
if (anActor is "Inventory" && Inventory(anActor).owner != NULL) continue;
if (anActor.bMissile && anActor.target == player) continue;
if (!anActor.isActorPlayingSound(CHAN_AUTO)) continue;
double distance = anActor.distance3D(player);
if (anActor.bBoss) distance = min(distance, MAX_DISTANCE / 2);
if (distance > MAX_DISTANCE) continue;
let type = ((anActor.bIsMonster && !anActor.bFriendly && anActor.health > 0)
|| (anActor.bMissile && anActor.damage > 0)) ? Danger : Noise;
let position = calculateActorScreenPosition(anActor);
mDots[type][position] += distanceToDotLevel(distance);
continue;
}
let aMover = Mover(aThinker);
if (aMover != NULL)
{
Sector aSector = aMover.getSector();
if (aSector.flags & Sector.SECF_SilentMove) continue;
// Important order: ask if moving before remembering.
bool isMoving = isMoving(aSector);
remember(aSector);
if (!isMoving) continue;
vector3 playerRelative = player.posRelative(aSector);
vector2 xy = aSector.centerSpot;
double z = aMover is "MovingCeiling"
? aSector.centerCeiling()
: aSector.centerFloor();
vector3 soundSource = (xy.x, xy.y, z);
vector3 diff = level.vec3Diff(soundSource, playerRelative);
double distance = diff.length();
if (distance > MAX_DISTANCE) continue;
let position = calculateSectorScreenPosition(aSector);
mDots[Geometry][position] += distanceToDotLevel(distance);
}
}
}
override void renderOverlay(RenderEvent event)
{
if (!mIsInitialized) return;
if (players[consolePlayer].mo == NULL) return;
if (automapActive && !mOnAutomapCvar.getInt())
{
resetUi();
return;
}
vector2 center = (mXPositionCvar.getFloat() * Screen.getWidth(),
mYPositionCvar.getFloat() * Screen.getHeight());
double radiusX = mXRadiusCvar.getFloat();
double radiusY = mYRadiusCvar.getFloat();
int dotSize = mDotSizeCvar.getInt();
Color baseColor = mBaseColorCvar.getString();
for (int i = 0; i < DOTS_COUNT; ++i)
drawDot(center + makePosition(i, 0.0, radiusX, radiusY), baseColor, dotSize);
if (level.mapTime < 2)
{
resetUi();
return;
}
for (int type = Noise; type < SoundTypesCount; ++type)
{
for (int i = 0; i < DOTS_COUNT; ++i)
{
double diff = mDotsInterpolated[type][i] - mDots[type][i];
mDotsInterpolated[type][i] -= diff * ((diff > 0) ? 0.2 : 0.4);
}
}
// Fake sound level changes.
double dotsVariance[SoundTypesCount][DOTS_COUNT];
if (level.time % 3 == 0)
{
for (int type = Noise; type < SoundTypesCount; ++type)
{
for (int i = 0; i < DOTS_COUNT; ++i)
dotsVariance[type][i] = frandom[SoundToScreen](-MIN_DOT_LEVEL, MIN_DOT_LEVEL);
}
}
// Gaussian blur, sigma 1.
static const double blurWeights[] =
{
0.0613595978134402 / 0.38774041331389975,
0.24477019552960988 / 0.38774041331389975,
1.0,
0.24477019552960988 / 0.38774041331389975,
0.0613595978134402 / 0.38774041331389975
};
double dotsBlurred[SoundTypesCount][DOTS_COUNT];
for (int type = Noise; type < SoundTypesCount; ++type)
{
for (int i = 0; i < DOTS_COUNT; ++i)
{
double sum = 0;
for (int j = -2; j <= 2; ++j)
{
int index = (i + j + DOTS_COUNT) % DOTS_COUNT;
sum += mDotsInterpolated[type][index] * blurWeights[2 + j];
if (mDotsInterpolated[type][index] > MIN_DOT_LEVEL)
sum += dotsVariance[type][index];
}
dotsBlurred[type][i] = min(1.0, sum);
}
}
for (int type = Noise; type < SoundTypesCount; ++type)
{
for (int i = 0; i < DOTS_COUNT; ++i)
{
double dotLevel = dotsBlurred[type][i];
if (dotLevel > MIN_DOT_LEVEL)
{
drawDot(center + makePosition(i, dotLevel, radiusX, radiusY),
mColors[type],
dotSize);
}
}
}
}
private ui void resetUi()
{
for (int type = Noise; type < SoundTypesCount; ++type)
for (int i = 0; i < DOTS_COUNT; ++i) mDotsInterpolated[type][i] = 0;
}
private ui vector2 makePosition(int i, double dotLevel, double radiusX, double radiusY)
{
double angle = i * (360.0 / DOTS_COUNT);
return ((1 + dotLevel) * radiusX * sin(angle),
-(1 + dotLevel) * radiusY * cos(angle));
}
private ui void drawDot(vector2 position, Color aColor, int dotSize)
{
int halfSize = dotSize / 2;
Screen.dim(aColor, 1.0,
int(round(position.x)) - halfSize,
int(round(position.y)) - halfSize,
dotSize,
dotSize);
}
private void initialize()
{
mIsInitialized = true;
mIterator = ThinkerIterator.create("Thinker");
PlayerInfo player = players[consolePlayer];
mXPositionCvar = Cvar.getCvar("st_x_position", player);
mYPositionCvar = Cvar.getCvar("st_y_position", player);
mXRadiusCvar = Cvar.getCvar("st_x_radius", player);
mYRadiusCvar = Cvar.getCvar("st_y_radius", player);
mDotSizeCvar = Cvar.getCvar("st_dot_size", player);
mOnAutomapCvar = Cvar.getCvar("st_on_automap", player);
mBaseColorCvar = Cvar.getCvar("st_color_base", player);
mColorCvars[Noise] = Cvar.getCvar("st_color_noise", player);
mColorCvars[Geometry] = Cvar.getCvar("st_color_geometry", player);
mColorCvars[Danger] = Cvar.getCvar("st_color_danger", player);
}
private static int calculateActorScreenPosition(Actor target)
{
PlayerInfo player = players[consolePlayer];
double angleToTarget = (player.mo.angle - player.mo.angleTo(target)) % 360.0;
return int(round(angleToTarget / (360.0 / DOTS_COUNT))) % DOTS_COUNT;
}
private static int calculateSectorScreenPosition(sector aSector)
{
PlayerInfo player = players[consolePlayer];
vector3 playerRelative = player.mo.posRelative(aSector);
vector2 diff = aSector.centerSpot - playerRelative.xy;
double angleToTarget = (player.mo.angle - atan2(diff.y, diff.x)) % 360.0;
return int(round(angleToTarget / (360.0 / DOTS_COUNT))) % DOTS_COUNT;
}
// No way to easily query if a sector is moving, hence this workaround.
private bool isMoving(Sector aSector)
{
let memory = st_SectorMemory(mSectorMemories.getIfExists(aSector.index()));
return memory != NULL
&& memory.time == level.time - 1
&& (memory.floorHeight != aSector.centerFloor()
|| memory.ceilingHeight != aSector.centerCeiling());
}
private void remember(Sector aSector)
{
let memory = st_SectorMemory(mSectorMemories.getIfExists(aSector.index()));
if (memory == NULL)
{
memory = new("st_SectorMemory");
mSectorMemories.insert(aSector.index(), memory);
}
memory.time = level.time;
memory.floorHeight = aSector.centerFloor();
memory.ceilingHeight = aSector.centerCeiling();
}
// Note: when changing this formula, adjust MAX_DISTANCE (reverse of this).
private double distanceToDotLevel(double distance)
{
return min(1.0, 1.0 / max(distance * distance / DISTANCE_FACTOR, 1.0));
}
enum SoundType {Noise, Geometry, Danger, SoundTypesCount}
const DOTS_COUNT = 32; // Should be divisible by 4 for clear base directions.
const MIN_DOT_LEVEL = 0.05;
const DISTANCE_FACTOR = 100000.0;
const MAX_DISTANCE = sqrt(DISTANCE_FACTOR / MIN_DOT_LEVEL);
private double mDots[SoundTypesCount][DOTS_COUNT];
private ui double mDotsInterpolated[SoundTypesCount][DOTS_COUNT];
private Color mColors[SoundTypesCount];
private bool mIsInitialized;
private ThinkerIterator mIterator;
private Map<int, Object> mSectorMemories;
private Cvar mXPositionCvar;
private Cvar mYPositionCvar;
private CVar mXRadiusCvar;
private CVar mYRadiusCvar;
private Cvar mDotSizeCvar;
private Cvar mOnAutomapCvar;
private Cvar mBaseColorCvar;
private Cvar mColorCvars[SoundTypesCount];
}
class st_SectorMemory
{
int time;
double floorHeight;
double ceilingHeight;
}GameInfo { EventHandlers = "stt_EventHandler" }version 4.14.3
class stt_EventHandler : StaticEventHandler
{
override void networkProcess(ConsoleEvent command)
{
if (command.name == "stt_spawn_imps")
{
int count = command.args[0];
Console.printf("Spawning %d imps...", count);
for (int i = 0; i < count; ++i)
Actor.spawn("doomimp", players[consolePlayer].mo.pos + (50, 0, 0));
}
}
}wait 2; map map01; wait 2; quit