Refactor metadata and internal data structures

Replace Metadata.h with Containers.h, introducing a HashTable
implementation with SIMD acceleration and specialized track tracking.
Cleanup PlayerEngine logic by merging it into the playback flow and
refactoring several internal API signatures.
This commit is contained in:
2026-06-20 22:04:26 +02:00
parent 80bffc0fdd
commit 20c0f9744f
14 changed files with 345 additions and 162 deletions
+88
View File
@@ -0,0 +1,88 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <vector>
#include <string>
#include <taglib/tag.h>
struct Metadata {
TagLib::String artist;
TagLib::String title;
TagLib::String album;
TagLib::String albumArtist;
unsigned int year;
TagLib::VariantList artwork;
TagLib::String genre;
unsigned int trackNr;
double length;
std::string format;
unsigned int sampleRate;
unsigned int bitRate;
};
struct Track {
TagLib::String artist;
TagLib::String title;
TagLib::String album;
unsigned int trackNr;
unsigned int length;
std::string filePath;
std::string_view artwork;
std::string_view genre;
std::string format;
};
using trackPtr = std::shared_ptr<Track>;
class HashTable {
public:
static constexpr uint8_t EMPTY = 0x80;
static constexpr uint8_t DELETED = 0xFE;
explicit HashTable ( size_t capacity = 64 );
// delete disabled - requires rehash
HashTable ( const HashTable & ) = delete;
HashTable &operator= ( const HashTable & ) = delete;
HashTable ( HashTable && ) noexcept = default;
HashTable &operator= ( HashTable && ) noexcept = default;
void ht_insert ( const std::string &key, trackPtr track );
trackPtr ht_lookup ( const std::string &key ) const;
bool ht_delete ( const std::string &key );
std::vector<std::string> listAll () const;
void save ( const std::string &filename ) const;
std::vector<std::unique_ptr<Track>> load ( const std::string &filename );
std::size_t size () const noexcept { return size_; }
std::size_t capacity () const noexcept { return ctrl_.size(); }
private:
static constexpr double MAX_LOAD = 0.75;
static constexpr std::size_t GROUP_SIZE = 32;
struct Entry {
std::string key;
uint64_t hash = 0;
trackPtr track;
};
// helpers
static uint64_t hashString ( const std::string &key ) noexcept;
static uint8_t fingerprint ( uint64_t hash ) noexcept;
std::ptrdiff_t probe ( const std::string &key, uint64_t hash, bool insert ) const noexcept;
void rehash ( std::size_t newCapacity );
std::vector<uint8_t> ctrl_;
std::vector<Entry> entries_;
std::size_t size_ = 0;
double loadFactor_ = static_cast<double> ( size_ ) / static_cast<double> ( ctrl_.size());
}; // Class HashTable
+4 -3
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include "Metadata.h" #include "Containers.h"
#include <sqlite3.h> #include <sqlite3.h>
@@ -10,8 +10,9 @@ class Database {
public: public:
Database( const char* filename, sqlite3** db ); Database( const char* filename, sqlite3** db );
~Database(); ~Database();
void fetch( const std::string& query ); void fetch( const std::string& query ) const;
void fetchAlbum( const std::string& album ); void fetchAlbum( const std::string& album ) const;
void fetchAll() const;
void addSong( ); void addSong( );
private: private:
sqlite3* db_; sqlite3* db_;
+7 -7
View File
@@ -1,22 +1,22 @@
#pragma once #pragma once
#include "Containers.h"
#include <filesystem> #include <filesystem>
#include <vector> #include <vector>
#include <string> #include <string>
#define MUSIC_FOLDER = {"data/"};
namespace fs = std::filesystem; namespace fs = std::filesystem;
class File { class File {
public: public:
void importFile( std::string fileName ); void importFile( std::string fileName );
void importFolder( std::string folderPath ); void importFolder( const std::string& folderPath, std::vector<Track>& tracks );
std::vector<std::string> filenames_; void readFileTag( const char* fileName );
std::string parseFiletype( const std::string& path );
private: private:
std::string path_; std::string path_;
fs::path musicFolder_ MUSIC_FOLDER; fs::path musicFolder_ = "data/";
std::string fileName_; std::string fileName_;
fs::path fp_ = musicFolder_ / fileName_;
}; };
std::string_view parseFiletype( std::string_view path );
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include <string>
#include <taglib/tag.h>
struct Metadata {
TagLib::String artist;
TagLib::String title;
TagLib::String album;
TagLib::String albumArtist;
unsigned int year;
TagLib::VariantList artwork;
TagLib::String genre;
unsigned int trackNr;
double length;
std::string format;
unsigned int sampleRate;
unsigned int bitRate;
};
struct Track {
std::string_view artist;
std::string_view title;
std::string_view album;
unsigned int trackNr;
unsigned int length;
const char *filePath;
};
+1 -1
View File
@@ -28,7 +28,7 @@ class PlaybackEngine {
void seek ( ma_uint64 frame ); void seek ( ma_uint64 frame );
int isFinished () const; int isFinished () const;
ma_uint64 getPosition (); uint64_t getPosition ();
private: private:
ma_device device_; ma_device device_;
+14 -8
View File
@@ -1,6 +1,6 @@
#pragma once #pragma once
#include "../include/Metadata.h" #include "../include/Containers.h"
#include "../include/PlaybackEngine.h" #include "../include/PlaybackEngine.h"
#include "../include/miniaudio.h" #include "../include/miniaudio.h"
@@ -8,7 +8,8 @@
#include <string> #include <string>
#include <vector> #include <vector>
using Queue = std::vector<Track>; using trackVec = std::vector<Track>;
using ht = HashTable;
class Player { class Player {
public: public:
@@ -33,16 +34,16 @@ class Player {
virtual std::string &getAlbum () const; virtual std::string &getAlbum () const;
virtual unsigned int getTrackNr () const; virtual unsigned int getTrackNr () const;
// Player Queue // Player trackVec
Queue getQueue () const; trackVec getQueue () const;
Queue clearQueue (); trackVec clearQueue ();
void addToQueue ( Track &track ); void addToQueue ( Track &track );
Queue rempveFromQueue ( Track &track ); trackVec rempveFromQueue ( Track &track );
Queue shuffleQueue (); trackVec shuffleQueue ();
protected: protected:
PlaybackEngine PE_; PlaybackEngine PE_;
Queue queue_; trackVec queue_;
private: private:
std::string artist_; std::string artist_;
@@ -53,4 +54,9 @@ class Player {
unsigned int samplerate_ = 48000; unsigned int samplerate_ = 48000;
unsigned int channels_ = 2; unsigned int channels_ = 2;
trackVec tracks;
ht songs;
ht artists;
ht albums;
}; };
-36
View File
@@ -1,36 +0,0 @@
#pragma once
#include "miniaudio.h"
class PlayerEngine {
private:
ma_engine engine_;
ma_engine_config engineConfig_;
ma_device device_;
ma_device_config deviceConfig_;
unsigned int sampleRate_;
unsigned int channels_ = 2;
ma_format format_ = ma_format_unknown;
ma_context context_;
public:
PlayerEngine();
PlayerEngine( unsigned int sampleRate, unsigned int channels, ma_format format );
~PlayerEngine();
ma_device getDevice() const;
unsigned int getChannels() const;
unsigned int getSampleRate() const;
ma_format getFormat() const;
void setDevice( ma_device device );
void setSampleRate( unsigned int sampleRate );
void setChannels( unsigned int channels );
void setFormat( ma_format format );
ma_device_data_proc data_callback();
void configureDevice( ma_context& context );
void initDevice ();
void initEngine();
};
+15 -9
View File
@@ -1,20 +1,26 @@
#pragma once #pragma once
#include "Metadata.h" #include "containers.h"
//#include "file.h"
#include <filesystem>
#include <memory> #include <memory>
#include <taglib/tstring.h> #include <taglib/tstring.h>
#include <taglib/fileref.h> #include <taglib/fileref.h>
#include <taglib/tvariant.h> #include <taglib/tvariant.h>
class Tag { namespace fs = std::filesystem;
public:
void readTag( TagLib::FileName fileName );
Metadata getMetadata() const;
const std::string& getFilePath() const; class tag {
const auto getArtwork() const; public:
void readtag( TagLib::filename filename );
metadata getmetadata() const;
const std::string& getfilepath() const;
const auto getartwork() const;
private: private:
Metadata metadata_; metadata metadata_;
std::string filePath_; std::string filepath_;
fs::path musicdir = music_folder;
}; };
BIN
View File
Binary file not shown.
+37 -8
View File
@@ -16,7 +16,7 @@ db::~Database() {
sqlite3_close( db_ ); sqlite3_close( db_ );
} }
void db::fetch( const std::string& query ) { void db::fetch( const std::string& query ) const {
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
int rc = sqlite3_prepare_v2( db_, query.c_str(), -1, &stmt, nullptr ); int rc = sqlite3_prepare_v2( db_, query.c_str(), -1, &stmt, nullptr );
@@ -49,9 +49,11 @@ void db::fetch( const std::string& query ) {
sqlite3_finalize( stmt ); sqlite3_finalize( stmt );
} }
void db::fetchAlbum( const std::string& album ) {
void db::fetchAlbum( const std::string& album ) const {
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
const char* sql = "SELECT artist, title, album, trackNr from songs WHERE album = ? ORDER BY trackNr; "; const char* sql = "SELECT artist, title, album, trackNr from music WHERE album = ? ORDER BY trackNr; ";
if ( sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ) != SQLITE_OK ) { if ( sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr ) != SQLITE_OK ) {
throw std::runtime_error( sqlite3_errmsg( db_ ) ); throw std::runtime_error( sqlite3_errmsg( db_ ) );
@@ -62,12 +64,39 @@ void db::fetchAlbum( const std::string& album ) {
while ( sqlite3_step( stmt ) == SQLITE_ROW ) { while ( sqlite3_step( stmt ) == SQLITE_ROW ) {
Track t; Track t;
Player p; Player p;
t.artist = reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 0 )); t.artist = sqlite3_column_text( stmt, 0 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 0 )) : "";
t.title = reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 1 )); t.title = sqlite3_column_text( stmt, 1 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 1 )) : "";
t.album = reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 1 )); t.album = sqlite3_column_text( stmt, 1 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 1 )) : "";
t.trackNr = sqlite3_column_int( stmt, 3 ); t.trackNr = sqlite3_column_int( stmt, 3 ) ? sqlite3_column_int( stmt, 3 ) : 0;
p.addToQueue( t ); p.addToQueue( t );
} }
sqlite3_finalize( stmt );
}
void db::fetchAll() const {
sqlite3_stmt* stmt;
const char* sql = "SELECT * FROM music";
int rc = sqlite3_prepare_v2( db_, sql, -1, &stmt, nullptr );
if ( rc != SQLITE_OK ) {
throw std::runtime_error( sqlite3_errmsg( db_ ));
}
while ( (rc = sqlite3_step( stmt )) == SQLITE_ROW ) {
Track t;
Player p;
t.artist = sqlite3_column_text( stmt, 0 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 0 )) : "";
t.title = sqlite3_column_text( stmt, 1 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 1 )) : "";
t.album = sqlite3_column_text( stmt, 1 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 1 )) : "";
t.trackNr = sqlite3_column_int( stmt, 3 ) ? sqlite3_column_int( stmt, 3 ) : 0;
t.filePath = sqlite3_column_text( stmt, 4 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 4 )) : "";
t.length = sqlite3_column_int( stmt, 5 ) ? sqlite3_column_int ( stmt, 5 ) : 0;
t.artwork = sqlite3_column_text( stmt, 6 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 6 )) : "";
t.genre = sqlite3_column_text( stmt, 7 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 7 )) : "";
t.format = sqlite3_column_text( stmt, 8 ) ? reinterpret_cast<const char*> ( sqlite3_column_text( stmt, 8 )) : "";
}
sqlite3_finalize( stmt );
} }
+15 -7
View File
@@ -3,24 +3,32 @@
#include <filesystem> #include <filesystem>
#include <vector> #include <vector>
#include <taglib/fileref.h>
namespace fs = std::filesystem; namespace fs = std::filesystem;
void File::importFolder( std::string folderPath ) { void File::importFolder(const std::string& folderPath, std::vector<Track>& tracks ) {
for ( const auto& audioFile : fs::directory_iterator{ folderPath }) { for ( const auto& audioFile : fs::directory_iterator{ folderPath }) {
filenames_.reserve( 20 ); Track t;
filenames_.push_back( audioFile.path() ); t.filePath = static_cast<std::string>(audioFile.path());
tracks.push_back( t );
} }
} }
void File::importFile( std::string fileName ) { void File::readFileTag( const char* fileName, std::vector<Track>& tracks ) {
fileName_ = fileName; TagLib::FileRef f( fileName );
fs::path fp = fp_; Track t;
if ( !f.isNull() ) {
t.artist = ( f.tag()->artist().isEmpty() ) ? "" : ( f.tag()->artist());
t.title = ( f.tag()->title().isEmpty() ) ? "" : ( f.tag()->title());
t.album = ( f.tag()->album().isEmpty() ) ? "" : ( f.tag()->album());
t.trackNr = ( f.tag()->track() );
} }
}
std::string_view parseFiletype( std::string_view path ){ std::string File::parseFiletype( const std::string& path ){
size_t length = path.size(); size_t length = path.size();
size_t dot = path.find_last_of( "."); size_t dot = path.find_last_of( ".");
+163
View File
@@ -0,0 +1,163 @@
#include "../include/Containers.h"
#include <cstdint>
#include <cstddef>
#include <stdexcept>
#ifndef __AVX2__
#include <immintrin.h>
#endif
using ht = HashTable;
using trackPtr = std::shared_ptr<Track>;
ht::HashTable( std::size_t capacity ) :ctrl_ ( capacity, EMPTY ), entries_ ( capacity ), size_( 0 ) {
if ( !capacity ) throw std::invalid_argument( "Capacity must be > 0" );
}
uint64_t ht::hashString( const std::string& key ) noexcept {
uint64_t hash = 14695981039346656037ULL;
for ( char c : key ) {
hash ^= static_cast<uint8_t> ( c );
hash *= 1099511628211ULL;
}
return hash;
}
uint8_t ht::fingerprint ( uint64_t hash ) noexcept {
return static_cast<uint8_t> ( hash & 0x7F ); // Use lower 7 bits for fingerprint
}
std::ptrdiff_t ht::probe ( const std::string &key, uint64_t hash, bool insert ) const noexcept {
const std::size_t capacity = ctrl_.size();
const uint8_t fp = fingerprint ( hash );
#ifdef __AVX2__
std::size_t group = ( hash % capacity ) & ~( GROUP_SIZE - 1 ); // Align to group boundary
__m256i fpVec = _mm256_set1_epi8 ( fp );
__m256i emptyVec = _mm256_set1_epi8 ( static_cast<char> ( EMPTY ) );
std::ptrdiff_t firstTombstone = -1;
for ( ;; ) {
__m256i ctrlVec = _mm256_loadu_si256 ( reinterpret_cast<const __m256i *> ( &ctrl_[ group ] ) );
uint32_t matchMask = _mm256_movemask_epi8 ( _mm256_cmpeq_epi8 ( ctrlVec, fpVec ) );
uint32_t emptyMask = _mm256_movemask_epi8 ( _mm256_cmpeq_epi8 ( ctrlVec, emptyVec ) );
// check candidates
for ( uint32_t mask = matchMask; mask != 0; mask &= ( mask - 1 ) ) {
std::ptrdiff_t idx = ( group + static_cast<std::size_t> ( __builtin_ctz ( mask ) ) ) % capacity;
if ( entries_[ idx ].hash == hash && entries_[ idx ].key == key ) {
return static_cast<std::ptrdiff_t> ( idx ); // Found
}
}
// record first tombstone for this group
if ( insert && firstTombstone == -1 ) {
for ( std::size_t i = 0; i < GROUP_SIZE; ++i ) {
std::size_t idx = ( group + i ) % capacity;
if ( ctrl_[ idx ] == DELETED ) {
firstTombstone = static_cast<std::ptrdiff_t> ( idx );
break;
}
}
}
if ( emptyMask ) {
if ( !insert )
return -1; // Not found
std::size_t slot = ( group + static_cast<std::size_t> ( __builtin_ctz ( emptyMask ) ) ) % capacity;
return firstTombstone != -1 ? firstTombstone : static_cast<std::ptrdiff_t> ( slot ); // Insert here
}
group = ( group + GROUP_SIZE ) % capacity; // Move to next group
}
#else // Fallback to scalar probing
std::size_t group = ( hash % capacity ) & ~( GROUP_SIZE - 1 ); // Align to group boundary
std::ptrdiff_t firstTombstone = -1;
for ( ;; ) {
bool foundEmpty = false;
std::size_t emptyIdx = 0;
for ( std::size_t i = 0; i < GROUP_SIZE; ++i ) {
std::size_t idx = ( group + i ) % capacity;
uint8_t c = ctrl_.at( idx );
if ( c == EMPTY ) {
if ( !foundEmpty ) {
foundEmpty = true;
emptyIdx = idx;
}
} else if ( c == DELETED ) {
if ( insert && firstTombstone == -1 ) {
firstTombstone = static_cast<std::ptrdiff_t> ( idx );
}
} else if ( c == fp && entries_.at( idx ).hash == hash && entries_.at( idx ).key == key ) {
return static_cast<std::ptrdiff_t> ( idx ); // Found
}
}
if ( foundEmpty ) {
if ( !insert )
return -1; // Not found
return firstTombstone != -1 ? firstTombstone : static_cast<std::ptrdiff_t> ( emptyIdx ); // Insert here
}
group = ( group + GROUP_SIZE ) % capacity; // Move to next group
} // end probeloop
#endif
} // probe
void ht::ht_insert( const std::string& key, trackPtr track ) {
//double loadFactor = static_cast<double> ( size_ ) / static_cast<double> ( ctrl_.size());
if ( loadFactor_ > MAX_LOAD ) rehash( ctrl_.size() * 2 );
uint64_t hash = hashString ( key );
auto idx = static_cast<std::size_t> ( probe ( key, hash, true ) );
if ( ctrl_.at( idx ) == EMPTY || ctrl_.at( idx ) == DELETED ) {
Entry tmp;
tmp.key = key;
tmp.hash = hash;
tmp.track = track;
entries_.at( idx ) = std::move( tmp );
++size_;
} else {
entries_.at( idx ).track = track;
}
ctrl_.at( idx ) = fingerprint( hash );
}
trackPtr ht::ht_lookup( const std::string& key ) const {
uint64_t hash = hashString( key );
auto idx = probe( key, hash, false );
if ( idx != -1 ) return entries_.at( static_cast<std::size_t> ( idx )).track;
return nullptr;
}
bool ht::ht_delete( const std::string& key ) {
uint64_t hash = hashString( key );
auto raw = probe( key, hash, false );
if ( raw == -1 ) return false;
auto idx = static_cast<size_t>( raw );
ctrl_.at( idx ) = DELETED;
entries_.at( idx ) = Entry();
--size_;
return true;
}
void ht::rehash( std::size_t newCapacity ) {
ht newTable( newCapacity );
for ( std::size_t i = 0; i < ctrl_.size(); ++i ) {
if ( ctrl_.at( i ) != EMPTY && ctrl_.at( i ) != DELETED ) {
const Entry& entry = entries_.at( i );
newTable.ht_insert( entry.key, entry.track );
}
}
ctrl_ = std::move( newTable.ctrl_ );
entries_ = std::move( newTable.entries_ );
size_ = newTable.size_;
}
+1 -1
View File
@@ -69,7 +69,7 @@ void PE::seek ( ma_uint64 frame ) {
ma_decoder_seek_to_pcm_frame ( &state.decoder, frame ); ma_decoder_seek_to_pcm_frame ( &state.decoder, frame );
} }
auto PE::getPosition () { uint64_t PE::getPosition () {
ma_decoder_get_cursor_in_pcm_frames ( &state.decoder, &cursor_ ); ma_decoder_get_cursor_in_pcm_frames ( &state.decoder, &cursor_ );
return cursor_; return cursor_;
} }
-54
View File
@@ -1,54 +0,0 @@
#include "../include/PlayerEngine.h"
#include <stdexcept>
using pe = PlayerEngine;
// Constructor
pe::PlayerEngine(){
configureDevice(context_ );
initDevice();
ma_device_start( &device_ );
}
pe::PlayerEngine( unsigned int sampleRate, unsigned int channels, ma_format format ) : sampleRate_{ sampleRate }, channels_{ channels }, format_( format ) {
configureDevice(context_ );
initDevice();
ma_device_start( &device_ );
}
pe::~PlayerEngine(){
ma_device_uninit( &device_ );
}
// Getters
ma_device pe::getDevice() const { return device_; }
ma_format pe::getFormat() const { return format_; }
unsigned int pe::getSampleRate() const { return sampleRate_; }
unsigned int pe::getChannels() const { return channels_; }
// Setters
void pe::setDevice( ma_device device ) { device_ = device; }
void pe::setSampleRate ( unsigned int sampleRate ) { sampleRate_ = sampleRate; }
void pe::setChannels( unsigned int channels ) { channels_ = channels; }
void pe::setFormat( ma_format format ) { format_ = format; }
void pe::configureDevice( ma_context& context ) {
ma_device_config conf = deviceConfig_;
conf = ma_device_config_init( ma_device_type_playback);
conf.playback.format = format_;
conf.playback.channels = channels_;
conf.sampleRate = sampleRate_;
conf.dataCallback = data_callback();
}
void pe::initDevice() {
if( ma_device_init( NULL, &deviceConfig_, &device_ ) != MA_SUCCESS) {
throw std::runtime_error{ "Device could not be initialized." };
}
}
void initEngine() {
ma_result result;
ma_engine_config conf;
conf = ma_engine_config_init();
// TODO resource Manager
}