├── .gitignore ├── media ├── scope.png ├── dropout.png ├── open-loop.png ├── waveform.png ├── closed-loop.png ├── cr-working.png ├── interpolation.drawio ├── frame.drawio ├── subcode2.drawio ├── cr.drawio ├── subcode.drawio ├── interpolation.svg ├── circ-dec.drawio ├── cr.svg ├── frame.svg └── subcode2.svg ├── analyze.py ├── decode.py ├── efm.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.drawio.bkp 2 | *.drawio.dtmp 3 | data/ 4 | -------------------------------------------------------------------------------- /media/scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carrotIndustries/redbook/HEAD/media/scope.png -------------------------------------------------------------------------------- /media/dropout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carrotIndustries/redbook/HEAD/media/dropout.png -------------------------------------------------------------------------------- /media/open-loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carrotIndustries/redbook/HEAD/media/open-loop.png -------------------------------------------------------------------------------- /media/waveform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carrotIndustries/redbook/HEAD/media/waveform.png -------------------------------------------------------------------------------- /media/closed-loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carrotIndustries/redbook/HEAD/media/closed-loop.png -------------------------------------------------------------------------------- /media/cr-working.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carrotIndustries/redbook/HEAD/media/cr-working.png -------------------------------------------------------------------------------- /media/interpolation.drawio: -------------------------------------------------------------------------------- 1 | 5VlRc6IwEP41PN5NCETxsdV693B3czedmz6nEDHXSLwQW+2vv0QCSLACVry2Os4Qls2XZL/dJRscb7xYfxF4Of/OI8IcCKK1400cCBGEjv6DaGMEYJAJYkGjTOSWglv6TIwQGOmKRiStKErOmaTLqjDkSUJCWZFhIfhTVW3GWXXUJY5JTXAbYlaX3tFIzjNpgEAp/0poPM9HdoF5ssC5soFI5zjiT5loq+PdON5YcC6z1mI9JkzbLrdLBjR94WkxMUES2abDht/9+RX+hWh04z4Eg1XM7yafDMojZiuzYDNZucktQJLoShtS3XGl53jXc7lQl4mrmoKvkojoEYC6U6pTyvJnGRKJagYuZ+wWdlD+Q/iCSLFRKk+lpXNDz3eMnMsEYVjSxyo8NoTHBVwxwk9O1cAQGN+EgcExrlng5hApX4mQmF67lrWAPGgBQQtIYhETWQNSjZ1ll6ItcR1IhBdMYqPtW5M4sIDAeUn0LpnEJtu3JdEHVSB3dF4S/QsmsdH2rUn0/y+J6IJJhJbt4fBIEhtfrj2TGFwyiVYkFlx0JrHJG3omcXTBJLr2O3FwLIlN3tAziXkl15LFkOE0peEhIt88dciyuH/sm7AJqG/q9pWHao/jy+xS51Fx8g3fEysOMaNxorlV/BChBI9ESKqK7CvzYEGjSGNcC5LSZ3y/xdNUL/XStotF1w6aFORrALKu8GNOAExnpygEdt3igIPWncXAg88+8oOTOEYwqvbgs1lK+mGuW034EYLOypfesTWEDeSeO18OXgi63x8q6GBD0A0Q8io85GdVr/STkQVqAfQYkt22pAlPyKF4TKXgD2TMGRfbvt50CtTvHUbq0edudqFYHBo3RKoyMd7sqBl/b18UIXBwXrUzqa76bkVfNbIZn9Yd65vrH1iTqhyS0ZAmcddEw8hMvrk0EzSkGYSGVdufxMWHcB9o/zkGdttrd84xOsNMp+8xxxxbPdVyTMuz/c45ZuTvnfCLVZ2tD8+QM2C9HrjNcwXAM73NUAh6u7Hkmj6efIQcApvqg7PkEP/1OUTdlp8bM/Xym6138w8= -------------------------------------------------------------------------------- /media/frame.drawio: -------------------------------------------------------------------------------- 1 | 7VzbdqM2FP0aP06WuJvHJE5mHjKpV726mnmUQcbqAHJl+TZfXwkQGIQTHN/UFj8k6CAJ65zNPlsSeGA9JtuvFC7m30mI4oEJwu3AGg1M0/IA/ysMu8JgmLkhojjMTUZlmOBfqDAW7aIVDtGyVpEREjO8qBsDkqYoYDUbpJRs6tVmJK5fdQEjpBgmAYxV6584ZPPc6gNQ2b8hHM3llV15JoGycmFYzmFINnsm62lgPVJCWH6UbB9RLHwn/ZK3ez5wtvxiFKWsS4PXmf82+eP30IAPzux1PXn5Lf72pehlDeNVMeCB6ca8v4cQr/lhJA4nuzSQZt7/3pmWyqYtjVParNZsnLmF7aSvKVmlIRJfF/DTmzlmaLKAgTi74eDitjlLYl4y+OEMx/EjiQnN2lqhg4ahze1LRslPtHdmaE4t1y2vt0aUoe1BHxplZDiiEUkQozteRTawi2DuynDn5U2FDacwzfdgIZvBAo1R2XMVMH5QxOyI+JlK/KzT3HoOJ/kfO8m4ppMsxUm8H2DYZwXgbIbcIGgDYOj5UwDO41sTfOxb85q+dTQEYHkL6gJAtwvLjjpTrHEbikUGJ1mvDeG+61nwTBRrWpoh3FYhrrgVpeG9EBu8lJIUiXDB5Tzzs1H3aT0AotYYMoZomp03gUhhaIvZW1FfHP8Qte+cojTaFo2zwk4WUj7Wt/3CXitRrJplJdkuHwoKFRX0Ybw+SHnSRlEMGV7Xu28LUnGFMcH8whUc3AYcho04L8mKBqhotS9/Gh1ZTeZsdsQgjRBTOsowUw77BBh1IgGhjsEIMsj/TXeMK99u97W45TBXrS9wiuIxWWKGiYDUlDBGEnHPSvUJ9sEpCjDGkaga8IAjWker7Pa+qMPIorz/pRw28t4X4psk20jMAu4SEvxcLe4SSMW/YEXj3QPNaEYlD5B9CrqZFENaklh4s0lBZd0QU6728zFu0JJdhnvKLFFLHC1gNy9GPp6G+dXSLb/awy731rh71tQl5c6GAWoXldOhYztnEpWWbinXaZuW9in3SinXaqZcz7/z3M9lXbuZdVv6unDidXScI9umZhTqqJPknkKPiKd283K7Szx5JTCGFLNdU28eHaNegV4iFd9egXpqLn6mMOGH99zqDLn0AsEcpmm27j/FbNkZGkUsz4KLCmZ6QQPBDBpxY+jlkM6AGWVR2lIxY/tXxYyOq4KWbinX65cFj4iebgnW03FmbuqGcV+Vlbf3kq3b+oXfSaz1TJBHQbfVCr/fILjhaoV9rg0C58YbBH6/QaD99KzJPbefnvk6yhBHuwTbbxCcFE/tUq7fp9zbpVznjBsE7u03CErC1opDXd2mcgbo9LBoT6KHAqrbCoYBzC4B7fcINBOhzWx8exFqAB2XeRz9GLRf5zkifPrxpauKTC7SJBsQyuYkIimMnyrrQ2V9IYKXMof+hRjbFS+7wBUjDXeTlD3DBMdi5I9cSGHOeCZ4RZuaWDVqErNSlT/2zx2QmFLOfgF3AHg1TWt71vuqlhfGiGLuVEHERylW7rtMF77r5WI5LVd979Vsh8+pD5w2WMOS3+dYZWu4B2j6arJWXZc0QPmpHSq45rctq4NSJuAYzVhLzk1wGGZ4p2iJf8Fp1pGAxkKMLxux8zBwRqInDvhljn1DIZFiqrbPONL03l0hTsr7UMrUkzZSGnuqVkuONdtyrHU59hn+N9nHrrGPA4zbso/fkX2sC7GPXyeN4WfZp/ESlgLMi7NP2xJNLk0EwviZGSxC4f69Ei9BPnxD8RoJVqlM5SuIq2lAQrQnbvJODqibfx19tUwqLsBipqeymHdVEpM0ekgBAxmnCgJZxBRAnICl74hGOI0G8mmlA5D6pLDuodcOPV+FXtszSReEnqHE6ub58yp58OOkJ+/KDuE9Nb8Z9fz2eXUtn5mQ+AJXzm/ydwz+F6/ORxSGmEOj9r6z+OydG6lPPZ6BR9wOW09neuOeF6tfY8hxUv2khfX0Dw== -------------------------------------------------------------------------------- /media/subcode2.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /media/cr.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /analyze.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | 5 | def read_csv(filename) : 6 | for line in open(filename, "r") : 7 | if line.startswith("#") : 8 | continue 9 | yield float(line.strip().split(",")[1]) 10 | 11 | def interpolate(samples, factor) : 12 | last = next(samples) 13 | yield last 14 | for sample in samples : 15 | for i in range(factor) : 16 | t = (i+1)/factor 17 | yield last*(1-t) + sample*(t) 18 | last = sample 19 | 20 | def slice_bits(samples) : 21 | threshold = 0 22 | hyst = .01 23 | for sample in samples : 24 | if sample > threshold: 25 | yield True 26 | threshold = -hyst 27 | else : 28 | yield False 29 | threshold = hyst 30 | 31 | 32 | #%% 33 | samples = list(itertools.islice(read_csv("data/scope.csv"), 200)) 34 | plt.plot(samples) 35 | plt.xlabel("sample") 36 | plt.ylabel("Voltage [V]") 37 | plt.xlim(0, 200) 38 | plt.grid() 39 | 40 | #%% 41 | 42 | samples_interpolated = interpolate( read_csv("data/scope.csv"), 20) 43 | 44 | all_bits = slice_bits(samples_interpolated) 45 | 46 | acc = 0 47 | acc_size = 1000 48 | ftw = 0 49 | ftw0 = 42.7 50 | lastbit = False 51 | 52 | bits = [] 53 | accs = [] 54 | sampled_bits = [] 55 | phase_deltas_filtered = [] 56 | phase_delta = 0 57 | phase_delta_filtered = 0 58 | integ = 0 59 | decoded_bits = [] 60 | last_acc = 0 61 | ftws = [] 62 | integs = [] 63 | integ_max = 927681 64 | 65 | alpha = .005 66 | Ki = .0000004 67 | Kp = .001 68 | 69 | 70 | debug = False 71 | 72 | for bit in all_bits: 73 | if debug : 74 | bits.append(bit) 75 | if bit != lastbit : # input transition 76 | phase_delta = (acc_size/2 - acc) 77 | if acc < last_acc : # phase accumulator has wrapped around 78 | sampled_bits.append(bit) 79 | last_acc = acc 80 | phase_delta_filtered = phase_delta*alpha + phase_delta_filtered*(1-alpha) 81 | integ += phase_delta_filtered 82 | if integ > integ_max : 83 | integ = integ_max 84 | elif integ < -integ_max : 85 | integ = -integ_max 86 | integs.append(integ) 87 | 88 | ftw = ftw0 + phase_delta_filtered*Kp + integ * Ki 89 | if debug : 90 | phase_deltas_filtered.append(phase_delta_filtered/acc_size) 91 | lastbit = bit 92 | acc = (acc+ftw)%acc_size 93 | if debug and len(bits) > 200000 : 94 | break 95 | if debug : 96 | accs.append(acc/acc_size) 97 | ftws.append(ftw) 98 | 99 | #%% 100 | plt.rcParams["figure.figsize"] = (15,4) 101 | if debug : 102 | plt.plot(np.array(phase_deltas_filtered[::10000])*360) 103 | plt.xlabel("sample") 104 | plt.ylabel("filtered phase error [degree]") 105 | plt.grid() 106 | print(ftw) 107 | #%% 108 | 109 | if debug : 110 | plt.xlim(len(bits)-400, len(bits)) 111 | plt.plot(accs, label="VCO phase (normalized)") 112 | plt.plot([0, len(bits)], [.5, .5], label="180°") 113 | plt.plot(bits, label="bits") 114 | plt.grid() 115 | plt.xlabel("sample") 116 | plt.legend() 117 | 118 | #%% 119 | 120 | nrz_bits = [a != b for a,b in zip(sampled_bits[1:], sampled_bits)] 121 | 122 | syncpat = [x == "1" for x in "1000-0000-0001-0000-0000-0010".replace("-", "")] 123 | last_i = None 124 | frames = [] 125 | frame_len = 588 126 | for i in range(len(nrz_bits)-len(syncpat)) : 127 | if nrz_bits[i:i+len(syncpat)] == syncpat : 128 | f = nrz_bits[i:i+frame_len] 129 | if len(f) == frame_len : 130 | frames.append(f) 131 | if last_i is not None : 132 | if i-last_i != frame_len : 133 | print("short fraeme", i-last_i) 134 | last_i = i 135 | 136 | #%% 137 | 138 | with open("data/frames.txt", "w") as ofile: 139 | ofile.write("\n".join("".join("01"[x] for x in frame) for frame in frames)) 140 | -------------------------------------------------------------------------------- /media/subcode.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /decode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Created on Mon Dec 26 21:38:11 2022 5 | 6 | @author: lukas 7 | """ 8 | 9 | import itertools 10 | import matplotlib.pyplot as plt 11 | import scipy.io 12 | import numpy as np 13 | #%% 14 | def extract_subcode(frame) : 15 | return frame[24+3:24+3+14] 16 | 17 | def list_to_int(l) : 18 | return sum(a*(1<>4)&0xf) 25 | 26 | #%% 27 | 28 | with open('data/frames.txt', 'r') as infile: 29 | frames = [[bool(int(x)) for x in line.strip()] for line in infile] 30 | 31 | #%% 32 | 33 | s0 = [x == "1" for x in '00100000000001'] 34 | s1 = [x == "1" for x in '00000000010010'] 35 | 36 | blocks = [] 37 | 38 | for i, frame in enumerate(frames) : 39 | subcode = extract_subcode(frame) 40 | if subcode == s0 : 41 | #print(i, "s0") 42 | if extract_subcode(frames[i+1]) == s1 : 43 | b = frames[i:i+98] 44 | if len(b) == 98 : 45 | blocks.append(b) 46 | elif subcode == s1 : 47 | #print(i, "s1") 48 | pass 49 | 50 | #%% 51 | 52 | efms = {} 53 | 54 | with open("efm.txt") as infile : 55 | for line in infile: 56 | dec, b, efm = line.strip().split() 57 | dec = int(dec) 58 | b = int(b, 2) 59 | efm = int(efm, 2) 60 | #assert(dec == b) 61 | efms[efm] = b 62 | #print(line) 63 | #%% 64 | 65 | def calc_crc(bits) : 66 | poly = 0x1021 67 | crc = 0 68 | for bit in bits : 69 | if (crc>>15)&1 != bit : 70 | crc = ((crc<<1)&0xffff) ^ poly 71 | else : 72 | crc = ((crc<<1)&0xffff) 73 | return crc 74 | 75 | 76 | 77 | for block in blocks : 78 | subcodes = [extract_subcode(frame) for frame in block] 79 | assert(subcodes[0] == s0) 80 | assert(subcodes[1] == s1) 81 | subcodes_payload = [efms.get(list_to_int(x), 0) for x in subcodes[2:]] 82 | assert(len(subcodes_payload) == 96) 83 | q = [bool(x & 64) for x in subcodes_payload] 84 | q_for_crc = [ not x if i>= 96-16 else x for i,x in enumerate(q)] 85 | #printbin(q_for_crc) 86 | crc_syndrome = calc_crc(q_for_crc) 87 | adr = list_to_int(q[4:4+4]) 88 | #print(adr) 89 | if crc_syndrome == 0 : 90 | if adr == 1: 91 | data_bytes = [list_to_int(q[x:x+8]) for x in range(8, 80, 8)] 92 | tno, idx, rmin, rsec, rframe, zero, amin, asec, aframe = (bcd_to_dec(x) for x in data_bytes) 93 | 94 | print(f"Track {tno}.{idx} R={rmin:02d}:{rsec:02d}:{rframe:02d} A={amin:02d}:{asec:02d}:{aframe:02d}") 95 | else : 96 | print("A", adr) 97 | else : 98 | print("CRC error") 99 | #printbin(q) 100 | 101 | #%% 102 | 103 | 104 | def symbols_from_frame(frame) : 105 | symbol_idxs = (24+3+14+3+(i*(14+3)) for i in range(32)) 106 | return [efms[list_to_int(frame[i:i+14])] for i in symbol_idxs] 107 | 108 | class Delay: 109 | def __init__(self, n_delay, fill = None) : 110 | self.register = [fill]*n_delay 111 | 112 | def step(self, v) : 113 | if len(self.register) == 0 : 114 | return v 115 | r = self.register[-1] 116 | self.register = [v] + self.register[:-1] 117 | return r 118 | 119 | 120 | deinterleave_tab = ( 121 | 0, 122 | 1, 123 | 6, 124 | 7, 125 | 16, 126 | 17, 127 | 22, 128 | 23, 129 | 2, 130 | 3, 131 | 8, 132 | 9, 133 | 18, 134 | 19, 135 | 24, 136 | 25, 137 | 4, 138 | 5, 139 | 10, 140 | 11, 141 | 20, 142 | 21, 143 | 26, 144 | 27, 145 | ) 146 | 147 | def has_last_delay(i) : 148 | return i in (4,5,6,7, 12,13,14,15, 20,21,22,23) 149 | 150 | def extract_audio(frames) : 151 | first_delays = [Delay(0 if i%2 == 0 else 1, 0) for i in range(32)] 152 | second_delays = [Delay(i*4, 0) for i in reversed(range(28))] 153 | third_delays = [Delay(2 if has_last_delay(i) else 0, 0) for i in range(24)] 154 | for frameidx, frame in enumerate(frames) : 155 | try : 156 | symbols_in = symbols_from_frame(frame) 157 | 158 | symbols_delayed1 = [d.step(x) for d,x in zip(first_delays, symbols_in)] 159 | #symbols_delayed1_inverted = [x != (i in (12,13,14,15, 28,29,30,31)) for i,x in enumerate(symbols_delayed1)] 160 | 161 | # skip c1 decoder 162 | 163 | symbols_delayed2 = [d.step(x) for d,x in zip(second_delays, symbols_delayed1)] 164 | 165 | #skip c2 decoder 166 | 167 | symbols_deinterleaved = [symbols_delayed2[i] for i in deinterleave_tab] 168 | 169 | symbols_out = [d.step(x) for d,x in zip(third_delays, symbols_deinterleaved)] 170 | 171 | yield from symbols_out 172 | 173 | except KeyError as e : 174 | print(i,e) 175 | 176 | 177 | def combine_samples(samples_raw) : 178 | while chunk := list(itertools.islice(samples_raw, 2)): 179 | if len(chunk) != 2 : 180 | continue 181 | i = (chunk[0] << 8) + chunk[1] 182 | if i & (1<<15) : 183 | i -= (1<<16) 184 | yield i 185 | 186 | 187 | #%% 188 | samples = np.array(list(combine_samples(extract_audio(frames))), dtype=np.int16) 189 | scipy.io.wavfile.write("data/cd.wav", 44100, samples.reshape((len(samples)//2, 2))) 190 | -------------------------------------------------------------------------------- /media/interpolation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
$$t$$
$$U$$
Naive slicing
Naive slicing
Slicing after interpolation
Slicing after interpolation
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /efm.txt: -------------------------------------------------------------------------------- 1 | 0 00000000 01001000100000 2 | 1 00000001 10000100000000 3 | 2 00000010 10010000100000 4 | 3 00000011 10001000100000 5 | 4 00000100 01000100000000 6 | 5 00000101 00000100010000 7 | 6 00000110 00010000100000 8 | 7 00000111 00100100000000 9 | 8 00001000 01001001000000 10 | 9 00001001 10000001000000 11 | 10 00001010 10010001000000 12 | 11 00001011 10001001000000 13 | 12 00001100 01000001000000 14 | 13 00001101 00000001000000 15 | 14 00001110 00010001000000 16 | 15 00001111 00100001000000 17 | 16 00010000 10000000100000 18 | 17 00010001 10000010000000 19 | 18 00010010 10010010000000 20 | 19 00010011 00100000100000 21 | 20 00010100 01000010000000 22 | 21 00010101 00000010000000 23 | 22 00010110 00010010000000 24 | 23 00010111 00100010000000 25 | 24 00011000 01001000010000 26 | 25 00011001 10000000010000 27 | 26 00011010 10010000010000 28 | 27 00011011 10001000010000 29 | 28 00011100 01000000010000 30 | 29 00011101 00001000010000 31 | 30 00011110 00010000010000 32 | 31 00011111 00100000010000 33 | 32 00100000 00000000100000 34 | 33 00100001 10000100001000 35 | 34 00100010 00001000100000 36 | 35 00100011 00100100100000 37 | 36 00100100 01000100001000 38 | 37 00100101 00000100001000 39 | 38 00100110 01000000100000 40 | 39 00100111 00100100001000 41 | 40 00101000 01001001001000 42 | 41 00101001 10000001001000 43 | 42 00101010 10010001001000 44 | 43 00101011 10001001001000 45 | 44 00101100 01000001001000 46 | 45 00101101 00000001001000 47 | 46 00101110 00010001001000 48 | 47 00101111 00100001001000 49 | 48 00110000 00000100000000 50 | 49 00110001 10000010001000 51 | 50 00110010 10010010001000 52 | 51 00110011 10000100010000 53 | 52 00110100 01000010001000 54 | 53 00110101 00000010001000 55 | 54 00110110 00010010001000 56 | 55 00110111 00100010001000 57 | 56 00111000 01001000001000 58 | 57 00111001 10000000001000 59 | 58 00111010 10010000001000 60 | 59 00111011 10001000001000 61 | 60 00111100 01000000001000 62 | 61 00111101 00001000001000 63 | 62 00111110 00010000001000 64 | 63 00111111 00100000001000 65 | 64 01000000 01001000100100 66 | 65 01000001 10000100100100 67 | 66 01000010 10010000100100 68 | 67 01000011 10001000100100 69 | 68 01000100 01000100100100 70 | 69 01000101 00000000100100 71 | 70 01000110 00010000100100 72 | 71 01000111 00100100100100 73 | 72 01001000 01001001000100 74 | 73 01001001 10000001000100 75 | 74 01001010 10010001000100 76 | 75 01001011 10001001000100 77 | 76 01001100 01000001000100 78 | 77 01001101 00000001000100 79 | 78 01001110 00010001000100 80 | 79 01001111 00100001000100 81 | 80 01010000 10000000100100 82 | 81 01010001 10000010000100 83 | 82 01010010 10010010000100 84 | 83 01010011 00100000100100 85 | 84 01010100 01000010000100 86 | 85 01010101 00000010000100 87 | 86 01010110 00010010000100 88 | 87 01010111 00100010000100 89 | 88 01011000 01001000000100 90 | 89 01011001 10000000000100 91 | 90 01011010 10010000000100 92 | 91 01011011 10001000000100 93 | 92 01011100 01000000000100 94 | 93 01011101 00001000000100 95 | 94 01011110 00010000000100 96 | 95 01011111 00100000000100 97 | 96 01100000 01001000100010 98 | 97 01100001 10000100100010 99 | 98 01100010 10010000100010 100 | 99 01100011 10001000100010 101 | 100 01100100 01000100100010 102 | 101 01100101 00000000100010 103 | 102 01100110 01000000100100 104 | 103 01100111 00100100100010 105 | 104 01101000 01001001000010 106 | 105 01101001 10000001000010 107 | 106 01101010 10010001000010 108 | 107 01101011 10001001000010 109 | 108 01101100 01000001000010 110 | 109 01101101 00000001000010 111 | 110 01101110 00010001000010 112 | 111 01101111 00100001000010 113 | 112 01110000 10000000100010 114 | 113 01110001 10000010000010 115 | 114 01110010 10010010000010 116 | 115 01110011 00100000100010 117 | 116 01110100 01000010000010 118 | 117 01110101 00000010000010 119 | 118 01110110 00010010000010 120 | 119 01110111 00100010000010 121 | 120 01111000 01001000000010 122 | 121 01111001 00001001001000 123 | 122 01111010 10010000000010 124 | 123 01111011 10001000000010 125 | 124 01111100 01000000000010 126 | 125 01111101 00001000000010 127 | 126 01111110 00010000000010 128 | 127 01111111 00100000000010 129 | 128 10000000 01001000100001 130 | 129 10000001 10000100100001 131 | 130 10000010 10010000100001 132 | 131 10000011 10001000100001 133 | 132 10000100 01000100100001 134 | 133 10000101 00000000100001 135 | 134 10000110 00010000100001 136 | 135 10000111 00100100100001 137 | 136 10001000 01001001000001 138 | 137 10001001 10000001000001 139 | 138 10001010 10010001000001 140 | 139 10001011 10001001000001 141 | 140 10001100 01000001000001 142 | 141 10001101 00000001000001 143 | 142 10001110 00010001000001 144 | 143 10001111 00100001000001 145 | 144 10010000 10000000100001 146 | 145 10010001 10000010000001 147 | 146 10010010 10010010000001 148 | 147 10010011 00100000100001 149 | 148 10010100 01000010000001 150 | 149 10010101 00000010000001 151 | 150 10010110 00010010000001 152 | 151 10010111 00100010000001 153 | 152 10011000 01001000000001 154 | 153 10011001 10000010010000 155 | 154 10011010 10010000000001 156 | 155 10011011 10001000000001 157 | 156 10011100 01000010010000 158 | 157 10011101 00001000000001 159 | 158 10011110 00010000000001 160 | 159 10011111 00100010010000 161 | 160 10100000 00001000100001 162 | 161 10100001 10000100001001 163 | 162 10100010 01000100010000 164 | 163 10100011 00000100100001 165 | 164 10100100 01000100001001 166 | 165 10100101 00000100001001 167 | 166 10100110 01000000100001 168 | 167 10100111 00100100001001 169 | 168 10101000 01001001001001 170 | 169 10101001 10000001001001 171 | 170 10101010 10010001001001 172 | 171 10101011 10001001001001 173 | 172 10101100 01000001001001 174 | 173 10101101 00000001001001 175 | 174 10101110 00010001001001 176 | 175 10101111 00100001001001 177 | 176 10110000 00000100100000 178 | 177 10110001 10000010001001 179 | 178 10110010 10010010001001 180 | 179 10110011 00100100010000 181 | 180 10110100 01000010001001 182 | 181 10110101 00000010001001 183 | 182 10110110 00010010001001 184 | 183 10110111 00100010001001 185 | 184 10111000 01001000001001 186 | 185 10111001 10000000001001 187 | 186 10111010 10010000001001 188 | 187 10111011 10001000001001 189 | 188 10111100 01000000001001 190 | 189 10111101 00001000001001 191 | 190 10111110 00010000001001 192 | 191 10111111 00100000001001 193 | 192 11000000 01000100100000 194 | 193 11000001 10000100010001 195 | 194 11000010 10010010010000 196 | 195 11000011 00001000100100 197 | 196 11000100 01000100010001 198 | 197 11000101 00000100010001 199 | 198 11000110 00010010010000 200 | 199 11000111 00100100010001 201 | 200 11001000 00001001000001 202 | 201 11001001 10000100000001 203 | 202 11001010 00001001000100 204 | 203 11001011 00001001000000 205 | 204 11001100 01000100000001 206 | 205 11001101 00000100000001 207 | 206 11001110 00000010010000 208 | 207 11001111 00100100000001 209 | 208 11010000 00000100100100 210 | 209 11010001 10000010010001 211 | 210 11010010 10010010010001 212 | 211 11010011 10000100100000 213 | 212 11010100 01000010010001 214 | 213 11010101 00000010010001 215 | 214 11010110 00010010010001 216 | 215 11010111 00100010010001 217 | 216 11011000 01001000010001 218 | 217 11011001 10000000010001 219 | 218 11011010 10010000010001 220 | 219 11011011 10001000010001 221 | 220 11011100 01000000010001 222 | 221 11011101 00001000010001 223 | 222 11011110 00010000010001 224 | 223 11011111 00100000010001 225 | 224 11100000 01000100000010 226 | 225 11100001 00000100000010 227 | 226 11100010 10000100010010 228 | 227 11100011 00100100000010 229 | 228 11100100 01000100010010 230 | 229 11100101 00000100010010 231 | 230 11100110 01000000100010 232 | 231 11100111 00100100010010 233 | 232 11101000 10000100000010 234 | 233 11101001 10000100000100 235 | 234 11101010 00001001001001 236 | 235 11101011 00001001000010 237 | 236 11101100 01000100000100 238 | 237 11101101 00000100000100 239 | 238 11101110 00010000100010 240 | 239 11101111 00100100000100 241 | 240 11110000 00000100100010 242 | 241 11110001 10000010010010 243 | 242 11110010 10010010010010 244 | 243 11110011 00001000100010 245 | 244 11110100 01000010010010 246 | 245 11110101 00000010010010 247 | 246 11110110 00010010010010 248 | 247 11110111 00100010010010 249 | 248 11111000 01001000010010 250 | 249 11111001 10000000010010 251 | 250 11111010 10010000010010 252 | 251 11111011 10001000010010 253 | 252 11111100 01000000010010 254 | 253 11111101 00001000010010 255 | 254 11111110 00010000010010 256 | 255 11111111 00100000010010 257 | -------------------------------------------------------------------------------- /media/circ-dec.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /media/cr.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
phase_delta
phase_delta
phase_delta_filtered
phase_delta_filtered
$$\phi$$
Phase detector
Phase dete...
bit
bit
Loop
filter
Loop...
$$\int$$
$$K_i$$
$$K_p$$
ftw
ftw
acc
acc
VCO
VCO
DQQ
bits from
 slicer
