Previous | Table of Contents | Next |

Encoding and decoding a stream of symbols using arithmetic coding is not too complicated. But at first glance it seems completely impractical. Most computers support floating-point numbers of around 80 bits. So do you have to start over every time you encode ten or fifteen symbols? Do you need a floating-point processor? Can machines with different floating-point formats communicate through arithmetic coding?

As it turns out, arithmetic coding is best accomplished using standard 16-bit and 32-bit integer math. Floating-point math is neither required nor helpful. What is required is an incremental transmission scheme in which fixed-size integer state variables receive new bits at the low end and shift them out at the high end, forming a single number that can be as long as necessary, conceivably millions or billions of bits.

Earlier, we saw that the algorithm works by keeping track of a high and low number that brackets the range of the possible output number. When the algorithm first starts, the low number is set to 0 and the high number is set to 1. The first simplification made to work with integer math is to change the 1 to .999 …, or .111… in binary. Mathematicians agree that .111… binary is exactly the same as 1 binary, and we take their assurance at face value. It simplifies encoding and decoding.

To store these numbers in integer registers, first justify them so the implied decimal point is on the left side of the word. Then load as much of the initial high and low values as will fit into the word size we are working with. My implementation uses 16-bit unsigned math, so the initial value of high is 0xFFFF, and low is 0. We know that the high value continues with Fs forever, and the low continues with zeros forever; so we can shift those extra bits in with impunity when needed.

If you imagine our “BILL GATES” example in a five-decimal digit register (I use decimal digits in this example for clarity), the decimal equivalent of our setup would look like what follows on the next page.

HIGH: 99999 implied digits => 999999999... LOW: 00000 implied digits => 000000000...

To find the new range of numbers, apply the encoding algorithm from the previous section. First, calculate the range between the low and high values. The difference between the two registers will be 100000, not 99999. This is because we assume the high register has an infinite number of 9s added to it, so we need to increment the calculated difference. We then compute the new high value using the formula from the previous section:

high = low + high_range(symbol)

In this case, the high range was .30, which gives a new value for high of 30000. Before storing the new value of high, we need to decrement it, once again because of the implied digits appended to the integer value. So the new value of high is 29999. The calculation of low follows the same procedure, with a resulting new value of 20000. So now high and low look like this:

high: 29999 (999...) low: 20000 (000...)

At this point, the most significant digits of high and low match. Due to the nature of our algorithm, high and low can continue to grow closer together without quite ever matching. So once they match in the most significant digit, that digit will never change. We can now output that digit as the first digit of our encoded number. This is done by shifting both high and low left by one digit and shifting in a 9 in the least significant digit of high. The equivalent operations are performed in binary in the C implementation of this algorithm.

As this process continues, high and low continually grow closer together, shifting digits out into the coded word. The process for our “BILL GATES” message is shown next.

High | Low | Range | Cumulative Output | |
---|---|---|---|---|

Initial state | 99999 | 00000 | 100000 | |

Encode B (0.2—0.3) | 29999 | 20000 | ||

Shift out 2 | 99999 | 00000 | 10000 | .2 |

Encode I (0.5—0.6) | 59999 | 50000 | .2 | |

Shift out 5 | 99999 | 00000 | 100000 | .25 |

Encode L (0.6—0.8) | 79999 | 60000 | 20000 | .25 |

Encode L (0.6—0.8) | 75999 | 72000 | .25 | |

Shift out 7 | 59999 | 20000 | 40000 | .257 |

Encode SPACE (0.0—0.1) | 23999 | 20000 | .257 | |

Shift out 2 | 39999 | 00000 | 40000 | .2572 |

Encode G (0.4—0.5) | 19999 | 16000 | .2572 | |

Shift out 1 | 99999 | 60000 | 40000 | .25721 |

Encode A (0.1—0.2) | 67999 | 64000 | .25721 | |

Shift out 6 | 79999 | 40000 | 40000 | .257216 |

Encode T (0.9—1.0) | 79999 | 76000 | .257216 | |

Shift out 7 | 99999 | 60000 | 40000 | .2572167 |

Encode E (0.3—0.4) | 75999 | 72000 | .2572167 | |

Shift out 7 | 59999 | 20000 | 40000 | .25721677 |

Encode S (0.8—0.9) | 55999 | 52000 | .25721677 | |

Shift out 5 | 59999 | 20000 | .257216775 | |

Shift out 2 | .2572167752 | |||

Shift out 0 | .25721677520 | |||

After all the letters are accounted for, two extra digits need to be shifted out of either the high or low value to finish the output word. This is so the decoder can properly track the input data. Part of the information about the data stream is still in the high and low registers, and we need to shift that information to the file for the decoder to use later.

Previous | Table of Contents | Next |