new way of logging, some tags fixes

This commit is contained in:
Blue 2023-07-23 09:04:26 -03:00
parent c5eafe7d98
commit 2c913d5f54
Signed by: blue
GPG Key ID: 9B203B252A63EE38
9 changed files with 253 additions and 68 deletions

View File

@ -1,8 +1,15 @@
# Changelog
## MLC 1.1.0 (July 23, 2023)
- New logging system
- Artist, Album artist, Album and Title are now utf16 encoded, should fix broten titles
- BPM tag is now rounded, as it supposed to be by spec
- Lyrics is not set now for USLT tag for unsychronized lyrics is not supported in LAME
- Date bug fix, it was MMDD instead of standard DDMM
## MLC 1.0.1 (July 21, 2023)
### Added multithreaded encoding
### Only the first artist vorbis tag is left in the id3 tags
- Added multithreaded encoding
- Only the first artist vorbis tag is left in the id3 tags
## MLC 1.0.0 (July 19, 2023)
### Initial release, only core functionality
- Initial release, only core functionality

View File

@ -23,6 +23,7 @@ add_executable(mlc
flactomp3.cpp
collection.cpp
taskmanager.cpp
loggable.cpp
)
target_link_libraries(mlc

View File

@ -1,16 +1,21 @@
#include "flactomp3.h"
#include <cmath>
constexpr uint16_t flacDefaultMaxBlockSize = 4096;
constexpr uint16_t id3v1TagSize = 128;
// constexpr std::wstring_view BOM ({(0xFEFF}); //one day...
static const std::string_view title ("TITLE=");
static const std::string_view artist ("ARTIST=");
static const std::string_view album ("ALBUM=");
static const std::string_view comment ("COMMENT=");
static const std::string_view genre ("GENRE=");
static const std::string_view track ("TRACKNUMBER=");
static const std::string_view date ("DATE=");
constexpr std::string_view title ("TITLE=");
constexpr std::string_view artist ("ARTIST=");
constexpr std::string_view album ("ALBUM=");
constexpr std::string_view comment ("COMMENT=");
constexpr std::string_view genre ("GENRE=");
constexpr std::string_view track ("TRACKNUMBER=");
constexpr std::string_view date ("DATE=");
constexpr std::string_view jpeg ("image/jpeg");
static const std::string_view jpeg ("image/jpeg");
static const std::map<std::string, const std::string> knownTags {
{"ALBUMARTIST=", "TPE2="},
@ -18,10 +23,12 @@ static const std::map<std::string, const std::string> knownTags {
{"LENGTH=", "TLEN="},
{"ISRC=", "TSRC="},
{"DISCNUMBER=", "TPOS="},
{"BPM=", "TBPM="}
{"BPM=", "TBPM="},
{"LYRICS=", "USLT="} //but it's not supported in LAME
};
FLACtoMP3::FLACtoMP3(uint8_t size) :
FLACtoMP3::FLACtoMP3(Severity severity, uint8_t size) :
Loggable(severity),
inPath(),
outPath(),
decoder(FLAC__stream_decoder_new()),
@ -36,7 +43,8 @@ FLACtoMP3::FLACtoMP3(uint8_t size) :
outputBuffer(nullptr),
outputBufferSize(0),
outputInitilized(false),
downscaleAlbumArt(false)
downscaleAlbumArt(false),
usc2convertor()
{
}
@ -47,6 +55,7 @@ FLACtoMP3::~FLACtoMP3() {
bool FLACtoMP3::run() {
FLAC__bool ok = FLAC__stream_decoder_process_until_end_of_stream(decoder);
uint32_t fileSize;
if (ok) {
if (pcmCounter > 0)
flush();
@ -55,24 +64,26 @@ bool FLACtoMP3::run() {
fwrite((char*)outputBuffer, nwrite, 1, output);
if (downscaleAlbumArt) {
fileSize = ftell(output);
lame_mp3_tags_fid(encoder, output);
} else {
int tag1Size = lame_get_id3v1_tag(encoder, outputBuffer, 128);
if (tag1Size > 128)
std::cout << std::endl << "couldn't write id3v1 tag";
int tag1Size = lame_get_id3v1_tag(encoder, outputBuffer, id3v1TagSize);
if (tag1Size > id3v1TagSize)
log(warning, "couldn't write id3v1 tag");
else
fwrite((char*)outputBuffer, tag1Size, 1, output);
fileSize = ftell(output);
fseek(output, 0, SEEK_SET);
int tag2Size = lame_get_id3v2_tag(encoder, outputBuffer, outputBufferSize);
if (tag2Size > outputBufferSize) {
std::cout << std::endl << "couldn't write id3v1 tag";
log(Loggable::error, "couldn't write id3v2 tag");
} else
fwrite((char*)outputBuffer, tag2Size, 1, output);
int vbrTagSize = lame_get_lametag_frame(encoder, outputBuffer, outputBufferSize);
if (vbrTagSize > outputBufferSize)
std::cout << std::endl << "couldn't write vbr tag";
log(Loggable::error, "couldn't write vbr tag");
fwrite((char*)outputBuffer, vbrTagSize, 1, output);
}
@ -92,6 +103,12 @@ bool FLACtoMP3::run() {
pcmSize = 0;
flacMaxBlockSize = 0;
outputBufferSize = 0;
if (ok) {
float MBytes = (float)fileSize / 1024 / 1024;
std::string strMBytes = std::to_string(MBytes);
strMBytes = strMBytes.substr(0, strMBytes.find(".") + 3) + " MiB";
log(info, "resulting file size: " + strMBytes);
}
return ok;
}
@ -130,13 +147,13 @@ bool FLACtoMP3::initializeOutput() {
output = fopen(outPath.c_str(), "w+b");
if (output == 0) {
output = nullptr;
std::cout << "Error opening file " << outPath << std::endl;
log(fatal, "Error opening file " + outPath);
return false;
}
int ret = lame_init_params(encoder);
if (ret < 0) {
std::cout << "Error occurred during parameters initializing. Code = " << ret << std::endl;
log(fatal, "Error initializing LAME parameters. Code = " + std::to_string(ret));
fclose(output);
output = nullptr;
return false;
@ -169,7 +186,9 @@ void FLACtoMP3::processInfo(const FLAC__StreamMetadata_StreamInfo& info) {
lame_set_in_samplerate(encoder, info.sample_rate);
lame_set_num_channels(encoder, info.channels);
flacMaxBlockSize = info.max_blocksize;
//std::cout << "bits per sample: " << info.bits_per_sample << std::endl;;
log(Loggable::info, "sample rate: " + std::to_string(info.sample_rate));
log(Loggable::info, "channels: " + std::to_string(info.channels));
log(Loggable::info, "bits per sample: " + std::to_string(info.bits_per_sample));
}
void FLACtoMP3::processTags(const FLAC__StreamMetadata_VorbisComment& tags) {
@ -178,14 +197,16 @@ void FLACtoMP3::processTags(const FLAC__StreamMetadata_VorbisComment& tags) {
const FLAC__StreamMetadata_VorbisComment_Entry& entry = tags.comments[i]; //gonna keep only the first one for now
std::string_view comm((const char*)entry.entry);
if (comm.find(title) == 0) {
id3tag_set_title(encoder, comm.substr(title.size()).data());
setTagTitle(comm.substr(title.size()));
} else if (comm.find(artist) == 0) {
if (!artistSet) {
id3tag_set_artist(encoder, comm.substr(artist.size()).data());
setTagArtist(comm.substr(artist.size()));
artistSet = true;
} else {
log(minor, "more than one artist tag, ignoring " + std::string(comm.substr(artist.size())));
}
} else if (comm.find(album) == 0) {
id3tag_set_album(encoder, comm.substr(album.size()).data());
setTagAlbum(comm.substr(album.size()));
} else if (comm.find(comment) == 0) {
id3tag_set_comment(encoder, comm.substr(comment.size()).data());
} else if (comm.find(genre) == 0) {
@ -194,13 +215,13 @@ void FLACtoMP3::processTags(const FLAC__StreamMetadata_VorbisComment& tags) {
id3tag_set_track(encoder, comm.substr(track.size()).data());
} else if (comm.find(date) == 0) {
std::string_view fullDate = comm.substr(date.size());
if (fullDate.size() == 10) {
if (fullDate.size() == 10) { //1999-11-11 kind of string
std::string_view month = fullDate.substr(5, std::size_t(2));
std::string_view day = fullDate.substr(8);
std::string md = "TDAT=" + std::string(month) + std::string(day);
std::string md = "TDAT=" + std::string(day) + std::string(month);
int res = id3tag_set_fieldvalue(encoder, md.c_str());
if (res != 0)
std::cout << "wasn't able to set the date tag (" << md << ")" << std::endl;
log(warning, "wasn't able to set the date tag (" + md + ")");
fullDate = fullDate.substr(0, 4); //year;
}
@ -209,27 +230,25 @@ void FLACtoMP3::processTags(const FLAC__StreamMetadata_VorbisComment& tags) {
std::string tag = "TXXX=" + std::string(comm);
int res = id3tag_set_fieldvalue(encoder, tag.c_str());
if (res != 0)
std::cout << "wasn't able to set user tag (" << comm << ")" << std::endl;
log(warning, "wasn't able to set user tag (" + tag + ")");
}
}
}
void FLACtoMP3::processPicture(const FLAC__StreamMetadata_Picture& picture) {
if (downscaleAlbumArt && picture.data_length > LAME_MAXALBUMART) {
std::cout << "embeded picture is too big (" << picture.data_length << " bytes) " << std::endl;
std::cout << "mime type is " << picture.mime_type << std::endl;
log(info, "embeded album art is too big (" + std::to_string(picture.data_length) + " bytes), rescaling");
log(debug, "mime type is " + std::string(picture.mime_type));
if (picture.mime_type == jpeg) {
if (scaleJPEG(picture)) {
std::cout << "successfully scaled album art" << std::endl;
} else {
std::cout << "failed to album art" << std::endl;
}
if (scaleJPEG(picture))
log(debug, "successfully rescaled album art");
else
log(warning, "failed to rescale album art");
}
} else {
int result = id3tag_set_albumart(encoder, (const char*)picture.data, picture.data_length);
if (result != 0) {
std::cout << "couldn't set album art tag, errcode: " << result << std::endl;
}
if (result != 0)
log(warning, "couldn't set album art tag, errcode: " + std::to_string(result));
}
}
@ -243,11 +262,11 @@ bool FLACtoMP3::scaleJPEG(const FLAC__StreamMetadata_Picture& picture) {
int rc = jpeg_read_header(&dinfo, TRUE);
if (rc != 1) {
std::cout << "error reading jpeg header" << std::endl;
log(Loggable::error, "error reading jpeg header");
return false;
}
uint64_t mem_size = LAME_MAXALBUMART;
uint8_t *mem = new uint8_t[mem_size];
uint64_t mem_size = 0;
uint8_t *mem = new uint8_t[LAME_MAXALBUMART + 1024 * 4]; //I allocate a little bit more not to corrupt someone else's the memory
dinfo.scale_num = 2; //need to tune it, feels like 500 by 500 is a good size
dinfo.scale_denom = 3;
@ -273,6 +292,12 @@ bool FLACtoMP3::scaleJPEG(const FLAC__StreamMetadata_Picture& picture) {
while (dinfo.output_scanline < dinfo.output_height) {
jpeg_read_scanlines(&dinfo, &row, 1);
jpeg_write_scanlines(&cinfo, &row, 1);
if (mem_size > LAME_MAXALBUMART) {
delete[] mem;
log(Loggable::error, "resized album art exceeded reserved buffer size, stopping before memory corrution");
return false;
}
}
jpeg_finish_decompress(&dinfo);
jpeg_destroy_decompress(&dinfo);
@ -291,10 +316,24 @@ bool FLACtoMP3::scaleJPEG(const FLAC__StreamMetadata_Picture& picture) {
bool FLACtoMP3::tryKnownTag(std::string_view source) {
for (const std::pair<const std::string, const std::string>& pair : knownTags) {
if (source.find(pair.first) == 0) {
std::string tag = pair.second + std::string(source.substr(pair.first.size()));
int res = id3tag_set_fieldvalue(encoder, tag.c_str());
if (res != 0)
std::cout << "wasn't able to set tag (" << source << ")" << std::endl;
if (pair.first == "LYRICS=") {
log(warning, "lyrics tag was not transfered, it is not supported currently");
} else if (pair.first == "BPM=") {
float bpm = std::atof(source.substr(pair.first.size()).data());
bpm = std::round(bpm); //id3 bpm must be integer
int result = id3tag_set_fieldvalue(encoder, std::string(pair.second + std::to_string((int)bpm)).c_str());
if (result != 0)
log(warning, "wasn't able to set BPM tag. Code = " + std::to_string(result));
} else if (pair.first == "ALBUMARTIST=") {
int res = setTagUSC2("TPE2", source.substr(pair.first.size()));
if (res != 0)
log(warning, "wasn't able to album artist tag. Code = " + std::to_string(res));
} else {
std::string tag = pair.second + std::string(source.substr(pair.first.size()));
int res = id3tag_set_fieldvalue(encoder, tag.c_str());
if (res != 0)
log(warning, "wasn't able to set tag (" + std::string(source) + "). Code = " + std::to_string(res));
}
return true;
}
@ -331,11 +370,11 @@ bool FLACtoMP3::flush() {
outputBufferSize
);
while (nwrite == -1) { //-1 is returned when there was not enough space in the given buffer
std::cout << outputBufferSize << " bytes in the output buffer wasn't enough" << std::endl;
log(major, std::to_string(outputBufferSize) + " bytes in the output buffer wasn't enough");;
outputBufferSize = outputBufferSize * 2;
delete[] outputBuffer;
outputBuffer = new uint8_t[outputBufferSize];
std::cout << "allocating " << outputBufferSize << " bytes" << std::endl;
log(major, "allocating " + std::to_string(outputBufferSize) + " bytes");
nwrite = lame_encode_buffer_interleaved(
encoder,
@ -352,10 +391,10 @@ bool FLACtoMP3::flush() {
return actuallyWritten == 1;
} else {
if (nwrite == 0) {
//std::cout << "encoding flush encoded 0 bytes, skipping write" << std::endl;
log(minor, "encoding flush encoded 0 bytes, skipping write");
return true;
} else {
std::cout << "encoding flush failed, error: " << nwrite << std::endl;
log(fatal, "encoding flush failed. Code = : " + std::to_string(nwrite));
return false;
}
}
@ -391,20 +430,20 @@ FLAC__StreamDecoderWriteStatus FLACtoMP3::write(
// std::cout << "ERROR: this example only supports 16bit stereo streams" << std::endl;
// return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
// }
FLACtoMP3* self = static_cast<FLACtoMP3*>(client_data);
if (frame->header.channels != 2) {
std::cout << "ERROR: This frame contains " << frame->header.channels << " channels (should be 2)" << std::endl;
self->log(fatal, "ERROR: This frame contains " + std::to_string(frame->header.channels) + " channels (should be 2)");
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
if (buffer[0] == NULL) {
std::cout << "ERROR: buffer [0] is NULL" << std::endl;
self->log(fatal, "ERROR: buffer [0] is NULL");
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
if (buffer[1] == NULL) {
std::cout << "ERROR: buffer [1] is NULL" << std::endl;
self->log(fatal, "ERROR: buffer [1] is NULL");
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
FLACtoMP3* self = static_cast<FLACtoMP3*>(client_data);
bool result = self->decodeFrame(buffer, frame->header.blocksize);
if (result)
@ -414,6 +453,34 @@ FLAC__StreamDecoderWriteStatus FLACtoMP3::write(
}
void FLACtoMP3::error(const FLAC__StreamDecoder* decoder, FLAC__StreamDecoderErrorStatus status, void* client_data) {
(void)decoder, (void)client_data;
std::cout << "Got error callback: " << FLAC__StreamDecoderErrorStatusString[status] << std::endl;
(void)decoder;
FLACtoMP3* self = static_cast<FLACtoMP3*>(client_data);
std::string errText(FLAC__StreamDecoderErrorStatusString[status]);
self->log(Loggable::error, "Got error callback: " + errText);
}
void FLACtoMP3::setTagTitle(const std::string_view& title) {
id3tag_set_title(encoder, title.data()); //to set at least some possibly encoding broken id3v1 tag
int res = setTagUSC2("TIT2", title);
if (res != 0)
log(warning, "Couldn't set Title tag. Code = " + std::to_string(res));
}
void FLACtoMP3::setTagAlbum(const std::string_view& album) {
id3tag_set_album(encoder, album.data()); //to set at least some possibly encoding broken id3v1 tag
int res = setTagUSC2("TALB", album);
if (res != 0)
log(warning, "Couldn't set Album tag. Code = " + std::to_string(res));
}
void FLACtoMP3::setTagArtist(const std::string_view& artist) {
id3tag_set_artist(encoder, artist.data()); //to set at least some possibly encoding broken id3v1 tag
int res = setTagUSC2("TPE1", artist);
if (res != 0)
log(warning, "Couldn't set Artist tag. Code = " + std::to_string(res));
}
int FLACtoMP3::setTagUSC2(const std::string_view& field, const std::string_view& value) {
std::u16string utf16 = std::u16string({0xFEFF}) + usc2convertor.from_bytes(value.data());
return id3tag_set_textinfo_utf16(encoder, field.data(), (unsigned short*)utf16.c_str());
}

View File

@ -6,13 +6,16 @@
#include <string>
#include <string_view>
#include <iostream>
#include <codecvt>
#include <locale>
#include <map>
#include <stdio.h>
class FLACtoMP3 {
#include "loggable.h"
class FLACtoMP3 : public Loggable {
public:
FLACtoMP3(uint8_t size = 4);
FLACtoMP3(Severity severity = info, uint8_t size = 4);
~FLACtoMP3();
void setInputFile(const std::string& path);
@ -29,6 +32,12 @@ private:
bool tryKnownTag(std::string_view source);
bool scaleJPEG(const FLAC__StreamMetadata_Picture& picture);
void setTagTitle(const std::string_view& title);
void setTagAlbum(const std::string_view& album);
void setTagArtist(const std::string_view& artist);
int setTagUSC2(const std::string_view& field, const std::string_view& value);
static void error(const FLAC__StreamDecoder *decoder, FLAC__StreamDecoderErrorStatus status, void *client_data);
static void metadata(const FLAC__StreamDecoder *decoder, const FLAC__StreamMetadata *metadata, void *client_data);
static FLAC__StreamDecoderWriteStatus write(
@ -56,4 +65,5 @@ private:
uint32_t outputBufferSize;
bool outputInitilized;
bool downscaleAlbumArt;
std::wstring_convert<std::codecvt_utf8<char16_t>, char16_t> usc2convertor;
};

18
loggable.cpp Normal file
View File

@ -0,0 +1,18 @@
#include "loggable.h"
Loggable::Loggable(Loggable::Severity severity):
currentSeverity(severity),
history()
{}
Loggable::~Loggable()
{}
void Loggable::log(Loggable::Severity severity, const std::string& comment) const {
if (severity >= currentSeverity)
history.emplace_back(severity, comment);
}
std::list<std::pair<Loggable::Severity, std::string>> Loggable::getHistory() const {
return history;
}

28
loggable.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <list>
#include <string>
class Loggable {
public:
enum Severity {
debug,
info,
minor,
major,
warning,
error,
fatal
};
typedef std::pair<Severity, std::string> Message;
Loggable(Severity severity);
~Loggable();
void log (Severity severity, const std::string& comment) const;
std::list<Message> getHistory() const;
private:
const Severity currentSeverity;
mutable std::list<Message> history;
};

View File

@ -17,13 +17,13 @@ int main(int argc, char **argv) {
}
const std::string firstArgument(argv[1]);
const std::string secondArgument(argv[2]);
if (firstArgument == "--help") {
printHelp();
return 0;
}
const std::string secondArgument(argv[2]);
TaskManager taskManager;
taskManager.start();
@ -35,7 +35,7 @@ int main(int argc, char **argv) {
taskManager.wait();
std::chrono::time_point end = std::chrono::system_clock::now();
std::chrono::duration<double> seconds = end - start;
std::cout << std::endl << "took " << seconds.count() << std::endl;
std::cout << std::endl << "Encoding is done, it took " << seconds.count() << " seconds in total, enjoy!" << std::endl;
taskManager.stop();

View File

@ -2,11 +2,32 @@
#include "flactomp3.h"
constexpr const std::array<std::string_view, Loggable::fatal + 1> logSettings({
/*debug*/ "\e[90m",
/*info*/ "\e[32m",
/*minor*/ "\e[34m",
/*major*/ "\e[94m",
/*warning*/ "\e[33m",
/*error*/ "\e[31m",
/*fatal*/ "\e[91m"
});
constexpr const std::array<std::string_view, Loggable::fatal + 1> logHeaders({
/*debug*/ "DEBUG: ",
/*info*/ "INFO: ",
/*minor*/ "MINOR: ",
/*major*/ "MAJOR: ",
/*warning*/ "WARNING: ",
/*error*/ "ERROR: ",
/*fatal*/ "FATAL: "
});
TaskManager::TaskManager():
busyThreads(0),
maxTasks(0),
completeTasks(0),
terminate(false),
printMutex(),
queueMutex(),
busyMutex(),
loopConditional(),
@ -62,9 +83,9 @@ void TaskManager::loop() {
jobs.pop();
}
job(pair.first, pair.second);
JobResult result = job(pair.first, pair.second);
++completeTasks;
printProgress();
printProgress(result, pair.first, pair.second);
--busyThreads;
waitConditional.notify_all();
}
@ -96,13 +117,38 @@ void TaskManager::wait() {
waitConditional.wait(lock, boundWaitCondition);
}
bool TaskManager::job(const std::string& source, const std::string& destination) {
TaskManager::JobResult TaskManager::job(const std::string& source, const std::string& destination) {
FLACtoMP3 convertor;
convertor.setInputFile(source);
convertor.setOutputFile(destination);
return convertor.run();
bool result = convertor.run();
return {result, convertor.getHistory()};
}
void TaskManager::printProgress() const {
std::unique_lock<std::mutex> lock(printMutex);
std::cout << "\r" << completeTasks << "/" << maxTasks << std::flush;
}
void TaskManager::printProgress(const TaskManager::JobResult& result, const std::string& source, const std::string& destination) const {
std::unique_lock<std::mutex> lock(printMutex);
if (result.first) {
if (result.second.size() > 0) {
std::cout << "\r\e[1m" << "Encoding complete but there are messages about it" << "\e[0m" << std::endl;
printLog(result, source, destination);
}
} else {
std::cout << "\r\e[1m" << "Encoding failed!" << "\e[0m" << std::endl;
printLog(result, source, destination);
}
std::cout << "\r" << completeTasks << "/" << maxTasks << std::flush;
}
void TaskManager::printLog(const TaskManager::JobResult& result, const std::string& source, const std::string& destination) {
std::cout << "Source: \t" << source << std::endl;
std::cout << "Destination: \t" << destination << std::endl;
for (const Loggable::Message& msg : result.second) {
std::cout << "\t" << logSettings[msg.first] << "\e[1m" << logHeaders[msg.first] << "\e[22m" << msg.second << std::endl;
}
std::cout << "\e[0m" << std::endl;
}

View File

@ -5,11 +5,17 @@
#include <thread>
#include <functional>
#include <vector>
#include <list>
#include <queue>
#include <string>
#include <atomic>
#include <iostream>
#include <array>
#include "loggable.h"
class TaskManager {
typedef std::pair<bool, std::list<Loggable::Message>> JobResult;
public:
TaskManager();
~TaskManager();
@ -20,18 +26,20 @@ public:
bool busy() const;
void wait();
void printProgress() const;
void printProgress(const JobResult& result, const std::string& source, const std::string& destination) const;
private:
void loop();
bool loopCondition() const;
bool waitCondition() const;
static bool job(const std::string& source, const std::string& destination);
static JobResult job(const std::string& source, const std::string& destination);
static void printLog(const JobResult& result, const std::string& source, const std::string& destination);
private:
std::atomic<uint32_t> busyThreads;
std::atomic<uint32_t> maxTasks;
std::atomic<uint32_t> completeTasks;
bool terminate;
mutable std::mutex printMutex;
mutable std::mutex queueMutex;
std::mutex busyMutex;
std::condition_variable loopConditional;