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 (Russia only) Сбербанк (только для РФ)
Direct transfer via phone number (Russia only) Перевод по номеру телефона (только для РФ)
USDT TRC20
Support via Cryptocurrency Поддержка криптовалютой