Частина 17 - Музика і звук

Цей розділ познайомить вас із завантаженням музики та звуків, оскільки це ключові особливості кожної гри та багатьох додатків.

SDL_mixer 2.0 для легкої підтримки музики та звуку

Хоча SDL 2.0 підтримує обробку музики та звуку, існує простіший спосіб відтворення музики та звукових файлів. Офіційний модуль SDL_mixer 2.0 (назва модуля SDL2_mixer) створений саме для цієї мети і підтримується тими ж авторами (Sam Lantinga, Stephane Peter, Ryan Gordon), що і сам SDL 2.0. Трансляція на Паскаль, на щастя, доступна і в заголовках Тіма Блюма.

Підтримувані формати музики та звукових файлів у SDL 2.0

Відповідно до офіційної документації SDL2_mixer підтримуються наступні формати музики та звуків:

  • WAVE/RIFF (.wav)
  • AIFF (.aiff)
  • VOC (.voc)
  • MOD (.mod .xm .s3m .669 .it .med і більше) потребують в системі libmikmod
  • MIDI (.mid) використовується timidity або рідне апаратне забезпечення midi
  • OggVorbis (.ogg) потребує в системі бібліотеки ogg/vorbis
  • MP3 (.mp3) потребує в системі бібліотеки SMPEG або MAD
  • FLAC (.flac) потребує в системі бібліотеку FLAC

Вам потрібні наступні файли:

Програмне забезпечення
Версія
Ім'я файла
Посилання
Опис
Динамічна бібліотека SDL2_mixer
2.0.4
SDL2_mixer-2.0.4-win32-x86.zip (32-bit Windows)
SDL2_mixer-2.0.4-win32-x64.zip (64-bit Windows)
https://www.libsdl.org/projects/SDL_mixer/
Це відповідний файл бібліотеки динамічного підключення для пристрою для Windows. Примітка: Також доступні версії Mac OS X та Linux.
звуковий файл dial.wav dial.wav
dial.wav
Простий звук телефонного набору. Джерело: pdsounds. Ліцензія: Суспільне надбання.
Музичний файл In my mind.ogg In my mind.ogg
In my mind.ogg
Гарний музичний зразок. Джерело/Творець: First. Ліцензія: CC BY-ND 3.0.

Ви повинні розпакувати zip-файл і отримати з нього кілька файлів. Це чотири текстові файли ліцензії та текстовий файл readme, крім того, у вас є вісім файлів DLL, включаючи важливий SDL2_mixer.dll. Інші файли dll необхідні для відтворення різних форматів звуку та музики. Скопіюйте всі файли в папку Windows System32 (або відповідне місце). Якщо Ви забудете це і запустите приклади нижче, то отримаєте повідомлення про помилку з exitcode = 309.

Тепер, коли ви готові, давайте розглянемо код.

program chap9_SDL2;
uses SDL2, SDL2_mixer;

var
sdlWindow1: PSDL_Window;
sdlEvent: PSDL_Event;
music: PMix_Music;
sound: PMix_Chunk;
exitloop: boolean = false;

