Skip to content

Commit

Permalink
MIDI import: split tracks by channels and controller numbers (#294)
Browse files Browse the repository at this point in the history
All piano events are grouped in tracks by channel,
automation events are are grouped by controller number
  • Loading branch information
peterrudenko committed Feb 3, 2024
1 parent 85e5d46 commit 97b1493
Show file tree
Hide file tree
Showing 12 changed files with 84 additions and 52 deletions.
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/AnnotationsSequence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ AnnotationsSequence::AnnotationsSequence(MidiTrack &track,
// Import/export
//===----------------------------------------------------------------------===//

void AnnotationsSequence::importMidi(const MidiMessageSequence &sequence, short timeFormat)
void AnnotationsSequence::importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> customFilter)
{
this->clearUndoHistory();
this->checkpoint();
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/AnnotationsSequence.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class AnnotationsSequence final : public MidiSequence
// Import/export
//===------------------------------------------------------------------===//

void importMidi(const MidiMessageSequence &sequence, short timeFormat) override;
void importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> customFilter) override;

//===------------------------------------------------------------------===//
// Undoable track editing
Expand Down
12 changes: 10 additions & 2 deletions Source/Core/Midi/Sequences/AutomationSequence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,25 @@ float AutomationSequence::getAverageControllerValue() const
// Import/export
//===----------------------------------------------------------------------===//

void AutomationSequence::importMidi(const MidiMessageSequence &sequence, short timeFormat)
void AutomationSequence::importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> filterByCV)
{
this->clearUndoHistory();
this->checkpoint();

for (int i = 0; i < sequence.getNumEvents(); ++i)
{
const MidiMessage &message = sequence.getEventPointer(i)->message;
const auto &message = sequence.getEventPointer(i)->message;
const float startBeat = MidiSequence::midiTicksToBeats(message.getTimeStamp(), timeFormat);

if (message.isController())
{
if (filterByCV.hasValue() &&
message.getControllerNumber() != *filterByCV)
{
continue;
}

const int controllerValue = message.getControllerValue();
const AutomationEvent event(this, startBeat, float(controllerValue) / 127.f);
this->importMidiEvent<AutomationEvent>(event);
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/AutomationSequence.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class AutomationSequence final : public MidiSequence
// Import/export
//===------------------------------------------------------------------===//

void importMidi(const MidiMessageSequence &sequence, short timeFormat) override;
void importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> filterByCV) override;

//===------------------------------------------------------------------===//
// Serializable
Expand Down
5 changes: 3 additions & 2 deletions Source/Core/Midi/Sequences/KeySignaturesSequence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ KeySignaturesSequence::KeySignaturesSequence(MidiTrack &track,
// Import/export
//===----------------------------------------------------------------------===//

void KeySignaturesSequence::importMidi(const MidiMessageSequence &sequence, short timeFormat)
void KeySignaturesSequence::importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> customFilter)
{
this->clearUndoHistory();
this->checkpoint();

for (int i = 0; i < sequence.getNumEvents(); ++i)
{
const MidiMessage &message = sequence.getEventPointer(i)->message;
const auto &message = sequence.getEventPointer(i)->message;
if (message.isKeySignatureMetaEvent())
{
const bool isMajor = message.isKeySignatureMajorKey();
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/KeySignaturesSequence.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class KeySignaturesSequence final : public MidiSequence
// Import/export
//===------------------------------------------------------------------===//

void importMidi(const MidiMessageSequence &sequence, short timeFormat) override;
void importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> customFilter) override;

//===------------------------------------------------------------------===//
// Undoable track editing
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/MidiSequence.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ class MidiSequence : public Serializable
//===------------------------------------------------------------------===//

static float midiTicksToBeats(double ticks, int timeFormat) noexcept;
virtual void importMidi(const MidiMessageSequence &sequence, short timeFormat) = 0;
virtual void importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> customFilter) = 0;
virtual void exportMidi(MidiMessageSequence &outSequence,
const Clip &clip, const KeyboardMapping &keyMap,
bool soloPlaybackMode, bool exportMetronome,
Expand Down
20 changes: 14 additions & 6 deletions Source/Core/Midi/Sequences/PianoSequence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,27 @@ PianoSequence::PianoSequence(MidiTrack &track,
// Import/export
//===----------------------------------------------------------------------===//

void PianoSequence::importMidi(const MidiMessageSequence &sequence, short timeFormat)
void PianoSequence::importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> filterByChannel)
{
this->clearUndoHistory();
this->checkpoint();

for (int i = 0; i < sequence.getNumEvents(); ++i)
{
const auto &messageOn = sequence.getEventPointer(i)->message;
if (messageOn.isNoteOn())
const auto &message = sequence.getEventPointer(i)->message;

if (filterByChannel.hasValue() &&
message.getChannel() != *filterByChannel)
{
continue;
}

if (message.isNoteOn())
{
const int key = messageOn.getNoteNumber();
const float velocity = messageOn.getVelocity() / 128.f;
const float startBeat = MidiSequence::midiTicksToBeats(messageOn.getTimeStamp(), timeFormat);
const int key = message.getNoteNumber();
const float velocity = message.getVelocity() / 128.f;
const float startBeat = MidiSequence::midiTicksToBeats(message.getTimeStamp(), timeFormat);
const int noteOffIndex = sequence.getIndexOfMatchingKeyUp(i);
if (noteOffIndex > 0)
{
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/PianoSequence.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ class PianoSequence final : public MidiSequence
// Import/export
//===------------------------------------------------------------------===//

void importMidi(const MidiMessageSequence &sequence, short timeFormat) override;
void importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> filterByChannel) override;

//===------------------------------------------------------------------===//
// Undoable track editing
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/TimeSignaturesSequence.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ TimeSignaturesSequence::TimeSignaturesSequence(MidiTrack &track,
// Import/export
//===----------------------------------------------------------------------===//

void TimeSignaturesSequence::importMidi(const MidiMessageSequence &sequence, short timeFormat)
void TimeSignaturesSequence::importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> customFilter)
{
this->clearUndoHistory();
this->checkpoint();
Expand Down
3 changes: 2 additions & 1 deletion Source/Core/Midi/Sequences/TimeSignaturesSequence.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class TimeSignaturesSequence final : public MidiSequence
// Import/export
//===------------------------------------------------------------------===//

void importMidi(const MidiMessageSequence &sequence, short timeFormat) override;
void importMidi(const MidiMessageSequence &sequence,
short timeFormat, Optional<int> customFilter) override;
void exportMidi(MidiMessageSequence &outSequence,
const Clip &clip, const KeyboardMapping &keyMap,
bool soloPlaybackMode, bool exportMetronome,
Expand Down
75 changes: 41 additions & 34 deletions Source/Core/Tree/ProjectNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ void ProjectNode::importMidi(InputStream &stream)
if (!tempFile.readFrom(stream))
{
DBG("Midi file appears corrupted");
jassertfalse;
return;
}

Expand All @@ -615,55 +616,53 @@ void ProjectNode::importMidi(InputStream &stream)
{
const auto *importedTrack = tempFile.getTrack(i);

bool hasPianoEvents = false;
bool hasControllerEvents = false;
int trackControllerNumber = 0;
int trackChannel = 1;

auto trackName = "Track " + String(i);
const auto trackColour = colours[r.nextInt(colours.size())]; // set some random colour
const auto trackColour = colours[r.nextInt(colours.size())];

// the MIDI standard allows to set channels and controller numbers per-message,
// while in Helio channels and controllers are per-track for simplicity,
// so let's group all found notes in each track by channel
// (a different channel often means a different instrument):
FlatHashSet<int> pianoChannels;

// and group automation events by controller number, picking the last
// found channel for it, which is not ideal but should be ok in practice:
FlatHashMap<int, int> automationControllers;

for (int j = 0; j < importedTrack->getNumEvents(); ++j)
{
const auto *event = importedTrack->getEventPointer(j);
const auto channel = jlimit(1, Globals::numChannels, event->message.getChannel());

if (event->message.isTrackNameEvent())
{
trackName = event->message.getTextFromTextMetaEvent();
}
else if (event->message.isMidiChannelMetaEvent())
{
trackChannel = event->message.getMidiChannelMetaEventChannel();
}
else if (event->message.isController())
{
trackControllerNumber = event->message.getControllerNumber();
hasControllerEvents = true;
if (event->message.getChannel() > 0)
{
trackChannel = event->message.getChannel();
}
automationControllers.insert({ event->message.getControllerNumber(), channel });
}
else if (event->message.isTempoMetaEvent())
{
trackControllerNumber = MidiTrack::tempoController;
hasControllerEvents = true;
automationControllers.insert({ MidiTrack::DefaultControllers::tempoController, channel });
}
else if (event->message.isNoteOnOrOff())
{
hasPianoEvents = true;
if (event->message.getChannel() > 0)
{
trackChannel = event->message.getChannel();
}
pianoChannels.insert(channel);
}
}

if (hasControllerEvents)
// split into several automation tracks, if needed
for (const auto trackInfo : automationControllers)
{
const String controllerName = trackControllerNumber == MidiTrack::tempoController ?
"Tempo" : MidiMessage::getControllerName(trackControllerNumber);
const auto trackControllerNumber = trackInfo.first;
const auto trackChannel = trackInfo.second;
const bool isTempoTrack = trackControllerNumber == MidiTrack::DefaultControllers::tempoController;
const String controllerName(MidiMessage::getControllerName(trackControllerNumber));

MidiTrackNode *trackNode = new AutomationTrackNode(trackName + " - " + controllerName);
MidiTrackNode *trackNode = new AutomationTrackNode(isTempoTrack ?
TRANS(I18n::Defaults::tempoTrackName) :
(controllerName.isEmpty() ? trackName : trackName + " - " + controllerName));

const Clip clip(trackNode->getPattern());
trackNode->getPattern()->insert(clip, false);
Expand All @@ -675,12 +674,17 @@ void ProjectNode::importMidi(InputStream &stream)
trackNode->setTrackControllerNumber(trackControllerNumber, dontSendNotification);
trackNode->setTrackChannel(trackChannel, false, dontSendNotification);
trackNode->setTrackColour(trackColour, false, dontSendNotification);
trackNode->getSequence()->importMidi(*importedTrack, timeFormat);

const auto isSingleControllerTrack = automationControllers.size() == 1;
trackNode->getSequence()->importMidi(*importedTrack, timeFormat,
isSingleControllerTrack ? Optional<int>() : trackControllerNumber);
}

if (hasPianoEvents)
// split into several piano tracks, if needed
for (const auto trackChannel : pianoChannels)
{
MidiTrackNode *trackNode = new PianoTrackNode(trackName);
MidiTrackNode *trackNode = new PianoTrackNode(trackChannel == 1 ?
trackName : trackName + " - " + String(trackChannel));

const Clip clip(trackNode->getPattern());
trackNode->getPattern()->insert(clip, false);
Expand All @@ -691,15 +695,18 @@ void ProjectNode::importMidi(InputStream &stream)

trackNode->setTrackChannel(trackChannel, false, dontSendNotification);
trackNode->setTrackColour(trackColour, false, dontSendNotification);
trackNode->getSequence()->importMidi(*importedTrack, timeFormat);

const auto isSingleChannelTrack = pianoChannels.size() == 1;
trackNode->getSequence()->importMidi(*importedTrack, timeFormat,
isSingleChannelTrack ? Optional<int>() : trackChannel);
}

// if the track contains any key/time signatures, try importing them all,
// skipping others (assuming that there might be cases where tracks contain
// events of different types, e.g. mostly notes but also some meta events):
this->timeline->getAnnotations()->getSequence()->importMidi(*importedTrack, timeFormat);
this->timeline->getKeySignatures()->getSequence()->importMidi(*importedTrack, timeFormat);
this->timeline->getTimeSignatures()->getSequence()->importMidi(*importedTrack, timeFormat);
this->timeline->getAnnotations()->getSequence()->importMidi(*importedTrack, timeFormat, {});
this->timeline->getKeySignatures()->getSequence()->importMidi(*importedTrack, timeFormat, {});
this->timeline->getTimeSignatures()->getSequence()->importMidi(*importedTrack, timeFormat, {});
}

this->isTracksCacheOutdated = true;
Expand Down

0 comments on commit 97b1493

Please sign in to comment.