- Added the writewave command to write the internal TiMidity's output to a

wave file.
- Changed the default channel velocity for MUS files from 64 to 100 to
  better match apparent MIDI practice. (Would like to know what this is
  supposed to be.)
- Changed the mus2midi channel assignments to match the internal player's.
- Fixed: apply_envelope_to_amp() should clamp the mix levels to 0.


SVN r926 (trunk)
This commit is contained in:
Randy Heit 2008-04-19 21:36:53 +00:00
parent f6866df0e6
commit effe9427fd
12 changed files with 320 additions and 48 deletions

View File

@ -1,3 +1,12 @@
April 19, 2008
- Added the writewave command to write the internal TiMidity's output to a
wave file.
- Changed the default channel velocity for MUS files from 64 to 100 to
better match apparent MIDI practice. (Would like to know what this is
supposed to be.)
- Changed the mus2midi channel assignments to match the internal player's.
- Fixed: apply_envelope_to_amp() should clamp the mix levels to 0.
April 18, 2008 April 18, 2008
- Made the maximum number of TiMidity voices configurable through the - Made the maximum number of TiMidity voices configurable through the
timidity_voices cvar. timidity_voices cvar.

View File

@ -33,10 +33,7 @@
** MUS files are essentially format 0 MIDI files with some ** MUS files are essentially format 0 MIDI files with some
** space-saving modifications. Conversion is quite straight-forward. ** space-saving modifications. Conversion is quite straight-forward.
** If you were to hook a main() into this that calls ProduceMIDI, ** If you were to hook a main() into this that calls ProduceMIDI,
** you could create a self-contained MUS->MIDI converter. However, if ** you could create a self-contained MUS->MIDI converter.
** you want to do that, you would be better off using qmus2mid, since
** it creates multitrack files that usually maintain running status
** better than single track files and are thus smaller.
*/ */
@ -52,9 +49,7 @@ static const BYTE StaticMIDIhead[] =
0, 70, // 70 divisions 0, 70, // 70 divisions
'M','T','r','k', 0, 0, 0, 0, 'M','T','r','k', 0, 0, 0, 0,
// The first event sets the tempo to 500,000 microsec/quarter note // The first event sets the tempo to 500,000 microsec/quarter note
0, 255, 81, 3, 0x07, 0xa1, 0x20, 0, 255, 81, 3, 0x07, 0xa1, 0x20
// Set the percussion channel to full volume
0, 0xB9, 7, 127
}; };
static const BYTE MUSMagic[4] = { 'M','U','S',0x1a }; static const BYTE MUSMagic[4] = { 'M','U','S',0x1a };
@ -122,9 +117,8 @@ bool ProduceMIDI (const BYTE *musBuf, TArray<BYTE> &outFile)
int deltaTime; int deltaTime;
const MUSHeader *musHead = (const MUSHeader *)musBuf; const MUSHeader *musHead = (const MUSHeader *)musBuf;
BYTE status; BYTE status;
BYTE chanUsed[16];
BYTE lastVel[16]; BYTE lastVel[16];
SBYTE chanMap[16];
int chanCount;
long trackLen; long trackLen;
// Do some validation of the MUS file // Do some validation of the MUS file
@ -143,10 +137,8 @@ bool ProduceMIDI (const BYTE *musBuf, TArray<BYTE> &outFile)
maxmus_p = LittleShort(musHead->SongLen); maxmus_p = LittleShort(musHead->SongLen);
mus_p = 0; mus_p = 0;
memset (lastVel, 64, 16); memset (lastVel, 100, 16);
memset (chanMap, -1, 15); memset (chanUsed, 0, 16);
chanMap[15] = 9;
chanCount = 0;
event = 0; event = 0;
deltaTime = 0; deltaTime = 0;
status = 0; status = 0;
@ -163,21 +155,27 @@ bool ProduceMIDI (const BYTE *musBuf, TArray<BYTE> &outFile)
t = musBuf[mus_p++]; t = musBuf[mus_p++];
} }
channel = event & 15; channel = event & 15;
if (channel == 15)
{
channel = 9;
}
else if (channel >= 9)
{
channel++;
}
if (chanMap[channel] < 0) if (!chanUsed[channel])
{ {
// This is the first time this channel has been used, // This is the first time this channel has been used,
// so sets its volume to 127. // so sets its volume to 127.
chanUsed[channel] = 1;
outFile.Push(0); outFile.Push(0);
outFile.Push(0xB0 | chanCount); outFile.Push(0xB0 | channel);
outFile.Push(7); outFile.Push(7);
outFile.Push(127); outFile.Push(127);
chanMap[channel] = chanCount++;
if (chanCount == 9)
++chanCount;
} }
midStatus = channel = chanMap[channel]; midStatus = channel;
midArgs = 0; // Most events have two args (0 means 2, 1 means 1) midArgs = 0; // Most events have two args (0 means 2, 1 means 1)
switch (event & 0x70) switch (event & 0x70)