begin
  if SDL_Init( SDL_INIT_VIDEO or SDL_INIT_AUDIO ) < 0 then HALT;

  //отримати вікно
  sdlWindow1 := SDL_CreateWindow( 'Music and Sound window', 50, 50, 500, 500, SDL_WINDOW_SHOWN );
  if sdlWindow1 = nil then HALT;

  //підготувати міксер
  if Mix_OpenAudio( MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT,
                    MIX_DEFAULT_CHANNELS, 4096 ) < 0 then HALT;

  //завантажити музику
  music := Mix_LoadMUS( 'In my mind.ogg' );
  if music = nil then HALT;
  Mix_VolumeMusic( MIX_MAX_VOLUME );

  //завантажити звук
  sound := Mix_LoadWAV( 'dial.wav' );
  if sound = nil then HALT;
  Mix_VolumeChunk( sound, MIX_MAX_VOLUME );

  //створити меню
  writeln( '(1) Play music once' );
  writeln( '(2) Pause music' );
  writeln( '(3) Resume music' );
  writeln( '(4) Rewind music' );
  writeln( '(5) Play sound once on channel 1' );
  writeln( '(6) Play sound once on free channel' );
  writeln( '(7) Pause sound on all channels' );
  writeln( '(8) Resume sound on all channels' );
  writeln( '(A) Fade in music within 3 seconds' );
  writeln( '(S) Fade out music within 3 seconds from now' );
  writeln( '(D) Fade in sound on channel 1 within 2 seconds' );
  writeln( '(F) Fade out sound on channel 1 within 1 second from now' );
  writeln;
  writeln( '=== Effects on channel 1 ===' );
  writeln( '(G) Panning: left: 255, right: 32' );
  writeln( '(H) Stop panning' );
  writeln( '(J) Distance: very far ');
  writeln( '(K) Unregister distance effect' );
  writeln( '(L) Position 45ø to the right, in front, middle distance' );
  writeln( '(M) Unregister position effect' );
  writeln;
  writeln( '(ESC) Exit program' );

  new( sdlEvent );

  while exitloop = false do
  begin
    while SDL_PollEvent( sdlEvent ) = 1 do
    begin

      case sdlEvent^.type_ of
        SDL_KEYDOWN: begin
                       case sdlEvent^.key.keysym.sym of
                         SDLK_1: if Mix_PlayMusic( music, 0 ) < 0 then HALT;
                         SDLK_2: Mix_PauseMusic;
                         SDLK_3: Mix_ResumeMusic;
                         SDLK_4: Mix_RewindMusic;
                         SDLK_5: if Mix_PlayChannel( 1, sound, 0 ) < 0 then HALT;
                         SDLK_6: if Mix_PlayChannel( -1, sound, 0 ) < 0 then HALT;
                         SDLK_7: Mix_Pause( -1 );
                         SDLK_8: Mix_Resume( -1 );
                         SDLK_A: if Mix_FadeInMusic( music, 1, 3000 ) < 0 then HALT;
                         SDLK_S: if Mix_FadeOutMusic( 3000 ) = 0 then HALT;
                         SDLK_D: if Mix_FadeInChannel( 1, sound, 0, 2000 ) < 0 then HALT;
                         SDLK_F: if Mix_FadeOutChannel( 1, 1000 ) < 0 then HALT;
                         SDLK_G: if Mix_SetPanning( 1, 255, 32 ) = 0 then HALT;
                         SDLK_H: if Mix_SetPanning( 1, 255, 255 ) = 0 then HALT; //скасувати панорамування
                         SDLK_J: if Mix_SetDistance( 1, 223 ) = 0 then HALT;
                         SDLK_K: if Mix_SetDistance( 1, 0 ) = 0 then HALT; //скасувати ефект дистанції
                         SDLK_L: if Mix_SetPosition( 1, 45, 127 ) = 0 then HALT;
                         SDLK_M: if Mix_SetPosition( 1, 0, 0 ) = 0 then HALT; //скасувати ефект позиції
                         SDLK_ESCAPE: exitloop := true;
                       end;
                     end;
      end;

    end;
    SDL_Delay( 5 );
  end;

  //очистка
  dispose( sdlEvent );
  SDL_DestroyWindow( sdlWindow1 );

  Mix_FreeMusic( music );
  Mix_FreeChunk( sound );

  Mix_CloseAudio;
  SDL_Quit;
end.

Запустивши цю програму, Ви отримаєте два вікна. Просте вікно SDL 2.0 без будь-якого вмісту (білого кольору) та заголовка “Music and Sound window” і вікно консолі, що відображає просте меню (список) можливих дій, щоб погратись з музикою та звуком. Майте на увазі, що фокус повинен бути зосереджений на вікні SDL 2.0, щоб працювало розпізнавання клавіш.

Chapter 9 application preview

Почнемо з першої частини коду.

program chap9_SDL2;
uses SDL2, SDL2_mixer;

var
sdlWindow1: PSDL_Window;
sdlEvent: PSDL_Event;
music: PMix_Music;
sound: PMix_Chunk;
exitloop: boolean = false;

begin
  if SDL_Init( SDL_INIT_VIDEO or SDL_INIT_AUDIO ) < 0 then HALT;

  //отримати вікно
  sdlWindow1 := SDL_CreateWindow( 'Music and Sound window', 50, 50, 500, 500, SDL_WINDOW_SHOWN );
  if sdlWindow1 = nil then HALT;

Програма називається “chap9_SDL2”. Вона використовує SDL2 і новий модуль SDL2_mixer.

