/*
This file is part of libdjinterop.
libdjinterop is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
libdjinterop is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with libdjinterop. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace djinterop
{
namespace enginelibrary
{
using std::chrono::duration_cast;
using std::chrono::milliseconds;
using std::chrono::seconds;
using std::chrono::system_clock;
namespace
{
stdx::optional to_time_point(
stdx::optional timestamp)
{
stdx::optional result;
if (timestamp)
{
result = system_clock::time_point{seconds(*timestamp)};
}
return result;
}
stdx::optional to_timestamp(
stdx::optional time)
{
stdx::optional result;
if (time)
{
result = duration_cast(time->time_since_epoch()).count();
}
return result;
}
/// Calculate the quantisation number for waveforms, given a sample rate.
///
/// A few numbers written to the waveform performance data are rounded
/// to multiples of a particular "quantisation number", that is equal to
/// the sample rate divided by 105, and then rounded to the nearest
/// multiple of two.
int64_t quantisation_number(int64_t sample_rate)
{
return (sample_rate / 210) * 2;
}
/// Calculate the samples-per-entry in an overview waveform.
///
/// An overview waveform always has 1024 entries, and the number of samples
/// that each one represents must be calculated from the true sample count by
/// rounding the number of samples to the quantisation number first.
int64_t calculate_overview_waveform_samples_per_entry(
int64_t sample_rate, int64_t sample_count)
{
auto qn = quantisation_number(sample_rate);
return ((sample_count / qn) * qn) / 1024;
}
} // namespace
el_track_impl::el_track_impl(std::shared_ptr storage, int64_t id) :
track_impl{id}, storage_{std::move(storage)}
{
}
stdx::optional el_track_impl::get_metadata_str(
metadata_str_type type)
{
stdx::optional result;
storage_->db << "SELECT text FROM MetaData WHERE id = ? AND "
"type = ? AND text IS NOT NULL"
<< id() << static_cast(type) >>
[&](std::string text) {
if (!result)
{
result = std::move(text);
}
else
{
throw track_database_inconsistency{
"More than one MetaData entry of the same type for the "
"same track",
id()};
}
};
return result;
}
void el_track_impl::set_metadata_str(
metadata_str_type type, stdx::optional content)
{
if (content)
{
set_metadata_str(type, std::string{*content});
}
else
{
storage_->db
<< "REPLACE INTO MetaData (id, type, text) VALUES (?, ?, ?)" << id()
<< static_cast(type) << nullptr;
}
}
void el_track_impl::set_metadata_str(
metadata_str_type type, const std::string& content)
{
storage_->db << "REPLACE INTO MetaData (id, type, text) VALUES (?, ?, ?)"
<< id() << static_cast(type) << content;
}
stdx::optional el_track_impl::get_metadata_int(metadata_int_type type)
{
stdx::optional result;
storage_->db << "SELECT value FROM MetaDataInteger WHERE id = "
"? AND type = ? AND value IS NOT NULL"
<< id() << static_cast(type) >>
[&](int64_t value) {
if (!result)
{
result = value;
}
else
{
throw track_database_inconsistency{
"More than one MetaDataInteger entry of the same type "
"for the same track",
id()};
}
};
return result;
}
void el_track_impl::set_metadata_int(
metadata_int_type type, stdx::optional content)
{
storage_->db
<< "REPLACE INTO MetaDataInteger (id, type, value) VALUES (?, ?, ?)"
<< id() << static_cast(type) << content;
}
beat_data el_track_impl::get_beat_data()
{
return get_perfdata("beatData");
}
void el_track_impl::set_beat_data(beat_data data)
{
set_perfdata("beatData", data);
}
high_res_waveform_data el_track_impl::get_high_res_waveform_data()
{
return get_perfdata("highResolutionWaveFormData");
}
void el_track_impl::set_high_res_waveform_data(high_res_waveform_data data)
{
set_perfdata("highResolutionWaveFormData", data);
}
loops_data el_track_impl::get_loops_data()
{
return get_perfdata("loops");
}
void el_track_impl::set_loops_data(loops_data data)
{
set_perfdata("loops", data);
}
overview_waveform_data el_track_impl::get_overview_waveform_data()
{
return get_perfdata("overviewWaveFormData");
}
void el_track_impl::set_overview_waveform_data(overview_waveform_data data)
{
// As the overview waveform does not store opacity, it is defaulted to 255
// when read back. If we also set it to 255 here, we can apply a check in
// `set_perfdata` that a round-trip encode/decode gives the same data.
for (auto&& entry : data.waveform)
{
entry.low.opacity = 255;
entry.mid.opacity = 255;
entry.high.opacity = 255;
}
set_perfdata("overviewWaveFormData", data);
}
quick_cues_data el_track_impl::get_quick_cues_data()
{
return get_perfdata("quickCues");
}
void el_track_impl::set_quick_cues_data(quick_cues_data data)
{
set_perfdata("quickCues", data);
}
track_data el_track_impl::get_track_data()
{
return get_perfdata("trackData");
}
void el_track_impl::set_track_data(track_data data)
{
set_perfdata("trackData", data);
}
std::vector el_track_impl::adjusted_beatgrid()
{
auto beat_d = get_beat_data();
return std::move(beat_d.adjusted_beatgrid);
}
void el_track_impl::set_adjusted_beatgrid(std::vector beatgrid)
{
el_transaction_guard_impl trans{storage_};
auto beat_d = get_beat_data();
beat_d.adjusted_beatgrid = std::move(beatgrid);
set_beat_data(std::move(beat_d));
trans.commit();
}
double el_track_impl::adjusted_main_cue()
{
return get_quick_cues_data().adjusted_main_cue;
}
void el_track_impl::set_adjusted_main_cue(double sample_offset)
{
el_transaction_guard_impl trans{storage_};
auto quick_cues_d = get_quick_cues_data();
quick_cues_d.adjusted_main_cue = sample_offset;
set_quick_cues_data(std::move(quick_cues_d));
trans.commit();
}
stdx::optional el_track_impl::album()
{
return get_metadata_str(metadata_str_type::album);
}
void el_track_impl::set_album(stdx::optional album)
{
set_metadata_str(metadata_str_type::album, album);
}
stdx::optional el_track_impl::album_art_id()
{
int64_t cell = get_cell("idAlbumArt");
stdx::optional album_art_id;
if (cell < 1)
{
// TODO (haslersn): Throw something.
}
else if (cell > 1)
{
album_art_id = cell;
}
return album_art_id;
}
void el_track_impl::set_album_art_id(stdx::optional album_art_id)
{
if (album_art_id && *album_art_id <= 1)
{
// TODO (haslersn): Throw something.
}
set_cell("idAlbumArt", album_art_id.value_or(1));
// 1 is the magic number for "no album art"
}
stdx::optional el_track_impl::artist()
{
return get_metadata_str(metadata_str_type::artist);
}
void el_track_impl::set_artist(stdx::optional artist)
{
set_metadata_str(metadata_str_type::artist, artist);
}
stdx::optional el_track_impl::average_loudness()
{
return get_track_data().average_loudness;
}
void el_track_impl::set_average_loudness(
stdx::optional average_loudness)
{
el_transaction_guard_impl trans{storage_};
auto track_d = get_track_data();
// Zero average loudness is interpreted as no average loudness.
track_d.average_loudness =
average_loudness.value_or(0) == 0 ? stdx::nullopt : average_loudness;
set_track_data(track_d);
trans.commit();
}
stdx::optional el_track_impl::bitrate()
{
return get_cell >("bitrate");
}
void el_track_impl::set_bitrate(stdx::optional bitrate)
{
set_cell("bitrate", bitrate);
}
stdx::optional el_track_impl::bpm()
{
return get_cell >("bpmAnalyzed");
}
void el_track_impl::set_bpm(stdx::optional bpm)
{
set_cell("bpmAnalyzed", bpm);
stdx::optional ceiled_bpm;
if (bpm)
{
ceiled_bpm = static_cast(std::ceil(*bpm));
}
set_cell("bpm", ceiled_bpm);
}
stdx::optional el_track_impl::comment()
{
return get_metadata_str(metadata_str_type::comment);
}
void el_track_impl::set_comment(stdx::optional comment)
{
set_metadata_str(metadata_str_type::comment, comment);
}
stdx::optional el_track_impl::composer()
{
return get_metadata_str(metadata_str_type::composer);
}
void el_track_impl::set_composer(stdx::optional composer)
{
set_metadata_str(metadata_str_type::composer, composer);
}
database el_track_impl::db()
{
return database{std::make_shared(storage_)};
}
std::vector el_track_impl::containing_crates()
{
std::vector results;
storage_->db << "SELECT crateId FROM CrateTrackList WHERE trackId = ?"
<< id() >>
[&](int64_t id) {
results.push_back(
crate{std::make_shared(storage_, id)});
};
return results;
}
std::vector el_track_impl::default_beatgrid()
{
auto beat_d = get_beat_data();
return std::move(beat_d.default_beatgrid);
}
void el_track_impl::set_default_beatgrid(std::vector beatgrid)
{
el_transaction_guard_impl trans{storage_};
auto beat_d = get_beat_data();
beat_d.default_beatgrid = std::move(beatgrid);
set_beat_data(std::move(beat_d));
trans.commit();
}
double el_track_impl::default_main_cue()
{
return get_quick_cues_data().default_main_cue;
}
void el_track_impl::set_default_main_cue(double sample_offset)
{
el_transaction_guard_impl trans{storage_};
auto quick_cues_d = get_quick_cues_data();
quick_cues_d.default_main_cue = sample_offset;
set_quick_cues_data(std::move(quick_cues_d));
trans.commit();
}
stdx::optional el_track_impl::duration()
{
auto smp = sampling();
if (smp)
{
double secs = smp->sample_count / smp->sample_rate;
return milliseconds{static_cast(1000 * secs)};
}
auto secs = get_cell >("length");
if (secs)
{
return milliseconds{*secs * 1000};
}
return stdx::nullopt;
}
std::string el_track_impl::file_extension()
{
auto rel_path = relative_path();
return get_file_extension(rel_path).value_or(std::string{});
}
std::string el_track_impl::filename()
{
auto rel_path = relative_path();
return get_filename(rel_path);
}
stdx::optional el_track_impl::genre()
{
return get_metadata_str(metadata_str_type::genre);
}
void el_track_impl::set_genre(stdx::optional genre)
{
set_metadata_str(metadata_str_type::genre, genre);
}
stdx::optional el_track_impl::hot_cue_at(int32_t index)
{
auto quick_cues_d = get_quick_cues_data();
return std::move(quick_cues_d.hot_cues[index]);
}
void el_track_impl::set_hot_cue_at(int32_t index, stdx::optional cue)
{
el_transaction_guard_impl trans{storage_};
auto quick_cues_d = get_quick_cues_data();
quick_cues_d.hot_cues[index] = std::move(cue);
set_quick_cues_data(std::move(quick_cues_d));
trans.commit();
}
std::array, 8> el_track_impl::hot_cues()
{
auto quick_cues_d = get_quick_cues_data();
return std::move(quick_cues_d.hot_cues);
}
void el_track_impl::set_hot_cues(std::array, 8> cues)
{
el_transaction_guard_impl trans{storage_};
// TODO (haslersn): The following can be optimized because in this case we
// overwrite all hot_cues
auto quick_cues_d = get_quick_cues_data();
quick_cues_d.hot_cues = std::move(cues);
set_quick_cues_data(std::move(quick_cues_d));
trans.commit();
}
stdx::optional el_track_impl::import_info()
{
if (get_cell("isExternalTrack") == 0)
{
return stdx::nullopt;
}
return track_import_info{
get_cell("uuidOfExternalDatabase"),
get_cell("idTrackInExternalDatabase")};
// TODO (haslersn): How should we handle cells that unexpectedly don't
// contain integral values?
}
void el_track_impl::set_import_info(
const stdx::optional& import_info)
{
if (import_info)
{
set_cell("isExternalTrack", 1);
set_cell("uuidOfExternalDatabase", import_info->external_db_uuid);
set_cell("idTrackInExternalDatabase", import_info->external_track_id);
}
else
{
set_cell("isExternalTrack", 0);
set_cell("uuidOfExternalDatabase", nullptr);
set_cell("idTrackInExternalDatabase", nullptr);
}
}
bool el_track_impl::is_valid()
{
bool valid = false;
storage_->db << "SELECT COUNT(*) FROM Track WHERE id = ?" << id() >>
[&](int count) {
if (count == 1)
{
valid = true;
}
else if (count > 1)
{
throw track_database_inconsistency{
"More than one track with the same ID", id()};
}
};
return valid;
}
stdx::optional el_track_impl::key()
{
stdx::optional result;
auto key_num = get_metadata_int(metadata_int_type::musical_key);
if (key_num)
{
result = static_cast(*key_num);
}
return result;
}
void el_track_impl::set_key(stdx::optional key)
{
stdx::optional key_num;
if (key)
{
key_num = static_cast(*key);
}
el_transaction_guard_impl trans{storage_};
auto track_d = get_track_data();
track_d.key = key;
set_track_data(track_d);
set_metadata_int(metadata_int_type::musical_key, key_num);
trans.commit();
}
stdx::optional el_track_impl::last_accessed_at()
{
// TODO (haslersn): Is there a difference between
// `el_track_impl::last_accessed_at()` and
// `el_track_impl::last_played_at()`, except for the ceiling of the
// timestamp?
return to_time_point(get_metadata_int(metadata_int_type::last_accessed_ts));
}
void el_track_impl::set_last_accessed_at(
stdx::optional accessed_at)
{
if (accessed_at)
{
// Field is always ceiled to the midnight at the end of the day the
// track is played, it seems.
// TODO (haslersn): ^ Why "played" and not "accessed"?
// TODO (haslersn): Shouldn't we just set the unceiled time? This would
// leave the decision whether to ceil it to the library user. Also, it
// would make `el_track_impl::last_accessed_at()` consistent with the
// value that has been set using this method.
auto timestamp = *to_timestamp(accessed_at);
auto secs_per_day = 86400;
timestamp += secs_per_day - 1;
timestamp -= timestamp % secs_per_day;
set_metadata_int(metadata_int_type::last_accessed_ts, timestamp);
}
else
{
set_metadata_int(metadata_int_type::last_accessed_ts, stdx::nullopt);
}
}
stdx::optional el_track_impl::last_modified_at()
{
return to_time_point(get_metadata_int(metadata_int_type::last_modified_ts));
}
void el_track_impl::set_last_modified_at(
stdx::optional modified_at)
{
set_metadata_int(
metadata_int_type::last_modified_ts, to_timestamp(modified_at));
}
stdx::optional el_track_impl::last_played_at()
{
return to_time_point(get_metadata_int(metadata_int_type::last_played_ts));
}
void el_track_impl::set_last_played_at(
stdx::optional played_at)
{
static stdx::optional zero{"0"};
static stdx::optional one{"1"};
set_metadata_str(metadata_str_type::ever_played, played_at ? one : zero);
set_metadata_int(
metadata_int_type::last_played_ts, to_timestamp(played_at));
if (played_at)
{
// TODO (haslersn): Add entry to HistorylistTrackList
}
else
{
// TODO (haslersn): Should HistorylistTrackList now be cleared of this
// track?
}
}
stdx::optional el_track_impl::loop_at(int32_t index)
{
auto loops_d = get_loops_data();
return std::move(loops_d.loops[index]);
}
void el_track_impl::set_loop_at(int32_t index, stdx::optional l)
{
el_transaction_guard_impl trans{storage_};
auto loops_d = get_loops_data();
loops_d.loops[index] = std::move(l);
set_loops_data(std::move(loops_d));
trans.commit();
}
std::array, 8> el_track_impl::loops()
{
auto loops_d = get_loops_data();
return std::move(loops_d.loops);
}
void el_track_impl::set_loops(std::array, 8> cues)
{
el_transaction_guard_impl trans{storage_};
loops_data loops_d;
loops_d.loops = std::move(cues);
set_loops_data(std::move(loops_d));
trans.commit();
}
std::vector el_track_impl::overview_waveform()
{
auto overview_waveform_d = get_overview_waveform_data();
return std::move(overview_waveform_d.waveform);
}
stdx::optional el_track_impl::publisher()
{
return get_metadata_str(metadata_str_type::publisher);
}
void el_track_impl::set_publisher(stdx::optional publisher)
{
set_metadata_str(metadata_str_type::publisher, publisher);
}
int64_t el_track_impl::required_waveform_samples_per_entry()
{
auto smp = sampling();
if (!smp)
{
return 0;
}
if (smp->sample_rate <= 0)
{
throw track_database_inconsistency{
"Track has non-positive sample rate", id()};
}
// In high-resolution waveforms, the samples-per-entry is the same as
// the quantisation number.
return quantisation_number(smp->sample_rate);
}
std::string el_track_impl::relative_path()
{
return get_cell("path");
}
void el_track_impl::set_relative_path(std::string relative_path)
{
// TODO (haslersn): Should we check the path?
set_cell("path", std::string{relative_path});
auto filename = get_filename(relative_path);
set_cell("filename", std::string{filename});
auto extension = get_file_extension(filename);
set_metadata_str(metadata_str_type::file_extension, extension);
}
stdx::optional el_track_impl::sampling()
{
return get_track_data().sampling;
}
void el_track_impl::set_sampling(stdx::optional sampling)
{
el_transaction_guard_impl trans{storage_};
stdx::optional secs;
if (sampling)
{
secs = static_cast(
sampling->sample_count / sampling->sample_rate);
// Set metadata duration_mm_ss as "MM:SS"
std::ostringstream oss;
oss << std::setw(2) << std::setfill('0');
oss << (*secs / 60);
oss << ":";
oss << (*secs % 60);
auto str = oss.str();
set_metadata_str(metadata_str_type::duration_mm_ss, std::string{str});
}
else
{
set_metadata_str(metadata_str_type::duration_mm_ss, stdx::nullopt);
}
set_cell("length", secs);
set_cell("lengthCalculated", secs);
// read old data
auto track_d = get_track_data();
auto beat_d = get_beat_data();
auto high_res_waveform_d = get_high_res_waveform_data();
auto overview_waveform_d = get_overview_waveform_data();
// write new data
track_d.sampling = sampling;
beat_d.sampling = sampling;
set_beat_data(std::move(beat_d));
set_track_data(std::move(track_d));
int64_t sample_rate = sampling ? sampling->sample_rate : 0;
int64_t sample_count = sampling ? sampling->sample_count : 0;
if (!high_res_waveform_d.waveform.empty())
{
// The high-resolution waveform has a required number of samples per
// entry that is dependent on the sample rate. If the sample rate is
// genuinely changed using this method, note that the waveform is likely
// to need to be updated as well afterwards.
high_res_waveform_d.samples_per_entry =
quantisation_number(sample_rate);
set_high_res_waveform_data(std::move(high_res_waveform_d));
}
if (!overview_waveform_d.waveform.empty())
{
// The overview waveform has a varying number of samples per entry, as
// the number of entries is always fixed.
overview_waveform_d.samples_per_entry =
calculate_overview_waveform_samples_per_entry(
sample_rate, sample_count);
set_overview_waveform_data(std::move(overview_waveform_d));
}
trans.commit();
}
stdx::optional el_track_impl::title()
{
return get_metadata_str(metadata_str_type::title);
}
void el_track_impl::set_title(stdx::optional title)
{
set_metadata_str(metadata_str_type::title, title);
}
stdx::optional el_track_impl::track_number()
{
return get_cell >("playOrder");
}
void el_track_impl::set_track_number(stdx::optional track_number)
{
set_cell("playOrder", track_number);
}
std::vector el_track_impl::waveform()
{
auto high_res_waveform_d = get_high_res_waveform_data();
return std::move(high_res_waveform_d.waveform);
}
void el_track_impl::set_waveform(std::vector waveform)
{
el_transaction_guard_impl trans{storage_};
overview_waveform_data overview_waveform_d;
high_res_waveform_data high_res_waveform_d;
if (!waveform.empty())
{
auto smp = sampling();
int64_t sample_count = smp ? smp->sample_count : 0;
int64_t sample_rate = smp ? smp->sample_rate : 0;
// Calculate an overview waveform automatically.
// Note that the overview waveform always has 1024 entries in it.
overview_waveform_d.samples_per_entry =
calculate_overview_waveform_samples_per_entry(
sample_rate, sample_count);
overview_waveform_d.waveform.reserve(1024);
for (int32_t i = 0; i < 1024; ++i)
{
auto entry = waveform[waveform.size() * (2 * i + 1) / 2048];
overview_waveform_d.waveform.push_back(entry);
}
// Make the assumption that the client has respected the required number
// of samples per entry when constructing the waveform.
high_res_waveform_d.samples_per_entry =
quantisation_number(sample_rate);
high_res_waveform_d.waveform = std::move(waveform);
}
set_overview_waveform_data(std::move(overview_waveform_d));
set_high_res_waveform_data(std::move(high_res_waveform_d));
trans.commit();
}
stdx::optional el_track_impl::year()
{
return get_cell >("year");
}
void el_track_impl::set_year(stdx::optional year)
{
set_cell("year", year);
}
} // namespace enginelibrary
} // namespace djinterop