6dc8bf7329
This RNG is more predictable, which makes it great for testing. Signed-off-by: Anna Schumaker <Anna@OcarinaProject.net>
1334 lines
36 KiB
Plaintext
1334 lines
36 KiB
Plaintext
===============================================================================
|
|
= =
|
|
= Ocarina 6.0 =
|
|
= =
|
|
===============================================================================
|
|
|
|
Ocarina 6.0 is the 6th implementation of the Ocarina music player - a
|
|
lightweight, GTK+ based music player with all the features that I want.
|
|
Improvements over the 5.x series will include the existence of both a design
|
|
document (this file) and unit tests. This should make maintenance easier and
|
|
help me stay focused.
|
|
|
|
Ocarina 6.0 will use Gstreamer 1.0 for audio playback, GTK-MM 3 for user
|
|
interface development, and Taglib for extracting tags.
|
|
|
|
|
|
|
|
Install:
|
|
Ocarina will be compiled into a single executable placed under
|
|
/usr/bin/. Any extra files needed to run will be placed under
|
|
/usr/share/ocarina/.
|
|
|
|
|
|
|
|
Version:
|
|
This file contains a simple function for returning a string stating
|
|
the current version. The current version will be set by the build
|
|
system and passed using the CONFIG_VERSION macro.
|
|
|
|
API:
|
|
const char *get_version();
|
|
Returns a string describing the current version.
|
|
|
|
|
|
|
|
Errors:
|
|
This file contains an enum defining error codes used throughout the
|
|
codebase. Errors will be reported using "throw" and "catch".
|
|
|
|
Error Codes:
|
|
enum error_t {
|
|
E_AUDIO = 1, /* Audio error */
|
|
E_EXIST, /* Requested object does not exist */
|
|
E_INVAL, /* Invalid operation requested */
|
|
};
|
|
|
|
|
|
|
|
Printing:
|
|
Sometimes text needs to be printed to the screen so users (or debuggers)
|
|
can trace what is going on.
|
|
|
|
API:
|
|
void print(string fmt, ...);
|
|
void dprint(string fmt, ...);
|
|
Print text to the screen. The dprint() option will only only
|
|
be implemented when when CONFIG_DEBUG or CONFIG_TEST is enabled,
|
|
and will be an empty function otherwise.
|
|
|
|
|
|
|
|
Callbacks:
|
|
Callbacks are used to notify a unit test or the gui that something in
|
|
the backend has changed. The callbacks structure should be initialized
|
|
with no-op default values and filled in by the user through the
|
|
get_callbacks() function.
|
|
|
|
|
|
- Callback functions:
|
|
struct Callbacks {
|
|
void (*on_library_add)(unsigned int, library :: Library *);
|
|
void (*on_library_update)(unsigned int, library :: Library *);
|
|
void (*on_library_track_add)();
|
|
};
|
|
|
|
static struct Callbacks callbacks;
|
|
|
|
- API:
|
|
struct Callbacks *get_callbacks();
|
|
Return the Callbacks structure;
|
|
|
|
|
|
|
|
On-disk files:
|
|
Data will be stored in the user's home directory according to the
|
|
XDG / freedesktop.org specification. This means data will be stored
|
|
in a subdirectory of $XDG_DATA_HOME.
|
|
|
|
The filse class will support reading and writing files in the users
|
|
$XDG_CONFIG_HOME/ocarina{-debug|test}.
|
|
|
|
Items should be written to a file with either a space or new line
|
|
separating multiple values.
|
|
|
|
- Notation:
|
|
"File << aaaaa << bbbbb << endl" is translated into "aaaaa bbbbb\n"
|
|
|
|
- File version:
|
|
#define FILE_VERSION 0
|
|
|
|
- Open mode:
|
|
enum OpenMode {
|
|
OPEN_READ,
|
|
OPEN_WRITE,
|
|
NOT_OPEN,
|
|
}
|
|
|
|
- File:
|
|
class File : public std::fstream {
|
|
private:
|
|
unsigned int version;
|
|
OpenMode mode;
|
|
string filename;
|
|
|
|
public:
|
|
File(const std::string &);
|
|
~File();
|
|
const std::string get_filepath();
|
|
const unsigned int get_version();
|
|
bool exists();
|
|
void open(OpenMode);
|
|
void close();
|
|
string getline();
|
|
}
|
|
|
|
- File format:
|
|
File << FILE_VERSION << endl;
|
|
File << <OTHER_DATA>;
|
|
|
|
- API:
|
|
File :: File(const std::string &filename);
|
|
Store the name of the file.
|
|
|
|
File :: ~File();
|
|
Close the file stream if it is open.
|
|
|
|
const std::string File :: get_filepath();
|
|
If filename == "":
|
|
return ""
|
|
|
|
Otherwise, resolve filepath to one of:
|
|
XDG_DATA_HOME/ocarina/filepath
|
|
XDG_DATA_HOME/ocarina-debug/filepath
|
|
XDG_DATA_HOME/ocarina-test/filepath
|
|
|
|
and return the result.
|
|
|
|
const unsigned int File :: get_version();
|
|
Return the file version number.
|
|
|
|
bool File :: exists();
|
|
Return true if the file exists in the filesystem.
|
|
Return false otherwise.
|
|
|
|
bool File :: open(OpenMode mode);
|
|
Return false if:
|
|
- filename == ""
|
|
- mode == NOT_OPEN
|
|
- The file is already open
|
|
|
|
When opening a file for reading (mode == OPEN_READ),
|
|
- Return false if the file does not exist
|
|
- Open the file
|
|
- Read in version from the start of the file
|
|
|
|
When opening a file for writing (mode == OPEN_WRITE),
|
|
- Create missing directories as needed
|
|
- Write version information to the start of the file
|
|
|
|
Return true on success.
|
|
|
|
void File :: close();
|
|
Close a file after IO.
|
|
|
|
string File :: getline();
|
|
Read an entire line from the file and return it to the caller.
|
|
In theory a return value optimization will occur so returning
|
|
a string by value won't be a problem.
|
|
|
|
|
|
|
|
Database Entry:
|
|
The database entry class is a base class used for storing data inside
|
|
a database (below). The valid flag and id counter will be managed by
|
|
the database itself, and should be initialized to false and 0.
|
|
|
|
- DatabaseEntry:
|
|
class DatabaseEntry {
|
|
public:
|
|
unsigned int id;
|
|
|
|
DatabaseEntry();
|
|
virtual void ~DatabaseEntry() = 0;
|
|
virtual const std::string primary_key() const = 0;
|
|
|
|
virtual void write(File &) = 0;
|
|
virtual void read(File &) = 0;
|
|
};
|
|
|
|
- API:
|
|
DatabaseEntry :: DatabaseEntry():
|
|
Set id = 0.
|
|
|
|
const std::string DatabaseEntry :: primary_key() const;
|
|
This function should return a unique string representing this
|
|
DatabaseEntry instance, which will be used to prevent
|
|
duplicates in a database. This string is not expected to
|
|
change once a DatabaseEntry has been initialized.
|
|
|
|
void DatabaseEntry :: write(File &);
|
|
This function is called to write a specific DatabaseEntry to
|
|
file.
|
|
|
|
void DatabaseEntry :: read(File &);
|
|
This function is called to read a DatabaseEntry from a file.
|
|
|
|
|
|
|
|
Database:
|
|
Databases are a generic store for information used by Ocarina. Users
|
|
need to inherit from a DatabaseEntry class (above) to properly use a
|
|
database.
|
|
|
|
The Database class is a templated class, so code could potentially
|
|
get messy. Normal class declarations can still exist in the file
|
|
include/database.h and member functions are written in the file
|
|
include/database.hpp, which will be included by database.h. Any
|
|
function not relying on a template can be written in lib/database.cpp.
|
|
|
|
- Automatic saving:
|
|
Databases can save automatically whenever a new value is inserted or
|
|
deleted. This will be more efficient for Databases that do not change
|
|
often but will be a huge performance hit for Databases that have many
|
|
changes at once. All databases will be loaded automatically from disk.
|
|
|
|
- Primary keys:
|
|
Databases use the primary_key() function of a DatabaseEntry to enforce
|
|
uniqueness. This key is used when inserting a new value into the
|
|
Database, and will not be updated after.
|
|
|
|
- Id:
|
|
The "id" field of a DatabaseEntry is also managed by the entry's
|
|
Database container It will be set to the entry's ID when the entry is
|
|
inserted into the database.
|
|
|
|
- Database:
|
|
template <class T>
|
|
class Database {
|
|
private:
|
|
std::vector<T *> _db;
|
|
std::map<const std::string, unsigned int> _keys;
|
|
unsigned int _size; /* Number of valid rows */
|
|
bool _autosave;
|
|
File _file;
|
|
|
|
public:
|
|
typedef std::vector<T *>::iterator iterator;
|
|
typedef std::vector<T *>::const_iterator const_iterator;
|
|
|
|
Database(std::string, bool);
|
|
~Database();
|
|
void save();
|
|
void load();
|
|
|
|
T *insert(const T &);
|
|
void remove(unsigned int);
|
|
unsigned int size();
|
|
unsigned int actual_size();
|
|
|
|
iterator begin();
|
|
iterator end();
|
|
iterator next(iterator &);
|
|
|
|
T *at(unsigned int);
|
|
T *find(const std::string &);
|
|
};
|
|
|
|
- File format:
|
|
File << db.size() << endl
|
|
File << INDEX_0 << db[INDEX_0].valid << db[INDEX_0] << endl;
|
|
File << INDEX_1 << db[INDEX_1].valid << db[INDEX_1] << endl;
|
|
...
|
|
|
|
- API:
|
|
Database :: Database(std::string filepath, bool autosave);
|
|
Initialize a database using "filepath" as a location to store
|
|
data on disk. If the file already exists, read the data into
|
|
the backing vector.
|
|
|
|
Database :: ~Database();
|
|
Delete all entries remaining in the database.
|
|
|
|
void Database :: save();
|
|
Save the database to disk.
|
|
|
|
void Database :: load();
|
|
Load the database from disk.
|
|
|
|
T *Database :: insert(const T &item);
|
|
Look up the item in the _keys map.
|
|
If we find an item with the same key:
|
|
- Return NULL.
|
|
Otherwise:
|
|
- Use new to allocate memory for a new item.
|
|
- Add the new item to the end of the _db.
|
|
- Add the new item to _keys.
|
|
- Set new item.id to the index of the new item.
|
|
_ Increment _size.
|
|
- If autosave == true: save().
|
|
- Return a pointer to the new item.
|
|
|
|
unsigned int Database :: remove(unsigned int id);
|
|
- Remove the item from the _keys map.
|
|
- Delete _db[id]
|
|
- Set _db[id] = NULL
|
|
- If autosave == true: save().
|
|
- Decrement _size.
|
|
|
|
unsigned int Database :: size();
|
|
return _size;
|
|
|
|
unsigned int Database :: actual_size();
|
|
return _db.size();
|
|
|
|
iterator Database :: begin();
|
|
Return _db.end() if there are no valid entries
|
|
If the first entry is valid:
|
|
- return _db.begin();
|
|
Otherwise:
|
|
- return an iterator to the first valid entry.
|
|
|
|
iterator Database :: end();
|
|
return _db.end();
|
|
|
|
iterator Database :: next(iterator &cur);
|
|
Return the next DatabaseEntry with valid == true or _db.end()
|
|
if there are no valid entries left.
|
|
|
|
T *Database :: at(unsigned int i);
|
|
If i is beyond the end of the container:
|
|
Return NULL
|
|
Otherwise:
|
|
Return _db[i];
|
|
|
|
T *Database :: find(const std::string &key);
|
|
If key is in the _keys map:
|
|
Return a pointer to the corresponding entry.
|
|
Otherwise:
|
|
Return NULL;
|
|
|
|
|
|
|
|
Index:
|
|
An index is a special database used to map a std::string key to
|
|
multiple integer values.
|
|
|
|
- IndexEntry:
|
|
class IndexEntry : public DatabaseEntry {
|
|
public:
|
|
const std::string key;
|
|
set<unsigned int> values;
|
|
|
|
IndexEntry(const std::string &);
|
|
const std::string primary_key() const;
|
|
void insert(unsigned int);
|
|
void remove(unsigned int);
|
|
|
|
void write(File &);
|
|
void read(File &);
|
|
};
|
|
|
|
File << key << endl;
|
|
File << values.size() << values[0] << .. << values[N] << endl;
|
|
|
|
- IndexEntry API:
|
|
IndexEntry :: IndexEntry();
|
|
Creat an empty IndexEntry.
|
|
|
|
std::string IndexEntry :: primary_key() const;
|
|
return key;
|
|
|
|
void IndexEntry :: insert(unsigned int value);
|
|
Add value to the values set.
|
|
|
|
void IndexEntry :: remove(unsigned int value);
|
|
Remove value from the values set.
|
|
|
|
void IndexEntry :: write(File &f);
|
|
Write the values set to a file.
|
|
|
|
void IndexEntry :: read(File &f);
|
|
Read values from a file.
|
|
|
|
- Index:
|
|
class Index : public Database<IndexEntry> {
|
|
public:
|
|
Index(const std::string &, bool);
|
|
|
|
void insert(const std::string &, unsigned int);
|
|
void remove(const std::string &, unsigned int);
|
|
};
|
|
|
|
- Index API:
|
|
Index :: Index(const std::string &filepath, bool autosave);
|
|
Pass values on to the Database constructor.
|
|
|
|
void Index :: insert(const std::string &key, unsigned int value);
|
|
Create an IndexEntry for key if one does not exist yet.
|
|
Insert value into the IndexEntry corresponding to key.
|
|
If autosave is enabled, save().
|
|
|
|
void Index :: remove(const std::string &key, unsigned int value);
|
|
Remove value from the IndexEntry corresponding to key. Do not
|
|
remove the IndexEntry, even if it is empty.
|
|
If autosave is enabled, save().
|
|
|
|
|
|
|
|
Filter:
|
|
Filtering is used to generate a subset of songs displayed by the UI to
|
|
that users can choose from. The inverted index is generated at startup
|
|
so there is no need for a remove() function, since it will be wiped
|
|
the next time the application starts.
|
|
|
|
- Parsing:
|
|
- Scan over the input text to create a list of words using the following
|
|
characters as delimiters: \/,;()_-~+"
|
|
- While scanning, convert the string to lowercase and strip out any
|
|
other special characters.
|
|
|
|
- API:
|
|
std::string filter :: add(const std::string &key, unsigned int track_id);
|
|
Parse the key into words following the "Parsing" section above.
|
|
Generate substrings for each word and add each (substring,
|
|
track_id) pair to the index. Return the lowercased text to the
|
|
caller.
|
|
|
|
To generate substrings, iterate over the word starting from
|
|
the front. For example: "goron" would contain the substrings
|
|
{g, go, gor, goro, goron}.
|
|
|
|
std::string filter :: lowercase(const std::string &text);
|
|
Parse the text into lowercased words following the "Parsing"
|
|
section above. Return the lowercased string to the caller.
|
|
|
|
void filter :: search(const std::string &text, std::set<track_id> &res);
|
|
This function finds all track_ids matching the input text.
|
|
Parse the string into substrings and take the intersection of
|
|
all sets returned by the index for each substring.
|
|
|
|
|
|
|
|
Idle queue:
|
|
The idle queue is used to schedule function calls that run at a later
|
|
time.
|
|
|
|
A single queue instance needs to be able to run functions that take
|
|
different types of arguments. This is done by creating an IdleBase
|
|
base class that the templated IdleTask class inherits from.
|
|
|
|
- IdleBase:
|
|
class IdleBase {
|
|
private:
|
|
schedule();
|
|
|
|
public:
|
|
IdleBase();
|
|
~IdleBase();
|
|
virtual void run() = 0;
|
|
};
|
|
|
|
- IdleTask:
|
|
template <class T>
|
|
class IdleTask : IdleBase {
|
|
private:
|
|
void (*func)(T &);
|
|
T &data;
|
|
|
|
public:
|
|
IdleTask(void (*)(T &), T);
|
|
void run();
|
|
};
|
|
|
|
- Queue:
|
|
queue<IdleBase *> idle_queue;
|
|
float queued = 0.0
|
|
float serviced = 0.0
|
|
|
|
- API:
|
|
void IdleBase :: schedule();
|
|
Add the idle task to the idle queue. This should be called
|
|
by the IdleTask constructor.
|
|
queued++;
|
|
|
|
template <class T>
|
|
static inline void idle :: schedule(void (*)(T &), T);
|
|
Create a new IdleTask to run the function later.
|
|
|
|
bool idle :: run_task();
|
|
If there are tasks on the queue:
|
|
run the next task
|
|
scheduled++
|
|
If there are still tasks on the queue:
|
|
return true
|
|
else:
|
|
queued = 0
|
|
scheduled = 0
|
|
return false
|
|
|
|
float idle :: get_progress();
|
|
Return (serviced / queued) to the caller. If there are no
|
|
tasks, return 1.0 to indicate that the queue is finished (and
|
|
to avoid a divide-by-zero error).
|
|
|
|
|
|
|
|
Tag Database:
|
|
The tag database is actually several databases that describe every
|
|
track added by the user. I do not expect the artist_db, album_db,
|
|
genre_db and library_db to change often, so they can be created as
|
|
autosaving databases that will write to disk whenever they are changed.
|
|
The track_db can have several additions and removals in a row, so the
|
|
commit() function is used to control when this db is written to disk,
|
|
avoiding the huge performance hit that would come with saving on EVERY
|
|
change.
|
|
|
|
Tags are defined in the sections below.
|
|
|
|
- Databases:
|
|
Database<Artist> artist_db;
|
|
Database<Album> album_db;
|
|
Database<Genre> genre_db;
|
|
Database<Library> library_db;
|
|
Database<Track> track_db;
|
|
|
|
- API:
|
|
void tagdb :: init();
|
|
Load all databases from disk.
|
|
|
|
void tagdb :: commit();
|
|
Write track_db to disk.
|
|
|
|
Track *tagdb :: add_track(const std::string &filepath, Library *library);
|
|
Add a new track to the track_db and return a pointer to it.
|
|
Return NULL if this track is already in the database.
|
|
|
|
Library *tagdb :: add_library(const std::string &filepath);
|
|
Add a new path to library_db. Return a pointer to the new path
|
|
or return NULL if the path is already in the database.
|
|
|
|
void tagdb :: remove_track(unsigned int track_id);
|
|
Remove the track with id equal to track_id from the track_db.
|
|
|
|
void tagdb :: remove_library(unsigned int library_id);
|
|
Remove all tracks associated with this library from the
|
|
track_db, then remove this library from the library_db.
|
|
|
|
Track *tagdb :: lookup(unsigned int track_id);
|
|
Look up the track_id in the track database. Return NULL if
|
|
there is no matching track.
|
|
|
|
Database<Track> &tagdb :: get_track_db();
|
|
Return a reference to the track_db.
|
|
|
|
Database<Library> &tagdb :: get_library_db();
|
|
Return a reference to the library_db.
|
|
|
|
|
|
|
|
Artist Tag:
|
|
The arist tag is used to collect basic information about the various
|
|
artists that have been added to the library.
|
|
|
|
- Artist:
|
|
class Artist : public DatabaseEntry {
|
|
public:
|
|
std::string name;
|
|
std::string lower;
|
|
|
|
Artist();
|
|
Artist(const std::string &);
|
|
const std::string primary_key() const;
|
|
void read(File &);
|
|
void write(File &);
|
|
};
|
|
|
|
- File Format:
|
|
File << name;
|
|
|
|
- API:
|
|
Artist();
|
|
Initialize an invalid Artist instance.
|
|
|
|
Artist(const std::string &artist_name);
|
|
Set artist_name and find the lowercase form.
|
|
|
|
const std::string Artist :: primary_key() const;
|
|
Use artist name as primary key.
|
|
|
|
void Artist :: read(File &f);
|
|
Read artist name from file and find the lowercase form.
|
|
|
|
void Artist :: write(File &f);
|
|
Write artist name to file.
|
|
|
|
|
|
|
|
Album Tag:
|
|
The album tag is used to collect information about each artist's albums.
|
|
|
|
- Album:
|
|
class Album : public DatabaseEntry {
|
|
public:
|
|
std::string name;
|
|
std::string lower;
|
|
unsigned int year;
|
|
|
|
Album();
|
|
Album(const std::string &, unsigned int);
|
|
const std::string primary_key() const;
|
|
void read(File &);
|
|
void write(File &);
|
|
};
|
|
|
|
- File Format:
|
|
File << year << name;
|
|
|
|
- API:
|
|
Album();
|
|
Initialize an invalid Album instance.
|
|
|
|
Album(const std::string &album_name, unsigned int album_year);
|
|
Set name and year from album name and album_year. Find the
|
|
lowercase form of the album name.
|
|
|
|
const std::string Album :: primary_key() const;
|
|
Return the string: "$year.$name"
|
|
|
|
void Album :: read(File &f);
|
|
Read year, and name from file. Then set the lowercase form
|
|
of the album name.
|
|
|
|
void Artist :: write(File &f);
|
|
Write album information to file.
|
|
|
|
|
|
|
|
Genre Tag:
|
|
The genre tag is used to collect basic information about the various
|
|
genres of songs in the library.
|
|
|
|
- Genre:
|
|
class Genre : public DatabaseEntry {
|
|
public:
|
|
std::string genre;
|
|
std::string lower;
|
|
|
|
Genre();
|
|
Genre(const std::string &);
|
|
const std::string primary_key() const;
|
|
void read(File &);
|
|
void write(File &);
|
|
};
|
|
|
|
- File Format:
|
|
File << name;
|
|
|
|
- API:
|
|
Genre();
|
|
Initialize an invalid Genre instance.
|
|
|
|
Genre(const std::string &genre_name);
|
|
Set genre from genre name and find the lowercase form.
|
|
|
|
const std::string Genre :: primary_key() const;
|
|
Use genre as primary key.
|
|
|
|
void Genre :: read(File &f);
|
|
Read genre from file and find the lowercase form.
|
|
|
|
void Genre :: write(File &f);
|
|
Write genre to file.
|
|
|
|
|
|
|
|
Library Tag:
|
|
The library tag is used to store a single directory added to Ocarina
|
|
by the user. It is not an ID3 tag, and is instead something I use
|
|
internally to keep track of paths added by the user. The count field
|
|
will be managed by the Track tag class.
|
|
|
|
- Library:
|
|
class Library : public DatabaseEntry {
|
|
public:
|
|
std::string root_path;
|
|
unsigned int count;
|
|
bool enabled;
|
|
|
|
Library();
|
|
Library(const std::string &);
|
|
const std::string primary_key() const;
|
|
void read(File &);
|
|
void write(File &);
|
|
};
|
|
|
|
- File Format:
|
|
File << enabled << root_path
|
|
|
|
- API:
|
|
Library();
|
|
Initialize an invalid Library instance.
|
|
Set count = 0.
|
|
Set enabled = false.
|
|
|
|
Library(const std::string &path);
|
|
Set root_path from the provided path.
|
|
Set count = 0.
|
|
Set enabled = true.
|
|
|
|
const std::string Library :: primary_key() const;
|
|
Use root_path as the primary key,
|
|
|
|
void read(File &f);
|
|
Read a library path from file.
|
|
|
|
void write(File &f);
|
|
Write a library path to file.
|
|
|
|
|
|
|
|
Track Tag:
|
|
The track tag is used to store information about a single track in the
|
|
user's music collection.
|
|
|
|
- Sorting:
|
|
enum sort_t {
|
|
SORT_ARTIST,
|
|
SORT_ALBUM,
|
|
SORT_COUNT,
|
|
SORT_GENRE,
|
|
SORT_LENGTH,
|
|
SORT_PLAYED,
|
|
SORT_TITLE,
|
|
SORT_TRACK,
|
|
SORT_YEAR,
|
|
};
|
|
|
|
- Track:
|
|
class Track : public DatabaseEntry {
|
|
public:
|
|
Library *library;
|
|
Artist *artist;
|
|
Album *album;
|
|
Genre *genre;
|
|
|
|
unsigned int track;
|
|
unsigned int length;
|
|
unsigned int play_count;
|
|
unsigned int last_year;
|
|
unsigned int last_month;
|
|
unsigned int last_day;
|
|
|
|
std :: string title;
|
|
std :: string title_lower;
|
|
std :: string filepath;
|
|
std :: string length_str;
|
|
|
|
Track();
|
|
Track(const std::string &, Library *);
|
|
const std::string primary_key() const;
|
|
void read(File &);
|
|
void write(File &);
|
|
|
|
void tag();
|
|
const std::string path();
|
|
bool less_than(Track *, sort_t);
|
|
};
|
|
|
|
- File Format:
|
|
File << library->id << artist->id << album->id << genre->id << track;
|
|
File << last_year << last_month << last_day << play_count << length;
|
|
File << title << endl;
|
|
File << filepath << endl;
|
|
|
|
- API:
|
|
Track();
|
|
Initialize an invalid Track instance.
|
|
|
|
Track(const std::string &full_path, Library *lib);
|
|
This function will only set up the primary key, and the tag()
|
|
function must be called to find audio tags.
|
|
|
|
- Strip library path from the beginning of full_path and use
|
|
the result to set filepath.
|
|
- Set library = lib.
|
|
|
|
const std::string Track :: primary_key() const;
|
|
return path();
|
|
|
|
void read(File &f);
|
|
Read track information from file. Look up the corresponding
|
|
Library, Artist, Album and Genre pointers. Add title to the
|
|
filter and find the string version of length.
|
|
|
|
void write(File &f);
|
|
Write track information to file.
|
|
|
|
void Track :: tag();
|
|
Use TagLib to find tags and audio properties for this file.
|
|
|
|
- Insert Artist, Album, and Genre into their databases and
|
|
set the corresponding pointers.
|
|
- Find title, track number, and length.
|
|
- Set play_count, last_year, last_month and last_day = 0.
|
|
- Set lowercase title and find the string form of length.
|
|
|
|
const std::string Track :: path();
|
|
Combine library->path and filepath to find the full path to
|
|
the audio file.
|
|
|
|
int Track :: less_than(Track *rhs, sort_t field);
|
|
Compare the requested field for this Track instance to the same
|
|
field in the provided track.
|
|
|
|
Return -1 if this < rhs.
|
|
Return 0 if this == rhs.
|
|
Return 1 if this > rhs.
|
|
|
|
|
|
|
|
Random:
|
|
The random number generator is used by the Queue to pick a song as
|
|
randomly as possible. I use two different RNG implementations,
|
|
depending on if Ocarina is compiled with CONFIG_TEST or not.
|
|
|
|
When CONFIG_TEST is enabled, the RNG is a simple counter. Every time
|
|
_pick_random() is called, the counter is incremented and returned.
|
|
When compiled without CONFIG_TEST we will return values using the
|
|
rand() function from stdlib.h.
|
|
|
|
- API:
|
|
void random_seed(unsigned int n);
|
|
Seed the random number generator based on n.
|
|
|
|
void random(unsigned int min, unsigned int max);
|
|
Return a random number between min and max, inclusive.
|
|
If min >= max: return min.
|
|
|
|
|
|
|
|
Queue:
|
|
Queues are lists of songs that the user has requested to play. They
|
|
are the main interface for all music played by Ocarina.
|
|
|
|
- Flags:
|
|
enum queue_flag {
|
|
Q_ENABLED (1 << 0),
|
|
Q_RANDOM (1 << 1),
|
|
Q_REPEAT (1 << 2),
|
|
Q_NO_SORT (1 << 3),
|
|
};
|
|
|
|
- Sort info:
|
|
struct sort_info {
|
|
sort_t field;
|
|
bool ascending;
|
|
};
|
|
|
|
- Sorting:
|
|
Sorting is done using std::stable_sort() to make sure that orders won't
|
|
change unexpectedly.
|
|
|
|
- Queue:
|
|
class Queue {
|
|
protected:
|
|
vector<Track *> _tracks;
|
|
list<sort_t> _sort_order;
|
|
unsigned int _cur;
|
|
unsigned int _flags;
|
|
unsigned int _length;
|
|
|
|
public:
|
|
Queue();
|
|
Queue(unsigned int);
|
|
void read(File &);
|
|
void write(File &);
|
|
|
|
void set_flag(queue_flag);
|
|
void unset_flag(queue_flag);
|
|
bool has_flag(queue_flag);
|
|
|
|
unsigned int add(Track *);
|
|
void del(Track *);
|
|
void del(unsigned int);
|
|
void updated(Track *);
|
|
|
|
unsigned int size();
|
|
const std::string size_str();
|
|
const std::string length_str();
|
|
|
|
void sort(sort_t, bool, bool);
|
|
|
|
Track *next();
|
|
void track_selected(unsigned int);
|
|
};
|
|
|
|
File Format:
|
|
File << flags << tracks.size() << tracks[0] << tracks[1] << ... << tracks[N];
|
|
|
|
- API
|
|
Queue :: Queue();
|
|
Initialize _flags = 0, _cur = -1, _length = 0, and empty sort
|
|
order.
|
|
|
|
Queue :: Queue(unsigned int flags);
|
|
Initialize _flags = flags, _cur = -1, _length = 0, and empty
|
|
sort order.
|
|
|
|
void Queue :: read(File &f);
|
|
Read queue from file.
|
|
|
|
void Queue :: write(File &f);
|
|
Write queue to file.
|
|
|
|
void Queue :: set_flag(queue_flag f);
|
|
Set the appropriate flag.
|
|
|
|
void Queue :: unset_flag(queue_flag f);
|
|
Unset the appropriate flag.
|
|
|
|
bool Queue :: has_flag(queue_flag f);
|
|
Return true if the queue has the flag enabled and false
|
|
otherwise.
|
|
|
|
unsigned int Queue :: add(Track *track);
|
|
Add a new track to the tracks vector and return the index.
|
|
Increase length by the length of the track.
|
|
|
|
void Queue :: del(Track *track);
|
|
Remove all instances of the requested track from the queue.
|
|
|
|
void Queue :: del(unsigned int queue_id);
|
|
Remove the track at the given index from the queue.
|
|
|
|
void Queue :: updated(Track *track);
|
|
Find all indexes of the updated track and notify the UI that
|
|
it has changed.
|
|
|
|
unsigned int Queue :: size();
|
|
Return the number of tracks currently on the queue.
|
|
|
|
const std::string Queue :: size_str();
|
|
Return the number of tracks currently on the queue, in string
|
|
form.
|
|
|
|
const std::string Queue :: length_str();
|
|
Return the remaining length of the queue in a human-readable
|
|
format.
|
|
|
|
void Queue :: sort(sort_t field, bool ascending, bool reset);
|
|
Add a new sort field to the end of the sort order, then
|
|
resort the queue. If reset is set to true, clear the sorting
|
|
list before appending.
|
|
|
|
Track *Queue :: next();
|
|
Return the next track to play.
|
|
|
|
if (tracks.size() == 0)
|
|
return NULL;
|
|
|
|
if (flags & PL_RANDOM):
|
|
_cur += rand() % tracks.size();
|
|
else:
|
|
_cur += 1;
|
|
|
|
if (_cur >= tracks.size())
|
|
_cur -= tracks.size();
|
|
track = tracks[_cur];
|
|
|
|
if (!(flags & PL_REPEAT)):
|
|
del(_cur);
|
|
return track;
|
|
|
|
void Queue :: track_selected(unsigned int queue_id);
|
|
Set _cur to queue_id. If PQ_REPEAT is not set, remove the
|
|
track from the queue.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
- library queue should be set to the default sort order
|
|
The default sort order is (SORT_ARTIST, true),
|
|
(SORT_YEAR, true), (SORT_TRACK, true).
|
|
|
|
|
|
Library: (lib/library.cpp)
|
|
The library manages databases containing track information added by the
|
|
user. Ocarina 6 splits the library into multiple database tables for
|
|
storing content. The library will exist in a library namespace to
|
|
to make functions and classes more unique.
|
|
|
|
- Databases:
|
|
Database<library :: Album> album_db(album.db);
|
|
Database<library :: Artist> artist_db(artist.db);
|
|
Database<library :: Genre> genre_db(genre.db);
|
|
Database<library :: Library> library_db(library.db);
|
|
Database<library :: Track> track_db(track.db);
|
|
|
|
- Updating algorithm:
|
|
1) Use a single IdleTask to loop over each track in the library, check
|
|
if the track still exists in the filesystem and remove it from
|
|
library_db if not.
|
|
2) For each directory in the scan directory, create an IdleTask to
|
|
scan the next level of directories.
|
|
3) For each file in the scan directory, check if the file already
|
|
exists in the track_db and add it to the database if not. Save
|
|
each database after adding files.
|
|
|
|
The taglib library should be used for finding artist, album, etc. tags
|
|
for each track.
|
|
|
|
- Importing
|
|
Ocarina 5.11 stores library files in ~/.ocarina/library/. Importing
|
|
involves reading each file and adding them to the database. If the file
|
|
describes a path already in the database then DO NOT overwrite the
|
|
current path and instead move on to the next file. If version != 2 then
|
|
move on to the next file.
|
|
|
|
File format:
|
|
File << version << endl; /* version == 2 */
|
|
File << path << endl;
|
|
File << id << enabled << next_track_id << size << endl;
|
|
File << <track list>
|
|
|
|
Track format:
|
|
File << filepath << endl;
|
|
File << title << endl;
|
|
File << artist << endl;
|
|
File << album << endl;
|
|
File << comment << endl;
|
|
File << genre << endl;
|
|
File << lenstr << endl;
|
|
File << id << year << track << count;
|
|
File << last_day << last_month << last_year;
|
|
File << length << bitrate << sample << channels << banned << endl;
|
|
|
|
- Testing:
|
|
The script tests/library/gen_library.sh will create a sample library
|
|
in the /tmp/ directory for testing purposes. All the track files are
|
|
complete silence, but the script will fake up tags for each file.
|
|
|
|
To test importing, create several mock library files and copy them to
|
|
~/.ocarina-test/library/ and attempt to read them in.
|
|
|
|
- API
|
|
void library :: init();
|
|
Initialize databases and read files from disk. While reading
|
|
the library:
|
|
- Update the count of tracks in each library path
|
|
- Find the lowercase text of artist, album, genre, track
|
|
|
|
void library :: add_path(string dir);
|
|
If dir is not a directory:
|
|
throw -EINVAL
|
|
|
|
Trigger the on_library_add() callback on success.
|
|
|
|
void library :: del_path(unsigned int lib_id);
|
|
Invalidate a library_db row and all tracks owned by that path
|
|
if lib_id is not valid, throw -EEXIST.
|
|
|
|
void library :: update_path(lib_id);
|
|
Update the given library_db row.
|
|
If lib_id is not valid, throw -EEXIST.
|
|
|
|
Trigger the on_library_update() callback.
|
|
|
|
void library :: update_all();
|
|
Update all library paths.
|
|
Trigger the on_library_update() callback for each path.
|
|
|
|
struct Song library :: lookup(track_id);
|
|
Fill out a Song structure for the provided track_id.
|
|
Throw -EEXIST if there is no track mapping to track_id.
|
|
|
|
struct library :: Library *library :: lookup_path(unsigned int id);
|
|
Return the library path with index id.
|
|
Throw -EEXIST if there is no such path.
|
|
|
|
void library :: import();
|
|
Call this function to import an Ocarina 5.11 style library,
|
|
following the "Importing" section above.
|
|
|
|
|
|
|
|
Playlists: (lib/playlist.cpp)
|
|
Playlists are a new feature in Ocarina 6 and are modeled after Gmail
|
|
labels. Ocarina 6.0 will support two different playlists that the
|
|
user can add tracks to: banned and favorites.
|
|
|
|
Future releases will add support for more playlists.
|
|
|
|
- Database:
|
|
Database<database :: IndexEntry> playlist_db
|
|
|
|
- Default playlists:
|
|
Favorites:
|
|
The user will add music they really like to this playlist.
|
|
|
|
Banned:
|
|
The user should add music they do not like to this playlist.
|
|
Tracks should be removed from the Library playqueue when they
|
|
are banned and added back to the playqueue when they are
|
|
un-banned.
|
|
|
|
- API
|
|
void playlist :: init():
|
|
Load the playlist database.
|
|
|
|
void playlist :: add(name, track_id);
|
|
Add the track_id to the playlist named "name". Save the
|
|
database to disk.
|
|
Throw -EEXIST if "name" does not exist.
|
|
|
|
void playlist :: del(name, track_id);
|
|
Remove the track_id from the playlist named "name". Save the
|
|
database to disk. Attempting to remove a track_id that does
|
|
not exist is not an error.
|
|
Throw -EEXIST if "name" does not exist.
|
|
|
|
const std::set<unsigned int> &playlist :: get_tracks(name);
|
|
Return the set of tracks representing the requested group.
|
|
Throw -EEXIST if "name" does not exist.
|
|
|
|
void playlist :: clear();
|
|
This function only exists if CONFIG_TEST is enabled. Clear
|
|
(reset) the playlist database.
|
|
|
|
|
|
|
|
Deck: (lib/deck.cpp)
|
|
The playqueue deck is used to hold the temporary playqueues created by
|
|
the user.
|
|
|
|
This module also controls the library playqueue, which should be updated
|
|
using the on_library_track_add() and on_library_track_del() callback
|
|
functions. The library playqueue will always have PQ_ENABLED and
|
|
PQ_REPEAT set. This playlist will default to PQ_RANDOM unset.
|
|
|
|
- Deck:
|
|
list<Playqueue> deck;
|
|
Playqueue library_pq;
|
|
|
|
File << library_pq.random << deck.size() << endl;
|
|
File << deck[0] << endl;
|
|
File << deck[N] << endl;
|
|
|
|
- API
|
|
void deck :: init();
|
|
Set up callbacks used by the library.
|
|
|
|
void deck :: read(File &);
|
|
void deck :: write(File &);
|
|
Read or write the playqueue file. This will be called
|
|
from the audio layer to store state.
|
|
|
|
Playqueue *deck :: create();
|
|
Adds a new playqueue to the end of the deck and returns a
|
|
pointer to it.
|
|
|
|
void deck :: remove(N);
|
|
Remove playqueue N from the deck.
|
|
|
|
Playqueue *deck :: get(N);
|
|
Return playqueue N from the deck.
|
|
|
|
void deck :: move(M, N);
|
|
Move playqueue at index M to index N.
|
|
|
|
unsigned int deck :: next();
|
|
Iterate through the deck until you find a playqueue with the
|
|
flag PQ_ENABLED set. Call next() on this playqueue and return
|
|
the result.
|
|
|
|
If the playqueue is empty after calling next(), remove it from
|
|
the deck.
|
|
|
|
If there are no playqueues on the deck, play a song from the
|
|
library playqueue.
|
|
|
|
If there are no playable IDs, throw -EEXIST.
|
|
|
|
void deck :: reset();
|
|
This function only exists if CONFIG_TEST is enabled. Erase
|
|
all the playqueue information and reset the deck list.
|
|
|
|
void deck :: print_info();
|
|
This function only exists if CONFIG_TEST is enabled. Print
|
|
out helpful stats about the current state of the playqueue deck.
|
|
|
|
Playqueue *deck :: get_library_pq();
|
|
Return the library playqueue.
|
|
|
|
|
|
|
|
Audio: (lib/audio.cpp)
|
|
This file will introduce an "audio" namespace containing all of the
|
|
functions interacting with gstreamer. This will create a wrapper
|
|
namespace that will be easier to work with than using raw gstreamer
|
|
functions.
|
|
|
|
The audio layer is meant to be an interface used by the front end to
|
|
control most features of the backend library. This means implementing
|
|
next track, previous track, and so on here.
|
|
|
|
Gstreamer options passed to audio :: init() can be found by running
|
|
`gst-inspect-1.0 --help-gst` on the command line.
|
|
|
|
- Internal:
|
|
Set up a message bus to look for end-of-stream and error messages so
|
|
the next song can be played. This function should call the play
|
|
function after loading a track and after checking the "pause after N"
|
|
count.
|
|
|
|
The audio layer will also create an internal playqueue for tracking
|
|
recently played songs.
|
|
|
|
- State:
|
|
The audio layer will store the current trackid to disk, and then save
|
|
the playqueue deck.
|
|
|
|
File << current_track << endl
|
|
deck.write(File);
|
|
|
|
- API:
|
|
void audio :: init(argc, argv);
|
|
Initialize the gstreamer layer and reload the track that was
|
|
last loaded before shutdown. Gstreamer is supposed to remove
|
|
options from the argv array as they are processed, so pass
|
|
pointers to argc and argv to this function.
|
|
|
|
Read in the state file.
|
|
|
|
void audio :: quit();
|
|
Clean up memory allocated by gstreamer.
|
|
Write out the state file.
|
|
|
|
void audio :: play();
|
|
void audio :: pause();
|
|
Change the gstreamer state to either GST_STATE_PLAYING or
|
|
GST_STATE_PAUSED. Do nothing if there is not a track loaded
|
|
in the pipeline. Throw -EAUDIO if there is an error changing
|
|
state.
|
|
|
|
void audio :: seek_to(long);
|
|
Seek to a position X nanoseconds into the track. Throw -EAUDIO
|
|
if there is an error seeking to the requested position. Do
|
|
nothing if there is not a track loaded in the pipeline.
|
|
|
|
Seconds can be converted to nanoseconds by multiplying with
|
|
GST_SECOND.
|
|
|
|
void audio :: stop();
|
|
pause()
|
|
seek_to(0)
|
|
|
|
void audio :: next();
|
|
Call the deck :: next() function to get the next trackid,
|
|
and load that file into the gstreamer pipeline. Do not change
|
|
the state of the pipeline (if nothing is playing yet, don't
|
|
call play()). Throw -EEXIST if there is no track to load
|
|
into the pipeline.
|
|
|
|
When a track is loaded:
|
|
Is it already in the recently played playqueue?
|
|
If yes, remove it from the playqueue.
|
|
Add to the front of the recently played playqueue.
|
|
Reset the current pointer in the playqueue to 0.
|
|
|
|
Write out the state file.
|
|
|
|
void audio :: previous();
|
|
Call the playlist :: previous() function to iterate backwards
|
|
through the recently played playqueue. Load the returned trackid
|
|
without changing the pipeline state.
|
|
|
|
trackid audio :: current_trackid();
|
|
Return the trackid of the currently playing song. If no track
|
|
is loaded throw -EEXIST;
|
|
|
|
unsigned int audio :: position();
|
|
Return the number of seconds that the song has played.
|
|
|
|
unsigned int audio :: duration();
|
|
Return the duration of the current song in seconds.
|
|
|
|
void audio :: pause_after(bool enabled, unsigned int N);
|
|
Pause after N tracks. The first parameter is a bool
|
|
representing if this feature is enabled or not (true == enabled,
|
|
false == disabled).
|
|
|
|
The count will only be decremented when an end-of-stream message
|
|
is received by the gstreamer pipeline, and not when calling
|
|
next().
|
|
|
|
bool audio :: pause_enabled();
|
|
Return true if pausing is enabled, and false if pausing is
|
|
disabled.
|
|
|
|
unsigned int audio :: pause_count();
|
|
Return the number of tracks that will be played before
|
|
playback pauses.
|
|
|
|
|
|
|
|
Gui: (ocarina/*)
|
|
The GUI will be written in C++ using gtkmm3 for (hopefully) cleaner code.
|
|
|
|
- Design requirements:
|
|
- gtkmm3
|
|
- Front-end for the library to add, remove and modify paths
|
|
- This can be hidden since it's not a common task
|
|
- Double-click to play songs
|
|
- Front-end for groups to add and remove tracks
|
|
- This is a common task and should not be hidden
|
|
- Don't save / restore window size
|
|
- Some window managers already do this automatically
|
|
- Set reasonable defaults
|