View File

@ -72,7 +72,6 @@ extern HWND Window;
#define SPECTRUM_SIZE 256 #define SPECTRUM_SIZE 256
// TYPES ------------------------------------------------------------------- // TYPES -------------------------------------------------------------------
struct FEnumList struct FEnumList

View File

@ -151,6 +151,11 @@ MusInfo *MusInfo::GetOPLDumper(const char *filename)
return NULL; return NULL;
} }
MusInfo *MusInfo::GetWaveDumper(const char *filename, int rate)
{
return NULL;
}
void I_InitMusic (void) void I_InitMusic (void)
{ {
static bool setatterm = false; static bool setatterm = false;
@ -629,3 +634,41 @@ CCMD (writeopl)
Printf ("Usage: writeopl <filename>"); Printf ("Usage: writeopl <filename>");
} }
} }
//==========================================================================
//
// CCMD writewave
//
// If the current song can be represented as a waveform, dump it to
// the specified file on disk. The sample rate parameter is merely a
// suggestion, and the dumper is free to ignore it.
//
//==========================================================================
CCMD (writewave)
{
if (argv.argc() >= 2 && argv.argc() <= 3)
{
if (currSong == NULL)
{
Printf ("No song is currently playing.\n");
}
else
{
MusInfo *dumper = currSong->GetWaveDumper(argv[1], argv.argc() == 3 ? atoi(argv[2]) : 0);
if (dumper == NULL)
{
Printf ("Current song cannot be saved as wave data.\n");
}
else
{
dumper->Play(false);
delete dumper;
}
}
}
else
{
Printf ("Usage: writewave <filename> [sample rate]");
}
}

View File