Ми будемо використовувати обробку подій, як було обговорено в Частині 14 - Обробка подій. §1, обробка клавіатури щоб перевірити яка клавіша була натиснута. Отже, нам потрібна змінна “sdlWindow1” для вікна програми SDL 2.0 (для фокусу) та змінна події “sdlEvent”.

Змінна-вказівник “music” нового типу PMix_Music і пізніше вказує на завантажені музичні дані. Змінна-вказівник “sound” типу PMix_Chunk і пізніше вказує на завантажені звукові дані. Примітка: На кожну пісню посилається власний вказівник PMix_Music і кожен звуковий ефект (напр. вибухи, постріли, …) посилається на власний вказівник PMix_Chunk.

У чому різниця між музикою та звуком?

Музика асоціюється з вказівниками PMix_Music і звуки асоціюються з вказівниками PMix_Chunk, але навіщо їх взагалі розрізняти? Що ж, їх зручно розрізняти, оскільки вони мають досить різні властивості. Музика зазвичай має тривалість декілька хвилин, звуки зазвичай тривають декілька секунд. Зазвичай одночасно грає лише одна пісня, звуки за потреби потрібно змішувати. Для музики це, як правило, не відіграє ролі, якщо відбувається затримка перед початком відтворення, для звуків більша затримка зазвичай означає дивне відчуття для гравця. Тож, для музики існує лише один канал зарезервований для відтворення музики. Для звуків доступно вісім звукових каналів щоб зробити можливим їх змішування. А тепер давайте повернемось до коду.

Змінна “exitloop” логічна змінна щоб визначити коли відбудеться вихід з основного циклу програми.

Програма інізіалізуується функцією SDL_Init, як вже відомо. Зауважте, що встановлено не лише SDL_INIT_VIDEO, але додатково SDL_INIT_AUDIO. Вони поєднуються за допомогою оператора or (не оператором and :-)!). Після цього вікно встановлюється SDL_CreateWindow, як відомо.

  //підготуємо міксер
  if Mix_OpenAudio( MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT,
                    MIX_DEFAULT_CHANNELS, 4096 ) < 0 then HALT;

  //завантажити музику
  music := Mix_LoadMUS( 'In my mind.ogg' );
  if music = nil then HALT;
  Mix_VolumeMusic( MIX_MAX_VOLUME );

  //завантажити звук
  sound := Mix_LoadWAV( 'dial.wav' );
  if sound = nil then HALT;
  Mix_VolumeChunk( sound, MIX_MAX_VOLUME );

Для ініціалізації SDL2_mixer викликається

function Mix_OpenAudio(frequency: Integer; format: UInt16; channels: Integer; chunksize: Integer): Integer

Вона потребує чотири параметри. Це частота, формат аудіо, канали (mono чи audio) і розмір блока. Всі ці параметри цілого типу. Функція Mix_OpenAudio повертає 0 при успіху і -1 при помилці.

Частота в Герцах (1/s) в іграх зазвичай 22050 Hz або MIX_DEFAULT_FREQUENCY, але в двічі більша для звуку якості CD (44100 Hz). Чим вища частота, тим більше навантаження на процесор.

Далі ми повинні визначити аудіоформат. Він визначає спосіб збереження аудіоданих. MIX_DEFAULT_FORMAT відповідає AUDIO_S16SYS. У наступному списку наведені різні значення (з офіційної документації по SDL2_mixer ):

  • Ім'я константи форматуОпис
  • AUDIO_U8 Беззнакові 8-бітні семпли
  • AUDIO_S8 Знакові 8-бітні семпли
  • AUDIO_U16LSB Беззнакові 16-бітні семпли, з порядком байт від молодшого до старшого
  • AUDIO_S16LSB Знакові 16-бітні семпли, з порядком байт від молодшого до старшого
  • AUDIO_U16MSB Беззнакові 16-бітні семпли, з порядком байт від старшого до молодшого
  • AUDIO_S16MSB Знакові 16-бітні семпли, з порядком байт від старшого до молодшого
  • AUDIO_U16 те ж саме, що й AUDIO_U16LSB (ймовірно для оберненої сумісності)
  • AUDIO_S16 теж саме, що й AUDIO_S16LSB (ймовірно для оберненої сумісності)
  • AUDIO_U16SYS Беззнакові 16-бітні семпли, з системним порядком байт
  • AUDIO_S16SYS Знакові 16-бітні семпли, з системним порядком байт

