|
|
/*
|
|
|
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, <EFBFBD>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;
|
|
|
}
|
|
|
|
|
|
|