diff --git a/src/libmscore/CMakeLists.txt b/src/libmscore/CMakeLists.txt index ccb53d6f2b22d..7b4c2fb761c44 100644 --- a/src/libmscore/CMakeLists.txt +++ b/src/libmscore/CMakeLists.txt @@ -207,6 +207,8 @@ set(MODULE_SRC pedal.h pitch.cpp pitch.h + pitchadjuster.cpp + pitchadjuster.h pitchspelling.cpp pitchspelling.h pitchvalue.h diff --git a/src/libmscore/note.cpp b/src/libmscore/note.cpp index 2d5a1a79111e8..e8baee2d13fe6 100644 --- a/src/libmscore/note.cpp +++ b/src/libmscore/note.cpp @@ -726,6 +726,7 @@ Note::Note(const Note& n, bool link) _accidental = 0; _cachedNoteheadSym = n._cachedNoteheadSym; _cachedSymNull = n._cachedSymNull; + _pitchAdjuster = n._pitchAdjuster; if (n._accidental) { add(new Accidental(*(n._accidental))); @@ -1467,6 +1468,7 @@ void Note::write(XmlWriter& xml) const Pid::GHOST, Pid::HEAD_TYPE, Pid::VELO_TYPE, Pid::FIXED, Pid::FIXED_LINE }) { writeProperty(xml, id); } + _pitchAdjuster.write(xml); for (Spanner* e : _spannerFor) { e->writeSpannerStart(xml, this, track()); @@ -1643,6 +1645,8 @@ bool Note::readProperties(XmlReader& e) NoteDot* dot = new NoteDot(score()); dot->read(e); add(dot); + } else if (tag == "pitchAdjust") { + _pitchAdjuster.read(e); } else if (tag == "Events") { _playEvents.clear(); // remove default event while (e.readNextStartElement()) { @@ -2581,7 +2585,7 @@ int Note::ppitch() const } } - return _pitch + ottaveCapoFret(); + return _pitch + ottaveCapoFret() + _pitchAdjuster.getAlter(); } //--------------------------------------------------------- diff --git a/src/libmscore/note.h b/src/libmscore/note.h index a3f06165f52ef..dc5db0afa579f 100644 --- a/src/libmscore/note.h +++ b/src/libmscore/note.h @@ -37,6 +37,7 @@ #include "shape.h" #include "key.h" #include "sym.h" +#include "pitchadjuster.h" namespace Ms { class Tie; @@ -310,6 +311,8 @@ class Note final : public Element QString _fretString; + PitchAdjuster _pitchAdjuster; // guitar harmonics + void startDrag(EditData&) override; QRectF drag(EditData& ed) override; void endDrag(EditData&) override; diff --git a/src/libmscore/pitchadjuster.cpp b/src/libmscore/pitchadjuster.cpp new file mode 100644 index 0000000000000..bd5b1d769ea3a --- /dev/null +++ b/src/libmscore/pitchadjuster.cpp @@ -0,0 +1,588 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "pitchadjuster.h" +#include "property.h" +#include "style.h" +#include "xml.h" + +/* + * MusicXml tracks harmonics as BASE, TOUCHING and SOUNDING pitch. + * + * Base Pitch: the tuning for the string. For artificial harmonics its + * the tuning for the string + a fret number + * Touching Pitch: the pitch of the note if it was played normally. This + * is base pitch + fret number + * Sounding Pitch: the pitch of the harmonic. It may be several octaves higher + * than the base/touching pitch. + * + * PitchAdjuster lives within a MuseScore Note. For this reason the class was optimized + * for size (i.e. pitches are stored as unsigned char). This is ugly, but compact. + * + * PitchAdjuster does the following: + * + * 1. Tracks optional harmonic info for a Note that works for music notation and + * tablature. + * 2. Saves/loads harmonic info to/from MuseScore mscx/mscz files. + * 3. Provides a getAlter() method that does harmonic math + * 4. Supports fractional fret numbers (e.g. touching fret = 2.7). + * 5. Supports a way to alter a Note's pitch by a specific amount. This is needed to + * handle Tuner string bends (e.g. TablEdit Pitch Bends) + */ + +namespace Ms { +//--------------------------------------------------------- +// PitchAdjuster +//--------------------------------------------------------- + +PitchAdjuster::PitchAdjuster() + : _harmonicType(HarmonicType::None), + _basePitch(0), + _touchingFret(0), + _alter(0) +{ +} + +//--------------------------------------------------------- +// clear +//--------------------------------------------------------- + +void PitchAdjuster::clear() +{ + _harmonicType = HarmonicType::None; + _basePitch = 0; + _touchingFret = 0; + _alter = 0; +} + +//--------------------------------------------------------- +// setNaturalHarmonic +//--------------------------------------------------------- + +bool PitchAdjuster::setNaturalHarmonic(int basePitch, int touchingPitch) +{ + if (basePitch < 0 || basePitch > 127 + || touchingPitch < 0 || touchingPitch > 127 + || touchingPitch < basePitch || (touchingPitch - basePitch) > 24) { + clear(); + return false; + } + _harmonicType = HarmonicType::Natural; + setBasePitch(basePitch); + setTouchingFret(touchingPitch - basePitch); + return true; +} + +//--------------------------------------------------------- +// setArtificialHarmonic +//--------------------------------------------------------- + +bool PitchAdjuster::setArtificialHarmonic(int basePitch, int touchingPitch, HarmonicType hType) +{ + if (basePitch < 0 || basePitch > 127 + || touchingPitch < 0 || touchingPitch > 127 + || touchingPitch < basePitch || (touchingPitch - basePitch) > 24) { + clear(); + return false; + } + _harmonicType = hType; + setBasePitch(basePitch); + setTouchingFret(touchingPitch - basePitch); + return true; +} + +//--------------------------------------------------------- +// setNaturalHarmonic +//--------------------------------------------------------- + +bool PitchAdjuster::setNaturalHarmonic(int basePitch, double touchingPitch) +{ + if (basePitch < 0 || basePitch > 127 + || touchingPitch < 0.0 || touchingPitch > 127.0 + || touchingPitch < (double)basePitch || (touchingPitch - (double)basePitch) > 24.0) { + clear(); + return false; + } + _harmonicType = HarmonicType::Natural; + setBasePitch(basePitch); + setTouchingFret(touchingPitch - double(basePitch)); + return true; +} + +//--------------------------------------------------------- +// setNaturalHarmonic +//--------------------------------------------------------- + +bool PitchAdjuster::setNaturalHarmonic(int basePitch, const QString& touchingPitch) +{ + bool ok = false; + double touch = touchingPitch.toDouble(&ok); + if (!ok) { + clear(); + return false; + } + return setNaturalHarmonic(basePitch, touch); +} + +//--------------------------------------------------------- +// getHarmonicType +//--------------------------------------------------------- + +HarmonicType PitchAdjuster::getHarmonicType() const +{ + return _harmonicType; +} + +//--------------------------------------------------------- +// getBasePitch +//--------------------------------------------------------- + +int PitchAdjuster::getBasePitch() const +{ + return int(_basePitch); +} + +//--------------------------------------------------------- +// getTouchingPitch +//--------------------------------------------------------- + +int PitchAdjuster::getTouchingPitch() const +{ + return getBasePitch() + int(getTouchingFret()); +} + +//--------------------------------------------------------- +// getSoundingPitch +//--------------------------------------------------------- + +int PitchAdjuster::getSoundingPitch() const +{ + if (_harmonicType == HarmonicType::Natural) { + return getTouchingPitch() + getAlter(); + } + return getBasePitch() + getAlter(); +} + +//--------------------------------------------------------- +// getTouchingFret +//--------------------------------------------------------- + +double PitchAdjuster::getTouchingFret() const +{ + return (double)_touchingFret / 10.0; +} + +//--------------------------------------------------------- +// toStringTouchingFret +// e.g "12", "7", "3.2" +//--------------------------------------------------------- + +QString PitchAdjuster::toStringTouchingFret() const +{ + return QString::number(getTouchingFret(), 'f', (_touchingFret % 10) == 0 ? 0 : 1); +} + +//--------------------------------------------------------- +// getAlter +//--------------------------------------------------------- + +int PitchAdjuster::getAlter() const +{ + return getHarmonicAlter() + getPitchAlter(); +} + +//--------------------------------------------------------- +// getPitchAlter +//--------------------------------------------------------- + +int PitchAdjuster::getPitchAlter() const +{ + return (_alter & 0x80) ? 128 - (int)_alter : int(_alter); +} + +//--------------------------------------------------------- +// getHarmonicAlter +//--------------------------------------------------------- + +int PitchAdjuster::getHarmonicAlter() const +{ + int alt = 0; + if (_harmonicType == HarmonicType::Natural) { + alt = computeHarmonicFret() - int(getTouchingFret()); + } else if (_harmonicType != HarmonicType::None) { + alt = computeHarmonicFret(); + } + return alt; +} + +//--------------------------------------------------------- +// setBasePitch +//--------------------------------------------------------- + +void PitchAdjuster::setBasePitch(int pitch) +{ + if (pitch < 0 || pitch > 127) { + pitch = 60; // middle C + } + _basePitch = (unsigned char)pitch; +} + +//--------------------------------------------------------- +// setTouchingFret +//--------------------------------------------------------- + +void PitchAdjuster::setTouchingFret(int fret) +{ + if (fret < 1 || fret > 24) { + fret = 12; + } + _touchingFret = (unsigned char)(fret * 10); +} + +//--------------------------------------------------------- +// setTouchingFret +//--------------------------------------------------------- + +void PitchAdjuster::setTouchingFret(double fret) +{ + if (fret < 1.0 || fret > 24.0) { + fret = 12.0; + } + _touchingFret = (unsigned char)(fret * 10.0 + 0.5); +} + +//--------------------------------------------------------- +// setPitchAlter +//--------------------------------------------------------- + +void PitchAdjuster::setPitchAlter(int alter) +{ + if (alter < -100 || alter > 100) { + alter = 0; + } + _alter = (unsigned char)((alter < 0) ? 128 - alter : alter); +} + +//--------------------------------------------------------- +// computeHarmonicFret +// - returns the fret number with the same pitch as the harmonic +// +// Fraction | Alter | Frets +// -------------------------------------------------------- +// 1/2 | 12 | 12 +// 1/3 (P5) | 12+7 | 7, 19 +// 1/4 | 24 | 5, 24 +// +// The following are rare and hard to sound cleanly +// +// 1/5 (M3) | 24+4 | 3.9, 8.9, 15.9 +// 1/6 (P5) | 24+7 | 3.2 +// 1/7 (m7) | 24+10 | 2.7, 5.8, 9.7, 14.7, 21.7 +// 1/8 | 36 | 2.3, 8.1, 17 +// 1/9 (M2) | 36+2 | 2, 4.4, 10.2, 14 +// 1/10 (M3) | 36+4 | 1.8, 6.2, 20.8 +//--------------------------------------------------------- + +int PitchAdjuster::computeHarmonicFret() const +{ + switch ((int)_touchingFret) { + // 1/2 + case 120: // 12th fret + return 12; + + // 1/3 + case 70: // 7th fret + case 190: // 19th fret + return 19; + + // 1/4 + case 50: // 5th fret + case 240: // 24th fret + return 24; + + // 1/5 + case 39: + case 40: + case 89: + case 90: + case 159: + case 160: + return 28; + + // 1/6 + case 32: + return 31; + + // 1/7 + case 27: + case 58: + case 97: + case 147: + case 217: + return 34; + + // 1/8 + case 23: + case 24: + case 81: + case 170: + return 36; + + // 1/9 + case 20: + case 44: + case 102: + case 140: + return 38; + + // 1/10 + case 18: + case 62: + case 208: + return 40; + } + return 0; +} + +//--------------------------------------------------------- +// write +// +// +//--------------------------------------------------------- + +static const char* _xTag = "pitchAdjust"; +static const char* _xType = "type"; +static const char* _xBase = "base"; +static const char* _xTouch = "touch"; +static const char* _xAlter = "alter"; + +void PitchAdjuster::write(XmlWriter& xml) const +{ + if (_alter != 0 || _harmonicType != HarmonicType::None) { + QString tag(_xTag); + if (_harmonicType != HarmonicType::None) { + tag += QString(" %1=\"%2\" %3=\"%4\" %5=\"%6\"") + .arg(_xType).arg(int(_harmonicType)) + .arg(_xBase).arg(getBasePitch()) + .arg(_xTouch).arg(toStringTouchingFret()); + } + if (_alter != 0) { + tag += QString(" %1=\"%2\"").arg(_xAlter).arg(getPitchAlter()); + } + xml.tagE(tag); + } +} + +//--------------------------------------------------------- +// read +//--------------------------------------------------------- + +void PitchAdjuster::read(XmlReader& e) +{ + clear(); + if (e.hasAttribute(_xType)) { + _harmonicType = HarmonicType(e.intAttribute(_xType)); + } + if (e.hasAttribute(_xBase)) { + setBasePitch(e.intAttribute(_xBase)); + } + if (e.hasAttribute(_xTouch)) { + setTouchingFret(e.doubleAttribute(_xTouch)); + } + if (e.hasAttribute(_xAlter)) { + setPitchAlter(e.intAttribute(_xAlter)); + } + e.skipCurrentElement(); +} + +// debugging stuff +#if 0 +//--------------------------------------------------------- +// toString +// - used for debugging +// natural: "(12)", "(2.3)" +// artificial: "(3)15" +// pitch: "[2]+" or "[2]-" +//--------------------------------------------------------- + +QString PitchAdjuster::toString(int baseFret) const +{ + QString s; + switch (_harmonicType) { + case HarmonicType::Natural: + s = QString("(%1)").arg(toStringTouchingFret()); + break; + case HarmonicType::Artificial: + case HarmonicType::Pinch: + case HarmonicType::Tap: + case HarmonicType::Touch: + s = QString("(%1)%2").arg(baseFret).arg(baseFret + getHarmonicAlter()); + break; + case HarmonicType::None: + break; + } + int pa = getPitchAlter(); + if (pa > 0) { + s += QString("[%1]+").arg(pa); + } else if (pa < 0) { + s += QString("[%1]-").arg(-pa); + } + return s; +} + +//--------------------------------------------------------- +// test +//--------------------------------------------------------- + +void PitchAdjuster::test() const +{ + QString s; + int alter; + int base; + int sound; + double touch; + int bugs = 0; + int OPENG = 55; // G3 + + PitchAdjuster pa; + + // 12th fret + pa.clear(); + pa.setNaturalHarmonic(OPENG, OPENG + 12); + s = pa.toString(); + alter = pa.getAlter(); + base = pa.getBasePitch(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(12)" || alter != 0 || base != OPENG || sound != OPENG + 12 || int(touch) != OPENG + 12) { + bugs++; + } + + // 12th fret + pa.clear(); + pa.setNaturalHarmonic(OPENG, QString("67")); + s = pa.toString(); + alter = pa.getAlter(); + base = pa.getBasePitch(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(12)" || alter != 0 || base != OPENG || sound != OPENG + 12 || int(touch) != OPENG + 12) { + bugs++; + } + + // 7th fret + pa.clear(); + pa.setNaturalHarmonic(OPENG, OPENG + 7); + s = pa.toString(); + alter = pa.getAlter(); + base = pa.getBasePitch(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(7)" || alter != 12 || base != OPENG || sound != OPENG + 19 || int(touch) != OPENG + 7) { + bugs++; + } + + // 19th fret (same as 7th) + pa.clear(); + pa.setNaturalHarmonic(OPENG, OPENG + 19); + s = pa.toString(); + base = pa.getBasePitch(); + alter = pa.getAlter(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(19)" || alter != 0 || base != OPENG || sound != OPENG + 19 || int(touch) != OPENG + 19) { + bugs++; + } + + // 5th fret + pa.clear(); + pa.setNaturalHarmonic(OPENG, OPENG + 5); + s = pa.toString(); + alter = pa.getAlter(); + base = pa.getBasePitch(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(5)" || alter != 19 || base != OPENG || sound != OPENG + 24 || int(touch) != OPENG + 5) { + bugs++; + } + + // 24th fret (same as 5th) + pa.clear(); + pa.setNaturalHarmonic(OPENG, OPENG + 24); + s = pa.toString(); + alter = pa.getAlter(); + base = pa.getBasePitch(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(24)" || alter != 0 || base != OPENG || sound != OPENG + 24 || int(touch) != OPENG + 24) { + bugs++; + } + + // no harmonic, 2 semitones up + pa.clear(); + pa.setPitchAlter(2); + s = pa.toString(); + alter = pa.getAlter(); + if (s != "[2]+" || alter != 2) { + bugs++; + } + + // no harmonic, 2 semitones down + pa.clear(); + pa.setPitchAlter(-2); + s = pa.toString(); + alter = pa.getAlter(); + if (s != "[2]-" || alter != -2) { + bugs++; + } + + // 12th fret harmonic + 3 + pa.clear(); + pa.setArtificialHarmonic(OPENG + 3, OPENG + 15); + s = pa.toString(3); + alter = pa.getAlter(); + base = pa.getBasePitch(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(3)15" || alter != 12 || base != OPENG + 3 || sound != OPENG + 12 + 3 || int(touch) != OPENG + 15) { + bugs++; + } + + // 2.7 fret + pa.clear(); + pa.setNaturalHarmonic(OPENG, (double)OPENG + 2.7); + s = pa.toString(); + alter = pa.getAlter(); + base = pa.getBasePitch(); + sound = pa.getSoundingPitch(); + touch = pa.getTouchingPitch(); + if (s != "(2.7)" || alter != 32 || base != OPENG || sound != OPENG + 32 + 2 || int(touch) != OPENG + 2) { + bugs++; + } + + if (bugs > 0) { + qDebug() << "PitchAdjuster::test() failed.\n"; + } +} + +#endif +} // namespace diff --git a/src/libmscore/pitchadjuster.h b/src/libmscore/pitchadjuster.h new file mode 100644 index 0000000000000..ca19257fc7301 --- /dev/null +++ b/src/libmscore/pitchadjuster.h @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef __PITCHADJUSTER_H__ +#define __PITCHADJUSTER_H__ + +namespace Ms { +class XmlReader; +class XmlWriter; + +enum class HarmonicType : unsigned char { + None, Natural, Artificial, Pinch, Tap, Touch +}; + +class PitchAdjuster +{ + HarmonicType _harmonicType; + unsigned char _basePitch; + unsigned char _touchingFret; + unsigned char _alter; +public: + PitchAdjuster(); + void clear(); + + HarmonicType getHarmonicType() const; + int getBasePitch() const; + int getTouchingPitch() const; + int getSoundingPitch() const; + int getAlter() const; + + bool setNaturalHarmonic(int, int); + bool setNaturalHarmonic(int, double); + bool setNaturalHarmonic(int, const QString&); + bool setArtificialHarmonic(int, int, HarmonicType t = HarmonicType::Artificial); + + void setBasePitch(int); + void setTouchingFret(int); + void setTouchingFret(double); + void setPitchAlter(int); + + void write(XmlWriter&) const; + void read(XmlReader&); +protected: + int computeHarmonicFret() const; + double getTouchingFret() const; + QString toStringTouchingFret() const; + int getHarmonicAlter() const; + int getPitchAlter() const; + +#if 0 + QString toString(int baseFret = 0) const; + void test() const; +#endif +}; +} // namespace +#endif