Created
February 20, 2021 08:43
-
-
Save MCJack123/5a0fa48a8c4943c569dc60c4151d2511 to your computer and use it in GitHub Desktop.
Renders module files to WAV/FLAC/OGG/MP3/M4A with looping and fade out (requires libopenmpt; https://github.com/Raveler/ffmpeg-cpp for non-WAV conversion)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Compiling: cl /EHsc /MD /D_WIN32 /DFFMPEG_CPP /Feloopmodule.exe loopmodule.cpp /link libopenmpt.lib ffmpeg-cpp.lib /LTCG | |
// If not using ffmpeg-cpp: cl /EHsc /D_WIN32 /Feloopmodule.exe loopmodule.cpp /link libopenmpt.lib | |
#include <iostream> | |
#include <fstream> | |
#include <sstream> | |
#include <string> | |
#include <unordered_map> | |
#include <iomanip> | |
#include <algorithm> | |
#include <sys/stat.h> | |
#ifdef _WIN32 | |
#include <windows.h> | |
#include <io.h> | |
#if !defined(S_ISDIR) && defined(S_IFMT) && defined(S_IFDIR) | |
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) | |
#endif | |
#else | |
#include <dirent.h> | |
#include <errno.h> | |
#endif | |
#include <libopenmpt/libopenmpt.hpp> | |
#ifdef FFMPEG_CPP | |
#include "include/ffmpegcpp.h" | |
#endif | |
int sample_rate = 48000; | |
int interpolation = 0; // default | |
int max_length = 240; // 4 minutes | |
int fadeout_length = 10; | |
int format = 0; | |
int bitrate = 192; | |
std::unordered_map<std::string, std::string> metadata; | |
AVCodecID getCodecID() { | |
switch (format) { | |
case 0: return AV_CODEC_ID_PCM_F32LE; | |
case 1: return AV_CODEC_ID_FLAC; | |
case 2: return AV_CODEC_ID_VORBIS; | |
case 3: return AV_CODEC_ID_MP3; | |
case 4: return AV_CODEC_ID_AAC; | |
default: return (AVCodecID)0; | |
} | |
} | |
std::string getFormatExtension() { | |
switch (format) { | |
case 0: return ".wav"; | |
case 1: return ".flac"; | |
case 2: return ".ogg"; | |
case 3: return ".mp3"; | |
case 4: return ".m4a"; | |
default: return ".bin"; | |
} | |
} | |
int process_file(const char * input, const char * output) { | |
std::ifstream in(input, std::ios::binary); | |
if (!in.is_open()) { | |
std::cerr << "Could not open input file " << input << "\n"; | |
return 2; | |
} | |
std::stringstream out; | |
openmpt::module mod(in); | |
int length = mod.get_duration_seconds(); | |
int repeats = 0; | |
while ((repeats + 1) * length < max_length) repeats++; | |
mod.set_repeat_count(repeats); | |
mod.set_render_param(openmpt::module::render_param::RENDER_INTERPOLATIONFILTER_LENGTH, interpolation); | |
size_t count, frame_count = 0; | |
int fadeout_pos = 0; | |
float frames[4096]; | |
while (true) { | |
bool exit = false; | |
count = mod.read_interleaved_stereo(sample_rate, 2048, frames); | |
if (fadeout_pos > 0) { | |
for (int i = 0; i < count; i++) { | |
float multiplier = (float)fadeout_pos / (float)(sample_rate * fadeout_length); | |
frames[i*2] *= multiplier; | |
frames[i*2+1] *= multiplier; | |
if (--fadeout_pos == 0) { | |
count = i; | |
exit = true; | |
break; | |
} | |
} | |
} | |
frame_count += count; | |
out.write((const char*)frames, count * sizeof(float) * 2); | |
if (frame_count % 131072 == 0) std::cout << "Rendering: " << frame_count << " frames, " << std::setw(2) << std::setfill('0') << (frame_count / sample_rate / 60) << ":" << std::setw(2) << std::setfill('0') << (frame_count / sample_rate % 60) << "\r"; | |
if (count < 2048 || exit) break; | |
if (mod.get_repeat_count() == 0 && fadeout_pos == 0) fadeout_pos = sample_rate * fadeout_length; | |
} | |
std::string title = mod.get_metadata("title"), | |
artist = mod.get_metadata("artist"), | |
tracker = mod.get_metadata("tracker"), | |
date = mod.get_metadata("date"), | |
message = mod.get_metadata("message"); | |
in.close(); | |
std::cout << "Rendering: " << frame_count << " frames, " << std::setw(2) << std::setfill('0') << (frame_count / sample_rate / 60) << ":" << std::setw(2) << std::setfill('0') << (frame_count / sample_rate % 60) << "\n"; | |
#ifdef FFMPEG_CPP | |
if (format == 0) { | |
#endif | |
std::ofstream file(output, std::ios::binary); | |
if (!file.is_open()) { | |
std::cerr << "Could not open output file " << output << "\n"; | |
return 2; | |
} | |
std::string data = out.str(); | |
file.write("RIFF", 4); | |
uint32_t tmp = frame_count * 8 + 50; | |
file.write((const char*)&tmp, 4); | |
file.write("WAVEfmt \x12\0\0\0\x03\0\x02\0", 16); | |
tmp = sample_rate; | |
file.write((const char*)&tmp, 4); | |
tmp = frame_count * 8; | |
file.write((const char*)&tmp, 4); | |
file.write("\x08\0\x20\0\0\0fact\x04\0\0\0", 14); | |
tmp = frame_count; | |
file.write((const char*)&tmp, 4); | |
file.write("data", 4); | |
tmp = frame_count * 8; | |
file.write((const char*)&tmp, 4); | |
file.write(data.c_str(), data.size()); | |
file.close(); | |
#ifdef FFMPEG_CPP | |
} else { | |
try { | |
ffmpegcpp::Muxer muxer(output); | |
ffmpegcpp::AudioCodec codec(getCodecID()); | |
ffmpegcpp::AudioEncoder encoder(&codec, &muxer, bitrate * 1000); | |
if (!title.empty()) av_dict_set(&muxer.containerContext->metadata, "title", title.c_str(), 0); | |
if (!artist.empty()) av_dict_set(&muxer.containerContext->metadata, "artist", artist.c_str(), 0); | |
if (!tracker.empty()) av_dict_set(&muxer.containerContext->metadata, "encoded_by", tracker.c_str(), 0); | |
if (!date.empty()) av_dict_set(&muxer.containerContext->metadata, "creation_time", date.c_str(), 0); | |
if (!message.empty()) av_dict_set(&muxer.containerContext->metadata, "comment", message.c_str(), 0); | |
for (const auto& e : metadata) av_dict_set(&muxer.containerContext->metadata, e.first.c_str(), e.second.c_str(), 0); | |
ffmpegcpp::RawAudioDataSource source(AV_SAMPLE_FMT_FLT, sample_rate, 2, &encoder); | |
for (int i = 0; i < frame_count; i+=4) { | |
out.read((char*)frames, sizeof(float) * 8); | |
source.WriteData(frames, 4); | |
if (i % 131072 == 0) std::cout << "Encoding: " << i << " frames, " << std::setw(2) << std::setfill('0') << (i / sample_rate / 60) << ":" << std::setw(2) << std::setfill('0') << (i / sample_rate % 60) << "\r"; | |
} | |
std::cout << "Encoding: " << frame_count << " frames, " << std::setw(2) << std::setfill('0') << (frame_count / sample_rate / 60) << ":" << std::setw(2) << std::setfill('0') << (frame_count / sample_rate % 60) << "\nFinished!\n"; | |
source.Close(); | |
muxer.Close(); | |
} catch (ffmpegcpp::FFmpegException &e) { | |
char desc[AV_ERROR_MAX_STRING_SIZE]; | |
av_strerror(errno, desc, AV_ERROR_MAX_STRING_SIZE); | |
std::cerr << e.what() << ": " << desc << "\n"; | |
return 3; | |
} | |
} | |
#endif | |
return 0; | |
} | |
int main(int argc, const char * argv[]) { | |
const char * input = NULL; | |
const char * output = NULL; | |
int nextopt = 0; | |
for (int i = 1; i < argc; i++) { | |
if (nextopt) { | |
switch (nextopt) { | |
case 1: input = argv[i]; break; | |
case 2: output = argv[i]; break; | |
case 3: sample_rate = atoi(argv[i]); break; | |
case 4: interpolation = atoi(argv[i]); break; | |
case 5: max_length = atoi(argv[i]); break; | |
case 6: fadeout_length = atoi(argv[i]); break; | |
#ifdef FFMPEG_CPP | |
case 7: { | |
std::string arg(argv[i]); | |
std::transform(arg.begin(), arg.end(), arg.begin(), tolower); | |
if (arg == "wav") format = 0; | |
else if (arg == "flac") format = 1; | |
else if (arg == "ogg") format = 2; | |
else if (arg == "mp3") format = 3; | |
else if (arg == "m4a") format = 4; | |
else { | |
std::cerr << "Invalid format " << arg << "\n"; | |
return 1; | |
} | |
break; | |
} case 8: bitrate = atoi(argv[i]); break; | |
case 9: { | |
std::string arg(argv[i]); | |
std::string key = arg.substr(0, arg.find_first_of('=')), value = arg.substr(arg.find_first_of('=') + 1); | |
if (key.empty() || value.empty()) { | |
std::cerr << "Invalid metadata entry\n"; | |
return 1; | |
} | |
metadata[key] = value; | |
break; | |
} | |
#endif | |
} | |
nextopt = 0; | |
} else if (argv[i][0] == '-') { | |
switch (argv[i][1]) { | |
case 'h': | |
std::cout << "Usage: " << argv[0] << " [options...]\n" | |
"Options:\n" | |
" -i <input> Input file (required)\n" | |
" -o <output> Output file (required)\n" | |
" -r <rate> Sample rate (defaults to 48kHz)\n" | |
" -t <type> Interpolation method: 0 = default, 1 = none, 2 = linear, 4 = cubic, 8 = sinc\n" | |
" -l <seconds> Target length of song (defaults to 240/4 minutes)\n" | |
" -d <seconds> Fade out length (defaults to 10)\n" | |
#ifdef FFMPEG_CPP | |
" -f <format> Output format (wav, flac, ogg, mp3, m4a)\n" | |
" -b <bitrate> For lossy codecs, the bitrate (in kbps); for FLAC, the compression level\n" | |
" -m <key>=<value> Custom metadata tag to set in all output files\n"; | |
#endif | |
return 0; | |
case 'i': nextopt = 1; break; | |
case 'o': nextopt = 2; break; | |
case 'r': nextopt = 3; break; | |
case 't': nextopt = 4; break; | |
case 'l': nextopt = 5; break; | |
case 'd': nextopt = 6; break; | |
#ifdef FFMPEG_CPP | |
case 'f': nextopt = 7; break; | |
case 'b': nextopt = 8; break; | |
case 'm': nextopt = 9; break; | |
#endif | |
} | |
} | |
} | |
if (input == NULL || output == NULL) { | |
std::cerr << "Usage: " << argv[0] << " [options...] <-i input> <-o output>\n"; | |
return 1; | |
} | |
struct stat st; | |
if (stat(input, &st) == 0 && S_ISDIR(st.st_mode)) { | |
#ifdef _WIN32 | |
WIN32_FIND_DATA ffd; | |
HANDLE hFind; | |
char newpath[MAX_PATH + 5]; | |
#else | |
struct dirent * dir; | |
DIR * d; | |
#endif | |
if (stat(output, &st) != 0 || !S_ISDIR(st.st_mode)) { | |
if (stat(output, &st) != 0) | |
#ifdef _WIN32 | |
CreateDirectory(output, NULL); | |
#else | |
mkdir(output); | |
#endif | |
else { | |
fprintf(stderr, "Output directory %s does not exist.\n", output); | |
return 7; | |
} | |
} | |
if (strcmp(input, output) == 0) { | |
fprintf(stderr, "Input and output directories cannot be the same.\n"); | |
return 8; | |
} | |
#ifdef _WIN32 | |
strcpy(newpath, input); | |
strcat(newpath, "\\*.*"); | |
hFind = FindFirstFile(newpath, &ffd); | |
if (hFind == INVALID_HANDLE_VALUE) { | |
fprintf(stderr, "Could not open directory for reading: %d\n", GetLastError()); | |
return GetLastError(); | |
} | |
do { | |
std::string name(ffd.cFileName); | |
std::string ext = name.substr(name.find_last_of('.') + 1); | |
if (ext != "mod" && ext != "xm" && ext != "s3m" && ext != "it" && ext != "mptm") continue; | |
if ((ffd.dwFileAttributes & (FILE_ATTRIBUTE_DEVICE | FILE_ATTRIBUTE_DIRECTORY)) == 0) { | |
std::string from = std::string(input) + "\\" + name, to = std::string(output) + "\\" + name.substr(0, name.find_last_of('.')) + getFormatExtension(); | |
std::cout << "Converting " << name << "...\n"; | |
int val = process_file(from.c_str(), to.c_str()); | |
if (val) { | |
FindClose(hFind); | |
return val; | |
} | |
} | |
} while (FindNextFile(hFind, &ffd)); | |
FindClose(hFind); | |
#else | |
d = opendir(input); | |
if (d == NULL) { | |
fprintf(stderr, "Could not open directory for reading: %d\n", errno); | |
return errno; | |
} | |
while ((dir = readdir(d)) != NULL) { | |
const char * s = strrchr(dir->d_name, '.'); | |
if ((dir->d_type == DT_REG || dir->d_type == DT_UNKNOWN) && s != NULL) { | |
std::string name(dir->d_name); | |
std::string from = std::string(output) + "\\" + name, to = std::string(output) + "\\" + name.substr(0, name.find_last_of('.')) + getFormatExtension(); | |
int val = process_file(from.c_str(), to.c_str()); | |
if (val) { | |
closedir(d); | |
return val; | |
} | |
} | |
} | |
closedir(d); | |
#endif | |
} else return process_file(input, output); | |
return 0; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This patch is required to be able to export MP3s and to add metadata. You will need to use this | |
# if using ffmpeg-cpp. | |
diff --git a/source/ffmpeg-cpp/ffmpeg-cpp/Codecs/AudioCodec.cpp b/source/ffmpeg-cpp/ffmpeg-cpp/Codecs/AudioCodec.cpp | |
index 3d18e0c..e2c1402 100644 | |
--- a/source/ffmpeg-cpp/ffmpeg-cpp/Codecs/AudioCodec.cpp | |
+++ b/source/ffmpeg-cpp/ffmpeg-cpp/Codecs/AudioCodec.cpp | |
@@ -139,6 +139,7 @@ namespace ffmpegcpp | |
codecContext->bit_rate = bitRate; | |
codecContext->sample_fmt = format; | |
codecContext->sample_rate = sampleRate; | |
+ codecContext->frame_size = 8; | |
// deduce the best channel layout from the codec | |
codecContext->channel_layout = select_channel_layout(codecContext->codec); | |
diff --git a/source/ffmpeg-cpp/ffmpeg-cpp/Muxing/Muxer.h b/source/ffmpeg-cpp/ffmpeg-cpp/Muxing/Muxer.h | |
index 41e0c37..23170ff 100644 | |
--- a/source/ffmpeg-cpp/ffmpeg-cpp/Muxing/Muxer.h | |
+++ b/source/ffmpeg-cpp/ffmpeg-cpp/Muxing/Muxer.h | |
@@ -25,6 +25,7 @@ namespace ffmpegcpp { | |
AVCodec* GetDefaultVideoFormat(); | |
AVCodec* GetDefaultAudioFormat(); | |
+ AVFormatContext* containerContext = nullptr; | |
private: | |
@@ -36,7 +37,6 @@ namespace ffmpegcpp { | |
AVOutputFormat* containerFormat; | |
- AVFormatContext* containerContext = nullptr; | |
std::string fileName; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment