<?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: Ivan Mikheev</title>
    <description>The latest articles on DEV Community by Ivan Mikheev (@s131ph).</description>
    <link>https://dev.to/s131ph</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%2F4145973%2Fa0be06b6-14d4-4150-b22a-f8ada2635567.jpg</url>
      <title>DEV Community: Ivan Mikheev</title>
      <link>https://dev.to/s131ph</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/s131ph"/>
    <language>en</language>
    <item>
      <title>I built a music-looping tool for TTRPGs and game prototypes with Python and Web Audio</title>
      <dc:creator>Ivan Mikheev</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:16:46 +0000</pubDate>
      <link>https://dev.to/s131ph/i-built-a-music-looping-tool-for-ttrpgs-and-game-prototypes-with-python-and-web-audio-5en6</link>
      <guid>https://dev.to/s131ph/i-built-a-music-looping-tool-for-ttrpgs-and-game-prototypes-with-python-and-web-audio-5en6</guid>
      <description>&lt;p&gt;The boss finishes their speech. Your players reach for their dice. This is where the music should kick in…&lt;/p&gt;

&lt;p&gt;…and nothing. Instead, the track enters a quiet interlude.&lt;/p&gt;

&lt;p&gt;Or the opposite happens: the big musical climax arrives while the party is still arguing about whether to open the door.&lt;/p&gt;

&lt;p&gt;I've been running tabletop RPGs for five years, and this mismatch kept bothering me. My music library had the right atmosphere, but the tracks were never long enough to follow the pace of the game. A session follows a certain order — arrival, exploration, tension, combat, aftermath — but every stage has a different duration each time. The same rule of musical development applies to almost every video game.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0jhq2qhpggoqqnrfuxm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs0jhq2qhpggoqqnrfuxm.png" alt="A fixed track timeline compared with a game session whose stages have varying durations" width="800" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The track has one timeline. The session has the same order of stages, but each one lasts as long as it lasts.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/Siziff/musslop" rel="noopener noreferrer"&gt;musslop&lt;/a&gt;&lt;/strong&gt;: a free, open-source tool that lets you loop sections of an existing track, manually cue transitions, and export loops for game audio workflows.&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%2F39xr7vq4h4vttnmv62qd.gif" 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%2F39xr7vq4h4vttnmv62qd.gif" alt="Musslop demo: select a section, loop it, and cue the next section with the Next button" width="480" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A 30-second silent demo: select → loop → cue → transition. All transitions are manually cued.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You decide when the music moves on.&lt;/strong&gt; It doesn't listen to your session or automatically react to gameplay.&lt;/p&gt;

&lt;p&gt;The stack is Python, FastAPI, librosa, React, and the Web Audio API. Here's what I learned while building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a good track isn't necessarily a good loop
&lt;/h2&gt;

&lt;p&gt;Game music often uses two complementary techniques:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal re-sequencing:&lt;/strong&gt; move between musical sections, usually at a beat, bar, or phrase boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vertical layering:&lt;/strong&gt; keep the same musical passage playing while adding or removing instrumental layers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With purpose-built interactive music, composers can prepare compatible sections, stems, and transition points.&lt;/p&gt;

&lt;p&gt;An ordinary stereo recording gives you none of those guarantees.&lt;/p&gt;

&lt;p&gt;To make one more controllable, I needed to solve three separate problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find useful musical sections.&lt;/li&gt;
&lt;li&gt;Find boundaries that work when those sections repeat.&lt;/li&gt;
&lt;li&gt;Schedule playback and transitions accurately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those sound like variations of the same problem. They aren't.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Finding structure without a neural model
&lt;/h2&gt;

&lt;p&gt;My first analysis pipeline used classic music information retrieval techniques through librosa.&lt;/p&gt;

&lt;p&gt;The simplified version looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feigzhv6u29suovkhot5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feigzhv6u29suovkhot5r.png" alt="Analysis pipeline: beat tracking, beat-synchronous features, self-similarity matrix, novelty curve, section boundaries" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Heuristic pipeline: sections show up as blocks in the self-similarity matrix; novelty peaks become candidate boundaries.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For each beat, the analyzer extracts information about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Harmony, using chroma features.&lt;/li&gt;
&lt;li&gt;Timbre, using MFCCs.&lt;/li&gt;
&lt;li&gt;Energy, using loudness-related features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A self-similarity matrix compares positions in the track with one another. Repeated or internally consistent passages appear as blocks.&lt;/p&gt;

