{"id":604,"date":"2026-11-06T13:29:14","date_gmt":"2026-11-06T21:29:14","guid":{"rendered":"https:\/\/jasonsla.de\/?p=604"},"modified":"2026-11-06T13:29:15","modified_gmt":"2026-11-06T21:29:15","slug":"voice-swap-pipeline-seed-vc","status":"publish","type":"post","link":"https:\/\/jasonsla.de\/2026\/11\/06\/voice-swap-pipeline-seed-vc\/","title":{"rendered":"Then I Taught It to Sing as Someone Else: Voice-Swap on the Home GPU Server"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">In the previous post I wrote about teaching <code>compute01<\/code> \u2014 my home GPU server \u2014 to write full songs from text prompts with ACE-Step. Text goes in, a finished song with vocals comes out. It is a genuinely useful little studio, but it has one limitation that starts to itch after the first dozen songs: the singer never changes. ACE-Step&#8217;s vocalist is whatever the checkpoint is, every time, in every key, on every track.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix I wanted was not another generation run with a different prompt. I wanted an audio pipeline: take any song the server has already rendered, pull the vocal out, re-voice it with a reference singer, and remix it over the original instrumental. No re-generation, no re-rolling the dice on a take. And I wanted it to stay entirely on the box \u2014 no cloud API, no uploading anyone&#8217;s voice anywhere.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That pipeline is now built, and it works. The server can take a song it wrote, separate the vocal, shift it into the reference voice&#8217;s range, convert the timbre to a reference voice with Seed-VC, loudness-match the result, and mix it back over the instrumental \u2014 all on two P40 GPUs, all local.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Pipeline<\/h2>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"360\" height=\"968\" src=\"https:\/\/jasonsla.de\/wp-content\/uploads\/2026\/11\/voice-swap-pipeline-mermaid-1.png\" alt=\"Flowchart of the voice-swap pipeline: htdemucs stem split, auto-pitch with a \u00b112 semitone cap, Seed-VC conversion, EBU R128 loudness match, and ffmpeg remix\" class=\"wp-image-605\" srcset=\"https:\/\/jasonsla.de\/wp-content\/uploads\/2026\/11\/voice-swap-pipeline-mermaid-1.png 360w, https:\/\/jasonsla.de\/wp-content\/uploads\/2026\/11\/voice-swap-pipeline-mermaid-1-112x300.png 112w\" sizes=\"auto, (max-width: 360px) 100vw, 360px\" \/><figcaption class=\"wp-element-caption\">The voice-swap pipeline: split, pitch, convert, loudness-match, remix.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The whole thing is one command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code tabindex=\"0\">~\/seedvc\/venv\/bin\/python ~\/seedvc\/voiceswap.py &lt;song&gt; --voice &lt;name&gt; \\\n        &#91;--pitch-shift N] &#91;--vocal-gain dB]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The target voice lives as a single reference clip in <code>~\/voices\/<name>\/ref.wav<\/code> \u2014 a short clip of clean audio of whoever you want to hear sing (Seed-VC takes 1 to 30 seconds, and the pipeline uses up to 25). <code>--pitch-shift<\/code> defaults to auto, and <code>--vocal-gain<\/code> is there for when the converted vocal needs a nudge in the mix.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There are five stages, and two of them are the actual engineering:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. Stem split.<\/strong> <code>htdemucs<\/code> separates the song into vocals and instrumental. This is the only stage I did not have to think hard about \u2014 htdemucs is boring and reliable, and on a GPU it does a full song in a few seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. Auto-pitch correction.<\/strong> This is the first piece of real engineering. A tenor melody re-voiced by a baritone sounds strained, so the vocal is first moved into the reference voice&#8217;s range. The pipeline estimates the median fundamental frequency (f0) of the source vocal with librosa&#8217;s PYIN algorithm, does the same for the reference, and shifts the source by the nearest whole number of semitones between the two, capped at \u00b112. A <code>--pitch-shift 0<\/code> flag skips this if you want the original key preserved.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>3. Seed-VC conversion.<\/strong> Seed-VC&#8217;s f0-conditioned 44.1 kHz singing model re-voices the stem with the reference&#8217;s timbre while keeping the source&#8217;s melody and lyrics \u2014 it preserves the pitch contour, so the song stays the song. The conversion runs at 40 steps (30\u201350 is the recommended window) with a cfg of 0.7.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>4. EBU R128 loudness match.<\/strong> The second piece of real engineering, and the one people notice least until it is missing. A converted vocal that is a few LUFS louder than the rest of the track does not sit in the mix \u2014 it floats on top of it, like a karaoke track recorded by a different engineer. The pipeline measures integrated loudness of the source vocal (EBU R128) and aligns the converted vocal to it before anything is mixed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>5. Remix.<\/strong> <code>ffmpeg amix<\/code> blends the converted vocal over the original instrumental. No time stretching, no re-alignment \u2014 the conversion preserves length to within a few tens of milliseconds, so the remix is just a mix, and the result is exported as a 320 kbit\/s CBR MP3.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Before and After<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The same two songs from the previous post, run through the pipeline with my voice as the reference. Play the original first, then the swap: same lyrics, same melody, same instrumental, different singer. Both use a fixed \u221212 semitone shift (exactly one octave), so the vocal stays in the song&#8217;s key.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;Compute Zero One&#8221; (synth-pop)<\/h3>\n\n\n\n<figure class=\"wp-block-audio\"><audio controls src=\"https:\/\/jasonsla.de\/wp-content\/uploads\/2026\/11\/compute01-synthpop-acestep.mp3\"><\/audio><figcaption class=\"wp-element-caption\">Original: ACE-Step&#8217;s own singer.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-audio\"><audio controls src=\"https:\/\/jasonsla.de\/wp-content\/uploads\/2026\/11\/compute01-synthpop-jason-voice.mp3\"><\/audio><figcaption class=\"wp-element-caption\">Voice-swapped: my voice via Seed-VC. Conversion took about 4 minutes on one P40.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">&#8220;Little Server&#8221; (acoustic folk)<\/h3>\n\n\n\n<figure class=\"wp-block-audio\"><audio controls src=\"https:\/\/jasonsla.de\/wp-content\/uploads\/2026\/11\/compute01-folk-acestep.mp3\"><\/audio><figcaption class=\"wp-element-caption\">Original: ACE-Step&#8217;s own singer.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-audio\"><audio controls src=\"https:\/\/jasonsla.de\/wp-content\/uploads\/2026\/11\/compute01-folk-jason-voice.mp3\"><\/audio><figcaption class=\"wp-element-caption\">Voice-swapped: my voice via Seed-VC. The reference is one short clip, so expect a likeness rather than a perfect copy.<\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">What the Measured Numbers Say<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Seed-VC lives in <code>~\/seedvc<\/code> with its own virtualenv \u2014 torch 2.4.0+cu121 pinned for the P40s&#8217; sm_61 compute capability, and FP32 throughout. (Pascal cards emulate FP16 at a fraction of FP32 speed, so forcing FP32 on the P40 is both the safe choice and the fast one.) Peak VRAM is about 2.7 GB per conversion, and the model loads per call and frees the card afterwards.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Timing on the P40s, converting a full 60-second ACE-Step song with a Kokoro-generated male reference voice:<\/p>\n\n\n\n<figure tabindex=\"0\" class=\"wp-block-table\"><table><thead><tr><th>song<\/th><th>length<\/th><th>auto-shift<\/th><th>convert<\/th><th>total<\/th><th>duration delta<\/th><\/tr><\/thead><tbody><tr><td>Latin bolero (male tenor, Sp\/En lyrics)<\/td><td>60.0 s<\/td><td>\u221212 st (\u00b112 cap)<\/td><td>104.9 s<\/td><td>150.0 s<\/td><td>+0.029 s<\/td><\/tr><tr><td>Joyful salsa (female vocal, En lyrics)<\/td><td>59.9 s<\/td><td>\u221212 st (\u00b112 cap)<\/td><td>104.6 s<\/td><td>153.4 s<\/td><td>+0.043 s<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">A 60-second song converts in about two minutes \u2014 roughly half realtime \u2014 and total end-to-end is about two and a half minutes. Both test conversions were verified two ways: Whisper transcribes the lyrics cleanly from the converted vocal, so the diction survived, and ffprobe confirms the output length matches the source within 0.1 seconds, so the remix has nothing to fix. A CAMPPlus embedding check showed the timbre moved decisively toward the reference: cosine similarity to the reference voice went from 0.102 on the original vocal to 0.602 after conversion (and higher with a manual pitch shift).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What I Learned<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The \u00b112 semitone cap is the normal case, not an edge case.<\/strong> A Kokoro male voice sits around 112 Hz; the bolero&#8217;s tenor sits around 247 Hz. That&#8217;s more than an octave apart, so auto-pitch hits the \u221212 cap on both test songs. An octave down is still in key with the backing track, and the bolero conversion is genuinely listenable, but the voice ends up near the bottom of its range. The subtler problem shows up when the gap is smaller: a shift of, say, \u22129 or \u221211 semitones moves the vocal into a different key from the instrumental it gets mixed over. The next fix is to round the auto-shift to whole octaves (0 or \u00b112), so the vocal always stays in the song&#8217;s key.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>FP32 is not a fallback on a P40, it is the correct performance mode.<\/strong> The upstream Seed-VC code autocasts to FP16, which is the crippled path on sm_61. Forcing FP32 everywhere made the conversion both correct and *faster*.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Loudness matching is the difference between &#8220;converted&#8221; and &#8220;produced.&#8221;<\/strong> The pitch and timbre work gets the attention, but the R128 stage is what makes the result sound like a real cover instead of a vocal dropped onto a backing track. If I had skipped it, the demo would have been a lot less convincing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The reference material matters more than any parameter.<\/strong> Clean, modern-sounding references convert well. Noisy or distant reference audio degrades Seed-VC output quickly. And you don&#8217;t need much: the pipeline uses at most 25 seconds of reference.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Matters<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is what turns a text-to-song generator into a &#8220;make anyone sing this&#8221; studio. One command, fully local: pick a rendered song, point at a reference voice, get a finished MP3 where that voice sings it. Same lyrics, same melody, same instrumental \u2014 just a different singer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The part I find genuinely interesting is that the hard work in this pipeline is not the neural network. The engineering is the boring studio plumbing around the model: keeping the singer in a range that fits and in tune with the backing track, and keeping the converted vocal at the same loudness as everything else. Those two decisions are what separate a convincing voice swap from a party trick.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Next Steps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The pipeline is functional but the obvious next steps are staring at me:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>A better reference for my own voice.<\/strong> The pipeline clones from one short clip. A few minutes of me actually singing, and Seed-VC&#8217;s fine-tuning mode, should get noticeably closer.<\/li>\n\n\n\n<li><strong>Octave-only auto-pitch.<\/strong> Round the auto-shift to 0 or \u00b112 so the converted vocal never lands in a different key from the backing track.<\/li>\n\n\n\n<li><strong>Segmented conversion for duets.<\/strong> Right now the whole vocal stem gets converted, so a duet becomes the same person singing both parts. Converting only selected time ranges of the stem, then a single remix, would keep the two singers distinct.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">For now, though: the server writes the song, and then it sings it as someone else. Entirely on the box.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>ACE-Step writes the song; Seed-VC gives it a throat. I wired a voice-swap pipeline onto compute01 \u2014 htdemucs stem split, librosa PYIN auto-pitch with a \u00b112 semitone cap, a 40-step Seed-VC conversion, EBU R128 loudness matching, and a remix \u2014 so any generated song can be re-sung by a reference voice, entirely local.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[17,21],"tags":[],"class_list":["post-604","post","type-post","status-publish","format-standard","hentry","category-automation","category-build-logs"],"_links":{"self":[{"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/posts\/604","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/comments?post=604"}],"version-history":[{"count":4,"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/posts\/604\/revisions"}],"predecessor-version":[{"id":615,"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/posts\/604\/revisions\/615"}],"wp:attachment":[{"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/media?parent=604"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/categories?post=604"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jasonsla.de\/wp-json\/wp\/v2\/tags?post=604"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}