├── LICENSE ├── README.md ├── lpcplayer ├── __init__.py ├── player.py └── tables.py ├── python_wizard ├── python_wizard_gui ├── pywizard ├── Buffer.py ├── CodingTable.py ├── Filterer.py ├── FrameData.py ├── FrameDataBinaryEncoder.py ├── HammingWindow.py ├── HexConverter.py ├── PitchEstimator.py ├── PreEmphasizer.py ├── Processor.py ├── RMSNormalizer.py ├── Reflector.py ├── Segmenter.py ├── __init__.py ├── tools.py └── userSettings.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 ptwz 4 | Copyright (c) 2020 sintech 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Updates to base python_wizard 2 | 3 | - Added "lpcplayer" package (based on talkie) to support Play feature in GUI. 4 | 5 | - Added dirty support for other LPC coding tables. TMS5100 is default now. 6 | ``` 7 | -T {tms5220,tms5100}, --tablesVariant {tms5220,tms5100} 8 | Tables variant 9 | ``` 10 | 11 | - Support for python formatted output 12 | ``` 13 | -f {arduino,C,hex,python}, --outputFormat {arduino,C,hex,python} 14 | Output file format 15 | ``` 16 | - Small fixes and enhancements in GUI 17 | 18 | 19 | 20 | ------ 21 | This project is a python port of the great macOS tool BlueWizard (https://github.com/patrick99e99/BlueWizard), which is written in objective C and I was not familiar enough with this C dialect to make an portable command line application out of it. 22 | 23 | It is intended to convert (voice) audio streams into LPC bitstreams used in the TMS 5220 chip or e.g. in the Arduino library Talkie. Now you can generate your own LPC streams and make your chips say the things you want them to. 24 | 25 | Compared to BlueWizard some minor features have been added: 26 | 1. Ability to downsample a wave file automatically 27 | 2. Automated output formatters for C, Arduino (C-Dialect) and plain hex 28 | 29 | Prerequisites: 30 | - Python 2.7 31 | - SciPy >= 0.18.1 32 | 33 | Usage: 34 | ``` 35 | python_wizard.py [-h] [-u UNVOICEDTHRESHOLD] [-w WINDOWWIDTH] [-U] [-V] 36 | [-S] [-p] [-a PREEMPHASISALPHA] [-d] [-r PITCHRANGE] 37 | [-F FRAMERATE] [-m SUBMULTIPLETHRESHOLD] 38 | [-f {arduino,C,hex}] 39 | filename 40 | 41 | positional arguments: 42 | filename File name of a .wav file to be processed 43 | 44 | optional arguments: 45 | -h, --help show this help message and exit 46 | -u UNVOICEDTHRESHOLD, --unvoicedThreshold UNVOICEDTHRESHOLD 47 | Unvoiced frame threshold 48 | -w WINDOWWIDTH, --windowWidth WINDOWWIDTH 49 | Window width in frames 50 | -U, --normalizeUnvoicedRMS 51 | Normalize unvoiced frame RMS 52 | -V, --normalizeVoicedRMS 53 | Normalize voiced frame RMS 54 | -S, --includeExplicitStopFrame 55 | Create explicit stop frame (needed e.g. for Talkie) 56 | -p, --preEmphasis Pre emphasize sound to improve quality of translation 57 | -a PREEMPHASISALPHA, --preEmphasisAlpha PREEMPHASISALPHA 58 | Emphasis coefficient 59 | -d, --debug Enable (lots) of debug output 60 | -r PITCHRANGE, --pitchRange PITCHRANGE 61 | Comma separated range of available voice pitch in Hz. 62 | Default: 50,500 63 | -F FRAMERATE, --frameRate FRAMERATE 64 | -m SUBMULTIPLETHRESHOLD, --subMultipleThreshold SUBMULTIPLETHRESHOLD 65 | sub-multiple threshold 66 | -f {arduino,C,hex}, --outputFormat {arduino,C,hex} 67 | Output file format 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /lpcplayer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ptwz/python_wizard/add73ebf3c8fed306225dffd2e0229aee1a38e8f/lpcplayer/__init__.py -------------------------------------------------------------------------------- /lpcplayer/player.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class LpcDecoder: 4 | """Decode LPC encoded data""" 5 | 6 | def __init__(self, tms_coeff, batch=False): 7 | self.bitpos = 0 8 | self.tables = tms_coeff 9 | self.batch = batch 10 | 11 | def rev_byte(self, byte): 12 | out_byte = 0 13 | for i in range(8): 14 | bit = (byte >> (7 - i)) & 1 15 | out_byte = out_byte | (bit << i) 16 | return out_byte 17 | 18 | def get_bits(self, bits): 19 | listpos = self.bitpos // 8 20 | bitpos = self.bitpos % 8 21 | try: 22 | databyte = self.rev_byte(self.data[listpos]) 23 | if bitpos + bits + 1 > 8 and listpos + 1 < len(self.data): 24 | databyte = databyte << 8 | self.rev_byte(self.data[listpos + 1]) 25 | ret_bits = (databyte >> (15 - (bitpos + bits) + 1)) & ((1 << bits) - 1) 26 | self.bitpos += bits 27 | else: 28 | ret_bits = (databyte >> abs(7 - (bitpos + bits) + 1)) & (abs(1 << bits) - 1) 29 | self.bitpos += bits 30 | except IndexError as e: 31 | print(e) 32 | print("no more bits error") 33 | return -1 34 | return ret_bits 35 | 36 | def decode(self, lpc_data): 37 | self.bitpos = 0 38 | self.data = lpc_data 39 | synth = {} 40 | target_synth = {} 41 | synth['K1'] = 0 42 | synth['K2'] = 0 43 | synth['K3'] = 0 44 | synth['K4'] = 0 45 | synth['K5'] = 0 46 | synth['K6'] = 0 47 | synth['K7'] = 0 48 | synth['K8'] = 0 49 | synth['K9'] = 0 50 | synth['K10'] = 0 51 | synth['period'] = 0 52 | synth['energy'] = 0 53 | for k in synth: 54 | target_synth[k] = synth[k] 55 | period_counter = 0 56 | frame_counter = 0 57 | 58 | x0 = x1 = x2 = x3 = x4 = x5 = x6 = x7 = x8 = x9 = x10 = 0 59 | 60 | samples = [] 61 | logging.debug(f" Frame:NN - | Energy | R | Pitch | koeff") 62 | while True: 63 | debug_r="" 64 | debug_p="" 65 | debug_k="" 66 | 67 | energy = self.get_bits(self.tables.energy_bits) 68 | debug_e=f" {str(bin(energy))[2:].zfill(self.tables.energy_bits)}, {energy:<2} |" 69 | 70 | if energy == -1: 71 | break 72 | if energy == 0: 73 | target_synth['energy'] = 0 74 | elif energy == 0b1111: 75 | logging.debug(f" Frame:{frame_counter:2d} - | 1111, 15 |") 76 | break 77 | else: 78 | target_synth['energy'] = self.tables.energytable[energy] 79 | repeat = self.get_bits(1) 80 | debug_r=f" {repeat} |" 81 | period = self.get_bits(self.tables.pitch_bits) 82 | target_synth['period'] = self.tables.pitchtable[period] 83 | debug_p=f"{str(bin(period))[2:].zfill(self.tables.pitch_bits)}, {period:<2} |" 84 | debug_k="" 85 | if repeat == 0: 86 | for k in range(10): 87 | if period == 0 and k >= 4: 88 | continue 89 | k_bits=self.get_bits(self.tables.kbits[k]) 90 | target_synth[f"K{k + 1}"] = self.tables.ktable[k][k_bits] 91 | debug_k+=f' {str(bin(k_bits))[2:].zfill(self.tables.kbits[k])}, {k_bits:>2} |' 92 | 93 | logging.debug(f" Frame:{frame_counter:2d} - |{debug_e}{debug_r}{debug_p}{debug_k}") 94 | 95 | if frame_counter == 0: 96 | # copy target to current keys 97 | for k in target_synth: 98 | synth[k] = target_synth[k] 99 | 100 | interp_count = 0 101 | for sample_num in range(200): 102 | # interpolation time 103 | if interp_count == 0 and sample_num > 0: 104 | interp_period = int(sample_num / 25) 105 | for k in target_synth: 106 | synth[k] += (target_synth[k] - synth[k]) >> self.tables.interp_coeff[interp_period] 107 | 108 | if synth['energy'] == 0: 109 | u10 = 0 110 | elif synth['period'] > 0: 111 | # Voiced frame 112 | if period_counter < synth['period']: 113 | period_counter += 1 114 | else: 115 | period_counter = 0 116 | if period_counter < len(self.tables.chirptable): 117 | u10 = int(self.tables.chirptable[period_counter] * synth['energy'] >> 6) 118 | else: 119 | u10 = 0 120 | else: 121 | # Unvoiced 122 | synthRand = 1 123 | synthRand = (synthRand >> 1) ^ (0xB800 if synthRand & 1 else 0) 124 | u10 = synth['energy'] if (synthRand & 1) else -synth['energy'] 125 | 126 | # Lattice filter forward path 127 | u9 = u10 - int((synth['K10'] * x9) >> 9) 128 | u8 = u9 - int((synth['K9'] * x8) >> 9) 129 | u7 = u8 - int((synth['K8'] * x7) >> 9) 130 | u6 = u7 - int((synth['K7'] * x6) >> 9) 131 | u5 = u6 - int((synth['K6'] * x5) >> 9) 132 | u4 = u5 - int((synth['K5'] * x4) >> 9) 133 | u3 = u4 - int((synth['K4'] * x3) >> 9) 134 | u2 = u3 - int((synth['K3'] * x2) >> 9) 135 | u1 = u2 - int((synth['K2'] * x1) >> 9) 136 | u0 = u1 - int((synth['K1'] * x0) >> 9) 137 | 138 | # Output clamp 139 | if u0 > 511: 140 | u0 = 511 141 | if u0 < -512: 142 | u0 = -512 143 | 144 | # Lattice filter reverse path 145 | x9 = x8 + int((synth['K9'] * u8) >> 9) 146 | x8 = x7 + int((synth['K8'] * u7) >> 9) 147 | x7 = x6 + int((synth['K7'] * u6) >> 9) 148 | x6 = x5 + int((synth['K6'] * u5) >> 9) 149 | x5 = x4 + int((synth['K5'] * u4) >> 9) 150 | x4 = x3 + int((synth['K4'] * u3) >> 9) 151 | x3 = x2 + int((synth['K3'] * u2) >> 9) 152 | x2 = x1 + int((synth['K2'] * u1) >> 9) 153 | x1 = x0 + int((synth['K1'] * u0) >> 9) 154 | x0 = u0 155 | 156 | sample = (u0 >> 2) + 0x80 157 | samples.append(sample) 158 | interp_count = (interp_count + 1) % 25 159 | # return 160 | frame_counter += 1 161 | if self.batch: 162 | return (self.bitpos // 8) + 1 163 | else: 164 | return samples 165 | -------------------------------------------------------------------------------- /lpcplayer/tables.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | 4 | class tms_coeffs(NamedTuple): 5 | num_k: int 6 | energy_bits: int 7 | pitch_bits: int 8 | kbits: tuple 9 | energytable: tuple 10 | pitchtable: tuple 11 | ktable: tuple 12 | chirptable: tuple 13 | interp_coeff: tuple 14 | 15 | 16 | TI_0280_PATENT_ENERGY = (0, 0, 1, 1, 2, 3, 5, 7, 10, 15, 21, 30, 43, 61, 86, 0) 17 | 18 | TI_028X_LATER_ENERGY = (0, 1, 2, 3, 4, 6, 8, 11, 16, 23, 33, 47, 63, 85, 114, 0) 19 | 20 | # pitch 21 | 22 | TI_0280_2801_PATENT_PITCH = (0, 41, 43, 45, 47, 49, 51, 53, 23 | 55, 58, 60, 63, 66, 70, 73, 76, 24 | 79, 83, 87, 90, 94, 99, 103, 107, 25 | 112, 118, 123, 129, 134, 140, 147, 153) 26 | 27 | TI_2802_PITCH = (0, 16, 18, 19, 21, 24, 26, 28, 28 | 31, 35, 37, 42, 44, 47, 50, 53, 29 | 56, 59, 63, 67, 71, 75, 79, 84, 30 | 89, 94, 100, 106, 112, 126, 141, 150) 31 | 32 | TI_5110_PITCH = (0, 15, 16, 17, 19, 21, 22, 25, 33 | 26, 29, 32, 36, 40, 42, 46, 50, 34 | 55, 60, 64, 68, 72, 76, 80, 84, 35 | 86, 93, 101, 110, 120, 132, 144, 159) 36 | 37 | TI_2501E_PITCH= (0, 14, 15, 16, 17, 18, 19, 20, 38 | 21, 22, 23, 24, 25, 26, 27, 28, 39 | 29, 30, 31, 32, 34, 36, 38, 40, 40 | 41, 43, 45, 48, 49, 51, 54, 55, 41 | 57, 60, 62, 64, 68, 72, 74, 76, 42 | 81, 85, 87, 90, 96, 99, 103, 107, 43 | 112, 117, 122, 127, 133, 139, 145, 151, 44 | 157, 164, 171, 178, 186, 194, 202, 211) 45 | 46 | TI_5220_PITCH = ( 47 | # /* P */ 48 | 0, 15, 16, 17, 18, 19, 20, 21, 49 | 22, 23, 24, 25, 26, 27, 28, 29, 50 | 30, 31, 32, 33, 34, 35, 36, 37, 51 | 38, 39, 40, 41, 42, 44, 46, 48, 52 | 50, 52, 53, 56, 58, 60, 62, 65, 53 | 68, 70, 72, 76, 78, 80, 84, 86, 54 | 91, 94, 98, 101, 105, 109, 114, 118, 55 | 122, 127, 132, 137, 142, 148, 153, 159) 56 | 57 | TI_0280_PATENT_LPC = ( 58 | # /* K1 */ 59 | (-501, -497, -493, -488, -480, -471, -460, -446, 60 | -427, -405, -378, -344, -305, -259, -206, -148, 61 | -86, -21, 45, 110, 171, 227, 277, 320, 62 | 357, 388, 413, 434, 451, 464, 474, 498), 63 | # /* K2 */ 64 | (-349, -328, -305, -280, -252, -223, -192, -158, 65 | -124, -88, -51, -14, 23, 60, 97, 133, 66 | 167, 199, 230, 259, 286, 310, 333, 354, 67 | 372, 389, 404, 417, 429, 439, 449, 506), 68 | # /* K3 */ 69 | (-397, -365, -327, -282, -229, -170, -104, -36, 70 | 35, 104, 169, 228, 281, 326, 364, 396), 71 | # /* K4 */ 72 | (-369, -334, -293, -245, -191, -131, -67, -1, 73 | 64, 128, 188, 243, 291, 332, 367, 397), 74 | # /* K5 */ 75 | (-319, -286, -250, -211, -168, -122, -74, -25, 76 | 24, 73, 121, 167, 210, 249, 285, 318), 77 | # /* K6 */ 78 | (-290, -252, -209, -163, -114, -62, -9, 44, 79 | 97, 147, 194, 238, 278, 313, 344, 371), 80 | # /* K7 */ 81 | (-291, -256, -216, -174, -128, -80, -31, 19, 82 | 69, 117, 163, 206, 246, 283, 316, 345), 83 | # /* K8 */ 84 | (-218, -133, -38, 59, 152, 235, 305, 361), 85 | # /* K9 */ 86 | (-226, -157, -82, -3, 76, 151, 220, 280), 87 | # /* K10 */ 88 | (-179, -122, -61, 1, 62, 123, 179, 231)) 89 | 90 | TI_2801_2501E_LPC = ( 91 | # /* K1 */ 92 | ( -501, -498, -495, -490, -485, -478, -469, -459, 93 | -446, -431, -412, -389, -362, -331, -295, -253, 94 | -207, -156, -102, -45, 13, 70, 126, 179, 95 | 228, 272, 311, 345, 374, 399, 420, 437 ), 96 | # /* K2 */ 97 | ( -376, -357, -335, -312, -286, -258, -227, -195, 98 | -161, -124, -87, -49, -10, 29, 68, 106, 99 | 143, 178, 212, 243, 272, 299, 324, 346, 100 | 366, 384, 400, 414, 427, 438, 448, 506 ), 101 | # /* K3 */ 102 | ( -407, -381, -349, -311, -268, -218, -162, -102, 103 | -39, 25, 89, 149, 206, 257, 302, 341 ), 104 | # /* K4 */ 105 | ( -290, -252, -209, -163, -114, -62, -9, 44, 106 | 97, 147, 194, 238, 278, 313, 344, 371 ), 107 | # /* K5 */ 108 | ( -318, -283, -245, -202, -156, -107, -56, -3, 109 | 49, 101, 150, 196, 239, 278, 313, 344 ), 110 | # /* K6 */ 111 | ( -193, -152, -109, -65, -20, 26, 71, 115, 112 | 158, 198, 235, 270, 301, 330, 355, 377 ), 113 | # /* K7 */ 114 | ( -254, -218, -180, -140, -97, -53, -8, 36, 115 | 81, 124, 165, 204, 240, 274, 304, 332 ), 116 | # /* K8 */ 117 | ( -205, -112, -10, 92, 187, 269, 336, 387 ), 118 | # /* K9 */ 119 | ( -249, -183, -110, -32, 48, 126, 198, 261 ), 120 | # /* on patents 4,403,965 and 4,946,391 the 4th entry is 0x3ED (-19) which is a typo of the correct value of 0x3E0 (-32)*/ 121 | # /* K10 */ 122 | ( -190, -133, -73, -10, 53, 115, 173, 227)) 123 | 124 | 125 | TI_5110_5220_LPC = ( 126 | # /* K1 */ 127 | (-501, -498, -497, -495, -493, -491, -488, -482, 128 | -478, -474, -469, -464, -459, -452, -445, -437, 129 | -412, -380, -339, -288, -227, -158, -81, -1, 130 | 80, 157, 226, 287, 337, 379, 411, 436), 131 | # /* K2 */ 132 | (-328, -303, -274, -244, -211, -175, -138, -99, 133 | -59, -18, 24, 64, 105, 143, 180, 215, 134 | 248, 278, 306, 331, 354, 374, 392, 408, 135 | 422, 435, 445, 455, 463, 470, 476, 506), 136 | # /* K3 */ 137 | (-441, -387, -333, -279, -225, -171, -117, -63, 138 | -9, 45, 98, 152, 206, 260, 314, 368), 139 | # /* K4 */ 140 | (-328, -273, -217, -161, -106, -50, 5, 61, 141 | 116, 172, 228, 283, 339, 394, 450, 506), 142 | # /* K5 */ 143 | (-328, -282, -235, -189, -142, -96, -50, -3, 144 | 43, 90, 136, 182, 229, 275, 322, 368), 145 | # /* K6 */ 146 | (-256, -212, -168, -123, -79, -35, 10, 54, 147 | 98, 143, 187, 232, 276, 320, 365, 409), 148 | # /* K7 */ 149 | (-308, -260, -212, -164, -117, -69, -21, 27, 150 | 75, 122, 170, 218, 266, 314, 361, 409), 151 | # /* K8 */ 152 | (-256, -161, -66, 29, 124, 219, 314, 409), 153 | # /* K9 */ 154 | (-256, -176, -96, -15, 65, 146, 226, 307), 155 | # /* K10 */ 156 | (-205, -132, -59, 14, 87, 160, 234, 307)) 157 | 158 | # /* chirp */ 159 | TI_0280_PATENT_CHIRP = ( 160 | # /* Chirp table */ 161 | 0x00, 0x2a, 0xd4, 0x32, 0xb2, 0x12, 0x25, 0x14, 162 | 0x02, 0xe1, 0xc5, 0x02, 0x5f, 0x5a, 0x05, 0x0f, 163 | 0x26, 0xfc, 0xa5, 0xa5, 0xd6, 0xdd, 0xdc, 0xfc, 164 | 0x25, 0x2b, 0x22, 0x21, 0x0f, 0xff, 0xf8, 0xee, 165 | 0xed, 0xef, 0xf7, 0xf6, 0xfa, 0x00, 0x03, 0x02, 166 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 167 | 0x00, 0x00, 0x00, 0x00) 168 | 169 | TI_LATER_CHIRP = ( 170 | # /* Chirp table */ 171 | 0x00, 0x03, 0x0f, 0x28, 0x4c, 0x6c, 0x71, 0x50, 172 | 0x25, 0x26, 0x4c, 0x44, 0x1a, 0x32, 0x3b, 0x13, 173 | 0x37, 0x1a, 0x25, 0x1f, 0x1d, 0x00, 0x00, 0x00, 174 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 175 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 176 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 177 | 0x00, 0x00, 0x00, 0x00) 178 | 179 | # /* Interpolation Table */ 180 | TI_INTERP = ( 181 | # /* interpolation shift coefficients */ 182 | 0, 3, 3, 3, 2, 2, 1, 1) 183 | 184 | tms5100 = tms_coeffs( 185 | 10, 186 | 4, 187 | 5, 188 | (5, 5, 4, 4, 4, 4, 4, 3, 3, 3), 189 | TI_0280_PATENT_ENERGY, 190 | TI_0280_2801_PATENT_PITCH, 191 | TI_0280_PATENT_LPC, 192 | TI_0280_PATENT_CHIRP, 193 | TI_INTERP 194 | ) 195 | 196 | tms5110a = tms_coeffs( 197 | 10, 198 | 4, 199 | 5, 200 | (5, 5, 4, 4, 4, 4, 4, 3, 3, 3), 201 | TI_028X_LATER_ENERGY, 202 | TI_5110_PITCH, 203 | TI_5110_5220_LPC, 204 | TI_LATER_CHIRP, 205 | TI_INTERP 206 | ) 207 | 208 | tms5200 = tms_coeffs( 209 | 10, 210 | 4, 211 | 6, 212 | (5, 5, 4, 4, 4, 4, 4, 3, 3, 3), 213 | TI_028X_LATER_ENERGY, 214 | TI_2501E_PITCH, 215 | TI_2801_2501E_LPC, 216 | TI_LATER_CHIRP, 217 | TI_INTERP 218 | ) 219 | 220 | tms5220 = tms_coeffs( 221 | 10, 222 | 4, 223 | 6, 224 | (5, 5, 4, 4, 4, 4, 4, 3, 3, 3), 225 | TI_028X_LATER_ENERGY, 226 | TI_5220_PITCH, 227 | TI_5110_5220_LPC, 228 | TI_LATER_CHIRP, 229 | TI_INTERP 230 | ) 231 | 232 | tables = {"tms5100": tms5100, 233 | "tms5110a": tms5110a, 234 | "tms5200": tms5200, 235 | "tms5220": tms5220, 236 | } 237 | -------------------------------------------------------------------------------- /python_wizard: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import logging 3 | import re 4 | import os 5 | import sys 6 | from lpcplayer.tables import tables 7 | 8 | ''' 9 | Command line application for processing WAV files into LPC data 10 | for use with TMS5220 (emulators) 11 | ''' 12 | 13 | import argparse 14 | parser = argparse.ArgumentParser() 15 | parser.add_argument("-u", "--unvoicedThreshold", 16 | help="Unvoiced frame threshold", 17 | action="store", 18 | default=0.3, 19 | type=float) 20 | parser.add_argument("-w", "--windowWidth", 21 | help="Window width in frames", 22 | action="store", 23 | default=2, 24 | type=int) 25 | parser.add_argument("-U", "--normalizeUnvoicedRMS", 26 | help="Normalize unvoiced frame RMS", 27 | action="store_true") 28 | parser.add_argument("-V", "--normalizeVoicedRMS", 29 | help="Normalize voiced frame RMS", 30 | action="store_true") 31 | parser.add_argument("-S", "--includeExplicitStopFrame", 32 | help="Create explicit stop frame (needed e.g. for Talkie)", 33 | action="store_true") 34 | parser.add_argument("-p", "--preEmphasis", 35 | help="Pre emphasize sound to improve quality of translation", 36 | action="store_true") 37 | parser.add_argument("-a", "--preEmphasisAlpha", 38 | help="Emphasis coefficient", 39 | type=float, 40 | default=-0.9373) 41 | parser.add_argument("-d", "--debug", 42 | help="Enable (lots) of debug output", 43 | action="store_true") 44 | parser.add_argument("-r", "--pitchRange", 45 | help="Comma separated range of available voice pitch in Hz. Default: 50,500", 46 | default="50,500") 47 | parser.add_argument("-F", "--frameRate", 48 | default=25, 49 | type=int) 50 | parser.add_argument("-m", "--subMultipleThreshold", 51 | help="sub-multiple threshold", 52 | default=0.9, 53 | type=float) 54 | parser.add_argument("-f", "--outputFormat", 55 | help="Output file format", 56 | choices=["arduino", "C", "hex", "python"], 57 | default="hex") 58 | parser.add_argument("-T", "--tablesVariant", 59 | help="Tables variant", 60 | choices=tables.keys(), 61 | default="tms5220") 62 | 63 | parser.add_argument("filename", 64 | help="File name of a .wav file to be processed", 65 | action="store") 66 | 67 | args = parser.parse_args() 68 | 69 | if args.debug: 70 | logging.basicConfig(level=logging.DEBUG) 71 | 72 | from pywizard.userSettings import settings 73 | settings.import_from_argparse(args) 74 | 75 | from pywizard.FrameDataBinaryEncoder import BitPacker 76 | from pywizard.Processor import Processor 77 | from pywizard.Buffer import Buffer 78 | #settings.import_from_argparse(args) 79 | from pywizard.CodingTable import CodingTable 80 | 81 | b = Buffer.fromWave(args.filename) 82 | if b is None: 83 | sys.exit() 84 | 85 | x=Processor(b, args.tablesVariant) 86 | 87 | result = BitPacker.pack(x) 88 | 89 | new_varname = os.path.basename(args.filename) 90 | new_varname = os.path.splitext(new_varname)[0] 91 | 92 | if re.match("^[^A-Za-z_]", new_varname): 93 | new_varname = "_"+new_varname 94 | 95 | print(result.replace("FILENAME", new_varname)) 96 | -------------------------------------------------------------------------------- /python_wizard_gui: -------------------------------------------------------------------------------- 1 | from tkinter.ttk import * 2 | from tkinter import * 3 | from tkinter import filedialog, messagebox 4 | import logging 5 | from collections import OrderedDict 6 | try: 7 | import pyaudio 8 | except ModuleNotFoundError: 9 | pyaudio = None 10 | 11 | #from backend import BitPacker, Processor, Buffer, settings, CodingTable 12 | from pywizard.FrameDataBinaryEncoder import BitPacker 13 | from pywizard.Processor import Processor 14 | from pywizard.Buffer import Buffer 15 | from pywizard.CodingTable import CodingTable 16 | from pywizard.userSettings import settings 17 | 18 | class Gui(object): 19 | NUM_ROWS = 15 20 | 21 | def __init__(self, root): 22 | self.codingTable = CodingTable(settings.tablesVariant) 23 | self.root = root 24 | self.master = Frame(root, name='master') # create Frame in "root" 25 | self.filename = None 26 | self.statusbar = Label(root, text="Ready…",bd=1, relief=SUNKEN, anchor=W) 27 | self.statusbar.pack(side=BOTTOM, fill=X) 28 | 29 | self.output_text = None 30 | 31 | root.title('Python Wizard') # title for top-level window 32 | # quit if the window is deleted 33 | root.protocol("WM_DELETE_WINDOW", self.quit) 34 | self.graph_frame = self._make_graphs() 35 | self.settings_frame = self._make_settings() 36 | self.table_frame = self._make_table() 37 | self.menu = self._make_menus() 38 | root.option_add('*tearOff', FALSE) 39 | 40 | self.first_line = 0 41 | self.run = False 42 | self.frames = [] 43 | self.bytes=0 44 | 45 | 46 | def quit(self): 47 | self.master.quit() 48 | 49 | def _file_open(self): 50 | self.filename = filedialog.askopenfilename(parent=self.master,filetypes=[('Waves',"*.wav")]) 51 | self.run = True 52 | 53 | 54 | def _make_menus(self): 55 | menubar = Menu(self.master) 56 | self.root['menu'] = menubar 57 | 58 | menu_file = Menu(menubar) 59 | menubar.add_cascade(menu=menu_file, label="File") 60 | 61 | menu_file.add_command(label="Open WAV", command=self._file_open) 62 | menu_file.add_command(label="Quit", command=self.quit) 63 | 64 | # menu_play = Menu(menubar) 65 | # menubar.add_cascade(menu=menu_play,label="Play") 66 | if pyaudio: 67 | menubar.add_command(label="Play",command=self.play) 68 | 69 | 70 | return menubar 71 | 72 | def _make_graphs(self): 73 | graphs = Frame(self.root) 74 | 75 | #graph_original_img = Image.fromarray(arr) 76 | #graph_original = = ImageTk.PhotoImage(graph_original_img) 77 | 78 | return graphs 79 | 80 | def _make_settings(self): 81 | settings_vars = {} 82 | self.settings_widgets = {} 83 | settings_frame = Frame(self.root) 84 | raw_settings = settings.export_to_odict() 85 | self.raw_settings = raw_settings 86 | row = 1 87 | col = 1 88 | rows = 6 89 | var = {} 90 | for key in raw_settings: 91 | Label(settings_frame, text=key).grid(row=row,column=col) 92 | 93 | v = None 94 | typ = type(raw_settings[key]) 95 | if typ is float: 96 | v=DoubleVar() 97 | elif typ is int: 98 | v=IntVar() 99 | elif typ is bool: 100 | v=IntVar() 101 | else: 102 | v=StringVar() 103 | v.set(raw_settings[key]) 104 | if typ is not bool: 105 | e = Entry(settings_frame, textvar=v) 106 | else: 107 | e = Checkbutton(settings_frame, variable=v) 108 | 109 | e.grid(row=row,column=col+1) 110 | self.settings_widgets[key] = e 111 | 112 | settings_vars[key] = v 113 | settings_vars[key].set(raw_settings[key]) 114 | row += 1 115 | if (row>rows): 116 | col += 2 117 | row = 1 118 | 119 | self.settings_vars = settings_vars 120 | settings_frame.pack() 121 | return settings_frame 122 | 123 | def _get_data(self): 124 | to_import = OrderedDict() 125 | for key in self.raw_settings: 126 | try: 127 | to_import[key] = self.settings_vars[key].get() 128 | except ValueError: 129 | self.settings_widgets[key].configure(background='red') 130 | return None 131 | 132 | return to_import 133 | 134 | def check_change(self): 135 | new_cfg = self._get_data() 136 | if new_cfg is None: 137 | return 138 | 139 | raw_settings = settings.export_to_odict() 140 | 141 | if new_cfg != raw_settings: 142 | for k in raw_settings: 143 | if new_cfg[k] != raw_settings[k]: 144 | logging.info(f"{k}, {raw_settings[k]}, {new_cfg[k]}") 145 | errors = settings.import_from_dict(new_cfg) 146 | if errors is None: 147 | if self.filename is not None: 148 | self.run = True 149 | else: 150 | # Mark wrong input fiels appropriately 151 | for e in errors: 152 | self.settings_widgets[e].configure(background='red') 153 | pass 154 | 155 | def _make_table(self, data=None): 156 | table=Frame(self.root) 157 | table_data=Frame(table) 158 | table_scrollbar=Scrollbar(table, orient="vertical", 159 | command=self._scrolled) 160 | table_data.grid(row=1, column=1) 161 | table_scrollbar.grid(row=1, column=2, sticky="NS") 162 | 163 | output_text = Text(table, width=40, height=20) 164 | output_text.grid(row=1, column=3) 165 | output_text_scrollbar = Scrollbar(table) 166 | output_text_scrollbar.grid(row=1, column=4, sticky="NS") 167 | output_text_scrollbar.config(command=output_text.yview) 168 | output_text.config(yscrollcommand=output_text_scrollbar.set) 169 | self.output_text = output_text 170 | table.pack(side="left") 171 | 172 | # Create header 173 | parameters = self.codingTable.parameters() 174 | for (p,col) in zip(parameters, range(len(parameters))): 175 | p = p[10:] 176 | Label(table_data, text=p).grid(row=1, column=col+2) 177 | Label(table_data, text='size').grid(row=1, column=col+1 + 2) 178 | 179 | table_vars = {} 180 | table_labels = {} 181 | for row in range(self.NUM_ROWS): 182 | table_labels[row] = StringVar() 183 | table_labels[row].set(str(row)) 184 | Label(table_data, textvariable=table_labels[row]).grid(row=row+2, column=1,padx=5) 185 | 186 | for (p, col) in zip(parameters, range(len(parameters))): 187 | v = IntVar() 188 | e = Entry(table_data, width=5, textvariable=v) 189 | table_vars[ (row, p) ] = v 190 | e.grid(row=row+2, column=col+2) 191 | v = IntVar() 192 | size=Label(table_data,width=5,textvariable=v) 193 | table_vars[(row, 'size')] = v 194 | size.grid(row=row+2, column=col+1+2) 195 | self.table_labels = table_labels 196 | table.pack() 197 | 198 | 199 | print (0, max( [1, self.NUM_ROWS / len(parameters)] ) ) 200 | table_scrollbar.set(0, max( [1, self.NUM_ROWS / len(parameters)] ) ) 201 | self.table_scrollbar = table_scrollbar 202 | self.table_vars = table_vars 203 | 204 | return table 205 | 206 | def _make_scroll_pos(self): 207 | frame_count = float(len(self.frames)) 208 | # First line of scrolled data 209 | scroll_offset = self.first_line / (frame_count+self.NUM_ROWS) 210 | page_size = float(self.NUM_ROWS) / (frame_count+self.NUM_ROWS) 211 | 212 | top = scroll_offset 213 | bottom = scroll_offset + page_size 214 | 215 | # Clip to bottom 216 | if bottom > 1: 217 | top = 1 - page_size 218 | bottom = 1 219 | 220 | return (top, bottom) 221 | 222 | def _scrolled(self, cmd, value, opt=None): 223 | frame_count = float(len(self.frames)) 224 | logging.debug("cmd={}, value={}, opt={}, frame_count={}".format(cmd, value, opt, frame_count)) 225 | page_size = float(self.NUM_ROWS) / frame_count 226 | 227 | if cmd == SCROLL: 228 | if opt == "units": 229 | self.first_line += int(value) 230 | if opt == "pages": 231 | self.first_line += int(self.NUM_ROWS * int(value)) 232 | elif cmd == MOVETO: 233 | self.first_line = int( (float(value)) * frame_count - self.NUM_ROWS / 2) 234 | if self.first_line < 0: 235 | self.first_line = 0 236 | elif self.first_line+self.NUM_ROWS > frame_count: 237 | self.first_line = frame_count - self.NUM_ROWS 238 | 239 | self.first_line = max(0, self.first_line) 240 | self.first_line = int(min(frame_count-self.NUM_ROWS, self.first_line)) 241 | 242 | self.table_scrollbar.set( *self._make_scroll_pos() ) 243 | 244 | #self.first_line = int(self.table_scrollbar.get()[0] * frame_count) 245 | self._update_table() 246 | 247 | 248 | def _table_iterator(self, frames): 249 | if frames is not None: 250 | self.frames = frames 251 | else: 252 | frames = self.frames 253 | 254 | if len(frames)==0: 255 | return [] 256 | 257 | scroll_offset = self.first_line / float(self.NUM_ROWS) 258 | 259 | l = min( ( self.first_line+self.NUM_ROWS, len(frames) ) ) 260 | 261 | page_size = float(self.NUM_ROWS) / len(frames) 262 | 263 | scroll_pos = self.first_line * page_size 264 | 265 | self.table_scrollbar.set(scroll_offset, scroll_offset+page_size ) 266 | 267 | return zip(range(0, self.NUM_ROWS), range(self.first_line,l), frames[self.first_line:l]) 268 | 269 | def _update_table(self, frames = None): 270 | prev=0 271 | parameters = self.codingTable.parameters() 272 | for (line, n, frame) in self._table_iterator(frames): 273 | params = frame.parameters() 274 | self.table_labels[line].set( str(n) ) 275 | for p in parameters: 276 | self.table_vars[(line, p)].set(0) 277 | for p in params: 278 | self.table_vars[ (line, p) ].set(params[p]) 279 | if (line-1, 'size') in self.table_vars: 280 | prev=self.table_vars[(line-1, 'size')].get() 281 | self.table_vars[(line, 'size')].set(prev+15) 282 | 283 | def _readback_table(self, frames = None): 284 | for (line, n, frame) in self._table_iterator(frames): 285 | params = frame.parameters() 286 | self.table_labels[line].set( str(n) ) 287 | changed = False 288 | for p in params: 289 | value = self.table_vars[ (line, p) ].get() 290 | changed = changed or not ( int(params[p]) == int(value) ) 291 | if not (int(params[p]) == int(value)): 292 | print(p, value) 293 | params[p] = int(value) 294 | if changed: 295 | self._update_output() 296 | 297 | def _repeatedly(self): 298 | self.root.after(500, self._repeatedly) 299 | self.check_change() 300 | self._readback_table() 301 | if self.run: 302 | self.run = False 303 | self.codingTable = CodingTable(settings.tablesVariant) 304 | b = Buffer.fromWave(self.filename) 305 | if b is None: 306 | messagebox.showerror("Could not read file", "The specified file could not be read!\nMust be mono, 8 or 16 bit WAV file.") 307 | return 308 | self.processor = Processor(b, settings.tablesVariant) 309 | self._update_table(self.processor.frames) 310 | self._update_output() 311 | 312 | def _update_output(self): 313 | result = BitPacker.pack(self.processor) 314 | self.output_text.delete(1.0, END) 315 | self.output_text.insert(END, result) 316 | 317 | def play(self): 318 | encoded_frames = BitPacker.raw_stream(self.processor) 319 | self.statusbar['text'] = str(len(encoded_frames)) + " bytes" 320 | from lpcplayer import player,tables 321 | player = player.LpcDecoder(tables.tms5100 if settings.tablesVariant=='tms5100' else tables.tms5220) 322 | samples=player.decode(encoded_frames) 323 | # instantiate PyAudio (1) 324 | p = pyaudio.PyAudio() 325 | # open stream (2) 326 | stream = p.open(format=pyaudio.paUInt8, channels=1, rate=8000, output=True) 327 | # play stream (3) 328 | stream.write(bytes(samples)) 329 | # stop stream (4) 330 | stream.stop_stream() 331 | stream.close() 332 | # close PyAudio (5) 333 | p.terminate() 334 | 335 | def mainloop(self): 336 | self.root.after(500, self._repeatedly) 337 | self.master.mainloop() 338 | 339 | settings.outputFormat='python' 340 | settings.tablesVariant='tms5100' 341 | 342 | 343 | logging.basicConfig(level=logging.INFO) 344 | # start the app 345 | root = Tk() # create a top-level window 346 | gui = Gui(root) 347 | if __name__ == "__main__": 348 | gui.mainloop() # call master's Frame.mainloop() method. 349 | #root.destroy() # if mainloop quits, destroy window 350 | -------------------------------------------------------------------------------- /pywizard/Buffer.py: -------------------------------------------------------------------------------- 1 | from scipy import signal 2 | from scipy.io import wavfile 3 | import scipy as sp 4 | import numpy as np 5 | import logging 6 | 7 | 8 | class Buffer(object): 9 | @classmethod 10 | def copy(cls, orig, applyFilter=None): 11 | if applyFilter is None: 12 | return cls(size=orig.size, sampleRate=orig.sampleRate, 13 | samples=orig.samples, start=orig.start, end=orig.end) 14 | else: 15 | return cls(size=orig.size, sampleRate=orig.sampleRate, 16 | samples=applyFilter(orig.samples), 17 | start=orig.start, end=orig.end) 18 | 19 | @classmethod 20 | def fromWave(cls, filename): 21 | try: 22 | (rate, data) = wavfile.read(filename) 23 | except ValueError as e: 24 | logging.error("Audio data could not be read: "+str(e)) 25 | return None 26 | 27 | if len(data.shape) != 1: 28 | logging.error("Only mono audio files are supported, sorry") 29 | return None 30 | 31 | expected_rate = 8.0e3 32 | downsample_factor = rate/expected_rate 33 | assert downsample_factor >= 1 34 | 35 | d2 = np.array(data, 'float') 36 | if data.dtype.name == 'int16': 37 | d2 /= 2**15 38 | elif data.dtype.name == 'int32': 39 | d2 /= 2**31 40 | elif data.dtype.name == 'uint8': 41 | d2 -= 128 42 | d2 /= 127 43 | 44 | assert max(d2) <= 1 45 | 46 | if downsample_factor > 1: 47 | data = sp.signal.resample(d2, int(len(d2)/downsample_factor)) 48 | logging.debug("downsampled: was %d samples, now %d samples", 49 | len(d2), len(data)) 50 | else: 51 | data = d2 52 | 53 | return cls(sampleRate=expected_rate, samples=data) 54 | 55 | def __init__(self, size=None, sampleRate=None, samples=None, 56 | start=None, end=None): 57 | self.sampleRate = sampleRate 58 | if (samples is None): 59 | # Equivalent to initWithSize 60 | assert size is not None and sampleRate is not None 61 | self.size = size 62 | self.samples = np.zeros(samples, dtype=float) 63 | else: 64 | self.samples = np.array(samples) 65 | self.size = len(self.samples) 66 | 67 | if start is None: 68 | self.start = 0 69 | else: 70 | self.start = start 71 | if end is None: 72 | self.end = self.size 73 | else: 74 | self.end = end 75 | 76 | def __len__(self): 77 | return self.size 78 | 79 | def copySamples(self, samples): 80 | self.samples = samples[self.start:self.end] 81 | 82 | def energy(self): 83 | return self.sumOfSquaresFor() 84 | 85 | def sumOfSquaresFor(self): 86 | return np.square(self.samples[self.start:self.end]).sum() 87 | 88 | def getCoefficientsFor(self): 89 | logging.debug("getCoefficientsFor max(self.samples)=%d", 90 | max(self.samples)) 91 | coefficients = [0]*11 92 | for i in range(0, 11): 93 | logging.debug("i={}".format(i)) 94 | coefficients[i] = self.aForLag(i) 95 | return coefficients 96 | 97 | def aForLag(self, lag): 98 | samples = self.size - lag 99 | return sum(self.samples[0:samples] * self.samples[lag:samples+lag]) 100 | 101 | def rms(self, x): 102 | return np.sqrt(x.dot(x)/x.size) 103 | 104 | def getNormalizedCoefficientsFor(self, minimumPeriod, maximumPeriod): 105 | logging.debug("getNormalizedCoefficientsFor minimumPeriod=%d maximumPeriod=%d", 106 | minimumPeriod, maximumPeriod) 107 | coefficients = [0]*(maximumPeriod+1) 108 | 109 | for lag in range(0, maximumPeriod+1): 110 | if (lag < minimumPeriod): 111 | coefficients[lag] = 0.0 112 | continue 113 | 114 | right = self.samples[lag:] 115 | left = self.samples[:-lag] 116 | if np.std(right) == 0 or np.std(left) == 0: 117 | coefficients[lag] = np.nan 118 | continue 119 | 120 | corr = np.corrcoef(right, left) 121 | c = abs(corr[0][1]) 122 | 123 | if c <= 1e-15: 124 | coefficients[lag] = np.nan 125 | else: 126 | coefficients[lag] = c 127 | return coefficients 128 | -------------------------------------------------------------------------------- /pywizard/CodingTable.py: -------------------------------------------------------------------------------- 1 | from lpcplayer.tables import tables 2 | 3 | 4 | class CodingTable(object): 5 | kStopFrameIndex = 15 6 | 7 | rms = (0.0, 52.0, 87.0, 123.0, 174.0, 246.0, 348.0, 491.0, 694.0, 981.0, 8 | 1385.0, 1957.0, 2764.0, 3904.0, 5514.0, 7789.0) 9 | 10 | def __init__(self, name): 11 | chip = tables[name] 12 | 13 | for k in range(1, 11): 14 | ks = list(chip.ktable[k-1]) 15 | for i, k_value in enumerate(ks): 16 | ks[i] = k_value/512 17 | setattr(self, 'k'+str(k), ks) 18 | 19 | self.pitch = chip.pitchtable 20 | self.bits = (chip.energy_bits,)\ 21 | + (1,)\ 22 | + (chip.pitch_bits,)\ 23 | + chip.kbits 24 | 25 | def kSizeFor(self, k): 26 | if k > 10: 27 | raise Exception("RangeError") 28 | return 1 << self.bits[k+2] 29 | 30 | def rmsSize(self): 31 | return 1 << self.bits[0] 32 | 33 | def pitchSize(self): 34 | return 1 << self.bits[2] 35 | 36 | def kBinFor(self, k): 37 | mapping = {1: self.k1, 2: self.k2, 3: self.k3, 4: self.k4, 5: self.k5, 38 | 6: self.k6, 7: self.k7, 8: self.k8, 9: self.k9, 10: self.k10 39 | } 40 | return mapping[k] 41 | 42 | @classmethod 43 | def parameters(cls): 44 | return [ 45 | 'kParameterGain', 46 | 'kParameterRepeat', 47 | 'kParameterPitch', 48 | 'kParameterK1', 49 | 'kParameterK2', 50 | 'kParameterK3', 51 | 'kParameterK4', 52 | 'kParameterK5', 53 | 'kParameterK6', 54 | 'kParameterK7', 55 | 'kParameterK8', 56 | 'kParameterK9', 57 | 'kParameterK10' 58 | ] 59 | 60 | bits = (4, 1, 6, 5, 5, 4, 4, 4, 4, 4, 3, 3, 3) 61 | -------------------------------------------------------------------------------- /pywizard/Filterer.py: -------------------------------------------------------------------------------- 1 | from scipy import signal 2 | from pywizard.Buffer import Buffer 3 | 4 | 5 | class Filterer(object): 6 | def __init__(self, buf, lowPassCutoffInHZ, highPassCutoffInHZ, gain, 7 | order=5): 8 | self.gain = gain 9 | self.buf = buf 10 | nyq = 0.5 * buf.sampleRate 11 | low = lowPassCutoffInHZ / nyq 12 | # Avoid highpass frequency above nyqist-freq, this leads to 13 | # weird behavior 14 | high = min((highPassCutoffInHZ / nyq, 1)) 15 | self.b, self.a = signal.butter(order, [low, high], btype='band') 16 | 17 | def process(self): 18 | return Buffer.copy(self.buf, 19 | applyFilter=lambda x: signal.filtfilt(self.b, self.a, x)) 20 | -------------------------------------------------------------------------------- /pywizard/FrameData.py: -------------------------------------------------------------------------------- 1 | from pywizard.Reflector import Reflector 2 | from copy import deepcopy 3 | from pywizard.tools import ClosestValueFinder 4 | from pywizard.userSettings import settings 5 | 6 | 7 | class FrameData(object): 8 | def stopFrame(self): 9 | reflector = Reflector(self.codingTable) 10 | reflector.rms = self.codingTable.rms[self.codingTable.kStopFrameIndex] 11 | fd = FrameData(reflector=reflector, pitch=0, repeat=False) 12 | fd.decodeFrame = False 13 | fd.stopFrame = True 14 | return fd 15 | 16 | @classmethod 17 | def frameForDecoding(cls): 18 | reflector = Reflector() 19 | fd = cls(reflector=reflector, pitch=0, repeat=False) 20 | fd.decodeFrame = True 21 | return fd 22 | 23 | def __repr__(self): 24 | return "FrameData(reflector={}, pitch={}, repeat={}, parameters{})".format(self.reflector, self.pitch, self.repeat, self.parameters()) 25 | 26 | def __init__(self, reflector, pitch, repeat, parameters=None): 27 | self.reflector = reflector 28 | self.codingTable = reflector.codingTable 29 | self.pitch = pitch 30 | self._stopFrame = False 31 | self.decodeFrame = False 32 | self.repeat = repeat 33 | self._parameters = parameters 34 | 35 | def parameters(self): 36 | if self._parameters is None: 37 | self._parameters = self.parametersWithTranslate(False) 38 | return self._parameters 39 | 40 | def translatedParameters(self): 41 | if self._translatedParameters is None: 42 | self._translatedParameters = self.parametersWithTranslate(True) 43 | return self._translatedParameters 44 | 45 | def parametersWithTranslate(self, translate): 46 | parameters = {} 47 | parameters["kParameterGain"] = self.parameterizedValueForRMS(self.reflector.rms, translate=translate) 48 | 49 | if parameters["kParameterGain"] > 0: 50 | parameters["kParameterRepeat"] = self.parameterizedValueForRepeat(self.repeat) 51 | parameters["kParameterPitch"] = self.parameterizedValueForPitch(self.pitch, translate) 52 | 53 | if not parameters["kParameterRepeat"]: 54 | ks = self.kParametersFrom(1, 4, translate=translate) 55 | if ks is not None: 56 | parameters.update(ks) 57 | if parameters["kParameterPitch"] != 0 and (self.decodeFrame or self.reflector.isVoiced): 58 | ks = self.kParametersFrom(5, 10, translate=translate) 59 | parameters.update(ks) 60 | 61 | return deepcopy(parameters) 62 | 63 | def setParameter(self, parameter, value=None, translatedValue=None): 64 | self.parameters = None 65 | 66 | if parameter == 'kParameterGain': 67 | if translatedValue is None: 68 | index = int(value) 69 | rms = self.codingTable.rms(index) 70 | else: 71 | rms = translatedValue 72 | self.reflector.rms = float(rms) 73 | 74 | elif parameter == "kParameterRepeat": 75 | self.repeat = bool(value) 76 | 77 | elif parameter == "kParameterPitch": 78 | if translatedValue is None: 79 | pitch = self.codingTable.pitch[int(value)] 80 | else: 81 | pitch = translatedValue 82 | self.pitch = float(pitch) 83 | 84 | else: 85 | bin_no = int(parameter[1]) 86 | if translatedValue is None: 87 | index = int(value) 88 | k = self.codingTable.kBinFor(index) 89 | l = k[index] 90 | else: 91 | l = float(translatedValue) 92 | self.reflector.ks[bin_no] = float(l) 93 | 94 | def parameterizedValueForK(self, k, bin_no, translate): 95 | index = ClosestValueFinder(k, table=self.codingTable.kBinFor(bin_no)) 96 | if translate: 97 | return self.codingTable.kBinFor(bin_no)[index] 98 | else: 99 | return index 100 | 101 | def parameterizedValueForRMS(self, rms, translate): 102 | index = ClosestValueFinder(rms, table=self.codingTable.rms) 103 | if translate: 104 | return self.codingTable.rms[index] 105 | else: 106 | return index 107 | 108 | def parameterizedValueForPitch(self, pitch, translate): 109 | index = 0 110 | if self.decodeFrame: 111 | if pitch == 0: 112 | return 0 113 | if settings.overridePitch: 114 | index = int(settings.overridePitch) 115 | return self.codingTable.pitch[index] 116 | elif self.reflector.isUnvoiced() or pitch == 0: 117 | return 0 118 | 119 | if settings.overridePitch: 120 | offset = 0 121 | else: 122 | offset = settings.pitchOffset 123 | 124 | index = ClosestValueFinder(pitch, table=self.codingTable.pitch) 125 | 126 | index += offset 127 | 128 | if index > 63: 129 | index = 63 130 | if index < 0: 131 | index = 0 132 | 133 | if translate: 134 | return self.codingTable.pitch[index] 135 | else: 136 | return index 137 | 138 | def parameterizedValueForRepeat(self, repeat): 139 | return bool(repeat) 140 | 141 | def kParametersFrom(self, frm, to, translate): 142 | if self._stopFrame: 143 | return None 144 | parameters = {} 145 | for k in range(frm, to+1): 146 | key = self.parameterKeyForK(k) 147 | parameters[key] = self.parameterizedValueForK(self.reflector.ks[k], 148 | bin_no=k, 149 | translate=translate) 150 | return deepcopy(parameters) 151 | 152 | def parameterKeyForK(self, k): 153 | return "kParameterK{}".format(int(k)) 154 | -------------------------------------------------------------------------------- /pywizard/FrameDataBinaryEncoder.py: -------------------------------------------------------------------------------- 1 | from pywizard.tools import BitHelpers 2 | from pywizard.HexConverter import HexConverter 3 | import logging 4 | 5 | 6 | class BitPacker(object): 7 | @classmethod 8 | def raw_stream(cls, processor): 9 | frameData = processor.frames 10 | parametersList = [x.parameters() for x in frameData] 11 | raw_data = FrameDataBinaryEncoder.process(processor.codingTable, 12 | parametersList) 13 | return HexConverter.preprocess(raw_data) 14 | 15 | @classmethod 16 | def pack(cls, processor): 17 | frameData = processor.frames 18 | parametersList = [x.parameters() for x in frameData] 19 | raw_data = FrameDataBinaryEncoder.process(processor.codingTable, 20 | parametersList) 21 | return HexConverter.process(raw_data) 22 | 23 | 24 | class FrameDataBinaryEncoder(object): 25 | @classmethod 26 | def process(cls, codingTable, parametersList): 27 | bits = codingTable.bits 28 | binary = "" 29 | for parameters in parametersList: 30 | params = codingTable.parameters() 31 | for (param_name, idx) in zip(params, range(len(params))): 32 | if param_name not in parameters: 33 | break 34 | value = parameters[param_name] 35 | binaryValue = BitHelpers.valueToBinary(value, bits[idx]) 36 | logging.debug("param_name={} idx={} value={} binaryValue={}".format(param_name, idx, value, binaryValue)) 37 | binary += binaryValue 38 | return cls.nibblesFrom(binary) 39 | 40 | @classmethod 41 | def nibblesFrom(cls, binary): 42 | nibbles = [] 43 | while len(binary) >= 4: 44 | nibble = binary[0:4] 45 | binary = binary[4:] 46 | nibbles.append(nibble) 47 | return nibbles 48 | -------------------------------------------------------------------------------- /pywizard/HammingWindow.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import numpy as np 3 | 4 | class HammingWindow(object): 5 | _windows = {} 6 | 7 | @classmethod 8 | def processBuffer(cls, buf): 9 | l = len(buf) 10 | if l not in cls._windows: 11 | logging.debug("HammingWindow: Generate window for len {}".format(l)) 12 | cls._windows[l] = [(0.54 - 0.46 * np.cos(2 * np.pi * i / (l - 1))) for i in range(l)] 13 | 14 | buf.samples *= cls._windows[l] 15 | -------------------------------------------------------------------------------- /pywizard/HexConverter.py: -------------------------------------------------------------------------------- 1 | from pywizard.tools import formatSpecifier 2 | from pywizard.userSettings import settings 3 | import logging 4 | 5 | 6 | class HexConverter(object): 7 | formats = { 8 | "arduino": formatSpecifier("const unsigned char FILENAME[] PROGMEM = {", 9 | "0x{:02X}", 10 | ",", 11 | "};"), 12 | "C": formatSpecifier("const unsigned char FILENAME[] = {", 13 | "0x{:02X}", 14 | ",", 15 | "};"), 16 | "hex": formatSpecifier("", 17 | "{:02x}", 18 | " ", 19 | ""), 20 | "python": formatSpecifier("(", 21 | "0x{:02X}", 22 | ",", 23 | ")"), 24 | } 25 | 26 | @classmethod 27 | def preprocess(cls, nibbles): 28 | ''' 29 | Creates nibble swapped, reversed data bytestream as hex value list. 30 | Used to be NibbleBitReverser, NibbleSwitcher and HexConverter 31 | ''' 32 | result = [] 33 | for u, l in zip(nibbles[0::2], nibbles[1::2]): 34 | raw = (u+l)[::-1] 35 | result.append(int(raw, base=2)) 36 | return result 37 | 38 | @classmethod 39 | def process(cls, nibbles): 40 | formatter = cls.formats[settings.outputFormat] 41 | logging.debug("Will format output using {} ({})".format(settings.outputFormat, formatter)) 42 | result = [formatter.formatString.format(data) for data in cls.preprocess(nibbles)] 43 | return formatter.header + formatter.separator.join(result) + formatter.trailer 44 | -------------------------------------------------------------------------------- /pywizard/PitchEstimator.py: -------------------------------------------------------------------------------- 1 | from pywizard.userSettings import settings 2 | import logging 3 | import numpy as np 4 | 5 | 6 | class PitchEstimator(object): 7 | @classmethod 8 | def pitchForPeriod(cls, buf): 9 | return cls(buf).estimate() 10 | 11 | def __init__(self, buf): 12 | self._bestPeriod = None 13 | self.buf = buf 14 | self._normalizedCoefficients = self.getNormalizedCoefficients() 15 | 16 | def isOutOfRange(self): 17 | x = self.bestPeriod() 18 | return ((self._normalizedCoefficients[x] < self._normalizedCoefficients[x-1]) 19 | and 20 | (self._normalizedCoefficients[x] < self._normalizedCoefficients[x+1]) 21 | ) 22 | 23 | def interpolated(self): 24 | bestPeriod = int(self.bestPeriod()) 25 | middle = self._normalizedCoefficients[bestPeriod] 26 | left = self._normalizedCoefficients[bestPeriod - 1] 27 | right = self._normalizedCoefficients[bestPeriod + 1] 28 | 29 | if (2*middle - left - right) == 0: 30 | return bestPeriod 31 | else: 32 | return bestPeriod + .5 * (right - left) / (2 * middle - left - right) 33 | 34 | def estimate(self): 35 | bestPeriod = int(self.bestPeriod()) 36 | maximumMultiple = bestPeriod / self.minimumPeriod() 37 | 38 | found = False 39 | 40 | estimate = self.interpolated() 41 | if np.isnan(estimate): 42 | return 0.0 43 | while not found and maximumMultiple >= 1: 44 | subMultiplesAreStrong = True 45 | for i in range(0, int(maximumMultiple)): 46 | logging.debug("estimate={} maximumMultiple={}".format(estimate, maximumMultiple)) 47 | subMultiplePeriod = int(np.floor((i+1) * estimate / maximumMultiple + .5)) 48 | try: 49 | curr = self._normalizedCoefficients[subMultiplePeriod] 50 | except IndexError: 51 | curr = None 52 | if (curr is not None) and (curr < settings.subMultipleThreshold * self._normalizedCoefficients[bestPeriod]): 53 | subMultiplesAreStrong = False 54 | if subMultiplesAreStrong: 55 | estimate /= maximumMultiple 56 | maximumMultiple -= 1 57 | 58 | return estimate 59 | 60 | def getNormalizedCoefficients(self): 61 | minimumPeriod = self.minimumPeriod() - 1 62 | maximumPeriod = self.maximumPeriod() + 1 63 | return self.buf.getNormalizedCoefficientsFor(minimumPeriod, maximumPeriod) 64 | 65 | def bestPeriod(self): 66 | if self._bestPeriod is None: 67 | bestPeriod = self.minimumPeriod() 68 | maximumPeriod = self.maximumPeriod() 69 | 70 | bestPeriod = self._normalizedCoefficients.index(max(self._normalizedCoefficients)) 71 | logging.debug("_normalizedCoefficients = {}".format(self._normalizedCoefficients)) 72 | logging.debug("bestPeriod={} minimumPeriod={} maximumPeriod={}".format(bestPeriod, self.minimumPeriod(), self.maximumPeriod())) 73 | if bestPeriod < self.minimumPeriod(): 74 | bestPeriod = self.minimumPeriod() 75 | if bestPeriod > maximumPeriod: 76 | bestPeriod = maximumPeriod 77 | 78 | self._bestPeriod = int(bestPeriod) 79 | 80 | return self._bestPeriod 81 | 82 | def maxPitchInHZ(self): 83 | return settings.maximumPitchInHZ 84 | 85 | def minPitchInHZ(self): 86 | return settings.minimumPitchInHZ 87 | 88 | def minimumPeriod(self): 89 | return int(np.floor(self.buf.sampleRate / settings.maximumPitchInHZ - 1)) 90 | 91 | def maximumPeriod(self): 92 | return int(np.floor(self.buf.sampleRate / settings.minimumPitchInHZ + 1)) 93 | 94 | 95 | def ClosestValueFinder(actual, table): 96 | if actual < table[0]: 97 | return 0 98 | 99 | return table.index(min(table, key=lambda x: abs(x-actual))) 100 | -------------------------------------------------------------------------------- /pywizard/PreEmphasizer.py: -------------------------------------------------------------------------------- 1 | from pywizard.userSettings import settings 2 | import numpy as np 3 | 4 | 5 | class PreEmphasizer(object): 6 | @classmethod 7 | def processBuffer(cls, buf): 8 | preEnergy = buf.energy() 9 | 10 | alpha = cls.alpha() 11 | 12 | first_sample = buf.samples[0] 13 | buf.samples = buf.samples[1:] + (buf.samples[:-1] * alpha) 14 | buf.samples = np.insert(buf.samples, 0, first_sample) 15 | 16 | cls.scaleBuffer(buf, preEnergy, buf.energy()) 17 | 18 | @classmethod 19 | def alpha(cls): 20 | return settings.preEmphasisAlpha 21 | 22 | @classmethod 23 | def scaleBuffer(cls, buf, preEnergy, postEnergy): 24 | scale = np.sqrt(preEnergy / postEnergy) 25 | 26 | buf.samples *= scale 27 | -------------------------------------------------------------------------------- /pywizard/Processor.py: -------------------------------------------------------------------------------- 1 | from pywizard.Buffer import Buffer 2 | from pywizard.Filterer import Filterer 3 | from pywizard.Reflector import Reflector 4 | from pywizard.Segmenter import Segmenter 5 | from pywizard.PitchEstimator import PitchEstimator 6 | from pywizard.userSettings import settings 7 | from pywizard.HammingWindow import HammingWindow 8 | from pywizard.FrameData import FrameData 9 | from pywizard.PreEmphasizer import PreEmphasizer 10 | from pywizard.CodingTable import CodingTable 11 | import numpy as np 12 | 13 | 14 | class Processor(object): 15 | def __init__(self, buf, model=None): 16 | self.mainBuffer = buf 17 | self.pitchTable = None 18 | self.pitchBuffer = Buffer.copy(buf) 19 | self.codingTable = CodingTable(model) 20 | 21 | if settings.preEmphasis: 22 | PreEmphasizer.processBuffer(buf) 23 | 24 | self.pitchTable = {} 25 | wrappedPitch = False 26 | if settings.overridePitch: 27 | wrappedPitch = settings.pitchValue 28 | else: 29 | self.pitchTable = self.pitchTableForBuffer(self.pitchBuffer) 30 | 31 | coefficients = np.zeros(11) 32 | 33 | segmenter = Segmenter(buf=self.mainBuffer, windowWidth=settings.windowWidth) 34 | 35 | frames = [] 36 | for (cur_buf, i) in segmenter.eachSegment(): 37 | HammingWindow.processBuffer(cur_buf) 38 | coefficients = cur_buf.getCoefficientsFor() 39 | reflector = Reflector.translateCoefficients(self.codingTable, coefficients, cur_buf.size) 40 | 41 | if wrappedPitch: 42 | pitch = int(wrappedPitch) 43 | else: 44 | pitch = self.pitchTable[i] 45 | 46 | frameData = FrameData(reflector, pitch, repeat=False) 47 | 48 | frames.append(frameData) 49 | 50 | if settings.includeExplicitStopFrame: 51 | frames.append(frameData.stopFrame()) 52 | 53 | self.frames = frames 54 | 55 | def pitchTableForBuffer(self, pitchBuffer): 56 | filterer = Filterer(pitchBuffer, lowPassCutoffInHZ=settings.minimumPitchInHZ, highPassCutoffInHZ=settings.maximumPitchInHZ, gain=1) 57 | buf = filterer.process() 58 | 59 | segmenter = Segmenter(buf, windowWidth=2) 60 | pitchTable = np.zeros(segmenter.numberOfSegments()) 61 | 62 | for (buf, index) in segmenter.eachSegment(): 63 | pitchTable[index] = PitchEstimator.pitchForPeriod(buf) 64 | 65 | return pitchTable 66 | 67 | def process(self): 68 | return self.frameData 69 | 70 | -------------------------------------------------------------------------------- /pywizard/RMSNormalizer.py: -------------------------------------------------------------------------------- 1 | from pywizard.userSettings import settings 2 | 3 | class RMSNormalizer(object): 4 | @classmethod 5 | def normalize(cls, codingTable, frameData, Voiced): 6 | """ 7 | Normalize Voice levels to maximum RMS. 8 | 9 | >>> from FrameData import FrameData 10 | 11 | >>> from Reflector import Reflector 12 | 13 | >>> from CodingTable import CodingTable 14 | 15 | >>> ct = CodingTable() 16 | 17 | >>> r = Reflector() 18 | 19 | >>> rms = ct.rms[2] 20 | 21 | >>> RMSNormalizer.maxRMSIndex 22 | 3 23 | 24 | >>> framedata = FrameData(reflector=r, pitch=0, repeat=False) 25 | 26 | >>> RMSNormalizer.normalize([framedata], Voiced=True) 27 | 28 | >>> reflector.rms == ct.rms[3] 29 | True 30 | """ 31 | maximum = max([x.rms for x in frameData if x.isVoiced() == Voiced]) 32 | 33 | if maximum <= 0: 34 | return 35 | 36 | if Voiced: 37 | ref = cls.maxRMSIndex() 38 | else: 39 | ref = cls.maxUnvoicedRMSIndex() 40 | scale = codingTable.rms[cls.ref] / maximum 41 | 42 | for frame in FrameData: 43 | if not frame.reflector.isVoiced() == Voiced: 44 | frame.reflector.rms *= scale 45 | 46 | @classmethod 47 | def applyUnvoicedMultiplier(cls, frameData): 48 | mutiplier = cls.unvoicedRMSMultiplier() 49 | for frame in frameData: 50 | if frame.reflector.isUnvoiced(): 51 | frame.reflector.rms *= mutiplier 52 | 53 | @classmethod 54 | def maxRMSIndex(cls): 55 | return settings.rmsLimit 56 | 57 | @classmethod 58 | def maxUnvoicedRMSIndex(cls): 59 | return settings.unvoicedRMSLimit 60 | 61 | @classmethod 62 | def unvoicedMultiplier(cls): 63 | return settings.unvoicedRMSMultiplier 64 | -------------------------------------------------------------------------------- /pywizard/Reflector.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from pywizard.userSettings import settings 3 | 4 | 5 | class Reflector(object): 6 | """ 7 | Implements the reflector parameter guessing for the LPC 8 | algorithm. 9 | 10 | Test if stop frames do not accidentally created 11 | >>> from CodingTable import CodingTable 12 | 13 | >>> ct = CodingTable() 14 | 15 | >>> r = Reflector() 16 | 17 | >>> r.rms = ct.rms[15] 18 | 19 | >>> r.limitRMS = True 20 | 21 | >>> r.rms == ct.rms[14] 22 | True 23 | """ 24 | kNumberOfKParameters = 11 25 | 26 | def __init__(self, codingTable, k=None, rms=None, limitRMS=None): 27 | # TODO From config!! 28 | self.unvoicedThreshold = settings.unvoicedThreshold 29 | self.codingTable = codingTable 30 | if (k is None): 31 | assert rms is None 32 | assert limitRMS is None 33 | self.limitRMS = False 34 | self.ks = [0] * self.kNumberOfKParameters 35 | else: 36 | assert rms is not None 37 | assert limitRMS is not None 38 | self._rms = rms 39 | self.ks = k 40 | self.limitRMS = limitRMS 41 | 42 | @classmethod 43 | def formattedRMS(cls, rms, numberOfSamples): 44 | return np.sqrt(rms / numberOfSamples) * (1 << 15) 45 | 46 | @classmethod 47 | def translateCoefficients(cls, codingTable, r, numberOfSamples): 48 | '''Leroux Guegen algorithm for finding K's''' 49 | 50 | k = [0.0] * 11 51 | b = [0.0] * 11 52 | d = [0.0] * 12 53 | 54 | k[1] = -r[1] / r[0] 55 | d[1] = r[1] 56 | d[2] = r[0] + (k[1] * r[1]) 57 | 58 | for i in range(2, 11): 59 | y = r[i] 60 | b[1] = y 61 | 62 | for j in range(1, i): 63 | b[j + 1] = d[j] + (k[j] * y) 64 | y = y + (k[j] * d[j]) 65 | d[j] = b[j] 66 | 67 | k[i] = -y / d[i] 68 | d[i + 1] = d[i] + (k[i] * y) 69 | d[i] = b[i] 70 | rms = cls.formattedRMS(d[11], numberOfSamples) 71 | return cls(codingTable, k=k, rms=rms, limitRMS=True) 72 | 73 | @property 74 | def rms(self): 75 | if self.limitRMS and self._rms >= self.codingTable.rms[self.codingTable.kStopFrameIndex - 1]: 76 | return self.codingTable.rms[self.codingTable.kStopFrameIndex - 1] 77 | else: 78 | return self._rms 79 | 80 | @rms.setter 81 | def rms(self, rms): 82 | self._rms = rms 83 | 84 | def isVoiced(self): 85 | return not self.isUnvoiced() 86 | 87 | def isUnvoiced(self): 88 | return self.ks[1] >= self.unvoicedThreshold 89 | -------------------------------------------------------------------------------- /pywizard/Segmenter.py: -------------------------------------------------------------------------------- 1 | from pywizard.userSettings import settings 2 | from pywizard.Buffer import Buffer 3 | import numpy as np 4 | 5 | 6 | class Segmenter(object): 7 | def __init__(self, buf, windowWidth): 8 | milliseconds = settings.frameRate 9 | self.size = int(np.ceil(buf.sampleRate / 1e3 * milliseconds)) 10 | self.buf = buf 11 | self.windowWidth = windowWidth 12 | 13 | def eachSegment(self): 14 | length = self.numberOfSegments() 15 | for i in range(0, length): 16 | samples = self.samplesForSegment(i) 17 | buf = Buffer(samples=samples, size=self.sizeForWindow, 18 | sampleRate=self.buf.sampleRate) 19 | yield (buf, i) 20 | 21 | def samplesForSegment(self, index): 22 | length = self.sizeForWindow() 23 | 24 | samples = self.buf.samples[index * self.size: index * self.size + length] 25 | samples = np.append(samples, [0]*(length-len(samples))) 26 | 27 | return samples 28 | 29 | def sizeForWindow(self): 30 | return int(self.size * self.windowWidth) 31 | 32 | def numberOfSegments(self): 33 | return int(np.ceil(float(self.buf.size) / float(self.size))) 34 | -------------------------------------------------------------------------------- /pywizard/__init__.py: -------------------------------------------------------------------------------- 1 | #!python 2 | 3 | -------------------------------------------------------------------------------- /pywizard/tools.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | class BitHelpers(object): 5 | @classmethod 6 | def valueToBinary(cls, value, bits): 7 | return format(value, "0{}b".format(bits)) 8 | 9 | @classmethod 10 | def valueForBinary(cls, binary): 11 | return int(binary, 2) 12 | 13 | 14 | def ClosestValueFinder(actual, table): 15 | ''' 16 | Find the tabulated value closest to the given one. 17 | 18 | >>> floats = [1.0, 2.0] 19 | 20 | >>> ClosestValueFinder(1.25, floats) 21 | 0 22 | >>> ClosestValueFinder(1.75, floats) 23 | 1 24 | >>> floats = [5.0, 6.0] 25 | 26 | >>> ClosestValueFinder(-1.0, floats) 27 | 0 28 | >>> ClosestValueFinder(8.0, floats) 29 | 1 30 | ''' 31 | if actual < table[0]: 32 | return 0 33 | 34 | return table.index(min(table, key=lambda x: abs(x-actual))) 35 | 36 | 37 | formatSpecifier = namedtuple("formatSpecifier", 38 | ["header", "formatString", "separator", "trailer"] 39 | ) 40 | -------------------------------------------------------------------------------- /pywizard/userSettings.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from collections import OrderedDict 3 | 4 | 5 | class userSettings(object): 6 | availableSettings = ["pitchValue", "unvoicedThreshold", "windowWidth", 7 | "normalizeUnvoicedRMS", "normalizeUnvoicedRMS", 8 | "includeExplicitStopFrame", "preEmphasis", "preEmphasisAlpha", 9 | "overridePitch", "pitchOffset", "minimumPitchInHZ", "maximumPitchInHZ", 10 | "frameRate", "subMultipleThreshold", "outputFormat", "rmsLimit", 11 | "tablesVariant"] 12 | pitchValue = 0 13 | unvoicedThreshold = 0.3 14 | windowWidth = 2 15 | normalizeUnvoicedRMS = False 16 | normalizeVoicedRMS = False 17 | includeExplicitStopFrame = True 18 | preEmphasis = True 19 | preEmphasisAlpha = -0.9373 20 | overridePitch = False 21 | pitchOffset = 0 22 | maximumPitchInHZ = 500 23 | minimumPitchInHZ = 50 24 | frameRate = 25 25 | subMultipleThreshold = 0.9 26 | outputFormat = "arduino" 27 | rmsLimit = 14 28 | tablesVariant = "tms5100" 29 | 30 | def import_from_argparse(self, raw): 31 | v = vars(raw) 32 | self.import_from_dict(v) 33 | 34 | def import_from_dict(self, input_dict): 35 | error_list = [] 36 | for key in input_dict: 37 | if key == 'pitchRange': 38 | (self.minimumPitchInHZ, self.maximumPitchInHZ) = [int(x) for x in input_dict[key].split(",")] 39 | else: 40 | try: 41 | self.__setattr__(key, type(self.__getattribute__(key))(input_dict[key])) 42 | except AttributeError: 43 | logging.debug("Discarding argument {}={}".format(key, input_dict[key])) 44 | except ValueError: 45 | error_list.append(key) 46 | if len(error_list) > 0: 47 | return error_list 48 | else: 49 | return None 50 | 51 | def export_to_odict(self): 52 | r = OrderedDict() 53 | for k in self.availableSettings: 54 | r[k] = self.__getattribute__(k) 55 | return r 56 | 57 | 58 | settings = userSettings() 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | import setuptools 3 | 4 | setup( 5 | name = "pywizard", 6 | version = '1.4.3', 7 | description = 'A tool to convert Wave-Files to LPC bytestreams used by TMS5220 chips and emulators like the Arduino Talkie library', 8 | url = 'http://github.com/ptwz/python_wizard', 9 | author = 'Peter Turczak (python port), Patrick J. Collins (original code and most of the work), Special thanks to: Richard Wiggins Jonathan Gevaryahu Gene Frantz Frank Palazzolo', 10 | license = 'MIT', 11 | packages = ["pywizard", "lpcplayer"], 12 | install_requires=['scipy'], 13 | extras_require = {"gui": ["pyaudio"]}, 14 | python_requires = '>=3.5', 15 | scripts = ['python_wizard', 'python_wizard_gui'] 16 | ) 17 | --------------------------------------------------------------------------------