<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mike</title>
    <description>The latest articles on DEV Community by Mike (@icevl).</description>
    <link>https://dev.to/icevl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2445813%2Fc4801c34-3471-4bbe-9efa-cb319fe541f0.jpg</url>
      <title>DEV Community: Mike</title>
      <link>https://dev.to/icevl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/icevl"/>
    <language>en</language>
    <item>
      <title>How we made a MIDI controller run our browser radio studio</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Sat, 05 Sep 2026 15:11:14 +0000</pubDate>
      <link>https://dev.to/icevl/how-we-made-a-midi-controller-run-our-browser-radio-studio-3ha0</link>
      <guid>https://dev.to/icevl/how-we-made-a-midi-controller-run-our-browser-radio-studio-3ha0</guid>
      <description>&lt;p&gt;A live radio screen is fine until the host has to do three things at once. The mouse becomes the wrong tool the moment you need to open a mic, pull music down, and hit a jingle without looking away.&lt;/p&gt;

&lt;p&gt;That is when we decided our browser studio had to understand cheap MIDI controllers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wanted from the hardware
&lt;/h2&gt;

&lt;p&gt;The Broadcast Studio at Tunio already ran in the browser. It had a mixer, pad grid, mic channels, guest links, a master section, recording, and a Go Live button. When nobody was live, the stream played the scheduled rotation. When the host stopped, the rotation came back.&lt;/p&gt;

&lt;p&gt;That worked with a mouse.&lt;/p&gt;

&lt;p&gt;But live radio has a body memory to it. A fader should be under a finger. A jingle should be on a pad. Mute and cue should be buttons you can find without thinking.&lt;/p&gt;

&lt;p&gt;I did not want people to install a studio app, route audio through OBS, or buy a sound card just to get that feeling. The constraint was simple and annoying: keep the studio in the browser, but make it feel less like a web page.&lt;/p&gt;

&lt;p&gt;Web MIDI gave us the door. In Chrome and Edge, the browser can see USB-MIDI devices. A user plugs in a controller, opens the studio, and the device can send notes, knobs, faders, and button presses straight into the page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtfpbh2c5u0pziv0ecq2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmtfpbh2c5u0pziv0ecq2.webp" alt="Internet radio setup: a laptop running Tunio's Broadcast Studio, a MIDI controller and headphones" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That sounded easy at first. It was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part was not receiving MIDI
&lt;/h2&gt;

&lt;p&gt;Reading MIDI messages is the small part. The harder question was: what should a message mean?&lt;/p&gt;

&lt;p&gt;Different controllers describe the same human action in different ways. A pad may send a note. A button may send a control change. A fader sends a stream of values. Some controls send one value on press and another on release.&lt;/p&gt;

&lt;p&gt;I did not want a settings screen where the user had to know any of that. Most radio hosts do not care about note numbers or CC values. They want to click “bind”, touch a control, and move on.&lt;/p&gt;

&lt;p&gt;So we built MIDI Learn around the action, not around the protocol.&lt;/p&gt;

&lt;p&gt;The user picks a thing in the studio: a jingle pad, a channel level, Go Live, Mute, CUE, start or stop. Then they touch the hardware control. We watch what comes in for a short moment.&lt;/p&gt;

&lt;p&gt;If it behaves like one press, it becomes a button binding. If it sweeps through a range, it becomes a fader or knob binding. The studio decides the type from the movement instead of making the user choose it.&lt;/p&gt;

&lt;p&gt;That small decision removed a lot of friction. It also made the code more careful. We had to wait long enough to tell a click from a sweep, but not so long that binding felt slow. Too eager, and a fader can look like a button. Too cautious, and the UI feels like it is ignoring you.&lt;/p&gt;

&lt;p&gt;I still do not love that edge. The interface hides complexity, but the complexity does not disappear. It just moves into our timing rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Matching the controller instead of forcing our layout
&lt;/h2&gt;

&lt;p&gt;The next problem was shape.&lt;/p&gt;

&lt;p&gt;Controllers are not standard in the way a keyboard is standard. One person has 8 pads. Another has 16. One has a 4×4 grid. Another has pads in a row. Some have faders next to pads. Some only have knobs.&lt;/p&gt;

&lt;p&gt;Our rule became: the screen should copy the hardware where it matters.&lt;/p&gt;