&lt;p&gt;A checkerboard-shaped kernel along the matrix diagonal produces a novelty curve: peaks suggest places where the musical material changes. This approach goes back to Jonathan Foote's work on audio segmentation.&lt;/p&gt;

&lt;p&gt;The next step is to move candidate boundaries onto a musical grid.&lt;/p&gt;

&lt;p&gt;That is already an approximation. The heuristic downbeat estimation assumes 4/4, and real recordings can have pickups, tempo changes, or ambiguous accents.&lt;/p&gt;

&lt;p&gt;Still, it provides a useful baseline without requiring a large model download.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A section boundary and a loop boundary are different things
&lt;/h2&gt;

&lt;p&gt;This was the most useful lesson in the project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Detecting where the music changes doesn't tell you where it will repeat cleanly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F82814l2jki1jdcp5ow3q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F82814l2jki1jdcp5ow3q.png" alt="A section boundary from the novelty curve vs a loop boundary refined for loop closure" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The novelty peak says where the chorus starts. The loop-closure refinement moves the cut to a nearby downbeat where the section repeats cleanly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A boundary might correctly identify the start of a chorus but produce an awkward jump when the preceding section loops.&lt;/p&gt;

&lt;p&gt;I added a refinement step that tries nearby downbeat positions and balances several signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Loop closure: how compatible the end and beginning of the section are.&lt;/li&gt;
&lt;li&gt;Phrase length: a preference for common phrase lengths, such as four or eight bars.&lt;/li&gt;
&lt;li&gt;Transition evidence: whether there is an onset or energy change at the boundary.&lt;/li&gt;
&lt;li&gt;Structural novelty: whether the position is still close to the detected section change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last signal matters.&lt;/p&gt;

&lt;p&gt;Without a structural anchor, an optimizer can find a locally neat loop that no longer corresponds to the musical section you intended to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A better local score can produce a worse arrangement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The approach was partly inspired by Paul Lamere's &lt;a href="https://musicmachinery.com/2012/11/12/the-infinite-jukebox/" rel="noopener noreferrer"&gt;Infinite Jukebox&lt;/a&gt;, which explores extending music through jumps between compatible beats.&lt;/p&gt;

&lt;h3&gt;
  
  
  Some passages work better as one-shots
&lt;/h3&gt;

&lt;p&gt;Build-ups are another interesting case.&lt;/p&gt;

&lt;p&gt;If a passage keeps increasing in energy, a repeat throws the listener off a cliff: tension, more tension, even more tension — and suddenly back to the start.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffpe8dum64h9a4z00ollx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffpe8dum64h9a4z00ollx.png" alt="A normal section repeats naturally; a build-up climbs in energy so every repeat drops off a cliff" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A steady section can repeat. A build-up keeps climbing, so every repeat is a cliff — it should play once.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Occasionally that's a usable effect. Usually it just sounds broken.&lt;/p&gt;

&lt;p&gt;musslop uses energy trends and spectral brightness as signals for identifying potential build-ups. Those sections can play once instead of repeating.&lt;/p&gt;

&lt;p&gt;It's a suggestion, not a musical law. The user can change the loop behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Scheduling playback in the browser
&lt;/h2&gt;

&lt;p&gt;The frontend uses React for the interface and Web Audio for playback.&lt;/p&gt;

&lt;p&gt;The important distinction is between &lt;strong&gt;UI timing&lt;/strong&gt; and &lt;strong&gt;audio timing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A JavaScript timer can help schedule upcoming work, but it shouldn't be the clock that determines the exact moment of an audible transition. Playback needs to be scheduled against the audio context's clock.&lt;/p&gt;

&lt;p&gt;Each loop pass uses a new &lt;code&gt;AudioBufferSourceNode&lt;/code&gt; with a scheduled start time. When the user presses &lt;strong&gt;Next&lt;/strong&gt;, the player queues the following section at the selected musical boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next is a cue, not an immediate seek.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The core of the scheduler is small. Every chunk is a fresh source node with its own gain envelope, started at an absolute time on the audio clock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;scheduleChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fadeIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fadeOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBufferSource&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;fadeIn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fadeOut&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;master&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;when&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// sample-accurate&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;setInterval&lt;/code&gt; tick runs every 60 ms and only makes sure the next chunk is scheduled ~350 ms ahead. When a cue is pending, that next chunk simply comes from the target section instead of the current one — so the transition lands exactly on the boundary without any timer jitter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwjwgs999gvmk81megoa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhwjwgs999gvmk81megoa.png" alt="Web Audio timeline: each loop pass is its own source node; a cue schedules the next section at a boundary; natural, soon and now cue modes" width="800" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each loop pass is a separately scheduled source node. A cue is scheduled for a boundary: natural (loop end), soon (phrase) or now (crossfade).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is useful during a session: I can request a change and let it land at a suitable point instead of abruptly cutting the audio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accurate timing still needs good transitions
&lt;/h3&gt;

