DEV Community

Alex Creatune
Alex Creatune

Posted on

Joining Audio Is Not Mixing: Verify the Difference with Two Tones

Putting a voice clip and a backing track into an audio joiner does not automatically place the voice over the music. A direct join places one clip after another. A mix combines signals that occupy the same time interval.

I’m Alex from the Creatune team. I checked this distinction with two synthetic WAV files and an actual export from our public Audio Joiner. The observations below are file measurements, not listening impressions.

Write the expected timeline before choosing a tool

For two clips that each last two seconds, a direct join should produce roughly four seconds. Starting both clips together in a mix would instead produce roughly two seconds, assuming neither is extended. A crossfade overlaps only part of a transition; it is not the same as placing two complete tracks together.

In sample terms, concatenation is A followed by B. Mixing is gain_A * A[n] + gain_B * B[n] at each shared time position, with alignment and headroom decisions. A louder sum can clip even when neither original clips, so simply adding arrays is not a complete mixing workflow.

Make a test whose parts are easy to identify

Use Python’s standard library to create a 440 Hz tone and an 880 Hz tone, both two seconds long at 44,100 Hz. These are our own test signals, not music taken from another artist.

import math, struct, wave

for name, hz in (("first-440.wav", 440), ("second-880.wav", 880)):
    with wave.open(name, "wb") as w:
        w.setparams((1, 2, 44100, 0, "NONE", "not compressed"))
        w.writeframes(b"".join(
            struct.pack("<h", round(8192 * math.sin(2 * math.pi * hz * i / 44100)))
            for i in range(88200)
        ))
Enter fullscreen mode Exit fullscreen mode

On Creatune Audio Joiner, choose the two files and keep 440 Hz first. Leave their start and end values at the full two seconds. Keep fades and “Trim silence at joins” off, and confirm the transition says “Direct join.” Choose WAV as the output format.

Two two-second test files arranged in order with a direct join and WAV output

The public page shows local files remain on the device. The screenshots use the signed-out interface. I clicked Download and measured the resulting WAV; no music generation was involved.

The same public workspace after the WAV export reports Download ready

Inspect duration and two interior windows

The exported file contained 176,400 frames at 44,100 Hz: exactly 4.000000 seconds, with two channels. The input files were mono, so this export changed the channel count. That observation belongs to this test; it is not a promise about every source or browser.

To distinguish sequential tones from simultaneous tones, inspect windows away from the join: 0.5–1.0 seconds and 2.5–3.0 seconds. The following standard-library check measures the amplitudes at the two known frequencies in the first output channel. It is a narrow test for these fixtures, not a general music pitch detector.

import array, math, sys, wave
from pathlib import Path
path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('joined.wav')
with wave.open(str(path), 'rb') as w:
    assert w.getsampwidth() == 2, 'This check expects 16-bit PCM WAV'
    rate, channels, frames = w.getframerate(), w.getnchannels(), w.getnframes()
    samples = array.array('h', w.readframes(frames))
if sys.byteorder != 'little':
    samples.byteswap()
x = [v / 32768 for v in samples[::channels]]
print(f'{rate} Hz; {channels} channels; {frames} frames; {frames/rate:.6f} seconds')
for start in (0.5, 2.5):
    window = x[round(start * rate):round((start + 0.5) * rate)]
    def amplitude(hz):
        angle = 2 * math.pi * hz / rate
        real = math.fsum(v * math.cos(angle * i) for i, v in enumerate(window))
        imag = math.fsum(v * math.sin(angle * i) for i, v in enumerate(window))
        return 2 * math.hypot(real, imag) / len(window)
    print(f'{start:.1f}-{start+0.5:.1f}s: A440={amplitude(440):.6f}, A880={amplitude(880):.6f}')

Enter fullscreen mode Exit fullscreen mode

Run it as python verify-join.py joined.wav, using your downloaded file’s name. My output was:

44100 Hz; 2 channels; 176400 frames; 4.000000 seconds
0.5-1.0s: A440=0.250000, A880=0.000001
2.5-3.0s: A440=0.000000, A880=0.250000
Enter fullscreen mode Exit fullscreen mode

The first window contains the 440 Hz test tone; the second contains the 880 Hz tone. Small residual values are not a second musical layer. Together with the duration and direct-join setting, these measurements support sequential placement. They do not establish perceptual quality or guarantee sample-identical output.

Use the result to choose the next operation

A sequence of interview answers or a listening reel needs joining. A voice over background music needs a mixer or multitrack editor with time alignment and independent levels. If the goal is a short transition, a crossfade may be appropriate, but check the new total duration and the overlap separately.

For a longer sequence, verify clip order with three identifiable tones before judging transitions. If your source files use different sample rates, check duration using the output frame count; that catches a different class of timeline error.

Top comments (0)