&lt;p&gt;So the studio settings let the user set the number of pads and how many pads sit in a row. If the controller is 4×4, the on-screen pad grid can be 4×4. If it is 8 across, the screen can match that too.&lt;/p&gt;

&lt;p&gt;This sounds cosmetic. It is not. When someone presses the top-left hardware pad, they expect the top-left on-screen jingle to fire. If the shapes do not match, the host has to translate under pressure. That is exactly what hardware is meant to avoid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapgdwjz2tgj7dgybc2dk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fapgdwjz2tgj7dgybc2dk.webp" alt="Studio settings: hotkeys and MIDI bindings, pad grid size, mixer channel matrix" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mixer channels had a different issue. A jingle pad is stable. A guest channel is not.&lt;/p&gt;

&lt;p&gt;If “Guest 1” is bound to a fader, what happens when that guest leaves and another person joins? We decided the binding should attach to the slot, not the human. The first guest seat stays on that fader. Whoever is in that seat gets controlled by it.&lt;/p&gt;

&lt;p&gt;That is less personal, but more useful. The operator keeps the same hand position. The show can change without breaking the controller map.&lt;/p&gt;

&lt;p&gt;We used the same idea for channel actions. Air, Mute, CUE, and level can each be placed on different hardware controls. Some controllers have a neat strip per channel. Many do not. We could not assume a perfect mixer surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The controller also had to answer back
&lt;/h2&gt;

&lt;p&gt;Listening to hardware was only half of it.&lt;/p&gt;

&lt;p&gt;A real console tells you state. A button lights when a channel is on. A mute key stays lit. You do not have to stare at the screen to know what is happening.&lt;/p&gt;

&lt;p&gt;So we made the studio send MIDI back to the controller where the device supports it. When a channel goes on air or gets muted, the matching hardware button can light up.&lt;/p&gt;

&lt;p&gt;This made the system feel much more like an instrument. It also created another source of truth problem.&lt;/p&gt;

&lt;p&gt;The broadcast state can change from the mouse, a hotkey, the MIDI controller, or the studio itself. The lights cannot just reflect the last MIDI action. They have to reflect the actual state of the broadcast.&lt;/p&gt;

&lt;p&gt;That pushed us to treat MIDI output as a view of studio state, not as a side effect of button presses. When state changes, update the screen and the controller. That is a cleaner model, and I wish we had started there in every part of the studio.&lt;/p&gt;

&lt;p&gt;There are still limits I do not like. Safari does not support Web MIDI, so this is not a universal browser feature. Controllers also vary a lot. “Plug in and learn” works because we avoid asking users to understand MIDI, but MIDI is still old, loose, and full of device quirks.&lt;/p&gt;

&lt;p&gt;The part I am happiest with is that the browser stopped feeling like the weak link. A laptop, a web page, and a small controller can run a live show with pads, faders, cue, mute, guests, and automatic return to the scheduled stream after Stop.&lt;/p&gt;

&lt;p&gt;If you have built browser control for physical hardware, how did you handle device differences without turning the settings screen into a MIDI manual?&lt;/p&gt;

</description>
      <category>audio</category>
      <category>webdev</category>
      <category>programming</category>
      <category>streaming</category>
    </item>
    <item>
      <title>From Frustration with News to an AI Radio Startup (with Full Stack Breakdown)</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Tue, 10 Jun 2025 12:29:22 +0000</pubDate>
      <link>https://dev.to/icevl/from-frustration-with-news-to-an-ai-radio-startup-with-full-stack-breakdown-3ndg</link>
      <guid>https://dev.to/icevl/from-frustration-with-news-to-an-ai-radio-startup-with-full-stack-breakdown-3ndg</guid>
      <description>&lt;p&gt;At some point, I wanted to dive into Kubernetes and mobile development, but I wasn’t sure where to begin. At the same time, I was deeply frustrated by the lack of news free from political agendas and negativity. The internet felt flooded with manipulative content, and I found myself wishing for a filter that could cut through all the noise.&lt;/p&gt;

&lt;p&gt;Eventually, I started by creating my own AI-powered online radio that broadcasts copyright-free music and what I consider “clean” news — free from bias and negativity. Later, when people began reaching out, asking how to create their own internet radio stations, I decided to focus my efforts on building a full-fledged platform with AI hosts, podcasts, and an intuitive management system.&lt;/p&gt;