Тоді ми вибираємо тип звукового каналу, що означає або стерео, або моно. Для стереозвуку ми вибираємо 2, а для моно - 1. MIX_DEFAULT_CHANNELS дорівнює стереовиходу.

Нарешті, має бути встановлено chunksize, де 4096 байт на семпл є хорошим і типовим значенням. Занадто низькі значення можуть призвести до пропуску семплів. Занадто високі значення можуть призвести до затримки відтворення.

Завантаження музики та звуків із файлів у SDL 2.0

Ми завантажимо музику за допомогою

function Mix_LoadMUS(_file: PAnsiChar): PMix_Music.

_file може бути абсолютним шляхом. Якщо файл Pascal і музичний файл знаходяться в одній папці, достатньо назви файлу (як показано в прикладі). Якщо завантаження не вдалося, функція повертає nil. Гучність можна встановити за допомогою

function Mix_VolumeMusic(volume: Integer): Integer.

Гучність може бути встановлена в межах від 0 (тиша) до 128 (максимальна гучність). Останнє дорівнює MIX_MAX_VOLUME. До речі, якщо Ви встановите -1 як аргумент, повернене значення відповідає встановленій гучності.

Звукові файли завантажуються досить подібно за допомогою

function Mix_LoadWAV(_file: PAnsiChar): PMix_Chunk.

Хоча назва функції не вказує на це, використовуйте цю функцію для завантаження звукових файлів будь-якого формату (Wave, Aiff, Riff, Ogg, Voc), а не лише для звукових файлів .wav. При помилці повертає nil. Існує незначна різниця при встановленні гучності за допомогою

function Mix_VolumeChunk(chunk: PMix_Chunk; volume: Integer): Integer.

Замість того, щоб просто встановити загальну гучність, гучність прив’язана до певного звуку. У цьому прикладі звук має змінну “sound”. Просто щоб це згадати тут, є третя можливість, встановити гучність для певного каналу:

function Mix_Volume(channel: Integer; volume: Integer): Integer.

Це може бути дуже зручно, в будь-якому випадку, канали обговорюються трохи пізніше нижче. Якщо Ваш аргумент дорівнює -1, буде повернено середню гучність.

  //створити меню
  writeln( '(1) Play music once' );
  writeln( '(2) Pause music' );
  writeln( '(3) Resume music' );
  writeln( '(4) Rewind music' );
  writeln( '(5) Play sound once on channel 1' );
  writeln( '(6) Play sound once on free channel' );
  writeln( '(7) Pause sound on all channels' );
  writeln( '(8) Resume sound on all channels' );
  writeln( '(A) Fade in music within 3 seconds' );
  writeln( '(S) Fade out music within 3 seconds from now' );
  writeln( '(D) Fade in sound on channel 1 within 2 seconds' );
  writeln( '(F) Fade out sound on channel 1 within 1 second from now' );
  writeln;
  writeln( '=== Effects on channel 1 ===' );
  writeln( '(G) Panning: left: 255, right: 32' );
  writeln( '(H) Stop panning' );
  writeln( '(J) Distance: very far ');
  writeln( '(K) Unregister distance effect' );
  writeln( '(L) Position 45ø to the right, in front, middle distance' );
  writeln( '(M) Unregister position effect' );
  writeln;
  writeln( '(ESC) Exit program' );

  new( sdlEvent );

Тут немає нічого нового. Виводиться меню і виділяється пам'ять під вказівник для обробки подій. Давайте продовжимо.

Відтворення, призупинення, відновлення музики та звуків

  while exitloop = false do
  begin
    while SDL_PollEvent( sdlEvent ) = 1 do
    begin

      case sdlEvent^.type_ of
        SDL_KEYDOWN: begin
                       case sdlEvent^.key.keysym.sym of
                         SDLK_1: if Mix_PlayMusic( music, 0 ) < 0 then HALT;
                         SDLK_2: Mix_PauseMusic;
                         SDLK_3: Mix_ResumeMusic;
                         SDLK_4: Mix_RewindMusic;
                         SDLK_5: if Mix_PlayChannel( 1, sound, 0 ) < 0 then HALT;
                         SDLK_6: if Mix_PlayChannel( -1, sound, 0 ) < 0 then HALT;
                         SDLK_7: Mix_Pause( -1 );
                         SDLK_8: Mix_Resume( -1 );

