<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Programming - Bagaag</title>
	<atom:link href="https://www.bagaag.com/category/programming/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.bagaag.com</link>
	<description></description>
	<lastBuildDate>Mon, 20 Oct 2025 02:33:18 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://www.bagaag.com/wp-content/uploads/2025/09/cropped-bagaag-favicon-32x32.png</url>
	<title>Programming - Bagaag</title>
	<link>https://www.bagaag.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
					<title>Todoist Actual Backup</title>
					<link>https://www.bagaag.com/todoist-actual-backup/</link>
					<comments>https://www.bagaag.com/todoist-actual-backup/#respond</comments>
		
		<dc:creator><![CDATA[matt]]></dc:creator>
		<pubDate>Sun, 19 Oct 2025 14:07:20 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[ai]]></category>
		<category><![CDATA[python]]></category>
		<guid isPermaLink="false">https://www.bagaag.com/?p=636</guid>

					<description><![CDATA[I built a Python script to export data from a Todoist account. There are a few topics here: SaaS customer data access, using AI for development, and the app itself. Why did I need to build this? Todoist is a mobile and web-based task manager app. As a maker of lists, I love this service. [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I built a Python script to export data from a Todoist account. There are a few topics here: SaaS customer data access, using AI for development, and the app itself. </p>



<ul class="wp-block-list">
<li><a href="https://www.bagaag.com/todoist-actual-backup/#why">Part 1</a>: Why did I need to build this?</li>



<li><a href="https://www.bagaag.com/todoist-actual-backup/#ai" data-type="internal" data-id="#ai">Part 2</a>: Using AI to write software</li>



<li><a href="https://www.bagaag.com/todoist-actual-backup/#script">Part 3</a>: The end product with source code and instructions for use</li>
</ul>



<span id="more-636"></span>



<h4 class="wp-block-heading" id="why">Why did I need to build this? </h4>



<p><a href="https://www.todoist.com/">Todoist</a> is a mobile and web-based task manager app. As a maker of lists, I love this service. A desktop web interface and snappy mobile app stay out of the way and assist my mind in the way only great software can. They’ve gracefully combined all the features you could want in a task list app without making it overly complicated to use. Hats off for that.</p>



<p>As much as I love Todoist, it’s a “software as a service” or cloud app. That means they store and control my data, ultimately deciding what data to retain and for how long, what data to make available and how. It’s a reasonable trade-off for a great app with seamless synchronization between desktop and mobile environments. That said, I pour considerable amounts of personal data that is valuable to me into this app. My access to that data outside the confines of a paid service is equally important to me.</p>



<p>Todoist provides a nice backup page where you can download daily zips of your data. Unfortunately, those backups lack much of the data you’ve put into the app. They do not include completed tasks, related sub-tasks, comments or attachments. The backup provided is a zip of CSV files, not easily viewable outside of a spreadsheet. That’s a lot of data and access they’re choosing not to make available to paying customers.</p>



<p>The motivation behind that decision is obvious. Many SaaS businesses are willing to shackle customer data in order to subtly force reliance on the monthly subscription. Making it a little bit harder to leave the service helps maintain the all-important <a href="https://en.wikipedia.org/wiki/Revenue_stream">MRR</a>. Todoist is not alone in this. As subscription software services have become the norm, so has vendor lock-in via data access limitations. Customers who aren’t technically savvy or interested enough to host their own open source software apps are stuck with little recourse.</p>



<p>For some reason, all that missing data in Todoist backups is accessible if you’re a developer. Kudos to them for providing a nice stable API with — as far as I can tell — complete data access. But shame on them for significantly limiting the data paying customers can export unless they have software development skills. Come on, guys.</p>



<p>Faced with this conundrum, I decided to build a solution for exporting all the data from Todoist using their API, so customers without development skills can back up and access their own data outside the paid service.</p>



<h4 class="wp-block-heading" id="ai">Using AI to Write Software</h4>



<figure class="wp-block-image size-full is-resized"><img fetchpriority="high" decoding="async" width="924" height="660" src="https://www.bagaag.com/wp-content/uploads/2025/10/and-now-for-something-completely-different.png" alt class="wp-image-652" style="width:400px" srcset="https://www.bagaag.com/wp-content/uploads/2025/10/and-now-for-something-completely-different.png 924w, https://www.bagaag.com/wp-content/uploads/2025/10/and-now-for-something-completely-different-300x214.png 300w, https://www.bagaag.com/wp-content/uploads/2025/10/and-now-for-something-completely-different-768x549.png 768w" sizes="(max-width: 924px) 100vw, 924px"></figure>



<p>And now for something completely different. The fear and loathing and unchecked optimism around AI in the software development community is rampant right now. As a developer wanting to remain relevant in this economy, it is a technology I need to be using. Would I have enjoyed writing this script on my own? Yes. Would it have taken <em>substantially</em> longer to do so? Yes.</p>



<p>A popular expression that describes what’s going on with AI and software development right now is that “English is the new programming language”. Software developers have traditionally spent their day writing code in some programming language like C# or JavaScript or Python. AI now does the programming well enough that it’s more efficient if developers become orchestrators, specification writers and code reviewers, telling the AI what to do and monitoring the results in a feedback loop until the job is done to satisfaction.</p>



<p>On one hand, I’m blown away by this technology. It’s really good. I love being able to whip up a quick shell script in the time it takes to describe what it should do. On the other hand I see the imminent death of software development as we know it. I love programming, so that’s sad.</p>



<p>After learning Todoist has a robust API that provides access to the data I wanted to backup, I decided to use Github Copilot to build it for me in Python. <a href="https://en.wikipedia.org/wiki/Large_language_model">LLMs</a> are <em>really</em> good at language, and programming is just another language to them. Copilot is implemented as a chat bot that sits in the IDE, the software developers use to write, build, run and test code. I used Visual Studio Code for this project.</p>



<p>I started with this basic prompt:</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.75rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">Markdown</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>Create a command line script that exports all active and completed tasks from the Todoist API to a JSON file.
- Use the Todoist API documented at https://developer.todoist.com/api/v1/
- Use the Todoist python client documented at https://doist.github.io/todoist-api-python/
- Use a .venv to scope the python environment.
- Include tasks from all active and archived projects.
- Include all active and completed tasks available in the API. This may require paging through results.
- Save attachments to an ./attachments folder and reference the attachment name for each in the JSON output.
- Tasks are nested under project objects in the JSON result, with each project object containing all available project fields in the API.
- Include all task fields available in the API for each task.
- If a task has comments, nest them in an array under the task in the JSON output.
- Name the JSON file Todoist-Actual-Backup-YYYY-MM-DD.json where YYYY-MM-DD is the current date. Overwrite the file if it already exists. 
- Use the API key provided in the TODOIST_KEY environment variable.
- Run the export if the script is run with the "export" argument. Any other argument, or no argument, displays a description of what the script does and how to use it.
- After writing the JSON result, use its contents to create an attractive human readable version in a single file HTML document named Todoist-Actual-Backup-YYYY-MM-DD.html. This file groups tasks by project and includes all project and task details, including attachments (link to the locally downloaded files) and comments. Use a Jinja2 template to generate the HTML file.</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #D8DEE9FF">Create a command line script that exports all active and completed tasks from the Todoist API to a JSON file.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Use the Todoist API documented at https://developer.todoist.com/api/v1/</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Use the Todoist python client documented at https://doist.github.io/todoist-api-python/</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Use a .venv to scope the python environment.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Include tasks from all active and archived projects.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Include all active and completed tasks available in the API. This may require paging through results.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Save attachments to an ./attachments folder and reference the attachment name for each in the JSON output.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Tasks are nested under project objects in the JSON result, with each project object containing all available project fields in the API.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Include all task fields available in the API for each task.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> If a task has comments, nest them in an array under the task in the JSON output.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Name the JSON file Todoist-Actual-Backup-YYYY-MM-DD.json where YYYY-MM-DD is the current date. Overwrite the file if it already exists. </span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Use the API key provided in the TODOIST_KEY environment variable.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> Run the export if the script is run with the "export" argument. Any other argument, or no argument, displays a description of what the script does and how to use it.</span></span>
<span class="line"><span style="color: #ECEFF4">-</span><span style="color: #D8DEE9FF"> After writing the JSON result, use its contents to create an attractive human readable version in a single file HTML document named Todoist-Actual-Backup-YYYY-MM-DD.html. This file groups tasks by project and includes all project and task details, including attachments (link to the locally downloaded files) and comments. Use a Jinja2 template to generate the HTML file.</span></span></code></pre></div>



<p>From this prompt and after waiting a minute or two, I got a finished, but not working Python script. That AI produced what it did is impressive, but it’s still a lot like working with a junior developer who knows every programming language. It doesn’t make the best decisions if not provided with sufficient direction.</p>



<p>After several cycles of run, paste the error into chat, repeat, I had a working script that met the requirements I’d given. That initial process didn’t take more than a few hours.</p>



<p>I then tested and tweaked, adding several features along the way. For example, I realized I was only getting 90 days of completed tasks and that was a limitation imposed by Todoist. So I asked it to maintain a local file of completed tasks across runs to extend their storage beyond 90 days and include them in exports.</p>



<p>Adding features like this works a lot like the initial prompt. Just explain what the feature should do, and maybe provide some technical direction where needed. The AI comes back with detailed explanations of the logic its going through in planning and executing a solution, all in plain English. Source code control allows the developer to clearly see and review the changes the AI has made at each step. </p>



<p>Creating this script felt collaborative. In addition to generating and updating code, Copilot is great for discussing feature implementation options and providing recommendations that are backed with solid reasoning. For example, when I ran into API rate limits during testing, we had a good discussion over the pros and cons of caching API calls vs a more complex rate limiting mechanism. During that, I realized the issue was less that we needed to limit the rate and more that the initial implementation was highly inefficient in its use of the API. That led to a huge decrease in the number of API calls required to run the script.</p>



<p>Even though this is programming in English, someone who doesn’t understand software development would probably struggle with anything even marginally complicated. If you’re not careful, you can easily end up with software that seems to work but is a real mess under the hood. How much does that matter if AI is doing the coding? It’s a good question.</p>



<p>Sadly, developers today are helping to train AI to do this job with full autonomy. Tools like Google’s <a href="https://jules.google/">Jules</a> can be assigned tasks via a ticketing system, which are then completed, tested and submitted for review by AI. As this becomes the norm, a team of 5 software developers and a team lead could easily be reduced to just the team lead. And how strong of a developer will that team lead be when they haven’t written their own code in years?</p>



<p>In any case, I spent maybe 8 hours total on this script. My direct code edits were limited to small visual changes to the HTML output that weren’t worth bothering with the AI to make. It would have taken me substantially longer to write this from scratch. So it’s a win for AI. I enjoyed working with Copilot to build it — it’s literally like assigning tasks to a developer and reviewing the results in a feedback loop.</p>



<h4 class="wp-block-heading" id="script">The Todoist Actual Backup script</h4>



<p>OK, enough with the blah bidy blah. You can grab the code at <a href="https://git.bagaag.com/matt/Todoist-Actual-Backup/">https://git.bagaag.com/matt/Todoist-Actual-Backup/</a>. Here’s the README for the project:</p>



<p><strong>Todoist Actual Backup</strong></p>



<p>Todoist is a SaaS task manager. Todoist provides backups of current tasks, but they do not include completed tasks, subtask relationships, comments or attachments. Nor does it provide a human-readable backup in HTML. This Python script provides a command-line tool to export all available active and completed tasks from the Todoist API to a JSON file, including attachments, subtasks and comments, and generates a human-readable HTML backup.</p>



<p><strong>Features</strong></p>



<ul class="wp-block-list">
<li>Exports all active and completed tasks from all projects (active and archived)</li>



<li>Nests tasks under their respective projects, including all available fields</li>



<li>Includes comments for each task</li>



<li>Downloads attachments to <code>output/attachments/</code> and references them in the JSON and HTML output</li>



<li>JSON and HTML files are named with the current date when the script is run</li>



<li>Maintains <code>Todoist-Completed-History.json</code> so completed tasks older than Todoist’s 90-day API window stay in future exports</li>



<li>Reuses archived comments for completed tasks to avoid unnecessary API calls (assumes no new comments after completion)<a href="https://git.bagaag.com/matt/Todoist-Actual-Backup/#setup"></a></li>
</ul>



<p><strong>Setup</strong></p>



<ul class="wp-block-list">
<li>Ensure you have Python 3.8 or newer installed. Check with <code>python --version</code> on the command line.</li>



<li>The script uses a <code>.venv</code> for dependencies. Run the following in a terminal from an empty project folder: <br><code>python -m venv .venv </code><br><code>source .venv/bin/activate </code><br><code>pip install -r requirements.txt</code></li>



<li>Get your API key from <a href="https://app.todoist.com/app/settings/integrations/developer">Todoist</a></li>



<li>Optionally set your Todoist API key in the <code>TODOIST_KEY</code> environment variable. If the environment variable is not set, the script will prompt for it.<a href="https://git.bagaag.com/matt/Todoist-Actual-Backup/#usage"></a></li>
</ul>



<p><strong>Usage</strong></p>



<ol class="wp-block-list">
<li>In a terminal, run <code>source .venv/bin/activate</code> if needed to enter the virtual environment.</li>



<li>Run the script with the <code>export</code> argument: <code>python export_todoist.py export</code></li>
</ol>



<p>This will create <code>output/Todoist-Actual-Backup-YYYY-MM-DD.json</code> and <code>output/Todoist-Actual-Backup-YYYY-MM-DD.html</code>, and it will update <code>output/attachments/</code> with any downloaded files while leaving <code>Todoist-Completed-History.json</code> in the project root. Keep <code>Todoist-Completed-History.json</code> somewhere safe (e.g., in source control or a backup location); it is the only way the exporter can retain completed tasks older than Todoist’s 90-day API retention window.</p>



<hr class="wp-block-separator has-alpha-channel-opacity">



<p>I’ve incorporated this script into my weekly backup process. It was fun to build and hopefully it will be useful to others. I may explore providing a stand-alone executable for it so folks who aren’t comfortable installing Python can use it. Let me know in the comments if that would be useful to you.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bagaag.com/todoist-actual-backup/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
					<title>Single-file URL Shortener</title>
					<link>https://www.bagaag.com/single-file-url-shortener/</link>
					<comments>https://www.bagaag.com/single-file-url-shortener/#respond</comments>
		
		<dc:creator><![CDATA[matt]]></dc:creator>
		<pubDate>Wed, 01 Oct 2025 00:59:53 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://www.bagaag.com/?p=311</guid>

					<description><![CDATA[Recently I decided it would be fun to write a URL shortener. Something I can run on my domain to share shortened URLs that redirect to a longer one. (If you want to skip the blah-bidy-blah, here is the code.) My requirements were fairly straightforward: While I generally prefer Python, setting it up on a [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>Recently I decided it would be fun to write a URL shortener. Something I can run on my domain to share shortened URLs that redirect to a longer one. (If you want to skip the blah-bidy-blah, <a href="https://git.bagaag.com/matt/shortener/src/branch/main/index.php">here is the code</a>.)</p>



<span id="more-311"></span>



<p>My requirements were fairly straightforward:</p>



<ol class="wp-block-list">
<li>Generate unique keys that are as short as possible.</li>



<li>Simple web interface to shorten a URL.</li>



<li>Redirect requests for generated keys to the corresponding URL.</li>



<li>Secure against external abuse.</li>



<li>Build <a href="https://www.ronjeffries.com/xprog/articles/practices/pracsimplest/">the simplest thing that works</a>.</li>
</ol>



<p>While I generally prefer Python, setting it up on a web server is something of a bother. My server already supports PHP, so I created a one-letter subdomain on bagaag.com and started with an empty PHP file. I coded in Visual Studio Code connected to the server via SSH. ♫ It’s my server and I’ll code on production if I want to! ♫</p>



<p>The first requirement was the most interesting to me. I’ve often wondered about the algorithm that sites like <a href="https://tinyurl.com/">tinyurl.com</a> use. I thought about the problem, what kind of keys I’d like to see, and came up with this algorithm:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Given a unique set of characters, iterate through each in order, adding characters as required to retain uniqueness.</p>
</blockquote>



<p>I implemented this in a function that takes as input any string the algorithm might generate and returns the next string in the sequence.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">PHP</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// Unique set of characters for generating keys
$charset = 'acr8mqbs7di4tuh6e9fjv2gwx0nlk5poy1z3'; 

// Provides the next key by incrementing the previous key.
// Start with empty string and then feed the return value 
// back into this function to get the next key, and repeat.
function increment_string($s) {
    global $charset;
    // convert the charset to an array of characters
    $chars = str_to_chars($charset);
    // if the string is empty, return the first character
    if ($s === '') {
        return $chars[0];
    }
    // convert the input string to an array of characters
    $string = str_to_chars($s);
    // iterate over the input string characters from end to beginning
    for ($index = count($string) - 1; $index &gt;= 0; $index--) {
        // find and validate the position of the character in the charset
        $char = $string[$index];
        $pos = array_search($char, $chars);
        if ($pos === false) {
            return "Character not in set: $char";
        }
        // if the character is not the last in the charset, increment 
        //it and break
        if ($pos &lt; count($chars) - 1) {
            $string[$index] = $chars[$pos + 1];
            break;
        } 
        // character is the last in the charset; wrap around to the 
        // first character
        $string[$index] = $chars[0];
        // if we are at the beginning of the string, prepend the first 
        // character and break
        if ($index === 0) {
            array_unshift($string, $chars[0]);
            break;
        }
    }
    return join('', $string);
}</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// Unique set of characters for generating keys</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">charset</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">acr8mqbs7di4tuh6e9fjv2gwx0nlk5poy1z3</span><span style="color: #ECEFF4">'</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Provides the next key by incrementing the previous key.</span></span>
<span class="line"><span style="color: #616E88">// Start with empty string and then feed the return value </span></span>
<span class="line"><span style="color: #616E88">// back into this function to get the next key, and repeat.</span></span>
<span class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">increment_string</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">s</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">global</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">charset</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// convert the charset to an array of characters</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">chars</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">str_to_chars</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">charset</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// if the string is empty, return the first character</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">s</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">===</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">''</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">chars</span><span style="color: #ECEFF4">[</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">]</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// convert the input string to an array of characters</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">string</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">str_to_chars</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">s</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// iterate over the input string characters from end to beginning</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">index</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">count</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">index</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&gt;=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">index</span><span style="color: #81A1C1">--</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// find and validate the position of the character in the charset</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">char</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">[</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">index</span><span style="color: #ECEFF4">]</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">pos</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">array_search</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">char</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">chars</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">pos</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">===</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Character not in set: </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">char</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// if the character is not the last in the charset, increment </span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">//it and break</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">pos</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&lt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">count</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">chars</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">[</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">index</span><span style="color: #ECEFF4">]</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">chars</span><span style="color: #ECEFF4">[</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">pos</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">+</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">]</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">break;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// character is the last in the charset; wrap around to the </span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// first character</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">[</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">index</span><span style="color: #ECEFF4">]</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">chars</span><span style="color: #ECEFF4">[</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">]</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// if we are at the beginning of the string, prepend the first </span></span>
<span class="line"><span style="color: #ECEFF4">        </span><span style="color: #616E88">// character and break</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">index</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">===</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #88C0D0">array_unshift</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">chars</span><span style="color: #ECEFF4">[</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">])</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">break;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">join</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">''</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">string</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>I like this algorithm because it can be easily customized in terms of the characters it uses in the keys. The value for <code>$charset</code> came from randomizing “abcdefghijklmnopqrstuvwxyz0123456789”. I did that so the keys look meaningless to the casual observer. If you change <code>$charset</code> to <code>'abc'</code> the sequence of keys looks like this: a b c aa ab ac ba bb bc ca cb cc aaa aab aac.… So by giving it a whole bunch of characters to choose from and randomizing them, you get seemingly meaningless and unique keys that are as short as possible. </p>



<p>I could have included capital letters and exponentially increased the key capacity, but mixed case keys look messy to me and I don’t need the capacity. Without them, I’m only waist deep in four character keys at the one million mark. I’m not building a SaaS product — this is just for personal use.</p>



<p>After writing this, I did some research on how this is generally done to see how far off I was. A base-62 encoder seems like the obvious path among the various implementations I found. Instead of <a href="https://mathspp.com/blog/base-conversion-in-python#converting-an-integer-to-any-base">math</a>, I used fairly efficient string manipulation to achieve a similar result.</p>



<p>With the fun part over, I whipped up storage for the last key generated and a mapping of keys to URLs. Again, simple as possible. Even SQLite would be massive overkill here. </p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">PHP</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>$last_file = 'last.txt'; // file to store the last incremented key
$urls_file = 'urls.txt'; // file to store the URLs with their keys

// Returns the contents of $last_file, or empty string if the file 
// does not exist
function get_last() {
    global $last_file;
    if (file_exists($last_file)) {
        return trim(file_get_contents($last_file));
    }
    return '';
}

// Writes to $last_file
function set_last($s) {
    global $last_file;
    file_put_contents($last_file, $s);
}

// Reads the last key, increments it, saves it, and returns it
function next_key() {
    $last = get_last();
    $next = increment_string($last);
    set_last($next);
    return $next;
}

// Adds a URL to urls.txt and returns the generated key
function add_url($url) {
    global $urls_file;
    $key = next_key();
    // append the key and url to urls.txt
    file_put_contents($urls_file, $key . ' ' . $url . "\n", FILE_APPEND);
    return $key;
}

// Looks up a key in urls.txt and returns the corresponding URL, 
// or null if not found
function get_url($key) {
    global $urls_file;
    $lines = file($urls_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        list($k, $url) = explode(' ', $line, 2);
        if ($k === $key) {
            return $url;
        }
    }
    return null;
}</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last_file</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">last.txt</span><span style="color: #ECEFF4">'</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// file to store the last incremented key</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">urls_file</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">urls.txt</span><span style="color: #ECEFF4">'</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// file to store the URLs with their keys</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Returns the contents of $last_file, or empty string if the file </span></span>
<span class="line"><span style="color: #616E88">// does not exist</span></span>
<span class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_last</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">global</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last_file</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">file_exists</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last_file</span><span style="color: #ECEFF4">))</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">trim</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">file_get_contents</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last_file</span><span style="color: #ECEFF4">))</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">''</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Writes to $last_file</span></span>
<span class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">set_last</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">s</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">global</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last_file</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">file_put_contents</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last_file</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">s</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Reads the last key, increments it, saves it, and returns it</span></span>
<span class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">next_key</span><span style="color: #ECEFF4">()</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_last</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">next</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">increment_string</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">last</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">set_last</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">next</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">next</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Adds a URL to urls.txt and returns the generated key</span></span>
<span class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">add_url</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">global</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">urls_file</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">next_key</span><span style="color: #ECEFF4">()</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// append the key and url to urls.txt</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">file_put_contents</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">urls_file</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">.</span><span style="color: #88C0D0"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C"> </span><span style="color: #ECEFF4">'</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">.</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">.</span><span style="color: #88C0D0"> </span><span style="color: #ECEFF4">"</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">FILE_APPEND</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Looks up a key in urls.txt and returns the corresponding URL, </span></span>
<span class="line"><span style="color: #616E88">// or null if not found</span></span>
<span class="line"><span style="color: #81A1C1">function</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_url</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">global</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">urls_file</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">lines</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">file</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">urls_file</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">FILE_IGNORE_NEW_LINES</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">|</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">FILE_SKIP_EMPTY_LINES</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">foreach</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">lines</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">as</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">line</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">list</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">k</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">explode</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C"> </span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">line</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #B48EAD">2</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">k</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">===</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">null;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>For security, I started by limiting the domains that can be shortened to a defined list. Then I decided I’d rather just have that be unlimited for my personal use and added a hard-coded password. You can use either or both of these options by tweaking the configuration at the top.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">PHP</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// list of allowed domains for URL shortening
$allowed_domains = []; 
// password to shorten a URL, leave empty to disable password protection
$password = 'open sesame'; 
</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// list of allowed domains for URL shortening</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">allowed_domains</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #616E88">// password to shorten a URL, leave empty to disable password protection</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">password</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">open sesame</span><span style="color: #ECEFF4">'</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"></span></code></pre></div>



<p>With all the necessary functions out of the way, I focused on the HTTP request handling to tie them all together. <code>get_input</code> just looks for a variable in either _GET or _POST.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">PHP</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>// read input parameters
$url = get_input('url');
$input_password = get_input('password');
$key = get_input('key');

// If a URL is provided, attempt to shorten it
if ($url) {
    // if a password is set, check it
    if ($password !== '' &amp;&amp; $input_password !== $password) {
        http_response_code(401); // Unauthorized
        echo "Unauthorized: Incorrect password.\n";
        exit;
    }
    // return 422 if the URL is longer than max_url_length
    if (strlen($url) &gt; $max_url_length) {
        http_response_code(422); // Unprocessable Entity
        echo "Invalid URL.\n";
	    exit;
    }
    // return 403 if the URL's domain is not in the allowed list
    $parsed_url = parse_url($url);
    if ($parsed_url === false || 
            !isset($parsed_url['host']) || 
            !test_host($parsed_url['host'])) {
        http_response_code(403);
        echo "Forbidden: Domain not allowed.\n";
        exit;
    }
    // validate the URL and shorten it
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        $key = add_url($url);
        $shortened_url = "https://$_SERVER[HTTP_HOST]/$key";
        echo "$shortened_url\n";
    } else {
        http_response_code(422); // Unprocessable Entity
        echo "Invalid URL.\n";
    }
    exit;
}

// Otherwise, assume the request URI is a key to look up
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$key = trim($path, '/');

// If the key is valid, look up the URL and redirect
if ($key !== '') {
    $url = get_url($key);
    if ($url !== null) {
        header("Location: $url", true, 301);
        exit;
    } else {
        http_response_code(404);
        echo "Not Found.\n";
        exit;
    }
}</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88">// read input parameters</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_input</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">url</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">input_password</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_input</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">password</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_input</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">key</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// If a URL is provided, attempt to shorten it</span></span>
<span class="line"><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// if a password is set, check it</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">password</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">!==</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">''</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&amp;&amp;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">input_password</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">!==</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">password</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">http_response_code</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">401</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// Unauthorized</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Unauthorized: Incorrect password.</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">exit;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// return 422 if the URL is longer than max_url_length</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">strlen</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">max_url_length</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">http_response_code</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">422</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// Unprocessable Entity</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Invalid URL.</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">	    </span><span style="color: #81A1C1">exit;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// return 403 if the URL's domain is not in the allowed list</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">parsed_url</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">parse_url</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">parsed_url</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">===</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">false</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">||</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">!isset</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">parsed_url</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">host</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">])</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">||</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">!</span><span style="color: #88C0D0">test_host</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">parsed_url</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">host</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">]))</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">http_response_code</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">403</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Forbidden: Domain not allowed.</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">exit;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">    </span><span style="color: #616E88">// validate the URL and shorten it</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">filter_var</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">FILTER_VALIDATE_URL</span><span style="color: #ECEFF4">))</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">add_url</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">shortened_url</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">https://</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">_SERVER</span><span style="color: #A3BE8C">[HTTP_HOST]/</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">shortened_url</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">http_response_code</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">422</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span><span style="color: #D8DEE9FF"> </span><span style="color: #616E88">// Unprocessable Entity</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Invalid URL.</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">exit;</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// Otherwise, assume the request URI is a key to look up</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">path</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">parse_url</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">_SERVER</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">REQUEST_URI</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">],</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">PHP_URL_PATH</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">trim</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">path</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">/</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #616E88">// If the key is valid, look up the URL and redirect</span></span>
<span class="line"><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">!==</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">''</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_url</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">key</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">!==</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">null</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">header</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Location: </span><span style="color: #81A1C1">$</span><span style="color: #D8DEE9">url</span><span style="color: #ECEFF4">"</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #81A1C1">true</span><span style="color: #ECEFF4">,</span><span style="color: #88C0D0"> </span><span style="color: #B48EAD">301</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">exit;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">else</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">http_response_code</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">404</span><span style="color: #ECEFF4">)</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">echo</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Not Found.</span><span style="color: #EBCB8B">\n</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">exit;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"><span style="color: #ECEFF4">}</span></span></code></pre></div>



