Contents
A digital audio file is a long list of measurements of a sound wave. The audio sample rate is how many of those measurements the file stores for each second of sound. Music is often stored at 44.1 kHz. Video usually runs at 48 kHz.
Most video projects mix both. A track from a music library plays under a voiceover recorded for the video, and the two files don’t match. This guide explains what the numbers mean, why video uses 48 kHz, and how to check the rate of every file before you render.
The examples use Python and Shotstack’s video editing API. We measured the rendered audio with FFmpeg.
TL;DR
- Sample rate is the number of audio samples per second. 44.1 kHz is 44,100 samples a second, and 48 kHz is 48,000.
- Both rates hold every frequency people can hear. They differ in where they came from and how they fit video.
- 44.1 kHz is the CD rate. 48 kHz is the standard for video, and YouTube recommends it for uploads.
- At 24 fps, the film rate, 48 kHz gives each frame exactly 2,000 samples. 44.1 kHz gives 1,837.5.
- Shotstack’s Probe endpoint reports the sample rate of a file at a URL. In our render, a 44.1 kHz song and a 48 kHz voice came out as one 48 kHz track.
What is audio sample rate?
A microphone produces a continuous wave. To store it, a recorder measures the wave at fixed intervals and saves each measurement as a number. Each measurement is a sample. The sample rate is the number of samples per second, given in hertz (Hz) or kilohertz (kHz).
The rate sets the highest frequency a file can hold, which is half the sample rate. A textbook definition quoted in Wikipedia’s article on the Nyquist frequency puts it this way: it “is half the sampling rate and corresponds to the highest frequency which a sampled data system can reproduce without error”.
| Sample rate | Highest frequency it can hold |
|---|---|
| 24 kHz | 12 kHz |
| 44.1 kHz | 22.05 kHz |
| 48 kHz | 24 kHz |
Wikipedia’s article on hearing range says “the human range is commonly given as 20 to 20,000 Hz”. Both 44.1 kHz and 48 kHz cover all of it. A lower rate such as 24 kHz stops at 12 kHz and loses the top of that range.
Sample rate isn’t the same as bit depth or bitrate. Bit depth is how precisely each sample is stored. Bitrate is how much data a compressed file, such as an MP3, uses each second. Our video bitrate guide covers bitrate in detail.
44.1 kHz vs 48 kHz
Where 44.1 kHz comes from
44.1 kHz is older than the CD. Wikipedia’s article on 44,100 Hz says the rate “originated in the late 1970s with PCM adaptors”, devices that stored digital audio on video tape. It “was deemed the highest usable rate compatible with both PAL and NTSC video”. The CD kept the rate, and audio made for CD still uses it.
Why video uses 48 kHz
48 kHz became the rate for film and television. Wikipedia’s article on 48,000 Hz says it “has become the standard for professional audio and video”. The same article notes that it “is evenly divisible by 24, a common frame rate for media, such as film, unlike 44.1 kHz.”
Video platforms ask for it too. YouTube’s recommended upload encoding settings list “Sample rate: 48kHz” for the audio track.
Samples per video frame
The table shows how many audio samples fall within one video frame at common frame rates.
| Frame rate | At 44.1 kHz | At 48 kHz |
|---|---|---|
| 24 fps | 1,837.5 | 2,000 |
| 25 fps | 1,764 | 1,920 |
| 30 fps | 1,470 | 1,600 |
| 60 fps | 735 | 800 |
Both rates give whole numbers at 25, 30 and 60 fps. The difference is 24 fps, the film rate. At 44.1 kHz it lands on half a sample, so frame edges and sample edges only meet on every second frame.
NTSC rates such as 29.97 fps don’t divide evenly at either rate. Our guide to video frame rates covers the frame rates themselves.
Which one should you use?
Use 48 kHz for anything that ends up as video. Keep 44.1 kHz for audio that goes to CD or to music-only releases. When a project mixes the two, the finished file still has a single rate, so one of them gets converted.
Check a file’s sample rate
With ffprobe
ffprobe comes with FFmpeg. This command prints the sample rate and channel count of the first audio stream in a file:
ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate,channels -of csv=p=0 music.mp3For the 44.1 kHz stereo music file used below, it prints 44100,2. Our guide on how to use ffprobe explains the other fields you can read.
With the Shotstack Probe endpoint
ffprobe needs the file on your machine. When your media is already online, the Probe endpoint reads it at its URL and returns the same metadata as JSON. Each audio stream has a sample_rate field. Our article on inspecting media metadata walks through the full response.
Mix 44.1 kHz and 48 kHz audio in one video with Python
The script below checks the sample rate of two files, then renders them into one video. The voice is a 48 kHz podcast ad. The music is a 44.1 kHz track at 20% volume under the voice. Both are sample files that Shotstack hosts.
The script uses only the Python standard library. Set SHOTSTACK_API_KEY to the API key from your Shotstack dashboard, then run it with Python 3. Each render uses credits from your plan, as listed on the pricing page.
import html
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
API = "https://api.shotstack.io/edit/v1"
ASSETS = "https://shotstack-assets.s3.ap-southeast-2.amazonaws.com"
SOURCES = {
"Voice": f"{ASSETS}/audio/tailor-podcast-advert.mp3",
"Music": f"{ASSETS}/music/unminus/ambisax.mp3",
}
MUSIC_VOLUME = 0.2
CSS = """html,body{margin:0;padding:0;width:1920px;height:1080px;overflow:hidden;
background:#10131a;color:#fff;font-family:Arial,sans-serif}
.card{display:flex;flex-direction:column;justify-content:center;height:1080px;padding-left:220px}
h1{font-size:44px;font-weight:normal;letter-spacing:9px;color:#8a93a6;margin:0 0 60px}
.row{display:flex;align-items:baseline;font-size:96px;line-height:170px}
.name{width:440px;color:#8a93a6;font-size:60px}"""
def call(url, body=None):
data = json.dumps(body).encode() if body is not None else None
headers = {
"x-api-key": os.environ["SHOTSTACK_API_KEY"],
"Content-Type": "application/json",
}
try:
request = urllib.request.Request(url, data, headers)
with urllib.request.urlopen(request) as response:
return json.load(response)
except urllib.error.HTTPError as error:
sys.exit(f"HTTP {error.code}: {' '.join(error.read().decode().split())[:300]}")
def probe(url):
"""Return a file's first audio stream (or None) and its duration in seconds."""
encoded = urllib.parse.quote(url, safe="")
metadata = call(f"{API}/probe/{encoded}")["response"]["metadata"]
audio = next((s for s in metadata["streams"] if s["codec_type"] == "audio"), None)
return audio, float(metadata["format"]["duration"])
def describe(audio):
if audio is None:
return "no audio"
rate = int(audio["sample_rate"])
return f"{audio['codec_name']}, {rate} Hz, {audio['channels']} channels"
def card(rates):
rows = "".join(
f'<div class="row"><div class="name">{html.escape(name)}</div>'
f"<div>{rate / 1000:g} kHz</div></div>"
for name, rate in rates.items()
)
return {
"type": "html5",
"html": f'<div class="card"><h1>SAMPLE RATE</h1>{rows}</div>',
"css": CSS,
}
def build_edit(rates, length):
voice = {"type": "audio", "src": SOURCES["Voice"]}
music = {
"type": "audio",
"src": SOURCES["Music"],
"volume": MUSIC_VOLUME,
"effect": "fadeOut",
}
full_frame = {"width": 1920, "height": 1080}
tracks = [(card(rates), full_frame), (voice, {}), (music, {})]
return {
"timeline": {
"tracks": [
{"clips": [{"asset": asset, "start": 0, "length": length, **size}]}
for asset, size in tracks
]
},
"output": {"format": "mp4", "resolution": "1080", "fps": 25},
}
if __name__ == "__main__":
if not os.environ.get("SHOTSTACK_API_KEY"):
sys.exit("Set SHOTSTACK_API_KEY first.")
rates, length = {}, None
for name, url in SOURCES.items():
audio, duration = probe(url)
print(f"{name:6} {describe(audio)}, {duration:.1f} s")
if audio is not None:
rates[name] = int(audio["sample_rate"])
if name == "Voice":
length = round(duration, 2)
render_id = call(f"{API}/render", build_edit(rates, length))["response"]["id"]
print("Render queued:", render_id)
while True:
time.sleep(5)
status = call(f"{API}/render/{render_id}")["response"]
if status["status"] == "done":
break
if status["status"] == "failed":
sys.exit(f"Render failed: {status.get('error')}")
audio, duration = probe(status["url"])
print(f"Output {describe(audio)}, {duration:.1f} s")
print(status["url"])Run python sample_rates.py. It prints each source, the render ID, the probed output, and the URL of the video:
Voice mp3, 48000 Hz, 2 channels, 21.9 s
Music mp3, 44100 Hz, 2 channels, 80.2 s
Render queued: 6f8cca83-7229-48ba-b2ad-f18ce8bec0f8
Output aac, 48000 Hz, 2 channels, 21.9 s
https://shotstack-api-v1-output.s3-ap-southeast-2.amazonaws.com/35tqpmb0ya/6f8cca83-7229-48ba-b2ad-f18ce8bec0f8.mp4This is the rendered video. The card shows the two rates that the script read from the sources:
How the script works
probe()sends the file’s URL, encoded, to the Probe endpoint. It returns the first stream whosecodec_typeisaudio, and the file’s duration.describe()turns that stream into one line: codec, sample rate and channels.- The loop probes each source. The video is as long as the voice, so the voice’s duration becomes the
lengthof every clip. build_edit()puts three clips on three tracks: an HTML5 card that shows the rates, the voice, and the music withvolumeset to 0.2 and afadeOuteffect.- After the render, the script probes the finished MP4 the same way. You see the output’s sample rate next to the inputs.
What the rendered audio shows
The output had one AAC track at 48 kHz in stereo, 21.9 seconds long, which is the length of the voice. Its loudest peak was −3.3 dBFS, so nothing clipped where the voice and the music overlap.
To check that the conversion kept pitch and level, we generated two test tones as WAV files and rendered them with the same API. The first was a 1 kHz sine wave in a 44.1 kHz WAV file that peaks at −20 dBFS. The second added a 440 Hz tone saved at 24 kHz on another track.
| Render | Output sample rate | Strongest frequencies in the output |
|---|---|---|
| 1 kHz tone at 44.1 kHz | 48 kHz | 1,000.0 Hz |
| 1 kHz tone at 44.1 kHz, plus a 440 Hz tone at 24 kHz | 48 kHz | 1,000.0 Hz and 440.0 Hz |
- The 1 kHz tone kept its exact pitch. If 44.1 kHz samples were played back as 48 kHz, the tone would rise to about 1,088 Hz.
- Its level stayed the same. A sine wave that peaks at −20 dBFS has an RMS level of about −23 dB, and the output measured −23.0 dB.
- The 24 kHz tone came through at 440 Hz next to the 1 kHz tone, in the same 48 kHz track.
Tips for working with sample rates
- Record at 48 kHz. Set cameras, recorders and microphones to 48 kHz for anything that ends up as video, so the voice already matches the picture.
- Keep the original files. In our test, the render turned the 44.1 kHz music into the 48 kHz output, so there was no need to convert it first.
- Don’t convert up to add quality. A 24 kHz file converted to 48 kHz gains no high frequencies. The highest note it can hold stays at 12 kHz.
- Keep the music under the voice. The script sets one music
volumefor the whole video. To lower the music only while someone speaks, see our guide to audio ducking with the Edit API.
Check and mix your audio with Shotstack
You now know what the sample rate numbers mean and why video uses 48 kHz. The script checks each source with the Probe endpoint, mixes music and voice into one video, and confirms the rate of the result. Swap the two URLs in SOURCES for your own voice and music files and run it again.
Create a free Shotstack account to get your API key and check your first files.
Frequently asked questions (FAQs)
Is 48 kHz better quality than 44.1 kHz?
Not in a way people can hear. The extra range sits between 22.05 kHz and 24 kHz, above the 20,000 Hz top of the commonly given hearing range. An uncompressed 48 kHz file is about 9% larger than the same sound at 44.1 kHz, because it stores 48,000 samples a second instead of 44,100.
Do I need to convert 44.1 kHz music to 48 kHz before a Shotstack render?
No, not in our test. The 44.1 kHz music and the 48 kHz voice came out as one 48 kHz track, and a 1 kHz test tone at 44.1 kHz kept its pitch and level in the 48 kHz output.
Why does audio sound fast and high after a sample rate mix-up?
A player that reads 44.1 kHz samples as 48 kHz plays them about 8.8% too fast. Every note rises by the same amount, so a 1 kHz tone becomes about 1,088 Hz. A 60 second track ends after about 55 seconds, so the sound drifts ahead of the picture.
How do I read the sample rate from the Probe response?
Find the stream in response.metadata.streams whose codec_type is audio, and read its sample_rate. The value is a string, such as "48000", so convert it to a number before you compare it. The script above does this with int(audio["sample_rate"]).