Skip to content

Latest commit

 

History

History
104 lines (56 loc) · 6.64 KB

DevelopmentNotes.md

File metadata and controls

104 lines (56 loc) · 6.64 KB

Development notes

In here are a few notes about how the code is organized, used concepts, etc.

The main code is all pure Python. It is highly modular. The main playing engine is implemented in C as a Python module (ffmpeg.c). It uses FFmpeg for decoding and PortAudio for output.

A basic principle is to keep the code as simple as possible so that it works. I really want to avoid to overcomplicate things.

The main entry point is main. It initializes all the modules. The list of modules is defined in State.modules. It contains for example queue, tracker, mediakeys, gui, etc.

Module

A module is controlled by the utils.Module class. It refers to a Python module (for example queue).

When you start a module (Module.start), it starts a new thread and executes the <modulename>Main function.

A module is supposed to be reloadable. There is the function Module.reload and State.reloadModules is supposed to reload all modules. This is mostly only used for/while debugging, though and is probably not stable and not well tested.

Multithreading and multiprocessing

The whole code makes heavy use of multithreading and multiprocessing. Every module already runs in its own thread. But some modules itself spawn also other threads. The GUI module spawns a new thread for most actions. Heavy calculations should be done in a seperate process so that the GUI and the playing engine (which run both in the main process) are always responsive. There is utils.AsyncTask and utils.asyncCall for an easy and stable way to do something in a seperate process.

Playing engine

This is all the Python native-C module ffmpeg. It provides a player object which represents the player. It needs a generator player.queue which yields Song objects which provide a way to read file data and seek in the file. See the source code for further detailed reference.

It has the following functionality:

  • Plays audio data via the player object. Uses FFmpeg for decoding and PortAudio for playing.
  • Can modify the volume via player.volume and also song.gain (see source code for details).
  • Prevents clipping via a smooth limiting functions which still leaves most sounds unaffected and keeps the dynamic range (see smoothClip).
  • Can calculate the ReplayGain value for a song (see pyCalcReplayGain). This is as far as I know the only other implementation of ReplayGain despite the original from mp3gain (gain_analysis.c).
  • Can calculate the AcoustId audio fingerprint (see pyCalcAcoustIdFingerprint). This one is also used by MusicBrainz. It uses the Chromaprint lib for implementation.
  • Provides a simple way to access the song metadata.
  • Provides a way to calculate a visual thumbnail for a song which shows the amplitude and the spectral centroid of the frequencies per time (see pyCalcBitmapThumbnail). Inspired by this project.

The player module creates the player object as State.state.player. It setups the queue as queue.queue. State.state provides also some functions to control the player state (playPause, nextSong).

GUI

The basic idea is that Python objects are directly represented in the GUI. The main window corresponds to the State.state object. Attributes of an object which should be shown in the GUI are marked via the utils.UserAttrib decorator. There, you can specify some further information to specify more concretely how an attribute should be displayed.

The GUI has its own module gui. At the moment, only an OSX Cocoa interface (guiCocoa) is implemented but a PyQt implementation is planned. There is some special handling for this module as it needs to be run in the main thread in most cases. See main for further reference.

Database

This is the module songdb.

The database is intended to be an optional system which stores some extra data/statistics about a song and also caches some data which is heavy to calculate (e.g. the fingerprint).

It provides several ways to identify a song:

  • By the SHA1 of its path name (relative to the users home dir).
  • By the SHA1 of its file.
  • By the SHA1 of its AcoustId fingerprint.

This is so that the database stays robust in case the user moves a song file around or changes its metadata.

It uses LevelDB as its backend via py-leveldb.

It uses binstruct for the serialization.

Song attribute knowledge system

Some of the initial ideas are presented attribs.txt. This is implemented now mostly for the Song class.

There are several sources where we can get some song attribute from:

  • The local song.__dict__.
  • The database.
  • The file metadata (e.g. artist, title, duration).
  • Calculate it from the file (e.g. duration, fingerprint, ReplayGain).
  • Look it up from some Internet service like MusicBrainz.

To have a generic attribute read interface which captures all different cases, there is the function:

Song.get(self, attrib, timeout, accuracy)

For each attrib, there might be functions:

  • Song._estimate_<attrib>, which is supposed to be fast. This is called no matter what the timeout is, in case we did not get it from the database.
  • Song._calc_<attrib>, which is supposed to return the exact value but is heavy to call. If this is needed, it will be executed in a seperate process.

See Song for further reference.

Playlist queue

The playlist queue is managed by the queue module. It has the logic to autofill the queue if there are too less songs in it. The algorithm to automatically select a new song uses the random file queue generator. This is a lazy directory unfolder and random picker, implemented in RandomFileQueue. Every time, it looks at a few songs and selects some song based on

  • the song rating,
  • the current recently played context (mostly the song genre / tag map).

Debugging

The module stdinconsole, when started with --shell, provides a handy IPython shell to the running application (in addition to the GUI which is still loaded). This is quite helpful to play around. In addition, as said earlier, all the modules are reloadable. I made this so I don't need to interrupt my music playing when playing with the code.