03:00:0050% OFFClaim

Use this local workflow when a handoff needs a repeatable loudness observation tied to the exact input file. The deliverable is a JSON report and the meter log. The script writes no processed audio and applies no normalization target. Its sample-peak field helps compare signals; that field alone does not predict programme loudness.

Measure integrated loudness without changing the audio

Deliver a report before choosing a level change

Use this local workflow when a handoff needs a repeatable loudness observation tied to the exact input file. The deliverable is a JSON report and the meter log. The script writes no processed audio and applies no normalization target. Its sample-peak field helps compare signals; that field alone does not predict programme loudness.

This candidate is a separate Python/FFmpeg workflow, not a button currently available in the website. Use audio you own or are authorized to process. The numerical examples below are synthesized locally from the displayed code, with no paid API call.

What the measurement covers

Use an existing Python 3.12 and FFmpeg 6.0 executable on macOS. The tested environment is macOS 14.5, Python 3.12.3, and FFmpeg 6.0. No dependency is installed by these files. Inputs are nonempty uncompressed signed PCM16 WAV, mono or stereo, exactly 48,000 Hz, from 0.4 through 120 seconds and smaller than 16,000,000 bytes. The RIFF reader rejects a partial audio frame and accepts valid odd metadata padding. It copies input bytes into a private temporary directory, reads that fixed snapshot, and checks the original file hash again before committing the report.

Mono is measured as one channel: dual-mono compensation is explicitly disabled. Stereo is measured with its declared two channels. Surround layouts, LFE routing, other sample rates, compressed uploads and browser decoding are outside this candidate. The script does not automatically convert them.

The command uses ebur128 with peak=none and a null output. The report retains the final integrated-loudness and relative-threshold text values at the meter's one-decimal display precision. It does not reconstruct block membership. A zero signal, or a reported integrated value at or below the conservative -69.9 LUFS display-floor policy, produces a null integratedLufs and NO_REPORTABLE_MEASUREMENT. That status is not a failed FFmpeg launch, and exit code zero is not evidence of programme-level compliance.

Complete local source

Create a fresh folder and save all four files below under their exact names. Keep the three Python files and version.json together. The lock binds all three Python source files. Every --out destination must be new, and its parent must already exist. Report commit uses the same macOS exclusive-rename helper as the bounded PCM work; other operating systems are not supported here. Catchable errors clean up the temporary directory; abrupt process termination or power loss can leave a temporary sibling.

media_core.py

from pathlib import Path
import ctypes, errno, hashlib, io, json, math, os, struct, sys, wave

class ValidationError(Exception):
    pass

def require(ok, code):
    if not ok:
        raise ValidationError(code)

def sha(path):
    return hashlib.sha256(Path(path).read_bytes()).hexdigest()

def dump(path, value):
    Path(path).write_text(json.dumps(value, ensure_ascii=False, indent=2,
                                    sort_keys=True, allow_nan=False)+'\n')

def read_contract(path):
    p=Path(path)
    require(p.is_file() and not p.is_symlink(), 'CONTRACT_FILE')
    require(p.stat().st_size <= 4096, 'CONTRACT_SIZE')
    def invalid_constant(value):
        raise ValidationError('CONTRACT_NONFINITE')
    try:
        data=json.loads(p.read_text(), parse_constant=invalid_constant)
    except (ValueError, UnicodeError):
        raise ValidationError('CONTRACT_JSON')
    require(type(data) is dict, 'CONTRACT_OBJECT')
    return data

def contract_keys(c, required):
    require(type(c) is dict, 'CONTRACT_OBJECT')
    require(set(required) <= set(c), 'CONTRACT_MISSING_KEY')
    require(set(c) <= set(required) | {'purpose'}, 'CONTRACT_UNKNOWN_KEY')
    if 'purpose' in c:
        require(type(c['purpose']) is str, 'CONTRACT_PURPOSE')

