Converts ordinary video files into .bnd bundles that the
Video Player app by LTVA can play on
a Flipper Zero, using FFmpeg for all the heavy lifting.
This is a modified version of JacobTDC's converter.py. All of the original conversion logic — dithering, scaling, frame-rate conversion, the bundle format, the sample-drop desync fix — is JacobTDC's work. The additions here are about audio volume: the player app has no volume control, so a video whose audio was mastered quietly ends up nearly inaudible on the Flipper's speaker. This version measures and normalizes the audio so it comes out loud and clear.
| Change | Detail |
|---|---|
| Audio normalization | New -n/--normalize option with four modes, defaulting to dynamic. See Audio normalization. |
| Audio tuning knobs | -l/--level, -c/--compress, --highpass and -g/--gain to adjust the normalization. |
| Custom FFmpeg paths | --ffmpeg / --ffprobe (or the FFMPEG_BINARY / FFPROBE_BINARY environment variables) to use a specific FFmpeg build instead of whatever is on PATH. |
| Readable errors | If FFmpeg can't be found or Windows refuses to start it, you get an explanation instead of a Python traceback. |
| Extra info line | The summary printed before conversion now includes what normalization will be applied, plus the measured levels. |
Nothing else about the output changed: running with -n none produces a .bnd file that is
byte-for-byte identical to the original script's output.
- Python 3.8 or newer
- FFmpeg — both the
ffmpegandffprobeexecutables - ffmpeg-python — the Python wrapper module
Windows: install from python.org (tick Add python.exe to
PATH during setup) or run winget install Python.Python.3.13.
macOS: brew install python
Linux: it's almost certainly already installed; otherwise sudo apt install python3 python3-pip.
FFmpeg is a separate program, not a Python package. You need it on your PATH (or you can
point the script at it later with --ffmpeg / --ffprobe).
Windows — winget (easiest):
winget install Gyan.FFmpegThen close and reopen your terminal so the new PATH takes effect.
Windows — manual: download a build from gyan.dev or
BtbN, extract it somewhere permanent (e.g.
C:\ffmpeg), then add the bin folder to your PATH:
Settings → System → About → Advanced system settings → Environment Variables → Path → Edit →
New → C:\ffmpeg\bin
macOS: brew install ffmpeg
Linux: sudo apt install ffmpeg (Debian/Ubuntu), sudo dnf install ffmpeg (Fedora),
sudo pacman -S ffmpeg (Arch)
pip install ffmpeg-python
ffmpeg-python, not ffmpeg or python-ffmpeg. Those are
different, incompatible packages and the script will not work with them.
ffmpeg -version
ffprobe -version
python -c "import ffmpeg; print('ffmpeg-python ok')"
All three should print something rather than an error. If they do, you're ready.
python converter.py source.mp4 output.bnd
That's it — the defaults pick the best fit for the Flipper's 128×64 screen, keep the source
frame rate and sample rate, apply sierra3 dithering, and normalize the audio.
A more explicit example matching what the player app documents (mono audio at 44.1 kHz, 15 or 30 fps):
python converter.py source.mp4 output.bnd -f 30 -r 44100
Then copy the .bnd file to your Flipper's SD card under:
apps_data/video_player/
…and open it from the Video Player app. If playback runs at the wrong speed or drifts out of
sync, try the explicit -f 30 -r 44100 above — the player app's own documentation describes
its format as mono 8-bit PCM at 44100 Hz with a 15 or 30 fps video track.
Heads up on file size. These bundles are raw, uncompressed video and audio. A minute of 128×64 @ 30 fps with 44.1 kHz audio is about 4.3 MB. The script prints an estimate before it starts.
| Option | Description |
|---|---|
-d, --dither ALGORITHM |
Dithering algorithm: bayer, heckbert, floyd_steinberg, sierra2, sierra2_4a, sierra3, burkes, atkinson, none. Default sierra3. |
--bayer-scale N |
Crosshatch scale (0–5) for -d bayer. |
-t, --threshold N |
Plain black/white threshold (0–256) instead of dithering. Mutually exclusive with -d. |
-f, --frame-rate N |
Output frame rate; may be a fraction like 30000/1001. Defaults to the source. |
-s, --scale WxH |
Output size, up to 128×64. Default is the largest fit that keeps the aspect ratio. Width is padded up to a multiple of 8 automatically. |
| Option | Description |
|---|---|
-r, --sample-rate N |
Output sample rate in Hz. Defaults to the source. |
-n, --normalize MODE |
dynamic (default), loudness, peak, or none. See below. |
-l, --level N |
Target level for loudness (LUFS, default −9) or peak (dBFS, default −1). |
-c, --compress N |
Dynamic-range compression before normalizing, 1–30 where lower means more compression, or 0 to disable. Default 5. dynamic mode only. |
--highpass N |
Roll off audio below N Hz before normalizing. Default 200. 0 disables. |
-g, --gain N |
Extra gain in dB applied after normalization. Default 0. |
| Option | Description |
|---|---|
--ffmpeg PATH |
Which ffmpeg executable to use. Also settable via FFMPEG_BINARY. |
--ffprobe PATH |
Which ffprobe executable to use. Also settable via FFPROBE_BINARY. |
-q, --quiet |
Suppress info output. Use twice (-qq) to also suppress warnings. |
The Flipper's speaker is perfectly capable of being heard across a room — the problem is that most videos are mastered with their peaks well below full scale, and the player app has no volume control to make up the difference. So the script does it at conversion time.
Every mode except none works on exactly the signal the Flipper will play (mono, at the final sample rate),
rolls off everything below 200 Hz first (the speaker can't reproduce it, and it only eats
headroom), and finishes with a limiter so nothing clips into the 8-bit output.
dynamic (default) — loudest, best for speech and most videos
Measures the file, lifts the whole thing up to just under full scale, then runs a dynamic normalizer that keeps every passage near that ceiling. A whispered line and an explosion come out at similar volume. This is what you want if the goal is "I can hear it without holding the Flipper to my ear."
python converter.py source.mp4 output.bnd
loudness — broadcast-style, more natural
Two-pass EBU R128 normalization to −9 LUFS (adjustable with -l). Applies one constant gain to
the whole file, so the original dynamics are preserved. Quiet scenes stay quiet.
python converter.py source.mp4 output.bnd -n loudness
python converter.py source.mp4 output.bnd -n loudness -l -6 # louder
peak — most conservative
Two-pass peak normalization: finds the loudest sample and scales the file so it lands at
−1 dBFS (adjustable with -l). Nothing else is touched. If a video is already well mastered
and just needs a lift, this is the transparent option.
none — original behavior
No audio processing at all. Produces output identical to the original script.
- Still not loud enough? Add
-g 3(or more). The limiter keeps it from clipping, but pushing hard will start to sound crunchy. - Sounds over-compressed or "pumpy"? Raise the compress factor (
-c 15), or switch to-n loudness. - Hearing hiss or background noise in quiet moments? That's normalization doing its job on a
noisy source — try
-n loudnessor-n peakinstead, which don't lift quiet passages. - Want the bass back?
--highpass 0. It will sound fuller on headphones and no different on the Flipper, but you lose some loudness. - Nearly silent audio track? The script detects it (peak below −60 dBFS), skips normalization and warns instead of amplifying pure noise.
What it means: Windows blocked the FFmpeg executable from starting. This is
Smart App Control
(on by default on clean installs of Windows 11 22H2 and newer), and it is judging
ffmpeg.exe / ffprobe.exe, not this script. The script's only involvement is asking Windows
to run FFmpeg; Python reports whatever Windows says back.
Confirm it for yourself — in PowerShell, run:
ffmpeg -version
ffprobe -versionIf either one fails with the same message, the block has nothing to do with the script.
Why it can appear out of nowhere: Smart App Control checks each binary against Microsoft's
cloud reputation service. Most FFmpeg Windows builds are not code-signed, so the verdict
depends on that build's reputation — which means a winget upgrade that swaps in a new
FFmpeg build, or a change on Microsoft's side, can flip a binary from allowed to blocked
without you changing anything.
Options, roughly in order of least disruption:
-
Try a different FFmpeg build. The policy decides per binary, so another build may run fine. For example:
winget install BtbN.FFmpeg.GPL
or
Gyan.FFmpeg.Shared, or a manual download from gyan.dev / BtbN. Then point the script at it:python converter.py source.mp4 output.bnd --ffmpeg "C:\ffmpeg\bin\ffmpeg.exe" --ffprobe "C:\ffmpeg\bin\ffprobe.exe"
or set it once per session:
$env:FFMPEG_BINARY = "C:\ffmpeg\bin\ffmpeg.exe" $env:FFPROBE_BINARY = "C:\ffmpeg\bin\ffprobe.exe"
-
Run the conversion in WSL, where Windows app-control policies don't apply:
wsl --installthen inside WSL:
sudo apt update && sudo apt install ffmpeg python3-pip pip install ffmpeg-python python3 converter.py /mnt/c/Users/you/Videos/source.mp4 output.bnd -
Turn Smart App Control off — Windows Security → App & browser control → Smart App Control → Off.
⚠️ This is one-way: Microsoft does not support turning it back on without reinstalling Windows, so treat it as a last resort. Microsoft does not provide per-app exclusions for Smart App Control.
A note on trust: no script can (or should) bypass a Windows Application Control policy — that's the whole point of the feature. Anything claiming to disable or work around Smart App Control from inside a script deserves your suspicion. All this script does is let you choose which FFmpeg binary it runs.
FFmpeg isn't on your PATH. Either add it (see Installation) or pass
--ffmpeg / --ffprobe with full paths. On Windows, remember to reopen your terminal after
changing PATH.
Install the wrapper: pip install ffmpeg-python. If you have several Pythons installed, use
python -m pip install ffmpeg-python so it lands in the interpreter you're actually running.
The bundle format requires both video and audio. If your source has no audio, add a silent track first:
ffmpeg -i source.mp4 -f lavfi -i anullsrc=r=44100:cl=mono -shortest -c:v copy -c:a aac with_audio.mp4
The audio and video streams in the source don't have exactly the same duration, so one ran out
before the other. Usually harmless, but if playback cuts short, re-encode the source so both
streams are the same length (-shortest).
Try an exact -f 15 or -f 30 with -r 44100. Fractional source frame rates (29.97) mean the
converter has to drop or duplicate frames, and the player app's timing assumes the rates it
documents.
- Original converter: JacobTDC — all of the bundle format handling, video processing and A/V sync logic.
- Player app: Video Player by LTVA (Flipper Lab).
- Audio normalization additions: this fork.
The original gist does not state a license, so it remains under whatever terms JacobTDC chooses; credits to them for the original work.
The modifications in this version (audio normalization, FFmpeg path options, error handling and this README) are released under the MIT License:
Copyright (c) 2026 Yahya Hammad
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.