ocarina/DESIGN

690 lines
19 KiB
Plaintext

===============================================================================
= =
= Ocarina 6.3 =
= =
===============================================================================
Ocarina 6.2 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.2 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/.
Core Layers:
Ocarina was written with an "every file is a layer" strategy. This
means code can only call functions that reside in a lower layer.
From top to bottom, core layers are:
* Audio (include/core/audio.h)
* Audio driver (include/core/driver.h)
* Deck (include/core/deck.h)
* Playlist (include/core/playlist.h)
* Library (include/core/library.h)
* Queue (include/core/queue.h)
* RNG (include/core/random.h)
* Tags (include/core/tags/*.h)
* Idle queue (include/core/idle.h)
* Filter (include/core/filter.h)
* Index (include/core/index.h)
* Databases (include/core/database.h)
* File (include/core/file.h)
* Callbacks (include/core/callback.h)
* Version (include/core/version.h)
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;
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),
};
- Sorting:
enum sort_t {
SORT_ARTIST,
SORT_ALBUM,
SORT_COUNT,
SORT_GENRE,
SORT_LENGTH,
SORT_PLAYED,
SORT_TITLE,
SORT_TRACK,
SORT_YEAR,
};
- 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 *);
Track *next();
unsigned int size();
const std::string size_str();
const std::string length_str();
void sort(sort_t, bool);
Track *operator[](unsigned int);
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.
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;
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 reset);
If the field is already in the sort order, toggle its
ascending value. Otherwise, add a new sort field to the end
of the sort order with ascending set to true. If reset is set
to true, clear the sorting list before appending.
Track *Queue :: operator[](unsigned int i);
Return the track and index i.
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:
The library is in charge of scanning and updating library paths added
to the tag database. In addition, the library layer is also in charge
of managing a library queue used by the UI. This queue has a special
file format, and will be saved to the file "library.q".
- Queue:
class LibraryQueue : public Queue {
public:
LibraryQueue();
save();
load();
set_flag(queue_flag);
unset_flag(queue_flag);
sort(sort_t, bool);
};
File << flags << _sort_order.size()
File << _sort_order[N].field << _sort_order[N].ascending << ...
- Validation:
Use a single idle function to loop over each track in the track
database. Check if the track still exists in the filesystem and remove
it from the tagdb if not.
- Updating:
Scan over all files in the current directory directory.
For each encountered directory:
Use the idle queue to call the update function with the new
directory as the "current" directory.
For each encountered file:
Attempt to add the file to the track_db.
Commit the database if at least one new file has been added.
- 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.
- LibraryQueue API:
LibraryQueue :: LibraryQueue();
Initialize a Queue with the flags Q_ENABLED and Q_REPEAT. The
default sorting order should be artist, year, track.
LibraryQueue :: save();
Write a library queue to disk.
LibraryQueue :: load();
Read a library queue from disk.
LibraryQueue :: set_flag(queue_flag f);
LibraryQueue :: unset_flag(queue_flag f);
LibraryQueue :: sort(sort_t field, bool reset);
These functions are wrappers around the default Queue
implementation. First call the original function, then use
save() to store the changes.
- API
void library :: init();
Scan the tagdb track list, and add each track to the library
queue.
Library *library :: add(string dir);
If dir is not a directory:
return NULL
Add a new path to the tag database, trigger an update, and
then return the corresponding Library tag to the caller.
void library :: remove(Library *library);
Invalidate a library_db row and all tracks owned by that path.
Do not use the library pointer after calling this function.
void library :: update(Library *library);
First, validate all tracks in the given library.
Next, trigger an update on the given library.
void library :: update_all();
Update all valid library paths.
void library :: set_enabled(Library *library, bool enabled);
Toggle if a library path is enabled or not. A disabled
library path will have its tracks removed from the
LibraryQueue.
Queue *library :: get_queue();
Return the LibraryQueue to the caller.
Playlist:
Playlists are a new feature in Ocarina 6 and are modeled after Gmail
labels. Ocarina 6.2 will support two different playlists that the
user can add tracks to: banned and favorites.
The playlist layer will maintain a queue that is used by the UI to
display tracks in a given playlist. This queue is inherited from
the base Queue class to provide extra features.
Future releases will add support for more playlists.
- Index:
Index playlist_db("playlist.db", true);
- Queue:
class PlaylistQueue : public Queue {
public:
PlaylistQueue();
fill(IndexEntry *);
};
- 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.
- PlaylistQueue API:
PlaylistQueue :: PlaylistQueue();
Initialize a Queue with the flags Q_ENABLED, Q_REPEAT, and
Q_NO_SORT set. Default sorting order should be artist, year,
track.
PlaylistQueue :: fill(IndexEntry *ent);
Remove all tracks in the queue and repopulate using ent.
- API
void playlist :: init():
Load the playlist index from file. Remove every banned song
from the LibraryQueue in the library layer.
void playlist :: add(Track *track, const std::string &name);
Add track->id to the playlist named "name" and return true.
Return false if the playlist does not exist.
If "name" is the currently selected playlist, add the track
to the PlaylistQueue.
If "name" is "Banned", remove the track from the LibraryQueue
in the library layer.
void playlist :: del(Track *track, const std::string &name);
Remove track->id from the playlist named "name" and return true.
Return false if the playlist does not exist or if the track
is not in the playlist.
If "name" is the currently selected playlist, remove the track
from the PlaylistQueue.
If "name" is "Banned", add the track to the LibraryQueue in the
library layer.
bool playlist :: has(Track *track, const std::string &name);
Return true if the chosen playlist has the given track.
Return false otherwise.
void playlist :: select(const std::string &name);
Change the currently displayed playlist to "name".
const IndexEntry *playlist :: get_tracks(const std::string &name);
Return the IndexEntry represeting the requested playlist.
Return NULL if the requested playlist does not exist.
Queue *playlist :: get_queue();
Return the PlaylistQueue to the caller.
Deck:
The deck is used to hold temporary queues created by the user. This
layer is also in charge of maintaining a "recently played" queue of
tracks.
The deck will be saved to the file "deck". When upgrading from file
version V0 to V1, use the saved random flag and sort order to set up
the library_q.
- TempQueue:
class TempQueue : public Queue {
public:
TempQueue(bool);
void set_flag(queue_flag);
void unset_flag(queue_flag);
unsigned int add(Track *);
void del(Track *);
void del(unsigned int);
void sort(sort_t, bool);
};
- Deck:
list<TempQueue> deck;
V0:
File << library_q.random << library_q.sort_order().size();
File << lib12order()[N].field << lib12order()[N].ascending;
File << deck.size() << endl;
File << deck[0] << endl;
File << deck[N] << endl;
V1:
File << deck.size() << endl;
File << deck[0] << endl;
File << deck[1] << endl;
- RecentQueue:
class RecentQueue : public Queue {
public:
RecentQueue();
unsigned int add(Track *);
};
- TempQueue API:
TempQueue :: TempQueue(bool random);
Initialize a new TempQueue with the flag Q_ENABLED set.
If random is True then also set the Q_RANDOM flag.
void TempQueue :: set_flag(queue_flag flag);
void TempQueue :: unset_flag(queue_flag flag);
unsigned int TempQueue :: add(Track *track);
void TempQueue :: del(Track *track);
void TempQueue :: del(unsigned int index);
void TempQueue :: sort(sort_t field, bool ascending);
These functions are all wrappers around the basic Queue
functions of the same name. First, call the corresponding
Queue :: <whatever>() function to make the correct Queue
modification. Then, call deck :: write() to save changes to
disk.
- RecentQueue API:
RecentQueue :: RecentQueue();
Initialize a Queue with the flags Q_ENABLED, Q_REPEAT, and
Q_NO_SORT set.
unsigned int RecentQueue :: add(Track *track);
The RecentQueue is designed to be a uniqueue queue that displays
the most recent tracks first.
del(track);
_cur = 0;
return _add_at(track, 0);
- API
void deck :: init();
Read the deck file from disk and restore the queues.
void deck :: write(File &);
Read or write the playqueue file. This will be called
from the audio layer to store state.
Queue *deck :: create(bool random);
Adds a new queue to the end of the deck and return a pointer
to it. Save the deck to disk.
void deck :: destroy(Queue *queue);
Remove the requested queue from the deck and trigger the
on_pq_removed() callback. Save the deck to disk.
void deck :: move(Queue *queue, unsigned int pos);
Move the queue to the new location in the deck. Save the deck
to disk.
unsigned int deck :: index(Queue *queue);
Return the index of the queue in the deck or deck.size() if
the queue is not currently in the deck.
Queue *deck :: get(unsigned int index);
Return the queue at the requested index, or NULL if no queue
is found.
Track *deck :: next();
Find the first enabled queue on the deck and return the track
given by queue->next().
If the queue is empty after calling next(), call destroy() to
remove it from the list.
If there are no enabled queues, return a track from the library
queue. If the library queue is empty, return NULL.
If the result is non-NULL, add the found track to the recent
queue.
Save the deck before returning.
Track *deck :: prev();
Return the track given by recent_queue->next(). If the recent
queue is empty, return NULL.
list<Queue> &deck :: get_queues();
Return the list of queues to the caller.
Queue *deck :: get_queue();
Return the RecentQueue to the caller.
Audio Driver:
The audio driver creates a way to fake audio playback for testing. This
will allow for more accurate tests, since I will know in advance what
values are returned to the Audio layer. This layer will derive from
the Driver class to implement either the GSTDriver or the TestDriver.
- Seconds -> Nanoseconds conversion:
static const unsigned long O_SECOND = 1000000000;
- Driver:
class Driver {
protected:
void (*on_eos)();
public:
Driver();
~Driver();
virtual void init(int *, char ***, void (*)(), void (*)()) = 0;
virtual void load(const std::string &) = 0;
virtual void play() = 0;
virtual void pause() = 0;
virtual void is_playing() = 0;
virtual void seek_to(long) = 0;
virtual long position() = 0;
virtual long duration() = 0
};
- Driver API:
void Driver :: Driver();
Initialize the audio driver. This involves setting up a GST
Bus in the GSTDriver case.
void Driver :: ~Driver();
In the GSTDriver case, call gst_deinit() to avoid memory leak
false positives.
void Driver :: init(int argc, char **argv, void (*eos_cb)(),
void (*error_cb)());
The GSTDriver will use this function to set up the playbin2.
When an end-of-stream message is received, call eos_cb().
If there is an error, call error_cb();
void Driver :: load(const std::string &file);
Load file for playback, but do not begin playback yet.
void Driver :: play();
Start playback. Return true if the state change operation
succeeds, false otherwise.
void Driver :: pause();
Pause playback. Return true if the state change operation
succeeds, false otherwise.
bool Driver :: is_playing();
Return true if the player is currently playing, false otherwise.
void Driver :: seek_to(long pos);
Change playback position in the current track in nanoseconds.
long Driver :: position();
Return the current position in the track in nanoseconds.
long Driver :: duration();
Return the duration of the track in nanoseconds.
- API:
Driver *driver :: get_driver();
Return the current driver to be used for audio playback. This
could be either the GSTDriver or the TestDriver depending on
if CONFIG_TEST is set when compiling.
Audio:
The audio layer uses the configured driver to control audio playback.
Gstreamer options passed to audio :: init() can be found by running
`gst-inspect-1.0 --help-gst` on the command line.
- File:
File cur_track("cur_track");
File << current_track->id << endl;
- API:
void audio :: init(int *argc, char ***argv);
Initialize the audio driver through argc and argv. Read in
the current track file and load the track.
void audio :: play();
void audio :: pause();
void audio :: seek_to(long pos);
Call the corresponding function from the audio driver, but only
if a track is loaded.
void audio :: stop();
pause()
seek_to(0)
long audio :: position();
long audio :: duration();
Call the corresponding function from the audio driver. Return
0 if no track is currently loaded.
std::string audio :: position_str();
Return the current audio position in string form.
Return an empty string if there is no current track.
void audio :: next();
Call the deck :: next() function to get the next track that
should be played and use the audio driver to load the track.
Save that track's ID to the cur_track file.
void audio :: prev();
Call the deck :: previous() function to find a new track to
play and use the audio driver to load the track.
Save that track's ID to the cur_track file.
void audio :: load_track(Track *track);
Load the requested track.
Save that track's ID to the cur_track file.
Track *audio :: current_track();
Return the currently playing Track.
Return NULL if there is no current track.
void audio :: pause_after(bool enabled, unsigned int N);
If enabled == true:
Configure Ocarina to pause playback after N tracks
have been played.
If enabled == false:
Do not automatically pause.
If N is greater than the current pause count then enabled should
be set to true.
bool audio :: pause_enabled();
unsigned int audio :: pause_count();
Use these functions to access the current "pause after N" state.
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