• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / score / 30325599898

28 Jul 2026 03:19AM UTC coverage: 15.319% (-0.002%) from 15.321%
30325599898

Pull #2158

github

web-flow
Merge c4b3e196e into f1cbe121b
Pull Request #2158: patternist: fix note-offs that never reach the output

1 of 41 new or added lines in 2 files covered. (2.44%)

4 existing lines in 1 file now uncovered.

30450 of 198773 relevant lines covered (15.32%)

997.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/plugins/score-plugin-midi/Patternist/PatternExecutor.cpp
1
// This is an open source non-commercial project. Dear PVS-Studio, please check
2
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
#include "PatternExecutor.hpp"
4

5
#include <score/document/DocumentContext.hpp>
6
#include <score/tools/Bind.hpp>
7

8
#include <ossia/dataflow/port.hpp>
9

10
#include <QTimer>
11

12
#include <Patternist/PatternModel.hpp>
13
#include <libremidi/ump_events.hpp>
14

15
#include <algorithm>
16
#include <cmath>
17

18
namespace Patternist
19
{
20
// The model stores channels as 1-16, the wire format as 0-15
NEW
21
static uint8_t to_midi_channel(int c) noexcept
×
22
{
NEW
23
  return std::clamp(c - 1, 0, 15);
×
24
}
25

26
class pattern_node : public ossia::nonowning_graph_node
27
{
28
public:
29
  ossia::midi_outlet out;
30
  ossia::value_outlet accent_out;
31
  ossia::value_outlet slide_out;
32
  Pattern pattern;
33
  ossia::flat_set<uint8_t> in_flight;
34

35
  int current = 0;
×
36
  int last = -1;
×
37
  uint8_t channel{1};
×
38
  // Channel the notes currently in flight were started on: a channel change
39
  // must not leave them stranded on the previous one.
NEW
40
  uint8_t in_flight_channel{1};
×
NEW
41
  bool release_pending{};
×
42

43
  pattern_node()
×
44
  {
×
45
    in_flight.reserve(32);
×
46
    m_outlets.push_back(&out);
×
47
    m_outlets.push_back(&accent_out);
×
48
    m_outlets.push_back(&slide_out);
×
49
  }
×
50

51
  std::string label() const noexcept override { return "pattern_node"; }
×
52

53
  bool legato(int note) const noexcept
×
54
  {
55
    for(const Lane& lane : pattern.lanes)
×
56
      if(lane.note == note && ossia::valid_index(current, lane.pattern))
×
57
        return lane.pattern[current] == Note::Legato;
×
58
    return false;
×
59
  }
×
60

NEW
61
  void release_all(int64_t timestamp) noexcept
×
62
  {
NEW
63
    auto& mess = out.target<ossia::midi_port>()->messages;
×
NEW
64
    for(uint8_t note : in_flight)
×
65
    {
NEW
66
      mess.push_back(libremidi::from_midi1::note_off(in_flight_channel, note, 0));
×
NEW
67
      mess.back().timestamp = timestamp;
×
68
    }
NEW
69
    in_flight.clear();
×
NEW
70
  }
×
71

NEW
72
  void set_channel(uint8_t c) noexcept
×
73
  {
NEW
74
    if(c == channel)
×
NEW
75
      return;
×
NEW
76
    channel = c;
×
77
    // Released from run(), where a timestamp inside the tick is available
NEW
78
    release_pending = true;
×
NEW
79
  }
×
80

UNCOV
81
  void run(const ossia::token_request& tk, ossia::exec_state_facade st) noexcept override
×
82
  {
83
    using namespace ossia;
84
    if(tk.model_read_duration() == 0_tv)
×
85
      return;
×
86

NEW
87
    const double samplesratio = st.modelToSamples();
×
NEW
88
    const double speed = tk.speed != 0. ? tk.speed : 1.;
×
NEW
89
    const int64_t tick_start = std::floor(tk.offset.impl * samplesratio / speed);
×
90

UNCOV
91
    if(tk.end_discontinuous)
×
92
    {
93
      // Stamping at 0 puts the message before the beginning of the tick as soon
94
      // as the interval does not start on a buffer boundary; every consumer
95
      // that windows on [tick_start; tick_start + frames[ then drops it, and
96
      // since in_flight is cleared here the note is never released again.
NEW
97
      release_all(tick_start);
×
UNCOV
98
      return;
×
99
    }
100

NEW
101
    if(release_pending)
×
102
    {
NEW
103
      release_all(tick_start);
×
NEW
104
      in_flight_channel = channel;
×
NEW
105
      release_pending = false;
×
NEW
106
    }
×
107

NEW
108
    if(pattern.length <= 0)
×
NEW
109
      return;
×
110

111
    // TODO on bar change, reset to start of pattern?
112
    if(auto d = tk.get_quantification_date(pattern.division))
×
113
    {
NEW
114
      const int64_t date
×
NEW
115
          = std::floor((*d - tk.prev_date + tk.offset).impl * samplesratio / speed);
×
116

117
      last = current;
×
118
      auto& mess = out.target<ossia::midi_port>()->messages;
×
119

120
      for(auto it = in_flight.begin(); it != in_flight.end();)
×
121
      {
122
        uint8_t note = *it;
×
123
        if(!legato(note))
×
124
        {
NEW
125
          mess.push_back(libremidi::from_midi1::note_off(in_flight_channel, note, 0));
×
126
          mess.back().timestamp = date;
×
127
          it = in_flight.erase(it);
×
128
        }
×
129
        else
130
        {
131
          ++it;
×
132
        }
133
      }
134

NEW
135
      in_flight_channel = channel;
×
136

UNCOV
137
      for(Lane& lane : pattern.lanes)
×
138
      {
139
        if(lane.note <= 127 && ossia::valid_index(current, lane.pattern))
×
140
        {
141
          switch(lane.pattern[current])
×
142
          {
143
            case Note::Note:
144
              mess.push_back(libremidi::from_midi1::note_on(channel, lane.note, 100));
×
145
              mess.back().timestamp = date;
×
146
              in_flight.insert(lane.note);
×
147
              break;
×
148
            case Note::Legato:
149
              if(!in_flight.contains(lane.note))
×
150
              {
151
                mess.push_back(libremidi::from_midi1::note_on(channel, lane.note, 100));
×
152
                mess.back().timestamp = date;
×
153
                in_flight.insert(lane.note);
×
154
              }
×
155
              break;
×
156
            case Note::Rest:
157
              if(in_flight.contains(lane.note))
×
158
              {
NEW
159
                mess.push_back(
×
NEW
160
                    libremidi::from_midi1::note_off(in_flight_channel, lane.note, 0));
×
161
                mess.back().timestamp = date;
×
162
                in_flight.erase(lane.note);
×
163
              }
×
164
              break;
×
165
          }
166
        }
×
167
      }
168

169
      for(Lane& lane : pattern.lanes)
×
170
      {
171
        if(ossia::valid_index(current, lane.pattern))
×
172
        {
173
          if(lane.note == 255)
×
174
          {
175
            if(lane.pattern[current] != Note::Rest)
×
176
              accent_out->write_value(1., date);
×
177
            else
178
              accent_out->write_value(0., date);
×
179
          }
×
180
          else if(lane.note == 254)
×
181
          {
182
            if(lane.pattern[current] != Note::Rest)
×
183
              slide_out->write_value(1., date);
×
184
            else
185
              slide_out->write_value(0., date);
×
186
          }
×
187
        }
×
188
      }
189

190
      current = (current + 1) % pattern.length;
×
191
    }
×
192
  }
×
193

NEW
194
  void all_notes_off() noexcept override { release_all(0); }
×
195
};
196

197
Executor::Executor(
×
198
    Patternist::ProcessModel& element, const Execution::Context& ctx, QObject* parent)
199
    : ::Execution::ProcessComponent_T<Patternist::ProcessModel, ossia::node_process>{
×
200
        element, ctx, "PatternComponent", parent}
×
201
{
×
202
  auto node = ossia::make_node<pattern_node>(*ctx.execState);
×
NEW
203
  node->channel = to_midi_channel(element.channel());
×
NEW
204
  node->in_flight_channel = node->channel;
×
205
  node->pattern = element.patterns()[element.currentPattern()];
×
206
  node->current = 0;
×
207

208
  this->node = node;
×
209
  m_ossia_process = std::make_shared<ossia::node_process>(node);
×
210

NEW
211
  con(element, &Patternist::ProcessModel::channelChanged, this, [this, node](int c) {
×
NEW
212
    in_exec([node, c = to_midi_channel(c)] { node->set_channel(c); });
×
NEW
213
  });
×
214
  con(element, &Patternist::ProcessModel::currentPatternChanged, this,
×
215
      [this, node, &element](int c) {
×
216
    in_exec([node, p = element.patterns()[c]] { node->pattern = p; });
×
217
  });
×
218
  con(element, &Patternist::ProcessModel::patternsChanged, this,
×
219
      [this, node, &element]() {
×
220
    in_exec(
×
221
        [node, p = element.patterns()[element.currentPattern()]] { node->pattern = p; });
×
222
  });
×
223
  con(ctx.doc.execTimer, &QTimer::timeout, this, [&element, node] {
×
224
    int c = node->last;
×
225
    element.execPosition(c);
×
226
  });
×
227
}
×
228

229
void Executor::stop()
×
230
{
231
  ProcessComponent::stop();
×
232
  this->process().execPosition(-1);
×
233
}
×
234
Executor::~Executor() { }
×
235
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc