<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://blog.cytrogen.icu/feed.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Cytrogen&#39;s Blog (English)</title>
  
  <subtitle>万圣节恶魔的领地</subtitle>
  <link href="https://blog.cytrogen.icu/atom-en.xml" rel="self"/>
  
  <link href="https://blog.cytrogen.icu/"/>
  <updated>2026-08-06T19:31:48.297Z</updated>
  <id>https://blog.cytrogen.icu/</id>
  
  <author>
    <name>Cytrogen</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Hate gifts</title>
    <link href="https://blog.cytrogen.icu/posts/b3bc.html"/>
    <id>https://blog.cytrogen.icu/posts/b3bc.html</id>
    <published>2026-08-06T19:30:54.000Z</published>
    <updated>2026-08-06T19:31:48.297Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>There is a gray box under my desk, dedicated to items that do not belong on top of it, such as batteries and toys. A portion of the gifts I received from others lives there as well.</p><span id="more"></span><p>Yesterday, because the box was overflowing and finding a bettery had become a struggle, I decided to clear it out. The biggest space-hogs were my toys -- things like Rubik's cubes -- alongside merchandise gifted by friends. The toys I bought for myself never bothered me because I purchased them with the intent to use them. The gifts from friends, however, took up physical space and felt honestly annoying.</p><p>While cleaning the bottom of the box, I found some stray building block pieces. Years ago, my roommate bought me a cheap programmer-themed building block set. It lacked proper instructions, the plastic quality was awful, and the finished product looked dreadful. When I had placed it on my desk, our cat Pico knocked it off onto the floor every night, causing numerous pieces to vanish over time.</p><p><em>Might as well throw it out,</em> I thought, tossing it into the trash.</p><p>My roommate happened to see this and became deeply upset.</p><p>Even though I apologized afterward and got the blocks back from the garbage can, her anger persisted. This incident highlighted a basic gap in our mutual understanding: to me, that set was useless junk, something I would never touch again for the rest of my life; to her, throwing away a gift is as same as disrespect herself. If she were to discard something I had given her, I would certainly feel hurt, so why should she not be allowed to feel angry?</p><p>Yet the logic still falls apart for me. She keeps my gifts because they hold genuine value and are worth preserving. The blocks she gave me, by contrast, hold no value at all; they are no different from the unwanted items other friends force upon me. She easily recognizes the gifts from those other friends as garbage, so why can she not acknowledge that her own gift belongs in the same category? I never asked for that building block set in the first place.</p><p><a href="https://blog.cytrogen.icu/posts/893f.html#:~:text=%E5%85%B3%E4%BA%8E%E6%84%9F%E6%83%85,%E4%BB%80%E4%B9%88,-%E3%80%82">Last month</a>, I learned about the concept of the <em>Five Love Languages</em> and ranked <em>Receiving Gifts</em> at the very bottom. The reason is simple: I hate gifts.</p><p>I do not have a habit of giving gifts, nor do I enjoy receiving them. For a friend's birthday this year, I let them choose their own present on the day itself. Giving someone a gift should mean giving them something they actually want. Vice versa, the items others give me are always things <em>they</em> assume I will like. In reality, I neither want nor need them; they simply sit in a box, gathering dust.</p><p>I also despise receiving bouquets as gifts. They are living plants. Upon receiving one, I feel an obligation to take responsibility and keep it alive. That forces me to spend my own money on pots and soil, or at least prepare water... Yet cut flowers rarely survive long anyway, not to mention that I have a cat at home prone to chewing on plants.</p><p>Buying pets as gifts without prior permission is the absolute worst. The giver gets all the satisfaction of buying, completely indifferent to the huge energy required from me to care for the animal. In that sense, it bears a striking similarity to pregnancy and childbirth.</p><p>During my childhood, adults frequently forced items onto me -- clothes, jewelry, and things I never requested. Accepting them felt redundant, yet refusing them seemed disrespectful. I invariably ended up trapped with a pile of trash, unable to throw it away or keep it comfortably. What a predicament! Meanwhile, the things I actually desired were almost never given to me. Over time, this birthed my bitterness toward gifts. If you force garbage onto me and still expect a gift in return, there is not a chance.</p></body></html>]]></content>
    
    
    <summary type="html">&lt;p&gt;There is a gray box under my desk, dedicated to items that do not belong on top of it, such as batteries and toys. A portion of the gifts I received from others lives there as well.&lt;/p&gt;</summary>
    
    
    
    <category term="Personal Musings" scheme="https://blog.cytrogen.icu/categories/Personal-Musings/"/>
    
    
  </entry>
  
  <entry>
    <title>DeGoogling and DeMicrosoft is a new start for me</title>
    <link href="https://blog.cytrogen.icu/posts/ccb5.html"/>
    <id>https://blog.cytrogen.icu/posts/ccb5.html</id>
    <published>2026-03-18T04:00:00.000Z</published>
    <updated>2026-03-18T23:19:55.928Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>I haven't been writing blogs in English for a while, a long long long while. If you switched the language of this website from English to Simplified Chinese, you would find out I wrote way more articles there, including a <a href="/categories/%E6%83%B3%E6%B3%95%E8%BF%AD%E4%BB%A3/">monthly series</a>. I initially started to write blogs for practicing writing and thinking skills, and it went very well. I met more blogger friends, and was able to talk about my online experiences in my real life; it's like double winning.</p><span id="more"></span><p>Lately, one topic we have been discussing a lot is data sovereignty. Free and comfortable services from massive corporations are not always gifts from angels. The monopoly of today's internet is not just in the cloud or browsers; it invades our local workspaces and digital identities as well.</p><p>Google and Microsoftare prime examples. They love ads. I mean, Google started as an advertising company and remains one at its core. Microsoft, on the other hand, is the one of the few lucky ones who currently making massive profits from AI, heavily driven by ads. The situation goes beyond the browser. Windows 11 has become the worst operating system in my tier ranking. It's bloated with ads and telemetry. With Copilot, ew I hate this name now, being forced into every corner of the operating system, it feels less like a helpful assistant and more like a surveillance camera constantly monitoring my local workspace.</p><p>People often underestimate how powerful this advertising and tracking model is. Corporations earn trillions of dollars by showing ads on their customers' devices. We think we are getting free services, but we are actually guinea pigs in a wheel, running and running to generate profits for them.</p><p>To understand how I decided to fight back against this, let we wind back a little. Everything started when I began using Mastodon.</p><p>Mastodon is a Twitter-like software from the Fediverse. If you are unfamiliar, you can think of the Fediverse as the United States of America: a collection of federated states. New York can have different state laws than California, but they all follow federal laws. New Yorkers can also travel to California without police checking their passports. Substituted into software concepts: different servers (or instances) have their own rules, but they all communicate using the same <em>federal law</em> -- the ActivityPub protocol.</p><p>The problem the Fediverse tries to solve is centralization. On modern social platforms, you must register separate accounts for Twitter, Facebook, or Reddit, and your data is locked within their walls. When Elon Musk took over Twitter (now, unfortunately, X), it highlighted the fatal flaw of centralized platforms: the company holds all your data, can arbitrarily ban you, manipulate the algorithm to make you watch more ads, or completely change the platform's nature. You are just renting space in their realm. Mastodon, by contrast, has no algorithms. You follow who you want, read your chronological feed, and if your instance administrator goes rogue, you can pack up your data and migrate to another instance.</p><p>Using Fediverse software opened my eyes, but it was only the first step. By listening to Cory Doctorow's speech <em>The Post-American Internet</em> and learning about his concept of <em>Enshittification</em>, I realized the inevitable fate of centralized platforms: they lure you in with free services, lock you in, and then degrade the experience to extract maximum value. Furthermore, Richard Stallman and the GNU project taught me that user freedom is entirely dependent on software freedom.</p><p>This philosophical shift triggered my massive migration. To reclaim my local control, I abandoned Windows entirely and switched to Arch Linux (EndeavourOS). I started replacing proprietary software with free and open-source (FOSS) alternatives. I learned to use GNU Emacs; instead of Gmail and Outlook, I use Fairmail and self-hosted emails; instead of YouTube Music, I use Tempo and Feishin with local files and radios; instead of YouTube and Bilibili, I use LibreTube and PiliPlus clients with zero data tracking; instead of Google Maps, I use Organic Maps; instead of Google Calendar, I use Etar -- surprisingly, they all went very well!</p><p>The most significant change in my daily routine, however, was subscribing to Kagi Search. Kagi fights back against Google's search monopoly. It aggregates results but heavily filters out AI slop, ad-heavy sites, and data-tracking JavaScript.</p><p>The common counter-argument I hear is: <em>Why buy a service when you can get Google for free?</em> I used to think the same way. Unfortunately, data-driven corporations don't see users of free services as customers (and even sadly, users of paid services are sometimes not seen as customers as well); they see them as organisms generating valuable data. You pay with your privacy. Paying a small business like Kagi to treat me like a human being who deserves privacy is not a big deal. It is an investment in a healthier internet.</p><p>Eventually, I realized that true data sovereignty requires hosting your own infrastructure. My personal blog used to be hosted on Netlify and protected by Cloudflare -- both centralized platforms. To fix this, I rented a VPS from IONOS. I will be honest: a cheap VPS located in the United States is not the ultimate solution for privacy, as European providers offer much stronger legal data protections (I'm fully aware of this, I even wrote an <a href="/posts/2590.html">article</a> about this in Chinese). However, working within a tight budget, this US-based IONOS server was my practical starting point.</p><p>On this Linux server, I host my personal websites, a Gemini capsule version of my blog (see <code>gemini://cytrogen.icu</code> using a Gemini client), and services like FreshRSS. The most critical piece of infrastructure I deployed is a self-hosted email service using Stalwart.</p><p>Replacing Google with a self-hosted email server gave me ultimate control. To prevent spam and track data leaks, I use an email alias system. For example, I use the alias <code>amazon@mydomain.com</code> strictly for my Amazon account. This alias automatically forwards to my main inbox. If <code>amazon@</code> ever starts receiving unrelated promotions or junk mail, I will know exactly which company sold or leaked my information.</p><p>No, I am not saying Amazon sold my personal information for real. At least I didn't investigate that yet.</p><p>It is sometimes sad to see friends looking down on these efforts, mocking me with <em>just use Google</em>. They forget that the internet originally started as a free (in the gratis and libre sense) and open commons. It was not supposed to be a series of walled gardens owned by tech giants. Although the digital world is currently occupied by these monopolies, reclaiming our operating systems, paying for ethical search engines, and hosting our own data is how we fight the momentum back. DeGoogling and DeMicrosoft is not just a technical experiment for me; it is a new start!</p></body></html>]]></content>
    
    
    <summary type="html">&lt;p&gt;I haven&#39;t been writing blogs in English for a while, a long long long while. If you switched the language of this website from English to Simplified Chinese, you would find out I wrote way more articles there, including a &lt;a href=&quot;/categories/%E6%83%B3%E6%B3%95%E8%BF%AD%E4%BB%A3/&quot;&gt;monthly series&lt;/a&gt;. I initially started to write blogs for practicing writing and thinking skills, and it went very well. I met more blogger friends, and was able to talk about my online experiences in my real life; it&#39;s like double winning.&lt;/p&gt;</summary>
    
    
    
    <category term="Personal Musings" scheme="https://blog.cytrogen.icu/categories/Personal-Musings/"/>
    
    
    <category term="Data sovereignty" scheme="https://blog.cytrogen.icu/tags/Data-sovereignty/"/>
    
    <category term="Kagi" scheme="https://blog.cytrogen.icu/tags/Kagi/"/>
    
  </entry>
  
  <entry>
    <title>I had (maybe) the worst computer class in my life</title>
    <link href="https://blog.cytrogen.icu/posts/e955.html"/>
    <id>https://blog.cytrogen.icu/posts/e955.html</id>
    <published>2025-12-19T02:57:50.000Z</published>
    <updated>2026-02-09T17:16:54.713Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>This article was written under my anger, so I apologize for any illogical and immature thoughts. Actually, forget that, because I value true feelings.</p><p>It all started this August... I'm starting my Junior year at this college. Last year, I failed my computer class because I skipped classes and exams -- I deserved it for sure. So, I decided to retake this CIS 2300 class this semester. Why? Because goddamn it is a prerequisite of literally every major class I have.</p><span id="more"></span> <p>This CIS 2300 is titled "Programming and Computational Thinking." Sounds super fundamental right? It is actually just a Intro to Python class worth 3 credits, which needs $1,860 out of your pocket because each credit costs $620, and worse than the free Intro to Python videos on YouTube.</p><p>By the way, this is an online course, so I don't have to move myself to that tiny building, with some people that I hate, which is nice!</p><p>Okay back to the points. The first few weeks seemed okay. The professor was a bit strict, but I thought it's okay, so I just let them deduct my points for not having the perfect and most-efficient codes.</p><p>Everything went wrong in November.</p><p>We had just finished learning dictionary and tuple. If you have watched any Intro to Python tutorials, or other object-oriented languages tutorials, you had expect "ah ha here we go the class/object."</p><p>Nope. This course never even mentioned them, not a single concept. Instead, we were suddenly expected to use BeautifulSoup to scrape web pages.</p><p>Why? Just why?</p><p>Maybe many of you know how to scrape web contents. Well yes one of the tasks that Python masters in is web scraping. I was a web scraper myself before, so I presumed I would do super well at this.</p><p>Haha, nope again. I got a big fat F at my in-class group assignment.</p><p>I wrote <a href="/posts/d38e.html">an article</a> in Chinese explaining why this is happening. If you hate reading the prerequisite to this article (the one that you are reading right now), or don't want to translate that article, I can give you a TL;DR: The professor didn't teach the students much about web scraping, and didn't expect the students to know HTML and other things, so they want us to use AI in this assignment while disclosing any AI usage, and show step-by-step work.</p><p>Step-by-step work meant leaving every iteration of your code inside the <code>.ipynb</code> file, which feels weird to me.</p><p>I, and my team members, got a F because "we did not disclose AI usage and did not show step-by-step work."</p><p>I emailed the professor, telling them: okay first of all I did not use AI for something I already knew, second of all I showed everything I could show. I can literally tell what to extract when reading the source code of the web page -- what are you even expecting?</p><p>Of course I did not say all these in this rude tone.</p><p>The professor responded one week after, saying they had consulted the course coordinator and the department chair. They all agreed that we had not provided enough step-by-step explanation, and decided not to go further with AI disclosure issue.</p><p>Okay, so I was punished for actually knowing the material, I believe?</p><p>Fortunately, they were happy to give us partial credits, if this assignment is big enough to change our final grade from one letter to another, like B to A.</p><p>But this assignment weighs only 2% of the grade.</p><p>As it turns out, the entire department seems fine with professors assigning work that was never taught. Great. What's the point of a class if we are just told to "go ask AI?"</p><p>The following week, we were suddenly thrust into pandas. This is out of no where as well. How can students understand data structures without a solid foundation, especially when you are forced to use AI for topics not covered in class?</p><p>They don't understand these things. <em>What's the point of having API? Why we have to use pagination when requesting data? How is requesting data even work? What do you mean by getting data from a server? What's the point of doing all these? <strong>Programming is so hard!</strong></em></p><p>Because I learned about computer and programming before, so I can tell how ridiculous this is.</p><p>Programming should not be taught like this. Students should be learning what a computer is, how a computer works, how a computer communicates with us, how we talk to computer using languages that look more like natural languages. Not throwing a bunch of knowledge from no where without context to their face.</p><p>In addition, isn't this class titled "Programming and Computational Thinking?" Programming and Computational Thinking where? Isn't programming and computational thinking something that teaches students to understand logics, data structures, and how to build systems? This course so far, only teaches students how to use libraries, like random low-budget bootcamps.</p><p>Well, a bootcamp does not cost $1,860 for 10 weeks of "just ask AI."</p><p>Today I finished the final exam of this course. I was exhausted from many things from my life, and found half of the exam is about writing code on paper.</p><p>Using a pen.</p><p>Seems like the department can't decide if we are supposed to be AI prompters or 1970s scribes, huh?</p></body></html>]]></content>
    
    
    <summary type="html">&lt;p&gt;This article was written under my anger, so I apologize for any illogical and immature thoughts. Actually, forget that, because I value true feelings.&lt;/p&gt;
&lt;p&gt;It all started this August... I&#39;m starting my Junior year at this college. Last year, I failed my computer class because I skipped classes and exams -- I deserved it for sure. So, I decided to retake this CIS 2300 class this semester. Why? Because goddamn it is a prerequisite of literally every major class I have.&lt;/p&gt;</summary>
    
    
    
    <category term="Personal Musings" scheme="https://blog.cytrogen.icu/categories/Personal-Musings/"/>
    
    
    <category term="College" scheme="https://blog.cytrogen.icu/tags/College/"/>
    
  </entry>
  
  <entry>
    <title>Raspberry Pi Pico Plays Music Via Active Buzzer</title>
    <link href="https://blog.cytrogen.icu/posts/9781.html"/>
    <id>https://blog.cytrogen.icu/posts/9781.html</id>
    <published>2025-07-25T00:02:18.000Z</published>
    <updated>2026-02-09T17:16:54.757Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>How to use MicroPython to play music via the active buzzer on a Raspberry Pi Pico.</p><span id="more"></span><h1 id="introduction"><a class="markdownIt-Anchor" href="#introduction"></a> Introduction</h1><p>I got my Raspberry Pi Pico earlier this year, but did not have the time to play with it, until recently I got this weird disease called senioritis.</p><p>When I bought this Raspberry Pi Pico, I purchased the pre-soldered version because I'm lazy, shown as this image below:</p><img src="/posts/9781/1.webp" alt="MAKEROBO PicoBlock expansion board, pre-soldered Raspberry Pi Pico Motherboard, breadboard, LED lights, arrow keys, active buzzer, sensors, etc." loading="lazy"><p>However, this article focuses on MicroPython, so it won't go into detail about the hardware. The same goes for basic topics like configuring the Raspberry Pi Pico or installing an IDE; you can search for that information online.</p><div class="danger"><ul><li>The IDE that this article is using is Thonny</li><li>All the music scores are from <a href="https://onlinesequencer.net/">Online Sequencer</a></li></ul></div><h1 id="preparation"><a class="markdownIt-Anchor" href="#preparation"></a> Preparation</h1><h2 id="importing-libraries"><a class="markdownIt-Anchor" href="#importing-libraries"></a> Importing Libraries</h2><pre><code class="language-python">import timefrom machine import PWM, Pin</code></pre><ul><li>The <code>time</code> library needs no introduction; it's used for creating delays</li><li><code>machine</code> library contains methods related to the hardware on a specific board. For example, <code>PWM</code> and <code>Pin</code> that we just imported. For details you can check <a href="https://docs.micropython.org/en/latest/library/machine.html">MicroPython's official documentation</a></li></ul><h2 id="notes-and-frequencies"><a class="markdownIt-Anchor" href="#notes-and-frequencies"></a> Notes and Frequencies</h2><pre><code class="language-python">tones = {    'C0': 16, 'C#0': 17, 'D0': 18, 'D#0': 19, 'E0': 21, 'F0': 22,    'F#0': 23, 'G0': 24, 'G#0': 26, 'A0': 28, 'A#0': 29, 'B0': 31,    'C1': 33, 'C#1': 35, 'D1': 37, 'D#1': 39, 'E1': 41, 'F1': 44,    'F#1': 46, 'G1': 49, 'G#1': 52, 'A1': 55, 'A#1': 58, 'B1': 62,    'C2': 65, 'C#2': 69, 'D2': 73, 'D#2': 78, 'E2': 82, 'F2': 87,    'F#2': 92, 'G2': 98, 'G#2': 104, 'A2': 110, 'A#2': 117, 'B2': 123,    'C3': 131, 'C#3': 139, 'D3': 147, 'D#3': 156, 'E3': 165, 'F3': 175,    'F#3': 185, 'G3': 196, 'G#3': 208, 'A3': 220, 'A#3': 233, 'B3': 247,    'C4': 262, 'C#4': 277, 'D4': 294, 'D#4': 311, 'E4': 330, 'F4': 349,    'F#4': 370, 'G4': 392, 'G#4': 415, 'A4': 440, 'A#4': 466, 'B4': 494,    'C5': 523, 'C#5': 554, 'D5': 587, 'D#5': 622, 'E5': 659, 'F5': 698,    'F#5': 740, 'G5': 784, 'G#5': 831, 'A5': 880, 'A#5': 932, 'B5': 988,    'C6': 1047, 'C#6': 1109, 'D6': 1175, 'D#6': 1245, 'E6': 1319, 'F6': 1397,    'F#6': 1480, 'G6': 1568, 'G#6': 1661, 'A6': 1760, 'A#6': 1865, 'B6': 1976,    'C7': 2093, 'C#7': 2217, 'D7': 2349, 'D#7': 2489, 'E7': 2637, 'F7': 2794,    'F#7': 2960, 'G7': 3136, 'G#7': 3322, 'A7': 3520, 'A#7': 3729, 'B7': 3951,    'C8': 4186, 'C#8': 4435, 'D8': 4699, 'D#8': 4978, 'E8': 5274, 'F8': 5588,    'F#8': 5920, 'G8': 6272, 'G#8': 6645, 'A8': 7040, 'A#8': 7459, 'B8': 7902,    'C9': 8372, 'C#9': 8870, 'D9': 9397, 'D#9': 9956, 'E9': 10548, 'F9': 11175,    'F#9': 11840, 'G9': 12544, 'G#9': 13290, 'A9': 14080, 'A#9': 14917, 'B9': 15804}</code></pre><p>This dictionary contains all the notes' frequencies, from C0 to B9, in the unit of Hz. These frequencies will be used to set up the frequency of PWM.</p><h4 id="aside"><a class="markdownIt-Anchor" href="#aside"></a> Aside</h4><p>If you don't know notes, but still wish to play using the actual music score in reality, what should you do? (Of course, if you do know, you can jump to <a href="#setting-up-the-buzzer">this section</a>)</p><p>Let's see what a simple music score looks like. Welcome to our guest -- <em>Twinkle Twinkle Little Star</em>:</p><img src="/posts/9781/2.webp" alt="Music Score of Twinkle Twinkle Little Star" loading="lazy"><p>First, in a music score every line has multiple measures, separated by the vertical lines.</p><p>At the beginning of the staff, there's a symbol representing the time signature. For instance, this music score has the time signature of <strong>4/4</strong>, which means every measure has 4 beats, every beat is one quarter note.</p><p>What is a quarter note? We have to know the duration of notes.</p><p>Notes' durations are divided into whole note, half note, quarter note, eighth note, sixteenth note, thirty-second note, etc. To make them easier to tell apart, take a look at this image:</p><img src="/posts/9781/3.webp" alt="A chart showing different musical notes and their durations: whole note (4 beats), half note (2 beats), quarter note (1 beat), eighth note (1/2 beat), and so on" loading="lazy"><p>Now back to the music score of <em>Twinkle Twinkle Little Star</em>, we can tell the first measure has exactly 4 quarter notes, and the second measure has 2 quarter notes and 1 half note.</p><img src="/posts/9781/4.webp" alt="The first and second measure of the simplified notation of Twinkle Twinkle Little Star" loading="lazy"><p>In the actual play, what is the difference between these two measures?</p><p>4 quarter notes be like: <em>Ah</em> <em>Ah</em> <em>Ah</em> <em>Ah</em>;<br>2 quarter notes &amp; 1 half note be like: <em>Ah</em> <em>Ah</em> <em>Ah--</em>.</p><p>That being said, in this score 1 half note is the same as combining two quarter notes, making one long sound.</p><p>Now that we have covered duration, let's look at the position of the notes on the musical staff. You might have noticed, some notes are above the line, some are below the lines, some are in the middle, some are in between... why?</p><p>This brings us to the pitch of notes. To be clear, <em>Twinkle Twinkle Little Star</em>'s music score has marked <code>1=C</code>, which is C major. C major is composed by C, D, E, F, G, A, B these 7 notes, also the only major that has neither sharp nor flat.</p><blockquote><p>If you ever know how a piano looks like, it has those white and black keys. White keys are typically divided into C, D, E, F, G, A, B 7 notes; black keys in general are divided into C#, D#, F#, G#, A# 5 notes.</p><p>The # symbol represents a sharp, which raises a note's pitch by a half step. Examples, C# is a half step above C, also the note that is between C and D.<br>There is also a b symbol, representing a flat. Opposite to the # symbol, the b symbol lowers a note's pitch by a half step. Db is the half step below D, also the note between C and D, also C# itself.</p></blockquote><p>We have discussed so many about C, D, E, F, G, A, B, but in music score of <em>Twinkle Twinkle Little Star</em> above, we only saw numbers. This is because in simplified notation, Arabic numerals are used to represent the pitch of notes. 1 is the same as C, 2 is the same as D, and so on.</p><blockquote><p>While in reality, the most commonly used ones to represent the pitch of notes are solfège, which are do, re, mi, fa, so, la, si. do means C, re means D, and so on.</p></blockquote><img src="/posts/9781/6.webp" alt="Chart comparing Arabic numerals, solfège names, and Chinese characters for musical notes" loading="lazy"><p>We now know how to distinguish the pitch of notes using Arabic numbers, but this method only works in simplified notation. We still have to learn how to read the musical staff. C major's scale looks like this:</p><img src="/posts/9781/7.webp" alt="The C Major Scale" loading="lazy"><p>Let's check the first measure of <em>Twinkle Twinkle Little Star</em>. You don't have to read the Arabic numbers, by referring to the image above, you can tell it's CCGG:</p><img src="/posts/9781/5.webp" alt="The first measure of Twinkle Twinkle Little Star (notes 1155)" loading="lazy"><p>But in our previous <a href="#notes-and-frequencies">note-to-frequency-dictionary</a>, C, D, E, F, G, A, B are followed by other numbers. For instance, C4, C5, etc. these numbers are representing the frequency of the notes, which is also the pitch of the sound. C4 is normally known as the Middle C; C5 is the C that's one octave higher.</p><blockquote><p>An octave represents a doubling of frequency. For example, the frequency of C5 (523.25 Hz) is double the frequency of C4 (261.63 Hz).</p><p>However, we don't need such precise decimals; we can just round them to the nearest integer.</p></blockquote><p>For convenience, we will use the Middle C (C4) as standard. This way, we can combine the music score and notes of <em>Twinkle Twinkle Little Star</em>:</p><pre><code class="language-plaintext">C4 C4 G4 G4 A4 A4 G4- F4 F4 E4 E4 D4 D4 C4- G4 G4 F4 F4 E4 E4 D4- G4 G4 F4 F4 E4 E4 D4- C4 C4 G4 G4 A4 A4 G4- F4 F4 E4 E4 D4 D4 C4-</code></pre><div class="danger">This is not the final version of music scores we will be using, it's for easier understanding.</div><h2 id="setting-up-the-buzzer"><a class="markdownIt-Anchor" href="#setting-up-the-buzzer"></a> Setting Up the Buzzer</h2><p>Let's set up the GPIO pin of the buzzer. I typically use GP15 (as shown in <a href="#introduction">the previous picture</a>, the GP15 pin is already connected to the buzzer). Of course, you can change the pin to suit your own needs:</p><pre><code class="language-python">buzzer = PWM(Pin(15))buzzer.freq(50000)buzzer.duty_u16(int(65536 * 0.2))</code></pre><p>At first, we create a <code>PWM</code> object, set the PWM frequency to 50,000 Hz (meaning no sound at all), and set the duty cycle to 20%.</p><h1 id="writing-the-code"><a class="markdownIt-Anchor" href="#writing-the-code"></a> Writing the Code</h1><h2 id="getting-the-music-score"><a class="markdownIt-Anchor" href="#getting-the-music-score"></a> Getting the Music Score</h2><p>I chose to use the <a href="https://onlinesequencer.net/25966">classic music</a> from Super Mario for demonstration. Online Sequencer's music score can be copied and pasted, like below:</p><pre><code class="language-plaintext">Online Sequencer:616413:0 F#5 1 7;0 E6 1 7;2 F#5 1 7;2 E6 1 7;6 F#5 1 7;6 E6 1 7;10 F#5 1 7;10 C6 1 7;12 F#5 1 7;12 E6 1 7;16 G5 1 7;16 B5 1 7;16 G6 1 7;24 G5 1 7;32 E5 1 7;32 C6 1 7;38 C5 1 7;38 G5 1 7;44 G4 1 7;44 E5 1 7;50 C5 1 7;50 A5 1 7;54 D5 1 7;54 B5 1 7;58 C#5 1 7;58 A#5 1 7;60 C5 1 7;60 A5 1 7;64 C5 1 7;64 G5 1 7;66 G5 1 7;66 E6 1 7;69 B5 1 7;69 G6 1 7;72 C6 1 7;72 A6 1 7;76 A5 1 7;76 F6 1 7;78 B5 1 7;78 G6 1 7;82 A5 1 7;82 E6 1 7;86 E5 1 7;86 C6 1 7;88 F5 1 7;88 D6 1 7;90 D5 1 7;90 B5 1 7;96 E5 1 7;96 C6 1 7;102 C5 1 7;102 G5 1 7;108 G4 1 7;108 E5 1 7;114 C5 1 7;114 A5 1 7;118 D5 1 7;118 B5 1 7;122 C#5 1 7;122 A#5 1 7;124 C5 1 7;124 A5 1 7;128 C5 1 7;128 G5 1 7;130 G5 1 7;130 E6 1 7;133 B5 1 7;133 G6 1 7;136 C6 1 7;136 A6 1 7;140 A5 1 7;140 F6 1 7;142 B5 1 7;142 G6 1 7;146 A5 1 7;146 E6 1 7;150 E5 1 7;150 C6 1 7;152 F5 1 7;152 D6 1 7;154 D5 1 7;154 B5 1 7;164 E6 1 7;164 G6 1 7;166 D#6 1 7;166 F#6 1 7;168 D6 1 7;168 F6 1 7;170 B5 1 7;170 D#6 1 7;174 C6 1 7;174 E6 1 7;178 E5 1 7;178 G#5 1 7;180 F5 1 7;180 A5 1 7;182 G5 1 7;182 C6 1 7;186 C5 1 7;186 A5 1 7;188 E5 1 7;188 C6 1 7;190 F5 1 7;190 D6 1 7;196 E6 1 7;196 G6 1 7;198 D#6 1 7;198 F#6 1 7;200 D6 1 7;200 F6 1 7;202 B5 1 7;202 D#6 1 7;206 C6 1 7;206 E6 1 7;210 F6 1 7;210 G6 1 7;210 C7 1 7;214 F6 1 7;214 G6 1 7;214 C7 1 7;216 F6 1 7;216 G6 1 7;216 C7 1 7;228 E6 1 7;228 G6 1 7;230 D#6 1 7;230 F#6 1 7;232 D6 1 7;232 F6 1 7;234 B5 1 7;234 D#6 1 7;238 C6 1 7;238 E6 1 7;242 E5 1 7;242 G#5 1 7;244 F5 1 7;244 A5 1 7;246 G5 1 7;246 C6 1 7;250 C5 1 7;250 A5 1 7;252 E5 1 7;252 C6 1 7;254 F5 1 7;254 D6 1 7;260 G#5 1 7;260 D#6 1 7;266 F5 1 7;266 D6 1 7;272 C5 1 7;272 E5 1 7;272 C6 1 7;292 E6 1 7;292 G6 1 7;294 D#6 1 7;294 F#6 1 7;296 D6 1 7;296 F6 1 7;298 B5 1 7;298 D#6 1 7;302 C6 1 7;302 E6 1 7;306 E5 1 7;306 G#5 1 7;308 F5 1 7;308 A5 1 7;310 G5 1 7;310 C6 1 7;314 C5 1 7;314 A5 1 7;316 E5 1 7;316 C6 1 7;318 F5 1 7;318 D6 1 7;324 E6 1 7;324 G6 1 7;326 D#6 1 7;326 F#6 1 7;328 D6 1 7;328 F6 1 7;330 B5 1 7;330 D#6 1 7;334 C6 1 7;334 E6 1 7;338 F6 1 7;338 G6 1 7;338 C7 1 7;342 F6 1 7;342 G6 1 7;342 C7 1 7;344 F6 1 7;344 G6 1 7;344 C7 1 7;356 E6 1 7;356 G6 1 7;358 D#6 1 7;358 F#6 1 7;360 D6 1 7;360 F6 1 7;362 B5 1 7;362 D#6 1 7;366 C6 1 7;366 E6 1 7;370 E5 1 7;370 G#5 1 7;372 F5 1 7;372 A5 1 7;374 G5 1 7;374 C6 1 7;378 C5 1 7;378 A5 1 7;380 E5 1 7;380 C6 1 7;382 F5 1 7;382 D6 1 7;388 G#5 1 7;388 D#6 1 7;394 F5 1 7;394 D6 1 7;400 E5 1 7;400 C6 1 7;416 G#5 1 7;416 C6 1 7;418 G#5 1 7;418 C6 1 7;422 G#5 1 7;422 C6 1 7;426 G#5 1 7;426 C6 1 7;428 A#5 1 7;428 D6 1 7;432 G5 1 7;432 E6 1 7;434 E5 1 7;434 C6 1 7;438 E5 1 7;438 A5 1 7;440 C5 1 7;440 G5 1 7;448 G#5 1 7;448 C6 1 7;450 G#5 1 7;450 C6 1 7;454 G#5 1 7;454 C6 1 7;458 G#5 1 7;458 C6 1 7;460 A#5 1 7;460 D6 1 7;462 G5 1 7;462 E6 1 7;480 G#5 1 7;480 C6 1 7;482 G#5 1 7;482 C6 1 7;486 G#5 1 7;486 C6 1 7;490 G#5 1 7;490 C6 1 7;492 A#5 1 7;492 D6 1 7;496 G5 1 7;496 E6 1 7;498 E5 1 7;498 C6 1 7;502 E5 1 7;502 A5 1 7;504 C5 1 7;504 G5 1 7;512 F#5 1 7;512 E6 1 7;514 F#5 1 7;514 E6 1 7;518 F#5 1 7;518 E6 1 7;522 F#5 1 7;522 C6 1 7;524 F#5 1 7;524 E6 1 7;528 G5 1 7;528 B5 1 7;528 G6 1 7;536 G5 1 7;544 E5 1 7;544 C6 1 7;550 C5 1 7;550 G5 1 7;556 G4 1 7;556 E5 1 7;562 C5 1 7;562 A5 1 7;566 D5 1 7;566 B5 1 7;570 C#5 1 7;570 A#5 1 7;572 C5 1 7;572 A5 1 7;576 C5 1 7;576 G5 1 7;578 G5 1 7;578 E6 1 7;581 B5 1 7;581 G6 1 7;584 C6 1 7;584 A6 1 7;588 A5 1 7;588 F6 1 7;590 B5 1 7;590 G6 1 7;594 A5 1 7;594 E6 1 7;598 E5 1 7;598 C6 1 7;600 F5 1 7;600 D6 1 7;602 D5 1 7;602 B5 1 7;608 E5 1 7;608 C6 1 7;614 C5 1 7;614 G5 1 7;620 G4 1 7;620 E5 1 7;626 C5 1 7;626 A5 1 7;630 D5 1 7;630 B5 1 7;634 C#5 1 7;634 A#5 1 7;636 C5 1 7;636 A5 1 7;640 C5 1 7;640 G5 1 7;642 G5 1 7;642 E6 1 7;645 B5 1 7;645 G6 1 7;648 C6 1 7;648 A6 1 7;652 A5 1 7;652 F6 1 7;654 B5 1 7;654 G6 1 7;658 A5 1 7;658 E6 1 7;662 E5 1 7;662 C6 1 7;664 F5 1 7;664 D6 1 7;666 D5 1 7;666 B5 1 7;672 C6 1 7;672 E6 1 7;674 A5 1 7;674 C6 1 7;678 E5 1 7;678 G5 1 7;684 E5 1 7;684 G#5 1 7;688 F5 1 7;688 A5 1 7;690 C6 1 7;690 F6 1 7;694 C6 1 7;694 F6 1 7;696 F5 1 7;696 A5 1 7;704 G5 1 7;704 B5 1 7;706 F6 1 7;706 A6 1 7;709 F6 1 7;709 A6 1 7;712 F6 1 7;712 A6 1 7;714 E6 1 7;714 G6 1 7;717 D6 1 7;717 F6 1 7;720 C6 1 7;720 E6 1 7;722 A5 1 7;722 C6 1 7;726 F5 1 7;726 A5 1 7;728 E5 1 7;728 G5 1 7;736 C6 1 7;736 E6 1 7;738 A5 1 7;738 C6 1 7;742 E5 1 7;742 G5 1 7;748 E5 1 7;748 G#5 1 7;752 F5 1 7;752 A5 1 7;754 C6 1 7;754 F6 1 7;758 C6 1 7;758 F6 1 7;760 F5 1 7;760 A5 1 7;768 G5 1 7;768 B5 1 7;770 D6 1 7;770 F6 1 7;774 D6 1 7;774 F6 1 7;776 D6 1 7;776 F6 1 7;778 C6 1 7;778 E6 1 7;781 B5 1 7;781 D6 1 7;784 G5 1 7;784 C6 1 7;786 E5 1 7;790 E5 1 7;792 C5 1 7;800 C6 1 7;800 E6 1 7;802 A5 1 7;802 C6 1 7;806 E5 1 7;806 G5 1 7;812 E5 1 7;812 G#5 1 7;816 F5 1 7;816 A5 1 7;818 C6 1 7;818 F6 1 7;822 C6 1 7;822 F6 1 7;824 F5 1 7;824 A5 1 7;832 G5 1 7;832 B5 1 7;834 F6 1 7;834 A6 1 7;837 F6 1 7;837 A6 1 7;840 F6 1 7;840 A6 1 7;842 E6 1 7;842 G6 1 7;845 D6 1 7;845 F6 1 7;848 C6 1 7;848 E6 1 7;850 A5 1 7;850 C6 1 7;854 F5 1 7;854 A5 1 7;856 E5 1 7;856 G5 1 7;864 C6 1 7;864 E6 1 7;866 A5 1 7;866 C6 1 7;870 E5 1 7;870 G5 1 7;876 E5 1 7;876 G#5 1 7;880 F5 1 7;880 A5 1 7;882 C6 1 7;882 F6 1 7;886 C6 1 7;886 F6 1 7;888 F5 1 7;888 A5 1 7;896 G5 1 7;896 B5 1 7;898 D6 1 7;898 F6 1 7;902 D6 1 7;902 F6 1 7;904 D6 1 7;904 F6 1 7;906 C6 1 7;906 E6 1 7;909 B5 1 7;909 D6 1 7;912 G5 1 7;912 C6 1 7;914 E5 1 7;918 E5 1 7;920 C5 1 7;928 G#5 1 7;928 C6 1 7;930 G#5 1 7;930 C6 1 7;934 G#5 1 7;934 C6 1 7;938 G#5 1 7;938 C6 1 7;940 A#5 1 7;940 D6 1 7;944 G5 1 7;944 E6 1 7;946 E5 1 7;946 C6 1 7;950 E5 1 7;950 A5 1 7;952 C5 1 7;952 G5 1 7;960 G#5 1 7;960 C6 1 7;962 G#5 1 7;962 C6 1 7;966 G#5 1 7;966 C6 1 7;970 G#5 1 7;970 C6 1 7;972 A#5 1 7;972 D6 1 7;974 G5 1 7;974 E6 1 7;992 G#5 1 7;992 C6 1 7;994 G#5 1 7;994 C6 1 7;998 G#5 1 7;998 C6 1 7;1002 G#5 1 7;1002 C6 1 7;1004 A#5 1 7;1004 D6 1 7;1008 G5 1 7;1008 E6 1 7;1010 E5 1 7;1010 C6 1 7;1014 E5 1 7;1014 A5 1 7;1016 C5 1 7;1016 G5 1 7;1024 F#5 1 7;1024 E6 1 7;1026 F#5 1 7;1026 E6 1 7;1030 F#5 1 7;1030 E6 1 7;1034 F#5 1 7;1034 C6 1 7;1036 F#5 1 7;1036 E6 1 7;1040 G5 1 7;1040 B5 1 7;1040 G6 1 7;1048 G5 1 7;1056 C6 1 7;1056 E6 1 7;1058 A5 1 7;1058 C6 1 7;1062 E5 1 7;1062 G5 1 7;1068 E5 1 7;1068 G#5 1 7;1072 F5 1 7;1072 A5 1 7;1074 C6 1 7;1074 F6 1 7;1078 C6 1 7;1078 F6 1 7;1080 F5 1 7;1080 A5 1 7;1088 G5 1 7;1088 B5 1 7;1090 F6 1 7;1090 A6 1 7;1093 F6 1 7;1093 A6 1 7;1096 F6 1 7;1096 A6 1 7;1098 E6 1 7;1098 G6 1 7;1101 D6 1 7;1101 F6 1 7;1104 C6 1 7;1104 E6 1 7;1106 A5 1 7;1106 C6 1 7;1110 F5 1 7;1110 A5 1 7;1112 E5 1 7;1112 G5 1 7;1120 C6 1 7;1120 E6 1 7;1122 A5 1 7;1122 C6 1 7;1126 E5 1 7;1126 G5 1 7;1132 E5 1 7;1132 G#5 1 7;1136 F5 1 7;1136 A5 1 7;1138 C6 1 7;1138 F6 1 7;1142 C6 1 7;1142 F6 1 7;1144 F5 1 7;1144 A5 1 7;1152 G5 1 7;1152 B5 1 7;1154 D6 1 7;1154 F6 1 7;1158 D6 1 7;1158 F6 1 7;1160 D6 1 7;1160 F6 1 7;1162 C6 1 7;1162 E6 1 7;1165 B5 1 7;1165 D6 1 7;1168 G5 1 7;1168 C6 1 7;1170 E5 1 7;1174 E5 1 7;1176 C5 1 7;0 D4 1 7;2 D4 1 7;6 D4 1 7;10 D4 1 7;12 D4 1 7;24 G4 1 7;32 G4 1 7;38 E4 1 7;44 C4 1 7;50 F4 1 7;54 G4 1 7;58 F#4 1 7;60 F4 1 7;64 E4 1 7;66 C5 1 7;69 E5 1 7;72 F5 1 7;76 D5 1 7;78 E5 1 7;82 C5 1 7;86 A4 1 7;88 B4 1 7;90 G4 1 7;96 G4 1 7;102 E4 1 7;108 C4 1 7;114 F4 1 7;118 G4 1 7;122 F#4 1 7;124 F4 1 7;128 E4 1 7;130 C5 1 7;133 E5 1 7;136 F5 1 7;140 D5 1 7;142 E5 1 7;146 C5 1 7;150 A4 1 7;152 B4 1 7;154 G4 1 7;160 C4 1 7;166 G4 1 7;172 C5 1 7;176 F4 1 7;182 C5 1 7;184 C5 1 7;188 F4 1 7;192 C4 1 7;198 E4 1 7;204 G4 1 7;206 C5 1 7;220 G4 1 7;224 C4 1 7;230 G4 1 7;236 C5 1 7;240 F4 1 7;246 C5 1 7;248 C5 1 7;252 F4 1 7;256 C4 1 7;260 G#4 1 7;266 A#4 1 7;278 G4 1 7;280 G4 1 7;284 C4 1 7;288 C4 1 7;294 G4 1 7;300 C5 1 7;304 F4 1 7;310 C5 1 7;312 C5 1 7;316 F4 1 7;320 C4 1 7;326 E4 1 7;332 G4 1 7;334 C5 1 7;348 G4 1 7;352 C4 1 7;358 G4 1 7;364 C5 1 7;368 F4 1 7;374 C5 1 7;376 C5 1 7;380 F4 1 7;384 C4 1 7;388 G#4 1 7;394 A#4 1 7;400 C5 1 7;406 G4 1 7;408 G4 1 7;412 C4 1 7;416 G#3 1 7;422 D#4 1 7;428 G#4 1 7;432 G4 1 7;438 C4 1 7;444 G3 1 7;448 G#3 1 7;454 D#4 1 7;460 G#4 1 7;464 G4 1 7;470 C4 1 7;476 G3 1 7;480 G#3 1 7;486 D#4 1 7;492 G#4 1 7;496 G4 1 7;502 C4 1 7;508 G3 1 7;512 D4 1 7;514 D4 1 7;518 D4 1 7;522 D4 1 7;524 D4 1 7;536 G4 1 7;544 G4 1 7;550 E4 1 7;556 C4 1 7;562 F4 1 7;566 G4 1 7;570 F#4 1 7;572 F4 1 7;576 E4 1 7;578 C5 1 7;581 E5 1 7;584 F5 1 7;588 D5 1 7;590 E5 1 7;594 C5 1 7;598 A4 1 7;600 B4 1 7;602 G4 1 7;608 G4 1 7;614 E4 1 7;620 C4 1 7;626 F4 1 7;630 G4 1 7;634 F#4 1 7;636 F4 1 7;640 E4 1 7;642 C5 1 7;645 E5 1 7;648 F5 1 7;652 D5 1 7;654 E5 1 7;658 C5 1 7;662 A4 1 7;664 B4 1 7;666 G4 1 7;672 C4 1 7;678 F#4 1 7;680 G4 1 7;684 C5 1 7;688 F4 1 7;692 F4 1 7;696 C5 1 7;698 C5 1 7;700 F4 1 7;704 D4 1 7;710 F4 1 7;712 G4 1 7;716 B4 1 7;720 G4 1 7;724 G4 1 7;728 C5 1 7;730 C5 1 7;732 G4 1 7;736 C4 1 7;742 F#4 1 7;744 G4 1 7;748 C5 1 7;752 F4 1 7;756 F4 1 7;760 C5 1 7;762 C5 1 7;764 F4 1 7;768 G4 1 7;774 G4 1 7;776 G4 1 7;778 A4 1 7;781 B4 1 7;784 C5 1 7;788 G4 1 7;792 C4 1 7;800 C4 1 7;806 F#4 1 7;808 G4 1 7;812 C5 1 7;816 F4 1 7;820 F4 1 7;824 C5 1 7;826 C5 1 7;828 F4 1 7;832 D4 1 7;838 F4 1 7;840 G4 1 7;844 B4 1 7;848 G4 1 7;852 G4 1 7;856 C5 1 7;858 C5 1 7;860 G4 1 7;864 C4 1 7;870 F#4 1 7;872 G4 1 7;876 C5 1 7;880 F4 1 7;884 F4 1 7;888 C5 1 7;890 C5 1 7;892 F4 1 7;896 G4 1 7;902 G4 1 7;904 G4 1 7;906 A4 1 7;909 B4 1 7;912 C5 1 7;916 G4 1 7;920 C4 1 7;928 G#3 1 7;934 D#4 1 7;940 G#4 1 7;944 G4 1 7;950 C4 1 7;956 G3 1 7;960 G#3 1 7;966 D#4 1 7;972 G#4 1 7;976 G4 1 7;982 C4 1 7;988 G3 1 7;992 G#3 1 7;998 D#4 1 7;1004 G#4 1 7;1008 G4 1 7;1014 C4 1 7;1020 G3 1 7;1024 D4 1 7;1026 D4 1 7;1030 D4 1 7;1034 D4 1 7;1036 D4 1 7;1048 G4 1 7;1056 C4 1 7;1062 F#4 1 7;1064 G4 1 7;1068 C5 1 7;1072 F4 1 7;1076 F4 1 7;1080 C5 1 7;1082 C5 1 7;1084 F4 1 7;1088 D4 1 7;1094 F4 1 7;1096 G4 1 7;1100 B4 1 7;1104 G4 1 7;1108 G4 1 7;1112 C5 1 7;1114 C5 1 7;1116 G4 1 7;1120 C4 1 7;1126 F#4 1 7;1128 G4 1 7;1132 C5 1 7;1136 F4 1 7;1140 F4 1 7;1144 C5 1 7;1146 C5 1 7;1148 F4 1 7;1152 G4 1 7;1158 G4 1 7;1160 G4 1 7;1162 A4 1 7;1165 B4 1 7;1168 C5 1 7;1172 G4 1 7;1176 C4 1 7;:</code></pre><p>That's a lot~</p><p>Let's break down the format of Online Sequencer music score:</p><ul><li><code>Online Sequencer:616413:</code> is useless in our situation, therefore should be removed</li><li><code>0 F#5 1 7;</code> is the key. <code>0</code> means the time position, or the beat, of the note; <code>F#5</code> is the pitch of the note; <code>1</code> is the duration of the note; <code>7</code> is the type of instrument when playing. Every note in Online Sequencer's music score is in this format, separated by semicolon</li><li>Also, note that there's one <code>:</code>, it should be removed as well</li></ul><p>Then let's write the code to convert the Online Sequencer music score to Python dictionary.</p><p>We have to remove the beginning and the ending part from the Online Sequencer music score first, then split every note in the music score regarding to the <code>;</code>.</p><pre><code class="language-python">song = song_file.split(":")[2]notes_list = song.split(";")[:-1]</code></pre><p>Next, we extract the beat of the note, setting it to the key of the dictionary. Note's pitch, duration and instrument type will be set as the value:</p><pre><code class="language-python">notes_dict = {}for note in notes_list:    note_parts = note.split()    beat = float(note_parts[0])    # If the beat is added to the dictionary for the first time    if beat not in notes_dict:        # Add the note information to the dictionary        notes_dict[beat] = note_parts[1:]    # If the beat has already been added to the dictionary    else:        # If the current note's pitch is higher than the note's pitch in the dictionary        if tones[note_parts[1]] &gt; tones[notes_dict[beat][0]]:            # Replace the previous note by the current note            notes_dict[beat] = note_parts[1:]</code></pre><p>Since there is only one buzzer playing, it cannot play multiple notes at the same time. When there's more than one note at the same time position, we can only choose to play the note with the highest pitch.</p><p>At this point, we have a Python dictionary as our music score.</p><h2 id="playing-audio"><a class="markdownIt-Anchor" href="#playing-audio"></a> Playing Audio</h2><p>Making the buzzer produce sound is simple. The code is as follows:</p><pre><code class="language-python">buzzer.freq(tones[tone])time.sleep(duration)</code></pre><p><code>buzzer.freq()</code> will set the frequency of the buzzer, and <code>time.sleep()</code> pauses the program for a moment (in our case, it's used to hold the note for its duration).</p><h2 id="playing-the-score"><a class="markdownIt-Anchor" href="#playing-the-score"></a> Playing the Score</h2><p>Combining the previous two points together, we can play the notes from the dictionary one by one. But before that, we have to set the playing speed of the music score.</p><pre><code class="language-python">bpm = 199  # beat per minutetime_signature = [4, 4]beat_duration = (60 / bpm) / time_signature[0]  # duration per beat</code></pre><p>As we discussed in the <a href="#aside">Aside on music theory</a>, the time signature determines how many beats are in each measure and which kind of note gets one beat. From the <a href="https://onlinesequencer.net/25966">website</a> that we get Super Mario music, we can tell it have the bpm of 199 and time signature of 4/4.</p><p>With a quick calculation, we can determine that each beat's duration is <code>(60 / 199) / 4</code>.</p><p>Continuing with the code to play the score:</p><pre><code class="language-python"># Start playing from the 0th beatcurrent_beat = float(0)# Play until the time position of the last notewhile current_beat &lt;= max(notes_dict.keys()):       # Extract the duration (of beats) of the current note, multiply it with the duration of every beat, get the duration (of seconds) of the current note    note_duration = float(notes_dict[current_beat][1]) * beat_duration     # If the current time position has a note, then play it    if current_beat in notes_dict:        note_name = notes_dict[current_beat][0]                print(str(current_beat) + "th beat: playing note " + note_name + ", duration is " + str(note_duration) + " seconds")        buzzer.freq(tones[note_name])        time.sleep(note_duration)        current_beat += float(notes_dict[current_beat][1])        buzzer.freq(50000)        # If the current time position has no note, then pause    else:        print(str(current_beat) + "th beat: pause, duration is " + str(note_duration) + " seconds")        buzzer.freq(50000)        time.sleep(beat_duration)        current_beat += 1print("Finished playing")buzzer.freq(50000)</code></pre><p>And with that, our simple program for playing a music score is complete.</p><p>Final version of code can be found in <a href="https://github.com/Cytrogen/Music_Player_Pico">this repo</a>.</p></body></html>]]></content>
    
    
    <summary type="html">This article is a tutorial that uses MicroPython to play music via the active buzzer on a Raspberry Pi Pico. Using the Super Mario theme song as an example, this tutorial guides you on how to parse a musical score from Online Sequencer into a data structure the program can use, and how to use MicroPython&#39;s PWM feature to precisely control the buzzer&#39;s frequency and duration. The content covers not only the code implementation but also explains basic music theory concepts (like notes and beats) for readers who might be unfamiliar with the subject. This makes the tutorial perfect for Pico beginners and anyone interested in hardware-based music projects.</summary>
    
    
    
    <category term="Programming Notes" scheme="https://blog.cytrogen.icu/categories/Programming-Notes/"/>
    
    
    <category term="Python" scheme="https://blog.cytrogen.icu/tags/Python/"/>
    
    <category term="Raspberry Pi" scheme="https://blog.cytrogen.icu/tags/Raspberry-Pi/"/>
    
  </entry>
  
  <entry>
    <title>Markdown Rendering in React</title>
    <link href="https://blog.cytrogen.icu/posts/f27d.html"/>
    <id>https://blog.cytrogen.icu/posts/f27d.html</id>
    <published>2025-02-21T01:46:08.000Z</published>
    <updated>2026-02-09T17:16:54.733Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>One of our project requirements was to support Markdown rendering, aiming to replicate effects similar to ChatGPT and Claude.</p><p>This article aims to document the problems I encountered and their solutions while implementing this feature.</p><span id="more"></span><div class="danger"><p>This is an English translation of an article originally published in Chinese on <strong>February 20, 2024</strong>. The content of the original article may be <strong>outdated or deprecated</strong>. Please verify the current status of any tools, libraries, or methods mentioned before implementing them in your projects.</p></div><h1 id="react-markdown"><a class="markdownIt-Anchor" href="#react-markdown"></a> <code>react-markdown</code></h1><p>First, install <code>react-markdown</code>.</p><pre><code class="language-bash">npm install react-markdown</code></pre><p>Let's test using the official example text from the <code>react-markdown</code> library:</p><pre><code class="language-plaintext"># A demo of `react-markdown``react-markdown` is a markdown component for React.👉 Changes are re-rendered as you type.👈 Try writing some markdown on the left.## Overview* Follows [CommonMark](https://commonmark.org)* Optionally follows [GitHub Flavored Markdown](https://github.github.com/gfm/)* Renders actual React elements instead of using `dangerouslySetInnerHTML`* Lets you define your own components (to render `MyHeading` instead of `'h1'`)* Has a lot of plugins## ContentsHere is an example of a plugin in action([`remark-toc`](https://github.com/remarkjs/remark-toc)).**This section is replaced by an actual table of contents**.## Syntax highlightingHere is an example of a plugin to highlight code:[`rehype-highlight`](https://github.com/rehypejs/rehype-highlight).```jsimport React from 'react'import ReactDOM from 'react-dom'import Markdown from 'react-markdown'import rehypeHighlight from 'rehype-highlight'const markdown = `# Your markdown here`ReactDOM.render(  &lt;Markdown rehypePlugins={[rehypeHighlight]}&gt;{markdown}&lt;/Markdown&gt;,  document.querySelector('#content'))\```&gt; Pretty neat, eh?## GitHub flavored markdown (GFM)For GFM, you can *also* use a plugin:[`remark-gfm`](https://github.com/remarkjs/react-markdown#use).It adds support for GitHub-specific extensions to the language:tables, strikethrough, tasklists, and literal URLs.These features **do not work by default**.👆 Use the toggle above to add the plugin.| Feature    | Support              || ---------: | :------------------- || CommonMark | 100%                 || GFM        | 100% w/ `remark-gfm` |~~strikethrough~~* [ ] task list* [x] checked itemhttps://example.com## HTML in markdown⚠️ HTML in markdown is quite unsafe, but if you want to support it, you canuse [`rehype-raw`](https://github.com/rehypejs/rehype-raw).You should probably combine it with[`rehype-sanitize`](https://github.com/rehypejs/rehype-sanitize).&lt;blockquote&gt;  👆 Use the toggle above to add the plugin.&lt;/blockquote&gt;## ComponentsYou can pass components to change things:```markdownimport React from 'react'import ReactDOM from 'react-dom'import Markdown from 'react-markdown'import MyFancyRule from './components/my-fancy-rule.js'const markdown = `# Your markdown here`ReactDOM.render(  &lt;Markdown    components={{      // Use h2s instead of h1s      h1: 'h2',      // Use a component instead of hrs      hr(props) {        const {node, ...rest} = props        return &lt;MyFancyRule {...rest} /&gt;      }    }}  &gt;    {markdown}  &lt;/Markdown&gt;,  document.querySelector('#content'))/```</code></pre><div class="danger"><p>In the test text, there are also code blocks, but since I'm using Hexo for my blog, these texts that should be in code blocks (as I set) were all rendered as code blocks.</p><p>Therefore, I added a backslash after each code block to prevent them from being rendered as code blocks. <strong>You can remove these backslashes when testing</strong>.</p></div><pre><code class="language-jsx">import ReactMarkdown from "react-markdown";function App() {  const markdownContent = `{The Markdown test text mentioned earlier}`;  return (    &lt;&gt;      &lt;ReactMarkdown&gt;{markdownContent}&lt;/ReactMarkdown&gt;    &lt;/&gt;  );}</code></pre><p>The rendered effect (remember to replace the value of <code>markdownContent</code>):</p><p><img src="/posts/f27d/1.webp" alt="Rendered Markdown without table, task lists, and strikethrough" loading="lazy"></p><p>As you can see, there are some differences compared to Typora or GitHub's rendering effects. For example, the code blocks and regular text styles are too similar, and we'd prefer code blocks with distinct background colors and syntax highlighting.</p><p>Additionally, special styles like tables, task lists, and strikethrough weren't rendered.</p><h1 id="remark-gfm"><a class="markdownIt-Anchor" href="#remark-gfm"></a> <code>remark-gfm</code></h1><p>Actually, the test text already told us why: these are GFM (GitHub Flavored Markdown) features, and <code>react-markdown</code> doesn't support GFM by default.</p><p>So we need to install the <code>remark-gfm</code> plugin to support GFM:</p><pre><code class="language-bash">npm install remark-gfm</code></pre><p>Using plugins with <code>react-markdown</code> is quite simple:</p><pre><code class="language-jsx">import remarkGfm from "remark-gfm";// ...&lt;ReactMarkdown remarkPlugins={[remarkGfm]}&gt;{markdownContent}&lt;/ReactMarkdown&gt;</code></pre><p>The rendered effect:</p><p><img src="/posts/f27d/2.webp" alt="Rendered Markdown without table borders" loading="lazy"></p><p>At this point, you're probably wondering: where are the table borders??</p><p>If we inspect the table's styles using developer tools, we'll find that the table is indeed rendered as a <code>table</code> tag, but the User Agent Stylesheet has applied some processing to the <code>table</code> tag. So we need to write some styles to override these default styles:</p><pre><code class="language-css">table {  border-spacing: 0 !important;  border-collapse: collapse !important;  border-color: inherit !important;  display: block !important;  width: max-content !important;  max-width: 100% !important;  overflow: auto !important;}tbody, td, tfoot, th, thead, tr {  border-color: inherit !important;  border-style: solid !important;  border-width: 2px !important;}</code></pre><p>Add <code>!important</code> to all styles to override the User Agent Stylesheet styles.</p><p>The rendered effect now:</p><p><img src="/posts/f27d/3.webp" alt="Rendered Markdown with bold table border" loading="lazy"></p><div class="danger"><p>Due to security considerations, I decided not to use HTML support, so I won't demonstrate it here.</p></div><p>Now we've completed a simple Markdown rendering feature. However, there's one crucial feature in this requirement: code block highlighting.</p><h1 id="code-block-highlighting"><a class="markdownIt-Anchor" href="#code-block-highlighting"></a> Code Block Highlighting</h1><p>Due to project requirements, code blocks must be prominent and have syntax highlighting, which applies to inline code blocks as well.</p><p>Here we can use <code>react-syntax-highlighter</code>.</p><pre><code class="language-bash">npm install react-syntax-highlighter</code></pre><p><code>react-syntax-highlighter</code> has two engines: <code>prism</code> and <code>highlight.js</code>. You can search online for their detailed differences.</p><p>Here we'll use the <code>prism</code> engine with the <code>oneDark</code> theme:</p><pre><code class="language-jsx">import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";</code></pre><p>In the <code>component</code> prop of <code>ReactMarkdown</code>, we can customize how code blocks are rendered. If you've seen other tutorials online, they almost all write it like this:</p><pre><code class="language-jsx">&lt;ReactMarkdown  remarkPlugins={[remarkGfm]}  components={{    code({ node, inline, className, children, ...props }) {      const match = /language-(\w+)/.exec(className || '')      return !inline &amp;&amp; match ? (        &lt;SyntaxHighlighter          style={nightOwl}          language={match[1]}          PreTag="div"          children={String(children).replace(/\n$/, '')}          {...props}        /&gt;      ) : (        &lt;code className={className} {...props} children={children} /&gt;      )    }  }}&gt;    {markdownContent}&lt;/ReactMarkdown&gt;</code></pre><p>Here we customized the rendering behavior of the <code>code</code> component.</p><blockquote><p>The parameters in <code>code({ node, inline, className, children, ...props }) {}</code> represent:</p><ul><li><code>node</code>: current node</li><li><code>inline</code>: whether it's an inline code block</li><li><code>className</code>: class name</li><li><code>children</code>: child nodes (content in code block)</li><li><code>...props</code>: other properties</li></ul></blockquote><p>Then we use regex to match <code>language-xxx</code> in <code>className</code>, and if there's a match and it's not an inline code block, we use <code>SyntaxHighlighter</code> to render the code block; otherwise, we use the default <code>code</code> tag.</p><p><em><strong>But!!!</strong></em></p><p>The <code>inline</code> property has been deprecated and won't be passed as a parameter in the new version of <code>react-markdown</code>!!!</p><p>This really gave me a headache at the time, and I couldn't find a solution after searching online. I decided to temporarily solve another problem: rendering code blocks without defined languages.</p><p>After rendering, the biggest difference between code blocks and inline code blocks is that code blocks have a <code>pre</code> tag wrapper. Since the <code>code</code> tag as a child tag can't get the parent tag's styles from <code>props</code>, but thinking reversely, the <code>pre</code> tag can get the <code>code</code> tag's styles!</p><p>So I wrote this wild solution:</p><pre><code class="language-jsx">&lt;Markdown  remarkPlugins={[remarkGfm]}  components={{    pre({node, className, children, ...props}) {      if (children["type"] === "code") {        try {          const match = children["props"]["className"].match(/language-(\w+)/)          return (            &lt;pre&gt;              &lt;SyntaxHighlighter                style={oneDark}                language={match[1]}                PreTag="div"                showLineNumbers                wrapLongLines                children={String(children["props"]["children"]).replace(/\n$/, '')}              /&gt;            &lt;/pre&gt;          )        } catch (e) {          return (            &lt;pre&gt;              &lt;SyntaxHighlighter                style={oneDark}                language="python"                PreTag="div"                showLineNumbers                wrapLongLines                children={String(children["props"]["children"]).replace(/\n$/, '')}              /&gt;            &lt;/pre&gt;          )        }      }    }  }}&gt;  {markdownContent}&lt;/Markdown&gt;</code></pre><p>Directly check the <code>children</code>'s type in the <code>pre</code> tag - if it's <code>code</code>, then it must be a code block, and then match the language type in <code>className</code>.</p><p>If there's no match, it will cause an error, so I temporarily added a <code>try...catch</code> statement - if there's no match, just default to rendering it as a Python language code block.</p><p>The effect is not bad:</p><p><img src="/posts/f27d/4.webp" alt="Rendered Code Block with styles" loading="lazy"></p><h1 id="afterword"><a class="markdownIt-Anchor" href="#afterword"></a> Afterword</h1><p>There are still some issues with this implementation, such as unresolved inline code block rendering, the <code>try...catch</code> statement not being a good solution, and the technical debt of defaulting to Python language for undefined language code blocks is quite messy!</p><p>However, due to project time constraints, I didn't delve deeper into these issues. Perhaps when I have more time in the future, I'll revisit and solve these problems. That's for another time.</p></body></html>]]></content>
    
    
    <summary type="html">This article is a practical guide on rendering Markdown in React, using the react-markdown library. It covers adding support for GitHub Flavored Markdown (GFM) with remark-gfm and implementing syntax highlighting for code blocks with react-syntax-highlighter. The tutorial provides a unique workaround for a common issue in newer versions of react-markdown where the inline prop is deprecated, offering a clever solution to correctly style code blocks. This guide is ideal for developers seeking an up-to-date method for creating rich Markdown experiences in their React applications.</summary>
    
    
    
    <category term="Programming Notes" scheme="https://blog.cytrogen.icu/categories/Programming-Notes/"/>
    
    
    <category term="JavaScript" scheme="https://blog.cytrogen.icu/tags/JavaScript/"/>
    
    <category term="React.js" scheme="https://blog.cytrogen.icu/tags/React-js/"/>
    
  </entry>
  
  <entry>
    <title>Batch Merging TS Files Using ffmpy3</title>
    <link href="https://blog.cytrogen.icu/posts/6976.html"/>
    <id>https://blog.cytrogen.icu/posts/6976.html</id>
    <published>2025-02-21T01:37:56.000Z</published>
    <updated>2026-02-09T17:16:54.681Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>How to use Python's <code>ffmpy3</code> package to batch merge TS files into a single MP4 file.</p><span id="more"></span><div class="danger"><p>Before reading, you need to know:</p><ul><li><code>ffmpy3</code> is a Python wrapper for FFmpeg</li><li><code>ffmpy3</code> complies FFmpeg command line based on provided parameters and options</li></ul></div><h1 id="using-ffmpy3"><a class="markdownIt-Anchor" href="#using-ffmpy3"></a> Using <code>ffmpy3</code></h1><h2 id="installing-ffmpy3-package"><a class="markdownIt-Anchor" href="#installing-ffmpy3-package"></a> Installing <code>ffmpy3</code> Package</h2><p>Install using <code>pip</code>:</p><pre><code class="language-bash">pip install ffmpy3</code></pre><h2 id="simple-ffmpy3-example"><a class="markdownIt-Anchor" href="#simple-ffmpy3-example"></a> Simple <code>ffmpy3</code> Example</h2><pre><code class="language-python">import ffmpy3ff = ffmpy3.FFmpeg(    inputs={'input_file': 'parameter1'},    outputs={'output_file': 'parameter2'})</code></pre><p>The final result is equivalent to entering in terminal:</p><pre><code class="language-bash">FFmpeg parameter1 -i input_file parameter2 output_file</code></pre><h1 id="batch-merging-ts-files"><a class="markdownIt-Anchor" href="#batch-merging-ts-files"></a> Batch Merging TS Files</h1><h2 id="directory-structure"><a class="markdownIt-Anchor" href="#directory-structure"></a> Directory Structure</h2><pre><code class="language-plaintext">├───folder│    python_file.py│    file.txt│    fileA.ts│    fileB.ts│    fileC.ts│    fileD.ts...</code></pre><h4 id="filetxt"><a class="markdownIt-Anchor" href="#filetxt"></a> <code>file.txt</code></h4><p>Write the TS filenames in <code>file.txt</code>:</p><pre><code class="language-plaintext">file 'fileA.ts'file 'fileB.ts'file 'fileC.ts'file 'fileD.ts'</code></pre><blockquote><p>Note:</p><ol><li>Use single quotes, not double quotes - the latter will cause errors!</li><li>Use relative paths within the quotes</li></ol></blockquote><h4 id="python-file"><a class="markdownIt-Anchor" href="#python-file"></a> Python File</h4><p>Use this code to batch merge TS files:</p><pre><code class="language-python">ff = ffmpy3.FFmpeg(        inputs={f'file.txt': '-f concat'},        outputs={f'filename.mp4': '-c copy'})ff.run()</code></pre><p>The above code is equivalent to entering in terminal:</p><pre><code class="language-bash">FFmpeg -f concat -i file.txt -c copy filename.mp4</code></pre></body></html>]]></content>
    
    
    <summary type="html">This article provides a concise tutorial on how to batch merge multiple TS video files into a single MP4 file using Python. It demonstrates the use of the ffmpy3 library, a Python wrapper for FFmpeg, explaining how to create a file list and use a simple script to execute the -f concat and -c copy commands for a fast, lossless merge.</summary>
    
    
    
    <category term="Programming Notes" scheme="https://blog.cytrogen.icu/categories/Programming-Notes/"/>
    
    
    <category term="Python" scheme="https://blog.cytrogen.icu/tags/Python/"/>
    
    <category term="ffmpy3" scheme="https://blog.cytrogen.icu/tags/ffmpy3/"/>
    
  </entry>
  
  <entry>
    <title>Hexo i18n Configuration</title>
    <link href="https://blog.cytrogen.icu/posts/567.html"/>
    <id>https://blog.cytrogen.icu/posts/567.html</id>
    <published>2024-12-03T03:37:58.000Z</published>
    <updated>2026-02-09T17:16:54.709Z</updated>
    
    <content type="html"><![CDATA[<html><head></head><body><p>When running a personal blog, you might encounter this requirement: wanting your website to support multiple languages so that readers from different regions can easily read your content.</p><p>This is where website internationalization (also known as i18n) comes into play.</p><p>For blogs built with Hexo, implementing internationalization requires consideration not only of content translation but also template rendering and other issues.</p><span id="more"></span><div class="danger"><ul><li><p>This article primarily uses the <code>hexo-generator-plus</code> plugin.</p><p><a href="https://github.com/kiwirafe/hexo-generator-plus"><img src="https://gh-card.dev/repos/kiwirafe/hexo-generator-plus.svg?fullname=" alt="kiwirafe/hexo-generator-plus - GitHub" loading="lazy"></a></p><p>Before starting the configuration, please ensure you have uninstalled the following plugins to avoid conflicts:</p><ul><li><code>hexo-generator-archive</code></li><li><code>hexo-generator-category</code></li><li><code>hexo-generator-index</code></li><li><code>hexo-generator-tag</code></li></ul></li><li><p>This article uses the <strong>Pug</strong> templating language.</p></li><li><p>For the language switching solution in the navigation bar, I have only implemented bilingual logic.</p></li></ul></div><h2 id="basic-configuration"><a class="markdownIt-Anchor" href="#basic-configuration"></a> Basic Configuration</h2><blockquote><p>To avoid confusion:</p><ol><li><p>The <code>_config.yml</code> in the Hexo root directory will be referred to as <strong>Hexo Configuration</strong></p></li><li><p><code>themes/**/_config.yml</code> will be referred to as <strong>Theme Configuration</strong></p></li></ol></blockquote><p>First, we need to make some basic settings in Hexo's configuration file. These settings will determine the website's language options and URL structure.</p><pre><code class="language-yaml">language: [zh, en]  # Supported language list, first one is defaultnew_post_name: :title.md  # New article naming convention# If you have hexo-abbrlink installedpermalink: posts/:abbrlink.html  # Article permalink formatabbrlink:  rep: hex  # Use hexadecimal as unique identifier# hexo-generator-plus configurationgenerator_plus:  language: [zh, en]  # Generator supported languages  pagination_dir: 'page'  # Pagination directory  generator: ["index", "archive", "category", "tag"]  # Pages to generate  # Index generator configuration  index_generator:    per_page: 10  # These numbers and order can be customized    order_by: -date  # Archive page configuration  archive_generator:    per_page: 25    order_by: -date  # Category page configuration  category_generator:    per_page: 25    order_by: -date    enable_index_page: true  # If you want a category index page  # Tag page configuration  tag_generator:    per_page: 25    order_by: name    enable_index_page: true  # If you want a tag index page</code></pre><p>Then configure the desired menu links in the theme configuration:</p><pre><code class="language-yaml">menu:   home: /   archive: /archives   categories: /categories   tags: /tags   about: /about   GitHub: https://github.com/cytrogen   RSS: /atom.xml</code></pre><h2 id="directory-structure"><a class="markdownIt-Anchor" href="#directory-structure"></a> Directory Structure</h2><p>Here is the required directory structure:</p><pre><code class="language-plaintext">source/├── _posts/                     # Default language blog posts│   └── *.md                    # No subdirectories allowed├── en/                         # English-specific content│   └── Same structure as source directory├── archives/                   # Archive page│   └── index.md                # layout: archive├── categories/                 # Category page│   └── index.md                # layout: category-index└── tags/                       # Tag page    └── index.md                # layout: tag-index</code></pre><blockquote><p>Of course, you can choose other languages, but other language directories need to match the names in <code>themes/**/languages/*.yml</code>.</p></blockquote><p>Please ensure each <code>*.md</code> file has <code>lang: **</code> in its Front-Matter.</p><h2 id="language-file-configuration"><a class="markdownIt-Anchor" href="#language-file-configuration"></a> Language File Configuration</h2><p>Fixed website text (such as navigation menus, button text, etc.) needs to be internationalized through language files. These files should be placed in the <code>themes/**/languages/</code> directory.</p><p>Here's my examples:</p><ol><li><p><code>zh.yml</code>:</p> <pre><code class="language-yaml">menu:home: 首页archive: 归档tags: 标签categories: 分类about: 关于friendlinks: 友情链接archive_title: 归档tags_title: 标签categories_title: 分类prev: 上一页next: 下一页prev_post: 上一篇next_post: 下一篇more: ...阅读全文translated: 翻译 · 原文地址</code></pre></li><li><p><code>en.yml</code>:</p> <pre><code class="language-yaml">menu:home: HOMEarchive: ARCtags: TAGScategories: CATEabout: ABOUTfriendlinks: Friend Linksarchive_title: Archivetags_title: Tagscategories_title: Categoriesprev: PREVnext: NEXTprev_post: PREV POSTnext_post: NEXT POSTmore: ...MOREtranslated: Translate · Original Link</code></pre></li></ol><h2 id="template-file-implementation"><a class="markdownIt-Anchor" href="#template-file-implementation"></a> Template File Implementation</h2><p>From here on, I'll only write about the solution used for my blog website.</p><p><strong>Please modify according to your own theme.</strong></p><h4 id="category-page-templates"><a class="markdownIt-Anchor" href="#category-page-templates"></a> Category Page Templates</h4><p>Category pages need two templates: category index page and specific category page.</p><ol><li><p>Category list page (<code>category-index.pug</code>):</p> <pre><code class="language-plaintext">extends partial/layoutblock container    .archive        // Title content prioritizes page.title        // If it doesn't exist, use the i18n function __() to get categories_title translation        h2.archive-title= page.title || __('categories_title')        .category-list            // Get all category data            each category in get_categories().data                // Calculate number of posts matching current language for each category                - var postCount = category.posts.data ? category.posts.data.filter(post =&gt; post.lang === page.lang).length : 0                if postCount &gt; 0                    .category-item                        // Each category shows as a link, including category name and post count                        // url_for_lang() generates multilingual-supported URL                        - var categoryPath = category.slug || category.name                        a.post-title-link(href=url_for_lang(page.lang, 'categories/' + categoryPath))                            = category.name                            span.category-count= ` (${postCount})`</code></pre></li><li><p>Specific category page (<code>category.pug</code>):</p> <pre><code class="language-plaintext">extends partial/layoutblock container    include mixins/post    .archive        h2.archive-title= page.category        +postList()</code></pre><p>Here, post-related functionality is encapsulated in a series of mixins for reuse across different pages (<code>mixins/post.pug</code>):</p> <pre><code class="language-plaintext">mixin postInfo(item)    .post-info        != full_date(item.date, 'l')        // If post has from property and current page is home or post page        if item.from &amp;&amp; (is_home() || is_post())            // Show a link indicating post translation source            a.post-from(href=item.from target="_blank" title=item.from)!= __('translated')mixin posts()    ul.home.post-list        // Iterate through all posts        - for (var post of page.posts.data || [])            // Only show posts matching current page language            - if (post.lang == page.lang)                li.post-list-item                    article.post-block                        h2.post-title                            a.post-title-link(href= url_for(post.path))                                != post.title                        +postInfo(post)                        // If there's an excerpt, show it with "read more" link                        if post.excerpt                            .post-content                                != post.excerpt                            a.read-more(href= url_for(post.path))!= __('more')                        else                            .post-content                                != post.content</code></pre></li></ol><h4 id="tag-page-templates"><a class="markdownIt-Anchor" href="#tag-page-templates"></a> Tag Page Templates</h4><p>Almost identical to category page templates:</p><ol><li><p><code>tag-index.pug</code>:</p> <pre><code class="language-plaintext">extends partial/layoutblock container    include mixins/post    .archive        h2.archive-title= page.title || __('tags_title')        .tag-list            each tag in get_tags().data                - var postCount = tag.posts.data ? tag.posts.data.filter(post =&gt; post.lang === page.lang).length : 0                if postCount &gt; 0                    .tag-item                        - var tagPath = tag.slug || tag.name                        a.post-title-link(href=url_for_lang(page.lang, 'tags/' + tagPath))                            = tag.name                            span.tag-count= ` (${postCount})`block pagination    include mixins/paginator    +home()block copyright    include partial/copyright</code></pre></li><li><p><code>tag.pug</code>:</p> <pre><code class="language-plaintext">extends partial/layoutblock container    include mixins/post    .archive        h2.archive-title= page.tag        +postList()block pagination    include mixins/paginator    +home()block copyright    include partial/copyright</code></pre></li></ol><h4 id="archive-page-template"><a class="markdownIt-Anchor" href="#archive-page-template"></a> Archive Page Template</h4><p>Archive page only needs one <code>archive.pug</code>:</p><pre><code class="language-plaintext">extends partial/layoutblock container    include mixins/post    +postList()block pagination    include mixins/paginator    +home()block copyright    include partial/copyright</code></pre><p>Its mixin:</p><pre><code class="language-plaintext">mixin postList()    .archive        // Check if there are posts        if page.posts            // Ensure post list exists and is not empty            - var posts = page.posts.data || page.posts            if posts &amp;&amp; posts.length                // Create a years object for grouping                // Only process posts matching current page language                // Get year from post date                // Add posts to corresponding year array                - var years = {}                - for (var post of posts)                    - if (post.lang == page.lang)                        - var year = new Date(post.date).getFullYear()                        - if (!years[year]) years[year] = []                        - years[year].push(post)                // Sort years in descending order (show largest year first)                - Object.keys(years).sort((a, b) =&gt; b - a).forEach(function(year) {                    h2.archive-year!= year                    - years[year].forEach(function(post) {                        .post-item                            +postInfo(post)                            a.post-title-link(href= url_for(post.path))                                != post.title                    - })                - })</code></pre><h4 id="navigation-bar-implementation"><a class="markdownIt-Anchor" href="#navigation-bar-implementation"></a> Navigation Bar Implementation</h4><p>The navigation bar is the key interface for language switching (<code>nav.pug</code>).</p><p>Since my blog theme's navigation bar isn't wide, I wrote top and bottom sections to separate some links:</p><pre><code class="language-plaintext">ul.nav.nav-list    // Top section    div.nav-list-top        // Iterate through all keys and values in theme menu config        each path, key in theme.menu            // Exclude links to be placed at bottom, in my case GitHub and RSS            if key !== 'GitHub' &amp;&amp; key !== 'RSS'                li.nav-list-item                    // Check if external link                    // If yes, open in new tab                    // Add /en prefix for English pages                    // Check if current page is active (for highlighting)                    - var re = /^(http|https):\/\/*/gi;                    - var tar = re.test(path) ? "_blank" : "_self"                    - var fullPath = page.lang === 'en' ? '/en' + path : path                    - var act = !re.test(path) &amp;&amp; "/" + page.current_url === fullPath                    a.nav-list-link(class={active: act} href=url_for(fullPath) target=tar)                        != __(('menu.' + key))    // Bottom section    div.nav-list-bottom        // Language switch button        li.nav-list-item.lang-switch            if page.lang == 'en'                a.nav-list-link(href=url_for('/')) 中文            else                a.nav-list-link(href=url_for('/en')) ENGLISH</code></pre><h2 id="usage"><a class="markdownIt-Anchor" href="#usage"></a> Usage</h2><p>After completing the above configuration, specify the language in the Front-Matter when creating new articles:</p><pre><code class="language-markdown">---title: Article Titledate: 2024-01-01lang: en---</code></pre><p>If you want to create versions of the same article in other languages, just create a new Markdown file, specify the appropriate <code>lang</code>, and link to the original article using the <code>from</code> field in the Front-Matter:</p><pre><code class="language-markdown">---title: 文章标题date: 2024-01-01lang: zhfrom: /posts/original-post.html---</code></pre></body></html>]]></content>
    
    
    <summary type="html">This article is a comprehensive guide to setting up internationalization (i18n) for a Hexo blog, primarily using the hexo-generator-plus plugin. It walks you through the entire process, from initial configuration in _config.yml, organizing your directory structure, and creating language files, to modifying Pug templates for your archive, category, and tag pages. The tutorial also includes a practical implementation of a navigation bar language switcher. Follow this guide to seamlessly add bilingual or multilingual support to your website.</summary>
    
    
    
    <category term="Programming Notes" scheme="https://blog.cytrogen.icu/categories/Programming-Notes/"/>
    
    
    <category term="Hexo" scheme="https://blog.cytrogen.icu/tags/Hexo/"/>
    
    <category term="Pug" scheme="https://blog.cytrogen.icu/tags/Pug/"/>
    
  </entry>
  
</feed>
