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