<p>And finally, the bare minimum HTML to make it usable.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.75rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">HTML</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Bagaag.com Micro URL Shortener&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;h1&gt;Bagaag.com Micro URL Shortener&lt;/h1&gt;
    &lt;form method="post" action=""&gt;
        &lt;label for="url"&gt;Enter URL to shorten:&lt;/label&gt;&lt;br&gt;
        &lt;input type="text" id="url" name="url" size="50" required&gt;&lt;br&gt;&lt;br&gt;
        &lt;?php if ($password !== ''): ?&gt;
            &lt;label for="password"&gt;Enter the password:&lt;/label&gt;&lt;br&gt;
            &lt;input type="password" id="password" name="password" size="20"   
              required&gt;&lt;br&gt;&lt;br&gt;
        &lt;?php endif; ?&gt;
        &lt;input type="submit" value="Shorten"&gt;
    &lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #81A1C1">&lt;!DOCTYPE</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">html</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;html</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">lang</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">en</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;head&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;meta</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">charset</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">UTF-8</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;meta</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">viewport</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">content</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">width=device-width, initial-scale=1.0</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;title&gt;</span><span style="color: #D8DEE9FF">Bagaag.com Micro URL Shortener</span><span style="color: #81A1C1">&lt;/title&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/head&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;body&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;h1&gt;</span><span style="color: #D8DEE9FF">Bagaag.com Micro URL Shortener</span><span style="color: #81A1C1">&lt;/h1&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;form</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">method</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">post</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">action</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">""</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;label</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">for</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">url</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">Enter URL to shorten:</span><span style="color: #81A1C1">&lt;/label&gt;&lt;br&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;input</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">text</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">url</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">url</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">size</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">50</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">required</span><span style="color: #81A1C1">&gt;&lt;br&gt;&lt;br&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">&lt;</span><span style="color: #D8DEE9FF">?php if ($password !== ''): ?&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;label</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">for</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">password</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF">Enter the password:</span><span style="color: #81A1C1">&lt;/label&gt;&lt;br&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">&lt;input</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">password</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">id</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">password</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">name</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">password</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">size</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">20</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF">   </span></span>