bits from...
sampled
bits
sampled...
for bit in all_bits:
    if bit != lastbit : # input transition
        phase_delta = (acc_size/2 - acc)
    last_acc = acc
    phase_delta_filtered = phase_delta*alpha + phase_delta_filtered*(1-alpha)
    integ += phase_delta_filtered
    
    ftw = ftw0 + phase_delta_filtered*Kp + integ * Ki
    lastbit = bit
    acc = (acc+ftw)%acc_size

    if acc < last_acc : # phase accumulator has wrapped around
        sampled_bits.append(bit)
for bit in all_bits:...
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Reading the red book – decoding compact disc digital audio 2 | ========================================================== 3 | 4 | 5 | Much has been 6 | [written](https://en.wikipedia.org/wiki/Compact_Disc_Digital_Audio) and 7 | [said](https://www.youtube.com/playlist?list=PLv0jwu7G_DFWBEyCKt4tKHIk8ez_pZS_P) about the technical details of 8 | compact disc digital audio, but so far, I've found no public 9 | record [^1] of anyone actually decoding the pits and lands on a compact disc 10 | to PCM samples. So I decided to do so. For an introduction on how compact discs work, I highly 11 | recommend the linked Wikipedia article and [Technolgy Connection's video 12 | series](https://www.youtube.com/playlist?list=PLv0jwu7G_DFWBEyCKt4tKHIk8ez_pZS_P). 13 | 14 | # Ingredients 15 | 16 | ## Literature 17 | 18 | There's a lot of secondary literature on compact disc digital audio, 19 | but nothing's better than getting the information straight from the horses 20 | mouth. In our case, this horse is called "IEC 60908 Audio recording – 21 | Compact disc digital audio system". It's available for purchase from 22 | several publishers at the totally reasonable price of just €345. 23 | Fortunately someone uploaded it to 24 | [archive.org](https://archive.org/details/RedBookAudioRecordingCompactDiscDigitalAudioSystemIEC60908SecondEdition199902ISBN2831846382) 25 | , which is where such a fundamental standard belongs. 26 | The PDF is bilingual with every other page being in french, but that's 27 | noting that can't be fixed with poppler's `pdfseparate` and `pdfunite`. 28 | 29 | ## Reading pits and lands 30 | 31 | Since we don't want to read the pits and lands from the disc with a microscope[^1], we need some kind of machine that converts them to 32 | some form that easier to process. Luckily such machines exist in the 33 | form of CD players, we just need to get directly to the electrical signal from the 34 | optical pickup that corresponds to pits and lands on the disc and 35 | ignore the fact that it already decodes everything. 36 | 37 | So I got hold of an old DVD player, put in an audio CD and started probing pins on the 38 | connector that connects the optical pickup to the main board. I quickly 39 | found a plausible-looking signal of about 300 mVpp amplitude. 40 | 41 | ![Oscilloscope screenshot showing an intensity-graded eye diagram-like 42 | waveform.](media/scope.png) 43 | 44 | For some reason, ~probably dust or scratches on the disc~ ([see 45 | later](#somethings-not-quite-right)), the signal 46 | sometimes drops out. To make my life easier, I captured a 4 MSa portion of 47 | the signal without any dropouts at a rate of 20MSa/s[^2] and transferred it to my computer for 48 | further analysis in python. 49 | 50 | # Interpolation and slicing 51 | 52 | For reference, here's what the captured signal looks like, rendered 53 | using linear interpolation: 54 | 55 | ![Plot of a low pass filtered two-level signal.](media/waveform.png) 56 | 57 | It's important to note that this signal isn't generated by an electronic circuit, 58 | but rather by the pits and lands flying past the pickup. 59 | 60 | The signal captured from the pickup is an analog two-level signal that 61 | we need to convert to ones and zeros for further processing. It's 62 | tempting to just look at each sample individually and turn it into a 63 | '1' if it's > 0 and to a '0' if not. However, we lose some significant 64 | information that way since the exact voltage at the zero crossing 65 | carries sub-sample timing information that comes in handy for easier 66 | clock recovery as it reduces jitter. One lazy way around this is to 67 | interpolate the acquired samples and then do the threshold detection. 68 | 69 | Diagram showing how 
 70 | interpolation is used to accurately find zero crossings. 71 | 72 | I've found that linear interpolation gives comparable results to proper 73 | sinc interpolation, so that's what I went with. To avoid glitches, I 74 | added some hysteresis to the threshold detection. The interpolation 75 | ratio is 20. 76 | 77 | # Clock recovery 78 | 79 | The first step in decoding any kind of signal without an explicit clock 80 | is recovering the clock from the signal itself. This usually aided by 81 | some kind of line coding that ensures that there are only so many 82 | consecutive bits without a transition. In our case the line coding is 83 | [Eight-to-fourteen 84 | modulation](https://en.wikipedia.org/wiki/Eight-to-fourteen_modulation) 85 | that maps one byte to 14 channel bits. It is then 86 | pressed onto the disc using NRZ-I (Non-return to zero inverted) 87 | encoding, that is a '1' is encoded as a transition 88 | and a '0' as no transition. The combination of these two 89 | encodings guarantees that there are no more than 11 and no less 90 | than 3 unchanging consecutive bits. 91 | 92 | This means that when looking at the signal we can't assume that the 93 | shortest time between two transitions is one unit interval, instead 94 | it's three. 95 | 96 | The usual way of recovering the clock from a signal with an embedded 97 | clock is by means of a [phase-locked 98 | loop](https://en.wikipedia.org/wiki/Phase-locked_loop). While these are 99 | usually implemented in a mixed-signal circuit, implementing one in 100 | software can be surprisingly easy. In this instance, it can be seen as 101 | a discrete-time simulation that's clocked at 400 MHz, i.e. on every 102 | interpolated bit. 103 | 104 | Same as with a hardware PLL we need three main components. VCO, phase 105 | detector and loop filter. 106 | 107 | ## VCO 108 | 109 | A simple way of implementing a VCO in software is by means of an 110 | [Numerically-controlled 111 | oscillator](https://en.wikipedia.org/wiki/Numerically-controlled_oscillator). 112 | Since all we need is know when to sample the input signal, the 113 | phase-to-amplitude converter part of the NCO can be reduced to 114 | detecting if the accumulator has wrapped around. 115 | 116 | The phase accumulator is as simple as adding the frequency tuning word 117 | to the accumulator modulo the accumulator size on every clock cycle : 118 | 119 | ```python 120 | acc = 0 121 | last_acc = 0 122 | ftw = 42 123 | acc_size = 1000 124 | for bit in all_bits : 125 | if acc < last_acc : 126 | # integrator has wrapped around, sample the input 127 | last_acc = acc 128 | acc = (acc+ftw)%acc_size # that's the actual NCO 129 | ``` 130 | 131 | ## Phase detector 132 | 133 | The job of the phase detector is to convert the phase difference 134 | between the output of the VCO and the incoming data stream into a 135 | proportional voltage. With the VCO being an NCO, implementing the phase detector is 136 | as simple as sampling the value of the phase accumulator whenever the 137 | input signal changes. That way, the phase detector also keeps its output 138 | constant in the absence of transitions at the input. To keep the sampling point 139 | as far away from the input transitions as possible, we want the phase of the 140 | VCO to be 180° at the transitions. 141 | 142 | ```python 143 | last_bit = False 144 | phase_delta = 0 145 | for bit in all_bits : 146 | if last_bit != bit : 147 | phase_delta = (acc_size/2 - acc) 148 | last_bit = bit 149 | 150 | ``` 151 | 152 | ## Loop filter 153 | 154 | To smooth the output of the phase detector before feeding it into the 155 | VCO, we need some kind of low-pass filter. I went with the equivalent 156 | of a first order low pass since that's trivial to implement and turned 157 | out be good enough. 158 | 159 | ```python 160 | last_bit = False 161 | phase_delta = 0 162 | delta_filtered = 0 163 | for bit in all_bits : 164 | ... 165 | alpha = .005 166 | delta_filtered = delta*alpha + delta_filtered*(1-alpha) 167 | ... 168 | ``` 169 | 170 | ## Putting it all together 171 | 172 | Here's how it all looks connected: 173 | 174 | ![Block diagram of a PLL-based clock recovery with the equivalent 175 | python code on the right.](media/cr.svg) 176 | 177 | I found it really instructive to 178 | discover that a 179 | PLL-based clock recovery can be implemented in about a dozen lines of 180 | Python or any other imperative language without the use of any 181 | high-level tools such as Simulink. Apart from that it's quite 182 | fascinating how little it takes to implement a system that exhibits 183 | complex dynamic behaviour. Contrast that to other code, where the same 184 | number of lines just adds a couple of buttons to a window or so and 185 | requires calling into thousands of lines of library code. 186 | 187 | ## Making it lock 188 | 189 | Anyone who has ever dealt with closed-loop feedback systems will tell you that 190 | debugging them can be really difficult since it's hard to separate 191 | cause from effect. 192 | 193 | To get around this, we first operate our PLL open-loop by setting the loop 194 | gain to zero. It's also worth noting that a PLL with this kind of phase 195 | detector that's not sensitive to frequency will have a fairly tight 196 | lock range, which means that we need to get the VCO center frequency 197 | fairly close to its nominal frequency. 198 | 199 | After playing with the center frequency of the VCO and the loop filter 200 | corner frequency this is what we get: 201 | 202 | ![A noisy and glitchy sawtooth waveform.](media/open-loop.png) 203 | 204 | We can see that the phase detector outputs a sawtooth waveform which 205 | indicates that there's a frequency offset between the VCO and input 206 | frequency. Closing the loop by increasing the loop gain, the PLL locks 207 | and the phase error becomes constant. Why constant and not zero, you 208 | may ask? To bring the VCO to the correct frequency, its tuning voltage, 209 | i.e. the output of the phase detector and loop filter must be non-zero, 210 | resulting in a residual phase offset. We can eliminate that offset by 211 | introducing and integrator the loop so that we can get a zero phase 212 | detector output and non-zero tuning voltage. Tweaking the integrator 213 | gain, we get this: 214 | 215 | ![A noisy signal settling to zero.](media/closed-loop.png) 216 | 217 | 218 | The output of the phase detector being relatively close to zero indicates that our 219 | PLL is working as intended so we can move on to the next step, that is 220 | sampling the input signal with the recovered clock. As mentioned in the 221 | VCO section, this is as simple as capturing the value of the input 222 | signal every time the phase accumulator overflows. 223 | 224 | ```python 225 | sampled_bits = [] 226 | for bit in all_bits : 227 | if acc < last_acc : 228 | sampled_bits.append(bit) 229 | ... 230 | ``` 231 | 232 | This leaves us with a stream of bits that we need to make sense of. 233 | 234 | Here's another plot to verify that the clock recovery is working as it 235 | should: 236 | 237 | ![A sawtooth waveform and and rectangular waveform.](media/cr-working.png) 238 | 239 | We see that the VCO's phase is close to 180° when the input 240 | transitions and thus the phase wraparounds are as far from the edges as 241 | they can be. 242 | 243 | ## Lock range 244 | 245 | Now that the clock recovery was working, I was curious to see how close 246 | to the actual frequency I need to get the VCO's initial frequency, i.e. 247 | what the PLL's lock range is. The correct frequency tuning word the 248 | locked PLL settles on is 42.6. The minimum initial frequency tuning 249 | word to achieve this is 41.6, the maximum is 43.9. This results in a 250 | lock range of about ±2.3%. I don't know if this is particularly good or 251 | bad for a PLL-based clock recovery. 252 | 253 | # NRZI decoding 254 | 255 | As mentioned before, ones and zeros aren't encoded as-is on a CD, 256 | instead a '1' is econded as 257 | a transition and a '0' as no transition. This means it's insignificant 258 | whether a land on the disc is a high level or a low level. Decoding it is as simple as 259 | comparing each value to its predecessor: 260 | 261 | ```python 262 | nrz_bits = [a != b for a,b in zip(sampled_bits[1:], sampled_bits)] 263 | ``` 264 | 265 | # Framing 266 | 267 | Data on a compact disc is structured as frames of 588 channel bits: 268 | 269 | ![A diagram showing the frame structure.](media/frame.svg) 270 | 271 | First, we have a 24 bit long sync pattern. The sync pattern has been 272 | chosen in such a way that it can't appear in valid data, so we can be 273 | absolutely sure to have found the start of a frame if we've seen the 274 | sync pattern. 275 | 276 | To extract the frames from the bitstream, we do exactly this and write 277 | the sync pattern as well as the following 588-24 = 564 channel bits to 278 | a file for further decoding. The file contains each frame encoded as 588 279 | `1`s and `0`s per line. 280 | 281 | The sync word is followed by the EFM-encoded control byte and 32 282 | payload bytes, 24 of which are actual PCM samples and 8 of which are 283 | parity bytes. Each EFM-encoded byte is separated from its neighboring 284 | bytes by three merging bits to guarantee DC balance and meet the 285 | specified run length requirements. The actual value of the merging bits 286 | is irrelevant and isn't used in the decoding process. 287 | 288 | See [analyze.py](/analyze.py) for the implementation of all of this. 289 | 290 | # Frame decoding 291 | 292 | With the frames neatly put into a file we're now safely in the digital 293 | realm and can start to make sense of 294 | the data stored in them. Since the python code for this is much less 295 | interesting and magic than the clock recovery, there'll be fewer code snippets throughout this section. 296 | 297 | ## Subcode 298 | 299 | Apart from the actual PCM samples, there's metadata in the form of 300 | subcode on the disc. We'll deal with this first since it's simpler to 301 | decode than the PCM samples and it's much easier to tell if we got it right. 302 | 303 | This is the first time where we actually have to decode the 304 | eight-to-fourteen modulation. Decoding it is as simple as going through 305 | a lookup table that maps 14 bit words to 8 bit words. The contents of 306 | the lookup table are given as images in the standard. Too lazy to type 307 | thousands of ones and zeros, I OCR'd them using tesseract and 308 | massaged the resulting text into a lookup table that's read from the 309 | python script[^3]. 310 | 311 | The subcode is stored in a way that I found confusing at first: A CD 312 | contains 8 channels of subcode, one of which is actually interesting. 313 | Rather than storing the content of each subcode channel contiguously, 314 | The subcode byte in the frame carries 1 bit for each of the 8 subcode 315 | channels at once, so we get one bit for each subcode channel per frame. 316 | 317 | ![A diagram showing how the subcode is extracted from frames.](media/subcode.svg) 318 | 319 | 320 | The subcode itself also has some kind of framing structure, called 321 | blocks. Each block contains 96 payload bits. And is delimited by two 322 | 14-bit synchronization words S₀ and S₁ that don't map to anything the EFM LUT, so 323 | there's no ambiguity in finding the start of a block. 324 | 325 | The first (P) subcode channel encodes the start of a track and is '0' 326 | for the remainder of the track. The second (Q) channel is the one 327 | that contains data we can make sense of. 328 | 329 | All Q-channel blocks I decoded have the ADR bits set to `0001`, meaning that the 330 | DATA-Q bits are to be interpreted as Mode 1: 331 | 332 | ![A diagram showing the contents of the Q-Channel subcode.](media/subcode2.svg) 333 | 334 | It's a bit odd that all numbers are BCD-encoded, I guess it was done 335 | that way so displaying them on a 7-segment display requires the least 336 | amount of logic. 337 | 338 | - Track number: Current track on the Disc 339 | - Index: Tracks can be subdivided by indices, but very few discs use this 340 | - Min/Sec: Current run time of the track 341 | - Frame: Each second is subdivided into 75 frames (This frame is 342 | different from the 588-bit channel frame) 343 | 344 | Given that one frame of 588 channel bits contains 24 bytes of audio 345 | that make up 6 samples at a sample rate of 44.1 kHz, we get 346 | frames at a rate of 44.1kHz / 6 = 7350 frames/sec. Since it takes 98 347 | frames to form a block, this results in a block rate of 44.1kHz / 6 / 348 | 98 = 75 Hz. This explains why each second is subdivided the way it is. 349 | 350 | Decoding the data I captured, we get this: 351 | 352 | ``` 353 | Track 1.1 R=01:08:70 A=01:10:70 354 | Track 1.1 R=01:08:71 A=01:10:71 355 | Track 1.1 R=01:08:72 A=01:10:72 356 | Track 1.1 R=01:08:73 A=01:10:73 357 | Track 1.1 R=01:08:74 A=01:10:74 358 | Track 1.1 R=01:09:00 A=01:11:00 359 | Track 1.1 R=01:09:01 A=01:11:01 360 | Track 1.1 R=01:09:02 A=01:11:02 361 | Track 1.1 R=01:09:03 A=01:11:03 362 | ``` 363 | 364 | We can see that track number matches what was on the DVD player when I 365 | captured the waveform and that the frame number is incrementing as it 366 | should and wraps around at 75. 367 | 368 | Looking good so far! 369 | 370 | The only thing left to do is to check that the CRC is correct. The 371 | standard specifies the polynomial to be x¹⁶ + x¹² + x⁵ + 1 which is 372 | identical to the 16-bit CRC-CCITT and that the 373 | parity bits are stored inverted. Feeding the CONTROL, ADR, DATA-Q and 374 | inverted parity bits into the CRC should yield a zero CRC. 375 | 376 | ```python 377 | def calc_crc(bits) : 378 | poly = 0x1021 379 | crc = 0 380 | for bit in bits : 381 | if (crc>>15)&1 != bit : 382 | crc = ((crc<<1)&0xffff) ^ poly 383 | else : 384 | crc = ((crc<<1)&0xffff) 385 | return crc 386 | ``` 387 | 388 | I was both surprised and relieved when the CRC did indeed turned out to 389 | be zero for all blocks, since I wouldn't have had much of an idea where to start 390 | debugging this other than staring at the code. 391 | 392 | ## Samples 393 | 394 | Successfully having decoded the Q-Channel subcode gave me the confidence that 395 | all of the previous steps, including OCR'ing the EFM LUT, were done correctly, so it's now time for the 396 | main event, the audio samples in glorious 16 bit linear PCM. 397 | 398 | Revisiting the frame structure, we're now looking at the data and 399 | parity bytes: 400 | 401 | ![A diagram showing the frame structure.](media/frame.svg) 402 | 403 | Each frame contains 32 payload bytes, 24 of which are data and 8 of 404 | which are parity byes. 405 | 406 | As every article on compact disc digital audio mentions, samples are 407 | encoded using [Cross-interleaved reed-solomon coding 408 | (CIRC)](https://en.wikipedia.org/wiki/Cross-interleaved_Reed%E2%80%93Solomon_coding). The 409 | specification helpfully provides block diagrams of the encoder and 410 | decoder, which I've redrawn and put side-by-side for clarity: 411 | 412 | ![A complicated diagram of the CIRC encoder and decoder.](media/circ-enc-dec.svg) 413 | 414 | Tracing each byte from input to output, we see that each byte is 415 | subject to the same delay of 111 frames, so the data stays as-is. 416 | 417 | The important takeaway from the encoder block diagram is that the 418 | reed-solomon forward error correction (FEC) in the C₁ and C₂ encoders merely tacks on extra parity bytes and leaves the 419 | sample data as-is. That means we can focus on deinterleaving first 420 | and deal with the FEC later on. 421 | 422 | Ignoring the FEC, the decoder is exactly the inverse of the 423 | encoder. 424 | 425 | Based on that knowledge, we can substitute the C₁ and C₂ decoder boxes in 426 | the above block with pass-throughs as indicated by the dashed lines. This leaves us 427 | with having to implement the various delay and reordering elements. 428 | 429 | I implemented the delay elements as a shift register encapsulated in a 430 | class. Calling the `step` method returns the value from a prior 431 | invocation, depending on `n_delay`. 432 | 433 | ```python 434 | class Delay: 435 | def __init__(self, n_delay, fill = None) : 436 | self.register = [fill]*n_delay 437 | 438 | def step(self, v) : 439 | if len(self.register) == 0 : 440 | return v 441 | r = self.register[-1] 442 | self.register = [v] + self.register[:-1] 443 | return r 444 | 445 | d = Delay(3) 446 | print(d.step(1)) # prints None 447 | print(d.step(2)) # prints None 448 | print(d.step(3)) # prints None 449 | print(d.step(4)) # prints 1 450 | print(d.step(5)) # prints 2 451 | .... 452 | 453 | ``` 454 | 455 | For each column of delay stages, we create a list that has the 456 | individual delay elements: 457 | 458 | ```python 459 | first_delays = [Delay(0 if i%2 == 0 else 1, 0) for i in range(32)] 460 | ``` 461 | 462 | Applying the delay to all input symbols then is as simple as zipping 463 | them with the delay elements and calling `step` on each of them: 464 | 465 | ```python 466 | symbols_delayed1 = [d.step(x) for d,x in zip(first_delays, symbols_in)] 467 | ``` 468 | 469 | The other delay columns work more or less identical apart from different 470 | delay values. 471 | 472 | Right before the last delay columns, we need to shuffle the samples as 473 | indicated in the decoder diagram. Thanks to list comprehensions, that's 474 | a one-liner as well: 475 | 476 | ```python 477 | # output to input sample 478 | deinterleave_tab = ( 0, 1, 6, 7, 16, 17, 22, 23, 2, 3, 8, 9, 18, 19, 24, 25, 4, 5, 10, 11, 20, 21, 26, 27) 479 | symbols_deinterleaved = [symbols_delayed2[i] for i in deinterleave_tab] 480 | ``` 481 | 482 | After this deinterleaving step, we get 24 bytes that form 6 consecutive 483 | stereo samples. All that's left to do is combine the bytes into the 484 | appropriate samples taking into account two's complement. 485 | 486 | We then run this process for all frames and dump the samples into a 487 | wave file at 44.1 kHz sample rate, so we get something we can 488 | listen to. Even though we only got about 0.8 s of audio, it sounds 489 | plausible. To know if I got it all right, I ripped that particular 490 | track as an uncompressed wave file and imported it into Audacity. 491 | I aligned it to the decoded audio snippet with the help of the 492 | timestamps from the subcode and inverted it. Adding both tracks 493 | resulted in absolute silence, confirming that the decoded samples are 494 | indeed correct! 495 | 496 | See [decode.py](/decode.py) for the implementation of all of this. 497 | 498 | 499 | # Something's not quite right... 500 | 501 | Remember that we initially captured 4 MSa at 20 MSa/s on the 502 | oscilloscope? This 503 | corresponds to a record length of 0.2 s. However, we got 0.78  504 | seconds of audio. Since compact disc digital audio operates in real 505 | time, this is doesn't quite add up. The frequency tuning word from the 506 | PLL backs this up: The raw bit rate of a compact disc should be: 507 | 44.1 kHz / 6 samples per frame × 588 bits per frame = 508 | 4.3218 MBit/s. However, the bit rate as per the VCO frequency is 509 | 42.6 / 1000 × 20 MSa/s × 20 = 17.04 MBit/s. Multiplying this 510 | by the ratio between the oscilloscope record length and the length 511 | of the decoded audio, we get: 17.04 MBit/s × (0.2 s / 512 | 0.78 s) = 4.33 MBit/s, which is fairly close to the expected 513 | bit rate, so at least that that checks out. 514 | 515 | All of this didn't quite make sense at first. If the DVD player is 516 | reading the disc about 4× faster than it needs to, where's that data 517 | going? Then I remembered the dropouts I initially saw on the 518 | oscilloscope. Maybe these dropouts aren't actually dust or scratches, 519 | but the DVD player too fast to and then jumping 520 | back on the disc to maintain the 521 | correct data rate on average? 522 | 523 | To find out if that's the case, I captured another portion of the 524 | waveform that has such a dropout: 525 | 526 | ![Oscilloscope screenshot showing an intensity-graded waveform that 527 | drops in amplitude three times. The middle dropout is magnified.](media/dropout.png) 528 | 529 | At first attempt, the clock recovery PLL didn't relock after the first 530 | dropout and stayed unlocked thereafter. Looking at the PLL's signals, I 531 | found out that the integrator went off the rails during the dropout to 532 | the point where it was so far off that the PLL had no chance of ever 533 | locking again. Clamping the magnitude of the integrator value to 534 | slightly above its nominal value did the trick to get the PLL to relock 535 | after the dropouts. 536 | 537 | With that obstacle out of the way we can decode the Q-Channel subcode 538 | as before: 539 | 540 | ``` 541 | [...] 542 | Track 2.1 R=00:17:45 A=06:32:53 543 | Track 2.1 R=00:17:46 A=06:32:54 544 | Track 2.1 R=00:17:47 A=06:32:55 545 | CRC error 546 | Track 2.1 R=00:17:27 A=06:32:35 547 | Track 2.1 R=00:17:28 A=06:32:36 548 | Track 2.1 R=00:17:29 A=06:32:37 549 | [...] 550 | Track 2.1 R=00:17:45 A=06:32:53 551 | Track 2.1 R=00:17:46 A=06:32:54 552 | Track 2.1 R=00:17:47 A=06:32:55 553 | CRC error 554 | Track 2.1 R=00:17:27 A=06:32:35 555 | Track 2.1 R=00:17:28 A=06:32:36 556 | Track 2.1 R=00:17:29 A=06:32:37 557 | [...] 558 | Track 2.1 R=00:17:45 A=06:32:53 559 | Track 2.1 R=00:17:46 A=06:32:54 560 | Track 2.1 R=00:17:47 A=06:32:55 561 | CRC error 562 | Track 2.1 R=00:17:27 A=06:32:35 563 | Track 2.1 R=00:17:28 A=06:32:36 564 | Track 2.1 R=00:17:29 A=06:32:37 565 | ``` 566 | 567 | Based on time codes, we can see that the DVD player is indeed reading 568 | the same part of the disc multiple times. Suspicion confirmed! 569 | 570 | My best guess for why the DVD player isn't reading the disc at the 571 | nominal rate is that the signal conditioning and clock recovery circuitry in the 572 | SoC can't handle the nominal rate since it also has to 573 | process the significantly faster signal from a DVD. 574 | 575 | # Closing thoughts 576 | 577 | Decoding compact disc digital audio from the pickup signal to PCM 578 | samples was a really interesting project and was surprisingly easy 579 | after getting the clock recovery right and fixing a couple of stupid 580 | bugs in the deinterleaver. It's also worth noting that this project 581 | only required a digital oscilloscope with half-decent memory depth, a CD 582 | player and some basic DSP and bit shuffling knowledge, so I'm wondering 583 | why I haven't found much evidence of people having done this before. 584 | 585 | One thing that I didn't cover is implementing the Reed-Solomon 586 | decoder. That'll has to wait until I've developed a better 587 | understanding of Reed-Solomon forward error correction. 588 | 589 | 590 | [^1]: While writing this article, a friend of mine pointed me to 591 | [this](http://www.pmonta.com/compact-disc-microscopy.html) post where 592 | someone did just that! 593 | 594 | [^2]: In the process of implementing the clock recovery, I was scratching my 595 | head why I was seeing lots of jitter on the acquired signal. Turns out 596 | that Keysight InfiniiVision oscilloscopes by default randomize the the 597 | time between samples at low sample rates to prevent aliasing. Turning 598 | off the antialiasing option in the display menu indeed fixed the 599 | problem. Another thing worth mentioning is that one needs to press the 600 | single shot button to get maximum memory depth on this series of 601 | oscilloscopes. Just pressing stop only yields half the memory depth. 602 | 603 | [^3]: Only later I discovered that the freely available [ECMA-130 604 | Standard](https://www.ecma-international.org/wp-content/uploads/ECMA-130_2nd_edition_june_1996.pdf) 605 | for CD-ROM includes a textual representation of the EFM LUT. 606 | -------------------------------------------------------------------------------- /media/frame.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Sync
24
Sync...
3
3
C 14
C 14
3
3
D
14
D...
12 Data bytes
12 Data bytes
3
3
P
14
P...
3
3
P
14
P...
4 Parity bytes
4 Parity bytes
Frame: 588 channel bits
Frame: 588 channel bits
3
3
D
14
D...
3
3
3
3
D
14
D...
12 Data bytes
12 Data bytes
3
3
P
14
P...
3
3
P
14
P...
4 Parity bytes
4 Parity bytes
3
3
D
14
D...
100000000001000000000010
100000000001000000000010
Subcode
Subcode
Merging bits
Merging bits
Sync
24
Sync...
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /media/subcode2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
S₀
S₀
S₁
S₁
CTRL (4)
CTRL (4)
ADR (4)
ADR (4)
DATA-Q (72)
DATA-Q (72)
CRC (16)
CRC (16)
0
0
X
X
0
0
0
0
0
0
X
X
1
1
0
0
0
0
0
0
X
X
X
X
0
0
1
1
X
X
X
X
96 bits
96 bits
stereo audio without pre-emphasis
stereo audio without pre-emphasis
stereo audio with pre-emphasis
stereo audio with pre-emphasis
please don't copy me 
please don't copy me 
copying is fine
copying is fine
DATA-Q mode
0 … 3
DATA-Q mode...
TNO
TNO
IDX
IDX
MIN
MIN
SEC
SEC
FRM
FRM
ZERO
ZERO
AMIN
AMIN
ASEC
ASEC
AFRM
AFRM
MODE 1
MODE 1
Track number, 2 digits BCD
Track number, 2 digits BCD
Index in track, 2 digits BCD
Index in track, 2 digits...
Track running time, BCD
Track running time, BCD
Disc running time, BCD
Disc running time, BCD
0
0
4
4
8
8
8
8
16
16
24
24
32
32
40
40
48
48
56
56
64
64
72
72
80
80
Text is not SVG - cannot display
--------------------------------------------------------------------------------