Przeglądaj kategorię

Poradniki

Niestandardowe strumieniowanie audio

Trochę teorii


Na początku musimy sobie odpowiedzieć na pytanie czym jest strumieniowanie audio. Strumieniowanie audio (audio stream) jest podobne do muzyki (klasa sf::Music), posiada podobne funkcje, które zachowują się w ten sam sposób. Podstawową różnicą jest to że nie odtwarzamy tutaj pliku audio, lecz odtwarzamy audio bezpośrednio ze źródła, które odtwarzamy. Inaczej rzecz ujmując, możesz wtedy odtworzyć np: audio otrzymane z sieci (komunikator głowowy w twojej grze), muzykę wygenerowaną przez twój program, czy też format audio, którego SFML jeszcze nie wspiera.

Klasa sf::Music jest wyspecjalizowana w strumieniowaniu audio poprzez pobieranie małych fragmentów (samples) z pliku.

Z racji że mówimy o strumieniowaniu audio będziemy mówili o audio, które nie może być załadowane w całości do pamięci, lecz będziemy je wczytywali fragmentami i dopiero odtwarzali. Jeżeli twoje dźwięki mogą być załadowane  w całości do pamięci to strumieniowanie audio nie pomoże ci, skorzystaj wtedy z sf::SoundBuffer oraz odtwórz swój dźwięk za pomocą sf::Sound.

sf::SoundStream


Aby utworzyć swoją klasę do strumieniowania audio musisz dziedziczyć po klasie abstrakcyjnej sf::SoundStream. Posiada ona 2 wirtualne klasy, które musisz napisać: onGetData oraz onSeek.

Funkcja onGetData jest wywoływana za każdym razem gdy zostanie odtworzony obecnie pobrany fragment muzyki i będzie potrzebny kolejny. Przez tą funkcję musisz dostarczyć kolejnego fragmentu używając argumentu data.

Musisz zwrócić true jeżeli wszystko udało się wczytać, w przeciwnym razie (lub gdy nie ma więcej danych do odtworzenia) zwróć false, wtedy odtwarzanie audio zostanie zatrzymane.

SFML tworzy kopie sample’i w momencie gdy zostanie zwrócona prawda w onGetData, więc nie musisz trzymać tych danych, możesz spokojnie je usunąć.

Metoda onSeek jest wywoływana w momencie wywołania funkcji setPlayingOffset. Służy ona do zmiany wskaźnika odtwarzanych danych na odpowiedni. Jako argument przyjmuje czas utworu, który ma być odtworzony (czas jest podawany licząc od początku utworu nie od obecnej pozycji). Ta funkcja czasami jest niemożliwa do zaimplementowania, wtedy możesz zostawić ją pustą i powiedzieć użytkownikowi, że nie da się przewinąć utworu.

Twoja klasa jest już prawie gotowa do pracy. Klasa SoundStream potrzebuje jeszcze wiedzieć o numerze kanału (channel number) oraz o częstotliwości próbkowania (sample rate). Aby przekazać te parametry musisz użyć funkcji protected initialize najczęściej zaraz po wczytaniu/zainicjalizowaniu strumieniowania.

 

Kwestia wątkowania (thread)


Strumieniowanie audio zawsze odbywa się w oddzielnym wątku, jednak powinieneś wiedzieć co się tam dzieje i gdzie.

onSeek jest wywoływane bezpośrednio z funkcji setPlayingOffset więc jest zawsze wykonywana w wątku z którego została wezwana. Jednakże onGetData jest wzywane dopóki jest odtwarzane audio w oddzielny wątku stworzonym przez SFML. Więc jeżeli twój strumień audio używa danych może zostać przydzielony jednocześnie do obu wątków: tego w którym został uruchomiony oraz w wątku odtwarzającym, który musisz chronić za pomocą np. mutex’ów tak aby uniknąć równoczesnego dostępu do tych samych danych. W przeciwnym razie będzie mogło dojść do nieprzewidywalnych zachowań (zniszczenie odtwarzanych danych, crash, itd).

