1 /**
2 * Copyright © Underground Rekordz 2019
3 * License: MIT (https://github.com/UndergroundRekordz/Musicpulator/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module musicpulator.songmelody;
7 
8 import std.algorithm : map, joiner;
9 import std.array : array, join;
10 import std..string : format;
11 
12 import musicpulator.songnote;
13 import musicpulator.musicalscale;
14 import musicpulator.musicalprogression;
15 import musicpulator.tools;
16 import musicpulator.core;
17 
18 /// Alias for the note collection.
19 private alias NoteCollection = InternalMaxSizeCollection!(SongNote, 32);
20 
21 /// Wrapper around a song melody.
22 final class SongMelody
23 {
24   private:
25   /// The scales of the melody.
26   MusicalScale[] _scales;
27   /// Boolean determining whether the melody has found its scale or not yet.
28   bool _foundScale;
29   /// The progression of the melody.
30   MusicalProgression _progression;
31   /// Boolean determining whether the melody has found its progression or not yet.
32   bool _foundProgression;
33   /// The notes of the melody.
34   NoteCollection _notes;
35 
36   public:
37   final:
38   /**
39   * Creates a new song melody.
40   * Params:
41   *   notes = The notes of the melody.
42   */
43   this(SongNote[] notes)
44   {
45     if (notes && notes.length)
46     {
47       foreach (note; notes.map!(n => new SongNote(n.note, n.length, n.step, n.octave, n.bar)))
48       {
49         _notes.add(note);
50       }
51     }
52   }
53 
54   /// Creates a new song melody.
55   this()
56   {
57 
58   }
59 
60   @property
61   {
62     /// Gets the notes of the melody.
63     NoteCollection notes() { return _notes; }
64 
65     /// Gets the scales of the melody.
66     const(MusicalScale[]) scales()
67     {
68       if (!_foundScale)
69       {
70         _foundScale = true;
71 
72         _scales = findScales(_notes.map!(n => n.note).array);
73       }
74 
75       return _scales;
76     }
77 
78     /// Gets the progression of the melody.
79     MusicalProgression progression()
80     {
81       import musicpulator.notes;
82       import musicpulator.direction;
83       import musicpulator.melodies;
84 
85       if (!_foundProgression)
86       {
87         _foundProgression = true;
88 
89         NoteId[] noteIds;
90 
91         foreach (note; _notes)
92         {
93           noteIds ~= getNoteId(note.note, note.octave);
94         }
95 
96         if (noteIds && noteIds.length)
97         {
98           auto directions = getNoteDirections(noteIds);
99 
100           if (directions)
101           {
102             _progression = getProgression(directions);
103           }
104         }
105       }
106 
107       return _progression;
108     }
109   }
110 
111   /**
112   * Appends a note to the melody.
113   * Params:
114   *   note = The note to append.
115   */
116   void appendNote(SongNote note)
117   {
118     if (!note)
119     {
120       return;
121     }
122 
123     _notes.add(note);
124 
125     _foundScale = false;
126     _foundProgression = false;
127   }
128 
129   /// Converts the musical notes to their relative flat notes.
130   void convertToFlat()
131   {
132     foreach (note; _notes)
133     {
134       note.convertToFlat;
135     }
136 
137     _foundScale = false;
138     _foundProgression = false;
139   }
140 
141   /// Converts the musical notes from flat notes to their relative notes.
142   void convertFromFlat()
143   {
144     foreach (note; _notes)
145     {
146       note.convertFromFlat();
147     }
148 
149     _foundScale = false;
150     _foundProgression = false;
151   }
152 
153   /// Converts the melody to a string. This will call toJson().
154   override string toString()
155   {
156     return toJson();
157   }
158 
159   /// Converts the melody to json.
160   string toJson()
161   {
162     auto notesJson = "[";
163 
164     if (_notes.length)
165     {
166       foreach (note; _notes)
167       {
168         notesJson ~= note.toJson() ~ ",";
169       }
170 
171       notesJson.length -= 1;
172     }
173 
174     notesJson ~= "]";
175 
176     auto scalesJson = "[";
177 
178     if (scales && _scales.length)
179     {
180       foreach (scale; _scales)
181       {
182         scalesJson ~= scale.toJson() ~ ",";
183       }
184 
185       scalesJson.length -= 1;
186     }
187 
188     scalesJson ~= "]";
189 
190     return `{"notes":%s,"scales":%s,"progression":%s}`
191       .format(notesJson, scalesJson, progression.toJson());
192   }
193 
194   /// Converts the melody to xml.
195   string toXml()
196   {
197     auto noteXml = "";
198 
199     if (_notes.length)
200     {
201       noteXml = _notes.map!(n => n.toXml()).array.join;
202     }
203 
204     auto scaleXml = "";
205 
206     if (scales && scales.length)
207     {
208       scaleXml = _scales.map!(s => s.toXml()).array.join;
209     }
210 
211     return `<SongMelody progression="%s"><SongNotes>%s</SongNotes><Scales>%s</Scales></SongMelody>`
212       .format(progression.toXml(), noteXml, scaleXml);
213   }
214 }