Implement audio mixing

This commit is contained in:
2025-07-20 13:49:53 +03:00
parent 10f9c47929
commit 72fbf406a7
22 changed files with 428 additions and 32 deletions
Binary file not shown.
+33
View File
@@ -6,6 +6,15 @@ Room
0D00DD0pD0
GGGGGGGGGG
Room
5555555555
0000000000
0000000000
00t0000t00
0d00000p00
GGGGGGGGGG
Room
5555555555
00t00p0t00
@@ -14,6 +23,14 @@ Room
000lggggl0
GGGGggggGG
Room
5555555555
00t00pdt00
000lGGGGl0
000lggggl0
000lggggl0
GGGGggggGG
Room
0000000000
00t00p0t00
@@ -22,6 +39,14 @@ Room
0lDDDgDDDl
GGGGGGGGGG
Room
0000000000
00t00p0t00
0lg00g00gl
0l000g000l
0lDdDgDdDl
GGGGGGGGGG
Room
0000000000
00t0000t00
@@ -29,3 +54,11 @@ Room
00gggggg00
0gggggggg0
GggggggggG
Room
0000000000
00t0000t00
00000pd000
00gggggg00
0gggggggg0
GggggggggG
+16
View File
@@ -6,6 +6,14 @@ Room
0tp00000t0
GGGGPPPPGG
Room
0000000000
0000000000
00t00000t0
0000000000
0dp0000000
GGGGPPPPGG
Room
0000000000
0000000000
@@ -21,3 +29,11 @@ Room
0t000000t0
000000p000
GPPGPPGPPG
Room
0000000000
0000000000
0000000000
0t000000t0
000d00p000
GPPGPPGPPG
+17
View File
@@ -6,6 +6,15 @@ Room
0000l0p000
GGGGGGGGGG
Room
0000l00000
0000l00000
0000l00000
0t00l000t0
000dldp000
GGGGGGGGGG
Room
0000l00000
00t0l00t00
@@ -13,3 +22,11 @@ Room
0l000000l0
0l00p000l0
GGGGGGGGGG
Room
0000l00000
00t0l00t00
0lggggggl0
0l000000l0
0ld0p000l0
GGGGGGGGGG
+16
View File
@@ -6,6 +6,14 @@ Room
0p00l00000
GGPPlPPPGG
Room
0000l00000
0000l00000
0000l00000
0t00l000t0
0p00l0d000
GGPPlPPPGG
Room
0000l00000
0t00l000t0
@@ -13,3 +21,11 @@ ggggggglgg
0000000l00
0000l0pl00
GGPPlGGGGG
Room
0000l00000
0t00l000t0
ggggggglgg
0000000l00
0d00l0pl00
GGPPlGGGGG
+4 -3
View File
@@ -3,17 +3,18 @@
SRCS = \
src/Alarm.cpp \
src/App.cpp \
src/AudioManager.cpp \
src/Common.cpp \
src/DigitManager.cpp \
src/Entity.cpp \
src/EntityManager.cpp \
src/FileManager.cpp \
src/FontManager.cpp \
src/Gridmap.cpp \
src/Main.cpp \
src/RenderManager.cpp \
src/RoomEntity.cpp \
src/SpriteManager.cpp \
src/State.cpp \
src/SurfaceManager.cpp \
src/FontManager.cpp \
src/DigitManager.cpp \
src/RoomEntity.cpp \
src/Variable.cpp
+10
View File
@@ -26,6 +26,7 @@ TApp::TApp() {
EntityManager_ = std::make_unique<TEntityManager>(*RenderManager_, 1000 / 60);
FontManager_ = std::make_unique<TFontManager>(*SpriteManager_);
DigitManager_ = std::make_unique<TDigitManager>(*SpriteManager_);
AudioManager_ = std::make_unique<TAudioManager>(*FileManager_);
}
TApp::~TApp() {
@@ -43,6 +44,7 @@ void TApp::Process() {
RenderManager_->Reset();
EntityManager_->Run();
RenderManager_->Run();
AudioManager_->Run();
}
int TApp::Run() {
@@ -86,6 +88,10 @@ TDigitManager& TApp::DigitManager() {
return *DigitManager_;
}
TAudioManager& TApp::AudioManager() {
return *AudioManager_;
}
const TFileManager& TApp::FileManager() const {
return *FileManager_;
}
@@ -114,4 +120,8 @@ const TDigitManager& TApp::DigitManager() const {
return *DigitManager_;
}
const TAudioManager& TApp::AudioManager() const {
return *AudioManager_;
}
} // namespace NGame
+4
View File
@@ -2,6 +2,7 @@
#include "Common.h"
#include "AudioManager.h"
#include "DigitManager.h"
#include "EntityManager.h"
#include "FileManager.h"
@@ -35,6 +36,7 @@ public:
TSpriteManager& SpriteManager();
TFontManager& FontManager();
TDigitManager& DigitManager();
TAudioManager& AudioManager();
const TFileManager& FileManager() const;
const TRenderManager& RenderManager() const;
@@ -43,6 +45,7 @@ public:
const TSpriteManager& SpriteManager() const;
const TFontManager& FontManager() const;
const TDigitManager& DigitManager() const;
const TAudioManager& AudioManager() const;
private:
TApp();
@@ -57,6 +60,7 @@ private:
std::unique_ptr<TSpriteManager> SpriteManager_;
std::unique_ptr<TFontManager> FontManager_;
std::unique_ptr<TDigitManager> DigitManager_;
std::unique_ptr<TAudioManager> AudioManager_;
TState State_;
};
+137
View File
@@ -0,0 +1,137 @@
#include "AudioManager.h"
#include <cmath>
namespace NGame {
TAudioManager::TAudioManager(TFileManager& fileManager)
: FileManager_(fileManager) {
SDL_AudioSpec desired;
desired.freq = 44100;
desired.format = AUDIO_F32;
desired.channels = 2;
desired.samples = 4096;
desired.callback = NULL;
Device_ = SDL_OpenAudioDevice(NULL, 0, &desired, NULL, 0);
if (!Device_) {
throw std::runtime_error("no audio device");
}
Stream_ = SDL_NewAudioStream(AUDIO_U8, 1, 22050, AUDIO_F32, 2, 44100);
if (!Stream_) {
throw std::runtime_error("can't create audio stream");
}
for (std::size_t i = 0; i < 16; ++i) {
Channels_[i].InUse = false;
}
SDL_PauseAudioDevice(Device_, SDL_FALSE);
LastTick_ = SDL_GetTicks();
}
void TAudioManager::Run() {
std::uint32_t currentTick = SDL_GetTicks();
Paint(Stream_, currentTick - LastTick_);
if (SDL_GetQueuedAudioSize(Device_) < 8192) {
const int bytesRemaining = SDL_AudioStreamAvailable(Stream_);
if (bytesRemaining > 0) {
const int samplesRemaining = SDL_min(bytesRemaining, 32 * 1024);
static Uint8 convertedBuffer[32 * 1024];
auto bytesRead = SDL_AudioStreamGet(Stream_, convertedBuffer, samplesRemaining);
SDL_QueueAudio(Device_, convertedBuffer, bytesRead);
}
}
LastTick_ = currentTick;
}
void TAudioManager::Play(const std::string& path) {
TSound* sound = Load(path);
for (std::size_t i = 0; i < 16; i++) {
if (Channels_[i].InUse) {
continue;
}
Channels_[i].InUse = true;
Channels_[i].Position = 0;
Channels_[i].Sound = sound;
break;
}
}
void TAudioManager::Paint(SDL_AudioStream* stream, std::uint32_t ticks) {
std::uint8_t temporary[512];
std::size_t samples = std::min((ticks * 22050 + 999) / 1000, 512u);
// Mix
for (size_t i = 0; i < samples; ++i) {
int16_t accumulator = 0;
for (size_t j = 0; j < 16; j++) {
auto& channel = Channels_[j];
if (!channel.InUse) {
continue;
}
accumulator += channel.Sound->Data[channel.Position];
accumulator -= 128;
channel.Position++;
if (channel.Position > channel.Sound->Size) {
channel.InUse = false;
}
}
accumulator += 128;
if (accumulator < 0) {
accumulator = 0;
}
if (accumulator > 255) {
accumulator = 255;
}
temporary[i] = accumulator;
}
// Output
if (samples > 0) {
SDL_AudioStreamPut(stream, temporary, samples * sizeof (std::uint8_t));
}
}
TAudioManager::TSound* TAudioManager::Load(const std::string& path) {
auto it = Sounds_.find(path);
if (it != Sounds_.end()) {
return it->second;
}
TSound* newSound = new TSound;
auto data = FileManager_.Get(path);
SDL_RWops* rw = SDL_RWFromConstMem(data.data(), data.size());
if (!SDL_LoadWAV_RW(rw, 1, &newSound->Spec, &newSound->Data, &newSound->Size)) {
throw std::runtime_error("can't read WAV file " + path);
}
if (newSound->Spec.channels != 1) {
throw std::runtime_error("not mono WAV file " + path);
}
if (newSound->Spec.format != AUDIO_U8) {
throw std::runtime_error("not 8bit pcm WAV file " + path);
}
if (newSound->Spec.freq != 22050) {
throw std::runtime_error("not 22050hz WAV file " + path);
}
Sounds_[path] = newSound;
return newSound;
}
} // namespace NGame
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "Common.h"
#include "FileManager.h"
#include <SDL2/SDL.h>
namespace NGame {
class TAudioManager {
public:
TAudioManager(TFileManager& fileManager);
DELETE_COPY(TAudioManager)
void Run();
void Play(const std::string& sound);
void Paint(SDL_AudioStream* stream, std::uint32_t ticks);
private:
struct TSound {
SDL_AudioSpec Spec;
std::uint8_t* Data;
std::uint32_t Size;
};
struct TSoundChannel {
bool InUse;
TSound* Sound;
std::uint32_t Position;
};
private:
TSound* Load(const std::string& sound);
private:
TFileManager& FileManager_;
TSoundChannel Channels_[16];
std::unordered_map<std::string, TSound*> Sounds_;
SDL_AudioDeviceID Device_;
SDL_AudioStream* Stream_;
std::uint32_t LastTick_;
};
} // namespace NGame
+1 -1
View File
@@ -53,7 +53,7 @@ static void MainLoop(void) {
int main(int argc, char** argv)
{
SDL_Init(SDL_INIT_VIDEO);
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
srand(time(NULL));
+131 -16
View File
@@ -120,6 +120,8 @@ TExplosionEntity::TExplosionEntity(NGame::TEntity::TId id)
SetSize({64, 64});
Sprite_ = NGame::TApp::Instance()->SpriteManager().Get("Sprites/Light.txt");
Alarm_.Set(0, 50);
App_->AudioManager().Play("Audio/Explosion2.wav");
}
void TExplosionEntity::Update(std::uint32_t delta) {
@@ -191,10 +193,15 @@ void TMineEntity::Update(std::uint32_t delta) {
switch (State_) {
case Dormant:
if (!entityManager.IsPlaceEmpty(Position() + Size() / 2 - TriggerRadius_ / 2, TriggerRadius_, HERO_GROUP)) {
Remaining_ = rand() % 5 + 5;
if (App_->State().Variable("InstantMines").Bool()) {
Remaining_ = 1;
} else {
Remaining_ = rand() % 3 + 7;
if (!(Remaining_ % 2)) {
Remaining_ += 1;
}
}
Alarm_.Set(0, 100);
State_ = Active;
}
@@ -232,6 +239,9 @@ void TMineEntity::Alarm(NGame::TAlarm::TId id) {
if (Remaining_) {
--Remaining_;
}
if (AlternateBlink_) {
App_->AudioManager().Play("Audio/Select.wav");
}
}
}
@@ -671,8 +681,14 @@ THero::THero(NGame::TEntity::TId id)
SetSize(NGame::Vec2i(4, 12));
SetPosition(NGame::Vec2i(0, 0));
SetCollisionGroup(HERO_GROUP);
if (App_->State().Variable("DarkLevel").Bool()) {
NGame::TApp::Instance()->RenderManager().EnableLight(true);
NGame::TApp::Instance()->RenderManager().SetDefaultLightColor(0, 0, 0);
} else {
NGame::TApp::Instance()->RenderManager().EnableLight(false);
NGame::TApp::Instance()->RenderManager().SetDefaultLightColor(0, 0, 0);
}
Alarm_.Set(0, 100);
@@ -687,6 +703,9 @@ THero::THero(NGame::TEntity::TId id)
break;
}
}
RemainingTime_ = App_->State().Variable("Time").Double();
Grenades_ = App_->State().Variable("Grenades").Int();
}
void THero::Input(SDL_Event* event) {
@@ -781,6 +800,8 @@ void THero::Update(std::uint32_t delta) {
Speed_.X = -100;
}
if (KeysPressed_[static_cast<int>(EKeys::X)]) {
if (Grenades_) {
Grenades_--;
auto entity = entityManager.MakeEntityByName("GrenadeEntity");
auto targetEntity = dynamic_cast<TGrenadeEntity*>(entity.get());
@@ -792,24 +813,20 @@ void THero::Update(std::uint32_t delta) {
}
entityManager.UpdateCollision(entity);
}
}
if (KeysPressed_[static_cast<int>(EKeys::Up)]) {
auto otherIds = entityManager.CollisionList(Position(), Size(), PASSAGE_GROUP, Id());
for (auto otherId : otherIds) {
auto other = entityManager.Entity(otherId);
if (dynamic_cast<TExitEntity*>(other.get())) {
App_->State().Variable("Time").SetDouble(RemainingTime_);
App_->State().Variable("Grenades").SetInt(Grenades_);
entityManager.AddToDeferred("EulaMenu");
entityManager.Reset();
}
}
}
if (KeysPressed_[static_cast<int>(EKeys::C)]) {
auto entity = entityManager.MakeEntityByName("FloatingTextEntity");
auto targetEntity = dynamic_cast<TFloatingTextEntity*>(entity.get());
targetEntity->SetText("You died, LMAO");
targetEntity->SetColor(NGame::TFontManager::Red);
targetEntity->SetPosition(Position());
}
if (!entityManager.IsPlaceEmpty(Position(), Size(), LADDER_GROUP)) {
if (KeysHeld_[static_cast<int>(EKeys::Up)] || KeysHeld_[static_cast<int>(EKeys::Down)]) {
@@ -829,6 +846,7 @@ void THero::Update(std::uint32_t delta) {
if (KeysPressed_[static_cast<int>(EKeys::Z)]) {
Speed_.Y = -std::sqrt(2 * Gravity_ * (16 + (16 - Size().Y)));
State_ = EState::Jump;
App_->AudioManager().Play("Audio/jump.wav");
continue;
}
}
@@ -972,6 +990,18 @@ void THero::Update(std::uint32_t delta) {
}
}
if (State_ != EState::Dead) {
RemainingTime_ -= delta;
if (RemainingTime_ < 0) {
RemainingTime_ = 0;
App_->AudioManager().Play("Audio/Laser.wav");
State_ = EState::Dead;
if (!Alarm_.IsSet(1)) {
Alarm_.Set(1, 3000);
}
}
}
// Explictly check damage sources and flick the body around the screen
{
auto entityList = entityManager.CollisionList(Position(), Size(), DAMAGE_GROUP, Id());
@@ -1009,6 +1039,7 @@ void THero::Update(std::uint32_t delta) {
}
if (previousState != State_) {
App_->AudioManager().Play("Audio/Laser.wav");
if (!Alarm_.IsSet(1)) {
Alarm_.Set(1, 3000);
}
@@ -1067,7 +1098,11 @@ void THero::Update(std::uint32_t delta) {
if (!moveResult.second.second) {
// Fall damage
if (App_->State().Variable("FallDamage").Bool() && (Position().Y - LastStablePosition_.Y > 40)) {
App_->AudioManager().Play("Audio/Laser.wav");
State_ = EState::Dead;
if (!Alarm_.IsSet(1)) {
Alarm_.Set(1, 3000);
}
}
Speed_.Y = 0;
@@ -1123,6 +1158,16 @@ void THero::Draw() const {
auto light = SpriteManager_.Get("Sprites/Light.txt");
SpriteManager_.Draw(light, 0, Position() - (light->Frames[0].Size * 4 / 2), {4, 4});
RenderManager_.SetLayer(NGame::TRenderManager::Interface);
RenderManager_.SetColor(0, 0, 0, 128);
RenderManager_.DrawRect({0, 0}, {320, 16}, true);
App_->DigitManager().Draw({320 - 48 - 2, 2}, RemainingTime_, 6, 3);
auto itemSprite = SpriteManager_.Get("Sprites/Grenade.txt");
for (std::size_t i = 0; i < Grenades_; i++) {
SpriteManager_.Draw(itemSprite, 0, NGame::Vec2i(200 + i * 8, 3));
}
if (!App_->State().Variable("NoCompas").Bool()) {
auto heroCenter = RenderManager_.Camera() + RenderManager_.Size() / 2;
auto delta = ExitPosition_ - heroCenter;
@@ -1146,22 +1191,19 @@ void THero::Draw() const {
index = 3;
}
}
RenderManager_.SetLayer(NGame::TRenderManager::Interface);
screenPosition.X = std::min(std::max(screenPosition.X, 0), RenderManager_.Size().X - 16);
screenPosition.Y = std::min(std::max(screenPosition.Y, 16), RenderManager_.Size().Y - 16);
auto arrow = SpriteManager_.Get("Sprites/Arrow.txt");
SpriteManager_.Draw(arrow, index, screenPosition);
}
}
if (DisplayEnd_) {
RenderManager_.SetLayer(NGame::TRenderManager::Interface);
App_->FontManager().Draw(NGame::TFontManager::Red, {}, "You are dead! Press X");
App_->FontManager().Draw(NGame::TFontManager::Red, {4, 4}, "You are dead! Press X");
}
if (FadeAmount_) {
RenderManager_.SetLayer(NGame::TRenderManager::Interface);
if (WhiteFade_) {
RenderManager_.SetColor(255, 255, 255, FadeAmount_);
} else {
@@ -1174,6 +1216,9 @@ void THero::Draw() const {
void THero::Alarm(NGame::TAlarm::TId id) {
if (id == 0) {
AlternateRun_ = !AlternateRun_;
if (AlternateRun_ && Speed_.X && State_ == EState::Normal) {
App_->AudioManager().Play("Audio/Walk.wav");
}
} else if (id == 1) {
DisplayEnd_ = true;
}
@@ -1268,6 +1313,14 @@ void TMainMenu::Input(SDL_Event* event) {
switch (event->type) {
case SDL_KEYDOWN:
if (event->key.keysym.sym == 'x') {
App_->AudioManager().Play("Audio/Pickup.wav");
App_->State().Variable("Time").SetDouble(60000);
App_->State().Variable("FallDamage").SetBool(false);
App_->State().Variable("InstantMines").SetBool(false);
App_->State().Variable("LimitedGrenades").SetBool(false);
App_->State().Variable("DarkLevel").SetBool(false);
App_->State().Variable("NoCompas").SetBool(false);
App_->State().Variable("Grenades").SetInt(3);
App_->EntityManager().AddToDeferred("RoomEntity");
App_->EntityManager().Reset();
}
@@ -1294,12 +1347,51 @@ TEulaMenu::TEulaMenu(NGame::TEntity::TId id)
, RenderManager_(App_->RenderManager())
, SpriteManager_(App_->SpriteManager()) {
RenderManager_.EnableLight(false);
std::vector<std::string> toDisableTo;
if (!App_->State().Variable("FallDamage").Bool()) {
toDisableTo.push_back("FallDamage");
}
if (!App_->State().Variable("InstantMines").Bool()) {
toDisableTo.push_back("InstantMines");
}
if (!App_->State().Variable("LimitedGrenades").Bool()) {
toDisableTo.push_back("LimitedGrenades");
}
if (!App_->State().Variable("DarkLevel").Bool()) {
toDisableTo.push_back("DarkLevel");
}
if (!App_->State().Variable("NoCompas").Bool()) {
toDisableTo.push_back("NoCompas");
}
if (!toDisableTo.empty()) {
Choice_ = toDisableTo[rand() % toDisableTo.size()];
}
}
void TEulaMenu::Input(SDL_Event* event) {
switch (event->type) {
case SDL_KEYDOWN:
if (event->key.keysym.sym == 'x') {
if (!Choice_.empty()) {
App_->State().Variable(Choice_).SetBool(true);
if (!App_->State().Variable("LimitedGrenades").Bool()) {
App_->State().Variable("Grenades").SetInt(3);
}
App_->AudioManager().Play("Audio/PowerUp.wav");
App_->State().Variable("Time").SetDouble(App_->State().Variable("Time").Double() + 30000);
App_->EntityManager().AddToDeferred("RoomEntity");
App_->EntityManager().Reset();
}
}
else if (event->key.keysym.sym == 'c') {
if (!App_->State().Variable("LimitedGrenades").Bool()) {
App_->State().Variable("Grenades").SetInt(3);
}
App_->AudioManager().Play("Audio/Pickup.wav");
App_->EntityManager().AddToDeferred("RoomEntity");
App_->EntityManager().Reset();
}
@@ -1313,11 +1405,33 @@ void TEulaMenu::Update(std::uint32_t delta) {
void TEulaMenu::Draw() const {
RenderManager_.SetLayer(NGame::TRenderManager::Interface);
App_->FontManager().Draw(NGame::TFontManager::Gold, {}, "Press X to gain debuff and continue");
NGame::Vec2i position = {50, 50};
std::string text;
if (Choice_.empty()) {
text = "There is nothing that you can do";
text += "\n\nPress C to ingore";
App_->FontManager().Draw(NGame::TFontManager::Red, position, text);
} else {
if (Choice_ == "FallDamage") {
text = "Gain 30 seconds, but there will be\nfall damage";
} else if (Choice_ == "InstantMines") {
text = "Gain 30 seconds, but mines will explode\ninstantly";
} else if (Choice_ == "LimitedGrenades") {
text = "Gain 30 seconds, but grenades will\nno longer replenish";
} else if (Choice_ == "DarkLevel") {
text = "Gain 30 seconds, but the cave is now\nshrouded in darkness";
} else if (Choice_ == "NoCompas") {
text = "Gain 30 seconds, but you lose your compass";
}
text += "\n\nPress X to accept or C to ingore";
App_->FontManager().Draw(NGame::TFontManager::Gold, position, text);
}
}
void TEulaMenu::Alarm(NGame::TAlarm::TId id) {
}
TIntroMenu::TIntroMenu(NGame::TEntity::TId id)
@@ -1332,6 +1446,7 @@ void TIntroMenu::Input(SDL_Event* event) {
switch (event->type) {
case SDL_KEYDOWN:
if (event->key.keysym.sym == 'x') {
App_->AudioManager().Play("Audio/Pickup.wav");
App_->EntityManager().AddToDeferred("MainMenu");
App_->EntityManager().Reset();
}
@@ -1419,7 +1534,7 @@ TGrenadeEntity::TGrenadeEntity(NGame::TEntity::TId id)
, SpriteManager_(App_->SpriteManager()) {
SetSize(9);
Sprite_ = NGame::TApp::Instance()->SpriteManager().Get("Sprites/Grenade.txt");
Alarm_.Set(0, 3000 + rand() % 1000);
Alarm_.Set(0, 1000 + rand() % 1000);
Alarm_.Set(1, 100);
}
+3
View File
@@ -319,6 +319,8 @@ private:
bool DisplayEnd_ = false;
bool WhiteFade_ = false;
std::size_t FadeAmount_ = 0;
double RemainingTime_ = 0;
int Grenades_ = 0;
};
class TBackgroundTiler : public NGame::TEntity {
@@ -385,6 +387,7 @@ private:
NGame::TApp* App_;
NGame::TSpriteManager& SpriteManager_;
NGame::TRenderManager& RenderManager_;
std::string Choice_;
};
class TIntroMenu : public NGame::TEntity {