view tools/mod2wav.c @ 2208:90ec1ec89c56

Revamp the palette handling in lib64gfx somewhat, add helper functions to lib64util for handling external palette file options and add support for specifying one of the "internal" palettes or external (.act) palette file to gfxconv and 64vw.
author Matti Hamalainen <ccr@tnsp.org>
date Fri, 14 Jun 2019 05:01:12 +0300
parents e3f0eaf23f4f
children c146033f1f6a
line wrap: on
line source

/*
 * mod2wav - Render XM/JSSMOD module to WAV waveform file
 * Programmed and designed by Matti 'ccr' Hamalainen
 * (C) Copyright 2007 Tecnic Software productions (TNSP)
 *
 * Please read file 'COPYING' for information on license and distribution.
 */
#include "dmtool.h"
#include <stdio.h>
#include <stdlib.h>
#include "jss.h"
#include "jssmod.h"
#include "jssmix.h"
#include "jssplr.h"
#include "dmlib.h"
#include "dmargs.h"
#include "dmfile.h"
#include "dmmutex.h"


#define DM_WAVE_FORMAT_PCM     (1)
#define DM_WAVE_RIFF_ID        "RIFF"
#define DM_WAVE_WAVE_ID        "WAVE"
#define DM_WAVE_FMT_ID         "fmt "
#define DM_WAVE_DATA_ID        "data"


typedef struct
{
    Uint8     chunkID[4];
    Uint32    chunkSize;
} DMWaveChunk;


typedef struct
{
    Uint8     riffID[4];
    Uint32    fileSize;
    Uint8     riffType[4];

    DMWaveChunk chFormat;

    Uint16    wFormatTag;
    Uint16    nChannels;
    Uint32    nSamplesPerSec;
    Uint32    nAvgBytesPerSec;
    Uint16    nBlockAlign;
    Uint16    wBitsPerSample;

    DMWaveChunk chData;
    // Data follows here
} DMWaveFile;


char    *optInFilename = NULL, *optOutFilename = NULL;
int     optOutFormat = JSS_AUDIO_S16,
        optOutChannels = 2,
        optOutFreq = 44100,
        optMuteOChannels = -1,
        optStartOrder = -1;
BOOL    optUsePlayTime = FALSE;
size_t  optPlayTime;


static const DMOptArg optList[] =
{
    {  0, '?', "help",     "Show this help", OPT_NONE },
    {  2, 'v', "verbose",  "Be more verbose", OPT_NONE },
    {  3, '1', "16bit",    "16-bit output", OPT_NONE },
    {  4, '8', "8bit",     "8-bit output", OPT_NONE },
    {  5, 'm', "mono",     "Mono output", OPT_NONE },
    {  6, 's', "stereo",   "Stereo output", OPT_NONE },
    {  7, 'f', "freq",     "Output frequency", OPT_ARGREQ },
    {  8, 'M', "mute",     "Mute other channels than #", OPT_ARGREQ },
    {  9, 'o', "order",    "Start from order #", OPT_ARGREQ },
    { 10, 't', "time",     "Play for # seconds", OPT_ARGREQ },
//    {10, 'l', "loop",    "Loop for # times", OPT_ARGREQ },
};

const int optListN = sizeof(optList) / sizeof(optList[0]);


BOOL argHandleOpt(const int optN, char *optArg, char *currArg)
{
    (void) optArg;

    switch (optN)
    {
        case 0:
            dmPrintBanner(stdout, dmProgName,
                "[options] [sourcefile] [destfile]");

            dmArgsPrintHelp(stdout, optList, optListN, 0);
            exit(0);
            break;

        case 2:
            dmVerbosity++;
            break;

        case 3:
            optOutFormat = JSS_AUDIO_S16;
            break;

        case 4:
            optOutFormat = JSS_AUDIO_U8;
            break;

        case 5:
            optOutChannels = JSS_AUDIO_MONO;
            break;

        case 6:
            optOutChannels = JSS_AUDIO_STEREO;
            break;

        case 7:
            optOutFreq = atoi(optArg);
            break;

        case 8:
            optMuteOChannels = atoi(optArg);
            break;

        case 9:
            optStartOrder = atoi(optArg);
            break;

        case 10:
            optPlayTime = atoi(optArg);
            optUsePlayTime = TRUE;
            break;

        default:
            dmErrorMsg("Unimplemented option argument '%s'.\n", currArg);
            return FALSE;
    }

    return TRUE;
}