@ -49,6 +49,7 @@ public:
virtual void Update(); virtual void Update();
virtual FString GetStats(); virtual FString GetStats();
virtual MusInfo *GetOPLDumper(const char *filename); virtual MusInfo *GetOPLDumper(const char *filename);
virtual MusInfo *GetWaveDumper(const char *filename, int rate);
enum EState enum EState
{ {
@ -225,6 +226,7 @@ class TimidityMIDIDevice : public MIDIDevice
{ {
public: public:
TimidityMIDIDevice(); TimidityMIDIDevice();
TimidityMIDIDevice(int rate);
~TimidityMIDIDevice(); ~TimidityMIDIDevice();
int Open(void (*callback)(unsigned int, void *, DWORD, DWORD), void *userdata); int Open(void (*callback)(unsigned int, void *, DWORD, DWORD), void *userdata);
@ -268,6 +270,20 @@ protected:
DWORD Position; DWORD Position;
}; };
// Internal TiMidity disk writing version of a MIDI device ------------------
class TimidityWaveWriterMIDIDevice : public TimidityMIDIDevice
{
public:
TimidityWaveWriterMIDIDevice(const char *filename, int rate);
~TimidityWaveWriterMIDIDevice();
int Resume();
void Stop();
protected:
FILE *File;
};
// Base class for streaming MUS and MIDI files ------------------------------ // Base class for streaming MUS and MIDI files ------------------------------
// MIDI device selection. // MIDI device selection.
@ -297,7 +313,7 @@ public:
FString GetStats(); FString GetStats();
protected: protected:
MIDIStreamer(const char *dumpname); MIDIStreamer(const char *dumpname, EMIDIDevice type);
void OutputVolume (DWORD volume); void OutputVolume (DWORD volume);
int FillBuffer(int buffer_num, int max_events, DWORD max_time); int FillBuffer(int buffer_num, int max_events, DWORD max_time);
@ -363,9 +379,10 @@ public:
~MUSSong2(); ~MUSSong2();
MusInfo *GetOPLDumper(const char *filename); MusInfo *GetOPLDumper(const char *filename);
MusInfo *GetWaveDumper(const char *filename, int rate);
protected: protected:
MUSSong2(const MUSSong2 *original, const char *filename); //OPL dump constructor MUSSong2(const MUSSong2 *original, const char *filename, EMIDIDevice type); // file dump constructor
void DoInitialSetup(); void DoInitialSetup();
void DoRestart(); void DoRestart();
@ -388,9 +405,10 @@ public:
~MIDISong2(); ~MIDISong2();
MusInfo *GetOPLDumper(const char *filename); MusInfo *GetOPLDumper(const char *filename);
MusInfo *GetWaveDumper(const char *filename, int rate);
protected: protected:
MIDISong2(const MIDISong2 *original, const char *filename); // OPL dump constructor MIDISong2(const MIDISong2 *original, const char *filename, EMIDIDevice type); // file dump constructor
void CheckCaps(); void CheckCaps();
void DoInitialSetup(); void DoInitialSetup();

View File

@ -891,17 +891,28 @@ void MIDISong2::Precache()
MusInfo *MIDISong2::GetOPLDumper(const char *filename) MusInfo *MIDISong2::GetOPLDumper(const char *filename)
{ {
return new MIDISong2(this, filename); return new MIDISong2(this, filename, MIDI_OPL);
} }
//========================================================================== //==========================================================================
// //
// MIDISong2 OPL Dumping Constructor // MIDISong2 :: GetWaveDumper
// //
//========================================================================== //==========================================================================
MIDISong2::MIDISong2(const MIDISong2 *original, const char *filename) MusInfo *MIDISong2::GetWaveDumper(const char *filename, int rate)
: MIDIStreamer(filename) {
return new MIDISong2(this, filename, MIDI_Timidity);
}
//==========================================================================
//
// MIDISong2 File Dumping Constructor
//
//==========================================================================
MIDISong2::MIDISong2(const MIDISong2 *original, const char *filename, EMIDIDevice type)
: MIDIStreamer(filename, type)
{ {
SongLen = original->SongLen; SongLen = original->SongLen;
MusHeader = new BYTE[original->SongLen]; MusHeader = new BYTE[original->SongLen];

View File

@ -97,12 +97,12 @@ MIDIStreamer::MIDIStreamer(EMIDIDevice type)
// //
//========================================================================== //==========================================================================
MIDIStreamer::MIDIStreamer(const char *dumpname) MIDIStreamer::MIDIStreamer(const char *dumpname, EMIDIDevice type)
: :
#ifdef _WIN32 #ifdef _WIN32
PlayerThread(0), ExitEvent(0), BufferDoneEvent(0), PlayerThread(0), ExitEvent(0), BufferDoneEvent(0),
#endif #endif
MIDI(0), Division(0), InitialTempo(500000), DeviceType(MIDI_OPL), DumpFilename(dumpname) MIDI(0), Division(0), InitialTempo(500000), DeviceType(type), DumpFilename(dumpname)
{ {
#ifdef _WIN32 #ifdef _WIN32
BufferDoneEvent = NULL; BufferDoneEvent = NULL;
@ -196,7 +196,14 @@ void MIDIStreamer::Play(bool looping)
assert(MIDI == NULL); assert(MIDI == NULL);
if (DumpFilename.IsNotEmpty()) if (DumpFilename.IsNotEmpty())
{ {
MIDI = new OPLDumperMIDIDevice(DumpFilename); if (DeviceType == MIDI_OPL)
{
MIDI = new OPLDumperMIDIDevice(DumpFilename);
}
else if (DeviceType == MIDI_Timidity)
{
MIDI = new TimidityWaveWriterMIDIDevice(DumpFilename, 0);
}
} }
else switch(DeviceType) else switch(DeviceType)
{ {

View File

@ -150,7 +150,7 @@ void MUSSong2::DoInitialSetup()
{ {
for (int i = 0; i < 16; ++i) for (int i = 0; i < 16; ++i)
{ {
LastVelocity[i] = 64; LastVelocity[i] = 100;
ChannelVolumes[i] = 127; ChannelVolumes[i] = 127;
} }
} }
@ -340,7 +340,18 @@ end:
MusInfo *MUSSong2::GetOPLDumper(const char *filename) MusInfo *MUSSong2::GetOPLDumper(const char *filename)
{ {
return new MUSSong2(this, filename); return new MUSSong2(this, filename, MIDI_OPL);
}
//==========================================================================
//
// MUSSong2 :: GetWaveDumper
//
//==========================================================================
MusInfo *MUSSong2::GetWaveDumper(const char *filename, int rate)
{
return new MUSSong2(this, filename, MIDI_Timidity);
} }
//========================================================================== //==========================================================================
@ -349,8 +360,8 @@ MusInfo *MUSSong2::GetOPLDumper(const char *filename)
// //
//========================================================================== //==========================================================================
MUSSong2::MUSSong2(const MUSSong2 *original, const char *filename) MUSSong2::MUSSong2(const MUSSong2 *original, const char *filename, EMIDIDevice type)
: MIDIStreamer(filename) : MIDIStreamer(filename, type)
{ {
int songstart = LittleShort(original->MusHeader->SongStart); int songstart = LittleShort(original->MusHeader->SongStart);
MaxMusP = original->MaxMusP; MaxMusP = original->MaxMusP;

View File

@ -44,6 +44,27 @@
// MACROS ------------------------------------------------------------------ // MACROS ------------------------------------------------------------------
// TYPES -------------------------------------------------------------------
struct FmtChunk
{
DWORD ChunkID;
DWORD ChunkLen;
WORD FormatTag;
WORD Channels;
DWORD SamplesPerSec;
DWORD AvgBytesPerSec;
WORD BlockAlign;
WORD BitsPerSample;
WORD ExtensionSize;
WORD ValidBitsPerSample;
DWORD ChannelMask;
DWORD SubFormatA;
WORD SubFormatB;
WORD SubFormatC;
BYTE SubFormatD[8];
};
// EXTERNAL FUNCTION PROTOTYPES -------------------------------------------- // EXTERNAL FUNCTION PROTOTYPES --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ---------------------------------------------- // PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
@ -77,6 +98,26 @@ TimidityMIDIDevice::TimidityMIDIDevice()
Renderer = new Timidity::Renderer(GSnd->GetOutputRate()); Renderer = new Timidity::Renderer(GSnd->GetOutputRate());
} }
//==========================================================================
//
// TimidityMIDIDevice Constructor with rate parameter
//
//==========================================================================
TimidityMIDIDevice::TimidityMIDIDevice(int rate)
{
// Need to support multiple instances with different playback rates
// before we can use this parameter.
rate = (int)GSnd->GetOutputRate();
Stream = NULL;
Tempo = 0;
Division = 0;
Events = NULL;
Started = false;
Renderer = NULL;
Renderer = new Timidity::Renderer((float)rate);
}
//========================================================================== //==========================================================================
// //
// TimidityMIDIDevice Destructor // TimidityMIDIDevice Destructor
@ -509,7 +550,7 @@ bool TimidityMIDIDevice::ServiceStream (void *buff, int numbytes)
{ // end of song { // end of song
if (numsamples > 0) if (numsamples > 0)
{ {
Renderer->ComputeOutput(samples1, samplesleft); Renderer->ComputeOutput(samples1, numsamples);
} }
res = false; res = false;
break; break;
@ -521,6 +562,10 @@ bool TimidityMIDIDevice::ServiceStream (void *buff, int numbytes)
} }
} }
} }
if (Events == NULL)
{
res = false;
}
CritSec.Leave(); CritSec.Leave();
return res; return res;
} }
@ -596,3 +641,124 @@ FString TimidityMIDIDevice::GetStats()
} }
return out; return out;
} }
//==========================================================================
//
// TimidityWaveWriterMIDIDevice Constructor
//
//==========================================================================
TimidityWaveWriterMIDIDevice::TimidityWaveWriterMIDIDevice(const char *filename, int rate)
{
File = fopen(filename, "wb");
if (File != NULL)
{ // Write wave header
DWORD work[3];
FmtChunk fmt;
work[0] = MAKE_ID('R','I','F','F');
work[1] = 0; // filled in later
work[2] = MAKE_ID('W','A','V','E');
if (3 != fwrite(work, 4, 3, File)) goto fail;
fmt.ChunkID = MAKE_ID('f','m','t',' ');
fmt.ChunkLen = LittleLong(sizeof(fmt) - 8);
fmt.FormatTag = LittleShort(0xFFFE); // WAVE_FORMAT_EXTENSIBLE
fmt.Channels = LittleShort(2);
fmt.SamplesPerSec = LittleLong((int)Renderer->rate);
fmt.AvgBytesPerSec = LittleLong((int)Renderer->rate * 8);
fmt.BlockAlign = LittleShort(8);
fmt.BitsPerSample = LittleShort(32);
fmt.ExtensionSize = LittleShort(2 + 4 + 16);
fmt.ValidBitsPerSample = LittleShort(32);
fmt.ChannelMask = LittleLong(3);
fmt.SubFormatA = LittleLong(0x00000003); // Set subformat to KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
fmt.SubFormatB = LittleShort(0x0000);
fmt.SubFormatC = LittleShort(0x0010);
fmt.SubFormatD[0] = 0x80;
fmt.SubFormatD[1] = 0x00;
fmt.SubFormatD[2] = 0x00;
fmt.SubFormatD[3] = 0xaa;
fmt.SubFormatD[4] = 0x00;
fmt.SubFormatD[5] = 0x38;
fmt.SubFormatD[6] = 0x9b;
fmt.SubFormatD[7] = 0x71;
if (1 != fwrite(&fmt, sizeof(fmt), 1, File)) goto fail;
work[0] = MAKE_ID('d','a','t','a');
work[1] = 0; // filled in later
if (2 != fwrite(work, 4, 2, File)) goto fail;
return;
fail:
Printf("Failed to write %s: %s\n", filename, strerror(errno));
fclose(File);
File = NULL;
}
}
//==========================================================================
//
// TimidityWaveWriterMIDIDevice Destructor
//
//==========================================================================
TimidityWaveWriterMIDIDevice::~TimidityWaveWriterMIDIDevice()
{
if (File != NULL)
{
long pos = ftell(File);
DWORD size;
// data chunk size
size = LittleLong(pos - 8);
if (0 == fseek(File, 4, SEEK_SET))
{
if (1 == fwrite(&size, 4, 1, File))
{
size = LittleLong(pos - 12 - sizeof(FmtChunk) - 8);
if (0 == fseek(File, 4 + sizeof(FmtChunk) + 4, SEEK_CUR))
{
if (1 == fwrite(&size, 4, 1, File))
{
fclose(File);
return;
}
}
}
}
Printf("Could not finish writing wave file: %s\n", strerror(errno));
fclose(File);
}
}
//==========================================================================
//
// TimidityWaveWriterMIDIDevice :: Resume
//
//==========================================================================
int TimidityWaveWriterMIDIDevice::Resume()
{
float writebuffer[4096];
while (ServiceStream(writebuffer, sizeof(writebuffer)))
{
if (fwrite(writebuffer, sizeof(writebuffer), 1, File) != 1)
{
Printf("Could not write entire wave file: %s\n", strerror(errno));
return 1;
}
}
return 0;
}
//==========================================================================
//
// TimidityWaveWriterMIDIDevice Stop
//
//==========================================================================
void TimidityWaveWriterMIDIDevice::Stop()
{
}