Jeżeli nie czujesz się pewnie w kategorii wątków zapraszam do poradnika o wątkach.

 

Użycie własnego strumieniowania audio


Skoro już posiadamy zdefiniowaną klasę, przyjrzyjmy się jak należy ją poprawnie użyć. Jest to bardzo proste, wręcz podobne do tego co pokazywałem w poradniku o sf::Music. Możesz kontrolować audio za pomocą: play, pause, stop i setPlayeingOffset. Oczywiście możesz je także odtworzyć z takimi właściwościami jak np. głośność. Możesz się także odnieść do dokumentacji API lub do innych poradników o audio.

 

Przykład


Poniżej mamy prosty przykład strumieniowania audio, gdzie jest odtwarzanie audio z bufora audio. Całość w sama sobie jest totalnie bezużyteczna, ale chodzi tutaj o pokazanie w jaki sposób dane (data) są strumieniowanie przez klasę, nie ważne z jakiego źródła.

Oryginalny artykuł

 

 

 

Nagrywanie dźwięku

Nagrywanie dźwięku do bufora


W większości przypadków do zapisywanie przechwytywanych dźwięków używa się klasy sf::SoundBuffer, która pozwala na odtwarzanie i zapisywanie dźwięków.

Jednak do samego nagrywania używa się sf::SoundBufferRecorder.

SoundBufferRecorder::isAvailable sprawdza czy nagrywanie jest możliwe na tym systemie, jeżeli zwróci false możesz sobie darować nagrywanie i nic na to nie poradzimy.

Nagrywanie odbywa się w oddzielnym wątku więc nie musisz kombinować aby nagrać cokolwiek, po prostu używasz play i zaczyna się nagrywanie niezależnie od tego co robi twoja gra.

Co możesz zrobić z nagranymi danymi?

– zapisać do pliku

– odtworzyć

– uzyskać dostęp do surowych danych, przekształcić je i cokolwiek chciałbyś z nimi zrobić

 

Nagrywanie niestandardowe


Jeżeli przechowywanie i zapisywanie przechwyconych dźwięków to dla ciebie za mało, to możesz utworzyć swoją własną nagrywarkę. W ten sposób będziesz mógł np streamować przechwycona audio do sieci itp.

Aby tego dokonać musisz utworzyć klasę, która dziedziczy po SoundRecorder. Posiada ona w sobie klasę wirtualną onProcessSamples podczas gdy każdy chunk został przechwycony, więc to jest miejsce gdzie musisz zaimplementować własne elementy.

Są także dwie opcjonalne metody do nadpisania: onStart onStop które są wywoływane podczas rozpoczęcia/zatrzymania nagrywania.

Poniżej masz przykładowy szkielet takiej klasy:

Funkcje isAvailable/start/stop zostały zdefiniowane w sf::SoundRecorder i są dziedziczone w każdej klasie potomnej tej klasy, co oznacza że ich użycie wygląda tak samo jak w klasie sf::SoundBufferRecorder.

Oryginalny artykuł

Odtwarzanie muzyki i dźwięków

sf::Sounds vs sf::Music


W SFML istnieją dwie klasy do odtwarzania audio: sf::Music oraz sf::Sound. Nie róźnią się one zbytnio w tym co oferują, ale bardziej sposobem w jaki działają.

sf::Sound jest lżejszym obiektem, który do odtwarzania audio potrzebuje sf::SoundBuffer. Ta klasa jest odtwarzana w całości w pamięci i powinna być używana do odtwarzania krótkich utworów audio (czyli dźwięków), czyli np. do odgłosów strzałów, chodzenia itp.

sf::Music nie jest od razu wczytywana w całości w pamięci, jest wczytywana w „locie” tzn. w trakcie działania programu w miarę potrzeb. Jest obiektem cięższym oraz może odtwarzać dłuższe utwory, które trwają dużo dłużej niż krótkie dźwięki.

 

