view tools/mod2wav.c @ 2530:aacf3bd1cceb

Cleanups.
author Matti Hamalainen <ccr@tnsp.org>
date Sat, 16 May 2020 06:38:52 +0300
parents bc05bcfc4598
children d56a0e86067a
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 },
    {  1,   0, "license"         , "Print out this program's license agreement", OPT_NONE },
    {  2, 'v', "verbose"         , "Be more verbose", OPT_NONE },

    { 10, '1', "16bit"           , "16-bit output", OPT_NONE },
    { 12, '8', "8bit"            , "8-bit output", OPT_NONE },
    { 14, 'm', "mono"            , "Mono output", OPT_NONE },
    { 16, 's', "stereo"          , "Stereo output", OPT_NONE },
    { 18, 'f', "freq"            , "Output frequency", OPT_ARGREQ },
    { 20, 'M', "mute"            , "Mute other channels than #", OPT_ARGREQ },
    { 22, 'o', "order"           , "Start from order #", OPT_ARGREQ },
    { 24, 't', "time"            , "Play for # seconds", OPT_ARGREQ },
//  { 26, 'l', "loop"            , "Loop for # times", OPT_ARGREQ },
};

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


void argShowHelp()
{
    dmPrintBanner(stdout, dmProgName, "[options] [sourcefile] [destfile.wav]");
    dmArgsPrintHelp(stdout, optList, optListN, 0, 80 - 2);
}


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

    switch (optN)
    {
        case 0:
            argShowHelp();
            exit(0);
            break;

        case 1:
            dmPrintLicense(stdout);
            exit(0);
            break;

        case 2:
            dmVerbosity++;
            break;

        case 10:
            optOutFormat = JSS_AUDIO_S16;
            break;

        case 12:
            optOutFormat = JSS_AUDIO_U8;
            break;

        case 14:
            optOutChannels = JSS_AUDIO_MONO;
            break;

        case 16:
            optOutChannels = JSS_AUDIO_STEREO;
            break;

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

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

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

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

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

    return TRUE;
}


BOOL argHandleFile(char *currArg)
{
    if (optInFilename == NULL)
        optInFilename = currArg;
    else
    if (optOutFilename == NULL)
        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;
    size_t bufLen = 1024*4, dataTotal, dataWritten, sampSize;
    Uint8 *dataBuf = NULL;
    int res;

    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))
    {
        res = 1;
        goto exit;
    }


    // Check arguments
    if (optInFilename == NULL || optOutFilename == NULL)
    {
        res = dmError(DMERR_INVALID_ARGS,
            "Input or output file not specified. Try --help.\n");
        goto exit;
    }

    // Initialize miniJSS
    jssInit();

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

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

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

    // Check if we have anything
    if (mod == NULL)
    {
        res = dmError(DMERR_INIT_FAIL,
            "Could not load module file.\n");
        goto exit;
    }

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

    // Open mixer
    dev = jvmInit(optOutFormat, optOutChannels, optOutFreq, JMIX_AUTO);
    if (dev == NULL)
    {
        res = dmError(DMERR_INIT_FAIL,
            "jvmInit() returned NULL\n");
        goto exit;
    }

    sampSize = jvmGetSampleSize(dev);
    if ((dataBuf = dmMalloc(bufLen * sampSize)) == NULL)
    {
        res = dmError(DMERR_MALLOC,
            "Could not allocate mixing buffer.\n");
        goto exit;
    }

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

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

    // 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)
    {
        for (int i = 0; i < mod->nchannels; i++)
            jvmMute(dev, i, TRUE);

        jvmMute(dev, optMuteOChannels - 1, FALSE);
    }

    // Open output file
    if ((outFile = fopen(optOutFilename, "wb")) == NULL)
    {
        res = dmGetErrno();
        dmErrorMsg("Error opening output file '%s': %s.\n",
            optInFilename, dmErrorStr(res));
        goto exit;
    }

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

    // Render audio data and output to file
    if (optUsePlayTime)
        dmMsg(1, "Rendering module (%" DM_PRIu_SIZE_T " 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, dataBuf, writeLen);
#if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
            jssEncodeSample16((Uint16 *) dataBuf, writeLen * optOutChannels, jsampSwapEndianess);
#endif
            dataWritten = fwrite(dataBuf, sampSize, writeLen, outFile);
            if (dataWritten < writeLen)
            {
                res = dmError(DMERR_FWRITE,
                    "Error writing audio data!\n");
                goto exit;
            }
            dataTotal += dataWritten;
        }

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

    // Write the correct header
    if (fseek(outFile, 0L, SEEK_SET) != 0)
    {
        res = dmError(DMERR_FSEEK,
            "Error rewinding to header position!\n");
        goto exit;
    }

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

    // Done!
    dmMsg(1, "OK.\n");

exit:
    if (outFile != NULL)
        fclose(outFile);

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

    return res;
}