Запускається основний цикл програми, який слід залишити, якщо змінна “exitloop” є істинною.

Як було відомо з попередньої Частини 14 - Обробка подій. §.1, обробка клавіатури, ми спочатку опитуємо події функцією SDL_PollEvent() і входимо в подальший цикл. Друге, якщо знайдено подію яка чекає в черзі на обробку, ми перевіряємо виразом case яка в дійсності була натиснута клавіша.

Для кожної клавіші щоб щось виконати використовується відповідна функція залежна від звуку чи музики. Давайте обговоримо їх більш детально. Ми починаємо з функцій, що використовуються для клавіш 1-4:

function Mix_PlayMusic(music: PMix_Music; loops: Integer): Integer

procedure Mix_PauseMusic

procedure Mix_ResumeMusic

procedure Mix_RewindMusic

Mix_PlayMusic відтворює музику. Перший аргумент це безпосередньо музика, яку ми попередньо отримали з музичного файлу зразка формату .ogg і прив'язали з вказівником “music” типу PMix_Music. Другий аргумент визначає скільки музика буде відтворюватись. Додатково -1 означає безкінечне відтворення. Ця функція повертає 0 при успішному виконанні і -1 при помилці.

Значення трьох процедур Mix_PauseMusic, Mix_ResumeMusic і Mix_RewindMusic виходить з їхніх назв. Вони використовуються для призупинення (паузи), відновлення (якщо попередньо була пауза) і відтворення спочатку музики, що відтворюється.

Тепер показані аналогічні функції і процедури для звукових каналів:

function Mix_PlayChannel(channel: Integer; chunk: PMix_Chunk; loops: Integer): Integer

procedure Mix_Pause(channel: Integer)

procedure Mix_Resume(channel: Integer)

Як бачите, функція Mix_PlayChannel має три параметри. Перший параметр запитує номер каналу, з яким буде асоційовано звук. SDL2_mixer підтримує вісім звукових каналів. Зазвичай Ви можете використовувати -1, що означає використовувати перший вільний канал для відтворення звуку. В будь-якому випадку, Ви можете тут вказати конкретний канал, якщо захочете. Другий параметр запитує звуковий вказівник типу PMix_Chunk. Третій параметр такий самий, як і для музики, -1 означає безкінечний цикл. Будьте уважні на осбливості повторень. Значення кількості повторень збільшується на 1. Тож, якщо Ви хочте, щоб звук відтворився один раз, потрібно вказати 0. Значення, що повертається, відповідає звуковому каналу, на якому відтворюється звук, або -1 при помилці.

Процедури Mix_Pause і Mix_Resume говорять самі за себе. Використовуйте їх для паузи або поновлення конкретного каналу, який передається як аргумент. Якщо аргумент має значення -1 (як в коді прикладу) Ви призупините або поновите відтворення всіх каналів. До речі, ви могли помітити, що аналога перемотування назад не існує, оскільки звук не перемотується.

Ефекти затухання та відстані

                         SDLK_A: if Mix_FadeInMusic( music, 1, 3000 ) < 0 then HALT;
                         SDLK_S: if Mix_FadeOutMusic( 3000 ) = 0 then HALT;
                         SDLK_D: if Mix_FadeInChannel( 1, sound, 0, 2000 ) < 0 then HALT;
                         SDLK_F: if Mix_FadeOutChannel( 1, 1000 ) < 0 then HALT;
                         SDLK_G: if Mix_SetPanning( 1, 255, 32 ) = 0 then HALT;
                         SDLK_H: if Mix_SetPanning( 1, 255, 255 ) = 0 then HALT; //скасувати  панорамування
                         SDLK_J: if Mix_SetDistance( 1, 223 ) = 0 then HALT;
                         SDLK_K: if Mix_SetDistance( 1, 0 ) = 0 then HALT; //скасувати ефект дистанції
                         SDLK_L: if Mix_SetPosition( 1, 45, 127 ) = 0 then HALT;
                         SDLK_M: if Mix_SetPosition( 1, 0, 0 ) = 0 then HALT; //скасувати ефект позиціонування
                         SDLK_ESCAPE: exitloop := true;
                       end;
                     end;
      end;

    end;
    SDL_Delay( 5 );
  end;