Wczytywanie i odtwarzanie dźwięków


Tak jak wspomniałem powyżej nie można odtworzyć dźwięków bezpośrednio z sf::Sound tylko trzeba je odpowiednio przygotować, czyli musimy je na początku wczytać przez sf::SoundBuffer.

Oczywiście można także do wczytania skorzystać z loadFromMemory, czy też własnego strumienia danych (loadFromMemory). Pełna lista wspieranych rozszerzeń plików audio jest dostępna w dokumentacji API.

Można także wczytać dźwięk z tzw. sample’i, jednak tego tutaj nie pokażę (zapraszam do oryginalnego poradnika).

Aby odtworzyć audio należy należy użyć klasy Sound.

Co jest fajnego to fakt, że możesz przypisać do jednego bufora wiele dźwięków i odtworzyć je jednocześnie. Co jest ważne dźwięki oraz muzyka są odtwarzane w osobnym wątku, co oznacza że po uruchomieniu play możesz robić co chcesz (nie licząc usunięcia muzyki).

 

Odtwarzanie muzyki


W przeciwieństwie do dźwięków sf::Music nie potrzebuje klasy pomocniczej do odtworzenia audio, tzn otwieranie pliku i jego odtworzenie jest robione przez tą jedną klasę.

Nie bez powodu funkcja nazywa się openFromFile, a nie loadFromFile, jest to dlatego że plik jest wczytywany na bieżąco, a nie kopiowany w całości do pamięci. Można także użyć: openFromMemory oraz openFromStream.

 

Co jeszcze można robić?


Poniżej pokazane funkcje wyglądają tak samo dla Sound jak i Music.

  • play uruchamia lub kontynuuje odtwarzanie audio
  • pause wstrzymuje odtwarzanie muzyki
  • stop zatrzymuje odtwarzanie dźwięku oraz przewija go
  • setPlayingOffset zmienia obecnie odtwarzaną pozycję

getStatus zwraca nam obecny status, czyli mówi nam czy audio jest w trakcie odtwarzania, spauzowane itp. Muzyka oraz dźwięki posiadają kilka artrybutów które można zmieniać w dowolnej chwili:

Można ustawić pitch, które wpływa na ton dźwięku a co za tym idzie na prędkość jego odtwarzania. Ustawiamy tutaj faktor, który gdy jest > 1 to sprawia, że dźwięk jest niższy, < 1 wyższy, == 1 normalny.

Można ustawić także volume, czyli głośność dźwięku w przedziale [0,100].

loop decyduje o tym czy utwór ma być zapętlany, czyli odtwarzany w nieskończoność.

Jest dostępnych także więcej atrybutów, jednak o nich porozmawiamy w poradniku o dźwiękach 3D.

 

Najczęściej spotykane błędy


Zniszczenie bufora

Jest wtedy gdy deklarujesz go w sposób podobny jak poniżej, pamiętaj że musi on być cały czas dostępny w pamięci programu gdy go używasz.

 

Za dużo dźwięków

SFML posiada limit ilości dźwięków, czyli 256, jeżeli chcesz używać ich więcej to musisz pozbyć się z pamięci tych których nie używasz i wczytać jedynie te których potrzebujesz.

 

Usunięcie źródła muzyki z pamięci

Pamiętaj, że podczas odtwarzania muzyki plik cały czas musi być dostępny. Zwykle w przypadku gdy jest na dysku nie ma z tym problemu, al może się zdarzyć, że otworzysz go np  z pamięci, wtedy łatwo przez przypadek go usunąć.

 

sf::Music nie można kopiować

Czyli nie można zrobić czegoś takiego:

Oryginalny artykuł

 

Kontrolowanie kamery przy użyciu View

Czym jest view?


View jak nazwa wskazuje jest podglądem poziomu, a konkretnie jakiegoś mniejszego obszaru. Stosuje się go w grach gdzie poziom gry jest większy niż ekran, dzięki czemu możemy decydować, który kawałek poziomu ma być wyświetlany.

