1234567891011121314151617181920import { CHIRP_PAYLOAD_LENGTH } from "./sync-types";2122232425export const enum GgwaveProtocol {26 AUDIBLE_NORMAL = 0,27 AUDIBLE_FAST = 1,28 AUDIBLE_FASTEST = 2,29 ULTRASOUND_NORMAL = 3,30 ULTRASOUND_FAST = 4,31 ULTRASOUND_FASTEST = 5, 32 DT_NORMAL = 6,33 DT_FAST = 7,34 DT_FASTEST = 8,35}3637interface GgwaveParameters {38 sampleRateInp: number;39 sampleRateOut: number;40 samplesPerFrame: number;41 soundMarkerThreshold: number;42 payloadLength: number;43 operatingMode: number;44}4546interface GgwaveModule {47 ProtocolId: Record<string, number>;48 GGWAVE_OPERATING_MODE_RX: number;49 GGWAVE_OPERATING_MODE_TX: number;50 GGWAVE_OPERATING_MODE_RX_AND_TX: number;51 getDefaultParameters(): GgwaveParameters;52 init(p: GgwaveParameters): number; 53 free(instance: number): void;54 encode(instance: number, data: Uint8Array | string, protocol: number, volumePercent: number): Float32Array;55 decode(instance: number, samples: Float32Array | Int16Array): Uint8Array | null;56}5758type GgwaveFactory = () => Promise<GgwaveModule>;5960declare global {61 interface Window {62 ggwave_factory?: GgwaveFactory;63 }64 65 var ggwave_factory: GgwaveFactory | undefined;66}6768let modulePromise: Promise<GgwaveModule> | null = null;69707172737475function ggwaveScriptUrl(): string {76 if (typeof chrome !== "undefined" && chrome.runtime?.getURL) {77 return chrome.runtime.getURL("ggwave/ggwave.js");78 }79 return "./ggwave/ggwave.js";80}8182async function injectScript(url: string): Promise<void> {83 84 85 86 87 await import( url);88}8990export async function loadGgwave(): Promise<GgwaveModule> {91 if (modulePromise) return modulePromise;92 modulePromise = (async () => {93 if (typeof globalThis.ggwave_factory !== "function") {94 try {95 await injectScript(ggwaveScriptUrl());96 } catch (err) {97 throw new Error(98 `ggwave glue could not be loaded — vendored asset missing? ${(err as Error).message}`,99 );100 }101 }102 const factory = globalThis.ggwave_factory;103 if (typeof factory !== "function") {104 throw new Error("ggwave_factory not exposed after script load");105 }106 return factory();107 })();108 return modulePromise;109}110111112113114115116117118119120121export interface ChirpCodec {122 123 encode(payload: Uint8Array): Float32Array;124 125 decode(samples: Float32Array): Uint8Array | null;126 127 free(): void;128}129130export interface ChirpCodecOptions {131 sampleRate: number;132 protocol?: GgwaveProtocol;133 volumePercent?: number;134}135136export async function createChirpCodec(opts: ChirpCodecOptions): Promise<ChirpCodec> {137 const mod = await loadGgwave();138 const params = mod.getDefaultParameters();139 params.sampleRateInp = opts.sampleRate;140 params.sampleRateOut = opts.sampleRate;141 params.payloadLength = CHIRP_PAYLOAD_LENGTH;142 params.operatingMode = mod.GGWAVE_OPERATING_MODE_RX_AND_TX;143 const protocol = opts.protocol ?? GgwaveProtocol.ULTRASOUND_FASTEST;144 const volume = opts.volumePercent ?? 10;145 let instance: number | null = mod.init(params);146147 return {148 encode(payload: Uint8Array): Float32Array {149 if (instance === null) throw new Error("chirp codec is freed");150 if (payload.length !== CHIRP_PAYLOAD_LENGTH) {151 throw new Error(`chirp payload must be ${CHIRP_PAYLOAD_LENGTH} bytes`);152 }153 const result = mod.encode(instance, payload, protocol, volume);154 if (!result || result.length === 0) {155 throw new Error("ggwave.encode returned an empty waveform");156 }157 return result;158 },159 decode(samples: Float32Array): Uint8Array | null {160 if (instance === null) throw new Error("chirp codec is freed");161 const decoded = mod.decode(instance, samples);162 if (!decoded) return null;163 164 165 return new Uint8Array(decoded);166 },167 free(): void {168 if (instance !== null) {169 mod.free(instance);170 instance = null;171 }172 },173 };174}175176177178179180181export function __setGgwaveModuleForTest(mod: GgwaveModule | null): void {182 if (mod === null) {183 modulePromise = null;184 return;185 }186 modulePromise = Promise.resolve(mod);187}