&lt;p&gt;Scheduling alone doesn't make arbitrary cuts sound natural.&lt;/p&gt;

&lt;p&gt;The player combines several techniques:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short fades to reduce clicks at cuts.&lt;/li&gt;
&lt;li&gt;Equal-power crossfades to blend outgoing and incoming material.&lt;/li&gt;
&lt;li&gt;Outgoing tails so the previous section doesn't stop abruptly.&lt;/li&gt;
&lt;li&gt;Bass-swap handling to reduce overlapping low-frequency content.&lt;/li&gt;
&lt;li&gt;Stingers such as a hit, cymbal, or riser around the transition.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an equal-power crossfade, the outgoing and incoming gains follow &lt;code&gt;cos(t·π/2)&lt;/code&gt; and &lt;code&gt;sin(t·π/2)&lt;/code&gt; for &lt;code&gt;t&lt;/code&gt; from 0 to 1:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fib3xwupus79ajldm4my6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fib3xwupus79ajldm4my6.png" alt="Linear crossfade vs equal-power cosine/sine crossfade curves" width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Linear fades dip in power in the middle; cos/sin curves keep the mix at constant power. Loop repeats are correlated, so they get a linear fade instead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Their squared gains sum to one. For uncorrelated signals, that helps avoid the power dip of a simple linear crossfade.&lt;/p&gt;

&lt;p&gt;It doesn't guarantee constant perceived loudness for every pair of musical passages. Correlation, arrangement, and frequency content still matter.&lt;/p&gt;

&lt;p&gt;There's also a small but important distinction with stingers: a hit should generally start on the transition, while a riser may need to end there.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Adding neural analysis didn't eliminate the editor
&lt;/h2&gt;

&lt;p&gt;The heuristic pipeline was useful, but complex arrangements exposed its limits.&lt;/p&gt;

&lt;p&gt;I added two optional structure-analysis engines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://github.com/mir-aidj/all-in-one" rel="noopener noreferrer"&gt;All-In-One&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ASLP-lab/SongFormer" rel="noopener noreferrer"&gt;SongFormer&lt;/a&gt;&lt;/strong&gt;, paired with &lt;strong&gt;&lt;a href="https://github.com/CPJKU/beat_this" rel="noopener noreferrer"&gt;Beat This!&lt;/a&gt;&lt;/strong&gt; for beat and downbeat tracking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These can identify section boundaries and provide labels such as intro, verse, and chorus.&lt;/p&gt;

&lt;p&gt;On my small, manually annotated set of tracks, the newer model wasn't uniformly better. SongFormer worked well on some material but struggled with an orchestral example that All-In-One handled better.&lt;/p&gt;

&lt;p&gt;That's a practical observation from a limited personal evaluation, not a general benchmark.&lt;/p&gt;

&lt;p&gt;The takeaway was to keep multiple analysis options and make the output editable.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Drag boundaries with bar snapping.&lt;/li&gt;
&lt;li&gt;Split and merge sections.&lt;/li&gt;
&lt;li&gt;Adjust the loop repeat start.&lt;/li&gt;
&lt;li&gt;Change whether a section loops.&lt;/li&gt;
&lt;li&gt;Undo edits.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The analyzer proposes an arrangement. The user gets the final say.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Experimenting with intensity layers
&lt;/h2&gt;

&lt;p&gt;For vertical layering, musslop can run &lt;strong&gt;&lt;a href="https://github.com/facebookresearch/demucs" rel="noopener noreferrer"&gt;Demucs&lt;/a&gt;&lt;/strong&gt; to split a track into drums, bass, vocals and everything else, and lets you mix those layers live. The same recording can then play as a sparse "exploration" arrangement or a full "combat" one.&lt;/p&gt;

&lt;p&gt;There are limits: separated stems carry artifacts, and muting the drums doesn't magically turn a battle track into ambience. But it's a cheap way to test how an existing recording might behave as interactive music before anyone writes stems for real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits
&lt;/h2&gt;

