1 /**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 NoteTrack.cpp
6
7 Dominic Mazzoni
8
9 *******************************************************************//*!
10
11 \class NoteTrack
12 \brief A Track that is used for Midi notes. (Somewhat old code).
13
14 *//*******************************************************************/
15
16
17
18 #include "NoteTrack.h"
19
20
21
22 #include <wx/wxcrtvararg.h>
23 #include <wx/dc.h>
24 #include <wx/brush.h>
25 #include <wx/pen.h>
26 #include <wx/intl.h>
27
28 #if defined(USE_MIDI)
29 #include "../lib-src/header-substitutes/allegro.h"
30
31 #include <sstream>
32
33 #define ROUND(x) ((int) ((x) + 0.5))
34
35 #include "AColor.h"
36 #include "Prefs.h"
37 #include "Project.h"
38 #include "prefs/ImportExportPrefs.h"
39
40 #include "InconsistencyException.h"
41
42 #include "effects/TimeWarper.h"
43 #include "tracks/ui/TrackView.h"
44 #include "tracks/ui/TrackControls.h"
45
46 #include "AllThemeResources.h"
47 #include "Theme.h"
48
49 #ifdef SONIFY
50 #include <portmidi.h>
51
52 #define SON_PROGRAM 0
53 #define SON_AutoSave 67
54 #define SON_ModifyState 60
55 #define SON_NoteBackground 72
56 #define SON_NoteForeground 74
57 #define SON_Measures 76 /* "bar line" */
58 #define SON_Serialize 77
59 #define SON_Unserialize 79
60 #define SON_VEL 100
61
62
63 PmStream *sonMidiStream;
64 bool sonificationStarted = false;
65
SonifyBeginSonification()66 void SonifyBeginSonification()
67 {
68 PmError err = Pm_OpenOutput(&sonMidiStream, Pm_GetDefaultOutputDeviceID(),
69 NULL, 0, NULL, NULL, 0);
70 if (err) sonMidiStream = NULL;
71 if (sonMidiStream)
72 Pm_WriteShort(sonMidiStream, 0, Pm_Message(0xC0, SON_PROGRAM, 0));
73 sonificationStarted = true;
74 }
75
76
SonifyEndSonification()77 void SonifyEndSonification()
78 {
79 if (sonMidiStream) Pm_Close(sonMidiStream);
80 sonificationStarted = false;
81 }
82
83
84
85
SonifyNoteOnOff(int p,int v)86 void SonifyNoteOnOff(int p, int v)
87 {
88 if (!sonificationStarted)
89 SonifyBeginSonification();
90 if (sonMidiStream)
91 Pm_WriteShort(sonMidiStream, 0, Pm_Message(0x90, p, v));
92 }
93
94 #define SONFNS(name) \
95 void SonifyBegin ## name() { SonifyNoteOnOff(SON_ ## name, SON_VEL); } \
96 void SonifyEnd ## name() { SonifyNoteOnOff(SON_ ## name, 0); }
97
98 SONFNS(NoteBackground)
SONFNS(NoteForeground)99 SONFNS(NoteForeground)
100 SONFNS(Measures)
101 SONFNS(Serialize)
102 SONFNS(Unserialize)
103 SONFNS(ModifyState)
104 SONFNS(AutoSave)
105
106 #undef SONFNS
107
108 #endif
109
110
111
112 static ProjectFileIORegistry::ObjectReaderEntry readerEntry{
113 "notetrack",
114 []( AudacityProject &project ){
115 auto &tracks = TrackList::Get( project );
116 auto result = tracks.Add( std::make_shared<NoteTrack>());
117 TrackView::Get( *result );
118 TrackControls::Get( *result );
119 return result;
120 }
121 };
122
NoteTrack()123 NoteTrack::NoteTrack()
124 : NoteTrackBase()
125 {
126 SetDefaultName(_("Note Track"));
127 SetName(GetDefaultName());
128
129 mSeq = NULL;
130 mSerializationLength = 0;
131
132 #ifdef EXPERIMENTAL_MIDI_OUT
133 mVelocity = 0;
134 #endif
135 mBottomNote = MinPitch;
136 mTopNote = MaxPitch;
137
138 mVisibleChannels = ALL_CHANNELS;
139 }
140
~NoteTrack()141 NoteTrack::~NoteTrack()
142 {
143 }
144
GetSeq() const145 Alg_seq &NoteTrack::GetSeq() const
146 {
147 if (!mSeq) {
148 if (!mSerializationBuffer)
149 mSeq = std::make_unique<Alg_seq>();
150 else {
151 std::unique_ptr<Alg_track> alg_track
152 { Alg_seq::unserialize
153 ( mSerializationBuffer.get(), mSerializationLength ) };
154 wxASSERT(alg_track->get_type() == 's');
155 mSeq.reset( static_cast<Alg_seq*>(alg_track.release()) );
156
157 // Preserve the invariant that at most one of the representations is
158 // valid
159 mSerializationBuffer.reset();
160 mSerializationLength = 0;
161 }
162 }
163 wxASSERT(mSeq);
164 return *mSeq;
165 }
166
Clone() const167 Track::Holder NoteTrack::Clone() const
168 {
169 auto duplicate = std::make_shared<NoteTrack>();
170 duplicate->Init(*this);
171 // The duplicate begins life in serialized state. Often the duplicate is
172 // pushed on the Undo stack. Then we want to un-serialize it (or a further
173 // copy) only on demand after an Undo.
174 if (mSeq) {
175 SonifyBeginSerialize();
176 wxASSERT(!mSerializationBuffer);
177 // serialize from this to duplicate's mSerializationBuffer
178 void *buffer;
179 mSeq->serialize(&buffer,
180 &duplicate->mSerializationLength);
181 duplicate->mSerializationBuffer.reset( (char*)buffer );
182 SonifyEndSerialize();
183 }
184 else if (mSerializationBuffer) {
185 // Copy already serialized data.
186 wxASSERT(!mSeq);
187 duplicate->mSerializationLength = this->mSerializationLength;
188 duplicate->mSerializationBuffer.reset
189 ( safenew char[ this->mSerializationLength ] );
190 memcpy( duplicate->mSerializationBuffer.get(),
191 this->mSerializationBuffer.get(), this->mSerializationLength );
192 }
193 else {
194 // We are duplicating a default-constructed NoteTrack, and that's okay
195 }
196 // copy some other fields here
197 duplicate->SetBottomNote(mBottomNote);
198 duplicate->SetTopNote(mTopNote);
199 duplicate->mVisibleChannels = mVisibleChannels;
200 duplicate->SetOffset(GetOffset());
201 #ifdef EXPERIMENTAL_MIDI_OUT
202 duplicate->SetVelocity(GetVelocity());
203 #endif
204 return duplicate;
205 }
206
207
GetOffset() const208 double NoteTrack::GetOffset() const
209 {
210 return mOffset;
211 }
212
GetStartTime() const213 double NoteTrack::GetStartTime() const
214 {
215 return GetOffset();
216 }
217
GetEndTime() const218 double NoteTrack::GetEndTime() const
219 {
220 return GetStartTime() + GetSeq().get_real_dur();
221 }
222
WarpAndTransposeNotes(double t0,double t1,const TimeWarper & warper,double semitones)223 void NoteTrack::WarpAndTransposeNotes(double t0, double t1,
224 const TimeWarper &warper,
225 double semitones)
226 {
227 double offset = this->GetOffset(); // track is shifted this amount
228 auto &seq = GetSeq();
229 seq.convert_to_seconds(); // make sure time units are right
230 t1 -= offset; // adjust time range to compensate for track offset
231 t0 -= offset;
232 if (t1 > seq.get_dur()) { // make sure t0, t1 are within sequence
233 t1 = seq.get_dur();
234 if (t0 >= t1) return;
235 }
236 Alg_iterator iter(mSeq.get(), false);
237 iter.begin();
238 Alg_event_ptr event;
239 while (0 != (event = iter.next()) && event->time < t1) {
240 if (event->is_note() && event->time >= t0) {
241 event->set_pitch(event->get_pitch() + semitones);
242 }
243 }
244 iter.end();
245 // now, use warper to warp the tempo map
246 seq.convert_to_beats(); // beats remain the same
247 Alg_time_map_ptr map = seq.get_time_map();
248 map->insert_beat(t0, map->time_to_beat(t0));
249 map->insert_beat(t1, map->time_to_beat(t1));
250 int i, len = map->length();
251 for (i = 0; i < len; i++) {
252 Alg_beat &beat = map->beats[i];
253 beat.time = warper.Warp(beat.time + offset) - offset;
254 }
255 // about to redisplay, so might as well convert back to time now
256 seq.convert_to_seconds();
257 }
258
259 // Draws the midi channel toggle buttons within the given rect.
260 // The rect should be evenly divisible by 4 on both axis.
DrawLabelControls(const NoteTrack * pTrack,wxDC & dc,const wxRect & rect,int highlightedChannel)261 void NoteTrack::DrawLabelControls
262 ( const NoteTrack *pTrack, wxDC & dc, const wxRect &rect, int highlightedChannel )
263 {
264 dc.SetTextForeground(theTheme.Colour(clrLabelTrackText));
265 wxASSERT_MSG(rect.width % 4 == 0, "Midi channel control rect width must be divisible by 4");
266 wxASSERT_MSG(rect.height % 4 == 0, "Midi channel control rect height must be divisible by 4");
267
268 auto cellWidth = rect.width / 4;
269 auto cellHeight = rect.height / 4;
270
271 wxRect box;
272 for (int row = 0; row < 4; row++) {
273 for (int col = 0; col < 4; col++) {
274 // chanName is the "external" channel number (1-16)
275 // used by AColor and button labels
276 int chanName = row * 4 + col + 1;
277
278 box.x = rect.x + col * cellWidth;
279 box.y = rect.y + row * cellHeight;
280 box.width = cellWidth;
281 box.height = cellHeight;
282
283 bool visible = pTrack ? pTrack->IsVisibleChan(chanName - 1) : true;
284 if (visible) {
285 // highlightedChannel counts 0 based
286 if ( chanName == highlightedChannel + 1 )
287 AColor::LightMIDIChannel(&dc, chanName);
288 else
289 AColor::MIDIChannel(&dc, chanName);
290 dc.DrawRectangle(box);
291 // two choices: channel is enabled (to see and play) when button is in
292 // "up" position (original Audacity style) or in "down" position
293 //
294 #define CHANNEL_ON_IS_DOWN 1
295 #if CHANNEL_ON_IS_DOWN
296 AColor::DarkMIDIChannel(&dc, chanName);
297 #else
298 AColor::LightMIDIChannel(&dc, chanName);
299 #endif
300 AColor::Line(dc, box.x, box.y, box.x + box.width - 1, box.y);
301 AColor::Line(dc, box.x, box.y, box.x, box.y + box.height - 1);
302
303 #if CHANNEL_ON_IS_DOWN
304 AColor::LightMIDIChannel(&dc, chanName);
305 #else
306 AColor::DarkMIDIChannel(&dc, chanName);
307 #endif
308 AColor::Line(dc,
309 box.x + box.width - 1, box.y,
310 box.x + box.width - 1, box.y + box.height - 1);
311 AColor::Line(dc,
312 box.x, box.y + box.height - 1,
313 box.x + box.width - 1, box.y + box.height - 1);
314 } else {
315 if ( chanName == highlightedChannel + 1 )
316 AColor::LightMIDIChannel(&dc, chanName);
317 else
318 AColor::MIDIChannel(&dc, 0);
319 dc.DrawRectangle(box);
320 #if CHANNEL_ON_IS_DOWN
321 AColor::LightMIDIChannel(&dc, 0);
322 #else
323 AColor::DarkMIDIChannel(&dc, 0);
324 #endif
325 AColor::Line(dc, box.x, box.y, box.x + box.width - 1, box.y);
326 AColor::Line(dc, box.x, box.y, box.x, box.y + box.height - 1);
327
328 #if CHANNEL_ON_IS_DOWN
329 AColor::DarkMIDIChannel(&dc, 0);
330 #else
331 AColor::LightMIDIChannel(&dc, 0);
332 #endif
333 AColor::Line(dc,
334 box.x + box.width - 1, box.y,
335 box.x + box.width - 1, box.y + box.height - 1);
336 AColor::Line(dc,
337 box.x, box.y + box.height - 1,
338 box.x + box.width - 1, box.y + box.height - 1);
339
340 }
341
342 wxString text;
343 wxCoord w;
344 wxCoord h;
345
346 text.Printf(wxT("%d"), chanName);
347 dc.GetTextExtent(text, &w, &h);
348
349 dc.DrawText(text, box.x + (box.width - w) / 2, box.y + (box.height - h) / 2);
350 }
351 }
352 dc.SetTextForeground(theTheme.Colour(clrTrackPanelText));
353 AColor::MIDIChannel(&dc, 0); // always return with gray color selected
354 }
355
FindChannel(const wxRect & rect,int mx,int my)356 int NoteTrack::FindChannel(const wxRect &rect, int mx, int my)
357 {
358 wxASSERT_MSG(rect.width % 4 == 0, "Midi channel control rect width must be divisible by 4");
359 wxASSERT_MSG(rect.height % 4 == 0, "Midi channel control rect height must be divisible by 4");
360
361 auto cellWidth = rect.width / 4;
362 auto cellHeight = rect.height / 4;
363
364 int col = (mx - rect.x) / cellWidth;
365 int row = (my - rect.y) / cellHeight;
366
367 return row * 4 + col;
368 }
369
370
371 // Handles clicking within the midi controls rect (same as DrawLabelControls).
372 // This is somewhat oddly written, as these aren't real buttons - they act
373 // when the mouse goes down; you can't hold it pressed and move off of it.
374 // Left-clicking toggles a single channel; right-clicking turns off all other channels.
LabelClick(const wxRect & rect,int mx,int my,bool right)375 bool NoteTrack::LabelClick(const wxRect &rect, int mx, int my, bool right)
376 {
377 auto channel = FindChannel(rect, mx, my);
378 if (right)
379 SoloVisibleChan(channel);
380 else
381 ToggleVisibleChan(channel);
382
383 return true;
384 }
385
SetSequence(std::unique_ptr<Alg_seq> && seq)386 void NoteTrack::SetSequence(std::unique_ptr<Alg_seq> &&seq)
387 {
388 mSeq = std::move(seq);
389 }
390
PrintSequence()391 void NoteTrack::PrintSequence()
392 {
393 FILE *debugOutput;
394
395 debugOutput = fopen("debugOutput.txt", "wt");
396 wxFprintf(debugOutput, "Importing MIDI...\n");
397
398 // This is called for debugging purposes. Do not compute mSeq on demand
399 // with GetSeq()
400 if (mSeq) {
401 int i = 0;
402
403 while(i < mSeq->length()) {
404 wxFprintf(debugOutput, "--\n");
405 wxFprintf(debugOutput, "type: %c\n",
406 ((Alg_event_ptr)mSeq->track_list.tracks[i])->get_type());
407 wxFprintf(debugOutput, "time: %f\n",
408 ((Alg_event_ptr)mSeq->track_list.tracks[i])->time);
409 wxFprintf(debugOutput, "channel: %li\n",
410 ((Alg_event_ptr)mSeq->track_list.tracks[i])->chan);
411
412 if(((Alg_event_ptr)mSeq->track_list.tracks[i])->get_type() == wxT('n'))
413 {
414 wxFprintf(debugOutput, "pitch: %f\n",
415 ((Alg_note_ptr)mSeq->track_list.tracks[i])->pitch);
416 wxFprintf(debugOutput, "duration: %f\n",
417 ((Alg_note_ptr)mSeq->track_list.tracks[i])->dur);
418 wxFprintf(debugOutput, "velocity: %f\n",
419 ((Alg_note_ptr)mSeq->track_list.tracks[i])->loud);
420 }
421 else if(((Alg_event_ptr)mSeq->track_list.tracks[i])->get_type() == wxT('n'))
422 {
423 wxFprintf(debugOutput, "key: %li\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->get_identifier());
424 wxFprintf(debugOutput, "attribute type: %c\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type());
425 wxFprintf(debugOutput, "attribute: %s\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_name());
426
427 if(((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type() == wxT('r'))
428 {
429 wxFprintf(debugOutput, "value: %f\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.r);
430 }
431 else if(((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type() == wxT('i')) {
432 wxFprintf(debugOutput, "value: %li\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.i);
433 }
434 else if(((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type() == wxT('s')) {
435 wxFprintf(debugOutput, "value: %s\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.s);
436 }
437 else {}
438 }
439
440 i++;
441 }
442 }
443 else {
444 wxFprintf(debugOutput, "No sequence defined!\n");
445 }
446
447 fclose(debugOutput);
448 }
449
Cut(double t0,double t1)450 Track::Holder NoteTrack::Cut(double t0, double t1)
451 {
452 if (t1 < t0)
453 THROW_INCONSISTENCY_EXCEPTION;
454
455 double len = t1-t0;
456 //auto delta = -(
457 //( std::min( t1, GetEndTime() ) ) - ( std::max( t0, GetStartTime() ) )
458 //);
459
460 auto newTrack = std::make_shared<NoteTrack>();
461
462 newTrack->Init(*this);
463
464 auto &seq = GetSeq();
465 seq.convert_to_seconds();
466 newTrack->mSeq.reset(seq.cut(t0 - GetOffset(), len, false));
467 newTrack->SetOffset(0);
468
469 // Not needed
470 // Alg_seq::cut seems to handle this
471 //AddToDuration( delta );
472
473 // What should be done with the rest of newTrack's members?
474 //(mBottomNote,
475 // mSerializationBuffer, mSerializationLength, mVisibleChannels)
476
477 return newTrack;
478 }
479
Copy(double t0,double t1,bool) const480 Track::Holder NoteTrack::Copy(double t0, double t1, bool) const
481 {
482 if (t1 < t0)
483 THROW_INCONSISTENCY_EXCEPTION;
484
485 double len = t1-t0;
486
487 auto newTrack = std::make_shared<NoteTrack>();
488
489 newTrack->Init(*this);
490
491 auto &seq = GetSeq();
492 seq.convert_to_seconds();
493 newTrack->mSeq.reset(seq.copy(t0 - GetOffset(), len, false));
494 newTrack->SetOffset(0);
495
496 // What should be done with the rest of newTrack's members?
497 // (mBottomNote, mSerializationBuffer,
498 // mSerializationLength, mVisibleChannels)
499
500 return newTrack;
501 }
502
Trim(double t0,double t1)503 bool NoteTrack::Trim(double t0, double t1)
504 {
505 if (t1 < t0)
506 return false;
507 auto &seq = GetSeq();
508 //auto delta = -(
509 //( GetEndTime() - std::min( GetEndTime(), t1 ) ) +
510 //( std::max(t0, GetStartTime()) - GetStartTime() )
511 //);
512 seq.convert_to_seconds();
513 // DELETE way beyond duration just in case something is out there:
514 seq.clear(t1 - GetOffset(), seq.get_dur() + 10000.0, false);
515 // Now that stuff beyond selection is cleared, clear before selection:
516 seq.clear(0.0, t0 - GetOffset(), false);
517 // want starting time to be t0
518 SetOffset(t0);
519
520 // Not needed
521 // Alg_seq::clear seems to handle this
522 //AddToDuration( delta );
523
524 return true;
525 }
526
Clear(double t0,double t1)527 void NoteTrack::Clear(double t0, double t1)
528 {
529 if (t1 < t0)
530 THROW_INCONSISTENCY_EXCEPTION;
531
532 double len = t1-t0;
533
534 auto &seq = GetSeq();
535
536 auto offset = GetOffset();
537 auto start = t0 - offset;
538 if (start < 0.0) {
539 // AlgSeq::clear will shift the cleared interval, not changing len, if
540 // start is negative. That's not what we want to happen.
541 if (len > -start) {
542 seq.clear(0, len + start, false);
543 SetOffset(t0);
544 }
545 else
546 SetOffset(offset - len);
547 }
548 else {
549 //auto delta = -(
550 //( std::min( t1, GetEndTime() ) ) - ( std::max( t0, GetStartTime() ) )
551 //);
552 seq.clear(start, len, false);
553
554 // Not needed
555 // Alg_seq::clear seems to handle this
556 // AddToDuration( delta );
557 }
558 }
559
Paste(double t,const Track * src)560 void NoteTrack::Paste(double t, const Track *src)
561 {
562 // Paste inserts src at time t. If src has a positive offset,
563 // the offset is treated as silence which is also inserted. If
564 // the offset is negative, the offset is ignored and the ENTIRE
565 // src is inserted (otherwise, we would either lose data from
566 // src by not inserting things at negative times, or inserting
567 // things at negative times could overlap things already in
568 // the destination track).
569
570 //Check that src is a non-NULL NoteTrack
571 bool bOk = src && src->TypeSwitch< bool >( [&](const NoteTrack *other) {
572
573 auto myOffset = this->GetOffset();
574 if (t < myOffset) {
575 // workaround strange behavior described at
576 // http://bugzilla.audacityteam.org/show_bug.cgi?id=1735#c3
577 SetOffset(t);
578 InsertSilence(t, myOffset - t);
579 }
580
581 double delta = 0.0;
582 auto &seq = GetSeq();
583 auto offset = other->GetOffset();
584 if ( offset > 0 ) {
585 seq.convert_to_seconds();
586 seq.insert_silence( t - GetOffset(), offset );
587 t += offset;
588 // Is this needed or does Alg_seq::insert_silence take care of it?
589 //delta += offset;
590 }
591
592 // This seems to be needed:
593 delta += std::max( 0.0, t - GetEndTime() );
594
595 // This, not:
596 //delta += other->GetSeq().get_real_dur();
597
598 seq.paste(t - GetOffset(), &other->GetSeq());
599
600 AddToDuration( delta );
601
602 return true;
603 });
604
605 if ( !bOk )
606 // THROW_INCONSISTENCY_EXCEPTION; // ?
607 (void)0;// intentionally do nothing
608 }
609
Silence(double t0,double t1)610 void NoteTrack::Silence(double t0, double t1)
611 {
612 if (t1 < t0)
613 THROW_INCONSISTENCY_EXCEPTION;
614
615 auto len = t1 - t0;
616
617 auto &seq = GetSeq();
618 seq.convert_to_seconds();
619 // XXX: do we want to set the all param?
620 // If it's set, then it seems like notes are silenced if they start or end in the range,
621 // otherwise only if they start in the range. --Poke
622 seq.silence(t0 - GetOffset(), len, false);
623 }
624
InsertSilence(double t,double len)625 void NoteTrack::InsertSilence(double t, double len)
626 {
627 if (len < 0)
628 THROW_INCONSISTENCY_EXCEPTION;
629
630 auto &seq = GetSeq();
631 seq.convert_to_seconds();
632 seq.insert_silence(t - GetOffset(), len);
633
634 // is this needed?
635 // AddToDuration( len );
636 }
637
638 #ifdef EXPERIMENTAL_MIDI_OUT
SetVelocity(float velocity)639 void NoteTrack::SetVelocity(float velocity)
640 {
641 if (mVelocity != velocity) {
642 mVelocity = velocity;
643 Notify();
644 }
645 }
646 #endif
647
648 // Call this function to manipulate the underlying sequence data. This is
649 // NOT the function that handles horizontal dragging.
Shift(double t)650 bool NoteTrack::Shift(double t) // t is always seconds
651 {
652 if (t > 0) {
653 auto &seq = GetSeq();
654 // insert an even number of measures
655 seq.convert_to_beats();
656 // get initial tempo
657 double tempo = seq.get_tempo(0.0);
658 double beats_per_measure = seq.get_bar_len(0.0);
659 int m = ROUND(t * tempo / beats_per_measure);
660 // need at least 1 measure, so if we rounded down to zero, fix it
661 if (m == 0) m = 1;
662 // compute NEW tempo so that m measures at NEW tempo take t seconds
663 tempo = beats_per_measure * m / t; // in beats per second
664 seq.insert_silence(0.0, beats_per_measure * m);
665 seq.set_tempo(tempo * 60.0 /* bpm */, 0.0, beats_per_measure * m);
666 seq.write("afterShift.gro");
667 } else if (t < 0) {
668 auto &seq = GetSeq();
669 seq.convert_to_seconds();
670 seq.clear(0, t, true);
671 } else { // offset is zero, no modifications
672 return false;
673 }
674 return true;
675 }
676
NearestBeatTime(double time) const677 QuantizedTimeAndBeat NoteTrack::NearestBeatTime( double time ) const
678 {
679 // Alg_seq knows nothing about offset, so remove offset time
680 double seq_time = time - GetOffset();
681 double beat;
682 auto &seq = GetSeq();
683 seq_time = seq.nearest_beat_time(seq_time, &beat);
684 // add the offset back in to get "actual" audacity track time
685 return { seq_time + GetOffset(), beat };
686 }
687
PasteInto(AudacityProject &) const688 Track::Holder NoteTrack::PasteInto( AudacityProject & ) const
689 {
690 auto pNewTrack = std::make_shared<NoteTrack>();
691 pNewTrack->Paste(0.0, this);
692 return pNewTrack;
693 }
694
GetIntervals() const695 auto NoteTrack::GetIntervals() const -> ConstIntervals
696 {
697 ConstIntervals results;
698 results.emplace_back( GetStartTime(), GetEndTime() );
699 return results;
700 }
701
GetIntervals()702 auto NoteTrack::GetIntervals() -> Intervals
703 {
704 Intervals results;
705 results.emplace_back( GetStartTime(), GetEndTime() );
706 return results;
707 }
708
AddToDuration(double delta)709 void NoteTrack::AddToDuration( double delta )
710 {
711 auto &seq = GetSeq();
712 #if 0
713 // PRL: Would this be better ?
714 seq.set_real_dur( seq.get_real_dur() + delta );
715 #else
716 seq.convert_to_seconds();
717 seq.set_dur( seq.get_dur() + delta );
718 #endif
719 }
720
StretchRegion(QuantizedTimeAndBeat t0,QuantizedTimeAndBeat t1,double newDur)721 bool NoteTrack::StretchRegion
722 ( QuantizedTimeAndBeat t0, QuantizedTimeAndBeat t1, double newDur )
723 {
724 auto &seq = GetSeq();
725 bool result = seq.stretch_region( t0.second, t1.second, newDur );
726 if (result) {
727 const auto oldDur = t1.first - t0.first;
728 AddToDuration( newDur - oldDur );
729 }
730 return result;
731 }
732
733 namespace
734 {
swap(std::unique_ptr<Alg_seq> & a,std::unique_ptr<Alg_seq> & b)735 void swap(std::unique_ptr<Alg_seq> &a, std::unique_ptr<Alg_seq> &b)
736 {
737 std::unique_ptr<Alg_seq> tmp = std::move(a);
738 a = std::move(b);
739 b = std::move(tmp);
740 }
741 }
742
MakeExportableSeq(std::unique_ptr<Alg_seq> & cleanup) const743 Alg_seq *NoteTrack::MakeExportableSeq(std::unique_ptr<Alg_seq> &cleanup) const
744 {
745 cleanup.reset();
746 double offset = GetOffset();
747 if (offset == 0)
748 return &GetSeq();
749 // make a copy, deleting events that are shifted before time 0
750 double start = -offset;
751 if (start < 0) start = 0;
752 // notes that begin before "start" are not included even if they
753 // extend past "start" (because "all" parameter is set to false)
754 cleanup.reset( GetSeq().copy(start, GetSeq().get_dur() - start, false) );
755 auto seq = cleanup.get();
756 if (offset > 0) {
757 {
758 // swap cleanup and mSeq so that Shift operates on the NEW copy
759 swap( this->mSeq, cleanup );
760 auto cleanup2 = finally( [&] { swap( this->mSeq, cleanup ); } );
761
762 const_cast< NoteTrack *>( this )->Shift(offset);
763 }
764 #ifdef OLD_CODE
765 // now shift events by offset. This must be done with an integer
766 // number of measures, so first, find the beats-per-measure
767 double beats_per_measure = 4.0;
768 Alg_time_sig_ptr tsp = NULL;
769 if (seq->time_sig.length() > 0 && seq->time_sig[0].beat < ALG_EPS) {
770 // there is an initial time signature
771 tsp = &(seq->time_sig[0]);
772 beats_per_measure = (tsp->num * 4) / tsp->den;
773 }
774 // also need the initial tempo
775 double bps = ALG_DEFAULT_BPM / 60;
776 Alg_time_map_ptr map = seq->get_time_map();
777 Alg_beat_ptr bp = &(map->beats[0]);
778 if (bp->time < ALG_EPS) { // tempo change at time 0
779 if (map->beats.len > 1) { // compute slope to get tempo
780 bps = (map->beats[1].beat - map->beats[0].beat) /
781 (map->beats[1].time - map->beats[0].time);
782 } else if (seq->get_time_map()->last_tempo_flag) {
783 bps = seq->get_time_map()->last_tempo;
784 }
785 }
786 // find closest number of measures to fit in the gap
787 // number of measures is offset / measure_time
788 double measure_time = beats_per_measure / bps; // seconds per measure
789 int n = ROUND(offset / measure_time);
790 if (n == 0) n = 1;
791 // we will insert n measures. Compute the desired duration of each.
792 measure_time = offset / n;
793 bps = beats_per_measure / measure_time;
794 // insert integer multiple of measures at beginning
795 seq->convert_to_beats();
796 seq->insert_silence(0, beats_per_measure * n);
797 // make sure time signature at 0 is correct
798 if (tsp) {
799 seq->set_time_sig(0, tsp->num, tsp->den);
800 }
801 // adjust tempo to match offset
802 seq->set_tempo(bps * 60.0, 0, beats_per_measure * n);
803 #endif
804 } else {
805 auto &mySeq = GetSeq();
806 // if offset is negative, it might not be a multiple of beats, but
807 // we want to preserve the relative positions of measures. I.e. we
808 // should shift barlines and time signatures as well as notes.
809 // Insert a time signature at the first bar-line if necessary.
810
811 // Translate start from seconds to beats and call it beat:
812 double beat = mySeq.get_time_map()->time_to_beat(start);
813 // Find the time signature in mySeq in effect at start (beat):
814 int i = mySeq.time_sig.find_beat(beat);
815 // i is where you would insert a NEW time sig at beat,
816 // Case 1: beat coincides with a time sig at i. Time signature
817 // at beat means that there is a barline at beat, so when beat
818 // is shifted to 0, the relative barline positions are preserved
819 if (mySeq.time_sig.length() > 0 &&
820 within(beat, mySeq.time_sig[i].beat, ALG_EPS)) {
821 // beat coincides with time signature change, so offset must
822 // be a multiple of beats
823 /* do nothing */ ;
824 // Case 2: there is no time signature before beat.
825 } else if (i == 0 && (mySeq.time_sig.length() == 0 ||
826 mySeq.time_sig[i].beat > beat)) {
827 // If beat does not fall on an implied barline, we need to
828 // insert a time signature.
829 double measures = beat / 4.0;
830 double imeasures = ROUND(measures);
831 if (!within(measures, imeasures, ALG_EPS)) {
832 double bar_offset = ((int)(measures) + 1) * 4.0 - beat;
833 seq->set_time_sig(bar_offset, 4, 4);
834 }
835 // This case should never be true because if i == 0, either there
836 // are no time signatures before beat (Case 2),
837 // or there is one time signature at beat (Case 1)
838 } else if (i == 0) {
839 /* do nothing (might be good to assert(false)) */ ;
840 // Case 3: i-1 must be the effective time sig position
841 } else {
842 i -= 1; // index the time signature in effect at beat
843 Alg_time_sig_ptr tsp = &(mySeq.time_sig[i]);
844 double beats_per_measure = (tsp->num * 4) / tsp->den;
845 double measures = (beat - tsp->beat) / beats_per_measure;
846 int imeasures = ROUND(measures);
847 if (!within(measures, imeasures, ALG_EPS)) {
848 // beat is not on a measure, so we need to insert a time sig
849 // to force a bar line at the first measure location after
850 // beat
851 double bar = tsp->beat + beats_per_measure * ((int)(measures) + 1);
852 double bar_offset = bar - beat;
853 // insert NEW time signature at bar_offset in NEW sequence
854 // It will have the same time signature, but the position will
855 // force a barline to match the barlines in mSeq
856 seq->set_time_sig(bar_offset, tsp->num, tsp->den);
857 }
858 // else beat coincides with a barline, so no need for an extra
859 // time signature to force barline alignment
860 }
861 }
862 return seq;
863 }
864
865
ExportMIDI(const wxString & f) const866 bool NoteTrack::ExportMIDI(const wxString &f) const
867 {
868 std::unique_ptr<Alg_seq> cleanup;
869 auto seq = MakeExportableSeq(cleanup);
870 bool rslt = seq->smf_write(f.mb_str());
871 return rslt;
872 }
873
ExportAllegro(const wxString & f) const874 bool NoteTrack::ExportAllegro(const wxString &f) const
875 {
876 double offset = GetOffset();
877 auto in_seconds = ImportExportPrefs::AllegroStyleSetting.ReadEnum();
878 auto &seq = GetSeq();
879 if (in_seconds) {
880 seq.convert_to_seconds();
881 } else {
882 seq.convert_to_beats();
883 }
884 return seq.write(f.mb_str(), offset);
885 }
886
887
888 namespace {
IsValidVisibleChannels(const int nValue)889 bool IsValidVisibleChannels(const int nValue)
890 {
891 return (nValue >= 0 && nValue < (1 << 16));
892 }
893 }
894
HandleXMLTag(const std::string_view & tag,const AttributesList & attrs)895 bool NoteTrack::HandleXMLTag(const std::string_view& tag, const AttributesList &attrs)
896 {
897 if (tag == "notetrack") {
898 for (auto pair : attrs)
899 {
900 auto attr = pair.first;
901 auto value = pair.second;
902
903 long nValue;
904 double dblValue;
905 if (this->Track::HandleCommonXMLAttribute(attr, value))
906 ;
907 else if (this->NoteTrackBase::HandleXMLAttribute(attr, value))
908 {}
909 else if (attr == "offset" && value.TryGet(dblValue))
910 SetOffset(dblValue);
911 else if (attr == "visiblechannels") {
912 if (!value.TryGet(nValue) ||
913 !IsValidVisibleChannels(nValue))
914 return false;
915 mVisibleChannels = nValue;
916 }
917 #ifdef EXPERIMENTAL_MIDI_OUT
918 else if (attr == "velocity" && value.TryGet(dblValue))
919 mVelocity = (float) dblValue;
920 #endif
921 else if (attr == "bottomnote" && value.TryGet(nValue))
922 SetBottomNote(nValue);
923 else if (attr == "topnote" && value.TryGet(nValue))
924 SetTopNote(nValue);
925 else if (attr == "data") {
926 std::string s(value.ToWString());
927 std::istringstream data(s);
928 mSeq = std::make_unique<Alg_seq>(data, false);
929 }
930 } // while
931 return true;
932 }
933 return false;
934 }
935
HandleXMLChild(const std::string_view & WXUNUSED (tag))936 XMLTagHandler *NoteTrack::HandleXMLChild(const std::string_view& WXUNUSED(tag))
937 {
938 return NULL;
939 }
940
WriteXML(XMLWriter & xmlFile) const941 void NoteTrack::WriteXML(XMLWriter &xmlFile) const
942 // may throw
943 {
944 std::ostringstream data;
945 Track::Holder holder;
946 const NoteTrack *saveme = this;
947 if (!mSeq) {
948 // replace saveme with an (unserialized) duplicate, which is
949 // destroyed at end of function.
950 holder = Clone();
951 saveme = static_cast<NoteTrack*>(holder.get());
952 }
953 saveme->GetSeq().write(data, true);
954 xmlFile.StartTag(wxT("notetrack"));
955 saveme->Track::WriteCommonXMLAttributes( xmlFile );
956 this->NoteTrackBase::WriteXMLAttributes(xmlFile);
957 xmlFile.WriteAttr(wxT("offset"), saveme->GetOffset());
958 xmlFile.WriteAttr(wxT("visiblechannels"), saveme->mVisibleChannels);
959
960 #ifdef EXPERIMENTAL_MIDI_OUT
961 xmlFile.WriteAttr(wxT("velocity"), (double) saveme->mVelocity);
962 #endif
963 xmlFile.WriteAttr(wxT("bottomnote"), saveme->mBottomNote);
964 xmlFile.WriteAttr(wxT("topnote"), saveme->mTopNote);
965 xmlFile.WriteAttr(wxT("data"), wxString(data.str().c_str(), wxConvUTF8));
966 xmlFile.EndTag(wxT("notetrack"));
967 }
968
SetBottomNote(int note)969 void NoteTrack::SetBottomNote(int note)
970 {
971 if (note < MinPitch)
972 note = MinPitch;
973 else if (note > 96)
974 note = 96;
975
976 wxCHECK(note <= mTopNote, );
977
978 mBottomNote = note;
979 }
980
SetTopNote(int note)981 void NoteTrack::SetTopNote(int note)
982 {
983 if (note > MaxPitch)
984 note = MaxPitch;
985
986 wxCHECK(note >= mBottomNote, );
987
988 mTopNote = note;
989 }
990
SetNoteRange(int note1,int note2)991 void NoteTrack::SetNoteRange(int note1, int note2)
992 {
993 // Bounds check
994 if (note1 > MaxPitch)
995 note1 = MaxPitch;
996 else if (note1 < MinPitch)
997 note1 = MinPitch;
998 if (note2 > MaxPitch)
999 note2 = MaxPitch;
1000 else if (note2 < MinPitch)
1001 note2 = MinPitch;
1002 // Swap to ensure ordering
1003 if (note2 < note1) { auto tmp = note1; note1 = note2; note2 = tmp; }
1004
1005 mBottomNote = note1;
1006 mTopNote = note2;
1007 }
1008
ShiftNoteRange(int offset)1009 void NoteTrack::ShiftNoteRange(int offset)
1010 {
1011 // Ensure everything stays in bounds
1012 if (mBottomNote + offset < MinPitch || mTopNote + offset > MaxPitch)
1013 return;
1014
1015 mBottomNote += offset;
1016 mTopNote += offset;
1017 }
1018
1019 #if 0
1020 void NoteTrack::StartVScroll()
1021 {
1022 mStartBottomNote = mBottomNote;
1023 }
1024
1025 void NoteTrack::VScroll(int start, int end)
1026 {
1027 int ph = GetPitchHeight();
1028 int delta = ((end - start) + ph / 2) / ph;
1029 ShiftNoteRange(delta);
1030 }
1031 #endif
1032
Zoom(const wxRect & rect,int y,float multiplier,bool center)1033 void NoteTrack::Zoom(const wxRect &rect, int y, float multiplier, bool center)
1034 {
1035 NoteTrackDisplayData data = NoteTrackDisplayData(this, rect);
1036 int clickedPitch = data.YToIPitch(y);
1037 int extent = mTopNote - mBottomNote + 1;
1038 int newExtent = (int) (extent / multiplier);
1039 float position;
1040 if (center) {
1041 // center the pitch that the user clicked on
1042 position = .5;
1043 } else {
1044 // align to keep the pitch that the user clicked on in the same place
1045 position = extent / (clickedPitch - mBottomNote);
1046 }
1047 int newBottomNote = clickedPitch - (newExtent * position);
1048 int newTopNote = clickedPitch + (newExtent * (1 - position));
1049 SetNoteRange(newBottomNote, newTopNote);
1050 }
1051
1052
ZoomTo(const wxRect & rect,int start,int end)1053 void NoteTrack::ZoomTo(const wxRect &rect, int start, int end)
1054 {
1055 wxRect trackRect(0, rect.GetY(), 1, rect.GetHeight());
1056 NoteTrackDisplayData data = NoteTrackDisplayData(this, trackRect);
1057 int pitch1 = data.YToIPitch(start);
1058 int pitch2 = data.YToIPitch(end);
1059 if (pitch1 == pitch2) {
1060 // Just zoom in instead of zooming to show only one note
1061 Zoom(rect, start, 1, true);
1062 return;
1063 }
1064 // It's fine for this to be in either order
1065 SetNoteRange(pitch1, pitch2);
1066 }
1067
ZoomAllNotes()1068 void NoteTrack::ZoomAllNotes()
1069 {
1070 Alg_iterator iterator( &GetSeq(), false );
1071 iterator.begin();
1072 Alg_event_ptr evt;
1073
1074 // Go through all of the notes, finding the minimum and maximum value pitches.
1075 bool hasNotes = false;
1076 int minPitch = MaxPitch;
1077 int maxPitch = MinPitch;
1078
1079 while (NULL != (evt = iterator.next())) {
1080 if (evt->is_note()) {
1081 int pitch = (int) evt->get_pitch();
1082 hasNotes = true;
1083 if (pitch < minPitch)
1084 minPitch = pitch;
1085 if (pitch > maxPitch)
1086 maxPitch = pitch;
1087 }
1088 }
1089
1090 if (!hasNotes) {
1091 // Semi-arbitrary default values:
1092 minPitch = 48;
1093 maxPitch = 72;
1094 }
1095
1096 SetNoteRange(minPitch, maxPitch);
1097 }
1098
NoteTrackDisplayData(const NoteTrack * track,const wxRect & r)1099 NoteTrackDisplayData::NoteTrackDisplayData(const NoteTrack* track, const wxRect &r)
1100 {
1101 auto span = track->GetTopNote() - track->GetBottomNote() + 1; // + 1 to make sure it includes both
1102
1103 mMargin = std::min((int) (r.height / (float)(span)) / 2, r.height / 4);
1104
1105 // Count the number of dividers between B/C and E/F
1106 int numC = 0, numF = 0;
1107 auto botOctave = track->GetBottomNote() / 12, botNote = track->GetBottomNote() % 12;
1108 auto topOctave = track->GetTopNote() / 12, topNote = track->GetTopNote() % 12;
1109 if (topOctave == botOctave)
1110 {
1111 if (botNote == 0) numC = 1;
1112 if (topNote <= 5) numF = 1;
1113 }
1114 else
1115 {
1116 numC = topOctave - botOctave;
1117 numF = topOctave - botOctave - 1;
1118 if (botNote == 0) numC++;
1119 if (botNote <= 5) numF++;
1120 if (topOctave <= 5) numF++;
1121 }
1122 // Effective space, excluding the margins and the lines between some notes
1123 auto effectiveHeight = r.height - (2 * (mMargin + 1)) - numC - numF;
1124 // Guaranteed that both the bottom and top notes will be visible
1125 // (assuming that the clamping below does not happen)
1126 mPitchHeight = effectiveHeight / ((float) span);
1127
1128 if (mPitchHeight < MinPitchHeight)
1129 mPitchHeight = MinPitchHeight;
1130 if (mPitchHeight > MaxPitchHeight)
1131 mPitchHeight = MaxPitchHeight;
1132
1133 mBottom = r.y + r.height - GetNoteMargin() - 1 - GetPitchHeight(1) +
1134 botOctave * GetOctaveHeight() + GetNotePos(botNote);
1135 }
1136
IPitchToY(int p) const1137 int NoteTrackDisplayData::IPitchToY(int p) const
1138 { return mBottom - (p / 12) * GetOctaveHeight() - GetNotePos(p % 12); }
1139
YToIPitch(int y) const1140 int NoteTrackDisplayData::YToIPitch(int y) const
1141 {
1142 y = mBottom - y; // pixels above pitch 0
1143 int octave = (y / GetOctaveHeight());
1144 y -= octave * GetOctaveHeight();
1145 // result is approximate because C and G are one pixel taller than
1146 // mPitchHeight.
1147 // Poke 1-13-18: However in practice this seems not to be an issue,
1148 // as long as we use mPitchHeight and not the rounded version
1149 return (y / mPitchHeight) + octave * 12;
1150 }
1151
1152 const float NoteTrack::ZoomStep = powf( 2.0f, 0.25f );
1153
1154 #include <wx/log.h>
1155 #include <wx/sstream.h>
1156 #include <wx/txtstrm.h>
1157 #include "AudioIOBase.h"
1158 #include "portmidi.h"
1159
1160 // FIXME: When EXPERIMENTAL_MIDI_IN is added (eventually) this should also be enabled -- Poke
GetMIDIDeviceInfo()1161 wxString GetMIDIDeviceInfo()
1162 {
1163 wxStringOutputStream o;
1164 wxTextOutputStream s(o, wxEOL_UNIX);
1165
1166 if (AudioIOBase::Get()->IsStreamActive()) {
1167 return XO("Stream is active ... unable to gather information.\n")
1168 .Translation();
1169 }
1170
1171
1172 // XXX: May need to trap errors as with the normal device info
1173 int recDeviceNum = Pm_GetDefaultInputDeviceID();
1174 int playDeviceNum = Pm_GetDefaultOutputDeviceID();
1175 int cnt = Pm_CountDevices();
1176
1177 // PRL: why only into the log?
1178 wxLogDebug(wxT("PortMidi reports %d MIDI devices"), cnt);
1179
1180 s << wxT("==============================\n");
1181 s << XO("Default recording device number: %d\n").Format( recDeviceNum );
1182 s << XO("Default playback device number: %d\n").Format( playDeviceNum );
1183
1184 auto recDevice = MIDIRecordingDevice.Read();
1185 auto playDevice = MIDIPlaybackDevice.Read();
1186
1187 // This gets info on all available audio devices (input and output)
1188 if (cnt <= 0) {
1189 s << XO("No devices found\n");
1190 return o.GetString();
1191 }
1192
1193 for (int i = 0; i < cnt; i++) {
1194 s << wxT("==============================\n");
1195
1196 const PmDeviceInfo* info = Pm_GetDeviceInfo(i);
1197 if (!info) {
1198 s << XO("Device info unavailable for: %d\n").Format( i );
1199 continue;
1200 }
1201
1202 wxString name = wxSafeConvertMB2WX(info->name);
1203 wxString hostName = wxSafeConvertMB2WX(info->interf);
1204
1205 s << XO("Device ID: %d\n").Format( i );
1206 s << XO("Device name: %s\n").Format( name );
1207 s << XO("Host name: %s\n").Format( hostName );
1208 /* i18n-hint: Supported, meaning made available by the system */
1209 s << XO("Supports output: %d\n").Format( info->output );
1210 /* i18n-hint: Supported, meaning made available by the system */
1211 s << XO("Supports input: %d\n").Format( info->input );
1212 s << XO("Opened: %d\n").Format( info->opened );
1213
1214 if (name == playDevice && info->output)
1215 playDeviceNum = i;
1216
1217 if (name == recDevice && info->input)
1218 recDeviceNum = i;
1219
1220 // XXX: This is only done because the same was applied with PortAudio
1221 // If PortMidi returns -1 for the default device, use the first one
1222 if (recDeviceNum < 0 && info->input){
1223 recDeviceNum = i;
1224 }
1225 if (playDeviceNum < 0 && info->output){
1226 playDeviceNum = i;
1227 }
1228 }
1229
1230 bool haveRecDevice = (recDeviceNum >= 0);
1231 bool havePlayDevice = (playDeviceNum >= 0);
1232
1233 s << wxT("==============================\n");
1234 if (haveRecDevice)
1235 s << XO("Selected MIDI recording device: %d - %s\n").Format( recDeviceNum, recDevice );
1236 else
1237 s << XO("No MIDI recording device found for '%s'.\n").Format( recDevice );
1238
1239 if (havePlayDevice)
1240 s << XO("Selected MIDI playback device: %d - %s\n").Format( playDeviceNum, playDevice );
1241 else
1242 s << XO("No MIDI playback device found for '%s'.\n").Format( playDevice );
1243
1244 // Mention our conditional compilation flags for Alpha only
1245 #ifdef IS_ALPHA
1246
1247 // Not internationalizing these alpha-only messages
1248 s << wxT("==============================\n");
1249 #ifdef EXPERIMENTAL_MIDI_OUT
1250 s << wxT("EXPERIMENTAL_MIDI_OUT is enabled\n");
1251 #else
1252 s << wxT("EXPERIMENTAL_MIDI_OUT is NOT enabled\n");
1253 #endif
1254 #ifdef EXPERIMENTAL_MIDI_IN
1255 s << wxT("EXPERIMENTAL_MIDI_IN is enabled\n");
1256 #else
1257 s << wxT("EXPERIMENTAL_MIDI_IN is NOT enabled\n");
1258 #endif
1259
1260 #endif
1261
1262 return o.GetString();
1263 }
1264
1265 StringSetting MIDIPlaybackDevice{ L"/MidiIO/PlaybackDevice", L"" };
1266 StringSetting MIDIRecordingDevice{ L"/MidiIO/RecordingDevice", L"" };
1267 IntSetting MIDISynthLatency_ms{ L"/MidiIO/SynthLatency", 5 };
1268
1269 #endif // USE_MIDI
1270