Wprowadzenie
Zapewne wielu z nas chciało kiedyś w łatwy sposób odpalić dźwięk z aplikacji w C++. Niestety na moment pisania artykułu nie ma w
standardzie C++ do
wersji C++23 włącznie, jak również w tzw. "poczekalni do standardu C++" oficjalnie zwaną
biblioteką boost czegoś co by mogło odtworzyć plik muzyczny, czy nawet binarne dane w jakimkolwiek formacie.
Jedyne co mamy zarówno w C jak i C++ to znak
ASCII, ale wtedy bazujemy na dźwięku systemowym, którego nie możemy zmienić:
#include <iostream>
int main()
{
std::cout << "\a" << std::endl;
}
Wielu z nas chciałby mieć następujące możliwości w standardzie:
#include <audio>
int main()
{
std::audio::play( "song.wav" );
}
Ewentualnie jeszcze możliwość, sterowania obiektem muzycznym - wstrzymywanie, wznawianie, zatrzymywanie, zmiana utworu, czy też pobieranie danych na temat tego utworu takich jak długość. Właśnie najprostrzym rozwiązaniem, które nie jest wspierane od prawie 20 lat jest
biblioteka audiere, która w prawie tak prosty sposób umożliwiała odtwarzanie plików muzycznych.
O czym jest niniejszy artykuł
W artykule tym właśnie chce wpierw przeanalizować dostępne biblioteki do odtwarzania muzyki w C++ pod następującym względem (warunki są posortowane od najistotniejszych dla mnie):
Następnie przedstawiam przykładowe kody uruchamiające dźwięk przy użyciu trzech wybranych bibliotek, wraz z przedstawieniem wydruku, opisem "bardziej zawiłych" miejsc w kodzie, a także przedstawiam sposób zbudowania w sposób multiplatformowy (wg
poradnika opisującego manager pakietów VCPKG). Wspominam też o licencjach zaprezentowanych narzędzi, ale zastrzegam, że nie jestem od nich specjalistą - te kwestie przed podjęciem decyzji biznesowych trzeba sobie sprawdzić samemu.
Twórca języka C++ Bjarne Stroustrup podczas Cpp.chat Episode #44 powiedział:
"C++ is there to deal with hardware at a low level, and to abstract away from it with zero overhead."
Tak się składa, że obsługa dźwięku jest blisko sprzętu, a standard C++ tego nie zauważa.
Mini teoria
Wg wspomnianego
wystąpienia Timura gdy odtwarzany jest dźwięk to pod spodem odbywa się automatyczne wołanie pewnej funkcji (ang. callback), która to ma za zadanie ustawić pewne bufory danymi audio do odtwarzania. Wszystkie biblioteki obsługujące dźwięk robią to w taki właśnie sposób (jak nie wprost to pod spodem). Zarejestrowanie takiej funkcji to jedyny aspekt, którego się nie da zrobić w standardowym C++, całą resztę się da ... ale posiadając wiedzę w temacie przetwarzania audio. Ważnym aspektem jest, że ta funkcja zwrotna (callback) nie powinna wykonywać operacji o niedeterministycznym czasie trwania, czyli:
W oparciu o tę teorię zapewne dostrzegasz, Drogi Czytelniku, potrzebę aby skorzystać z biblioteki, która ma teorię zaimplementowaną w praktyce.
Biblioteki do odtwarzania dźwięku - analiza
Timur Doumler bibliotekę
libstdaudio zaimplementował jedynie na MacOS, przez co nie spełnia ona opisanych powyżej wymagań względem biblioteki, które wymieniłem wcześniej. Poza tym jego propozycja nie dostarcza mechanizmu do odtwarzania plików dźwiękowych w łatwy sposób dla osoby nie znającej od podszewki jak działa odtwarzanie dźwięku. W poszukiwaniach "najlepszej" biblioteki do odtwarzania dźwięków za punkt startowy posłużyła mi lista multiplatformowych bibliotek, które on wymienił:
Przeanalizowałem te biblioteki pod kontem moich kryteriów, wszystkie są multiplatformowe, ale:
Żadna z powyższych bibliotek nie spełniła wszystkich ww. kryteriów, stąd szukałem kolejnych multiplatformowych frameworków, oto one:
Kodeki do obsługi MP3
Gdy wybrana przez nas biblioteka nie posiada wbudowanej obsługi MP3 (lub licencja na dany komponent w bibliotece nas nie satysfakcjonuje) to możemy skorzystać z kodeków np. dekodujących MP3. Wtedy dane binarne pliku muzycznego są zamieniane na odpowiednią tablicę zdekodowanych danych, która może być odtworzona przez naszą bibliotekę. Oto przykładowe kodeki do obsługi MP3:
Z kodekami również należy uważać pod względem licencyjnym, gdyż jeśli użyjemy kodeka na licencji GPL to wymusza to zrobienie naszego kodu na tejże licencji.
SFML - najlepsza (wg moich kryteriów) biblioteka do odtwarzania dźwięku
SFML (na
licencji ZLIB) jest potężną biblioteką multimedialną, w której można tworzyć nawet gry, operować na multimediach, wykonywać obliczenia na kartach graficznych i inne.
SFML (wg
dokumentacji) wspiera następujące formaty audio: WAV (PCM), OGG/Vorbis, FLAC i MP3 (tylko dekodowanie).
Kod odtwarzający pliki muzyczne w SFMLu
Poniżej znajduje się przykładowy kod odtwarzający muzykę:
#include <iostream>
#include <thread>
#include <chrono>
#include <SFML/Audio.hpp>
int main() {
const std::string filePath = "audio.wav";
sf::Music music;
if( !music.openFromFile( filePath ) ) {
std::cerr << "Error: Could not load file " << filePath << std::endl;
return 1;
}
std::cout << "Now playing: " << filePath << ", duration: " << music.getDuration().asSeconds() << " seconds" << std::endl;
music.play();
while( sf::Music::Playing == music.getStatus() ) {
std::cout << "\rCurrent position: " << music.getPlayingOffset().asSeconds()
<< " / " << music.getDuration().asSeconds() << " seconds" << std::flush;
std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );
}
std::cout << "\nPlayback finished." << std::endl;
}
Przykładowy wydruk:
Now playing: audio.wav, duration: 4.34746 seconds
Current position: 3.98199 / 4.34746 seconds
Playback finished.
SFML ma bardzo dobrą dokumentacje, dużą społeczność i oficjalne tutoriale. Tak się składa, że na oficjalnej stronie znajduje się
oficjalny tutorial jak odtwarzać dźwięk przy pomocy kodu używając SFMLa.
Poza tym na moje oko jest to najprostrzy kod do odtwarzania plików audio ze wszystkich innych bibliotek.
SFML zawiera również obiekty do odtwarzania dźwięku już wczytanego w formie danych binarnych, do tego celu służą
sf::Sound
i
sf::SoundBuffer
. Można użyć tego mechanizmu odtwarzając dane z sieci, lub gdy mamy do czynienia z większym plikiem (którego wczytanie do pamięci byłoby zauważalne dla zużycia zasobów). Kolejnym zastosowaniem jest użycie zewnętrznego kodeka.
Kod odtwarzający pliki muzyczne w SFMLu przy użyciu zewnętrznego kodeka: FFmpeg
SFML nie odtwarza wszystkich formatów, stąd jeśli potrzebujemy odtworzyć taki plik konieczne jest użycie dodatkowego kodeka.
Zdecydowałem o użyciu
kodeka FFmpeg, gdyż obsługuje on olbrzymią gamę formatów multimedialnych, nie tylko audio, ale też video.
Oto kod, który dekoduje wiele formatów multimedialnych (również video) przy pomocy FFmpeg, a następnie uruchamia muzykę za pomocą SFMLa:
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
#include <SFML/Audio.hpp>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libavutil/channel_layout.h>
#include <libavutil/samplefmt.h>
#include <libavutil/mem.h>
}
struct AudioFileData
{
int channels;
int sampleRate;
std::vector < sf::Int16 > pcmAudioBuffer;
bool successReading;
};
AudioFileData decodeAudioWithFFmpeg( const std::string & filePath );
void playSound( const AudioFileData & audioFileData );
int main( int argc, char * * argv ) {
if( argc < 2 ) {
std::cerr << "Usage: " << argv[ 0 ] << " <audio file>" << std::endl;
return 1;
}
const std::string filePath = argv[ 1 ];
AudioFileData audioData = decodeAudioWithFFmpeg( filePath );
if( audioData.pcmAudioBuffer.empty() )
std::cerr << "Failed to decode or play audio from file " << filePath << std::endl;
else
playSound( audioData );
}
AudioFileData decodeAudioWithFFmpeg( const std::string & filePath ) {
avformat_network_init();
AVFormatContext * formatContext = nullptr;
if( avformat_open_input( & formatContext, filePath.c_str(), nullptr, nullptr ) != 0 ) {
std::cerr << "Error: Could not open audio file with FFmpeg: " << filePath << std::endl;
return { };
}
if( avformat_find_stream_info( formatContext, nullptr ) < 0 ) {
std::cerr << "Error: Could not retrieve stream info." << std::endl;
avformat_close_input( & formatContext );
return { };
}
const AVCodec * codec = nullptr;
AVCodecContext * codecContext = nullptr;
int streamIndex = - 1;
for( unsigned int i = 0; i < formatContext->nb_streams; ++i ) {
if( formatContext->streams[ i ]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO ) {
codec = avcodec_find_decoder( formatContext->streams[ i ]->codecpar->codec_id );
if( !codec ) {
std::cerr << "Error: Unsupported codec for audio stream." << std::endl;
avformat_close_input( & formatContext );
return { };
}
codecContext = avcodec_alloc_context3( codec );
avcodec_parameters_to_context( codecContext, formatContext->streams[ i ]->codecpar );
if( avcodec_open2( codecContext, codec, nullptr ) < 0 ) {
std::cerr << "Error: Could not open codec." << std::endl;
avcodec_free_context( & codecContext );
avformat_close_input( & formatContext );
return { };
}
streamIndex = i;
break;
}
}
if( streamIndex == - 1 ) {
std::cerr << "Error: No audio stream found in file." << std::endl;
avcodec_free_context( & codecContext );
avformat_close_input( & formatContext );
return { };
}
SwrContext * swrContext = swr_alloc();
if( !swrContext ) {
std::cerr << "Error: Could not allocate SwrContext." << std::endl;
avcodec_free_context( & codecContext );
avformat_close_input( & formatContext );
return { };
}
swr_alloc_set_opts2( & swrContext,
& codecContext->ch_layout, AV_SAMPLE_FMT_S16, codecContext->sample_rate,
& codecContext->ch_layout, codecContext->sample_fmt, codecContext->sample_rate,
0, nullptr );
if( swr_init( swrContext ) < 0 ) {
std::cerr << "Error: Could not initialize SwrContext." << std::endl;
swr_free( & swrContext );
avcodec_free_context( & codecContext );
avformat_close_input( & formatContext );
return { };
}
AVPacket * packet = av_packet_alloc();
AVFrame * frame = av_frame_alloc();
AudioFileData audioFileData;
audioFileData.sampleRate = codecContext->sample_rate;
audioFileData.channels = codecContext->ch_layout.nb_channels;
while( av_read_frame( formatContext, packet ) >= 0 ) {
if( packet->stream_index == streamIndex ) {
if( avcodec_send_packet( codecContext, packet ) == 0 ) {
while( avcodec_receive_frame( codecContext, frame ) == 0 ) {
int bufferSize = av_samples_get_buffer_size( nullptr, audioFileData.channels, frame->nb_samples, AV_SAMPLE_FMT_S16, 1 );
std::vector < uint8_t > tempBuffer( bufferSize );
uint8_t * tempBufferPtr = tempBuffer.data();
swr_convert( swrContext, & tempBufferPtr, frame->nb_samples, frame->data, frame->nb_samples );
audioFileData.pcmAudioBuffer.insert( end( audioFileData.pcmAudioBuffer ),
( sf::Int16 * ) tempBuffer.data(),
( sf::Int16 * ) tempBuffer.data() + bufferSize / sizeof( sf::Int16 ) );
}
}
}
av_packet_unref( packet );
}
av_frame_free( & frame );
av_packet_free( & packet );
swr_free( & swrContext );
avcodec_free_context( & codecContext );
avformat_close_input( & formatContext );
return audioFileData;
}
void playSound( const AudioFileData & audioFileData ) {
sf::SoundBuffer soundBuffer;
if( !soundBuffer.loadFromSamples( audioFileData.pcmAudioBuffer.data(), audioFileData.pcmAudioBuffer.size(), audioFileData.channels, audioFileData.sampleRate ) ) {
std::cerr << "Error: Could not load buffer for playback" << std::endl;
return;
}
sf::Sound sound;
sound.setBuffer( soundBuffer );
sound.play();
std::cout << "Playing file, duration: " << soundBuffer.getDuration().asSeconds() << " seconds" << std::endl;
while( sound.getStatus() == sf::Sound::Playing ) {
std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );
}
std::cout << "Playback finished." << std::endl;
}
Widzimy tutaj wręcz przepaść między prostotą SFMLa, a niskopoziomowym dekoderem.
Z tym kodem trzeba uważać o tyle, że FFmpeg może być skonfigurowany jako używający bibliotek GPL, wtedy nasz kod powinniśmy ustawić jako GPL. Aby tego uniknąć warto skonfigurować sobie FFmpeg aby nie używał GPLa.
Wpierw sprawdźmy, jaką mamy konfiguracje:
ffmpeg -version
i czy tam jest
--enable-gpl
, jeśli tak to trzeba skonfigurować bez tej opcji, przykładowo w taki sposób
--disable-gpl --enable-nonfree
, ale jeśli chcemy być w pełni legalni to na
oficjalnej stronie FFmpeg opisali konieczne kroki (checklistę) aby być zabezpieczonymi.
Kod odtwarzający pliki muzyczne w SFMLu przy użyciu C++owej nakładki na FFmpeg: AvCpp
FFmpeg jest potężnym narzędziem, jednakże jest w C, dlatego możemy się pokusić o użycie C++owej nakładki na FFmpeg (dwa rodzaje licencji:
licencja BSD lub LGPL):
AvCpp. Projekt co prawda ma jeszcze mniej niż 1000 commitów, niemniej jednak jak utworzyłem
ISSUE na Githubie z prośbą o przykład na integracje z SFMLem to dość szybko autor mi taki przykład zrobił umieszczając jako odpowiedź do ISSUE. W poniższym kodzie jest ten przykład, oczyszczony przeze mnie, zapewne kiedyś pojawi się jako
jeden z przykładów):
#include <iostream>
#include <vector>
#include <thread>
#include <chrono>
#include <filesystem>
#include <optional>
#include <SFML/Audio.hpp>
#include "avcpp/av.h"
#include "avcpp/format.h"
#include "avcpp/formatcontext.h"
#include "avcpp/codec.h"
#include "avcpp/codeccontext.h"
#include "avcpp/audioresampler.h"
struct AudioFileData
{
int channels;
int sampleRate;
std::vector < sf::Int16 > pcmAudioBuffer;
bool successReading;
};
AudioFileData decodeAudioWithAvcpp( const std::filesystem::path & filePath );
void playSound( const AudioFileData & audioFileData );
int main( int argc, char * * argv ) {
if( argc < 2 ) {
std::cerr << "Usage: " << argv[ 0 ] << " <audio file>" << std::endl;
return 1;
}
const std::string filePath = argv[ 1 ];
AudioFileData audioData = decodeAudioWithAvcpp( filePath );
if( audioData.pcmAudioBuffer.empty() )
std::cerr << "Failed to decode or play audio from file " << filePath << std::endl;
else
playSound( audioData );
}
AudioFileData decodeAudioWithAvcpp( const std::filesystem::path & filePath )
{
std::error_code ec;
std::optional < size_t > audioStream;
av::init();
av::setFFmpegLoggingLevel( AV_LOG_DEBUG );
av::FormatContext ictx;
av::Stream ast;
av::AudioDecoderContext adec;
ictx.openInput( filePath ); ictx.findStreamInfo(); for( auto i = 0u; i < ictx.streamsCount(); ++i ) {
auto st = ictx.stream( i );
if( st.isAudio() ) {
audioStream = i;
ast = st;
break;
}
}
if( ast.isNull() ) {
std::cerr << "Audio stream not found\n";
exit( 1 );
}
if( ast.isValid() ) {
adec = av::AudioDecoderContext( ast );
adec.open( av::Codec() ); }
av::AudioResampler resampler( adec.channelLayout(), adec.sampleRate(), AV_SAMPLE_FMT_S16,
adec.channelLayout(), adec.sampleRate(), adec.sampleFormat() );
AudioFileData audioFileData;
audioFileData.sampleRate = adec.sampleRate();
audioFileData.channels = adec.channels();
while( true ) {
auto pkt = ictx.readPacket( ec );
if( ec ) {
std::clog << "Packet reading error: " << ec << ", " << ec.message() << std::endl;
break;
}
if( !pkt ) {
break;
}
if( pkt.streamIndex() != * audioStream )
continue;
std::clog << "Read packet: " << pkt.pts() << " / " << pkt.pts().seconds() << " / " << pkt.timeBase() << " / st: " << pkt.streamIndex() << std::endl;
auto samples = adec.decode( pkt );
std::clog << " Samples [in]: " << samples.samplesCount()
<< ", ch: " << samples.channelsCount()
<< ", freq: " << samples.sampleRate()
<< ", name: " << samples.channelsLayoutString()
<< ", pts: " << samples.pts().seconds()
<< ", ref=" << samples.isReferenced() << ":" << samples.refCount()
<< std::endl;
if( !samples )
continue;
resampler.push( samples );
auto ouSamples = av::AudioSamples::null();
while(( ouSamples = resampler.pop( samples.samplesCount(), ec ) ) ) {
std::clog << " Samples [ou]: " << ouSamples.samplesCount()
<< ", ch: " << ouSamples.channelsCount()
<< ", freq: " << ouSamples.sampleRate()
<< ", name: " << ouSamples.channelsLayoutString()
<< ", pts: " << ouSamples.pts().seconds()
<< ", ref=" << ouSamples.isReferenced() << ":" << ouSamples.refCount()
<< ", size=" << ouSamples.size()
<< std::endl;
audioFileData.pcmAudioBuffer.insert( std::end( audioFileData.pcmAudioBuffer ),
( sf::Int16 * ) ouSamples.data(),
( sf::Int16 * ) ouSamples.data() + ouSamples.size() / sizeof( sf::Int16 ) );
}
}
{
while( resampler.delay() ) {
auto ouSamples = resampler.pop( 0, ec ); if( ec ) {
std::clog << "Resampling status: " << ec << ", text: " << ec.message() << std::endl;
break;
} else if( !ouSamples ) {
break;
} else {
std::clog << " Samples [ou]: " << ouSamples.samplesCount()
<< ", ch: " << ouSamples.channelsCount()
<< ", freq: " << ouSamples.sampleRate()
<< ", name: " << ouSamples.channelsLayoutString()
<< ", pts: " << ouSamples.pts().seconds()
<< ", ref=" << ouSamples.isReferenced() << ":" << ouSamples.refCount()
<< ", size=" << ouSamples.size()
<< std::endl;
audioFileData.pcmAudioBuffer.insert( std::end( audioFileData.pcmAudioBuffer ),
( sf::Int16 * ) ouSamples.data(),
( sf::Int16 * ) ouSamples.data() + ouSamples.size() / sizeof( sf::Int16 ) );
}
}
}
return audioFileData;
}
void playSound( const AudioFileData & audioFileData ) {
sf::SoundBuffer soundBuffer;
if( !soundBuffer.loadFromSamples( audioFileData.pcmAudioBuffer.data(), audioFileData.pcmAudioBuffer.size(), audioFileData.channels, audioFileData.sampleRate ) ) {
std::cerr << "Error: Could not load buffer for playback" << std::endl;
return;
}
sf::Sound sound;
sound.setBuffer( soundBuffer );
sound.play();
std::cout << "Playing file, duration: " << soundBuffer.getDuration().asSeconds() << " seconds" << std::endl;
while( sound.getStatus() == sf::Sound::Playing ) {
std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );
}
std::cout << "Playback finished." << std::endl;
}
Kod co prawda nie jest krótszy niż analogiczny przykład w czystym FFmpeg, jednakże wersja dostarczona przez utrzymującego AvCpp wygląda czytelniej, ma lepszą obsługę błędów, oraz wiele cennych komentarzy ... zwłaszcza dla osób o mniejszym doświadczeniu w odtwarzaniu muzyki z poziomu C++.
Mimo iż AvCpp ma do wyboru licencje BSD lub LGPL to należy uważać przy instalacji przy użyciu VCPKG, gdyż tamta wersja zaciąga "wirus GPL", przez co korzystając z AvCpp również musimy dostarczyć nasz kod jako GPL. Utworzyłem
issue dla VCPKG aby to naprawili, ale póki to nie nastąpi trzeba budować AvCpp na własną rękę.
Budowanie SFMLa
Sposób budowania SFMLa jest opisany na
stronie domowej, jednakże ja jestem zwolennikiem zastosowania
managera pakietów VCPKG wraz z CMakeLists.txt.
Przy pomocy tego managera pakietów zasadniczo wystarczy zawołać (na systemie linuxowym):
git clone https://github.com/Microsoft/vcpkg.git --depth=1
./vcpkg/bootstrap-vcpkg.sh -disableMetrics
# instalacja SFMLa
./vcpkg/vcpkg install 'sfml[audio]'
# gdybyśmy chcieli jeszcze FFmpeg to można np.
./vcpkg/vcpkg install 'ffmpeg[all]'
# tylko trzeba sprawdzić, czy nie zainstalowały się nam kodeki GPL
# dlatego lepiej pojedyncze kodeki instalować
# można wyszukać dostępne konfiguracje powyższych:
./vcpkg/vcpkg search sfml
./vcpkg/vcpkg search ffmpeg
# nastepnie musimy "doinstalować" zainstalowane pakiety przez:
./vcpkg/vcpkg integrate install
# zależnie od systemu operacyjnego pojawi się nam argument do podania do CMake'a lub nie
Po dokonaniu instalacji pojawią się nam polecenia do dodania do naszego pliku CMakeLists.txt, oto przykładowy plik:
cmake_minimum_required(VERSION 3.31)
project(AudioTerminal LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# binary1 (just SFML)
find_package(SFML COMPONENTS system window graphics audio CONFIG REQUIRED)
add_executable(SFML_AUDIO sfml1.cpp)
target_link_libraries(SFML_AUDIO PRIVATE sfml-system sfml-network sfml-graphics sfml-window sfml-audio)
# binary2 (SFML + FFmpeg)
find_package(PkgConfig REQUIRED)
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
libavformat
libavcodec
libswresample
libavutil
)
add_executable(SFML_FFMPEG sfml_ffmpeg.cpp)
target_include_directories(SFML_FFMPEG PRIVATE ${PROJECT_SOURCE_DIR} PkgConfig::FFMPEG)
target_link_libraries(SFML_FFMPEG PRIVATE PkgConfig::FFMPEG sfml-audio sfml-system)
Odtwarzanie dźwięku przy użyciu Qt Multimedia
Biblioteka Qt wraz z modułem
Qt Multimedia niemalże spełnia wszystkie przedstawione kryteria.
Zarzutem względem niej jest to, że Qt jest olbrzymią biblioteką do robienia programów (nawet zawiera konkurencyjne implementacje wielu aspektów względem biblioteki standardowej C++), zawierająca bardzo wiele funkcjonalności multimedialnych, a my potrzebujemy jedynie odtwarzać audio. Jest na szczęście możliwe uruchomienie audio bez interfejsu graficznego GUI (w interfejsie tekstowym). Olbrzymią zaletą tej biblioteki jest to, iż nie tylko wspiera format MP3, ale również można odtwarzać dźwięk z ... filmów.
Informacje licencyjne
Różne moduły biblioteki Qt są udostępniane na różnych licencjach, m.in.
licencji GPL lub
LGPL. Qt Multimedia jest udostępniana na licencji LGPL, co pozwala na używanie jej w projektach zamkniętych czy nawet komercyjnych, bez konieczności udostępniania kodu źródłowego aplikacji, pod warunkiem spełnienia wymogów licencji LGPL. Wymagania te to m.in.:
Uwaga: W przypadku modyfikacji kodu Qt należy udostępnić zmodyfikowany kod źródłowy biblioteki, zgodnie z wymogami licencji LGPL.
Przykładowy kod odtwarzający dźwięki z plików multimedialnych (nie tylko audio) z poziomu Qt (wersja minimalistyczna)
Poniżej znajduje się przykładowy kod, następnie wydruk z uruchomienia programu, a w dalszej kolejności opisanie aspektów charakterystycznych dla Qt. Osoby mające doświadczenie z Qt szybko się połapią. A oto kod:
#include <QCoreApplication>
#include <QMediaPlayer>
#include <QTimer>
#include <QDebug>
#include <QAudioOutput>
int main( int argc, char * argv[ ] ) {
QCoreApplication app( argc, argv ); QMediaPlayer player; QAudioOutput audioOutput; player.setAudioOutput( & audioOutput );
player.setSource( QUrl::fromLocalFile( "song.mp3" ) );
QObject::connect( & player, & QMediaPlayer::playbackStateChanged,[ & app ]( QMediaPlayer::PlaybackState state ) { if( QMediaPlayer::StoppedState == state ) {
app.quit();
}
} );
player.play(); return app.exec(); }
Wydruk (występujący u mnie dla wskazanego pliku):
[mp3float @ 0x567ef54c5480] Could not update timestamps for skipped samples.
[mp3float @ 0x567ef54c5480] Could not update timestamps for discarded samples.
Jak widać po zakończeniu programu mogą się pojawić komunikaty, które są produkowane przez backendy używane przez Qt (np. GStreamer, FFmpeg, Media Foundation), których nie da się wyłączyć z poziomu Qt w multiplatformowy sposób.
Opis (bardziej dla osób nie znających Qt i tego jak ono działa):
Bardziej rozbudowany kod odtwarzający dźwięki z plików multimedialnych (nie tylko audio) z poziomu Qt
Poniżej ciut bardziej rozbudowany programik, który nasluchuje na więcej sygnałów, oraz kończy się po pewnym czasie (nawet przed zakończeniem odtwarzania pliku dźwiękowego):
#include <QCoreApplication>
#include <QMediaPlayer>
#include <QTimer>
#include <QDebug>
#include <QAudioOutput>
int main( int argc, char * argv[ ] ) {
QCoreApplication app( argc, argv );
QMediaPlayer player;
QAudioOutput audioOutput;
player.setAudioOutput( & audioOutput );
player.setSource( QUrl::fromLocalFile( "song.mp3" ) );
QObject::connect( & player, & QMediaPlayer::playbackStateChanged,[ & app ]( QMediaPlayer::PlaybackState state ) {
if( QMediaPlayer::StoppedState == state ) {
qDebug() << "Playback finished.";
app.quit();
}
} );
QObject::connect( & player, & QMediaPlayer::durationChanged,[ & player ]() {
qDebug() << "Duration (ms, 1s=1000ms):" << player.duration();
} );
QObject::connect( & player, & QMediaPlayer::errorOccurred,[ & player ]() {
qCritical() << "Error occurred:" << player.error() << player.errorString();
} );
QObject::connect( & player, & QMediaPlayer::mediaStatusChanged,[ ]( QMediaPlayer::MediaStatus status ) {
qDebug() << "Media status changed:" << status;
} );
player.play(); QTimer::singleShot( 2000, & app, & QCoreApplication::quit );
qDebug() << "Playback started, You should hear!";
return app.exec();
}
Przykładowy wydruk z drugiego programu:
Playback started, You should hear!
Duration (ms, 1s=1000ms): 2795
Media status changed: QMediaPlayer::LoadedMedia
Media status changed: QMediaPlayer::BufferingMedia
[mp3float @ 0x5929f94abd80] Could not update timestamps for skipped samples.
Media status changed: QMediaPlayer::BufferedMedia
Jak widzimy patrząc na kody programów w SFMLu i w QT, to tak właściwie jeśli chcemy odtwarzać z mniej popularnych formatów lub z plików video to kod w QT jest prostrzy niż kod w SFMLu w połączeniu z kodekiem FFmpeg. Oczywiście FFmpeg obsługuje więcej formatów niż QT.
Instalacja biblioteki Qt
Instalacja biblioteki Qt zależy od zastosowania. Zakładając zastosowanie ogólne (czyli również komercyjne) multiplatformowo można zainstalować na dwa sposoby:
1. Przez użycie managera pakietów VCPKG (można skorzystać z
opisu) a następnie CMake'a.
2. Przez pobranie odpowiedniego instalatora ze strony
Qt.io i zaznaczenie w instalatorze Qt Multimedia (domyślnie nie musi być zaznaczony).
Opiszę ten pierwszy krok. Możemy zainstalować VCPKG (pierwsze kroki analogiczne jak wyżej), a następnie zawołać:
git clone https://github.com/Microsoft/vcpkg.git --depth=1
./vcpkg/bootstrap-vcpkg.sh -disableMetrics
# instalacja Qt Multimedia
./vcpkg/vcpkg install 'qtmultimedia[gstreamer]' # instalujemy z backendem GStreamer, który ma licencje LGPL.
# jakbyśmy chcieli zobaczyć jeszcze inne dostępne opcje (m.in. jakie backendy mają dostępne poza GStreamer)
./vcpkg/vcpkg search qtmultimedia
W rezultacie instalacji pojawi się trochę poleceń do pliku CMakeLists.txt, ale w moim przypadku nie wszystkie były konieczne, działający CMakeLists.txt (dla obydwu przykładowych kodów z QT ten sam):
cmake_minimum_required(VERSION 3.31)
project(AudioTerminal LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# binary3 (Qt):
add_executable(QT_MULTIMEDIA_MINIMUM qt_multimedia_minimum.cpp)
find_package(Qt6Multimedia CONFIG REQUIRED)
target_link_libraries(QT_MULTIMEDIA_MINIMUM PRIVATE Qt6::Multimedia Qt6::MultimediaPrivate)
Biblioteka MiniAudio - lekkie rozwiązanie w C
Innym rozwiązaniem, które spełnia wszystkie ww. kryteria poza tym, że jest w C (a nie C++) jest biblioteka
MiniAudio, która tak właściwie jest jednym plikiem nagłówkowym na kiladziesiąt tysięcy linii:
#include <miniaudio/miniaudio.h>
, przez co jest zdecydowanie lżejsza niż SFML czy QT. W przeciwieństwie do Qt biblioteka MiniAudio ma
licencje MIT-0. Jest to biblioteka do odtwarzania dźwięku, odtwarza popularne formaty muzyczne (m.in. MP3, WAV), ale nie odtwarza bezpośrednio (czyli bez dodatkowych kodeków) muzyki z plików video. Jest to kod bardziej niskopoziomowy niż wcześniejsze przykłady, ale wciąż łatwy w użyciu przez osoby mniej obeznane z programowaniem audio.
Przykładowy kod odtwarzający dźwięk z poziomu biblioteki MiniAudio
Oto przykładowy kod odtwarzający dźwięk (z dużą liczbą komunikatów debugowych):
#define MINIAUDIO_IMPLEMENTATION
#include <miniaudio/miniaudio.h>
#include <iostream>
#include <csignal>
#include <thread>
#include <atomic>
#include <chrono>
std::atomic < bool > isPlaying { true };
ma_engine engine;
ma_sound sound;
void signal_handler( int signal ) {
if( SIGINT == signal ) {
std::cout << "\nPlayback interrupted by signal." << std::endl;
isPlaying = false;
}
}
void cleanup_engine() {
ma_engine_uninit( & engine );
std::cout << "Audio engine uninitialized." << std::endl;
}
void cleanup_sound() {
ma_sound_stop( & sound );
ma_sound_uninit( & sound );
std::cout << "Sound uninitialized." << std::endl;
}
int main() {
const char * filePath = "audio.mp3";
std::signal( SIGINT, signal_handler );
if( ma_engine_init( nullptr, & engine ) != MA_SUCCESS ) {
std::cerr << "Failed to initialize audio engine." << std::endl;
return 1;
}
std::atexit( cleanup_engine ); if( ma_sound_init_from_file( & engine, filePath, 0, nullptr, nullptr, & sound ) != MA_SUCCESS ) {
std::cerr << "Failed to load sound: " << filePath << std::endl;
return 1;
}
std::atexit( cleanup_sound ); float durationInSeconds = 0.0f;
if( ma_sound_get_length_in_seconds( & sound, & durationInSeconds ) != MA_SUCCESS ) {
std::cerr << "Failed to get sound length." << std::endl;
return 1;
}
std::cout << "File: " << filePath << std::endl;
std::cout << "Duration: " << durationInSeconds << " seconds" << std::endl;
if( ma_sound_start( & sound ) != MA_SUCCESS ) {
std::cerr << "Failed to start playback." << std::endl;
return 1;
}
std::cout << "Playing: " << filePath << std::endl;
while( isPlaying ) {
float currentTime = { };
if( ma_sound_get_cursor_in_seconds( & sound, ¤ tTime ) != MA_SUCCESS ) {
std::cerr << "\nFailed to get current playback position." << std::endl;
break;
}
std::cout << "\rCurrent position: " << currentTime << " / " << durationInSeconds << " seconds" << std::flush;
if( currentTime >= durationInSeconds ) {
std::cout << "\nPlayback finished." << std::endl;
break;
}
std::this_thread::sleep_for( std::chrono::milliseconds( 500 ) );
}
}
Wydruk:
File: audio.mp3
Duration: 2.82122 seconds
Playing: audio.mp3
Current position: 2.82122 / 2.82122 seconds
Playback finished.
Sound uninitialized.
Audio engine uninitialized.
Wiele bibliotek w języku C, gdy korzystamy z pewnych obiektów to potem musimy samodzielnie sprzątać zasoby, jest to zrobione przez
std::atexit
. Poza tym nie mając mechanizmu wyjątków musimy sprawdzać kody błędów zwracana przez funkcje.
Instalacja biblioteki MiniAudio
Biblioteka ta to w zasadzie plik nagłówkowy, co prawda na kilkadziesiąt tysięcy linii, ale jednak wystarczy go wrzucić obok projektu. Alternatywnie można też skorzystać z VCPKG, tak jak wcześniej opisałem, wołając analogiczną komendę:
./vcpkg/vcpkg search miniaudio
a komendy do dodania do pliku CMakeLists.txt zostają wyświetlone na standardowe wyjście.
Wyróżnienie - QtAVPlayer
QtAVPlayer to wysokopoziomowy odtwarzacz korzystający z biblioteki Qt, oraz implementujący wiele funkcjonalności potężnego narzędzia ffmpeg, taka wybuchowa kombinacja umożliwia m.in.:
Przykłady demonstrujące użycie tych funkcjonalności znajdują się na
stronie projektu. Tam znajdują się również informacje jak użyć bibliotekę.
Sam QtAVPlayer jest na
licencji MIT, chociaż używane pod spodem Qt, oraz FFMPEG mają swoje licencje. Warto się upewnić, że nie używamy kodeków na licencji GPL, o ile nie chcemy naszego kodu "zarazić" tą licencją.
Liczba commitów tego projektu, na moment pisania artykułu to 770, czyli nie spełnia on pierwszego z ww. kryteriów, natomiast projekt jest wciąż na bieżąco aktualizowany, pozostaje mieć więc nadzieję, że biblioteka ta będzie dalej rozwijana.
Dodatek - otwartoźródłowe, wieloplatformowe narzędzie do odtwarzania multimediów z konsoli
Przeszukując biblioteki znalazłem również bibliotekę
musikcore, która jest niestety (prawie) nieużywalna w sposób bezpośredni, gdyż jest zakorzeniona w narzędziu
musikcube. Narzędzie to stanowi ciekawą alternatywę dla znanych nam narzędzi do odtwarzania muzyki, zawiera duży wachlarz funkcjonalności, a wszystko w terminalu. Jest z nim jednak jak z
VIMem - trzeba się nauczyć pewnych kombinacji klawiszy i staje się najszybszym w użyciu.
Gwarancje i ich brak
Artykuł został napisany przez osobę, która nie jest specjalistą zarówno jeśli chodzi o użycie multimediów z poziomu programowania, jak również w kwestii prawa. Artykuł może zawierać błędy w kwestii prawa, dlatego przed wyborem któregoś z zaprezentowanych rozwiązań warto sprawdzić czy licencje zostały poprawnie zidentyfikowane, oraz czy producenci bibliotek i kodeków nie zmienili licencji.
Autorzy artykułu
Rok napisania: 2025.