<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel>
  <title>Shamil Aminov</title>
  <link>https://aminov.sh/en</link>
  <description>A blog, projects and favorites</description>
  <language>en</language>
  <item>
    <title>PhoneMic</title>
    <link>https://aminov.sh/en/blog/phonemic</link>
    <guid isPermaLink="true">https://aminov.sh/en/blog/phonemic</guid>
    <pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate>
    <description>&lt;p&gt;PhoneMic turns an Android phone into a microphone for Windows. The phone
listens, the PC receives the sound over Wi-Fi or a USB cable, and Discord, Zoom,
OBS and every other program see it as an ordinary microphone. You pair once, by
QR code, and from then on it is one tap on the phone.&lt;/p&gt;
&lt;p&gt;Apps that do this already exist, and they tend to fail in the same two ways: the
connection drops after a while, and getting it going is fiddly. PhoneMic is
built around doing neither.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/phonemic-apps-en.webp&quot; alt=&quot;The phone and the PC while streaming: the same wave on both sides&quot; width=&quot;809&quot; height=&quot;811&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The wave is the switch&lt;/h2&gt;
&lt;p&gt;There is almost nothing on the phone screen. In the middle is a wave, and it is
also the switch: tap it and the microphone is on, tap again and it is off. On the
phone the wave swells with your voice; in the PC window it is drawn from the
audio that actually arrived, so it shows that the link is alive, not just that
the phone hears something.&lt;/p&gt;
&lt;p&gt;The dot above the wave says what is going on: red when the microphone is off,
amber while the phone looks for the PC, cyan when you are live. At the bottom is
an island with the paired PC, the link and its latency; it also holds the gear
for noise suppression and the QR button to pair a different PC.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/phonemic-states-en.webp&quot; alt=&quot;Off, connecting, live: the wave grows from a dim thread into a ribbon&quot; width=&quot;1334&quot; height=&quot;892&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The wave is not an animation from a file but one twisted ribbon of thirty-four
thin lines. Each line mixes two wave shapes, &lt;code&gt;cos θ · f(x) + sin θ · g(x)&lt;/code&gt;, with
θ stepping across the bundle. Where both shapes cross zero the lines pinch into
a knot; elsewhere they fan out, and that is where the depth comes from. It is
all drawn twice, additively: wide and faint for the glow, then thin and bright.
Off, the ribbon collapses into a dim thread; while connecting, it breathes.&lt;/p&gt;
&lt;p&gt;Both apps are true black, which on an OLED screen costs no light at all.&lt;/p&gt;
&lt;h2&gt;Two roads&lt;/h2&gt;
&lt;p&gt;Over Wi-Fi the sound travels as UDP, in 10 ms packets, one packet per datagram.
Over USB it is TCP through &lt;code&gt;adb reverse&lt;/code&gt;, and the phone dials &lt;code&gt;127.0.0.1&lt;/code&gt;. The
cable needs nothing but USB debugging: the PC app sets up the port for every
connected phone by itself, and repairs it if some other program restarts the adb
server.&lt;/p&gt;
&lt;p&gt;The phone always tries the cable first. Pull the cable and the stream moves to
Wi-Fi at once. Plug it back in and a few seconds later, once the cable has
clearly stayed put, it returns to USB. The microphone never switches off in
between.&lt;/p&gt;
&lt;p&gt;On the network the phone looks for the PC at every address the QR code carried,
and if none answers, with a broadcast hello. A new IP from the router does not
break pairing.&lt;/p&gt;
&lt;p&gt;So that the link does not die with the screen, recording runs in a foreground
service with wake and Wi-Fi locks: Android can neither mute the app nor let the
radio doze. If the link drops anyway, both sides bring it back by themselves.
Three seconds of silence from the other side count as a drop, and the phone goes
back to saying hello until it gets through.&lt;/p&gt;
&lt;p&gt;Over the cable latency is about 30 ms, over Wi-Fi 50–100.&lt;/p&gt;
&lt;h2&gt;The buffer&lt;/h2&gt;
&lt;p&gt;Wi-Fi delivers packets unevenly, and all of PhoneMic&amp;#39;s difficulty lives in the
buffer on the PC that hides it.&lt;/p&gt;
&lt;p&gt;Every packet carries a sequence number, so it is known when it would have arrived
on a perfect network. The earliest recent arrival is the baseline; lateness is
how far behind it a packet lands. The baseline creeps upward slowly, half a
millisecond per second, so a phone whose clock runs a little slower than the
PC&amp;#39;s is not mistaken for growing jitter.&lt;/p&gt;
&lt;p&gt;The buffer remembers the worst lateness for a long time, because Wi-Fi hiccups
come in waves, but with a cap. A half-second stall, such as a Wi-Fi scan, cannot
be buffered away without making every call lag, so it is allowed to glitch once.&lt;/p&gt;
&lt;p&gt;The gap between how much audio the buffer holds and how much it needs is closed
where nobody can hear it. Pauses in speech do most of the work: with too much
audio, quiet 5 ms stretches are dropped; with too little, a pause is stretched by
5 ms. A signal without pauses is left with playback speed, which the buffer
nudges by at most 1%. If the audio does run out, it fades rather than clicking
into silence.&lt;/p&gt;
&lt;p&gt;There are three modes: &lt;em&gt;Low latency&lt;/em&gt;, 20–40 ms, for the cable or excellent
Wi-Fi; &lt;em&gt;Balanced&lt;/em&gt;, the default, usually 50–100; and &lt;em&gt;Stable&lt;/em&gt;, 100–150, for a
crowded or weak network.&lt;/p&gt;
&lt;h2&gt;Checking without a voice&lt;/h2&gt;
&lt;p&gt;The protocol is written down together with the exact bytes of reference
packets, and both sides, Kotlin and C#, check those very bytes in their tests.
Change the format on one side and the tests on the other fail.&lt;/p&gt;
&lt;p&gt;The buffer runs on a virtual clock against a simulated network: ten minutes of
bad Wi-Fi with drifting clocks pass in a few seconds. Checking the whole chain
needs nobody to speak: in debug mode the phone plays a 1 kHz tone, and &lt;code&gt;probe&lt;/code&gt;
records what comes out of the virtual cable and reports frequency, level and
dropouts.&lt;/p&gt;
&lt;h2&gt;Get it&lt;/h2&gt;
&lt;p&gt;Windows cannot turn a program&amp;#39;s sound into a microphone without a driver, so
PhoneMic plays into VB-Audio Virtual Cable, a free virtual audio device. Its
other end, CABLE Output, is the microphone you pick in Discord, Zoom or OBS.&lt;/p&gt;
&lt;p&gt;The app &lt;a href=&quot;https://github.com/shamil-aminov/PhoneMic&quot;&gt;is on GitHub&lt;/a&gt; under MIT,
with builds in the &lt;a href=&quot;https://github.com/shamil-aminov/PhoneMic/releases/latest&quot;&gt;latest release&lt;/a&gt;:
the &lt;code&gt;.apk&lt;/code&gt; for the phone and the &lt;code&gt;.exe&lt;/code&gt; for the PC. It needs Windows 10 or 11
with &lt;a href=&quot;https://vb-audio.com/Cable/&quot;&gt;VB-Audio Virtual Cable&lt;/a&gt;, and Android 8.0 with
Google Play services for the built-in QR scanner. For Wi-Fi the phone and the PC
have to be on the same network; for the cable, USB debugging on the phone and
&lt;code&gt;adb&lt;/code&gt; on the PC.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/phonemic-pairing-en.webp&quot; alt=&quot;First launch: the phone asks to scan a QR code, the PC app shows it&quot; width=&quot;846&quot; height=&quot;692&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;No account, no cloud, no analytics: the sound goes straight from the phone to
the PC. There is no encryption — the code in the QR keeps other devices from
wandering into your microphone by accident, but does not protect against
someone listening on the network on purpose. On a network you do not trust, use
the cable.&lt;/p&gt;
</description>
  </item>
  <item>
    <title>Alternate</title>
    <link>https://aminov.sh/en/blog/alternate</link>
    <guid isPermaLink="true">https://aminov.sh/en/blog/alternate</guid>
    <pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate>
    <description>&lt;p&gt;Alternate turns an Android phone into a USB MIDI keypad for osu!. The whole
