Devesh36/pocket-audio-player
A portable, rechargeable microSD audio player built around an RP2040, SPI flash, USB‑C charging/power management, LCD/button controls, PCM5102A I²S DAC, and TPA6132A headphone amplifier in a 3D‑printed enclosure.
- Version
- 1.0.12
- License
- unset
- Stars
- 0
scripts/embed-oled-model.ts
// tsci export lcd-export.circuit.tsx -f glb -o dist/enclosure/oled-model.glb
// Restore source colors lost by the native CAD union export, then align its axes.
const raw=new Uint8Array(await Bun.file('dist/enclosure/oled-model.glb').arrayBuffer())
const view=new DataView(raw.buffer),jsonLength=view.getUint32(12,true)
const model=JSON.parse(new TextDecoder().decode(raw.slice(20,20+jsonLength)))
const nativeBin=raw.slice(28+jsonLength,28+jsonLength+view.getUint32(20+jsonLength,true))
const primitive=model.meshes[0].primitives[0],position=model.accessors[primitive.attributes.POSITION],pv=model.bufferViews[position.bufferView]
const data=new DataView(nativeBin.buffer,nativeBin.byteOffset),colors=new Float32Array(position.count*3)
for(let i=0;i<position.count;i+=3){
const center=[0,0,0]
for(let k=0;k<3;k++)for(let axis=0;axis<3;axis++)center[axis]+=data.getFloat32(pv.byteOffset+(i+k)*12+axis*4,true)/3
const x=-center[0],y=center[2]-7+6.8,z=center[1]+0.8
let color=[.1,.1,.1]
if(z>10.04)color=y>27.96&&y<31.65?[.94,.94,.91]:[.33,.34,.31]
else if(z>10.023)color=[.33,.34,.31]
else if(z>10.00)color=[.85,.85,.80]
else if(z>=6.5&&z<=8.1)color=[.0086,.041,.153]
else if(x>-13.3&&x<-8.7&&y>16.4&&y<31.6)color=[.89,.89,.95]
else if(Math.abs(x)>20&&z<8.1)color=[.64,.47,.19]
else if(z<6.5)color=[.48,.48,.48]
for(let k=0;k<3;k++)colors.set(color,(i+k)*3)
}
const offset=Math.ceil(nativeBin.length/4)*4;let binary=new Uint8Array(offset+colors.byteLength)
binary.set(nativeBin);binary.set(new Uint8Array(colors.buffer),offset)
primitive.attributes.COLOR_0=model.accessors.length
model.accessors.push({bufferView:model.bufferViews.length,componentType:5126,count:position.count,type:'VEC3'})
model.bufferViews.push({buffer:0,byteOffset:offset,byteLength:colors.byteLength,target:34962})
model.buffers[0].byteLength=binary.length
model.materials[primitive.material].pbrMetallicRoughness.baseColorFactor=[1,1,1,1]
model.materials[primitive.material].alphaMode='OPAQUE'
// Deduplicate identical native vertices, preserving the exported normals/UVs/colors.
const attrs=Object.entries(primitive.attributes) as [string,number][]
const oldData=new DataView(binary.buffer),values=attrs.map(([,id])=>({id,a:model.accessors[id],v:model.bufferViews[model.accessors[id].bufferView],width:model.accessors[id].type==='VEC2'?2:3,rows:[] as number[]}))
const remap=new Uint32Array(position.count),unique=new Map<string,number>()
for(let i=0;i<position.count;i++){
const row=values.map(x=>Array.from({length:x.width},(_,j)=>oldData.getFloat32((x.v.byteOffset??0)+(x.a.byteOffset??0)+(i*x.width+j)*4,true)))
const key=row.flat().map(v=>Math.round(v*1e5)).join(',')
let id=unique.get(key)
if(id===undefined){id=unique.size;unique.set(key,id);row.forEach((r,k)=>values[k].rows.push(...r))}
remap[i]=id
}
const ia=model.accessors[primitive.indices],iv=model.bufferViews[ia.bufferView]
const indices=Uint32Array.from({length:ia.count},(_,i)=>remap[oldData.getUint32((iv.byteOffset??0)+(ia.byteOffset??0)+i*4,true)])
const arrays=[...values.map(x=>({id:x.id,array:new Float32Array(x.rows),count:unique.size,target:34962})),{id:primitive.indices,array:indices,count:indices.length,target:34963}]
let total=0;model.bufferViews=[]
for(const x of arrays){const a=model.accessors[x.id];a.bufferView=model.bufferViews.length;a.count=x.count;delete a.byteOffset;model.bufferViews.push({buffer:0,byteOffset:total,byteLength:x.array.byteLength,target:x.target});total+=x.array.byteLength}
binary=new Uint8Array(total);let cursor=0;for(const x of arrays){binary.set(new Uint8Array(x.array.buffer),cursor);cursor+=x.array.byteLength}model.buffers[0].byteLength=total
console.log(`Native vertices: ${position.count} -> ${unique.size}`)
model.nodes[0].rotation=[0,Math.SQRT1_2,Math.SQRT1_2,0]
function serialize(){const j=new TextEncoder().encode(JSON.stringify(model)),p=Math.ceil(j.length/4)*4,b=new Uint8Array(28+p+binary.length),v=new DataView(b.buffer);b.set(raw.slice(0,20));b.fill(32,20,20+p);b.set(j,20);v.setUint32(8,b.length,true);v.setUint32(12,p,true);v.setUint32(20+p,binary.length,true);v.setUint32(24+p,0x004e4942,true);b.set(binary,28+p);return b}
const bytes=serialize(),base64=Buffer.from(bytes).toString('base64')
await Bun.write('oled-model.ts',`// Native tscircuit export of the Waveshare 1.9-inch manufacturer CAD + static menu.\n// Generated by scripts/embed-oled-model.ts; regenerate after editing the model.\nconst lcdGlbBase64=${JSON.stringify(base64)}\nexport const oledModelUrl=\`data:model/gltf-binary;base64,\${lcdGlbBase64}#ext=glb\`\nfunction explodedUrl(){\n const raw=Uint8Array.from(atob(lcdGlbBase64),c=>c.charCodeAt(0)),v=new DataView(raw.buffer),n=v.getUint32(12,true)\n const j=JSON.parse(new TextDecoder().decode(raw.slice(20,20+n)));j.nodes[0].translation[2]+=35\n const json=new TextEncoder().encode(JSON.stringify(j)),p=Math.ceil(json.length/4)*4,tail=raw.slice(20+n),out=new Uint8Array(20+p+tail.length),h=new DataView(out.buffer)\n out.set(raw.slice(0,20));out.fill(32,20,20+p);out.set(json,20);out.set(tail,20+p);h.setUint32(8,out.length,true);h.setUint32(12,p,true)\n let encoded='';for(let i=0;i<out.length;i+=8192)encoded+=String.fromCharCode(...out.subarray(i,i+8192))\n return \`data:model/gltf-binary;base64,\${btoa(encoded)}#ext=glb\`\n}\nexport const oledExplodedModelUrl=explodedUrl()\n`)
await Bun.write('dist/enclosure/waveshare-19-display.glb',bytes)
console.log(`Embedded ${bytes.length} byte real LCD model`)