- 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
- Made the maximum number of TiMidity voices configurable through the
timidity_voices cvar.

View File

@ -33,10 +33,7 @@
** MUS files are essentially format 0 MIDI files with some
** space-saving modifications. Conversion is quite straight-forward.
** If you were to hook a main() into this that calls ProduceMIDI,
** you could create a self-contained MUS->MIDI converter. However, if
** 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.
** you could create a self-contained MUS->MIDI converter.
*/
@ -52,9 +49,7 @@ static const BYTE StaticMIDIhead[] =
0, 70, // 70 divisions
'M','T','r','k', 0, 0, 0, 0,
// The first event sets the tempo to 500,000 microsec/quarter note
0, 255, 81, 3, 0x07, 0xa1, 0x20,
// Set the percussion channel to full volume
0, 0xB9, 7, 127
0, 255, 81, 3, 0x07, 0xa1, 0x20
};
static const BYTE MUSMagic[4] = { 'M','U','S',0x1a };
@ -122,9 +117,8 @@ bool ProduceMIDI (const BYTE *musBuf, TArray<BYTE> &outFile)
int deltaTime;
const MUSHeader *musHead = (const MUSHeader *)musBuf;
BYTE status;
BYTE chanUsed[16];
BYTE lastVel[16];
SBYTE chanMap[16];
int chanCount;
long trackLen;
// Do some validation of the MUS file
@ -143,10 +137,8 @@ bool ProduceMIDI (const BYTE *musBuf, TArray<BYTE> &outFile)
maxmus_p = LittleShort(musHead->SongLen);
mus_p = 0;
memset (lastVel, 64, 16);
memset (chanMap, -1, 15);
chanMap[15] = 9;
chanCount = 0;
memset (lastVel, 100, 16);
memset (chanUsed, 0, 16);
event = 0;
deltaTime = 0;
status = 0;
@ -163,21 +155,27 @@ bool ProduceMIDI (const BYTE *musBuf, TArray<BYTE> &outFile)
t = musBuf[mus_p++];
}
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,
// so sets its volume to 127.
chanUsed[channel] = 1;
outFile.Push(0);
outFile.Push(0xB0 | chanCount);
outFile.Push(0xB0 | channel);
outFile.Push(7);
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)
switch (event & 0x70)

View File

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

View File

@ -151,6 +151,11 @@ MusInfo *MusInfo::GetOPLDumper(const char *filename)
return NULL;
}
MusInfo *MusInfo::GetWaveDumper(const char *filename, int rate)
{
return NULL;
}
void I_InitMusic (void)
{
static bool setatterm = false;
@ -629,3 +634,41 @@ CCMD (writeopl)
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 FString GetStats();
virtual MusInfo *GetOPLDumper(const char *filename);
virtual MusInfo *GetWaveDumper(const char *filename, int rate);
enum EState
{
@ -225,6 +226,7 @@ class TimidityMIDIDevice : public MIDIDevice
{
public:
TimidityMIDIDevice();
TimidityMIDIDevice(int rate);
~TimidityMIDIDevice();
int Open(void (*callback)(unsigned int, void *, DWORD, DWORD), void *userdata);
@ -268,6 +270,20 @@ protected:
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 ------------------------------
// MIDI device selection.
@ -297,7 +313,7 @@ public:
FString GetStats();
protected:
MIDIStreamer(const char *dumpname);
MIDIStreamer(const char *dumpname, EMIDIDevice type);
void OutputVolume (DWORD volume);
int FillBuffer(int buffer_num, int max_events, DWORD max_time);
@ -363,9 +379,10 @@ public:
~MUSSong2();
MusInfo *GetOPLDumper(const char *filename);
MusInfo *GetWaveDumper(const char *filename, int rate);
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 DoRestart();
@ -388,9 +405,10 @@ public:
~MIDISong2();
MusInfo *GetOPLDumper(const char *filename);
MusInfo *GetWaveDumper(const char *filename, int rate);
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 DoInitialSetup();

View File

@ -891,17 +891,28 @@ void MIDISong2::Precache()
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)
: MIDIStreamer(filename)
MusInfo *MIDISong2::GetWaveDumper(const char *filename, int rate)
{
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;
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
PlayerThread(0), ExitEvent(0), BufferDoneEvent(0),
#endif
MIDI(0), Division(0), InitialTempo(500000), DeviceType(MIDI_OPL), DumpFilename(dumpname)
MIDI(0), Division(0), InitialTempo(500000), DeviceType(type), DumpFilename(dumpname)
{
#ifdef _WIN32
BufferDoneEvent = NULL;
@ -196,7 +196,14 @@ void MIDIStreamer::Play(bool looping)
assert(MIDI == NULL);
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)
{

View File

@ -150,7 +150,7 @@ void MUSSong2::DoInitialSetup()
{
for (int i = 0; i < 16; ++i)
{
LastVelocity[i] = 64;
LastVelocity[i] = 100;
ChannelVolumes[i] = 127;
}
}
@ -340,7 +340,18 @@ end:
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)
: MIDIStreamer(filename)
MUSSong2::MUSSong2(const MUSSong2 *original, const char *filename, EMIDIDevice type)
: MIDIStreamer(filename, type)
{
int songstart = LittleShort(original->MusHeader->SongStart);
MaxMusP = original->MaxMusP;

View File

@ -44,6 +44,27 @@
// 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 --------------------------------------------
// PUBLIC FUNCTION PROTOTYPES ----------------------------------------------
@ -77,6 +98,26 @@ TimidityMIDIDevice::TimidityMIDIDevice()
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
@ -509,7 +550,7 @@ bool TimidityMIDIDevice::ServiceStream (void *buff, int numbytes)
{ // end of song
if (numsamples > 0)
{
Renderer->ComputeOutput(samples1, samplesleft);
Renderer->ComputeOutput(samples1, numsamples);
}
res = false;
break;
@ -521,6 +562,10 @@ bool TimidityMIDIDevice::ServiceStream (void *buff, int numbytes)
}
}
}
if (Events == NULL)
{
res = false;
}
CritSec.Leave();
return res;
}
@ -596,3 +641,124 @@ FString TimidityMIDIDevice::GetStats()
}
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);
}
// Note: The pan offsets are negative.
v->left_mix = (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->left_mix = MAX(0.f, (float)calc_gf1_amp(env_vol + v->left_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)
@ -440,7 +440,7 @@ void mix_voice(Renderer *song, float *buf, Voice *v, int c)
{
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)
{
@ -451,7 +451,7 @@ void mix_voice(Renderer *song, float *buf, Voice *v, int c)
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)
{

View File

@ -215,6 +215,17 @@ void Renderer::recompute_amp(Voice *v)
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)
{
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. */
void Renderer::note_on(int chan, int note, int vel)
{
if (vel == 0)
{
note_off(chan, note, 0);
return;
}
int i = voices, lowest = -1;
float lv = 1e10, v;
@ -574,14 +591,7 @@ void Renderer::HandleEvent(int status, int parm1, int parm2)
switch (command)
{
case ME_NOTEON:
if (parm2 == 0)
{
note_off(chan, parm1, 0);
}
else
{
note_on(chan, parm1, parm2);
}
note_on(chan, parm1, parm2);
break;
case ME_NOTEOFF:

View File

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