W SFML istnieją 2 podstawowe założenia odnośnie view: możesz decydować jaki kawałek poziomu będzie wyświetlany, a także gdzie jest wyświetlany oraz w jaki sposób (czy jest powiększony itd).

Podsumowując view pozwoli ci powiększać, obracać i przewijać poziom. Jest on także przydatny przy robieniu split screenów oraz mini mapach.

 

Definiowanie podglądu view


Klasa służąca do podglądu w SFML została zhermetyzowana w sf::View. Możemy jeszcze w konstruktorze zdecydować co ma wyświetlać:

Obie definicje powyżej tworzą podgląd na obszar mapy o środku na pozycji (350,300) oraz wielkości 300×200.

Możesz także edytować je poza konstruktorem:

Teraz kiedy zdefiniowałeś już swój podgląd (view) to możesz go transformować w dowolny sposób.

 

Poruszanie podglądem (przewijanie/scrolling)

W przeciwieństwie to obiektów Drawable, których pozycja standardowo była umieszczona w lewym górnym rogu to w klasie View umieszczana jest ona zawsze w centrum obiektu. To jest także powodem dla którego ustawianie pozycji w tej klasie brzmi setCenter, a nie setPosition.

Obracanie view

Czyli przechylanie naszej kamery. Jako argument podajemy wartości w stopniach.

 

Powiększanie (zoomowanie/skalowanie)

Zoomowanie jest odpowiednikiem zmiany rozmiaru, a więc używamy tutaj setSize.

 

Definiowanie gdzie view ma być wyświetlane


Skoro już wiemy jak ustawić co view ma wyświetlać, czas zająć się tym jak ustawić gdzie ma być wyświetlane. Standardowo view zajmuje cały ekran, jeżeli podgląd jest tej samej wielkości co okno wszystko jest renderowane w stosunku 1:1. Jezeli jest innych wymiarów wszystkie obiekty są odpowiednio skalowane.

Standardowe zachowanie tej klasy jest odpowiednie dla większości przypadków jednak czasami chcemy aby było inne, np w przypadku gry multiplayer gdzie chcemy użyć dzielenia ekranu (split screen) poprzez użycie dwóch viewów, które będą zajmowały jedynie po połowie ekranu. Możesz także tego użyć to wyświetlania mapy, która będzie pomniejszoną wersją wyświetlanego obszaru. Do zrobienia czegoś takiego używamy viewPort.

Aby zdefiniować viewPort musisz użyć funkcji setViewPort.

Jak możesz zauważyć viewport nie jest definiowany w pikselach lecz za pomocą faktora size. Takie zastosowanie ma swoje powody, dzięki temu nie musisz pilnować czy nastąpił event resize, aby zaktualizować jego wielkość.

Użycie viewport dla split screena w większości gier wygląda podobnie.

lub dla minimapy:

 

Użycie View


Aby narysować coś za pomocą View, musisz wcześniej ustawić View w obiekcie, który może renderować inne obiekty (RenderWindow, RenderTexture)

Jeżeli nie chcesz używać już ustawionego przez siebie View, lecz chcesz skorzystać z normalnego poglądu okna możesz to zrobić w ten sposób:

Podczas uruchamiania setView jest tworzona kopia view, a nie wskaźnik do niego co oznacza, że po każdej zmianie na View (np skalowanie) musisz wywołać ponownie setView.

 

Zmiana zachowania view


Standardowo po zmianie rozmiaru okna wszystkie obiekty są obiekty są rysowane na tych samych pozycjach tylko odpowiednio przeskalowane do nowej wielkości okna.

Możliwe że będziesz chciał zmienić ten sposób zachowania i np. przy powiększeniu okna chciałbyś aby był widoczny większy obszar mapy. Wszystko co musisz w tym wypadku zrobić to zsynchronizować view do wielkości okna.

 

Konwersja współrzędnych


