Skip to content

Instantly share code, notes, and snippets.

@MCJack123
Created February 20, 2021 08:43
Show Gist options
  • Save MCJack123/5a0fa48a8c4943c569dc60c4151d2511 to your computer and use it in GitHub Desktop.
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)
// 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 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