[go: up one dir, main page]

Menu

[r19]: / trunk / SongState.cpp  Maximize  Restore  History

Download this file

374 lines (344 with data), 13.0 kB

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
// Copyleft 2014 Chris Korda
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation; either version 2 of the License, or any later version.
/*
chris korda
revision history:
rev date comments
00 07may14 initial version
01 16may14 in MoveChords, rebuild section map before inserting
02 23jul14 add InsertSection
03 09sep14 use default memberwise copy
04 18sep14 add Transpose and ChangeLength
05 10jun15 in Transpose, handle negative bass note
song editing container
*/
#include "stdafx.h"
#include "Resource.h"
#include "SongState.h"
#include "SectionPropsDlg.h"
#include <math.h> // for fabs in ChangeLength
void CSongState::MakeSectionMap()
{
int nChords = m_Chord.GetSize();
m_SectionMap.SetSize(nChords); // one map element for each chord
int iSection = 0;
int iBeat = 0;
for (int iChord = 0; iChord < nChords; iChord++) { // for each chord
if (iBeat > m_Section[iSection].End()) // if reached end of section
iSection++; // advance to next section
m_SectionMap[iChord] = iSection; // store section index in map element
iBeat += m_Chord[iChord].m_Duration;
}
}
void CSongState::MakeSections()
{
CSong::CSectionArray Section;
CStringArrayEx SectionName;
int nChords = m_SectionMap.GetSize();
if (nChords) { // if at least one chord
int iPrevSec = -1;
CSong::CSection sec(0, 0, 0);
int iBeat = 0;
for (int iChord = 0; iChord < nChords; iChord++) { // for each chord
int iSection = m_SectionMap[iChord];
if (iSection != iPrevSec) { // if section changed
int nSecLen = iBeat - sec.m_Start; // compute section length
if (nSecLen > 0) { // if previous section pending
sec.m_Length = nSecLen; // update section length
Section.Add(sec); // add section to output array
SectionName.Add(m_SectionName[iPrevSec]);
}
sec = m_Section[iSection];
sec.m_Start = iBeat;
iPrevSec = iSection;
}
iBeat += m_Chord[iChord].m_Duration;
}
int nSecLen = iBeat - sec.m_Start; // compute final section length
if (nSecLen > 0) { // if previous section pending
sec.m_Length = nSecLen; // update section length
Section.Add(sec); // add section to output array
SectionName.Add(m_SectionName[iPrevSec]);
}
}
m_Section = Section;
m_SectionName = SectionName;
}
void CSongState::MergeDuplicateChords()
{
int nChords = m_Chord.GetSize();
int iChord = 1;
while (iChord < nChords) { // for each chord
const CSong::CChord& chord = m_Chord[iChord];
CSong::CChord& PrevChord = m_Chord[iChord - 1];
// if chord and previous chord are identical except for duration
if (chord.EqualNoDuration(PrevChord)
&& m_SectionMap[iChord] == m_SectionMap[iChord - 1]) { // and belong to same section
PrevChord.m_Duration += chord.m_Duration; // sum durations
RemoveAt(iChord); // delete duplicate chord
nChords--;
} else // chords differ
iChord++;
}
}
void CSongState::OnChordCountChange()
{
MergeDuplicateChords();
MakeSections(); // rebuild sections from section map
}
int CSongState::GetStartBeat(int ChordIdx) const
{
int iBeat = 0;
for (int iChord = 0; iChord < ChordIdx; iChord++) // for each preceding chord
iBeat += m_Chord[iChord].m_Duration; // add chord's duration to sum
return(iBeat);
}
int CSongState::FindChord(int Beat, int& Offset) const
{
int dur = 0;
int nChords = GetChordCount();
for (int iChord = 0; iChord < nChords; iChord++) { // for each chord
int NewDur = dur + m_Chord[iChord].m_Duration;
if (Beat < NewDur) { // if target beat lies within chord
// compute target beat's offset from start of containing chord, in beats
Offset = Beat - dur;
return(iChord); // return containing chord's index
}
dur = NewDur;
}
return(-1); // target beat not found
}
CIntRange CSongState::FindChordRange(CIntRange BeatRange, CIntRange& Offset) const
{
CIntRange ChordRange;
ChordRange.Start = FindChord(BeatRange.Start, Offset.Start);
ChordRange.End = FindChord(BeatRange.End, Offset.End);
ASSERT(IsValidChordIndex(ChordRange.Start));
ASSERT(IsValidChordIndex(ChordRange.End));
// reverse direction of ending offset, so chord boundary is zero
Offset.End = m_Chord[ChordRange.End].m_Duration - Offset.End - 1;
return(ChordRange);
}
int CSongState::GetSection(int ChordIdx)
{
int iSection;
if (ChordIdx < GetChordCount()) // if index within chord array
iSection = m_SectionMap[ChordIdx]; // return this chord's section
else { // inserting at end of chord array
if (ChordIdx > 0) // if at least one chord
iSection = m_SectionMap[ChordIdx - 1]; // return last chord's section
else { // empty chord array
// assume section's start and length will be set by MakeSections
CSong::CSection sec(0, 0, 1, CSong::CSection::F_IMPLICIT);
m_Section.Add(sec); // create initial section
m_SectionName.Add(_T(""));
iSection = 0; // return newly created section
}
}
return(iSection);
}
void CSongState::InsertAt(int ChordIdx, CSong::CChord Chord)
{
int iSection = GetSection(ChordIdx);
m_Chord.InsertAt(ChordIdx, Chord);
m_SectionMap.InsertAt(ChordIdx, iSection); // add chord to section map
}
void CSongState::InsertAt(int ChordIdx, const CSong::CChordArray& Chord)
{
int iSection = GetSection(ChordIdx);
m_Chord.InsertAt(ChordIdx, const_cast<CSong::CChordArray*>(&Chord));
int iEndChord = ChordIdx + Chord.GetSize();
for (int iChord = ChordIdx; iChord < iEndChord; iChord++) // for each chord
m_SectionMap.InsertAt(iChord, iSection); // add chord to section map
}
void CSongState::RemoveAt(int ChordIdx, int Count)
{
m_Chord.RemoveAt(ChordIdx, Count);
m_SectionMap.RemoveAt(ChordIdx, Count);
}
void CSongState::GetChords(CIntRange BeatRange, CSong::CChordArray& Chord) const
{
CIntRange ChordRange, Offset;
ChordRange = FindChordRange(BeatRange, Offset);
int nChords = ChordRange.LengthInclusive();
Chord.SetSize(nChords);
for (int iChord = 0; iChord < nChords; iChord++) // for each chord in range
Chord[iChord] = m_Chord[ChordRange.Start + iChord]; // copy to destination
// compensate durations of first and last chords for beat offsets
Chord[0].m_Duration -= Offset.Start;
Chord[nChords - 1].m_Duration -= Offset.End;
}
void CSongState::InsertChords(int Beat, const CSong::CChordArray& Chord)
{
int Offset;
int iChord = FindChord(Beat, Offset);
if (iChord < 0) {
iChord = GetChordCount();
Offset = 0;
}
if (Offset) { // if beat not on chord boundary
InsertAt(iChord, m_Chord[iChord]); // split target chord
m_Chord[iChord].m_Duration = Offset; // pre-insert portion
iChord++; // move insert position between two portions
m_Chord[iChord].m_Duration -= Offset; // post-insert portion
}
InsertAt(iChord, Chord); // insert caller's chord array
OnChordCountChange();
}
void CSongState::RemoveChords(CIntRange BeatRange)
{
CIntRange ChordRange, Offset;
ChordRange = FindChordRange(BeatRange, Offset);
if (Offset.Start) { // if starting beat not on chord boundary
m_Chord[ChordRange.Start].m_Duration = Offset.Start;
ChordRange.Start++;
}
if (Offset.End) { // if ending beat not on chord boundary
if (ChordRange.Start > ChordRange.End) // if beat range within one chord
m_Chord[ChordRange.End].m_Duration += Offset.End;
else { // beat range spans at least two chords
m_Chord[ChordRange.End].m_Duration = Offset.End;
ChordRange.End--;
}
}
int nChords = ChordRange.LengthInclusive();
if (nChords > 0)
RemoveAt(ChordRange.Start, nChords);
OnChordCountChange();
}
int CSongState::MoveChords(CIntRange BeatRange, int Beat)
{
CSong::CChordArray chord;
GetChords(BeatRange, chord);
RemoveChords(BeatRange);
MakeSectionMap(); // must rebuild section map before inserting
if (Beat > BeatRange.End)
Beat -= BeatRange.LengthInclusive();
Beat = min(Beat, CSong::CountBeats(m_Chord));
InsertChords(Beat, chord);
return(Beat); // return possibly updated beat
}
void CSongState::SetChord(CIntRange BeatRange, const CSong::CChord& Chord)
{
RemoveChords(BeatRange);
MakeSectionMap(); // must rebuild section map before inserting
CSong::CChordArray ch;
ch.SetSize(1);
ch[0] = Chord; // copy caller's chord
ch[0].m_Duration = BeatRange.LengthInclusive(); // same duration as beat range
InsertChords(BeatRange.Start, ch); // insert chord at start of beat range
}
void CSongState::AssignToSection(CIntRange BeatRange, int SectionIdx)
{
CIntRange ChordRange, Offset;
ChordRange = FindChordRange(BeatRange, Offset);
// for each chord in range
for (int iChord = ChordRange.Start; iChord <= ChordRange.End; iChord++)
m_SectionMap[iChord] = SectionIdx; // assign chord to section
MakeSections(); // rebuild sections from section map
}
void CSongState::MergeImplicitSections()
{
int iImplicit = m_Section.FindImplicit(); // find first implicit section
if (iImplicit < 0) // if no implicit sections exist
return; // nothing to do
int nChords = GetChordCount();
for (int iChord = 0; iChord < nChords; iChord++) { // for each chord
// if chord belongs to any implicit section
if (m_Section[m_SectionMap[iChord]].Implicit())
m_SectionMap[iChord] = iImplicit; // assign it to first implicit section
}
MakeSections(); // rebuild sections from section map
}
void CSongState::CreateSection(CIntRange BeatRange)
{
CSong::CSection sec(BeatRange.Start, BeatRange.LengthInclusive(), 0);
InsertSection(sec, _T(""));
}
bool CSongState::InsertSection(CSong::CSection& Section, CString Name)
{
CIntRange BeatRange(Section.m_Start, Section.End());
if (BeatRange.Start < 0 || BeatRange.End >= CSong::CountBeats(m_Chord))
return(FALSE); // section not within song
int iSection = INT64TO32(m_Section.Add(Section));
m_SectionName.Add(Name);
AssignToSection(BeatRange, iSection);
return(TRUE);
}
bool CSongState::DeleteSection(int Beat)
{
int iSection = m_Section.FindBeat(Beat);
// if section not found, or section is implicit
if (iSection < 0 || m_Section[iSection].Implicit())
return(FALSE);
// delete section by making it implicit
m_Section[iSection].m_Flags = CSong::CSection::F_IMPLICIT;
m_Section[iSection].m_Repeat = 1; // repeat count must be one
m_SectionName[iSection].Empty(); // reset section name too
MergeImplicitSections();
return(TRUE);
}
bool CSongState::EditSectionProperties(int Beat)
{
int iSection = m_Section.FindBeat(Beat);
// if section not found, or section is implicit
if (iSection < 0 || m_Section[iSection].Implicit())
return(FALSE);
CSong::CSection sec(m_Section[iSection]);
CString name(m_SectionName[iSection]);
CSectionPropsDlg dlg(sec, name);
if (dlg.DoModal() != IDOK)
return(FALSE);
// if section start or length changed
if (sec.m_Start != m_Section[iSection].m_Start
|| sec.m_Length != m_Section[iSection].m_Length) {
if (!DeleteSection(Beat)) // delete section
return(FALSE);
MakeSectionMap(); // rebuild section map before inserting
if (!InsertSection(sec, name)) // reinsert section
return(FALSE);
} else { // start and length unchanged
m_Section[iSection] = sec;
m_SectionName[iSection] = name;
}
return(TRUE);
}
void CSongState::RemoveSectionMap()
{
m_SectionMap.RemoveAll();
}
void CSongState::Transpose(CIntRange BeatRange, int Steps)
{
CIntRange ChordRange, Offset;
ChordRange = FindChordRange(BeatRange, Offset);
for (int iChord = ChordRange.Start; iChord <= ChordRange.End; iChord++) {
CSong::CChord& ch = m_Chord[iChord];
ch.m_Root = CNote(ch.m_Root + Steps).Normal();
if (ch.m_Bass >= 0) // if slash chord
ch.m_Bass = CNote(ch.m_Bass + Steps).Normal();
}
}
bool CSongState::ChangeLength(CIntRange& BeatRange, double Scale)
{
CIntRange ChordRange, Offset;
ChordRange = FindChordRange(BeatRange, Offset);
bool RoundingError = FALSE;
int NewSelDur = 0;
for (int iChord = ChordRange.Start; iChord <= ChordRange.End; iChord++) {
CSong::CChord& ch = m_Chord[iChord];
double fDur = ch.m_Duration * Scale; // scale chord duration
int iDur = round(fDur); // chord durations are stored as integers
if (fabs(iDur - fDur) > .01) // if rounding error exceeds +/- 1%
RoundingError = TRUE; // set flag
iDur = max(iDur, 1); // enforce minimum duration
ch.m_Duration = iDur; // update chord duration
NewSelDur += iDur; // add duration to selection length
}
MakeSections(); // rebuild sections from section map
// return scaled beat range to caller by overwriting beat range argument
int iStartBeat = GetStartBeat(ChordRange.Start);
BeatRange = CIntRange(iStartBeat, iStartBeat + NewSelDur - 1);
return(!RoundingError); // return false if rounding errors occurred
}