def riff_pcm_layout(data):
    require(len(data)>=12 and data[:4]==b'RIFF' and data[8:12]==b'WAVE', 'WAV_RIFF_HEADER')
    riff_end=8+struct.unpack_from('<I',data,4)[0]
    require(riff_end<=len(data), 'TRUNCATED_PCM')
    require(riff_end==len(data), 'WAV_RIFF_SIZE')
    offset=12;fmt=None;audio=None
    while offset<riff_end:
        require(offset+8<=riff_end, 'WAV_CHUNK_HEADER')
        kind=data[offset:offset+4]
        size=struct.unpack_from('<I',data,offset+4)[0]
        start=offset+8;end=start+size
        require(end<=riff_end, 'TRUNCATED_PCM' if kind==b'data' else 'WAV_CHUNK_BOUNDS')
        # RIFF chunkSize excludes the one-byte pad for odd payload sizes.
        next_offset=end+(size&1)
        require(next_offset<=riff_end, 'WAV_CHUNK_PAD')
        if kind==b'fmt ':
            require(fmt is None and audio is None, 'WAV_FMT_ORDER')
            require(size>=16, 'WAV_FMT_SIZE')
            fmt=struct.unpack_from('<HHIIHH',data,start)
        elif kind==b'data':
            require(fmt is not None, 'WAV_FMT_ORDER')
            require(audio is None, 'WAV_MULTIPLE_DATA')
            audio=(start,size)
        offset=next_offset
    require(fmt is not None and audio is not None, 'WAV_REQUIRED_CHUNK')
    tag,channels,rate,byte_rate,block_align,bits=fmt
    require(tag in (1,0xfffe) and channels in (1,2) and bits==16, 'PCM16_ONLY')
    require(block_align==channels*2, 'PCM_BLOCK_ALIGN')
    require(audio[1]%block_align==0, 'PCM_PARTIAL_FRAME')
    return rate,channels,block_align,audio[0],audio[1]