Важливо знати про такі ефекти, як затухання. Вони виконуються на льоту до ваших даних, тому чим більше ефектів ви використовуєте, тим більше обчислювальної потужності вони потребують. Важливо вивантажити ефекти, які вам більше не потрібні.

Це чотири функції затухання, що запускаються наступними клавішами (A-F):

function Mix_FadeInMusic(music: PMix_Music; loops: Integer; ms: Integer): Integer

function Mix_FadeOutMusic(ms: Integer): Integer

function Mix_FadeInChannel(channel: Integer; chunk: PMix_Chunk; loops: Integer; ms: Integer): Integer

function Mix_FadeOutChannel(which: Integer; ms: Integer): Integer

Функція Mix_FadeInMusic працює дуже схожим способом як Mix_PlayMusic вище, але є ще один параметр, який дозволяє поступово згасати в мілісекундах. 3000 мілісекунд дорівнює 3 секундам. Ця функція повертає 0 при успіху та -1 при помилці.

Mix_FadeOutMusic відтворює звук протягом заданого часу в мілісекундах у момент виклику функції (отже, натискання клавіші в прикладі програми). Увага, вона повертає 1 при успіху і 0 при помилці.

Дуже схоже працюють функції Mix_FadeInChannel і Mix_FadeOutChannel. Замість вказівника на музику, потрібно передати вказівник на звукові дані. Додатково вказується канал. До речі, тут також кількість разів відтворення звуку збільшується на 1, тому, якщо Ви хочете відтворити звук із застосуванням затухання, аргумент повинен бути 0. Обидві функції повертають 0 при успіху та -1 при помилці.

Функції, що запускаються клавішами G-M:

function Mix_SetPanning(channel: Integer; left: UInt8; right: UInt8): Integer

Для налаштування панорамування потрібен канал, до якого слід застосовувати панорамування, і значення гучності, які можуть варіюватися від 0 (тихо) до 255 (найгучніше). Для послідовного ефекту панорамування можна відняти ліве значення від правого (ліве: гучність, праве: 255-гучність). Щоб скасувати реєстрацію цього ефекту, слід встановити для обох значень значення 255, що рекомендується робити, якщо панорамування більше не потрібно. Функція повертає 0 при помилках.

function Mix_SetDistance(channel: Integer; distance: UInt8): Integer

Чим більша відстань, тим тихішим видається звук. Значення може коливатися від 0 (поблизу) до 255 (далеко). Якщо відстань встановлено на 0, ефект не реєструється. Функція повертає 0 при помилці.

function Mix_SetPosition(channel: Integer; angle: SInt16; distance: UInt8): Integer

Ця функція регулює гучність відповідно до кута та значення відстані, отже, вона поєднує в собі ефекти двох обговорених раніше функцій щодо панорамування та відстані. Значення відстані працює так само, як це було обговорено для Mix_SetDistance. Кут 0 (в градусах) означає перед собою, 90 означає прямо вправо, 180 означає безпосередньо ззаду, 270 означає прямо вліво.

Нарешті, якщо було натиснуто SDLK_ESCAPE, для змінної зупинки циклу “exitloop” встановлено значення true, що зупинить основний цикл.

  //очистка
  dispose( sdlEvent );
  SDL_DestroyWindow( sdlWindow1 );

  Mix_FreeMusic( music );
  Mix_FreeChunk( sound );

  Mix_CloseAudio;
  SDL_Quit;
end.

Усередині основного циклу використовується коротка затримка 5 мілісекунд, щоб запобігти запуску програми з порожнім циклом, коли не очікується жодної події.

Очистка означає звільнити sdlEvent, і використати процедури Mix_FreeMusic і Mix_FreeChunk щоб звільнити дані звуку і музики.

procedure Mix_FreeMusic(music: PMix_Music)

procedure Mix_FreeChunk(chunk: PMix_Chunk)

Аргументтами відповідно повинні бути вказівники на музику і звук.

SDL_mixer 2.0 повинен бути закритий за допомогою

procedure Mix_CloseAudio.

Нарешті, SDL 2.0 і всі програма завершуються як вже відомо.

← попередня частина | наступна частина →

Немає коментарів:

Дописати коментар