Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /*
- Programming MIDI: Parsing, Displaying (& Playing) MIDI Files
- "Better get these done before im virused..." - javidx9
- License (OLC-3)
- ~~~~~~~~~~~~~~~
- Copyright 2018-2020 OneLoneCoder.com
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions
- are met:
- 1. Redistributions or derivations of source code must retain the above
- copyright notice, this list of conditions and the following disclaimer.
- 2. Redistributions or derivative works in binary form must reproduce
- the above copyright notice. This list of conditions and the following
- disclaimer must be reproduced in the documentation and/or other
- materials provided with the distribution.
- 3. Neither the name of the copyright holder nor the names of its
- contributors may be used to endorse or promote products derived
- from this software without specific prior written permission.
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- Relevant Video: https://youtu.be/040BKtnDdg0
- Links
- ~~~~~
- YouTube: https://www.youtube.com/javidx9
- https://www.youtube.com/javidx9extra
- Discord: https://discord.gg/WhwHUMV
- Twitter: https://www.twitter.com/javidx9
- Twitch: https://www.twitch.tv/javidx9
- GitHub: https://www.github.com/onelonecoder
- Patreon: https://www.patreon.com/javidx9
- Homepage: https://www.onelonecoder.com
- Community: https://community.onelonecoder.com
- Author
- ~~~~~~
- David Barr, aka javidx9, ©OneLoneCoder 2018, 2019, 2020
- */
- #define OLC_PGE_APPLICATION
- #include "olcPixelGameEngine.h"
- #include <fstream>
- #include <array>
- //#pragma comment(lib, "winmm.lib")
- struct MidiEvent
- {
- enum class Type
- {
- NoteOff,
- NoteOn,
- Other
- } event;
- uint8_t nKey = 0;
- uint8_t nVelocity = 0;
- uint32_t nDeltaTick = 0;
- };
- struct MidiNote
- {
- uint8_t nKey = 0;
- uint8_t nVelocity = 0;
- uint32_t nStartTime = 0;
- uint32_t nDuration = 0;
- };
- struct MidiTrack
- {
- std::string sName;
- std::string sInstrument;
- std::vector<MidiEvent> vecEvents;
- std::vector<MidiNote> vecNotes;
- uint8_t nMaxNote = 64;
- uint8_t nMinNote = 64;
- };
- class MidiFile
- {
- public:
- enum EventName : uint8_t
- {
- VoiceNoteOff = 0x80,
- VoiceNoteOn = 0x90,
- VoiceAftertouch = 0xA0,
- VoiceControlChange = 0xB0,
- VoiceProgramChange = 0xC0,
- VoiceChannelPressure = 0xD0,
- VoicePitchBend = 0xE0,
- SystemExclusive = 0xF0,
- };
- enum MetaEventName : uint8_t
- {
- MetaSequence = 0x00,
- MetaText = 0x01,
- MetaCopyright = 0x02,
- MetaTrackName = 0x03,
- MetaInstrumentName = 0x04,
- MetaLyrics = 0x05,
- MetaMarker = 0x06,
- MetaCuePoint = 0x07,
- MetaChannelPrefix = 0x20,
- MetaEndOfTrack = 0x2F,
- MetaSetTempo = 0x51,
- MetaSMPTEOffset = 0x54,
- MetaTimeSignature = 0x58,
- MetaKeySignature = 0x59,
- MetaSequencerSpecific = 0x7F,
- };
- public:
- MidiFile()
- {
- }
- MidiFile(const std::string& sFileName)
- {
- ParseFile(sFileName);
- }
- void Clear()
- {
- }
- bool ParseFile(const std::string& sFileName)
- {
- // Open the MIDI File as a stream
- std::ifstream ifs;
- ifs.open(sFileName, std::fstream::in | std::ios::binary);
- if (!ifs.is_open())
- return false;
- // Helper Utilities ====================
- // Swaps byte order of 32-bit integer
- auto Swap32 = [](uint32_t n)
- {
- return (((n >> 24) & 0xff) | ((n << 8) & 0xff0000) | ((n >> 8) & 0xff00) | ((n << 24) & 0xff000000));
- };
- // Swaps byte order of 16-bit integer
- auto Swap16 = [](uint16_t n)
- {
- return ((n >> 8) | (n << 8));
- };
- // Reads nLength bytes form file stream, and constructs a text string
- auto ReadString = [&ifs](uint32_t nLength)
- {
- std::string s;
- for (uint32_t i = 0; i < nLength; i++) s += ifs.get();
- return s;
- };
- // Reads a compressed MIDI value. This can be up to 32 bits long. Essentially if the first byte, first
- // bit is set to 1, that indicates that the next byte is required to construct the full word. Only
- // the bottom 7 bits of each byte are used to construct the final word value. Each successive byte
- // that has MSB set, indicates a further byte needs to be read.
- auto ReadValue = [&ifs]()
- {
- uint32_t nValue = 0;
- uint8_t nByte = 0;
- // Read byte
- nValue = ifs.get();
- // Check MSB, if set, more bytes need reading
- if (nValue & 0x80)
- {
- // Extract bottom 7 bits of read byte
- nValue &= 0x7F;
- do
- {
- // Read next byte
- nByte = ifs.get();
- // Construct value by setting bottom 7 bits, then shifting 7 bits
- nValue = (nValue << 7) | (nByte & 0x7F);
- }
- while (nByte & 0x80); // Loop whilst read byte MSB is 1
- }
- // Return final construction (always 32-bit unsigned integer internally)
- return nValue;
- };
- uint32_t n32 = 0;
- uint16_t n16 = 0;
- // Read MIDI Header (Fixed Size)
- ifs.read((char*)&n32, sizeof(uint32_t));
- uint32_t nFileID = Swap32(n32);
- ifs.read((char*)&n32, sizeof(uint32_t));
- uint32_t nHeaderLength = Swap32(n32);
- ifs.read((char*)&n16, sizeof(uint16_t));
- uint16_t nFormat = Swap16(n16);
- ifs.read((char*)&n16, sizeof(uint16_t));
- uint16_t nTrackChunks = Swap16(n16);
- ifs.read((char*)&n16, sizeof(uint16_t));
- uint16_t nDivision = Swap16(n16);
- for (uint16_t nChunk = 0; nChunk < nTrackChunks; nChunk++)
- {
- std::cout << "===== NEW TRACK" << std::endl;
- // Read Track Header
- ifs.read((char*)&n32, sizeof(uint32_t));
- uint32_t nTrackID = Swap32(n32);
- ifs.read((char*)&n32, sizeof(uint32_t));
- uint32_t nTrackLength = Swap32(n32);
- bool bEndOfTrack = false;
- vecTracks.push_back(MidiTrack());
- uint32_t nWallTime = 0;
- uint8_t nPreviousStatus = 0;
- while (!ifs.eof() && !bEndOfTrack)
- {
- // Fundamentally all MIDI Events contain a timecode, and a status byte*
- uint32_t nStatusTimeDelta = 0;
- uint8_t nStatus = 0;
- // Read Timecode from MIDI stream. This could be variable in length
- // and is the delta in "ticks" from the previous event. Of course this value
- // could be 0 if two events happen simultaneously.
- nStatusTimeDelta = ReadValue();
- // Read first byte of message, this could be the status byte, or it could not...
- nStatus = ifs.get();
- // All MIDI Status events have the MSB set. The data within a standard MIDI event
- // does not. A crude yet utilised form of compression is to omit sending status
- // bytes if the following sequence of events all refer to the same MIDI Status.
- // This is called MIDI Running Status, and is essential to succesful decoding of
- // MIDI streams and files.
- //
- // If the MSB of the read byte was not set, and on the whole we were expecting a
- // status byte, then Running Status is in effect, so we refer to the previous
- // confirmed status byte.
- if (nStatus < 0x80)
- {
- // MIDI Running Status is happening, so refer to previous valid MIDI Status byte
- nStatus = nPreviousStatus;
- // We had to read the byte to assess if MIDI Running Status is in effect. But!
- // that read removed the byte form the stream, and that will desync all of the
- // following code because normally we would have read a status byte, but instead
- // we have read the data contained within a MIDI message. The simple solution is
- // to put the byte back :P
- ifs.seekg(-1, std::ios_base::cur);
- }
- if ((nStatus & 0xF0) == EventName::VoiceNoteOff)
- {
- nPreviousStatus = nStatus;
- uint8_t nChannel = nStatus & 0x0F;
- uint8_t nNoteID = ifs.get();
- uint8_t nNoteVelocity = ifs.get();
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::NoteOff, nNoteID, nNoteVelocity, nStatusTimeDelta });
- }
- else if ((nStatus & 0xF0) == EventName::VoiceNoteOn)
- {
- nPreviousStatus = nStatus;
- uint8_t nChannel = nStatus & 0x0F;
- uint8_t nNoteID = ifs.get();
- uint8_t nNoteVelocity = ifs.get();
- if(nNoteVelocity == 0)
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::NoteOff, nNoteID, nNoteVelocity, nStatusTimeDelta });
- else
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::NoteOn, nNoteID, nNoteVelocity, nStatusTimeDelta });
- }
- else if ((nStatus & 0xF0) == EventName::VoiceAftertouch)
- {
- nPreviousStatus = nStatus;
- uint8_t nChannel = nStatus & 0x0F;
- uint8_t nNoteID = ifs.get();
- uint8_t nNoteVelocity = ifs.get();
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::Other });
- }
- else if ((nStatus & 0xF0) == EventName::VoiceControlChange)
- {
- nPreviousStatus = nStatus;
- uint8_t nChannel = nStatus & 0x0F;
- uint8_t nControlID = ifs.get();
- uint8_t nControlValue = ifs.get();
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::Other });
- }
- else if ((nStatus & 0xF0) == EventName::VoiceProgramChange)
- {
- nPreviousStatus = nStatus;
- uint8_t nChannel = nStatus & 0x0F;
- uint8_t nProgramID = ifs.get();
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::Other });
- }
- else if ((nStatus & 0xF0) == EventName::VoiceChannelPressure)
- {
- nPreviousStatus = nStatus;
- uint8_t nChannel = nStatus & 0x0F;
- uint8_t nChannelPressure = ifs.get();
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::Other });
- }
- else if ((nStatus & 0xF0) == EventName::VoicePitchBend)
- {
- nPreviousStatus = nStatus;
- uint8_t nChannel = nStatus & 0x0F;
- uint8_t nLS7B = ifs.get();
- uint8_t nMS7B = ifs.get();
- vecTracks[nChunk].vecEvents.push_back({ MidiEvent::Type::Other });
- }
- else if ((nStatus & 0xF0) == EventName::SystemExclusive)
- {
- nPreviousStatus = 0;
- if (nStatus == 0xFF)
- {
- // Meta Message
- uint8_t nType = ifs.get();
- uint8_t nLength = ReadValue();
- switch (nType)
- {
- case MetaSequence:
- std::cout << "Sequence Number: " << ifs.get() << ifs.get() << std::endl;
- break;
- case MetaText:
- std::cout << "Text: " << ReadString(nLength) << std::endl;
- break;
- case MetaCopyright:
- std::cout << "Copyright: " << ReadString(nLength) << std::endl;
- break;
- case MetaTrackName:
- vecTracks[nChunk].sName = ReadString(nLength);
- std::cout << "Track Name: " << vecTracks[nChunk].sName << std::endl;
- break;
- case MetaInstrumentName:
- vecTracks[nChunk].sInstrument = ReadString(nLength);
- std::cout << "Instrument Name: " << vecTracks[nChunk].sInstrument << std::endl;
- break;
- case MetaLyrics:
- std::cout << "Lyrics: " << ReadString(nLength) << std::endl;
- break;
- case MetaMarker:
- std::cout << "Marker: " << ReadString(nLength) << std::endl;
- break;
- case MetaCuePoint:
- std::cout << "Cue: " << ReadString(nLength) << std::endl;
- break;
- case MetaChannelPrefix:
- std::cout << "Prefix: " << ifs.get() << std::endl;
- break;
- case MetaEndOfTrack:
- bEndOfTrack = true;
- break;
- case MetaSetTempo:
- // Tempo is in microseconds per quarter note
- if (m_nTempo == 0)
- {
- (m_nTempo |= (ifs.get() << 16));
- (m_nTempo |= (ifs.get() << 8));
- (m_nTempo |= (ifs.get() << 0));
- m_nBPM = (60000000 / m_nTempo);
- std::cout << "Tempo: " << m_nTempo << " (" << m_nBPM << "bpm)" << std::endl;
- }
- break;
- case MetaSMPTEOffset:
- std::cout << "SMPTE: H:" << ifs.get() << " M:" << ifs.get() << " S:" << ifs.get() << " FR:" << ifs.get() << " FF:" << ifs.get() << std::endl;
- break;
- case MetaTimeSignature:
- std::cout << "Time Signature: " << ifs.get() << "/" << (2 << ifs.get()) << std::endl;
- std::cout << "ClocksPerTick: " << ifs.get() << std::endl;
- // A MIDI "Beat" is 24 ticks, so specify how many 32nd notes constitute a beat
- std::cout << "32per24Clocks: " << ifs.get() << std::endl;
- break;
- case MetaKeySignature:
- std::cout << "Key Signature: " << ifs.get() << std::endl;
- std::cout << "Minor Key: " << ifs.get() << std::endl;
- break;
- case MetaSequencerSpecific:
- std::cout << "Sequencer Specific: " << ReadString(nLength) << std::endl;
- break;
- default:
- std::cout << "Unrecognised MetaEvent: " << nType << std::endl;
- }
- }
- if (nStatus == 0xF0)
- {
- // System Exclusive Message Begin
- std::cout << "System Exclusive Begin: " << ReadString(ReadValue()) << std::endl;
- }
- if (nStatus == 0xF7)
- {
- // System Exclusive Message Begin
- std::cout << "System Exclusive End: " << ReadString(ReadValue()) << std::endl;
- }
- }
- else
- {
- std::cout << "Unrecognised Status Byte: " << nStatus << std::endl;
- }
- }
- }
- // Convert Time Events to Notes
- for (auto& track : vecTracks)
- {
- uint32_t nWallTime = 0;
- std::list<MidiNote> listNotesBeingProcessed;
- for (auto& event : track.vecEvents)
- {
- nWallTime += event.nDeltaTick;
- if (event.event == MidiEvent::Type::NoteOn)
- {
- // New Note
- listNotesBeingProcessed.push_back({ event.nKey, event.nVelocity, nWallTime, 0 });
- }
- if (event.event == MidiEvent::Type::NoteOff)
- {
- auto note = std::find_if(listNotesBeingProcessed.begin(), listNotesBeingProcessed.end(), [&](const MidiNote& n) { return n.nKey == event.nKey; });
- if (note != listNotesBeingProcessed.end())
- {
- note->nDuration = nWallTime - note->nStartTime;
- track.vecNotes.push_back(*note);
- track.nMinNote = std::min(track.nMinNote, note->nKey);
- track.nMaxNote = std::max(track.nMaxNote, note->nKey);
- listNotesBeingProcessed.erase(note);
- }
- }
- }
- }
- return true;
- }
- public:
- std::vector<MidiTrack> vecTracks;
- uint32_t m_nTempo = 0;
- uint32_t m_nBPM = 0;
- };
- class olcMIDIViewer : public olc::PixelGameEngine
- {
- public:
- olcMIDIViewer()
- {
- sAppName = "MIDI File Viewer";
- }
- MidiFile midi;
- //HMIDIOUT hInstrument;
- size_t nCurrentNote[16]{ 0 };
- double dSongTime = 0.0;
- double dRunTime = 0.0;
- uint32_t nMidiClock = 0;
- public:
- bool OnUserCreate() override
- {
- midi.ParseFile("ff7_battle.mid");
- /*
- int nMidiDevices = midiOutGetNumDevs();
- if (nMidiDevices > 0)
- {
- if (midiOutOpen(&hInstrument, 2, NULL, 0, NULL) == MMSYSERR_NOERROR)
- {
- std::cout << "Opened midi" << std::endl;
- }
- }
- */
- return true;
- }
- float nTrackOffset = 1000;
- bool OnUserUpdate(float fElapsedTime) override
- {
- Clear(olc::BLACK);
- uint32_t nTimePerColumn = 50;
- uint32_t nNoteHeight = 2;
- uint32_t nOffsetY = 0;
- if (GetKey(olc::Key::LEFT).bHeld) nTrackOffset -= 10000.0f * fElapsedTime;
- if (GetKey(olc::Key::RIGHT).bHeld) nTrackOffset += 10000.0f * fElapsedTime;
- for (auto& track : midi.vecTracks)
- {
- if (!track.vecNotes.empty())
- {
- uint32_t nNoteRange = track.nMaxNote - track.nMinNote;
- FillRect(0, nOffsetY, ScreenWidth(), (nNoteRange + 1) * nNoteHeight, olc::DARK_GREY);
- DrawString(1, nOffsetY + 1, track.sName);
- for (auto& note : track.vecNotes)
- {
- FillRect((note.nStartTime - nTrackOffset) / nTimePerColumn, (nNoteRange - (note.nKey - track.nMinNote)) * nNoteHeight + nOffsetY, note.nDuration / nTimePerColumn, nNoteHeight, olc::WHITE);
- }
- nOffsetY += (nNoteRange + 1) * nNoteHeight + 4;
- }
- }
- // BELOW - ABSOLUTELY HORRIBLE BODGE TO PLAY SOUND
- // DO NOT USE THIS CODE...
- /*
- dRunTime += fElapsedTime;
- uint32_t nTempo = 4;
- int nTrack = 1;
- while (dRunTime >= 1.0 / double(midi.m_nBPM * 8))
- {
- dRunTime -= 1.0 / double(midi.m_nBPM * 8);
- // Single MIDI Clock
- nMidiClock++;
- int i = 0;
- int nTrack = 1;
- //for (nTrack = 1; nTrack < 3; nTrack++)
- {
- if (nCurrentNote[nTrack] < midi.vecTracks[nTrack].vecEvents.size())
- {
- if (midi.vecTracks[nTrack].vecEvents[nCurrentNote[nTrack]].nDeltaTick == 0)
- {
- uint32_t nStatus = 0;
- uint32_t nNote = midi.vecTracks[nTrack].vecEvents[nCurrentNote[nTrack]].nKey;
- uint32_t nVelocity = midi.vecTracks[nTrack].vecEvents[nCurrentNote[nTrack]].nVelocity;
- if (midi.vecTracks[nTrack].vecEvents[nCurrentNote[nTrack]].event == MidiEvent::Type::NoteOn)
- nStatus = 0x90;
- else
- nStatus = 0x80;
- midiOutShortMsg(hInstrument, (nVelocity << 16) | (nNote << 8) | nStatus);
- nCurrentNote[nTrack]++;
- }
- else
- midi.vecTracks[nTrack].vecEvents[nCurrentNote[nTrack]].nDeltaTick--;
- }
- }
- }
- if (GetKey(olc::Key::SPACE).bPressed)
- {
- midiOutShortMsg(hInstrument, 0x00403C90);
- }
- if (GetKey(olc::Key::SPACE).bReleased)
- {
- midiOutShortMsg(hInstrument, 0x00003C80);
- }
- */
- return true;
- }
- };
- int main()
- {
- olcMIDIViewer demo;
- if (demo.Construct(1280, 960, 1, 1))
- demo.Start();
- return 0;
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement