From 2ca6db0ccdbf2f210eac1810d6911873101de0f2 Mon Sep 17 00:00:00 2001 From: Javidx9 <25419386+OneLoneCoder@users.noreply.github.com> Date: Mon, 9 Mar 2020 20:17:54 +0000 Subject: [PATCH] Added MIDI Video --- Videos/OneLoneCoder_PGE_MIDI.cpp | 626 +++++++++++++++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 Videos/OneLoneCoder_PGE_MIDI.cpp diff --git a/Videos/OneLoneCoder_PGE_MIDI.cpp b/Videos/OneLoneCoder_PGE_MIDI.cpp new file mode 100644 index 0000000..9494328 --- /dev/null +++ b/Videos/OneLoneCoder_PGE_MIDI.cpp @@ -0,0 +1,626 @@ +/* + 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 +#include + +//#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 vecEvents; + std::vector 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 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 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; +} +