J.S. Cruz

Kadio: basic audio player

As of the last post, we have a very basic, two-column window that displays text read from a file.

In this post, we’re going to add the ability to play audio files. We should be able to pause and play the audio. The left-side pane should list audio file names, which we can click on to play.

Qt Multimedia

Qt’s Multimedia API makes it easy to play audio files. We just need to create a QMediaPlayer object, give it a file with setMedia(), and set it to play with play().

setMedia() takes a QMediaContent object which can be a playlist, a network request, or a QUrl, pointing to a local file or to a remote stream. We’ll use a local music file, so we’ll give it a QUrl from QUrl::fromLocalFile(). Note that the path to the file needs to be absolute.

kadio.cpp:

1kadio::kadio(const QVector<QString>& words, QWidget *parent)
2    : KXmlGuiWindow(parent)
3{
4    // [... previous setup ...]
5    QMediaPlayer* mediaplayer = new QMediaPlayer(this);
6    mediaplayer->setMedia(QUrl::fromLocalFile("/home/cruz/kadio/Cantique de Noël.mp3"));
7    mediaplayer->play();
8}

You can download the music I used here. When you run the program, the music automatically starts playing.

We could have just as well passed, say, QUrl("https://dispatcher.rndfnk.com/br/brklassik/live/mp3/high") to setMedia(), and our program would play BR Klassik’s current broadcast.

The above URL is actually a 302-redirect to a different website where the music stream is actually located. QUrl takes care of this automatically.

Play button

Of course, we need a way to control the playback. To do this, we can use a simple QPushButton. We can specify the button’s icon and text in the constructor, both of which should change according to the current playback state.

You can install a program like Cuttlefish to browse through icons, or you can poke around in /usr/share/icons/ for all the icons in all your installed themes. Freedesktop.org also defines a number of standard icons.

Since our program automatically plays the audio, the button should start with the pause icon.

Whenever a button is pressed, it emits the signal QPushButton::clicked(). We can define a new function, playPauseMedia, and connect the signal to it.

kadio.cpp:

 1kadio::kadio(const QVector<QString>& words, QWidget *parent)
 2    : KXmlGuiWindow(parent)
 3{
 4    // [... layout setup ...]
 5    QPushButton* play_button = new QPushButton(QIcon::fromTheme("media-pause"), "Pause", window);
 6    main_layout->addWidget(play_button);
 7    connect(play_button, &QPushButton::clicked, this, &kadio::playPauseMedia);
 8
 9    this->setCentralWidget(window);
10    // [... media setup ...]
11}

kadio.h:

1// [... rest of class ...]
2public slots:
3    void playPauseMedia();
4};

In the playPauseMedia function, we need to query the playback state and, if it’s running, pause() it. We also need to change the button’s icon and text. However, to do this, we need to access the play_button and mediaplayer objects that we’ve defined in the constructor.

To retrieve the button, we can navigate directly through the widget tree. We inserted the button as the central widget’s second child. The simplest way of doing this index-based navigation is to access the target widget via layouts:

1QPushButton* button = static_cast<QPushButton*>(this->centralWidget()->layout()->itemAt(1)->widget());

QObject (the base class of all Qt objects) also defines a function, children(), which returns a list of all the objects’s children, not just its direct descendants. We could use that, but it wouldn’t be as directly applicable.

To access the media object, we have to go at it in a different way, since it is not a widget, and therefore not a part of the widget tree. Luckily, QObject provides two functions, findChild() and findChildren(), which return one (resp. all) child widget of the type.

We can also control whether we want to search in all descendants or just in the direct ones. Since the QMediaPlayer object was attached directly to this, we can call it directly.

QMediaPlayer* mediaplayer = this->findChild<QMediaPlayer*>();

Having done this, changing the text and playing status is easy:

kadio.cpp:

 1void kadio::playPauseMedia()
 2{
 3    QPushButton* button = static_cast<QPushButton*>(this->centralWidget()->layout()->itemAt(1)->widget());
 4    QMediaPlayer* mediaplayer = this->findChild<QMediaPlayer*>();
 5    if (mediaplayer->state() == QMediaPlayer::PlayingState) {
 6        mediaplayer->pause();
 7        button->setIcon(QIcon::fromTheme("media-play"));
 8        button->setText("Play");
 9    }
10    else {
11        mediaplayer->play();
12        button->setIcon(QIcon::fromTheme("media-pause"));
13        button->setText("Pause");
14    }
15}

Of course, navigating through the widget tree is very silly and, while findChild() works fine, it’s somewhat brittle to future changes (what if we add another button)? We can give names to Qt objects with QObject::setObjectName() and use them as arguments to findChild/findChildren, but it seems much easier to just store a pointer to the objects in the kadio class if we know we’re going to access them frequently.

