Sing-song: a speakable encoding for long numbers and keys

Sing-song: a speakable encoding for long numbers and keys

Sing-song:一种用于长数字和密钥的可读编码

This is a fun experiment that started as a way to generate deterministic “usernames” for Nostr npub keys (see the last section). English is really hard to deal with even for simple syllables, so the result is not as good as I’d want it to be (a string you can read to an other person over the phone and make sure they get it right), but it’s not bad either —if everyone spoke Italian sing-song would be much more valuable :-)

这是一个有趣的实验,最初是为了给 Nostr npub 密钥生成确定性的“用户名”(见最后一节)。英语即使在处理简单的音节时也非常困难,所以结果并没有达到我想要的理想程度(即一段可以通过电话读给别人听并确保对方准确记录的字符串),但它也不算太差——如果每个人都说意大利语,Sing-song 的价值会高得多 :-)

Sing-song: speakable encodings for long numbers and keys Status: draft, v0.1.0 --- comments welcome.

Sing-song:用于长数字和密钥的可读编码 状态:草案,v0.1.0 --- 欢迎评论。

Abstract

摘要

Sing-song is a reversible encoding of arbitrary byte strings as pronounceable CV syllables. Its 64-syllable alphabet maps each 6-bit value directly to one syllable. Complete encodings preserve byte length and leading zero bytes, require no external length metadata, and are canonical. The encoding is prefix-stable: shared input prefixes produce shared syllable prefixes. An optional variant suffix provides alternative reversible representations of the same byte string.

Sing-song 是一种将任意字节字符串转换为可发音的 CV(辅音-元音)音节的可逆编码。其 64 个音节的字母表将每个 6 位值直接映射为一个音节。完整的编码保留了字节长度和前导零字节,不需要外部长度元数据,并且是规范的。该编码具有前缀稳定性:共享的输入前缀会产生共享的音节前缀。可选的变体后缀为同一字节字符串提供了另一种可逆的表示形式。

Motivation

动机

Machine-oriented encodings such as hex and Base58 are compact but awkward to speak, transcribe, and remember. Sing-song trades some written density for a small, regular pronunciation grammar while remaining deterministic, reversible, and computationally simple.

面向机器的编码(如十六进制和 Base58)虽然紧凑,但在口述、转录和记忆时却很别扭。Sing-song 以牺牲一定的书写密度为代价,换取了一种小巧、规则的发音语法,同时保持了确定性、可逆性和计算上的简单性。

Design goals

设计目标

The encoding should be deterministic, reversible, prefix-stable, speakable and transcribable without training, self-sizing for complete byte strings, and self-delimiting without load-bearing punctuation.

该编码应具备以下特性:确定性、可逆性、前缀稳定性、无需训练即可口述和转录、对于完整字节字符串可自适应大小,以及无需依赖标点符号即可实现自定界。

The encoding Alphabet

编码字母表

PositionSymbolsCount
Consonant (odd)b d f g j k l m n p r s t v w z16
Vowel (even)a i o u4
位置符号数量
辅音 (奇数位)b d f g j k l m n p r s t v w z16
元音 (偶数位)a i o u4

Consonants and vowels strictly alternate, producing 64 open CV syllables with no clusters or codas. Position parity determines the symbol table. h, y, and e are omitted because their pronunciation is comparatively unstable.

辅音和元音严格交替,产生 64 个开放的 CV 音节,没有辅音丛或韵尾。位置奇偶性决定了符号表。h、y 和 e 被省略,因为它们的发音相对不稳定。

Syllables and grouping

音节与分组

Display groups contain two syllables (zila, sibo) separated by cosmetic hyphens. Parsers MUST ignore hyphens: zilasibotivajuzu and zila-sibo-tiva-juzu are identical. Groups provide natural speaking checkpoints.

显示分组包含两个音节(如 zila, sibo),中间用装饰性连字符分隔。解析器必须忽略连字符:zilasibotivajuzuzila-sibo-tiva-juzu 是相同的。分组提供了自然的口述检查点。

Algorithm

算法

Treat the input as a bit stream and split it into 6-bit chunks, most significant bit first. Each chunk maps directly to one syllable: bits 5..2 → consonant index 0..15 bits 1..0 → vowel index 0..3

将输入视为位流,并将其拆分为 6 位的数据块,最高有效位在前。每个数据块直接映射到一个音节: 位 5..2 → 辅音索引 0..15 位 1..0 → 元音索引 0..3

For L input bytes, emit n = ceil(8·L / 6) syllables. If the final chunk has fewer than six input bits, zero-fill its low bits. These zeroes are canonical padding and carry no information. A complete encoding is self-sizing: L = floor(6·n / 8). The decoder reconstructs the 6-bit chunks, infers L, returns the first 8·L bits, and MUST reject non-canonical syllable counts or non-zero padding. Leading zero bytes are preserved.

对于 L 个输入字节,输出 n = ceil(8·L / 6) 个音节。如果最后一个数据块不足 6 位,则在其低位补零。这些零是规范填充,不携带任何信息。完整的编码是自适应大小的:L = floor(6·n / 8)。解码器重构 6 位数据块,推断出 L,返回前 8·L 位,并且必须拒绝非规范的音节数量或非零填充。前导零字节会被保留。

