Merge lp:~mixxxdevelopers/mixxx/features_trackbeats into lp:~mixxxdevelopers/mixxx/trunk

Proposed by Phillip Whelan
Status: Merged
Merged at revision: 2654
Proposed branch: lp:~mixxxdevelopers/mixxx/features_trackbeats
Merge into: lp:~mixxxdevelopers/mixxx/trunk
Diff against target: 1529 lines (+960/-92)
20 files modified
mixxx/build/depends.py (+4/-0)
mixxx/res/schema.xml (+9/-0)
mixxx/src/analyserbpm.cpp (+12/-7)
mixxx/src/analyserbpm.h (+0/-2)
mixxx/src/basetrackplayer.cpp (+2/-1)
mixxx/src/library/dao/trackdao.cpp (+79/-16)
mixxx/src/library/trackcollection.cpp (+1/-1)
mixxx/src/track/beatfactory.cpp (+43/-0)
mixxx/src/track/beatfactory.h (+19/-0)
mixxx/src/track/beatgrid.cpp (+186/-0)
mixxx/src/track/beatgrid.h (+75/-0)
mixxx/src/track/beatmatrix.cpp (+271/-0)
mixxx/src/track/beatmatrix.h (+67/-0)
mixxx/src/track/beats.h (+106/-0)
mixxx/src/trackinfoobject.cpp (+37/-0)
mixxx/src/trackinfoobject.h (+14/-1)
mixxx/src/waveform/waveformrenderbeat.cpp (+18/-45)
mixxx/src/waveform/waveformrenderbeat.h (+7/-12)
mixxx/src/waveform/waveformrenderer.cpp (+9/-6)
mixxx/src/waveform/waveformrenderer.h (+1/-1)
To merge this branch: bzr merge lp:~mixxxdevelopers/mixxx/features_trackbeats
Reviewer Review Type Date Requested Status
Albert Santoni Approve
RJ Skerry-Ryan Approve
Review via email: mp+49371@code.launchpad.net

This proposal supersedes a proposal from 2011-02-04.

Description of the change

This branch adds the TrackBeats data type for tracking beats as sample offsets. This version uses SoundTouch to derive the beats. A future version will work with aubio, possibly only in the Analyse pane.

To post a comment you must log in.
2536. By RJ Skerry-Ryan

Add Beats abstract interface with a BeatGrid implementation.

2537. By RJ Skerry-Ryan

Tighten up the Beats/BeatGrid interface a little bit. Add a reimplementation of TrackBeats as BeatMatrix

2538. By RJ Skerry-Ryan

Remove TrackBeats. Change TIO, Analyser, and Waveform to use Beats generic interface instead. Add versioned serialization to TrackDAO. Move creation/destruction of the Beats variants into a BeatFactory class. Most awesome class name, ever.

2539. By RJ Skerry-Ryan

Fix lack of deletion of RenderObjects in WaveformRenderer

2540. By RJ Skerry-Ryan

Add destructor traces for TIO. Delete the WaveformRenderer.

2541. By RJ Skerry-Ryan

Tracks are no longer being deleted. Turning on deletion print in TrackDAO. Clear the track cache when TrackDAO is destroyed.

Revision history for this message
RJ Skerry-Ryan (rryan) wrote :

Hey Phil,

Nice work. I went ahead and started hacking on it. Here's the rough outline of what I did:

1) Serialized beats had no version associated with them, so we have no way to deprecate a given way of generating them. I added a beats_version column in the schema to go along w/ the beats blob.

2) I wrote an abstract interface I called "Beats" which is the core of the methods behind TrackBeats, except without all the Seconds variants of the methods. I think it's best just to support one and make the caller deal w/ it. It makes the interface simpler.

3) I expanded the interface to cover measuring the average BPM, scaling the entire range, also finding the beat closest to a given sample.

4) I added a capabilities enum to the Beats so that a Beats implementation can express whether it supports mutation, translation, scaling, add/removing beats, and moving beats.

5) I made two Beats subclasses -- BeatGrid and BeatMatrix. A BeatGrid is a mathematically-backed beatgrid from an average BPM and a sample location of the first beat. A BeatMatrix is just TrackBeats renamed, it stores a BeatList of beat sample locations. I didn't really think it made sense to use a BeatMatrix for the transition, since it would waste a lot of database space from something that could be mathematically generated for free. A BeatGrid is serialized to the database as just those two doubles.

6) I updated the WaveformRenderBeat and AnalyserBpm to use the track's Beats instance.

7) I created a BeatFactory class to handle the creation and deletion of Beats instances.

8) Fixed some merge issues with trunk, some parameters in SQL statements were dropped.

I think you mentioned you saw the Beats were not getting destroyed. I see a bigger problem now which is that the Track's are not getting destroyed either. I think that this is a bug in Mixxx trunk right now too, so hopefully it gets fixed when it's fixed there. There are a lot of random parts of Mixxx that are not getting deleted correctly, so we just have to find all these.

Since I'm hacking on this too, I think Albert should be the branch reviewer.

review: Approve
2542. By RJ Skerry-Ryan

Fix SQL statement that got munged by a merge.

Revision history for this message
RJ Skerry-Ryan (rryan) wrote :

Oops, accidentally approved. Ignore that.

2543. By RJ Skerry-Ryan

Rephrase :wavesummaryhex binding to match :beats

2544. By RJ Skerry-Ryan

Doh. Forgot to add BeatFactory files

2545. By Phillip Whelan

FIX: error in ternary syntax.

2546. By Phillip Whelan

FIX: generate BeatGrid if we do not have a beatgrid but we have detected the BPM of the song already.

2547. By Phillip Whelan

Various changes will bring the new Beats classes into feature parity with the
old trackbeats code.

  * beatfactory.cpp: include beatgrid.h and beatmatrix.h because forward
                     declaration proved to not be enough.
  * beats.h, beatgrid.h, beatmatrix.h: adding findNthBeat method.
  * beatgrid.cpp: many changes and fixes, including adding findNthBeat.
      - implement findNthBeat. calculate the beat using m_dFirstBeat.
      - reimplement findPrevBeat and findNextBeat using findNthBeat.
      - replace all other beat calculations with calls to findNextBeat.
  * beatmatrix.cpp:
      - implement findNthBeat.
      - reimplement findPrevBeat and findNextBeat using findNthBeat.

2548. By RJ Skerry-Ryan

Add language to beats.h that makes clear the behavior of findNextBeat and findPrevBeat when dSamples refers to the location of a beat.

2549. By RJ Skerry-Ryan

BeatMatrix::findNthBeat fixes for going in the negative direction. Changed variable from offset to n and updated comments to reflect that.

2550. By RJ Skerry-Ryan

Fixup BeatGrid::findNextBeat a little bit. It wasn't doing the previous direction correctly.

2551. By RJ Skerry-Ryan

Add some parameter checks to BeatGrid.

2552. By RJ Skerry-Ryan

Move BeatGrid legacy check from AnalyserBpm to TrackDAO so that any track loaded from DB will get a BeatGrid if it does not have one already and already has a BPM (or if the file metadata has a stored BPM)

2553. By Phillip Whelan

Merging from trunk.

2554. By RJ Skerry-Ryan

Move beat-related classes to track/

Revision history for this message
Albert Santoni (gamegod) wrote :

- Fix the commented pointer leak in TrackDAO::bindTrackToLibraryInsert()

- What was the final verdict on how adding these large blobs to the database will impact subsequent query performance? It would be good to document this since it likely won't be the last time we have to store blobs in the database. (eg. cover art)

- Probably wouldn't hurt to make those version strings in BeatFactory constants defined in the header.

- For the BeatGrid byte array, would it make sense to pack/unpack the blob as a C-style struct? You've got code like this:
+ pBuffer[0] = m_dBpm;
+ pBuffer[1] = m_dFirstBeat;
... and your schema for the format of this buffer is spread out all over the place. For example, BeatGrid::toByteArray() has a hardcoded sizeof(double) * 2 here:

+ QByteArray* pByteArray = new QByteArray((char*)pBuffer, sizeof(double) * 2);

and similarly, readByteArray() has an assert with the same hardcoded length. Using a C-style struct will probably make that code less fragile (eg. sizeof(MyBeatsStruct)).

- void BeatGrid::readByteArray(QByteArray* pByteArray) ---> Did you mean a const QByteArray* ?
:)

- BeatGrid::findNthBeat(...) might lead to unintended behaviour if dSamples is negative. (???)

- The naming of these classes is very confusing. Matrix and grid are synonyms, so I'm barring you from using those words to describe different classes. Names that may be more clear could be "ExtrapolatedBeatGrid" and "CustomBeatGrid".

Otherwise, code looks pretty good. I hope it works. :)

I'll do some testing next time I have a chance and take another pass at the code.

Thanks guys!
Albert

review: Needs Fixing
2555. By Phillip Whelan

Merging from Repository.

2556. By Phillip Whelan

Use a struct for (de)serializing the beat, for beatgrid, from the database.

2557. By Phillip Whelan

Undo application of SSE3 patch (not the time nor the place for it).

Revision history for this message
Phillip Whelan (pwhelan) wrote :

> - BeatGrid::findNthBeat(...) might lead to unintended behaviour if dSamples is negative. (???)