View File

@ -82,8 +82,8 @@ void apply_envelope_to_amp(Voice *v)
env_vol *= v->envelope_volume / float(1 << 30); env_vol *= v->envelope_volume / float(1 << 30);
} }
// Note: The pan offsets are negative. // Note: The pan offsets are negative.
v->left_mix = (float)calc_gf1_amp(env_vol + v->left_offset) * final_amp; v->left_mix = MAX(0.f, (float)calc_gf1_amp(env_vol + v->left_offset) * final_amp);
v->right_mix = (float)calc_gf1_amp(env_vol + v->right_offset) * final_amp; v->right_mix = MAX(0.f, (float)calc_gf1_amp(env_vol + v->right_offset) * final_amp);
} }
static int update_envelope(Voice *v) static int update_envelope(Voice *v)
@ -440,7 +440,7 @@ void mix_voice(Renderer *song, float *buf, Voice *v, int c)
{ {
return; return;
} }
if (v->left_offset == 0) // All the way to the left if (v->right_mix == 0) // All the way to the left
{ {
if (v->envelope_increment != 0 || v->tremolo_phase_increment != 0) if (v->envelope_increment != 0 || v->tremolo_phase_increment != 0)
{ {
@ -451,7 +451,7 @@ void mix_voice(Renderer *song, float *buf, Voice *v, int c)
mix_single_left(sp, buf, v, count); mix_single_left(sp, buf, v, count);
} }
} }
else if (v->right_offset == 0) // All the way to the right else if (v->left_mix == 0) // All the way to the right
{ {
if (v->envelope_increment != 0 || v->tremolo_phase_increment != 0) if (v->envelope_increment != 0 || v->tremolo_phase_increment != 0)
{ {

View File

@ -215,6 +215,17 @@ void Renderer::recompute_amp(Voice *v)
void Renderer::compute_pan(int panning, float &left_offset, float &right_offset) void Renderer::compute_pan(int panning, float &left_offset, float &right_offset)
{ {
// Round the left- and right-most positions to their extremes, since
// most songs only do coarse panning.
if (panning < 128)
{
panning = 0;
}
else if (panning > 127*128)
{
panning = 32767;
}
if (panning == 0) if (panning == 0)
{ {
left_offset = 0; left_offset = 0;
@ -355,6 +366,12 @@ void Renderer::kill_note(int i)
/* Only one instance of a note can be playing on a single channel. */ /* Only one instance of a note can be playing on a single channel. */
void Renderer::note_on(int chan, int note, int vel) void Renderer::note_on(int chan, int note, int vel)
{ {
if (vel == 0)
{
note_off(chan, note, 0);
return;
}
int i = voices, lowest = -1; int i = voices, lowest = -1;
float lv = 1e10, v; float lv = 1e10, v;
@ -574,14 +591,7 @@ void Renderer::HandleEvent(int status, int parm1, int parm2)
switch (command) switch (command)
{ {
case ME_NOTEON: case ME_NOTEON:
if (parm2 == 0) note_on(chan, parm1, parm2);
{
note_off(chan, parm1, 0);
}
else
{
note_on(chan, parm1, parm2);
}
break; break;
case ME_NOTEOFF: case ME_NOTEOFF:

View File

@ -66,7 +66,7 @@ config.h
/* For some reason the sample volume is always set to maximum in all /* For some reason the sample volume is always set to maximum in all
patch files. Define this for a crude adjustment that may help patch files. Define this for a crude adjustment that may help
equalize instrument volumes. */ equalize instrument volumes. */
#define ADJUST_SAMPLE_VOLUMES //#define ADJUST_SAMPLE_VOLUMES
/* The number of samples to use for ramping out a dying note. Affects /* The number of samples to use for ramping out a dying note. Affects
click removal. */ click removal. */