for column, element in enumerate(column_table):
            # Start BELOW matrix and accumulate bin weights UP, saves math
            first_bin = element[0]
            column_top = glasses.height + 1
            for bin_offset, weight in enumerate(element[1]):
                column_top -= data[first_bin + bin_offset] * weight

            if column_top < element[3]:  #       Above current falling dot?
                element[3] = column_top - 0.5  # Move dot up
                element[4] = 0  #                and clear out velocity
            else:
                element[3] += element[4]  #      Move dot down
                element[4] += 0.2  #             and accelerate

            column_top = int(column_top)  #      Quantize to pixel space
            for row in range(column_top):  #     Erase area above column
                glasses.pixel(column, row, 0)
            for row in range(column_top, 5):  #  Draw column
                glasses.pixel(column, row, element[2])
            glasses.pixel(column, int(element[3]), 0xE08080)  # Draw peak dot

        glasses.show()  # Buffered mode MUST use show() to refresh matrix

        frames += 1
        # print(frames / (monotonic() - start_time), "FPS")

    except OSError:  # See "try" notes above regarding rare I2C errors.
        print("Restarting")
        reload()
        # Each row (except last) is then processed, top-to-bottom. This
        # order is important because it's an iterative algorithm...the
        # output of each frame serves as input to the next, and the steps
        # below (looking at the pixels below each row) are what makes the
        # "flames" appear to move "up."
        for y in range(5):  #         Current row of pixels
            y1 = data[y + 1]  #       One row down
            for x in range(1, 19):  # Skip left, right columns in data
                # Each pixel is sort of the average of the three pixels
                # under it (below left, below center, below right), but not
                # exactly. The below center pixel has more 'weight' than the
                # others, and the result is scaled to intentionally land
                # short, making each row bit darker as they move up.
                data[y][x] = (y1[x] + ((y1[x - 1] + y1[x + 1]) * 0.33)) * 0.35
                glasses.pixel(x - 1, y, colormap[min(31, int(data[y][x]))])

        # That's all well and good for the matrix, but what about the extra
        # LEDs in the rings? Since these don't align to the pixel grid,
        # rather than trying to extend the raster data and filter it in
        # somehow, we'll fill those arcs with colors interpolated from the
        # endpoints where rings and matrix intersect. Maybe not perfect,
        # but looks okay enough!
        interp(glasses.left_ring, 7, 17, data[4][8], data[4][1])
        interp(glasses.left_ring, 21, 29, data[0][2], data[2][8])
        interp(glasses.right_ring, 7, 17, data[4][18], data[4][11])
        interp(glasses.right_ring, 19, 27, data[2][11], data[0][17])

        glasses.show()  # Buffered mode MUST use show() to refresh matrix

    except OSError:  # See "try" notes above regarding rare I2C errors.