<span class="line"><span style="color: #D8DEE9FF">              </span><span style="color: #8FBCBB">required</span><span style="color: #81A1C1">&gt;&lt;br&gt;&lt;br&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #D8DEE9">&lt;</span><span style="color: #D8DEE9FF">?php endif; ?&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">&lt;input</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">type</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">submit</span><span style="color: #ECEFF4">"</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">value</span><span style="color: #ECEFF4">=</span><span style="color: #ECEFF4">"</span><span style="color: #A3BE8C">Shorten</span><span style="color: #ECEFF4">"</span><span style="color: #81A1C1">&gt;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">&lt;/form&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/body&gt;</span></span>
<span class="line"><span style="color: #81A1C1">&lt;/html&gt;</span></span></code></pre></div>



<p>Anyway, I had fun with this. It’s up and running for me at r.bagaag.com. Here is a shortened link to see it working: <a href="https://r.bagaag.com/j">https://r.bagaag.com/j</a></p>



<p><a href="https://git.bagaag.com/matt/shortener/src/branch/main/index.php" rel="nofollow">Here is the full source</a>.</p>



<p>To run it yourself, just host it on a domain of your choosing, like r.yourdomain.com. Make sure PHP has write access to the files named at the top. I recommend placing the text files outside the site root, or configure your web server to block requests to them.</p>



