SDL3 Desktop: 2D Spatial Audio (MinGW 13.1) SDL3 для Desktop: Пространственный 2D-звук (MinGW 13.1)
A guide to configuring and implementing 2D spatial audio using the SDL3_mixer extension library with CMake and MinGW. Руководство по настройке и реализации пространственного 2D-звука с использованием библиотеки расширения SDL3_mixer, CMake и MinGW.
1. Project Structure and Assets 1. Структура проекта и ресурсы
Before creating the files, download the required asset: Перед созданием файлов скачайте необходимый ресурс:
- Download the WAV file: Picked Coin Echo 2.zip Скачайте WAV-файл: Picked Coin Echo 2.zip
- Note: This is a free file taken from this link. Примечание. Это бесплатный файл, который был взят по ссылке.
C Project: Create an empty folder named spatial-2d-audio-sdl3-mixer-c and set up the following hierarchy by creating new CMakeLists.txt and main.c files:
Проект на C: Создайте пустую папку с именем spatial-2d-audio-sdl3-mixer-c и подготовьте следующую иерархию, создав файлы CMakeLists.txt и main.c:
spatial-2d-audio-sdl3-mixer-c/
├── CMakeLists.txt
├── assets/audio/
│ └── Picked Coin Echo 2.wav
└── src/
└── main.c
C++ Project: Create an empty folder named spatial-2d-audio-sdl3-mixer-cpp and set up the following hierarchy by creating new CMakeLists.txt and main.cpp files:
Проект на C++: Создайте пустую папку с именем spatial-2d-audio-sdl3-mixer-cpp и подготовьте следующую иерархию, создав файлы CMakeLists.txt и main.cpp:
spatial-2d-audio-sdl3-mixer-cpp/
├── CMakeLists.txt
├── assets/audio/
│ └── Picked Coin Echo 2.wav
└── src/
└── main.cpp
2. CMake Configuration 2. Конфигурация CMake
C Project: Copy and paste the following code into the CMakeLists.txt file:
Проект на C: Скопируйте и вставьте следующее содержимое в файл CMakeLists.txt:
set(CMAKE_BUILD_TYPE "Debug")
cmake_minimum_required(VERSION 3.21)
project(mixer-sdl3-c)
set(SDL3_DIR "C:/libs/SDL3-devel-3.4.8-mingw/lib/cmake/SDL3")
set(SDL3_mixer_DIR "C:/libs/SDL3_mixer-devel-3.2.2-mingw/lib/cmake/SDL3_mixer")
find_package(SDL3 REQUIRED)
find_package(SDL3_mixer REQUIRED)
add_executable(app)
target_sources(app PRIVATE src/main.c)
# Copy the assets folder to the dist folder
if(EXISTS "${CMAKE_SOURCE_DIR}/assets")
add_custom_command(TARGET app POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/assets"
"$<TARGET_FILE_DIR:app>/assets"
COMMENT "Copying assets directory"
)
endif()
target_link_libraries(app PRIVATE SDL3_mixer::SDL3_mixer SDL3::SDL3)
target_link_options(app PRIVATE -mconsole)
C++ Project: Copy and paste the following code into the CMakeLists.txt file:
Проект на C++: Скопируйте и вставьте следующее содержимое в файл CMakeLists.txt:
set(CMAKE_BUILD_TYPE "Debug")
cmake_minimum_required(VERSION 3.21)
project(mixer-sdl3-cpp)
set(SDL3_DIR "C:/libs/SDL3-devel-3.4.8-mingw/lib/cmake/SDL3")
set(SDL3_mixer_DIR "C:/libs/SDL3_mixer-devel-3.2.2-mingw/lib/cmake/SDL3_mixer")
find_package(SDL3 REQUIRED)
find_package(SDL3_mixer REQUIRED)
add_executable(app)
target_sources(app PRIVATE src/main.cpp)
# Copy the assets folder to the dist folder
if(EXISTS "${CMAKE_SOURCE_DIR}/assets")
add_custom_command(TARGET app POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/assets"
"$<TARGET_FILE_DIR:app>/assets"
COMMENT "Copying assets directory"
)
endif()
target_link_libraries(app PRIVATE SDL3_mixer::SDL3_mixer SDL3::SDL3)
target_link_options(app PRIVATE -mconsole)
3. Source Code 3. Исходный код
Copy and paste the following code into the src/main.c (or src/main.cpp) file:
Скопируйте и вставьте следующее содержимое в файл src/main.c (или src/main.cpp):
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_mixer/SDL_mixer.h>
// Window dimensions
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
// Structures for game objects
typedef struct
{
float x, y;
float size;
float speed;
float angle; // Player's facing direction (in radians)
} Player;
typedef struct
{
float x, y;
float size;
float maxSoundDistance;
} SoundSource;
// Global variables (in a production project, it is better to wrap these into appstate)
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static MIX_Mixer *mixer = NULL;
static MIX_Audio *audio = NULL;
static MIX_Track *track = NULL;
static Player player;
static SoundSource coin;
// Keyboard state tracking
static bool keys[SDL_SCANCODE_COUNT] = { false };
// Function to update audio parameters based on 2D coordinates
void Update2DSound(MIX_Track *track, Player p, SoundSource src)
{
// Calculate distance and distance-based volume attenuation
float dx = src.x - p.x;
float dy = src.y - p.y;
float distance = sqrtf(dx * dx + dy * dy);
float volume = 0.0f;
if (distance < src.maxSoundDistance)
{
volume = 1.0f - (distance / src.maxSoundDistance);
}
// Prevent the track from entering a hard stop/sleep state.
// If volume drops to 0, we set it to a tiny value instead.
// This keeps the track active and looping in the background.
if (volume <= 0.0f)
{
volume = 0.0001f;
}
// Apply global track volume (gain)
MIX_SetTrackGain(track, volume);
// Before returning, reset the stereo panning to the center
// so the mixer handles the silent loop properly.
// Check against the safety cutoff value.
if (volume <= 0.0001f || distance < 0.1f)
{
MIX_StereoGains flatGains = { 1.0f, 1.0f };
MIX_SetTrackStereo(track, &flatGains);
return;
}
// Calculate left and right channel balance
float viewX = cosf(p.angle);
float viewY = sinf(p.angle);
float soundX = dx / distance;
float soundY = dy / distance;
// Cross product: < 0 (left side), > 0 (right side)
float cross = (viewX * soundY) - (viewY * soundX);
// Structure to hold stereo channel gain coefficients
MIX_StereoGains gains;
// Minimum volume threshold for the opposite ear (20%)
// This ensures the opposite channel never drops into complete silence
float minStereoLeak = 0.2f; // Sound leakage
// Note: The minStereoLeak value can be adjusted between
// 0.1f and 0.3f to find the most natural acoustic balance.
if (cross > 0.0f)
{
// Source is on the right: fade out the left ear but keep it above minStereoLeak,
// while the right ear stays at maximum volume (1.0)
// cross peaks at 1.0 when the object is strictly at a 90-degree angle to the right
gains.left = 1.0f - (cross * (1.0f - minStereoLeak));
gains.right = 1.0f;
}
else
{
// Source is on the left: fade out the right ear but keep it above minStereoLeak,
// while the left ear stays at maximum volume
// Since cross is negative, we add it to subtract the value correctly
gains.left = 1.0f;
gains.right = 1.0f + (cross * (1.0f - minStereoLeak));
}
// Clamp values to the [0.0, 1.0] range
if (gains.left < 0.0f)
gains.left = 0.0f;
if (gains.right < 0.0f)
gains.right = 0.0f;
// Pass the gains structure pointer to the SDL3 Mixer function
MIX_SetTrackStereo(track, &gains);
}
void DrawCircle(SDL_Renderer *r, float centerX, float centerY, float radius, int segments)
{
float angleStep = (2.0f * (float)M_PI) / (float)segments;
// Set the initial starting point (at angle = 0)
float prevX = centerX + radius;
float prevY = centerY;
for (int i = 1; i <= segments; i++)
{
float angle = (float)i * angleStep;
float nextX = centerX + cosf(angle) * radius;
float nextY = centerY + sinf(angle) * radius;
// Draw a line segment from the previous point to the current one
SDL_RenderLine(r, prevX, prevY, nextX, nextY);
prevX = nextX;
prevY = nextY;
}
}
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO))
{
return SDL_APP_FAILURE;
}
if (!MIX_Init())
{
return SDL_APP_FAILURE;
}
// Create the window and renderer in a single call using SDL3
if (!SDL_CreateWindowAndRenderer("SDL3 2D Spatial Audio", SCREEN_WIDTH, SCREEN_HEIGHT, 0, &window, &renderer))
{
return SDL_APP_FAILURE;
}
mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
if (!mixer)
{
return SDL_APP_FAILURE;
}
// Load a looping sound effect (e.g., ambient hum from an object or ticking clock)
audio = MIX_LoadAudio(mixer, "assets/audio/Picked Coin Echo 2.wav", true);
if (!audio)
{
SDL_Log("Load failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
track = MIX_CreateTrack(mixer);
if (!track)
return SDL_APP_FAILURE;
MIX_SetTrackAudio(track, audio);
MIX_SetTrackLoops(track, -1); // -1 specifies infinite looping
// Initialize player state
player.x = 150.0f;
player.y = 250.0f;
player.size = 20.0f;
player.speed = 150.0f; // Pixels per second
player.angle = -M_PI_2; // Facing straight up by default (-90 degrees)
// Initialize sound source state (located in the upper right section)
coin.x = 600.0f;
coin.y = 150.0f;
coin.size = 15.0f;
coin.maxSoundDistance = 400.0f; // Audio audibility radius
// Enforce an immediate spatial audio update matching initial positions
Update2DSound(track, player, coin);
// Start playing the audio track
MIX_PlayTrack(track, 0);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
// Process keyboard inputs
if (event->type == SDL_EVENT_KEY_DOWN)
{
keys[event->key.scancode] = true;
}
if (event->type == SDL_EVENT_KEY_UP)
{
keys[event->key.scancode] = false;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
// Retrieve delta_time directly in SDL3 to ensure smooth frame-rate independent updates
static Uint64 last_time = 0;
if (last_time == 0)
last_time = SDL_GetTicks();
Uint64 current_time = SDL_GetTicks();
float dt = (current_time - last_time) / 1000.0f;
last_time = current_time;
// --- 1. INPUT PROCESSING & MOVEMENT ---
// Movement translation (WASD and arrow keys)
if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP])
player.y -= player.speed * dt;
if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN])
player.y += player.speed * dt;
if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT])
player.x -= player.speed * dt;
if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT])
player.x += player.speed * dt;
// Head rotation / view orientation (Q and E keys)
// Allows testing spatial audio panning adjustments on rotation
if (keys[SDL_SCANCODE_Q])
player.angle -= 3.0f * dt; // Rotate counter-clockwise
if (keys[SDL_SCANCODE_E])
player.angle += 3.0f * dt; // Rotate clockwise
// Check if the short sound effect sample has finished playing.
// If it has stopped, restart it manually to create a seamless loop effect.
if (!MIX_TrackPlaying(track))
{
MIX_PlayTrack(track, 0);
}
// --- 2. AUDIO UPDATE ---
Update2DSound(track, player, coin);
// --- 3. RENDER PASS ---
// Clear the screen buffer with a flat black color
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// Draw the sound audibility boundary (a gray circle built from 64 segments)
// This provides a visual representation of the maxSoundDistance range
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
DrawCircle(renderer, coin.x, coin.y, coin.maxSoundDistance, 64);
// Draw the sound source (Green square)
SDL_FRect coinRect = { coin.x - coin.size / 2, coin.y - coin.size / 2, coin.size, coin.size };
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
SDL_RenderFillRect(renderer, &coinRect);
// Draw the Player (Blue square)
SDL_FRect playerRect = { player.x - player.size / 2, player.y - player.size / 2, player.size, player.size };
SDL_SetRenderDrawColor(renderer, 0, 100, 255, 255);
SDL_RenderFillRect(renderer, &playerRect);
// Draw the player's vector line of sight (Red line) to visualize facing direction
float lineLength = 25.0f;
float targetX = player.x + cosf(player.angle) * lineLength;
float targetY = player.y + sinf(player.angle) * lineLength;
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderLine(renderer, player.x, player.y, targetX, targetY);
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
MIX_Quit();
}#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_mixer/SDL_mixer.h>
// Размеры окна
#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600
// Структуры для игровых объектов
typedef struct
{
float x, y;
float size;
float speed;
float angle; // Куда смотрит игрок (в радианах)
} Player;
typedef struct
{
float x, y;
float size;
float maxSoundDistance;
} SoundSource;
// Глобальные переменные (в реальном проекте лучше упаковать в appstate)
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static MIX_Mixer *mixer = NULL;
static MIX_Audio *audio = NULL;
static MIX_Track *track = NULL;
static Player player;
static SoundSource coin;
// Состояние нажатых клавиш
static bool keys[SDL_SCANCODE_COUNT] = { false };
// Функция обновления звука на основе 2D координат
void Update2DSound(MIX_Track *track, Player p, SoundSource src)
{
// Расчет расстояния и громкости
float dx = src.x - p.x;
float dy = src.y - p.y;
float distance = sqrtf(dx * dx + dy * dy);
float volume = 0.0f;
if (distance < src.maxSoundDistance)
{
volume = 1.0f - (distance / src.maxSoundDistance);
}
// Защита от "засыпания" трека.
// Если volume равен 0, ставим ничтожно малое число.
// Трек продолжит крутиться в фоне.
if (volume <= 0.0f)
{
volume = 0.0001f;
}
// Общая громкость (усиление) трека
MIX_SetTrackGain(track, volume);
// Перед return сначала сбрасываем стерео-баланс в центр,
// чтобы микшер корректно обрабатывал «тишину» в цикле.
// Сравниваем со значением отсечки безопасности.
if (volume <= 0.0001f || distance < 0.1f)
{
MIX_StereoGains flatGains = { 1.0f, 1.0f };
MIX_SetTrackStereo(track, &flatGains);
return;
}
// Расчет баланса левого и правого каналов
float viewX = cosf(p.angle);
float viewY = sinf(p.angle);
float soundX = dx / distance;
float soundY = dy / distance;
// Косое произведение: < 0 (слева), > 0 (справа)
float cross = (viewX * soundY) - (viewY * soundX);
// Создаем структуру для хранения коэффициентов громкости каналов
MIX_StereoGains gains;
// Минимальный порог звука для противоположного уха (20%)
// Благодаря этому противоположное ухо никогда не замолкнет полностью
float minStereoLeak = 0.2f; // Утечка звука
// Замечание. Значение minStereoLeak можно крутить в пределах от
// 0.1f до 0.3f, подбирая наиболее приятный на слух баланс
if (cross > 0.0f)
{
// Источник справа: левое ухо затухает, но не ниже порога minStereoLeak,
// а правое горит на максимум (1.0)
// cross на максимуме равен 1.0 (когда объект строго справа под 90 градусов)
gains.left = 1.0f - (cross * (1.0f - minStereoLeak));
gains.right = 1.0f;
}
else
{
// Источник слева: правое ухо затухает, но не ниже порога minStereoLeak,
// а левое на максимум
// Поскольку cross отрицательный, используем fabsf() или + cross
gains.left = 1.0f;
gains.right = 1.0f + (cross * (1.0f - minStereoLeak));
}
// Защита от выхода за пределы [0.0, 1.0]
if (gains.left < 0.0f)
gains.left = 0.0f;
if (gains.right < 0.0f)
gains.right = 0.0f;
// Передаем указатель на структуру в функцию SDL3 Mixer
MIX_SetTrackStereo(track, &gains);
}
void DrawCircle(SDL_Renderer *r, float centerX, float centerY, float radius, int segments)
{
float angleStep = (2.0f * (float)M_PI) / (float)segments;
// Задаем начальную точку (при угле = 0)
float prevX = centerX + radius;
float prevY = centerY;
for (int i = 1; i <= segments; i++)
{
float angle = (float)i * angleStep;
float nextX = centerX + cosf(angle) * radius;
float nextY = centerY + sinf(angle) * radius;
// Соединяем предыдущую точку с текущей
SDL_RenderLine(r, prevX, prevY, nextX, nextY);
prevX = nextX;
prevY = nextY;
}
}
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO))
{
return SDL_APP_FAILURE;
}
if (!MIX_Init())
{
return SDL_APP_FAILURE;
}
// Создаем окно и рендерер одной функцией в SDL3
if (!SDL_CreateWindowAndRenderer("SDL3 2D Spatial Audio", SCREEN_WIDTH, SCREEN_HEIGHT, 0, &window, &renderer))
{
return SDL_APP_FAILURE;
}
mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
if (!mixer)
{
return SDL_APP_FAILURE;
}
// Загружаем зацикленный звук (например, фоновый гул от объекта или тиканье)
audio = MIX_LoadAudio(mixer, "assets/audio/Picked Coin Echo 2.wav", true);
if (!audio)
{
SDL_Log("Load failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
track = MIX_CreateTrack(mixer);
if (!track)
return SDL_APP_FAILURE;
MIX_SetTrackAudio(track, audio);
MIX_SetTrackLoops(track, -1); // -1 означает бесконечный повтор звука
// Инициализация игрока
player.x = 150.0f;
player.y = 250.0f;
player.size = 20.0f;
player.speed = 150.0f; // пикселей в секунду
player.angle = -M_PI_2; // смотрит вверх по умолчанию (-90 градусов)
// Инициализация источника звука (в правой верхней части)
coin.x = 600.0f;
coin.y = 150.0f;
coin.size = 15.0f;
coin.maxSoundDistance = 400.0f; // Радиус слышимости звука
// Принудительно обновляем 3D параметры звука под стартовые позиции
Update2DSound(track, player, coin);
// Запускаем трек
MIX_PlayTrack(track, 0);
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
if (event->type == SDL_EVENT_QUIT)
{
return SDL_APP_SUCCESS;
}
// Считываем нажатия клавиш
if (event->type == SDL_EVENT_KEY_DOWN)
{
keys[event->key.scancode] = true;
}
if (event->type == SDL_EVENT_KEY_UP)
{
keys[event->key.scancode] = false;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
// В SDL3 для плавной анимации мы получаем delta_time (время кадра) напрямую
static Uint64 last_time = 0;
if (last_time == 0)
last_time = SDL_GetTicks();
Uint64 current_time = SDL_GetTicks();
float dt = (current_time - last_time) / 1000.0f;
last_time = current_time;
// Обработка ввода и движение
// Движение (WASD и стрелки)
if (keys[SDL_SCANCODE_W] || keys[SDL_SCANCODE_UP])
player.y -= player.speed * dt;
if (keys[SDL_SCANCODE_S] || keys[SDL_SCANCODE_DOWN])
player.y += player.speed * dt;
if (keys[SDL_SCANCODE_A] || keys[SDL_SCANCODE_LEFT])
player.x -= player.speed * dt;
if (keys[SDL_SCANCODE_D] || keys[SDL_SCANCODE_RIGHT])
player.x += player.speed * dt;
// Вращение взгляда игрока (например, на клавиши Q и E)
// Чтобы вы могли проверить, как меняется панорама при повороте "головы"
if (keys[SDL_SCANCODE_Q])
player.angle -= 3.0f * dt; // поворот против часовой
if (keys[SDL_SCANCODE_E])
player.angle += 3.0f * dt; // поворот по часовой
// Проверяем, не закончился ли наш короткий сэмпл.
// Если он остановился — запускаем его заново, создавая эффект петли (loop)
if (!MIX_TrackPlaying(track))
{
MIX_PlayTrack(track, 0);
}
// Обновление звука
Update2DSound(track, player, coin);
// Рендеринг (отрисовка)
// Очищаем экран (черный цвет)
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// Рисуем радиус слышимости источника звука (серая окружность из 64 отрезков)
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
DrawCircle(renderer, coin.x, coin.y, coin.maxSoundDistance, 64);
// Рисуем источник звука (Зеленый квадрат)
SDL_FRect coinRect = { coin.x - coin.size / 2, coin.y - coin.size / 2, coin.size, coin.size };
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 255);
SDL_RenderFillRect(renderer, &coinRect);
// Рисуем Игрока (Синий квадрат)
SDL_FRect playerRect = { player.x - player.size / 2, player.y - player.size / 2, player.size, player.size };
SDL_SetRenderDrawColor(renderer, 0, 100, 255, 255);
SDL_RenderFillRect(renderer, &playerRect);
// Рисуем "линию взгляда" игрока (Красная линия), чтобы видеть куда он повернут
float lineLength = 25.0f;
float targetX = player.x + cosf(player.angle) * lineLength;
float targetY = player.y + sinf(player.angle) * lineLength;
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderLine(renderer, player.x, player.y, targetX, targetY);
SDL_RenderPresent(renderer);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
MIX_Quit();
}
4. Opening the Project in IDEs 4. Открытие проекта в IDE
Open the CMakeLists.txt file in CLion or Qt Creator. CMake will handle the rest. Откройте файл CMakeLists.txt в CLion или Qt Creator. CMake позаботится об остальном.
5. Automation Scripts (.bat) 5. Скрипты автоматизации (.bat)
You can open the project folder in Sublime Text 4 (or Notepad++). Create the following .bat scripts in the project root directory to automate the configuration, building, and running of your application:
Вы можете открыть папку проекта в Sublime Text 4 (или Notepad++). Создайте следующие .bat скрипты в корневой директории проекта для автоматизации конфигурации, сборки и запуска вашего приложения:
1. config-exe.bat
cmake -G "MinGW Makefiles" -S . -B dist/exe
2. build-exe.bat
cd dist\exe
cmake --build .
cd ..\..
3. run-exe.bat
dist\exe\app
To build and launch the application, run these scripts in the terminal in the following order:
Чтобы собрать и запустить приложение, выполните эти скрипты в терминале в следующем порядке:
config-exe
build-exe
run-exe
6. Source Code & Downloads 6. Исходный код и загрузки
You can download the complete configured project as a ZIP archive or explore the source code directly on GitHub: Вы можете скачать готовую настроенную сборку проекта в виде ZIP-архива или изучить исходный код прямо на GitHub:
Support My Work Поддержать проект
If these tutorials helped you, consider buying me a coffee! Если эти туториалы вам помогли, вы можете поддержать автора.
Sberbank
Direct transfer via phone number Перевод по номеру телефона
Bybit (USDT TRC20)
Support via Cryptocurrency Поддержка криптовалютой