Skip to content

Commit 548094e

Browse files
committed
feat: store tracklist as tag comment on recording
1 parent a3a4bc4 commit 548094e

19 files changed

Lines changed: 299 additions & 64 deletions

src/encoder/encoder.cpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,22 @@ EncoderRecordingSettingsPointer EncoderFactory::getEncoderRecordingSettings(Enco
155155
return std::make_shared<EncoderWaveSettings>(pConfig, ENCODING_WAVE);
156156
}
157157
}
158+
159+
void Encoder::addToTracklist(const QString& artist,
160+
const QString& title,
161+
std::chrono::seconds timestamp) {
162+
auto recordedDurationSeconds = timestamp.count();
163+
m_trackList.append(QStringLiteral("%1:%2:%3: %4 - %5")
164+
.arg(QString::number(recordedDurationSeconds / (60 * 60))
165+
.rightJustified(2, '0'), // hours
166+
QString::number((recordedDurationSeconds / 60) % 60)
167+
.rightJustified(2, '0'), // minutes
168+
QString::number(recordedDurationSeconds % 60)
169+
.rightJustified(2, '0'),
170+
artist.trimmed().isEmpty()
171+
? QObject::tr("(Unknown Artist)")
172+
: artist,
173+
title.trimmed().isEmpty()
174+
? QObject::tr("(Unknown Title)")
175+
: title));
176+
}

src/encoder/encoder.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <chrono>
34
#include <memory>
45

56
#include "encoder/encoderrecordingsettings.h"
@@ -33,11 +34,25 @@ class Encoder {
3334
virtual void encodeBuffer(const CSAMPLE* samples, const std::size_t bufferSize) = 0;
3435
// Adds metadata to the encoded audio, i.e., the ID3 tag. Currently only used
3536
// by EngineRecord, ShoutConnection does something different.
36-
virtual void updateMetaData(const QString& artist, const QString& title, const QString& album) = 0;
37+
virtual void updateMetaData(const QString& artist,
38+
const QString& title,
39+
const QString& album,
40+
std::chrono::seconds timestamp = {}) = 0;
3741
// called at the end when encoding is finished
3842
virtual void flush() = 0;
3943
// Setup the encoder with the specific settings
4044
virtual void setEncoderSettings(const EncoderSettings& settings) = 0;
45+
46+
protected:
47+
void addToTracklist(const QString& artist,
48+
const QString& title,
49+
std::chrono::seconds timestamp);
50+
const QStringList& getTrackList() const {
51+
return m_trackList;
52+
}
53+
54+
private:
55+
QStringList m_trackList;
4156
};
4257

4358
typedef std::shared_ptr<Encoder> EncoderPointer;

src/encoder/encoderfdkaac.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,9 +440,10 @@ void EncoderFdkAac::processFIFO() {
440440
}
441441
}
442442

