Files
2026-04-23 11:16:24 -04:00

1855 lines
54 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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.
// Youll 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;
}