<p>If you just want to test it locally, use the terminal to run it like this from the directory that contains index.php:</p>



<pre class="wp-block-code"><code>$ php -S localhost:3000</code></pre>



<p>Then browse to http://localhost:3000. You’ll need to change “https” to “http” in the script if you’re not going to use HTTPS.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bagaag.com/single-file-url-shortener/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
					<title>Midi Sequencer in Python</title>
					<link>https://www.bagaag.com/midi-sequencer-in-python/</link>
					<comments>https://www.bagaag.com/midi-sequencer-in-python/#respond</comments>
		
		<dc:creator><![CDATA[matt]]></dc:creator>
		<pubDate>Wed, 17 Sep 2025 03:03:15 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[midi]]></category>
		<category><![CDATA[python]]></category>
		<guid isPermaLink="false">https://www.bagaag.com/?p=70</guid>

					<description><![CDATA[I’ve always thought it would be cool to be able to compose music in code. There are some languages out there that do this, but I was more interested in direct midi programming than in a DSL for composing music. Enter&#160;mido. mido&#160;is a midi library for Python that wraps the lower-level midi library&#160;python-rtmidi&#160;in a friendlier [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I’ve always thought it would be cool to be able to compose music in code. There are some languages out there that do this, but I was more interested in direct midi programming than in a DSL for composing music. Enter&nbsp;<code>mido</code>.</p>



<span id="more-70"></span>



<p><a href="https://github.com/mido/mido">mido</a>&nbsp;is a midi library for Python that wraps the lower-level midi library&nbsp;<a href="https://pypi.org/project/python-rtmidi/">python-rtmidi</a>&nbsp;in a friendlier API. It can do a lot of things, but I was primarily interested in using code to control a synth in Bitwig Studio. I ended up writing a little step sequencer API. I used to love playing with <a href="https://www.bagaag.com/wp-content/uploads/2025/09/seq303.jpg" target="_blank" rel="noreferrer noopener">Seq 303</a> back in the 90s, and that’s sort of what I had in mind in terms of how to model midi notes into a song. </p>



<p>I started by defining a Note class.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">Python</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly># note.py
import mido

class Note:

    # http://bradthemad.org/guitar/tempo_explanation.php
    LENGTHS = {
        'w': 240,
        'h': 120,
        'q': 60,
        'e': 30,
        's': 15,
        't': 7.5,
        'dq': 90,
        'de': 45,
        'ds': 22.5,
        'tq': 40,
        'te': 20,
        'ts': 10
    }

    # what is the equivalent of '1' numeric duration value
    DURATION_MULTIPLIER = LENGTHS['q']

    def __init__(self, value:int, duration, multiplier=1, velocity:int=64):
        self.value = value
        self.duration = duration
        self.multiplier = multiplier
        self.velocity = velocity

    # Converts a note length value like dotted eighth to a value 
    # in seconds based on default or provided bpm tempo. 
    # Length abbrs: w, h, q, e, s, t, dq, de, ds, tq, te, ts 
    # (whole/half/quarter/eight/sixteenth/32nd, dotted/triplet)
    def seconds(self, tempo:int):
        if  type(self.duration) == str:
            if self.duration in Note.LENGTHS:
                len = Note.LENGTHS[self.duration]
                return len * self.multiplier / tempo
            else:
                return 0
        else:
            return self.duration * Note.DURATION_MULTIPLIER / tempo
        
    # Returns mido "note_on" message
    def get_on(self):
        print(f"On {self.value} {self.duration}")
        return mido.Message('note_on', note=self.value, 
          velocity=self.velocity)

    # Returns mido "note_off" message
    def get_off(self):
        print(f"Off {self.value} {self.duration}")
        return mido.Message('note_off', note=self.value)</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88"># note.py</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> mido</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Note</span><span style="color: #ECEFF4">:</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># http://bradthemad.org/guitar/tempo_explanation.php</span></span>
<span class="line"><span style="color: #D8DEE9FF">    LENGTHS </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">{</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">w</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">240</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">h</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">120</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">q</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">60</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">e</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">30</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">s</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">15</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">t</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">7.5</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">dq</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">90</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">de</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">45</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">ds</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">22.5</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">tq</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">40</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">te</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">20</span><span style="color: #ECEFF4">,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">ts</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">10</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #ECEFF4">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># what is the equivalent of '1' numeric duration value</span></span>
<span class="line"><span style="color: #D8DEE9FF">    DURATION_MULTIPLIER </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> LENGTHS</span><span style="color: #ECEFF4">[</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">q</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">__init__</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #ECEFF4">:</span><span style="color: #88C0D0">int</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">duration</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">multiplier</span><span style="color: #81A1C1">=</span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">velocity</span><span style="color: #ECEFF4">:</span><span style="color: #88C0D0">int</span><span style="color: #81A1C1">=</span><span style="color: #B48EAD">64</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> value</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">duration </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> duration</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">multiplier </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> multiplier</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">velocity </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> velocity</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># Converts a note length value like dotted eighth to a value </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># in seconds based on default or provided bpm tempo. </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># Length abbrs: w, h, q, e, s, t, dq, de, ds, tq, te, ts </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># (whole/half/quarter/eight/sixteenth/32nd, dotted/triplet)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">seconds</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">tempo</span><span style="color: #ECEFF4">:</span><span style="color: #88C0D0">int</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF">  </span><span style="color: #88C0D0">type</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">duration</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">str</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">duration </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> Note</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">LENGTHS</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #88C0D0">len</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> Note</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">LENGTHS</span><span style="color: #ECEFF4">[</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">duration</span><span style="color: #ECEFF4">]</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">len</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">multiplier </span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9FF"> tempo</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">else</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">else</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">duration </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> Note</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">DURATION_MULTIPLIER </span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9FF"> tempo</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># Returns mido "note_on" message</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_on</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">"On </span><span style="color: #EBCB8B">{</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> </span><span style="color: #EBCB8B">{</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">duration</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">"</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> mido</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">Message</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">note_on</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">note</span><span style="color: #81A1C1">=self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">          </span><span style="color: #D8DEE9">velocity</span><span style="color: #81A1C1">=self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">velocity</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #616E88"># Returns mido "note_off" message</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">get_off</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">"Off </span><span style="color: #EBCB8B">{</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C"> </span><span style="color: #EBCB8B">{</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">duration</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">"</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> mido</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">Message</span><span style="color: #ECEFF4">(</span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">note_off</span><span style="color: #ECEFF4">'</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">note</span><span style="color: #81A1C1">=self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #ECEFF4">)</span></span></code></pre></div>



<p>And then a basic sequencer to play them.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">Python</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly># sequencer.py
import concurrent
from concurrent.futures import ThreadPoolExecutor
import mido
from threading import Thread
import time

from note import Note

class Step: 
    def __init__(self, value:int, duration:int=1, start_tick:int=0):
        self.enabled = True
        self.start_tick = start_tick
        self.adjust_duration_for_start_tick = True
        self.note = Note(value, duration)

class Sequencer:
    def __init__(self): 
        self.tick_count = 16
        self.tempo = 100
        self.step_length = 60
        self.step_seconds = self.step_length / self.tempo
        # an array of steps played in sequence,
        # nested arrays are chords
        self.steps = []
        # this is system specific
        port_id = 'Virtual Raw MIDI 0-0:VirMIDI 0-0 16:0'
        self.port = mido.open_output(port_id)

    def play(self):
        # using a thread for each note feels heavy-handed,
        # an interesting optimization problem
        with ThreadPoolExecutor(max_workers=5) as executor:
            for ix in range(len(self.steps)):
                step = self.steps[ix]
                if type(step) == Step and step.enabled:
                    print(f"Step {ix+1}: {step.note.value}")
                    self.play_step(executor, step)
                elif type(step) == list:
                    print(f"Step {ix+1}: {self.chord_str(step)}")
                    for substep in step:
                        self.play_step(executor, substep)
                time.sleep(self.step_seconds)

    def play_step(self, executor, step):
        future = executor.submit(note_player, self, step)
        future.add_done_callback(note_finished)

    def chord_str(self, steps):
        return [step.note.value for step in steps]

def note_player(seq:Sequencer, step:Step):
    applied_length = seq.step_length / seq.tempo
    delay_seconds = step.start_tick * ( applied_length ) / seq.tick_count
    note_seconds = step.note.seconds(seq.tempo)
    if delay_seconds &gt; 0:
        time.sleep(delay_seconds)
        if step.adjust_duration_for_start_tick:
            note_seconds = note_seconds - delay_seconds
    seq.port.send(step.note.get_on());
    time.sleep(note_seconds)
    seq.port.send(step.note.get_off());
    return step

def note_finished(future:concurrent.futures.Future):
    print(f"Finished: {future.result().note.value}")
</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88"># sequencer.py</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> concurrent</span></span>
<span class="line"><span style="color: #81A1C1">from</span><span style="color: #D8DEE9FF"> concurrent</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">futures </span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> ThreadPoolExecutor</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> mido</span></span>
<span class="line"><span style="color: #81A1C1">from</span><span style="color: #D8DEE9FF"> threading </span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> Thread</span></span>
<span class="line"><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> time</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">from</span><span style="color: #D8DEE9FF"> note </span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> Note</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Step</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">__init__</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">value</span><span style="color: #ECEFF4">:</span><span style="color: #88C0D0">int</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">duration</span><span style="color: #ECEFF4">:</span><span style="color: #88C0D0">int</span><span style="color: #81A1C1">=</span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">start_tick</span><span style="color: #ECEFF4">:</span><span style="color: #88C0D0">int</span><span style="color: #81A1C1">=</span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">enabled </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">True</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">start_tick </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> start_tick</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">adjust_duration_for_start_tick </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">True</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">note </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Note</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">value</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> duration</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">class</span><span style="color: #D8DEE9FF"> </span><span style="color: #8FBCBB">Sequencer</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">__init__</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">):</span><span style="color: #D8DEE9FF"> </span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">tick_count </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">16</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">tempo </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">100</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">step_length </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">60</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">step_seconds </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">step_length </span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">tempo</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #616E88"># an array of steps played in sequence,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #616E88"># nested arrays are chords</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">steps </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[]</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #616E88"># this is system specific</span></span>
<span class="line"><span style="color: #D8DEE9FF">        port_id </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">'</span><span style="color: #A3BE8C">Virtual Raw MIDI 0-0:VirMIDI 0-0 16:0</span><span style="color: #ECEFF4">'</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">port </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> mido</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">open_output</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">port_id</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">play</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #616E88"># using a thread for each note feels heavy-handed,</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #616E88"># an interesting optimization problem</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">with</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">ThreadPoolExecutor</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">max_workers</span><span style="color: #81A1C1">=</span><span style="color: #B48EAD">5</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">as</span><span style="color: #D8DEE9FF"> executor</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">            </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> ix </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">range</span><span style="color: #ECEFF4">(</span><span style="color: #88C0D0">len</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">steps</span><span style="color: #ECEFF4">)):</span></span>
<span class="line"><span style="color: #D8DEE9FF">                step </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">steps</span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">ix</span><span style="color: #ECEFF4">]</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">type</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">step</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> Step </span><span style="color: #81A1C1">and</span><span style="color: #D8DEE9FF"> step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">enabled</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">"Step </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">ix</span><span style="color: #81A1C1">+</span><span style="color: #B48EAD">1</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">: </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">note</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">"</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">play_step</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">executor</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                </span><span style="color: #81A1C1">elif</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">type</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">step</span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">==</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">list</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">"Step </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">ix</span><span style="color: #81A1C1">+</span><span style="color: #B48EAD">1</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">: </span><span style="color: #EBCB8B">{</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">chord_str</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">step</span><span style="color: #ECEFF4">)</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">"</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                    </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> substep </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> step</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">                        </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">play_step</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">executor</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> substep</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">                time</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">sleep</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">step_seconds</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">play_step</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">executor</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">step</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        future </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> executor</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">submit</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">note_player</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        future</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">add_done_callback</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">note_finished</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">chord_str</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">self</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">steps</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">note</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value </span><span style="color: #81A1C1">for</span><span style="color: #D8DEE9FF"> step </span><span style="color: #81A1C1">in</span><span style="color: #D8DEE9FF"> steps</span><span style="color: #ECEFF4">]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">note_player</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">seq</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF">Sequencer</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #D8DEE9">step</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF">Step</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">    applied_length </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> seq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">step_length </span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9FF"> seq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">tempo</span></span>
<span class="line"><span style="color: #D8DEE9FF">    delay_seconds </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">start_tick </span><span style="color: #81A1C1">*</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF"> applied_length </span><span style="color: #ECEFF4">)</span><span style="color: #D8DEE9FF"> </span><span style="color: #81A1C1">/</span><span style="color: #D8DEE9FF"> seq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">tick_count</span></span>
<span class="line"><span style="color: #D8DEE9FF">    note_seconds </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">note</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">seconds</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">seq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">tempo</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> delay_seconds </span><span style="color: #81A1C1">&gt;</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">0</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">        time</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">sleep</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">delay_seconds</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">        </span><span style="color: #81A1C1">if</span><span style="color: #D8DEE9FF"> step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">adjust_duration_for_start_tick</span><span style="color: #ECEFF4">:</span></span>
<span class="line"><span style="color: #D8DEE9FF">            note_seconds </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> note_seconds </span><span style="color: #81A1C1">-</span><span style="color: #D8DEE9FF"> delay_seconds</span></span>
<span class="line"><span style="color: #D8DEE9FF">    seq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">port</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">send</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">note</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get_on</span><span style="color: #ECEFF4">())</span><span style="color: #D8DEE9">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    time</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">sleep</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">note_seconds</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">    seq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">port</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">send</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9FF">step</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">note</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">get_off</span><span style="color: #ECEFF4">())</span><span style="color: #D8DEE9">;</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #81A1C1">return</span><span style="color: #D8DEE9FF"> step</span></span>
<span class="line"></span>
<span class="line"><span style="color: #81A1C1">def</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">note_finished</span><span style="color: #ECEFF4">(</span><span style="color: #D8DEE9">future</span><span style="color: #ECEFF4">:</span><span style="color: #D8DEE9FF">concurrent</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">futures</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">Future</span><span style="color: #ECEFF4">):</span></span>
<span class="line"><span style="color: #D8DEE9FF">    </span><span style="color: #88C0D0">print</span><span style="color: #ECEFF4">(</span><span style="color: #81A1C1">f</span><span style="color: #A3BE8C">"Finished: </span><span style="color: #EBCB8B">{</span><span style="color: #D8DEE9FF">future</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">result</span><span style="color: #ECEFF4">().</span><span style="color: #D8DEE9FF">note</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">value</span><span style="color: #EBCB8B">}</span><span style="color: #A3BE8C">"</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span></code></pre></div>



<p>And finally, a little celebratory tune.</p>



<div class="wp-block-kevinbatdorf-code-block-pro" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#39404f;color:#c8d0e0">Python</span><span role="button" tabindex="0" style="color:#d8dee9ff;display:none" aria-label="Copy" class="code-block-pro-copy-button"><pre class="code-block-pro-copy-button-pre" aria-hidden="true"><textarea class="code-block-pro-copy-button-textarea" tabindex="-1" aria-hidden="true" readonly># tada.py
from sequencer import Sequencer, Step

step1 = Step(60, 1)
step2 = Step(64, 1)
step3 = Step(67, 1)
step4 = Step(72, 1)

step1a = Step(60, 4, 1)
step2a = Step(64, 4, 2)
step3a = Step(67, 4, 3)
step4a = Step(72, 4, 4)

steps = [step1, step2, step3, step4 ]
steps.append([step1a, step2a, step3a, step4a])

seq = Sequencer()
seq.steps = steps
seq.play()</textarea></pre><svg style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" /><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg></span><pre class="shiki nord" style="background-color: #2e3440ff" tabindex="0"><code><span class="line"><span style="color: #616E88"># tada.py</span></span>
<span class="line"><span style="color: #81A1C1">from</span><span style="color: #D8DEE9FF"> sequencer </span><span style="color: #81A1C1">import</span><span style="color: #D8DEE9FF"> Sequencer</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> Step</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">step1 </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">60</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">step2 </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">64</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">step3 </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">67</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">step4 </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">72</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">step1a </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">60</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">4</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">1</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">step2a </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">64</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">4</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">2</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">step3a </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">67</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">4</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">3</span><span style="color: #ECEFF4">)</span></span>
<span class="line"><span style="color: #D8DEE9FF">step4a </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Step</span><span style="color: #ECEFF4">(</span><span style="color: #B48EAD">72</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">4</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> </span><span style="color: #B48EAD">4</span><span style="color: #ECEFF4">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">steps </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #ECEFF4">[</span><span style="color: #D8DEE9FF">step1</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step2</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step3</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step4 </span><span style="color: #ECEFF4">]</span></span>
<span class="line"><span style="color: #D8DEE9FF">steps</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">append</span><span style="color: #ECEFF4">([</span><span style="color: #D8DEE9FF">step1a</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step2a</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step3a</span><span style="color: #ECEFF4">,</span><span style="color: #D8DEE9FF"> step4a</span><span style="color: #ECEFF4">])</span></span>
<span class="line"></span>
<span class="line"><span style="color: #D8DEE9FF">seq </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> </span><span style="color: #88C0D0">Sequencer</span><span style="color: #ECEFF4">()</span></span>
<span class="line"><span style="color: #D8DEE9FF">seq</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9FF">steps </span><span style="color: #81A1C1">=</span><span style="color: #D8DEE9FF"> steps</span></span>
<span class="line"><span style="color: #D8DEE9FF">seq</span><span style="color: #ECEFF4">.</span><span style="color: #88C0D0">play</span><span style="color: #ECEFF4">()</span></span></code></pre></div>



<p>Here’s the output of <code>python tada.py</code>.</p>



<pre class="wp-block-code"><code>Step 1: 60
On 60 1
Off 60 1
Finished: 60
Step 2: 64
On 64 1
Step 3: 67
Off 64 1
Finished: 64
On 67 1
Step 4: 72
On 72 1
Off 67 1
Finished: 67
Step 5: [60, 64, 67, 72]
Off 72 1
Finished: 72
On 60 4
On 64 4
On 67 4
On 72 4
Off 60 4
Finished: 60
Off 64 4
Finished: 64
Off 67 4
Finished: 67
Off 72 4
Finished: 72</code></pre>



<p>And here’s what it sounds like running through the Bitwig Organ.</p>



<figure class="wp-block-audio"><audio controls src="https://www.bagaag.com/wp-content/uploads/2025/06/tada.mp3"></audio></figure>



<p>If you want to run this locally, you’ll need a midi device to convert the midi notes into sound. For me, that is Bitwig Studio. It might just work using the built-in software synth on Windows, but I haven’t tried it.</p>



<p>You’ll also need to install some python packages. Here’s the <code>requirements.txt</code>.</p>



<pre class="wp-block-code"><code>mido
python-rtmidi
importlib_metadata</code></pre>



<p>And here are shell commands to create a virtual environment, install these requirements, and run the program (I’m using bash).</p>



<pre class="wp-block-code"><code>mkdir midosequencer
cd midosequencer
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python song.py</code></pre>



<p>This was a fun little project. It was interesting to realize how conceptual music time has to be converted into literal time, down to when each note should stop and start, calculated from tempo and note length — something humans do without thinking about it. It also makes me curious how real sequencers like Bitwig handle concurrency and timing.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bagaag.com/midi-sequencer-in-python/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://www.bagaag.com/wp-content/uploads/2025/06/tada.mp3" length="29615" type="audio/mpeg" />

			</item>
		<item>
					<title>Code Painting</title>
					<link>https://www.bagaag.com/code-painting/</link>
					<comments>https://www.bagaag.com/code-painting/#respond</comments>
		
		<dc:creator><![CDATA[matt]]></dc:creator>
		<pubDate>Tue, 08 Jul 2025 04:42:00 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://www.bagaag.com/?p=76</guid>

					<description><![CDATA[My favorite stage of a programming project is the initial creative act, figuring out what a piece of software should do, how it should look, and how it should be written. I often refactor wildly during this phase. For example, I completely scrapped the initial Python-based front end I had created for this project in [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>My favorite stage of a programming project is the initial creative act, figuring out what a piece of software should do, how it should look, and how it should be written. I often refactor wildly during this phase. For example, I completely scrapped the initial Python-based front end I had created for this project in favor of Tiny File Manager, a full featured file management UI in a single PHP file. It was fun getting to know that code and build a plugin system for it so I could add screens and navigation items without mucking too much with the source. But, having used it a bit, now I’m thinking a tree-based IDE-style UI will work better. Every good CMS has a content tree. So I’m going to mock up something for that in HTML/CSS/JS and see what works. This is why I code for fun on my own time: you can’t do this when you’re getting paid. Maybe something great will come of it, maybe not. Either way, I had a lot of fun along the way and learned some things, too. This is is what I refer to as code painting.</p>



<p>This topic reminds of my favorite poem by Frank O’Hara:&nbsp;<a href="https://poets.org/poem/why-i-am-not-painter">Why I Am Not a Painter</a>.</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bagaag.com/code-painting/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
					<title></title>
					<link>https://www.bagaag.com/python-container-non-root/</link>
					<comments>https://www.bagaag.com/python-container-non-root/#respond</comments>
		
		<dc:creator><![CDATA[matt]]></dc:creator>
		<pubDate>Tue, 01 Jul 2025 04:39:00 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<guid isPermaLink="false">https://www.bagaag.com/?p=74</guid>

					<description><![CDATA[Thank you to Florian Dahlitz for getting me over the hump on&#160;configuring a Python app in a container as a non-root user. My python app container now creates files in the volume mapped to my local drive as my local user rather than as root.]]></description>
										<content:encoded><![CDATA[
<p>Thank you to Florian Dahlitz for getting me over the hump on&nbsp;<a href="https://medium.com/@DahlitzF/run-python-applications-as-non-root-user-in-docker-containers-by-example-cba46a0ff384">configuring a Python app in a container as a non-root user</a>. My python app container now creates files in the volume mapped to my local drive as my local user rather than as root.</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.bagaag.com/python-container-non-root/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