443-
void EncoderFdkAac::updateMetaData(
444-
const QString& artist, const QString& title, const QString& album) {
445-
(void)artist, (void)title, (void)album;
443+
void EncoderFdkAac::updateMetaData(const QString&,
444+
const QString&,
445+
const QString&,
446+
std::chrono::seconds) {
446447
}
447448

448449
void EncoderFdkAac::flush() {

src/encoder/encoderfdkaac.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ class EncoderFdkAac : public Encoder {
1717
int initEncoder(mixxx::audio::SampleRate sampleRate,
1818
QString* pUserErrorMessage) override;
1919
void encodeBuffer(const CSAMPLE* samples, const std::size_t bufferSize) override;
20-
void updateMetaData(const QString& artist, const QString& title, const QString& album) override;
20+
void updateMetaData(const QString& artist,
21+
const QString& title,
22+
const QString& album,
23+
std::chrono::seconds timecode = {}) override;
2124
void flush() override;
2225
void setEncoderSettings(const EncoderSettings& settings) override;
2326

src/encoder/encoderffmpegcore.cpp

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,17 +184,10 @@ void EncoderFfmpegCore::encodeBuffer(const CSAMPLE* samples, const std::size_t b
184184
free(l_fNormalizedSamples);
185185
}
186186

187-
// Originally called from enginebroadcast.cpp to update metadata information
188-
// when streaming, however, this causes pops
189-
//
190-
// Currently this method is used before init() once to save artist, title and album
191-
//
192-
void EncoderFfmpegCore::updateMetaData(const QString& artist, const QString& title, const QString& album) {
193-
qDebug() << "ffmpegencodercore: UpdateMetadata: !" << artist << " - " << title <<
194-
" - " << album;
195-
m_strMetaDataTitle = title;
196-
m_strMetaDataArtist = artist;
197-
m_strMetaDataAlbum = album;
187+
void EncoderFfmpegCore::updateMetaData(const QString&,
188+
const QString&,
189+
const QString&,
190+
std::chrono::seconds) {
198191
}
199192

200193
int EncoderFfmpegCore::initEncoder(

src/encoder/encoderffmpegcore.h

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ extern "C" {
1919

2020
#include <QBuffer>
2121
#include <QByteArray>
22+
#include <QFile>
2223
#include <QLibrary>
2324

2425
#include "encoder/encoder.h"
@@ -44,7 +45,10 @@ class EncoderFfmpegCore : public Encoder {
4445
~EncoderFfmpegCore();
4546
int initEncoder(mixxx::audio::SampleRate sampleRate, QString* pUserErrorMessage) override;
4647
void encodeBuffer(const CSAMPLE* samples, const std::size_t bufferSize) override;
47-
void updateMetaData(const QString& artist, const QString& title, const QString& album) override;
48+
void updateMetaData(const QString& artist,
49+
const QString& title,
50+
const QString& album,
51+
std::chrono::seconds timecode = {}) override;
4852
void flush() override;
4953
void setEncoderSettings(const EncoderSettings& settings) override;
5054
protected:
@@ -70,9 +74,6 @@ class EncoderFfmpegCore : public Encoder {
7074
EncoderCallback* m_pCallback;
7175
TrackPointer m_pMetaData;
7276

73-
QString m_strMetaDataTitle;
74-
QString m_strMetaDataArtist;
75-
QString m_strMetaDataAlbum;
7677
QFile m_pFile;
7778

7879
QByteArray m_strReadByteArray;

src/encoder/encodermp3.cpp

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
#include "encoder/encodermp3.h"
22

3-
#include <limits.h>
4-
5-
#include <QObject>
6-
#include <QtDebug>
3+
#include <id3v2tag.h>
74

85
#include "audio/types.h"
96
#include "encoder/encodercallback.h"
107
#include "encoder/encodermp3settings.h"
118

9+
namespace {
10+
constexpr size_t kHeaderPadding = 10000;
11+
} // namespace
12+
1213
// Automatic thresholds for switching the encoder to mono
1314
// They have been chosen by testing and to keep the same number
1415
// of values for the slider.
@@ -134,9 +135,54 @@ void EncoderMp3::flush() {
134135
numBytes = lame_get_lametag_frame(
135136
m_lameFlags, m_bufferOut, m_bufferOutSize);
136137
}
137-
// Write the lame/xing header.
138+
139+
TagLib::ID3v2::Tag id3Tag;
140+
141+
// Ideally, we should shift the file content forward to insert the header
142+
// (ID3v2 & Xing frame) dynamically, but `EncoderCallback` doesn't support
143+
// that so we use the static header padding and truncate the tracklist if
144+
// too long
145+
if (!m_metaDataTitle.isEmpty()) {
146+
id3Tag.setTitle(QStringToTString(m_metaDataTitle));
147+
}
148+
if (!m_metaDataArtist.isEmpty()) {
149+
id3Tag.setArtist(QStringToTString(m_metaDataArtist));
150+
}
151+
if (!m_metaDataAlbum.isEmpty()) {
152+
id3Tag.setAlbum(QStringToTString(m_metaDataAlbum));
153+
}
154+
qsizetype tracklistMaxByteSize = kHeaderPadding -
155+
id3Tag.render().size();
156+
DEBUG_ASSERT(tracklistMaxByteSize > 0);
157+
158+
TagLib::String trackList;
159+
qsizetype currentTracklistByteSize = 0;
160+
161+
for (const auto& track : getTrackList()) {
162+
if (!trackList.isEmpty()) {
163+
trackList += "\n";
164+
currentTracklistByteSize += 1;
165+
}
166+
auto tagTrack = QStringToTString(track);
167+
currentTracklistByteSize += tagTrack.data(TagLib::String::Type::UTF8).size();
168+
// ellipse + 1 since we need a byte for the NULL terminator
169+
if (currentTracklistByteSize > tracklistMaxByteSize - 4) {
170+
trackList += "";
171+
break;
172+
} else {
173+
trackList += tagTrack;
174+
}
175+
}
176+
id3Tag.setComment(trackList);
177+
178+
TagLib::ByteVector id3Buffer = id3Tag.render();
179+
138180
m_pCallback->seek(0);
139-
m_pCallback->write(nullptr, m_bufferOut, 0, static_cast<int>(numBytes));
181+
// Write the lame/xing header.
182+
m_pCallback->write(reinterpret_cast<const unsigned char*>(id3Buffer.data()),
183+
m_bufferOut,
184+
static_cast<int>(id3Buffer.size()),
185+
static_cast<int>(numBytes));
140186
}
141187

142188
void EncoderMp3::encodeBuffer(const CSAMPLE* samples, const std::size_t bufferSize) {
@@ -176,6 +222,13 @@ void EncoderMp3::initStream() {
176222

177223
m_bufferIn[0] = (float *)malloc(m_bufferOutSize * sizeof(float));
178224
m_bufferIn[1] = (float *)malloc(m_bufferOutSize * sizeof(float));
225+
226+
// Add a static header padding, which will be filled one termination
227+
QByteArray headerPad(kHeaderPadding, 0);
228+
m_pCallback->write(nullptr,
229+
reinterpret_cast<const unsigned char*>(headerPad.constData()),
230+
0,
231+
headerPad.size());
179232
}
180233

181234
int EncoderMp3::initEncoder(mixxx::audio::SampleRate sampleRate,
@@ -234,17 +287,7 @@ int EncoderMp3::initEncoder(mixxx::audio::SampleRate sampleRate,
234287

235288
lame_set_quality(m_lameFlags, 2);
236289

237-
//ID3 Tag if fields are not NULL
238-
id3tag_init(m_lameFlags);
239-
if (!m_metaDataTitle.isEmpty()) {
240-
id3tag_set_title(m_lameFlags, m_metaDataTitle.toLatin1().constData());
241-
}
242-
if (!m_metaDataArtist.isEmpty()) {
243-
id3tag_set_artist(m_lameFlags, m_metaDataArtist.toLatin1().constData());
244-
}
245-
if (!m_metaDataAlbum.isEmpty()) {
246-
id3tag_set_album(m_lameFlags,m_metaDataAlbum.toLatin1().constData());
247-
}
290+
// ID3 tag will be written during flushing
248291

249292
int ret = lame_init_params(m_lameFlags);
250293
if (ret < 0) {
@@ -257,8 +300,15 @@ int EncoderMp3::initEncoder(mixxx::audio::SampleRate sampleRate,
257300
return 0;
258301
}
259302

260-
void EncoderMp3::updateMetaData(const QString& artist, const QString& title, const QString& album) {
261-
m_metaDataTitle = title;
262-
m_metaDataArtist = artist;
263-
m_metaDataAlbum = album;
303+
void EncoderMp3::updateMetaData(const QString& artist,
304+
const QString& title,
305+
const QString& album,
306+
std::chrono::seconds timecode) {
307+
if (m_bufferOut == nullptr) {
308+
m_metaDataTitle = title;
309+
m_metaDataArtist = artist;
310+
m_metaDataAlbum = album;
311+
} else {
312+
addToTracklist(artist, title, timecode);
313+
}
264314
}

src/encoder/encodermp3.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ class EncoderMp3 final : public Encoder {
1717
int initEncoder(mixxx::audio::SampleRate sampleRate,
1818
QString* pUserErrorMessage) override;
1919
void encodeBuffer(const CSAMPLE* samples, const std::size_t bufferSize) override;
20-
void updateMetaData(const QString& artist, const QString& title, const QString& album) override;
20+
void updateMetaData(const QString& artist,
21+
const QString& title,
22+
const QString& album,
23+
std::chrono::seconds timecode = {}) override;
2124
void flush() override;
2225
void setEncoderSettings(const EncoderSettings& settings) override;
2326

src/encoder/encoderopus.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include "encoder/encoderopus.h"
22

3+
#include <qglobal.h>
4+
35
#include <QByteArray>
46
#include <QMapIterator>
57
#include <QRandomGenerator>
@@ -461,10 +463,17 @@ void EncoderOpus::writePage(ogg_packet* pPacket) {
461463
} while(!ogg_page_eos(&m_oggPage));
462464
}
463465

464-
void EncoderOpus::updateMetaData(const QString& artist, const QString& title, const QString& album) {
465-
m_opusComments.insert("ARTIST", artist);
466-
m_opusComments.insert("TITLE", title);
467-
m_opusComments.insert("ALBUM", album);
466+
void EncoderOpus::updateMetaData(const QString& artist,
467+
const QString& title,
468+
const QString& album,
469+
std::chrono::seconds) {
470+
// We assume all the base tags are added at the same time, so only check for ARTIST presence
471+
if (!m_opusComments.contains("ARTIST")) {
472+
m_opusComments.insert("ARTIST", artist);
473+
m_opusComments.insert("TITLE", title);
474+
m_opusComments.insert("ALBUM", album);
475+
}
476+
// Tracklist tag not supported in OPUS
468477
}
469478

470479
void EncoderOpus::flush() {

src/encoder/encoderopus.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ class EncoderOpus: public Encoder {
2929
int initEncoder(mixxx::audio::SampleRate sampleRate,
3030
QString* pUserErrorMessage) override;
3131
void encodeBuffer(const CSAMPLE* samples, const std::size_t bufferSize) override;
32-
void updateMetaData(const QString& artist, const QString& title, const QString& album) override;
32+
void updateMetaData(const QString& artist,
33+
const QString& title,
34+
const QString& album,
35+
std::chrono::seconds timecode = {}) override;
3336
void flush() override;
3437
void setEncoderSettings(const EncoderSettings& settings) override;
3538

0 commit comments

Comments
 (0)