screen is one button, and every tap fires one of two notes, alternating. A
single finger produces the alternating key presses a stream needs, at speeds no
single key can reach.&lt;/p&gt;
&lt;p&gt;That is the entire idea, and the app does nothing else.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/alternate-pad.webp&quot; alt=&quot;The pad in use: chain, tempo, total taps and the gauge on the right&quot; width=&quot;1600&quot; height=&quot;686&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Why MIDI&lt;/h2&gt;
&lt;p&gt;Because osu!lazer speaks it natively. No bridge to install, no driver, no
key-mapping utility in the middle: the phone appears as a MIDI device and the
game reads it as a keyboard. One cable, one setting in the game.&lt;/p&gt;
&lt;p&gt;The flip side of that decision is iOS. An iPhone cannot act as a USB MIDI device
at all; what is left is Network MIDI over Wi-Fi and Bluetooth LE MIDI, and both
add latency and jitter a 200 BPM stream will feel in your hand. The logic would
port in an evening. The one thing that makes the app worth using would not.&lt;/p&gt;
&lt;h2&gt;One number&lt;/h2&gt;
&lt;p&gt;Everything inside is built around the time from glass to wire.&lt;/p&gt;
&lt;p&gt;The MIDI note goes out first: the engine decides the note and the byte is on the
wire before any animation state is touched. The tempo maths runs afterwards, in
the frame loop. Nothing allocates on the hot path — engine state is primitive
arrays, the message buffer is allocated once, particle pools are ring buffers.
No garbage means no collection pause mid-stream. Sending is synchronous on the
touch thread, because handing the write to another thread would add scheduling
to the latency and give nothing back.&lt;/p&gt;
&lt;p&gt;Intervals are measured from the input event&amp;#39;s own clock rather than the
handler&amp;#39;s: a busy UI thread must not skew the tempo. On Android 13+ touch
dispatch is unbuffered, which saves up to a frame. The fastest display mode is
requested at startup — more frames per second also means the touch sensor is
polled more often.&lt;/p&gt;
&lt;p&gt;There are exactly as many settings as it takes to line the pad up with the game and
with a finger. A 12 ms debounce swallows a finger rolling off the glass and eats
nothing real: alternate tapping at twenty taps a second is a 50 ms period.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/alternate-settings.webp&quot; alt=&quot;The settings screen: debounce, chain window, notes&quot; width=&quot;1600&quot; height=&quot;686&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;h2&gt;The gauge&lt;/h2&gt;
&lt;p&gt;The pad shows three numbers: the current chain, the tempo and the taps this
session. The tempo also appears as a column of blocks, and that gauge is
deliberately not linear.&lt;/p&gt;
&lt;p&gt;Nobody streams below 100, so a linear 0–260 bar would waste its bottom third and
give a few pixels to the stretch that actually decides whether you pass a map.
Each zone gets a share of the bar sized by how much it matters: warm-up to 140
takes the first third, working tempo to 200 the second, everything above splits
the rest. 220 sits near the top, which is what &amp;quot;over the limit&amp;quot; should look
like.&lt;/p&gt;
&lt;p&gt;The gauge is one colour. Height already carries the whole message, and a colour
ramp on top of it was a second voice saying the same thing.&lt;/p&gt;
&lt;h2&gt;Take it&lt;/h2&gt;
&lt;p&gt;The app is &lt;a href=&quot;https://github.com/shamil-aminov/alternate&quot;&gt;on GitHub&lt;/a&gt; under MIT. You need Android 7.0 with USB MIDI support, a
data cable — not a charge-only one — and osu!lazer on the computer. Without a
cable the pad still opens: the START button reads DEMO instead, the MIDI goes
nowhere, and you can try the feel before finding one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/alternate-connect.webp&quot; alt=&quot;The first screen: no device found, the button reads DEMO&quot; width=&quot;1600&quot; height=&quot;686&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The engine, the tempo maths and the gauge live in a module with no Android
imports at all. That is what lets the interesting part be tested without a
phone.&lt;/p&gt;
</description>
  </item>
  <item>
    <title>Alm</title>
    <link>https://aminov.sh/en/blog/alm</link>
    <guid isPermaLink="true">https://aminov.sh/en/blog/alm</guid>
    <pubDate>Wed, 02 Sep 2026 00:00:00 GMT</pubDate>
    <description>&lt;p&gt;I had wanted a personal space on the internet for a long time. I looked at other
