diff --git a/docs/rh-log.txt b/docs/rh-log.txt
index 0ff748c66..aa1cffafd 100644
--- a/docs/rh-log.txt
+++ b/docs/rh-log.txt
@@ -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.
diff --git a/src/mus2midi.cpp b/src/mus2midi.cpp
index 804adc11e..1e6573565 100644
--- a/src/mus2midi.cpp
+++ b/src/mus2midi.cpp
@@ -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)
diff --git a/src/sound/fmodsound.cpp b/src/sound/fmodsound.cpp
index 5a2946ee3..3f3242718 100644
--- a/src/sound/fmodsound.cpp
+++ b/src/sound/fmodsound.cpp
@@ -72,7 +72,6 @@ extern HWND Window;
 
 #define SPECTRUM_SIZE				256
 
-
 // TYPES -------------------------------------------------------------------
 
 struct FEnumList
diff --git a/src/sound/i_music.cpp b/src/sound/i_music.cpp
index ea0a4e1a1..87cd99c6f 100644
--- a/src/sound/i_music.cpp
+++ b/src/sound/i_music.cpp
@@ -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]");
+	}
+}
diff --git a/src/sound/i_musicinterns.h b/src/sound/i_musicinterns.h
index 0d7f8c665..6edde4691 100644
--- a/src/sound/i_musicinterns.h
+++ b/src/sound/i_musicinterns.h
@@ -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();
diff --git a/src/sound/music_midi_midiout.cpp b/src/sound/music_midi_midiout.cpp
index 71e6899e4..773320876 100644
--- a/src/sound/music_midi_midiout.cpp
+++ b/src/sound/music_midi_midiout.cpp
@@ -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];
diff --git a/src/sound/music_midistream.cpp b/src/sound/music_midistream.cpp
index 0e025efe1..f411ab127 100644
--- a/src/sound/music_midistream.cpp
+++ b/src/sound/music_midistream.cpp
@@ -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)
 	{
diff --git a/src/sound/music_mus_midiout.cpp b/src/sound/music_mus_midiout.cpp
index 6d5960580..d38729326 100644
--- a/src/sound/music_mus_midiout.cpp
+++ b/src/sound/music_mus_midiout.cpp
@@ -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;
diff --git a/src/sound/music_timidity_mididevice.cpp b/src/sound/music_timidity_mididevice.cpp
index a3c03c19e..e86e9140c 100644
--- a/src/sound/music_timidity_mididevice.cpp
+++ b/src/sound/music_timidity_mididevice.cpp
@@ -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()
+{
+}
diff --git a/src/timidity/mix.cpp b/src/timidity/mix.cpp
index 8abc2c0fc..e4ae9af4c 100644
--- a/src/timidity/mix.cpp
+++ b/src/timidity/mix.cpp
@@ -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)
 			{
diff --git a/src/timidity/playmidi.cpp b/src/timidity/playmidi.cpp
index dcda31a5a..1c8a332e4 100644
--- a/src/timidity/playmidi.cpp
+++ b/src/timidity/playmidi.cpp
@@ -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:
diff --git a/src/timidity/timidity.h b/src/timidity/timidity.h
index 0e8837199..db29e3462 100644
--- a/src/timidity/timidity.h
+++ b/src/timidity/timidity.h
@@ -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. */