Please remember: this is a community reverse-engineering mod, not an official HiBy release. Everything below was verified by diffing a full unpacked stock rootfs against the modded one, file by file. Test it, break it, report back. Also, I'm not a developer — I'm just a curious reverse-engineering addict who can't resist poking at firmware to see what happens. 😄
I basically flip every switch I can find, cross my fingers, and release the modded firmware into the wild. There's no way I can test every single feature or setting on my own. So think of this as a community experiment: you break it, test it, report it, and together we'll figure out what actually works and what still needs tweaking. Every bug report, success story, and weird discovery helps make the next release better. Without your feedback, I'm just a guy enthusiastically pressing random buttons in an editor.
I am currently raising funds to purchase other HiBy devices to expand this modding ecosystem.
Your support directly funds the purchase of hardware for reverse engineering and firmware testing. Next Targeted devicees - Hiby R3 Pro II, Hiditzs AP80 Pro-X, Tempotec V1 and others
via USDT : 0xcb6989985389f0a983bb20582d97e665128735e1 ( BEP20 )
via Paypal : bidhata@gmail.com
Every dollar keeps this project alive and motivates future updates!
HiBy pushed V2.0 to their OTA server before posting a public download link anywhere, so I asked the server politely and it handed it over. Then I spent a weekend inside a MIPS binary with Ghidra and a lot of coffee. This is what I found.
The rule this time: nothing here is a guess from
strings. Every claim abouthiby_playerwas traced to the code that actually uses it. (Lesson learned the hard way. More on that in §9.)
The package: official r1.upt, MD5 c9577de224859fc76272ca340ed6eb82, 44,255,232 bytes. It came from otaserver.hiby.com, which redirects to oss.hiby.com/HiBy R1/en-us/2.0/…. Built 2026-10-09.
Surprise: the .upt is just an ISO 9660 image wearing a fancy extension. Same container as previous releases, so the repo's unpack_pack/unpack.sh and repack.sh still apply (with one catch, below).
ota_config.in current_version=0
ota_v0/ota_compatibility.in compatibility=hiby_r1_x1600_auto_rf_auto
ota_v0/ota_update.in kernel xImage 3,731,520 B md5 c446c0a3…
rootfs rootfs.squashfs 40,103,936 B md5 f50a0ca0…
ota_v0/ota_md5_<img>.<md5> per-chunk md5 list
ota_v0/xImage.NNNN.<md5> 8 × 512 KiB chunks
ota_v0/rootfs.squashfs.NNNN.<md5> 77 × 512 KiB chunks
ota_v0/ota_v0.ok
Things worth noticing:
- The compatibility string grew a suffix:
…_auto_rf_auto. One image now serves two different wireless chipsets and figures out at boot which one it's on. See §3; this is the biggest change in the whole release. - Integrity is MD5 only. No signature. Each chunk's file name carries the MD5 of the chunk before it, a cute little hash chain. It catches corrupted downloads, and it lets us roll our own firmware. 🎉
⚠️ The repo'srepack.shdoesn't writeota_v0/ota_compatibility.in. V2.0 ships one, so a V2.0 mod should too.
| Thing | Value |
|---|---|
| SoC | Ingenic X1600 family. MIPS32r2, 32-bit little-endian. Yes, MIPS, in 2026. Respect. |
| Kernel | Linux 4.4.94+ PREEMPT, gcc 5.2.0 (Ingenic r3.2.1), build #112, Fri Oct 9 12:04 CST 2026 |
| Userland | Buildroot 2020.02.1 + BusyBox 1.31.1 (built 2026-09-14) |
| Libraries | OpenSSL 1.1.1f, libcurl 7.68.0, BlueZ 5 (libbluetooth 3.19.2), bluealsa, ALSA, glib/dbus |
| Hostname | ingenic (nobody renamed it, and honestly, same) |
| Rootfs | read-only squashfs, 88 MB unpacked, 4,409 files |
| Your stuff | UBIFS volume userdata on /usr/data (/data is a symlink to it) |
| Script | What it does |
|---|---|
| S10mdev | creates device nodes |
| S11jpeg_display_shell | the boot logo. Reads a theme:N byte from /dev/mtd5 at 0x20000 and shows logo.jpeg, logo1.jpeg or logo2.jpeg to match your theme, then sets the backlight to 60 |
| S11module_driver_default | loads every kernel module in /module_driver |
| S21mount_ubifs | mounts userdata. If the mount fails it formats the partition and tries again. Bold. |
| S30dbus | dbus |
| S39_recovery.recovery | if /data/recovery_all exists: factory reset, then reboot |
| S40network | networking |
| S43wifi_bcm_init_config | MAC setup, Broadcom units only (bails out on AIC units) |
| S50sys_server | the sys_server daemon (clears /mnt/* first) |
| S52wifi_aic_init_config | 🆕 new: MAC setup for the AIC8800 |
| S80_bt_init | Bluetooth |
| S92_03_start_music_player | runs hiby_player.sh, which starts hiby_player and reboots the device if the player ever exits. No desktop to fall back to here. |
| T90adb | ADB gadget (VID 0x18d1 / PID 0xd002) |
bootmode.sh flips between two kernel slots by writing ota:kernel or ota:kernel2 to /dev/mtd5.
This is the headline, and it isn't in any changelog. A new script, module_driver/r1_wireless_select.sh, asks the AXP2101 power chip for the SoC ID and loads a different radio driver depending on the answer:
| SoC ID | SoC | Wireless chip | Driver |
|---|---|---|---|
| 0x0A00 | X1600E | Broadcom/Cypress (the original R1) | cywdhd.ko via cywdhd.r1 |
| 0x4A00 | X1600EN | AIC8800D40 | aic8800_bsp.ko, aic8800_btlpm.ko, aic8800_fdrv.ko |
| 0x4900 | X1600HN | AIC8800D40 | same as above |
| unknown | — | falls back to Broadcom | — |
So HiBy quietly swapped the Wi-Fi/Bluetooth chip (and the SoC variant) on newer units. Details:
- The verdict is saved to
/run/r1_wireless_type(bcmoraic), and other scripts check it. - On AIC units the Wi-Fi driver loads only when needed (
aic8800_fdrv.load) withdisable_5g=1. New-board R1s are 2.4 GHz only. axp2101.shgot a new knob,dcdc4_auto_x1600e_en_hn=1, which sets the core voltage per SoC variant.- New helpers came along:
aic_radio.sh,bt_enable_aic.sh,bt_test,cmd_gpio, and eveniperf3.
🚨 For modders, this matters a lot. If you bought an R1 recently, it may have the new X1600EN/HN + AIC8800 board. Mods built on ≤1.8b2 have no AIC drivers, so Wi-Fi and Bluetooth simply won't work on those units. Build new mods on 2.0, and don't touch
r1_wireless_select.sh,aic8800_*,lib/firmware/aic8800d40_sdio_fworS52wifi_aic_init_config.
| Part | Detail |
|---|---|
| DAC | Cirrus CS43131. I²C bus 3 is bit-banged on PB30/PB31 (old school), power PB02, reset PB21 |
| PMIC | AXP2101, charges to 4.40 V, 100 mA termination |
| Fuel gauge | CW2015 with a custom battery profile |
| Display | LG35583 panel, 480×800, SPI bus 5, PWM backlight on PC00 at 30 kHz (max 101) |
| Touch | CST8xx at 0x15 on I²C1, single touch |
| Keys | Power PC31, Next PC28 (GPIO). ADC keys: 164 Play/Pause, 114/115 Vol |
| Inline remote | sa_earpods_adc on ADC ch2 (play/pause, vol+, vol−) |
| LEDs | red PC01 (breathing!), blue PC02 |
| USB-C | TCS1421 (CFG0 PA09 / CFG1 PB24) |
| SD | msc1, removable, card-detect PB22, power PC25, 50 MHz |
sau.ko |
a small new "SmartAction" module exporting sa_set/get_sau_verify. Smells like a device-verification hook. Keeping an eye on it 👀 |
usr/bin/hiby_player: 5,103,852 bytes, MIPS32 ELF, stripped, dynamically linked (libasound, libcurl, OpenSSL 1.1, libqrencode, libz, libwpa_client).- One binary runs three products: HiBy R1, MiDi R1 and the unreleased R1 Pro. A config JSON decides which one it is.
🎚️ PEQ (parametric EQ), in layout/theme*/peq/hiby_peq.view and dialog/peq_filter.dlg
- Filter types: Low Shelf, Peaking, High Shelf. Fc 30–20,000 Hz, Q 0.2–20, gain ±9 dB, preamp −9…+2 dB.
- Presets live in
/data/peq/<name>.peq. - Import: drop files exported from the HiBy Music app into a
peqfolder on the SD card. - 🤓 It also parses AutoEQ text format:
Preamp: %f dBandFilter N: ON PK Fc %f Hz Gain %f dB Q %f. AParametricEQ.txtfrom AutoEQ should import directly. (The error message for a bad file reads, verbatim, "Parse autoeq faile not txt". We love it.) - There's a hidden internal debug dialog, "PEQ Combined Settings", with 32 bands (Band 0–31) and an FFT view.
🎮 Games, with a launcher icon at litegui/theme*/launcher/games.png
- 2048: save file
/data/game_2048.dat - Sudoku: difficulty levels, hints, a timer, best score. Save file
/data/game_sudoku.dat - 24 Points: the card game where you make 24 from four numbers (
games/hiby_24.view, icongames/24_points.png). It's right there in the Games menu, not hidden.
⏸️ Timed playback pause, under Play settings → "Timed playback pause". If it clashes with standby, power-saving shutdown or sleep shutdown, the app offers to turn those off.
| Endpoint | Why |
|---|---|
otaserver.hiby.com/app/ota/getOtaInfo (POST productName=HiBy R1 — yes, with a space — plus versionNumber, deviceNumber, languageCode) |
firmware updates (this is how I got V2.0 early) |
otaserver.hiby.com/app/ota/updateDeviceVersion?productName=%s%%20%s |
reports your version |
otaserver.hiby.com/app/radio/getRadioInfo |
internet radio list |
otaserver.hiby.com/app/earphoneSet/getEarphoneSet |
headphone presets |
otaserver.hiby.com/app/disclaimer/getDisclaimer |
the EULA |
track.hiby.com/app/product/guide |
online manual |
api.tidal.com, auth.tidal.com (device OAuth) |
TIDAL |
www.qobuz.com/api.json/0.2 |
Qobuz |
http://<ip>:4399 |
Wi-Fi file transfer (thttpd + udp_server) |
Also on board: AirPlay (shairport), a DLNA renderer (dmrd), a Samba client, HiByLink, Bluetooth input, USB DAC (UAC) and SPDIF screens.
LDAC (HQ/SQ/ABR), aptX/aptX-HD, SBC. Gapless, ReplayGain (track/album), a resampler, DSD2PCM (DSD64–512) and native DSD.
Demuxers: FLAC, APE, DSF, DFF, AAC, AIFF, ASF, TS, WAV, MP3, and M4B with bookmarks kept in /usr/data/m4b_bookmarks.sqlite3. Audiobook people, rejoice.
product: HiBy / R1 / version 2.0 / ota_name "HiBy R1" / VID 0x32BB PID 0x0101 / DAC PID 0x0004
screen: 480×800, topbar 50, list item 126
volume: default 20, warn 34, lock 50 (headset)
function: ON: book_set_percent most_played dark_theme_enable screen_short_enable lyric_color_enable
OFF: dac_to_store tf_image_cache_enable tf_music_db_enable explorer_in_cue_enable
otg_scan_enable qplay3_enable playmenu_eq_disable
usb_mode, dac_charge_disable, dac_feedback, car_mode, car_mode_auto_play, double_touch_wakeup, volkey_locked, rotation, operating_instruction.
Tempting! But most of these come from HiBy's shared codebase for its other players, so "the row exists" doesn't mean "it does anything on the R1". §9.2 has the verdicts from the actual code.
Three files give it away: r1pro_config.json, r1pro_ot_devices.json, and r1pro_bt_name ("HiBy R1PRO").
device: R1PRO,version: 0.01,ota_name: HiBy R1PRO. Same 480×800 screen, the same USB VID/PID and the same feature flags.- Its output-device table is identical to the R1's except for three extra ports:
balance(priority 5),balance_lineout(4) andlineout(2). - Translation: the R1 Pro adds a balanced headphone output, balanced line out and single-ended line out. Sample rates (up to 384 kHz), DSD over DoP and the volume tables are unchanged.
- So it's the same brain with more jacks. You can't "enable" that on an R1: there are no extra jacks to enable.
midi_config.json, midi_set_functions.json, layout/midi, litegui/midi. The MiDi-branded twin is still alive and gets PEQ too.
english, french, german, italy, japanese, korean, poland, russian, simplified_chinese, spain, thai, traditional_chinese, ukrainian. The .ini files are UTF-16 XML (because of course they are).
Fonts: default.otf (a chunky 8.3 MB), msyh.ttf, Korean.ttf, Thai.ttf.
| Path | What it does |
|---|---|
<SD>/r1.upt |
local firmware update (the code looks for %s/sd_0/%s.upt) |
<SD>/peq/ |
PEQ imports |
<SD>/hiby_linux_factory_mode |
🏭 opens the factory test screen on every boot until you delete it (§9.3) |
<SD>/hiby_linux_auto_test |
arms a touch-logging auto-test that wakes up after 5 taps in one spot (§9.3) |
<SD>/battery_log.txt, temperature_log.txt |
if the file exists, battery or temperature logging turns on. battery_check.txt gets deleted on mount (§9.3) |
<SD>/batlog.txt |
written by batd if /usr/bin/batd exists (it isn't shipped) |
<SD>/.temp/{usrlocal_media,most_played,image_cache}.db |
library databases |
<SD>/list_cover, plane_cover, get_cover_switch |
cover-art caches and a debug switch |
<SD>/%s_Screenshot |
screenshots |
/data/recovery_all |
factory reset on next boot. Containing recovery_all wipes everything; anything else keeps the media DB |
/data/peq/*.peq, /data/game_*.dat, /data/theme_id, /data/time_zone |
your state |
/usr/data/macaddr.txt, wpa_supplicant.conf |
Wi-Fi |
roothas an empty password (/etc/shadow), and there's a root shell on the serial console.busyboxis setuid root.thttpdruns as root on port 4399 with CGI on (**.cgi) and no chroot, whenever Wi-Fi transfer is active. Maybe don't enable that on airport Wi-Fi.✈️ - ADB is available (USB gadget, plus an
sa_adbpath inside the player). - OpenSSL 1.1.1f and curl 7.68.0 date from 2020 and have plenty of known CVEs.
- TIDAL and Qobuz client credentials sit in the binary in plain text.
- No firmware signature, only MD5. Bad for security, great for us.
It's a music player, not a bank. But it's good to know what you're carrying around.
- Rebase on 2.0. It's the first release that runs the new X1600EN/HN + AIC8800 board with working radios. Old bases = deaf radios on new units.
- PEQ is official now (shelves + peaking + AutoEQ import), so the repo's PEQ enablement patch is probably redundant. Check how many bands the UI exposes before you drop it.
patch_brightness.pyworks on 2.0 unchanged.patch_bt_input.pyneeded a fix (§9.5). Any other patch with hard-coded 1.7/1.8b offsets has to be re-located.- Replace all three boot logos (
logo.jpeg,logo1.jpeg,logo2.jpeg). The theme byte in mtd5 decides which one shows up, and they should be 480×800. - Flags the code proves will work:
car_mode,dac_charge_disable,usb_mode,tf_music_db_enable,tf_image_cache_enable,explorer_in_cue_enable. Flags that do nothing:double_touch_wakeup,car_mode_auto_play,dac_feedback,volkey_locked, androtation(§9.2). - Keep
ota_compatibility.inashiby_r1_x1600_auto_rf_autowhen repacking.
🧠 9. Deep dive: what's really hidden in hiby_player (Ghidra-verified)
Confession time. My first pass at this binary was strings | grep plus vibes, and the vibes lied. I called the 24-point game "hidden" (it's in the Games menu). I said battery_check.txt was a trigger (the player deletes it). I told people to avoid the factory-mode file (it's actually the door to the factory test menu). So I threw all of that out and did it properly.
Method:
- Ghidra 12.1.4, headless.
- Ghidra missed about 120 functions, so I found them by scanning for MIPS function prologues (
addiu sp,sp,-N,0x27bdXXXX) and created them by hand. - 6,920 functions decompiled; every interesting string traced to the code that reads it.
- The project, scripts and exports live in
re/. For code and rodata, file offset = VA − 0x400000.
FUN_00419ee0 parses these into a bitmask at DAT_008EDF6C (bit n = the n-th key), and FUN_0041c040(bit) reads them back.
| Bit | Key | What the code actually does when it's 1 |
|---|---|---|
| 0 | dac_to_store | FUN_0052e840/FUN_0052ec40 save and restore settings 0x71/0x33/0x23/0x08/0x5f around USB-DAC mode, so your player setup survives being a DAC |
| 7 | tf_image_cache_enable | FUN_004305a0 puts the album-art cache DB in <SD>/.temp (with PRAGMA synchronous=OFF, living dangerously) |
| 8 | tf_music_db_enable | adds "The song database is stored in TF card" to Play settings → Music update mode (FUN_004ddec0). Once on, it needs >512 MB free on the card (FUN_004de2c0), stores the DB at <SD>/.temp/usrlocal_media.db, and 🚀 raises the library limit from 80,000 to 200,000 songs (FUN_00505920) |
| 9 | explorer_in_cue_enable | adds "Supports song embedded CUE in file browsing" (setting 0x91) to the same page |
| 10 | otg_scan_enable | adds "Scan OTG" (setting 0x92) to the same page |
| 11 | qplay3_enable | allows setting 0x94 (QPlay3 over DLNA). With the bit off, the setting is forced to 0 at boot (FUN_004191a0) |
Bonus ghost: mqa_decoder ("MQA decoder detection") has a tap handler but is never added to any list. Dead code on the R1. RIP. 🪦
FUN_0041c080 builds System settings from z:\set_functions.json, in file order. Here's the fun bit: any row name the code knows can be added, even rows that aren't in the stock file. Just write "name":1.
The master list sits in .data at 0x8C2FF4 (stride 0x44, int id; char name[64]), and FUN_004dcd20 handles taps.
| Row (id) | Stock | What tapping it really does | Works on R1? |
|---|---|---|---|
| car_mode (12) | 0 | toggles setting 0x72, which is used twice: resumes playback at boot (FUN_00508760), and when the charger is unplugged it pops up "Whether to enter the vehicle shutdown" (FUN_00482600 → msg 0x104 → FUN_004ea460). Real car-stereo behaviour 🚗 |
✅ Yes |
| usb_mode (9) | 0 | opens "USB device mode": Storage / Audio. That's internal setting 8 (1 = Storage, 0 = Audio). Audio is USB DAC mode; the same switch also lives in the pull-down menu (FUN_005544a0) |
✅ Yes |
| dac_charge_disable (10) | 0 | "USB current limited", setting 0x5f. In DAC mode it writes MaxPower=10, charge_control_limit_max=100, constant_charge_current_max=0, so no charging while it's a DAC (FUN_00474000/FUN_00474200/FUN_0047fe80 → FUN_00473e00). Less noise, kinder to your laptop battery |
✅ Yes |
| rotation (28) | 0 | opens Screen rotation. FUN_0047bb20 → ioctl JZFB_ROTATE (0° or 180°). soc_fb.ko takes the ioctl, then hands it to the panel driver's rotate callback at lcdc_data+160… which lcd_lg35583.ko leaves NULL. The ioctl returns success and nothing happens. Confirmed dead on a real device 😔 |
❌ No (needs a kernel panel driver) |
| car_mode_auto_play (13) | 0 | stores setting 0x73. Nothing ever reads it | ❌ No effect |
| dac_feedback (11) | 0 | setting 0x75 → FUN_00474860, which is an empty function. A button wired to nothing |
❌ No effect |
| volkey_locked (25) | 0 | toggles setting 0x5b (stored inverted). No other reader in hiby_player or sys_server |
❌ No effect found |
| double_touch_wakeup (22) | 0 | stores setting 0x6f. Never read, and the touch driver has no gesture code anyway | ❌ No effect |
| operating_instruction (33) | 0 | opens track.hiby.com/app/product/guide?... |
🟡 online manual |
| update_type (1) | absent | the Music update mode page, already reachable from Play settings | — duplicate |
| step_record (23) | absent | setting 0x59 → writes /sys/class/input/input*/stepcnt_enable for a KX126 accelerometer… which the R1 doesn't have |
❌ No sensor |
| fn_key (37) | absent | opens the vg_listview_fn_key page |
🟡 untested |
| wireless_set (35), bluetooth_set (36), stream_media (2), brightness (3), bkl_time (34) | absent | shortcuts to pages that already exist | 🟡 duplicates |
| File in the SD root | Where in the code | What happens |
|---|---|---|
hiby_linux_factory_mode |
FUN_00508760 (main activity create) |
The start screen becomes factory and the factory test screen opens (FUN_00577060 → hiby_test): keys, LCD, touch, TF card, charging, Bluetooth, audio channels. It resets the next start to launcher, but the file is still there, so it opens again every boot until you delete it. On entry it also sets 0x27=1, 0x15=1, 0x7a=0, 0x0e=0 and starts a 3-minute timer |
hiby_linux_auto_test |
FUN_004868a0 |
The file only arms it. It activates after 5 taps in a row inside one fixed rectangle on screen, then logs every touch and swipe (MONKEY log lines, autotest_capture). A touch logger, not a random tapper |
battery_log.txt / temperature_log.txt |
FUN_004555a0 (on SD mount) |
If the file exists, battery or temperature logging switches on (msg 0x19 / 0x1a) |
battery_check.txt |
FUN_004555a0 |
Deleted on mount. Not a trigger, just tidying up |
get_cover_switch |
FUN_0047ef60 (startup) |
creates <SD>/list_cover and <SD>/plane_cover and flips a cover-art debug flag |
- Darwin FPGA filters (
.fltfiles written tojz-spi0.0/filter_path). That's R-series DAC magic, not R1. - Step counter (KX126): no sensor.
- FM radio and recording: no tuner.
- Voice recorder: no dmic driver.
- SPDIF, coax and optical: no jack.
- AirKiss: no
akissbinary. /data/harmonic.json: zero code references, a leftover from an older JSON-driven version. 👻 But the Harmonic Controller itself is real; see §9.7.
patch_brightness.py: one clean match at file offset0x6e760(sltiu v1,v1,4, VA0x46e760). Works as is.patch_bt_input.py: the string and instruction search still works (blocklist at0x10407c, menu at0xb6eb4). But its code-cave finder grabbed0x38b69a. That's not 4-byte aligned (MIPS will not run code there), and it overwrote the NUL ending thez:\layout\string the UI depends on. Double whammy. 💥- Fixed: it now leaves the terminator alone and aligns the cave, giving
0x38b69c. The disassembly checks out:lui/move/jal 0x4b58e0/addiuforvolume_sync, again forbt_input, thenj 0x4b6ec4back home.
- Fixed: it now leaves the terminator alone and aligns the cave, giving
The repo has been calling patch_bt_input.py the "Bluetooth controller input unlock". After tracing it: it's not about game controllers at all. bt_input is the Bluetooth audio receiver screen (A2DP sink). Your phone streams to the R1 and the R1 plays it, showing title, artist, album and sample rate (bt_input_tv_media_*, bt_input_tv_sample_rate). HiBy built it, then hid it on the R1 with a blocklist entry (FUN_004f7220). The patch removes that entry and adds a menu row.
What happens when you open it:
FUN_00522080(screen create) sets setting 0x25 = 1.- The settings setter (
FUN_00417800, case 0x25) callsFUN_004702c0("a2dp-sink"), which sendsBT:A2DPPROFILE:a2dp-sinkandBT:DISCOVERtosys_server. sys_serverruns/usr/bin/bluealsa_profile a2dp-sink. That kills bluealsa and bt-agent and restarts them asbluealsa -p a2dp-sink --a2dp-volumeplusbt-agent -c NoInputNoOutput.FUN_0071e1e0starts thebt_sinkplayback thread.- Leaving the screen (
FUN_005210c0) sets 0x25 = 0 and switches back toa2dp-source.
So the whole software chain is there. On a real device: the phone sees the R1, tries to connect, and fails. Main suspects:
- The phone cached the R1's old SDP records from when it was source-only, and needs to forget and re-pair while the screen is open.
- HiBy hid it for a reason (unfinished on the R1, or a radio/firmware limitation).
btmonlogs from the device will tell which.
"Harmonic Controller" (Harmonic_control in darwin.ini) is a row in HiBy's Darwin menu, next to NOS, Digital Filter and DSD Filter. The chain:
- Tapping the row (
FUN_004efc60) opens a slider dialog bound to setting 0x6b: 0 = "off", up to 255. - The setter (
FUN_00417800case 0x6b) →FUN_00474640(rejects >255) →FUN_00721aa0. - That writes two fields of the analog output device object at
0x9292f0:+0xc0= level, and+0xc4= gain = (65536 − level) / 65536. - In the analog device's write callback (
0x7255e0, a label Ghidra never made into a function), if level ≠ 0:
for each 32-bit sample s:
if (s < 0) s = (int)(s * gain); // only the NEGATIVE half-wave is scaled
Shrinking one half of the waveform makes it asymmetric, and asymmetry = even-order harmonics (mostly 2nd). That's the classic single-ended-tube "warmth" recipe, done in about 10 MIPS instructions. 🔥
How much? With asymmetry a = level/65536, a sine gets a 2nd harmonic of about a·2/(3π):
| Level | Gain on negative half | 2nd harmonic |
|---|---|---|
| 1 | 0.99998 | ≈ −110 dB |
| 64 | 0.99902 | ≈ −74 dB (0.02%) |
| 255 (max) | 0.99611 | ≈ −62 dB (0.08%) |
It also adds a tiny DC offset (≈ a/π of full scale). Subtle, a seasoning, not a fuzz pedal.
Real-device verdict: "almost no effect." That matches the math. HiBy's scale tops out at a −62 dB 2nd harmonic, which is inaudible through IEMs. The path is active: the analog device declares 4-byte samples (field 0xd = 4), so the 32-bit loop fits, and only DSD (+4 == 2) skips it. So the mod rescales it. In the writer (0x721ac4), lui v1,0x1 becomes li v1,2048, and the 1/65536 load is pointed at a new 1/2048 float in the cave (0x875b6c). Gain is now (2048 − L)/2048:
| Level | 2nd harmonic (mod) |
|---|---|
| 32 | ≈ −50 dB (0.33%) |
| 128 | ≈ −38 dB (1.3%) |
| 255 | ≈ −32 dB (2.6%) |
That's single-ended-tube territory at the top of the slider. 🔥
Round two: still "almost null" at 2048. I re-traced every early exit in the analog write callback:
+184is a one-shot error log.+4 == 2is DSD bit-reversal (FUN_0072cbc0).- The loop length is in bytes:
FUN_0072c280divides by channels × bits / 8 forWRITEI_FRAMES. - Samples are 32-bit, then
>>8to 24-bit inFUN_0072cb80.
None of these skips PCM. So either 2.6% H2 is simply subtle on music (likely), or something only visible at runtime is in the way. The scale is now 512, so the top of the slider is unmistakable: L=16 ≈ 0.7%, L=64 ≈ 2.6%, L=128 ≈ 5%, L=255 ≈ 10.6% H2.
On the R1 (stock): the DSP code is pure software, so it would run. But the Darwin menu can't be opened. The R1's resources have no vg_listview_darwin / dw_* layout files and no launcher or pull-down entry for it, and the rest of Darwin (NOS, .flt filters, osr_bypass on jz-spi0.0) needs the FPGA the R-series has and the R1 doesn't. Turning harmonics on would mean setting 0x6b some other way (a patched default, or a small new settings row).
- It does run: the headphone output uses 32-bit samples, which matches the effect's code. Only DSD playback skips it.
- It's far too faint: HiBy's formula shrinks the negative half of the waveform by level / 65536. Even at 255 that's 0.39%, which makes a 2nd harmonic around −62 dB (0.08%). That's inaudible through IEMs. On HiBy's R-series it probably works together with their hardware filters, so on its own it does almost nothing.
Fix: I kept HiBy's code and changed the 65536 to 2048, so the same slider now covers an audible range:
I made the slider 4× stronger again (scale 512 instead of 2048). The top of the range is now impossible to miss:
| Slider | 2nd harmonic | Expect |
|---|---|---|
| 16 | 0.7% | subtle warmth |
| 64 | 2.6% | the previous maximum |
| 128 | 5% | obvious tube colour |
| 255 | 10.6% | clearly audible distortion |
To make it, I added a 3rd part to patch_harmonic.py. It swaps two instructions and adds one new constant, I checked the result by disassembly, and running the script twice changes nothing. If 255 turns out too much or too little, I can change the scale (1024 is stronger, 4096 gentler).
The mod fixes this with Old_New_Mods/patch_harmonic.py:
- The System settings row
operating_instruction(the online manual, row 0x21) now opens HiBy's own Harmonic Controller slider (FUN_004efc60). The branch at0x4dd2f0gets the same 5-instruction call the brightness row uses. - The row is relabelled in all 13 languages, using each language's own
Harmonic_controltranslation fromdarwin.ini. - The level is saved in
/data/user.inilike every other setting, but stock never re-applies it at boot: the analog device init (which resets it to off) runs before settings load. So the singlejal FUN_00418ec0(settings load) at0x47f0ecnow goes to an 11-instruction stub that loads the settings, then callsFUN_00474640(get(0x6b)). - The stub lives in the 1,216 bytes of zero padding at the end of the executable segment (
0x875b40), and the firstPT_LOAD's size is extended to cover it. - Every site is found by instruction signature, and the script is idempotent.
All of the above, turned into a flashable firmware.
| What | Change |
|---|---|
| Boot | Bidhata boot menu + new 480×800 "HiBy R1 · with Bidhata's" logos for all three themes |
hiby_player |
brightness down to 1, the hidden Bluetooth receiver screen unlocked (with the fixed cave; see §9.6) |
| Play settings | SD-card song DB (200k library), SD-card art cache, CUE + OTG scan rows, settings saved around DAC mode |
| System settings | Harmonic Controller (0–255 slider, §9.7), plus rows for USB device mode (DAC), USB current limited and car mode. All still off until you turn them on. (Rotation was dropped after device testing proved it dead.) |
| Audio | no volume warning popup and no 50/100 headphone cap (lock_vol 50 → 100, see below), last MDB/LDB volume step 12 → 0 |
| Storage | noatime on root and userdata, SD read-ahead 2048 KB, slower cache reclaim (via a new S91_bidhata_tune init script, because the boot menu skips hiby_player.sh) |
The volume-lock gotcha (found by a real-device test): setting 0x7a ("volume locked", default 1) makes FUN_00416de0 cap the max volume at lock_vol (headset 50). Stock firmware only clears 0x7a when you accept the "Extended exposure to high volume…" dialog. With vol_warn_enable: 0 that dialog never appears, so the old mod left you stuck at 50 with "Set maximum volume reached". Setting lock_vol to 100 turns the lock into a no-op.
Round two: the popup still appeared. There's a second trigger. At every boot FUN_004191a0 sets 0x77 = (0x7a == 1). Then FUN_00536180, called from the volume handlers FUN_0043ae40/FUN_0043af40, shows the hearing popup whenever 0x77 == 1 and the new volume is above warn_vol (headset 34). That path never checks vol_warn_enable. Fix: warn_vol → 100, which volume can never exceed. The other readers of the warn level only turn the volume number red. Both changes are config-only, no binary patch.
If you flashed an earlier build, raise Play settings → Max volume back to 100 once.
Left out on purpose:
- the five do-nothing toggles (rotation joined the club after a real-device test)
- the tiny font, which would eat CJK glyphs
- the old Game Boy launcher, which the Bidhata menu replaces
Checked: the hash chain and layout match stock, and the rootfs differs from stock only in the intended files. Ownership, modes and even CRLF line endings are kept. The kernel is byte-identical to stock.
Real-device scoreboard (older Broadcom-board R1, 2026-10-17):
| Feature | Result |
|---|---|
| Flashing, boot logos, Bidhata menu | ✅ works |
| Full volume range, no hearing popup | ✅ works (after the lock_vol + warn_vol fixes) |
| Harmonic Controller slider | ✅ appears and saves. 🟡 Effect still too faint to hear at scales 65536 and 2048; scale 512 build plus sine-tone test pending |
Bluetooth receiver (bt_input) |
🟡 screen opens, phone sees the R1, connection fails. Needs btmon logs |
| Everything else | 🟡 untested. Flash it, poke everything, report back 🫡 |
Built by a curious reverse-engineering addict who can't leave firmware alone. If something here is wrong, the code is the source of truth. Tell me and I'll go back into Ghidra. ☕