diff options
Diffstat (limited to 'content/blog/rockbox_stats')
| -rw-r--r-- | content/blog/rockbox_stats/index.md | 547 | ||||
| -rw-r--r-- | content/blog/rockbox_stats/log-setting.bmp | bin | 0 -> 153666 bytes | |||
| -rw-r--r-- | content/blog/rockbox_stats/playback-settings.bmp | bin | 0 -> 153666 bytes | |||
| -rw-r--r-- | content/blog/rockbox_stats/player.bmp | bin | 0 -> 153666 bytes |
4 files changed, 547 insertions, 0 deletions
diff --git a/content/blog/rockbox_stats/index.md b/content/blog/rockbox_stats/index.md new file mode 100644 index 0000000..626cbe9 --- /dev/null +++ b/content/blog/rockbox_stats/index.md @@ -0,0 +1,547 @@ ++++ +title = "Rockbox Stat Tracking" +date = "2025-09-02" ++++ + +<article> +<section> +<div class="text-section"> +<p>In this post I talk about how I went about setting up a <a href="/rockstats" target="_blank">stat visualization page</a> for my rockbox mp3 player.</p> +</div> +<figure class="cover-image"> +<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360"> +<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect> +<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline> +<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline> +<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline> +<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline> +<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline> +<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline> +<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline> +<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline> +<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path> +<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path> +<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path> +<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path> +<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path> +<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path> +<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path> +<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path> +<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text> +<style> +.zr0-cls-0:hover { +cursor:pointer; +} +.zr0-cls-1:hover { +cursor:pointer; +fill:rgba(253,138,140,1); +} +.zr0-cls-2:hover { +cursor:pointer; +fill:rgba(253,167,128,1); +} +.zr0-cls-3:hover { +cursor:pointer; +fill:rgba(240,206,139,1); +} +.zr0-cls-4:hover { +cursor:pointer; +fill:rgba(183,211,140,1); +} +.zr0-cls-5:hover { +cursor:pointer; +fill:rgba(144,211,160,1); +} +.zr0-cls-6:hover { +cursor:pointer; +fill:rgba(139,205,196,1); +} +.zr0-cls-7:hover { +cursor:pointer; +fill:rgba(235,168,200,1); +} +.zr0-cls-8:hover { +cursor:pointer; +fill:rgba(255,93,90,1); +} +</style> +</svg> +<figcaption>A static site generation experiment</figcaption> +</figure> +</section> +<section class="text-section"> + <h2>Preamble: Digital Sovereignity & Rockbox</h2> + <p> + I've been building up a pretty sizeable collection of digital music + over the last couple of years. I think there's a lot of value in owning + the music I pay for and being able to choose how I listen to it. + Purchasing music also allows me to support artists in a more direct + and substantial way than the fractions of cents for using streaming services, + but that's more of a happy consequence than some moral obligation I feel. + </p> + <p> + Over the years, I've enjoyed listening to my music in a variety of ways. + For years I kept all of my music files on all of my devices and used + various local music clients depending on the platform, most notably mpd + and ncmpcpp on linux. Eventually, as I charged headlong into the glorious + world of self-hosting, I began using a central Jellyfin media server that + I stream music and video from. It's super convenient, and works on all of + my devices (including my TV!). + </p> + <p> + My media server is great, and it's been the primary way I listen to music + for a while now. But it has limitations. For example, I don't expose my media + server to the internet, so I'm unable to stream from it while I'm out and + about. And even if I could, the bandwidth requirements would be pretty high. + I figured I would need a dedicated music player if I wanted to take my music + library on the go, and settled on the HIFI Walker H2 after reading some + online recommendations. The ability to install <a href="https://rockbox.org" target="_blank">Rockbox</a>, an open-source firmware, + was a big factor in my decision. I couldn't tell you how the device works + out of the box, since I flashed the firmware pretty much immediately once I got it, + but I've been super impressed with how the device works while running Rockbox. + </p> + <p> + <figure class="fig fig-right"> + <img src="player.bmp" alt="Screenshot of Rockbox player showing cool theme."> + <figcaption>I'm using a modified version of the <a + href="https://themes.rockbox.org/index.php?themeid=3266&target=aigoerosq" + target="_blank">InfoMatrix-v2</a> theme, which looks great.</figcaption> + </figure> + Rockbox comes with many codecs for common audio formats including FLAC and MP3. The + device boots extremely quickly, and the interface is snappy. Virtually every aspect + of the user experience is tweakable and customizable to a crazy degree. I've even begun + listening to music on my player even at home, since a device specifically for the + purpose provides less distraction while I'm trying to be productive. + </p> + <p> + All this to say I'm pretty much sold on Rockbox. But there's certain things I + still miss from my days of being a user of popular services like Spotify with + fancy APIs and data tracking. Things like Spotify wrapped or third-party apps + for visualizing playback statistics are a fun way to see what my listening history + looks like and could potentially be used to help find more music that I'd enjoy. + This is why when I noticed that Rockbock has a playback logging feature, a little + lightbulb lit up over my head. + </p> +</section> +<section class="text-section"> + <h2>Generating and Parsing Logs</h2> + <p> + <figure class="fig fig-right"> + <img src="log-setting.bmp" alt="Logging"> + <figcaption>The logging feature can be accessed through the settings menu.</figcaption> + </figure> + Rockbox has a feature that logs playback information to a text file. This feature can + be enabled by setting <b>Playback Settings > Logging</b> to "On". With this setting enabled, a + new line gets added to the end of the <b>.rockbox/playback.log</b> file every time you play a track, + containing info about what you played and when. + </p> + <p> + The logging feature is actually already used by the LastFM scrobbler plugin that comes preloaded with + Rockbox, which is probably the simplest way to get insights into your playback. However, + I personally want to avoid using third-party services as much as possible, because it's more fun. + </p> + <p> + If I take a look at a logfile generated after a bit of listening, I'll see that I've wound up with + a series of lines that each look something like this: + <figure class="fig"> + <pre><samp>1758478258:336689:336718:/<microSD0>/Music/This Is The Glasshouse/This Is The Glasshouse - 867/This Is The Glasshouse - 867 - 01 Streetlight By Streetlight.flac</samp></pre> + <figcaption>An example of a log entry for "Streetlight by Streetlight" by This is the Glasshouse. + </figcaption> + </figure> + </p> + <p> + I wasn't really able to find any information online about the format of these logs, but they appear + to be simple enough to figure out. From what I can tell, each event is broken up into 4 pieces: + <ol> + <li><b>Timestamp:</b> The number of milliseconds since the UNIX epoch. + <li><b>Playback Duration:</b> The amount of the song that was played, in milliseconds. + <li><b>Total Track Length:</b> The length of the played track, in milliseconds. + <li><b>File Path:</b> An absolute path to the file containing the track on the filesystem. + </ol> + All of this is enough to know what I was listening to and when. I can use the file path to check for + audio tags which can help glean even more information about my listening habits. + </p> + <p>Now that I have this information and know how to interpret it, I'm ready to start processing it!</p> +</section> +<section class="text-section"> + <h2>Analyzing Playback History</h2> + <p> + In order to get some useful information out of my playback history, I think it's a good idea to start by + building + a database. I created a sqlite database with the following tables: + <div class="sunken-panel" style="width: min-content;"> + <table class="schema-table"> + <thead> + <tr> + <th colspan="3">songs</th> + </tr> + </thead> + <tbody> + <tr> + <td>id</td> + <td>i64</td> + <td>PK</td> + </tr> + <tr> + <td>title</td> + <td>String</td> + <td></td> + </tr> + <tr> + <td>artists</td> + <td>JSON</td> + <td></td> + </tr> + <tr> + <td>album_id</td> + <td>i64?</td> + <td></td> + </tr> + <tr> + <td>genre</td> + <td>String?</td> + <td></td> + </tr> + </tbody> + </table> + </div> + <div class="sunken-panel" style="width: min-content;"> + <table class="schema-table"> + <thead> + <tr> + <th colspan="3">albums</th> + </tr> + </thead> + <tbody> + <tr> + <td>id</td> + <td>i64</td> + <td>PK</td> + </tr> + <tr> + <td>title</td> + <td>String</td> + <td></td> + </tr> + <tr> + <td>artist</td> + <td>String</td> + <td></td> + </tr> + <tr> + <td>cover_art</td> + <td>Blob?</td> + <td></td> + </tr> + </tbody> + </table> + </div> + <div class="sunken-panel" style="width: min-content;"> + <table class="schema-table"> + <thead> + <tr> + <th colspan="3">history</th> + </tr> + </thead> + <tbody> + <tr> + <td>id</td> + <td>i64</td> + <td>PK</td> + </tr> + <tr> + <td>timestamp</td> + <td>Datetime</td> + <td></td> + </tr> + <tr> + <td>duration</td> + <td>i64</td> + <td></td> + </tr> + <tr> + <td>song_id</td> + <td>i64</td> + <td></td> + </tr> + </tbody> + </table> + </div> + <br> + I can add more columns later, but this is a good place to start. + </p> + <p> + Now, as I read through the logfile line-by-line, I can check if each album exists before + inserting it into the database: + <figure class="fig"> + <pre><code>for line in log_file.lines().flatten() { +println!("{line}"); +// Skip comments +if line.starts_with("#") { +continue; +} +let chunks = line.split(":").collect::<Vec<_>>(); + +let timestamp = DateTime::from_timestamp_secs( +i64::from_str_radix(chunks[0], 10).context("Failed to parse timestamp")?, +) +.context("Failed to convert timestamp")?; + +// Load tags from file on device +let file_path = chunks[chunks.len() - 1][1..] +.split_once("/") +.context("Missing file")? +.1; +let tags = Tag::new() +.read_from_path(args.mount_point.join(file_path)) +.context("Failed to read audio tags")?; + +//... +}</code></pre> + <figcaption>Parsing log entry and loading audio metadata.</figcaption> + </figure> + <figure class="fig"> + <pre><code>if let Some(existing_album) = +sqlx::query("SELECT id FROM albums WHERE title=$1 AND artist=$2") +.bind(album_title) +.bind(album_artist) +.fetch_optional(&mut *db) +.await +.context("Failed to execute query to find existing album")? +{ +let album_id: i64 = existing_album.get("id"); +info!("Album already exists, id {album_id}"); +//... +} else { +info!("Inserting new album: {album_title} by {album_artist}"); +//... +let result = sqlx::query( +"INSERT INTO albums (title, artist, cover_art) VALUES ($1, $2, $3);", +) +.bind(album_title) +.bind(album_artist) +.bind(cover) +.execute(&mut *db) +.await +.context("Failed to execute query to insert album into database")?; + +//... +}</code></pre> + <figcaption>Checking for an album with matching artist and title before creating a new row in the + database.</figcaption> + </figure> + I did something similar with the <b>songs</b> and <b>history</b> tables, basically building up a cache + of history information and skipping anything that's already in the database on repeat runs. + </p> + <p> + With this database constructed, it's pretty easy to get a bunch of different information + about my listening. For example (forgive me if my SQL skills are kind of ass lol): + <figure class="fig"> + <pre><code>SELECT +songs.title AS song_title, +songs.artists AS song_artists, +songs.genre AS song_genre, +albums.title AS album_title, +albums.artist AS album_artist, +history.timestamp AS timestamp, +history.duration AS duration +FROM history +CROSS JOIN songs ON songs.id = history.song_id +CROSS JOIN albums ON albums.id = songs.album_id +ORDER BY timestamp DESC;</code></pre> + <figcaption>Querying for a list of each history entry along with track metadata, sorted from most to + least recent.</figcaption> + </figure> + <figure class="fig"> + <pre><code>SELECT +songs.genre, +SUM(history.duration) AS total_duration +FROM history +CROSS JOIN songs ON history.song_id = songs.id +GROUP BY genre +ORDER BY total_duration DESC +LIMIT 10; </code></pre> + <figcaption>Querying for the top 10 most listened genres by playtime.</figcaption> + </figure> + </p> + <p> + It's all well and good to be able to view this information using a database client, + but it would be really cool if I could visualize this data somehow. + </p> +</section> +<section class="text-section"> + <h2>Visualizing this Data Somehow</h2> + <p> + I wanted to make this data available on my website for people to view, and for a bunch of mostly trivial + reasons I won't get into here, I have a couple of requirements for pages on this site: + <ol> + <li>Pages need to be static. + <li>Pages need to be JavaScript-free. + </ol> + This means any chart rendering needs to be done automatically at build time before + deploying. I don't currently use a static site generator for my site (just for fun), + so I'm basically going to need to write one specifically to generate this page. + </p> + <p> + I won't get too deep into the specifics of how I queried the database and generated each visualization + on + the page, but I can explain the visualizations I created using the queries from the previous section. + For the + listening history I wanted to generate a table displaying the information. To accomplish this, I first + used a combination of <a href="https://crates.io/crates/sqlx" target="_blank">sqlx</a>'s ability to convert a row to a struct and <a href="https://crates.io/crates/serde" target="_blank">serde</a> to serialize + the rows as JSON values. + <figure class="fig"> + <pre><code>#[derive(Serialize, Deserialize, FromRow)] +struct HistoryEntry { +song_title: String, +song_artists: Value, +timestamp: DateTime<Utc>, +duration: i64, +album_title: String, +album_artist: Option<String>, +song_genre: Option<String>, +} + +//...later +let history = sqlx::query_as::<_, HistoryEntry>( +/* SELECT... */ +).fetch_all(&mut *db).await; + +//...later still, tera context accepts +let mut context = tera::Context::new(); +context.insert("history", &history); +</code></pre> + <figcaption>Struct definition for a history entry, allowing conversion from a sqlx row and + de/serialization from/to JSON.</figcaption> + </figure> + </p> + <p> + In order to keep the generation as painless as possible, I decided to use the <a href="https://keats.github.io/tera" target="_blank">Tera</a> template + engine, which allows me to define a template HTML file and substitute in values from + a context which I can define before rendering. In the case of the table, I can just generate a <code><tr></code> + matching the data for each item: + <figure class="fig"> + <pre><code>{% macro history_table(history) %} +<h3>Playback History</h3> +<div class="table-container"> +<table> +<thead> + <tr> + <th>Timestamp</th> + <th>Played Duration</th> + <th>Title</th> + <th>Artists</th> + <th>Album</th> + <th>Genre</th> + </tr> +</thead> +<tbody> + {% for item in history %}<tr> + <td>{{ item.timestamp | date(format="%Y-%m-%d %H:%M:%S") }}</td> + <td>{{ item.duration | hms }}</td> + <td>{{ item.song_title }}</td> + <td>{{ item.song_artists }}</td> + <td>{{ item.album_title }}</td> + <td>{{ item.song_genre }}</td> + </tr> + {% endfor %} +</tbody> +</table> +</div> +{% endmacro history_table %}</code></pre> + <figcaption> + A Tera macro for generating a table from a list of playback history items. + I used a macro so I can re-use this later if I want to add time range views. + (last month, year, etc.) + </figcaption> + </figure> + </p> + <p> + I wrote similar macros for each of the visualizations I wanted to create. Most are + easy, but for my top 10 genres I wanted to display a pie chart. I found a pretty decent + data visualization crate called <a href="https://crates.io/crates/charming" target="_blank">charming</a> that's able to render to html, however + the output contains javascript so it's a no-go for me. Luckily, it can also render to + an SVG which I can embed nicely within the page. + <figure class="fig"> +<svg width="100%" height="360" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="full" viewBox="0 0 640 360"> +<rect width="640" height="360" x="0" y="0" fill="rgb(0,0,0)" fill-opacity="0"></rect> +<polyline points="427.2 233.9 440.6 240.7 455.6 240.7" fill="transparent" stroke="#e67e80" class="zr0-cls-0"></polyline> +<polyline points="202.3 203.4 192.6 206.3 177.6 206.3" fill="transparent" stroke="#e69875" class="zr0-cls-0"></polyline> +<polyline points="220 113.6 197.5 135.5 182.5 135.5" fill="transparent" stroke="#dbbc7f" class="zr0-cls-0"></polyline> +<polyline points="273.2 69.5 190.3 117.4 190.3 117.4" fill="transparent" stroke="#a7c080" class="zr0-cls-0"></polyline> +<polyline points="304.6 61 201.7 99.3 201.7 99.3" fill="transparent" stroke="#83c092" class="zr0-cls-0"></polyline> +<polyline points="310.7 60.4 217.9 81.2 217.9 81.2" fill="transparent" stroke="#7fbbb3" class="zr0-cls-0"></polyline> +<polyline points="315.4 60.1 242.2 63.1 242.2 63.1" fill="transparent" stroke="#d699b6" class="zr0-cls-0"></polyline> +<polyline points="318.6 60 318.4 45 303.4 45" fill="transparent" stroke="#f85552" class="zr0-cls-0"></polyline> +<path d="M320 60A120 120 0 1 1 223.6 251.5L320 180Z" fill="#e67e80" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="0" ecmeta_ssr_type="chart" class="zr0-cls-1"></path> +<path d="M223.6 251.5A120 120 0 0 1 203.6 150.7L320 180Z" fill="#e69875" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="1" ecmeta_ssr_type="chart" class="zr0-cls-2"></path> +<path d="M203.6 150.7A120 120 0 0 1 247.9 84.1L320 180Z" fill="#dbbc7f" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="2" ecmeta_ssr_type="chart" class="zr0-cls-3"></path> +<path d="M247.9 84.1A120 120 0 0 1 301.3 61.5L320 180Z" fill="#a7c080" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="3" ecmeta_ssr_type="chart" class="zr0-cls-4"></path> +<path d="M301.3 61.5A120 120 0 0 1 307.8 60.6L320 180Z" fill="#83c092" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="4" ecmeta_ssr_type="chart" class="zr0-cls-5"></path> +<path d="M307.8 60.6A120 120 0 0 1 313.6 60.2L320 180Z" fill="#7fbbb3" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="5" ecmeta_ssr_type="chart" class="zr0-cls-6"></path> +<path d="M313.6 60.2A120 120 0 0 1 317.2 60L320 180Z" fill="#d699b6" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="6" ecmeta_ssr_type="chart" class="zr0-cls-7"></path> +<path d="M317.2 60A120 120 0 0 1 320 60L320 180Z" fill="#f85552" stroke="var(--bg0)" stroke-linejoin="round" ecmeta_series_index="0" ecmeta_data_index="7" ecmeta_ssr_type="chart" class="zr0-cls-8"></path> +<text dominant-baseline="central" text-anchor="start" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(460.5907 240.6868)" fill="var(--fg)">Progressive Rock</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(172.5853 206.2935)" fill="var(--fg)">Alternative</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(177.5244 135.5094)" fill="var(--fg)">Post-Rock</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(185.3496 117.4094)" fill="var(--fg)">Post-Hardcore</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(196.7011 99.3094)" fill="var(--fg)">Post-Metal</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(212.8733 81.2094)" fill="var(--fg)">Rock</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" transform="translate(237.2337 63.1094)" fill="var(--fg)">Shoegaze</text> +<text dominant-baseline="central" text-anchor="end" style="font-size:16px;font-family:unifont;" xml:space="preserve" transform="translate(298.4082 45.0094)" fill="var(--fg)">Progressive Metal</text> +<style> +.zr0-cls-0:hover { +cursor:pointer; +} +.zr0-cls-1:hover { +cursor:pointer; +fill:rgba(253,138,140,1); +} +.zr0-cls-2:hover { +cursor:pointer; +fill:rgba(253,167,128,1); +} +.zr0-cls-3:hover { +cursor:pointer; +fill:rgba(240,206,139,1); +} +.zr0-cls-4:hover { +cursor:pointer; +fill:rgba(183,211,140,1); +} +.zr0-cls-5:hover { +cursor:pointer; +fill:rgba(144,211,160,1); +} +.zr0-cls-6:hover { +cursor:pointer; +fill:rgba(139,205,196,1); +} +.zr0-cls-7:hover { +cursor:pointer; +fill:rgba(235,168,200,1); +} +.zr0-cls-8:hover { +cursor:pointer; +fill:rgba(255,93,90,1); +} + + + +</style> +</svg> + <figcaption>Here's one I generated just now.</figcaption> + </figure> + </p> + <p> + And that's pretty much all there is to it! The finished thing can be found <a href="/rockstats" target="_blank">here</a>. + </p> +</section> +</article>
\ No newline at end of file diff --git a/content/blog/rockbox_stats/log-setting.bmp b/content/blog/rockbox_stats/log-setting.bmp Binary files differnew file mode 100644 index 0000000..29789fa --- /dev/null +++ b/content/blog/rockbox_stats/log-setting.bmp diff --git a/content/blog/rockbox_stats/playback-settings.bmp b/content/blog/rockbox_stats/playback-settings.bmp Binary files differnew file mode 100644 index 0000000..cee3cfb --- /dev/null +++ b/content/blog/rockbox_stats/playback-settings.bmp diff --git a/content/blog/rockbox_stats/player.bmp b/content/blog/rockbox_stats/player.bmp Binary files differnew file mode 100644 index 0000000..452a057 --- /dev/null +++ b/content/blog/rockbox_stats/player.bmp |
