/********************************************************************** Audacity: A Digital Audio Editor NoteTrack.cpp Dominic Mazzoni *******************************************************************//*! \class NoteTrack \brief A Track that is used for Midi notes. (Somewhat old code). *//*******************************************************************/ #include "NoteTrack.h" #include #include #include #include #include #if defined(USE_MIDI) #include "../lib-src/header-substitutes/allegro.h" #include #define ROUND(x) ((int) ((x) + 0.5)) #include "AColor.h" #include "Prefs.h" #include "ProjectFileIORegistry.h" #include "prefs/ImportExportPrefs.h" #include "InconsistencyException.h" #include "effects/TimeWarper.h" #include "tracks/ui/TrackView.h" #include "tracks/ui/TrackControls.h" #include "AllThemeResources.h" #include "Theme.h" #ifdef SONIFY #include "../lib-src/portmidi/pm_common/portmidi.h" #define SON_PROGRAM 0 #define SON_AutoSave 67 #define SON_ModifyState 60 #define SON_NoteBackground 72 #define SON_NoteForeground 74 #define SON_Measures 76 /* "bar line" */ #define SON_Serialize 77 #define SON_Unserialize 79 #define SON_VEL 100 PmStream *sonMidiStream; bool sonificationStarted = false; void SonifyBeginSonification() { PmError err = Pm_OpenOutput(&sonMidiStream, Pm_GetDefaultOutputDeviceID(), NULL, 0, NULL, NULL, 0); if (err) sonMidiStream = NULL; if (sonMidiStream) Pm_WriteShort(sonMidiStream, 0, Pm_Message(0xC0, SON_PROGRAM, 0)); sonificationStarted = true; } void SonifyEndSonification() { if (sonMidiStream) Pm_Close(sonMidiStream); sonificationStarted = false; } void SonifyNoteOnOff(int p, int v) { if (!sonificationStarted) SonifyBeginSonification(); if (sonMidiStream) Pm_WriteShort(sonMidiStream, 0, Pm_Message(0x90, p, v)); } #define SONFNS(name) \ void SonifyBegin ## name() { SonifyNoteOnOff(SON_ ## name, SON_VEL); } \ void SonifyEnd ## name() { SonifyNoteOnOff(SON_ ## name, 0); } SONFNS(NoteBackground) SONFNS(NoteForeground) SONFNS(Measures) SONFNS(Serialize) SONFNS(Unserialize) SONFNS(ModifyState) SONFNS(AutoSave) #undef SONFNS #endif static ProjectFileIORegistry::Entry registerFactory{ wxT( "notetrack" ), []( AudacityProject &project ){ auto &tracks = TrackList::Get( project ); auto result = tracks.Add( std::make_shared()); TrackView::Get( *result ); TrackControls::Get( *result ); return result; } }; NoteTrack::NoteTrack() : NoteTrackBase() { SetDefaultName(_("Note Track")); SetName(GetDefaultName()); mSeq = NULL; mSerializationLength = 0; #ifdef EXPERIMENTAL_MIDI_OUT mVelocity = 0; #endif mBottomNote = MinPitch; mTopNote = MaxPitch; mVisibleChannels = ALL_CHANNELS; } NoteTrack::~NoteTrack() { } Alg_seq &NoteTrack::GetSeq() const { if (!mSeq) { if (!mSerializationBuffer) mSeq = std::make_unique(); else { std::unique_ptr alg_track { Alg_seq::unserialize ( mSerializationBuffer.get(), mSerializationLength ) }; wxASSERT(alg_track->get_type() == 's'); mSeq.reset( static_cast(alg_track.release()) ); // Preserve the invariant that at most one of the representations is // valid mSerializationBuffer.reset(); mSerializationLength = 0; } } wxASSERT(mSeq); return *mSeq; } Track::Holder NoteTrack::Clone() const { auto duplicate = std::make_shared(); duplicate->Init(*this); // The duplicate begins life in serialized state. Often the duplicate is // pushed on the Undo stack. Then we want to un-serialize it (or a further // copy) only on demand after an Undo. if (mSeq) { SonifyBeginSerialize(); wxASSERT(!mSerializationBuffer); // serialize from this to duplicate's mSerializationBuffer void *buffer; mSeq->serialize(&buffer, &duplicate->mSerializationLength); duplicate->mSerializationBuffer.reset( (char*)buffer ); SonifyEndSerialize(); } else if (mSerializationBuffer) { // Copy already serialized data. wxASSERT(!mSeq); duplicate->mSerializationLength = this->mSerializationLength; duplicate->mSerializationBuffer.reset ( safenew char[ this->mSerializationLength ] ); memcpy( duplicate->mSerializationBuffer.get(), this->mSerializationBuffer.get(), this->mSerializationLength ); } else { // We are duplicating a default-constructed NoteTrack, and that's okay } // copy some other fields here duplicate->SetBottomNote(mBottomNote); duplicate->SetTopNote(mTopNote); duplicate->mVisibleChannels = mVisibleChannels; duplicate->SetOffset(GetOffset()); #ifdef EXPERIMENTAL_MIDI_OUT duplicate->SetVelocity(GetVelocity()); #endif return duplicate; } double NoteTrack::GetOffset() const { return mOffset; } double NoteTrack::GetStartTime() const { return GetOffset(); } double NoteTrack::GetEndTime() const { return GetStartTime() + GetSeq().get_real_dur(); } void NoteTrack::WarpAndTransposeNotes(double t0, double t1, const TimeWarper &warper, double semitones) { double offset = this->GetOffset(); // track is shifted this amount auto &seq = GetSeq(); seq.convert_to_seconds(); // make sure time units are right t1 -= offset; // adjust time range to compensate for track offset t0 -= offset; if (t1 > seq.get_dur()) { // make sure t0, t1 are within sequence t1 = seq.get_dur(); if (t0 >= t1) return; } Alg_iterator iter(mSeq.get(), false); iter.begin(); Alg_event_ptr event; while (0 != (event = iter.next()) && event->time < t1) { if (event->is_note() && event->time >= t0) { event->set_pitch(event->get_pitch() + semitones); } } iter.end(); // now, use warper to warp the tempo map seq.convert_to_beats(); // beats remain the same Alg_time_map_ptr map = seq.get_time_map(); map->insert_beat(t0, map->time_to_beat(t0)); map->insert_beat(t1, map->time_to_beat(t1)); int i, len = map->length(); for (i = 0; i < len; i++) { Alg_beat &beat = map->beats[i]; beat.time = warper.Warp(beat.time + offset) - offset; } // about to redisplay, so might as well convert back to time now seq.convert_to_seconds(); } // Draws the midi channel toggle buttons within the given rect. // The rect should be evenly divisible by 4 on both axis. void NoteTrack::DrawLabelControls ( const NoteTrack *pTrack, wxDC & dc, const wxRect &rect, int highlightedChannel ) { dc.SetTextForeground(theTheme.Colour(clrLabelTrackText)); wxASSERT_MSG(rect.width % 4 == 0, "Midi channel control rect width must be divisible by 4"); wxASSERT_MSG(rect.height % 4 == 0, "Midi channel control rect height must be divisible by 4"); auto cellWidth = rect.width / 4; auto cellHeight = rect.height / 4; wxRect box; for (int row = 0; row < 4; row++) { for (int col = 0; col < 4; col++) { // chanName is the "external" channel number (1-16) // used by AColor and button labels int chanName = row * 4 + col + 1; box.x = rect.x + col * cellWidth; box.y = rect.y + row * cellHeight; box.width = cellWidth; box.height = cellHeight; bool visible = pTrack ? pTrack->IsVisibleChan(chanName - 1) : true; if (visible) { // highlightedChannel counts 0 based if ( chanName == highlightedChannel + 1 ) AColor::LightMIDIChannel(&dc, chanName); else AColor::MIDIChannel(&dc, chanName); dc.DrawRectangle(box); // two choices: channel is enabled (to see and play) when button is in // "up" position (original Audacity style) or in "down" position // #define CHANNEL_ON_IS_DOWN 1 #if CHANNEL_ON_IS_DOWN AColor::DarkMIDIChannel(&dc, chanName); #else AColor::LightMIDIChannel(&dc, chanName); #endif AColor::Line(dc, box.x, box.y, box.x + box.width - 1, box.y); AColor::Line(dc, box.x, box.y, box.x, box.y + box.height - 1); #if CHANNEL_ON_IS_DOWN AColor::LightMIDIChannel(&dc, chanName); #else AColor::DarkMIDIChannel(&dc, chanName); #endif AColor::Line(dc, box.x + box.width - 1, box.y, box.x + box.width - 1, box.y + box.height - 1); AColor::Line(dc, box.x, box.y + box.height - 1, box.x + box.width - 1, box.y + box.height - 1); } else { if ( chanName == highlightedChannel + 1 ) AColor::LightMIDIChannel(&dc, chanName); else AColor::MIDIChannel(&dc, 0); dc.DrawRectangle(box); #if CHANNEL_ON_IS_DOWN AColor::LightMIDIChannel(&dc, 0); #else AColor::DarkMIDIChannel(&dc, 0); #endif AColor::Line(dc, box.x, box.y, box.x + box.width - 1, box.y); AColor::Line(dc, box.x, box.y, box.x, box.y + box.height - 1); #if CHANNEL_ON_IS_DOWN AColor::DarkMIDIChannel(&dc, 0); #else AColor::LightMIDIChannel(&dc, 0); #endif AColor::Line(dc, box.x + box.width - 1, box.y, box.x + box.width - 1, box.y + box.height - 1); AColor::Line(dc, box.x, box.y + box.height - 1, box.x + box.width - 1, box.y + box.height - 1); } wxString text; wxCoord w; wxCoord h; text.Printf(wxT("%d"), chanName); dc.GetTextExtent(text, &w, &h); dc.DrawText(text, box.x + (box.width - w) / 2, box.y + (box.height - h) / 2); } } dc.SetTextForeground(theTheme.Colour(clrTrackPanelText)); AColor::MIDIChannel(&dc, 0); // always return with gray color selected } int NoteTrack::FindChannel(const wxRect &rect, int mx, int my) { wxASSERT_MSG(rect.width % 4 == 0, "Midi channel control rect width must be divisible by 4"); wxASSERT_MSG(rect.height % 4 == 0, "Midi channel control rect height must be divisible by 4"); auto cellWidth = rect.width / 4; auto cellHeight = rect.height / 4; int col = (mx - rect.x) / cellWidth; int row = (my - rect.y) / cellHeight; return row * 4 + col; } // Handles clicking within the midi controls rect (same as DrawLabelControls). // This is somewhat oddly written, as these aren't real buttons - they act // when the mouse goes down; you can't hold it pressed and move off of it. // Left-clicking toggles a single channel; right-clicking turns off all other channels. bool NoteTrack::LabelClick(const wxRect &rect, int mx, int my, bool right) { auto channel = FindChannel(rect, mx, my); if (right) SoloVisibleChan(channel); else ToggleVisibleChan(channel); return true; } void NoteTrack::SetSequence(std::unique_ptr &&seq) { mSeq = std::move(seq); } void NoteTrack::PrintSequence() { FILE *debugOutput; debugOutput = fopen("debugOutput.txt", "wt"); wxFprintf(debugOutput, "Importing MIDI...\n"); // This is called for debugging purposes. Do not compute mSeq on demand // with GetSeq() if (mSeq) { int i = 0; while(i < mSeq->length()) { wxFprintf(debugOutput, "--\n"); wxFprintf(debugOutput, "type: %c\n", ((Alg_event_ptr)mSeq->track_list.tracks[i])->get_type()); wxFprintf(debugOutput, "time: %f\n", ((Alg_event_ptr)mSeq->track_list.tracks[i])->time); wxFprintf(debugOutput, "channel: %li\n", ((Alg_event_ptr)mSeq->track_list.tracks[i])->chan); if(((Alg_event_ptr)mSeq->track_list.tracks[i])->get_type() == wxT('n')) { wxFprintf(debugOutput, "pitch: %f\n", ((Alg_note_ptr)mSeq->track_list.tracks[i])->pitch); wxFprintf(debugOutput, "duration: %f\n", ((Alg_note_ptr)mSeq->track_list.tracks[i])->dur); wxFprintf(debugOutput, "velocity: %f\n", ((Alg_note_ptr)mSeq->track_list.tracks[i])->loud); } else if(((Alg_event_ptr)mSeq->track_list.tracks[i])->get_type() == wxT('n')) { wxFprintf(debugOutput, "key: %li\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->get_identifier()); wxFprintf(debugOutput, "attribute type: %c\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type()); wxFprintf(debugOutput, "attribute: %s\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_name()); if(((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type() == wxT('r')) { wxFprintf(debugOutput, "value: %f\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.r); } else if(((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type() == wxT('i')) { wxFprintf(debugOutput, "value: %li\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.i); } else if(((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.attr_type() == wxT('s')) { wxFprintf(debugOutput, "value: %s\n", ((Alg_update_ptr)mSeq->track_list.tracks[i])->parameter.s); } else {} } i++; } } else { wxFprintf(debugOutput, "No sequence defined!\n"); } fclose(debugOutput); } Track::Holder NoteTrack::Cut(double t0, double t1) { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; double len = t1-t0; //auto delta = -( //( std::min( t1, GetEndTime() ) ) - ( std::max( t0, GetStartTime() ) ) //); auto newTrack = std::make_shared(); newTrack->Init(*this); auto &seq = GetSeq(); seq.convert_to_seconds(); newTrack->mSeq.reset(seq.cut(t0 - GetOffset(), len, false)); newTrack->SetOffset(0); // Not needed // Alg_seq::cut seems to handle this //AddToDuration( delta ); // What should be done with the rest of newTrack's members? //(mBottomNote, // mSerializationBuffer, mSerializationLength, mVisibleChannels) return newTrack; } Track::Holder NoteTrack::Copy(double t0, double t1, bool) const { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; double len = t1-t0; auto newTrack = std::make_shared(); newTrack->Init(*this); auto &seq = GetSeq(); seq.convert_to_seconds(); newTrack->mSeq.reset(seq.copy(t0 - GetOffset(), len, false)); newTrack->SetOffset(0); // What should be done with the rest of newTrack's members? // (mBottomNote, mSerializationBuffer, // mSerializationLength, mVisibleChannels) return newTrack; } bool NoteTrack::Trim(double t0, double t1) { if (t1 < t0) return false; auto &seq = GetSeq(); //auto delta = -( //( GetEndTime() - std::min( GetEndTime(), t1 ) ) + //( std::max(t0, GetStartTime()) - GetStartTime() ) //); seq.convert_to_seconds(); // DELETE way beyond duration just in case something is out there: seq.clear(t1 - GetOffset(), seq.get_dur() + 10000.0, false); // Now that stuff beyond selection is cleared, clear before selection: seq.clear(0.0, t0 - GetOffset(), false); // want starting time to be t0 SetOffset(t0); // Not needed // Alg_seq::clear seems to handle this //AddToDuration( delta ); return true; } void NoteTrack::Clear(double t0, double t1) { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; double len = t1-t0; auto &seq = GetSeq(); auto offset = GetOffset(); auto start = t0 - offset; if (start < 0.0) { // AlgSeq::clear will shift the cleared interval, not changing len, if // start is negative. That's not what we want to happen. if (len > -start) { seq.clear(0, len + start, false); SetOffset(t0); } else SetOffset(offset - len); } else { //auto delta = -( //( std::min( t1, GetEndTime() ) ) - ( std::max( t0, GetStartTime() ) ) //); seq.clear(start, len, false); // Not needed // Alg_seq::clear seems to handle this // AddToDuration( delta ); } } void NoteTrack::Paste(double t, const Track *src) { // Paste inserts src at time t. If src has a positive offset, // the offset is treated as silence which is also inserted. If // the offset is negative, the offset is ignored and the ENTIRE // src is inserted (otherwise, we would either lose data from // src by not inserting things at negative times, or inserting // things at negative times could overlap things already in // the destination track). //Check that src is a non-NULL NoteTrack bool bOk = src && src->TypeSwitch< bool >( [&](const NoteTrack *other) { auto myOffset = this->GetOffset(); if (t < myOffset) { // workaround strange behavior described at // http://bugzilla.audacityteam.org/show_bug.cgi?id=1735#c3 SetOffset(t); InsertSilence(t, myOffset - t); } double delta = 0.0; auto &seq = GetSeq(); auto offset = other->GetOffset(); if ( offset > 0 ) { seq.convert_to_seconds(); seq.insert_silence( t - GetOffset(), offset ); t += offset; // Is this needed or does Alg_seq::insert_silence take care of it? //delta += offset; } // This seems to be needed: delta += std::max( 0.0, t - GetEndTime() ); // This, not: //delta += other->GetSeq().get_real_dur(); seq.paste(t - GetOffset(), &other->GetSeq()); AddToDuration( delta ); return true; }); if ( !bOk ) // THROW_INCONSISTENCY_EXCEPTION; // ? (void)0;// intentionally do nothing } void NoteTrack::Silence(double t0, double t1) { if (t1 < t0) THROW_INCONSISTENCY_EXCEPTION; auto len = t1 - t0; auto &seq = GetSeq(); seq.convert_to_seconds(); // XXX: do we want to set the all param? // If it's set, then it seems like notes are silenced if they start or end in the range, // otherwise only if they start in the range. --Poke seq.silence(t0 - GetOffset(), len, false); } void NoteTrack::InsertSilence(double t, double len) { if (len < 0) THROW_INCONSISTENCY_EXCEPTION; auto &seq = GetSeq(); seq.convert_to_seconds(); seq.insert_silence(t - GetOffset(), len); // is this needed? // AddToDuration( len ); } #ifdef EXPERIMENTAL_MIDI_OUT void NoteTrack::SetVelocity(float velocity) { if (mVelocity != velocity) { mVelocity = velocity; Notify(); } } #endif // Call this function to manipulate the underlying sequence data. This is // NOT the function that handles horizontal dragging. bool NoteTrack::Shift(double t) // t is always seconds { if (t > 0) { auto &seq = GetSeq(); // insert an even number of measures seq.convert_to_beats(); // get initial tempo double tempo = seq.get_tempo(0.0); double beats_per_measure = seq.get_bar_len(0.0); int m = ROUND(t * tempo / beats_per_measure); // need at least 1 measure, so if we rounded down to zero, fix it if (m == 0) m = 1; // compute NEW tempo so that m measures at NEW tempo take t seconds tempo = beats_per_measure * m / t; // in beats per second seq.insert_silence(0.0, beats_per_measure * m); seq.set_tempo(tempo * 60.0 /* bpm */, 0.0, beats_per_measure * m); seq.write("afterShift.gro"); } else if (t < 0) { auto &seq = GetSeq(); seq.convert_to_seconds(); seq.clear(0, t, true); } else { // offset is zero, no modifications return false; } return true; } QuantizedTimeAndBeat NoteTrack::NearestBeatTime( double time ) const { // Alg_seq knows nothing about offset, so remove offset time double seq_time = time - GetOffset(); double beat; auto &seq = GetSeq(); seq_time = seq.nearest_beat_time(seq_time, &beat); // add the offset back in to get "actual" audacity track time return { seq_time + GetOffset(), beat }; } Track::Holder NoteTrack::PasteInto( AudacityProject & ) const { auto pNewTrack = std::make_shared(); pNewTrack->Paste(0.0, this); return pNewTrack; } auto NoteTrack::GetIntervals() const -> ConstIntervals { ConstIntervals results; results.emplace_back( GetStartTime(), GetEndTime() ); return results; } auto NoteTrack::GetIntervals() -> Intervals { Intervals results; results.emplace_back( GetStartTime(), GetEndTime() ); return results; } void NoteTrack::AddToDuration( double delta ) { auto &seq = GetSeq(); #if 0 // PRL: Would this be better ? seq.set_real_dur( seq.get_real_dur() + delta ); #else seq.convert_to_seconds(); seq.set_dur( seq.get_dur() + delta ); #endif } bool NoteTrack::StretchRegion ( QuantizedTimeAndBeat t0, QuantizedTimeAndBeat t1, double newDur ) { auto &seq = GetSeq(); bool result = seq.stretch_region( t0.second, t1.second, newDur ); if (result) { const auto oldDur = t1.first - t0.first; AddToDuration( newDur - oldDur ); } return result; } namespace { void swap(std::unique_ptr &a, std::unique_ptr &b) { std::unique_ptr tmp = std::move(a); a = std::move(b); b = std::move(tmp); } } Alg_seq *NoteTrack::MakeExportableSeq(std::unique_ptr &cleanup) const { cleanup.reset(); double offset = GetOffset(); if (offset == 0) return &GetSeq(); // make a copy, deleting events that are shifted before time 0 double start = -offset; if (start < 0) start = 0; // notes that begin before "start" are not included even if they // extend past "start" (because "all" parameter is set to false) cleanup.reset( GetSeq().copy(start, GetSeq().get_dur() - start, false) ); auto seq = cleanup.get(); if (offset > 0) { { // swap cleanup and mSeq so that Shift operates on the NEW copy swap( this->mSeq, cleanup ); auto cleanup2 = finally( [&] { swap( this->mSeq, cleanup ); } ); const_cast< NoteTrack *>( this )->Shift(offset); } #ifdef OLD_CODE // now shift events by offset. This must be done with an integer // number of measures, so first, find the beats-per-measure double beats_per_measure = 4.0; Alg_time_sig_ptr tsp = NULL; if (seq->time_sig.length() > 0 && seq->time_sig[0].beat < ALG_EPS) { // there is an initial time signature tsp = &(seq->time_sig[0]); beats_per_measure = (tsp->num * 4) / tsp->den; } // also need the initial tempo double bps = ALG_DEFAULT_BPM / 60; Alg_time_map_ptr map = seq->get_time_map(); Alg_beat_ptr bp = &(map->beats[0]); if (bp->time < ALG_EPS) { // tempo change at time 0 if (map->beats.len > 1) { // compute slope to get tempo bps = (map->beats[1].beat - map->beats[0].beat) / (map->beats[1].time - map->beats[0].time); } else if (seq->get_time_map()->last_tempo_flag) { bps = seq->get_time_map()->last_tempo; } } // find closest number of measures to fit in the gap // number of measures is offset / measure_time double measure_time = beats_per_measure / bps; // seconds per measure int n = ROUND(offset / measure_time); if (n == 0) n = 1; // we will insert n measures. Compute the desired duration of each. measure_time = offset / n; bps = beats_per_measure / measure_time; // insert integer multiple of measures at beginning seq->convert_to_beats(); seq->insert_silence(0, beats_per_measure * n); // make sure time signature at 0 is correct if (tsp) { seq->set_time_sig(0, tsp->num, tsp->den); } // adjust tempo to match offset seq->set_tempo(bps * 60.0, 0, beats_per_measure * n); #endif } else { auto &mySeq = GetSeq(); // if offset is negative, it might not be a multiple of beats, but // we want to preserve the relative positions of measures. I.e. we // should shift barlines and time signatures as well as notes. // Insert a time signature at the first bar-line if necessary. // Translate start from seconds to beats and call it beat: double beat = mySeq.get_time_map()->time_to_beat(start); // Find the time signature in mySeq in effect at start (beat): int i = mySeq.time_sig.find_beat(beat); // i is where you would insert a NEW time sig at beat, // Case 1: beat coincides with a time sig at i. Time signature // at beat means that there is a barline at beat, so when beat // is shifted to 0, the relative barline positions are preserved if (mySeq.time_sig.length() > 0 && within(beat, mySeq.time_sig[i].beat, ALG_EPS)) { // beat coincides with time signature change, so offset must // be a multiple of beats /* do nothing */ ; // Case 2: there is no time signature before beat. } else if (i == 0 && (mySeq.time_sig.length() == 0 || mySeq.time_sig[i].beat > beat)) { // If beat does not fall on an implied barline, we need to // insert a time signature. double measures = beat / 4.0; double imeasures = ROUND(measures); if (!within(measures, imeasures, ALG_EPS)) { double bar_offset = ((int)(measures) + 1) * 4.0 - beat; seq->set_time_sig(bar_offset, 4, 4); } // This case should never be true because if i == 0, either there // are no time signatures before beat (Case 2), // or there is one time signature at beat (Case 1) } else if (i == 0) { /* do nothing (might be good to assert(false)) */ ; // Case 3: i-1 must be the effective time sig position } else { i -= 1; // index the time signature in effect at beat Alg_time_sig_ptr tsp = &(mySeq.time_sig[i]); double beats_per_measure = (tsp->num * 4) / tsp->den; double measures = (beat - tsp->beat) / beats_per_measure; int imeasures = ROUND(measures); if (!within(measures, imeasures, ALG_EPS)) { // beat is not on a measure, so we need to insert a time sig // to force a bar line at the first measure location after // beat double bar = tsp->beat + beats_per_measure * ((int)(measures) + 1); double bar_offset = bar - beat; // insert NEW time signature at bar_offset in NEW sequence // It will have the same time signature, but the position will // force a barline to match the barlines in mSeq seq->set_time_sig(bar_offset, tsp->num, tsp->den); } // else beat coincides with a barline, so no need for an extra // time signature to force barline alignment } } return seq; } bool NoteTrack::ExportMIDI(const wxString &f) const { std::unique_ptr cleanup; auto seq = MakeExportableSeq(cleanup); bool rslt = seq->smf_write(f.mb_str()); return rslt; } bool NoteTrack::ExportAllegro(const wxString &f) const { double offset = GetOffset(); auto in_seconds = ImportExportPrefs::AllegroStyleSetting.ReadEnum(); auto &seq = GetSeq(); if (in_seconds) { seq.convert_to_seconds(); } else { seq.convert_to_beats(); } return seq.write(f.mb_str(), offset); } bool NoteTrack::HandleXMLTag(const wxChar *tag, const wxChar **attrs) { if (!wxStrcmp(tag, wxT("notetrack"))) { while (*attrs) { const wxChar *attr = *attrs++; const wxChar *value = *attrs++; if (!value) break; const wxString strValue = value; long nValue; double dblValue; if (this->Track::HandleCommonXMLAttribute(attr, strValue)) ; else if (this->NoteTrackBase::HandleXMLAttribute(attr, value)) {} else if (!wxStrcmp(attr, wxT("offset")) && XMLValueChecker::IsGoodString(strValue) && Internat::CompatibleToDouble(strValue, &dblValue)) SetOffset(dblValue); else if (!wxStrcmp(attr, wxT("visiblechannels"))) { if (!XMLValueChecker::IsGoodInt(strValue) || !strValue.ToLong(&nValue) || !XMLValueChecker::IsValidVisibleChannels(nValue)) return false; mVisibleChannels = nValue; } #ifdef EXPERIMENTAL_MIDI_OUT else if (!wxStrcmp(attr, wxT("velocity")) && XMLValueChecker::IsGoodString(strValue) && Internat::CompatibleToDouble(strValue, &dblValue)) mVelocity = (float) dblValue; #endif else if (!wxStrcmp(attr, wxT("bottomnote")) && XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue)) SetBottomNote(nValue); else if (!wxStrcmp(attr, wxT("topnote")) && XMLValueChecker::IsGoodInt(strValue) && strValue.ToLong(&nValue)) SetTopNote(nValue); else if (!wxStrcmp(attr, wxT("data"))) { std::string s(strValue.mb_str(wxConvUTF8)); std::istringstream data(s); mSeq = std::make_unique(data, false); } } // while return true; } return false; } XMLTagHandler *NoteTrack::HandleXMLChild(const wxChar * WXUNUSED(tag)) { return NULL; } void NoteTrack::WriteXML(XMLWriter &xmlFile) const // may throw { std::ostringstream data; Track::Holder holder; const NoteTrack *saveme = this; if (!mSeq) { // replace saveme with an (unserialized) duplicate, which is // destroyed at end of function. holder = Clone(); saveme = static_cast(holder.get()); } saveme->GetSeq().write(data, true); xmlFile.StartTag(wxT("notetrack")); saveme->Track::WriteCommonXMLAttributes( xmlFile ); this->NoteTrackBase::WriteXMLAttributes(xmlFile); xmlFile.WriteAttr(wxT("offset"), saveme->GetOffset()); xmlFile.WriteAttr(wxT("visiblechannels"), saveme->mVisibleChannels); #ifdef EXPERIMENTAL_MIDI_OUT xmlFile.WriteAttr(wxT("velocity"), (double) saveme->mVelocity); #endif xmlFile.WriteAttr(wxT("bottomnote"), saveme->mBottomNote); xmlFile.WriteAttr(wxT("topnote"), saveme->mTopNote); xmlFile.WriteAttr(wxT("data"), wxString(data.str().c_str(), wxConvUTF8)); xmlFile.EndTag(wxT("notetrack")); } void NoteTrack::SetBottomNote(int note) { if (note < MinPitch) note = MinPitch; else if (note > 96) note = 96; wxCHECK(note <= mTopNote, ); mBottomNote = note; } void NoteTrack::SetTopNote(int note) { if (note > MaxPitch) note = MaxPitch; wxCHECK(note >= mBottomNote, ); mTopNote = note; } void NoteTrack::SetNoteRange(int note1, int note2) { // Bounds check if (note1 > MaxPitch) note1 = MaxPitch; else if (note1 < MinPitch) note1 = MinPitch; if (note2 > MaxPitch) note2 = MaxPitch; else if (note2 < MinPitch) note2 = MinPitch; // Swap to ensure ordering if (note2 < note1) { auto tmp = note1; note1 = note2; note2 = tmp; } mBottomNote = note1; mTopNote = note2; } void NoteTrack::ShiftNoteRange(int offset) { // Ensure everything stays in bounds if (mBottomNote + offset < MinPitch || mTopNote + offset > MaxPitch) return; mBottomNote += offset; mTopNote += offset; } #if 0 void NoteTrack::StartVScroll() { mStartBottomNote = mBottomNote; } void NoteTrack::VScroll(int start, int end) { int ph = GetPitchHeight(); int delta = ((end - start) + ph / 2) / ph; ShiftNoteRange(delta); } #endif void NoteTrack::Zoom(const wxRect &rect, int y, float multiplier, bool center) { NoteTrackDisplayData data = NoteTrackDisplayData(this, rect); int clickedPitch = data.YToIPitch(y); int extent = mTopNote - mBottomNote + 1; int newExtent = (int) (extent / multiplier); float position; if (center) { // center the pitch that the user clicked on position = .5; } else { // align to keep the pitch that the user clicked on in the same place position = extent / (clickedPitch - mBottomNote); } int newBottomNote = clickedPitch - (newExtent * position); int newTopNote = clickedPitch + (newExtent * (1 - position)); SetNoteRange(newBottomNote, newTopNote); } void NoteTrack::ZoomTo(const wxRect &rect, int start, int end) { wxRect trackRect(0, rect.GetY(), 1, rect.GetHeight()); NoteTrackDisplayData data = NoteTrackDisplayData(this, trackRect); int pitch1 = data.YToIPitch(start); int pitch2 = data.YToIPitch(end); if (pitch1 == pitch2) { // Just zoom in instead of zooming to show only one note Zoom(rect, start, 1, true); return; } // It's fine for this to be in either order SetNoteRange(pitch1, pitch2); } void NoteTrack::ZoomAllNotes() { Alg_iterator iterator( &GetSeq(), false ); iterator.begin(); Alg_event_ptr evt; // Go through all of the notes, finding the minimum and maximum value pitches. bool hasNotes = false; int minPitch = MaxPitch; int maxPitch = MinPitch; while (NULL != (evt = iterator.next())) { if (evt->is_note()) { int pitch = (int) evt->get_pitch(); hasNotes = true; if (pitch < minPitch) minPitch = pitch; if (pitch > maxPitch) maxPitch = pitch; } } if (!hasNotes) { // Semi-arbitrary default values: minPitch = 48; maxPitch = 72; } SetNoteRange(minPitch, maxPitch); } NoteTrackDisplayData::NoteTrackDisplayData(const NoteTrack* track, const wxRect &r) { auto span = track->GetTopNote() - track->GetBottomNote() + 1; // + 1 to make sure it includes both mMargin = std::min((int) (r.height / (float)(span)) / 2, r.height / 4); // Count the number of dividers between B/C and E/F int numC = 0, numF = 0; auto botOctave = track->GetBottomNote() / 12, botNote = track->GetBottomNote() % 12; auto topOctave = track->GetTopNote() / 12, topNote = track->GetTopNote() % 12; if (topOctave == botOctave) { if (botNote == 0) numC = 1; if (topNote <= 5) numF = 1; } else { numC = topOctave - botOctave; numF = topOctave - botOctave - 1; if (botNote == 0) numC++; if (botNote <= 5) numF++; if (topOctave <= 5) numF++; } // Effective space, excluding the margins and the lines between some notes auto effectiveHeight = r.height - (2 * (mMargin + 1)) - numC - numF; // Guaranteed that both the bottom and top notes will be visible // (assuming that the clamping below does not happen) mPitchHeight = effectiveHeight / ((float) span); if (mPitchHeight < MinPitchHeight) mPitchHeight = MinPitchHeight; if (mPitchHeight > MaxPitchHeight) mPitchHeight = MaxPitchHeight; mBottom = r.y + r.height - GetNoteMargin() - 1 - GetPitchHeight(1) + botOctave * GetOctaveHeight() + GetNotePos(botNote); } int NoteTrackDisplayData::IPitchToY(int p) const { return mBottom - (p / 12) * GetOctaveHeight() - GetNotePos(p % 12); } int NoteTrackDisplayData::YToIPitch(int y) const { y = mBottom - y; // pixels above pitch 0 int octave = (y / GetOctaveHeight()); y -= octave * GetOctaveHeight(); // result is approximate because C and G are one pixel taller than // mPitchHeight. // Poke 1-13-18: However in practice this seems not to be an issue, // as long as we use mPitchHeight and not the rounded version return (y / mPitchHeight) + octave * 12; } const float NoteTrack::ZoomStep = powf( 2.0f, 0.25f ); #endif // USE_MIDI