Skip to content

Commit

Permalink
Increase the resolution of MIDI pitch wheel events.
Browse files Browse the repository at this point in the history
Previously we were only using the upper 7 bits of the available 14 bits. This enables much smoother pitch wheel behaviour, but required some changes to avoid generating thousands of MIDI events.

Bug: #54
  • Loading branch information
cameronwhite committed Nov 3, 2024
1 parent d41823a commit 1288791
Show file tree
Hide file tree
Showing 9 changed files with 71 additions and 35 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Thanks to the following contributors who worked on this release:
- When attempting to insert a note at the end of a system, a space is now automatically inserted to allow the note to be added (#439)
- Extended the view filter menu to support filtering by a specific player without requiring a filter to be explicitly created (#301)
- Copying and pasting selections which include barlines is now supported (#416)
- Improved the smoothness of MIDI pitch wheel events (bends, slides, etc), which now use the full 14-bit resolution instead of 7-bit
- Translations
- Added Turkish translation (#406)
- Updated French translation (#406)
Expand Down
9 changes: 5 additions & 4 deletions source/audio/midioutputdevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,13 @@ bool MidiOutputDevice::setPan(int channel, uint8_t pan)
return sendMidiMessage(ControlChange + channel, PanChange, pan);
}

bool MidiOutputDevice::setPitchBend (int channel, uint8_t bend)
bool
MidiOutputDevice::setPitchBend(int channel, uint16_t amount)
{
if (bend > 127)
bend = 127;
uint8_t lower_bits, upper_bits;
Midi::splitIntoBytes(amount, lower_bits, upper_bits);

return sendMidiMessage(PitchWheel + channel, 0, bend);
return sendMidiMessage(PitchWheel + channel, lower_bits, upper_bits);
}

bool MidiOutputDevice::playNote(int channel, uint8_t pitch, uint8_t velocity)
Expand Down
2 changes: 1 addition & 1 deletion source/audio/midioutputdevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class MidiOutputDevice
bool setPatch(int channel, uint8_t patch);
bool setVolume(int channel, uint8_t volume);
bool setPan(int channel, uint8_t pan);
bool setPitchBend(int channel, uint8_t bend);
bool setPitchBend(int channel, uint16_t bend);
bool playNote(int channel, uint8_t pitch, uint8_t velocity);
bool stopNote(int channel, uint8_t pitch);
bool setVibrato(int channel, uint8_t modulation);
Expand Down
2 changes: 1 addition & 1 deletion source/audio/midiplayer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ MidiPlayer::playEvents(MidiFile &file, const SystemLocation &start_location,
SystemLocation current_location = start_location;
DurationType clock_drift(0);

std::array<uint8_t, Midi::NUM_MIDI_CHANNELS_PER_PORT> initial_pitch_wheel;
std::array<uint16_t, Midi::NUM_MIDI_CHANNELS_PER_PORT> initial_pitch_wheel;
initial_pitch_wheel.fill(Midi::DEFAULT_BEND);

for (const MidiEvent &event : events)
Expand Down
17 changes: 12 additions & 5 deletions source/midi/midievent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

#include "midievent.h"

#include <score/generalmidi.h>

#include <cassert>

enum Controller : uint8_t
Expand Down Expand Up @@ -69,10 +71,11 @@ bool MidiEvent::isPitchWheel() const
return (getStatusByte() & theStatusByteMask) == StatusByte::PitchWheel;
}

uint8_t MidiEvent::getPitchWheelValue() const
uint16_t
MidiEvent::getPitchWheelValue() const
{
assert(isPitchWheel());
return myData[2];
return Midi::combineBytes(myData[1], myData[2]);
}

bool MidiEvent::isTrackEnd() const
Expand Down Expand Up @@ -162,11 +165,15 @@ MidiEvent MidiEvent::holdPedal(int ticks, uint8_t channel, bool enabled)
SystemLocation());
}

MidiEvent MidiEvent::pitchWheel(int ticks, uint8_t channel, uint8_t amount)
MidiEvent
MidiEvent::pitchWheel(int ticks, uint8_t channel, uint16_t amount)
{
assert(amount <= Midi::MAX_BEND);

uint8_t lower_bits, upper_bits;
Midi::splitIntoBytes(amount, lower_bits, upper_bits);
return MidiEvent(
ticks,
{ static_cast<uint8_t>(StatusByte::PitchWheel + channel), 0, amount },
ticks, { static_cast<uint8_t>(StatusByte::PitchWheel + channel), lower_bits, upper_bits },
SystemLocation());
}

Expand Down
5 changes: 3 additions & 2 deletions source/midi/midievent.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class MidiEvent
bool isMetaMessage() const;
bool isTempoChange() const;
bool isPitchWheel() const;
uint8_t getPitchWheelValue() const;
uint16_t getPitchWheelValue() const;
bool isVolumeChange() const;
bool isTrackEnd() const;
Midi::Tempo getTempo() const;
Expand All @@ -80,7 +80,8 @@ class MidiEvent
static MidiEvent programChange(int ticks, uint8_t channel, uint8_t preset);
static MidiEvent modWheel(int ticks, uint8_t channel, uint8_t width);
static MidiEvent holdPedal(int ticks, uint8_t channel, bool enabled);
static MidiEvent pitchWheel(int ticks, uint8_t channel, uint8_t amount);
/// Note the pitch wheel values can be up to 14 bits (0x3fff).
static MidiEvent pitchWheel(int ticks, uint8_t channel, uint16_t amount);
static MidiEvent positionChange(int ticks, const SystemLocation &location);
static std::vector<MidiEvent> pitchWheelRange(int ticks, uint8_t channel,
uint8_t semitones);
Expand Down
44 changes: 24 additions & 20 deletions source/midi/midifile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ static constexpr int SLIDE_OUT_STEPS = 5;

/// Pitch bend amount to bend a note by a quarter tone.
static const boost::rational<int> BEND_QUARTER_TONE(
(Midi::MAX_MIDI_CHANNEL_EFFECT_LEVEL - DEFAULT_BEND), 2 * PITCH_BEND_RANGE);
(Midi::MAX_BEND - DEFAULT_BEND), 2 * PITCH_BEND_RANGE);

static const int SLIDE_BELOW_BEND = boost::rational_cast<int>(
DEFAULT_BEND - SLIDE_OUT_STEPS * 2 * BEND_QUARTER_TONE);
Expand Down Expand Up @@ -152,7 +152,7 @@ void MidiFile::load(const Score &score, const LoadOptions &options)
initializeChannel(regular_tracks[i], Midi::getPlayerChannel(i));

SystemLocation location(0, 0);
std::vector<uint8_t> active_bends;
std::vector<uint16_t> active_bends;
int system_index = -1;
int current_tick = 0;
Midi::Tempo current_tempo = Midi::BEAT_DURATION_120_BPM;
Expand Down Expand Up @@ -500,31 +500,35 @@ static int getArpeggioOffset(int ppq, Midi::Tempo current_tempo)
/// function.
struct BendEventInfo
{
BendEventInfo(int tick, uint8_t bend_amount)
BendEventInfo(int tick, uint16_t bend_amount)
: myTick(tick), myBendAmount(bend_amount)
{
}

int myTick;
uint8_t myBendAmount;
uint16_t myBendAmount;
};

static void generateGradualBend(std::vector<BendEventInfo> &bends,
int start_tick, int duration, int start_bend,
int release_bend)
static void
generateGradualBend(std::vector<BendEventInfo> &bends, int start_tick, int duration, int start_bend,
int release_bend)
{
const int num_events = std::abs(start_bend - release_bend);
if (!num_events)
const int total_bend = release_bend - start_bend;
if (total_bend == 0)
return;

const int event_duration = duration / num_events;
// At most we generate one event per tick (or bend increment), but avoid generating thousands of
// events if the note duration / bend range is large.
const int max_bend_events = std::min(duration, 128);
const int num_events = std::min(std::abs(total_bend), max_bend_events);

const boost::rational<int> bend_step(total_bend, num_events);
const boost::rational<int> event_duration(duration, num_events);

for (int i = 1; i <= num_events; ++i)
{
const int tick = start_tick + i * event_duration;
if (start_bend < release_bend)
bends.push_back(BendEventInfo(tick, start_bend + i));
else
bends.push_back(BendEventInfo(tick, start_bend - i));
const int tick = start_tick + boost::rational_cast<int>(i * event_duration);
bends.push_back(BendEventInfo(tick, start_bend + boost::rational_cast<int>(i * bend_step)));
}
}

Expand Down Expand Up @@ -563,9 +567,9 @@ computeFollowingNotesDuration(const Voice &voice, int start_idx, int num_notes,
}

static void
generateBends(std::vector<BendEventInfo> &bends, uint8_t &active_bend,
int start_tick, int note_duration, int ticks_per_beat,
const Voice &voice, const Position &pos, const Note &note)
generateBends(std::vector<BendEventInfo> &bends, uint16_t &active_bend, int start_tick,
int note_duration, int ticks_per_beat, const Voice &voice, const Position &pos,
const Note &note)
{
const Bend &bend = note.getBend();

Expand Down Expand Up @@ -658,7 +662,7 @@ generateBends(std::vector<BendEventInfo> &bends, uint8_t &active_bend,
}

static void
generateTremoloBar(std::vector<BendEventInfo> &bends, uint8_t &active_bend,
generateTremoloBar(std::vector<BendEventInfo> &bends, uint16_t &active_bend,
int start_tick, int note_duration, int ticks_per_beat,
const Voice &voice, const Position &pos)
{
Expand Down Expand Up @@ -846,7 +850,7 @@ generateVolumeSwell(const int start_tick, int duration,

int
MidiFile::addEventsForBar(std::vector<MidiEventList> &tracks,
uint8_t &active_bend, int current_tick,
uint16_t &active_bend, int current_tick,
Midi::Tempo current_tempo, const Score &score,
const System &system, int system_index,
const Staff &staff, int staff_index,
Expand Down
2 changes: 1 addition & 1 deletion source/midi/midifile.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class MidiFile
int bar_start, int bar_end);

int addEventsForBar(std::vector<MidiEventList> &tracks,
uint8_t &active_bend, int current_tick,
uint16_t &active_bend, int current_tick,
Midi::Tempo current_tempo, const Score &score,
const System &system, int system_index,
const Staff &staff, int staff_index, const Voice &voice,
Expand Down
24 changes: 23 additions & 1 deletion source/score/generalmidi.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#ifndef SCORE_GENERALMIDI_H
#define SCORE_GENERALMIDI_H

#include <cassert>
#include <cstdint>
#include <string>
#include <vector>
Expand Down Expand Up @@ -50,7 +51,9 @@ namespace Midi
inline constexpr int PERCUSSION_CHANNEL = 9;

/// Default pitch wheel value.
inline constexpr uint8_t DEFAULT_BEND = 64;
inline constexpr uint16_t DEFAULT_BEND = 0x2000;
/// Max pitch wheel value.
inline constexpr uint16_t MAX_BEND = 0x3fff;
/// Default pan value.
inline constexpr uint8_t DEFAULT_PAN = 64;

Expand Down Expand Up @@ -529,6 +532,25 @@ namespace Midi
player_idx++;
return player_idx;
}

/// Split a 14-bit value into two 7-bit values. This is used for e.g. pitch wheel values.
inline void
splitIntoBytes(uint16_t value, uint8_t &lower_bits, uint8_t &upper_bits)
{
assert(value <= MAX_BEND);
lower_bits = static_cast<uint8_t>(value & 0x7f);
upper_bits = static_cast<uint8_t>((value >> 7) & 0x7f);
}

/// Inverse of splitIntoBytes(): combine two 7-bit values into a 14-bit value.
inline uint16_t
combineBytes(uint8_t lower_bits, uint8_t upper_bits)
{
uint16_t value = static_cast<uint8_t>(upper_bits & 0x7f);
value <<= 7;
value |= static_cast<uint16_t>(lower_bits & 0x7f);
return value;
}
}

#endif

0 comments on commit 1288791

Please sign in to comment.