Compare commits

..

2 Commits

12 changed files with 92 additions and 117 deletions

View File

@ -1,10 +1,7 @@
# Changelog
## MLC 1.3.4 (March 30, 2025)
## MLC 1.3.4 (UNRELEASED)
- Build fixes
- Source and Destination paths now can contain spaces
- Regex to copy non music files can have spaces
- A feature to define a regex to exclude paths from rendering
## MLC 1.3.3 (October 13, 2023)
- Regex to specify non-music files to copy
@ -31,7 +28,7 @@
- New logging system
- Artist, Album artist, Album and Title are now utf16 encoded, should fix broken titles
- BPM tag is now rounded, as it supposed to be by spec
- Lyrics is not set now for USLT tag for unsynchronized lyrics is not supported in LAME
- 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)

View File

@ -13,13 +13,13 @@ This is a program to compile your local lossless music library to lossy formats
### Building
```shell
git clone https://git.macaw.me/blue/mlc
cd mlc
mkdir build
cd build
cmake ..
cmake --build .
```sh
$ git clone https://git.macaw.me/blue/mlc
$ cd mlc
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
```
### Usage
@ -27,19 +27,19 @@ cmake --build .
Just to compile lossless library to lossy you can use this command
assuming you are in the same directory you have just built `MLC`:
```shell
```
./mlc path/to/lossless/library path/to/store/lossy/library
```
There are more ways to use `MLC`, please refer to help for more options:
```shell
./mlc help
```sh
$ ./mlc help
# or
./mlc --help
$ ./mlc --help
#
# or
./mlc -h
$ ./mlc -h
```
`MLC` has a way to configure conversion process, it will generate global for you user config on the first launch.
@ -49,16 +49,14 @@ You can also make local configs for each directory you launch mlc from.
To output the default config run the following, assuming you are in the same directory you have just built `MLC`:
```shell
./mlc config #to print
#or
./mlc config > config.conf #to save to config.conf in current directory
```sh
$ ./mlc config
```
To use non default config run the following, assuming you are in the same directory you have just built `MLC`:
```shell
./mlc path/to/lossless/library path/to/store/lossy/library -c path/to/config/file
```sh
$ ./mlc path/to/lossless/library path/to/store/lossy/library -c path/to/config/file
```
### About

View File

@ -6,12 +6,11 @@ namespace fs = std::filesystem;
static const std::string flac(".flac");
Collection::Collection(const std::filesystem::path& path, const std::shared_ptr<TaskManager>& tm, const std::shared_ptr<Settings>& st):
Collection::Collection(const std::filesystem::path& path, TaskManager* tm) :
path(path),
countMusical(0),
counted(false),
taskManager(tm),
settings(st)
taskManager(tm)
{}
Collection::~Collection()
@ -34,16 +33,13 @@ uint32_t Collection::countMusicFiles() const {
++countMusical;
} else if (fs::is_directory(path)) {
for (const fs::directory_entry& entry : fs::directory_iterator(path)) {
if (settings->isExcluded(entry.path()))
continue;
switch (entry.status().type()) {
case fs::file_type::regular:
if (isMusic(entry.path()))
++countMusical;
break;
case fs::file_type::directory: {
Collection collection(entry.path(), taskManager, settings);
Collection collection(entry.path());
countMusical += collection.countMusicFiles();
} break;
default:
@ -57,24 +53,25 @@ uint32_t Collection::countMusicFiles() const {
}
void Collection::convert(const std::string& outPath) {
if (taskManager == nullptr)
throw 6;
fs::path out = fs::absolute(outPath);
fs::create_directories(out);
out = fs::canonical(outPath);
for (const fs::directory_entry& entry : fs::directory_iterator(path)) {
fs::path sourcePath = entry.path();
if (settings->isExcluded(sourcePath))
continue;
switch (entry.status().type()) {
case fs::file_type::regular: {
fs::path sourcePath = entry.path();
if (isMusic(sourcePath))
taskManager->queueConvert(sourcePath, out / sourcePath.stem());
else
taskManager->queueCopy(sourcePath, out / sourcePath.filename());
} break;
case fs::file_type::directory: {
Collection collection(sourcePath, taskManager, settings);
fs::path sourcePath = entry.path();
Collection collection(sourcePath, taskManager);
fs::path::iterator itr = sourcePath.end();
--itr;
collection.convert(std::string(out / *itr));

View File

@ -3,16 +3,14 @@
#include <string>
#include <iostream>
#include <filesystem>
#include <memory>
#include "settings.h"
#include "flactomp3.h"
class TaskManager;
class Collection {
public:
Collection(const std::filesystem::path& path, const std::shared_ptr<TaskManager>& tm, const std::shared_ptr<Settings>& st);
Collection(const std::filesystem::path& path, TaskManager* tm = nullptr);
~Collection();
void list() const;
@ -26,7 +24,6 @@ private:
std::filesystem::path path;
mutable uint32_t countMusical;
mutable bool counted;
std::shared_ptr<TaskManager> taskManager;
std::shared_ptr<Settings> settings;
TaskManager* taskManager;
};

View File

@ -92,15 +92,7 @@
# Variable bitrate
# Switches on or off variable bitrate
# VBR files are usually smaller, but not supposed to be worse
# VBR files are usually smaller, but not supped to be worse
# in terms of quality. VBR files might be a bit more tricky for the player
# Allowed values are: [true, false]
#vbr true
# Exclude
# MLC renders any music file it finds in source directory
# UNLESS its path matches the following regex
# Allowed value is the regex without any additional syntax,
# for example: exclude [Ss]hamefull?\s[Ss]ong[Ss]
# If you don't want to exclude anything leave this option blank
#exclude

View File

@ -294,16 +294,28 @@ bool FLACtoMP3::scaleJPEG(const FLAC__StreamMetadata_Picture& picture) {
return true;
}
bool FLACtoMP3::decodeFrame(const int32_t * const buffer[], uint32_t size) {
bool FLACtoMP3::decodeFrame(const int32_t * const buffer[], uint32_t size, const uint32_t bitsPerSample) {
if (!outputInitilized) {
bool success = initializeOutput();
if (!success)
return false;
}
if (bitsPerSample >= 16)
return decodeFrameImpl(buffer, size, 1 << (bitsPerSample - 16));
return false;
}
bool FLACtoMP3::decodeFrameImpl(const int32_t * const buffer[], uint32_t size, const uint32_t divisor)
{
for (size_t i = 0; i < size; ++i) {
pcm[pcmCounter++] = (int16_t)buffer[0][i];
pcm[pcmCounter++] = (int16_t)buffer[1][i];
int32_t left = buffer[0][i];
int32_t right = buffer[1][i];
left /= divisor;
right /= divisor;
pcm[pcmCounter++] = (int16_t)left;
pcm[pcmCounter++] = (int16_t)right;
if (pcmCounter == pcmSize)
return flush();
@ -395,7 +407,7 @@ FLAC__StreamDecoderWriteStatus FLACtoMP3::write(
return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT;
}
bool result = self->decodeFrame(buffer, frame->header.blocksize);
bool result = self->decodeFrame(buffer, frame->header.blocksize, frame->header.bits_per_sample);
if (result)
return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE;

View File

@ -29,7 +29,8 @@ private:
void processTags(const FLAC__StreamMetadata_VorbisComment& tags);
void processInfo(const FLAC__StreamMetadata_StreamInfo& info);
void processPicture(const FLAC__StreamMetadata_Picture& picture);
bool decodeFrame(const int32_t * const buffer[], uint32_t size);
bool decodeFrame(const int32_t * const buffer[], uint32_t size, const uint32_t bitsPerSample);
bool decodeFrameImpl(const int32_t * const buffer[], uint32_t size, const uint32_t divisor);
bool flush();
bool initializeOutput();
bool scaleJPEG(const FLAC__StreamMetadata_Picture& picture);

View File

@ -57,16 +57,16 @@ int main(int argc, char **argv) {
}
logger->setSeverity(settings->getLogLevel());
std::shared_ptr<TaskManager> taskManager = std::make_shared<TaskManager>(settings, logger);
taskManager->start();
TaskManager taskManager(settings, logger);
taskManager.start();
std::chrono::time_point start = std::chrono::system_clock::now();
Collection collection(input, taskManager, settings);
Collection collection(input, &taskManager);
collection.convert(output);
taskManager->wait();
taskManager.wait();
std::cout << std::endl;
taskManager->stop();
taskManager.stop();
std::chrono::time_point end = std::chrono::system_clock::now();
std::chrono::duration<double> seconds = end - start;

View File

@ -17,7 +17,6 @@ enum class Option {
destination,
parallel,
filesToCopy,
exclude,
encodingQuality,
outputQuality,
vbr,
@ -43,7 +42,6 @@ constexpr std::array<std::string_view, static_cast<int>(Option::_optionsSize)> o
"destination",
"parallel",
"filesToCopy",
"exclude",
"encodingQuality",
"outputQuality",
"vbr"
@ -266,74 +264,69 @@ void Settings::readConfigLine(const std::string& line) {
if (option == Option::_optionsSize)
return;
std::string value;
std::getline(stream >> std::ws, value);
strip(value);
if (value.empty())
return;
switch (option) {
case Option::level: {
std::string lv;
if (!logLevel.has_value() && std::istringstream(value) >> lv) {
if (!logLevel.has_value() && stream >> lv) {
Logger::Severity level = Logger::stringToSeverity(lv);
if (level < Logger::Severity::_severitySize)
logLevel = level;
}
} break;
case Option::type: {
std::string tp;
if (!outputType.has_value() && std::istringstream(value) >> tp) {
Type type = stringToType(tp);
std::string lv;
if (!outputType.has_value() && stream >> lv) {
Type type = stringToType(lv);
if (type < _typesSize)
outputType = type;
}
} break;
case Option::source: {
if (!input.has_value()) {
input = value;
} else if (!output.has_value()) {
output = input;
input = value;
std::string path;
if (stream >> path) {
if (!input.has_value()) {
input = path;
} else if (!output.has_value()) {
output = input;
input = path;
}
}
} break;
case Option::destination: {
if (!output.has_value())
output = value;
std::string path;
if (!output.has_value() && stream >> path)
output = path;
} break;
case Option::parallel: {
unsigned int count;
if (!threads.has_value() && std::istringstream(value) >> count)
if (!threads.has_value() && stream >> count)
threads = count;
} break;
case Option::filesToCopy: {
if (!nonMusic.has_value()) {
if (value == "all")
value = "";
else if (value == "none")
value = "a^";
std::string regex;
if (!nonMusic.has_value() && stream >> regex) {
if (regex == "all")
regex = "";
else if (regex == "none")
regex = "a^";
nonMusic = value;
nonMusic = regex;
}
} break;
case Option::exclude: {
if (!excluded.has_value())
excluded = value;
} break;
case Option::outputQuality: {
unsigned int quality;
if (!outputQuality.has_value() && std::istringstream(value) >> quality)
outputQuality = std::clamp(quality, minQuality, maxQuality);
unsigned int value;
if (!outputQuality.has_value() && stream >> value)
outputQuality = std::clamp(value, minQuality, maxQuality);
} break;
case Option::encodingQuality: {
unsigned int quality;
if (!encodingQuality.has_value() && std::istringstream(value) >> quality)
encodingQuality = std::clamp(quality, minQuality, maxQuality);
unsigned int value;
if (!encodingQuality.has_value() && stream >> value)
encodingQuality = std::clamp(value, minQuality, maxQuality);
} break;
case Option::vbr: {
bool vb;
if (!vbr.has_value() && std::istringstream(value) >> std::boolalpha >> vb)
vbr = vb;
bool value;
if (!vbr.has_value() && stream >> std::boolalpha >> value)
vbr = value;
} break;
default:
break;
@ -372,16 +365,9 @@ std::string Settings::resolvePath(const std::string& line) {
}
bool Settings::matchNonMusic(const std::string& fileName) const {
if (!nonMusic.has_value())
if (nonMusic.has_value())
return std::regex_search(fileName, nonMusic.value());
else
return true;
return std::regex_search(fileName, nonMusic.value());
}
bool Settings::isExcluded(const std::string& path) const {
if (!excluded.has_value())
return false;
return std::regex_search(path, excluded.value());
}

View File

@ -40,7 +40,6 @@ public:
Action getAction() const;
unsigned int getThreads() const;
bool matchNonMusic(const std::string& fileName) const;
bool isExcluded(const std::string& path) const;
unsigned char getEncodingQuality() const;
unsigned char getOutputQuality() const;
bool getVBR() const;
@ -70,7 +69,6 @@ private:
std::optional<std::string> configPath;
std::optional<unsigned int> threads;
std::optional<std::regex> nonMusic;
std::optional<std::regex> excluded;
std::optional<unsigned char> encodingQuality;
std::optional<unsigned char> outputQuality;
std::optional<bool> vbr;

View File

@ -22,9 +22,6 @@ TaskManager::~TaskManager() {
}
void TaskManager::queueConvert(const std::filesystem::path& source, const std::filesystem::path& destination) {
if (settings->isExcluded(source))
return;
std::unique_lock<std::mutex> lock(queueMutex);
jobs.emplace(Job::convert, source, destination);
@ -86,7 +83,7 @@ void TaskManager::loop() {
lock.lock();
++completeTasks;
printResult(job, result);
printResilt(job, result);
--busyThreads;
lock.unlock();
waitConditional.notify_all();
@ -141,7 +138,7 @@ TaskManager::JobResult TaskManager::execute(Job& job) {
}};
}
void TaskManager::printResult(const TaskManager::Job& job, const TaskManager::JobResult& result) {
void TaskManager::printResilt(const TaskManager::Job& job, const TaskManager::JobResult& result) {
std::string msg;
switch (job.type) {
case Job::copy:

View File

@ -36,7 +36,7 @@ public:
private:
void loop();
JobResult execute(Job& job);
void printResult(const Job& job, const JobResult& result);
void printResilt(const Job& job, const JobResult& result);
static JobResult mp3Job(const Job& job, const std::shared_ptr<Settings>& settings);
static JobResult copyJob(const Job& job, const std::shared_ptr<Settings>& settings);