people&amp;#39;s personal sites, always found them cool, and the urge surfaced now and
then — but it never got past being an urge. I bought the domain &lt;code&gt;aminov.sh&lt;/code&gt; six
months ago and let it sit idle for six months.&lt;/p&gt;
&lt;p&gt;What finally pushed me was how easy this got. A twenty-dollar subscription,
Claude Code, and it does what you ask it to. A week instead of the month-plus
that would have gone into &amp;quot;first figure it out&amp;quot;. The other push was DHH on Lex
Fridman&amp;#39;s podcast: we live in a time when the technology keeps getting better and
lets you use a computer about as natively as it is currently possible to.&lt;/p&gt;
&lt;p&gt;This is not the first iteration. The earlier attempts died not on the code but on
the question of what to fill them with.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/alm-try-1.webp&quot; alt=&quot;The first attempt: a white page, &amp;quot;Hello&amp;quot;, a gradient across the screen&quot; width=&quot;1200&quot; height=&quot;721&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The first was light, with a full-screen gradient and two sections I never thought
of anything to put in.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/alm-try-2.webp&quot; alt=&quot;The second: a black screen and a wall of glowing particles&quot; width=&quot;1200&quot; height=&quot;708&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;The second was the exact opposite of where I ended up: a black screen and a wall
of sixty thousand WebGL particles that drifts on its own and answers to the
scroll. Beautiful for about ten seconds, and then you notice there is nothing
behind it — not one section I would have kept filling.&lt;/p&gt;
&lt;h2&gt;What it is against&lt;/h2&gt;
&lt;p&gt;The modern internet annoys me — not the big sites, those are understandable and
built for people, but the small ones. Any utility you need once a month. First
half the screen is taken by &amp;quot;we use cookies, please confirm&amp;quot;, and I do not want
to confirm anything, I want to reject all of it. Then the ads. Then a sign-up
through a Google account: two clicks, sure, but what for. I get tracked
everywhere and my data pooled together so that I can remove a background from an
image.&lt;/p&gt;
&lt;p&gt;A site for that should be a field you drop an image into and an image without a
background coming back. That is all.&lt;/p&gt;
&lt;p&gt;Same story with bloggers. &amp;quot;Join my Telegram channel, buy a Boosty subscription,
subscribe here and there.&amp;quot; I need a channel exactly when I need it, not sitting
in my list permanently; to look and leave you have to subscribe, and the
messenger where I wanted to keep personal chats turns into a dump. Too much
attention is demanded of me, and too pushily.&lt;/p&gt;
&lt;p&gt;My site is not pushy. You can visit it, you can not visit it. It does not save
itself anywhere on your side — to come back you have to bookmark it yourself, and
that is your decision, not mine. It asks for nothing, counts nothing and will
never disturb you. If that sounds like a manifesto — well, to some extent it is.&lt;/p&gt;
&lt;p&gt;Everything else follows from that: no analytics, no comments, no login, not a
single popup. There is nowhere to write to the site, because there is nowhere to
write at all: the content is files in a repository and a build is a folder of
static files.&lt;/p&gt;
&lt;h2&gt;Four sections&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Home&lt;/strong&gt; is a greeting. Somebody landed somewhere, and within ten seconds they
should know who I am and what all this is for.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blog&lt;/strong&gt; is the thing people start a Telegram channel for, only on my own ground.
Markdown, a file per language, publishing is &lt;code&gt;git push&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Projects&lt;/strong&gt; is a showcase. A card leads not to a project page of its own but into
the blog: if I made something, there is almost certainly a post about it, and a
second page about the same thing would drift away from the first on the second
edit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Favorite&lt;/strong&gt; is films, music, games. It exists for me first: I do not want to
forget that I listened to this and watched that and that it mattered. And whoever
is curious about someone else&amp;#39;s taste can open it and look. There is no &amp;quot;all&amp;quot;
tab: a 2:3 poster and a 1:1 sleeve are different shapes, and together they add up
to mush.&lt;/p&gt;
&lt;h2&gt;How it looks&lt;/h2&gt;
&lt;p&gt;I did design non-professionally, but I did it: headers, covers, mockups. I like
things spare, without extra elements. Two things met here — Swiss typography, from
a poster I once came across, and Metro UI, meaning Windows 8 and Windows Phone. I
even had the idea of buying myself a Windows Phone as an artifact of its era:
obsolete, but beautiful. I did not copy; I took the visual language from there and
made my own version of it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/posts/alm-poster.webp&quot; alt=&quot;The Yeah!Air Girona poster: a red rectangle with a leaf, the name set across the full width below&quot; width=&quot;1000&quot; height=&quot;1414&quot; loading=&quot;lazy&quot;&gt;&lt;/p&gt;
&lt;p&gt;That poster — &lt;a href=&quot;https://www.typographicposters.com/quim-marin&quot;&gt;Quim Marin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;What came out is four type sizes, two colors and a half-tone. Large, medium, small
and one more step for the captions under covers — it starts shrinking earlier than
the others, because on a phone a seventeen-pixel caption took three lines and the
rows of the showcase drifted out of level. Black background, white text, half-tone
only where one thing is subordinate to another: the author under a card&amp;#39;s title.
Inter as the typeface — the base that is everywhere, and that is its merit.&lt;/p&gt;
&lt;p&gt;There is not a single icon on the site. Only words and type size.&lt;/p&gt;
&lt;p&gt;Which is also why the active item is shown by size and not by color. There are two
colors, the half-tone is taken by subordination, there is nothing left to
highlight with — so the current item is simply one step larger. The same move in
the header and in the tabs.&lt;/p&gt;
&lt;p&gt;There was a light theme, and I removed it: I did not like it.&lt;/p&gt;
&lt;h2&gt;The header, which took the most&lt;/h2&gt;
&lt;p&gt;A small component, you would think. It took more time than all the pages put
together.&lt;/p&gt;
&lt;p&gt;On a desktop the items are set small, and the one under the cursor grows smoothly.
Sounds simple, but size moves the layout: an item grows, pushes its neighbour, the
neighbour slides out from under the cursor, the state drops, everything travels
back. The header shivered while the cursor sat a hair away from a letter. I had to
stop asking the browser what the cursor is over and work it out myself.&lt;/p&gt;
&lt;p&gt;On a phone the sections simply do not fit. I did not want to wrap them onto a
second line — then navigation takes a quarter of the screen and stops reading as a
row — so the row travels sideways instead, like a panorama in Windows Phone. That
is why on a narrow screen it is larger, not smaller: at a small size four words
almost fit, and there was nothing left to turn.&lt;/p&gt;
&lt;p&gt;The row also turns itself: the current section stops not at the edge but a step
inside it, so the tail of the previous one stays visible and fades into the dark.
The order of the sections is a map, and it should show where you stand relative to
your neighbours.&lt;/p&gt;
&lt;p&gt;And there is a spring at the edge: a sharp swipe hits it like a wall, and a short
bounce makes the stop feel alive. It is also where the dumbest bug in the whole
project turned up. The browser rounded two numbers in opposite directions, so the
row, having travelled all the way, believed one pixel of travel was still left —
and nothing on the right worked at all. On the next section the numbers agreed,
and for half a day I was sure the section was to blame.&lt;/p&gt;
&lt;p&gt;And the swipe between pages. The site is nicer to use from a phone than from a
computer, and that did not happen by itself either.&lt;/p&gt;
&lt;h2&gt;Why it is a template&lt;/h2&gt;
&lt;p&gt;I did not want to build a site that only fits me. So it came out as two
repositories: &lt;a href=&quot;https://github.com/shamil-aminov/alm&quot;&gt;the template is out in the open&lt;/a&gt;,
and my site is a separate repo with my content in it. Everything that makes an
installation yours sits in one file: name, address, languages, sections, kinds of
favorites.&lt;/p&gt;
&lt;p&gt;The second part matters more to me: you clone it and you get a working site. Nine
posts, eight projects, eight cards for every kind. Not an empty frame — a filled
one.&lt;/p&gt;
&lt;p&gt;This one comes from a sore spot. The same thing keeps coming up at work: there is
a production database and there is a local one built from seeds, and everybody
neglects the seeds — nothing checks them and over time they turn to mush. Half the
errors during development come from some data simply not being there. I wanted the
demo content to show the whole site, and I wanted eight cards to show what two
cannot: scrolling, rows, wrapping. The names in it are deliberately nothing —
&amp;quot;Project 3&amp;quot;, &amp;quot;Film 7&amp;quot;: the name of a real place or person starts pretending to be
content, and people edit it instead of replacing it with their own.&lt;/p&gt;
&lt;h2&gt;In the end&lt;/h2&gt;
&lt;p&gt;I built the site for myself and I am happy with the result. The loop is closed:
the domain has stopped sitting idle, there is a place that looks the way I want,
and all that is left is to fill it — without asking anyone&amp;#39;s permission and
without depending on anyone.&lt;/p&gt;
&lt;p&gt;Maybe in a year my view will change and I will build something more elaborate. For
now I like it exactly like this.&lt;/p&gt;
</description>
  </item>
</channel></rss>