add features to little rpcli
This commit is contained in:
1865
zefie_wtvp_minisrv/ra_win/minimp3.h
Normal file
1865
zefie_wtvp_minisrv/ra_win/minimp3.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,18 @@
|
||||
#include <oleauto.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#import "prct3260.ocx" no_namespace named_guids raw_interfaces_only
|
||||
|
||||
#define MINIMP3_IMPLEMENTATION
|
||||
#include "minimp3.h"
|
||||
|
||||
static void die(const char *msg);
|
||||
static void cleanup_temp_input(void);
|
||||
|
||||
static char g_temp_input_path[MAX_PATH] = {0};
|
||||
|
||||
static BSTR ansi_to_bstr(const char *s) {
|
||||
int wlen;
|
||||
@@ -89,15 +96,421 @@ static void die_last_error(IProducerControl *ctl, const char *fallback) {
|
||||
fprintf(stderr, "error: %s (code=%ld)\n", fallback, lastError);
|
||||
}
|
||||
|
||||
cleanup_temp_input();
|
||||
ExitProcess(1);
|
||||
}
|
||||
|
||||
// Simple helper: print and exit
|
||||
static void die(const char *msg) {
|
||||
fprintf(stderr, "error: %s\n", msg);
|
||||
cleanup_temp_input();
|
||||
ExitProcess(1);
|
||||
}
|
||||
|
||||
static void cleanup_temp_input(void) {
|
||||
if (g_temp_input_path[0]) {
|
||||
DeleteFileA(g_temp_input_path);
|
||||
g_temp_input_path[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static const char *path_ext(const char *path) {
|
||||
const char *dot;
|
||||
|
||||
if (!path) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dot = strrchr(path, '.');
|
||||
return dot ? dot : NULL;
|
||||
}
|
||||
|
||||
static int is_mpeg_audio_input(const char *path) {
|
||||
const char *ext = path_ext(path);
|
||||
|
||||
if (!ext) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _stricmp(ext, ".mp1") == 0 ||
|
||||
_stricmp(ext, ".mp2") == 0 ||
|
||||
_stricmp(ext, ".mp3") == 0;
|
||||
}
|
||||
|
||||
static int is_wav_input(const char *path) {
|
||||
const char *ext = path_ext(path);
|
||||
|
||||
if (!ext) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return _stricmp(ext, ".wav") == 0;
|
||||
}
|
||||
|
||||
static double gain_db_to_scale(double gainDb) {
|
||||
return pow(10.0, gainDb / 20.0);
|
||||
}
|
||||
|
||||
static int parse_gain_db(const char *s, double *gainDbOut) {
|
||||
char *end = NULL;
|
||||
double value;
|
||||
|
||||
if (!s || !*s || !gainDbOut) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
value = strtod(s, &end);
|
||||
if (!end || *end != '\0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*gainDbOut = value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static short apply_gain_sample(short sample, double gainScale) {
|
||||
double scaled = (double)sample * gainScale;
|
||||
if (scaled > 32767.0) {
|
||||
return 32767;
|
||||
}
|
||||
if (scaled < -32768.0) {
|
||||
return -32768;
|
||||
}
|
||||
return (short)(scaled >= 0.0 ? scaled + 0.5 : scaled - 0.5);
|
||||
}
|
||||
|
||||
static void write_u16le(unsigned char *dst, unsigned value) {
|
||||
dst[0] = (unsigned char)(value & 0xffu);
|
||||
dst[1] = (unsigned char)((value >> 8) & 0xffu);
|
||||
}
|
||||
|
||||
static void write_u32le(unsigned char *dst, unsigned value) {
|
||||
dst[0] = (unsigned char)(value & 0xffu);
|
||||
dst[1] = (unsigned char)((value >> 8) & 0xffu);
|
||||
dst[2] = (unsigned char)((value >> 16) & 0xffu);
|
||||
dst[3] = (unsigned char)((value >> 24) & 0xffu);
|
||||
}
|
||||
|
||||
static void fill_wav_header(unsigned char *header,
|
||||
unsigned dataBytes,
|
||||
int sampleRate,
|
||||
int channels,
|
||||
int bitsPerSample) {
|
||||
unsigned byteRate = (unsigned)(sampleRate * channels * bitsPerSample / 8);
|
||||
unsigned blockAlign = (unsigned)(channels * bitsPerSample / 8);
|
||||
|
||||
memcpy(header + 0, "RIFF", 4);
|
||||
write_u32le(header + 4, 36u + dataBytes);
|
||||
memcpy(header + 8, "WAVE", 4);
|
||||
memcpy(header + 12, "fmt ", 4);
|
||||
write_u32le(header + 16, 16u);
|
||||
write_u16le(header + 20, 1u);
|
||||
write_u16le(header + 22, (unsigned)channels);
|
||||
write_u32le(header + 24, (unsigned)sampleRate);
|
||||
write_u32le(header + 28, byteRate);
|
||||
write_u16le(header + 32, blockAlign);
|
||||
write_u16le(header + 34, (unsigned)bitsPerSample);
|
||||
memcpy(header + 36, "data", 4);
|
||||
write_u32le(header + 40, dataBytes);
|
||||
}
|
||||
|
||||
static unsigned char *read_entire_file(const char *path, size_t *sizeOut) {
|
||||
FILE *fp;
|
||||
unsigned char *data;
|
||||
__int64 fileSize;
|
||||
|
||||
if (!sizeOut) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*sizeOut = 0;
|
||||
fp = fopen(path, "rb");
|
||||
if (!fp) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (_fseeki64(fp, 0, SEEK_END) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fileSize = _ftelli64(fp);
|
||||
if (fileSize < 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (_fseeki64(fp, 0, SEEK_SET) != 0) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
data = (unsigned char *)malloc((size_t)fileSize);
|
||||
if (!data) {
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fileSize > 0 && fread(data, 1, (size_t)fileSize, fp) != (size_t)fileSize) {
|
||||
free(data);
|
||||
fclose(fp);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
*sizeOut = (size_t)fileSize;
|
||||
return data;
|
||||
}
|
||||
|
||||
static int make_temp_wav_path(char *pathBuf, size_t pathBufSize) {
|
||||
char tempDir[MAX_PATH];
|
||||
char tempFile[MAX_PATH];
|
||||
char *dot;
|
||||
|
||||
if (!pathBuf || pathBufSize < MAX_PATH) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!GetTempPathA((DWORD)sizeof(tempDir), tempDir)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!GetTempFileNameA(tempDir, "rpc", 0, tempFile)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
DeleteFileA(tempFile);
|
||||
dot = strrchr(tempFile, '.');
|
||||
if (dot) {
|
||||
strcpy(dot, ".wav");
|
||||
}
|
||||
|
||||
strcpy(pathBuf, tempFile);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int decode_mpeg_audio_to_wav(const char *inputPath,
|
||||
char *outputPath,
|
||||
size_t outputPathSize,
|
||||
double gainScale) {
|
||||
unsigned char *inputData = NULL;
|
||||
size_t inputSize = 0;
|
||||
mp3dec_t dec;
|
||||
size_t pos = 0;
|
||||
FILE *out = NULL;
|
||||
unsigned char wavHeader[44] = {0};
|
||||
unsigned dataBytes = 0;
|
||||
int sampleRate = 0;
|
||||
int channels = 0;
|
||||
int haveAudio = 0;
|
||||
|
||||
if (!make_temp_wav_path(outputPath, outputPathSize)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
inputData = read_entire_file(inputPath, &inputSize);
|
||||
if (!inputData) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
out = fopen(outputPath, "wb");
|
||||
if (!out) {
|
||||
free(inputData);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fwrite(wavHeader, 1, sizeof(wavHeader), out) != sizeof(wavHeader)) {
|
||||
fclose(out);
|
||||
DeleteFileA(outputPath);
|
||||
free(inputData);
|
||||
return 0;
|
||||
}
|
||||
|
||||
mp3dec_init(&dec);
|
||||
while (pos < inputSize) {
|
||||
mp3dec_frame_info_t info;
|
||||
mp3d_sample_t pcm[MINIMP3_MAX_SAMPLES_PER_FRAME];
|
||||
int samples = mp3dec_decode_frame(&dec, inputData + pos, (int)(inputSize - pos), pcm, &info);
|
||||
|
||||
if (info.frame_bytes <= 0) {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
pos += (size_t)info.frame_bytes;
|
||||
if (samples <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!haveAudio) {
|
||||
sampleRate = info.hz;
|
||||
channels = info.channels;
|
||||
haveAudio = 1;
|
||||
} else if (sampleRate != info.hz || channels != info.channels) {
|
||||
fclose(out);
|
||||
DeleteFileA(outputPath);
|
||||
free(inputData);
|
||||
die("mpeg input changed sample rate or channel count mid-stream");
|
||||
}
|
||||
|
||||
if (gainScale != 1.0) {
|
||||
int sampleIndex;
|
||||
for (sampleIndex = 0; sampleIndex < samples; ++sampleIndex) {
|
||||
pcm[sampleIndex] = apply_gain_sample(pcm[sampleIndex], gainScale);
|
||||
}
|
||||
}
|
||||
|
||||
if (fwrite(pcm, sizeof(mp3d_sample_t), (size_t)samples, out) != (size_t)samples) {
|
||||
fclose(out);
|
||||
DeleteFileA(outputPath);
|
||||
free(inputData);
|
||||
return 0;
|
||||
}
|
||||
dataBytes += (unsigned)(samples * sizeof(mp3d_sample_t));
|
||||
}
|
||||
|
||||
free(inputData);
|
||||
|
||||
if (!haveAudio) {
|
||||
fclose(out);
|
||||
DeleteFileA(outputPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fill_wav_header(wavHeader, dataBytes, sampleRate, channels, 16);
|
||||
if (fseek(out, 0, SEEK_SET) != 0 || fwrite(wavHeader, 1, sizeof(wavHeader), out) != sizeof(wavHeader)) {
|
||||
fclose(out);
|
||||
DeleteFileA(outputPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int find_wav_data_chunk(const unsigned char *data,
|
||||
size_t size,
|
||||
size_t *fmtOffset,
|
||||
unsigned *fmtSize,
|
||||
size_t *dataOffset,
|
||||
unsigned *dataSize) {
|
||||
size_t pos = 12;
|
||||
|
||||
if (!data || size < 44 || memcmp(data, "RIFF", 4) != 0 || memcmp(data + 8, "WAVE", 4) != 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*fmtOffset = 0;
|
||||
*fmtSize = 0;
|
||||
*dataOffset = 0;
|
||||
*dataSize = 0;
|
||||
|
||||
while (pos + 8 <= size) {
|
||||
unsigned chunkSize = (unsigned)data[pos + 4] |
|
||||
((unsigned)data[pos + 5] << 8) |
|
||||
((unsigned)data[pos + 6] << 16) |
|
||||
((unsigned)data[pos + 7] << 24);
|
||||
size_t chunkData = pos + 8;
|
||||
|
||||
if (chunkData + chunkSize > size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (memcmp(data + pos, "fmt ", 4) == 0) {
|
||||
*fmtOffset = chunkData;
|
||||
*fmtSize = chunkSize;
|
||||
} else if (memcmp(data + pos, "data", 4) == 0) {
|
||||
*dataOffset = chunkData;
|
||||
*dataSize = chunkSize;
|
||||
}
|
||||
|
||||
pos = chunkData + chunkSize + (chunkSize & 1u);
|
||||
}
|
||||
|
||||
return *fmtOffset != 0 && *dataOffset != 0;
|
||||
}
|
||||
|
||||
static int rewrite_wav_with_gain(const char *inputPath,
|
||||
char *outputPath,
|
||||
size_t outputPathSize,
|
||||
double gainScale) {
|
||||
unsigned char *data = NULL;
|
||||
size_t size = 0;
|
||||
size_t fmtOffset;
|
||||
unsigned fmtSize;
|
||||
size_t dataOffset;
|
||||
unsigned dataSize;
|
||||
unsigned short formatTag;
|
||||
unsigned short channels;
|
||||
unsigned sampleRate;
|
||||
unsigned short bitsPerSample;
|
||||
FILE *out = NULL;
|
||||
unsigned char header[44] = {0};
|
||||
unsigned sampleCount;
|
||||
unsigned i;
|
||||
|
||||
if (!make_temp_wav_path(outputPath, outputPathSize)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
data = read_entire_file(inputPath, &size);
|
||||
if (!data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!find_wav_data_chunk(data, size, &fmtOffset, &fmtSize, &dataOffset, &dataSize) || fmtSize < 16) {
|
||||
free(data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
formatTag = (unsigned short)(data[fmtOffset] | (data[fmtOffset + 1] << 8));
|
||||
channels = (unsigned short)(data[fmtOffset + 2] | (data[fmtOffset + 3] << 8));
|
||||
sampleRate = (unsigned)data[fmtOffset + 4] |
|
||||
((unsigned)data[fmtOffset + 5] << 8) |
|
||||
((unsigned)data[fmtOffset + 6] << 16) |
|
||||
((unsigned)data[fmtOffset + 7] << 24);
|
||||
bitsPerSample = (unsigned short)(data[fmtOffset + 14] | (data[fmtOffset + 15] << 8));
|
||||
|
||||
if (formatTag != 1 || bitsPerSample != 16 || channels == 0) {
|
||||
free(data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
out = fopen(outputPath, "wb");
|
||||
if (!out) {
|
||||
free(data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fill_wav_header(header, dataSize, (int)sampleRate, (int)channels, 16);
|
||||
if (fwrite(header, 1, sizeof(header), out) != sizeof(header)) {
|
||||
fclose(out);
|
||||
DeleteFileA(outputPath);
|
||||
free(data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
sampleCount = dataSize / 2u;
|
||||
for (i = 0; i < sampleCount; ++i) {
|
||||
short sample = (short)((unsigned short)data[dataOffset + i * 2] |
|
||||
((unsigned short)data[dataOffset + i * 2 + 1] << 8));
|
||||
short scaled = apply_gain_sample(sample, gainScale);
|
||||
unsigned char outBytes[2];
|
||||
outBytes[0] = (unsigned char)(scaled & 0xff);
|
||||
outBytes[1] = (unsigned char)(((unsigned short)scaled >> 8) & 0xff);
|
||||
if (fwrite(outBytes, 1, 2, out) != 2) {
|
||||
fclose(out);
|
||||
DeleteFileA(outputPath);
|
||||
free(data);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(out);
|
||||
free(data);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Map rating string to something we can store as a property
|
||||
static const char *rating_to_str(const char *r) {
|
||||
if (_stricmp(r, "G") == 0) return "G";
|
||||
@@ -136,6 +549,16 @@ typedef struct {
|
||||
char name[512];
|
||||
} selected_codec_t;
|
||||
|
||||
typedef struct {
|
||||
char title[256];
|
||||
char artist[256];
|
||||
char album[256];
|
||||
char year[64];
|
||||
char genre[128];
|
||||
char comment[512];
|
||||
char copyright[256];
|
||||
} id3_metadata_t;
|
||||
|
||||
static target_t parse_target(const char *s) {
|
||||
if (!s) return TGT_WEBTV;
|
||||
if (_stricmp(s, "WebTV") == 0) return TGT_WEBTV;
|
||||
@@ -192,6 +615,313 @@ static int parse_codec_index(const char *s) {
|
||||
return (int)value;
|
||||
}
|
||||
|
||||
static void trim_trailing_space(char *s) {
|
||||
size_t len;
|
||||
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
|
||||
len = strlen(s);
|
||||
while (len > 0) {
|
||||
char ch = s[len - 1];
|
||||
if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n' && ch != '\0') {
|
||||
break;
|
||||
}
|
||||
s[len - 1] = '\0';
|
||||
--len;
|
||||
}
|
||||
}
|
||||
|
||||
static void copy_latin1_text(char *dst, size_t dstSize, const unsigned char *src, size_t srcSize) {
|
||||
size_t i;
|
||||
size_t out = 0;
|
||||
|
||||
if (!dst || dstSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dst[0] = '\0';
|
||||
if (!src || srcSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < srcSize && out + 1 < dstSize; ++i) {
|
||||
unsigned char ch = src[i];
|
||||
if (ch == 0) {
|
||||
break;
|
||||
}
|
||||
dst[out++] = (char)ch;
|
||||
}
|
||||
dst[out] = '\0';
|
||||
trim_trailing_space(dst);
|
||||
}
|
||||
|
||||
static void copy_utf16_text(char *dst, size_t dstSize, const unsigned char *src, size_t srcSize, int bigEndian) {
|
||||
wchar_t *wideBuf;
|
||||
size_t chars;
|
||||
size_t i;
|
||||
|
||||
if (!dst || dstSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dst[0] = '\0';
|
||||
if (!src || srcSize < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
chars = srcSize / 2;
|
||||
wideBuf = (wchar_t *)malloc((chars + 1) * sizeof(wchar_t));
|
||||
if (!wideBuf) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < chars; ++i) {
|
||||
unsigned value;
|
||||
if (bigEndian) {
|
||||
value = ((unsigned)src[i * 2] << 8) | (unsigned)src[i * 2 + 1];
|
||||
} else {
|
||||
value = (unsigned)src[i * 2] | ((unsigned)src[i * 2 + 1] << 8);
|
||||
}
|
||||
wideBuf[i] = (wchar_t)value;
|
||||
if (value == 0) {
|
||||
chars = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
wideBuf[chars] = L'\0';
|
||||
|
||||
WideCharToMultiByte(CP_ACP, 0, wideBuf, -1, dst, (int)dstSize, NULL, NULL);
|
||||
trim_trailing_space(dst);
|
||||
free(wideBuf);
|
||||
}
|
||||
|
||||
static void decode_id3_text_payload(const unsigned char *src, size_t srcSize, char *dst, size_t dstSize) {
|
||||
unsigned char enc;
|
||||
|
||||
if (!dst || dstSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dst[0] = '\0';
|
||||
if (!src || srcSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
enc = src[0];
|
||||
src += 1;
|
||||
srcSize -= 1;
|
||||
|
||||
switch (enc) {
|
||||
case 0:
|
||||
case 3:
|
||||
copy_latin1_text(dst, dstSize, src, srcSize);
|
||||
break;
|
||||
case 1:
|
||||
if (srcSize >= 2 && src[0] == 0xFE && src[1] == 0xFF) {
|
||||
copy_utf16_text(dst, dstSize, src + 2, srcSize - 2, 1);
|
||||
} else if (srcSize >= 2 && src[0] == 0xFF && src[1] == 0xFE) {
|
||||
copy_utf16_text(dst, dstSize, src + 2, srcSize - 2, 0);
|
||||
} else {
|
||||
copy_utf16_text(dst, dstSize, src, srcSize, 0);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
copy_utf16_text(dst, dstSize, src, srcSize, 1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned read_synchsafe32(const unsigned char *src) {
|
||||
return ((unsigned)(src[0] & 0x7f) << 21) |
|
||||
((unsigned)(src[1] & 0x7f) << 14) |
|
||||
((unsigned)(src[2] & 0x7f) << 7) |
|
||||
(unsigned)(src[3] & 0x7f);
|
||||
}
|
||||
|
||||
static unsigned read_be32(const unsigned char *src) {
|
||||
return ((unsigned)src[0] << 24) |
|
||||
((unsigned)src[1] << 16) |
|
||||
((unsigned)src[2] << 8) |
|
||||
(unsigned)src[3];
|
||||
}
|
||||
|
||||
static void assign_if_empty(char *dst, size_t dstSize, const char *src) {
|
||||
if (!dst || dstSize == 0 || !src || !src[0] || dst[0]) {
|
||||
return;
|
||||
}
|
||||
strncpy(dst, src, dstSize - 1);
|
||||
dst[dstSize - 1] = '\0';
|
||||
}
|
||||
|
||||
static void parse_id3v1_tag(const unsigned char *data, size_t size, id3_metadata_t *meta) {
|
||||
if (!data || size < 128 || !meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (memcmp(data + size - 128, "TAG", 3) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
assign_if_empty(meta->title, sizeof(meta->title), "");
|
||||
copy_latin1_text(meta->title, sizeof(meta->title), data + size - 125, 30);
|
||||
copy_latin1_text(meta->artist, sizeof(meta->artist), data + size - 95, 30);
|
||||
copy_latin1_text(meta->album, sizeof(meta->album), data + size - 65, 30);
|
||||
copy_latin1_text(meta->year, sizeof(meta->year), data + size - 35, 4);
|
||||
copy_latin1_text(meta->comment, sizeof(meta->comment), data + size - 31, 30);
|
||||
}
|
||||
|
||||
static void parse_id3v2_comment(const unsigned char *payload, size_t payloadSize, char *dst, size_t dstSize) {
|
||||
unsigned char enc;
|
||||
size_t offset;
|
||||
|
||||
if (!payload || payloadSize < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
enc = payload[0];
|
||||
offset = 4;
|
||||
|
||||
if (enc == 0 || enc == 3) {
|
||||
while (offset < payloadSize && payload[offset] != 0) {
|
||||
++offset;
|
||||
}
|
||||
if (offset < payloadSize) {
|
||||
++offset;
|
||||
}
|
||||
} else {
|
||||
while (offset + 1 < payloadSize && (payload[offset] != 0 || payload[offset + 1] != 0)) {
|
||||
offset += 2;
|
||||
}
|
||||
if (offset + 1 < payloadSize) {
|
||||
offset += 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset < payloadSize) {
|
||||
unsigned char *tmp = (unsigned char *)malloc(payloadSize - offset + 1);
|
||||
if (!tmp) {
|
||||
return;
|
||||
}
|
||||
tmp[0] = enc;
|
||||
memcpy(tmp + 1, payload + offset, payloadSize - offset);
|
||||
decode_id3_text_payload(tmp, payloadSize - offset + 1, dst, dstSize);
|
||||
free(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
static void parse_id3v2_tag(const unsigned char *data, size_t size, id3_metadata_t *meta) {
|
||||
unsigned version;
|
||||
unsigned flags;
|
||||
size_t tagSize;
|
||||
size_t pos;
|
||||
|
||||
if (!data || size < 10 || !meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (memcmp(data, "ID3", 3) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
version = data[3];
|
||||
flags = data[5];
|
||||
tagSize = read_synchsafe32(data + 6);
|
||||
pos = 10;
|
||||
|
||||
if ((flags & 0x40) != 0 && size >= pos + 4) {
|
||||
unsigned extSize = version == 4 ? read_synchsafe32(data + pos) : read_be32(data + pos);
|
||||
pos += extSize;
|
||||
}
|
||||
|
||||
while (pos + 10 <= size && pos < tagSize + 10) {
|
||||
char frameId[5];
|
||||
unsigned frameSize;
|
||||
const unsigned char *payload;
|
||||
|
||||
memcpy(frameId, data + pos, 4);
|
||||
frameId[4] = '\0';
|
||||
if (frameId[0] == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
frameSize = version == 4 ? read_synchsafe32(data + pos + 4) : read_be32(data + pos + 4);
|
||||
pos += 10;
|
||||
if (frameSize == 0 || pos + frameSize > size) {
|
||||
break;
|
||||
}
|
||||
|
||||
payload = data + pos;
|
||||
if (strcmp(frameId, "TIT2") == 0) {
|
||||
decode_id3_text_payload(payload, frameSize, meta->title, sizeof(meta->title));
|
||||
} else if (strcmp(frameId, "TPE1") == 0) {
|
||||
decode_id3_text_payload(payload, frameSize, meta->artist, sizeof(meta->artist));
|
||||
} else if (strcmp(frameId, "TALB") == 0) {
|
||||
decode_id3_text_payload(payload, frameSize, meta->album, sizeof(meta->album));
|
||||
} else if (strcmp(frameId, "TYER") == 0 || strcmp(frameId, "TDRC") == 0) {
|
||||
decode_id3_text_payload(payload, frameSize, meta->year, sizeof(meta->year));
|
||||
} else if (strcmp(frameId, "TCON") == 0) {
|
||||
decode_id3_text_payload(payload, frameSize, meta->genre, sizeof(meta->genre));
|
||||
} else if (strcmp(frameId, "TCOP") == 0) {
|
||||
decode_id3_text_payload(payload, frameSize, meta->copyright, sizeof(meta->copyright));
|
||||
} else if (strcmp(frameId, "COMM") == 0) {
|
||||
parse_id3v2_comment(payload, frameSize, meta->comment, sizeof(meta->comment));
|
||||
}
|
||||
|
||||
pos += frameSize;
|
||||
}
|
||||
}
|
||||
|
||||
static void id3_meta_to_description(const id3_metadata_t *meta, char *dst, size_t dstSize) {
|
||||
int wrote = 0;
|
||||
|
||||
if (!dst || dstSize == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
dst[0] = '\0';
|
||||
if (!meta) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (meta->album[0]) {
|
||||
_snprintf(dst + strlen(dst), dstSize - strlen(dst), "%sAlbum: %s", wrote ? " | " : "", meta->album);
|
||||
wrote = 1;
|
||||
}
|
||||
if (meta->year[0]) {
|
||||
_snprintf(dst + strlen(dst), dstSize - strlen(dst), "%sYear: %s", wrote ? " | " : "", meta->year);
|
||||
wrote = 1;
|
||||
}
|
||||
if (meta->comment[0]) {
|
||||
_snprintf(dst + strlen(dst), dstSize - strlen(dst), "%s%s", wrote ? " | " : "", meta->comment);
|
||||
}
|
||||
}
|
||||
|
||||
static int load_id3_metadata(const char *path, id3_metadata_t *meta) {
|
||||
unsigned char *data;
|
||||
size_t size;
|
||||
|
||||
if (!path || !meta) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memset(meta, 0, sizeof(*meta));
|
||||
data = read_entire_file(path, &size);
|
||||
if (!data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
parse_id3v2_tag(data, size, meta);
|
||||
parse_id3v1_tag(data, size, meta);
|
||||
free(data);
|
||||
|
||||
return meta->title[0] || meta->artist[0] || meta->album[0] || meta->year[0] ||
|
||||
meta->genre[0] || meta->comment[0] || meta->copyright[0];
|
||||
}
|
||||
|
||||
static int get_audio_codec_by_index(IProducerControl *ctl, int codecIndex, selected_codec_t *selected) {
|
||||
IEnumIDispatch *enumDisp = NULL;
|
||||
HRESULT hr;
|
||||
@@ -445,11 +1175,13 @@ static void set_clip_properties(IProducerControl *ctl,
|
||||
|
||||
static void usage(void) {
|
||||
fprintf(stderr,
|
||||
"Usage: rpcli [options] input.wav [output.ra]\n"
|
||||
"Usage: rpcli [options] input.(wav|mp1|mp2|mp3) [output.ra]\n"
|
||||
"Options:\n"
|
||||
" --ra5 | --g2 Set PlayerCompatibility (default: G2 / PLAYER_6)\n"
|
||||
" --codec-list List available audio codecs and exit\n"
|
||||
" --codec N Select codec list entry N; mutually exclusive with --target\n"
|
||||
" --gain DB Apply PCM gain in dB before encoding; negative values reduce level\n"
|
||||
" --id3 Fill unset metadata fields from ID3 tags when present\n"
|
||||
" --mode voice|voice-bgm|stereo|music (default: music)\n"
|
||||
" --target WebTV|56k|ISDN|Cable|PC (default: WebTV)\n"
|
||||
" --title TEXT\n"
|
||||
@@ -473,8 +1205,16 @@ int main(int argc, char **argv) {
|
||||
int player_is_ra5 = 0;
|
||||
int do_codec_list = 0;
|
||||
int codec_index = -1;
|
||||
int use_gain = 0;
|
||||
int use_id3 = 0;
|
||||
int target_was_explicit = 0;
|
||||
selected_codec_t selected_codec;
|
||||
id3_metadata_t id3_meta;
|
||||
char id3_description[640] = {0};
|
||||
const char *encoder_input = NULL;
|
||||
char decoded_input[MAX_PATH] = {0};
|
||||
double gain_db = 0.0;
|
||||
double gain_scale = 1.0;
|
||||
|
||||
const char *infile = NULL;
|
||||
char outbuf[MAX_PATH] = {0};
|
||||
@@ -494,6 +1234,13 @@ int main(int argc, char **argv) {
|
||||
if (codec_index < 0) {
|
||||
die("--codec requires a non-negative integer");
|
||||
}
|
||||
} else if (strcmp(argv[i], "--gain") == 0 && i+1 < argc) {
|
||||
if (!parse_gain_db(argv[++i], &gain_db)) {
|
||||
die("--gain requires a numeric dB value");
|
||||
}
|
||||
use_gain = 1;
|
||||
} else if (strcmp(argv[i], "--id3") == 0) {
|
||||
use_id3 = 1;
|
||||
} else if (strcmp(argv[i], "--mode") == 0 && i+1 < argc) {
|
||||
mode = argv[++i];
|
||||
} else if (strcmp(argv[i], "--target") == 0 && i+1 < argc) {
|
||||
@@ -534,6 +1281,10 @@ int main(int argc, char **argv) {
|
||||
die("--codec is mutually exclusive with --target");
|
||||
}
|
||||
|
||||
if (use_gain) {
|
||||
gain_scale = gain_db_to_scale(gain_db);
|
||||
}
|
||||
|
||||
if (!outfile && infile) {
|
||||
// derive output: input + ".ra"
|
||||
const char *p = strrchr(infile, '.');
|
||||
@@ -544,6 +1295,24 @@ int main(int argc, char **argv) {
|
||||
outfile = outbuf;
|
||||
}
|
||||
|
||||
memset(&id3_meta, 0, sizeof(id3_meta));
|
||||
if (use_id3 && !load_id3_metadata(infile, &id3_meta)) {
|
||||
fprintf(stderr, "warning: no usable ID3 metadata found in %s\n", infile);
|
||||
}
|
||||
|
||||
if (use_id3) {
|
||||
if (!title && id3_meta.title[0]) title = id3_meta.title;
|
||||
if (!author && id3_meta.artist[0]) author = id3_meta.artist;
|
||||
if (!copyright && id3_meta.copyright[0]) copyright = id3_meta.copyright;
|
||||
if (!keywords && id3_meta.genre[0]) keywords = id3_meta.genre;
|
||||
if (!description) {
|
||||
id3_meta_to_description(&id3_meta, id3_description, sizeof(id3_description));
|
||||
if (id3_description[0]) {
|
||||
description = id3_description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
||||
if (FAILED(hr)) die("CoInitialize failed");
|
||||
|
||||
@@ -569,6 +1338,25 @@ int main(int argc, char **argv) {
|
||||
selected_codec.codecId = -1;
|
||||
selected_codec.flavorId = -1;
|
||||
|
||||
encoder_input = infile;
|
||||
if (is_mpeg_audio_input(infile)) {
|
||||
if (!decode_mpeg_audio_to_wav(infile, decoded_input, sizeof(decoded_input), gain_scale)) {
|
||||
ctl->Release();
|
||||
CoUninitialize();
|
||||
die("failed to decode mpeg audio input with minimp3");
|
||||
}
|
||||
strcpy(g_temp_input_path, decoded_input);
|
||||
encoder_input = g_temp_input_path;
|
||||
} else if (use_gain && is_wav_input(infile)) {
|
||||
if (!rewrite_wav_with_gain(infile, decoded_input, sizeof(decoded_input), gain_scale)) {
|
||||
ctl->Release();
|
||||
CoUninitialize();
|
||||
die("failed to apply gain to wav input; only PCM 16-bit WAV is supported");
|
||||
}
|
||||
strcpy(g_temp_input_path, decoded_input);
|
||||
encoder_input = g_temp_input_path;
|
||||
}
|
||||
|
||||
if (codec_index >= 0 && !get_audio_codec_by_index(ctl, codec_index, &selected_codec)) {
|
||||
ctl->Release();
|
||||
CoUninitialize();
|
||||
@@ -578,7 +1366,7 @@ int main(int argc, char **argv) {
|
||||
// Input properties
|
||||
ctl->put_InputType(INPUT_SOURCE_FILE);
|
||||
{
|
||||
BSTR b = ansi_to_bstr(infile);
|
||||
BSTR b = ansi_to_bstr(encoder_input);
|
||||
ctl->put_InputFilename(b);
|
||||
SysFreeString(b);
|
||||
}
|
||||
@@ -664,6 +1452,7 @@ int main(int argc, char **argv) {
|
||||
if (selected_codec.cookie) selected_codec.cookie->Release();
|
||||
ctl->Release();
|
||||
CoUninitialize();
|
||||
cleanup_temp_input();
|
||||
|
||||
printf("Encoded %s -> %s\n", infile, outfile);
|
||||
return 0;
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user