kadio.h:

 1class kadio : public KXmlGuiWindow
 2{
 3    Q_OBJECT
 4
 5private:
 6    QMediaPlayer* mediaplayer;
 7    QPushButton* play_button;
 8
 9public:
10    explicit kadio(const QVector<QString>& words, QWidget *parent = nullptr);
11    ~kadio() override;
12
13public slots:
14    void playPauseMedia();
15};

Don’t forget to remove the local declarations in the constructor:

kadio.cpp:

 1kadio::kadio(const QVector<QString>& words, QWidget *parent)
 2    : KXmlGuiWindow(parent)
 3{
 4    // [... layout setup ...]
 5
 6    play_button = new QPushButton(QIcon::fromTheme("media-pause"), "Pause");
 7    main_layout->addWidget(play_button);
 8    connect(play_button, &QPushButton::clicked, this, &kadio::playPauseMedia);
 9
10    this->setCentralWidget(window);
11
12    mediaplayer = new QMediaPlayer(this);
13    mediaplayer->setMedia(QUrl::fromLocalFile("/home/cruz/kadio/Cantique de Noël.mp3"));
14    mediaplayer->play();
15}
16
17
18void kadio::playPauseMedia()
19{
20    if (mediaplayer->state() == QMediaPlayer::PlayingState) {
21        mediaplayer->pause();
22        play_button->setIcon(QIcon::fromTheme("media-play"));
23        play_button->setText("Play");
24    }
25    else {
26        mediaplayer->play();
27        play_button->setIcon(QIcon::fromTheme("media-pause"));
28        play_button->setText("Pause");
29    }
30}
Media player with list and working button
Media player with list and working button

Interactive list

We have a functioning player and play/pause button, but the audio is still hard-coded. Let’s use the list of words from the last post to put paths to audio files, show them on the left pane and, when we click on one, change the currently playing media.

Clicking on a QLabel calls the event handler QLabel::mousePressEvent(), which processes the mouse click. We cannot customize this function directly from QLabel (plus, it’s protected), so we need subclass QLabel and change it there. Our new class will be called StationListItem.

The idea is that we’ll define a new signal, labelClicked, with a QString parameter. Since all the strings we’re passing around are stored in kadio, we don’t need to create extra copies, so we’ll make it a const reference to QString. This new signal will be emitted by StationListItem::mousePressEvent(), but only if the user presses the left mouse button.

station_list_item.h:

 1class StationListItem : public QLabel
 2{
 3    Q_OBJECT
 4
 5public:
 6    explicit StationListItem(const QString& label_text);
 7    ~StationListItem () override;
 8
 9    void mousePressEvent(QMouseEvent* event) override;
10
11signals:
12    void labelClicked(const QString& my_text);
13};

station_list_item.cpp:

1void StationListItem::mousePressEvent(QMouseEvent* event)
2{
3    if (event->button() == Qt::LeftButton) {
4        emit labelClicked(this->text());
5    }
6}

Remember to add it to CMakeLists.txt:

[...]
set(kadio_SRC
  src/main.cpp
  src/kadio.cpp
  src/station_list_item.cpp
)
[...]

We can now use this class instead of QLabel when we’re filling the left pane, taking care to connect each object appropriately.

kadio.cpp:

 1kadio::kadio(const QVector<QString>& words, QWidget *parent)
 2    : KXmlGuiWindow(parent)
 3{
 4    // [... window setup ...]
 5
 6    for (const QString& w: words) {
 7        auto line = new StationListItem(w);
 8        connect(line, &StationListItem::labelClicked, this, &kadio::changeTrack);
 9        left_pane_layout->addWidget(line);
10    }
11    // [... media and button setup ...]
12}

Now, whenever labelClicked() is emitted – whenever we click on a line – kadio::changeTrack() will be called with the label’s text as argument. Defining it is pretty simple:

kadio.cpp:

1void kadio::changeTrack(const QString& new_track)
2{
3    mediaplayer->setMedia(QUrl::fromLocalFile(new_track));
4    mediaplayer->play();
5    play_button->setIcon(QIcon::fromTheme("media-pause"));
6    play_button->setText("Pause");
7}

This works! Of course, the left pane doesn’t look too good, since it’s just a list. Also, note that the button expands horizontally to fill its part of the window, if the window is resized.

We’ll deal with those stylistic things in a future post.

Conclusion

Ignoring the fact we have to put full paths to audio files in a file, we have a proper audio player!

In the next post, we’ll define some menus, tool bars and status bars, to have our program start looking a bit more decent. As of now, our git repository looks like this.

Tags: #kde #kadio