def pcm_read(path):
    p=Path(path)
    require(p.is_file() and not p.is_symlink(), 'INPUT_FILE')
    require(p.stat().st_size < 16000000, 'INPUT_TOO_LARGE')
    data=p.read_bytes()
    require(len(data)<16000000, 'INPUT_TOO_LARGE')
    expected_rate,expected_ch,frame_bytes,audio_start,audio_bytes=riff_pcm_layout(data)
    try:
        with wave.open(io.BytesIO(data), 'rb') as w:
            require(w.getsampwidth()==2 and w.getnchannels() in (1,2)
                    and w.getcomptype()=='NONE', 'PCM16_ONLY')
            rate,ch,n=w.getframerate(),w.getnchannels(),w.getnframes()
            require((rate,ch,n)==(expected_rate,expected_ch,audio_bytes//frame_bytes), 'PCM_GEOMETRY')
            require(n>0, 'EMPTY_PCM')
            require(0<rate<=384000, 'SAMPLE_RATE')
            require(n*ch*2<16000000, 'INPUT_TOO_LARGE')
            raw=w.readframes(n)
    except (wave.Error, EOFError, struct.error):
        raise ValidationError('WAV_INVALID')
    require(len(raw)==audio_bytes and raw==data[audio_start:audio_start+audio_bytes], 'TRUNCATED_PCM')
    return rate,ch,list(struct.unpack('<'+'h'*(len(raw)//2),raw))

def pcm_write(path, rate, channels, samples):
    require(type(rate) is int and 0<rate<=384000, 'SAMPLE_RATE')
    require(type(channels) is int and channels in (1,2), 'PCM_CHANNELS')
    require(samples and len(samples)%channels==0, 'PCM_CHANNEL_ALIGNMENT')
    require(all(type(v) is int and -32768<=v<=32767 for v in samples), 'PCM_RANGE')
    with wave.open(str(path), 'wb') as w:
        w.setparams((channels,2,rate,0,'NONE','not compressed'))
        w.writeframes(struct.pack('<'+'h'*len(samples),*samples))

def rms(samples):
    return math.sqrt(sum(x*x for x in samples)/len(samples))

def commit_directory(stage, target):
    require(sys.platform=='darwin', 'ATOMIC_PLATFORM_UNSUPPORTED')
    libc=ctypes.CDLL(None, use_errno=True)
    require(hasattr(libc,'renamex_np'), 'ATOMIC_API_UNAVAILABLE')
    rename=libc.renamex_np
    rename.argtypes=[ctypes.c_char_p,ctypes.c_char_p,ctypes.c_uint]
    rename.restype=ctypes.c_int
    if rename(os.fsencode(stage),os.fsencode(target),4)!=0:
        code=ctypes.get_errno()
        if code in (errno.EEXIST,errno.ENOTEMPTY):
            raise ValidationError('OUTPUT_EXISTS')
        raise ValidationError('ATOMIC_COMMIT_FAILED:'+str(code))

measure.py

from pathlib import Path
import argparse, hashlib, json, math, os, re, shutil, subprocess, sys, tempfile
from media_core import ValidationError, require, pcm_read, commit_directory, dump, sha

VERSION = '0.1.0'

def main():
    ap=argparse.ArgumentParser()
    ap.add_argument('--ffmpeg',required=True)
    ap.add_argument('--input',required=True)
    ap.add_argument('--out',required=True)
    args=ap.parse_args()
    code=Path(__file__).resolve().parent
    lock=json.loads((code/'version.json').read_text())
    require(lock.get('version')==VERSION,'VERSION_LOCK')
    require(set(lock.get('files',{}))=={'media_core.py','measure.py','make-inputs.py'},'LOCK_FILES')
    for name,digest in lock['files'].items():
        require(sha(code/name)==digest,'CODE_HASH:'+name)
    source=Path(args.input).absolute()
    require(source.is_file() and not source.is_symlink(),'INPUT_FILE')
    require(source.stat().st_size<16000000,'INPUT_TOO_LARGE')
    data=source.read_bytes()
    require(len(data)<16000000,'INPUT_TOO_LARGE')
    original_hash=hashlib.sha256(data).hexdigest()
    target=Path(args.out).absolute()
    require(target.parent.is_dir() and not target.parent.is_symlink(),'OUTPUT_PARENT')
    require(not os.path.lexists(target),'OUTPUT_EXISTS')
    binary=Path(args.ffmpeg).resolve()
    require(binary.is_file() and os.access(binary,os.X_OK),'FFMPEG_EXECUTABLE')
    require(sys.platform=='darwin','ATOMIC_PLATFORM_UNSUPPORTED')
    stage=Path(tempfile.mkdtemp(prefix='.'+target.name+'-',dir=target.parent))
    try:
        # The decoder reads this fixed snapshot; the user's source is never an output.
        staged_input=stage/'input.wav';staged_input.write_bytes(data)
        rate,ch,samples=pcm_read(staged_input)
        frames=len(samples)//ch
        require(rate==48000,'RATE_48000_REQUIRED')
        require(19200<=frames<=5760000,'DURATION_0_4_TO_120_SECONDS')
        version_args=[str(binary),'-version']
        version=subprocess.run(version_args,capture_output=True,text=True,timeout=10)
        require(version.returncode==0,'FFMPEG_VERSION_FAILED')
        first_line=version.stdout.splitlines()[0] if version.stdout else ''
        require(re.match(r'^ffmpeg version 6\.0(?:\s|[-.])',first_line) is not None,'FFMPEG_6_0_REQUIRED')
        argv=[str(binary),'-hide_banner','-nostdin','-nostats','-loglevel','info',
              '-i',str(staged_input),'-map','0:a:0','-af',
              'ebur128=peak=none:dualmono=false','-f','null','-']
        result=subprocess.run(argv,capture_output=True,text=True,timeout=60)
        require(result.returncode==0,'FFMPEG_SCAN_FAILED')
        found=re.findall(r'Integrated loudness:\s*I:\s*([-+0-9.inf]+) LUFS\s*Threshold:\s*([-+0-9.inf]+) LUFS',result.stderr)
        require(bool(found),'SUMMARY_MISSING')
        integrated,threshold=map(float,found[-1])
        require(math.isfinite(integrated) and math.isfinite(threshold),'SUMMARY_NONFINITE')
        require(sha(staged_input)==original_hash,'SNAPSHOT_CHANGED')
        require(sha(source)==original_hash,'INPUT_CHANGED')
        sample_peak=max(abs(v) for v in samples)
        # FFmpeg can emit a finite display floor with exit 0 for empty gated energy.
        # This is a conservative report policy, not a reconstruction of gate membership.
        eligible=sample_peak>0 and integrated>-69.9
        reason=None if eligible else ('ZERO_SAMPLES' if sample_peak==0 else 'DISPLAY_FLOOR_OR_NO_REPORTABLE_ENERGY')
        report={
            'schema':'original-loudness-observation-v1','version':VERSION,
            'input':{'name':source.name,'bytes':len(data),'sha256':original_hash,
                     'sampleRate':rate,'channels':ch,'frames':frames,'durationSeconds':frames/rate,
                     'signedPcm16SamplePeak':sample_peak},
            'inputUnchangedAtFinalHashCheck':True,
            'measurementStatus':'MEASURED' if eligible else 'NO_REPORTABLE_MEASUREMENT',
            'reason':reason,
            'integratedLufs':integrated if eligible else None,
            'rawFfmpegIntegratedLufs':integrated,
            'rawFfmpegRelativeThresholdLufs':threshold,
            'reportedPrecisionLufs':0.1,
            'dualMonoCompensation':False,
            'meter':{'versionLine':first_line,'binarySha256':sha(binary),'argv':argv,
                     'exitCode':result.returncode,'logSha256':hashlib.sha256(result.stderr.encode()).hexdigest()},
            'codeLock':lock,'python':sys.version,
            'audioOutputWritten':False,'targetNormalizationApplied':False,
            'truePeakMeasured':False,'gateMembershipReconstructed':False,
            'standardsCertification':False,'deliveryTargetPass':None,
            'scope':'Bounded FFmpeg 6.0 observation; mono/stereo PCM16 at 48000 Hz, 0.4–120 seconds.'}
        staged_input.unlink()
        (stage/'ffmpeg.log').write_text(result.stderr)
        dump(stage/'execution.json',{'versionArgv':version_args,'versionExitCode':version.returncode,
                                    'versionStdout':version.stdout,'versionStderr':version.stderr,
                                    'scanArgv':argv,'scanExitCode':result.returncode,'scanStdout':result.stdout})
        dump(stage/'report.json',report)
        dump(stage/'version.json',lock)
        commit_directory(stage,target)
        print(json.dumps({'out':str(target),'measurementStatus':report['measurementStatus'],
                          'integratedLufs':report['integratedLufs']},allow_nan=False))
    finally:
        if stage.exists():shutil.rmtree(stage)

if __name__=='__main__':
    try:main()
    except (ValidationError, OSError, ValueError, subprocess.TimeoutExpired) as error:
        print(str(error),file=sys.stderr);sys.exit(2)

make-inputs.py

from pathlib import Path
import argparse, json, math, shutil, tempfile
from media_core import require, pcm_write, commit_directory, dump

ap=argparse.ArgumentParser()
ap.add_argument('--out',required=True)
a=ap.parse_args()
out=Path(a.out).absolute()
require(out.parent.is_dir() and not out.exists() and not out.is_symlink(),'NEW_OUTPUT_REQUIRED')
stage=Path(tempfile.mkdtemp(prefix='.'+out.name+'-',dir=out.parent))
try:
    rate=48000
    cycle=[round(10000*math.sin(2*math.pi*i/48)) for i in range(48)]
    steady=cycle*10000
    quieter=cycle*1000+[round(v/2) for v in cycle]*9000
    signals={'steady':steady,'same-peak-lower-body':quieter,
             'same-peak-lower-body-plus-silence':quieter+[0]*(rate*10),
             'all-zero':[0]*(rate*2)}
    for name,samples in signals.items():pcm_write(stage/(name+'.wav'),rate,1,samples)
    dump(stage/'construction.json',{'sampleRate':rate,'channels':1,'bits':16,
         'steady':'10 seconds of a 1000 Hz numeric sine, integer amplitude 10000',
         'same-peak-lower-body':'first second identical; remaining 9 seconds half integer amplitude',
         'same-peak-lower-body-plus-silence':'previous signal followed by 10 seconds of zero samples',
         'all-zero':'2 seconds of zero samples','externalAudioUsed':False})
    commit_directory(stage,out)
    print(json.dumps({'out':str(out),'files':len(signals)}))
finally:
    if stage.exists():shutil.rmtree(stage)

version.json

{
  "version": "0.1.0",
  "files": {
    "media_core.py": "02252e46781c5fea4cdb03b13d3d7eaacb09a7b86d2dbbfaebc8b623ba42fe86",
    "measure.py": "81e0b391d35f81611ff726d64b4d6616d507d5de3369b7c8df1c7d3a60200724",
    "make-inputs.py": "dc3744bf19251042b4417c4140e799fec6c29dfafb8a8dd05878c23e02e0ec6b"
  },
  "status": "author-candidate-pending-independent-review"
}

Run with your own input

Replace the executable and input paths with existing local files, then run from the folder containing the four source files.

python3 measure.py --ffmpeg "/absolute/path/to/ffmpeg" --input "/absolute/path/to/owned.wav" --out "my-report"

The new directory contains report.json, ffmpeg.log, execution.json and version.json. There is no output WAV. The report records the input SHA-256, frame geometry, binary SHA-256, version line, exact meter arguments, source lock and final hash check. The temporary input path in the recorded arguments identifies the snapshot used in that run; it is removed before the result directory is committed.

Read measurementStatus first. MEASURED means this bounded parser obtained an eligible finite observation, not that the file passes a distributor's specification. NO_REPORTABLE_MEASUREMENT keeps the raw meter values for diagnosis and leaves integratedLufs null. Wrong sample rate, unsupported duration, malformed PCM, a changed code lock or an existing output directory is an error; correct the cause and choose a fresh output path.

Construct and compare numerical examples

python3 make-inputs.py --out "my-inputs"
python3 measure.py --ffmpeg "/absolute/path/to/ffmpeg" --input "my-inputs/steady.wav" --out "steady-report"
python3 measure.py --ffmpeg "/absolute/path/to/ffmpeg" --input "my-inputs/same-peak-lower-body.wav" --out "quieter-report"
python3 measure.py --ffmpeg "/absolute/path/to/ffmpeg" --input "my-inputs/same-peak-lower-body-plus-silence.wav" --out "silence-report"
python3 measure.py --ffmpeg "/absolute/path/to/ffmpeg" --input "my-inputs/all-zero.wav" --out "zero-report"

The first signal is a ten-second numerical 1 kHz sine with sample peak 10000. The second retains the same first second and peak, then uses half amplitude for nine seconds. The third appends ten seconds of zero samples to the second. The last is two seconds of zeros. On the pinned FFmpeg 6.0 binary, prior bounded observations were -13.3, -18.3 and -18.4 LUFS; their logged thresholds were -23.3, -28.3 and -28.4 LUFS. The zero signal logged -70.0 LUFS and threshold 0.0, but its reportable value is null. These numbers are reference observations for these exact constructed inputs, not universal predictions for speech or music.

The two nonzero ten-second inputs have equal peak but a 5.0 LU difference in this observation. Appending the zero tail changes the reported value by -0.1 LU. The latter illustrates this meter's gated result on this example; it does not prove that silence always has zero effect. Recreate the examples and inspect both the report and raw log. Never accept a report solely because its directory exists.

Use the result in an editing handoff

Include the report with the exact source hash, and state whether mono was intended as mono or later playback on two channels. If you decide to change the level, use the editor or a normalization workflow, then measure the resulting file separately and retain both reports. This workflow reports an observation tied to the input file; it does not change the audio or choose a normalization target.

Do not label this report EBU-certified, BS.1770-certified, true-peak-safe or ready for every delivery platform. A reportable integrated value cannot by itself establish a true-peak limit, a channel-layout decision or the receiving service's acceptance. The current author checks cover constructed signals and selected input/output guards. Independent numerical gate reconstruction, standardized meter test vectors, diverse real recordings, stereo numerical calibration and publication admission remain separate work.

First-party references

The distinction between peak and programme loudness is grounded in EBU R 128 version 5.0. The algorithm reference is ITU-R BS.1770-5. The command and options follow the FFmpeg 6.0 filter documentation. These references describe their own methods; they do not certify this wrapper or grant publication rights to its examples. This text is an original explanation; no standards document or third-party audio is bundled.

Prepare your input

Replace the executable and input paths with existing local files, then run from the folder containing the four source files.

Use an existing Python 3.12 and FFmpeg 6.0 executable on macOS. The tested environment is macOS 14.5, Python 3.12.3, and FFmpeg 6.0. No dependency is installed by these files. Inputs are nonempty uncompressed signed PCM16 WAV, mono or stereo, exactly 48,000 Hz, from 0.4 through 120 seconds and smaller than 16,000,000 bytes. The RIFF reader rejects a partial audio frame and accepts valid odd metadata padding. It copies input bytes into a private temporary directory, reads that fixed snapshot, and checks the original file hash again before committing the report.

Mono is measured as one channel: dual-mono compensation is explicitly disabled. Stereo is measured with its declared two channels. Surround layouts, LFE routing, other sample rates, compressed uploads and browser decoding are outside this candidate. The script does not automatically convert them.

The first signal is a ten-second numerical 1 kHz sine with sample peak 10000. The second retains the same first second and peak, then uses half amplitude for nine seconds. The third appends ten seconds of zero samples to the second. The last is two seconds of zeros. On the pinned FFmpeg 6.0 binary, prior bounded observations were -13.3, -18.3 and -18.4 LUFS; their logged thresholds were -23.3, -28.3 and -28.4 LUFS. The zero signal logged -70.0 LUFS and threshold 0.0, but its reportable value is null. These numbers are reference observations for these exact constructed inputs, not universal predictions for speech or music.

  • Optional: Use an existing Python 3.12 and FFmpeg 6.0 executable on macOS. The tested environment is macOS 14.5, Python 3.12.3, and FFmpeg 6.0. No dependency is installed by these files. Inputs are nonempty uncompressed signed PCM16 WAV, mono or stereo, exactly 48,000 Hz, from 0.4 through 120 seconds and smaller than 16,000,000 bytes. The RIFF reader rejects a partial audio frame and accepts valid odd metadata padding. It copies input bytes into a private temporary directory, reads that fixed snapshot, and checks the original file hash again before committing the report.
Measure integrated loudness without changing the audio