&lt;h3&gt;
  
  
  At a tabletop session
&lt;/h3&gt;

&lt;p&gt;Prepare the track before the game, check the loops, and correct any boundaries that need attention.&lt;/p&gt;

&lt;p&gt;During play:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select a section.&lt;/li&gt;
&lt;li&gt;Keep it looping while the scene unfolds.&lt;/li&gt;
&lt;li&gt;Press Next when you want to move on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The purpose is to reduce the time spent searching and scrubbing through music while also running the session.&lt;/p&gt;

&lt;h3&gt;
  
  
  During game development
&lt;/h3&gt;

&lt;p&gt;Use it to audition loops, section transitions, and intensity changes before implementing the playback behaviour in your game.&lt;/p&gt;

&lt;p&gt;You can export sections as WAV loops for your audio workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's currently a standalone tool, not an FMOD/Wwise replacement or a drop-in game-engine integration.&lt;/strong&gt; The in-game logic is still yours to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it locally
&lt;/h2&gt;

&lt;p&gt;The project requires Python 3.10+ and Git for the commands below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux / macOS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Siziff/musslop.git
&lt;span class="nb"&gt;cd &lt;/span&gt;musslop
./setup.sh
./run.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="kd"&gt;git&lt;/span&gt; &lt;span class="kd"&gt;clone&lt;/span&gt; &lt;span class="kd"&gt;https&lt;/span&gt;://github.com/Siziff/musslop.git
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="kd"&gt;musslop&lt;/span&gt;
&lt;span class="kd"&gt;setup&lt;/span&gt;.bat
&lt;span class="nb"&gt;run&lt;/span&gt;.bat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:8801" rel="noopener noreferrer"&gt;http://localhost:8801&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The base setup includes heuristic analysis, editing, playback, and loop export. Neural analysis and stem separation use optional components; installation details are in the README.&lt;/p&gt;

&lt;p&gt;Once the required dependencies and model weights have been downloaded, processing works locally and offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free, open source, and personal
&lt;/h2&gt;

&lt;p&gt;musslop is &lt;strong&gt;free and open source under the MIT license&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;No subscriptions, accounts, registration, email collection, or SMS verification. There's no paid tier or planned subscription.&lt;/p&gt;

&lt;p&gt;This is a personal project I'd wanted to build for a long time. I finally made it for myself, and I'm sharing it because other people might find it useful too.&lt;/p&gt;

&lt;p&gt;For transparency, I used AI assistance for the UI. Separately, the optional audio-processing features use pretrained models for analysis and stem separation. The tool works with existing music rather than generating tracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd like feedback on
&lt;/h2&gt;

&lt;p&gt;I'm especially interested in hearing from developers working with audio and people running tabletop sessions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do you balance responsive transitions against musical phrasing?&lt;/li&gt;
&lt;li&gt;What makes a long-running loop feel less repetitive?&lt;/li&gt;
&lt;li&gt;For a game prototype, would WAV export be enough, or would you also need section and transition metadata?&lt;/li&gt;
&lt;li&gt;What would stop you from using a tool like this?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/Siziff/musslop" rel="noopener noreferrer"&gt;github.com/Siziff/musslop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it looks useful for your table or game, a star helps other people discover it. Reports about awkward loops or confusing controls are just as welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Adapted from my original &lt;a href="https://habr.com/ru/articles/1086674/" rel="noopener noreferrer"&gt;Russian-language article on Habr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.ee.columbia.edu/~dpwe/pubs/Ellis07-beattrack.pdf" rel="noopener noreferrer"&gt;Ellis — Beat Tracking by Dynamic Programming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fxpal.com/publications/automatic-audio-segmentation-using-a-measure-of-audio-novelty.pdf" rel="noopener noreferrer"&gt;Foote — Automatic Audio Segmentation Using a Measure of Audio Novelty&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://musicmachinery.com/2012/11/12/the-infinite-jukebox/" rel="noopener noreferrer"&gt;The Infinite Jukebox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mir-aidj/all-in-one" rel="noopener noreferrer"&gt;All-In-One&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ASLP-lab/SongFormer" rel="noopener noreferrer"&gt;SongFormer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/CPJKU/beat_this" rel="noopener noreferrer"&gt;Beat This!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facebookresearch/demucs" rel="noopener noreferrer"&gt;Demucs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>music</category>
      <category>gamedev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