&lt;p&gt;I ended up calling the project &lt;em&gt;Tunio&lt;/em&gt;, from “tune” and “I/O”, but happy to hear if it evokes something else to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streams that demonstrate the overall result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://app.tunio.ai/stream/en" rel="noopener noreferrer"&gt;Tunio Showcase&lt;/a&gt; - News, podcasts, announcement, host, jingles&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://app.tunio.ai/stream/tunio-lounge" rel="noopener noreferrer"&gt;Tunio – Lounge&lt;/a&gt; - Only music with jingles&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://app.tunio.ai/stream/dark-ambient" rel="noopener noreferrer"&gt;Tunio – Post-Apocalyptic Dark Ambient&lt;/a&gt; (featuring an ambient rain layer)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick breakdown of the stack I used
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Stack Overview&lt;/strong&gt; (Kubernetes-based):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audio&lt;/strong&gt;: Icecast2, Liquidsoap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic&lt;/strong&gt;: Golang (real-time playlist management and background conversion jobs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt;: PostgreSQL (pgvector), S3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Processing&lt;/strong&gt;: ffmpeg for news and podcasts — used for normalization and preparing all audio files before streaming.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: Prometheus + Grafana&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TTS&lt;/strong&gt;: ElevenLabs, Piper TTS (self-hosted)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Next.js, React Native (Expo)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  TTS / Audio Generation
&lt;/h2&gt;

&lt;p&gt;Originally used ElevenLabs, but later started using self-hosted Piper TTS to avoid high costs in the early stages of the project and to stay unlimited in content generation.&lt;/p&gt;

&lt;p&gt;News and podcasts are generated from RSS → filtered from negativity, politics, ads, and promotions using AI models → deduplicated with vector embeddings → summarized using GPT → voiced → sent to the live stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;All music is copyright-free, generated or licensed via open models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://app.tunio.ai/api/d/generated/bda5bd79-f05a-4b03-a001-3bb5420ca47f.m4a" rel="noopener noreferrer"&gt;News&lt;/a&gt; is categorized (tech, sports, gaming, etc.) and updated frequently from RSS-feeds&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://app.tunio.ai/api/d/assets/7ea46286-1f4d-4ff8-b75e-1d7a30758e5e.m4a" rel="noopener noreferrer"&gt;Jingles&lt;/a&gt; are also generated using TTS, with voices provided by ElevenLabs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://app.tunio.ai/podcast/history-of-things-en" rel="noopener noreferrer"&gt;Podcasts&lt;/a&gt; are generated based on user prompts, voiced by the selected TTS provider, and sent to the broadcast.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frontend
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js with next-intl for i18n for &lt;a href="https://app.tunio.ai/" rel="noopener noreferrer"&gt;application&lt;/a&gt; and control panel&lt;/li&gt;
&lt;li&gt;React Native (Expo) app with WebView for &lt;a href="https://play.google.com/store/apps/details?id=com.icevl.station101app" rel="noopener noreferrer"&gt;mobile access&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Distribution
&lt;/h2&gt;

&lt;p&gt;Built-in restreaming to &lt;a href="https://www.youtube.com/watch?v=U42ztLmJE1Q" rel="noopener noreferrer"&gt;YouTube&lt;/a&gt;, Twitch, and Telegram directly from the platform — no third-party software needed.&lt;/p&gt;

&lt;p&gt;I’ve set up a few demo stations to test different use cases (lounge, ambient, etc.) — would be curious to hear what you think: &lt;a href="https://app.tunio.ai" rel="noopener noreferrer"&gt;https://app.tunio.ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m not able to fully onboard users yet, as the company registration process is still underway and I can’t accept payments at this stage — but the platform is open for testing.&lt;/p&gt;

&lt;p&gt;If anyone’s interested, I’d be happy to share my story — the mistakes I made early on and how I had to rethink and redo some of the initial decisions.&lt;/p&gt;

&lt;p&gt;Feedback of any kind is welcome — tech suggestions, naming thoughts, or just impressions!&lt;/p&gt;

</description>
      <category>startup</category>
      <category>radio</category>
      <category>streaming</category>
      <category>development</category>
    </item>
  </channel>
</rss>
