1855 lines
54 KiB
C++
1855 lines
54 KiB
C++
// rpcli.cpp - Minimal RealAudio 5/6 (G2) CLI encoder using RealProducer ActiveX
|
||
// Build (MSVC example):
|
||
// cl rpcli.cpp /O2 /MT /Fe:rpcli.exe
|
||
//
|
||
// This tool imports and loads the RealProducer control type library (prct3260.ocx)
|
||
// from the same folder as the executable at runtime (no global COM registration required).
|
||
|
||
#ifndef __cplusplus
|
||
#error rpcli.cpp uses C++ COM features (#import, __uuidof). Compile with MSVC C++.
|
||
#endif
|
||
|
||
#define COBJMACROS
|
||
#include <windows.h>
|
||
#include <ole2.h>
|
||
#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 HRESULT create_local_producer_control(IProducerControl **ctlOut, char *loadedPath, size_t loadedPathSize);
|
||
static void unload_local_ocx(void);
|
||
|
||
static char g_temp_input_path[MAX_PATH] = {0};
|
||
static char g_temp_resample_path[MAX_PATH] = {0};
|
||
static HMODULE g_local_ocx_module = NULL;
|
||
|
||
typedef HRESULT (STDAPICALLTYPE *DllGetClassObjectFn)(REFCLSID rclsid, REFIID riid, LPVOID *ppv);
|
||
|
||
static BSTR ansi_to_bstr(const char *s) {
|
||
int wlen;
|
||
BSTR b;
|
||
if (!s) {
|
||
return NULL;
|
||
}
|
||
|
||
wlen = MultiByteToWideChar(CP_ACP, 0, s, -1, NULL, 0);
|
||
if (wlen <= 0) {
|
||
return NULL;
|
||
}
|
||
|
||
b = SysAllocStringLen(NULL, (UINT)(wlen - 1));
|
||
if (!b) {
|
||
return NULL;
|
||
}
|
||
|
||
if (MultiByteToWideChar(CP_ACP, 0, s, -1, b, wlen) <= 0) {
|
||
SysFreeString(b);
|
||
return NULL;
|
||
}
|
||
|
||
return b;
|
||
}
|
||
|
||
static void bstr_to_ansi(BSTR b, char *buf, size_t bufSize) {
|
||
if (!buf || bufSize == 0) {
|
||
return;
|
||
}
|
||
|
||
buf[0] = '\0';
|
||
if (!b) {
|
||
return;
|
||
}
|
||
|
||
WideCharToMultiByte(CP_ACP, 0, b, -1, buf, (int)bufSize, NULL, NULL);
|
||
}
|
||
|
||
static void pump_pending_messages(void) {
|
||
MSG msg;
|
||
|
||
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
|
||
TranslateMessage(&msg);
|
||
DispatchMessage(&msg);
|
||
}
|
||
}
|
||
|
||
static void die_last_error(IProducerControl *ctl, const char *fallback) {
|
||
long lastError = 0;
|
||
BSTR errorText = NULL;
|
||
char buf[512];
|
||
|
||
if (!ctl) {
|
||
die(fallback);
|
||
}
|
||
|
||
if (FAILED(ctl->get_LastError(&lastError)) || lastError == 0) {
|
||
die(fallback);
|
||
}
|
||
|
||
if (SUCCEEDED(ctl->GetErrorString(lastError, &errorText)) && errorText) {
|
||
bstr_to_ansi(errorText, buf, sizeof(buf));
|
||
SysFreeString(errorText);
|
||
fprintf(stderr, "error: %s (code=%ld)\n", buf[0] ? buf : fallback, lastError);
|
||
} else {
|
||
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';
|
||
}
|
||
if (g_temp_resample_path[0]) {
|
||
DeleteFileA(g_temp_resample_path);
|
||
g_temp_resample_path[0] = '\0';
|
||
}
|
||
}
|
||
|
||
static void unload_local_ocx(void) {
|
||
if (g_local_ocx_module) {
|
||
FreeLibrary(g_local_ocx_module);
|
||
g_local_ocx_module = NULL;
|
||
}
|
||
}
|
||
|
||
static HRESULT create_local_producer_control(IProducerControl **ctlOut, char *loadedPath, size_t loadedPathSize) {
|
||
char ocxPath[MAX_PATH];
|
||
DWORD pathLen;
|
||
HMODULE module = NULL;
|
||
DllGetClassObjectFn dllGetClassObject;
|
||
IClassFactory *factory = NULL;
|
||
HRESULT hr;
|
||
|
||
if (!ctlOut) {
|
||
return E_POINTER;
|
||
}
|
||
|
||
*ctlOut = NULL;
|
||
if (loadedPath && loadedPathSize > 0) {
|
||
loadedPath[0] = '\0';
|
||
}
|
||
|
||
pathLen = GetModuleFileNameA(NULL, ocxPath, (DWORD)sizeof(ocxPath));
|
||
if (pathLen == 0 || pathLen >= sizeof(ocxPath)) {
|
||
return HRESULT_FROM_WIN32(GetLastError());
|
||
}
|
||
{
|
||
char *lastSep = strrchr(ocxPath, '\\');
|
||
if (!lastSep) lastSep = strrchr(ocxPath, '/');
|
||
if (lastSep) {
|
||
strcpy(lastSep + 1, "prct3260.ocx");
|
||
} else {
|
||
strcpy(ocxPath, "prct3260.ocx");
|
||
}
|
||
}
|
||
/* Set CWD to the exe folder so the patched OCX's "DT_Codecs=.\" and
|
||
"DT_Plugins=.\" paths resolve to the directory containing the OCX/DLLs. */
|
||
{
|
||
char exeDir[MAX_PATH];
|
||
char *lastSep2;
|
||
strncpy(exeDir, ocxPath, sizeof(exeDir) - 1);
|
||
exeDir[sizeof(exeDir) - 1] = '\0';
|
||
lastSep2 = strrchr(exeDir, '\\');
|
||
if (lastSep2) {
|
||
*lastSep2 = '\0';
|
||
SetCurrentDirectoryA(exeDir);
|
||
}
|
||
}
|
||
|
||
if (GetFileAttributesA(ocxPath) == INVALID_FILE_ATTRIBUTES) {
|
||
return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);
|
||
}
|
||
|
||
module = LoadLibraryExA(ocxPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
|
||
if (!module) {
|
||
return HRESULT_FROM_WIN32(GetLastError());
|
||
}
|
||
|
||
dllGetClassObject = (DllGetClassObjectFn)GetProcAddress(module, "DllGetClassObject");
|
||
if (!dllGetClassObject) {
|
||
FreeLibrary(module);
|
||
return HRESULT_FROM_WIN32(GetLastError());
|
||
}
|
||
|
||
hr = dllGetClassObject(__uuidof(ProducerControl), __uuidof(IClassFactory), (void **)&factory);
|
||
if (FAILED(hr) || !factory) {
|
||
FreeLibrary(module);
|
||
return FAILED(hr) ? hr : E_NOINTERFACE;
|
||
}
|
||
|
||
hr = factory->CreateInstance(NULL, __uuidof(IProducerControl), (void **)ctlOut);
|
||
factory->Release();
|
||
|
||
if (FAILED(hr) || !*ctlOut) {
|
||
FreeLibrary(module);
|
||
return FAILED(hr) ? hr : E_FAIL;
|
||
}
|
||
|
||
g_local_ocx_module = module;
|
||
if (loadedPath && loadedPathSize > 0) {
|
||
strncpy(loadedPath, ocxPath, loadedPathSize - 1);
|
||
loadedPath[loadedPathSize - 1] = '\0';
|
||
}
|
||
|
||
return S_OK;
|
||
}
|
||
|
||
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 resolve_full_path(const char *path, char *pathBuf, size_t pathBufSize) {
|
||
DWORD written;
|
||
|
||
if (!path || !*path || !pathBuf || pathBufSize < MAX_PATH) {
|
||
return 0;
|
||
}
|
||
|
||
written = GetFullPathNameA(path, (DWORD)pathBufSize, pathBuf, NULL);
|
||
if (written == 0 || written >= pathBufSize) {
|
||
return 0;
|
||
}
|
||
|
||
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;
|
||
unsigned long long gainSamples = 0;
|
||
unsigned long long gainChanged = 0;
|
||
unsigned long long loopCount = 0;
|
||
unsigned long long decodeCalls = 0;
|
||
unsigned long long zeroFrameBytes = 0;
|
||
unsigned long long zeroSampleFrames = 0;
|
||
unsigned long long writtenFrames = 0;
|
||
int sampleRate = 0;
|
||
int channels = 0;
|
||
int haveAudio = 0;
|
||
|
||
if (!make_temp_wav_path(outputPath, outputPathSize)) {
|
||
fprintf(stderr,
|
||
"decode: failed to create temp wav path (outputPathSize=%llu)\n",
|
||
(unsigned long long)outputPathSize);
|
||
return 0;
|
||
}
|
||
|
||
inputData = read_entire_file(inputPath, &inputSize);
|
||
if (!inputData) {
|
||
fprintf(stderr,
|
||
"decode: failed to read input file '%s'\n",
|
||
inputPath ? inputPath : "(null)");
|
||
DeleteFileA(outputPath);
|
||
return 0;
|
||
}
|
||
|
||
out = fopen(outputPath, "wb");
|
||
if (!out) {
|
||
fprintf(stderr,
|
||
"decode: failed to open temp wav for write '%s'\n",
|
||
outputPath);
|
||
free(inputData);
|
||
DeleteFileA(outputPath);
|
||
return 0;
|
||
}
|
||
|
||
if (fwrite(wavHeader, 1, sizeof(wavHeader), out) != sizeof(wavHeader)) {
|
||
fprintf(stderr,
|
||
"decode: failed to write placeholder wav header '%s'\n",
|
||
outputPath);
|
||
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);
|
||
int totalSamples;
|
||
|
||
++loopCount;
|
||
++decodeCalls;
|
||
|
||
if (info.frame_bytes <= 0) {
|
||
++zeroFrameBytes;
|
||
++pos;
|
||
continue;
|
||
}
|
||
|
||
pos += (size_t)info.frame_bytes;
|
||
if (samples <= 0) {
|
||
++zeroSampleFrames;
|
||
continue;
|
||
}
|
||
|
||
if (info.channels <= 0) {
|
||
fprintf(stderr,
|
||
"decode: invalid channels=%d at loop=%llu pos=%llu frame_bytes=%d samples=%d hz=%d\n",
|
||
info.channels,
|
||
loopCount,
|
||
(unsigned long long)pos,
|
||
info.frame_bytes,
|
||
samples,
|
||
info.hz);
|
||
fclose(out);
|
||
DeleteFileA(outputPath);
|
||
free(inputData);
|
||
return 0;
|
||
}
|
||
|
||
totalSamples = samples * info.channels;
|
||
|
||
if (!haveAudio) {
|
||
sampleRate = info.hz;
|
||
channels = info.channels;
|
||
haveAudio = 1;
|
||
} else if (sampleRate != info.hz || channels != info.channels) {
|
||
fprintf(stderr,
|
||
"decode: stream format changed at loop=%llu pos=%llu from %dHz/%dch to %dHz/%dch\n",
|
||
loopCount,
|
||
(unsigned long long)pos,
|
||
sampleRate,
|
||
channels,
|
||
info.hz,
|
||
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 < totalSamples; ++sampleIndex) {
|
||
short before = (short)pcm[sampleIndex];
|
||
short after = apply_gain_sample(before, gainScale);
|
||
pcm[sampleIndex] = after;
|
||
++gainSamples;
|
||
if (after != before) {
|
||
++gainChanged;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (fwrite(pcm, sizeof(mp3d_sample_t), (size_t)totalSamples, out) != (size_t)totalSamples) {
|
||
fprintf(stderr,
|
||
"decode: fwrite failed at frame=%llu totalSamples=%d\n",
|
||
writtenFrames,
|
||
totalSamples);
|
||
fclose(out);
|
||
DeleteFileA(outputPath);
|
||
free(inputData);
|
||
return 0;
|
||
}
|
||
++writtenFrames;
|
||
dataBytes += (unsigned)(totalSamples * sizeof(mp3d_sample_t));
|
||
}
|
||
|
||
free(inputData);
|
||
|
||
if (!haveAudio) {
|
||
fprintf(stderr,
|
||
"decode: failed - no decodable audio frames found\n");
|
||
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)) {
|
||
fprintf(stderr, "decode: failed writing wav header\n");
|
||
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;
|
||
unsigned long long gainSamples = 0;
|
||
unsigned long long gainChanged = 0;
|
||
|
||
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);
|
||
++gainSamples;
|
||
if (scaled != sample) {
|
||
++gainChanged;
|
||
}
|
||
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);
|
||
|
||
if (gainScale != 1.0) {
|
||
fprintf(stderr,
|
||
"gain: rewrote wav with scale=%.6f, changed %llu/%llu samples\n",
|
||
gainScale,
|
||
gainChanged,
|
||
gainSamples);
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
static int nearest_supported_rate(int rate) {
|
||
static const int rates[] = {11025, 16000, 22050, 32000, 44100};
|
||
int i;
|
||
int best = rates[0];
|
||
int bestDiff = abs(rate - rates[0]);
|
||
for (i = 1; i < (int)(sizeof(rates) / sizeof(rates[0])); ++i) {
|
||
int diff = abs(rate - rates[i]);
|
||
if (diff < bestDiff) {
|
||
bestDiff = diff;
|
||
best = rates[i];
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
static unsigned char *resample_pcm16(const unsigned char *inputBytes,
|
||
unsigned inputSamples,
|
||
int channels,
|
||
int inRate,
|
||
int outRate,
|
||
unsigned *outputSamplesOut) {
|
||
unsigned inputFrames;
|
||
unsigned outputFrames;
|
||
unsigned totalOut;
|
||
short *output;
|
||
unsigned f;
|
||
|
||
if (!inputBytes || inputSamples == 0 || channels <= 0 || inRate <= 0 || outRate <= 0) {
|
||
return NULL;
|
||
}
|
||
|
||
inputFrames = inputSamples / (unsigned)channels;
|
||
outputFrames = (unsigned)((unsigned long long)inputFrames * (unsigned)outRate / (unsigned)inRate);
|
||
if (outputFrames == 0) {
|
||
return NULL;
|
||
}
|
||
|
||
totalOut = outputFrames * (unsigned)channels;
|
||
output = (short *)malloc(totalOut * sizeof(short));
|
||
if (!output) {
|
||
return NULL;
|
||
}
|
||
|
||
for (f = 0; f < outputFrames; ++f) {
|
||
double pos = (double)f * inRate / outRate;
|
||
unsigned p0 = (unsigned)pos;
|
||
unsigned p1 = p0 + 1;
|
||
double frac = pos - (double)p0;
|
||
int c;
|
||
if (p1 >= inputFrames) {
|
||
p1 = inputFrames - 1;
|
||
}
|
||
for (c = 0; c < channels; ++c) {
|
||
unsigned i0 = (p0 * (unsigned)channels + (unsigned)c) * 2u;
|
||
unsigned i1 = (p1 * (unsigned)channels + (unsigned)c) * 2u;
|
||
short s0 = (short)((unsigned short)inputBytes[i0] | ((unsigned short)inputBytes[i0 + 1] << 8));
|
||
short s1 = (short)((unsigned short)inputBytes[i1] | ((unsigned short)inputBytes[i1 + 1] << 8));
|
||
double sv = (double)s0 + ((double)s1 - (double)s0) * frac;
|
||
short out;
|
||
if (sv > 32767.0) out = 32767;
|
||
else if (sv < -32768.0) out = -32768;
|
||
else out = (short)(sv >= 0.0 ? sv + 0.5 : sv - 0.5);
|
||
output[f * (unsigned)channels + (unsigned)c] = out;
|
||
}
|
||
}
|
||
|
||
*outputSamplesOut = totalOut;
|
||
return (unsigned char *)output;
|
||
}
|
||
|
||
static int resample_wav_if_needed(const char *inputPath,
|
||
char *outputPath,
|
||
size_t outputPathSize) {
|
||
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;
|
||
int targetRate;
|
||
unsigned outputSamples = 0;
|
||
unsigned char *resampled = NULL;
|
||
FILE *out = NULL;
|
||
unsigned char header[44] = {0};
|
||
|
||
if (!outputPath || outputPathSize == 0) {
|
||
return -1;
|
||
}
|
||
|
||
outputPath[0] = '\0';
|
||
data = read_entire_file(inputPath, &size);
|
||
if (!data) {
|
||
return -1;
|
||
}
|
||
|
||
if (!find_wav_data_chunk(data, size, &fmtOffset, &fmtSize, &dataOffset, &dataSize) || fmtSize < 16) {
|
||
free(data);
|
||
return -1;
|
||
}
|
||
|
||
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 -1;
|
||
}
|
||
|
||
targetRate = nearest_supported_rate((int)sampleRate);
|
||
if (targetRate == (int)sampleRate) {
|
||
free(data);
|
||
return 0;
|
||
}
|
||
|
||
fprintf(stderr, "resampling: %u Hz -> %d Hz\n", sampleRate, targetRate);
|
||
|
||
resampled = resample_pcm16(data + dataOffset, dataSize / 2u,
|
||
(int)channels, (int)sampleRate, targetRate,
|
||
&outputSamples);
|
||
free(data);
|
||
|
||
if (!resampled) {
|
||
return -1;
|
||
}
|
||
|
||
if (!make_temp_wav_path(outputPath, outputPathSize)) {
|
||
free(resampled);
|
||
return -1;
|
||
}
|
||
|
||
out = fopen(outputPath, "wb");
|
||
if (!out) {
|
||
free(resampled);
|
||
outputPath[0] = '\0';
|
||
return -1;
|
||
}
|
||
|
||
fill_wav_header(header, outputSamples * 2u, targetRate, (int)channels, 16);
|
||
if (fwrite(header, 1, sizeof(header), out) != sizeof(header) ||
|
||
fwrite(resampled, 2u, (size_t)outputSamples, out) != (size_t)outputSamples) {
|
||
fclose(out);
|
||
DeleteFileA(outputPath);
|
||
free(resampled);
|
||
outputPath[0] = '\0';
|
||
return -1;
|
||
}
|
||
|
||
fclose(out);
|
||
free(resampled);
|
||
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";
|
||
if (_stricmp(r, "PG") == 0) return "PG";
|
||
if (_stricmp(r, "R") == 0) return "R";
|
||
if (_stricmp(r, "X") == 0) return "X";
|
||
return NULL;
|
||
}
|
||
|
||
// Mode → AUDIO_CONTENT_* (from docs)
|
||
static AUDIO_CONTENT mode_to_audio_content(const char *m) {
|
||
// These constants come from the RealProducer type library.
|
||
// You’ll need to confirm exact values in the imported header:
|
||
// AUDIO_CONTENT_VOICE, AUDIO_CONTENT_VOICE_OVER_MUSIC, AUDIO_CONTENT_MUSIC, AUDIO_CONTENT_STEREO, etc.
|
||
if (_stricmp(m, "voice") == 0) return AUDIO_CONTENT_VOICE;
|
||
if (_stricmp(m, "voice-bgm") == 0) return AUDIO_CONTENT_VOICE_BACKGROUND;
|
||
if (_stricmp(m, "stereo") == 0) return AUDIO_CONTENT_MUSIC_STEREO;
|
||
// default: music
|
||
return AUDIO_CONTENT_MUSIC;
|
||
}
|
||
|
||
// Target → TARGET_* + TargetXXModem property
|
||
typedef enum {
|
||
TGT_WEBTV,
|
||
TGT_56K,
|
||
TGT_ISDN,
|
||
TGT_CABLE,
|
||
TGT_PC
|
||
} target_t;
|
||
|
||
typedef struct {
|
||
ICodecCookie *cookie;
|
||
AUDIO_CONTENT content;
|
||
long codecId;
|
||
long flavorId;
|
||
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;
|
||
if (_stricmp(s, "56k") == 0) return TGT_56K;
|
||
if (_stricmp(s, "ISDN") == 0) return TGT_ISDN;
|
||
if (_stricmp(s, "Cable") == 0) return TGT_CABLE;
|
||
if (_stricmp(s, "PC") == 0) return TGT_PC;
|
||
return TGT_WEBTV;
|
||
}
|
||
|
||
static TARGET_AUDIENCE target_to_audience(target_t tgt) {
|
||
switch (tgt) {
|
||
case TGT_WEBTV:
|
||
return TARGET_28_MODEM;
|
||
case TGT_56K:
|
||
return TARGET_56_MODEM;
|
||
case TGT_ISDN:
|
||
return TARGET_SINGLE_ISDN;
|
||
case TGT_CABLE:
|
||
return TARGET_LAN_LOW;
|
||
case TGT_PC:
|
||
return TARGET_LAN_HIGH;
|
||
}
|
||
|
||
return TARGET_28_MODEM;
|
||
}
|
||
|
||
static AUDIO_CONTENT codec_name_to_audio_content(const char *name) {
|
||
if (!name) {
|
||
return AUDIO_CONTENT_MUSIC;
|
||
}
|
||
if (strstr(name, "Stereo") != NULL) {
|
||
return AUDIO_CONTENT_MUSIC_STEREO;
|
||
}
|
||
if (strstr(name, "Music") != NULL) {
|
||
return AUDIO_CONTENT_MUSIC;
|
||
}
|
||
return AUDIO_CONTENT_VOICE;
|
||
}
|
||
|
||
static int parse_codec_index(const char *s) {
|
||
char *end = NULL;
|
||
long value;
|
||
|
||
if (!s || !*s) {
|
||
return -1;
|
||
}
|
||
|
||
value = strtol(s, &end, 10);
|
||
if (!end || *end != '\0' || value < 0 || value > 0x7fffffffL) {
|
||
return -1;
|
||
}
|
||
|
||
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;
|
||
LONG count = 0;
|
||
IDispatch *pDisp = NULL;
|
||
LONG i;
|
||
|
||
if (!selected) {
|
||
return 0;
|
||
}
|
||
|
||
memset(selected, 0, sizeof(*selected));
|
||
selected->codecId = -1;
|
||
selected->flavorId = -1;
|
||
selected->content = AUDIO_CONTENT_MUSIC;
|
||
|
||
hr = ctl->get_AudioCodecEnum(&enumDisp);
|
||
if (FAILED(hr) || !enumDisp) {
|
||
return 0;
|
||
}
|
||
|
||
enumDisp->GetCount(&count);
|
||
if (codecIndex < 0 || codecIndex >= count) {
|
||
enumDisp->Release();
|
||
return 0;
|
||
}
|
||
|
||
enumDisp->First(&pDisp);
|
||
for (i = 0; i < count && pDisp; ++i) {
|
||
if (i == codecIndex) {
|
||
IAudioCodecInfo *codec = NULL;
|
||
if (SUCCEEDED(pDisp->QueryInterface(__uuidof(IAudioCodecInfo), (void**)&codec))) {
|
||
BSTR name = NULL;
|
||
codec->get_CodecName(&name);
|
||
codec->get_CodecCookie(&selected->cookie);
|
||
if (selected->cookie) {
|
||
selected->cookie->get_codecId(&selected->codecId);
|
||
selected->cookie->get_flavorId(&selected->flavorId);
|
||
}
|
||
bstr_to_ansi(name, selected->name, sizeof(selected->name));
|
||
selected->content = codec_name_to_audio_content(selected->name);
|
||
if (name) SysFreeString(name);
|
||
codec->Release();
|
||
}
|
||
pDisp->Release();
|
||
enumDisp->Release();
|
||
return selected->cookie != NULL;
|
||
}
|
||
|
||
{
|
||
IDispatch *next = NULL;
|
||
enumDisp->Next(&next);
|
||
pDisp->Release();
|
||
pDisp = next;
|
||
}
|
||
}
|
||
|
||
if (pDisp) pDisp->Release();
|
||
enumDisp->Release();
|
||
return 0;
|
||
}
|
||
|
||
static int apply_audio_codec_to_target(ITargetAudienceInfo *info,
|
||
AUDIO_CONTENT content,
|
||
ICodecCookie *cookie) {
|
||
if (!info || !cookie) {
|
||
return 0;
|
||
}
|
||
|
||
return SUCCEEDED(info->put_AudioCodec(TARGET_AUDIENCES_AUDIO, content, cookie));
|
||
}
|
||
|
||
static int apply_selected_audio_codec(IProducerControl *ctl,
|
||
target_t tgt,
|
||
const selected_codec_t *selected) {
|
||
ITargetAudienceInfo *info = NULL;
|
||
TARGET_AUDIENCE audienceList[] = {
|
||
TARGET_28_MODEM,
|
||
TARGET_56_MODEM,
|
||
TARGET_SINGLE_ISDN,
|
||
TARGET_DUAL_ISDN,
|
||
TARGET_LAN_LOW,
|
||
TARGET_LAN_HIGH
|
||
};
|
||
int applied = 0;
|
||
int i;
|
||
|
||
if (!ctl || !selected || !selected->cookie) {
|
||
return 0;
|
||
}
|
||
|
||
for (i = 0; i < (int)(sizeof(audienceList) / sizeof(audienceList[0])); ++i) {
|
||
if (SUCCEEDED(ctl->get_TargetAudienceInfo(audienceList[i], &info)) && info) {
|
||
applied |= apply_audio_codec_to_target(info, selected->content, selected->cookie);
|
||
info->Release();
|
||
info = NULL;
|
||
}
|
||
}
|
||
|
||
if (SUCCEEDED(ctl->get_TargetAudienceInfo(target_to_audience(tgt), &info)) && info) {
|
||
applied |= apply_audio_codec_to_target(info, selected->content, selected->cookie);
|
||
info->Release();
|
||
}
|
||
|
||
return applied;
|
||
}
|
||
|
||
// Enable only one target audience based on CLI
|
||
static void set_target_audience(IProducerControl *ctl, target_t tgt) {
|
||
// First, clear all target audiences
|
||
ctl->put_Target28KModem(VARIANT_FALSE);
|
||
ctl->put_Target56KModem(VARIANT_FALSE);
|
||
ctl->put_TargetSingleISDN(VARIANT_FALSE);
|
||
ctl->put_TargetDSLCableModem(VARIANT_FALSE);
|
||
ctl->put_TargetLAN(VARIANT_FALSE);
|
||
|
||
switch (tgt) {
|
||
case TGT_WEBTV:
|
||
ctl->put_Target28KModem(VARIANT_TRUE);
|
||
break;
|
||
case TGT_56K:
|
||
ctl->put_Target56KModem(VARIANT_TRUE);
|
||
break;
|
||
case TGT_ISDN:
|
||
ctl->put_TargetSingleISDN(VARIANT_TRUE);
|
||
break;
|
||
case TGT_CABLE:
|
||
ctl->put_TargetDSLCableModem(VARIANT_TRUE);
|
||
break;
|
||
case TGT_PC:
|
||
ctl->put_TargetLAN(VARIANT_TRUE);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// List all audio codecs using AudioCodecEnum
|
||
static void list_codecs(IProducerControl *ctl) {
|
||
IEnumIDispatch *enumDisp = NULL;
|
||
HRESULT hr = ctl->get_AudioCodecEnum(&enumDisp);
|
||
if (FAILED(hr) || !enumDisp) {
|
||
die("failed to get AudioCodecEnum");
|
||
}
|
||
|
||
LONG count = 0;
|
||
enumDisp->GetCount(&count);
|
||
|
||
printf("Available audio codecs:\n");
|
||
|
||
IDispatch *pDisp = NULL;
|
||
enumDisp->First(&pDisp);
|
||
for (LONG i = 0; i < count && pDisp; ++i) {
|
||
IAudioCodecInfo *codec = NULL;
|
||
if (SUCCEEDED(pDisp->QueryInterface(__uuidof(IAudioCodecInfo), (void**)&codec))) {
|
||
BSTR name = NULL;
|
||
ICodecCookie *cookie = NULL;
|
||
long codecId = -1;
|
||
long flavorId = -1;
|
||
codec->get_CodecName(&name);
|
||
codec->get_CodecCookie(&cookie);
|
||
if (cookie) {
|
||
cookie->get_codecId(&codecId);
|
||
cookie->get_flavorId(&flavorId);
|
||
}
|
||
|
||
char buf[512];
|
||
WideCharToMultiByte(CP_ACP, 0, name, -1, buf, sizeof(buf), NULL, NULL);
|
||
printf(" %ld: %s (codecId=%ld, flavorId=%ld)\n", i, buf, codecId, flavorId);
|
||
|
||
SysFreeString(name);
|
||
if (cookie) cookie->Release();
|
||
codec->Release();
|
||
}
|
||
IDispatch *next = NULL;
|
||
enumDisp->Next(&next);
|
||
pDisp->Release();
|
||
pDisp = next;
|
||
}
|
||
|
||
if (pDisp) pDisp->Release();
|
||
enumDisp->Release();
|
||
}
|
||
|
||
// Set basic clip properties (Title, Author, etc.)
|
||
static void set_clip_properties(IProducerControl *ctl,
|
||
const char *title,
|
||
const char *author,
|
||
const char *copyright,
|
||
const char *description,
|
||
const char *keywords,
|
||
const char *rating) {
|
||
if (title) {
|
||
BSTR b = ansi_to_bstr(title);
|
||
ctl->put_Title(b);
|
||
SysFreeString(b);
|
||
}
|
||
if (author) {
|
||
BSTR b = ansi_to_bstr(author);
|
||
ctl->put_Author(b);
|
||
SysFreeString(b);
|
||
}
|
||
if (copyright) {
|
||
BSTR b = ansi_to_bstr(copyright);
|
||
ctl->put_Copyright(b);
|
||
SysFreeString(b);
|
||
}
|
||
|
||
// Extra metadata via SetStringProperty (from docs)
|
||
if (description) {
|
||
BSTR key = SysAllocString(L"Description");
|
||
BSTR val = ansi_to_bstr(description);
|
||
ctl->SetStringProperty(key, val);
|
||
SysFreeString(key);
|
||
SysFreeString(val);
|
||
}
|
||
if (keywords) {
|
||
BSTR key = SysAllocString(L"Keywords");
|
||
BSTR val = ansi_to_bstr(keywords);
|
||
ctl->SetStringProperty(key, val);
|
||
SysFreeString(key);
|
||
SysFreeString(val);
|
||
}
|
||
if (rating) {
|
||
const char *r = rating_to_str(rating);
|
||
if (r) {
|
||
BSTR key = SysAllocString(L"Rating");
|
||
BSTR val = ansi_to_bstr(r);
|
||
ctl->SetStringProperty(key, val);
|
||
SysFreeString(key);
|
||
SysFreeString(val);
|
||
}
|
||
}
|
||
|
||
// SelectiveRecord, MobilePlay, Indexable → false via custom numeric props
|
||
// Names are inferred; adjust to actual property names if they exist directly.
|
||
{
|
||
BSTR key = SysAllocString(L"SelectiveRecord");
|
||
ctl->SetNumberProperty(key, 0);
|
||
SysFreeString(key);
|
||
}
|
||
{
|
||
BSTR key = SysAllocString(L"MobilePlay");
|
||
ctl->SetNumberProperty(key, 0);
|
||
SysFreeString(key);
|
||
}
|
||
{
|
||
BSTR key = SysAllocString(L"Indexable");
|
||
ctl->SetNumberProperty(key, 0);
|
||
SysFreeString(key);
|
||
}
|
||
}
|
||
|
||
static void usage(void) {
|
||
fprintf(stderr,
|
||
"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"
|
||
" --author TEXT\n"
|
||
" --copyright TEXT\n"
|
||
" --description TEXT\n"
|
||
" --keywords TEXT\n"
|
||
" --rating G|PG|R|X\n"
|
||
);
|
||
}
|
||
|
||
int main(int argc, char **argv) {
|
||
const char *mode = "music";
|
||
const char *target_str = "WebTV";
|
||
const char *title = NULL;
|
||
const char *author = NULL;
|
||
const char *copyright = NULL;
|
||
const char *description = NULL;
|
||
const char *keywords = NULL;
|
||
const char *rating = NULL;
|
||
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};
|
||
char infile_abs[MAX_PATH] = {0};
|
||
char outfile_abs[MAX_PATH] = {0};
|
||
char ocx_loaded_path[MAX_PATH] = {0};
|
||
const char *outfile = NULL;
|
||
|
||
// Parse options
|
||
int i = 1;
|
||
for (; i < argc; ++i) {
|
||
if (strcmp(argv[i], "--ra5") == 0) {
|
||
player_is_ra5 = 1;
|
||
} else if (strcmp(argv[i], "--g2") == 0) {
|
||
player_is_ra5 = 0;
|
||
} else if (strcmp(argv[i], "--codec-list") == 0) {
|
||
do_codec_list = 1;
|
||
} else if (strcmp(argv[i], "--codec") == 0 && i+1 < argc) {
|
||
codec_index = parse_codec_index(argv[++i]);
|
||
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) {
|
||
target_str = argv[++i];
|
||
target_was_explicit = 1;
|
||
} else if (strcmp(argv[i], "--title") == 0 && i+1 < argc) {
|
||
title = argv[++i];
|
||
} else if (strcmp(argv[i], "--author") == 0 && i+1 < argc) {
|
||
author = argv[++i];
|
||
} else if (strcmp(argv[i], "--copyright") == 0 && i+1 < argc) {
|
||
copyright = argv[++i];
|
||
} else if (strcmp(argv[i], "--description") == 0 && i+1 < argc) {
|
||
description = argv[++i];
|
||
} else if (strcmp(argv[i], "--keywords") == 0 && i+1 < argc) {
|
||
keywords = argv[++i];
|
||
} else if (strcmp(argv[i], "--rating") == 0 && i+1 < argc) {
|
||
rating = argv[++i];
|
||
} else if (strncmp(argv[i], "--", 2) == 0) {
|
||
fprintf(stderr, "unknown option: %s\n", argv[i]);
|
||
usage();
|
||
return 1;
|
||
} else {
|
||
// Positional arguments can appear anywhere: first is input, second is output.
|
||
if (!infile) {
|
||
infile = argv[i];
|
||
} else if (!outfile) {
|
||
outfile = argv[i];
|
||
} else {
|
||
fprintf(stderr, "unexpected positional argument: %s\n", argv[i]);
|
||
usage();
|
||
return 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!infile && !do_codec_list) {
|
||
usage();
|
||
return 1;
|
||
}
|
||
|
||
if (codec_index >= 0 && target_was_explicit) {
|
||
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, '.');
|
||
size_t len = p ? (size_t)(p - infile) : strlen(infile);
|
||
if (len >= sizeof(outbuf)-4) die("input filename too long");
|
||
memcpy(outbuf, infile, len);
|
||
strcpy(outbuf + len, ".ra");
|
||
outfile = outbuf;
|
||
}
|
||
|
||
if (infile && !resolve_full_path(infile, infile_abs, sizeof(infile_abs))) {
|
||
die("failed to resolve full path for input file");
|
||
}
|
||
if (outfile && !resolve_full_path(outfile, outfile_abs, sizeof(outfile_abs))) {
|
||
die("failed to resolve full path for output file");
|
||
}
|
||
|
||
if (infile) {
|
||
infile = infile_abs;
|
||
}
|
||
if (outfile) {
|
||
outfile = outfile_abs;
|
||
}
|
||
|
||
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");
|
||
|
||
IProducerControl *ctl = NULL;
|
||
hr = create_local_producer_control(&ctl, ocx_loaded_path, sizeof(ocx_loaded_path));
|
||
if (FAILED(hr) || !ctl) {
|
||
CoUninitialize();
|
||
die("failed to create ProducerControl from local prct3260.ocx in current folder");
|
||
}
|
||
|
||
if (do_codec_list) {
|
||
list_codecs(ctl);
|
||
ctl->Release();
|
||
unload_local_ocx();
|
||
CoUninitialize();
|
||
return 0;
|
||
}
|
||
|
||
memset(&selected_codec, 0, sizeof(selected_codec));
|
||
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();
|
||
unload_local_ocx();
|
||
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();
|
||
unload_local_ocx();
|
||
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;
|
||
}
|
||
|
||
// Resample to nearest supported rate: 11025, 16000, 22050, 32000, 44100 (cap at 44100)
|
||
{
|
||
int rsResult = resample_wav_if_needed(encoder_input, g_temp_resample_path, sizeof(g_temp_resample_path));
|
||
if (rsResult < 0) {
|
||
fprintf(stderr, "warning: could not check sample rate of input; using as-is\n");
|
||
} else if (rsResult > 0) {
|
||
if (g_temp_input_path[0]) {
|
||
DeleteFileA(g_temp_input_path);
|
||
g_temp_input_path[0] = '\0';
|
||
}
|
||
encoder_input = g_temp_resample_path;
|
||
}
|
||
}
|
||
|
||
if (codec_index >= 0 && !get_audio_codec_by_index(ctl, codec_index, &selected_codec)) {
|
||
ctl->Release();
|
||
unload_local_ocx();
|
||
CoUninitialize();
|
||
die("invalid codec index");
|
||
}
|
||
|
||
// Input properties
|
||
ctl->put_InputType(INPUT_SOURCE_FILE);
|
||
{
|
||
BSTR b = ansi_to_bstr(encoder_input);
|
||
ctl->put_InputFilename(b);
|
||
SysFreeString(b);
|
||
}
|
||
ctl->put_InputDoAudio(VARIANT_TRUE);
|
||
ctl->put_InputDoVideo(VARIANT_FALSE);
|
||
ctl->put_InputDoEvents(VARIANT_FALSE);
|
||
|
||
// Output properties
|
||
ctl->put_DoOutputFile(VARIANT_TRUE);
|
||
{
|
||
BSTR b = ansi_to_bstr(outfile);
|
||
ctl->put_OutputFilename(b);
|
||
SysFreeString(b);
|
||
}
|
||
|
||
// PlayerCompatibility: RA5 vs G2 (PLAYER_5 / PLAYER_6)
|
||
ctl->put_PlayerCompatibility(player_is_ra5 ? PLAYER_5 : PLAYER_6);
|
||
|
||
// SureStream: hard-coded false (single-rate)
|
||
ctl->put_SureStream(VARIANT_FALSE);
|
||
|
||
// AudioContent (mode)
|
||
ctl->put_AudioContent(codec_index >= 0 ? selected_codec.content : mode_to_audio_content(mode));
|
||
|
||
// Disable producer-side emphasis so manual --gain attenuation is preserved.
|
||
ctl->put_EmphasizeAudio(VARIANT_FALSE);
|
||
|
||
// Target audience
|
||
set_target_audience(ctl, parse_target(target_str));
|
||
|
||
if (codec_index >= 0 && !apply_selected_audio_codec(ctl, parse_target(target_str), &selected_codec)) {
|
||
if (selected_codec.cookie) selected_codec.cookie->Release();
|
||
ctl->Release();
|
||
unload_local_ocx();
|
||
CoUninitialize();
|
||
die("failed to apply selected codec");
|
||
}
|
||
|
||
// Clip properties + custom props
|
||
set_clip_properties(ctl, title, author, copyright,
|
||
description, keywords, rating);
|
||
|
||
// Start encoding
|
||
hr = ctl->StartEncoding();
|
||
if (FAILED(hr)) {
|
||
ctl->Release();
|
||
unload_local_ocx();
|
||
CoUninitialize();
|
||
die("StartEncoding failed");
|
||
}
|
||
|
||
// RealProducer uses STA COM callbacks/messages while encoding.
|
||
// Pump the queue so the control can make progress in a console app.
|
||
VARIANT_BOOL isEnc = VARIANT_FALSE;
|
||
DWORD startTick = GetTickCount();
|
||
do {
|
||
long lastError = 0;
|
||
|
||
pump_pending_messages();
|
||
ctl->get_IsEncoding(&isEnc);
|
||
|
||
if (SUCCEEDED(ctl->get_LastError(&lastError)) && lastError != 0) {
|
||
ctl->StopEncoding();
|
||
if (selected_codec.cookie) selected_codec.cookie->Release();
|
||
die_last_error(ctl, "encoding failed");
|
||
}
|
||
|
||
if (GetTickCount() - startTick > 300000) {
|
||
ctl->StopEncoding();
|
||
if (selected_codec.cookie) selected_codec.cookie->Release();
|
||
die_last_error(ctl, "encoding timed out");
|
||
}
|
||
|
||
MsgWaitForMultipleObjects(0, NULL, FALSE, 100, QS_ALLINPUT);
|
||
} while (isEnc == VARIANT_TRUE);
|
||
|
||
{
|
||
long lastError = 0;
|
||
if (SUCCEEDED(ctl->get_LastError(&lastError)) && lastError != 0) {
|
||
if (selected_codec.cookie) selected_codec.cookie->Release();
|
||
die_last_error(ctl, "encoding completed with an error");
|
||
}
|
||
}
|
||
|
||
// StopEncoding (in case)
|
||
ctl->StopEncoding();
|
||
|
||
if (selected_codec.cookie) selected_codec.cookie->Release();
|
||
ctl->Release();
|
||
unload_local_ocx();
|
||
CoUninitialize();
|
||
cleanup_temp_input();
|
||
|
||
printf("Encoded %s -> %s\n", infile, outfile);
|
||
return 0;
|
||
}
|