#!/usr/bin/env python3 """Encode 8/16 kHz mono 16-bit WAV speech as independent IMA ADPCM clips.""" import argparse from pathlib import Path import struct import wave STEPS = (7,8,9,10,11,12,13,14,16,17,19,21,23,25,28,31,34,37,41,45,50,55, 60,66,73,80,88,97,107,118,130,143,157,173,190,209,230,253,279,307, 337,371,408,449,494,544,598,658,724,796,876,963,1060,1166,1282,1411, 1552,1707,1878,2066,2272,2499,2749,3024,3327,3660,4026,4428,4871, 5358,5894,6484,7132,7845,8630,9493,10442,11487,12635,13899,15289, 16818,18500,20350,22385,24623,27086,29794,32767) ADJUST = (-1,-1,-1,-1,2,4,6,8) def encode(samples): predictor = index = 0 codes = [] for sample in samples: step = STEPS[index] difference = sample - predictor code = 8 if difference < 0 else 0 difference = abs(difference) delta = step >> 3 for bit, part in ((4,step),(2,step >> 1),(1,step >> 2)): if difference >= part: code |= bit difference -= part delta += part predictor = max(-32768,min(32767,predictor + (-delta if code & 8 else delta))) index = max(0,min(88,index + ADJUST[code & 7])) codes.append(code) if len(codes) & 1: codes.append(0) return bytes(a | b << 4 for a,b in zip(codes[::2], codes[1::2])) def read(path, rate=8000): with wave.open(str(path),'rb') as wav: if (wav.getnchannels(),wav.getsampwidth(),wav.getframerate()) != (1,2,rate): raise ValueError(f'Expected mono 16-bit PCM at {rate} Hz') data = wav.readframes(wav.getnframes()) return struct.unpack('<' + 'h' * (len(data)//2),data) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--on',type=Path,required=True) parser.add_argument('--off',type=Path,required=True) parser.add_argument('--output',type=Path,required=True) parser.add_argument('--rate',type=int,choices=(8000,16000),default=8000) parser.add_argument('--voice', help='Speech voice used to synthesize the source WAVs') args = parser.parse_args() if args.voice is not None and (not args.voice.strip() or '\n' in args.voice or '\r' in args.voice): parser.error('--voice must be a nonempty single-line label') lines = [f'// Generated by scripts/generate_gps_voice.py. {args.rate/1000:g} kHz IMA ADPCM.', '// Phrases: "GPS on", "GPS off". No runtime speech engine.', '#pragma once','#include ', 'namespace mesh { namespace audio {'] if args.voice is not None: lines.insert(2, f'// Voice: {args.voice}') for name,path in (('On',args.on),('Off',args.off)): samples = read(path,args.rate) encoded = encode(samples) lines.append(f'static const uint8_t gps{name}Data[] = {{') for i in range(0,len(encoded),20): lines.append(' '+','.join(f'0x{b:02x}' for b in encoded[i:i+20])+',') lines.extend(['};',f'static constexpr VoiceClip gps{name}Clip = {{', f' gps{name}Data, sizeof(gps{name}Data), {len(samples)}, {args.rate}', '};']) print(f'GPS {name.lower()}: {len(samples)/args.rate:.3f}s, {len(encoded)} bytes') lines.append('}} // namespace mesh::audio') args.output.write_text('\n'.join(lines)+'\n',encoding='utf-8') if __name__ == '__main__': main()