Skip to content

Commit

Permalink
Merge pull request #3906 from brdvd/feat/octave-midi
Browse files Browse the repository at this point in the history
Octave handling for MIDI
  • Loading branch information
lpugin authored Jan 6, 2025
2 parents 00550f0 + 0d60320 commit e535b90
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 8 deletions.
8 changes: 8 additions & 0 deletions include/vrv/horizontalaligner.h
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ class Alignment : public Object {
Fraction GetTime() const { return m_time; }
///@}

/**
* @name Weak ordering: for alignments in the same measure it is based on time, otherwise on the measure order
*/
///@{
bool operator==(const Alignment &other) const;
std::weak_ordering operator<=>(const Alignment &other) const;
///@}

/**
* Add the LayerElement to the appropriate AlignmentReference child.
* Looks at the cross-staff situation (@staff or parent @staff).
Expand Down
35 changes: 34 additions & 1 deletion include/vrv/midifunctor.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ class InitTimemapTiesFunctor : public Functor {
// InitMIDIFunctor
//----------------------------------------------------------------------------

/**
* Helper struct to store octave data
*/
struct OctaveInfo {
const Octave *octave;
int staffN;
int layerN;
int octaveShift;
bool isActive;
};

/**
* This class initializes the MIDI export.
* Captures information (i.e. from control elements) for MIDI interpretation
Expand All @@ -207,6 +218,7 @@ class InitMIDIFunctor : public ConstFunctor {
///@{
void SetCurrentTempo(double tempo) { m_currentTempo = tempo; }
const std::map<const Note *, double> &GetDeferredNotes() const { return m_deferredNotes; }
const std::list<OctaveInfo> &GetOctaves() const { return m_octaves; }
///@}

/*
Expand All @@ -215,6 +227,7 @@ class InitMIDIFunctor : public ConstFunctor {
///@{
FunctorCode VisitArpeg(const Arpeg *arpeg) override;
FunctorCode VisitMeasure(const Measure *measure) override;
FunctorCode VisitOctave(const Octave *octave) override;
///@}

protected:
Expand All @@ -228,6 +241,8 @@ class InitMIDIFunctor : public ConstFunctor {
double m_currentTempo;
// Deferred notes which start slightly later
std::map<const Note *, double> m_deferredNotes;
// Octave info which is collected
std::list<OctaveInfo> m_octaves;
};

//----------------------------------------------------------------------------
Expand Down Expand Up @@ -292,14 +307,16 @@ class GenerateMIDIFunctor : public ConstFunctor {
*/
///@{
void SetChannel(int channel) { m_midiChannel = channel; }
void SetControlEvents(bool controlEvents) { m_controlEvents = controlEvents; }
void SetCueExclusion(bool cueExclusion) { m_cueExclusion = cueExclusion; }
void SetCurrentTempo(double tempo) { m_currentTempo = tempo; }
void SetDeferredNotes(const std::map<const Note *, double> &deferredNotes) { m_deferredNotes = deferredNotes; }
void SetLayerN(int layerN) { m_layerN = layerN; }
void SetOctaves(const std::list<OctaveInfo> &octaves) { m_octaves = octaves; }
void SetStaffN(int staffN) { m_staffN = staffN; }
void SetTempoEventTicks(const std::set<int> &ticks) { m_tempoEventTicks = ticks; }
void SetTrack(int track) { m_midiTrack = track; }
void SetTransSemi(int transSemi) { m_transSemi = transSemi; }
void SetControlEvents(bool controlEvents) { m_controlEvents = controlEvents; }
///@}

/*
Expand Down Expand Up @@ -339,6 +356,16 @@ class GenerateMIDIFunctor : public ConstFunctor {
*/
void GenerateGraceNoteMIDI(const Note *refNote, double startTime, int tpq, int channel, int velocity);

/**
* Change the octave shift at the begin/end of octaves
*/
void HandleOctave(const LayerElement *layerElement);

/**
* Convenience helper
*/
int GetMIDIPitch(const Note *note) { return note->GetMIDIPitch(m_transSemi, m_octaveShift); }

public:
//
private:
Expand All @@ -352,8 +379,12 @@ class GenerateMIDIFunctor : public ConstFunctor {
double m_totalTime;
// The current staff number
int m_staffN;
// The current layer number
int m_layerN;
// The semi tone transposition for the current track
int m_transSemi;
// The octave shift for the current track
int m_octaveShift;
// The current tempo
double m_currentTempo;
// Tempo events are always added on track 0
Expand All @@ -365,6 +396,8 @@ class GenerateMIDIFunctor : public ConstFunctor {
std::map<const Note *, MIDINoteSequence> m_expandedNotes;
// Deferred notes which start slightly later
std::map<const Note *, double> m_deferredNotes;
// Octave info which is used to determine the octave shift
std::list<OctaveInfo> m_octaves;
// Grace note sequence
MIDIChordSequence m_graceNotes;
// Indicates whether the last grace note/chord was accented
Expand Down
2 changes: 1 addition & 1 deletion include/vrv/note.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ class Note : public LayerElement,
/**
* MIDI pitch
*/
int GetMIDIPitch(int shift = 0) const;
int GetMIDIPitch(int shift = 0, int octaveShift = 0) const;

/**
* Get pitch class of the current note
Expand Down
2 changes: 2 additions & 0 deletions src/doc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -541,10 +541,12 @@ void Doc::ExportMIDI(smf::MidiFile *midiFile)
generateMIDI.SetChannel(midiChannel);
generateMIDI.SetTrack(midiTrack);
generateMIDI.SetStaffN(staves.first);
generateMIDI.SetLayerN(layers.first);
generateMIDI.SetTempoEventTicks(tempoEventTicks);
generateMIDI.SetTransSemi(transSemi);
generateMIDI.SetCurrentTempo(tempo);
generateMIDI.SetDeferredNotes(initMIDI.GetDeferredNotes());
generateMIDI.SetOctaves(initMIDI.GetOctaves());
generateMIDI.SetCueExclusion(this->GetOptions()->m_midiNoCue.GetValue());
generateMIDI.SetControlEvents(controlEvents);

Expand Down
23 changes: 23 additions & 0 deletions src/horizontalaligner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,29 @@ bool Alignment::IsSupportedChild(Object *child)
return true;
}

bool Alignment::operator==(const Alignment &other) const
{
const Measure *measure = vrv_cast<const Measure *>(this->GetFirstAncestor(MEASURE));
const Measure *otherMeasure = vrv_cast<const Measure *>(other.GetFirstAncestor(MEASURE));
assert(measure && otherMeasure);

return (measure == otherMeasure) && (this->GetTime() == other.GetTime());
}

std::weak_ordering Alignment::operator<=>(const Alignment &other) const
{
const Measure *measure = vrv_cast<const Measure *>(this->GetFirstAncestor(MEASURE));
const Measure *otherMeasure = vrv_cast<const Measure *>(other.GetFirstAncestor(MEASURE));
assert(measure && otherMeasure);

if (measure == otherMeasure) {
return this->GetTime() <=> other.GetTime();
}
else {
return Object::IsPreOrdered(measure, otherMeasure) ? std::weak_ordering::less : std::weak_ordering::greater;
}
}

bool Alignment::HasAccidVerticalOverlap(const Alignment *otherAlignment, int staffN) const
{
if (!otherAlignment) return false;
Expand Down
78 changes: 74 additions & 4 deletions src/midifunctor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "gracegrp.h"
#include "layer.h"
#include "multirest.h"
#include "octave.h"
#include "pedal.h"
#include "rest.h"
#include "staff.h"
Expand Down Expand Up @@ -352,6 +353,31 @@ FunctorCode InitMIDIFunctor::VisitMeasure(const Measure *measure)
return FUNCTOR_CONTINUE;
}

FunctorCode InitMIDIFunctor::VisitOctave(const Octave *octave)
{
const Measure *measure = vrv_cast<const Measure *>(octave->GetFirstAncestor(MEASURE));
assert(measure);
std::vector<const Staff *> staffList = octave->GetTstampStaves(measure, octave);
if (staffList.size() != 1) return FUNCTOR_CONTINUE;
const Staff *staff = staffList[0];

const bool raisePitch = (octave->GetDisPlace() != STAFFREL_basic_below);
int shift = 0;
switch (octave->GetDis()) {
case OCTAVE_DIS_8: shift = 1; break;
case OCTAVE_DIS_15: shift = 2; break;
case OCTAVE_DIS_22: shift = 3; break;
default: break;
}

const Layer *layer = vrv_cast<const Layer *>(raisePitch ? staff->GetFirst(LAYER) : staff->GetLast(LAYER));
assert(layer);

m_octaves.push_back({ octave, staff->GetN(), layer->GetN(), (raisePitch ? shift : -shift), false });

return FUNCTOR_CONTINUE;
}

//----------------------------------------------------------------------------
// GenerateMIDIFunctor
//----------------------------------------------------------------------------
Expand All @@ -364,6 +390,7 @@ GenerateMIDIFunctor::GenerateMIDIFunctor(smf::MidiFile *midiFile) : ConstFunctor
m_totalTime = 0.0;
m_staffN = 0;
m_transSemi = 0;
m_octaveShift = 0;
m_currentTempo = MIDI_TEMPO;
m_lastNote = NULL;
m_accentedGraceNote = false;
Expand Down Expand Up @@ -429,7 +456,7 @@ FunctorCode GenerateMIDIFunctor::VisitBTrem(const BTrem *bTrem)
auto expandNote = [this, noteInQuarterDur, num](const Object *obj) {
const Note *note = vrv_cast<const Note *>(obj);
assert(note);
const int pitch = note->GetMIDIPitch(m_transSemi);
const int pitch = this->GetMIDIPitch(note);
const double totalInQuarterDur
= note->GetScoreTimeDuration().ToDouble() + note->GetScoreTimeTiedDuration().ToDouble();
int multiplicity = totalInQuarterDur / noteInQuarterDur;
Expand Down Expand Up @@ -460,14 +487,16 @@ FunctorCode GenerateMIDIFunctor::VisitBTrem(const BTrem *bTrem)

FunctorCode GenerateMIDIFunctor::VisitChord(const Chord *chord)
{
this->HandleOctave(chord);

// Handle grace chords
if (chord->IsGraceNote()) {
std::set<int> pitches;
const ListOfConstObjects &notes = chord->GetList();
for (const Object *obj : notes) {
const Note *note = vrv_cast<const Note *>(obj);
assert(note);
pitches.insert(note->GetMIDIPitch(m_transSemi));
pitches.insert(this->GetMIDIPitch(note));
}

double quarterDuration = 0.0;
Expand All @@ -491,6 +520,8 @@ FunctorCode GenerateMIDIFunctor::VisitChord(const Chord *chord)

FunctorCode GenerateMIDIFunctor::VisitFTrem(const FTrem *fTrem)
{
this->HandleOctave(fTrem);

if (fTrem->HasUnitdur()) {
LogWarning("FTrem produces incorrect MIDI output");
}
Expand Down Expand Up @@ -560,6 +591,8 @@ FunctorCode GenerateMIDIFunctor::VisitLayerElement(const LayerElement *layerElem
{
if (layerElement->IsScoreDefElement()) return FUNCTOR_SIBLINGS;

this->HandleOctave(layerElement);

// Only resolve simple sameas links to avoid infinite recursion
const LayerElement *sameas = dynamic_cast<const LayerElement *>(layerElement->GetSameasLink());
if (sameas && !sameas->HasSameasLink()) {
Expand Down Expand Up @@ -595,6 +628,8 @@ FunctorCode GenerateMIDIFunctor::VisitMRpt(const MRpt *mRpt)

FunctorCode GenerateMIDIFunctor::VisitNote(const Note *note)
{
this->HandleOctave(note);

// Skip linked notes
if (note->HasSameasLink()) {
return FUNCTOR_SIBLINGS;
Expand All @@ -612,7 +647,7 @@ FunctorCode GenerateMIDIFunctor::VisitNote(const Note *note)

// Handle grace notes
if (note->IsGraceNote()) {
const int pitch = note->GetMIDIPitch(m_transSemi);
const int pitch = this->GetMIDIPitch(note);

double quarterDuration = 0.0;
const data_DURATION dur = note->GetDur();
Expand Down Expand Up @@ -662,7 +697,7 @@ FunctorCode GenerateMIDIFunctor::VisitNote(const Note *note)
}
}
else {
const int pitch = note->GetMIDIPitch(m_transSemi);
const int pitch = this->GetMIDIPitch(note);

if (note->HasTabCourse() && (note->GetTabCourse() >= 1)) {
// Tablature 'rule of holds'. A note on a course is held until the next note
Expand Down Expand Up @@ -907,6 +942,41 @@ void GenerateMIDIFunctor::GenerateGraceNoteMIDI(
}
}

void GenerateMIDIFunctor::HandleOctave(const LayerElement *layerElement)
{
// Handle octave end
auto octaveIter = std::find_if(m_octaves.begin(), m_octaves.end(), [this, layerElement](const OctaveInfo &octave) {
if (octave.isActive && (octave.staffN == m_staffN) && (octave.layerN == m_layerN)) {
const Alignment *endAlignment = octave.octave->GetEnd()->GetAlignment();
const Alignment *alignment = layerElement->GetAlignment();
if (endAlignment && alignment) {
return *endAlignment < *alignment;
}
}
return false;
});
if (octaveIter != m_octaves.end()) {
m_octaveShift -= octaveIter->octaveShift;
m_octaves.erase(octaveIter);
}

// Handle octave begin
octaveIter = std::find_if(m_octaves.begin(), m_octaves.end(), [this, layerElement](const OctaveInfo &octave) {
if (!octave.isActive && (octave.staffN == m_staffN) && (octave.layerN == m_layerN)) {
const Alignment *startAlignment = octave.octave->GetStart()->GetAlignment();
const Alignment *alignment = layerElement->GetAlignment();
if (startAlignment && alignment) {
return *startAlignment <= *alignment;
}
}
return false;
});
if (octaveIter != m_octaves.end()) {
m_octaveShift += octaveIter->octaveShift;
octaveIter->isActive = true;
}
}

//----------------------------------------------------------------------------
// GenerateTimemapFunctor
//----------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions src/note.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -787,7 +787,7 @@ bool Note::IsEnharmonicWith(const Note *note) const
return (this->GetMIDIPitch() == note->GetMIDIPitch());
}

int Note::GetMIDIPitch(const int shift) const
int Note::GetMIDIPitch(const int shift, const int octaveShift) const
{
int pitch = 0;

Expand All @@ -797,7 +797,7 @@ int Note::GetMIDIPitch(const int shift) const
else if (this->HasPname() || this->HasPnameGes()) {
const int pclass = this->GetPitchClass();

int oct = this->GetOct();
int oct = this->GetOct() + octaveShift;
if (this->HasOctGes()) oct = this->GetOctGes();

pitch = pclass + (oct + 1) * 12;
Expand Down

0 comments on commit e535b90

Please sign in to comment.