From effe9427fda06766abdf3714aadaa5a645c5bcfd Mon Sep 17 00:00:00 2001 From: Randy Heit Date: Sat, 19 Apr 2008 21:36:53 +0000 Subject: [PATCH] - 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) --- docs/rh-log.txt | 9 ++ src/mus2midi.cpp | 36 +++-- src/sound/fmodsound.cpp | 1 - src/sound/i_music.cpp | 43 ++++++ src/sound/i_musicinterns.h | 24 +++- src/sound/music_midi_midiout.cpp | 19 ++- src/sound/music_midistream.cpp | 13 +- src/sound/music_mus_midiout.cpp | 19 ++- src/sound/music_timidity_mididevice.cpp | 168 +++++++++++++++++++++++- src/timidity/mix.cpp | 8 +- src/timidity/playmidi.cpp | 26 ++-- src/timidity/timidity.h | 2 +- 12 files changed, 320 insertions(+), 48 deletions(-) diff --git a/docs/rh-log.txt b/docs/rh-log.txt index 0ff748c667..aa1cffafd5 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 804adc11e3..1e6573565c 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 &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 &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 &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 5a2946ee34..3f32427186 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 ea0a4e1a11..87cd99c6f0 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 "); } } + +//========================================================================== +// +// 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 [sample rate]"); + } +} diff --git a/src/sound/i_musicinterns.h b/src/sound/i_musicinterns.h index 0d7f8c665e..6edde46914 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 71e6899e47..773320876c 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 0e025efe11..f411ab127a 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 6d59605804..d387293268 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 a3c03c19e7..e86e9140c8 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 8abc2c0fc6..e4ae9af4c0 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 dcda31a5a6..1c8a332e4a 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 0e8837199a..db29e3462f 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. */