ocarina/DESIGN

1226 lines
34 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}. In addition, Ocarina 6.0 will
support reading library files from the Ocarina 5.10 directory:
$HOME/.ocarina{-debug}.
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
- Hint where the file is located:
enum FileLocHint {
FILE_TYPE_DATA,
FILE_TYPE_LEGACY,
FILE_TYPE_INVALID,
}
- Open mode:
enum OpenMode {
OPEN_READ,
OPEN_WRITE,
NOT_OPEN,
}
- File:
class File : public std::fstream {
private:
unsigned int version;
OpenMode mode;
FileLocHint hint;
string filepath;
public:
File(string, FileLocHint);
~File();
const char *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(string filepath, FileLocHint hint);
Resolve filepath to one of:
XDG_DATA_HOME/ocarina/filepath
XDG_DATA_HOME/ocarina-debug/filepath
XDG_DATA_HOME/ocarina-test/filepath
$HOME/.ocarina/
$HOME/.ocarina-debug/
$HOME/.ocarina-test/
If filepath is an empty string, set the file hint to
FILE_TYPE_INVALID and do not set the filepath field.
File :: ~File();
Close the file stream if it is open.
const char *File :: get_filepath();
Return the full filepath to the file.
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:
- hint == FILE_TYPE_INVALID
- 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),
- Return false if the file has FILE_TYPE_LEGACY set
- 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 will be managed by the database
itself, and should be initialized to false.
- DatabaseEntry:
class DatabaseEntry {
public:
bool valid;
DatabaseEntry();
virtual void ~DatabaseEntry() = 0;
virtual const std::string primary_key() = 0;
virtual void write(File &) = 0;
virtual void read(File &) = 0;
};
- API:
DatabaseEntry :: DatabaseEntry():
Set valid = false.
const std::string DatabaseEntry :: primary_key();
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.
- Valid bit
The "valid" bit of a DatabaseEntry is completely managed by the entry's
Database container. It will be set to true when an entry is inserted
and false when deleted. The Database is also in charge of writing the
valid bit to file.
- 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);
void save();
void load();
unsigned int insert(T);
void remove(unsigned int);
unsigned int size();
unsigned int actual_size();
iterator begin();
iterator end();
iterator next(iterator &);
iterator at(unsigned int);
iterator 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.
void Database :: save();
Save the database to disk.
void Database :: load();
Load the database from disk.
unsigned int Database :: insert(T &item);
Look up the item in the _keys map.
If we find an item with the same key:
- Return the index of the item to the caller.
Otherwise:
- Add the new item to the end of the _db.
- Add the new item to _keys.
- Set item.valid = true.
_ Increment _size.
- If autosave == true: save().
- Return the index of the new item.
unsigned int Database :: remove();
- Remove the item from the _keys map.
- Set item.valid = false.
- 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.
iterator Database :: at(unsigned int i);
If _db[i].valid == true:
Return an iterator pointing to _db[i];
Otherwise:
Return _db.end();
iterator Database :: find(const std::string &key);
If key is in the _keys map:
Return an iterator pointing to the corresponding entry.
Otherwise:
Return _db.end();
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();
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();
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: (lib/filter.cpp)
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.
- Index:
Database<database :: IndexEntry> filter_index;
map<string, string> lowercase_cache;
unsigned int lowercase_cache_hits;
- Parsing:
1) Convert the provided string into a list of words, using whitespace
and the following characters as delimiters: \/,;()_-~+"
For each word:
2) Check the lowercase_cache to see if we have seen the word before,
a) If we have, return the stored string
b) Convert the string to lowercase and strip out remaining
special characters. Add the result to the lowercase_cache;
- API:
void filter :: add(string, track_id);
Parse the string into substrings following the "Parsing"
section (above). Add each (substring, track_id) pair to the
filter_index.
To generate substrings, iterate over the word starting from
the front. For example: "goron" would contain the substrings
{g, go, gor, goro, goron}.
void filter :: search(string, set<track_id> &);
Parse the string into substrings following the "Parsing"
section (above). We want to find track_ids that match ALL
substrings, so take the intersection of all sets returned by
the filter_index for a given substring.
std::string filter :: to_lowercase(const std::string &string);
Split the string into words following step 1 of "Parsing"
(above). Assemble and return a result string using the lower
case cache to convert each term to lowercase.
void filter :: print_cache_stats();
Print cache hit and size information.
void filter :: get_index();
This function only exists if CONFIG_TEST is enabled.
Return the index storing all the filter data.
Idle queue: (lib/idle.cpp)
The idle queue is used to schedule tasks to run at a later time. Idle
tasks must inherit from the IdleBase class so that multiple templated
IdleTask pointers can be placed on the same idle queue.
Creating an idle queue in idle.hpp will create a new queue for every
file that idle.h is included in. I want to have a single, shared
idle queue used by the entire application so to get around this the
IdleBase class is used and implemented in lib/idle.cpp.
- 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).
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.
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:
enum DB_Type {
DB_ALBUM,
DB_ARTIST,
DB_GENRE,
DB_LIBRARY,
DB_TRACK,
};
- Album:
class library :: Album : public DatabaseEntry {
public:
/* primary_key = "$name.$year.$artist_id" */
string name;
string name_lower;
unsigned int year;
unsigned int artist_id;
};
File << artist_id << year << name
- Artist and Genre:
class library :: AGInfo : public DatabaseEntry {
public:
string name; /* primary key */
string name_lower;
};
File << name
- Library:
class library :: Library : public DatabaseEntry {
public:
string root_path; /* primary_key */
unsigned int count;
bool enabled;
};
File << enabled << root_path
- Track:
The primary key for a track is the full filepath (library.root_path +
track.filepath)
class library :: Track : public DatabaseEntry {
public:
/* primary_key = library.root_path + "/" + filepath */
unsigned int library_id;
unsigned int artist_id;
unsigned int album_id;
unsigned int genre_id;
unsigned int track;
unsigned int last_year;
unsigned int last_month;
unsigned int last_day;
unsigned int play_count;
unsigned int length;
string length_str;
string title;
string title_lower;
string filepath;
};
File << library_id << artist_id << album_id << genre_id;
File << track << last_year << last_month << last_day << play_count;
File << length << length_str << endl
File << title << endl;
File << filepath << endl;
- Song:
struct Song {
library :: Album *album;
library :: AGInfo *artist;
library :: AGInfo *genre;
library :: Library *library;
library :: Track *track;
};
- Databases:
Database<library :: Album> album_db(album.db);
Database<library :: AGInfo> artist_db(artist.db);
Database<library :: AGInfo> 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.
#ifdef CONFIG_TEST
void library :: print_db(DB_Type);
Print the database corresponding to DB_Type
void library :: reset();
Clear all databases, returning the library to an empty state.
endif /* CONFIG_TEST */
Playqueue: (lib/playqueue.cpp)
Playqueues are a list of songs that the user has requested to play.
- Flags:
enum playqueue_flags {
PQ_ENABLED (1 << 0),
PQ_RANDOM (1 << 1),
PQ_REPEAT (1 << 2),
};
- Sort order:
enum sort_t {
SORT_ARTIST_ASC = 1,
SORT_ARTIST_DESC = 2,
SORT_ALBUM_ASC = 3,
SORT_ALBUM_DESC = 4,
SORT_COUNT_ASC = 5,
SORT_COUNT_DESC = 6,
SORT_GENRE_ASC = 7,
SORT_GENRE_DESC = 8,
SORT_LENGTH_ASC = 9,
SORT_LENGTH_DESC = 10,
SORT_PLAYED_ASC = 11,
SORT_PLAYED_DESC = 12,
SORT_TITLE_ASC = 13,
SORT_TITLE_DESC = 14,
SORT_TRACK_ASC = 15,
SORT_TRACK_DESC = 16,
SORT_YEAR_ASC = 17,
SORT_YEAR_DESC = 18,
};
- Playqueue:
class Playqueue {
private:
vector<track_id> tracks;
list<sort_t> sort_order; /* default = { SORT_ARTIST_ASC,
SORT_YEAR_ASC,
SORT_TRACK_ASC };
unsigned int cur;
unsigned int flags;
unsigned int length;
public:
Playqueue(flags);
void write(File &);
void read(File &);
void set_flag(playqueue_flags);
void unset_flag(playqueue_flags);
const unsigned int get_flags();
string get_length();
unsigned int add(track_id);
unsigned int add_front(track_id);
void del(playqueue_id);
unsigned int size();
void reset_sort();
void add_sort(sort_t, bool);
void sort();
unsigned int next();
void reset_cur();
}
File << flags << tracks.size() << tracks[0] << tracks[1] << ... << tracks[N];
- API
Playqueue :: Playlist(unsigned int flags);
Create a new playqueue with the appropriate flags set.
sort_order = { (SORT_ARTIST, true), (SORT_YEAR, true),
(SORT_TRACK, true) };
unsigned int Playqueue :: add(unsigned int track_id);
unsigned int Playqueue :: add_front(unsigned int track_id);
Add a new track to the tracks vector and return the index. If
add_front is called, the track will be added to the front of
the playqueue (index = 0).
length += track.length.
void Playqueue :: del(unsigned int playqueue_id);
Erase tracks[playqueue_id] from the tracks vector.
length -= track.length.
void Playqueue :: del_track(unsigned int track_id);
Erase all tracks with track id track_id.
void Playqueue :: set_flag(playqueue_flags flag);
void Playqueue :: unset_flag(playqueue_flags flag);
Set or unset the given flag.
const unsigned int Playqueue :: get_flags();
Return the currently enabled flags.
song Playqueue :: get_length();
Convert the length variable into a string and return the result
to the caller.
unsigned int Playqueue :: size();
Return tracks.size();
void Playqueue :: write(File &);
void Playqueue :: read(File &);
Read or write the playqueue to the file.
void Playqueue :: reset_sort();
Reset the sort_order list to empty.
void Playqueue :: add_sort(sort_t type, bool ascending);
Add a new term to the sort order.
void Playqueue :: sort();
Perform a stable sort on the entire playqueue. Compare tracks
based on the sort_order list.
unsigned int Playqueue :: next();
Return the next track_id to play.
if (tracks.size() == 0)
throw -EEXIST;
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)):
length -= track.length;
tracks.erase(cur);
return track;
void Playqueue :: reset_cur();
This function is intended to be used by the audio layer when
managing the recently played playqueue.
cur = 0;
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
Future work:
I want to set reasonable expectations for Ocarina 6 so that I don't
have to spend a large amount of time coding before releasing something
to the wild. This section will be a list of features that I want, but
should be deferred to a future release so basic support can be coded.
Hint: If feature B depends on A, implement A in 6.x and B in 6.x+1
- New default groups:
Unplayed tracks
- Categories:
Use these to make "groups of groups" for better organization.
Different categories can include Album, Artist and Genere
dynamic groups in addition to user created groups (see below)
The Artist, Album and Genre "tables" can be used to populate
these categories.
- User created song groups:
Basic add and remove features can be implemented using the
Library and Banned Songs groups. This will give me a chance
to test saving groups on a small scale before letting users
create anything they want.
- Save a user's playlist as a group:
- Library defragment:
Ocarina 6.0 will leave holes in the library when tracks are
deleted, potentially leading to fragmentation and larger-than-
needed file sizes. A "defragment" utility can be created to
clean up unused slots.
To help with fixing groups, a mapping of (old values) ->
(new values) should be kept.
- Fix track durations:
Some tracks in my library are tagged with the wrong duration,
so fix them as they are played.
- Track tag editor:
Make a pop-up window for editing the tags of a track. Be sure
to update the library information and the on-disk file.
- Album art:
(easy) Start with album art fetching script
(hard) Build in to Ocarina
- Playlist custom sorting:
Click column headers to choos sort order
Keep a list of fields that the user has selected and place new
fields in the front of this list. Use a recursive stable sort
to do the sorting.
- Better design file format:
Todo list in each document instead of all at once in the footer.
Leave the Todo list out of the official "design document" and
keep it in each individual section instead.
XML?
Code formatting?
Checkbox for when features are done? (reference commit)
- Copy a song group to a different directory:
This can be useful for keeping an external device (like an
Android phone) updated with the music you want to listen to.
Complications: I have an mp3 mirror of all my music, and I
want the mp3s to be synced. Perhaps track mirrors in Ocarina?
- Mirror directory:
I rip music to .flac, but keep an mp3 mirror directory to sync
to other computers and phones. An Ocarina tool to manage a
COMPLETE library mirror might be a good idea so I no longer
need to manage it externally. This can still be done with a
script, a cron job, and maybe a "mirror this track" option in
the library? Perhaps create a mirror group?
- Extra testing ideas:
- Run tests through valgrind to help find memory leaks
- Some way to enable / disable tests during development
- Run tests based on dependencies
- Fix tests that will only work on my computer
- Double check that all inputs and outputs are tested
- Simple testing library?
- Stop tests on failure
- Test callbacks
- Preferences:
- Set default sort
- Save window size
- Set album art size
- Rip out Ocarina 5.x importing and legacy file support
- AirPlay / remote audio support
- Media keys
- Replaygain support
External script to calculate values?
Calculate value after first playback?
Store in library :: Track structure
- Autosave databases
Artist, album, genere and library path don't change often so
saving on every add / remove won't be a huge performance hit
and may actually be more efficient in the long run.
- Scons improvements:
Why does everything compile with the same CONFIG_* parameters?
Cleanups to tests/Sconscript
Global dependency resolution (rather than having multiple ways
in lib/ design/ and tests/)
- Playqueue and database inherit from common class
- "About" dialog