This applies to complete encodings. A truncated prefix does not indicate whether more syllables follow. A prefix of k syllables commits to the first 6·k bits of the encoded value and is verified by recomputation, not decoding.

以上适用于完整编码。截断的前缀无法指示后面是否还有更多音节。k 个音节的前缀确定了编码值的前 6·k 位,并通过重新计算而非解码来进行验证。

Variants

变体

A variant is an alternative reversible representation of the same byte string. The variant identifier is encoded in the representation, so decoding requires no external metadata. For input X and variant v = 0…15: M(0, n) = 0^n M(v, n) = SHAKE-256(“sing-song/variant” ‖ byte(v), n) for v > 0 Y = X XOR M(v, len(X)) Encode Y with the ordinary Sing-song codec. Because XOR is self-inverse: X = Y XOR M(v, len(Y))

变体是同一字节字符串的另一种可逆表示。变体标识符被编码在表示中,因此解码不需要外部元数据。对于输入 X 和变体 v = 0…15: M(0, n) = 0^n M(v, n) = SHAKE-256(“sing-song/variant” ‖ byte(v), n) (当 v > 0 时) Y = X XOR M(v, len(X)) 使用普通的 Sing-song 编解码器对 Y 进行编码。由于异或运算是自逆的:X = Y XOR M(v, len(Y))

The mask is public and provides no confidentiality. SHAKE-256 produces a deterministic stream, preserving the prefix-stability property described above. Variant 0 is the direct encoding. The variant identifier is rendered as a trailing two-letter suffix: one vowel followed by one of l m n r: v = 4·i + j where vowel = “aiou”[i], consonant = “lmnr”[j]

掩码是公开的,不提供机密性。SHAKE-256 产生确定性的流,保留了上述的前缀稳定性。变体 0 是直接编码。变体标识符呈现为末尾的两个字母后缀:一个元音后跟 l、m、n、r 中的一个: v = 4·i + j,其中 vowel = “aiou”[i], consonant = “lmnr”[j]

Variant 0 SHOULD be rendered without a suffix; parsers MUST accept an explicit al as equivalent. Parity disambiguates the suffix: content consonants occupy odd positions, so a vowel at an odd position can only begin the variant suffix. Parsers MUST require exactly two trailing letters (vowel then l/m/n/r) and reject other parity violations.

变体 0 应在不带后缀的情况下呈现;解析器必须接受显式的 al 作为等价形式。奇偶性消除了后缀的歧义:内容辅音占据奇数位置,因此出现在奇数位置的元音只能是变体后缀的开始。解析器必须要求恰好有两个末尾字母(元音后跟 l/m/n/r),并拒绝其他奇偶性违规。

Prefix stability

前缀稳定性

Each complete syllable represents exactly six consecutive input bits. Therefore, if two byte strings share their first 6k bits, their direct Sing-song encodings share their first k syllables. For byte-aligned prefixes, a boundary is both byte- and syllable-aligned every 24 bits: 3 bytes = 24 bits = 4 syllables. At those boundaries, truncating the encoding is exactly equivalent to encoding the truncated byte string: SingSong(X)[0:4k syllables] = SingSong(X[0:3k bytes]).

每个完整的音节恰好代表连续的 6 位输入。因此,如果两个字节字符串共享其前 6k 位,则它们的直接 Sing-song 编码共享其前 k 个音节。对于字节对齐的前缀,每 24 位边界既是字节对齐的也是音节对齐的:3 字节 = 24 位 = 4 音节。在这些边界处,截断编码与编码截断后的字节字符串完全等价:SingSong(X)[0:4k 音节] = SingSong(X[0:3k 字节])。

Transcription and error handling

转录与错误处理

Position parity makes limited corrections safe: 0→o, 1→l, and e→i. Parsers MAY apply these and MUST reject other out-of-alphabet or parity errors. Errors localize by group: when a prefix stops matching, the client can request repetition of that group rather than the whole string.

位置奇偶性使得有限的纠错变得安全:0→o, 1→l, 和 e→i。解析器可以应用这些规则,并且必须拒绝其他超出字母表或奇偶性错误。错误按组定位:当某个前缀不再匹配时,客户端可以请求重复该组,而不是整个字符串。

Alternatives considered

考虑过的替代方案

The base grammar survived three alternatives worth recording. Curated syllable dictionaries: A hand-picked codebook can exclude confusable minimal pairs by merging sounds such as b/p, d/t, g/k, f/v, s/z, m/n, and l/r into equivalence classes. Roughly ten onset classes × four vowels × three coda classes gives about 120 robust syllables, or ~6.9 bits each: about 17% fewer syllables than Sing-song, with better error tolerance. The cost is a large lookup table, longer written forms.

基础语法经历了三种值得记录的替代方案。精心挑选的音节词典:通过将 b/p、d/t、g/k、f/v、s/z、m/n 和 l/r 等易混淆的最小对音合并为等价类,可以排除易混淆的音节。大约 10 个声母类 × 4 个元音 × 3 个韵尾类,可以得到约 120 个稳健的音节,每个音节约 6.9 位:比 Sing-song 少约 17% 的音节,且具有更好的容错性。代价是需要一个庞大的查找表,且书写形式更长。