view mod2wav.c @ 96:6bf5220fa47e

Urgh .. use memset to silence some bogus GCC warnings about using potentially uninitialized values, while that will not actually be possible. In any case, it is annoying.
author Matti Hamalainen <ccr@tnsp.org>
date Tue, 02 Oct 2012 18:52:28 +0300
parents 6b42aed2745b
children 1f8f4d7cb33b
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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include "jss.h"
#include "jssmod.h"
#include "jssmix.h"
#include "jssplr.h"
#include "dmlib.h"
#include "dmargs.h"
#include "dmfile.h"


#define JSS_WAVE_FORMAT_PCM     (1)
#define JSS_WAVE_RIFF_ID        "RIFF"
#define JSS_WAVE_WAVE_ID        "WAVE"
#define JSS_WAVE_FMT_ID         "fmt "
#define JSS_WAVE_DATA_ID        "data"


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


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

    JSSWaveChunk chFormat;

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

    JSSWaveChunk chData;
    // Data follows here
} JSSWaveFile;


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


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);
            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:
            dmError("Unknown argument '%s'.\n", currArg);
            return FALSE;
    }
    
    return TRUE;
}


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


BOOL jssWriteChunk(FILE * f, JSSWaveChunk *ch)
{
    if (!dm_fwrite_str(f, ch->chunkID, 4)) return FALSE;
    return dm_fwrite_le32(f, ch->chunkSize);
}


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


void jssWriteWAVHeader(FILE *outFile, int sampBits, int sampFreq, int sampChn, size_t sampLen)
{
    JSSWaveFile wav;
    
    // PCM WAVE chunk
    jssMakeChunk(&wav.chFormat, JSS_WAVE_FMT_ID, (2 + 2 + 4 + 4 + 2 + 2));

    wav.wFormatTag = JSS_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
    jssMakeChunk(&wav.chData, JSS_WAVE_DATA_ID, (sampLen * wav.nBlockAlign));

    // RIFF header
    memcpy(&wav.riffID, (const void *) JSS_WAVE_RIFF_ID, 4);
    memcpy(&wav.riffType, (const void *) JSS_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));
    jssWriteChunk(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);
    
    jssWriteChunk(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, TRUE))
        exit(1);

    // Check arguments
    if (!srcFilename || !destFilename)
    {
        dmError("Input or output file not specified!\n");
        return 1;
    }
    
    // Initialize miniJSS
    jssInit();
    
    // Open the source file
    if ((inFile = dmf_create_stdio(srcFilename, "rb")) == NULL)
    {
        dmError("Error opening input file '%s', %d: %s\n",
            srcFilename, errno, strerror(errno));
        return 1;
    }

    // Read module file
    fprintf(stderr, "Reading file: %s\n", srcFilename);
#ifdef JSS_SUP_XM
    fprintf(stderr, "* Trying XM...\n");
    result = jssLoadXM(inFile, &mod);
#endif
#ifdef JSS_SUP_JSSMOD
    if (result != 0)
    {
        size_t bufgot, bufsize = dmfsize(inFile);
        Uint8 *buf = dmMalloc(bufsize);
        dmfseek(inFile, 0L, SEEK_SET);
        fprintf(stderr, "* Trying JSSMOD (%d bytes, %p)...\n", bufsize, buf);
        if ((bufgot = dmfread(buf, 1, bufsize, inFile)) != bufsize)
        {
            fprintf(stderr, "Error reading file (not enough data %d), #%d: %s\n",
                bufgot, dmferror(inFile), dmErrorStr(dmferror(inFile)));
            return 2;
        }
        result = jssLoadJSSMOD(buf, bufsize, &mod);
        dmFree(buf);
    }
#endif
    dmf_close(inFile);
    if (result != DMERR_OK)
    {
        dmError("Error loading module file, %d: %s\n",
            result, dmErrorStr(result));
        return 3;
    }

    // Try to convert it
    if ((result = jssConvertModuleForPlaying(mod)) != DMERR_OK)
    {
        dmError("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)
    {
        dmError("jvmInit() returned NULL\n");
        return 4;
    }

    sampSize = jvmGetSampleSize(dev);
    if ((mb = dmMalloc(bufLen * sampSize)) == NULL)
    {
        dmError("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)
    {
        dmError("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, 50);
    
    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(destFilename, "wb")) == NULL)
    {
        dmError("Error opening output file '%s'. (%s)\n", srcFilename, strerror(errno));
        return 7;
    }

    // Write initial header
    jssWriteWAVHeader(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)
            {
                dmError("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)
    {
        dmError("Error rewinding to header position!\n");
        return 9;
    }
    
    jssWriteWAVHeader(outFile, jvmGetSampleRes(dev), optOutFreq, optOutChannels, dataTotal);
    
    // Done!
    fclose(outFile);

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

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