BOOL argHandleFile(char *currArg)
{
    if (!optInFilename)
        optInFilename = currArg;
    else
    if (!optOutFilename)
        optOutFilename = currArg;
    else
    {
        dmErrorMsg("Too many filename arguments (only source and dest needed) '%s'\n", currArg);
        return FALSE;
    }

    return TRUE;
}


BOOL dmWriteWAVChunk(FILE * f, DMWaveChunk *ch)
{
    return dm_fwrite_str(f, ch->chunkID, 4) && dm_fwrite_le32(f, ch->chunkSize);
}


void dmMakeWAVChunk(DMWaveChunk *ch, const char *chunkID, const Uint32 chunkSize)
{
    memcpy(&(ch->chunkID), (const void *) chunkID, 4);
    ch->chunkSize = chunkSize;
}


void dmWriteWAVHeader(FILE *outFile, int sampBits, int sampFreq, int sampChn, size_t sampLen)
{
    DMWaveFile wav;

    // PCM WAVE chunk
    dmMakeWAVChunk(&wav.chFormat, DM_WAVE_FMT_ID, (2 + 2 + 4 + 4 + 2 + 2));

    wav.wFormatTag = DM_WAVE_FORMAT_PCM;
    wav.nChannels = sampChn;
    wav.nSamplesPerSec = sampFreq;
    wav.nAvgBytesPerSec = (sampBits * sampChn * sampFreq) / 8;
    wav.nBlockAlign = (sampBits * sampChn) / 8;
    wav.wBitsPerSample = sampBits;

    // Data chunk
    dmMakeWAVChunk(&wav.chData, DM_WAVE_DATA_ID, (sampLen * wav.nBlockAlign));

    // RIFF header
    memcpy(&wav.riffID, (const void *) DM_WAVE_RIFF_ID, 4);
    memcpy(&wav.riffType, (const void *) DM_WAVE_WAVE_ID, 4);
    wav.fileSize = ((4 + 4 + 4) + wav.chFormat.chunkSize + wav.chData.chunkSize);

    // Write header
    dm_fwrite_str(outFile, wav.riffID, sizeof(wav.riffID));
    dm_fwrite_le32(outFile, wav.fileSize);

    dm_fwrite_str(outFile, wav.riffType, sizeof(wav.riffType));
    dmWriteWAVChunk(outFile, &wav.chFormat);

    dm_fwrite_le16(outFile, wav.wFormatTag);
    dm_fwrite_le16(outFile, wav.nChannels);
    dm_fwrite_le32(outFile, wav.nSamplesPerSec);
    dm_fwrite_le32(outFile, wav.nAvgBytesPerSec);
    dm_fwrite_le16(outFile, wav.nBlockAlign);
    dm_fwrite_le16(outFile, wav.wBitsPerSample);

    dmWriteWAVChunk(outFile, &wav.chData);
}
int main(int argc, char *argv[])
{
    DMResource *inFile = NULL;
    FILE *outFile = NULL;
    JSSModule *mod = NULL;
    JSSMixer *dev = NULL;
    JSSPlayer *plr = NULL;
    int result = -1;
    size_t bufLen = 1024*4, dataTotal, dataWritten, sampSize;
    Uint8 *mb = NULL;

    dmInitProg("mod2wav", "XM/JSSMOD to WAV renderer", "0.2", NULL, NULL);
    dmVerbosity = 1;

    // Parse arguments
    if (!dmArgsProcess(argc, argv, optList, optListN,
        argHandleOpt, argHandleFile, OPTH_BAILOUT))
        exit(1);

    // Check arguments
    if (optInFilename == NULL || optOutFilename == NULL)
    {
        dmErrorMsg("Input or output file not specified. Try --help.\n");
        return 1;
    }

    // Initialize miniJSS
    jssInit();

    // Open the source file
    if ((result = dmf_open_stdio(optInFilename, "rb", &inFile)) != DMERR_OK)
    {
        dmErrorMsg("Error opening input file '%s', %d: %s\n",
            optInFilename, result, dmErrorStr(result));
        return 1;
    }

    // Read module file
    dmMsg(1, "Reading file: %s\n", optInFilename);
#ifdef JSS_SUP_XM
    result = jssLoadXM(inFile, &mod, TRUE);
#endif
#ifdef JSS_SUP_JSSMOD
    dmfreset(inFile);
    if (result != DMERR_OK)
    {
        dmMsg(1, "* Trying JSSMOD ...\n");
        result = jssLoadJSSMOD(inFile, &mod, TRUE);
        dmfreset(inFile);
        if (result == DMERR_OK)
            result = jssLoadJSSMOD(inFile, &mod, FALSE);
    }
    else
    {
        dmMsg(2, "* Trying XM...\n");
        result = jssLoadXM(inFile, &mod, FALSE);
    }
#endif
    dmf_close(inFile);

    // Check for errors, we still might have some data tho
    if (result != DMERR_OK)
    {
        dmErrorMsg("Error loading module file, %d: %s\n",
            result, dmErrorStr(result));
    }

    // Check if we have anything
    if (mod == NULL)
        return 3;

    // Try to convert it
    if ((result = jssConvertModuleForPlaying(mod)) != DMERR_OK)
    {
        dmErrorMsg("Could not convert module for playing, %d: %s\n",
            result, dmErrorStr(result));
        return 3;
    }

    // Open mixer
    dev = jvmInit(optOutFormat, optOutChannels, optOutFreq, JMIX_AUTO);
    if (dev == NULL)
    {
        dmErrorMsg("jvmInit() returned NULL\n");
        return 4;
    }

    sampSize = jvmGetSampleSize(dev);
    if ((mb = dmMalloc(bufLen * sampSize)) == NULL)
    {
        dmErrorMsg("Could not allocate mixing buffer\n");
        return 5;
    }

    dmMsg(1, "Using fmt=%d, bits=%d, channels=%d, freq=%d [%d / sample]\n",
        optOutFormat, jvmGetSampleRes(dev), optOutChannels, optOutFreq,
        sampSize);

    // Initialize player
    if ((plr = jmpInit(dev)) == NULL)
    {
        dmErrorMsg("jmpInit() returned NULL.\n");
        return 6;
    }

    // Set callback
    jvmSetCallback(dev, jmpExec, plr);

    // Initialize playing
    jmpSetModule(plr, mod);
    if (optStartOrder >= 0)
    {
        dmMsg(1, "Starting from song order #%d\n", optStartOrder);
    } else
        optStartOrder = 0;

    jmpPlayOrder(plr, optStartOrder);
    jvmSetGlobalVol(dev, 150);

    if (optMuteOChannels > 0 && optMuteOChannels <= mod->nchannels)
    {
        int i;
        for (i = 0; i < mod->nchannels; i++)
            jvmMute(dev, i, TRUE);
        jvmMute(dev, optMuteOChannels - 1, FALSE);
    }

    // Open output file
    if ((outFile = fopen(optOutFilename, "wb")) == NULL)
    {
        int err = dmGetErrno();
        dmErrorMsg("Error opening output file '%s' #%d: %s.\n",
            optInFilename, err, dmErrorStr(err));
        return 7;
    }

    // Write initial header
    dmWriteWAVHeader(outFile, jvmGetSampleRes(dev), optOutFreq, optOutChannels, 1024);

    // Render audio data and output to file
    if (optUsePlayTime)
        dmMsg(1, "Rendering module (%d seconds) ...\n", optPlayTime);
    else
        dmMsg(1, "Rendering module ...\n");

    optPlayTime *= optOutFreq;
    dataTotal = 0;
    dataWritten = 1;
    while (plr->isPlaying && dataWritten > 0)
    {
        size_t writeLen = bufLen;
        if (optUsePlayTime && (writeLen + dataTotal) > optPlayTime)
            writeLen = optPlayTime - dataTotal;

        if (writeLen > 0)
        {
            jvmRenderAudio(dev, mb, writeLen);
#if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
            jssEncodeSample16((Uint16 *)mb, writeLen * optOutChannels, jsampSwapEndianess);
#endif
            dataWritten = fwrite(mb, sampSize, writeLen, outFile);
            if (dataWritten < writeLen)
            {
                dmErrorMsg("Error writing data!\n");
                fclose(outFile);
                return 8;
            }
            dataTotal += dataWritten;
        }

        if (optUsePlayTime && dataTotal >= optPlayTime)
            break;
    }

    // Write the correct header
    if (fseek(outFile, 0L, SEEK_SET) != 0)
    {
        dmErrorMsg("Error rewinding to header position!\n");
        return 9;
    }

    dmWriteWAVHeader(outFile, jvmGetSampleRes(dev), optOutFreq, optOutChannels, dataTotal);

    // Done!
    fclose(outFile);

    jmpClose(plr);
    jvmClose(dev);
    jssFreeModule(mod);
    jssClose();

    dmMsg(1, "OK.\n");
    return 0;
}