Line data Source code
1 : /*
2 : * SPDX-License-Identifier: GPL-3.0-only
3 : * MuseScore-Studio-CLA-applies
4 : *
5 : * MuseScore Studio
6 : * Music Composition & Notation
7 : *
8 : * Copyright (C) 2025 MuseScore Limited and others
9 : *
10 : * This program is free software: you can redistribute it and/or modify
11 : * it under the terms of the GNU General Public License version 3 as
12 : * published by the Free Software Foundation.
13 : *
14 : * This program is distributed in the hope that it will be useful,
15 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 : * GNU General Public License for more details.
18 : *
19 : * You should have received a copy of the GNU General Public License
20 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
21 : */
22 :
23 : #include "playbackeventsrenderer.h"
24 :
25 : #include "log.h"
26 :
27 : #include "dom/chord.h"
28 : #include "dom/harmony.h"
29 : #include "dom/measure.h"
30 : #include "dom/note.h"
31 : #include "dom/sig.h"
32 : #include "dom/staff.h"
33 : #include "dom/utils.h"
34 :
35 : #include "types/bps.h"
36 :
37 : #include "utils/arrangementutils.h"
38 :
39 : #include "metaparsers/chordarticulationsparser.h"
40 : #include "metaparsers/notearticulationsparser.h"
41 :
42 : #include "renderers/chordarticulationsrenderer.h"
43 :
44 : #include "filters/chordfilter.h"
45 :
46 : using namespace mu::engraving;
47 : using namespace muse;
48 : using namespace muse::mpe;
49 :
50 631 : static ArticulationMap makeStandardArticulationMap(const ArticulationsProfilePtr profile, timestamp_t timestamp, duration_t duration)
51 : {
52 631 : IF_ASSERT_FAILED(profile) {
53 0 : return {};
54 : }
55 :
56 : ArticulationMeta meta(ArticulationType::Standard,
57 : profile->pattern(ArticulationType::Standard),
58 : timestamp,
59 : duration,
60 : 0,
61 631 : 0);
62 :
63 631 : ArticulationMap articulations;
64 631 : articulations.emplace(ArticulationType::Standard, mpe::ArticulationAppliedData(std::move(meta), 0, mpe::HUNDRED_PERCENT));
65 631 : articulations.preCalculateAverageData();
66 :
67 631 : return articulations;
68 631 : }
69 :
70 627 : static muse::mpe::NoteEvent buildMetronomeEvent(const TimeSigFrac& timeSig, const double bps,
71 : const BeatType beatType, const muse::mpe::timestamp_t actualTimestamp,
72 : const muse::mpe::ArticulationsProfilePtr profile)
73 : {
74 627 : int ticksPerBeat = timeSig.ticks() / timeSig.numerator();
75 627 : duration_t duration = durationFromTempoAndTicks(bps, ticksPerBeat);
76 :
77 : pitch_level_t eventPitchLevel = beatType == BeatType::DOWNBEAT
78 627 : ? pitchLevel(PitchClass::E, 5) // high wood block
79 471 : : pitchLevel(PitchClass::F, 5); // low wood block
80 :
81 627 : const ArticulationMap articulations = makeStandardArticulationMap(profile, actualTimestamp, duration);
82 :
83 : return mpe::NoteEvent(actualTimestamp,
84 : duration,
85 : 0,
86 : 0,
87 : eventPitchLevel,
88 : dynamicLevelFromType(mpe::DynamicType::mf),
89 : articulations,
90 1254 : bps);
91 1254 : }
92 :
93 1071 : void PlaybackEventsRenderer::render(const EngravingItem* item, const int tickPositionOffset,
94 : const ArticulationsProfilePtr profile, const PlaybackContextPtr playbackCtx,
95 : PlaybackEventsMap& result) const
96 : {
97 1071 : IF_ASSERT_FAILED(item->isChord()) {
98 0 : return;
99 : }
100 :
101 1071 : renderNoteEvents(toChord(item), tickPositionOffset, profile, playbackCtx, result);
102 : }
103 :
104 2 : void PlaybackEventsRenderer::render(const EngravingItem* item, const mpe::timestamp_t actualTimestamp,
105 : const mpe::duration_t actualDuration, const mpe::dynamic_level_t actualDynamicLevel,
106 : const PlaybackContextPtr playbackCtx, const ArticulationsProfilePtr profile,
107 : PlaybackEventsMap& result) const
108 : {
109 2 : if (item->isChord()) {
110 1 : const Chord* chord = toChord(item);
111 1 : mpe::PlaybackEventList& events = result[actualTimestamp];
112 :
113 4 : for (const Note* note : chord->notes()) {
114 3 : renderFixedNoteEvent(note, actualTimestamp, actualDuration,
115 : actualDynamicLevel, playbackCtx, profile, events);
116 : }
117 1 : } else if (item->isNote()) {
118 1 : renderFixedNoteEvent(toNote(item), actualTimestamp, actualDuration,
119 1 : actualDynamicLevel, playbackCtx, profile, result[actualTimestamp]);
120 : } else {
121 0 : UNREACHABLE;
122 : }
123 2 : }
124 :
125 0 : void PlaybackEventsRenderer::renderChordSymbol(const Harmony* chordSymbol,
126 : const int ticksPositionOffset,
127 : const mpe::ArticulationsProfilePtr profile,
128 : const PlaybackContextPtr playbackCtx,
129 : mpe::PlaybackEventsMap& result) const
130 : {
131 0 : if (!chordSymbol->isRealizable()) {
132 0 : return;
133 : }
134 :
135 0 : const Staff* staff = chordSymbol->staff();
136 0 : IF_ASSERT_FAILED(staff) {
137 0 : return;
138 : }
139 :
140 0 : const RealizedHarmony& realized = chordSymbol->getRealizedHarmony();
141 0 : const RealizedHarmony::PitchMap& notes = realized.notes();
142 :
143 0 : const Score* score = chordSymbol->score();
144 0 : int positionTick = chordSymbol->tick().ticks();
145 0 : int positionTickWithOffset = positionTick + ticksPositionOffset;
146 :
147 0 : timestamp_t eventTimestamp = timestampFromTicks(score, positionTickWithOffset);
148 0 : PlaybackEventList& events = result[eventTimestamp];
149 :
150 0 : int durationTicks = realized.getActualDuration(positionTickWithOffset).ticks();
151 0 : duration_t duration = timestampFromTicks(score, positionTickWithOffset + durationTicks) - eventTimestamp;
152 :
153 0 : voice_layer_idx_t voiceIdx = static_cast<voice_layer_idx_t>(chordSymbol->voice());
154 0 : staff_layer_idx_t staffIdx = static_cast<staff_layer_idx_t>(chordSymbol->staffIdx());
155 0 : Key key = staff->key(chordSymbol->tick());
156 :
157 0 : ArticulationMap articulations = makeStandardArticulationMap(profile, eventTimestamp, duration);
158 :
159 0 : double bps = score->multipliedTempoAtUtick(positionTickWithOffset).val;
160 :
161 0 : for (auto it = notes.cbegin(); it != notes.cend(); ++it) {
162 0 : int pitch = it->first;
163 0 : int tpc = pitch2tpc(pitch, key, Prefer::NEAREST);
164 0 : int octave = playingOctave(pitch, tpc);
165 0 : pitch_level_t pitchLevel = notePitchLevel(tpc, octave);
166 :
167 0 : events.emplace_back(mpe::NoteEvent(eventTimestamp,
168 : duration,
169 : voiceIdx,
170 : staffIdx,
171 : pitchLevel,
172 : playbackCtx->appliableDynamicLevel(chordSymbol->track(), positionTickWithOffset),
173 : articulations,
174 0 : bps));
175 : }
176 0 : }
177 :
178 0 : void PlaybackEventsRenderer::renderChordSymbol(const Harmony* chordSymbol, const mpe::timestamp_t actualTimestamp,
179 : const mpe::duration_t actualDuration, const mpe::dynamic_level_t actualDynamicLevel,
180 : const ArticulationsProfilePtr profile, mpe::PlaybackEventsMap& result) const
181 : {
182 0 : if (!chordSymbol->isRealizable()) {
183 0 : return;
184 : }
185 :
186 0 : const Staff* staff = chordSymbol->staff();
187 0 : IF_ASSERT_FAILED(staff) {
188 0 : return;
189 : }
190 :
191 0 : const RealizedHarmony& realized = chordSymbol->getRealizedHarmony();
192 0 : const RealizedHarmony::PitchMap& notes = realized.notes();
193 :
194 0 : PlaybackEventList& events = result[actualTimestamp];
195 :
196 0 : voice_layer_idx_t voiceIdx = static_cast<voice_layer_idx_t>(chordSymbol->voice());
197 0 : staff_layer_idx_t staffIdx = static_cast<staff_layer_idx_t>(chordSymbol->staffIdx());
198 0 : Key key = staff->key(chordSymbol->tick());
199 :
200 0 : ArticulationMap articulations = makeStandardArticulationMap(profile, actualTimestamp, actualDuration);
201 :
202 0 : for (auto it = notes.cbegin(); it != notes.cend(); ++it) {
203 0 : int pitch = it->first;
204 0 : int tpc = pitch2tpc(pitch, key, Prefer::NEAREST);
205 0 : int octave = playingOctave(pitch, tpc);
206 0 : pitch_level_t pitchLevel = notePitchLevel(tpc, octave);
207 :
208 0 : events.emplace_back(mpe::NoteEvent(actualTimestamp,
209 : actualDuration,
210 : voiceIdx,
211 : staffIdx,
212 : pitchLevel,
213 : actualDynamicLevel,
214 : articulations,
215 0 : Constants::DEFAULT_TEMPO.val));
216 : }
217 0 : }
218 :
219 148 : void PlaybackEventsRenderer::renderMetronome(const Score* score, const Measure* measure, const int ticksPositionOffset,
220 : const muse::mpe::ArticulationsProfilePtr profile, mpe::PlaybackEventsMap& result) const
221 : {
222 148 : IF_ASSERT_FAILED(score) {
223 0 : return;
224 : }
225 :
226 148 : int measureStartTick = measure->tick().ticks();
227 148 : int measureEndTick = measure->endTick().ticks();
228 :
229 148 : TimeSigFrac timeSignatureFraction = score->sigmap()->timesig(measureStartTick).nominal();
230 :
231 148 : const BeatsPerSecond measureBps = score->multipliedTempoAtUtick(measureStartTick + ticksPositionOffset);
232 148 : const int step = timeSignatureFraction.isBeatedCompound(measureBps.val)
233 148 : ? timeSignatureFraction.beatTicks() : timeSignatureFraction.dUnitTicks();
234 :
235 148 : int startTick = measureStartTick;
236 148 : int rtick = 0;
237 :
238 148 : if (measure->isAnacrusis()) {
239 0 : int remainingTicks = measure->ticks().ticks() % step;
240 0 : startTick += remainingTicks;
241 0 : rtick = remainingTicks + timeSignatureFraction.ticksPerMeasure() - measure->ticks().ticks();
242 : }
243 :
244 : // Tempo can change mid-measure (automation point or ramp), so re-read it for every click
245 : // rather than reusing the tempo from the start of the measure; the click grid itself stays fixed
246 750 : for (int tick = startTick; tick < measureEndTick; tick += step, rtick += step) {
247 602 : const BeatsPerSecond bps = score->multipliedTempoAtUtick(tick + ticksPositionOffset);
248 :
249 602 : timestamp_t eventTimestamp = timestampFromTicks(score, tick + ticksPositionOffset);
250 602 : BeatType beatType = timeSignatureFraction.rtick2beatType(rtick);
251 602 : mpe::NoteEvent event = buildMetronomeEvent(timeSignatureFraction, bps.val, beatType, eventTimestamp, profile);
252 :
253 602 : result[eventTimestamp].emplace_back(std::move(event));
254 602 : }
255 : }
256 :
257 0 : void PlaybackEventsRenderer::renderMetronome(const Score* score, const int tick, const mpe::timestamp_t actualTimestamp,
258 : const muse::mpe::ArticulationsProfilePtr profile, mpe::PlaybackEventsMap& result) const
259 : {
260 0 : IF_ASSERT_FAILED(score) {
261 0 : return;
262 : }
263 :
264 0 : TimeSigFrac timeSignatureFraction = score->sigmap()->timesig(tick).timesig();
265 0 : BeatsPerSecond bps = score->multipliedTempo(Fraction::fromTicks(tick));
266 0 : BeatType beatType = score->tick2beatType(Fraction::fromTicks(tick));
267 0 : mpe::NoteEvent event = buildMetronomeEvent(timeSignatureFraction, bps.val, beatType, actualTimestamp, profile);
268 :
269 0 : result[actualTimestamp].emplace_back(std::move(event));
270 0 : }
271 :
272 5 : void PlaybackEventsRenderer::renderCountIn(const Score* score, const int startTick, const muse::mpe::timestamp_t actualTimestamp,
273 : const muse::mpe::ArticulationsProfilePtr profile,
274 : muse::mpe::PlaybackEventsMap& result, muse::mpe::duration_t& countInDuration) const
275 : {
276 5 : const Measure* measure = score->tick2measure(Fraction::fromTicks(startTick));
277 5 : if (!measure) {
278 0 : return;
279 : }
280 :
281 5 : int measureStartTick = measure->tick().ticks();
282 5 : TimeSigFrac timeSignatureFraction = score->sigmap()->timesig(measureStartTick).nominal();
283 5 : BeatsPerSecond bps = score->multipliedTempo(Fraction::fromTicks(measureStartTick));
284 5 : int ticksPerMeasure = timeSignatureFraction.ticksPerMeasure();
285 :
286 5 : int step = timeSignatureFraction.isBeatedCompound(bps.val)
287 5 : ? timeSignatureFraction.beatTicks() : timeSignatureFraction.dUnitTicks();
288 :
289 5 : duration_t stepDuration = durationFromTempoAndTicks(bps.val, step);
290 :
291 : // Add extra clicks if...
292 5 : int endTick = ticksPerMeasure + (startTick - measureStartTick); // ... not starting playback at beginning of measure
293 5 : int remainingTicks = 0;
294 :
295 5 : if (measure->isAnacrusis()) { // ... measure is incomplete (anacrusis)
296 1 : int measureTicks = measure->ticks().ticks();
297 1 : endTick += ticksPerMeasure - measureTicks;
298 1 : remainingTicks = measureTicks % step;
299 : }
300 :
301 5 : MeasureBeat measureBeat = findBeat(score, startTick);
302 5 : int closestMainBeatTick = score->sigmap()->bar2tick(measureBeat.measureIndex, std::ceil(measureBeat.beat));
303 5 : remainingTicks += closestMainBeatTick - startTick;
304 :
305 5 : timestamp_t eventTimestamp = actualTimestamp;
306 :
307 30 : for (int tick = 0; tick < endTick; tick += step) {
308 25 : int rtick = tick % ticksPerMeasure;
309 25 : BeatType beatType = timeSignatureFraction.rtick2beatType(rtick);
310 25 : mpe::NoteEvent event = buildMetronomeEvent(timeSignatureFraction, bps.val, beatType, eventTimestamp, profile);
311 :
312 25 : result[eventTimestamp].emplace_back(std::move(event));
313 25 : eventTimestamp += stepDuration;
314 25 : }
315 :
316 5 : countInDuration = eventTimestamp - actualTimestamp;
317 5 : if (remainingTicks > 0) {
318 1 : countInDuration -= durationFromTempoAndTicks(bps.val, remainingTicks);
319 : }
320 : }
321 :
322 1071 : void PlaybackEventsRenderer::renderNoteEvents(const Chord* chord, const int tickPositionOffset,
323 : const mpe::ArticulationsProfilePtr profile, const PlaybackContextPtr playbackCtx,
324 : PlaybackEventsMap& result) const
325 : {
326 1071 : IF_ASSERT_FAILED(chord) {
327 0 : return;
328 : }
329 :
330 1071 : RenderingContext ctx = engraving::buildRenderingCtx(chord, tickPositionOffset, profile, playbackCtx);
331 :
332 1071 : if (!ChordFilter::isItemPlayable(chord, ctx)) {
333 0 : return;
334 : }
335 :
336 1071 : ChordArticulationsParser::buildChordArticulationMap(chord, ctx, ctx.commonArticulations);
337 :
338 1071 : PlaybackEventList newEvents;
339 1071 : ChordArticulationsRenderer::render(chord, ArticulationType::Last, ctx, newEvents);
340 :
341 1071 : if (!newEvents.empty()) {
342 1047 : PlaybackEventList& list = result[ctx.nominalTimestamp];
343 1047 : list.insert(list.end(), std::make_move_iterator(newEvents.begin()), std::make_move_iterator(newEvents.end()));
344 : }
345 1071 : }
346 :
347 4 : void PlaybackEventsRenderer::renderFixedNoteEvent(const Note* note, const mpe::timestamp_t actualTimestamp,
348 : const mpe::duration_t actualDuration,
349 : const mpe::dynamic_level_t actualDynamicLevel,
350 : const PlaybackContextPtr playbackCtx,
351 : const mpe::ArticulationsProfilePtr profile, mpe::PlaybackEventList& result) const
352 : {
353 4 : static const ArticulationMap articulations;
354 :
355 4 : const Score* score = note->score();
356 4 : const int durationTicks = ticksFromTempoAndDuration(Constants::DEFAULT_TEMPO.val, actualDuration);
357 4 : const int tick = note->tick().ticks();
358 4 : const int utick = score ? score->repeatList().tick2utick(tick) : tick;
359 4 : const int tickOffset = utick - tick;
360 :
361 4 : RenderingContext ctx{ actualTimestamp,
362 : actualDuration,
363 : actualDynamicLevel,
364 : tick, /*nominalPositionStartTick*/
365 : durationTicks, /*nominalPositionEndTick*/
366 : durationTicks, /*nominalDurationTicks*/
367 : tickOffset,
368 : Constants::DEFAULT_TEMPO,
369 : TimeSigMap::DEFAULT_TIME_SIGNATURE,
370 : articulations,
371 : score,
372 : profile,
373 4 : playbackCtx };
374 :
375 4 : NoteArticulationsParser::parsePlayingTechnique(ctx, note->track(), ctx.commonArticulations, false /*sustainAllowed*/);
376 4 : NoteArticulationsParser::parseGhostNote(note, ctx, ctx.commonArticulations);
377 4 : NoteArticulationsParser::parseNoteHead(note, ctx, ctx.commonArticulations);
378 4 : NoteArticulationsParser::parseSymbols(note, ctx, ctx.commonArticulations);
379 :
380 4 : if (ctx.commonArticulations.empty()) {
381 4 : ctx.commonArticulations = makeStandardArticulationMap(profile, actualTimestamp, actualDuration);
382 : } else {
383 0 : ctx.commonArticulations.preCalculateAverageData();
384 : }
385 :
386 4 : NominalNoteCtx noteCtx(note, ctx);
387 4 : result.emplace_back(buildNoteEvent(noteCtx));
388 8 : }
|