Podczas gdy używasz view piksele będą mogły nie pasować do pozycji swojej pozycji, ponieważ ich pozycja będzie zależna od przeskalowania. Np klikając na pozycję (10,40) będzie mogło się okazać, że kliknęliśmy rzekomo na pozycję (30, -40) w świecie gry. Aby to poprawić musisz skonwertować pozycję na ekranie do pozycji w świecie gry: mapPixelToCoords.

Standardowo mapPixelToCoords używa obecnie używanego view, jeżeli chcesz użyć view, które nie jest obecnie aktywne, możesz to zrobić poprzez podanie go jako argument tej funkcji.

Możesz także przeprowadzić konwersję w drugą stronę: mapCoordsToPixel.

Oryginalny artykuł

Użycie shader’ów

Wstęp


Shader to nic innego jak mały program, który uruchamiany jest bezpośrednio na kracie graficznej, które pozwolą nam programistom dodanie specjalnych efektów do naszej gry.

Shader’y są pisane w GLSL (OpenGL Shading Language), który jest bardzo podobny do C.

Istnieją dwa typy shader’ów: vertex shader, fragment (lub piksel) shader. Ten poradnik skupi się na wczytywaniu shader’ów nie pisaniu ich. Aby dowiedzieć się jak je pisać zapraszam do poradników GLSL dostępnych w internecie.

 

Wczytywanie


W SFML shader’y są reprezentowane przez sf::Shader. Mimo, że shader’y są coraz częściej spotykane to wciąż zdarzają się karty graficzne, które ich nie obsługują, dlatego powinieneś sprawdzić czy są obsługiwane:

Najczęściej używanym sposobem wczytywania shader’ów jest wczytywanie z pliku, do tego służy metoda loadFromFile.

Shader’y to po prostu pliki tekstowe z innym rozszerzeniem (tak jak w c++), a więc ich rozszerzenie nie ma znaczenia, wykorzystane tutaj „.frag” oraz „.vert” to tylko taka konwencja.

Shader’y mogą być także wczytywane bezpośrednio z klasy string, aby to zrobić musisz użyć loadFromMemory.

Tak jak reszta obiektów w SFML, można wczytać je także przy pomocy loadFromStream, o tym można przeczytać w TYM poradniku.

 

Używanie shader’wów


Aby użyć shader’a na jakimś obiekcie należy go dostarczyć bezpośrednio do draw.

 

Zmiennie w shaderach


Tak samo jak w innych programach komputerowych mogą mieć własne zmienne, deklarujemy je jako zmienne globalne z przedrostkiem uniform.

Zmienne uniform mogą być zmieniane (zmianie może ulec ich wartość) w programie c++, poprzez użycie setParameter z klasy Shader.

Przeładowania setParameter wspierają wszystkie typy dostarczone przez SFML:

  • float (typ GLSL: float)
  • 2 float‚y, sf::Vector2f (typ GLSL: vec2)
  • 3 float‚y, sf::Vector3f (typ GLSL: vec3)
  • 4 float‚y (typ GLSL: vec4)
  • sf::Color (typ GLSL: vec4)
  • sf::Transform (typ GLSL: mat4)
  • sf::Texture (typ GLSL: sampler2D)

Oryginalny artykuł (osoby które są zainteresowane shader’ami powinny przeczytać końcówkę dotyczącą minimal shader i openGL)

Pozycja, rotacja, skalowanie: przekształcanie obiektów

Przekształcenia


Wszystkie obiekty, które można przekształcać (transform) dziedziczą po klasie sf::Transformable, to ona pozwala im na te wszystkie czynności.

Ta klasa ma 4 właściwości: pozycję (position), rotację(rotation), skalę (scale) oraz origin. Ich użycie jest bardzo proste i intuicyjne.

Pozycja

Pozycja, to po prostu pozycja obiektu na scenie 2D, każdy kto miał do czynienia z układem współrzędnych wie o co chodzi :)

Standardowo obiekty porusza się względem punktu, który leży w lewym górnym rogu obiektu, można to zmienić, ale o tym później.

 

Rotacja

Jest to po prostu obracanie obiektów, jest ona definiowana w stopniach zgodnie z ruchem wskazówek zegara.

Zauważ że pobierana rotacja zawsze zawiera się w przedziale [0,360]. Tak jak w przypadku pozycji, tak samo rotacja jest dokonywana względem standardowo lewego górnego rogu.

 

Skala

Czyli faktor, który wskazuje zmianę rozmiaru obiektu. Jeżeli jest większy niż 1 to wiem że obiekt został powiększony, mniejszy od 1 pokazuje że obiekt został pomniejszony, a równy 1 mówi że obiekt jest nieskalowany.

 

Origin

Jest to punkt, wg którego wykonywane są operacje typu: przesuwanie, obracanie obiekt, itp. Standardowo jest on ustawiany na lewy górny róg obiektu. Jeżeli chcesz go zmienić to nie ma problemu, można w bardzo łatwy sposób go zmienić.

Zauważ, że po zmianie punktu origin, zmianie także ulega wizualna pozycja obiektu.

 

Przekształcanie (transformowanie) własnej klasy


sf::Transfromable możesz dziedziczyć, dzięki czemu możesz uzyskać jej właściwości w twojej klasie.

Aby użyć ostatecznie właściwości Transformable musisz użyć getTransform (zazwyczaj podczas wykonywania draw), poniżej nieco bardziej to wyjaśnię.

Jeżeli nie chcesz korzystać z całej funkcjonalności tej klasy, to możesz użyć jej po prostu jako członka swojej klasy, a funkcje możesz albo pozostawić w obecnej formie, albo możesz napisać własne przez zasłonięcie tych z tej klasy.

 

Rozszerzanie możliwości Transformable


Klasa Transformable jest łatwa w użyciu, ale jednocześnie bardzo ograniczona. Może się zdarzyć, że będziesz potrzebował więcej możliwości, typu łączenie kilku przekształceń w  jedno itp. Jeżeli tak jest w twoim przypadku, to jest nisko poziomowa klasa sf::Transform, która jest 3×3 matrix’em, dzięki czemu może być reprezentowana przez przekształcenia w świecie 2D.

Jest wiele sposobów na użycie sf::Transform:

  • przez użycie predefiniowanych funkcji dla najczęściej spotykanych transformacji (translacja, skala, rotacja)
  • przez połączenie dwóch transformacji
  • przez bezpośrednie określenie 9 elementów.

Kilka przykładów:

Możesz oczywiście wszystkie transformacje zawrzeć w jednej:

Aby wykorzystać te transformacje musisz:

Forma powyżej jest krótszą wersją:

 

Bounding boxes


Po wykonaniu  transformacji być może chciałbyś wykonać jakieś obliczenia na nich, np do sprawdzenia kolizji.

Obiekty w SFML mogą podać ci tzw bounding box, czyli najmniejszy prostokąt potrzebny do zamknięcia obiektu w prostpkącie.

Bounding box może być przydatny w sprawdzaniu kolizji i można go pobrać bardzo szybko.

Funkcja getGlobalBounds pobiera bounding box z uwzględnieniem transformacji (rotacja itp), getLocalBounds nie bierze tych zmian pod uwagę.

Jednak może się okazać, że ten rodzaj kolizji nie będzie dla ciebie odpowiednio dokładny, możliwe że przyda ci się to co przekazałem w moim kursie Piszemy grę w SFML w lekcji o kolizjach.

 

Hierarchia obiektów


Dzięki temu co napisaliśmy wcześniej możemy w łatwy sposób napisać klasę, która będzie decydowała o hierarchii obiektów, gdzie potomkowie danych obiektów (children) będą transformowane zgodnie z ich rodzicami (parents). Wszystko co musisz zrobić to przekazanie połączenia przekształcenia z rodzica na dzieci podczas ich rysowania.

Oryginalny artykuł