With the extrapolated beat grid (currently called 'BeatGrid') this will simply return a negative sample offset. Caller beware.

As for the custom beat grid (currently called 'BeatMatrix'), it should just return a simple 0.

2558. By Phillip Whelan

Make the QByteArray argument to readByteArray and both Beat classes a const.

2559. By Albert Santoni

Rename struct to BeatGridData and fixed QByteArray pointer leak in TrackDAO.

2560. By Albert Santoni

Merge from trunk

Revision history for this message
Albert Santoni (gamegod) wrote :

Phil and I fixed the remaining stuff from my review. Approve and merged into trunk tonight.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'mixxx/build/depends.py'
2--- mixxx/build/depends.py 2011-03-04 09:09:44 +0000
3+++ mixxx/build/depends.py 2011-03-07 05:53:37 +0000
4@@ -540,6 +540,10 @@
5
6 "sampleutil.cpp",
7 "trackinfoobject.cpp",
8+ "track/beatgrid.cpp",
9+ "track/beatmatrix.cpp",
10+ "track/beatfactory.cpp",
11+
12 "baseplayer.cpp",
13 "basetrackplayer.cpp",
14 "deck.cpp",
15
16=== modified file 'mixxx/res/schema.xml'
17--- mixxx/res/schema.xml 2011-02-21 07:01:11 +0000
18+++ mixxx/res/schema.xml 2011-03-07 05:53:37 +0000
19@@ -266,4 +266,13 @@
20 );
21 </sql>
22 </revision>
23+ <revision version="12" min_compatible="3">
24+ <description>
25+ Add beats column to library table.
26+ </description>
27+ <sql>
28+ ALTER TABLE Library ADD COLUMN beats BLOB;
29+ ALTER TABLE Library ADD COLUMN beats_version TEXT;
30+ </sql>
31+ </revision>
32 </schema>
33
34=== modified file 'mixxx/src/analyserbpm.cpp'
35--- mixxx/src/analyserbpm.cpp 2010-10-19 01:35:26 +0000
36+++ mixxx/src/analyserbpm.cpp 2011-03-07 05:53:37 +0000
37@@ -3,6 +3,8 @@
38
39 #include "BPMDetect.h"
40 #include "trackinfoobject.h"
41+#include "track/beatgrid.h"
42+#include "track/beatfactory.h"
43 #include "analyserbpm.h"
44
45
46@@ -20,9 +22,9 @@
47 // int defaultrange = m_pConfig->getValueString(ConfigKey("[BPM]","BPMAboveRangeEnabled")).toInt();
48 bool bpmEnabled = (bool)m_pConfig->getValueString(ConfigKey("[BPM]","BPMDetectionEnabled")).toInt();
49
50- // If BPM detection is not enabled, or the track already has BPM detection done.
51- if(bpmEnabled &&
52- tio->getBpm() == 0.) {
53+ // If BPM detection is enabled and the track does not have a BPM already,
54+ // create a detector.
55+ if(bpmEnabled && tio->getBpm() == 0.) {
56 // All SoundSource's return stereo data, no matter the real file's type
57 m_pDetector = new soundtouch::BPMDetect(2, sampleRate);
58 //m_pDetector = new BPMDetect(tio->getChannels(), sampleRate);
59@@ -31,10 +33,7 @@
60 }
61 }
62
63-
64-
65 void AnalyserBPM::process(const CSAMPLE *pIn, const int iLen) {
66-
67 // Check if BPM detection is enabled
68 if(m_pDetector == NULL) {
69 return;
70@@ -42,7 +41,6 @@
71 //qDebug() << "AnalyserBPM::process() processing " << iLen << " samples";
72
73 m_pDetector->inputSamples(pIn, iLen/2);
74-
75 }
76
77 float AnalyserBPM::correctBPM( float BPM, int min, int max, int aboveRange) {
78@@ -71,6 +69,13 @@
79
80 tio->setBpm(newbpm);
81 tio->setBpmConfirm();
82+
83+ // Currently, the BPM is only analyzed if the track has no BPM. This
84+ // means we don't have to worry that the track already has an existing
85+ // BeatGrid.
86+ BeatsPointer pBeats = BeatFactory::makeBeatGrid(tio, newbpm, 0.0f);
87+ tio->setBeats(pBeats);
88+
89 //if(pBpmReceiver) {
90 //pBpmReceiver->setComplete(tio, false, bpm);
91 //}
92
93=== modified file 'mixxx/src/analyserbpm.h'
94--- mixxx/src/analyserbpm.h 2010-07-15 19:07:16 +0000
95+++ mixxx/src/analyserbpm.h 2011-03-07 05:53:37 +0000
96@@ -8,7 +8,6 @@
97
98
99 class AnalyserBPM : public Analyser {
100-
101 public:
102 AnalyserBPM(ConfigObject<ConfigValue> *_config);
103 void initialise(TrackPointer tio, int sampleRate, int totalSamples);
104@@ -22,7 +21,6 @@
105 soundtouch::BPMDetect *m_pDetector;
106 int m_iMinBpm, m_iMaxBpm;
107 bool m_bProcessEntireSong;
108-
109 };
110
111 #endif
112
113=== modified file 'mixxx/src/basetrackplayer.cpp'
114--- mixxx/src/basetrackplayer.cpp 2010-11-17 21:02:24 +0000
115+++ mixxx/src/basetrackplayer.cpp 2011-03-07 05:53:37 +0000
116@@ -98,6 +98,7 @@
117 delete m_pPlayPosition;
118 delete m_pBPM;
119 delete m_pReplayGain;
120+ delete m_pWaveformRenderer;
121 }
122
123 void BaseTrackPlayer::slotLoadTrack(TrackPointer track, bool bStartFromEndPos)
124@@ -184,7 +185,7 @@
125 // Read the tags if required
126 if(!m_pLoadedTrack->getHeaderParsed())
127 SoundSourceProxy::ParseHeader(m_pLoadedTrack.data());
128-
129+
130 m_pLoadedTrack->incTimesPlayed();
131
132 // Generate waveform summary
133
134=== modified file 'mixxx/src/library/dao/trackdao.cpp'
135--- mixxx/src/library/dao/trackdao.cpp 2010-12-02 22:17:33 +0000
136+++ mixxx/src/library/dao/trackdao.cpp 2011-03-07 05:53:37 +0000
137@@ -2,7 +2,10 @@
138 #include <QtDebug>
139 #include <QtCore>
140 #include <QtSql>
141+
142 #include "trackinfoobject.h"
143+#include "track/beats.h"
144+#include "track/beatfactory.h"
145 #include "library/dao/trackdao.h"
146 #include "audiotagger.h"
147
148@@ -35,6 +38,8 @@
149
150 TrackDAO::~TrackDAO()
151 {
152+ qDebug() << "~TrackDAO()";
153+ clearCache();
154 }
155
156 void TrackDAO::initialize()
157@@ -218,13 +223,13 @@
158 "filetype, location, comment, url, duration, rating, key, "
159 "bitrate, samplerate, cuepoint, bpm, replaygain, wavesummaryhex, "
160 "timesplayed, "
161- "channels, mixxx_deleted, header_parsed) "
162+ "channels, mixxx_deleted, header_parsed, beats_version, beats) "
163 "VALUES (:artist, "
164 ":title, :album, :year, :genre, :tracknumber, "
165- ":filetype, :location, :comment, :url, :duration, :rating, :key,"
166+ ":filetype, :location, :comment, :url, :duration, :rating, :key, "
167 ":bitrate, :samplerate, :cuepoint, :bpm, :replaygain, :wavesummaryhex, "
168 ":timesplayed, "
169- ":channels, :mixxx_deleted, :header_parsed)");
170+ ":channels, :mixxx_deleted, :header_parsed, :beats_version, :beats)");
171 }
172
173 void TrackDAO::bindTrackToLibraryInsert(QSqlQuery& query, TrackInfoObject* pTrack, int trackLocationId) {
174@@ -243,17 +248,33 @@
175 query.bindValue(":bitrate", pTrack->getBitrate());
176 query.bindValue(":samplerate", pTrack->getSampleRate());
177 query.bindValue(":cuepoint", pTrack->getCuePoint());
178- query.bindValue(":bpm", pTrack->getBpm());
179+
180 query.bindValue(":replaygain", pTrack->getReplayGain());
181 query.bindValue(":key", pTrack->getKey());
182 const QByteArray* pWaveSummary = pTrack->getWaveSummary();
183- if (pWaveSummary) //Avoid null pointer deref
184- query.bindValue(":wavesummaryhex", *pWaveSummary);
185+ query.bindValue(":wavesummaryhex", pWaveSummary ? *pWaveSummary : QVariant(QVariant::ByteArray));
186 query.bindValue(":timesplayed", pTrack->getTimesPlayed());
187 //query.bindValue(":datetime_added", pTrack->getDateAdded());
188 query.bindValue(":channels", pTrack->getChannels());
189 query.bindValue(":mixxx_deleted", 0);
190 query.bindValue(":header_parsed", pTrack->getHeaderParsed() ? 1 : 0);
191+
192+ const QByteArray* pBeatsBlob = NULL;
193+ QString blobVersion = "";
194+ BeatsPointer pBeats = pTrack->getBeats();
195+ // Fall back on cached BPM
196+ double dBpm = pTrack->getBpm();
197+
198+ if (pBeats) {
199+ pBeatsBlob = pBeats->toByteArray();
200+ blobVersion = pBeats->getVersion();
201+ dBpm = pBeats->getBpm();
202+ }
203+
204+ query.bindValue(":bpm", dBpm);
205+ query.bindValue(":beats_version", blobVersion);
206+ query.bindValue(":beats", pBeatsBlob ? *pBeatsBlob : QVariant(QVariant::ByteArray));
207+ delete pBeatsBlob;
208 }
209
210 void TrackDAO::addTracks(QList<TrackInfoObject*> tracksToAdd) {
211@@ -306,7 +327,8 @@
212 bindTrackToLibraryInsert(query, pTrack, pTrack->getId());
213
214 if (!query.exec()) {
215- qDebug() << "Failed to INSERT new track into library"
216+ qDebug() << "Failed to INSERT new track into library:"
217+ << pTrack->getFilename()
218 << __FILE__ << __LINE__ << query.lastError();
219 m_database.rollback();
220 return;
221@@ -495,7 +517,7 @@
222 void TrackDAO::deleteTrack(TrackInfoObject* pTrack) {
223 Q_ASSERT(pTrack);
224
225- //qDebug() << "Got deletion call for track" << pTrack << "ID" << pTrack->getId() << pTrack->getInfo();
226+ qDebug() << "Got deletion call for track" << pTrack << "ID" << pTrack->getId() << pTrack->getInfo();
227
228 // Save dirty tracks.
229 pTrack->save();
230@@ -521,8 +543,8 @@
231
232 int locationId = -1;
233 while (query.next()) {
234+ // Good god! Assign query.record() to a freaking variable!
235 int trackId = query.value(query.record().indexOf("id")).toInt();
236-
237 QString artist = query.value(query.record().indexOf("artist")).toString();
238 QString title = query.value(query.record().indexOf("title")).toString();
239 QString album = query.value(query.record().indexOf("album")).toString();
240@@ -550,6 +572,7 @@
241 bool header_parsed = query.value(query.record().indexOf("header_parsed")).toBool();
242
243 TrackInfoObject* track = new TrackInfoObject(location, false);
244+ TrackPointer pTrack = TrackPointer(track, this->deleteTrack);
245
246 // TIO already stats the file to see if it exists, what its length is,
247 // etc. So don't bother setting it.
248@@ -575,6 +598,14 @@
249 track->setReplayGain(replaygain.toFloat());
250 track->setWaveSummary(wavesummaryhex, false);
251 delete wavesummaryhex;
252+
253+ QString beatsVersion = query.value(query.record().indexOf("beats_version")).toString();
254+ QByteArray beatsBlob = query.value(query.record().indexOf("beats")).toByteArray();
255+ BeatsPointer pBeats = BeatFactory::loadBeatsFromByteArray(pTrack, beatsVersion, &beatsBlob);
256+ if (pBeats) {
257+ pTrack->setBeats(pBeats);
258+ }
259+
260 track->setTimesPlayed(timesplayed);
261 track->setPlayed(played);
262 track->setChannels(channels);
263@@ -593,8 +624,6 @@
264 connect(track, SIGNAL(save()),
265 this, SLOT(slotTrackSave()));
266
267- TrackPointer pTrack = TrackPointer(track, this->deleteTrack);
268-
269 // Automatic conversion to a weak pointer
270 m_tracks[trackId] = pTrack;
271 m_trackCache.insert(trackId, new TrackPointer(pTrack));
272@@ -606,6 +635,15 @@
273 pTrack->parse();
274 }
275
276+ if (!pBeats && pTrack->getBpm() != 0.0f) {
277+ // The track has no stored beats but has a previously detected BPM
278+ // or a BPM loaded from metadata. Automatically create a beatgrid
279+ // for the track. This dirties the track so we have to do it after
280+ // all the signal connections, etc. are in place.
281+ BeatsPointer pBeats = BeatFactory::makeBeatGrid(pTrack, pTrack->getBpm(), 0.0f);
282+ pTrack->setBeats(pBeats);
283+ }
284+
285 return pTrack;
286 }
287 //query.finish();
288@@ -647,7 +685,17 @@
289 time.start();
290 QSqlQuery query(m_database);
291
292- query.prepare("SELECT library.id, artist, title, album, year, genre, tracknumber, filetype, rating, key, track_locations.location as location, track_locations.filesize as filesize, comment, url, duration, bitrate, samplerate, cuepoint, bpm, replaygain, wavesummaryhex, channels, header_parsed, timesplayed, played FROM Library INNER JOIN track_locations ON library.location = track_locations.id WHERE library.id=" + QString("%1").arg(id));
293+ query.prepare(
294+ "SELECT library.id, artist, title, album, year, genre, tracknumber, "
295+ "filetype, rating, key, track_locations.location as location, "
296+ "track_locations.filesize as filesize, comment, url, duration, bitrate, "
297+ "samplerate, cuepoint, bpm, replaygain, wavesummaryhex, channels, "
298+ "header_parsed, timesplayed, played, beats_version, beats "
299+ "FROM Library "
300+ "INNER JOIN track_locations "
301+ "ON library.location = track_locations.id "
302+ "WHERE library.id=" + QString("%1").arg(id)
303+ );
304
305 TrackPointer pTrack;
306
307@@ -686,7 +734,8 @@
308 "bitrate=:bitrate, samplerate=:samplerate, cuepoint=:cuepoint, "
309 "bpm=:bpm, replaygain=:replaygain, wavesummaryhex=:wavesummaryhex, "
310 "timesplayed=:timesplayed, played=:played, "
311- "channels=:channels, header_parsed=:header_parsed "
312+ "channels=:channels, header_parsed=:header_parsed, "
313+ "beats_version=:beats_version, beats=:beats "
314 "WHERE id="+QString("%1").arg(trackId));
315 query.bindValue(":artist", pTrack->getArtist());
316 query.bindValue(":title", pTrack->getTitle());
317@@ -701,19 +750,33 @@
318 query.bindValue(":bitrate", pTrack->getBitrate());
319 query.bindValue(":samplerate", pTrack->getSampleRate());
320 query.bindValue(":cuepoint", pTrack->getCuePoint());
321- query.bindValue(":bpm", pTrack->getBpm());
322+
323 query.bindValue(":replaygain", pTrack->getReplayGain());
324 query.bindValue(":key", pTrack->getKey());
325 query.bindValue(":rating", pTrack->getRating());
326 const QByteArray* pWaveSummary = pTrack->getWaveSummary();
327- if (pWaveSummary) //Avoid null pointer deref
328- query.bindValue(":wavesummaryhex", *pWaveSummary);
329+ query.bindValue(":wavesummaryhex", pWaveSummary ? *pWaveSummary : QVariant(QVariant::ByteArray));
330 query.bindValue(":timesplayed", pTrack->getTimesPlayed());
331 query.bindValue(":played", pTrack->getPlayed());
332 query.bindValue(":channels", pTrack->getChannels());
333 query.bindValue(":header_parsed", pTrack->getHeaderParsed() ? 1 : 0);
334 //query.bindValue(":location", pTrack->getLocation());
335
336+ BeatsPointer pBeats = pTrack->getBeats();
337+ QByteArray* pBeatsBlob = NULL;
338+ QString beatsVersion = "";
339+ double dBpm = pTrack->getBpm();
340+
341+ if (pBeats) {
342+ pBeatsBlob = pBeats->toByteArray();
343+ beatsVersion = pBeats->getVersion();
344+ dBpm = pBeats->getBpm();
345+ }
346+
347+ query.bindValue(":beats", pBeatsBlob ? *pBeatsBlob : QVariant(QVariant::ByteArray));
348+ query.bindValue(":beats_version", beatsVersion);
349+ query.bindValue(":bpm", dBpm);
350+
351 if (!query.exec()) {
352 qDebug() << query.lastError();
353 m_database.rollback();
354
355=== modified file 'mixxx/src/library/trackcollection.cpp'
356--- mixxx/src/library/trackcollection.cpp 2011-02-21 07:01:11 +0000
357+++ mixxx/src/library/trackcollection.cpp 2011-03-07 05:53:37 +0000
358@@ -68,7 +68,7 @@
359 return false;
360 }
361
362- int requiredSchemaVersion = 11;
363+ int requiredSchemaVersion = 12;
364 if (!SchemaManager::upgradeToSchemaVersion(m_pConfig, m_db,
365 requiredSchemaVersion)) {
366 QMessageBox::warning(0, tr("Cannot upgrade database schema"),
367
368=== added directory 'mixxx/src/track'
369=== added file 'mixxx/src/track/beatfactory.cpp'
370--- mixxx/src/track/beatfactory.cpp 1970-01-01 00:00:00 +0000
371+++ mixxx/src/track/beatfactory.cpp 2011-03-07 05:53:37 +0000
372@@ -0,0 +1,43 @@
373+#include <QtDebug>
374+
375+#include "track/beatgrid.h"
376+#include "track/beatmatrix.h"
377+#include "track/beatfactory.h"
378+
379+BeatsPointer BeatFactory::loadBeatsFromByteArray(TrackPointer pTrack, QString beatsVersion,
380+ QByteArray* beatsSerialized) {
381+ // TODO(XXX) make it so the version strings are not in multiple places
382+ if (beatsVersion == "BeatGrid-1.0") {
383+ BeatGrid* pGrid = new BeatGrid(pTrack, beatsSerialized);
384+ pGrid->moveToThread(pTrack->thread());
385+ pGrid->setParent(pTrack.data());
386+ qDebug() << "Successfully deserialized BeatGrid";
387+ return BeatsPointer(pGrid, &BeatFactory::deleteBeats);
388+ } else if (beatsVersion == "BeatMatrix-1.0") {
389+ BeatMatrix* pMatrix = new BeatMatrix(pTrack, beatsSerialized);
390+ pMatrix->moveToThread(pTrack->thread());
391+ pMatrix->setParent(pTrack.data());
392+ qDebug() << "Successfully deserialized BeatMatrix";
393+ return BeatsPointer(pMatrix, &BeatFactory::deleteBeats);
394+ }
395+ qDebug() << "BeatFactory::loadBeatsFromByteArray could not parse serialized beats.";
396+ return BeatsPointer();
397+}
398+
399+BeatsPointer BeatFactory::makeBeatGrid(TrackPointer pTrack, double dBpm, double dFirstBeatSample) {
400+ BeatGrid* pGrid = new BeatGrid(pTrack);
401+ pGrid->setGrid(dBpm, dFirstBeatSample);
402+ return BeatsPointer(pGrid, &BeatFactory::deleteBeats);
403+}
404+
405+void BeatFactory::deleteBeats(Beats* pBeats) {
406+ // This assumes all Beats* variants multiply-inherit from QObject. Kind of ugly. Oh well.
407+ QObject* pObject = dynamic_cast<QObject*>(pBeats);
408+
409+ if (pObject != NULL) {
410+ qDebug() << "Deleting QObject instance.";
411+ pObject->deleteLater();
412+ } else {
413+ qDebug() << "Could not delete Beats instance, did not dynamic_cast to QObject";
414+ }
415+}
416
417=== added file 'mixxx/src/track/beatfactory.h'
418--- mixxx/src/track/beatfactory.h 1970-01-01 00:00:00 +0000
419+++ mixxx/src/track/beatfactory.h 2011-03-07 05:53:37 +0000
420@@ -0,0 +1,19 @@
421+#ifndef BEATFACTORY_H
422+#define BEATFACTORY_H
423+
424+#include "track/beats.h"
425+#include "track/beatgrid.h"
426+#include "track/beatmatrix.h"
427+
428+class BeatFactory {
429+ public:
430+ static BeatsPointer loadBeatsFromByteArray(TrackPointer pTrack,
431+ QString beatsVersion,
432+ QByteArray* beatsSerialized);
433+ static BeatsPointer makeBeatGrid(TrackPointer pTrack,
434+ double dBpm, double dFirstBeatSample);
435+ private:
436+ static void deleteBeats(Beats* pBeats);
437+};
438+
439+#endif /* BEATFACTORY_H */
440
441=== added file 'mixxx/src/track/beatgrid.cpp'
442--- mixxx/src/track/beatgrid.cpp 1970-01-01 00:00:00 +0000
443+++ mixxx/src/track/beatgrid.cpp 2011-03-07 05:53:37 +0000
444@@ -0,0 +1,186 @@
445+#include <QMutexLocker>
446+#include <QDebug>
447+
448+#include "track/beatgrid.h"
449+
450+
451+struct BeatGridData {
452+ double bpm;
453+ double firstBeat;
454+};
455+
456+BeatGrid::BeatGrid(TrackPointer pTrack, const QByteArray* pByteArray)
457+ : QObject(),
458+ m_mutex(QMutex::Recursive),
459+ m_iSampleRate(pTrack->getSampleRate()),
460+ m_dBpm(0.0f),
461+ m_dFirstBeat(0.0f),
462+ m_dBeatLength(0.0f) {
463+ connect(pTrack.data(), SIGNAL(bpmUpdated(double)),
464+ this, SLOT(slotTrackBpmUpdated(double)));
465+
466+ qDebug() << "New BeatGrid";
467+ if (pByteArray != NULL) {
468+ readByteArray(pByteArray);
469+ }
470+}
471+
472+BeatGrid::~BeatGrid() {
473+
474+}
475+
476+void BeatGrid::setGrid(double dBpm, double dFirstBeatSample) {
477+ QMutexLocker lock(&m_mutex);
478+ m_dBpm = dBpm;
479+ m_dFirstBeat = dFirstBeatSample;
480+ m_dBeatLength = 60.0 * m_iSampleRate / m_dBpm;
481+}
482+
483+QByteArray* BeatGrid::toByteArray() const {
484+ QMutexLocker locker(&m_mutex);
485+ BeatGridData blob = { m_dBpm, m_dFirstBeat };
486+ QByteArray* pByteArray = new QByteArray((char *)&blob, sizeof(blob));
487+ return pByteArray;
488+}
489+
490+void BeatGrid::readByteArray(const QByteArray* pByteArray) {
491+ if ( pByteArray->size() != sizeof(BeatGridData))
492+ return;
493+ BeatGridData *blob = (BeatGridData *)pByteArray->data();
494+ setGrid(blob->bpm, blob->firstBeat);
495+}
496+
497+QString BeatGrid::getVersion() const {
498+ QMutexLocker locker(&m_mutex);
499+ return "BeatGrid-1.0";
500+}
501+
502+// internal use only
503+bool BeatGrid::isValid() const {
504+ return m_iSampleRate > 0 && m_dBpm > 0;
505+}
506+
507+// This could be implemented in the Beats Class itself.
508+// If necessary, the child class can redefine it.
509+double BeatGrid::findNextBeat(double dSamples) const {
510+ return findNthBeat(dSamples, +1);
511+}
512+
513+// This could be implemented in the Beats Class itself.
514+// If necessary, the child class can redefine it.
515+double BeatGrid::findPrevBeat(double dSamples) const {
516+ return findNthBeat(dSamples, -1);
517+}
518+
519+// This is an internal call. This could be implemented in the Beats Class itself.
520+double BeatGrid::findClosestBeat(double dSamples) const {
521+ QMutexLocker locker(&m_mutex);
522+ if (!isValid()) {
523+ return -1;
524+ }
525+ double nextBeat = findNextBeat(dSamples);
526+ double prevBeat = findPrevBeat(dSamples);
527+ return (nextBeat - dSamples > dSamples - prevBeat) ? prevBeat : nextBeat;
528+}
529+
530+double BeatGrid::findNthBeat(double dSamples, int n) const {
531+ QMutexLocker locker(&m_mutex);
532+ if (!isValid() || n == 0) {
533+ return -1;
534+ }
535+
536+ double dClosestBeat;
537+ if (n > 0) {
538+ // We're going forward, so use ceilf to round up to the next multiple of
539+ // m_dBeatLength
540+ dClosestBeat = ceilf(dSamples/m_dBeatLength) * m_dBeatLength + m_dFirstBeat;
541+ n = n - 1;
542+ } else {
543+ // We're going backward, so use floorf to round up to the next multiple
544+ // of m_dBeatLength
545+ dClosestBeat = floorf(dSamples/m_dBeatLength) * m_dBeatLength + m_dFirstBeat;
546+ n = n + 1;
547+ }
548+
549+ return dClosestBeat + n * m_dBeatLength;
550+}
551+
552+void BeatGrid::findBeats(double startSample, double stopSample, QList<double>* pBeatsList) const {
553+ QMutexLocker locker(&m_mutex);
554+ if (!isValid() || startSample > stopSample) {
555+ return;
556+ }
557+ double curBeat = findNextBeat(startSample);
558+ while (curBeat <= stopSample) {
559+ pBeatsList->append(curBeat);
560+ curBeat += m_dBeatLength;
561+ }
562+}
563+
564+bool BeatGrid::hasBeatInRange(double startSample, double stopSample) const {
565+ QMutexLocker locker(&m_mutex);
566+ if (!isValid() || startSample > stopSample) {
567+ return false;
568+ }
569+ double curBeat = findNextBeat(startSample);
570+ if (curBeat <= stopSample) {
571+ return true;
572+ }
573+ return false;
574+}
575+
576+double BeatGrid::getBpm() const {
577+ QMutexLocker locker(&m_mutex);
578+ if (!isValid()) {
579+ return -1;
580+ }
581+ return m_dBpm;
582+}
583+
584+double BeatGrid::getBpmRange(double startSample, double stopSample) const {
585+ QMutexLocker locker(&m_mutex);
586+ if (!isValid() || startSample > stopSample) {
587+ return -1;
588+ }
589+ return m_dBpm;
590+}
591+
592+void BeatGrid::addBeat(double dBeatSample) {
593+ //QMutexLocker locker(&m_mutex);
594+ return;
595+}
596+
597+void BeatGrid::removeBeat(double dBeatSample) {
598+ //QMutexLocker locker(&m_mutex);
599+ return;
600+}
601+
602+void BeatGrid::moveBeat(double dBeatSample, double dNewBeatSample) {
603+ //QMutexLocker locker(&m_mutex);
604+ return;
605+}
606+
607+void BeatGrid::translate(double dNumSamples) {
608+ QMutexLocker locker(&m_mutex);
609+ if (!isValid()) {
610+ return;
611+ }
612+ m_dFirstBeat += dNumSamples;
613+ locker.unlock();
614+ emit(updated());
615+}
616+
617+void BeatGrid::scale(double dScalePercentage) {
618+ QMutexLocker locker(&m_mutex);
619+ if (!isValid()) {
620+ return;
621+ }
622+ m_dBpm *= dScalePercentage;
623+ locker.unlock();
624+ emit(updated());
625+}
626+
627+void BeatGrid::slotTrackBpmUpdated(double dBpm) {
628+ QMutexLocker locker(&m_mutex);
629+ m_dBpm = dBpm;
630+}
631
632=== added file 'mixxx/src/track/beatgrid.h'
633--- mixxx/src/track/beatgrid.h 1970-01-01 00:00:00 +0000
634+++ mixxx/src/track/beatgrid.h 2011-03-07 05:53:37 +0000
635@@ -0,0 +1,75 @@
636+#ifndef BEATGRID_H
637+#define BEATGRID_H
638+
639+#include <QMutex>
640+#include <QObject>
641+
642+#include "trackinfoobject.h"
643+#include "track/beats.h"
644+
645+// BeatGrid is an implementation of the Beats interface that implements an
646+// infinite grid of beats, aligned to a song simply by a starting offset of the
647+// first beat and the song's average beats-per-minute.
648+class BeatGrid : public QObject, public virtual Beats {
649+ Q_OBJECT
650+ public:
651+ BeatGrid(TrackPointer pTrack, const QByteArray* pByteArray=NULL);
652+ virtual ~BeatGrid();
653+
654+ // Initializes the BeatGrid to have a BPM of dBpm and the first beat offset
655+ // of dFirstBeatSample. Does not generate an updated() signal, since it is
656+ // meant for initialization.
657+ void setGrid(double dBpm, double dFirstBeatSample);
658+
659+ // The following are all methods from the Beats interface, see method
660+ // comments in beats.h
661+
662+ virtual Beats::CapabilitiesFlags getCapabilities() const {
663+ return BEATSCAP_TRANSLATE | BEATSCAP_SCALE;
664+ }
665+
666+ virtual QByteArray* toByteArray() const;
667+ virtual QString getVersion() const;
668+
669+ ////////////////////////////////////////////////////////////////////////////
670+ // Beat calculations
671+ ////////////////////////////////////////////////////////////////////////////
672+
673+ virtual double findNextBeat(double dSamples) const;
674+ virtual double findPrevBeat(double dSamples) const;
675+ virtual double findClosestBeat(double dSamples) const;
676+ virtual double findNthBeat(double dSamples, int n) const;
677+ virtual void findBeats(double startSample, double stopSample, BeatList* pBeatsList) const;
678+ virtual bool hasBeatInRange(double startSample, double stopSample) const;
679+ virtual double getBpm() const;
680+ virtual double getBpmRange(double startSample, double stopSample) const;
681+
682+ ////////////////////////////////////////////////////////////////////////////
683+ // Beat mutations
684+ ////////////////////////////////////////////////////////////////////////////
685+
686+ virtual void addBeat(double dBeatSample);
687+ virtual void removeBeat(double dBeatSample);
688+ virtual void moveBeat(double dBeatSample, double dNewBeatSample);
689+ virtual void translate(double dNumSamples);
690+ virtual void scale(double dScalePercentage);
691+
692+ signals:
693+ void updated();
694+
695+ private slots:
696+ void slotTrackBpmUpdated(double bpm);
697+
698+ private:
699+ void readByteArray(const QByteArray* pByteArray);
700+ // For internal use only.
701+ bool isValid() const;
702+
703+ mutable QMutex m_mutex;
704+ int m_iSampleRate;
705+ double m_dBpm, m_dFirstBeat;
706+ double m_dBeatLength;
707+};
708+
709+
710+#endif /* BEATGRID_H */
711
712=== added file 'mixxx/src/track/beatmatrix.cpp'
713--- mixxx/src/track/beatmatrix.cpp 1970-01-01 00:00:00 +0000
714+++ mixxx/src/track/beatmatrix.cpp 2011-03-07 05:53:37 +0000
715@@ -0,0 +1,271 @@
716+#include <QtDebug>
717+#include <QMutexLocker>
718+
719+#include "track/beatmatrix.h"
720+
721+BeatMatrix::BeatMatrix(TrackPointer pTrack, const QByteArray* pByteArray)
722+ : QObject(),
723+ m_mutex(QMutex::Recursive),
724+ m_iSampleRate(pTrack->getSampleRate()) {
725+ if (pByteArray != NULL) {
726+ readByteArray(pByteArray);
727+ }
728+}
729+
730+BeatMatrix::~BeatMatrix() {
731+}
732+
733+unsigned int BeatMatrix::numBeats() const {
734+ return m_beatList.size();
735+}
736+
737+QByteArray* BeatMatrix::toByteArray() const {
738+ QMutexLocker locker(&m_mutex);
739+ // No guarantees BeatLists are made of a data type which located adjacent
740+ // items in adjacent memory locations.
741+ double* pBuffer = new double[m_beatList.size()];
742+ for (int i = 0; i < m_beatList.size(); ++i) {
743+ pBuffer[i] = m_beatList[i];
744+ }
745+ QByteArray* pByteArray = new QByteArray((char*)pBuffer, sizeof(pBuffer[0]) * m_beatList.size());
746+ delete [] pBuffer;
747+ return pByteArray;
748+}
749+
750+void BeatMatrix::readByteArray(const QByteArray* pByteArray) {
751+ if (pByteArray->size() % sizeof(double) != 0) {
752+ qDebug() << "ERROR: Could not parse BeatMatrix from QByteArray of size" << pByteArray->size();
753+ return;
754+ }
755+
756+ int numBeats = pByteArray->size() / sizeof(double);
757+ double* pBuffer = (double*)pByteArray->data();
758+ for (int i = 0; i < numBeats; ++i) {
759+ m_beatList.append(pBuffer[i]);
760+ }
761+}
762+
763+
764+QString BeatMatrix::getVersion() const {
765+ QMutexLocker locker(&m_mutex);
766+ return "BeatMatrix-1.0";
767+}
768+
769+// internal use only
770+bool BeatMatrix::isValid() const {
771+ return m_iSampleRate > 0 && m_beatList.size() > 0;
772+}
773+
774+double BeatMatrix::findNextBeat(double dSamples) const {
775+ return findNthBeat(dSamples, 1);
776+}
777+
778+double BeatMatrix::findPrevBeat(double dSamples) const {
779+ return findNthBeat(dSamples, -1);
780+}
781+
782+double BeatMatrix::findClosestBeat(double dSamples) const {
783+ QMutexLocker locker(&m_mutex);
784+ if (!isValid()) {
785+ return -1;
786+ }
787+ double nextBeat = findNextBeat(dSamples);
788+ double prevBeat = findPrevBeat(dSamples);
789+ return (nextBeat - dSamples > dSamples - prevBeat) ? prevBeat : nextBeat;
790+}
791+
792+double BeatMatrix::findNthBeat(double dSamples, int n) const {
793+ QMutexLocker locker(&m_mutex);
794+ BeatList::const_iterator it;
795+ int i;
796+
797+ if (!isValid() || n == 0) {
798+ return -1;
799+ }
800+
801+ if (n > 0) {
802+ it = qLowerBound(m_beatList.begin(), m_beatList.end(), dSamples);
803+
804+ // Count down until n=1
805+ while (it != m_beatList.end()) {
806+ if (n == 1) {
807+ return *it;
808+ }
809+ it++; n--;
810+ }
811+ }
812+ else if (n < 0) {
813+ it = qUpperBound(m_beatList.begin(), m_beatList.end(), dSamples);
814+
815+ // Count up until n=-1
816+ while (it != m_beatList.begin()) {
817+ // qUpperBound starts us off at the position just-one-past the last
818+ // occurence of dSamples-or-smaller in the list. In order to get the
819+ // last instance of dSamples-or-smaller, we decrement it by 1 before
820+ // touching it. The guard of this while loop guarantees this does
821+ // not put us before the start of the loop.
822+ it--;
823+ if (n == -1) {
824+ return *it;
825+ }
826+ n++;
827+ }
828+ }
829+
830+ return -1;
831+}
832+
833+void BeatMatrix::findBeats(double startSample, double stopSample, QList<double>* pBeatsList) const {
834+ QMutexLocker locker(&m_mutex);
835+ if (!isValid() || startSample > stopSample) {
836+ return;
837+ }
838+ BeatList::const_iterator curBeat = qLowerBound(m_beatList.begin(),
839+ m_beatList.end(),
840+ startSample);
841+ BeatList::const_iterator stopBeat = qUpperBound(m_beatList.begin(),
842+ m_beatList.end(),
843+ stopSample);
844+
845+ for (; curBeat != stopBeat; curBeat++) {
846+ pBeatsList->append(*curBeat);
847+ }
848+}
849+
850+bool BeatMatrix::hasBeatInRange(double startSample, double stopSample) const {
851+ QMutexLocker locker(&m_mutex);
852+ if (!isValid() || startSample > stopSample) {
853+ return false;
854+ }
855+ BeatList::const_iterator startBeat = qLowerBound(m_beatList.begin(),
856+ m_beatList.end(),
857+ startSample);
858+ BeatList::const_iterator stopBeat = qUpperBound(m_beatList.begin(),
859+ m_beatList.end(),
860+ stopSample);
861+
862+ if (startBeat != stopBeat)
863+ return true;
864+ return false;
865+}
866+
867+double BeatMatrix::getBpm() const {
868+ QMutexLocker locker(&m_mutex);
869+ if (!isValid()) {
870+ return -1;
871+ }
872+
873+ // TODO(XXX) not actually correct. We need the true song length.
874+ double startSample = *m_beatList.begin();
875+ double stopSample = *(m_beatList.end()-1);
876+ double songDurationMinutes =
877+ (stopSample - startSample) / (60.0f * m_iSampleRate);
878+ return m_beatList.size() / songDurationMinutes;
879+}
880+
881+double BeatMatrix::getBpmRange(double startSample, double stopSample) const {
882+ QMutexLocker locker(&m_mutex);
883+ if (!isValid() || startSample > stopSample) {
884+ return -1;
885+ }
886+
887+ BeatList::const_iterator startBeat = qLowerBound(m_beatList.begin(),
888+ m_beatList.end(),
889+ startSample);
890+ BeatList::const_iterator stopBeat = qUpperBound(m_beatList.begin(),
891+ m_beatList.end(),
892+ stopSample);
893+ double rangeDurationMinutes =
894+ (stopSample - startSample) / (60.0f * m_iSampleRate);
895+ // Subtracting returns the number of beats between the samples referred to
896+ // by the start and end.
897+ double beatsInRange = stopBeat - startBeat;
898+
899+ return beatsInRange / rangeDurationMinutes;
900+}
901+
902+void BeatMatrix::addBeat(double dBeatSample) {
903+ QMutexLocker locker(&m_mutex);
904+
905+ BeatList::iterator it = qLowerBound(m_beatList.begin(),
906+ m_beatList.end(),
907+ dBeatSample);
908+ // Don't insert a duplicate beat. TODO(XXX) determine what epsilon to
909+ // consider a beat identical to another.
910+ if (*it == dBeatSample)
911+ return;
912+
913+ m_beatList.insert(it, dBeatSample);
914+ locker.unlock();
915+ emit(updated());
916+}
917+
918+void BeatMatrix::removeBeat(double dBeatSample) {
919+ QMutexLocker locker(&m_mutex);
920+ BeatList::iterator it = qLowerBound(m_beatList.begin(),
921+ m_beatList.end(), dBeatSample);
922+
923+ // In case there are duplicates, remove every instance of dBeatSample
924+ // TODO(XXX) add invariant checks against this
925+ // TODO(XXX) determine what epsilon to consider a beat identical to another
926+ while (*it == dBeatSample) {
927+ it = m_beatList.erase(it);
928+ }
929+ locker.unlock();
930+ emit(updated());
931+}
932+
933+void BeatMatrix::moveBeat(double dBeatSample, double dNewBeatSample) {
934+ QMutexLocker locker(&m_mutex);
935+
936+ BeatList::iterator it = qLowerBound(m_beatList.begin(),
937+ m_beatList.end(), dBeatSample);
938+
939+ // Remove all beats from dBeatSample
940+ while (*it == dBeatSample) {
941+ it = m_beatList.erase(it);
942+ }
943+
944+ // Now add a beat to dNewBeatSample
945+ it = qLowerBound(m_beatList.begin(),
946+ m_beatList.end(), dNewBeatSample);
947+
948+ // TODO(XXX) beat epsilon
949+ if (*it != dNewBeatSample) {
950+ m_beatList.insert(it, dNewBeatSample);
951+ }
952+ locker.unlock();
953+ emit(updated());
954+}
955+
956+void BeatMatrix::translate(double dNumSamples) {
957+ QMutexLocker locker(&m_mutex);
958+ if (!isValid()) {
959+ return;
960+ }
961+
962+ for (BeatList::iterator it = m_beatList.begin();
963+ it != m_beatList.end(); ++it) {
964+ *it += dNumSamples;
965+ }
966+ locker.unlock();
967+ emit(updated());
968+}
969+
970+void BeatMatrix::scale(double dScalePercentage) {
971+ QMutexLocker locker(&m_mutex);
972+ if (!isValid()) {
973+ return;
974+ }
975+ for (BeatList::iterator it = m_beatList.begin();
976+ it != m_beatList.end(); ++it) {
977+ *it *= dScalePercentage;
978+ }
979+ locker.unlock();
980+ emit(updated());
981+}
982+
983+void BeatMatrix::slotTrackBpmUpdated(double dBpm) {
984+ //QMutexLocker locker(&m_mutex);
985+ // TODO(XXX) How do we handle this?
986+}
987
988=== added file 'mixxx/src/track/beatmatrix.h'
989--- mixxx/src/track/beatmatrix.h 1970-01-01 00:00:00 +0000
990+++ mixxx/src/track/beatmatrix.h 2011-03-07 05:53:37 +0000
991@@ -0,0 +1,67 @@
992+#ifndef BEATMATRIX_H
993+#define BEATMATRIX_H
994+
995+#include <QObject>
996+#include <QMutex>
997+
998+#include "trackinfoobject.h"
999+#include "track/beats.h"
1000+
1001+// BeatMatrix is an implementation of the Beats interface that implements a list
1002+// of finite beats that have been extracted or otherwise annotated for a track.
1003+class BeatMatrix : public QObject, public Beats {
1004+ Q_OBJECT
1005+ public:
1006+ BeatMatrix(TrackPointer pTrack, const QByteArray* pByteArray=NULL);
1007+ virtual ~BeatMatrix();
1008+
1009+ // See method comments in beats.h
1010+
1011+ virtual Beats::CapabilitiesFlags getCapabilities() const {
1012+ return BEATSCAP_TRANSLATE | BEATSCAP_SCALE | BEATSCAP_ADDREMOVE | BEATSCAP_MOVEBEAT;
1013+ }
1014+
1015+ virtual QByteArray* toByteArray() const;
1016+ virtual QString getVersion() const;
1017+
1018+ ////////////////////////////////////////////////////////////////////////////
1019+ // Beat calculations
1020+ ////////////////////////////////////////////////////////////////////////////
1021+
1022+ virtual double findNextBeat(double dSamples) const;
1023+ virtual double findPrevBeat(double dSamples) const;
1024+ virtual double findClosestBeat(double dSamples) const;
1025+ virtual double findNthBeat(double dSamples, int n) const;
1026+ virtual void findBeats(double startSample, double stopSample, QList<double>* pBeatsList) const;
1027+ virtual bool hasBeatInRange(double startSample, double stopSample) const;
1028+ virtual double getBpm() const;
1029+ virtual double getBpmRange(double startSample, double stopSample) const;
1030+
1031+ ////////////////////////////////////////////////////////////////////////////
1032+ // Beat mutations
1033+ ////////////////////////////////////////////////////////////////////////////
1034+
1035+ virtual void addBeat(double dBeatSample);
1036+ virtual void removeBeat(double dBeatSample);
1037+ virtual void moveBeat(double dBeatSample, double dNewBeatSample);
1038+ virtual void translate(double dNumSamples);
1039+ virtual void scale(double dScalePercentage);
1040+
1041+ signals:
1042+ void updated();
1043+
1044+ private slots:
1045+ void slotTrackBpmUpdated(double bpm);
1046+
1047+ private:
1048+ void readByteArray(const QByteArray* pByteArray);
1049+ // For internal use only.
1050+ bool isValid() const;
1051+ unsigned int numBeats() const;
1052+
1053+ mutable QMutex m_mutex;
1054+ int m_iSampleRate;
1055+ BeatList m_beatList;
1056+};
1057+
1058+#endif /* BEATMATRIX_H */
1059
1060=== added file 'mixxx/src/track/beats.h'
1061--- mixxx/src/track/beats.h 1970-01-01 00:00:00 +0000
1062+++ mixxx/src/track/beats.h 2011-03-07 05:53:37 +0000
1063@@ -0,0 +1,106 @@
1064+#ifndef BEATS_H
1065+#define BEATS_H
1066+
1067+#include <QString>
1068+#include <QList>
1069+#include <QByteArray>
1070+#include <QSharedPointer>
1071+
1072+class Beats;
1073+typedef QSharedPointer<Beats> BeatsPointer;
1074+
1075+// QList's are attractive because they pre-allocate an internal buffer that is
1076+// not free'd after a clear(). The downside is that they do not necessarily
1077+// store adjecent items in adjacent memory locations.
1078+typedef QList<double> BeatList;
1079+
1080+// Beats is a pure abstract base class for BPM and beat management classes. It
1081+// provides a specification of all methods a beat-manager class must provide, as
1082+// well as a capability model for representing optional features.
1083+class Beats {
1084+ public:
1085+ Beats() { }
1086+ virtual ~Beats() { }
1087+
1088+ enum Capabilities {
1089+ BEATSCAP_NONE = 0x0000,
1090+ BEATSCAP_ADDREMOVE = 0x0001,
1091+ BEATSCAP_TRANSLATE = 0x0002,
1092+ BEATSCAP_SCALE = 0x0004,
1093+ BEATSCAP_MOVEBEAT = 0x0008
1094+ };
1095+ typedef int CapabilitiesFlags; // Allows us to do ORing
1096+
1097+ virtual Beats::CapabilitiesFlags getCapabilities() const = 0;
1098+
1099+ // Serialization
1100+ virtual QByteArray* toByteArray() const = 0;
1101+
1102+ // A string representing the version of the beat-processing code that
1103+ // produced this Beats instance. Used by BeatsFactory for associating a
1104+ // given serialization with the version that produced it.
1105+ virtual QString getVersion() const = 0;
1106+
1107+ ////////////////////////////////////////////////////////////////////////////
1108+ // Beat calculations
1109+ ////////////////////////////////////////////////////////////////////////////
1110+
1111+ // Starting from sample dSamples, return the sample of the next beat in the
1112+ // track, or -1 if none exists. If dSamples refers to the location of a
1113+ // beat, dSamples is returned.
1114+ virtual double findNextBeat(double dSamples) const = 0;
1115+
1116+ // Starting from sample dSamples, return the sample of the previous beat in
1117+ // the track, or -1 if none exists. If dSamples refers to the location of
1118+ // beat, dSamples is returned.
1119+ virtual double findPrevBeat(double dSamples) const = 0;
1120+
1121+ // Starting from sample dSamples, return the sample of the closest beat in
1122+ // the track, or -1 if none exists.
1123+ virtual double findClosestBeat(double dSamples) const = 0;
1124+
1125+ // Find the Nth beat from sample dSamples. Works with both positive and
1126+ // negative values of n. Calling findNthBeat with n=0 is invalid. Calling
1127+ // findNthBeat with n=1 or n=-1 is equivalent to calling findNextBeat and
1128+ // findPrevBeat, respectively. If dSamples refers to the location of a beat,
1129+ // then dSamples is returned. If no beat can be found, returns -1.<
1130+ virtual double findNthBeat(double dSamples, int n) const = 0;
1131+
1132+ // Adds to pBeatsList the position in samples of every beat occuring between
1133+ // startPosition and endPosition
1134+ virtual void findBeats(double startSample, double stopSample, BeatList* pBeatsList) const = 0;
1135+
1136+ // Return whether or not a sample lies between startPosition and endPosition
1137+ virtual bool hasBeatInRange(double startSample, double stopSample) const = 0;
1138+
1139+ // Return the average BPM over the entire track if the BPM is
1140+ // valid, otherwise returns -1
1141+ virtual double getBpm() const = 0;
1142+
1143+ // Return the average BPM over the range from startSample to endSample,
1144+ // specified in samples if the BPM is valid, otherwise returns -1
1145+ virtual double getBpmRange(double startSample, double stopSample) const = 0;
1146+
1147+ ////////////////////////////////////////////////////////////////////////////
1148+ // Beat mutations
1149+ ////////////////////////////////////////////////////////////////////////////
1150+
1151+ // Add a beat at location dBeatSample. Beats instance must have the
1152+ // capability BEATSCAP_ADDREMOVE.
1153+ virtual void addBeat(double dBeatSample) = 0;
1154+
1155+ // Remove a beat at location dBeatSample. Beats instance must have the
1156+ // capability BEATSCAP_ADDREMOVE.
1157+ virtual void removeBeat(double dBeatSample) = 0;
1158+
1159+ // Translate all beats in the song by dNumSamples samples. Beats that lie
1160+ // before the start of the track or after the end of the track are not
1161+ // removed. Beats instance must have the capability BEATSCAP_TRANSLATE.
1162+ virtual void translate(double dNumSamples) = 0;
1163+
1164+ // Scale the position of every beat in the song by dScalePercentage. Beats
1165+ // class must have the capability BEATSCAP_SCALE.
1166+ virtual void scale(double dScalePercentage) = 0;
1167+};
1168+
1169+#endif /* BEATS_H */
1170
1171=== modified file 'mixxx/src/trackinfoobject.cpp'
1172--- mixxx/src/trackinfoobject.cpp 2010-11-24 15:20:09 +0000
1173+++ mixxx/src/trackinfoobject.cpp 2011-03-07 05:53:37 +0000
1174@@ -143,6 +143,7 @@
1175 }
1176
1177 TrackInfoObject::~TrackInfoObject() {
1178+ qDebug() << "~TrackInfoObject()";
1179 }
1180
1181 void TrackInfoObject::doSave() {
1182@@ -341,6 +342,42 @@
1183 m_bBpmConfirm = confirm;
1184 }
1185
1186+void TrackInfoObject::setBeats(BeatsPointer pBeats) {
1187+ QMutexLocker lock(&m_qMutex);
1188+
1189+ // This whole method is not so great. The fact that Beats is an ABC is
1190+ // limiting with respect to QObject and signals/slots.
1191+
1192+ QObject* pObject = NULL;
1193+ if (m_pBeats) {
1194+ pObject = dynamic_cast<QObject*>(m_pBeats.data());
1195+ if (pObject)
1196+ pObject->disconnect(this, SIGNAL(updated()));
1197+ }
1198+ m_pBeats = pBeats;
1199+ pObject = dynamic_cast<QObject*>(m_pBeats.data());
1200+ Q_ASSERT(pObject);
1201+ if (pObject) {
1202+ connect(pObject, SIGNAL(updated()),
1203+ this, SLOT(slotBeatsUpdated()));
1204+ }
1205+ setDirty(true);
1206+ lock.unlock();
1207+ emit(beatsUpdated());
1208+}
1209+
1210+BeatsPointer TrackInfoObject::getBeats() const {
1211+ QMutexLocker lock(&m_qMutex);
1212+ return m_pBeats;
1213+}
1214+
1215+void TrackInfoObject::slotBeatsUpdated() {
1216+ QMutexLocker lock(&m_qMutex);
1217+ setDirty(true);
1218+ lock.unlock();
1219+ emit(beatsUpdated());
1220+}
1221+
1222 bool TrackInfoObject::getHeaderParsed() const
1223 {
1224 QMutexLocker lock(&m_qMutex);
1225
1226=== modified file 'mixxx/src/trackinfoobject.h'
1227--- mixxx/src/trackinfoobject.h 2010-11-17 21:02:24 +0000
1228+++ mixxx/src/trackinfoobject.h 2011-03-07 05:53:37 +0000
1229@@ -28,7 +28,7 @@
1230 #include <QWeakPointer>
1231
1232 #include "defs.h"
1233-
1234+#include "track/beats.h"
1235 #include "library/dao/cue.h"
1236
1237 class QString;
1238@@ -242,6 +242,12 @@
1239 /** Set the track's full file path */
1240 void setLocation(QString location);
1241
1242+ // Get the track's Beats list
1243+ BeatsPointer getBeats() const;
1244+
1245+ // Set the track's Beats
1246+ void setBeats(BeatsPointer beats);
1247+
1248 const Segmentation<QString>* getChordData();
1249 void setChordData(Segmentation<QString> cd);
1250
1251@@ -251,6 +257,7 @@
1252 signals:
1253 void wavesummaryUpdated(TrackInfoObject*);
1254 void bpmUpdated(double bpm);
1255+ void beatsUpdated();
1256 void ReplayGainUpdated(double replaygain);
1257 void cuesUpdated();
1258 void changed();
1259@@ -258,6 +265,9 @@
1260 void clean();
1261 void save();
1262
1263+ private slots:
1264+ void slotBeatsUpdated();
1265+
1266 private:
1267
1268 // Common initialization function between all TIO constructors.
1269@@ -370,6 +380,9 @@
1270 double m_dVisualResampleRate;
1271 Segmentation<QString> m_chordData;
1272
1273+ // Storage for the track's beats
1274+ BeatsPointer m_pBeats;
1275+
1276 friend class TrackDAO;
1277 };
1278
1279
1280=== modified file 'mixxx/src/waveform/waveformrenderbeat.cpp'
1281--- mixxx/src/waveform/waveformrenderbeat.cpp 2010-11-25 01:06:46 +0000
1282+++ mixxx/src/waveform/waveformrenderbeat.cpp 2011-03-07 05:53:37 +0000
1283@@ -14,45 +14,26 @@
1284 #include "widget/wskincolor.h"
1285 #include "widget/wwidget.h"
1286 #include "trackinfoobject.h"
1287+#include "track/beats.h"
1288+
1289
1290 WaveformRenderBeat::WaveformRenderBeat(const char* group, WaveformRenderer *parent)
1291 : m_pParent(parent),
1292- m_pBpm(NULL),
1293- m_pBeatFirst(NULL),
1294 m_pTrackSamples(NULL),
1295 m_pTrack(),
1296 m_iWidth(0),
1297 m_iHeight(0),
1298- m_dBpm(-1),
1299- m_dBeatFirst(-1),
1300 m_dSamplesPerPixel(-1),
1301 m_dSamplesPerDownsample(-1),
1302- m_dBeatLength(-1),
1303 m_iNumSamples(0),
1304 m_iSampleRate(-1) {
1305- m_pBpm = new ControlObjectThreadMain(ControlObject::getControl(ConfigKey(group, "file_bpm")));
1306- connect(m_pBpm, SIGNAL(valueChanged(double)), this, SLOT(slotUpdateBpm(double)));
1307-
1308- //m_pBeatFirst = new ControlObjectThreadMain(ControlObject::getControl(ConfigKey(group, "BeatFirst")));
1309- //connect(m_pBeatFirst, SIGNAL(valueChanged(double)), this, SLOT(slotUpdateBeatFirst(double)));
1310-
1311- m_pTrackSamples = new ControlObjectThreadMain(ControlObject::getControl(ConfigKey(group,"track_samples")));
1312+ m_pTrackSamples = new ControlObjectThreadMain(
1313+ ControlObject::getControl(ConfigKey(group,"track_samples")));
1314 slotUpdateTrackSamples(m_pTrackSamples->get());
1315 connect(m_pTrackSamples, SIGNAL(valueChanged(double)),
1316 this, SLOT(slotUpdateTrackSamples(double)));
1317 }
1318
1319-void WaveformRenderBeat::slotUpdateBpm(double v) {
1320- //qDebug() << "WaveformRenderBeat :: BPM = " << v;
1321- m_dBpm = v;
1322- m_dBeatLength = -1;
1323-}
1324-
1325-void WaveformRenderBeat::slotUpdateBeatFirst(double v) {
1326- //qDebug() << "WaveformRenderBeat :: beatFirst = " << v;
1327- m_dBeatFirst = v;
1328-}
1329-
1330 void WaveformRenderBeat::slotUpdateTrackSamples(double samples) {
1331 //qDebug() << "WaveformRenderBeat :: samples = " << int(samples);
1332 m_iNumSamples = (int)samples;
1333@@ -65,9 +46,7 @@
1334
1335 void WaveformRenderBeat::newTrack(TrackPointer pTrack) {
1336 m_pTrack = pTrack;
1337- m_dBpm = -1;
1338- m_dBeatFirst = -1;
1339- m_dBeatLength = -1;
1340+
1341 m_iNumSamples = 0;
1342 m_iSampleRate = 0;
1343 m_dSamplesPerDownsample = -1;
1344@@ -90,13 +69,9 @@
1345
1346 m_dSamplesPerDownsample = n;
1347 m_dSamplesPerPixel = double(f)/z;
1348-
1349- //qDebug() << "WaveformRenderBeat sampleRate " << sampleRate << " samplesPerPixel " << m_dSamplesPerPixel;
1350-
1351 }
1352
1353 void WaveformRenderBeat::setup(QDomNode node) {
1354-
1355 colorMarks.setNamedColor(WWidget::selectNodeQString(node, "BeatColor"));
1356 colorMarks = WSkinColor::getCorrectColor(colorMarks);
1357
1358@@ -108,10 +83,9 @@
1359 colorHighlight = WSkinColor::getCorrectColor(colorHighlight);
1360 }
1361
1362-
1363-void WaveformRenderBeat::draw(QPainter *pPainter, QPaintEvent *event, QVector<float> *buffer, double dPlayPos, double rateAdjust) {
1364- if(m_dBpm == -1 || m_dBpm == 0)
1365- return;
1366+void WaveformRenderBeat::draw(QPainter *pPainter, QPaintEvent *event,
1367+ QVector<float> *buffer, double dPlayPos, double rateAdjust) {
1368+ int i;
1369
1370 slotUpdateTrackSamples(m_pTrackSamples->get());
1371
1372@@ -121,6 +95,10 @@
1373 if(buffer == NULL)
1374 return;
1375
1376+ BeatsPointer pBeats = m_pTrack->getBeats();
1377+ if (!pBeats)
1378+ return;
1379+
1380 int iCurPos = (int)(dPlayPos * m_iNumSamples);
1381
1382 if(iCurPos % 2 != 0)
1383@@ -137,11 +115,6 @@
1384 // Therefore, sample s is a beat if it satisfies s % 60f/b == 0.
1385 // where s is a /mono/ sample
1386
1387- // beat length in samples
1388- if(m_dBeatLength <= 0) {
1389- m_dBeatLength = 60.0 * m_iSampleRate / m_dBpm;
1390- }
1391-
1392 double subpixelsPerPixel = m_pParent->getSubpixelsPerPixel()*(1.0+rateAdjust);
1393 const int oversample = (int)subpixelsPerPixel;
1394
1395@@ -161,12 +134,13 @@
1396 double basePos = iCurPos - m_dSamplesPerPixel*halfw*(1.0+rateAdjust);
1397 double endPos = basePos + m_iWidth*m_dSamplesPerPixel*(1.0+rateAdjust);
1398
1399- // snap to the first beat
1400- double curPos = ceilf(basePos/m_dBeatLength)*m_dBeatLength;
1401+
1402+ m_beatList.clear();
1403+ pBeats->findBeats(basePos, endPos, &m_beatList);
1404
1405 bool reset = false;
1406- for(;curPos <= endPos; curPos+=m_dBeatLength) {
1407- if(curPos < 0)
1408+ foreach (double curPos, m_beatList) {
1409+ if (curPos < 0)
1410 continue;
1411
1412 // i relative to the current play position in subpixels
1413@@ -174,7 +148,7 @@
1414
1415 // If i is less than 20 subpixels from center, highlight it.
1416 if(abs(i) < 20) {
1417- pPainter->setPen(colorHighlight);
1418+ pPainter->setPen(QColor(255,255,255));
1419 reset = true;
1420 }
1421
1422@@ -190,5 +164,4 @@
1423 }
1424
1425 pPainter->restore();
1426-
1427 }
1428
1429=== modified file 'mixxx/src/waveform/waveformrenderbeat.h'
1430--- mixxx/src/waveform/waveformrenderbeat.h 2010-07-15 19:07:16 +0000
1431+++ mixxx/src/waveform/waveformrenderbeat.h 2011-03-07 05:53:37 +0000
1432@@ -12,40 +12,35 @@
1433 class QPainter;
1434 class QPaintEvent;
1435
1436-
1437 class ControlObjectThreadMain;
1438 class WaveformRenderer;
1439 class SoundSourceProxy;
1440
1441 class WaveformRenderBeat : public RenderObject {
1442 Q_OBJECT
1443-public:
1444+ public:
1445 WaveformRenderBeat(const char *group, WaveformRenderer *parent);
1446 void resize(int w, int h);
1447 void setup(QDomNode node);
1448- void draw(QPainter *pPainter, QPaintEvent *event, QVector<float> *buffer, double playPos, double rateAdjust);
1449+ void draw(QPainter *pPainter, QPaintEvent *event, QVector<float> *buffer,
1450+ double playPos, double rateAdjust);
1451 void newTrack(TrackPointer pTrack);
1452
1453-public slots:
1454- void slotUpdateBpm(double bpm);
1455- void slotUpdateBeatFirst(double beatfirst);
1456+ public slots:
1457 void slotUpdateTrackSamples(double samples);
1458-private:
1459+ private:
1460 WaveformRenderer *m_pParent;
1461- ControlObjectThreadMain *m_pBpm;
1462- ControlObjectThreadMain *m_pBeatFirst;
1463 ControlObjectThreadMain *m_pTrackSamples;
1464 TrackPointer m_pTrack;
1465 int m_iWidth, m_iHeight;
1466- double m_dBpm;
1467- double m_dBeatFirst;
1468 QColor colorMarks;
1469 QColor colorHighlight;
1470 double m_dSamplesPerPixel;
1471 double m_dSamplesPerDownsample;
1472- double m_dBeatLength;
1473 int m_iNumSamples;
1474 int m_iSampleRate;
1475+
1476+ BeatList m_beatList;
1477 };
1478
1479 #endif
1480
1481=== modified file 'mixxx/src/waveform/waveformrenderer.cpp'
1482--- mixxx/src/waveform/waveformrenderer.cpp 2010-08-27 20:10:20 +0000
1483+++ mixxx/src/waveform/waveformrenderer.cpp 2011-03-07 05:53:37 +0000
1484@@ -1,9 +1,3 @@
1485-/**
1486-
1487- A license and other info goes here!
1488-
1489- */
1490-
1491 #include <QDebug>
1492 #include <QDomNode>
1493 #include <QImage>
1494@@ -126,6 +120,8 @@
1495
1496
1497 WaveformRenderer::~WaveformRenderer() {
1498+ qDebug() << "~WaveformRenderer()";
1499+
1500 // Wait for the thread to quit
1501 m_bQuit = true;
1502 QThread::wait();
1503@@ -165,6 +161,13 @@
1504 if(m_pRenderBeat)
1505 delete m_pRenderBeat;
1506 m_pRenderBeat = NULL;
1507+
1508+ QMutableListIterator<RenderObject*> iter(m_renderObjects);
1509+ while (iter.hasNext()) {
1510+ RenderObject* ro = iter.next();
1511+ iter.remove();
1512+ delete ro;
1513+ }
1514 }
1515
1516 void WaveformRenderer::slotUpdatePlayPos(double v) {
1517
1518=== modified file 'mixxx/src/waveform/waveformrenderer.h'
1519--- mixxx/src/waveform/waveformrenderer.h 2010-07-15 19:07:16 +0000
1520+++ mixxx/src/waveform/waveformrenderer.h 2011-03-07 05:53:37 +0000
1521@@ -28,7 +28,7 @@
1522 Q_OBJECT
1523 public:
1524 WaveformRenderer(const char* group);
1525- ~WaveformRenderer();
1526+ virtual ~WaveformRenderer();
1527
1528 void resize(int w, int h);
1529 void draw(QPainter* pPainter, QPaintEvent *pEvent);

Subscribers

People subscribed via source and target branches