Skill Issues

Is That A Piano In Your Pants?

Hey, that's weird...

I was in the middle of something else, like I often am. I had ended up trying to mount my iPhone as a volume to get my photos onto my Linux box. It was at that point I noticed something I didn't expect to see. ifuse exposed not just the plain old photo albums, but it had data exposed for ringtones.

Kids, ringtones were a very big deal when I was young. Look it up. A few years back I started thinking about a problem where every time a text message came across I felt compelled to pick up my phone and check it. Was it something useful? someone I cared about? Sometimes. But it was also an interruption. I decided I wanted an audible clue as to what I would find.

Gather round, children

iOS already has a setting for each contact for what tone should be played, so it should be straightforward. Well... iPhone has been around a long damn time and it's never been straightforward as to how to get a damned ringtone onto it.

In the olden days you'd use iTunes on your computer, clip an existing mp3 down to a specific length no greater than some number of seconds, convert it to a different file format. After all that, you had to change the file extension for seemingly no reason. Now you have a file... now how to get it on the damn phone.

This dance changed from time to time and you'd find yourself searching youtube for the latest incantation ("iphone ringtone maker 2018") or downloading some malware claiming to do it for you.

Welp, it's pretty easy now.

TL;DR

Goal: Distinct piano chords per person: inconspicuous enough to not bother most people in most situations, different enough so I know which is which.

Solution:

Part one: making two seconds sound like a pianist

A text tone is about two and a half seconds. That is no room for a melody, so I chose a single chord as the form to take. However... playing all notes at once, all of them present, no dynamics... it sounds robotic. Luckily my good friend Claude, a robot, helped me figure out four rules that fix it.

The root goes low and goes alone. Left hand around C3, nothing else down there competing with it.

Color tones stack tight from Bb3 up. The notes that make the chord what it is, packed close together instead of sprayed across two octaves.

Drop the fifth once the chord gets crowded. On a 13th there is no room for it and it was not saying anything anyway.

Roll the notes, do not strike them. Bottom to top over roughly 24 milliseconds. Fast enough that you do not hear an arpeggio, slow enough that you hear a hand.

That last one is most of the effect. 24 ms is the difference between a MIDI file and a person.

The tones I ended up installing:

Here is the whole voicing step. Two magic numbers cover the first two rules: MIDI note 48 is C3, MIDI note 58 is Bb3.

def voice(root, intervals, bass_pc):
    """Return (bass_midi, [upper_midi...]).

    Root in the left hand around C3, a compact rootless voicing in the right
    starting at Bb3. Drops the 5th on dense chords so extensions stay audible.
    """
    bass = 48 + bass_pc

    upper_iv = [i for i in intervals if i != 0]
    if len(upper_iv) > 4 and 7 in upper_iv:
        upper_iv = [i for i in upper_iv if i != 7]  # 5th is the expendable one

    notes, cursor = [], 58
    for iv in upper_iv:
        pc = (root + iv) % 12
        n = cursor + ((pc - cursor) % 12)
        if n == cursor and notes:
            n += 12
        notes.append(n)
        cursor = n + 1
    return bass, notes

That % 12 walk is what keeps the right hand tight. Every note goes to the next free slot above the one before it, instead of wherever its interval would normally land.

The roll itself is over in the MIDI writer:

seq = [(bass, 62)] + [
    (n, min(112, 74 + i * 5)) for i, n in enumerate(uppers)
]
for i, (note, vel) in enumerate(seq):
    on = ms(i * roll_ms)                # roll_ms = 24
    off = ms(i * roll_ms + hold_ms)

Velocity climbs as the roll goes up, and the sustain pedal is down the whole time. Honestly, that ramp might be doing as much work as the 24ms is.

Making a tone is then one command:

python3 ~/Music/tones/mktone.py Am11

--audition glues every tone you asked for into a single mp3 so you can hear them back to back, and --list-qualities prints every chord symbol it knows.

Both scripts are in a gist. Fair warning: The vibes are strong with this and I've not read the code.

Part two: getting it onto the phone

ifuse mounts the phone's media partition over USB. No flags, no app id, just a mountpoint:

ifuse ~/iphone

Everything to do with tones lives in one directory:

~/iphone/Purchases/
  Ringtones.plist                                  the manifest
  import_4CE54437-0D28-4053-A5BE-9A77FCF69A12.m4r  the audio
  import_B2EC7624-8E1C-45BF-B6EA-6FB553A41B03.m4r

You do not get to pick those filenames. Purchases/ is a staging directory the sync protocol imports out of, and import_<GUID>.m4r is the shape it expects.

Installing a tone is one file and one manifest entry:

python3 ~/Music/tones/instone.py add Am11.m4r --name Wifey

One thing to know on the way out, since Arch does not ship fusermount:

fusermount3 -u ~/iphone

The bug that cost the most time

Ringtones.plist has to be a binary plist. Hand iOS an XML one and nothing happens. No error, no partial import, no log line, no help at all. The tone is just not in the picker.

The fix is one keyword argument:

plistlib.dump(data, fh, fmt=plistlib.FMT_BINARY)

Hours. That fix took hours to find...

iOS only looks at boot

With a correct binary manifest in place, the tones still did not show up. I worked through it by elimination.

What I tried What happened
Posted the iTunes sync notifications with idevicenotificationproxy Nothing
Unmounted and remounted the FUSE mount Nothing
Rebooted the phone Every tone appeared

So... reboot your phone. Maybe there's another way, but I didn't find it.

Part three: assigning the tone

I have the tones in my tone library, now I need to put them to work. Can I do it from the comfort of my terminal? NOPE!

Two separate walls.

AFC is chrooted. The protocol ifuse speaks only exposes /var/mobile/Media. Contacts do not live there.

What about editing on iCloud?

iCloud has the contact but not the field. CardDAV against iCloud genuinely works, RFC 6352, app-specific password, all of it. You can read and write your contacts from Linux today. But the ringtone assignment is not in the vCard and not in CardDAV, because it does not sync to iCloud at all. It exists on the device and in device backups and nowhere else.

So... Roll up your sleeves kids, and go into each person you've made a text tone for and assign it yourself... As long as your next phone is restored from a backup, you should only have to do this one time.

Conclusion

I had fun with this one and now I have a lot less friction to add that new friend to the text tone'd group of contacts. It was a full year since I'd revisited this, and now it's basically a Claude convo and a phone reboot. I love making tools like this where everything can boil down to what you see in a shell session and terminal coding agents are making that easier all the time for me.

#ios #linux